Files
apes-sidebar/src/client/DiffTab.tsx
T
Menghuan1918 36fbc5ac7e feat(sidebar): 文件页与文件变动页 UIUX 重构 + 全局组件抽取
文件页(FileTree/TreePanel)
- Ctrl/Cmd 多选 + Shift 按可见行序连选(Esc/点空白/右键收敛),批量条支持复制路径、批量删除(共享 ConfirmDialog + 顺序 fsRemove)
- Git 变更着色:行名按状态上色 + M/A/D/U 徽标,目录按 dirHasChanges 着色,非仓库静默降级(数据来自共享 git 状态存储)
- 新建文件夹:fs.mkdir 路由 + mkdirWorkspaceEntry + 内联输入行(Enter/Esc/blur/IME 守卫)
- 性能:FileRow/DirRow memo(含反向验证)、搜索 hidden 停车不再卸载树、level 拉取世代守卫 + AbortController、fs.tree truncated 提示、use-dir-watch 入参 memo
- 拖拽上传视觉 token 化(硬编码色清零);可见性下传后隐藏页签停止轮询
- 右键「打开方式」改为复用 DSH 自带 open-in-app(宿主探测的关联应用 + 文件管理器显示)

文件变动页(changes/diff)
- 36px 头(宿主 SegmentedControl)+ 单一 ChangeRow + SectionHeader 分组 + 吸底提交条单状态行 + 历史独立空态
- 修掉:双份空态、暂存失败 unhandled rejection、手动刷新被丢、仓库切换刷新两次、worktree 清单缺 repoRoot、工作树被自动切走、预览持旧快照、恢复高度未 clamp、session ops 无谓轮询
- DiffPane/DiffTab 共用 use-git-diff;修掉 DiffPane 的 untracked 预览从不渲染

全局组件与宿主接入
- 新增 src/client/ui(IconButton/Chip/SectionHeader/Notice/ConfirmDialog/StatusBadge + 共享 git 状态存储)
- 新增 src/client/open-in-app.ts(宿主 open-in-app 客户端适配),删除自研 open-with 全链路与 open.external 路由
- 客户端 inject 补 remote / remote.session:缺声明时 ctx.get('remote').session 会抛出并把整个面板打成错误条(真实挂载 lane 抓到)
- 词典:+25 键 / -20 openWith* / -noChanges,20 份同步

验证:typecheck / lint / test(126 文件 1322 通过)/ build 全绿;
DSH_CMD='npx -y --package @deepseek-ai/dsh@0.1.7-rc.1 dsh' pnpm test:mount 7/7 通过(含 tab sweep 与 perf lane)
2026-09-28 00:27:31 +08:00

58 lines
2.7 KiB
TypeScript

/**
* The diff tab: one change opened from the changes tab, like VSCode's diff
* editor. A worktree ref loads the file's unified diff (`git diff`, staged or
* not; untracked files — which git diff never covers — render as a full-file
* addition from their content), a commit ref loads the commit's full patch
* (`git.show`-style). The header carries a refresh button because the tab
* stays mounted while the changes tab's staging/discard operations change the
* very content it shows. Loading IS the shared `useGitDiffTarget` hook (the
* changes tab's inline preview runs the same one, so the staged-side and
* untracked fallbacks and the per-file fold cache cannot drift apart);
* rendering goes through the shared {@link DiffFiles} renderer.
*/
import { useMemo } from 'react'
import { IconRefreshOutlineRegular } from '@deepseek-ai/dsh-client-ui-primitives'
import type { SessionScope } from './api.ts'
import type { SidebarDiffRef } from './state.ts'
import { DiffFiles } from './diff/DiffFiles.tsx'
import { useGitDiffTarget } from './diff/use-git-diff.ts'
import { t } from './locales.ts'
import css from './sidebar.module.css'
export function DiffTab(props: { sessionId: string; cwd: string | undefined; diff: SidebarDiffRef }) {
const { sessionId, cwd, diff } = props
const scope = useMemo<SessionScope>(() => ({ sessionId, cwd }), [sessionId, cwd])
const { loading, error, diffText, untracked, refresh, resolveFold } = useGitDiffTarget(diff, scope)
return (
<div className={css.gitDiffTab}>
<div className={css.gitDiffTabHeader}>
<span className={css.gitDiffTabTitle} title={diff.kind === 'worktree' ? diff.path : `${diff.hash} ${diff.subject}`}>
{diff.kind === 'worktree' ? diff.path : `${diff.hash} ${diff.subject}`}
</span>
<button
type="button"
className={css.iconButton}
aria-label={t('refresh')}
title={t('refresh')}
onClick={refresh}
>
<IconRefreshOutlineRegular size={14} />
</button>
</div>
{loading && <div className={css.gitPlaceholder}>{t('loading')}</div>}
{!loading && error !== null && <div className={css.gitError}>{t('diffLoadError')}: {error}</div>}
{!loading && error === null && diffText !== null && (
<>
{untracked !== undefined
? <DiffFiles diff="" untrackedPath={diff.kind === 'worktree' ? diff.path : ''} untrackedContent={untracked} />
: <DiffFiles diff={diffText} resolveFold={resolveFold} />}
{diffText === '' && untracked === undefined && (
<div className={css.gitEmpty}>{t('diffEmpty')}</div>
)}
</>
)}
</div>
)
}