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)
This commit is contained in:
@@ -13,7 +13,7 @@
|
||||
* that one cached level and re-lists it. Folders the reader collapsed are
|
||||
* un-watched, so a long session does not accumulate handles.
|
||||
*/
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { useEffect, useMemo, useRef } from 'react'
|
||||
|
||||
/** One server frame: a stale directory, a watch verdict, or a refusal. */
|
||||
interface FsWatchFrame {
|
||||
@@ -47,15 +47,23 @@ const RETRY_MAX_MS = 15_000
|
||||
export function useDirectoryWatch(options: DirectoryWatchOptions): void {
|
||||
const { sessionId, root, dirs, onStale } = options
|
||||
// The expanded set is a fresh array on every render, so neither the socket
|
||||
// effect nor the staleness callback may depend on their identity.
|
||||
const wantedRef = useRef<string[]>([])
|
||||
wantedRef.current = root === undefined ? [...dirs] : [root, ...dirs]
|
||||
// effect nor the staleness callback may depend on their identity. The
|
||||
// CONTENT key (NUL-joined paths — no path can contain one) is what the
|
||||
// arrays are derived from, so a re-render with the same expanded set
|
||||
// rebuilds neither the wanted list nor the effect dependency.
|
||||
const dirsKey = dirs.join('\u0000')
|
||||
const wanted = useMemo(() => {
|
||||
const listed = dirsKey === '' ? [] : dirsKey.split('\u0000')
|
||||
return root === undefined ? listed : [root, ...listed]
|
||||
}, [root, dirsKey])
|
||||
const wantedRef = useRef(wanted)
|
||||
wantedRef.current = wanted
|
||||
const staleRef = useRef(onStale)
|
||||
staleRef.current = onStale
|
||||
/** Set by the socket effect; reconciles the host's watch set with the tree's. */
|
||||
const reconcileRef = useRef<() => void>(() => {})
|
||||
// One string per expanded-set change: a stable dependency for the effect below.
|
||||
const wantedKey = wantedRef.current.join('\u0000')
|
||||
const wantedKey = useMemo(() => wanted.join('\u0000'), [wanted])
|
||||
|
||||
useEffect(() => {
|
||||
if (sessionId === undefined) return
|
||||
|
||||
Reference in New Issue
Block a user