feat!: 合并 Git 面板与文件追踪为统一「文件变动」tab(双视角 + 统一 diff 栈)
在 PR #471(file-trace)基础上的演进: - 合并 tab:git tab(id 保留,兼容持久化布局)升级为「文件变动」双视角 tab——Git 视角(原 GitView 全部能力:staged/unstaged/提交/历史/worktree· 子仓库选择/右键菜单/确认弹窗)+ 本轮文件视角(原 file-trace:会话事件 日志实时折叠的模型读写编辑记录,按文件分组、新增类型筛选 chips、相对 时间);file-trace tab 移除(未随任何版本发布)。 - 统一 diff 组件:新 src/client/diff/ 模块(rows.ts 引擎 + DiffRows/ DiffFiles 渲染器 + highlight.ts 语法着色)——git diff 与会话操作 diff 共用一套渲染:mod 改写配对(del-run/add-run 对半标蓝)、行内字符级 高亮(修复了旧实现旧侧失配不高亮的问题,改为行对象身份键)、上下文 hunk 折叠、git hunk 间未输出区间按行号范围生成不可展开折叠、语法着色 色相落位 diff 三色空档、+n −m 统计 chips、行底色 tint;DiffTab 改用 该栈(git diff tab 从此也有高亮)。 - 交互:两视角共用底部可拖拽预览面板(点击文件/提交/操作即预览,不再 直接开 diff tab;面板可刷新、可键盘/拖拽调高、git 目标可一键展开为 独立 diff tab);视角选择与面板高度持久化在 tab.meta;git 行 XY 徽章 按状态字母着色;选中行高亮。 - i18n:fileTrace* 十键重命名为 changes* 前缀,git 键值更新为「文件 变动」,新增 7 键(视角/筛选/面板操作),全部 21 个词典同步。 - 结构:src/client/changes/(ChangesTab/GitLens/SessionLens/DiffPane/ ops)+ src/client/diff/;GitView/DiffView/filetrace/ 删除;git 样式 从 sidebar.module.css 迁至 changes.module.css。 - 测试:builtins 8→7(git tab 承接 badge pin)、diff-rows(unified 转换/mod 配对/间隙折叠/统计)、changes-tab(壳交互 jsdom)、 diff-files-collapse(原 DiffView 折叠行为钉住)、changes-ops(原 filetrace 数据层)、git-view-worktree 迁移至 GitLens;全量 1122 通过。 - 文档:README ×2 功能矩阵合并、external-plugin-guide §4.4 与内置清单 7 tab、mount e2e 菜单标题同步。
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
/**
|
||||
* The unified changes tab: one tab, two lenses on "what changed?" — Git
|
||||
* (repository truth: staged/unstaged files, commit box, history) and the
|
||||
* session round (agent truth: every file the model read, wrote, or edited).
|
||||
* Both lenses preview their selections in a shared resizable bottom pane
|
||||
* ({@link DiffPane}); git targets can expand into the dedicated diff tab.
|
||||
* The active lens and the pane height persist in the tab's meta, so the tab
|
||||
* reopens exactly where it was left.
|
||||
*/
|
||||
import { useState } from 'react'
|
||||
import type { TabComponentProps } from '../service.ts'
|
||||
import { t } from '../locales.ts'
|
||||
import type { SidebarDiffRef } from '../state.ts'
|
||||
import { GitLens } from './GitLens.tsx'
|
||||
import { SessionLens } from './SessionLens.tsx'
|
||||
import { DiffPane, diffTabOf, type ChangesPreview } from './DiffPane.tsx'
|
||||
import type { FileOp } from './ops.ts'
|
||||
import css from './changes.module.css'
|
||||
|
||||
/** The default preview pane height (px) before the first drag. */
|
||||
const PANE_HEIGHT_DEFAULT = 300
|
||||
|
||||
type Lens = 'git' | 'session'
|
||||
|
||||
/** The persisted tab meta (JSON-serializable; rides the layout). */
|
||||
interface ChangesMeta {
|
||||
lens?: Lens
|
||||
previewH?: number
|
||||
}
|
||||
|
||||
export function ChangesTab({ ctx, store, scope, tab, visible, onOpenFile, onOpenDiff }: TabComponentProps) {
|
||||
const meta = (tab.meta ?? {}) as ChangesMeta
|
||||
const [lens, setLens] = useState<Lens>(meta.lens === 'session' ? 'session' : 'git')
|
||||
const [preview, setPreview] = useState<ChangesPreview | null>(null)
|
||||
const [paneHeight, setPaneHeight] = useState<number>(
|
||||
typeof meta.previewH === 'number' && meta.previewH >= 140 ? meta.previewH : PANE_HEIGHT_DEFAULT,
|
||||
)
|
||||
|
||||
/** Persist a meta patch onto the tab (lens choice, pane height). */
|
||||
const patchMeta = (patch: ChangesMeta): void => {
|
||||
ctx.get('betterSidebar')?.updateTab(tab.id, {
|
||||
meta: { ...(tab.meta as ChangesMeta | undefined ?? {}), ...patch },
|
||||
})
|
||||
}
|
||||
|
||||
const chooseLens = (next: Lens): void => {
|
||||
if (next === lens) return
|
||||
setLens(next)
|
||||
patchMeta({ lens: next })
|
||||
}
|
||||
|
||||
/** Preview one git change (worktree file or commit) from the Git lens. */
|
||||
const previewGit = (ref: SidebarDiffRef): void => {
|
||||
setPreview({ kind: 'git', ref })
|
||||
}
|
||||
|
||||
/** Preview one session op (with its best-effort prior content snapshot). */
|
||||
const previewOp = (path: string, op: FileOp, prior: string | undefined): void => {
|
||||
setPreview({ kind: 'op', path, op, prior })
|
||||
}
|
||||
|
||||
const previewKey = (target: ChangesPreview): string => target.kind === 'git'
|
||||
? (target.ref.kind === 'worktree'
|
||||
? `git:w:${target.ref.path}:${target.ref.staged ? 's' : 'u'}`
|
||||
: `git:c:${target.ref.hashFull}`)
|
||||
: `op:${target.op.callId}`
|
||||
|
||||
return (
|
||||
<div className={css.root}>
|
||||
<div className={css.lensBar}>
|
||||
<div className={css.lensSwitch} role="group" aria-label={t('changes')}>
|
||||
<button
|
||||
type="button"
|
||||
className={css.lensButton}
|
||||
data-active={lens === 'git' ? 'true' : undefined}
|
||||
aria-pressed={lens === 'git'}
|
||||
onClick={() => { chooseLens('git') }}
|
||||
>
|
||||
{t('changesGitLens')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={css.lensButton}
|
||||
data-active={lens === 'session' ? 'true' : undefined}
|
||||
aria-pressed={lens === 'session'}
|
||||
onClick={() => { chooseLens('session') }}
|
||||
>
|
||||
{t('changesSessionLens')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{lens === 'git'
|
||||
? (
|
||||
<GitLens
|
||||
scope={scope}
|
||||
store={store}
|
||||
visible={visible}
|
||||
onOpenFile={onOpenFile ?? (() => { /* no-op */ })}
|
||||
onPreview={previewGit}
|
||||
selectedRef={preview !== null && preview.kind === 'git' ? preview.ref : null}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<SessionLens
|
||||
ctx={ctx}
|
||||
scope={scope}
|
||||
visible={visible}
|
||||
onPreview={previewOp}
|
||||
selectedCallId={preview !== null && preview.kind === 'op' ? preview.op.callId : null}
|
||||
/>
|
||||
)}
|
||||
{preview !== null && (
|
||||
<DiffPane
|
||||
key={previewKey(preview)}
|
||||
target={preview}
|
||||
scope={scope}
|
||||
height={paneHeight}
|
||||
onHeightCommit={(height) => { setPaneHeight(height); patchMeta({ previewH: height }) }}
|
||||
onClose={() => { setPreview(null) }}
|
||||
onExpand={() => {
|
||||
if (preview.kind === 'git') onOpenDiff?.(diffTabOf(preview.ref))
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user