From a955765b79bea91e2062bd0d96a2fd48d827b9da Mon Sep 17 00:00:00 2001 From: user Date: Thu, 12 Nov 2026 10:51:44 +0100 Subject: [PATCH 1/2] fix(web): typing in the file editor no longer jumps to the prompt Every edit in the file editor re-sent all of its comments to the composer. A comment that had already left the prompt (sent, or its chip removed) was inserted again at the composer caret, which took focus a frame later, so the next keystrokes landed in the prompt. The editor now keeps only drafts and comments still in the prompt, and edits only refresh those, matching the diff view. Co-Authored-By: Claude Opus 5.5 --- .../components/files/FilePreviewPanel.test.ts | 23 ++++++++++++++++++- .../src/components/files/FilePreviewPanel.tsx | 16 +++++++++++-- .../files/fileCommentAnnotations.ts | 16 +++++++++++++ 3 files changed, 52 insertions(+), 3 deletions(-) diff --git a/apps/web/src/components/files/FilePreviewPanel.test.ts b/apps/web/src/components/files/FilePreviewPanel.test.ts index dc68a540842cd..e3b8cd3d3da30 100644 --- a/apps/web/src/components/files/FilePreviewPanel.test.ts +++ b/apps/web/src/components/files/FilePreviewPanel.test.ts @@ -5,7 +5,7 @@ import { ProjectReadFileError } from "@t3tools/contracts"; import * as Schema from "effect/Schema"; import { describe, expect, it, vi } from "vite-plus/test"; -import { remapFileCommentAnnotations } from "./fileCommentAnnotations"; +import { remapFileCommentAnnotations, retainPromptFileComments } from "./fileCommentAnnotations"; import { filePreviewReadErrorMessage, isMarkdownPreviewFile, @@ -63,6 +63,27 @@ describe("file comment annotations", () => { }, ]); }); + + it("drops comments that left the prompt so edits cannot re-add them", () => { + const entry = (id: string, kind: "draft" | "comment") => ({ + id, + kind, + startLine: 3, + endLine: 3, + text: "", + }); + const annotations = [ + { lineNumber: 3, metadata: { entries: [entry("sent", "comment"), entry("new", "draft")] } }, + { lineNumber: 5, metadata: { entries: [entry("removed", "comment")] } }, + { lineNumber: 8, metadata: { entries: [entry("kept", "comment")] } }, + ]; + expect(retainPromptFileComments(annotations, new Set(["kept"]))).toEqual([ + { lineNumber: 3, metadata: { entries: [entry("new", "draft")] } }, + { lineNumber: 8, metadata: { entries: [entry("kept", "comment")] } }, + ]); + const unchanged = [annotations[2]!]; + expect(retainPromptFileComments(unchanged, new Set(["kept"]))).toBe(unchanged); + }); }); describe("isMarkdownPreviewFile", () => { diff --git a/apps/web/src/components/files/FilePreviewPanel.tsx b/apps/web/src/components/files/FilePreviewPanel.tsx index 0ec8e94facc92..4c350fbab6adb 100644 --- a/apps/web/src/components/files/FilePreviewPanel.tsx +++ b/apps/web/src/components/files/FilePreviewPanel.tsx @@ -76,7 +76,7 @@ import { resolvePathLinkTarget } from "@t3tools/shared/fileLinks"; import { ScrollArea } from "~/components/ui/scroll-area"; import { stackedThreadToast, toastManager } from "~/components/ui/toast"; import { type DraftId, useComposerDraftStore } from "~/composerDraftStore"; -import { buildFileReviewComment } from "~/reviewCommentContext"; +import { buildFileReviewComment, type ReviewCommentContext } from "~/reviewCommentContext"; import { assetEnvironment } from "~/state/assets"; import { usePreviewAvailable } from "~/browser/previewRuntime"; import { useEnvironmentHttpBaseUrl, usePrimaryEnvironmentId } from "~/state/environments"; @@ -105,6 +105,7 @@ import { nextFileCommentId, normalizeFileCommentRange, remapFileCommentAnnotations, + retainPromptFileComments, } from "./fileCommentAnnotations"; import { installFileEditorDismissal } from "./fileEditorDismissal"; import { @@ -602,6 +603,8 @@ function useFileLineReveal( }; } +const EMPTY_REVIEW_COMMENTS: ReadonlyArray = []; + const createFileEditor: EditorFactory = ( editorType, options, @@ -695,7 +698,16 @@ function EditableFileSurface({ }: EditableFileSurfaceProps) { const addReviewComment = useComposerDraftStore((store) => store.addReviewComment); const removeReviewComment = useComposerDraftStore((store) => store.removeReviewComment); + const promptCommentIds = new Set( + useComposerDraftStore( + (store) => + store.getComposerDraft(composerDraftTarget)?.reviewComments ?? EMPTY_REVIEW_COMMENTS, + ).map((comment) => comment.id), + ); const [lineAnnotations, setLineAnnotations] = useState([]); + // Sent or removed comments leave the editor too; re-adding one on edit would refocus the composer. + const promptLineAnnotations = retainPromptFileComments(lineAnnotations, promptCommentIds); + if (promptLineAnnotations !== lineAnnotations) setLineAnnotations(promptLineAnnotations); const [selectionOverride, setSelectionOverride] = useState(null); const selectedRange = selectionOverride?.revealRequestId === revealRequestId ? selectionOverride.range : null; @@ -743,7 +755,7 @@ function EditableFileSurface({ setLineAnnotations((current) => (current === nextLineAnnotations ? current : remapped)); for (const annotation of remapped) { for (const entry of annotation.metadata.entries) { - if (entry.kind !== "comment") continue; + if (entry.kind !== "comment" || !promptCommentIds.has(entry.id)) continue; addReviewComment( composerDraftTarget, buildFileReviewComment({ diff --git a/apps/web/src/components/files/fileCommentAnnotations.ts b/apps/web/src/components/files/fileCommentAnnotations.ts index cbd4394cbf745..3052925aaf930 100644 --- a/apps/web/src/components/files/fileCommentAnnotations.ts +++ b/apps/web/src/components/files/fileCommentAnnotations.ts @@ -52,3 +52,19 @@ export function remapFileCommentAnnotations( }, })); } + +/** Keeps drafts and the comments still in the prompt; returns the same array when nothing left. */ +export function retainPromptFileComments( + annotations: FileCommentLineAnnotation[], + promptCommentIds: ReadonlySet, +): FileCommentLineAnnotation[] { + const isRetained = (entry: FileCommentAnnotationEntry) => + entry.kind === "draft" || promptCommentIds.has(entry.id); + if (annotations.every((annotation) => annotation.metadata.entries.every(isRetained))) { + return annotations; + } + return annotations.flatMap((annotation) => { + const entries = annotation.metadata.entries.filter(isRetained); + return entries.length > 0 ? [{ ...annotation, metadata: { entries } }] : []; + }); +} From a64ed60adbe69daaf462a8009556bb145cb082a4 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 14 Nov 2026 18:01:23 +0000 Subject: [PATCH 2/2] fix(web): keep sent file comments annotated so a failed send can restore them Hiding comments that left the prompt now happens at render time instead of deleting them from editor state, so the editor keeps remapping them and a failed send that restores the draft brings the annotations back. Co-Authored-By: Claude Opus 5.5 --- .../components/files/FilePreviewPanel.test.ts | 21 +++---- .../src/components/files/FilePreviewPanel.tsx | 59 ++++++++++--------- .../files/fileCommentAnnotations.ts | 18 ++---- 3 files changed, 45 insertions(+), 53 deletions(-) diff --git a/apps/web/src/components/files/FilePreviewPanel.test.ts b/apps/web/src/components/files/FilePreviewPanel.test.ts index 1cf8f56eaf3ce..16572a85f416b 100644 --- a/apps/web/src/components/files/FilePreviewPanel.test.ts +++ b/apps/web/src/components/files/FilePreviewPanel.test.ts @@ -5,7 +5,7 @@ import { ProjectReadFileError } from "@t3tools/contracts"; import * as Schema from "effect/Schema"; import { describe, expect, it, vi } from "vite-plus/test"; -import { remapFileCommentAnnotations, retainPromptFileComments } from "./fileCommentAnnotations"; +import { promptFileCommentEntries, remapFileCommentAnnotations } from "./fileCommentAnnotations"; import { filePreviewReadErrorMessage, isMarkdownPreviewFile, @@ -64,7 +64,7 @@ describe("file comment annotations", () => { ]); }); - it("drops comments that left the prompt so edits cannot re-add them", () => { + it("shows only drafts and comments still in the prompt", () => { const entry = (id: string, kind: "draft" | "comment") => ({ id, kind, @@ -72,17 +72,12 @@ describe("file comment annotations", () => { endLine: 3, text: "", }); - const annotations = [ - { lineNumber: 3, metadata: { entries: [entry("sent", "comment"), entry("new", "draft")] } }, - { lineNumber: 5, metadata: { entries: [entry("removed", "comment")] } }, - { lineNumber: 8, metadata: { entries: [entry("kept", "comment")] } }, - ]; - expect(retainPromptFileComments(annotations, new Set(["kept"]))).toEqual([ - { lineNumber: 3, metadata: { entries: [entry("new", "draft")] } }, - { lineNumber: 8, metadata: { entries: [entry("kept", "comment")] } }, - ]); - const unchanged = [annotations[2]!]; - expect(retainPromptFileComments(unchanged, new Set(["kept"]))).toBe(unchanged); + expect( + promptFileCommentEntries( + [entry("sent", "comment"), entry("new", "draft"), entry("kept", "comment")], + new Set(["kept"]), + ), + ).toEqual([entry("new", "draft"), entry("kept", "comment")]); }); }); diff --git a/apps/web/src/components/files/FilePreviewPanel.tsx b/apps/web/src/components/files/FilePreviewPanel.tsx index 08089d611717c..f21183011f99c 100644 --- a/apps/web/src/components/files/FilePreviewPanel.tsx +++ b/apps/web/src/components/files/FilePreviewPanel.tsx @@ -105,7 +105,7 @@ import { nextFileCommentId, normalizeFileCommentRange, remapFileCommentAnnotations, - retainPromptFileComments, + promptFileCommentEntries, } from "./fileCommentAnnotations"; import { installFileEditorDismissal } from "./fileEditorDismissal"; import { @@ -770,9 +770,6 @@ function EditableFileSurface({ ).map((comment) => comment.id), ); const [lineAnnotations, setLineAnnotations] = useState([]); - // Sent or removed comments leave the editor too; re-adding one on edit would refocus the composer. - const promptLineAnnotations = retainPromptFileComments(lineAnnotations, promptCommentIds); - if (promptLineAnnotations !== lineAnnotations) setLineAnnotations(promptLineAnnotations); const [selectionOverride, setSelectionOverride] = useState(null); const selectedRange = selectionOverride?.revealRequestId === revealRequestId ? selectionOverride.range : null; @@ -986,29 +983,37 @@ function EditableFileSurface({ }} selectedLines={selectedRange} lineAnnotations={lineAnnotations} - renderAnnotation={(annotation) => ( -
- {annotation.metadata.entries.map((entry) => ( - removeAnnotationEntry(entry.id)} - onComment={(text) => submitAnnotationEntry(entry.id, text)} - onDelete={() => removeAnnotationEntry(entry.id)} - /> - ))} -
- )} + renderAnnotation={(annotation) => { + // Sent comments stay annotated so a failed send can restore them at remapped lines. + const entries = promptFileCommentEntries( + annotation.metadata.entries, + promptCommentIds, + ); + if (entries.length === 0) return null; + return ( +
+ {entries.map((entry) => ( + removeAnnotationEntry(entry.id)} + onComment={(text) => submitAnnotationEntry(entry.id, text)} + onDelete={() => removeAnnotationEntry(entry.id)} + /> + ))} +
+ ); + }} className="min-h-full" /> diff --git a/apps/web/src/components/files/fileCommentAnnotations.ts b/apps/web/src/components/files/fileCommentAnnotations.ts index 3052925aaf930..2ee2ccb0c0f8b 100644 --- a/apps/web/src/components/files/fileCommentAnnotations.ts +++ b/apps/web/src/components/files/fileCommentAnnotations.ts @@ -53,18 +53,10 @@ export function remapFileCommentAnnotations( })); } -/** Keeps drafts and the comments still in the prompt; returns the same array when nothing left. */ -export function retainPromptFileComments( - annotations: FileCommentLineAnnotation[], +/** Drafts and the comments still in the prompt; sent or removed ones are not shown or re-added. */ +export function promptFileCommentEntries( + entries: ReadonlyArray, promptCommentIds: ReadonlySet, -): FileCommentLineAnnotation[] { - const isRetained = (entry: FileCommentAnnotationEntry) => - entry.kind === "draft" || promptCommentIds.has(entry.id); - if (annotations.every((annotation) => annotation.metadata.entries.every(isRetained))) { - return annotations; - } - return annotations.flatMap((annotation) => { - const entries = annotation.metadata.entries.filter(isRetained); - return entries.length > 0 ? [{ ...annotation, metadata: { entries } }] : []; - }); +): FileCommentAnnotationEntry[] { + return entries.filter((entry) => entry.kind === "draft" || promptCommentIds.has(entry.id)); }