feat(core): copy file changes when warping (#26190)
This commit is contained in:
@@ -18,7 +18,7 @@ import { ProjectID } from "@/project/schema"
|
||||
import { Slug } from "@opencode-ai/core/util/slug"
|
||||
import { WorkspaceTable } from "./workspace.sql"
|
||||
import { getAdapter } from "./adapters"
|
||||
import { type WorkspaceInfo, WorkspaceInfo as WorkspaceInfoSchema } from "./types"
|
||||
import { type Target, type WorkspaceInfo, WorkspaceInfo as WorkspaceInfoSchema } from "./types"
|
||||
import { WorkspaceID } from "./schema"
|
||||
import { Session } from "@/session/session"
|
||||
import { SessionPrompt } from "@/session/prompt"
|
||||
@@ -31,6 +31,9 @@ import { WorkspaceContext } from "./workspace-context"
|
||||
import { EffectBridge } from "@/effect/bridge"
|
||||
import { withStatics } from "@/util/schema"
|
||||
import { zod as effectZod, zodObject } from "@/util/effect-zod"
|
||||
import { Vcs } from "@/project/vcs"
|
||||
import { InstanceStore } from "@/project/instance-store"
|
||||
import { InstanceBootstrap } from "@/project/bootstrap"
|
||||
|
||||
export const Info = WorkspaceInfoSchema
|
||||
export type Info = WorkspaceInfo
|
||||
@@ -86,6 +89,7 @@ export type CreateInput = Schema.Schema.Type<typeof CreateInput>
|
||||
export const SessionWarpInput = Schema.Struct({
|
||||
workspaceID: Schema.NullOr(WorkspaceID),
|
||||
sessionID: SessionID,
|
||||
copyChanges: Schema.optional(Schema.Boolean),
|
||||
}).pipe(withStatics((s) => ({ zod: effectZod(s), zodObject: zodObject(s) })))
|
||||
export type SessionWarpInput = Schema.Schema.Type<typeof SessionWarpInput>
|
||||
|
||||
@@ -137,6 +141,7 @@ type SessionWarpError =
|
||||
| WorkspaceNotFoundError
|
||||
| SessionEventsNotFoundError
|
||||
| SessionWarpHttpError
|
||||
| Vcs.PatchApplyError
|
||||
| HttpClientError.HttpClientError
|
||||
type WaitForSyncError = SyncTimeoutError | SyncAbortedError
|
||||
type SyncLoopError = SyncHttpError | HttpClientError.HttpClientError
|
||||
@@ -167,6 +172,7 @@ export const layer = Layer.effect(
|
||||
const prompt = yield* SessionPrompt.Service
|
||||
const http = yield* HttpClient.HttpClient
|
||||
const sync = yield* SyncEvent.Service
|
||||
const vcs = yield* Vcs.Service
|
||||
const connections = new Map<WorkspaceID, ConnectionStatus>()
|
||||
const syncFibers = yield* FiberMap.make<WorkspaceID, void, SyncLoopError>()
|
||||
|
||||
@@ -255,6 +261,66 @@ export const layer = Layer.effect(
|
||||
)
|
||||
})
|
||||
|
||||
const runInWorkspace = <A, E, R>(input: {
|
||||
workspaceID?: WorkspaceID
|
||||
local: () => Effect.Effect<A, E, R>
|
||||
remote: (input: {
|
||||
workspace: Info
|
||||
target: Extract<Target, { type: "remote" }>
|
||||
}) => HttpClientRequest.HttpClientRequest
|
||||
fallback: A
|
||||
response?: "json" | "text"
|
||||
}) =>
|
||||
Effect.gen(function* () {
|
||||
if (!input.workspaceID) return yield* input.local()
|
||||
|
||||
const workspace = yield* get(input.workspaceID)
|
||||
if (!workspace) return input.fallback
|
||||
|
||||
const adapter = getAdapter(workspace.projectID, workspace.type)
|
||||
const target = yield* EffectBridge.fromPromise(() => adapter.target(workspace))
|
||||
|
||||
if (target.type === "local") {
|
||||
const store = yield* InstanceStore.Service
|
||||
return yield* store.provide({ directory: target.directory }, input.local())
|
||||
}
|
||||
|
||||
const response = yield* http.execute(input.remote({ workspace, target })).pipe(
|
||||
Effect.catch((error) =>
|
||||
Effect.sync(() => {
|
||||
log.warn("workspace target request failed", {
|
||||
workspaceID: workspace.id,
|
||||
error: errorData(error),
|
||||
})
|
||||
}),
|
||||
),
|
||||
)
|
||||
if (!response) return input.fallback
|
||||
if (response.status < 200 || response.status >= 300) {
|
||||
const body = yield* response.text.pipe(Effect.catch(() => Effect.succeed("")))
|
||||
log.warn("workspace target request failed", {
|
||||
workspaceID: workspace.id,
|
||||
status: response.status,
|
||||
body,
|
||||
})
|
||||
return input.fallback
|
||||
}
|
||||
|
||||
const body = input.response === "text" ? response.text : response.json
|
||||
return yield* body.pipe(
|
||||
Effect.map((result) => result as A),
|
||||
Effect.catch((error) =>
|
||||
Effect.sync(() => {
|
||||
log.warn("workspace target response decode failed", {
|
||||
workspaceID: workspace.id,
|
||||
error: errorData(error),
|
||||
})
|
||||
return input.fallback
|
||||
}),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
const syncHistory = Effect.fn("Workspace.syncHistory")(function* (
|
||||
space: Info,
|
||||
url: URL | string,
|
||||
@@ -557,6 +623,36 @@ export const layer = Layer.effect(
|
||||
}
|
||||
}
|
||||
|
||||
const sourcePatch =
|
||||
input.copyChanges && current?.workspaceID
|
||||
? yield* runInWorkspace({
|
||||
workspaceID: current?.workspaceID ?? undefined,
|
||||
local: () => vcs.diffRaw(),
|
||||
remote: ({ target }) =>
|
||||
HttpClientRequest.get(route(target.url, "/vcs/diff/raw"), {
|
||||
headers: new Headers(target.headers),
|
||||
}),
|
||||
fallback: "",
|
||||
response: "text",
|
||||
}).pipe(Effect.provide(InstanceStore.defaultLayer.pipe(Layer.provide(InstanceBootstrap.defaultLayer))))
|
||||
: ""
|
||||
|
||||
if (sourcePatch) {
|
||||
// Attempt to apply the file changes to the new workspace.
|
||||
// We intentionally do first so if it fails we don't warp
|
||||
// the session.
|
||||
yield* runInWorkspace({
|
||||
workspaceID: input.workspaceID ?? undefined,
|
||||
local: () => vcs.apply({ patch: sourcePatch }),
|
||||
remote: ({ target }) =>
|
||||
HttpClientRequest.post(route(target.url, "/vcs/apply"), {
|
||||
headers: new Headers(target.headers),
|
||||
body: HttpBody.jsonUnsafe({ patch: sourcePatch }),
|
||||
}),
|
||||
fallback: { applied: false },
|
||||
}).pipe(Effect.provide(InstanceStore.defaultLayer.pipe(Layer.provide(InstanceBootstrap.defaultLayer))))
|
||||
}
|
||||
|
||||
if (input.workspaceID === null) {
|
||||
yield* Effect.sync(() =>
|
||||
SyncEvent.run(Session.Event.Updated, {
|
||||
@@ -866,6 +962,8 @@ export const defaultLayer = layer.pipe(
|
||||
Layer.provide(Session.defaultLayer),
|
||||
Layer.provide(SyncEvent.defaultLayer),
|
||||
Layer.provide(SessionPrompt.defaultLayer),
|
||||
Layer.provide(Project.defaultLayer),
|
||||
Layer.provide(Vcs.defaultLayer),
|
||||
Layer.provide(FetchHttpClient.layer),
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user