@sanqianx/claude-ai-workbench 0.13.2 → 0.13.3

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.
Files changed (120) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +156 -156
  3. package/apps/agent-terminal/app.css +636 -613
  4. package/apps/agent-terminal/app.js +125 -65
  5. package/apps/agent-terminal/conversation-composer.js +44 -0
  6. package/apps/agent-terminal/index.html +171 -170
  7. package/apps/agent-terminal/panel-host-proof.html +152 -152
  8. package/apps/agent-terminal/panel-proof-content.html +41 -41
  9. package/apps/agent-terminal/right-workspace-dock.css +187 -159
  10. package/apps/agent-terminal/right-workspace-dock.js +623 -519
  11. package/apps/ai-profile-settings-host/index.html +25 -25
  12. package/apps/ai-profile-settings-host/page.css +1 -1
  13. package/apps/ai-profile-settings-host/page.js +113 -113
  14. package/apps/minimal-test-host/README.md +15 -15
  15. package/apps/minimal-test-host/host.css +2 -2
  16. package/apps/minimal-test-host/host.js +35 -35
  17. package/apps/minimal-test-host/index.html +58 -58
  18. package/apps/minimal-test-host/optional-mock/fault-injection.js +15 -15
  19. package/docs/sdk/FILE_LAYOUT.md +31 -31
  20. package/docs/sdk/INTEGRATION.md +126 -126
  21. package/docs/sdk/README.md +21 -21
  22. package/docs/sdk/SESSION_EVENTS.md +13 -13
  23. package/docs/sdk/openapi.yaml +8 -1
  24. package/package.json +1 -1
  25. package/packages/client/README.md +29 -29
  26. package/packages/client/index.d.ts +122 -122
  27. package/packages/client/index.js +260 -260
  28. package/packages/client/package.json +2 -2
  29. package/packages/contracts/README.md +9 -9
  30. package/packages/contracts/index.d.ts +519 -517
  31. package/packages/contracts/index.js +312 -312
  32. package/packages/contracts/package.json +1 -1
  33. package/packages/core/README.md +8 -8
  34. package/packages/core/index.d.ts +76 -76
  35. package/packages/core/index.js +222 -222
  36. package/packages/core/package.json +2 -2
  37. package/packages/server/README.md +17 -17
  38. package/packages/server/bin/agent-terminal-server.js +42 -42
  39. package/packages/server/bin/claude-workbench.js +225 -225
  40. package/packages/server/index.d.ts +171 -171
  41. package/packages/server/index.js +230 -230
  42. package/packages/server/lib/agent-discovery.js +133 -133
  43. package/packages/server/lib/agent-registry.js +143 -143
  44. package/packages/server/lib/agent-spawn-env.js +17 -17
  45. package/packages/server/lib/attachment-store.js +78 -78
  46. package/packages/server/lib/bridges/app-server-bridge.js +373 -373
  47. package/packages/server/lib/bridges/opencode-server-bridge.js +506 -506
  48. package/packages/server/lib/capture-settings-store.js +35 -35
  49. package/packages/server/lib/claude-history-recovery.js +87 -87
  50. package/packages/server/lib/cli-session-recovery.js +28 -28
  51. package/packages/server/lib/context-resolver.js +55 -55
  52. package/packages/server/lib/conversation-capture.js +51 -51
  53. package/packages/server/lib/delegation-phase-store.js +357 -357
  54. package/packages/server/lib/delegation-store.js +474 -468
  55. package/packages/server/lib/diagnostics.js +32 -32
  56. package/packages/server/lib/drivers/cli-json-runner.js +974 -974
  57. package/packages/server/lib/drivers/codex-driver.js +1442 -1442
  58. package/packages/server/lib/drivers/opencode-driver.js +1554 -1554
  59. package/packages/server/lib/drivers/zcode-driver.js +1250 -1250
  60. package/packages/server/lib/drivers/zcode-profile-runtime.js +71 -71
  61. package/packages/server/lib/event-stream.js +138 -138
  62. package/packages/server/lib/folder-picker.js +623 -623
  63. package/packages/server/lib/http-handler.js +339 -339
  64. package/packages/server/lib/json-file.js +26 -26
  65. package/packages/server/lib/knowledge-context.js +33 -33
  66. package/packages/server/lib/model-detection.js +295 -295
  67. package/packages/server/lib/parent-context.js +44 -0
  68. package/packages/server/lib/profile-store.js +283 -283
  69. package/packages/server/lib/project-store.js +102 -102
  70. package/packages/server/lib/runtime-loader.js +120 -120
  71. package/packages/server/lib/session-archive-store.js +53 -53
  72. package/packages/server/lib/session-history.js +612 -612
  73. package/packages/server/lib/system-doctor.js +125 -125
  74. package/packages/server/lib/ui-preferences-store.js +48 -48
  75. package/packages/server/lib/workbench-service.js +1696 -1675
  76. package/packages/server/package.json +2 -2
  77. package/packages/ui/README.md +30 -30
  78. package/packages/ui/ai-profile-settings.d.ts +45 -45
  79. package/packages/ui/ai-profile-settings.js +283 -283
  80. package/packages/ui/claude-terminal.d.ts +31 -31
  81. package/packages/ui/claude-terminal.js +343 -343
  82. package/packages/ui/index.d.ts +18 -18
  83. package/packages/ui/index.js +51 -51
  84. package/packages/ui/package.json +2 -2
  85. package/packages/ui/theme.css +13 -13
  86. package/src/assets/claude-prompts.json +21 -21
  87. package/src/backend/http/ai-profile-routes.js +61 -61
  88. package/src/backend/http/claude-routes.js +170 -170
  89. package/src/backend/http/node-helpers.js +45 -45
  90. package/src/backend/index.js +21 -21
  91. package/src/backend/lib/ai-adapter.js +144 -144
  92. package/src/backend/lib/ai-profile-store.js +259 -259
  93. package/src/backend/lib/ai-vendor-presets.js +83 -83
  94. package/src/backend/lib/ai-workspace.js +140 -140
  95. package/src/backend/lib/automation-config.js +253 -253
  96. package/src/backend/lib/claude-cli-runner.js +2392 -2392
  97. package/src/backend/lib/data-dir.js +169 -169
  98. package/src/backend/lib/llm-client.js +176 -176
  99. package/src/backend/lib/model-context-windows.js +82 -82
  100. package/src/backend/lib/native-task-control.js +217 -217
  101. package/src/backend/lib/native-task-history-index.js +475 -475
  102. package/src/backend/lib/native-task-history-reader.js +526 -526
  103. package/src/backend/lib/native-task-messages.js +457 -457
  104. package/src/backend/lib/native-task-registry.js +325 -325
  105. package/src/backend/lib/native-task-runtime.js +242 -242
  106. package/src/backend/lib/native-task-trace.js +440 -440
  107. package/src/backend/lib/native-task-transcript.js +393 -393
  108. package/src/backend/lib/prompt-registry.js +166 -166
  109. package/src/backend/lib/runtime-config.js +121 -121
  110. package/src/backend/lib/sdk-session-stream.js +143 -143
  111. package/src/backend/lib/sdk-turn-stream.js +16 -16
  112. package/src/backend/lib/store-scope.js +131 -131
  113. package/src/backend/lib/task-identity.js +104 -104
  114. package/src/backend/lib/task-state-projection.js +58 -58
  115. package/src/index.js +7 -7
  116. package/src/sdk-wave-a.ts +1 -1
  117. package/src/sdk-wave-b.ts +1 -1
  118. package/src/sdk-wave-c.ts +1 -1
  119. package/src/sdk-wave-d.ts +1 -1
  120. package/src/sdk-wave-e.ts +1 -1
@@ -7,9 +7,9 @@
7
7
  (function (global) {
8
8
  'use strict';
9
9
 
10
- const DOCK_MIN_WIDTH = 280;
10
+ const DOCK_MIN_WIDTH = 340;
11
11
  const DOCK_MAX_WIDTH = 560;
12
- const DOCK_DEFAULT_WIDTH = 380;
12
+ const DOCK_DEFAULT_WIDTH = 420;
13
13
  const DOCK_WIDTH_STEP = 24;
14
14
  const DOCK_PAGE_LIMIT = 50;
15
15
  const DOCK_STORAGE_PREFIX = 'kb.workbench.dock:';
@@ -18,21 +18,11 @@
18
18
  const TASK_STATUSES = ['pending', 'running', 'completed', 'failed', 'stopped'];
19
19
  const TERMINAL_STATUSES = ['completed', 'failed', 'stopped'];
20
20
  const STATUS_LABELS = { pending: '待执行', running: '运行中', completed: '已完成', failed: '失败', stopped: '已停止' };
21
- const KIND_LABELS = { agent: '子代理', local_bash: '本地命令' };
22
21
  const RESYNC_CODES = ['FILE_ERA_CHANGED', 'ANCHOR_MISMATCH', 'HISTORY_MUTATED', 'HISTORY_TRUNCATED'];
23
22
  const LIVE_EVENT_TYPES = new Set([
24
23
  'claude/native-task-state', 'claude/native-task-transcript', 'claude/native-trace', 'claude/task-interrupted',
25
24
  'claude/task-started', 'claude/task-progress', 'claude/task-updated', 'claude/task-notification',
26
25
  ]);
27
- // Event types buildRow turns into timeline DOM. The reading anchor is
28
- // restored by scanning rendered rows, so anchor math may only use these.
29
- const RENDERABLE_ROW_TYPES = new Set([
30
- 'claude/native-task-state', 'claude/task-progress', 'claude/native-trace',
31
- 'claude/task-interrupted', 'claude/native-task-transcript',
32
- ]);
33
- const STOP_NOTE = '单任务精确停止 API 未交付(本切片明确禁用);不会以主会话 abort 替代。';
34
- const PICKER_NOTE = '仅列出原生后台任务(SDK 公共读取)。知识 / 浏览器 / 代码评审 provider 未在本切片交付,不显示为可用。';
35
- const PICKER_REGISTRY_NOTE = '原生后台任务来自 SDK 公共读取;可信宿主面板来自宿主注册(同源校验)。未注册 provider 的面板类型不显示为可用。';
36
26
  const NATIVE_PROVIDER_ID = 'workbench.native-subagent';
37
27
 
38
28
  function isId(value) { return typeof value === 'string' && value.trim() !== ''; }
@@ -70,11 +60,6 @@
70
60
  return Math.min(DOCK_MAX_WIDTH, Math.max(DOCK_MIN_WIDTH, n));
71
61
  }
72
62
 
73
- function shortId(value) {
74
- const s = String(value == null ? '' : value);
75
- return s.length > 14 ? s.slice(0, 12) + '…' : s;
76
- }
77
-
78
63
  // Same transition rules as the server index: terminal observations never
79
64
  // revive, and a second conflicting terminal outcome keeps the first.
80
65
  function nextStatus(current, incoming) {
@@ -97,6 +82,11 @@
97
82
  const apiBase = String((deps && deps.apiBase) || '');
98
83
  const storage = deps && deps.storage && typeof deps.storage.getItem === 'function' && typeof deps.storage.setItem === 'function' ? deps.storage : null;
99
84
  const renderMd = deps && typeof deps.renderMarkdown === 'function' ? deps.renderMarkdown : null;
85
+ const renderToolStep = deps && typeof deps.renderToolStep === 'function' ? deps.renderToolStep : null;
86
+ const renderToolResult = deps && typeof deps.renderToolResult === 'function' ? deps.renderToolResult : null;
87
+ const renderMessage = deps && typeof deps.renderMessage === 'function' ? deps.renderMessage : null;
88
+ const renderProcess = deps && typeof deps.renderProcess === 'function' ? deps.renderProcess : null;
89
+ const renderChanges = deps && typeof deps.renderChanges === 'function' ? deps.renderChanges : null;
100
90
  const toast = deps && typeof deps.toast === 'function' ? deps.toast : function () {};
101
91
  const storageKey = DOCK_STORAGE_PREFIX + apiBase;
102
92
  const registry = deps && deps.registry && typeof deps.registry.register === 'function' ? deps.registry : null;
@@ -134,6 +124,7 @@
134
124
  let persistTimer = null;
135
125
  const taskIndex = new Map();
136
126
  const delegationIndex = new Map();
127
+ const delegationTitles = new Map();
137
128
  let delegationState = { loading: false, error: null, code: null };
138
129
  let delegationsBusy = false;
139
130
  let delegationsQueued = false;
@@ -145,23 +136,25 @@
145
136
 
146
137
  function persistedShape() {
147
138
  return {
148
- v: 1, open, width, activeTabId,
139
+ v: 1, open, width, activeTabId, titles: Object.fromEntries(delegationTitles),
149
140
  // Host panel views are transient embed-session state; only native task
150
141
  // views persist per scope.
151
- tabs: tabs.filter(tab => tab.kind !== 'host').map(tab => tab.kind === 'delegated' ? ({
142
+ tabs: tabs.map(tab => ['draft', 'launcher', 'host'].includes(tab.kind) ? ({ id: tab.id, kind: tab.kind, title: tab.title, draft: tab.draft || '', requestId: tab.requestId, command: tab.command, uncertain: tab.uncertain || tab.sending || false, providerId: tab.providerId }) : tab.kind === 'delegated' ? ({
152
143
  id: tab.id,
153
144
  kind: 'delegated',
154
- delegationId: tab.delegationId,
145
+ delegationId: tab.delegationId, draft: tab.draft || '', title: tab.title,
155
146
  anchor: {
156
- follow: !tab.pane || tab.pane.follow !== false,
157
- sequence: tab.pane && Number.isFinite(tab.pane.anchorSeq) ? tab.pane.anchorSeq : null,
147
+ follow: tab.pane ? tab.pane.follow !== false : !tab.restoredAnchor || tab.restoredAnchor.follow !== false,
148
+ sequence: tab.pane ? tab.pane.anchorSeq : tab.restoredAnchor && tab.restoredAnchor.sequence,
149
+ top: tab.pane ? tab.pane.scroll.scrollTop : tab.restoredAnchor && tab.restoredAnchor.top,
158
150
  },
159
151
  }) : ({
160
152
  id: tab.id,
161
153
  taskRef: { ...tab.taskRef },
162
154
  anchor: {
163
- follow: !tab.pane || tab.pane.follow !== false,
164
- sequence: tab.pane && Number.isFinite(tab.pane.anchorSeq) ? tab.pane.anchorSeq : null,
155
+ follow: tab.pane ? tab.pane.follow !== false : !tab.restoredAnchor || tab.restoredAnchor.follow !== false,
156
+ sequence: tab.pane ? tab.pane.anchorSeq : tab.restoredAnchor && tab.restoredAnchor.sequence,
157
+ top: tab.pane ? tab.pane.scroll.scrollTop : tab.restoredAnchor && tab.restoredAnchor.top,
165
158
  },
166
159
  })),
167
160
  };
@@ -176,7 +169,7 @@
176
169
  const key = scopeStorageKey(scope);
177
170
  // A null storeScope bucket can never be re-bound (identity re-binding
178
171
  // restores the real bucket), so it must not linger as a residual entry.
179
- if (scope.storeScope && (tabs.length || open)) scopes[key] = { ...persistedShape(), updatedAt: Date.now() };
172
+ if (scope.storeScope && (tabs.length || open || delegationTitles.size)) scopes[key] = { ...persistedShape(), updatedAt: Date.now() };
180
173
  else delete scopes[key];
181
174
  const kept = Object.entries(scopes).sort((a, b) => (b[1].updatedAt || 0) - (a[1].updatedAt || 0)).slice(0, DOCK_SCOPES_KEPT);
182
175
  storage.setItem(storageKey, JSON.stringify({ v: 1, scopes: Object.fromEntries(kept) }));
@@ -190,6 +183,7 @@
190
183
  }
191
184
 
192
185
  function restoreFor(scopeObj, previousScope = scope) {
186
+ const carriedOpen = open;
193
187
  // A host can register a project-scoped knowledge panel after kb:ready
194
188
  // but before the terminal has opened/restored a session. Attaching that
195
189
  // first native scope must not destroy the already-open host view.
@@ -199,29 +193,40 @@
199
193
  if (!carryHostTabs.length) return;
200
194
  const known = new Set(tabs.map(tab => tab.id));
201
195
  for (const tab of carryHostTabs) if (!known.has(tab.id)) tabs.push(tab);
202
- open = true;
196
+ open = carriedOpen;
203
197
  if (!activeTabId && tabs.length) activeTabId = tabs[0].id;
204
198
  };
205
- tabs = []; activeTabId = null; open = false; width = DOCK_DEFAULT_WIDTH;
199
+ for (const tab of tabs) releaseSubscription(tab);
200
+ tabs = []; activeTabId = null; open = false; width = DOCK_DEFAULT_WIDTH; delegationTitles.clear();
206
201
  if (!scopeObj || !storage) { mergeCarriedHostTabs(); return; }
207
202
  try {
208
203
  const raw = storage.getItem(storageKey);
209
204
  const parsed = raw ? JSON.parse(raw) : null;
210
205
  const saved = parsed && parsed.scopes ? parsed.scopes[scopeStorageKey(scopeObj)] : null;
211
206
  if (!saved || typeof saved !== 'object') { mergeCarriedHostTabs(); return; }
207
+ for (const [id, title] of Object.entries(saved.titles || {})) if (typeof title === 'string') delegationTitles.set(id, title);
212
208
  open = saved.open === true;
213
209
  width = clampWidth(saved.width);
214
210
  const restored = [];
215
211
  for (const item of Array.isArray(saved.tabs) ? saved.tabs : []) {
212
+ if (item && ['draft', 'launcher', 'host'].includes(item.kind)) {
213
+ if (item.kind === 'host' && !isId(item.providerId)) continue;
214
+ restored.push({ id: isId(item.id) ? item.id : nextTabId(), kind: item.kind, providerId: item.providerId,
215
+ descriptor: item.kind === 'host' && registry ? registry.get(item.providerId) : null,
216
+ title: item.title, draft: typeof item.draft === 'string' ? item.draft : '', requestId: item.requestId || newRequestId(),
217
+ command: item.command && item.command.scope && item.command.scope.storeScope === scopeObj.storeScope && item.command.scope.contextId === scopeObj.contextId ? item.command : null,
218
+ uncertain: item.uncertain === true, pane: null, panel: null });
219
+ continue;
220
+ }
216
221
  const ref = sanitizeTaskRef(item && item.taskRef);
217
222
  if (item && item.kind === 'delegated') {
218
223
  if (!isId(item.delegationId)) continue;
219
224
  const dAnchor = item.anchor && typeof item.anchor === 'object' ? item.anchor : {};
220
225
  restored.push({
221
226
  id: isId(item.id) ? item.id : 'tab-' + (++tabSeq),
222
- kind: 'delegated', delegationId: String(item.delegationId).trim(), taskRef: null,
227
+ kind: 'delegated', delegationId: String(item.delegationId).trim(), taskRef: null, draft: typeof item.draft === 'string' ? item.draft : '', title: item.title,
223
228
  invalid: null, panel: null, pane: null, invalidRendered: null,
224
- restoredAnchor: { follow: dAnchor.follow !== false, sequence: Number.isFinite(Number(dAnchor.sequence)) ? Number(dAnchor.sequence) : null },
229
+ restoredAnchor: { follow: dAnchor.follow !== false, sequence: Number.isFinite(Number(dAnchor.sequence)) ? Number(dAnchor.sequence) : null, top: Number.isFinite(dAnchor.top) ? dAnchor.top : null },
225
230
  });
226
231
  continue;
227
232
  }
@@ -230,10 +235,11 @@
230
235
  restored.push({
231
236
  id: isId(item.id) ? item.id : 'tab-' + (++tabSeq),
232
237
  taskRef: ref, invalid: null, panel: null, pane: null, invalidRendered: null,
233
- restoredAnchor: { follow: anchor.follow !== false, sequence: Number.isFinite(Number(anchor.sequence)) ? Number(anchor.sequence) : null },
238
+ restoredAnchor: { follow: anchor.follow !== false, sequence: Number.isFinite(Number(anchor.sequence)) ? Number(anchor.sequence) : null, top: Number.isFinite(anchor.top) ? anchor.top : null },
234
239
  });
235
240
  }
236
241
  tabs = restored;
242
+ if (!tabs.length) open = false;
237
243
  if (saved.activeTabId && tabs.some(tab => tab.id === saved.activeTabId)) activeTabId = saved.activeTabId;
238
244
  else if (tabs.length) activeTabId = tabs[0].id;
239
245
  } catch { /* storage is optional */ }
@@ -245,6 +251,8 @@
245
251
  return {
246
252
  key: ref ? taskRefKey(ref) : 'bare|' + (row.sdkSessionId || '') + '|' + (row.taskId || ''),
247
253
  taskRef: ref,
254
+ description: isId(row.description) ? row.description : isId(row.summary) ? row.summary : null,
255
+ launcherToolUseId: row.owner && row.owner.launcherKind === 'root' && !row.owner.parentNativeAgentId && isId(row.owner.launcherToolUseId) ? row.owner.launcherToolUseId : null,
248
256
  sdkSessionId: isId(row.sdkSessionId) ? row.sdkSessionId : null,
249
257
  taskId: isId(row.taskId) ? row.taskId : null,
250
258
  taskKind: isId(row.taskKind) ? row.taskKind : null,
@@ -269,12 +277,16 @@
269
277
 
270
278
  function upsertIndexEntry(ref, patch) {
271
279
  const key = taskRefKey(ref);
280
+ // A trusted binding replaces the earlier unassigned status echo. Keeping
281
+ // both would leave a phantom running task after the bound task completes.
282
+ for (const [oldKey, old] of taskIndex) if (!old.taskRef && old.taskId === ref.taskId) taskIndex.delete(oldKey);
272
283
  const entry = taskIndex.get(key) || { key, taskRef: ref, sdkSessionId: null, taskId: null, taskKind: null, taskType: null, typeConflict: false, status: null, statusSource: null, association: null, rowCount: null };
273
284
  taskIndex.set(key, { ...entry, ...patch });
274
285
  }
275
286
 
276
287
  function upsertFromStateEvent(ref, event) {
277
288
  const patch = {};
289
+ if (event.owner && event.owner.launcherKind === 'root' && !event.owner.parentNativeAgentId && isId(event.owner.launcherToolUseId)) patch.launcherToolUseId = event.owner.launcherToolUseId;
278
290
  if (TASK_STATUSES.includes(event.status)) { patch.status = nextStatus((taskIndex.get(taskRefKey(ref)) || {}).status, event.status); patch.statusSource = isId(event.statusSource) ? event.statusSource : event.type; }
279
291
  if (isId(event.taskKind)) patch.taskKind = event.taskKind;
280
292
  if (isId(event.taskType)) patch.taskType = event.taskType;
@@ -433,25 +445,6 @@
433
445
  return items;
434
446
  }
435
447
 
436
- const DELEGATION_PHASE_LABELS = {
437
- 'reserved': '已预留', 'child-created': '子会话已创建', 'child-create-failed': '子会话创建失败',
438
- 'dispatching': '派发中', 'dispatched': '已派发', 'dispatch-unknown': '派发结果未知',
439
- };
440
- const DELEGATION_OUTCOME_LABELS = {
441
- 'not-started': '未启动', 'not-dispatched': '未派发', 'pending': '待定', 'dispatched': '已送达', 'unknown': '未知',
442
- };
443
- const DELEGATION_DISPATCH_LABELS = {
444
- 'not-dispatched': '未派发', 'awaiting-dispatch': '等待派发', 'dispatching': '派发中',
445
- 'dispatched': '已派发', 'dispatch-unknown': '派发未知',
446
- };
447
- const CHILD_STATE_LABELS = {
448
- 'created': '已创建', 'spawning': '启动中', 'running': '运行中', 'thinking': '思考中',
449
- 'tool-running': '执行工具', 'pending-permission': '等待授权', 'idle': '空闲',
450
- 'aborted': '已中止', 'failed': '失败', 'missing': '缺失',
451
- };
452
- const DELEGATION_PICKER_NOTE = '委派会话为只读视图:关闭视图不会中止子会话,也不会重新派发输入;停止控件在本切片保持禁用。';
453
- const DELEGATION_STOP_NOTE = '委派子会话的精确 stop 未交付:关闭视图只销毁面板,绝不中止子会话,也不能用父会话 abort 替代。';
454
-
455
448
  function sanitizeDelegationView(view) {
456
449
  if (!view || typeof view !== 'object') return null;
457
450
  const d = view.delegation;
@@ -471,6 +464,7 @@
471
464
  issuedAt: isId(d.issuedAt) ? String(d.issuedAt) : null,
472
465
  phase: isId(view.phase) ? String(view.phase) : 'unknown-phase',
473
466
  outcome: isId(view.outcome) ? String(view.outcome) : null,
467
+ context: view.context && view.context.mode === 'parent-snapshot' ? { mode: 'parent-snapshot', truncated: view.context.truncated === true } : null,
474
468
  dispatch: view.dispatch && typeof view.dispatch === 'object' ? {
475
469
  state: isId(view.dispatch.state) ? String(view.dispatch.state) : null,
476
470
  attemptId: isId(view.dispatch.attemptId) ? String(view.dispatch.attemptId) : null,
@@ -529,10 +523,8 @@
529
523
  return false;
530
524
  }
531
525
 
532
- // Live refresh rides the parent session's own change notifications (the
533
- // dock never subscribes a second SSE per child and never polls on a timer).
534
- // A changed summary for the parent or any known delegated child re-reads
535
- // the trusted delegation list; summaries for anything else are ignored.
526
+ // Parent/child summary changes refresh trusted relations. Only the active
527
+ // child conversation owns its own message stream; there is no timer poll.
536
528
  function notifySessionChanged(summary) {
537
529
  if (!scope || !summary || typeof summary !== 'object') return;
538
530
  const id = isId(summary.sessionId) ? String(summary.sessionId) : null;
@@ -587,6 +579,7 @@
587
579
  }
588
580
  delegationState = { loading: false, error: null, code: null };
589
581
  for (const tab of tabs) {
582
+ if (tab.kind === 'draft' && tab.command) { const recovered = [...delegationIndex.values()].find(view => view.requestId === tab.command.requestId); if (recovered && hasDispatch(recovered)) { acceptCreatedView(tab, { delegation: { ...recovered }, child: recovered.child, phase: recovered.phase, outcome: recovered.outcome, dispatch: recovered.dispatch, context: recovered.context }); } }
590
583
  if (tab.kind !== 'delegated') continue;
591
584
  const fresh = delegationIndex.get(tab.delegationId);
592
585
  if (fresh) {
@@ -612,10 +605,206 @@
612
605
  if (queued) void validateScopeDelegations();
613
606
  }
614
607
 
608
+ function newRequestId() { return 'side-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2); }
609
+ function hasDispatch(view) { return ['dispatched', 'completed', 'dispatching', 'dispatch-unknown'].includes(view.phase); }
610
+
611
+ function releaseSubscription(tab) {
612
+ tab.subscriptionToken = null;
613
+ if (tab.unsubscribe) { tab.unsubscribe(); tab.unsubscribe = null; }
614
+ }
615
+
616
+ function openNewAgent(text = '') {
617
+ if (!scope || !scope.storeScope) { toast('请先打开一个会话。', true); return null; }
618
+ const launcher = tabs.find(tab => tab.id === activeTabId && tab.kind === 'launcher');
619
+ const tab = launcher || { id: nextTabId() };
620
+ if (tab.pane && tab.pane.node) tab.pane.node.remove();
621
+ Object.assign(tab, { kind: 'draft', title: '新子代理', draft: text, requestId: newRequestId(), pane: null, panel: null });
622
+ if (!launcher) tabs.push(tab);
623
+ activeTabId = tab.id; open = true; pickerOpen = false;
624
+ render(); flushPersist();
625
+ return tab.id;
626
+ }
627
+
628
+ function acceptCreatedView(tab, raw) {
629
+ const view = sanitizeDelegationView(raw);
630
+ if (!view || view.requestId !== tab.command.requestId) throw new Error('untrusted child relation');
631
+ delegationIndex.set(view.delegationId, view);
632
+ delegationTitles.set(view.delegationId, tab.command.text.replace(/\s+/g, ' ').slice(0, 60));
633
+ if (tab.pane && tab.pane.node) tab.pane.node.remove();
634
+ Object.assign(tab, { kind: 'delegated', delegationId: view.delegationId, pane: null, panel: null,
635
+ title: tab.command.text.replace(/\s+/g, ' ').slice(0, 60), draft: '', sending: false, uncertain: false });
636
+ if (raw.context && raw.context.truncated) toast('主对话较长,已保留最近的完整消息作为上下文。');
637
+ render(); flushPersist(); notifyBackgroundActivity();
638
+ void loadDelegationFirstPage(tab);
639
+ }
640
+
641
+ async function submitDraft(tab) {
642
+ if (!scope || tab.sending || !String(tab.draft || '').trim()) return;
643
+ const epoch = scopeEpoch, parent = scope.parentSessionId;
644
+ if (!tab.command) tab.command = { ...(typeof deps.getSelection === 'function' ? deps.getSelection() : {}),
645
+ scope: { storeScope: scope.storeScope, contextId: scope.contextId }, requestId: tab.requestId,
646
+ text: tab.draft.trim(), permissionMode: 'default', contextMode: 'parent-snapshot' };
647
+ tab.sending = true; tab.error = null; flushPersist(); render();
648
+ try {
649
+ if (tab.uncertain) {
650
+ const page = await client.listDelegations(parent, tab.command.scope);
651
+ if (epoch !== scopeEpoch) return;
652
+ const existing = page.delegations.find(view => view.delegation && view.delegation.requestId === tab.command.requestId);
653
+ if (existing && hasDispatch(existing)) { acceptCreatedView(tab, existing); return; }
654
+ }
655
+ const result = await client.createDelegation(parent, tab.command);
656
+ if (epoch !== scopeEpoch) return;
657
+ if (!tabs.includes(tab)) { void validateScopeDelegations(); return; }
658
+ acceptCreatedView(tab, result);
659
+ } catch (error) {
660
+ if (epoch !== scopeEpoch || !tabs.includes(tab)) return;
661
+ tab.uncertain = !error.status || error.status >= 500 || ['DELEGATION_CONCURRENCY_LIMIT', 'DELEGATION_CHILD_BUSY'].includes(error.code);
662
+ // A known rejection before reservation can be edited; an uncertain command remains frozen.
663
+ if (!tab.uncertain && error.code !== 'DELEGATION_PAYLOAD_CONFLICT') tab.command = null;
664
+ tab.error = tab.uncertain ? '暂未确认任务是否创建。点击发送会先检查结果,避免重复创建。' : error.code === 'DELEGATION_AGENT_UNSUPPORTED' ? '当前 Agent 暂不支持新建子代理。' : '任务未能创建,内容已保留,请稍后重试。';
665
+ tab.sending = false; render(); flushPersist();
666
+ }
667
+ }
668
+
669
+ function buildComposer(tab) {
670
+ const options = { value: tab.draft || '', placeholder: tab.kind === 'draft' ? '输入一项任务…' : '继续与子代理对话…',
671
+ onInput: value => { tab.draft = value; saveTabDraft(tab); schedulePersist(); },
672
+ onSend: () => { if (tab.kind === 'draft') void submitDraft(tab); else void sendChild(tab); },
673
+ onStop: () => { void stopChild(tab); }, isBusy: () => childBusy(tab), disabled: () => Boolean(tab.sending || tab.uncertain || tab.invalid) };
674
+ if (typeof deps.createComposer === 'function') return deps.createComposer(options);
675
+ const node = el('div', 'composer-wrap rd-composer-wrap'), box = el('div', 'composer'), input = el('textarea'), send = el('button', 'send', '↑');
676
+ input.value = options.value; input.setAttribute('aria-label', options.placeholder);
677
+ input.addEventListener('input', () => options.onInput(input.value));
678
+ send.addEventListener('click', () => options.isBusy() ? options.onStop() : options.onSend());
679
+ box.append(input, send); node.append(box);
680
+ return { node, input, send, setBusy: () => {} };
681
+ }
682
+
683
+ function childBusy(tab) {
684
+ const view = delegationIndex.get(tab.delegationId);
685
+ return Boolean(tab.sending || view && view.child && view.child.active);
686
+ }
687
+
688
+ function saveTabDraft(tab) {
689
+ if (!scope || !storage || !tab.delegationId) return;
690
+ try { storage.setItem(storageKey + ':draft:' + scopeStorageKey(scope) + ':' + tab.delegationId, tab.draft || ''); } catch { /* optional */ }
691
+ }
692
+
693
+ async function sendChild(tab) {
694
+ const view = delegationIndex.get(tab.delegationId), text = String(tab.draft || '').trim();
695
+ if (!view || tab.invalid || childBusy(tab) || !text) return;
696
+ if ((!['dispatched', 'completed'].includes(view.phase)) && !(view.child && view.child.turns > 0)) {
697
+ tab.error = '上一次任务状态仍待确认,请稍后查看结果。'; render(); return;
698
+ }
699
+ const epoch = scopeEpoch;
700
+ tab.sending = true; tab.error = null; render();
701
+ try {
702
+ await client.send(view.childSessionId, text, { permissionMode: 'default' });
703
+ if (epoch !== scopeEpoch || !tabs.includes(tab)) return;
704
+ tab.draft = ''; saveTabDraft(tab); tab.pane.composer.input.value = '';
705
+ void loadDelegationFirstPage(tab); void validateScopeDelegations();
706
+ } catch { if (epoch === scopeEpoch && tabs.includes(tab)) tab.error = '消息暂未发送成功,草稿已保留。请查看对话结果后再决定是否重试。'; }
707
+ finally { if (epoch === scopeEpoch && tabs.includes(tab)) { tab.sending = false; render(); flushPersist(); } }
708
+ }
709
+
710
+ async function stopChild(tab) {
711
+ const view = delegationIndex.get(tab.delegationId);
712
+ if (!view || tab.invalid || tab.sending || !view.child.active) return;
713
+ try { await client.abort(view.childSessionId); } catch { toast('暂时无法停止此子代理,请稍后重试。', true); }
714
+ if (tabs.includes(tab)) void validateScopeDelegations();
715
+ }
716
+
717
+ function subscribeChild(tab) {
718
+ if (!open || tab.id !== activeTabId || typeof client.subscribe !== 'function' || tab.subscriptionToken || tab.invalid || !tab.panel || !tab.panel.loaded) return;
719
+ const view = delegationIndex.get(tab.delegationId);
720
+ if (!view || !view.child.present) return;
721
+ const epoch = scopeEpoch;
722
+ const token = {}; tab.subscriptionToken = token;
723
+ const current = () => epoch === scopeEpoch && tabs.includes(tab) && tab.subscriptionToken === token;
724
+ let opened = false;
725
+ tab.unsubscribe = client.subscribe(view.childSessionId, event => {
726
+ if (!current()) return;
727
+ const currentView = delegationIndex.get(tab.delegationId);
728
+ if (!currentView || currentView.childSessionId !== view.childSessionId) return;
729
+ tab.connectionError = null;
730
+ const panel = ensureDelegationPanelState(tab);
731
+ if (event.type === 'workbench/resync') {
732
+ panel.events = []; panel.olderEvents = []; panel.loadedEarlier = false;
733
+ void loadDelegationFirstPage(tab); return;
734
+ }
735
+ panel.events = mergeDelegationEvents(panel.events, [event]); panel.rendered = false;
736
+ panel.highWater = Math.max(panel.highWater || 0, Number(event.sequence) || 0);
737
+ if (event.type === 'claude/state') { currentView.child.state = event.state; currentView.child.active = ['created', 'spawning', 'running', 'thinking', 'tool-running', 'pending-permission'].includes(event.state); }
738
+ if (event.type === 'claude/result') { currentView.child.active = false; currentView.child.turns = (currentView.child.turns || 0) + 1; }
739
+ render(); notifyBackgroundActivity();
740
+ }, () => { if (current()) { tab.connectionError = '连接暂时中断,正在恢复对话。'; render(); } }, {
741
+ afterSequence: tab.panel.highWater || 0,
742
+ onOpen: () => {
743
+ if (!current()) return;
744
+ tab.connectionError = null;
745
+ if (opened) { void loadDelegationFirstPage(tab); void validateScopeDelegations(); }
746
+ else render();
747
+ opened = true;
748
+ },
749
+ });
750
+ }
751
+
752
+ function buildDraftPane(tab) {
753
+ const node = el('section', 'rd-pane rd-draft-pane');
754
+ const empty = el('div', 'rd-draft-empty');
755
+ empty.append(el('strong', null, '想让子代理处理什么?'), el('p', null, '沿用当前项目,独立开展对话。'));
756
+ const errorBox = el('div', 'rd-error');
757
+ const composer = buildComposer(tab);
758
+ node.append(empty, errorBox, composer.node);
759
+ tab.pane = { node, errorBox, composer };
760
+ }
761
+
762
+ function buildLauncherPane(tab) {
763
+ const node = el('section', 'rd-pane rd-launcher');
764
+ node.append(el('h3', 'rd-picker-heading', '工具'));
765
+ const create = el('button', 'rd-picker-row rd-create-agent', '新建子代理');
766
+ create.disabled = !scope || !scope.storeScope || typeof client.createDelegation !== 'function';
767
+ create.addEventListener('click', () => { openNewAgent(); }); node.append(create);
768
+ const providers = registry ? registry.listAvailable({ projectId: scope ? scope.contextId : null }) : [];
769
+ for (const descriptor of providers) {
770
+ const row = el('button', 'rd-picker-row rd-picker-host-row');
771
+ if (typeof deps.renderPageIcon === 'function') row.append(deps.renderPageIcon('file'));
772
+ row.append(el('span', null, descriptor.title));
773
+ row.addEventListener('click', () => { const id = tab.id; openPanel(descriptor.providerId); closeTab(id); });
774
+ node.append(row);
775
+ }
776
+ tab.pane = { node };
777
+ }
778
+
779
+ function renderPermissions(tab) {
780
+ const pane = tab.pane, view = delegationIndex.get(tab.delegationId);
781
+ if (!pane.permissions || !view) return;
782
+ pane.permissions.textContent = '';
783
+ if (!view.child.active) return;
784
+ const pending = new Map();
785
+ for (const event of tab.panel.events) {
786
+ if (event.type === 'claude/permission-request') pending.set(event.requestId, event);
787
+ if (event.type === 'claude/permission-resolved') pending.delete(event.requestId);
788
+ }
789
+ for (const [requestId, event] of pending) {
790
+ const card = el('div', 'perm'); card.append(el('span', 'p-title', (event.toolName || '子代理') + ' 请求执行操作'));
791
+ for (const allow of [true, false]) {
792
+ const button = el('button', allow ? 'btn-allow' : 'btn-deny', allow ? '允许' : '拒绝');
793
+ button.addEventListener('click', async () => {
794
+ if (tab.invalid || !tabs.includes(tab)) return;
795
+ button.disabled = true;
796
+ try { await client.resolvePermission(view.childSessionId, requestId, allow); void loadDelegationFirstPage(tab); }
797
+ catch { button.disabled = false; toast('授权操作未完成,请重试。', true); }
798
+ }); card.append(button);
799
+ }
800
+ pane.permissions.append(card);
801
+ }
802
+ }
803
+
615
804
  function openDelegation(delegationId) {
616
- if (!scope) { toast('尚未附加会话,无法打开委派视图。', true); return null; }
805
+ if (!scope) { toast('请先打开此任务所属的会话。', true); return null; }
617
806
  const id = typeof delegationId === 'string' ? delegationId.trim() : '';
618
- if (!isId(id)) { toast('无效的 delegationId,未打开委派视图。', true); return null; }
807
+ if (!isId(id)) { toast('暂时无法打开此任务。', true); return null; }
619
808
  const existing = tabs.find(tab => tab.kind === 'delegated' && tab.delegationId === id);
620
809
  if (existing) {
621
810
  activeTabId = existing.id;
@@ -625,9 +814,9 @@
625
814
  return existing.id;
626
815
  }
627
816
  const view = delegationIndex.get(id);
628
- if (!view) { toast('此 delegation 不在当前会话的可信委派关系中,不能打开。', true); return null; }
817
+ if (!view) { toast('此任务不属于当前会话,请返回所属会话查看。', true); return null; }
629
818
  const tab = {
630
- id: nextTabId(), kind: 'delegated', delegationId: id,
819
+ id: nextTabId(), kind: 'delegated', delegationId: id, draft: (() => { try { return storage && storage.getItem(storageKey + ':draft:' + scopeStorageKey(scope) + ':' + id) || ''; } catch { return ''; } })(),
631
820
  taskRef: null, providerId: null, descriptor: null,
632
821
  invalid: null, panel: null, pane: null, invalidRendered: null,
633
822
  };
@@ -686,10 +875,14 @@
686
875
  return;
687
876
  }
688
877
  const events = page && Array.isArray(page.events) ? page.events : [];
689
- panel.events = panel.loadedEarlier ? mergeDelegationEvents(panel.events, events) : events;
878
+ panel.events = mergeDelegationEvents(events, panel.events.filter(event => Number(event.sequence) > Number(page && page.highWater || Infinity)));
879
+ if (panel.loadedEarlier) panel.events = mergeDelegationEvents(panel.olderEvents || [], panel.events);
880
+ panel.highWater = Number(page && page.highWater) || Math.max(0, ...events.map(event => Number(event.sequence) || 0));
881
+ if (!delegationTitles.has(tab.delegationId)) { const prompt = events.find(event => event.type === 'claude/user-prompt' && event.text); if (prompt) delegationTitles.set(tab.delegationId, String(prompt.text).replace(/\s+/g, ' ').slice(0, 60)); }
690
882
  panel.nextCursor = page && page.nextCursor || null;
691
883
  panel.hasMore = isId(panel.nextCursor);
692
884
  panel.loaded = true;
885
+ tab.connectionError = null;
693
886
  panel.rendered = false;
694
887
  panel.lastTurns = view.child && Number.isFinite(Number(view.child.turns)) ? Number(view.child.turns) : null;
695
888
  render();
@@ -721,68 +914,163 @@
721
914
  panel.events = [...older.filter(row => !known.has(delegationRowKey(row))), ...panel.events];
722
915
  panel.nextCursor = page && page.nextCursor || null;
723
916
  panel.hasMore = isId(panel.nextCursor);
724
- panel.loadedEarlier = true;
917
+ panel.loadedEarlier = true; panel.olderEvents = panel.events.slice();
725
918
  panel.rendered = false;
726
919
  render();
727
920
  }
728
921
 
729
- function buildDelegationRow(event) {
730
- if (!event || typeof event !== 'object') return null;
731
- const type = String(event.type || '');
732
- const seq = Number(event.sequence);
733
- const node = el('div', 'rd-item');
734
- if (Number.isFinite(seq) && node.dataset) node.dataset.seq = String(seq);
735
- if (type === 'claude/user-prompt') {
736
- node.className = 'rd-item rd-dmsg user';
737
- node.append(el('span', 'rd-role', '用户'));
738
- const body = el('div', 'rd-text');
739
- body.textContent = isId(event.text) ? String(event.text).slice(0, 4000) : '(空输入)';
740
- node.append(body);
741
- } else if (type === 'claude/text-delta') {
742
- node.className = 'rd-item rd-dmsg assistant';
743
- node.append(el('span', 'rd-role', '助手'));
744
- const body = el('div', 'rd-text');
745
- renderMdInto(body, isId(event.text) ? String(event.text).slice(0, 8000) : '');
746
- node.append(body);
747
- } else if (type === 'claude/thinking-delta') {
748
- node.className = 'rd-item rd-dthink';
749
- node.append(el('span', 'rd-role', '思考'));
750
- const body = el('div', 'rd-text');
751
- body.textContent = isId(event.text) ? String(event.text).slice(0, 2000) : '';
752
- node.append(body);
753
- } else if (type === 'claude/result') {
754
- node.className = 'rd-item rd-dresult';
755
- node.append(el('span', 'rd-state-text', `回复完成${isId(event.model) ? '(' + shortId(event.model) + ')' : ''}`));
756
- if (isId(event.result)) node.append(el('pre', 'rd-pre', String(event.result).slice(0, 2000)));
757
- } else if (type === 'claude/aborted') {
758
- node.className = 'rd-item rd-interrupted';
759
- node.textContent = '子会话回复被中止(aborted)';
760
- } else if (type === 'claude/error') {
761
- node.className = 'rd-item rd-derror';
762
- node.textContent = '子会话错误:' + (isId(event.message) ? String(event.message).slice(0, 300) : isId(event.error) ? String(event.error).slice(0, 300) : '未知错误');
763
- } else if (type === 'claude/session-ready') {
764
- node.className = 'rd-item rd-dstate';
765
- node.textContent = `子会话就绪${isId(event.model) ? '(模型 ' + shortId(event.model) + ')' : ''}`;
766
- } else if (type === 'claude/state') {
767
- node.className = 'rd-item rd-dstate';
768
- node.textContent = '状态 ' + (isId(event.state) ? String(event.state) : '未知');
769
- } else {
770
- return null;
771
- }
922
+ function buildReadOnlyTool(event) {
923
+ if (renderToolStep) return renderToolStep(event, event.input || {});
924
+ const node = el('details', 'step rd-toolstep');
925
+ node.append(el('summary', null, event.name || event.toolName || '工具'));
926
+ const body = el('div', 'step-body');
927
+ body.append(el('pre', 'rd-pre', JSON.stringify(event.input || {}, null, 2)));
928
+ node.append(body);
772
929
  return node;
773
930
  }
774
931
 
932
+ function finishReadOnlyTool(step, event) {
933
+ if (renderToolResult) { renderToolResult(step, event); return; }
934
+ const output = event.output == null ? '' : typeof event.output === 'string' ? event.output : JSON.stringify(event.output, null, 2);
935
+ step.append(el('pre', 'rd-pre', output));
936
+ if (event.isError) step.classList.add('failed');
937
+ }
938
+
939
+ // Normalize transport events into conversation turns. Internal identities,
940
+ // dispatch state and diagnostics never enter customer DOM.
941
+ function createConversation(scroll) {
942
+ let turn = null;
943
+ const tools = new Map();
944
+ function current() {
945
+ if (!turn) turn = { messages: [], changes: [], process: null, live: null, thinking: null };
946
+ return turn;
947
+ }
948
+ function mark(node, sequence) {
949
+ if (Number.isFinite(Number(sequence))) node.dataset.seq = String(sequence);
950
+ return node;
951
+ }
952
+ function message(role, text, sequence) {
953
+ const node = renderMessage ? renderMessage(role, text) : el('div', role === 'user' ? 'msg user' : 'msg ai');
954
+ let body = node.querySelector && node.querySelector(role === 'user' ? '.bubble' : '.md-body');
955
+ if (!body) {
956
+ body = el('div', role === 'user' ? 'bubble' : 'md-body');
957
+ if (role === 'user') body.textContent = text;
958
+ else renderMdInto(body, text);
959
+ node.append(body);
960
+ }
961
+ mark(node, sequence);
962
+ scroll.append(node);
963
+ return { node, body, text };
964
+ }
965
+ function process(sequence) {
966
+ const t = current();
967
+ if (!t.process) {
968
+ if (renderProcess) t.process = renderProcess();
969
+ else {
970
+ const node = el('details', 'proc');
971
+ const meta = el('summary', 'p-meta');
972
+ const steps = el('div', 'proc-steps');
973
+ node.append(meta, steps);
974
+ t.process = { node, meta, steps };
975
+ }
976
+ t.process.node.open = false;
977
+ scroll.append(t.process.node);
978
+ }
979
+ for (const msg of t.messages.splice(0)) {
980
+ if (typeof msg.node.remove === 'function') msg.node.remove();
981
+ t.process.steps.append(msg.node);
982
+ }
983
+ t.live = null;
984
+ mark(t.process.node, sequence);
985
+ return t.process;
986
+ }
987
+ function updateProcess(p) { p.meta.textContent = p.steps.children.length + ' 个步骤'; }
988
+ function finish(sequence) {
989
+ if (!turn) return;
990
+ if (renderChanges && turn.changes.length) {
991
+ const changes = renderChanges(turn.changes);
992
+ if (changes) scroll.append(mark(changes, sequence));
993
+ }
994
+ turn = null;
995
+ }
996
+ function accept(event) {
997
+ if (!event) return;
998
+ const seq = event.sequence, type = event.type;
999
+ if (type === 'claude/user-prompt') {
1000
+ finish(seq);
1001
+ if (event.text) message('user', String(event.text), seq);
1002
+ current();
1003
+ } else if (type === 'claude/text-start') {
1004
+ current().live = null;
1005
+ } else if (type === 'claude/text-delta' || type === 'conversation/assistant') {
1006
+ const text = String(event.text || '');
1007
+ if (!text) return;
1008
+ const t = current();
1009
+ if (type === 'conversation/assistant' || !t.live || t.live.id !== event.id) {
1010
+ t.live = message('assistant', text, seq);
1011
+ t.live.id = event.id;
1012
+ t.messages.push(t.live);
1013
+ } else {
1014
+ t.live.text += text;
1015
+ renderMdInto(t.live.body, t.live.text);
1016
+ mark(t.live.node, seq);
1017
+ }
1018
+ t.thinking = null;
1019
+ } else if (type === 'claude/tool-use') {
1020
+ const p = process(seq);
1021
+ const step = buildReadOnlyTool(event);
1022
+ p.steps.append(step);
1023
+ tools.set(event.id, step);
1024
+ updateProcess(p);
1025
+ current().thinking = null;
1026
+ } else if (type === 'claude/tool-result') {
1027
+ const step = tools.get(event.id);
1028
+ const p = current().process || process(seq);
1029
+ const target = step || buildReadOnlyTool({ id: event.id, name: event.toolName || '工具', input: {} });
1030
+ if (!step) p.steps.append(target);
1031
+ finishReadOnlyTool(target, event);
1032
+ current().changes.push({ ...event, input: target._toolInput || {} });
1033
+ mark(p.node, seq);
1034
+ updateProcess(p);
1035
+ } else if (type === 'claude/thinking-delta') {
1036
+ const t = current();
1037
+ const p = t.process || process(seq);
1038
+ if (!t.thinking) {
1039
+ const node = el('div', 'think-seg');
1040
+ node.append(el('div', 'think-label', '思考'));
1041
+ const body = el('div', 'think-body');
1042
+ node.append(body);
1043
+ p.steps.append(node);
1044
+ t.thinking = body;
1045
+ }
1046
+ t.thinking.textContent += String(event.text || '');
1047
+ mark(p.node, seq);
1048
+ updateProcess(p);
1049
+ } else if (type === 'claude/result') {
1050
+ const t = current();
1051
+ if (event.isError) scroll.append(mark(el('div', 'turn-note', '回复失败,请稍后重试。'), seq));
1052
+ else if (!t.messages.length && event.result) message('assistant', String(event.result), seq);
1053
+ finish(seq);
1054
+ } else if (type === 'conversation/unavailable') {
1055
+ scroll.append(mark(el('div', 'turn-note', '任务记录暂不可用,请刷新重试。'), seq));
1056
+ } else if (type === 'claude/aborted' || type === 'claude/task-interrupted' || type === 'claude/error') {
1057
+ scroll.append(mark(el('div', 'turn-note', type === 'claude/error' ? '回复失败,请稍后重试。' : '回复已中断'), seq));
1058
+ finish(seq);
1059
+ }
1060
+ }
1061
+ return { accept, finish };
1062
+ }
1063
+
775
1064
  function renderDelegationTimeline(tab) {
776
1065
  const pane = tab.pane;
777
1066
  const panel = ensureDelegationPanelState(tab);
778
1067
  if (!pane) return;
779
1068
  const keepScroll = pane.follow === false ? pane.scroll.scrollTop : null;
780
1069
  pane.scroll.textContent = '';
1070
+ const conversation = createConversation(pane.scroll);
781
1071
  const ordered = [...panel.events].sort((a, b) => (Number(a && a.sequence) || 0) - (Number(b && b.sequence) || 0));
782
- for (const event of ordered) {
783
- const node = buildDelegationRow(event);
784
- if (node) pane.scroll.append(node);
785
- }
1072
+ for (const event of ordered) conversation.accept(event);
1073
+ pane.conversation = conversation;
786
1074
  panel.rendered = true;
787
1075
  if (keepScroll !== null) pane.scroll.scrollTop = keepScroll;
788
1076
  else if (pane.follow !== false) pane.scroll.scrollTop = pane.scroll.scrollHeight;
@@ -798,104 +1086,62 @@
798
1086
  return null;
799
1087
  }
800
1088
 
1089
+ function taskTitle(entry) {
1090
+ const title = entry && entry.launcherToolUseId && deps && typeof deps.taskTitle === 'function' ? deps.taskTitle(entry.launcherToolUseId) : null;
1091
+ return String(title || entry && entry.description || '子代理任务').replace(/\s+/g, ' ').slice(0, 60);
1092
+ }
1093
+
1094
+ function delegationTitle(id) {
1095
+ if (delegationTitles.has(id)) return delegationTitles.get(id);
1096
+ const tab = tabs.find(item => item.kind === 'delegated' && item.delegationId === id && item.panel);
1097
+ if (tab && tab.title) return tab.title;
1098
+ const prompt = tab && tab.panel.events.find(event => event.type === 'claude/user-prompt' && event.text);
1099
+ return prompt ? String(prompt.text).replace(/\s+/g, ' ').slice(0, 60) : '子代理任务';
1100
+ }
1101
+
1102
+ function delegationStatus(view) {
1103
+ if (!view) return { status: 'pending', label: '状态待确认' };
1104
+ if (view.phase === 'dispatch-unknown' || view.outcome === 'unknown') return { status: 'pending', label: '状态待确认' };
1105
+ if (view.phase === 'child-create-failed') return { status: 'failed', label: '任务未能启动' };
1106
+ const child = view.child;
1107
+ if (!child.present) return { status: 'failed', label: '任务暂不可用' };
1108
+ if (child.state === 'failed') return { status: 'failed', label: '失败' };
1109
+ if (child.state === 'aborted') return { status: 'stopped', label: '已停止' };
1110
+ if (child.state === 'pending-permission') return { status: 'pending', label: '等待授权' };
1111
+ if (child.active) return { status: 'running', label: '运行中' };
1112
+ if (['dispatched', 'completed'].includes(view.phase) && child.state === 'idle' && child.turns > 0) return { status: 'completed', label: '已完成' };
1113
+ return { status: 'pending', label: '待执行' };
1114
+ }
1115
+
801
1116
  function buildDelegationPane(tab) {
802
- const node = el('section', 'rd-pane rd-delegation-pane');
803
- node.hidden = true;
804
- const head = el('div', 'rd-head');
805
- const title = el('strong', 'rd-title', '委派 ' + shortId(tab.delegationId));
806
- const chips = el('span', 'rd-chips');
807
- head.append(title, chips);
808
- const meta = el('div', 'rd-meta');
809
- const stopRow = el('div', 'rd-stoprow');
810
- const stop = el('button', 'rd-stop');
811
- stop.disabled = true;
812
- stop.textContent = '停止子会话';
813
- stop.title = DELEGATION_STOP_NOTE;
814
- const stopNote = el('span', 'rd-stopnote', 'stop 未交付,禁用;关闭视图不中止子会话。');
815
- stopRow.append(stop, stopNote);
816
- const errorBox = el('div', 'rd-error');
817
- errorBox.hidden = true;
818
- const scroll = el('div', 'rd-scroll');
819
- scroll.addEventListener('scroll', () => {
820
- const pane = tab.pane;
821
- if (!pane) return;
822
- const distance = (scroll.scrollHeight || 0) - (scroll.scrollTop || 0) - (scroll.clientHeight || 0);
823
- const wasFollowing = pane.follow !== false;
824
- pane.follow = distance <= 2;
825
- if (wasFollowing && !pane.follow) pane.anchorSeq = lastDelegationSequenceOf(tab);
826
- schedulePersist();
827
- });
828
- const more = el('button', 'rd-more', '加载更早记录');
829
- more.addEventListener('click', () => { void loadEarlierDelegation(tab); });
830
- const note = el('div', 'rd-note', DELEGATION_PICKER_NOTE);
831
- node.append(head, meta, stopRow, errorBox, scroll, more, note);
832
- tab.pane = { node, head, title, chips, meta, stopRow, errorBox, scroll, more, note,
833
- follow: true, anchorSeq: null, anchorNote: null, pendingNew: false, finalNode: null };
1117
+ buildPane(tab);
1118
+ tab.pane.node.classList.add('rd-delegation-pane');
1119
+ tab.pane.composer = buildComposer(tab);
1120
+ tab.pane.permissions = el('div', 'rd-permissions');
1121
+ tab.pane.node.append(tab.pane.permissions, tab.pane.composer.node);
834
1122
  }
835
1123
 
836
- // Chips and meta mirror the trusted delegation view, never a guess: a
837
- // dispatch-unknown verdict shows as an explicit conflict and is never
838
- // rendered as a success, and a missing child stays a dangling fact.
839
1124
  function refreshDelegationPane(tab) {
840
1125
  const pane = tab.pane;
841
1126
  if (!pane) return;
842
1127
  const panel = ensureDelegationPanelState(tab);
843
1128
  const view = delegationIndex.get(tab.delegationId) || null;
1129
+ pane.title.textContent = delegationTitle(tab.delegationId);
1130
+ pane.status.textContent = delegationStatus(view).label;
1131
+ pane.errorBox.hidden = !tab.invalid && !panel.error;
1132
+ if (tab.invalid || panel.error) pane.errorBox.textContent = '暂时无法查看此任务,请刷新后重试。';
1133
+ pane.banner.hidden = true;
844
1134
  if (tab.invalid) {
845
- if (!tab.invalidRendered) {
846
- pane.chips.textContent = '';
847
- pane.meta.textContent = '';
848
- pane.scroll.textContent = '';
849
- pane.more.hidden = true;
850
- pane.errorBox.hidden = false;
851
- pane.errorBox.textContent = `委派不可用(${tab.invalid.code || '未知'}):${tab.invalid.message || ''}`;
852
- tab.invalidRendered = true;
853
- }
1135
+ pane.scroll.textContent = '';
1136
+ pane.more.hidden = true;
854
1137
  return;
855
1138
  }
856
- tab.invalidRendered = null;
857
- pane.errorBox.hidden = !panel.error;
858
- if (panel.error) pane.errorBox.textContent = panel.error;
859
- pane.chips.textContent = '';
860
- const phase = view ? view.phase : null;
861
- const phaseClass = phase === 'dispatch-unknown' || phase === 'child-create-failed' ? 'conflict' : (phase || 'unknown');
862
- pane.chips.append(el('span', 'rd-chip ' + phaseClass, view ? (DELEGATION_PHASE_LABELS[phase] || phase) : '关系未索引'));
863
- if (view && view.outcome) pane.chips.append(el('span', 'rd-chip kind', DELEGATION_OUTCOME_LABELS[view.outcome] || view.outcome));
864
- if (view) {
865
- const child = view.child;
866
- const childLabel = child.present ? (CHILD_STATE_LABELS[child.state] || child.state || '状态未知') : '子会话缺失(悬空)';
867
- const childClass = child.present ? (child.active ? 'running' : (child.state || 'unknown')) : 'missing';
868
- pane.chips.append(el('span', 'rd-chip ' + childClass, childLabel));
869
- }
870
- // Meta mirrors the latest trusted view instead of a load-time
871
- // snapshot, so dispatch state/attempt/turns stay current on refresh.
872
- pane.meta.textContent = '';
873
- const d = view || { parentSessionId: scope ? scope.parentSessionId : '' };
874
- pane.meta.append(
875
- metaRow('delegationId', tab.delegationId.length > 44 ? shortId(tab.delegationId) : tab.delegationId),
876
- metaRow('childSessionId', d.childSessionId ? shortId(d.childSessionId) : '—'),
877
- metaRow('parentSessionId', d.parentSessionId ? shortId(d.parentSessionId) : '—'),
878
- metaRow('workspaceRef', d.workspaceRef ? shortId(d.workspaceRef) : '—'),
879
- metaRow('requestId', d.requestId ? shortId(d.requestId) : '—'),
880
- metaRow('phase', d.phase ? (DELEGATION_PHASE_LABELS[d.phase] || d.phase) : '—'),
881
- metaRow('outcome', d.outcome ? (DELEGATION_OUTCOME_LABELS[d.outcome] || d.outcome) : '—'),
882
- );
883
- if (view && view.dispatch) {
884
- pane.meta.append(metaRow('dispatch', view.dispatch.state ? (DELEGATION_DISPATCH_LABELS[view.dispatch.state] || view.dispatch.state) : '—'));
885
- if (view.dispatch.attemptId) pane.meta.append(metaRow('attemptId', shortId(view.dispatch.attemptId)));
886
- if (view.dispatch.at) pane.meta.append(metaRow('派发时间', shortId(view.dispatch.at)));
887
- if (view.dispatch.reason) pane.meta.append(metaRow('派发原因', view.dispatch.reason));
888
- if (view.dispatch.code) pane.meta.append(metaRow('派发错误码', view.dispatch.code));
889
- if (view.dispatch.errorCode) pane.meta.append(metaRow('errorCode', view.dispatch.errorCode));
890
- }
891
- if (view && view.child) {
892
- if (view.child.agentId) pane.meta.append(metaRow('agentId', shortId(view.child.agentId)));
893
- if (view.child.model) pane.meta.append(metaRow('model', view.child.model));
894
- if (view.child.turns != null) pane.meta.append(metaRow('turns', view.child.turns));
895
- if (view.child.archived) pane.meta.append(metaRow('归档', '是'));
896
- }
897
- if (view && view.issuedAt) pane.meta.append(metaRow('issuedAt', shortId(view.issuedAt)));
898
1139
  if (!panel.rendered) renderDelegationTimeline(tab);
1140
+ if (tab.error || tab.connectionError) { pane.errorBox.hidden = false; pane.errorBox.textContent = tab.error || tab.connectionError; }
1141
+ if (pane.composer) { pane.composer.setBusy(childBusy(tab)); pane.composer.send.disabled = Boolean(tab.invalid || tab.sending || !view || !view.child.present); }
1142
+ if (pane.composer && view) pane.composer.setLabel?.((view.child.model || '子代理') + ' · 默认防护');
1143
+ renderPermissions(tab);
1144
+ subscribeChild(tab);
899
1145
  pane.more.hidden = !(panel.hasMore && panel.nextCursor);
900
1146
  pane.more.disabled = panel.loading === true;
901
1147
  }
@@ -926,6 +1172,7 @@
926
1172
  }
927
1173
  renderTabBar();
928
1174
  renderToggleState();
1175
+ for (const tab of tabs) if (!open || tab.id !== activeTabId) releaseSubscription(tab);
929
1176
  // Live native state events move task status immediately and with it
930
1177
  // backgroundActivity; notify the host listener on the same beat.
931
1178
  notifyBackgroundActivity();
@@ -963,15 +1210,11 @@
963
1210
  panel.usageSummary = event.usage.summary;
964
1211
  }
965
1212
  if (!panel.loaded || !tab.pane) return true;
966
- const built = buildRow(event, tab);
967
- if (built) {
968
- if (built.textNode && tab.pane.finalNode && tab.pane.finalNode.classList) tab.pane.finalNode.classList.remove('rd-final');
969
- tab.pane.scroll.append(built.node);
970
- if (built.textNode) { built.textNode.classList.add('rd-final'); tab.pane.finalNode = built.textNode; }
971
- }
1213
+ if (!tab.pane.conversation) renderTimeline(tab);
1214
+ else appendNativeConversation(tab.pane.conversation, event);
972
1215
  if (tab.pane.follow !== false) tab.pane.scroll.scrollTop = tab.pane.scroll.scrollHeight;
973
1216
  else tab.pane.pendingNew = true;
974
- refreshFoot(tab);
1217
+
975
1218
  return true;
976
1219
  }
977
1220
 
@@ -1037,6 +1280,7 @@
1037
1280
  flushPersist();
1038
1281
  scopeEpoch += 1;
1039
1282
  scope = null;
1283
+ for (const tab of tabs) releaseSubscription(tab);
1040
1284
  tabs = [];
1041
1285
  activeTabId = null;
1042
1286
  pickerOpen = false;
@@ -1047,10 +1291,19 @@
1047
1291
  notifyPanelsChanged('scope');
1048
1292
  }
1049
1293
 
1050
- function openTask(refInput) {
1294
+ function openTask(refInput, options = {}) {
1051
1295
  const ref = sanitizeTaskRef(refInput);
1052
1296
  if (!ref) { toast('无效的任务引用,未打开面板。', true); return null; }
1053
1297
  if (!scope || !scopeRefMatches(scope, ref)) { toast('该任务不属于当前会话范围;请在所属会话中打开。', true); return null; }
1298
+ const existing = options.reuse && tabs.find(tab => tab.taskRef && taskRefKey(tab.taskRef) === taskRefKey(ref));
1299
+ if (existing) {
1300
+ activeTabId = existing.id;
1301
+ open = true;
1302
+ pickerOpen = false;
1303
+ render();
1304
+ schedulePersist();
1305
+ return existing.id;
1306
+ }
1054
1307
  const tab = {
1055
1308
  id: 'tab-' + Date.now().toString(36) + '-' + (++tabSeq),
1056
1309
  taskRef: ref, invalid: null, panel: null, pane: null, invalidRendered: null,
@@ -1077,10 +1330,10 @@
1077
1330
  }
1078
1331
 
1079
1332
  function openPanel(providerIdInput) {
1080
- if (!registry) { toast('面板 registry 不可用。', true); return null; }
1333
+ if (!registry) { toast('暂时无法打开此内容。', true); return null; }
1081
1334
  const providerId = isId(providerIdInput) ? String(providerIdInput).trim() : '';
1082
1335
  const descriptor = registry.get(providerId);
1083
- if (!descriptor) { toast('未注册的面板 provider,未打开。', true); return null; }
1336
+ if (!descriptor) { toast('此内容当前不可用。', true); return null; }
1084
1337
  if (!providerAppliesToScope(descriptor)) { toast('该面板不适用于当前项目范围。', true); return null; }
1085
1338
  if (descriptor.multiInstance !== 'multiple') {
1086
1339
  const existing = tabs.find(tab => tab.kind === 'host' && tab.providerId === providerId);
@@ -1137,8 +1390,10 @@
1137
1390
  const index = tabs.findIndex(tab => tab.id === tabId);
1138
1391
  if (index === -1) return;
1139
1392
  const [tab] = tabs.splice(index, 1);
1393
+ saveTabDraft(tab); releaseSubscription(tab);
1140
1394
  if (tab.pane && tab.pane.node && typeof tab.pane.node.remove === 'function') tab.pane.node.remove();
1141
1395
  if (activeTabId === tabId) activeTabId = (tabs[index] || tabs[index - 1] || {}).id || null;
1396
+ if (!tabs.length) { open = false; pickerOpen = false; }
1142
1397
  render();
1143
1398
  schedulePersist();
1144
1399
  if (tab.kind === 'host') {
@@ -1150,6 +1405,7 @@
1150
1405
 
1151
1406
  function toggle() {
1152
1407
  open = !open;
1408
+ if (open && !tabs.length) { openPicker(); return open; }
1153
1409
  if (open) pickerOpen = false;
1154
1410
  render();
1155
1411
  schedulePersist();
@@ -1223,13 +1479,23 @@
1223
1479
  root.append(resizer, body);
1224
1480
  }
1225
1481
  if (toggleBtn) toggleBtn.addEventListener('click', () => { toggle(); });
1482
+ if (typeof global.ResizeObserver === 'function' && root && root.parentNode) { const observer = new global.ResizeObserver(applyDockVisibility); observer.observe(root.parentNode); }
1226
1483
  domBound = true;
1227
1484
  }
1228
1485
 
1229
1486
  function applyDockVisibility() {
1230
1487
  if (!root) return;
1231
1488
  root.hidden = !open;
1232
- root.style.width = open ? width + 'px' : '';
1489
+ const parent = root.parentNode;
1490
+ const rect = parent && typeof parent.getBoundingClientRect === 'function' ? parent.getBoundingClientRect() : null;
1491
+ const sidebar = parent && parent.querySelector ? parent.querySelector('.sidebar') : null;
1492
+ const sideWidth = sidebar && sidebar.getBoundingClientRect ? sidebar.getBoundingClientRect().width : 0;
1493
+ const available = rect ? rect.width - sideWidth : 0;
1494
+ const drawer = available > 0 && available < 821;
1495
+ if (root.classList) { if (drawer) root.classList.add('rd-drawer'); else root.classList.remove('rd-drawer'); }
1496
+ const effective = available ? drawer ? Math.min(width, available - 24) : Math.min(width, available - 481) : width;
1497
+ root.style.width = open ? Math.max(0, effective) + 'px' : '';
1498
+ if (root.parentNode && root.parentNode.classList) root.parentNode.classList.toggle('dock-open', open);
1233
1499
  }
1234
1500
 
1235
1501
  function runningCount() {
@@ -1243,7 +1509,7 @@
1243
1509
  if (!toggleBtn) return;
1244
1510
  toggleBtn.hidden = false;
1245
1511
  toggleBtn.setAttribute('aria-expanded', String(open));
1246
- toggleBtn.title = open ? '收起右侧任务面板' : '展开右侧任务面板(后台任务与委派会话)';
1512
+ toggleBtn.title = open ? '收起右侧面板' : '展开右侧面板';
1247
1513
  const badge = toggleBtn.querySelector && toggleBtn.querySelector('.rd-toggle-count');
1248
1514
  if (badge) {
1249
1515
  const running = runningCount();
@@ -1257,68 +1523,87 @@
1257
1523
  function render() {
1258
1524
  ensureDom();
1259
1525
  applyDockVisibility();
1526
+ for (const tab of tabs) if (!open || tab.id !== activeTabId) releaseSubscription(tab);
1260
1527
  renderTabBar();
1261
1528
  renderActivePane();
1262
1529
  renderPicker();
1263
1530
  renderToggleState();
1264
1531
  }
1265
1532
 
1533
+ function collapseButton() {
1534
+ const button = el('button', 'rd-collapse', '收起');
1535
+ button.type = 'button';
1536
+ button.setAttribute('aria-label', '收起任务面板');
1537
+ button.addEventListener('click', () => { toggle(); });
1538
+ return button;
1539
+ }
1540
+
1266
1541
  function renderTabBar() {
1267
1542
  if (!tabBar) return;
1268
1543
  tabBar.textContent = '';
1269
- if (!scope) { tabBar.hidden = true; return; }
1544
+
1270
1545
  tabBar.hidden = false;
1546
+ const tabList = el('div', 'rd-tab-list');
1547
+ tabBar.append(tabList);
1271
1548
  for (const tab of tabs) {
1272
1549
  const entry = tab.taskRef ? taskIndex.get(taskRefKey(tab.taskRef)) : null;
1273
- const status = entry ? entry.status : null;
1274
- const item = el('button', 'rd-tab' + (tab.id === activeTabId ? ' active' : ''));
1275
- const close = el('span', 'rd-tab-close');
1276
- if (tab.kind === 'host') {
1277
- const descriptor = tab.descriptor || (registry ? registry.get(tab.providerId) : null) || { title: tab.providerId, panelType: 'knowledge' };
1550
+ const status = entry && !entry.typeConflict ? entry.status : null;
1551
+ const item = el('div', 'rd-tab' + (tab.id === activeTabId ? ' active' : ''));
1552
+ const tabOpen = el('button', 'rd-tab-open'); tabOpen.setAttribute('role', 'tab'); tabOpen.setAttribute('aria-selected', String(tab.id === activeTabId));
1553
+ const close = el('button', 'rd-tab-close'); close.type = 'button';
1554
+ if (tab.kind === 'draft' || tab.kind === 'launcher') {
1555
+ item.title = tab.kind === 'launcher' ? '新标签页' : '新子代理';
1556
+ close.textContent = '×'; close.setAttribute('aria-label', '关闭' + item.title);
1557
+ close.addEventListener('click', ev => { if (ev && ev.stopPropagation) ev.stopPropagation(); closeTab(tab.id); });
1558
+ tabOpen.append(el('span', 'rd-tab-label', item.title)); item.append(tabOpen, close);
1559
+ } else if (tab.kind === 'host') {
1560
+ const descriptor = tab.descriptor || (registry ? registry.get(tab.providerId) : null) || { title: '参考资料', panelType: 'knowledge' };
1278
1561
  item.title = descriptor.title;
1279
1562
  const dot = el('span', 'rd-dot host');
1280
1563
  const label = el('span', 'rd-tab-label', descriptor.title);
1281
- close.title = '关闭此视图(只销毁面板,不停止任务)';
1564
+ close.title = '关闭';
1282
1565
  close.setAttribute('aria-label', close.title);
1283
1566
  close.textContent = '×';
1284
1567
  close.addEventListener('click', ev => { if (ev && ev.stopPropagation) ev.stopPropagation(); closeTab(tab.id); });
1285
- item.append(dot, label, close);
1568
+ tabOpen.append(dot, label); item.append(tabOpen, close);
1286
1569
  } else if (tab.kind === 'delegated') {
1287
- item.title = tab.delegationId;
1570
+ item.title = delegationTitle(tab.delegationId);
1288
1571
  const entry = delegationIndex.get(tab.delegationId);
1289
- const childState = entry && entry.child && entry.child.present ? entry.child.state : null;
1290
- const dotClass = entry && entry.child && entry.child.active ? 'running' : childState || 'unknown';
1572
+ const dotClass = delegationStatus(entry).status;
1291
1573
  const dot = el('span', 'rd-dot ' + dotClass);
1292
- const label = el('span', 'rd-tab-label', '委派 ' + shortId(tab.delegationId));
1293
- close.title = '关闭此视图(只销毁面板,不停止子会话,不重发输入)';
1574
+ const label = el('span', 'rd-tab-label', delegationTitle(tab.delegationId));
1575
+ close.title = '关闭';
1294
1576
  close.setAttribute('aria-label', close.title);
1295
1577
  close.textContent = '×';
1296
1578
  close.addEventListener('click', ev => { if (ev && ev.stopPropagation) ev.stopPropagation(); closeTab(tab.id); });
1297
- item.append(dot, label, close);
1579
+ tabOpen.append(dot, label); item.append(tabOpen, close);
1298
1580
  } else {
1299
- item.title = tab.taskRef.taskId;
1581
+ item.title = taskTitle(entry);
1300
1582
  const dot = el('span', 'rd-dot ' + (status || 'unknown'));
1301
- const label = el('span', 'rd-tab-label', '#' + shortId(tab.taskRef.taskId));
1302
- close.title = '关闭此视图(只销毁面板,不停止任务)';
1583
+ const label = el('span', 'rd-tab-label', taskTitle(entry));
1584
+ close.title = '关闭';
1303
1585
  close.setAttribute('aria-label', close.title);
1304
1586
  close.textContent = '×';
1305
1587
  close.addEventListener('click', ev => { if (ev && ev.stopPropagation) ev.stopPropagation(); closeTab(tab.id); });
1306
- item.append(dot, label, close);
1588
+ tabOpen.append(dot, label); item.append(tabOpen, close);
1307
1589
  }
1590
+ close.setAttribute('aria-label', '关闭' + item.title);
1308
1591
  item.addEventListener('click', () => {
1309
1592
  activeTabId = tab.id;
1310
1593
  render();
1311
1594
  schedulePersist();
1312
1595
  if (tab.kind === 'host') notifyPanelsChanged('active');
1313
1596
  });
1314
- tabBar.append(item);
1597
+ tabList.append(item);
1315
1598
  }
1316
1599
  const plus = el('button', 'rd-plus');
1317
- plus.title = '选择原生后台任务(同一任务可开多个视图)';
1600
+ plus.title = '新建标签页';
1318
1601
  plus.setAttribute('aria-label', plus.title);
1319
1602
  plus.textContent = '+';
1320
- plus.addEventListener('click', () => { pickerOpen ? closePicker() : openPicker(); });
1321
- tabBar.append(plus);
1603
+ plus.addEventListener('click', () => { openPicker(); });
1604
+ tabBar.append(plus, collapseButton());
1605
+ const selected = tabList.querySelector && tabList.querySelector('[aria-selected="true"]');
1606
+ if (selected && selected.scrollIntoView) selected.scrollIntoView({ block: 'nearest', inline: 'nearest' });
1322
1607
  }
1323
1608
 
1324
1609
  function renderActivePane() {
@@ -1336,24 +1621,33 @@
1336
1621
  tab.pane.node.hidden = tab.id !== activeTabId;
1337
1622
  }
1338
1623
  const activeBeforeScope = tabs.find(tab => tab.id === activeTabId) || tabs[0];
1624
+ if (activeBeforeScope && ['draft', 'launcher'].includes(activeBeforeScope.kind)) {
1625
+ const tab = activeBeforeScope;
1626
+ if (!tab.pane) { if (tab.kind === 'draft') buildDraftPane(tab); else buildLauncherPane(tab); paneHost.append(tab.pane.node); }
1627
+ hideEmptyPane(); tab.pane.node.hidden = false;
1628
+ if (tab.kind === 'draft') { tab.pane.errorBox.hidden = !tab.error; tab.pane.errorBox.textContent = tab.error || ''; tab.pane.composer.setBusy(tab.sending); tab.pane.composer.send.disabled = Boolean(tab.sending); }
1629
+ return;
1630
+ }
1339
1631
  if (!scope && (!activeBeforeScope || activeBeforeScope.kind !== 'host')) {
1340
- renderEmptyPane('尚未附加会话。打开一个会话后,这里显示它的原生后台任务。');
1632
+ renderEmptyPane('打开会话后,可在这里查看子代理任务。');
1341
1633
  return;
1342
1634
  }
1343
1635
  if (!tabs.length) {
1344
- renderEmptyPane(listState.loading ? '正在读取原生任务列表…' : '没有打开的任务视图。点击 + 选择原生后台任务。');
1636
+ renderEmptyPane('点击 + 添加页面。');
1345
1637
  return;
1346
1638
  }
1347
1639
  hideEmptyPane();
1348
1640
  const active = tabs.find(tab => tab.id === activeTabId) || tabs[0];
1349
1641
  if (active && active.id !== activeTabId) activeTabId = active.id;
1350
1642
  if (!active) return;
1643
+ if (active.kind === 'host' && !active.descriptor) { active.descriptor = registry && registry.get(active.providerId); if (!active.descriptor) { renderEmptyPane('正在准备知识引用…'); return; } }
1351
1644
  if (!active.pane) {
1352
1645
  if (active.kind === 'host') buildHostPane(active);
1353
1646
  else if (active.kind === 'delegated') buildDelegationPane(active);
1354
1647
  else buildPane(active);
1355
1648
  paneHost.append(active.pane.node);
1356
1649
  }
1650
+ active.pane.node.hidden = false;
1357
1651
  if (active.kind === 'host') refreshHostPane(active);
1358
1652
  else if (active.kind === 'delegated') refreshDelegationPane(active);
1359
1653
  else refreshPane(active);
@@ -1374,21 +1668,13 @@
1374
1668
  const node = el('section', 'rd-pane');
1375
1669
  node.hidden = true;
1376
1670
  const head = el('div', 'rd-head');
1377
- const title = el('strong', 'rd-title', '#' + tab.taskRef.taskId);
1378
- const chips = el('span', 'rd-chips');
1379
- head.append(title, chips);
1380
- const meta = el('div', 'rd-meta');
1381
- const stopRow = el('div', 'rd-stoprow');
1382
- const stop = el('button', 'rd-stop');
1383
- stop.disabled = true;
1384
- stop.textContent = '停止此任务';
1385
- stop.title = STOP_NOTE;
1386
- const stopNote = el('span', 'rd-stopnote', '精确 stop API 未交付,本切片禁用。');
1387
- stopRow.append(stop, stopNote);
1671
+ const title = el('strong', 'rd-title', '子代理任务');
1672
+ const status = el('span', 'rd-task-status');
1673
+ head.append(el('span', 'rd-thread-note', '关联主对话'), status); title.hidden = true;
1388
1674
  const banner = el('div', 'rd-banner');
1389
1675
  banner.hidden = true;
1390
1676
  const bannerText = el('span', 'rd-banner-text');
1391
- const resyncBtn = el('button', 'rd-resync', '重新同步');
1677
+ const resyncBtn = el('button', 'rd-resync', '刷新');
1392
1678
  resyncBtn.addEventListener('click', () => { void validateScopeTasks(); });
1393
1679
  banner.append(bannerText, resyncBtn);
1394
1680
  const errorBox = el('div', 'rd-error');
@@ -1400,15 +1686,27 @@
1400
1686
  const distance = (scroll.scrollHeight || 0) - (scroll.scrollTop || 0) - (scroll.clientHeight || 0);
1401
1687
  const wasFollowing = pane.follow !== false;
1402
1688
  pane.follow = distance <= 2;
1403
- if (wasFollowing && !pane.follow) pane.anchorSeq = lastSequenceOf(tab);
1689
+ if (wasFollowing && !pane.follow) pane.anchorSeq = tab.kind === 'delegated' ? lastDelegationSequenceOf(tab) : lastSequenceOf(tab);
1404
1690
  schedulePersist();
1405
1691
  });
1406
1692
  const more = el('button', 'rd-more', '加载更早记录');
1407
- more.addEventListener('click', () => { void loadEarlier(tab); });
1408
- const note = el('div', 'rd-note');
1409
- const foot = el('div', 'rd-foot');
1410
- node.append(head, meta, stopRow, banner, errorBox, scroll, more, note, foot);
1411
- tab.pane = { node, head, title, chips, meta, stopRow, banner, bannerText, resyncBtn, errorBox, scroll, more, note, foot, follow: true, anchorSeq: null, anchorNote: null, pendingNew: false, finalNode: null };
1693
+ more.addEventListener('click', () => { void (tab.kind === 'delegated' ? loadEarlierDelegation(tab) : loadEarlier(tab)); });
1694
+ node.append(head, banner, errorBox, scroll, more);
1695
+ if (tab.kind !== 'delegated') {
1696
+ const continuation = el('button', 'rd-continue', '基于此结果继续');
1697
+ continuation.addEventListener('click', () => {
1698
+ const texts = [];
1699
+ for (const event of ensurePanelState(tab).events) for (const entry of event.entries || []) {
1700
+ if (entry.role !== 'assistant') continue;
1701
+ const text = (entry.blocks || []).filter(block => block.kind === 'text' && typeof block.text === 'string').map(block => block.text).join('\n');
1702
+ if (text.trim()) texts.push(text);
1703
+ }
1704
+ openNewAgent('请基于“' + taskTitle(taskIndex.get(taskRefKey(tab.taskRef))) + '”的结果继续:\n\n' + (texts.pop() || ''));
1705
+ });
1706
+ node.append(continuation);
1707
+ }
1708
+ tab.pane = { node, head, title, status, banner, bannerText, resyncBtn, errorBox, scroll, more,
1709
+ follow: true, anchorSeq: null, anchorNote: null, pendingNew: false };
1412
1710
  }
1413
1711
 
1414
1712
  // Host panels are validated iframe descriptors from the registry. The
@@ -1419,17 +1717,13 @@
1419
1717
  const descriptor = tab.descriptor || {};
1420
1718
  const node = el('section', 'rd-pane rd-host-pane');
1421
1719
  node.hidden = true;
1422
- const head = el('div', 'rd-head');
1423
- const title = el('strong', 'rd-title', descriptor.title || tab.providerId);
1424
- const chips = el('span', 'rd-chips');
1425
- head.append(title, chips);
1426
- const status = el('div', 'rd-host-status', '正在加载宿主面板…');
1720
+ const status = el('div', 'rd-host-status', '正在加载…');
1427
1721
  const errorBox = el('div', 'rd-error');
1428
1722
  errorBox.hidden = true;
1429
1723
  const frameHolder = el('div', 'rd-host-frame-holder');
1430
1724
  const frame = el('iframe', 'rd-host-frame');
1431
1725
  frame.setAttribute('src', descriptor.url || '');
1432
- frame.setAttribute('title', descriptor.title || '宿主面板');
1726
+ frame.setAttribute('title', descriptor.title || '知识引用');
1433
1727
  frame.setAttribute('sandbox', 'allow-scripts');
1434
1728
  frame.setAttribute('referrerpolicy', 'no-referrer');
1435
1729
  frame.setAttribute('loading', 'eager');
@@ -1449,12 +1743,12 @@
1449
1743
  tab.pane.error = 'LOAD_FAILED';
1450
1744
  tab.pane.status.hidden = true;
1451
1745
  tab.pane.errorBox.hidden = false;
1452
- tab.pane.errorBox.textContent = '宿主面板加载失败(同源 URL 未成功载入)。';
1746
+ tab.pane.errorBox.textContent = '内容加载失败,请稍后重试。';
1453
1747
  notifyPanelsChanged('error');
1454
1748
  });
1455
1749
  frameHolder.append(frame);
1456
- node.append(head, status, errorBox, frameHolder);
1457
- tab.pane = { node, head, title, chips, status, errorBox, frameHolder, frame, follow: true, anchorSeq: null, anchorNote: null, pendingNew: false, finalNode: null, error: null };
1750
+ node.append(status, errorBox, frameHolder);
1751
+ tab.pane = { node, status, errorBox, frameHolder, frame, follow: true, anchorSeq: null, anchorNote: null, pendingNew: false, finalNode: null, error: null };
1458
1752
  }
1459
1753
 
1460
1754
  function refreshHostPane(tab) {
@@ -1462,12 +1756,9 @@
1462
1756
  if (!pane) return;
1463
1757
  const descriptor = tab.descriptor || (registry ? registry.get(tab.providerId) : null) || {};
1464
1758
  if (!tab.descriptor && descriptor) tab.descriptor = descriptor;
1465
- const typeLabel = registry && registry.panelTypeLabel ? registry.panelTypeLabel(descriptor.panelType) : descriptor.panelType;
1466
- pane.chips.textContent = '';
1467
- pane.chips.append(el('span', 'rd-chip host', typeLabel || '宿主面板'));
1468
- pane.chips.append(el('span', 'rd-chip kind', '宿主注册'));
1469
1759
  pane.errorBox.hidden = !pane.error;
1470
- if (pane.error) pane.errorBox.textContent = pane.error === 'LOAD_FAILED' ? '宿主面板加载失败(同源 URL 未成功载入)。' : pane.error === 'CONTENT_ACK_ERROR' ? '宿主面板报告内容渲染失败。' : String(pane.error);
1760
+ if (pane.error) pane.errorBox.textContent = '内容加载失败,请稍后重试。';
1761
+
1471
1762
  pane.status.hidden = pane.error != null || pane.loaded === true;
1472
1763
  }
1473
1764
 
@@ -1511,6 +1802,7 @@
1511
1802
  if (typeof data.version === 'number' && Number.isFinite(data.version)) version = data.version;
1512
1803
  else if (typeof data.version === 'string' && data.version.trim() && data.version.length <= 200) version = data.version.trim();
1513
1804
  if (version === null) return null;
1805
+ if (tab.pane.contentVersion == null || String(version) !== String(tab.pane.contentVersion)) return null;
1514
1806
  const ok = data.ok === true;
1515
1807
  const error = typeof data.error === 'string' && data.error.trim() ? data.error.slice(0, 300) : null;
1516
1808
  tab.pane.contentAck = { version, ok, error, at: Date.now() };
@@ -1532,19 +1824,7 @@
1532
1824
  const value = Number(children[i] && children[i].dataset ? children[i].dataset.seq : NaN);
1533
1825
  if (Number.isFinite(value)) return value;
1534
1826
  }
1535
- let last = null;
1536
- for (const row of (tab.panel && tab.panel.events) || []) {
1537
- if (!row || typeof row !== 'object' || !RENDERABLE_ROW_TYPES.has(String(row.type || ''))) continue;
1538
- const seq = Number(row.sequence);
1539
- if (Number.isFinite(seq) && (last === null || seq > last)) last = seq;
1540
- }
1541
- return last;
1542
- }
1543
-
1544
- function metaRow(label, value) {
1545
- const row = el('div', 'rd-meta-row');
1546
- row.append(el('span', 'rd-meta-label', label), el('code', 'rd-meta-value', value == null ? '—' : String(value)));
1547
- return row;
1827
+ return null;
1548
1828
  }
1549
1829
 
1550
1830
  function refreshPane(tab) {
@@ -1552,52 +1832,21 @@
1552
1832
  if (!pane) return;
1553
1833
  const panel = ensurePanelState(tab);
1554
1834
  const entry = taskIndex.get(taskRefKey(tab.taskRef)) || null;
1835
+ pane.title.textContent = taskTitle(entry);
1836
+ pane.status.textContent = entry && !entry.typeConflict ? (STATUS_LABELS[entry.status] || '状态待确认') : '状态待确认';
1837
+ pane.errorBox.hidden = !tab.invalid && !panel.error;
1838
+ if (tab.invalid || panel.error) pane.errorBox.textContent = '暂时无法查看此任务,请刷新后重试。';
1839
+ pane.banner.hidden = !panel.resync;
1840
+ if (panel.resync) pane.bannerText.textContent = '记录已更新,请刷新查看。';
1555
1841
  if (tab.invalid) {
1556
- if (!tab.invalidRendered) {
1557
- pane.chips.textContent = '';
1558
- pane.meta.textContent = '';
1559
- pane.scroll.textContent = '';
1560
- pane.foot.textContent = '';
1561
- pane.more.hidden = true;
1562
- pane.banner.hidden = true;
1563
- pane.errorBox.hidden = false;
1564
- pane.errorBox.textContent = `引用不可用(${tab.invalid.code || '未知'}):${tab.invalid.message || ''}`;
1565
- tab.invalidRendered = true;
1566
- }
1842
+ pane.scroll.textContent = '';
1843
+ pane.more.hidden = true;
1844
+ pane.banner.hidden = true;
1567
1845
  return;
1568
1846
  }
1569
- tab.invalidRendered = null;
1570
- pane.errorBox.hidden = !panel.error;
1571
- if (panel.error) pane.errorBox.textContent = panel.error;
1572
- if (panel.resync) {
1573
- pane.banner.hidden = false;
1574
- pane.bannerText.textContent = panel.resync === 'incomplete' ? '任务列表标记为不完整,建议重新同步。' : '检测到任务索引/历史已变更,需要重新同步。';
1575
- } else pane.banner.hidden = true;
1576
- const status = entry ? entry.status : null;
1577
- pane.chips.textContent = '';
1578
- pane.chips.append(el('span', 'rd-chip ' + (status || 'unknown'), status ? STATUS_LABELS[status] : '状态未知'));
1579
- const kindRaw = (entry && (entry.taskKind || entry.taskType)) || null;
1580
- pane.chips.append(el('span', 'rd-chip kind', kindRaw ? (KIND_LABELS[entry.taskKind] || kindRaw) : '种类未知'));
1581
- if (entry && entry.typeConflict) pane.chips.append(el('span', 'rd-chip conflict', '类型冲突'));
1582
- if (entry && entry.association) pane.chips.append(el('span', 'rd-chip assoc', entry.association === 'bound' ? '已绑定' : '未绑定'));
1583
- if (!pane.meta.dataset.filled) {
1584
- const ref = tab.taskRef;
1585
- pane.meta.append(
1586
- metaRow('storeScope', ref.storeScope ? shortId(ref.storeScope) : '—'),
1587
- metaRow('contextId', shortId(ref.contextId)),
1588
- metaRow('parentSessionId', shortId(ref.parentSessionId)),
1589
- metaRow('ownerRunId', shortId(ref.ownerRunId)),
1590
- metaRow('taskId', ref.taskId.length > 44 ? shortId(ref.taskId) : ref.taskId),
1591
- );
1592
- if (entry && entry.sdkSessionId) pane.meta.append(metaRow('sdkSessionId', shortId(entry.sdkSessionId)));
1593
- if (entry && entry.taskType) pane.meta.append(metaRow('taskType', entry.taskType));
1594
- if (entry && Number.isFinite(entry.rowCount)) pane.meta.append(metaRow('索引行数', entry.rowCount));
1595
- pane.meta.dataset.filled = '1';
1596
- }
1597
1847
  if (!panel.rendered) renderTimeline(tab);
1598
1848
  pane.more.hidden = !(panel.hasMore && panel.nextCursor);
1599
1849
  pane.more.disabled = panel.loading === true;
1600
- refreshFoot(tab);
1601
1850
  }
1602
1851
 
1603
1852
  function renderTimeline(tab) {
@@ -1606,61 +1855,36 @@
1606
1855
  if (!pane) return;
1607
1856
  const keepScroll = pane.follow === false ? pane.scroll.scrollTop : null;
1608
1857
  pane.scroll.textContent = '';
1609
- pane.finalNode = null;
1610
- let lastTextNode = null;
1858
+ const conversation = createConversation(pane.scroll);
1611
1859
  const ordered = [...panel.events].sort((a, b) => (Number(a && a.sequence) || 0) - (Number(b && b.sequence) || 0));
1612
- for (const event of ordered) {
1613
- const built = buildRow(event, tab);
1614
- if (!built) continue;
1615
- pane.scroll.append(built.node);
1616
- if (built.textNode) lastTextNode = built.textNode;
1617
- }
1618
- if (lastTextNode && lastTextNode.classList) { lastTextNode.classList.add('rd-final'); pane.finalNode = lastTextNode; }
1860
+ for (const event of ordered) appendNativeConversation(conversation, event);
1861
+ const entry = taskIndex.get(taskRefKey(tab.taskRef));
1862
+ if (entry && TERMINAL_STATUSES.includes(entry.status)) conversation.finish(ordered.length ? ordered[ordered.length - 1].sequence : null);
1863
+ pane.conversation = conversation;
1619
1864
  panel.rendered = true;
1620
1865
  if (keepScroll !== null) pane.scroll.scrollTop = keepScroll;
1621
1866
  else if (pane.follow !== false) pane.scroll.scrollTop = pane.scroll.scrollHeight;
1622
1867
  }
1623
1868
 
1624
- function buildRow(event, tab) {
1625
- if (!event || typeof event !== 'object') return null;
1626
- const type = String(event.type || '');
1627
- if (!RENDERABLE_ROW_TYPES.has(type)) return null;
1628
- let node = null;
1629
- let textNode = null;
1630
- if (type === 'claude/native-task-state') {
1631
- node = el('div', 'rd-item rd-state');
1632
- const status = TASK_STATUSES.includes(event.status) ? event.status : 'unknown';
1633
- node.append(el('span', 'rd-state-dot ' + status));
1634
- node.append(el('span', 'rd-state-text',
1635
- `状态 ${STATUS_LABELS[status] || status}${event.statusSource ? '(来源 ' + shortId(event.statusSource) + ')' : ''}`));
1636
- if (isId(event.taskType)) node.append(el('code', 'rd-dim', event.taskType));
1637
- } else if (type === 'claude/task-progress') {
1638
- node = el('div', 'rd-item rd-progress');
1639
- const status = TASK_STATUSES.includes(event.status) ? event.status : 'unknown';
1640
- node.append(el('span', 'rd-state-dot ' + status));
1641
- node.append(el('span', 'rd-state-text', isId(event.summary) ? String(event.summary).slice(0, 400) : '(进度更新,无摘要)'));
1642
- } else if (type === 'claude/native-trace') {
1643
- node = el('div', 'rd-item rd-trace');
1644
- const head = el('span', 'rd-trace-head', `工具 ${isId(event.toolName) ? event.toolName : '未知'}`);
1645
- node.append(head);
1646
- if (isId(event.nativeAgentId)) node.append(el('code', 'rd-dim', 'agent ' + shortId(event.nativeAgentId)));
1647
- if (isId(event.traceId)) node.append(el('code', 'rd-dim', 'trace ' + shortId(event.traceId)));
1648
- const response = event.toolResponse;
1649
- if (response != null) {
1650
- let preview = '';
1651
- try { preview = typeof response === 'string' ? response : JSON.stringify(response); } catch { preview = String(response); }
1652
- node.append(el('pre', 'rd-pre', preview.slice(0, 2000)));
1869
+ function appendNativeConversation(conversation, event) {
1870
+ if (!event || typeof event !== 'object') return;
1871
+ if (event.type === 'claude/task-interrupted') { conversation.accept(event); return; }
1872
+ if (event.type !== 'claude/native-task-transcript') return;
1873
+ if (!event.read || event.read.status !== 'ok') {
1874
+ conversation.accept({ type: 'conversation/unavailable', sequence: event.sequence });
1875
+ return;
1876
+ }
1877
+ for (const entry of Array.isArray(event.entries) ? event.entries : []) {
1878
+ if (!entry || typeof entry !== 'object') continue;
1879
+ const blocks = Array.isArray(entry.blocks) ? entry.blocks : [];
1880
+ const text = blocks.filter(block => block && block.kind === 'text' && typeof block.text === 'string').map(block => block.text).join('\n');
1881
+ if (text) conversation.accept({ type: entry.role === 'assistant' ? 'conversation/assistant' : 'claude/user-prompt', text, sequence: event.sequence });
1882
+ for (const block of blocks) {
1883
+ if (!block || typeof block !== 'object') continue;
1884
+ if (block.kind === 'tool_use') conversation.accept({ type: 'claude/tool-use', id: block.toolUseId || block.id, name: block.name, input: block.input || {}, sequence: event.sequence });
1885
+ else if (block.kind === 'tool_result') conversation.accept({ type: 'claude/tool-result', id: block.toolUseId || block.tool_use_id, output: (Array.isArray(block.texts) ? block.texts : []).filter(value => typeof value === 'string').join('\n'), isError: block.isError === true, sequence: event.sequence });
1653
1886
  }
1654
- } else if (type === 'claude/task-interrupted') {
1655
- node = el('div', 'rd-item rd-interrupted', '任务被中断(interrupted)');
1656
- } else if (type === 'claude/native-task-transcript') {
1657
- const built = buildTranscriptRow(event);
1658
- node = built.node;
1659
- textNode = built.textNode;
1660
1887
  }
1661
- const seq = Number(event.sequence);
1662
- if (node && Number.isFinite(seq) && node.dataset) node.dataset.seq = String(seq);
1663
- return { node, textNode };
1664
1888
  }
1665
1889
 
1666
1890
  function renderMdInto(node, text) {
@@ -1671,99 +1895,10 @@
1671
1895
  node.textContent = value;
1672
1896
  }
1673
1897
 
1674
- function buildTranscriptRow(event) {
1675
- const node = el('div', 'rd-item rd-transcript');
1676
- let textNode = null;
1677
- const read = event.read;
1678
- if (!read || read.status !== 'ok') {
1679
- node.append(el('div', 'rd-readfail', `任务记录读取${read && read.reason ? '失败:' + read.reason : '不可用'};未渲染任何内容`));
1680
- return { node, textNode: null };
1681
- }
1682
- const entries = Array.isArray(event.entries) ? event.entries : [];
1683
- for (const entry of entries) {
1684
- if (!entry || typeof entry !== 'object') continue;
1685
- const wrap = el('div', 'rd-msg ' + (entry.role === 'assistant' ? 'assistant' : 'user'));
1686
- wrap.append(el('span', 'rd-role', entry.role === 'assistant' ? '助手' : '用户'));
1687
- const blocks = Array.isArray(entry.blocks) ? entry.blocks : [];
1688
- for (const block of blocks) {
1689
- if (!block || typeof block !== 'object') continue;
1690
- if (block.kind === 'text' && typeof block.text === 'string') {
1691
- const p = el('div', 'rd-text');
1692
- renderMdInto(p, block.text);
1693
- wrap.append(p);
1694
- if (entry.role === 'assistant') textNode = p;
1695
- } else if (block.kind === 'tool_use') {
1696
- const step = el('div', 'rd-toolstep');
1697
- step.append(el('span', 'rd-toolname', `工具调用 ${isId(block.name) ? block.name : '未知'}`));
1698
- let dump = '';
1699
- try { dump = JSON.stringify(block.input == null ? {} : block.input, null, 2); } catch { dump = String(block.input); }
1700
- step.append(el('pre', 'rd-pre', dump.slice(0, 4000)));
1701
- wrap.append(step);
1702
- } else if (block.kind === 'tool_result') {
1703
- const res = el('div', 'rd-toolresult' + (block.isError === true ? ' failed' : ''));
1704
- res.append(el('span', 'rd-toolname', block.isError === true ? '工具结果(失败)' : '工具结果'));
1705
- const texts = Array.isArray(block.texts) ? block.texts.filter(t => typeof t === 'string') : [];
1706
- res.append(el('pre', 'rd-pre', texts.join('\n').slice(0, 4000)));
1707
- wrap.append(res);
1708
- }
1709
- }
1710
- node.append(wrap);
1711
- }
1712
- const steps = Array.isArray(event.toolSteps) ? event.toolSteps.length : 0;
1713
- if (steps) node.append(el('div', 'rd-dim', `工具步骤 ${steps}`));
1714
- const chunk = event.chunk;
1715
- if (chunk && (Number(chunk.count) > 1 || chunk.complete === false)) {
1716
- node.append(el('div', 'rd-dim', `分片 ${Number(chunk.index) + 1}/${Number(chunk.count)}${chunk.complete === false ? '(未完)' : ''}`));
1717
- }
1718
- return { node, textNode };
1719
- }
1720
1898
 
1721
- function usageRow(key, value) {
1722
- const line = el('div', 'rd-usage-row');
1723
- line.append(el('span', 'rd-usage-key', String(key)), el('span', 'rd-usage-val', value == null ? '未知' : String(value)));
1724
- return line;
1725
- }
1726
-
1727
- function refreshFoot(tab) {
1728
- const pane = tab.pane;
1729
- if (!pane) return;
1730
- const panel = ensurePanelState(tab);
1731
- pane.foot.textContent = '';
1732
- const usage = panel.usageSummary;
1733
- if (usage && typeof usage === 'object') {
1734
- const box = el('div', 'rd-usage');
1735
- box.append(el('span', 'rd-usage-title', '用量'));
1736
- for (const [key, value] of Object.entries(usage)) {
1737
- if (key === 'totals' || key === 'knownTotals') continue;
1738
- box.append(usageRow(key, value));
1739
- }
1740
- const totals = usage.totals;
1741
- if (totals && typeof totals === 'object') {
1742
- for (const [key, value] of Object.entries(totals)) box.append(usageRow(key, value));
1743
- } else {
1744
- const known = usage.knownTotals;
1745
- if (known && typeof known === 'object' && Object.keys(known).length) {
1746
- const line = el('div', 'rd-usage-row partial');
1747
- const parts = Object.entries(known).map(([k, v]) => `${k}=${v}`);
1748
- line.append(el('span', 'rd-usage-key', '已知部分'), el('span', 'rd-usage-val', parts.join(' / ') + '(总计未知)'));
1749
- box.append(line);
1750
- } else box.append(usageRow('总计', null));
1751
- }
1752
- pane.foot.append(box);
1753
- }
1754
- if (panel.diagnostics.length) {
1755
- const box = el('div', 'rd-diags');
1756
- box.append(el('span', 'rd-usage-title', '诊断'));
1757
- for (const item of panel.diagnostics.slice(0, 8)) {
1758
- box.append(el('code', 'rd-diag', item.code + (item.source ? ' @' + item.source : '')));
1759
- }
1760
- pane.foot.append(box);
1761
- }
1762
- }
1763
1899
 
1764
1900
  function renderAnchorNote(pane, text) {
1765
1901
  pane.anchorNote = text;
1766
- if (pane.note) { pane.note.textContent = text; pane.note.hidden = !text; }
1767
1902
  }
1768
1903
 
1769
1904
  function applyAnchor(tab) {
@@ -1779,6 +1914,7 @@
1779
1914
  renderAnchorNote(pane, '已定位到最新记录');
1780
1915
  return;
1781
1916
  }
1917
+ if (anchor && Number.isFinite(anchor.top)) { pane.scroll.scrollTop = anchor.top; renderAnchorNote(pane, '已恢复阅读位置'); return; }
1782
1918
  if (Number.isFinite(seq)) {
1783
1919
  let matched = null;
1784
1920
  for (const node of Array.from(pane.scroll.children || [])) {
@@ -1796,75 +1932,14 @@
1796
1932
  } else renderAnchorNote(pane, '已恢复读取位置(跟随关闭)');
1797
1933
  }
1798
1934
 
1799
- function openPicker() { pickerOpen = true; renderPicker(); }
1800
-
1801
- function closePicker() { pickerOpen = false; renderPicker(); }
1802
-
1803
- function renderPicker() {
1804
- if (!pickerHost) return;
1805
- pickerHost.hidden = !pickerOpen;
1806
- if (!pickerOpen) return;
1807
- pickerHost.textContent = '';
1808
- const head = el('div', 'rd-picker-head');
1809
- head.append(el('strong', null, '选择原生后台任务'));
1810
- const refresh = el('button', 'rd-picker-refresh', '刷新');
1811
- refresh.addEventListener('click', () => { void validateScopeTasks(); void validateScopeDelegations(); });
1812
- const close = el('button', 'rd-picker-close', '关闭');
1813
- close.addEventListener('click', () => { closePicker(); });
1814
- head.append(refresh, close);
1815
- pickerHost.append(head);
1816
- if (listState.loading) pickerHost.append(el('div', 'rd-picker-empty', '正在读取原生任务列表…'));
1817
- else if (listState.error) pickerHost.append(el('div', 'rd-picker-empty', listState.error));
1818
- else if (!taskIndex.size) pickerHost.append(el('div', 'rd-picker-empty', '当前会话没有原生后台任务记录。'));
1819
- else {
1820
- const rows = Array.from(taskIndex.values())
1821
- .sort((a, b) => String(a.taskId || '').localeCompare(String(b.taskId || '')));
1822
- for (const entry of rows) {
1823
- const row = el('button', 'rd-picker-row');
1824
- row.disabled = !entry.taskRef;
1825
- const status = entry.status || 'unknown';
1826
- let label = `#${shortId(entry.taskId || entry.sdkSessionId)} · ${STATUS_LABELS[status] || '状态未知'}`;
1827
- if (entry.taskKind) label += ` · ${KIND_LABELS[entry.taskKind] || entry.taskKind}`;
1828
- if (!entry.taskRef) label += '(未绑定 taskRef,不可打开)';
1829
- row.textContent = label;
1830
- row.title = entry.taskRef ? entry.taskRef.taskId : label;
1831
- if (entry.taskRef) row.addEventListener('click', () => { openTask(entry.taskRef); });
1832
- pickerHost.append(row);
1833
- }
1834
- }
1835
- if (delegationSupported) {
1836
- pickerHost.append(el('div', 'rd-picker-subhead', '委派会话(可信 delegation 关系)'));
1837
- if (delegationState.loading) pickerHost.append(el('div', 'rd-picker-empty', '正在读取委派会话…'));
1838
- else if (delegationState.error) pickerHost.append(el('div', 'rd-picker-empty', delegationState.error));
1839
- else if (!delegationIndex.size) pickerHost.append(el('div', 'rd-picker-empty', '当前会话没有委派记录。'));
1840
- else {
1841
- const dRows = Array.from(delegationIndex.values())
1842
- .sort((a, b) => String(a.issuedAt || '').localeCompare(String(b.issuedAt || '')));
1843
- for (const entry of dRows) {
1844
- const row = el('button', 'rd-picker-row rd-picker-delegation-row');
1845
- const childLabel = entry.child.present ? (CHILD_STATE_LABELS[entry.child.state] || entry.child.state || '状态未知') : '子会话缺失(悬空)';
1846
- row.textContent = `委派 ${shortId(entry.delegationId)} · ${DELEGATION_PHASE_LABELS[entry.phase] || entry.phase || '阶段未知'} · ${childLabel}`;
1847
- row.title = entry.delegationId;
1848
- row.addEventListener('click', () => { openDelegation(entry.delegationId); });
1849
- pickerHost.append(row);
1850
- }
1851
- }
1852
- pickerHost.append(el('div', 'rd-picker-note', DELEGATION_PICKER_NOTE));
1853
- }
1854
- const hostProviders = registry ? registry.listAvailable({ projectId: scope ? scope.contextId : null }) : [];
1855
- if (hostProviders.length) {
1856
- pickerHost.append(el('div', 'rd-picker-subhead', '可信宿主面板(已注册)'));
1857
- for (const descriptor of hostProviders) {
1858
- const row = el('button', 'rd-picker-row rd-picker-host-row');
1859
- const typeLabel = registry.panelTypeLabel ? registry.panelTypeLabel(descriptor.panelType) : descriptor.panelType;
1860
- row.textContent = `${descriptor.title} · ${typeLabel || descriptor.panelType}${descriptor.multiInstance === 'multiple' ? ' · 可多实例' : ''}`;
1861
- row.title = descriptor.providerId;
1862
- row.addEventListener('click', () => { openPanel(descriptor.providerId); });
1863
- pickerHost.append(row);
1864
- }
1865
- }
1866
- pickerHost.append(el('div', 'rd-picker-note', registry ? PICKER_REGISTRY_NOTE : PICKER_NOTE));
1935
+ function openPicker() {
1936
+ let tab = tabs.find(item => item.kind === 'launcher');
1937
+ if (!tab) { tab = { id: nextTabId(), kind: 'launcher', pane: null }; tabs.push(tab); }
1938
+ else if (tab.pane) { tab.pane.node.remove(); tab.pane = null; }
1939
+ activeTabId = tab.id; open = true; pickerOpen = true; render(); schedulePersist();
1867
1940
  }
1941
+ function closePicker() { pickerOpen = false; }
1942
+ function renderPicker() { if (pickerHost) pickerHost.hidden = true; }
1868
1943
 
1869
1944
  function snapshot() {
1870
1945
  return {
@@ -1885,6 +1960,7 @@
1885
1960
  providerId: tab.kind === 'host' ? tab.providerId : null,
1886
1961
  panelType: tab.kind === 'host' && tab.descriptor ? tab.descriptor.panelType : null,
1887
1962
  hostError: tab.pane && tab.pane.error ? tab.pane.error : null,
1963
+ hostReady: Boolean(tab.kind === 'host' && tab.pane && tab.pane.loaded),
1888
1964
  delegation: tab.kind === 'delegated' ? delegationSnapshotOf(tab) : null,
1889
1965
  content: tab.kind === 'host' && tab.pane ? {
1890
1966
  version: tab.pane.contentVersion !== undefined ? tab.pane.contentVersion : null,
@@ -1909,12 +1985,40 @@
1909
1985
  return Array.from(tab.pane.scroll.children || []).map(node => String(node.textContent || '')).join('\n');
1910
1986
  }
1911
1987
 
1912
- function pickerText() { return pickerHost ? String(pickerHost.textContent || '') : ''; }
1988
+ function pickerText() { const tab = tabs.find(item => item.kind === 'launcher'); return tab && tab.pane ? String(tab.pane.node.textContent || '') : ''; }
1989
+
1990
+ function taskViews(parentSessionId) {
1991
+ if (parentSessionId !== undefined && (!scope || scope.parentSessionId !== parentSessionId)) return [];
1992
+ const native = [...taskIndex.values()].filter(entry => entry.taskRef && scopeRefMatches(scope, entry.taskRef)).map(entry => ({
1993
+ taskRef: { ...entry.taskRef }, taskId: entry.taskId, status: entry.status,
1994
+ description: taskTitle(entry),
1995
+ }));
1996
+ const delegated = [...delegationIndex.values()].map(view => ({
1997
+ delegationId: view.delegationId, taskId: view.childSessionId,
1998
+ description: delegationTitle(view.delegationId),
1999
+ status: delegationStatus(view).status, statusLabel: delegationStatus(view).label,
2000
+ }));
2001
+ return [...native, ...delegated];
2002
+ }
2003
+
2004
+ async function openTaskForToolUseId(toolUseId) {
2005
+ if (!isId(toolUseId) || !scope) return null;
2006
+ const epoch = scopeEpoch;
2007
+ await validateScopeTasks();
2008
+ if (epoch !== scopeEpoch) return null;
2009
+ const matches = [...taskIndex.values()].filter(entry => entry.launcherToolUseId === toolUseId && entry.taskRef && scopeRefMatches(scope, entry.taskRef));
2010
+ if (matches.length !== 1) {
2011
+ toast('此子代理暂未提供可读取的独立任务记录。', true);
2012
+ return null;
2013
+ }
2014
+ return openTask(matches[0].taskRef, { reuse: true });
2015
+ }
1913
2016
 
1914
2017
  const api = {
1915
2018
  attachSession, detachSession, handleSessionEvent, handleResync,
1916
2019
  refreshIdentity,
1917
- openTask, openDelegation, notifySessionChanged, backgroundActivity,
2020
+ refreshProviders() { for (const tab of tabs) if (tab.kind === 'launcher' && tab.pane) { tab.pane.node.remove(); tab.pane = null; } render(); },
2021
+ openTask, openTaskForToolUseId, taskViews, openNewAgent, openDelegation, notifySessionChanged, backgroundActivity,
1918
2022
  closeTab, toggle, setWidth, adjustWidth,
1919
2023
  openPanel, closeHostPanels, activateHostPanel,
1920
2024
  relayHostPanelContent, ackHostPanelContent,