| ... | @@ -7,6 +7,8 @@ import { ChevronLeftIcon, ChevronRightIcon, Columns2Icon, Rows3Icon } from "luci | ... | @@ -7,6 +7,8 @@ import { ChevronLeftIcon, ChevronRightIcon, Columns2Icon, Rows3Icon } from "luci |
| 7 | import { useCallback, useEffect, useMemo, useRef, useState } from "react"; | 7 | import { useCallback, useEffect, useMemo, useRef, useState } from "react"; |
| 8 | import { checkpointDiffQueryOptions } from "~/lib/providerReactQuery"; | 8 | import { checkpointDiffQueryOptions } from "~/lib/providerReactQuery"; |
| 9 | import { cn } from "~/lib/utils"; | 9 | import { cn } from "~/lib/utils"; |
| | 10 | import { readNativeApi } from "../nativeApi"; |
| | 11 | import { preferredTerminalEditor, resolvePathLinkTarget } from "../terminal-links"; |
| 10 | import { parseDiffRouteSearch, stripDiffSearchParams } from "../diffRouteSearch"; | 12 | import { parseDiffRouteSearch, stripDiffSearchParams } from "../diffRouteSearch"; |
| 11 | import { isElectron } from "../env"; | 13 | import { isElectron } from "../env"; |
| 12 | import { useTheme } from "../hooks/useTheme"; | 14 | import { useTheme } from "../hooks/useTheme"; |
| ... | @@ -58,6 +60,21 @@ const DIFF_PANEL_UNSAFE_CSS = ` | ... | @@ -58,6 +60,21 @@ const DIFF_PANEL_UNSAFE_CSS = ` |
| 58 | border-block-color: var(--border) !important; | 60 | border-block-color: var(--border) !important; |
| 59 | color: var(--foreground) !important; | 61 | color: var(--foreground) !important; |
| 60 | } | 62 | } |
| | 63 | |
| | 64 | [data-title] { |
| | 65 | cursor: pointer; |
| | 66 | transition: |
| | 67 | color 120ms ease, |
| | 68 | text-decoration-color 120ms ease; |
| | 69 | text-decoration: underline; |
| | 70 | text-decoration-color: transparent; |
| | 71 | text-underline-offset: 2px; |
| | 72 | } |
| | 73 | |
| | 74 | [data-title]:hover { |
| | 75 | color: color-mix(in srgb, var(--foreground) 84%, var(--primary)) !important; |
| | 76 | text-decoration-color: currentColor; |
| | 77 | } |
| 61 | `; | 78 | `; |
| 62 | | 79 | |
| 63 | type RenderablePatch = | 80 | type RenderablePatch = |
| ... | @@ -144,6 +161,8 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) { | ... | @@ -144,6 +161,8 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) { |
| 144 | const diffSearch = useSearch({ strict: false, select: (search) => parseDiffRouteSearch(search) }); | 161 | const diffSearch = useSearch({ strict: false, select: (search) => parseDiffRouteSearch(search) }); |
| 145 | const activeThreadId = routeThreadId; | 162 | const activeThreadId = routeThreadId; |
| 146 | const activeThread = state.threads.find((thread) => thread.id === activeThreadId); | 163 | const activeThread = state.threads.find((thread) => thread.id === activeThreadId); |
| | 164 | const activeProject = state.projects.find((project) => project.id === activeThread?.projectId); |
| | 165 | const activeCwd = activeThread?.worktreePath ?? activeProject?.cwd; |
| 147 | const { turnDiffSummaries, inferredCheckpointTurnCountByTurnId } = | 166 | const { turnDiffSummaries, inferredCheckpointTurnCountByTurnId } = |
| 148 | useTurnDiffSummaries(activeThread); | 167 | useTurnDiffSummaries(activeThread); |
| 149 | const orderedTurnDiffSummaries = useMemo( | 168 | const orderedTurnDiffSummaries = useMemo( |
| ... | @@ -264,6 +283,18 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) { | ... | @@ -264,6 +283,18 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) { |
| 264 | target?.scrollIntoView({ block: "nearest" }); | 283 | target?.scrollIntoView({ block: "nearest" }); |
| 265 | }, [selectedFilePath, renderableFiles]); | 284 | }, [selectedFilePath, renderableFiles]); |
| 266 | | 285 | |
| | 286 | const openDiffFileInEditor = useCallback( |
| | 287 | (filePath: string) => { |
| | 288 | const api = readNativeApi(); |
| | 289 | if (!api) return; |
| | 290 | const targetPath = activeCwd ? resolvePathLinkTarget(filePath, activeCwd) : filePath; |
| | 291 | void api.shell.openInEditor(targetPath, preferredTerminalEditor()).catch((error) => { |
| | 292 | console.warn("Failed to open diff file in editor.", error); |
| | 293 | }); |
| | 294 | }, |
| | 295 | [activeCwd], |
| | 296 | ); |
| | 297 | |
| 267 | const selectTurn = (turnId: TurnId) => { | 298 | const selectTurn = (turnId: TurnId) => { |
| 268 | if (!activeThread) return; | 299 | if (!activeThread) return; |
| 269 | void navigate({ | 300 | void navigate({ |
| ... | @@ -508,7 +539,7 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) { | ... | @@ -508,7 +539,7 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) { |
| 508 | </div> | 539 | </div> |
| 509 | ) : renderablePatch.kind === "files" ? ( | 540 | ) : renderablePatch.kind === "files" ? ( |
| 510 | <Virtualizer | 541 | <Virtualizer |
| 511 | className="diff-render-surface h-full min-h-0 overflow-auto px-3 py-2" | 542 | className="diff-render-surface h-full min-h-0 overflow-auto p-2 " |
| 512 | config={{ | 543 | config={{ |
| 513 | overscrollSize: 600, | 544 | overscrollSize: 600, |
| 514 | intersectionObserverMargin: 1200, | 545 | intersectionObserverMargin: 1200, |
| ... | @@ -522,7 +553,17 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) { | ... | @@ -522,7 +553,17 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) { |
| 522 | <div | 553 | <div |
| 523 | key={themedFileKey} | 554 | key={themedFileKey} |
| 524 | data-diff-file-path={filePath} | 555 | data-diff-file-path={filePath} |
| 525 | className="diff-render-file rounded-md" | 556 | className="diff-render-file mb-2 rounded-md last:mb-0" |
| | 557 | onClickCapture={(event) => { |
| | 558 | const nativeEvent = event.nativeEvent as MouseEvent; |
| | 559 | const composedPath = nativeEvent.composedPath?.() ?? []; |
| | 560 | const clickedHeader = composedPath.some((node) => { |
| | 561 | if (!(node instanceof Element)) return false; |
| | 562 | return node.hasAttribute("data-title"); |
| | 563 | }); |
| | 564 | if (!clickedHeader) return; |
| | 565 | openDiffFileInEditor(filePath); |
| | 566 | }} |
| 526 | > | 567 | > |
| 527 | <FileDiff | 568 | <FileDiff |
| 528 | fileDiff={fileDiff} | 569 | fileDiff={fileDiff} |
| ... | @@ -539,7 +580,7 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) { | ... | @@ -539,7 +580,7 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) { |
| 539 | })} | 580 | })} |
| 540 | </Virtualizer> | 581 | </Virtualizer> |
| 541 | ) : ( | 582 | ) : ( |
| 542 | <div className="h-full overflow-auto px-3 py-2"> | 583 | <div className="h-full overflow-auto p-2"> |
| 543 | <div className="space-y-2"> | 584 | <div className="space-y-2"> |
| 544 | <p className="text-[11px] text-muted-foreground/75">{renderablePatch.reason}</p> | 585 | <p className="text-[11px] text-muted-foreground/75">{renderablePatch.reason}</p> |
| 545 | <pre className="max-h-[72vh] overflow-auto rounded-md border border-border/70 bg-background/70 p-3 font-mono text-[11px] leading-relaxed text-muted-foreground/90"> | 586 | <pre className="max-h-[72vh] overflow-auto rounded-md border border-border/70 bg-background/70 p-3 font-mono text-[11px] leading-relaxed text-muted-foreground/90"> |