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);
}