From 44034a2a8ed16e2df671e65c6278ebc5fbd9357f Mon Sep 17 00:00:00 2001 From: Markus Podar Date: Thu, 17 Sep 2026 19:55:06 +0200 Subject: [PATCH] feat(tui): copy assistant responses --- .../tui/src/routes/session/dialog-message.tsx | 37 +++++++++++++++++++ packages/tui/src/routes/session/index.tsx | 14 ++++++- .../tui/src/routes/session/message-parts.tsx | 5 ++- 3 files changed, 52 insertions(+), 4 deletions(-) diff --git a/packages/tui/src/routes/session/dialog-message.tsx b/packages/tui/src/routes/session/dialog-message.tsx index 117491cc7d83..077c0980ea19 100644 --- a/packages/tui/src/routes/session/dialog-message.tsx +++ b/packages/tui/src/routes/session/dialog-message.tsx @@ -88,3 +88,40 @@ export function DialogMessage(props: { /> ) } + +export function DialogAssistantMessage(props: { messageID: string; sessionID: string }) { + const data = useData() + const clipboard = useClipboard() + const toast = useToast() + const message = createMemo(() => data.session.message.get(props.sessionID, props.messageID)) + + return ( + { + const value = message() + const text = + value?.type === "assistant" + ? value.content + .filter((content) => content.type === "text") + .map((content) => content.text) + .join("\n") + .trim() + : "" + try { + await clipboard.write(text) + dialog.clear() + } catch (error) { + toast.error(error) + } + }, + }, + ]} + /> + ) +} diff --git a/packages/tui/src/routes/session/index.tsx b/packages/tui/src/routes/session/index.tsx index 9d35bb653313..42310200a2de 100644 --- a/packages/tui/src/routes/session/index.tsx +++ b/packages/tui/src/routes/session/index.tsx @@ -24,7 +24,7 @@ import { useTuiPaths, useTuiTerminalEnvironment } from "../../context/runtime" import { Spinner, SPINNER_FRAMES } from "../../component/spinner" import { PatchDiff } from "../../component/patch-diff" import { createSyntaxStyleMemo, ThemeContextProvider, useTheme, useThemes } from "../../context/theme" -import { BoxRenderable, ScrollBoxRenderable, addDefaultParsers, TextAttributes, RGBA, MouseEvent } from "@opentui/core" +import { BoxRenderable, ScrollBoxRenderable, addDefaultParsers, TextAttributes, RGBA, MouseButton, MouseEvent } from "@opentui/core" import { Prompt, type PromptRef } from "../../component/prompt" import type { SessionMessageInfo, @@ -59,7 +59,7 @@ import { useDialog } from "../../ui/dialog" import { DialogSelect } from "../../ui/dialog-select" import { DialogSessionRename } from "../../component/dialog-session-rename" import { DialogImagePreview } from "../../component/dialog-image-preview" -import { DialogMessage } from "./dialog-message" +import { DialogAssistantMessage, DialogMessage } from "./dialog-message" import { DialogFork } from "./dialog-fork" import { DialogTimeline } from "./dialog-timeline" import { Composer } from "./composer" @@ -1706,7 +1706,10 @@ function SessionMessageView(props: { message: SessionMessageInfo }) { } function SessionPartView(props: { partRef: PartRef; message: (messageID: string) => SessionMessageInfo | undefined }) { + const ctx = use() const message = createMemo(() => props.message(props.partRef.messageID)) + const dialog = useDialog() + const renderer = useRenderer() const part = createMemo(() => { const item = message() if (item?.type !== "assistant") return @@ -1721,6 +1724,13 @@ function SessionPartView(props: { partRef: PartRef; message: (messageID: string) part={item() as SessionMessageAssistantText} message={message() as SessionMessageAssistant} last={false} + onMouseUp={(event) => { + if (event.button !== MouseButton.RIGHT) return + if (renderer.getSelection()?.getSelectedText()) return + dialog.replace(() => ( + + )) + }} /> diff --git a/packages/tui/src/routes/session/message-parts.tsx b/packages/tui/src/routes/session/message-parts.tsx index 97ddd09ea3b3..202ac2ec7d36 100644 --- a/packages/tui/src/routes/session/message-parts.tsx +++ b/packages/tui/src/routes/session/message-parts.tsx @@ -1,5 +1,5 @@ import { createMemo, createSignal, Match, Show, Switch } from "solid-js" -import { RGBA, TextAttributes } from "@opentui/core" +import { MouseEvent, RGBA, TextAttributes } from "@opentui/core" import type { JSX } from "@opentui/solid" import type { SessionMessageAssistant, @@ -148,6 +148,7 @@ export function TextPart(props: { last: boolean part: SessionMessageAssistantText message: SessionMessageAssistant + onMouseUp?: (event: MouseEvent) => void }) { const ctx = use() const theme = useTheme() @@ -155,7 +156,7 @@ export function TextPart(props: { const plugins = usePlugin() return ( - + {/* Configure custom nodes before parsing; apply content before streaming so completion keeps the final tokens. */}