From db31c362135d7089766111eb3ed8e5a2f2cc0397 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 28 Feb 2026 21:23:50 -0800 Subject: [PATCH] Open diff files in editor from diff header clicks - Add click handling on diff file headers (`data-title`) to open files via native `openInEditor` - Resolve file paths against active thread/project cwd before launching editor - Add header hover affordance and tighten diff panel spacing --- apps/web/src/components/DiffPanel.tsx | 47 +++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/apps/web/src/components/DiffPanel.tsx b/apps/web/src/components/DiffPanel.tsx index 92c2af1d0624fbc2073d034813c4959e1c0517bb..3b77edda2eb5210422975cf866dd3a3c551dba9f 100644 --- a/apps/web/src/components/DiffPanel.tsx +++ b/apps/web/src/components/DiffPanel.tsx @@ -7,6 +7,8 @@ import { ChevronLeftIcon, ChevronRightIcon, Columns2Icon, Rows3Icon } from "luci import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { checkpointDiffQueryOptions } from "~/lib/providerReactQuery"; import { cn } from "~/lib/utils"; +import { readNativeApi } from "../nativeApi"; +import { preferredTerminalEditor, resolvePathLinkTarget } from "../terminal-links"; import { parseDiffRouteSearch, stripDiffSearchParams } from "../diffRouteSearch"; import { isElectron } from "../env"; import { useTheme } from "../hooks/useTheme"; @@ -58,6 +60,21 @@ const DIFF_PANEL_UNSAFE_CSS = ` border-block-color: var(--border) !important; color: var(--foreground) !important; } + +[data-title] { + cursor: pointer; + transition: + color 120ms ease, + text-decoration-color 120ms ease; + text-decoration: underline; + text-decoration-color: transparent; + text-underline-offset: 2px; +} + +[data-title]:hover { + color: color-mix(in srgb, var(--foreground) 84%, var(--primary)) !important; + text-decoration-color: currentColor; +} `; type RenderablePatch = @@ -144,6 +161,8 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) { const diffSearch = useSearch({ strict: false, select: (search) => parseDiffRouteSearch(search) }); const activeThreadId = routeThreadId; const activeThread = state.threads.find((thread) => thread.id === activeThreadId); + const activeProject = state.projects.find((project) => project.id === activeThread?.projectId); + const activeCwd = activeThread?.worktreePath ?? activeProject?.cwd; const { turnDiffSummaries, inferredCheckpointTurnCountByTurnId } = useTurnDiffSummaries(activeThread); const orderedTurnDiffSummaries = useMemo( @@ -264,6 +283,18 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) { target?.scrollIntoView({ block: "nearest" }); }, [selectedFilePath, renderableFiles]); + const openDiffFileInEditor = useCallback( + (filePath: string) => { + const api = readNativeApi(); + if (!api) return; + const targetPath = activeCwd ? resolvePathLinkTarget(filePath, activeCwd) : filePath; + void api.shell.openInEditor(targetPath, preferredTerminalEditor()).catch((error) => { + console.warn("Failed to open diff file in editor.", error); + }); + }, + [activeCwd], + ); + const selectTurn = (turnId: TurnId) => { if (!activeThread) return; void navigate({ @@ -508,7 +539,7 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) { ) : renderablePatch.kind === "files" ? ( { + const nativeEvent = event.nativeEvent as MouseEvent; + const composedPath = nativeEvent.composedPath?.() ?? []; + const clickedHeader = composedPath.some((node) => { + if (!(node instanceof Element)) return false; + return node.hasAttribute("data-title"); + }); + if (!clickedHeader) return; + openDiffFileInEditor(filePath); + }} > ) : ( -
+

{renderablePatch.reason}

-- 
2.54.0