feat: 会话下拉按用户面板工作区分组(optgroup分隔)+排除子代理/archived会话(新增只读workspaces路由)
This commit is contained in:
@@ -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<string>()
|
||||
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')
|
||||
|
||||
@@ -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<void>
|
||||
sendToSession: (sessionId: string, projectName: string, requirement: string) => Promise<void>
|
||||
}
|
||||
@@ -1252,7 +1252,7 @@ function CustomPane() {
|
||||
const [projectId, setProjectId] = useState<string | null>(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<string | null>(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) => (
|
||||
<option key={s.id} value={s.id}>{s.title}{s.isCurrent ? ' ' + T('custom.sessionCurrent') : ''}</option>
|
||||
))}
|
||||
{sessionGroups.map((g) => g.title
|
||||
? (
|
||||
<optgroup key={g.title} label={g.title}>
|
||||
{g.sessions.map((s) => (
|
||||
<option key={s.id} value={s.id}>{s.title}{s.isCurrent ? ' ' + T('custom.sessionCurrent') : ''}</option>
|
||||
))}
|
||||
</optgroup>
|
||||
)
|
||||
: g.sessions.map((s) => (
|
||||
<option key={s.id} value={s.id}>{s.title}{s.isCurrent ? ' ' + T('custom.sessionCurrent') : ''}</option>
|
||||
)),
|
||||
)}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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** 后分组生效;重启前为平铺回退。
|
||||
|
||||
@@ -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' ||
|
||||
|
||||
Reference in New Issue
Block a user