feat(sidebar): 菜单层级/性能 + 选中条吸底 + 打包进度 + 修复「在侧边打开」

用户实测反馈 5 条:

1) 文件列表性能 / 右键菜单不再刷屏(先测量后改,探针 tests/file-tree-menu-perf.spec.tsx)
   - 宿主应用列表按路径缓存:N 次打开菜单只请求 1 次(原每次 1 次 + Loading 帧)
   - 行上的 git 数据改为原始值(tone/letter):共享 store 每次轮询重建对象不再触达行
     (探针:开窗 folderIcons 4 → 0;root 行也 memo 化)
   - 菜单开关不触发目录重列(fsTree=0);实测「菜单打开期间暂停轮询」会让变更行瞬时
     掉色并多两次重渲染,故按测量结果放弃该做法(数字与理由写进探针注释)

2) 「已选择 N 项」条移到底部:滚动容器最后一个内容子节点 + sticky bottom(不遮最后
   一行),窄栏 flex-wrap 换行不出横向滚动条

3) 菜单层级重构:一级只留默认动作(宿主默认应用 / 打开方式 ▸ / 在文件管理器中显示 /
   新 Tab / 在侧边打开 / 下载 / 压缩并下载 / 复制路径 / 重命名 / 删除;目录行另有
   新建文件夹、上传到此处),应用列表整体收进「打开方式」子菜单;宿主探测到本机关联
   应用时默认不显示插件自研 open-with 项目,新增设置开关 openWithPluginTargets
   (editor descriptor 的 pluginToggles)可强制并存;宿主不可用时插件项目照旧

4) 压缩并下载显示进度:宿主改为可观测任务(archive.build / archive.status /
   /sidebar/archive?id=,4 并发 + 5 分钟 TTL + sessionId 绑定,buildZip 逐条目
   onProgress),客户端轮询 250ms 显示「正在打包 done/total · N%」,ready 后 blob 下载,
   失败进 zipFailed

5) 修复「在侧边打开没有效果」:根因是它只写插件底部工作台(原生右侧栏的 tab 不在
   bottomSplits 里),现按承载面分流 —— 原生 tab 走 service.openTab(target:'side') →
   宿主 openResource(preferNewPane, revealIfOpened:false) 真正分栏;底部工作台行为不变
   (接入 API 变更已同步 docs/external-plugin-guide.md)

验证:typecheck / lint / test(135 文件 1461 通过)/ build 全绿;
DSH_CMD='npx -y --package @deepseek-ai/dsh@0.1.7-rc.1 dsh' pnpm test:mount 7/7 通过;
新增/更新测试:menu-perf(3)、archive(10)、open-in-app(18)、open-with(10)、multiselect(9)、
zip(37)、service(64)、editor-host(11)、builtins(23)、locales(20)
This commit is contained in:
Menghuan1918
2026-09-28 20:11:46 +08:00
parent 13d4bae321
commit ee1fe05067
41 changed files with 2228 additions and 597 deletions
+37 -8
View File
@@ -440,15 +440,44 @@ export function downloadUrl(scope: SessionScope, path: string): string {
}
/**
* Absolute URL of the archive route: one ZIP of every `paths` entry (files
* and directories, recursive) served as an attachment. Same GET shape as
* {@link downloadUrl}; the repeated `path` parameter is the only difference.
* Start one archive build. The host fences and walks the selection, then
* returns an id IMMEDIATELY — the zip itself is produced in the background and
* polled through {@link api.archiveStatus}. `paths` are absolute paths in the
* session's namespace (the same values `fsTree` / `downloadUrl` take).
*/
export function archiveUrl(scope: SessionScope, paths: readonly string[], name: string): string {
const params = new URLSearchParams({ sessionId: scope.sessionId, name })
if (scope.cwd !== undefined && scope.cwd !== '') params.set('cwd', scope.cwd)
for (const path of paths) params.append('path', path)
return `/sidebar/archive?${params.toString()}`
export function archiveBuild(
scope: SessionScope,
paths: readonly string[],
name: string,
): Promise<{ id: string; entries: number }> {
return call<{ id: string; entries: number }>('archive.build', scopePayload(scope, { paths: [...paths], name }))
}
/** One archive build's progress (the shape `archive.status` returns). */
export interface ArchiveBuildStatus {
state: 'building' | 'ready' | 'error'
/** Entries finished so far. */
done: number
/** Entries the archive will contain. */
total: number
/** Uncompressed bytes read so far. */
bytes: number
/** Present only when `state === 'error'`. */
error?: string
}
/** Poll one archive build (its id came from {@link archiveBuild}). */
export function archiveStatus(id: string): Promise<ArchiveBuildStatus> {
return call<ArchiveBuildStatus>('archive.status', { id })
}
/**
* Absolute URL of one FINISHED archive's bytes (the `archive.status` result
* must be `ready` first): GET /sidebar/archive?id=…&sessionId=… The session
* scope rides along because the host answers only the session that built it.
*/
export function archiveDownloadUrl(scope: SessionScope, id: string): string {
return `/sidebar/archive?${new URLSearchParams({ sessionId: scope.sessionId, id }).toString()}`
}
/** Shared URL builder for the /sidebar/file route (media vs download). */