From d3a0059f80723302af785760eb55e79427294b69 Mon Sep 17 00:00:00 2001 From: Ariane Emory Date: Mon, 15 Dec 2025 00:23:21 -0500 Subject: [PATCH 1/5] feat: retain message content when forking session Add initialPrompt support to SessionRoute to populate input box with selected message content when forking a session. - Extend SessionRoute type to support optional initialPrompt - Add effect in session component to handle initial prompt from fork - Modify fork action to extract message content and pass it to new session Fixes #5495 --- .../src/cli/cmd/tui/context/route.tsx | 1 + .../cmd/tui/routes/session/dialog-message.tsx | 48 ++++++++++++------- .../src/cli/cmd/tui/routes/session/index.tsx | 43 ++++++++++------- 3 files changed, 58 insertions(+), 34 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/context/route.tsx b/packages/opencode/src/cli/cmd/tui/context/route.tsx index 22333a0589e7..358461921b20 100644 --- a/packages/opencode/src/cli/cmd/tui/context/route.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/route.tsx @@ -10,6 +10,7 @@ export type HomeRoute = { export type SessionRoute = { type: "session" sessionID: string + initialPrompt?: PromptInfo } export type Route = HomeRoute | SessionRoute diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/dialog-message.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/dialog-message.tsx index b9e6632ac8a1..a8527a049e3e 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/dialog-message.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/dialog-message.tsx @@ -71,22 +71,38 @@ export function DialogMessage(props: { dialog.clear() }, }, - { - title: "Fork", - value: "session.fork", - description: "create a new session", - onSelect: async (dialog) => { - const result = await sdk.client.session.fork({ - sessionID: props.sessionID, - messageID: props.messageID, - }) - route.navigate({ - sessionID: result.data!.id, - type: "session", - }) - dialog.clear() - }, - }, + { + title: "Fork", + value: "session.fork", + description: "create a new session", + onSelect: async (dialog) => { + const result = await sdk.client.session.fork({ + sessionID: props.sessionID, + messageID: props.messageID, + }) + const msg = message() + let initialPrompt: PromptInfo | undefined + if (msg) { + const parts = sync.data.part[msg.id] + initialPrompt = parts.reduce( + (agg, part) => { + if (part.type === "text") { + if (!part.synthetic) agg.input += part.text + } + if (part.type === "file") agg.parts.push(part) + return agg + }, + { input: "", parts: [] as PromptInfo["parts"] }, + ) + } + route.navigate({ + sessionID: result.data!.id, + type: "session", + initialPrompt, + }) + dialog.clear() + }, + }, ]} /> ) diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index 1c1e4b65ec1d..fd6b931170e7 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -144,24 +144,31 @@ export function Session() { return new CustomSpeedScroll(3) }) - createEffect(async () => { - await sync.session - .sync(route.sessionID) - .then(() => { - if (scroll) scroll.scrollBy(100_000) - }) - .catch((e) => { - console.error(e) - toast.show({ - message: `Session not found: ${route.sessionID}`, - variant: "error", - }) - return navigate({ type: "home" }) - }) - }) - - const toast = useToast() - const sdk = useSDK() + createEffect(async () => { + await sync.session + .sync(route.sessionID) + .then(() => { + if (scroll) scroll.scrollBy(100_000) + }) + .catch((e) => { + console.error(e) + toast.show({ + message: `Session not found: ${route.sessionID}`, + variant: "error", + }) + return navigate({ type: "home" }) + }) + }) + + const toast = useToast() + const sdk = useSDK() + + // Handle initial prompt from fork + createEffect(() => { + if (route.initialPrompt && prompt) { + prompt.set(route.initialPrompt) + } + }) // Auto-navigate to whichever session currently needs permission input createEffect(() => { From ddcc772e5c0eb9b387afadf40cfd6e3cc38e852c Mon Sep 17 00:00:00 2001 From: Ariane Emory Date: Mon, 15 Dec 2025 00:31:31 -0500 Subject: [PATCH 2/5] tidy: adjust indentation --- .../cmd/tui/routes/session/dialog-message.tsx | 64 +++++++++---------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/dialog-message.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/dialog-message.tsx index a8527a049e3e..c931b9dc2645 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/dialog-message.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/dialog-message.tsx @@ -71,38 +71,38 @@ export function DialogMessage(props: { dialog.clear() }, }, - { - title: "Fork", - value: "session.fork", - description: "create a new session", - onSelect: async (dialog) => { - const result = await sdk.client.session.fork({ - sessionID: props.sessionID, - messageID: props.messageID, - }) - const msg = message() - let initialPrompt: PromptInfo | undefined - if (msg) { - const parts = sync.data.part[msg.id] - initialPrompt = parts.reduce( - (agg, part) => { - if (part.type === "text") { - if (!part.synthetic) agg.input += part.text - } - if (part.type === "file") agg.parts.push(part) - return agg - }, - { input: "", parts: [] as PromptInfo["parts"] }, - ) - } - route.navigate({ - sessionID: result.data!.id, - type: "session", - initialPrompt, - }) - dialog.clear() - }, - }, + { + title: "Fork", + value: "session.fork", + description: "create a new session", + onSelect: async (dialog) => { + const result = await sdk.client.session.fork({ + sessionID: props.sessionID, + messageID: props.messageID, + }) + const msg = message() + let initialPrompt: PromptInfo | undefined + if (msg) { + const parts = sync.data.part[msg.id] + initialPrompt = parts.reduce( + (agg, part) => { + if (part.type === "text") { + if (!part.synthetic) agg.input += part.text + } + if (part.type === "file") agg.parts.push(part) + return agg + }, + { input: "", parts: [] as PromptInfo["parts"] }, + ) + } + route.navigate({ + sessionID: result.data!.id, + type: "session", + initialPrompt, + }) + dialog.clear() + }, + }, ]} /> ) From 6c413345478e7497aef76550e8d722ab9f510332 Mon Sep 17 00:00:00 2001 From: Ariane Emory Date: Mon, 15 Dec 2025 00:33:47 -0500 Subject: [PATCH 3/5] tidy: adjust indentation --- .../src/cli/cmd/tui/routes/session/index.tsx | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index fd6b931170e7..c3a53dc12212 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -144,31 +144,31 @@ export function Session() { return new CustomSpeedScroll(3) }) - createEffect(async () => { - await sync.session - .sync(route.sessionID) - .then(() => { - if (scroll) scroll.scrollBy(100_000) - }) - .catch((e) => { - console.error(e) - toast.show({ - message: `Session not found: ${route.sessionID}`, - variant: "error", - }) - return navigate({ type: "home" }) - }) - }) - - const toast = useToast() - const sdk = useSDK() - - // Handle initial prompt from fork - createEffect(() => { - if (route.initialPrompt && prompt) { - prompt.set(route.initialPrompt) - } - }) + createEffect(async () => { + await sync.session + .sync(route.sessionID) + .then(() => { + if (scroll) scroll.scrollBy(100_000) + }) + .catch((e) => { + console.error(e) + toast.show({ + message: `Session not found: ${route.sessionID}`, + variant: "error", + }) + return navigate({ type: "home" }) + }) + }) + + const toast = useToast() + const sdk = useSDK() + + // Handle initial prompt from fork + createEffect(() => { + if (route.initialPrompt && prompt) { + prompt.set(route.initialPrompt) + } + }) // Auto-navigate to whichever session currently needs permission input createEffect(() => { From 3a651bf523486943173ae37cd19213e8c332618b Mon Sep 17 00:00:00 2001 From: Ariane Emory <97994360+ariane-emory@users.noreply.github.com> Date: Fri, 19 Dec 2025 13:19:44 -0500 Subject: [PATCH 4/5] Update packages/opencode/src/cli/cmd/tui/routes/session/dialog-message.tsx Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .../cmd/tui/routes/session/dialog-message.tsx | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/dialog-message.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/dialog-message.tsx index c931b9dc2645..7a012710d421 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/dialog-message.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/dialog-message.tsx @@ -94,7 +94,21 @@ export function DialogMessage(props: { }, { input: "", parts: [] as PromptInfo["parts"] }, ) - } + const initialPrompt = (() => { + const msg = message() + if (!msg) return undefined + const parts = sync.data.part[msg.id] + return parts.reduce( + (agg, part) => { + if (part.type === "text") { + if (!part.synthetic) agg.input += part.text + } + if (part.type === "file") agg.parts.push(part) + return agg + }, + { input: "", parts: [] as PromptInfo["parts"] }, + ) + })() route.navigate({ sessionID: result.data!.id, type: "session", From 3e5e08b9b5b1dc2cc312c69c1974f78ac459f3ed Mon Sep 17 00:00:00 2001 From: Ariane Emory Date: Fri, 19 Dec 2025 13:32:03 -0500 Subject: [PATCH 5/5] fix(tui): fix syntax error and add initialPrompt to /fork command - Remove duplicate conflicting code in dialog-message.tsx that caused a syntax error from a bad merge - Add initialPrompt support to dialog-fork-from-timeline.tsx so the /fork command also retains the message text in the prompt input box --- .../routes/session/dialog-fork-from-timeline.tsx | 13 +++++++++++++ .../cli/cmd/tui/routes/session/dialog-message.tsx | 14 -------------- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/dialog-fork-from-timeline.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/dialog-fork-from-timeline.tsx index d47d1df3b104..62154cce5636 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/dialog-fork-from-timeline.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/dialog-fork-from-timeline.tsx @@ -6,6 +6,7 @@ import { Locale } from "@/util/locale" import { useSDK } from "@tui/context/sdk" import { useRoute } from "@tui/context/route" import { useDialog } from "../../ui/dialog" +import type { PromptInfo } from "@tui/component/prompt/history" export function DialogForkFromTimeline(props: { sessionID: string; onMove: (messageID: string) => void }) { const sync = useSync() @@ -35,9 +36,21 @@ export function DialogForkFromTimeline(props: { sessionID: string; onMove: (mess sessionID: props.sessionID, messageID: message.id, }) + const parts = sync.data.part[message.id] ?? [] + const initialPrompt = parts.reduce( + (agg, part) => { + if (part.type === "text") { + if (!part.synthetic) agg.input += part.text + } + if (part.type === "file") agg.parts.push(part) + return agg + }, + { input: "", parts: [] as PromptInfo["parts"] }, + ) route.navigate({ sessionID: forked.data!.id, type: "session", + initialPrompt, }) dialog.clear() }, diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/dialog-message.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/dialog-message.tsx index 7a012710d421..86317d62a347 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/dialog-message.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/dialog-message.tsx @@ -80,20 +80,6 @@ export function DialogMessage(props: { sessionID: props.sessionID, messageID: props.messageID, }) - const msg = message() - let initialPrompt: PromptInfo | undefined - if (msg) { - const parts = sync.data.part[msg.id] - initialPrompt = parts.reduce( - (agg, part) => { - if (part.type === "text") { - if (!part.synthetic) agg.input += part.text - } - if (part.type === "file") agg.parts.push(part) - return agg - }, - { input: "", parts: [] as PromptInfo["parts"] }, - ) const initialPrompt = (() => { const msg = message() if (!msg) return undefined