From a764454261e5e2c12cbe4f8b827997ff2f08d198 Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Mon, 3 Aug 2026 02:50:36 -0400 Subject: [PATCH 01/15] fix(web): polish right panel diff layout --- apps/web/src/components/ChatView.tsx | 9 +- apps/web/src/components/DiffPanel.tsx | 121 +++++++++++++++------ apps/web/src/components/DiffPanelShell.tsx | 68 ++++++++---- apps/web/src/components/RightPanelTabs.tsx | 73 ++++++------- apps/web/src/index.css | 7 ++ 5 files changed, 183 insertions(+), 95 deletions(-) diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 836687af3df..94904cd8f32 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -5643,7 +5643,14 @@ function ChatViewContent(props: ChatViewProps) { /> ); const panelLayoutControls = ( -
+
{rightPanelOpen && !shouldUsePlanSidebarSheet ? ( codeViewFiles.map((file) => file.fileKey), [codeViewFiles]); const allDiffFilesCollapsed = areAllDiffFilesCollapsed(diffFileKeys, collapsedDiffFileKeys); const diffLineStat = useMemo(() => getDiffLineStat(renderableFiles), [renderableFiles]); + const selectedDiffFileKey = selectedFilePath + ? (codeViewFiles.find((candidate) => candidate.filePath === selectedFilePath)?.fileKey ?? null) + : null; useEffect(() => { - if (!selectedFilePath) return; - const file = codeViewFiles.find((candidate) => candidate.filePath === selectedFilePath); - if (!file) return; - codeViewRef.current?.scrollTo({ type: "item", id: file.fileKey, align: "start" }); - }, [codeViewFiles, selectedFilePath, selectedFileRevealRequestId]); + if (!selectedDiffFileKey) return; + codeViewRef.current?.scrollTo({ type: "item", id: selectedDiffFileKey, align: "start" }); + }, [codeViewMountKey, selectedDiffFileKey, selectedFileRevealRequestId]); const openDiffFile = useCallback( (filePath: string) => { @@ -730,17 +781,17 @@ export default function DiffPanel({ render={
diff --git a/apps/web/src/components/DiffPanelShell.tsx b/apps/web/src/components/DiffPanelShell.tsx index e727a80055d..c13af4d9560 100644 --- a/apps/web/src/components/DiffPanelShell.tsx +++ b/apps/web/src/components/DiffPanelShell.tsx @@ -10,7 +10,8 @@ export type DiffPanelMode = "inline" | "sheet" | "sidebar" | "embedded"; function getDiffPanelHeaderRowClassName(mode: DiffPanelMode) { const shouldUseDragRegion = isElectron && mode !== "sheet" && mode !== "embedded"; return cn( - "flex items-center justify-between gap-2 px-4", + "flex items-center justify-between gap-2", + mode === "embedded" ? "px-2" : "px-4", shouldUseDragRegion ? "drag-region h-[52px] border-b border-border wco:h-[env(titlebar-area-height)] wco:pr-[calc(100vw-env(titlebar-area-width)-env(titlebar-area-x)+1em)]" : "surface-subheader", @@ -59,30 +60,53 @@ export function DiffPanelHeaderSkeleton() { ); } +function DiffFileHeaderSkeleton({ titleClassName }: { titleClassName: string }) { + return ( +
+
+ +
+ + +
+ + +
+
+ ); +} + +function DiffCodeLineSkeleton({ contentClassName }: { contentClassName: string }) { + return ( +
+ + +
+ ); +} + export function DiffPanelLoadingState(props: { label: string }) { return ( -
-
-
- - -
-
-
- - - - - -
- {props.label} -
+
+ +
+
+ +
+
+
+ + +
+ + + {props.label}
); } diff --git a/apps/web/src/components/RightPanelTabs.tsx b/apps/web/src/components/RightPanelTabs.tsx index fe652b6fde7..2b9fa1660d0 100644 --- a/apps/web/src/components/RightPanelTabs.tsx +++ b/apps/web/src/components/RightPanelTabs.tsx @@ -221,14 +221,14 @@ function surfaceTitle( function PreviewFavicon({ url }: { url: string | null }) { const faviconUrl = faviconUrlForOrigin(url, 32); const [failedUrl, setFailedUrl] = useState(null); - if (!faviconUrl || failedUrl === faviconUrl) return ; + if (!faviconUrl || failedUrl === faviconUrl) return ; return ( setFailedUrl(faviconUrl)} /> ); @@ -250,22 +250,22 @@ function SurfaceIcon({ return ; } case "diff": - return ; + return ; case "files": - return ; + return ; case "file": return ( ); case "terminal": - return ; + return ; case "plan": - return ; + return ; } } @@ -358,7 +358,7 @@ export function RightPanelTabs(props: RightPanelTabsProps) {
handleTabAuxClick(event, surface)} onContextMenu={(event) => void handleTabContextMenu(event, surface)} className={cn( - "group flex h-7 min-w-25 max-w-44 shrink-0 items-center gap-1.5 rounded-md px-2 text-sm", + "group/tab flex h-6 max-w-36 shrink-0 items-center gap-0.5 rounded-md pr-2 pl-1.5 text-xs", active ? "bg-accent text-foreground" : "text-muted-foreground hover:bg-accent/60 hover:text-foreground", )} > + props.onActivate(surface)} > - {title} } /> {title} -
); })} {props.surfaces.length > 0 ? ( - + {props.layoutControls}
-
+
{props.activeSurfaceId === null ? ( Date: Tue, 4 Aug 2026 10:08:17 -0400 Subject: [PATCH 02/15] fix(web): increase diff background contrast --- apps/web/src/components/DiffPanel.tsx | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/apps/web/src/components/DiffPanel.tsx b/apps/web/src/components/DiffPanel.tsx index b5973f0e012..a22d7f7cabb 100644 --- a/apps/web/src/components/DiffPanel.tsx +++ b/apps/web/src/components/DiffPanel.tsx @@ -102,13 +102,25 @@ const DIFF_PANEL_UNSAFE_CSS = ` --diffs-bg-separator-override: color-mix(in srgb, var(--background) 95%, var(--foreground)); --diffs-bg-buffer-override: color-mix(in srgb, var(--background) 90%, var(--foreground)); - --diffs-bg-addition-override: color-mix(in srgb, var(--background) 92%, var(--success)); - --diffs-bg-addition-number-override: color-mix(in srgb, var(--background) 88%, var(--success)); + --diffs-bg-addition-override: light-dark( + color-mix(in srgb, var(--background) 50%, var(--success)), + color-mix(in srgb, var(--background) 70%, var(--success)) + ); + --diffs-bg-addition-number-override: light-dark( + color-mix(in srgb, var(--background) 35%, var(--success)), + color-mix(in srgb, var(--background) 60%, var(--success)) + ); --diffs-bg-addition-hover-override: color-mix(in srgb, var(--background) 85%, var(--success)); --diffs-bg-addition-emphasis-override: color-mix(in srgb, var(--background) 80%, var(--success)); - --diffs-bg-deletion-override: color-mix(in srgb, var(--background) 92%, var(--destructive)); - --diffs-bg-deletion-number-override: color-mix(in srgb, var(--background) 88%, var(--destructive)); + --diffs-bg-deletion-override: light-dark( + color-mix(in srgb, var(--background) 50%, var(--destructive)), + color-mix(in srgb, var(--background) 70%, var(--destructive)) + ); + --diffs-bg-deletion-number-override: light-dark( + color-mix(in srgb, var(--background) 35%, var(--destructive)), + color-mix(in srgb, var(--background) 60%, var(--destructive)) + ); --diffs-bg-deletion-hover-override: color-mix(in srgb, var(--background) 85%, var(--destructive)); --diffs-bg-deletion-emphasis-override: color-mix( in srgb, From 1e69cf55e19ead701fab35c7d65b450ab184eb0c Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Tue, 4 Aug 2026 10:10:44 -0400 Subject: [PATCH 03/15] feat(web): add diff refresh controls --- apps/web/src/components/DiffPanel.tsx | 59 +++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/apps/web/src/components/DiffPanel.tsx b/apps/web/src/components/DiffPanel.tsx index a22d7f7cabb..aca24a8c304 100644 --- a/apps/web/src/components/DiffPanel.tsx +++ b/apps/web/src/components/DiffPanel.tsx @@ -15,6 +15,7 @@ import { ChevronsUpDownIcon, Columns2Icon, PilcrowIcon, + RefreshCwIcon, Rows3Icon, SearchIcon, TextWrapIcon, @@ -267,6 +268,10 @@ export default function DiffPanel({ fileKeys: EMPTY_COLLAPSED_DIFF_FILE_KEYS, })); const codeViewRef = useRef(null); + const lastCompletedTurnRefreshRef = useRef<{ + readonly threadKey: string | null; + readonly turnId: TurnId | null; + } | null>(null); const routeThreadRef = useParams({ strict: false, @@ -422,6 +427,38 @@ export default function DiffPanel({ const branchDiffPreview = shouldRetryBranchDiffAtEnvironmentCwd ? fallbackBranchDiffPreview : primaryBranchDiffPreview; + const refreshBranchDiffPreview = branchDiffPreview.refresh; + const canRefreshGitDiff = + isGitRepo && selectedTurnId === null && activeThread != null && activeCwd != null; + const activeThreadRefreshKey = routeThreadRef + ? `${routeThreadRef.environmentId}:${routeThreadRef.threadId}` + : null; + + useEffect(() => { + if (!canRefreshGitDiff) return; + const refreshOnFocus = () => refreshBranchDiffPreview(); + window.addEventListener("focus", refreshOnFocus); + return () => window.removeEventListener("focus", refreshOnFocus); + }, [canRefreshGitDiff, refreshBranchDiffPreview]); + + useEffect(() => { + const current = { + threadKey: activeThreadRefreshKey, + turnId: latestTurn?.turnId ?? null, + }; + const previous = lastCompletedTurnRefreshRef.current; + lastCompletedTurnRefreshRef.current = current; + if ( + !canRefreshGitDiff || + previous === null || + previous.threadKey !== current.threadKey || + previous.turnId === current.turnId + ) { + return; + } + refreshBranchDiffPreview(); + }, [activeThreadRefreshKey, canRefreshGitDiff, latestTurn?.turnId, refreshBranchDiffPreview]); + const selectedGitSource = branchDiffPreview.data?.sources.find( (source) => source.kind === (selectedGitScope === "unstaged" ? "working-tree" : "branch-range"), ); @@ -787,6 +824,28 @@ export default function DiffPanel({ layout="inline" /> )} + {canRefreshGitDiff && ( + + + } + > + + + + {branchDiffPreview.isPending ? "Refreshing diff…" : "Refresh diff"} + + + )} {codeViewFiles.length > 0 && ( Date: Tue, 4 Aug 2026 13:46:47 -0400 Subject: [PATCH 04/15] feat(review): load full diff file contents --- apps/server/src/auth/RpcAuthorization.ts | 1 + apps/server/src/review/ReviewService.ts | 23 +++ apps/server/src/server.test.ts | 20 +++ apps/server/src/vcs/GitVcsDriver.ts | 5 + apps/server/src/vcs/GitVcsDriverCore.test.ts | 47 ++++++ apps/server/src/vcs/GitVcsDriverCore.ts | 146 +++++++++++++++++++ apps/server/src/ws.ts | 6 + packages/client-runtime/src/state/review.ts | 25 +++- packages/contracts/src/ipc.ts | 10 +- packages/contracts/src/review.ts | 16 ++ packages/contracts/src/rpc.ts | 10 ++ 11 files changed, 307 insertions(+), 2 deletions(-) diff --git a/apps/server/src/auth/RpcAuthorization.ts b/apps/server/src/auth/RpcAuthorization.ts index fb753b9aa4b..c1d9f786adb 100644 --- a/apps/server/src/auth/RpcAuthorization.ts +++ b/apps/server/src/auth/RpcAuthorization.ts @@ -75,6 +75,7 @@ export const RPC_REQUIRED_SCOPES = { [WS_METHODS.vcsSwitchRef]: AuthOrchestrationOperateScope, [WS_METHODS.vcsInit]: AuthOrchestrationOperateScope, [WS_METHODS.reviewGetDiffPreview]: AuthReviewWriteScope, + [WS_METHODS.reviewGetDiffFileContents]: AuthReviewWriteScope, [WS_METHODS.terminalOpen]: AuthTerminalOperateScope, [WS_METHODS.terminalAttach]: AuthTerminalOperateScope, [WS_METHODS.terminalWrite]: AuthTerminalOperateScope, diff --git a/apps/server/src/review/ReviewService.ts b/apps/server/src/review/ReviewService.ts index db1dc5bc8d2..093906815a5 100644 --- a/apps/server/src/review/ReviewService.ts +++ b/apps/server/src/review/ReviewService.ts @@ -8,6 +8,8 @@ import * as Path from "effect/Path"; import { VcsRepositoryDetectionError, VcsUnsupportedOperationError, + type ReviewDiffFileContentsInput, + type ReviewDiffFileContentsResult, type ReviewDiffPreviewError, type ReviewDiffPreviewInput, type ReviewDiffPreviewResult, @@ -23,6 +25,9 @@ export class ReviewService extends Context.Service< readonly getDiffPreview: ( input: ReviewDiffPreviewInput, ) => Effect.Effect; + readonly getDiffFileContents: ( + input: ReviewDiffFileContentsInput, + ) => Effect.Effect; } >()("t3/review/ReviewService") {} @@ -106,8 +111,26 @@ export const make = Effect.gen(function* () { return yield* getDriverDiffPreview(input); }); + const getDiffFileContents: ReviewService["Service"]["getDiffFileContents"] = Effect.fn( + "ReviewService.getDiffFileContents", + )(function* (input) { + yield* assertWorkspaceBoundCwd(input.cwd); + + const handle = yield* vcsRegistry.detect({ cwd: input.cwd, requestedKind: "auto" }); + if (handle?.kind !== "git") { + return yield* new VcsUnsupportedOperationError({ + operation: "ReviewService.getDiffFileContents", + kind: handle?.kind ?? "unknown", + detail: "Unchanged diff expansion currently requires a Git repository.", + }); + } + + return yield* git.getReviewDiffFileContents(input); + }); + return ReviewService.of({ getDiffPreview, + getDiffFileContents, }); }); diff --git a/apps/server/src/server.test.ts b/apps/server/src/server.test.ts index 74d3fd2d594..ae7d4da0bbd 100644 --- a/apps/server/src/server.test.ts +++ b/apps/server/src/server.test.ts @@ -5229,6 +5229,11 @@ it.layer(NodeServices.layer)("server router seam", (it) => { }, ], }), + getDiffFileContents: () => + Effect.succeed({ + oldContents: "before\n", + newContents: "after\n", + }), }, }, }); @@ -5343,6 +5348,21 @@ it.layer(NodeServices.layer)("server router seam", (it) => { ), ); assert.equal(diffPreview.sources[0]?.diff, "dirty-diff"); + + const diffFileContents = yield* Effect.scoped( + withWsRpcClient(wsUrl, (client) => + client[WS_METHODS.reviewGetDiffFileContents]({ + cwd: "/tmp/repo", + sourceKind: "working-tree", + baseRef: "HEAD", + headRef: null, + oldPath: "README.md", + newPath: "README.md", + }), + ), + ); + assert.equal(diffFileContents.oldContents, "before\n"); + assert.equal(diffFileContents.newContents, "after\n"); }).pipe(Effect.provide(NodeHttpServer.layerTest)), ); diff --git a/apps/server/src/vcs/GitVcsDriver.ts b/apps/server/src/vcs/GitVcsDriver.ts index 55aa8f38835..192efe5a7d0 100644 --- a/apps/server/src/vcs/GitVcsDriver.ts +++ b/apps/server/src/vcs/GitVcsDriver.ts @@ -20,6 +20,8 @@ import { type VcsCreateWorktreeResult, type ReviewDiffPreviewInput, type ReviewDiffPreviewResult, + type ReviewDiffFileContentsInput, + type ReviewDiffFileContentsResult, type VcsInitInput, type VcsListRefsInput, type VcsListRefsResult, @@ -221,6 +223,9 @@ export class GitVcsDriver extends Context.Service< readonly getReviewDiffPreview: ( input: ReviewDiffPreviewInput, ) => Effect.Effect; + readonly getReviewDiffFileContents: ( + input: ReviewDiffFileContentsInput, + ) => Effect.Effect; readonly readConfigValue: ( cwd: string, key: string, diff --git a/apps/server/src/vcs/GitVcsDriverCore.test.ts b/apps/server/src/vcs/GitVcsDriverCore.test.ts index 24d53cd4846..e6badce5fbd 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.test.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.test.ts @@ -803,6 +803,53 @@ it.layer(TestLayer)("GitVcsDriver core integration", (it) => { ); }), ); + + it.effect("loads full file contents for working-tree diff expansion", () => + Effect.gen(function* () { + const cwd = yield* makeTmpDir(); + yield* initRepoWithCommit(cwd); + const driver = yield* GitVcsDriver.GitVcsDriver; + const pathService = yield* Path.Path; + yield* writeTextFile(cwd, "nested/.keep", ""); + yield* writeTextFile(cwd, "README.md", "# changed\nunchanged context\n"); + + const contents = yield* driver.getReviewDiffFileContents({ + cwd: pathService.join(cwd, "nested"), + sourceKind: "working-tree", + baseRef: "HEAD", + headRef: null, + oldPath: "README.md", + newPath: "README.md", + }); + + assert.strictEqual(contents.oldContents, "# test\n"); + assert.strictEqual(contents.newContents, "# changed\nunchanged context\n"); + }), + ); + + it.effect("loads merge-base and head contents for branch diff expansion", () => + Effect.gen(function* () { + const cwd = yield* makeTmpDir(); + const { initialBranch } = yield* initRepoWithCommit(cwd); + const driver = yield* GitVcsDriver.GitVcsDriver; + yield* git(cwd, ["checkout", "-b", "feature/context"]); + yield* writeTextFile(cwd, "README.md", "# branch change\nunchanged context\n"); + yield* git(cwd, ["add", "README.md"]); + yield* git(cwd, ["commit", "-m", "change readme"]); + + const contents = yield* driver.getReviewDiffFileContents({ + cwd, + sourceKind: "branch-range", + baseRef: initialBranch, + headRef: "feature/context", + oldPath: "README.md", + newPath: "README.md", + }); + + assert.strictEqual(contents.oldContents, "# test\n"); + assert.strictEqual(contents.newContents, "# branch change\nunchanged context\n"); + }), + ); }); describe("repository status", () => { diff --git a/apps/server/src/vcs/GitVcsDriverCore.ts b/apps/server/src/vcs/GitVcsDriverCore.ts index e44dc048634..4485246c90a 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.ts @@ -21,6 +21,7 @@ import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; import { GitCommandError, + type ReviewDiffFileContentsInput, type ReviewDiffPreviewInput, type ReviewDiffPreviewSource, type VcsRef, @@ -46,6 +47,7 @@ const RANGE_DIFF_SUMMARY_MAX_OUTPUT_BYTES = 19_000; const RANGE_DIFF_PATCH_MAX_OUTPUT_BYTES = 59_000; const REVIEW_DIFF_PATCH_MAX_OUTPUT_BYTES = 120_000; const REVIEW_UNTRACKED_DIFF_MAX_OUTPUT_BYTES = 80_000; +const REVIEW_DIFF_FILE_MAX_OUTPUT_BYTES = 1024 * 1024; const WORKSPACE_FILES_MAX_OUTPUT_BYTES = 120_000; const STATUS_UPSTREAM_REFRESH_INTERVAL = Duration.seconds(15); const STATUS_UPSTREAM_REFRESH_TIMEOUT = Duration.seconds(5); @@ -2270,6 +2272,149 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* }; }); + const reviewDiffFileError = ( + input: ReviewDiffFileContentsInput, + detail: string, + cause?: unknown, + ) => + new GitCommandError({ + operation: "GitVcsDriver.getReviewDiffFileContents", + command: "git", + cwd: input.cwd, + detail, + ...(cause === undefined ? {} : { cause }), + }); + + const readReviewFileAtRevision = Effect.fn("readReviewFileAtRevision")(function* ( + input: ReviewDiffFileContentsInput, + revision: string, + relativePath: string, + ) { + const result = yield* executeGit( + "GitVcsDriver.getReviewDiffFileContents.revision", + input.cwd, + ["show", `${revision}:${relativePath}`], + { maxOutputBytes: REVIEW_DIFF_FILE_MAX_OUTPUT_BYTES }, + ); + if (result.stdout.includes("\0")) { + return yield* reviewDiffFileError(input, `Cannot expand binary file '${relativePath}'.`); + } + return result.stdout; + }); + + const readWorkingTreeReviewFile = Effect.fn("readWorkingTreeReviewFile")(function* ( + input: ReviewDiffFileContentsInput, + repositoryRoot: string, + ) { + const requestedPath = path.resolve(repositoryRoot, input.newPath); + const relativeRequestedPath = path.relative(repositoryRoot, requestedPath); + if ( + relativeRequestedPath === ".." || + relativeRequestedPath.startsWith(`..${path.sep}`) || + path.isAbsolute(relativeRequestedPath) + ) { + return yield* reviewDiffFileError( + input, + `Diff file '${input.newPath}' resolves outside the review workspace.`, + ); + } + + const [realRepositoryRoot, realTarget] = yield* Effect.all([ + fileSystem.realPath(repositoryRoot), + fileSystem.realPath(requestedPath), + ]).pipe( + Effect.mapError((cause) => + reviewDiffFileError(input, `Could not resolve diff file '${input.newPath}'.`, cause), + ), + ); + const relativeRealPath = path.relative(realRepositoryRoot, realTarget); + if ( + relativeRealPath === ".." || + relativeRealPath.startsWith(`..${path.sep}`) || + path.isAbsolute(relativeRealPath) + ) { + return yield* reviewDiffFileError( + input, + `Diff file '${input.newPath}' resolves outside the review workspace.`, + ); + } + + const info = yield* fileSystem + .stat(realTarget) + .pipe( + Effect.mapError((cause) => + reviewDiffFileError(input, `Could not inspect diff file '${input.newPath}'.`, cause), + ), + ); + if (info.type !== "File") { + return yield* reviewDiffFileError(input, `Diff path '${input.newPath}' is not a file.`); + } + if (info.size > BigInt(REVIEW_DIFF_FILE_MAX_OUTPUT_BYTES)) { + return yield* reviewDiffFileError( + input, + `Diff file '${input.newPath}' exceeds the 1 MB expansion limit.`, + ); + } + + const bytes = yield* fileSystem + .readFile(realTarget) + .pipe( + Effect.mapError((cause) => + reviewDiffFileError(input, `Could not read diff file '${input.newPath}'.`, cause), + ), + ); + if (bytes.includes(0)) { + return yield* reviewDiffFileError(input, `Cannot expand binary file '${input.newPath}'.`); + } + return new TextDecoder("utf-8").decode(bytes); + }); + + const getReviewDiffFileContents = Effect.fn("getReviewDiffFileContents")(function* ( + input: ReviewDiffFileContentsInput, + ) { + if (input.sourceKind === "working-tree") { + const repositoryRoot = yield* runGitStdout( + "GitVcsDriver.getReviewDiffFileContents.repositoryRoot", + input.cwd, + ["rev-parse", "--show-toplevel"], + ).pipe(Effect.map((value) => value.trim())); + if (repositoryRoot.length === 0) { + return yield* reviewDiffFileError(input, "Could not resolve the Git repository root."); + } + const [oldContents, newContents] = yield* Effect.all( + [ + readReviewFileAtRevision(input, input.baseRef ?? "HEAD", input.oldPath), + readWorkingTreeReviewFile(input, repositoryRoot), + ], + { concurrency: 2 }, + ); + return { oldContents, newContents }; + } + + if (!input.baseRef || !input.headRef) { + return yield* reviewDiffFileError( + input, + "Branch diff file expansion requires both base and head refs.", + ); + } + const mergeBase = yield* runGitStdout( + "GitVcsDriver.getReviewDiffFileContents.mergeBase", + input.cwd, + ["merge-base", input.baseRef, input.headRef], + ).pipe(Effect.map((value) => value.trim())); + if (mergeBase.length === 0) { + return yield* reviewDiffFileError(input, "Could not resolve the branch comparison base."); + } + const [oldContents, newContents] = yield* Effect.all( + [ + readReviewFileAtRevision(input, mergeBase, input.oldPath), + readReviewFileAtRevision(input, input.headRef, input.newPath), + ], + { concurrency: 2 }, + ); + return { oldContents, newContents }; + }); + const readConfigValue: GitVcsDriver.GitVcsDriver["Service"]["readConfigValue"] = (cwd, key) => runGitStdout("GitVcsDriver.readConfigValue", cwd, ["config", "--get", key], true).pipe( Effect.map((stdout) => stdout.trim()), @@ -2903,6 +3048,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* pullCurrentBranch: (cwd) => withListRefsInvalidation(cwd, pullCurrentBranch(cwd)), readRangeContext, getReviewDiffPreview, + getReviewDiffFileContents, readConfigValue, listRefs, createWorktree: (input) => withListRefsInvalidation(input.cwd, createWorktree(input)), diff --git a/apps/server/src/ws.ts b/apps/server/src/ws.ts index 909a51a4cf5..4ece17cc1d5 100644 --- a/apps/server/src/ws.ts +++ b/apps/server/src/ws.ts @@ -1854,6 +1854,12 @@ const makeWsRpcLayer = ( observeRpcEffect(WS_METHODS.reviewGetDiffPreview, review.getDiffPreview(input), { "rpc.aggregate": "review", }), + [WS_METHODS.reviewGetDiffFileContents]: (input) => + observeRpcEffect( + WS_METHODS.reviewGetDiffFileContents, + review.getDiffFileContents(input), + { "rpc.aggregate": "review" }, + ), [WS_METHODS.terminalOpen]: (input) => observeRpcEffect(WS_METHODS.terminalOpen, terminalManager.open(input), { "rpc.aggregate": "terminal", diff --git a/packages/client-runtime/src/state/review.ts b/packages/client-runtime/src/state/review.ts index 0d78d6edd9f..a05c3e48f48 100644 --- a/packages/client-runtime/src/state/review.ts +++ b/packages/client-runtime/src/state/review.ts @@ -1,17 +1,40 @@ import { WS_METHODS } from "@t3tools/contracts"; import { Atom } from "effect/unstable/reactivity"; -import { createEnvironmentRpcQueryAtomFamily } from "./runtime.ts"; +import { + createAtomCommandScheduler, + createEnvironmentRpcCommand, + createEnvironmentRpcQueryAtomFamily, +} from "./runtime.ts"; import type { EnvironmentRegistry } from "../connection/registry.ts"; export function createReviewEnvironmentAtoms( runtime: Atom.AtomRuntime, ) { + const diffFileScheduler = createAtomCommandScheduler(); return { diffPreview: createEnvironmentRpcQueryAtomFamily(runtime, { label: "environment-data:review:diff-preview", tag: WS_METHODS.reviewGetDiffPreview, staleTimeMs: 5_000, }), + diffFileContents: createEnvironmentRpcCommand(runtime, { + label: "environment-data:review:diff-file-contents", + tag: WS_METHODS.reviewGetDiffFileContents, + scheduler: diffFileScheduler, + concurrency: { + mode: "singleFlight", + key: ({ environmentId, input }) => + JSON.stringify([ + environmentId, + input.cwd, + input.sourceKind, + input.baseRef, + input.headRef, + input.oldPath, + input.newPath, + ]), + }, + }), }; } diff --git a/packages/contracts/src/ipc.ts b/packages/contracts/src/ipc.ts index a92eb972d67..3eb710f0941 100644 --- a/packages/contracts/src/ipc.ts +++ b/packages/contracts/src/ipc.ts @@ -18,7 +18,12 @@ import type { VcsStatusInput, VcsStatusResult, } from "./git.ts"; -import type { ReviewDiffPreviewInput, ReviewDiffPreviewResult } from "./review.ts"; +import type { + ReviewDiffFileContentsInput, + ReviewDiffFileContentsResult, + ReviewDiffPreviewInput, + ReviewDiffPreviewResult, +} from "./review.ts"; import type { FilesystemBrowseInput, FilesystemBrowseResult } from "./filesystem.ts"; import type { AssetCreateUrlInput, AssetCreateUrlResult } from "./assets.ts"; import type { @@ -1203,6 +1208,9 @@ export interface EnvironmentApi { }; review: { getDiffPreview: (input: ReviewDiffPreviewInput) => Promise; + getDiffFileContents: ( + input: ReviewDiffFileContentsInput, + ) => Promise; }; orchestration: { dispatchCommand: (command: ClientOrchestrationCommand) => Promise<{ sequence: number }>; diff --git a/packages/contracts/src/review.ts b/packages/contracts/src/review.ts index a6b879a0c7f..8834517c9bc 100644 --- a/packages/contracts/src/review.ts +++ b/packages/contracts/src/review.ts @@ -25,6 +25,22 @@ export const ReviewDiffPreviewSource = Schema.Struct({ }); export type ReviewDiffPreviewSource = typeof ReviewDiffPreviewSource.Type; +export const ReviewDiffFileContentsInput = Schema.Struct({ + cwd: TrimmedNonEmptyString, + sourceKind: ReviewDiffPreviewSourceKind, + baseRef: Schema.NullOr(TrimmedNonEmptyString), + headRef: Schema.NullOr(TrimmedNonEmptyString), + oldPath: TrimmedNonEmptyString, + newPath: TrimmedNonEmptyString, +}); +export type ReviewDiffFileContentsInput = typeof ReviewDiffFileContentsInput.Type; + +export const ReviewDiffFileContentsResult = Schema.Struct({ + oldContents: Schema.String, + newContents: Schema.String, +}); +export type ReviewDiffFileContentsResult = typeof ReviewDiffFileContentsResult.Type; + export const ReviewDiffPreviewResult = Schema.Struct({ cwd: TrimmedNonEmptyString, generatedAt: Schema.DateTimeUtc, diff --git a/packages/contracts/src/rpc.ts b/packages/contracts/src/rpc.ts index 400011f8843..42cafae31fc 100644 --- a/packages/contracts/src/rpc.ts +++ b/packages/contracts/src/rpc.ts @@ -45,6 +45,8 @@ import { VcsStatusStreamEvent, } from "./git.ts"; import { + ReviewDiffFileContentsInput, + ReviewDiffFileContentsResult, ReviewDiffPreviewError, ReviewDiffPreviewInput, ReviewDiffPreviewResult, @@ -197,6 +199,7 @@ export const WS_METHODS = { // Review methods reviewGetDiffPreview: "review.getDiffPreview", + reviewGetDiffFileContents: "review.getDiffFileContents", // Terminal methods terminalOpen: "terminal.open", @@ -566,6 +569,12 @@ export const WsReviewGetDiffPreviewRpc = Rpc.make(WS_METHODS.reviewGetDiffPrevie error: Schema.Union([ReviewDiffPreviewError, EnvironmentAuthorizationError]), }); +export const WsReviewGetDiffFileContentsRpc = Rpc.make(WS_METHODS.reviewGetDiffFileContents, { + payload: ReviewDiffFileContentsInput, + success: ReviewDiffFileContentsResult, + error: Schema.Union([ReviewDiffPreviewError, EnvironmentAuthorizationError]), +}); + export const WsTerminalOpenRpc = Rpc.make(WS_METHODS.terminalOpen, { payload: TerminalOpenInput, success: TerminalSessionSnapshot, @@ -830,6 +839,7 @@ export const WsRpcGroup = RpcGroup.make( WsVcsSwitchRefRpc, WsVcsInitRpc, WsReviewGetDiffPreviewRpc, + WsReviewGetDiffFileContentsRpc, WsTerminalOpenRpc, WsTerminalAttachRpc, WsTerminalWriteRpc, From 990144fc9eb4a9b58edc58159e85cb28a0ef7d98 Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Tue, 4 Aug 2026 13:48:03 -0400 Subject: [PATCH 05/15] feat(web): expand unchanged diff context --- apps/web/src/components/DiffPanel.tsx | 109 ++++++++++++++++++++++++-- 1 file changed, 102 insertions(+), 7 deletions(-) diff --git a/apps/web/src/components/DiffPanel.tsx b/apps/web/src/components/DiffPanel.tsx index aca24a8c304..ea408212e99 100644 --- a/apps/web/src/components/DiffPanel.tsx +++ b/apps/web/src/components/DiffPanel.tsx @@ -1,4 +1,5 @@ import { useAtomValue } from "@effect/atom-react"; +import type { FileDiffContentsLoader } from "@pierre/diffs"; import { useParams } from "@tanstack/react-router"; import { isAtomCommandInterrupted, @@ -68,6 +69,7 @@ import { } from "./ui/menu"; import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip"; import { useEnvironmentQuery } from "../state/query"; +import { useAtomCommand } from "../state/use-atom-command"; import { serverEnvironment } from "../state/server"; import { reviewEnvironment } from "../state/review"; import { vcsEnvironment } from "../state/vcs"; @@ -177,12 +179,23 @@ const DIFF_PANEL_UNSAFE_CSS = ` color: color-mix(in srgb, var(--foreground) 52%, var(--background)) !important; font-family: var(--font-sans) !important; font-size: 11px !important; + text-decoration: none !important; } :is([data-separator="line-info"], [data-separator="line-info-basic"]) - [data-separator-content]::before, + [data-unmodified-lines] { + display: flex !important; + min-width: 0; + flex: 1 1 auto; + align-items: center; + gap: 8px; + cursor: pointer; +} + +:is([data-separator="line-info"], [data-separator="line-info-basic"]) + [data-unmodified-lines]::before, :is([data-separator="line-info"], [data-separator="line-info-basic"]) - [data-separator-content]::after { + [data-unmodified-lines]::after { width: auto; height: 1px; flex: 1 1 auto; @@ -190,15 +203,44 @@ const DIFF_PANEL_UNSAFE_CSS = ` background-color: color-mix(in srgb, var(--background) 92%, var(--foreground)); } -:is([data-separator="line-info"], [data-separator="line-info-basic"]) - [data-unmodified-lines] { - flex: 0 0 auto; +:is([data-separator="line-info"], [data-separator="line-info-basic"])[data-expand-index] + [data-separator-wrapper] { + grid-template-columns: 0 minmax(0, 1fr) !important; +} + +:is([data-separator="line-info"], [data-separator="line-info-basic"])[data-expand-index] + [data-separator-content] { + grid-column: 2 !important; } :is([data-separator="line-info"], [data-separator="line-info-basic"]) [data-expand-button] { - background-color: transparent !important; - border-color: color-mix(in srgb, var(--background) 92%, var(--foreground)) !important; + display: none !important; +} + +:is([data-separator="line-info"], [data-separator="line-info-basic"]):has( + [data-expand-button] + ) + [data-separator-content] { + cursor: pointer; +} + +:is([data-separator="line-info"], [data-separator="line-info-basic"]):has( + [data-expand-button] + ):hover + [data-separator-content] { + color: color-mix(in srgb, var(--foreground) 76%, var(--background)) !important; +} + +:is([data-separator="line-info"], [data-separator="line-info-basic"]):has( + [data-expand-button] + ):hover + [data-unmodified-lines]::before, +:is([data-separator="line-info"], [data-separator="line-info-basic"]):has( + [data-expand-button] + ):hover + [data-unmodified-lines]::after { + background-color: color-mix(in srgb, var(--background) 84%, var(--foreground)); } [data-diffs-header] [data-header-content] { @@ -296,6 +338,7 @@ export default function DiffPanel({ activeThread?.environmentId ?? null, serverConfig?.availableEditors ?? [], ); + const getDiffFileContents = useAtomCommand(reviewEnvironment.diffFileContents); const gitStatusQuery = useEnvironmentQuery( activeThread !== null && activeThread !== undefined && activeCwd != null ? vcsEnvironment.status({ @@ -462,6 +505,57 @@ export default function DiffPanel({ const selectedGitSource = branchDiffPreview.data?.sources.find( (source) => source.kind === (selectedGitScope === "unstaged" ? "working-tree" : "branch-range"), ); + const loadDiffFiles = useMemo(() => { + const preview = branchDiffPreview.data; + if (selectedTurnId !== null || !activeThread || !preview || !selectedGitSource) { + return undefined; + } + + const source = selectedGitSource; + return async (fileDiff) => { + const newPath = resolveFileDiffPath(fileDiff); + const oldPath = fileDiff.prevName + ? resolveFileDiffPath({ ...fileDiff, name: fileDiff.prevName }) + : newPath; + const result = await getDiffFileContents({ + environmentId: activeThread.environmentId, + input: { + cwd: preview.cwd, + sourceKind: source.kind, + baseRef: source.baseRef, + headRef: source.headRef, + oldPath, + newPath, + }, + }); + if (result._tag !== "Success") { + throw squashAtomCommandFailure(result); + } + + const newFile = { + name: newPath, + contents: result.value.newContents, + cacheKey: `${source.diffHash}:new:${newPath}`, + }; + if (fileDiff.type === "rename-pure") { + return { oldFile: null, newFile }; + } + return { + oldFile: { + name: oldPath, + contents: result.value.oldContents, + cacheKey: `${source.diffHash}:old:${oldPath}`, + }, + newFile, + }; + }; + }, [ + activeThread, + branchDiffPreview.data, + getDiffFileContents, + selectedGitSource, + selectedTurnId, + ]); const localBranchRefs = useEnvironmentQuery( selectedTurnId === null && selectedGitScope === "branch" && @@ -1044,6 +1138,7 @@ export default function DiffPanel({ themeType: resolvedTheme as DiffThemeType, unsafeCSS: DIFF_PANEL_UNSAFE_CSS, stickyHeaders: true, + ...(loadDiffFiles ? { loadDiffFiles } : {}), itemMetrics: { diffHeaderHeight: 32, hunkSeparatorHeight: 24, From 947821fd2feb09b183bd377acb79372a8fe57203 Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Tue, 4 Aug 2026 13:48:54 -0400 Subject: [PATCH 06/15] fix(web): collapse hydrated diffs consistently --- apps/web/src/components/DiffPanel.tsx | 17 ++++++++++---- apps/web/src/lib/diffRendering.test.ts | 31 +++++++++++++++++++++++++- apps/web/src/lib/diffRendering.ts | 5 ++++- 3 files changed, 47 insertions(+), 6 deletions(-) diff --git a/apps/web/src/components/DiffPanel.tsx b/apps/web/src/components/DiffPanel.tsx index ea408212e99..45e944c54d9 100644 --- a/apps/web/src/components/DiffPanel.tsx +++ b/apps/web/src/components/DiffPanel.tsx @@ -309,6 +309,7 @@ export default function DiffPanel({ scopeKey: null, fileKeys: EMPTY_COLLAPSED_DIFF_FILE_KEYS, })); + const [codeViewRevision, setCodeViewRevision] = useState(0); const codeViewRef = useRef(null); const lastCompletedTurnRefreshRef = useRef<{ readonly threadKey: string | null; @@ -407,7 +408,7 @@ export default function DiffPanel({ const collapseScopeKey = routeThreadRef ? `${routeThreadRef.environmentId}:${routeThreadRef.threadId}:${reviewSectionId}` : null; - const codeViewMountKey = collapseScopeKey ?? reviewSectionId; + const codeViewMountKey = `${collapseScopeKey ?? reviewSectionId}:${codeViewRevision}`; const collapsedDiffFileKeys = collapsedDiffFiles.scopeKey === collapseScopeKey ? collapsedDiffFiles.fileKeys @@ -632,10 +633,17 @@ export default function DiffPanel({ }), ); }, [renderablePatch]); + const renderableFileEntries = useMemo( + () => + renderableFiles.map((fileDiff) => ({ + fileDiff, + fileKey: buildFileDiffRenderKey(fileDiff), + })), + [renderableFiles], + ); const codeViewFiles = useMemo( () => - renderableFiles.map((fileDiff) => { - const fileKey = buildFileDiffRenderKey(fileDiff); + renderableFileEntries.map(({ fileDiff, fileKey }) => { return { fileDiff, filePath: resolveFileDiffPath(fileDiff), @@ -643,7 +651,7 @@ export default function DiffPanel({ collapsed: collapsedDiffFileKeys.has(fileKey), }; }), - [collapsedDiffFileKeys, renderableFiles], + [collapsedDiffFileKeys, renderableFileEntries], ); const diffFileKeys = useMemo(() => codeViewFiles.map((file) => file.fileKey), [codeViewFiles]); const allDiffFilesCollapsed = areAllDiffFilesCollapsed(diffFileKeys, collapsedDiffFileKeys); @@ -700,6 +708,7 @@ export default function DiffPanel({ ); const toggleDiffFileCollapse = useCallback(() => { + setCodeViewRevision((current) => current + 1); setCollapsedDiffFiles((current) => { const currentKeys = current.scopeKey === collapseScopeKey ? current.fileKeys : EMPTY_COLLAPSED_DIFF_FILE_KEYS; diff --git a/apps/web/src/lib/diffRendering.test.ts b/apps/web/src/lib/diffRendering.test.ts index 8ab8b25bb41..13cd58984de 100644 --- a/apps/web/src/lib/diffRendering.test.ts +++ b/apps/web/src/lib/diffRendering.test.ts @@ -1,5 +1,10 @@ import { describe, expect, it } from "vite-plus/test"; -import { buildPatchCacheKey, getDiffLineStat, getRenderablePatch } from "./diffRendering"; +import { + buildFileDiffRenderKey, + buildPatchCacheKey, + getDiffLineStat, + getRenderablePatch, +} from "./diffRendering"; describe("buildPatchCacheKey", () => { it("returns a stable cache key for identical content", () => { @@ -83,6 +88,30 @@ describe("getRenderablePatch", () => { }); }); +describe("buildFileDiffRenderKey", () => { + it("keeps file identity stable when Pierre hydrates a partial diff", () => { + const patch = [ + "diff --git a/example.ts b/example.ts", + "--- a/example.ts", + "+++ b/example.ts", + "@@ -1 +1 @@", + "-before", + "+after", + ].join("\n"); + const parsed = getRenderablePatch(patch, "hydrated-key"); + expect(parsed?.kind).toBe("files"); + if (parsed?.kind !== "files") return; + + const file = parsed.files[0]; + expect(file).toBeDefined(); + if (!file) return; + const key = buildFileDiffRenderKey(file); + file.cacheKey = `${file.cacheKey}:hydrated`; + + expect(buildFileDiffRenderKey(file)).toBe(key); + }); +}); + describe("getDiffLineStat", () => { it("totals additions and deletions across every file and hunk", () => { const patch = [ diff --git a/apps/web/src/lib/diffRendering.ts b/apps/web/src/lib/diffRendering.ts index 493474d8aa2..5a9e5d30df3 100644 --- a/apps/web/src/lib/diffRendering.ts +++ b/apps/web/src/lib/diffRendering.ts @@ -152,7 +152,10 @@ export function resolveFileDiffPath(fileDiff: FileDiffMetadata): string { } export function buildFileDiffRenderKey(fileDiff: FileDiffMetadata): string { - return fileDiff.cacheKey ?? `${fileDiff.prevName ?? "none"}:${fileDiff.name}`; + const cacheKey = fileDiff.cacheKey; + if (!cacheKey) return `${fileDiff.prevName ?? "none"}:${fileDiff.name}`; + + return cacheKey.endsWith(":hydrated") ? cacheKey.slice(0, -":hydrated".length) : cacheKey; } export function getDiffCollapseIconClassName(fileDiff: FileDiffMetadata): string { From 7a6a7d25983787f8686a092d3861d4a91f5ad96b Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Tue, 4 Aug 2026 13:49:22 -0400 Subject: [PATCH 07/15] fix(web): restore diff comment interactions --- .../diffs/AnnotatableCodeView.test.tsx | 58 +++++++++++++++++++ .../components/diffs/AnnotatableCodeView.tsx | 2 +- 2 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 apps/web/src/components/diffs/AnnotatableCodeView.test.tsx diff --git a/apps/web/src/components/diffs/AnnotatableCodeView.test.tsx b/apps/web/src/components/diffs/AnnotatableCodeView.test.tsx new file mode 100644 index 00000000000..4759ffbb45d --- /dev/null +++ b/apps/web/src/components/diffs/AnnotatableCodeView.test.tsx @@ -0,0 +1,58 @@ +import { renderToStaticMarkup } from "react-dom/server"; +import { beforeEach, describe, expect, it, vi } from "vite-plus/test"; + +const testState = vi.hoisted(() => ({ + codeViewOptions: null as Record | null, +})); + +vi.mock("@pierre/diffs/react", () => ({ + CodeView: (props: { options: Record }) => { + testState.codeViewOptions = props.options; + return null; + }, +})); + +vi.mock("~/composerDraftStore", () => ({ + useComposerDraftStore: (selector: (store: Record) => unknown) => + selector({ + addReviewComment: vi.fn(), + removeReviewComment: vi.fn(), + getComposerDraft: () => undefined, + }), +})); + +vi.mock("../files/LocalCommentAnnotation", () => ({ + LocalCommentAnnotation: () => null, +})); + +vi.mock("../files/fileCommentAnnotations", () => ({ + nextFileCommentId: () => "comment-test", +})); + +import { AnnotatableCodeView } from "./AnnotatableCodeView"; + +describe("AnnotatableCodeView", () => { + beforeEach(() => { + testState.codeViewOptions = null; + }); + + it("opens comments from Pierre's gutter action without ending line selection", () => { + renderToStaticMarkup( + null} + />, + ); + + expect(testState.codeViewOptions).toMatchObject({ + enableGutterUtility: true, + enableLineSelection: true, + onGutterUtilityClick: expect.any(Function), + }); + expect(testState.codeViewOptions).not.toHaveProperty("onLineSelectionEnd"); + }); +}); diff --git a/apps/web/src/components/diffs/AnnotatableCodeView.tsx b/apps/web/src/components/diffs/AnnotatableCodeView.tsx index 6cea64fb570..5cd6a7e0db0 100644 --- a/apps/web/src/components/diffs/AnnotatableCodeView.tsx +++ b/apps/web/src/components/diffs/AnnotatableCodeView.tsx @@ -241,7 +241,7 @@ export function AnnotatableCodeView({ ...options, enableGutterUtility: !hasOpenComment, enableLineSelection: !hasOpenComment, - onLineSelectionEnd: beginComment, + onGutterUtilityClick: beginComment, }} renderHeaderPrefix={(item) => item.type === "diff" From fcfbef7e81c775e20f6185b6eb7d9b997abc820f Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Tue, 4 Aug 2026 13:49:42 -0400 Subject: [PATCH 08/15] feat(web): simplify inline diff comments --- apps/web/src/components/DiffPanel.tsx | 57 +++++++++++++++++++ .../files/LocalCommentAnnotation.test.tsx | 49 ++++++++++++++++ .../files/LocalCommentAnnotation.tsx | 53 +++++++++-------- 3 files changed, 135 insertions(+), 24 deletions(-) create mode 100644 apps/web/src/components/files/LocalCommentAnnotation.test.tsx diff --git a/apps/web/src/components/DiffPanel.tsx b/apps/web/src/components/DiffPanel.tsx index 45e944c54d9..c80a526fcf5 100644 --- a/apps/web/src/components/DiffPanel.tsx +++ b/apps/web/src/components/DiffPanel.tsx @@ -134,6 +134,63 @@ const DIFF_PANEL_UNSAFE_CSS = ` background-color: var(--diffs-bg) !important; } +:is( + [data-line], + [data-line-annotation], + [data-merge-conflict], + [data-merge-conflict-actions], + [data-no-newline] +)[data-selected-line] { + --diffs-line-bg: light-dark( + color-mix( + in lab, + var(--background) 88%, + color-mix(in srgb, var(--background) 50%, var(--diffs-modified-base)) + ), + color-mix( + in lab, + var(--background) 80%, + color-mix(in srgb, var(--background) 70%, var(--diffs-modified-base)) + ) + ) !important; +} + +:is([data-gutter-buffer], [data-column-number])[data-selected-line] { + --diffs-line-bg: light-dark( + color-mix( + in lab, + var(--background) 91%, + color-mix(in srgb, var(--background) 35%, var(--diffs-modified-base)) + ), + color-mix( + in lab, + var(--background) 85%, + color-mix(in srgb, var(--background) 60%, var(--diffs-modified-base)) + ) + ) !important; +} + +[data-indicators="bars"] + :is([data-column-number], [data-gutter-buffer="annotation"])[data-selected-line] { + position: relative; +} + +[data-indicators="bars"] + :is([data-column-number], [data-gutter-buffer="annotation"])[data-selected-line]::before { + position: absolute !important; + inset-block: 0 !important; + inset-inline-start: 0 !important; + display: block !important; + width: 4px !important; + min-width: 4px !important; + max-width: 4px !important; + height: auto !important; + padding: 0 !important; + content: "" !important; + background-color: var(--diffs-modified-base) !important; + background-image: none !important; +} + [data-file-info] { background-color: var(--background) !important; border-block-color: transparent !important; diff --git a/apps/web/src/components/files/LocalCommentAnnotation.test.tsx b/apps/web/src/components/files/LocalCommentAnnotation.test.tsx new file mode 100644 index 00000000000..7efdcb1fe8c --- /dev/null +++ b/apps/web/src/components/files/LocalCommentAnnotation.test.tsx @@ -0,0 +1,49 @@ +import { renderToStaticMarkup } from "react-dom/server"; +import { describe, expect, it, vi } from "vite-plus/test"; + +import { LocalCommentAnnotation } from "./LocalCommentAnnotation"; + +const callbacks = { + onCancel: vi.fn(), + onComment: vi.fn(), + onDelete: vi.fn(), +}; + +describe("LocalCommentAnnotation", () => { + it("renders the draft composer directly in the selected diff", () => { + const markup = renderToStaticMarkup( + , + ); + + expect(markup).toContain("font-sans"); + expect(markup).not.toContain("chat-composer-glass"); + expect(markup).not.toContain("font-mono"); + expect(markup).not.toContain("Local comment"); + expect(markup).not.toContain("on +78"); + expect(markup).toContain("⌘/Ctrl Enter to send"); + expect(markup).toContain("Add a comment…"); + expect(markup).toContain(">Comment"); + expect(markup).toContain("autofocus"); + const textareaControl = markup.match(/]*data-slot="textarea-control"[^>]*>/)?.[0]; + expect(textareaControl).toBeDefined(); + expect(textareaControl).not.toContain("ring-ring"); + expect(markup).toContain("cursor-text"); + }); + + it("renders a saved comment without a nested card or redundant range label", () => { + const markup = renderToStaticMarkup( + , + ); + + expect(markup).toContain("font-sans"); + expect(markup).not.toContain("chat-composer-glass"); + expect(markup).not.toContain("on +78"); + expect(markup).toContain("Please keep this branch explicit."); + expect(markup).toContain('aria-label="Delete comment"'); + }); +}); diff --git a/apps/web/src/components/files/LocalCommentAnnotation.tsx b/apps/web/src/components/files/LocalCommentAnnotation.tsx index a2765b02d37..611d2cb28b7 100644 --- a/apps/web/src/components/files/LocalCommentAnnotation.tsx +++ b/apps/web/src/components/files/LocalCommentAnnotation.tsx @@ -1,4 +1,4 @@ -import { MessageCircle, Trash2 } from "lucide-react"; +import { Trash2 } from "lucide-react"; import { useState } from "react"; import { Button } from "~/components/ui/button"; @@ -27,21 +27,20 @@ export function LocalCommentAnnotation({ return (
event.stopPropagation()} > -
- - Local comment - {rangeLabel} - -
-

- {savedText} -

+

{savedText}

+
); } @@ -49,23 +48,23 @@ export function LocalCommentAnnotation({ return (
event.stopPropagation()} > -
- - Local comment -
-
Comment on lines {rangeLabel}