diff --git a/packages/ui/src/components/message-section.tsx b/packages/ui/src/components/message-section.tsx index 0077e7e6..3dde523c 100644 --- a/packages/ui/src/components/message-section.tsx +++ b/packages/ui/src/components/message-section.tsx @@ -33,7 +33,7 @@ export interface MessageSectionProps { showSidebarToggle?: boolean onSidebarToggle?: () => void forceCompactStatusLayout?: boolean - onQuoteSelection?: (text: string) => void + onQuoteSelection?: (text: string, mode: "quote" | "code") => void } export default function MessageSection(props: MessageSectionProps) { @@ -309,10 +309,10 @@ export default function MessageSection(props: MessageSectionProps) { updateQuoteSelectionFromSelection() } - function handleQuoteSelectionRequest() { + function handleQuoteSelectionRequest(mode: "quote" | "code") { const info = quoteSelection() if (!info || !props.onQuoteSelection) return - props.onQuoteSelection(info.text) + props.onQuoteSelection(info.text, mode) clearQuoteSelection() if (typeof window !== "undefined") { const selection = window.getSelection() @@ -618,9 +618,14 @@ export default function MessageSection(props: MessageSectionProps) { class="message-quote-popover" style={{ top: `${selection().top}px`, left: `${selection().left}px` }} > - +
+ + +
)} diff --git a/packages/ui/src/components/prompt-input.tsx b/packages/ui/src/components/prompt-input.tsx index 678e8223..c8f7ef3e 100644 --- a/packages/ui/src/components/prompt-input.tsx +++ b/packages/ui/src/components/prompt-input.tsx @@ -25,7 +25,7 @@ interface PromptInputProps { escapeInDebounce?: boolean isSessionBusy?: boolean onAbortSession?: () => Promise - registerQuoteHandler?: (handler: (text: string) => void) => void | (() => void) + registerQuoteHandler?: (handler: (text: string, mode: "quote" | "code") => void) => void | (() => void) } export default function PromptInput(props: PromptInputProps) { @@ -43,7 +43,7 @@ export default function PromptInput(props: PromptInputProps) { const [pasteCount, setPasteCount] = createSignal(0) const [imageCount, setImageCount] = createSignal(0) const [mode, setMode] = createSignal<"normal" | "shell">("normal") - const QUOTE_INSERT_MAX_LENGTH = 2000 + const SELECTION_INSERT_MAX_LENGTH = 2000 let textareaRef: HTMLTextAreaElement | undefined let containerRef: HTMLDivElement | undefined @@ -55,7 +55,13 @@ export default function PromptInput(props: PromptInputProps) { createEffect(() => { if (!props.registerQuoteHandler) return - const cleanup = props.registerQuoteHandler((text) => insertQuotedSelection(text)) + const cleanup = props.registerQuoteHandler((text, mode) => { + if (mode === "code") { + insertCodeSelection(text) + } else { + insertQuotedSelection(text) + } + }) onCleanup(() => { if (typeof cleanup === "function") { cleanup() @@ -881,20 +887,7 @@ export default function PromptInput(props: PromptInputProps) { textareaRef?.focus() } - function insertQuotedSelection(rawText: string) { - const normalized = (rawText ?? "").replace(/\r/g, "").trim() - if (!normalized) return - const limited = - normalized.length > QUOTE_INSERT_MAX_LENGTH ? normalized.slice(0, QUOTE_INSERT_MAX_LENGTH).trimEnd() : normalized - const lines = limited - .split(/\n/) - .map((line) => line.trim()) - .filter((line) => line.length > 0) - if (lines.length === 0) return - - const blockquote = lines.map((line) => `> ${line}`).join("\n") - if (!blockquote) return - + function insertBlockContent(block: string) { const textarea = textareaRef const current = prompt() const start = textarea ? textarea.selectionStart : current.length @@ -902,7 +895,7 @@ export default function PromptInput(props: PromptInputProps) { const before = current.substring(0, start) const after = current.substring(end) const needsLeading = before.length > 0 && !before.endsWith("\n") ? "\n" : "" - const insertion = `${needsLeading}${blockquote}\n\n` + const insertion = `${needsLeading}${block}` const nextValue = before + insertion + after setPrompt(nextValue) @@ -920,6 +913,38 @@ export default function PromptInput(props: PromptInputProps) { } } + function insertQuotedSelection(rawText: string) { + const normalized = (rawText ?? "").replace(/\r/g, "").trim() + if (!normalized) return + const limited = + normalized.length > SELECTION_INSERT_MAX_LENGTH + ? normalized.slice(0, SELECTION_INSERT_MAX_LENGTH).trimEnd() + : normalized + const lines = limited + .split(/\n/) + .map((line) => line.trim()) + .filter((line) => line.length > 0) + if (lines.length === 0) return + + const blockquote = lines.map((line) => `> ${line}`).join("\n") + if (!blockquote) return + + insertBlockContent(`${blockquote}\n\n`) + } + + function insertCodeSelection(rawText: string) { + const normalized = (rawText ?? "").replace(/\r/g, "") + const limited = + normalized.length > SELECTION_INSERT_MAX_LENGTH + ? normalized.slice(0, SELECTION_INSERT_MAX_LENGTH) + : normalized + const trimmed = limited.replace(/^\n+/, "").replace(/\n+$/, "") + if (!trimmed) return + + const block = "```\n" + trimmed + "\n```\n\n" + insertBlockContent(block) + } + const canStop = () => Boolean(props.isSessionBusy && props.onAbortSession) const hasHistory = () => history().length > 0 diff --git a/packages/ui/src/components/session/session-view.tsx b/packages/ui/src/components/session/session-view.tsx index a5d83d04..fe3f5ae1 100644 --- a/packages/ui/src/components/session/session-view.tsx +++ b/packages/ui/src/components/session/session-view.tsx @@ -38,7 +38,7 @@ export const SessionView: Component = (props) => { return getSessionBusyStatus(props.instanceId, currentSession.id) }) let scrollToBottomHandle: (() => void) | undefined - let quoteHandler: ((text: string) => void) | null = null + let quoteHandler: ((text: string, mode: "quote" | "code") => void) | null = null createEffect(() => { const currentSession = session() @@ -47,7 +47,7 @@ export const SessionView: Component = (props) => { } }) - function registerQuoteHandler(handler: (text: string) => void) { + function registerQuoteHandler(handler: (text: string, mode: "quote" | "code") => void) { quoteHandler = handler return () => { if (quoteHandler === handler) { @@ -56,9 +56,9 @@ export const SessionView: Component = (props) => { } } - function handleQuoteSelection(text: string) { + function handleQuoteSelection(text: string, mode: "quote" | "code") { if (quoteHandler) { - quoteHandler(text) + quoteHandler(text, mode) } } diff --git a/packages/ui/src/styles/messaging/message-section.css b/packages/ui/src/styles/messaging/message-section.css index acda92d4..7cb94733 100644 --- a/packages/ui/src/styles/messaging/message-section.css +++ b/packages/ui/src/styles/messaging/message-section.css @@ -238,25 +238,37 @@ pointer-events: none; } -.message-quote-button { +.message-quote-button-group { pointer-events: auto; - @apply inline-flex items-center gap-2; - padding: 0.35rem 0.85rem; - font-size: 0.8rem; - font-weight: 500; + display: inline-flex; border-radius: 9999px; border: 1px solid var(--list-item-highlight-border); background-color: var(--list-item-highlight-bg-solid); - color: var(--text-primary); box-shadow: var(--panel-shadow, 0 4px 16px rgba(0, 0, 0, 0.2)); + overflow: hidden; +} + +.message-quote-button { + pointer-events: auto; + @apply inline-flex items-center justify-center; + padding: 0.35rem 0.9rem; + font-size: 0.8rem; + font-weight: 500; + border: none; + background-color: transparent; + color: var(--text-primary); transition: background-color 0.2s ease, color 0.2s ease; } +.message-quote-button + .message-quote-button { + border-left: 1px solid var(--list-item-highlight-border); +} + .message-quote-button:hover { background-color: var(--surface-hover); } .message-quote-button:focus-visible { outline: none; - box-shadow: 0 0 0 2px var(--surface-base), 0 0 0 4px var(--accent-primary); + box-shadow: inset 0 0 0 2px var(--accent-primary); }