diff --git a/web/src/components/ChatView.jsx b/web/src/components/ChatView.jsx index 0c75f281c..0495fe6ea 100644 --- a/web/src/components/ChatView.jsx +++ b/web/src/components/ChatView.jsx @@ -3087,6 +3087,7 @@ export function ChatView({ sessionRef, sessionId, onSessionPromoted, onCommandWo onToggleMaximize={onToggleSidePanelMaximized} onSelectChangeFile={openSessionChangePreview} onSelectGitChangeFile={openGitChangePreview} + onOpenFilePreview={openFilePreview} /> )} diff --git a/web/src/components/GitChangeReview.jsx b/web/src/components/GitChangeReview.jsx index 47367436c..d8f566dda 100644 --- a/web/src/components/GitChangeReview.jsx +++ b/web/src/components/GitChangeReview.jsx @@ -58,7 +58,7 @@ function statusBadgeClass(status) { } } -function GitChangeFileRow({ row, open, selected, onToggle }) { +function GitChangeFileRow({ row, open, selected, onToggle, onOpenFile }) { const name = row.path.split(/[\\/]/).pop(); const parent = row.path.includes('/') ? row.path.slice(0, row.path.lastIndexOf('/')) : ''; return ( @@ -89,6 +89,25 @@ function GitChangeFileRow({ row, open, selected, onToggle }) { {row.statLabel} )} + {!!onOpenFile && ( + // 已删除文件磁盘上不存在,按钮隐形占位保持各行 ± 计数对齐。 + event.stopPropagation()} + onMouseDown={(event) => event.stopPropagation()} + onClick={(event) => { + event.preventDefault(); + event.stopPropagation(); + if (row.status !== 'D') onOpenFile(row.path); + }} + > + + + )} ); } @@ -101,6 +120,7 @@ export function GitChangeDetails({ expandedFileRevision = 0, busy = false, onSelectFile, + onOpenFilePreview, }) { const [list, setList] = useState(null); const [loading, setLoading] = useState(false); @@ -315,41 +335,30 @@ export function GitChangeDetails({ return (
-
-
- - 变更 - {(list?.branch) && ( - <> - {list.branch} - - - )} - {base} -
-
- {loading && } - {list && ( - - +{list.total_additions ?? 0}{' '} - -{list.total_deletions ?? 0} - - )} - -
-
- - {list && rows.length > 0 && ( -
- {buildSummaryLabel(list)} + {(list || loading) && ( +
+ {buildSummaryLabel(list)} + {loading && } + + {list && ( + + +{list.total_additions ?? 0}{' '} + -{list.total_deletions ?? 0} + + )} + +
)} @@ -387,6 +396,7 @@ export function GitChangeDetails({ open={open} selected={selected} onToggle={() => toggleFile(row.path, open)} + onOpenFile={onOpenFilePreview} /> {open && (
diff --git a/web/src/components/GitChangesPanel.jsx b/web/src/components/GitChangesPanel.jsx index 59919ae59..8bec92b6e 100644 --- a/web/src/components/GitChangesPanel.jsx +++ b/web/src/components/GitChangesPanel.jsx @@ -40,6 +40,7 @@ export function GitChangesPanel({ visible = true, selectedFile = '', onOpenFile, + onOpenFilePreview, onBaseChange, }) { const { candidates, initial } = useMemo( @@ -260,6 +261,25 @@ export function GitChangesPanel({ ) : {row.statLabel}} + {!!onOpenFilePreview && ( + // 已删除文件磁盘上不存在,按钮隐形占位保持各行 ± 计数对齐。 + event.stopPropagation()} + onMouseDown={(event) => event.stopPropagation()} + onClick={(event) => { + event.preventDefault(); + event.stopPropagation(); + if (row.status !== 'D') onOpenFilePreview(row.path); + }} + > + + + )} ))}
diff --git a/web/src/components/PreviewDetailsPanel.jsx b/web/src/components/PreviewDetailsPanel.jsx index 7a9f64f3c..b083b4277 100644 --- a/web/src/components/PreviewDetailsPanel.jsx +++ b/web/src/components/PreviewDetailsPanel.jsx @@ -185,6 +185,7 @@ export function PreviewDetailsPanel({ onToggleMaximize, onSelectChangeFile, onSelectGitChangeFile, + onOpenFilePreview, }) { const tabListRef = useRef(null); const tabDragRef = useRef(null); @@ -250,6 +251,7 @@ export function PreviewDetailsPanel({ expandedFileRevision={active.expandedFileRevision || 0} busy={busy} onSelectFile={onSelectGitChangeFile} + onOpenFilePreview={onOpenFilePreview} /> ); } @@ -263,7 +265,7 @@ export function PreviewDetailsPanel({ onToggleWrapPreview={() => setWrapPreview((prev) => !prev)} /> ); - }, [active, api, busy, changeGroups, changeSummary, cwd, onSelectChangeFile, onSelectGitChangeFile, setWrapPreview, wrapPreview]); + }, [active, api, busy, changeGroups, changeSummary, cwd, onOpenFilePreview, onSelectChangeFile, onSelectGitChangeFile, setWrapPreview, wrapPreview]); const handleTabWheel = useCallback((event) => { const el = tabListRef.current; diff --git a/web/src/components/SidePanel.jsx b/web/src/components/SidePanel.jsx index f48f040ef..d2cd74914 100644 --- a/web/src/components/SidePanel.jsx +++ b/web/src/components/SidePanel.jsx @@ -630,6 +630,7 @@ export function SidePanel({ visible={!collapsed && activeTab === 'changes'} selectedFile={selectedGitChangeFile} onOpenFile={onOpenGitChangePreview} + onOpenFilePreview={onOpenFilePreview} onBaseChange={onGitBaseChange} /> ) : ( diff --git a/web/src/styles/globals.css b/web/src/styles/globals.css index c7f7f6834..60f5b6895 100644 --- a/web/src/styles/globals.css +++ b/web/src/styles/globals.css @@ -2273,6 +2273,8 @@ body { min-height: 28px; display: grid; grid-template-columns: 18px minmax(0, 1fr) auto; + grid-auto-columns: max-content; + grid-auto-flow: column; align-items: center; gap: 5px; padding: 3px 8px; @@ -2323,6 +2325,60 @@ body { white-space: nowrap; } +/* 变更文件行的行内动作按钮(转到文件):hover / 选中 / 键盘聚焦时浮现, + 平时透明占位保持 ± 计数各行对齐;is-hidden = 已删除文件的隐形占位。 */ +.ace-change-row-action { + width: 20px; + height: 20px; + display: inline-flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + border-radius: 4px; + color: var(--ace-fg-mute); + opacity: 0; + transition: opacity .12s ease, color .15s ease, background .15s ease; +} +.ace-change-compact-row:hover .ace-change-row-action, +.ace-change-compact-row.is-selected .ace-change-row-action, +.ace-change-file-row:hover .ace-change-row-action, +.ace-change-file-row.is-selected .ace-change-row-action, +.ace-change-row-action:focus-visible { + opacity: 1; +} +.ace-change-row-action:hover { + color: var(--ace-fg); + background: var(--ace-surface-alt); +} +.ace-change-row-action.is-hidden { + visibility: hidden; + pointer-events: none; +} + +/* 变更详情栏「N 个文件已更改」行右端的折叠全部 diff 按钮。 */ +.ace-review-collapse-all-btn { + width: 24px; + height: 20px; + display: inline-flex; + align-items: center; + justify-content: center; + border: none; + border-radius: 4px; + background: transparent; + color: var(--ace-fg-mute); + cursor: pointer; + flex-shrink: 0; + transition: color .15s ease, background .15s ease; +} +.ace-review-collapse-all-btn:hover { + color: var(--ace-fg); + background: var(--ace-surface-alt); +} +.ace-review-collapse-all-btn:focus-visible { + outline: 2px solid var(--ace-accent); + outline-offset: -2px; +} + .ace-review-panel { flex: 1; min-height: 0;