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 1503e37d99e..e6b11bc9955 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -1173,6 +1173,7 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las const local = useLocal() const { theme } = useTheme() const sync = useSync() + const toast = useToast() const messages = createMemo(() => sync.data.message[props.message.sessionID] ?? []) const final = createMemo(() => { @@ -1187,6 +1188,60 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las return props.message.time.completed - user.time.created }) + const copyAssistantResponse = async () => { + const blocks: string[] = [] + + // Get all assistant messages in this chain + const chainMessages = messages() + .filter((m) => m.role === "assistant" && m.parentID === props.message.parentID) + .sort((a, b) => a.id.localeCompare(b.id)) + + for (const message of chainMessages) { + const parts = sync.data.part[message.id] ?? [] + for (const part of parts) { + if (part.type === "text" && !part.synthetic) { + blocks.push(part.text.trim()) + } else if (part.type === "tool") { + const toolName = part.tool + const state = part.state + if (state.status === "completed") { + const metadata = state.metadata ?? {} + + // Build header with title and optional description + const title = state.title || `[${toolName}]` + const desc = metadata.description && metadata.description !== state.title ? metadata.description : "" + const header = desc ? `# ${title}\n${desc}` : `# ${title}` + + // Get content from diff, metadata output, or state output + const content = + [metadata.diff, metadata.output, state.output] + .filter((v): v is string => typeof v === "string" && v.trim().length > 0) + .map((v) => stripAnsi(v.trim())) + .at(0) ?? "" + + if (content) { + blocks.push(`${header}\n\n${content}`) + } else { + blocks.push(header) + } + } + } + } + } + + const fullText = blocks.join("\n\n---\n\n") + + const count = chainMessages.length + await Clipboard.copy(fullText) + .then(() => + toast.show({ + message: `Copied assistant response chain (${count} link${count > 1 ? "s" : ""})`, + variant: "success", + }), + ) + .catch(() => toast.show({ message: "Failed to copy to clipboard", variant: "error" })) + } + return ( <> @@ -1220,7 +1275,7 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las - + ยท interrupted + + + [copy] + +