dsh-subagent-workspace-ui 1.2.4 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -1
- package/README.zh.md +35 -5
- package/lib/client.js +7 -6
- package/package.json +1 -2
package/README.md
CHANGED
|
@@ -49,7 +49,11 @@ If you previously disabled `ui-subagent` manually in `$DSH_HOME/profiles/web/cor
|
|
|
49
49
|
|
|
50
50
|
The public DSH Web session store exposes subagent summaries that have been discovered in the current browser runtime. It deliberately does not expose a global historical subagent index or a mode for every unvisited child. Therefore this first plugin version manages the discovered catalog; rows whose type is not yet loaded remain visible and searchable and fall back to DSH's retained session navigation. Exact catalog navigation is used automatically as soon as DSH supplies the address and mode.
|
|
51
51
|
|
|
52
|
-
A full persistent workspace-wide archive view requires a host-side catalog RPC (or an upstream DSH API) that enumerates every child address and its mode. The public `SessionSummary` does not expose the original prompt or provider/model route, so those are intentionally not queried or displayed. Live output and tool/context activity
|
|
52
|
+
A full persistent workspace-wide archive view requires a host-side catalog RPC (or an upstream DSH API) that enumerates every child address and its mode. The public `SessionSummary` does not expose the original prompt or provider/model route, so those are intentionally not queried or displayed. Live output and tool/context activity are read from the bound session automatically: on dsh **0.1.2-alpha.2** they are derived from the raw `binding.eventSource` event stream (showing the tool description or target filename), while older hosts (e.g. **0.1.1-rc.2**) fall back to `session.getSnapshot().chat.legacy`. Capability detection selects the path, so the plugin stays forward compatible. If the host publishes neither, the panel falls back to the durable summary. The UI is isolated in [`lib/client.js`](lib/client.js), so it can switch to a richer source without changing the panel interaction model.
|
|
53
|
+
|
|
54
|
+
## Compatibility
|
|
55
|
+
|
|
56
|
+
v1.3.1 is compatible with dsh **0.1.2-alpha.2** (new `binding.eventSource` live-output path) and **0.1.1-rc.2** (legacy `chat.legacy` snapshot path). Live output is selected by capability detection, so older hosts behave as before.
|
|
53
57
|
|
|
54
58
|
## Validation
|
|
55
59
|
|
|
@@ -57,6 +61,13 @@ A full persistent workspace-wide archive view requires a host-side catalog RPC (
|
|
|
57
61
|
pnpm run check
|
|
58
62
|
```
|
|
59
63
|
|
|
64
|
+
Smoke-test a specific dsh version:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
./test.sh # local dsh, port 8084
|
|
68
|
+
DSH_VERSION=0.1.1-rc.2 ./test.sh # pnpx @deepseek-ai/dsh@0.1.1-rc.2 (via proxychains4 -q)
|
|
69
|
+
```
|
|
70
|
+
|
|
60
71
|
## Acknowledgements
|
|
61
72
|
|
|
62
73
|
- Subagent permanent deletion and session cleanup design inspired by and referencing [@heiheiha798/dsh-plugin-subagent-delete](https://github.com/heiheiha798/dsh-plugin-subagent-delete).
|
package/README.zh.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
面向 DeepSeek Harness Web 的子代理管理插件。插件在会话标题栏提供一个紧凑的 `🧩 子代理 active/total` 入口,用于搜索、筛选、分组、排序、查看和批量归档当前运行时已发现的子代理。
|
|
4
4
|
|
|
5
|
-
当前发布版本:**v1.
|
|
5
|
+
当前发布版本:**v1.3.1**
|
|
6
6
|
|
|
7
7
|
## 主要功能
|
|
8
8
|
|
|
@@ -132,16 +132,23 @@
|
|
|
132
132
|
|
|
133
133
|
### 实时活动与流式输出
|
|
134
134
|
|
|
135
|
-
当 DSH 会话 API
|
|
135
|
+
当 DSH 会话 API 提供绑定会话与实时事件时,卡片底部会显示实时活动:
|
|
136
136
|
|
|
137
137
|
- 最新两行模型文本或思考文本。
|
|
138
|
-
- 正在运行的工具调用,例如 `Read`、`Bash
|
|
138
|
+
- 正在运行的工具调用,例如 `Read`、`Bash`(优先显示工具说明或目标文件名)。
|
|
139
139
|
- 最近完成的工具调用及成功/失败状态。
|
|
140
140
|
- 上下文注入,例如 `skill-catalog` 或插件系统提示。
|
|
141
141
|
- 命令状态。
|
|
142
142
|
|
|
143
143
|
实时输出具有金属光泽扫光动效;子代理结束后保留最后显示快照,并变为灰色。输出区域最多显示两行,避免持续滚动导致内容难以阅读。
|
|
144
144
|
|
|
145
|
+
**兼容两个 API 代际(自动探测)**:
|
|
146
|
+
|
|
147
|
+
- dsh **0.1.2-alpha.2**:使用 `binding.eventSource`(原始 `SessionEvent` 事件流)推导实时输出。
|
|
148
|
+
- dsh **0.1.1-rc.2** 及更早:回退 `session.getSnapshot().chat.legacy`(对话快照)路径。
|
|
149
|
+
|
|
150
|
+
按能力探测自动切换,向前兼容,老版本行为不变。
|
|
151
|
+
|
|
145
152
|
## 界面说明
|
|
146
153
|
|
|
147
154
|
从上到下依次为:
|
|
@@ -183,15 +190,21 @@ http://127.0.0.1:3080
|
|
|
183
190
|
|
|
184
191
|
插件只管理当前 DSH Web 客户端运行时已经发现的子代理目录,不伪造不存在的历史数据。首次加载以 40 条为一页;普通分页可以继续加载,批量时间选择最多扩展到 1000 条。
|
|
185
192
|
|
|
186
|
-
公共 `SessionSummary` 不保证提供原始提示词、provider/model 或全部历史日志,因此插件不会查询或显示 provider/model
|
|
193
|
+
公共 `SessionSummary` 不保证提供原始提示词、provider/model 或全部历史日志,因此插件不会查询或显示 provider/model。实时输出按能力探测自动切换两种公开接口:
|
|
187
194
|
|
|
188
195
|
```text
|
|
196
|
+
# dsh 0.1.2-alpha.2(新 API):绑定的事件源
|
|
197
|
+
sessions.binding(childId).eventSource
|
|
198
|
+
→ open()
|
|
199
|
+
→ getSnapshot().entries # 原始 SessionEvent:assistant/chunk、tool/call、tool/result…
|
|
200
|
+
|
|
201
|
+
# dsh 0.1.1-rc.2 及更早(旧 API):对话快照
|
|
189
202
|
sessions.binding(childId).session
|
|
190
203
|
→ session.open()
|
|
191
204
|
→ session.getSnapshot().chat.legacy
|
|
192
205
|
```
|
|
193
206
|
|
|
194
|
-
|
|
207
|
+
如果在当前宿主拿不到对应的实时数据,插件只能显示持久化的会话摘要和统计信息。本版本兼容 **dsh 0.1.2-alpha.2** 与 **0.1.1-rc.2**。
|
|
195
208
|
|
|
196
209
|
归档、分类和最近使用顺序保存在浏览器本地 `localStorage` 中,不会写入 DSH 会话日志。
|
|
197
210
|
|
|
@@ -210,10 +223,27 @@ pnpm run check
|
|
|
210
223
|
- `lib/client.js`
|
|
211
224
|
- `lib/index.js`
|
|
212
225
|
|
|
226
|
+
### 冒烟测试(可指定 dsh 版本)
|
|
227
|
+
|
|
228
|
+
```bash
|
|
229
|
+
./test.sh # 本地 dsh 跑 web,端口 8084
|
|
230
|
+
./test.sh 8085 # 指定端口
|
|
231
|
+
DSH_VERSION=0.1.1-rc.2 ./test.sh # 用 pnpx 拉取指定 dsh 版本跑 web(默认经 proxychains4 -q 走代理)
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
`DSH_HOME` 固定为 `$HOME/tmp/dsh-test`;`DSH_VERSION` 非空时用 `pnpx @deepseek-ai/dsh@<version>` 运行,可用于冒烟测试旧版本(如 0.1.1-rc.2)的兼容性。
|
|
235
|
+
|
|
213
236
|
## 致谢与参考
|
|
214
237
|
|
|
215
238
|
- 子代理永久删除、会话生命周期清理及快照刷新机制的设计参考并致谢开源项目:[@heiheiha798/dsh-plugin-subagent-delete](https://github.com/heiheiha798/dsh-plugin-subagent-delete)。
|
|
216
239
|
|
|
240
|
+
## v1.3.1 发布说明
|
|
241
|
+
|
|
242
|
+
- **兼容 dsh 0.1.2-alpha.2 与 0.1.1-rc.2**:实时输出按能力探测自动切换两条链路——新版本(0.1.2-alpha.2)走 `binding.eventSource` 原始事件流推导;旧版本(0.1.1-rc.2 及更早)回退 `chat.legacy` 对话快照。老版本行为不变,向前兼容。
|
|
243
|
+
- **实时工具调用显示说明/文件名**:进行中与已完成的工具调用现在优先显示工具 `description` 或目标 `path/file_path`(例如 `bash · Print current working directory`、`write · subagent-AJ.txt`),不再只有工具名。工具参数跨分片累积,参数完整后才渲染详情,避免流式过程中显示残缺 JSON。
|
|
244
|
+
- **清理**:移除已不存在的 `dsh-client-runtime` 客户端注入引用。
|
|
245
|
+
- **测试脚本支持指定版本**:`DSH_VERSION=0.1.1-rc.2 ./test.sh` 可冒烟测试旧版本兼容性。
|
|
246
|
+
|
|
217
247
|
## v1.2.4 发布说明
|
|
218
248
|
|
|
219
249
|
- **批量选择优化**:修复批量模式下直接点击 checkbox 偶现无响应的问题;支持 Shift 连选与连续批量取消选中(跟随上一次点击意图)。
|
package/lib/client.js
CHANGED
|
@@ -26,29 +26,30 @@ body[data-ds-dark-theme] .dsh-sam-row-hidden{background:rgba(255,255,255,0.03)!i
|
|
|
26
26
|
const DEFAULT_LIGHT_BG='#edf3fe', DEFAULT_DARK_BG='#1c2333'
|
|
27
27
|
const load=()=>{try{return JSON.parse(localStorage.getItem(key))||{}}catch{return {}}}
|
|
28
28
|
const age=t=>{const seconds=Math.max(0,Math.floor((Date.now()-(t||Date.now()))/1000));if(seconds<60)return `${seconds} 秒前`;const minutes=Math.floor(seconds/60);if(minutes<60)return `${minutes} 分钟前`;const hours=Math.floor(minutes/60);if(hours<24)return `${hours} 小时 ${minutes%60} 分钟前`;return `${Math.floor(hours/24)} 天前`}
|
|
29
|
-
const title=s=>s.title||s.displayTitle||s.id, short=v=>v?v.replace(/^session-/,'').slice(0,8):'未知', workspace=s=>s?.cwd?s.cwd.split(/[\\/]/).filter(Boolean).pop():'未知工作区', modeLabel=m=>m==='one-shot'?'一次性':m==='continuable'?'可继续':'类型待加载', tokenTotal=s=>{const u=s?.projectionValues?.tokenUsage;return u?u.uncachedInputTokens+u.outputTokens+u.cacheReadTokens+u.cacheWriteTokens:undefined},fmt=n=>n==null?'未知':n>=1e6?`${(n/1e6).toFixed(1)}m`:n>=1e3?`${(n/1e3).toFixed(1)}k`:String(n), promptPreview=s=>String(s?.prompt||s?.projectionValues?.prompt||'').slice(0,100)
|
|
29
|
+
const title=s=>s.title||s.displayTitle||s.id, short=v=>v?v.replace(/^session-/,'').slice(0,8):'未知', workspace=s=>s?.cwd?s.cwd.split(/[\\/]/).filter(Boolean).pop():'未知工作区', modeLabel=m=>m==='one-shot'?'一次性':m==='continuable'?'可继续':'类型待加载', tokenTotal=s=>{const u=s?.projectionValues?.tokenUsage;return u?u.uncachedInputTokens+u.outputTokens+u.cacheReadTokens+u.cacheWriteTokens:undefined},fmt=n=>n==null?'未知':n>=1e6?`${(n/1e6).toFixed(1)}m`:n>=1e3?`${(n/1e3).toFixed(1)}k`:String(n), promptPreview=s=>String(s?.prompt||s?.projectionValues?.prompt||'').slice(0,100)
|
|
30
30
|
const category=name=>{const clean=name.trim();const part=clean.split(/[::|—–-]/)[0].trim();return part.length>1&&part.length<28?part:clean.split(/\\s+/).slice(0,2).join(' ')||'未分类'},rootSession=(byId,id)=>{let current=id,seen=new Set();while(current&&byId[current]?.origin==='subagent'&&byId[current].parentId&&!seen.has(current)){seen.add(current);current=byId[current].parentId}return current}
|
|
31
31
|
const modeMap=c=>Object.values(c||{}).flatMap(x=>x.entries||[]).reduce((o,e)=>{if(e.kind==='child')o[e.id]={mode:e.mode,label:e.label};return o},{}), highlight=(text,term)=>{if(!term)return text;const parts=String(text).split(new RegExp(`(${term.replace(/[.*+?^${}()|[\\]\\\\]/g,'\\\\$&')})`,'ig'));return parts.map((part,i)=>i%2?jsx('mark',{children:part},i):part)}
|
|
32
32
|
let sessionsRt=null
|
|
33
33
|
const NOOP=()=>()=>{}
|
|
34
34
|
function LiveOutput({parentId,childId,running,prefix=true,autoLoad=true,onLoad}){
|
|
35
|
-
const [loading,setLoading]=React.useState(false),[,bump]=React.useState(0),[lastText,setLastText]=React.useState(''),
|
|
36
|
-
React.useEffect(()=>{if(!
|
|
37
|
-
const snap=session?.getSnapshot?.(),chat=snap?.chat,blocks=chat?.legacy?.partial?.blocks||[],runningCalls=chat?.legacy?.runningCalls||[],nodes=chat?.legacy?.nodes||[];
|
|
35
|
+
const [loading,setLoading]=React.useState(false),[,bump]=React.useState(0),[lastText,setLastText]=React.useState(''),binding=React.useMemo(()=>{if(!running&&!autoLoad)return null;try{return sessionsRt?.binding?.(childId)||null}catch{return null}},[childId,running,autoLoad]),session=binding?.session||null,feed=binding?.eventSource||null
|
|
36
|
+
React.useEffect(()=>{if(!binding)return;let alive=true,timer=null,scheduled=false;const notify=()=>{if(!alive||scheduled)return;scheduled=true;timer=setTimeout(()=>{scheduled=false;timer=null;if(alive)bump(v=>v+1)},100)};const setup=async()=>{try{if(parentId){const entries=sessionsRt?.list?.getSnapshot?.().subagentsByParent?.[parentId]?.entries||[],entry=entries.find(e=>e.kind==='child'&&e.id===childId);if(entry)session?.configureSubagent?.({parentSessionId:parentId,childSessionId:childId,mode:entry.mode})}await session?.open?.()}catch{}finally{if(alive){setLoading(false);bump(v=>v+1)}}};setup();const source=feed||session,unsub=source?.subscribe?.(notify)||NOOP();return()=>{alive=false;if(timer)clearTimeout(timer);unsub()}},[binding,session,feed,parentId,childId])
|
|
37
|
+
let text='';if(feed){const win=feed.getSnapshot?.();if(win&&win.entries){const out=liveFromEvents(win.entries,running);if(out.pendingCount)text=out.activity.join('\n');else if(running&&out.streamText)text=out.streamText;else if(out.activity.length)text=out.activity.join('\n');else text=out.finalText}}else{const snap=session?.getSnapshot?.(),chat=snap?.chat,blocks=chat?.legacy?.partial?.blocks||[],runningCalls=chat?.legacy?.runningCalls||[],nodes=chat?.legacy?.nodes||[];const activity=[];runningCalls.slice(-2).forEach(c=>activity.push(`▶ ${c.name||'工具调用'}${c.argsRaw?` · ${toolDetail(c.argsRaw)}`:''}`));if(!activity.length)nodes.slice(-8).filter(n=>n.kind==='context'||n.kind==='tool-result'||n.kind==='command').slice(-2).forEach(n=>{if(n.kind==='context')activity.push(`上下文注入 · ${n.provenance?.label||n.form||'context'}`);else if(n.kind==='tool-result')activity.push(`${n.isError?'✖':'✓'} ${n.call?.name||'工具调用'}${n.call?.argsRaw?` · ${toolDetail(n.call.argsRaw)}`:''}`);else activity.push(`⌘ ${n.name||'命令'}`)});let streamText='';for(let i=blocks.length-1;i>=0;i--){const b=blocks[i];if((b.kind==='text'||b.kind==='reasoning')&&b.text){const lines=b.text.split('\n').map(x=>x.trim()).filter(Boolean);if(lines.length){streamText=(b.kind==='reasoning'?'💭 ':'')+lines.slice(-2).join('\n');break}}}if(runningCalls.length)text=activity.join('\n');else if(running&&streamText)text=streamText;else if(activity.length)text=activity.join('\n');else for(let i=blocks.length-1;i>=0;i--){const b=blocks[i];if((b.kind==='text'||b.kind==='reasoning')&&b.text){const lines=b.text.split('\n').map(x=>x.trim()).filter(Boolean);if(lines.length){text=(b.kind==='reasoning'?'💭 ':'')+lines.slice(-2).join('\n');break}}else if(b.kind==='tool-call'&&b.name){text='⚙ 调用工具 '+b.name;break}}}
|
|
38
38
|
React.useEffect(()=>{if(text)setLastText(text)},[text])
|
|
39
39
|
if(!running&&!autoLoad){return jsx('button',{type:'button',className:'dsh-sam-load-live-btn',onClick:e=>{e.stopPropagation();setLoading(true);onLoad?.()},title:'加载该子代理的历史最新消息',children:loading?'⏳ 加载中...':'📥 加载最新消息'})}
|
|
40
40
|
if(!session)return null
|
|
41
41
|
const shown=text||lastText,lines=shown.slice(-400).split('\n').filter(Boolean);return shown?jsx('div',{className:`dsh-sam-live ${running?'dsh-sam-live-running':'dsh-sam-live-final'}`,children:[prefix&&jsx('span',{className:'dsh-sam-live-prefix',children:'实时输出:'}),lines.map((line,index)=>jsx('span',{className:index===lines.length-1?'dsh-sam-live-latest':'dsh-sam-live-old',children:line},index))]}):null
|
|
42
42
|
}
|
|
43
43
|
const elapsed=t=>{const seconds=Math.max(0,Math.floor((Date.now()-(t||Date.now()))/1000));return seconds>=3600?`${Math.floor(seconds/3600)}时${Math.floor(seconds%3600/60)}分`:seconds>=60?`${Math.floor(seconds/60)}分${seconds%60}秒`:`${seconds}秒`},compactPath=v=>{const value=String(v).replace(/\\/g,'/'),max=72;if(value.length<=max)return value;const parts=value.split('/').filter(Boolean);let result=parts.pop()||value;while(parts.length&&(`.../${parts[parts.length-1]}/${result}`).length<=max)result=`${parts.pop()}/${result}`;return `.../${result}`},toolDetail=v=>{if(!v)return '';try{const value=JSON.parse(v),key=value.description!=null?'description':value.command!=null?'command':value.file_path!=null?'file_path':value.path!=null?'path':value.prompt!=null?'prompt':null,preferred=key?value[key]:undefined;if(preferred)return (key==='file_path'||key==='path'?compactPath: String)(String(preferred).split('\n')[0].slice(0,100));const first=Object.values(value).find(x=>typeof x==='string');return first?first.split('\n')[0].slice(0,100):String(v).slice(0,100)}catch{return String(v).split('\n')[0].slice(0,100)}}
|
|
44
|
+
const liveFromEvents=(entries,running)=>{const tail=s=>{const lines=String(s||'').split('\n').map(x=>x.trim()).filter(Boolean);return lines.length?lines.slice(-2).join('\n'):''},finalOf=blocks=>{const list=Array.isArray(blocks)?blocks:[];for(let i=list.length-1;i>=0;i--){const b=list[i];if(b?.type==='text'||b?.type==='reasoning'){const t=tail(b.text);if(t)return (b.type==='reasoning'?'💭 ':'')+t}if(b?.type==='tool-call'&&b.name)return '⚙ 调用工具 '+b.name}return ''};let text='',reasoning='',finalText='',hasStream=false,lastKind='text';const pending=[],done=[],byCall=new Map();for(const entry of entries){if(!entry||entry.type!=='event')continue;const e=entry.event;if(!e||typeof e.type!=='string')continue;switch(e.type){case 'assistant/chunk':{const c=e.data?.chunk;if(!c)break;if(c.type==='text-delta'){text+=c.text||'';hasStream=true;lastKind='text'}else if(c.type==='reasoning-delta'){reasoning+=c.text||'';hasStream=true;lastKind='reasoning'}else if(c.type==='tool-call-delta'&&c.id){let rec=byCall.get(c.id);if(!rec){rec={name:c.name||'工具调用',args:'',complete:false,running:true};byCall.set(c.id,rec);pending.push(rec)}if(c.name)rec.name=c.name;rec.args+=c.argumentsDelta||''}else if(c.type==='block-end'&&c.block?.type==='tool-call'){let rec=byCall.get(c.block.id);if(!rec){rec={name:c.block.name||'工具调用',args:'',complete:false,running:true};byCall.set(c.block.id,rec);pending.push(rec)}if(c.block.name)rec.name=c.block.name;rec.args=c.block.arguments||rec.args;rec.complete=true}break}case 'assistant/message':{finalText=finalOf(e.data?.message?.content);text='';reasoning='';hasStream=false;break}case 'tool/call':{let rec=byCall.get(e.data?.callId);if(!rec){rec={name:e.data?.name||'工具调用',args:'',complete:false,running:true};byCall.set(e.data?.callId,rec);pending.push(rec)}if(e.data?.name)rec.name=e.data?.name;if(e.data?.arguments!=null){rec.args=e.data?.arguments;rec.complete=true}rec.running=true;break}case 'tool/result':{const rec=byCall.get(e.data?.message?.source?.callId),block=e.data?.message?.content?.[0];if(rec){rec.running=false;done.push({name:rec.name,args:rec.args,complete:rec.complete,isError:block?.isError})}break}default:break}}const open=pending.filter(r=>r.running!==false);return{pendingCount:open.length,activity:[...open.slice(-2).map(r=>`▶ ${r.name||'工具调用'}${r.complete&&r.args?` · ${toolDetail(r.args)}`:''}`),...done.slice(-2).map(r=>`${r.isError?'✖':'✓'} ${r.name||'工具调用'}${r.complete&&r.args?` · ${toolDetail(r.args)}`:''}`)],streamText:hasStream?((lastKind==='reasoning'?'💭 ':'')+tail(lastKind==='reasoning'?reasoning:text)):'',finalText}}
|
|
44
45
|
function ActiveFloat({rows,openRow,onHide,stopAgent,stopAllAgents,stoppingIds,liveEnabled,onLiveChange}){const [,tick]=React.useState(0),[position,setPosition]=React.useState(null),drag=React.useRef(null);React.useEffect(()=>{const timer=setInterval(()=>tick(v=>v+1),1000);return()=>clearInterval(timer)},[]);const move=e=>{if(!drag.current)return;const {offsetX,offsetY,width,height}=drag.current;setPosition({x:Math.max(8,Math.min(window.innerWidth-width-8,e.clientX-offsetX)),y:Math.max(8,Math.min(window.innerHeight-height-8,e.clientY-offsetY))})},startDrag=e=>{if(e.button!==0||e.target.closest('button,input,label'))return;const rect=e.currentTarget.parentElement.getBoundingClientRect();drag.current={offsetX:e.clientX-rect.left,offsetY:e.clientY-rect.top,width:rect.width,height:rect.height};e.currentTarget.setPointerCapture?.(e.pointerId)},stopDrag=()=>{drag.current=null},dragStyle=position?{left:position.x,top:position.y,right:'auto'}:undefined;const continuableRows=rows.filter(r=>r.mode!=='one-shot');return jsx('aside',{className:'dsh-sam-active-float',style:dragStyle,onPointerMove:move,onPointerUp:stopDrag,onPointerCancel:stopDrag,role:'status',children:[jsxs('div',{className:'dsh-sam-active-head dsh-sam-active-drag-handle',onPointerDown:startDrag,children: [jsx('span',{children:`活跃子代理 · ${rows.length}`}),jsxs('div',{className:'dsh-sam-active-actions',children:[continuableRows.length>0&&jsx('button',{type:'button',className:'dsh-sam-stop-all-btn',title:'一键暂停所有活跃可继续子代理',disabled:continuableRows.some(r=>stoppingIds.has(r.id)),onClick:e=>{e.stopPropagation();stopAllAgents(continuableRows)},children:continuableRows.some(r=>stoppingIds.has(r.id))?'暂停中':'⏸ 一键暂停'}),jsx('label',{className:'dsh-sam-active-live-toggle',children:[jsx('input',{type:'checkbox',checked:liveEnabled,onChange:e=>onLiveChange(e.target.checked)}),'实时输出']}),jsx('button',{type:'button',className:'dsh-sam-active-close',onClick:e=>{e.stopPropagation();onHide()},'aria-label':'隐藏活跃子代理浮窗',children:'×'})]})]}),rows.map(row=>jsxs('div',{className:'dsh-sam-active-item',onClick:()=>openRow(row),children:[jsxs('div',{className:'dsh-sam-active-title',children:[jsx('span',{className:'dsh-sam-active-icon',children:'🧩'}),jsx('span',{className:'dsh-sam-active-name',children:row.name}),jsx('span',{className:'dsh-sam-active-elapsed',children:elapsed(row.createdAt||row.updatedAt)}),row.mode!=='one-shot'&&jsx('button',{type:'button',className:'dsh-sam-stop-btn',disabled:stoppingIds.has(row.id),onClick:e=>{e.stopPropagation();stopAgent(row)},'aria-label':`暂停 ${row.name}`,'title':'暂停子代理',children:stoppingIds.has(row.id)?'暂停中':'⏸ 暂停'})]}),liveEnabled&&jsx(LiveOutput,{parentId:row.parentId,childId:row.id,running:true,prefix:false}),jsx('div',{className:'dsh-sam-active-stats',children:`输入 ${fmt(row.projectionValues?.tokenUsage?.uncachedInputTokens)} / 输出 ${fmt(row.projectionValues?.tokenUsage?.outputTokens)} · 缓存命中 ${row.projectionValues?.tokenUsage?.cacheReadTokens!=null&&tokenTotal(row)?Math.round(row.projectionValues.tokenUsage.cacheReadTokens/tokenTotal(row)*100):'未知'}% · 轮数 ${row.projectionValues?.sessionStats?.turns??row.projectionValues?.turns??'未知'} · 步数 ${row.projectionValues?.sessionStats?.steps??row.projectionValues?.steps??'未知'}`})]},row.id))]})}
|
|
45
46
|
function Manager({useSessions,openChild,openSession,refresh,setCatalogOpen,sessionId}){
|
|
46
47
|
const state=useSessions(s=>s),[open,setOpen]=React.useState(false),[query,setQuery]=React.useState(''),[grouping,setGrouping]=React.useState('session'),[sort,setSort]=React.useState('recent'),[prefs,setPrefs]=React.useState(load),[limit,setLimit]=React.useState(PAGE),[tab,setTab]=React.useState('all'),[scope,setScope]=React.useState('currentWorkspace'),[filterOpen,setFilterOpen]=React.useState(false),[activeOpen,setActiveOpen]=React.useState(true),[scopeDetail,setScopeDetail]=React.useState(''),[sessionScope,setSessionScope]=React.useState(''),[recentIds,setRecentIds]=React.useState([]),[batchMode,setBatchMode]=React.useState(false),[selectedIds,setSelectedIds]=React.useState(new Set),[lastSelectedIndex,setLastSelectedIndex]=React.useState(-1),[lastSelectedAction,setLastSelectedAction]=React.useState(null),[oldHours,setOldHours]=React.useState(null),[newAgents,setNewAgents]=React.useState([]),[floatHidden,setFloatHidden]=React.useState(false),[stoppingIds,setStoppingIds]=React.useState(new Set),[manualLoadedIds,setManualLoadedIds]=React.useState(new Set),runningSeen=React.useRef(false),runningIdsSeen=React.useRef(new Set),noticeTimers=React.useRef(new Map)
|
|
47
48
|
const tabs=[...defaultTabs,...(prefs.tabs||[])].filter(t=>t.id!=='filtered').filter((t,i,a)=>a.findIndex(x=>x.id===t.id)===i), archived=new Set(prefs.archived||[]), archivedParents=prefs.archivedParents||{}, currentId=rootSession(state.byId,sessionId||state.current), current=state.byId[currentId], currentCwd=current?.cwd, parentRows=Object.values(state.byId).filter(s=>s.origin!=='subagent'), scopeOptions=[...new Set(parentRows.map(s=>s.cwd).filter(Boolean))],workspaceKey=scope.startsWith('workspace:')?scope.slice(10):scope==='currentWorkspace'?currentCwd:undefined,availableSessions=parentRows.filter(s=>!workspaceKey||s.cwd===workspaceKey),selectedSession=sessionScope||currentId
|
|
48
49
|
const allRows=React.useMemo(()=>{const modes=modeMap(state.subagentsByParent),rawTerm=query.trim().toLocaleLowerCase(),idOnly=rawTerm.startsWith('id:'),term=idOnly?rawTerm.slice(3).trim():rawTerm;return Object.values(state.byId).filter(s=>s.origin==='subagent').map(s=>({...s,...modes[s.id],name:modes[s.id]?.label||title(s)})).filter(s=>{const parent=s.parentId?state.byId[s.parentId]:undefined;if(workspaceKey&&parent?.cwd!==workspaceKey)return false;if(selectedSession!=='all'&&s.parentId!==selectedSession)return false;const unfiltered=tab==='all';if(!prefs.showArchived&&archived.has(s.id))return false;if(prefs.hideOneShot&&s.mode==='one-shot')return false;if(!unfiltered&&prefs.hideOld&&!s.running&&Date.now()-s.updatedAt>(prefs.oldDays||30)*DAY)return false;const custom=tabs.find(t=>t.id===tab);if(custom?.regex==='__OTHER__'){const matched=tabs.some(t=>!['filtered','all','other'].includes(t.id)&&t.regex&&(()=>{try{return new RegExp(t.regex,'i').test(s.name)}catch{return false}})());if(matched)return false}else if(custom?.regex&&custom.regex!=='__ALL__'){try{if(!new RegExp(custom.regex,'i').test(s.name))return false}catch{return false}}return !term||`${idOnly?`${s.id} ${s.parentId||''}`:`${s.name} ${title(s)} ${workspace(s)}`}`.toLocaleLowerCase().includes(term)}).sort((a,b)=>{const ar=recentIds.indexOf(a.id),br=recentIds.indexOf(b.id);return ar!==br?(ar<0?1:br<0?-1:ar-br):a.running!==b.running?(b.running?1:-1):sort==='title'?a.name.localeCompare(b.name):sort==='type'?modeLabel(a.mode).localeCompare(modeLabel(b.mode)):b.updatedAt-a.updatedAt})},[state,query,sort,prefs,scope,currentCwd,currentId,scopeDetail,workspaceKey,selectedSession,tab,tabs,recentIds])
|
|
49
50
|
const activeRows=React.useMemo(()=>{const modes=modeMap(state.subagentsByParent);return Object.values(state.byId).filter(s=>s.origin==='subagent'&&s.running).map(s=>({...s,...modes[s.id],name:modes[s.id]?.label||title(s)})).filter(s=>{const parent=s.parentId?state.byId[s.parentId]:undefined;return(!currentCwd||parent?.cwd===currentCwd)&&(prefs.showArchived||!archived.has(s.id))&&(!prefs.hideOneShot||s.mode!=='one-shot')}).sort((a,b)=>b.updatedAt-a.updatedAt)},[state,currentCwd,prefs,archived])
|
|
50
|
-
const stopAgent=async row=>{if(row.mode==='one-shot'||stoppingIds.has(row.id))return;if(!window.confirm(`确认暂停子代理“${row.name}”?`))return;setStoppingIds(prev=>new Set(prev).add(row.id));try{const session=sessionsRt?.binding?.(row.id)?.session;if(session){if(row.parentId&&row.mode){session.configureSubagent?.({parentSessionId:row.parentId,childSessionId:row.id,mode:row.mode}
|
|
51
|
-
const stopAllAgents=async rowsToStop=>{const targetRows=rowsToStop.filter(r=>r.mode!=='one-shot'&&!stoppingIds.has(r.id));if(!targetRows.length)return;if(!window.confirm(`确认暂停选中的 ${targetRows.length} 个子代理?`))return;const ids=targetRows.map(r=>r.id);setStoppingIds(prev=>new Set([...prev,...ids]));try{await Promise.allSettled(targetRows.map(async row=>{try{const session=sessionsRt?.binding?.(row.id)?.session;if(session){if(row.parentId&&row.mode){session.configureSubagent?.({parentSessionId:row.parentId,childSessionId:row.id,mode:row.mode}
|
|
51
|
+
const stopAgent=async row=>{if(row.mode==='one-shot'||stoppingIds.has(row.id))return;if(!window.confirm(`确认暂停子代理“${row.name}”?`))return;setStoppingIds(prev=>new Set(prev).add(row.id));try{const session=sessionsRt?.binding?.(row.id)?.session;if(session){if(row.parentId&&row.mode){session.configureSubagent?.({parentSessionId:row.parentId,childSessionId:row.id,mode:row.mode})}await session.open?.();await session.cancel?.();if(row.parentId){sessionsRt?.refreshSubagents?.(row.parentId)}}}catch(e){console.warn('Unable to stop subagent',e)}finally{setStoppingIds(prev=>{const next=new Set(prev);next.delete(row.id);return next})}}
|
|
52
|
+
const stopAllAgents=async rowsToStop=>{const targetRows=rowsToStop.filter(r=>r.mode!=='one-shot'&&!stoppingIds.has(r.id));if(!targetRows.length)return;if(!window.confirm(`确认暂停选中的 ${targetRows.length} 个子代理?`))return;const ids=targetRows.map(r=>r.id);setStoppingIds(prev=>new Set([...prev,...ids]));try{await Promise.allSettled(targetRows.map(async row=>{try{const session=sessionsRt?.binding?.(row.id)?.session;if(session){if(row.parentId&&row.mode){session.configureSubagent?.({parentSessionId:row.parentId,childSessionId:row.id,mode:row.mode})}await session.open?.();await session.cancel?.();if(row.parentId){sessionsRt?.refreshSubagents?.(row.parentId)}}}catch(e){console.warn('Unable to stop subagent',row.id,e)}}))}finally{setStoppingIds(prev=>{const next=new Set(prev);ids.forEach(id=>next.delete(id));return next})}}
|
|
52
53
|
|
|
53
54
|
const deleteSubagent=async row=>{
|
|
54
55
|
if(row.running){
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-subagent-workspace-ui",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Searchable workspace subagent manager for DeepSeek Harness Web",
|
|
6
6
|
"license": "MIT",
|
|
@@ -27,7 +27,6 @@
|
|
|
27
27
|
},
|
|
28
28
|
"client": {
|
|
29
29
|
"inject": [
|
|
30
|
-
"@deepseek-ai/dsh-client-runtime",
|
|
31
30
|
"@deepseek-ai/dsh-client-locale",
|
|
32
31
|
"@deepseek-ai/dsh-client-ui-conversation",
|
|
33
32
|
"@deepseek-ai/dsh-client-ui-sidebar"
|