From b1491690cddf1c3351d3d9486209ab1ba1ae2b52 Mon Sep 17 00:00:00 2001 From: Shoubhit Dash Date: Sat, 30 May 2026 01:54:28 +0530 Subject: [PATCH] refactor(tui): extract shared message actions --- .../cmd/tui/routes/session/dialog-message.tsx | 67 ++++--------------- .../cmd/tui/routes/session/message-actions.ts | 64 ++++++++++++++++++ 2 files changed, 78 insertions(+), 53 deletions(-) create mode 100644 packages/opencode/src/cli/cmd/tui/routes/session/message-actions.ts 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 aeea2f52ad..cdf3fe3331 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 @@ -5,7 +5,7 @@ import { useSDK } from "@tui/context/sdk" import { useRoute } from "@tui/context/route" import * as Clipboard from "@tui/util/clipboard" import type { PromptInfo } from "@tui/component/prompt/history" -import { strip } from "@tui/component/prompt/part" +import { MessageActions } from "./message-actions" export function DialogMessage(props: { messageID: string @@ -26,29 +26,14 @@ export function DialogMessage(props: { value: "session.revert", description: "undo messages and file changes", onSelect: (dialog) => { - const msg = message() - if (!msg) return - - void sdk.client.session.revert({ + if (!message()) return + MessageActions.revert({ + sdk, + sync, sessionID: props.sessionID, - messageID: msg.id, + messageID: props.messageID, + setPrompt: props.setPrompt, }) - - if (props.setPrompt) { - const parts = sync.data.part[msg.id] - const promptInfo = parts.reduce( - (agg, part) => { - if (part.type === "text") { - if (!part.synthetic) agg.input += part.text - } - if (part.type === "file") agg.parts.push(strip(part)) - return agg - }, - { input: "", parts: [] as PromptInfo["parts"] }, - ) - props.setPrompt(promptInfo) - } - dialog.clear() }, }, @@ -57,18 +42,8 @@ export function DialogMessage(props: { value: "message.copy", description: "message text to clipboard", onSelect: async (dialog) => { - const msg = message() - if (!msg) return - - const parts = sync.data.part[msg.id] - const text = parts.reduce((agg, part) => { - if (part.type === "text" && !part.synthetic) { - agg += part.text - } - return agg - }, "") - - await Clipboard.copy(text) + if (!message()) return + await Clipboard.copy(MessageActions.collectText(sync, props.messageID)) dialog.clear() }, }, @@ -77,28 +52,14 @@ export function DialogMessage(props: { value: "session.fork", description: "create a new session", onSelect: async (dialog) => { - const result = await sdk.client.session.fork({ + if (!message()) return + await MessageActions.fork({ + sdk, + sync, + navigate: route.navigate, sessionID: props.sessionID, messageID: props.messageID, }) - const msg = message() - const prompt = msg - ? sync.data.part[msg.id].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"] }, - ) - : undefined - route.navigate({ - sessionID: result.data!.id, - type: "session", - prompt, - }) dialog.clear() }, }, diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/message-actions.ts b/packages/opencode/src/cli/cmd/tui/routes/session/message-actions.ts new file mode 100644 index 0000000000..1f1eb50fec --- /dev/null +++ b/packages/opencode/src/cli/cmd/tui/routes/session/message-actions.ts @@ -0,0 +1,64 @@ +import type { useSDK } from "@tui/context/sdk" +import type { useSync } from "@tui/context/sync" +import type { useRoute } from "@tui/context/route" +import type { PromptInfo } from "@tui/component/prompt/history" +import { strip } from "@tui/component/prompt/part" + +type SDK = ReturnType +type Sync = ReturnType +type Navigate = ReturnType["navigate"] + +// File parts keep their server identifiers when seeding a new session (fork) but +// are stripped when the draft is re-submitted into the same session (revert). +export function collectPrompt(sync: Sync, messageID: string, options: { stripFiles: boolean }): PromptInfo | undefined { + const parts = sync.data.part[messageID] + if (!parts) return + return parts.reduce( + (agg, part) => { + if (part.type === "text") { + if (!part.synthetic) agg.input += part.text + } + if (part.type === "file") agg.parts.push(options.stripFiles ? strip(part) : part) + return agg + }, + { input: "", parts: [] as PromptInfo["parts"] }, + ) +} + +export function collectText(sync: Sync, messageID: string) { + const parts = sync.data.part[messageID] ?? [] + return parts.reduce((text, part) => { + if (part.type === "text" && !part.synthetic) text += part.text + return text + }, "") +} + +export function revert(options: { + sdk: SDK + sync: Sync + sessionID: string + messageID: string + setPrompt?: (prompt: PromptInfo) => void +}) { + void options.sdk.client.session.revert({ sessionID: options.sessionID, messageID: options.messageID }) + if (!options.setPrompt) return + const prompt = collectPrompt(options.sync, options.messageID, { stripFiles: true }) + if (prompt) options.setPrompt(prompt) +} + +export async function fork(options: { + sdk: SDK + sync: Sync + navigate: Navigate + sessionID: string + messageID: string +}) { + const result = await options.sdk.client.session.fork({ sessionID: options.sessionID, messageID: options.messageID }) + options.navigate({ + type: "session", + sessionID: result.data!.id, + prompt: collectPrompt(options.sync, options.messageID, { stripFiles: false }), + }) +} + +export * as MessageActions from "./message-actions"