feat: 会话下拉按用户面板工作区分组(optgroup分隔)+排除子代理/archived会话(新增只读workspaces路由)

This commit is contained in:
Aisland
2026-08-18 11:11:36 +08:00
parent 2e54d38080
commit a84cb12c5f
5 changed files with 89 additions and 31 deletions
+37 -10
View File
@@ -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')
+19 -8
View File
@@ -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>
)}
+15
View File
@@ -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',