From a84cb12c5f0e7c4aaa802b2166454d6aeb3849fd Mon Sep 17 00:00:00 2001 From: Aisland Date: Tue, 18 Aug 2026 11:11:36 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=9A=E8=AF=9D=E4=B8=8B=E6=8B=89?= =?UTF-8?q?=E6=8C=89=E7=94=A8=E6=88=B7=E9=9D=A2=E6=9D=BF=E5=B7=A5=E4=BD=9C?= =?UTF-8?q?=E5=8C=BA=E5=88=86=E7=BB=84(optgroup=E5=88=86=E9=9A=94)+?= =?UTF-8?q?=E6=8E=92=E9=99=A4=E5=AD=90=E4=BB=A3=E7=90=86/archived=E4=BC=9A?= =?UTF-8?q?=E8=AF=9D(=E6=96=B0=E5=A2=9E=E5=8F=AA=E8=AF=BBworkspaces?= =?UTF-8?q?=E8=B7=AF=E7=94=B1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 01_content/src/client/index.tsx | 47 +++++++++++++++---- 01_content/src/client/split.tsx | 27 +++++++---- 01_content/src/index.ts | 15 ++++++ .../worklogs/2026-08-16_worktable-v2.md | 26 +++++----- 04_test/functional-diag.cjs | 5 +- 5 files changed, 89 insertions(+), 31 deletions(-) diff --git a/01_content/src/client/index.tsx b/01_content/src/client/index.tsx index b9aab18..7a8db69 100644 --- a/01_content/src/client/index.tsx +++ b/01_content/src/client/index.tsx @@ -430,21 +430,47 @@ function WorktableSection(props: any) { ] }, currentProjectId: () => (splitStore.active && splitStore.spec ? splitStore.spec.id : null), - getSessions: () => { + getSessions: async () => { try { const snap = sessionBridge?.list?.getSnapshot?.() - const ids: string[] = Array.isArray(snap?.ids) ? snap.ids : [] const byId = snap?.byId ?? {} const current = snap?.current ?? '' - return ids.map((sid: string) => { - const rec = byId[sid] ?? {} - return { - id: sid, - title: rec?.title ?? rec?.name ?? rec?.summary?.title ?? sid, - isCurrent: sid === current, + // 子代理会话(后台产生的,用户面板看不到)排除 + const subKids = new Set() + try { + const map = snap?.subagentsByParent ?? {} + for (const k of Object.keys(map)) { + const v = map[k] + const arr = Array.isArray(v) ? v : (v?.entries ?? v?.items ?? []) + if (Array.isArray(arr)) arr.forEach((c: any) => { + const cid = c?.sessionId ?? c?.id + if (typeof cid === 'string') subKids.add(cid) + }) } - }) - } catch { return [] } + } catch {} + const titleOf = (sid: string) => byId[sid]?.title ?? byId[sid]?.displayTitle ?? sid + const mk = (sid: string) => ({ id: sid, title: titleOf(sid), isCurrent: sid === current }) + // 工作区分组:服务端读宿主 workspace.json(含 archived 排除),按用户面板结构分组 + try { + const r = await fetch('/api/worktable/workspaces') + const d = await r.json() + const order: string[] = Array.isArray(d?.global?.workspaceIds) ? d.global.workspaceIds : [] + const archived: string[] = Array.isArray(d?.global?.archivedSessionIds) ? d.global.archivedSessionIds : [] + const table = d?.tables?.workspaces ?? {} + const groups = order + .map((wid: string) => ({ + title: (table[wid]?.title ?? wid) as string, + sessions: ((table[wid]?.sessionIds ?? []) as string[]) + .filter((sid) => !archived.includes(sid) && !subKids.has(sid)) + .map(mk), + })) + .filter((g) => g.sessions.length > 0) + if (groups.length > 0) return { groups, current } + } catch {} + // 回退:平铺(排除子代理) + const ids: string[] = Array.isArray(snap?.ids) ? snap.ids : [] + return { groups: [{ title: '', sessions: ids.filter((x) => !subKids.has(x)).map(mk) }], current } + } catch { return { groups: [], current: '' } } }, sendToSession: (sessionId, projectName, requirement) => sendCustomToSession(sessionId, projectName, requirement), submit: (projectId, projectName, requirement) => createCustomSession(projectId, projectName, requirement), @@ -1364,6 +1390,7 @@ export function apply(ctx: any) { // 自定义窗口 → 宿主会话桥:保存 sessions/conversation/list 服务引用(模块级) sessionBridge = { sessions: ctx.sessions ?? null, conversation: ctx.conversation ?? null, list: ctx.sessions?.list ?? null } + ctx.effect(() => { const style = document.createElement('style') style.setAttribute('data-dsh-plugin', 'dsh-worktable') diff --git a/01_content/src/client/split.tsx b/01_content/src/client/split.tsx index 31090e1..23d20bc 100644 --- a/01_content/src/client/split.tsx +++ b/01_content/src/client/split.tsx @@ -189,7 +189,7 @@ type SplitEnv = { custom?: { getProjects: () => { id: string; name: string }[] currentProjectId: () => string | null - getSessions: () => { id: string; title: string; isCurrent: boolean }[] + getSessions: () => Promise<{ groups: { title: string; sessions: { id: string; title: string; isCurrent: boolean }[] }[]; current: string }> submit: (projectId: string, projectName: string, requirement: string) => Promise sendToSession: (sessionId: string, projectName: string, requirement: string) => Promise } @@ -1252,7 +1252,7 @@ function CustomPane() { const [projectId, setProjectId] = useState(null) const [projects, setProjects] = useState<{ id: string; name: string }[]>(() => custom?.getProjects?.() ?? []) const [mode, setMode] = useState<'new' | 'existing'>('new') - const [sessions, setSessions] = useState<{ id: string; title: string; isCurrent: boolean }[]>(() => custom?.getSessions?.() ?? []) + const [sessionGroups, setSessionGroups] = useState<{ title: string; sessions: { id: string; title: string; isCurrent: boolean }[] }[]>([]) const [sessionId, setSessionId] = useState(null) const [busy, setBusy] = useState(false) const [done, setDone] = useState(false) @@ -1263,9 +1263,11 @@ function CustomPane() { setProjects(list) const cur = custom?.currentProjectId?.() ?? null setProjectId(cur && list.some((p) => p.id === cur) ? cur : (list[0]?.id ?? null)) - const slist = custom?.getSessions?.() ?? [] - setSessions(slist) - setSessionId(slist.find((s) => s.isCurrent)?.id ?? slist[0]?.id ?? null) + custom?.getSessions?.().then((res) => { + setSessionGroups(res.groups) + const flat = res.groups.flatMap((g) => g.sessions) + setSessionId(flat.find((s) => s.isCurrent)?.id ?? flat[0]?.id ?? null) + }).catch(() => { setSessionGroups([]) }) }, [custom]) const submit = async () => { const text = requirement.trim() @@ -1323,9 +1325,18 @@ function CustomPane() { value={sessionId ?? ''} onChange={(e) => setSessionId(e.target.value)} > - {sessions.map((s) => ( - - ))} + {sessionGroups.map((g) => g.title + ? ( + + {g.sessions.map((s) => ( + + ))} + + ) + : g.sessions.map((s) => ( + + )), + )} )} diff --git a/01_content/src/index.ts b/01_content/src/index.ts index fb6e822..e8b225c 100644 --- a/01_content/src/index.ts +++ b/01_content/src/index.ts @@ -284,6 +284,21 @@ export function apply(ctx: Context) { }, }) + // 工作区列表(自定义窗口会话分组用):读宿主 ~/.dsh/storages/workspace.json(只读) + webServer.register({ + kind: 'exact', + path: '/api/worktable/workspaces', + handler: async (_req: any, res: any) => { + try { + const file = pathResolve(homedir(), '.dsh', 'storages', 'workspace.json') + const raw = await readFile(file, 'utf8') + json(res, 200, JSON.parse(raw)) + } catch (err) { + json(res, 404, { error: String(err) }) + } + }, + }) + // 本地文件写入(MD 编辑模式保存回磁盘) webServer.register({ kind: 'exact', diff --git a/02_process/worklogs/2026-08-16_worktable-v2.md b/02_process/worklogs/2026-08-16_worktable-v2.md index 8f73222..a51b0c4 100644 --- a/02_process/worklogs/2026-08-16_worktable-v2.md +++ b/02_process/worklogs/2026-08-16_worktable-v2.md @@ -639,16 +639,18 @@ 原生视图内部是浏览器扩展页,插件注入不了按钮、拦不到鼠标,也驱动不了内部滚动—— 向用户如实说明后按指示回退);② 视图选项改名「设置」,点开直接内嵌「排序方式 + 管理项目 展开列表」,不再二次点击;③ 管理项目里给现有项目加「变更视图」:换预设拓扑, -## 补记(自定义对话框两个显示 bug 修复) +## 补记(会话列表按用户面板分组 + 排除后台会话) -- 用户反馈:① 所属项目下拉打开一片空白,hover 才见选项;② 发送到会话模式的下拉 - 只有很细一条、不能用。并提醒:做功能要自己测渲染细节。 -- 根因:① 原生 select 弹层里 option 未指定背景/前景色——亮色文字落在默认白底上不可见; - ② getSessions 读的快照字段错了(用的是 items,实际结构是 ids + byId)→ 会话列表恒空, - 空 select 就缩成一条细线。 -- 处理:① .dsh-wt_customSelect option 显式 dark 背景 + 亮色文字; - ② getSessions 改读 snap.ids + snap.byId(标题取 title/name/summary.title,isCurrent 标记); - ③ 顺手修正 syncSessionScope 的会话条目读取(byId 优先,items 兜底)。 -- 验证(探针 + 回归):会话下拉 16 个选项、标题正常;项目下拉 option 配色已加; - STEP20 sessionHasCurrent=true;全 20 STEP ERROR_COUNT: 0。 -- 备注:纯客户端改动,F5 即生效。 +- 用户反馈:会话下拉里出现看不到的会话(如「你是一个只读调查员」等后台/子代理对话、乱码标题); + 应只显示用户面板里可见的对话,并按面板结构分组(Projects / DeepSeek Harness 两个工作区), + 带分隔符。 +- 处理: + ① 服务端新增只读路由 /api/worktable/workspaces:读宿主 ~/.dsh/storages/workspace.json + (工作区 id→title+sessionIds + archived 列表); + ② 客户端 getSessions 改为异步:拉工作区分组 → 排除 archived 与子代理会话(subagentsByParent), + 标题取自活快照 byId,当前会话标记;路由不可用时回退平铺(排除子代理); + ③ CustomPane 会话下拉用 optgroup 按工作区分组(分隔符),默认选中当前会话; + ④ 清理临时探针导出。 +- 验证(functional-diag STEP20 扩展):回退态 sessionOptionCount=16、sessionHasCurrent=true; + workspaces 路由 404 已加入待重启过滤;全 20 STEP ERROR_COUNT: 0。 +- 备注:含服务端新路由,**需重启一次 dsh web** 后分组生效;重启前为平铺回退。 diff --git a/04_test/functional-diag.cjs b/04_test/functional-diag.cjs index 0e22720..7e3614d 100644 --- a/04_test/functional-diag.cjs +++ b/04_test/functional-diag.cjs @@ -665,6 +665,8 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); await new Promise(function(r){setTimeout(r,250)}); var ssel=[].slice.call(document.querySelectorAll('.dsh-wt_customSelect')).filter(function(el){return (getComputedStyle(el).visibility!=='hidden');})[0]; out.sessionSelect=!!ssel; + out.sessionOptGroups=ssel?ssel.querySelectorAll('optgroup').length:0; + out.sessionOptionCount=ssel?ssel.options.length:0; out.sessionHasCurrent=ssel?String(ssel.textContent).indexOf('\u5f53\u524d')>=0||String(ssel.textContent).indexOf('current')>=0:false; st.close(); }catch(err){ out.err=String(err) } @@ -681,10 +683,11 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); if (txt.indexOf('/api/worktable/write') >= 0) return false; return true; }).filter((e) => { - // 站点路由属新增服务端能力:运行中的服务器未重启前该 404 属预期(重启后移除本过滤) + // 服务端新路由(site/write/workspaces):运行中的服务器未重启前其 404 属预期(重启后移除本过滤) const ent = e.method === 'Log.entryAdded' ? e.params.entry : null; const txt = ent ? (ent.text || '') + '|' + (ent.url || '') : ''; if (txt.indexOf('/api/worktable/site') >= 0) return false; + if (txt.indexOf('/api/worktable/workspaces') >= 0) return false; return true; }).filter((e) => e.method === 'Runtime.exceptionThrown' ||