From d7a61083c90cc07b534fa84ab44466802ead34ca Mon Sep 17 00:00:00 2001 From: Thomas Ricouard Date: Wed, 14 Jan 2026 08:59:34 +0100 Subject: [PATCH 1/2] feat(diff): add line range selection composer insert --- src/App.tsx | 50 ++++++++++++++++- src/components/Composer.tsx | 12 +++++ src/components/ComposerInput.tsx | 13 +++++ src/components/DiffBlock.tsx | 55 +++++++++++++++++-- src/components/GitDiffViewer.tsx | 92 +++++++++++++++++++++++++++++++- src/styles/composer.css | 3 ++ src/styles/diff-viewer.css | 45 ++++++++++++++++ src/types.ts | 10 ++++ 8 files changed, 273 insertions(+), 7 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 5aafededb..aeb28504e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -56,7 +56,12 @@ import { useResizablePanels } from "./hooks/useResizablePanels"; import { useLayoutMode } from "./hooks/useLayoutMode"; import { useAppSettings } from "./hooks/useAppSettings"; import { useUpdater } from "./hooks/useUpdater"; -import type { AccessMode, QueuedMessage, WorkspaceInfo } from "./types"; +import type { + AccessMode, + DiffLineReference, + QueuedMessage, + WorkspaceInfo, +} from "./types"; function useWindowLabel() { const [label, setLabel] = useState("main"); @@ -100,6 +105,7 @@ function MainApp() { Record >({}); const [prefillDraft, setPrefillDraft] = useState(null); + const [composerInsert, setComposerInsert] = useState(null); const [settingsOpen, setSettingsOpen] = useState(false); const [reduceTransparency, setReduceTransparency] = useState(() => { const stored = localStorage.getItem("reduceTransparency"); @@ -422,6 +428,36 @@ function MainApp() { } } + function handleDiffLineReference(reference: DiffLineReference) { + const startLine = reference.newLine ?? reference.oldLine; + const endLine = + reference.endNewLine ?? reference.endOldLine ?? startLine ?? null; + const lineRange = + startLine && endLine && endLine !== startLine + ? `${startLine}-${endLine}` + : startLine + ? `${startLine}` + : null; + const lineLabel = lineRange ? `${reference.path}:${lineRange}` : reference.path; + const changeLabel = + reference.type === "add" + ? "added" + : reference.type === "del" + ? "removed" + : reference.type === "mixed" + ? "mixed" + : "context"; + const snippet = reference.lines.join("\n").trimEnd(); + const snippetBlock = snippet ? `\n\`\`\`\n${snippet}\n\`\`\`` : ""; + const label = reference.lines.length > 1 ? "Line range" : "Line reference"; + const text = `${label} (${changeLabel}): ${lineLabel}${snippetBlock}`; + setComposerInsert({ + id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, + text, + createdAt: Date.now(), + }); + } + async function handleSend(text: string) { const trimmed = text.trim(); if (!trimmed) { @@ -543,7 +579,7 @@ function MainApp() { }; const showComposer = !isCompact - ? centerMode === "chat" + ? centerMode === "chat" || centerMode === "diff" : (isTablet ? tabletTab : activeTab) === "codex"; const showGitDetail = Boolean(selectedDiffPath) && isPhone; const appClassName = `app ${isCompact ? "layout-compact" : "layout-desktop"}${ @@ -635,6 +671,12 @@ function MainApp() { setPrefillDraft(null); } }} + insertText={composerInsert} + onInsertHandled={(id) => { + if (composerInsert?.id === id) { + setComposerInsert(null); + } + }} onEditQueued={(item) => { if (!activeThreadId) { return; @@ -741,6 +783,7 @@ function MainApp() { selectedPath={selectedDiffPath} isLoading={isDiffLoading} error={diffError} + onLineReference={handleDiffLineReference} /> ) : ( messagesNode @@ -899,6 +942,7 @@ function MainApp() { selectedPath={selectedDiffPath} isLoading={isDiffLoading} error={diffError} + onLineReference={handleDiffLineReference} /> @@ -998,6 +1042,7 @@ function MainApp() { selectedPath={selectedDiffPath} isLoading={isDiffLoading} error={diffError} + onLineReference={handleDiffLineReference} /> @@ -1056,6 +1101,7 @@ function MainApp() { selectedPath={selectedDiffPath} isLoading={isDiffLoading} error={diffError} + onLineReference={handleDiffLineReference} /> )} diff --git a/src/components/Composer.tsx b/src/components/Composer.tsx index c39c30749..8deccca5d 100644 --- a/src/components/Composer.tsx +++ b/src/components/Composer.tsx @@ -27,6 +27,8 @@ type ComposerProps = { sendLabel?: string; prefillDraft?: QueuedMessage | null; onPrefillHandled?: (id: string) => void; + insertText?: QueuedMessage | null; + onInsertHandled?: (id: string) => void; }; export function Composer({ @@ -51,6 +53,8 @@ export function Composer({ sendLabel = "Send", prefillDraft = null, onPrefillHandled, + insertText = null, + onInsertHandled, }: ComposerProps) { const [text, setText] = useState(""); const [selectionStart, setSelectionStart] = useState(null); @@ -96,6 +100,14 @@ export function Composer({ onPrefillHandled?.(prefillDraft.id); }, [prefillDraft, onPrefillHandled]); + useEffect(() => { + if (!insertText) { + return; + } + setText(insertText.text); + onInsertHandled?.(insertText.id); + }, [insertText, onInsertHandled]); + return (