From 5489f95d1590a3a9d84b34d27e9be2ed71879a05 Mon Sep 17 00:00:00 2001 From: mask Date: Thu, 12 Mar 2026 20:20:29 -0500 Subject: [PATCH 1/5] Add draft diff context comments for turn diffs --- apps/web/src/components/ChatView.tsx | 43 +- .../components/DiffContextCommentDraft.tsx | 100 ++++ .../src/components/DiffPanel.logic.test.ts | 40 ++ apps/web/src/components/DiffPanel.logic.ts | 430 ++++++++++++++++++ apps/web/src/components/DiffPanel.tsx | 160 ++++++- apps/web/src/components/chat/ChatComposer.tsx | 43 +- .../CompactComposerControlsMenu.browser.tsx | 1 + .../chat/ComposerPendingDiffComments.tsx | 40 ++ .../chat/DiffContextCommentsAttachment.tsx | 37 ++ .../src/components/chat/MessagesTimeline.tsx | 34 +- .../components/chat/TraitsPicker.browser.tsx | 4 + apps/web/src/composerDraftStore.test.ts | 111 +++++ apps/web/src/composerDraftStore.ts | 240 ++++++++++ apps/web/src/lib/diffContextComments.test.ts | 88 ++++ apps/web/src/lib/diffContextComments.ts | 182 ++++++++ 15 files changed, 1517 insertions(+), 36 deletions(-) create mode 100644 apps/web/src/components/DiffContextCommentDraft.tsx create mode 100644 apps/web/src/components/DiffPanel.logic.test.ts create mode 100644 apps/web/src/components/DiffPanel.logic.ts create mode 100644 apps/web/src/components/chat/ComposerPendingDiffComments.tsx create mode 100644 apps/web/src/components/chat/DiffContextCommentsAttachment.tsx create mode 100644 apps/web/src/lib/diffContextComments.test.ts create mode 100644 apps/web/src/lib/diffContextComments.ts diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 0c76059b6a8..5a58c466aba 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -123,6 +123,7 @@ import { buildDraftThreadRouteParams } from "../threadRoutes"; import { type ComposerImageAttachment, type DraftThreadEnvMode, + flushComposerDraftStorage, useComposerDraftStore, type DraftId, } from "../composerDraftStore"; @@ -132,6 +133,7 @@ import { type TerminalContextDraft, type TerminalContextSelection, } from "../lib/terminalContext"; +import { appendDiffContextCommentsToPrompt } from "../lib/diffContextComments"; import { selectThreadTerminalState, useTerminalStateStore } from "../terminalStateStore"; import { ChatComposer, type ChatComposerHandle } from "./chat/ChatComposer"; import { ExpandedImageDialog } from "./chat/ExpandedImageDialog"; @@ -631,17 +633,15 @@ export default function ChatView(props: ChatViewProps) { const composerActiveProvider = useComposerDraftStore( (store) => store.getComposerDraft(composerDraftTarget)?.activeProvider ?? null, ); - const setComposerDraftPrompt = useComposerDraftStore((store) => store.setPrompt); - const addComposerDraftImages = useComposerDraftStore((store) => store.addImages); - const setComposerDraftTerminalContexts = useComposerDraftStore( - (store) => store.setTerminalContexts, - ); const setComposerDraftModelSelection = useComposerDraftStore((store) => store.setModelSelection); const setComposerDraftRuntimeMode = useComposerDraftStore((store) => store.setRuntimeMode); const setComposerDraftInteractionMode = useComposerDraftStore( (store) => store.setInteractionMode, ); const clearComposerDraftContent = useComposerDraftStore((store) => store.clearComposerContent); + const restoreComposerDraftSendContent = useComposerDraftStore( + (store) => store.restoreComposerSendContent, + ); const setDraftThreadContext = useComposerDraftStore((store) => store.setDraftThreadContext); const getDraftSessionByLogicalProjectKey = useComposerDraftStore( (store) => store.getDraftSessionByLogicalProjectKey, @@ -2379,7 +2379,9 @@ export default function ChatView(props: ChatViewProps) { if (!sendCtx) return; const { images: composerImages, + persistedAttachments: composerPersistedAttachments, terminalContexts: composerTerminalContexts, + diffContextComments: composerDiffContextComments, selectedProvider: ctxSelectedProvider, selectedModel: ctxSelectedModel, selectedProviderModels: ctxSelectedProviderModels, @@ -2412,7 +2414,9 @@ export default function ChatView(props: ChatViewProps) { return; } const standaloneSlashCommand = - composerImages.length === 0 && sendableComposerTerminalContexts.length === 0 + composerImages.length === 0 && + sendableComposerTerminalContexts.length === 0 && + composerDiffContextComments.length === 0 ? parseStandaloneComposerSlashCommand(trimmed) : null; if (standaloneSlashCommand) { @@ -2422,7 +2426,8 @@ export default function ChatView(props: ChatViewProps) { composerRef.current?.resetCursorState(); return; } - if (!hasSendableContent) { + const hasPendingDiffContextComments = composerDiffContextComments.length > 0; + if (!hasSendableContent && !hasPendingDiffContextComments) { if (expiredTerminalContextCount > 0) { const toastCopy = buildExpiredTerminalContextToastCopy( expiredTerminalContextCount, @@ -2459,11 +2464,17 @@ export default function ChatView(props: ChatViewProps) { beginLocalDispatch({ preparingWorktree: Boolean(baseBranchForWorktree) }); const composerImagesSnapshot = [...composerImages]; + const composerPersistedAttachmentsSnapshot = [...composerPersistedAttachments]; const composerTerminalContextsSnapshot = [...sendableComposerTerminalContexts]; - const messageTextForSend = appendTerminalContextsToPrompt( + const composerDiffContextCommentsSnapshot = [...composerDiffContextComments]; + const messageTextWithTerminalContexts = appendTerminalContextsToPrompt( promptForSend, composerTerminalContextsSnapshot, ); + const messageTextForSend = appendDiffContextCommentsToPrompt( + messageTextWithTerminalContexts, + composerDiffContextCommentsSnapshot, + ); const messageIdForSend = newMessageId(); const messageCreatedAt = new Date().toISOString(); const outgoingMessageText = formatOutgoingPrompt({ @@ -2526,6 +2537,7 @@ export default function ChatView(props: ChatViewProps) { } promptRef.current = ""; clearComposerDraftContent(composerDraftTarget); + flushComposerDraftStorage(); composerRef.current?.resetCursorState(); let turnStartSucceeded = false; @@ -2630,7 +2642,9 @@ export default function ChatView(props: ChatViewProps) { !turnStartSucceeded && promptRef.current.length === 0 && composerImagesRef.current.length === 0 && - composerTerminalContextsRef.current.length === 0 + composerTerminalContextsRef.current.length === 0 && + (useComposerDraftStore.getState().getComposerDraft(composerDraftTarget)?.diffContextComments + .length ?? 0) === 0 ) { setOptimisticUserMessages((existing) => { const removed = existing.filter((message) => message.id === messageIdForSend); @@ -2644,9 +2658,14 @@ export default function ChatView(props: ChatViewProps) { const retryComposerImages = composerImagesSnapshot.map(cloneComposerImageForRetry); composerImagesRef.current = retryComposerImages; composerTerminalContextsRef.current = composerTerminalContextsSnapshot; - setComposerDraftPrompt(composerDraftTarget, promptForSend); - addComposerDraftImages(composerDraftTarget, retryComposerImages); - setComposerDraftTerminalContexts(composerDraftTarget, composerTerminalContextsSnapshot); + restoreComposerDraftSendContent(composerDraftTarget, { + prompt: promptForSend, + images: retryComposerImages, + persistedAttachments: composerPersistedAttachmentsSnapshot, + terminalContexts: composerTerminalContextsSnapshot, + diffContextComments: composerDiffContextCommentsSnapshot, + }); + flushComposerDraftStorage(); composerRef.current?.resetCursorState({ cursor: collapseExpandedComposerCursor(promptForSend, promptForSend.length), prompt: promptForSend, diff --git a/apps/web/src/components/DiffContextCommentDraft.tsx b/apps/web/src/components/DiffContextCommentDraft.tsx new file mode 100644 index 00000000000..ca19abd5757 --- /dev/null +++ b/apps/web/src/components/DiffContextCommentDraft.tsx @@ -0,0 +1,100 @@ +import { useEffect, useRef } from "react"; +import { Button } from "./ui/button"; + +interface DiffContextCommentDraftProps { + filePath: string; + lineStart: number; + lineEnd: number; + body: string; + error: string; + onBodyChange: (value: string) => void; + onCancel: () => void; + onSubmit: () => void; + onDelete?: () => void; + submitLabel?: string; +} + +function formatLineRange(start: number, end: number): string { + return start === end ? `${start}` : `${start}-${end}`; +} + +export const DIFF_CONTEXT_COMMENT_CARD_STYLE = { + width: "min(44rem, calc(100cqw - 3.5rem), calc(100vw - 7.5rem))", + maxWidth: "100%", +} as const; + +export function DiffContextCommentDraft({ + filePath, + lineStart, + lineEnd, + body, + error, + onBodyChange, + onCancel, + onSubmit, + onDelete, + submitLabel = "Comment", +}: DiffContextCommentDraftProps) { + const textareaRef = useRef(null); + + useEffect(() => { + const textarea = textareaRef.current; + if (!textarea) { + return; + } + + textarea.focus(); + const cursorPosition = textarea.value.length; + textarea.setSelectionRange(cursorPosition, cursorPosition); + }, [filePath, lineStart, lineEnd]); + + return ( +
event.stopPropagation()} + onMouseDown={(event) => event.stopPropagation()} + onPointerDown={(event) => event.stopPropagation()} + > +
+
+