([])
- const load = useCallback((p: string, push: boolean) => {
- setLoading(true)
- postJson('/api/worktable/fs', {
- path: p,
- sessionId: splitEnv?.getScope()?.sessionId ?? '',
- cwd: splitEnv?.getScope()?.cwd ?? '',
- })
- .then((d) => {
- setPath(d.path ?? p)
- setEntries(d.entries ?? [])
- setError('')
- setHistory((h) => (push && p !== d.path ? [...h, p] : h))
+ const [, setTick] = useState(0)
+ const rerender = () => setTick((t) => t + 1)
+
+ const fetchDir = useCallback(async (path: string, force = false) => {
+ if (!force && cacheRef.current[path]) return { path, entries: cacheRef.current[path] }
+ try {
+ const d = await postJson('/api/worktable/fs', {
+ path,
+ sessionId: splitEnv?.getScope()?.sessionId ?? '',
+ cwd: splitEnv?.getScope()?.cwd ?? '',
})
- .catch((e) => setError(String(e)))
- .finally(() => setLoading(false))
+ const entries: any[] = d.entries ?? []
+ cacheRef.current[d.path] = entries
+ setError(d.error ? String(d.error) : '')
+ return { path: d.path, entries }
+ } catch (e) {
+ setError(String(e))
+ return { path, entries: [] }
+ } finally {
+ rerender()
+ }
}, [])
- useEffect(() => {
- load(splitEnv?.getScope()?.cwd ?? '', false)
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [])
- const back = () => {
- setHistory((h) => {
- if (h.length === 0) return h
- const prev = h[h.length - 1]
- load(prev, false)
- return h.slice(0, -1)
- })
+
+ const initRoot = useCallback(async () => {
+ const r = await fetchDir(splitEnv?.getScope()?.cwd ?? '')
+ setRootPath(r.path)
+ rerender()
+ }, [fetchDir])
+
+ useEffect(() => { initRoot() }, [initRoot])
+
+ const toggle = (path: string) => {
+ if (expandedRef.current.has(path)) expandedRef.current.delete(path)
+ else { expandedRef.current.add(path); fetchDir(path) }
+ rerender()
}
+
+ const refresh = () => {
+ cacheRef.current = {}
+ expandedRef.current.clear()
+ setError('')
+ initRoot()
+ }
+
+ const goUp = () => {
+ if (!rootPath) return
+ const parent = parentPathOf(rootPath)
+ if (parent === rootPath) return
+ cacheRef.current = {}
+ expandedRef.current.clear()
+ setError('')
+ fetchDir(parent).then((r) => { setRootPath(r.path); rerender() })
+ }
+
+ const renderLevel = (path: string, depth: number): any[] => {
+ const entries = cacheRef.current[path]
+ if (!entries) return []
+ const nodes: any[] = []
+ for (const e of entries) {
+ const isOpen = expandedRef.current.has(e.path)
+ nodes.push(
+
+
+ {e.isDir && isOpen && renderLevel(e.path, depth + 1)}
+
,
+ )
+ }
+ return nodes
+ }
+
return (
<>
-
-
-
- {path || '…'}
+
+
+ {rootPath || '…'}
{error &&
{error}
}
- {!error && !loading && entries.length === 0 &&
—
}
- {entries.map((e) => (
-
- ))}
+ {!error && cacheRef.current[rootPath]?.length === 0 &&
—
}
+ {renderLevel(rootPath, 0)}
>
)
@@ -818,7 +878,7 @@ function GitPane() {
)
}
-/** 任务管理窗(客户端 sessions 快照 jobsBySession;2s 刷新) */
+/** 任务管理窗:后台任务 + 子代理(Agent 情况;2s 刷新) */
function JobsPane() {
const [, setTick] = useState(0)
useEffect(() => {
@@ -826,8 +886,10 @@ function JobsPane() {
return () => window.clearInterval(timer)
}, [])
const jobs = splitEnv?.getJobs?.() ?? []
+ const subagents = splitEnv?.getSubagents?.() ?? []
return (
+
{T('pane.jobsTitle')}
{jobs.length === 0 &&
{T('pane.jobsEmpty')}
}
{jobs.map((j) => (
@@ -836,6 +898,15 @@ function JobsPane() {
{j.kind}
))}
+
{T('pane.subagents')}
+ {subagents.length === 0 &&
{T('pane.subagentsEmpty')}
}
+ {subagents.map((s: any, i: number) => (
+
+ ●
+ {s?.label ?? s?.title ?? s?.name ?? '—'}
+ {s?.status && {s.status}}
+
+ ))}
)
}
diff --git a/01_content/src/client/styles.ts b/01_content/src/client/styles.ts
index dbc61b3..7cb8b67 100644
--- a/01_content/src/client/styles.ts
+++ b/01_content/src/client/styles.ts
@@ -160,4 +160,12 @@ export const css = [
'.dsh-wt_tabTitle{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
'.dsh-wt_tabClose{flex:none;width:14px;height:14px;padding:0;border:none;border-radius:3px;background:transparent;color:var(--dsw-alias-label-tertiary,#6e7683);font-size:9px;line-height:1;cursor:pointer}',
'.dsh-wt_tabClose:hover{color:var(--dsw-alias-label-primary,#e6e8eb);background:var(--dsw-alias-fill-l1,rgba(255,255,255,.08))}',
+ // 树形资源管理器
+ '.dsh-wt_treeRow{display:flex;align-items:center;gap:5px;width:100%;box-sizing:border-box;padding:3px 6px;border:none;border-radius:5px;background:transparent;color:var(--dsw-alias-label-primary,#e6e8eb);font:inherit;font-size:11px;line-height:16px;cursor:pointer;text-align:left}',
+ '.dsh-wt_treeRow:hover{background:var(--dsw-alias-fill-l1,rgba(255,255,255,.05))}',
+ '.dsh-wt_treeArrow{flex:none;width:10px;font-size:8px;color:var(--dsw-alias-label-tertiary,#6e7683);transition:transform .12s ease}',
+ '.dsh-wt_treeArrowOpen{transform:rotate(90deg)}',
+ '.dsh-wt_treeIcon{flex:none}',
+ '.dsh-wt_treeName{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
+ '.dsh-wt_subSection{flex:none;padding:6px 8px 2px;font-size:10px;font-weight:600;letter-spacing:.05em;color:var(--dsw-alias-label-tertiary,#6e7683)}',
].join('\n')
diff --git a/01_content/src/index.ts b/01_content/src/index.ts
index cb92467..c786db3 100644
--- a/01_content/src/index.ts
+++ b/01_content/src/index.ts
@@ -1,7 +1,9 @@
import { Context } from '@deepseek-ai/cordis'
import { execFile } from 'node:child_process'
-import { readdir } from 'node:fs/promises'
-import { resolve, sep } from 'node:path'
+import { readdir, realpathSync } from 'node:fs'
+import { dirname, resolve as pathResolve, sep } from 'node:path'
+import { createRequire } from 'node:module'
+import { fileURLToPath, pathToFileURL } from 'node:url'
/**
* dsh-worktable 服务端:健康路由 + 工作区内容窗的数据路由。
@@ -19,6 +21,28 @@ export const HEALTH_PATH = '/api/worktable/health'
const MAX_ENTRIES = 500
+/**
+ * 从本插件模块位置向祖先方向查找并加载 node_modules 包(如 ws / node-pty)。
+ * 本包经 junction 链接进 profile,普通 import 可能解析不到 profile 级依赖;
+ * 同时尝试 junction 路径与 realpath 两条祖先链。
+ */
+function loadPkg(pkg: string): any | null {
+ const starts = new Set()
+ try { starts.add(dirname(fileURLToPath(import.meta.url))) } catch {}
+ try { starts.add(realpathSync(dirname(fileURLToPath(import.meta.url)))) } catch {}
+ for (const start of starts) {
+ let dir: string | null = start
+ while (dir && dir !== pathResolve(dir, '..')) {
+ try {
+ const req = createRequire(pathToFileURL(pathResolve(dir, '__wt_probe__.js')).href)
+ return req(pkg)
+ } catch {}
+ dir = pathResolve(dir, '..')
+ }
+ }
+ return null
+}
+
/** 解析会话工作目录:服务端 header.cwd 优先,其次客户端传入 cwd,最后进程 cwd */
function serverCwd(ctx: any, sessionId?: string, clientCwd?: string): string {
if (sessionId) {
@@ -89,7 +113,12 @@ async function setupTerminal(webServer: any, ctx: any) {
let ptyMod: any = null
try { wsMod = await import('ws') } catch {}
try { ptyMod = await import('node-pty') } catch {}
- if (!wsMod || !ptyMod) return
+ if (!wsMod) wsMod = loadPkg('ws')
+ if (!ptyMod) ptyMod = loadPkg('node-pty')
+ if (!wsMod || !ptyMod) {
+ ctx.logger?.warn('[dsh-worktable] 终端路由未注册:ws/node-pty 均不可用')
+ return
+ }
const WebSocketServer = wsMod.WebSocketServer ?? wsMod.default?.WebSocketServer
if (!WebSocketServer) return
const pty = ptyMod.default ?? ptyMod
diff --git a/02_process/worklogs/2026-08-16_worktable-v2.md b/02_process/worklogs/2026-08-16_worktable-v2.md
index 592c4c1..dd91430 100644
--- a/02_process/worklogs/2026-08-16_worktable-v2.md
+++ b/02_process/worklogs/2026-08-16_worktable-v2.md
@@ -389,3 +389,22 @@
③ 任务窗数据源不变(jobsBySession,无后台任务时显示空态文案)。
- 验证:构建 + node --check 通过;headless Chrome 回归零报错、区块正常渲染。
- 注意:服务端 cwd 解析需**重启 dsh web 后生效**;标签页模型为客户端改动、刷新即生效。
+
+## 补记(对照 better-sidebar 源码重做:树形资源管理器 / 子代理任务窗 / 终端依赖解析)
+
+- 用户反馈:① 资源管理器刷新/后退按钮失效(只有上一级能用);② 终端空白;③ 任务窗
+ 没有 Agent 情况;④ 要求对照 better-sidebar 源码重做(树形展开 + 重绘图标)。
+- 调研:better-sidebar 客户端 16px 描边 SVG 图标 + 子代理树(aria-expanded、缩进、当前高亮);
+ 服务端 inject ['webServer','sessions','webRuntime','tools']。
+- 实现:
+ ① 资源管理器重写为树形:懒加载子目录(缓存 + 展开集)、▸/▾ 旋转箭头、重绘文件夹/文件
+ SVG 图标、缩进层级;刷新(清缓存重载根)与上一级(根上移)修复可用(旧版在 setState
+ 更新器里做副作用导致按钮失效,已避免)。
+ ② 任务窗增加「子代理」区(sessions 快照 subagentsByParent[current],防御式取数,
+ 状态圆点 + 缩进),与后台任务并列。
+ ③ 终端空白根因:服务端 import('node-pty')/import('ws') 失败——本包经 junction 链接,
+ 模块真实路径在工作区,向上找不到 profile 级依赖。修复:loadPkg 沿「junction 路径 +
+ realpath」两条祖先链 createRequire 查找并加载;已本地验证 junction 路径可解析
+ node-pty/ws。**需重启 dsh web 后终端路由才注册。**
+- 验证:构建 + node --check 通过;headless Chrome 回归零报错;依赖解析测试通过。
+- 差距(M3 待续):文件打开/编辑器、SCM diff/暂存/提交、任务输出回放。