diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-remote-session-list.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-remote-session-list.tsx index 9cba3e011e09..bd11b6cda2ae 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-remote-session-list.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-remote-session-list.tsx @@ -10,6 +10,18 @@ type Session = { title?: string } +export function getRemoteBrowse(input: { root: Session; sessions: Session[]; fork?: boolean }) { + return { + title: input.fork ? "Fork from remote session" : "Continue remote session", + options: [input.root, ...input.sessions].map((item, idx) => ({ + title: item.title ?? item.id, + value: item.id, + footer: item.id, + description: input.fork ? (idx === 0 ? "Fork from root session" : "Fork from child session") : undefined, + })), + } +} + type OpenInput = { id: string fork?: boolean @@ -92,15 +104,12 @@ function DialogRemoteSessionBrowse(props: { root: Session; sessions: Session[]; const route = useRoute() const sdk = useSDK() const toast = useToast() + const browse = getRemoteBrowse(props) return ( ({ - title: item.title ?? item.id, - value: item.id, - footer: item.id, - }))} + title={browse.title} + options={browse.options} skipFilter={true} onSelect={(option) => { void openRemoteSession({ diff --git a/packages/opencode/test/cli/tui/attach-startup.test.ts b/packages/opencode/test/cli/tui/attach-startup.test.ts index 8ba070716c3e..39bab741e519 100644 --- a/packages/opencode/test/cli/tui/attach-startup.test.ts +++ b/packages/opencode/test/cli/tui/attach-startup.test.ts @@ -5,6 +5,45 @@ afterEach(() => { }) describe("attach startup", () => { + test("fork browse copy makes the fork target explicit", async () => { + const mod: Record = await import("../../../src/cli/cmd/tui/component/dialog-remote-session-list") + const fn = mod["getRemoteBrowse"] + expect(fn).toBeTypeOf("function") + + if (typeof fn !== "function") return + const result = fn({ + root: { + id: "sess_root", + title: "Root draft", + }, + sessions: [ + { + id: "sess_child", + title: "Child fix", + }, + ], + fork: true, + }) + + expect(result).toEqual({ + title: "Fork from remote session", + options: [ + { + title: "Root draft", + value: "sess_root", + footer: "sess_root", + description: "Fork from root session", + }, + { + title: "Child fix", + value: "sess_child", + footer: "sess_child", + description: "Fork from child session", + }, + ], + }) + }) + test("root without children navigates inside the tui flow", async () => { const mod: Record = await import("../../../src/cli/cmd/tui/component/dialog-remote-session-list") const fn = mod["selectRemoteSession"] @@ -159,4 +198,100 @@ describe("attach startup", () => { expect(sdk.client.session.fork).not.toHaveBeenCalled() expect(toast.show).not.toHaveBeenCalled() }) + + test("fork mode root selection forks from that root", async () => { + const mod: Record = await import("../../../src/cli/cmd/tui/component/dialog-remote-session-list") + const fn = mod["openRemoteSession"] + expect(fn).toBeTypeOf("function") + + const route = { + navigate: mock(() => {}), + } + const dialog = { + clear: mock(() => {}), + } + const fork = mock(async () => ({ + data: { + id: "sess_forked_root", + }, + })) + const sdk = { + client: { + session: { + fork, + }, + }, + } + const toast = { + show: mock(() => {}), + } + + if (typeof fn !== "function") return + await fn({ + id: "sess_root", + fork: true, + route, + dialog, + sdk, + toast, + }) + + expect(fork).toHaveBeenCalledWith({ + sessionID: "sess_root", + }) + expect(route.navigate).toHaveBeenCalledWith({ + type: "session", + sessionID: "sess_forked_root", + }) + expect(dialog.clear).toHaveBeenCalledTimes(1) + expect(toast.show).not.toHaveBeenCalled() + }) + + test("fork mode child selection forks from that child", async () => { + const mod: Record = await import("../../../src/cli/cmd/tui/component/dialog-remote-session-list") + const fn = mod["openRemoteSession"] + expect(fn).toBeTypeOf("function") + + const route = { + navigate: mock(() => {}), + } + const dialog = { + clear: mock(() => {}), + } + const fork = mock(async () => ({ + data: { + id: "sess_forked_child", + }, + })) + const sdk = { + client: { + session: { + fork, + }, + }, + } + const toast = { + show: mock(() => {}), + } + + if (typeof fn !== "function") return + await fn({ + id: "sess_child", + fork: true, + route, + dialog, + sdk, + toast, + }) + + expect(fork).toHaveBeenCalledWith({ + sessionID: "sess_child", + }) + expect(route.navigate).toHaveBeenCalledWith({ + type: "session", + sessionID: "sess_forked_child", + }) + expect(dialog.clear).toHaveBeenCalledTimes(1) + expect(toast.show).not.toHaveBeenCalled() + }) })