From 8bd51d110786e097b0057e9e23409a87dd65f89f Mon Sep 17 00:00:00 2001 From: Julius Marminge <51714798+juliusmarminge@users.noreply.github.com> Date: Thu, 17 Sep 2026 12:57:01 -0700 Subject: [PATCH 1/7] fix(server): reject file rewind in shared workspaces --- .../Layers/CheckpointReactor.test.ts | 98 +++++++++++++++++-- .../orchestration/Layers/CheckpointReactor.ts | 53 ++++++++++ 2 files changed, 142 insertions(+), 9 deletions(-) diff --git a/apps/server/src/orchestration/Layers/CheckpointReactor.test.ts b/apps/server/src/orchestration/Layers/CheckpointReactor.test.ts index 2cc7a4399915..6acf37339d2b 100644 --- a/apps/server/src/orchestration/Layers/CheckpointReactor.test.ts +++ b/apps/server/src/orchestration/Layers/CheckpointReactor.test.ts @@ -297,6 +297,7 @@ describe("CheckpointReactor", () => { readonly threadWorktreePath?: string | null; readonly threadBranch?: string | null; readonly secondThreadSharingWorktree?: boolean; + readonly secondThreadWorktreePath?: (cwd: string) => string; readonly localStatusRefName?: string | null; readonly providerSessionCwd?: string; readonly providerName?: ProviderDriverKind; @@ -436,7 +437,8 @@ describe("CheckpointReactor", () => { interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, runtimeMode: "approval-required", branch: options?.threadBranch ?? null, - worktreePath: options?.threadWorktreePath ?? cwd, + worktreePath: + options?.threadWorktreePath !== undefined ? options.threadWorktreePath : cwd, createdAt, }) .pipe( @@ -455,7 +457,8 @@ describe("CheckpointReactor", () => { interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, runtimeMode: "approval-required", branch: null, - worktreePath: options?.threadWorktreePath ?? cwd, + worktreePath: + options?.secondThreadWorktreePath?.(cwd) ?? options?.threadWorktreePath ?? cwd, createdAt, }), ) @@ -497,6 +500,67 @@ describe("CheckpointReactor", () => { }; } + effectIt.effect.each(["active", "archived", "alias", "project-root", "conversation"] as const)( + "preserves sibling files when reverting a shared workspace, owner=%s", + (owner) => + Effect.gen(function* () { + const harness = yield* Effect.promise(() => + createHarness({ + secondThreadSharingWorktree: true, + secondThreadWorktreePath: + owner === "alias" + ? (cwd) => { + const alias = `${cwd}-alias`; + NodeFS.symlinkSync(cwd, alias, "junction"); + tempDirs.push(alias); + return alias; + } + : undefined, + }), + ); + const createdAt = "2026-01-01T00:00:02.000Z"; + if (owner === "archived") + yield* harness.engine.dispatch({ + type: "thread.archive", + commandId: CommandId.make("cmd-archive-owner"), + threadId: ThreadId.make("thread-2"), + createdAt, + }); + if (owner === "project-root") + yield* harness.engine.dispatch({ + type: "thread.meta.update", + commandId: CommandId.make("cmd-root-owner"), + threadId: ThreadId.make("thread-2"), + worktreePath: null, + createdAt, + }); + const siblingFile = NodePath.join(harness.cwd, "sibling-work.txt"); + NodeFS.writeFileSync(siblingFile, "sibling work\n"); + yield* harness.engine.dispatch({ + type: + owner === "conversation" ? "thread.conversation.revert" : "thread.checkpoint.revert", + commandId: CommandId.make("cmd-shared-revert"), + threadId: ThreadId.make("thread-1"), + turnCount: 0, + createdAt, + }); + yield* Effect.promise(harness.drain); + expect(NodeFS.readFileSync(siblingFile, "utf8")).toBe("sibling work\n"); + expect(NodeFS.readFileSync(NodePath.join(harness.cwd, "README.md"), "utf8")).toBe("v3\n"); + const model = yield* Effect.promise(harness.readModel); + const failure = model.threads + .find((t) => t.id === "thread-1") + ?.activities.find((a) => a.kind === "checkpoint.revert.failed"); + if (owner === "conversation") expect(failure).toBeUndefined(); + else { + expect(failure?.payload).toMatchObject({ + detail: expect.stringContaining("isolated worktree"), + }); + expect(harness.provider.rollbackConversation).not.toHaveBeenCalled(); + } + }), + ); + effectIt.effect("captures baseline and large turn summaries before completion receipts", () => Effect.gen(function* () { const harness = yield* Effect.promise(() => @@ -1963,7 +2027,7 @@ describe("CheckpointReactor", () => { }); it.each([false, true])( - "reverts without an active session using project cwd fallback: %s", + "restores files only in an isolated worktree without an active session, project cwd=%s", async (useProjectCwd) => { const harness = await createHarness({ hasSession: false, @@ -1995,12 +2059,28 @@ describe("CheckpointReactor", () => { }), ); - await waitForEvent(harness.engine, (event) => event.type === "thread.reverted"); - expect(harness.provider.rollbackConversation).toHaveBeenCalledWith({ - threadId: ThreadId.make("thread-1"), - numTurns: 1, - }); - expect(NodeFS.readFileSync(NodePath.join(harness.cwd, "README.md"), "utf8")).toBe("v1\n"); + await harness.drain(); + if (useProjectCwd) { + expect(harness.provider.rollbackConversation).not.toHaveBeenCalled(); + expect(NodeFS.readFileSync(NodePath.join(harness.cwd, "README.md"), "utf8")).toBe("v3\n"); + const model = await harness.readModel(); + expect(model.threads[0]?.activities).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + kind: "checkpoint.revert.failed", + payload: expect.objectContaining({ + detail: expect.stringContaining("isolated worktree"), + }), + }), + ]), + ); + } else { + expect(harness.provider.rollbackConversation).toHaveBeenCalledWith({ + threadId: ThreadId.make("thread-1"), + numTurns: 1, + }); + expect(NodeFS.readFileSync(NodePath.join(harness.cwd, "README.md"), "utf8")).toBe("v1\n"); + } }, ); }); diff --git a/apps/server/src/orchestration/Layers/CheckpointReactor.ts b/apps/server/src/orchestration/Layers/CheckpointReactor.ts index d0d867fdd25e..bac1e79d8f10 100644 --- a/apps/server/src/orchestration/Layers/CheckpointReactor.ts +++ b/apps/server/src/orchestration/Layers/CheckpointReactor.ts @@ -15,6 +15,7 @@ import * as Crypto from "effect/Crypto"; import * as DateTime from "effect/DateTime"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; +import * as FileSystem from "effect/FileSystem"; import * as Option from "effect/Option"; import type * as PlatformError from "effect/PlatformError"; import * as Stream from "effect/Stream"; @@ -87,6 +88,7 @@ const make = Effect.gen(function* () { const checkpointStore = yield* CheckpointStore.CheckpointStore; const receiptBus = yield* RuntimeReceiptBus; const workspaceEntries = yield* WorkspaceEntries.WorkspaceEntries; + const fileSystem = yield* FileSystem.FileSystem; const vcsStatusBroadcaster = yield* VcsStatusBroadcaster; const pullRequests = yield* PullRequestService.PullRequestService; const startedTurns = new Map(); @@ -687,6 +689,46 @@ const make = Effect.gen(function* () { }); }); + // Checkpoints contain the whole checkout, so restoring a shared cwd can erase a sibling's work. + const isRestoreWorkspaceIsolated = Effect.fn("isRestoreWorkspaceIsolated")(function* ( + thread: { readonly id: ThreadId; readonly worktreePath: string | null }, + cwd: string, + ) { + if (thread.worktreePath === null) return false; + const canonicalCwd = yield* fileSystem.realPath(cwd); + if ((yield* fileSystem.realPath(thread.worktreePath)) !== canonicalCwd) return false; + const active = yield* projectionSnapshotQuery.getShellSnapshot(); + const archived = yield* projectionSnapshotQuery.getArchivedShellSnapshot(); + const projects = [...active.projects, ...archived.projects]; + const paths = new Set(); + for (const other of [...active.threads, ...archived.threads]) { + if (other.id === thread.id) continue; + const candidate = + other.worktreePath ?? + projects.find((project) => project.id === other.projectId)?.workspaceRoot; + if (candidate !== undefined) paths.add(candidate); + } + for (const session of yield* providerService.listSessions()) { + if ( + session.threadId !== thread.id && + session.status !== "closed" && + session.cwd !== undefined + ) + paths.add(session.cwd); + } + for (const candidate of paths) { + const otherCwd = yield* fileSystem + .realPath(candidate) + .pipe( + Effect.catch((error) => + error.reason._tag === "NotFound" ? Effect.succeed(null) : Effect.fail(error), + ), + ); + if (otherCwd === canonicalCwd) return false; + } + return true; + }); + const handleRevertRequested = Effect.fn("handleRevertRequested")(function* ( event: Extract, ) { @@ -742,6 +784,17 @@ const make = Effect.gen(function* () { return; } + if (!(yield* isRestoreWorkspaceIsolated(thread, checkpointCwd))) { + yield* appendRevertFailureActivity({ + threadId: thread.id, + turnCount: event.payload.turnCount, + detail: + "File restore requires an isolated worktree. This workspace may contain changes from another thread. Rewind the conversation without restoring files instead.", + createdAt: now, + }); + return; + } + const targetCheckpointRef = event.payload.turnCount === 0 ? checkpointRefForThreadTurn(event.payload.threadId, 0) From a8e0e5d292ad122ec7857be5839e6b08219848bd Mon Sep 17 00:00:00 2001 From: Julius Marminge <51714798+juliusmarminge@users.noreply.github.com> Date: Thu, 17 Sep 2026 13:02:06 -0700 Subject: [PATCH 2/7] test(server): align rewind fixtures with command schemas --- .../orchestration/Layers/CheckpointReactor.test.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/apps/server/src/orchestration/Layers/CheckpointReactor.test.ts b/apps/server/src/orchestration/Layers/CheckpointReactor.test.ts index 6acf37339d2b..c9598245b7ab 100644 --- a/apps/server/src/orchestration/Layers/CheckpointReactor.test.ts +++ b/apps/server/src/orchestration/Layers/CheckpointReactor.test.ts @@ -507,15 +507,16 @@ describe("CheckpointReactor", () => { const harness = yield* Effect.promise(() => createHarness({ secondThreadSharingWorktree: true, - secondThreadWorktreePath: - owner === "alias" - ? (cwd) => { + ...(owner === "alias" + ? { + secondThreadWorktreePath: (cwd: string) => { const alias = `${cwd}-alias`; NodeFS.symlinkSync(cwd, alias, "junction"); tempDirs.push(alias); return alias; - } - : undefined, + }, + } + : {}), }), ); const createdAt = "2026-01-01T00:00:02.000Z"; @@ -524,7 +525,6 @@ describe("CheckpointReactor", () => { type: "thread.archive", commandId: CommandId.make("cmd-archive-owner"), threadId: ThreadId.make("thread-2"), - createdAt, }); if (owner === "project-root") yield* harness.engine.dispatch({ @@ -532,7 +532,6 @@ describe("CheckpointReactor", () => { commandId: CommandId.make("cmd-root-owner"), threadId: ThreadId.make("thread-2"), worktreePath: null, - createdAt, }); const siblingFile = NodePath.join(harness.cwd, "sibling-work.txt"); NodeFS.writeFileSync(siblingFile, "sibling work\n"); From ad1c0295a82649380c816b8a24b35bed50a70d88 Mon Sep 17 00:00:00 2001 From: Julius Marminge <51714798+juliusmarminge@users.noreply.github.com> Date: Thu, 17 Sep 2026 13:06:34 -0700 Subject: [PATCH 3/7] fix(server): protect nested thread workspaces during rewind --- .../Layers/CheckpointReactor.test.ts | 125 ++++++++++-------- .../orchestration/Layers/CheckpointReactor.ts | 11 +- 2 files changed, 79 insertions(+), 57 deletions(-) diff --git a/apps/server/src/orchestration/Layers/CheckpointReactor.test.ts b/apps/server/src/orchestration/Layers/CheckpointReactor.test.ts index c9598245b7ab..47f5a1570e6d 100644 --- a/apps/server/src/orchestration/Layers/CheckpointReactor.test.ts +++ b/apps/server/src/orchestration/Layers/CheckpointReactor.test.ts @@ -500,64 +500,77 @@ describe("CheckpointReactor", () => { }; } - effectIt.effect.each(["active", "archived", "alias", "project-root", "conversation"] as const)( - "preserves sibling files when reverting a shared workspace, owner=%s", - (owner) => - Effect.gen(function* () { - const harness = yield* Effect.promise(() => - createHarness({ - secondThreadSharingWorktree: true, - ...(owner === "alias" - ? { - secondThreadWorktreePath: (cwd: string) => { - const alias = `${cwd}-alias`; - NodeFS.symlinkSync(cwd, alias, "junction"); - tempDirs.push(alias); - return alias; - }, - } - : {}), - }), - ); - const createdAt = "2026-01-01T00:00:02.000Z"; - if (owner === "archived") - yield* harness.engine.dispatch({ - type: "thread.archive", - commandId: CommandId.make("cmd-archive-owner"), - threadId: ThreadId.make("thread-2"), - }); - if (owner === "project-root") - yield* harness.engine.dispatch({ - type: "thread.meta.update", - commandId: CommandId.make("cmd-root-owner"), - threadId: ThreadId.make("thread-2"), - worktreePath: null, - }); - const siblingFile = NodePath.join(harness.cwd, "sibling-work.txt"); - NodeFS.writeFileSync(siblingFile, "sibling work\n"); + effectIt.effect.each([ + "active", + "archived", + "alias", + "nested", + "project-root", + "conversation", + ] as const)("preserves sibling files when reverting a shared workspace, owner=%s", (owner) => + Effect.gen(function* () { + const harness = yield* Effect.promise(() => + createHarness({ + secondThreadSharingWorktree: true, + ...(owner === "alias" || owner === "nested" + ? { + secondThreadWorktreePath: (cwd: string) => { + if (owner === "nested") { + const nested = NodePath.join(cwd, "nested-owner"); + NodeFS.mkdirSync(nested); + return nested; + } + const alias = `${cwd}-alias`; + NodeFS.symlinkSync(cwd, alias, "junction"); + tempDirs.push(alias); + return alias; + }, + } + : {}), + }), + ); + const createdAt = "2026-01-01T00:00:02.000Z"; + if (owner === "archived") yield* harness.engine.dispatch({ - type: - owner === "conversation" ? "thread.conversation.revert" : "thread.checkpoint.revert", - commandId: CommandId.make("cmd-shared-revert"), - threadId: ThreadId.make("thread-1"), - turnCount: 0, - createdAt, + type: "thread.archive", + commandId: CommandId.make("cmd-archive-owner"), + threadId: ThreadId.make("thread-2"), }); - yield* Effect.promise(harness.drain); - expect(NodeFS.readFileSync(siblingFile, "utf8")).toBe("sibling work\n"); - expect(NodeFS.readFileSync(NodePath.join(harness.cwd, "README.md"), "utf8")).toBe("v3\n"); - const model = yield* Effect.promise(harness.readModel); - const failure = model.threads - .find((t) => t.id === "thread-1") - ?.activities.find((a) => a.kind === "checkpoint.revert.failed"); - if (owner === "conversation") expect(failure).toBeUndefined(); - else { - expect(failure?.payload).toMatchObject({ - detail: expect.stringContaining("isolated worktree"), - }); - expect(harness.provider.rollbackConversation).not.toHaveBeenCalled(); - } - }), + if (owner === "project-root") + yield* harness.engine.dispatch({ + type: "thread.meta.update", + commandId: CommandId.make("cmd-root-owner"), + threadId: ThreadId.make("thread-2"), + worktreePath: null, + }); + const siblingFile = NodePath.join( + harness.cwd, + ...(owner === "nested" ? ["nested-owner"] : []), + "sibling-work.txt", + ); + NodeFS.writeFileSync(siblingFile, "sibling work\n"); + yield* harness.engine.dispatch({ + type: owner === "conversation" ? "thread.conversation.revert" : "thread.checkpoint.revert", + commandId: CommandId.make("cmd-shared-revert"), + threadId: ThreadId.make("thread-1"), + turnCount: 0, + createdAt, + }); + yield* Effect.promise(harness.drain); + expect(NodeFS.readFileSync(siblingFile, "utf8")).toBe("sibling work\n"); + expect(NodeFS.readFileSync(NodePath.join(harness.cwd, "README.md"), "utf8")).toBe("v3\n"); + const model = yield* Effect.promise(harness.readModel); + const failure = model.threads + .find((t) => t.id === "thread-1") + ?.activities.find((a) => a.kind === "checkpoint.revert.failed"); + if (owner === "conversation") expect(failure).toBeUndefined(); + else { + expect(failure?.payload).toMatchObject({ + detail: expect.stringContaining("isolated worktree"), + }); + expect(harness.provider.rollbackConversation).not.toHaveBeenCalled(); + } + }), ); effectIt.effect("captures baseline and large turn summaries before completion receipts", () => diff --git a/apps/server/src/orchestration/Layers/CheckpointReactor.ts b/apps/server/src/orchestration/Layers/CheckpointReactor.ts index bac1e79d8f10..91158a5e4189 100644 --- a/apps/server/src/orchestration/Layers/CheckpointReactor.ts +++ b/apps/server/src/orchestration/Layers/CheckpointReactor.ts @@ -16,6 +16,7 @@ import * as DateTime from "effect/DateTime"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as FileSystem from "effect/FileSystem"; +import * as Path from "effect/Path"; import * as Option from "effect/Option"; import type * as PlatformError from "effect/PlatformError"; import * as Stream from "effect/Stream"; @@ -89,6 +90,7 @@ const make = Effect.gen(function* () { const receiptBus = yield* RuntimeReceiptBus; const workspaceEntries = yield* WorkspaceEntries.WorkspaceEntries; const fileSystem = yield* FileSystem.FileSystem; + const path = yield* Path.Path; const vcsStatusBroadcaster = yield* VcsStatusBroadcaster; const pullRequests = yield* PullRequestService.PullRequestService; const startedTurns = new Map(); @@ -724,7 +726,14 @@ const make = Effect.gen(function* () { error.reason._tag === "NotFound" ? Effect.succeed(null) : Effect.fail(error), ), ); - if (otherCwd === canonicalCwd) return false; + if (otherCwd === null) continue; + const relative = path.relative(canonicalCwd, otherCwd); + // An owner in a subdirectory is still affected by a restore of the whole checkout. + if ( + relative === "" || + (!path.isAbsolute(relative) && relative !== ".." && !relative.startsWith(`..${path.sep}`)) + ) + return false; } return true; }); From 9f27af002cac094fcc89e812e4da1f7b90bf380b Mon Sep 17 00:00:00 2001 From: Julius Marminge <51714798+juliusmarminge@users.noreply.github.com> Date: Thu, 17 Sep 2026 13:15:44 -0700 Subject: [PATCH 4/7] fix(server): reject ancestor workspace ownership on rewind --- .../Layers/CheckpointReactor.test.ts | 2 +- .../orchestration/Layers/CheckpointReactor.ts | 16 +++++++++------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/apps/server/src/orchestration/Layers/CheckpointReactor.test.ts b/apps/server/src/orchestration/Layers/CheckpointReactor.test.ts index 47f5a1570e6d..1c171648124f 100644 --- a/apps/server/src/orchestration/Layers/CheckpointReactor.test.ts +++ b/apps/server/src/orchestration/Layers/CheckpointReactor.test.ts @@ -512,7 +512,7 @@ describe("CheckpointReactor", () => { const harness = yield* Effect.promise(() => createHarness({ secondThreadSharingWorktree: true, - ...(owner === "alias" || owner === "nested" + ...(owner === "alias" || owner === "nested" || owner === "ancestor" ? { secondThreadWorktreePath: (cwd: string) => { if (owner === "nested") { diff --git a/apps/server/src/orchestration/Layers/CheckpointReactor.ts b/apps/server/src/orchestration/Layers/CheckpointReactor.ts index 91158a5e4189..3504f8bbd533 100644 --- a/apps/server/src/orchestration/Layers/CheckpointReactor.ts +++ b/apps/server/src/orchestration/Layers/CheckpointReactor.ts @@ -727,13 +727,15 @@ const make = Effect.gen(function* () { ), ); if (otherCwd === null) continue; - const relative = path.relative(canonicalCwd, otherCwd); - // An owner in a subdirectory is still affected by a restore of the whole checkout. - if ( - relative === "" || - (!path.isAbsolute(relative) && relative !== ".." && !relative.startsWith(`..${path.sep}`)) - ) - return false; + const isWithin = (parent: string, child: string) => { + const relative = path.relative(parent, child); + return ( + relative === "" || + (!path.isAbsolute(relative) && relative !== ".." && !relative.startsWith(`..${path.sep}`)) + ); + }; + // Parent and nested owners can both have files inside the restore target. + if (isWithin(canonicalCwd, otherCwd) || isWithin(otherCwd, canonicalCwd)) return false; } return true; }); From 8c5f5ffabf2bfd2b65ff1d07783d4847f38c2c1d Mon Sep 17 00:00:00 2001 From: Julius Marminge <51714798+juliusmarminge@users.noreply.github.com> Date: Thu, 17 Sep 2026 13:17:36 -0700 Subject: [PATCH 5/7] test(server): exercise ancestor workspace ownership rejection --- apps/server/src/orchestration/Layers/CheckpointReactor.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/server/src/orchestration/Layers/CheckpointReactor.test.ts b/apps/server/src/orchestration/Layers/CheckpointReactor.test.ts index 1c171648124f..7091055cd2c2 100644 --- a/apps/server/src/orchestration/Layers/CheckpointReactor.test.ts +++ b/apps/server/src/orchestration/Layers/CheckpointReactor.test.ts @@ -505,6 +505,7 @@ describe("CheckpointReactor", () => { "archived", "alias", "nested", + "ancestor", "project-root", "conversation", ] as const)("preserves sibling files when reverting a shared workspace, owner=%s", (owner) => @@ -515,6 +516,7 @@ describe("CheckpointReactor", () => { ...(owner === "alias" || owner === "nested" || owner === "ancestor" ? { secondThreadWorktreePath: (cwd: string) => { + if (owner === "ancestor") return NodePath.dirname(cwd); if (owner === "nested") { const nested = NodePath.join(cwd, "nested-owner"); NodeFS.mkdirSync(nested); From 9f99f07f388fd72b58b21c9947ec395d88edfa5d Mon Sep 17 00:00:00 2001 From: Julius Marminge <51714798+juliusmarminge@users.noreply.github.com> Date: Thu, 17 Sep 2026 16:20:51 -0700 Subject: [PATCH 6/7] fix(web): hide file restore for project-directory threads The server now rejects file restore for threads without an isolated worktree, but the rewind dialog still offered it and surfaced the rejection as an error afterwards. Hide the option and explain why, note the rule in the user guide, and swallow a failed rejection activity append like the sibling failure paths. Co-Authored-By: Claude Fable 5 --- .../orchestration/Layers/CheckpointReactor.ts | 2 +- apps/web/src/components/ChatView.tsx | 25 +++++++++++-------- docs/user/composer.md | 4 ++- 3 files changed, 19 insertions(+), 12 deletions(-) diff --git a/apps/server/src/orchestration/Layers/CheckpointReactor.ts b/apps/server/src/orchestration/Layers/CheckpointReactor.ts index 3504f8bbd533..822df29d0f63 100644 --- a/apps/server/src/orchestration/Layers/CheckpointReactor.ts +++ b/apps/server/src/orchestration/Layers/CheckpointReactor.ts @@ -802,7 +802,7 @@ const make = Effect.gen(function* () { detail: "File restore requires an isolated worktree. This workspace may contain changes from another thread. Rewind the conversation without restoring files instead.", createdAt: now, - }); + }).pipe(Effect.catch(() => Effect.void)); return; } diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 267d420c9d44..cfde95e49133 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -10393,20 +10393,25 @@ export default function ChatView(props: ChatViewProps) { Rewind chat to before this message. Your prompt and attachments return to the composer. + {activeWorktreePath === null + ? " Files stay as they are because this thread shares the project directory." + : null} }>Cancel - + {activeWorktreePath !== null ? ( + + ) : null}