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:
@@ -182,6 +182,46 @@ export async function renameWorkspaceEntry(input: WorkspaceRenameInput): Promise
|
||||
return { path: safeDestination }
|
||||
}
|
||||
|
||||
/** Inputs of one new directory row. */
|
||||
export interface WorkspaceMkdirInput {
|
||||
/** The session workspace root; the new directory must stay inside it. */
|
||||
cwd: string
|
||||
/** Absolute path of the PARENT row as the tree displays it (a directory). */
|
||||
path: string
|
||||
/** The new directory's base name (single segment — mkdir never nests). */
|
||||
name: string
|
||||
/** Whether workspace containment is enforced (the `workspaceFence` setting; on by default). */
|
||||
fence?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Create one directory inside an existing tree row: `<path>/<name>`.
|
||||
* The name must be a single path segment; an existing destination is refused
|
||||
* (mkdir would otherwise fail with EEXIST anyway, but the explicit check
|
||||
* yields the same "already exists" sentence rename uses); the parent must
|
||||
* resolve inside the (fence-checked) workspace.
|
||||
*
|
||||
* @throws SidebarError with a wire code for shape, containment and existence
|
||||
* failures.
|
||||
*/
|
||||
export async function mkdirWorkspaceEntry(input: WorkspaceMkdirInput): Promise<{ path: string }> {
|
||||
const { cwd, path, name, fence = true } = input
|
||||
if (name === '' || name === '.' || name === '..' || name.includes('/') || name.includes('\\')) {
|
||||
throw new SidebarError('bad-request', 'name must be a single path segment', 400)
|
||||
}
|
||||
const { absolute } = await resolveEntry(cwd, path, fence)
|
||||
const destination = await ensureWorkspaceWritePath(cwd, join(absolute, name), fence)
|
||||
if (await pathExists(destination)) {
|
||||
throw new SidebarError('fs-error', `"${name}" already exists`, 409)
|
||||
}
|
||||
try {
|
||||
await mkdir(destination)
|
||||
} catch (error) {
|
||||
throw new SidebarError('fs-error', `cannot create "${name}": ${error instanceof Error ? error.message : String(error)}`, 400)
|
||||
}
|
||||
return { path: destination }
|
||||
}
|
||||
|
||||
/** Inputs of one tree-row delete. */
|
||||
export interface WorkspaceRemoveInput {
|
||||
/** The session workspace root; the removed entry must stay inside it. */
|
||||
|
||||
Reference in New Issue
Block a user