@sanqianx/claude-ai-workbench 0.13.2 → 0.13.4

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 (122) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +156 -156
  3. package/apps/agent-terminal/app.css +652 -613
  4. package/apps/agent-terminal/app.js +143 -67
  5. package/apps/agent-terminal/conversation-composer.js +44 -0
  6. package/apps/agent-terminal/index.html +172 -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 +189 -159
  10. package/apps/agent-terminal/right-workspace-dock.js +652 -521
  11. package/apps/agent-terminal/user-question-card.js +68 -0
  12. package/apps/ai-profile-settings-host/index.html +25 -25
  13. package/apps/ai-profile-settings-host/page.css +1 -1
  14. package/apps/ai-profile-settings-host/page.js +113 -113
  15. package/apps/minimal-test-host/README.md +15 -15
  16. package/apps/minimal-test-host/host.css +2 -2
  17. package/apps/minimal-test-host/host.js +35 -35
  18. package/apps/minimal-test-host/index.html +58 -58
  19. package/apps/minimal-test-host/optional-mock/fault-injection.js +15 -15
  20. package/docs/sdk/FILE_LAYOUT.md +31 -31
  21. package/docs/sdk/INTEGRATION.md +126 -126
  22. package/docs/sdk/README.md +21 -21
  23. package/docs/sdk/SESSION_EVENTS.md +15 -13
  24. package/docs/sdk/openapi.yaml +16 -1
  25. package/package.json +1 -1
  26. package/packages/client/README.md +29 -29
  27. package/packages/client/index.d.ts +122 -122
  28. package/packages/client/index.js +260 -260
  29. package/packages/client/package.json +2 -2
  30. package/packages/contracts/README.md +9 -9
  31. package/packages/contracts/index.d.ts +519 -517
  32. package/packages/contracts/index.js +312 -312
  33. package/packages/contracts/package.json +1 -1
  34. package/packages/core/README.md +8 -8
  35. package/packages/core/index.d.ts +76 -76
  36. package/packages/core/index.js +222 -222
  37. package/packages/core/package.json +2 -2
  38. package/packages/server/README.md +17 -17
  39. package/packages/server/bin/agent-terminal-server.js +42 -42
  40. package/packages/server/bin/claude-workbench.js +225 -225
  41. package/packages/server/index.d.ts +171 -171
  42. package/packages/server/index.js +230 -230
  43. package/packages/server/lib/agent-discovery.js +133 -133
  44. package/packages/server/lib/agent-registry.js +143 -143
  45. package/packages/server/lib/agent-spawn-env.js +17 -17
  46. package/packages/server/lib/attachment-store.js +78 -78
  47. package/packages/server/lib/bridges/app-server-bridge.js +373 -373
  48. package/packages/server/lib/bridges/opencode-server-bridge.js +506 -506
  49. package/packages/server/lib/capture-settings-store.js +35 -35
  50. package/packages/server/lib/claude-history-recovery.js +87 -87
  51. package/packages/server/lib/cli-session-recovery.js +28 -28
  52. package/packages/server/lib/context-resolver.js +55 -55
  53. package/packages/server/lib/conversation-capture.js +51 -51
  54. package/packages/server/lib/delegation-phase-store.js +357 -357
  55. package/packages/server/lib/delegation-store.js +474 -468
  56. package/packages/server/lib/diagnostics.js +32 -32
  57. package/packages/server/lib/drivers/cli-json-runner.js +974 -974
  58. package/packages/server/lib/drivers/codex-driver.js +1461 -1442
  59. package/packages/server/lib/drivers/opencode-driver.js +1554 -1554
  60. package/packages/server/lib/drivers/zcode-driver.js +1250 -1250
  61. package/packages/server/lib/drivers/zcode-profile-runtime.js +71 -71
  62. package/packages/server/lib/event-stream.js +138 -138
  63. package/packages/server/lib/folder-picker.js +623 -623
  64. package/packages/server/lib/http-handler.js +339 -339
  65. package/packages/server/lib/json-file.js +26 -26
  66. package/packages/server/lib/knowledge-context.js +33 -33
  67. package/packages/server/lib/model-detection.js +295 -295
  68. package/packages/server/lib/parent-context.js +44 -0
  69. package/packages/server/lib/profile-store.js +283 -283
  70. package/packages/server/lib/project-store.js +102 -102
  71. package/packages/server/lib/runtime-loader.js +120 -120
  72. package/packages/server/lib/session-archive-store.js +53 -53
  73. package/packages/server/lib/session-history.js +612 -612
  74. package/packages/server/lib/system-doctor.js +125 -125
  75. package/packages/server/lib/ui-preferences-store.js +48 -48
  76. package/packages/server/lib/user-questions.js +67 -0
  77. package/packages/server/lib/workbench-service.js +1696 -1675
  78. package/packages/server/package.json +2 -2
  79. package/packages/ui/README.md +30 -30
  80. package/packages/ui/ai-profile-settings.d.ts +45 -45
  81. package/packages/ui/ai-profile-settings.js +283 -283
  82. package/packages/ui/claude-terminal.d.ts +31 -31
  83. package/packages/ui/claude-terminal.js +343 -343
  84. package/packages/ui/index.d.ts +18 -18
  85. package/packages/ui/index.js +51 -51
  86. package/packages/ui/package.json +2 -2
  87. package/packages/ui/theme.css +13 -13
  88. package/src/assets/claude-prompts.json +21 -21
  89. package/src/backend/http/ai-profile-routes.js +61 -61
  90. package/src/backend/http/claude-routes.js +170 -170
  91. package/src/backend/http/node-helpers.js +45 -45
  92. package/src/backend/index.js +21 -21
  93. package/src/backend/lib/ai-adapter.js +144 -144
  94. package/src/backend/lib/ai-profile-store.js +259 -259
  95. package/src/backend/lib/ai-vendor-presets.js +83 -83
  96. package/src/backend/lib/ai-workspace.js +140 -140
  97. package/src/backend/lib/automation-config.js +253 -253
  98. package/src/backend/lib/claude-cli-runner.js +2408 -2392
  99. package/src/backend/lib/data-dir.js +169 -169
  100. package/src/backend/lib/llm-client.js +176 -176
  101. package/src/backend/lib/model-context-windows.js +82 -82
  102. package/src/backend/lib/native-task-control.js +217 -217
  103. package/src/backend/lib/native-task-history-index.js +475 -475
  104. package/src/backend/lib/native-task-history-reader.js +526 -526
  105. package/src/backend/lib/native-task-messages.js +457 -457
  106. package/src/backend/lib/native-task-registry.js +325 -325
  107. package/src/backend/lib/native-task-runtime.js +242 -242
  108. package/src/backend/lib/native-task-trace.js +440 -440
  109. package/src/backend/lib/native-task-transcript.js +393 -393
  110. package/src/backend/lib/prompt-registry.js +166 -166
  111. package/src/backend/lib/runtime-config.js +121 -121
  112. package/src/backend/lib/sdk-session-stream.js +143 -143
  113. package/src/backend/lib/sdk-turn-stream.js +16 -16
  114. package/src/backend/lib/store-scope.js +131 -131
  115. package/src/backend/lib/task-identity.js +104 -104
  116. package/src/backend/lib/task-state-projection.js +58 -58
  117. package/src/index.js +7 -7
  118. package/src/sdk-wave-a.ts +1 -1
  119. package/src/sdk-wave-b.ts +1 -1
  120. package/src/sdk-wave-c.ts +1 -1
  121. package/src/sdk-wave-d.ts +1 -1
  122. 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,228 @@
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
+ if (event.type === 'claude/permission-request') { panel.currentPermission = event; panel.permissionRevision = (panel.permissionRevision || 0) + 1; }
737
+ if (event.type === 'claude/permission-resolved') { if (panel.currentPermission?.requestId === event.requestId) panel.currentPermission = null; panel.permissionRevision = (panel.permissionRevision || 0) + 1; }
738
+ panel.highWater = Math.max(panel.highWater || 0, Number(event.sequence) || 0);
739
+ if (event.type === 'claude/state') { currentView.child.state = event.state; currentView.child.active = ['created', 'spawning', 'running', 'thinking', 'tool-running', 'pending-permission'].includes(event.state); }
740
+ if (event.type === 'claude/result') { currentView.child.active = false; currentView.child.turns = (currentView.child.turns || 0) + 1; }
741
+ render(); notifyBackgroundActivity();
742
+ }, () => { if (current()) { tab.connectionError = '连接暂时中断,正在恢复对话。'; render(); } }, {
743
+ afterSequence: tab.panel.highWater || 0,
744
+ onOpen: () => {
745
+ if (!current()) return;
746
+ tab.connectionError = null;
747
+ if (opened) { void loadDelegationFirstPage(tab); void validateScopeDelegations(); }
748
+ else render();
749
+ opened = true;
750
+ },
751
+ });
752
+ }
753
+
754
+ function buildDraftPane(tab) {
755
+ const node = el('section', 'rd-pane rd-draft-pane');
756
+ const empty = el('div', 'rd-draft-empty');
757
+ empty.append(el('strong', null, '想让子代理处理什么?'), el('p', null, '沿用当前项目,独立开展对话。'));
758
+ const errorBox = el('div', 'rd-error');
759
+ const composer = buildComposer(tab);
760
+ node.append(empty, errorBox, composer.node);
761
+ tab.pane = { node, errorBox, composer };
762
+ }
763
+
764
+ function buildLauncherPane(tab) {
765
+ const node = el('section', 'rd-pane rd-launcher');
766
+ node.append(el('h3', 'rd-picker-heading', '工具'));
767
+ const create = el('button', 'rd-picker-row rd-create-agent', '新建子代理');
768
+ create.disabled = !scope || !scope.storeScope || typeof client.createDelegation !== 'function';
769
+ create.addEventListener('click', () => { openNewAgent(); }); node.append(create);
770
+ const providers = registry ? registry.listAvailable({ projectId: scope ? scope.contextId : null }) : [];
771
+ for (const descriptor of providers) {
772
+ const row = el('button', 'rd-picker-row rd-picker-host-row');
773
+ if (typeof deps.renderPageIcon === 'function') row.append(deps.renderPageIcon('file'));
774
+ row.append(el('span', null, descriptor.title));
775
+ row.addEventListener('click', () => { const id = tab.id; openPanel(descriptor.providerId); closeTab(id); });
776
+ node.append(row);
777
+ }
778
+ tab.pane = { node };
779
+ }
780
+
781
+ function renderPermissions(tab) {
782
+ const pane = tab.pane, view = delegationIndex.get(tab.delegationId);
783
+ if (!pane.permissions || !view) return;
784
+ pane.permissions.textContent = '';
785
+ if (!view.child.active) return;
786
+ const pending = new Map();
787
+ for (const event of tab.panel.events) {
788
+ if (event.type === 'claude/permission-request') pending.set(event.requestId, event);
789
+ if (event.type === 'claude/permission-resolved') pending.delete(event.requestId);
790
+ }
791
+ if (tab.panel.currentPermission !== undefined) {
792
+ for (const [id, event] of pending) if (event.kind === 'question' && id !== tab.panel.currentPermission?.requestId) pending.delete(id);
793
+ if (tab.panel.currentPermission?.kind === 'question') pending.set(tab.panel.currentPermission.requestId, tab.panel.currentPermission);
794
+ }
795
+ for (const [id, card] of pane.questionCards || []) if (!pending.has(id)) { card.settle({ cancelled: true }); pane.questionCards.delete(id); }
796
+ for (const [requestId, event] of pending) {
797
+ if (event.kind === 'question' && typeof global.createUserQuestionCard === 'function') {
798
+ pane.questionCards ||= new Map();
799
+ let card = pane.questionCards.get(requestId);
800
+ if (!card) {
801
+ const childId = view.childSessionId;
802
+ card = global.createUserQuestionCard({ document: doc, event,
803
+ storageKey: `kb.workbench.questions:${deps.apiBase || ''}:${childId}:${requestId}`,
804
+ onSubmit: async answers => {
805
+ if (tab.invalid || !tabs.includes(tab)) throw new Error('任务已关闭');
806
+ await client.resolvePermission({ sessionId: childId, requestId, allow: true, answers });
807
+ },
808
+ }); pane.questionCards.set(requestId, card);
809
+ }
810
+ pane.permissions.append(card); continue;
811
+ }
812
+ const card = el('div', 'perm'); card.append(el('span', 'p-title', (event.toolName || '子代理') + ' 请求执行操作'));
813
+ for (const allow of [true, false]) {
814
+ const button = el('button', allow ? 'btn-allow' : 'btn-deny', allow ? '允许' : '拒绝');
815
+ button.addEventListener('click', async () => {
816
+ if (tab.invalid || !tabs.includes(tab)) return;
817
+ button.disabled = true;
818
+ try { await client.resolvePermission(view.childSessionId, requestId, allow); void loadDelegationFirstPage(tab); }
819
+ catch { button.disabled = false; toast('授权操作未完成,请重试。', true); }
820
+ }); card.append(button);
821
+ }
822
+ pane.permissions.append(card);
823
+ }
824
+ }
825
+
615
826
  function openDelegation(delegationId) {
616
- if (!scope) { toast('尚未附加会话,无法打开委派视图。', true); return null; }
827
+ if (!scope) { toast('请先打开此任务所属的会话。', true); return null; }
617
828
  const id = typeof delegationId === 'string' ? delegationId.trim() : '';
618
- if (!isId(id)) { toast('无效的 delegationId,未打开委派视图。', true); return null; }
829
+ if (!isId(id)) { toast('暂时无法打开此任务。', true); return null; }
619
830
  const existing = tabs.find(tab => tab.kind === 'delegated' && tab.delegationId === id);
620
831
  if (existing) {
621
832
  activeTabId = existing.id;
@@ -625,9 +836,9 @@
625
836
  return existing.id;
626
837
  }
627
838
  const view = delegationIndex.get(id);
628
- if (!view) { toast('此 delegation 不在当前会话的可信委派关系中,不能打开。', true); return null; }
839
+ if (!view) { toast('此任务不属于当前会话,请返回所属会话查看。', true); return null; }
629
840
  const tab = {
630
- id: nextTabId(), kind: 'delegated', delegationId: id,
841
+ id: nextTabId(), kind: 'delegated', delegationId: id, draft: (() => { try { return storage && storage.getItem(storageKey + ':draft:' + scopeStorageKey(scope) + ':' + id) || ''; } catch { return ''; } })(),
631
842
  taskRef: null, providerId: null, descriptor: null,
632
843
  invalid: null, panel: null, pane: null, invalidRendered: null,
633
844
  };
@@ -674,9 +885,14 @@
674
885
  panel.loading = true; panel.error = null; panel.loaded = false; panel.rendered = false;
675
886
  render();
676
887
  let page = null, error = null;
677
- try { page = await client.loadHistory(view.childSessionId, { limit: DOCK_PAGE_LIMIT }); }
678
- catch (err) { error = err; }
888
+ const permissionRevision = panel.permissionRevision || 0;
889
+ const [historyResult, snapshotResult] = await Promise.allSettled([
890
+ client.loadHistory(view.childSessionId, { limit: DOCK_PAGE_LIMIT }),
891
+ typeof client.loadSession === 'function' ? client.loadSession(view.childSessionId) : Promise.resolve(null),
892
+ ]);
893
+ if (historyResult.status === 'fulfilled') page = historyResult.value; else error = historyResult.reason;
679
894
  if (epoch !== scopeEpoch || !tabs.includes(tab) || panel.revision !== revision) return;
895
+ if (snapshotResult.status === 'fulfilled' && snapshotResult.value && permissionRevision === (panel.permissionRevision || 0)) panel.currentPermission = snapshotResult.value.permission || null;
680
896
  panel.loading = false;
681
897
  if (error) {
682
898
  const code = (error && error.code) || String((error && error.status) || '');
@@ -686,10 +902,14 @@
686
902
  return;
687
903
  }
688
904
  const events = page && Array.isArray(page.events) ? page.events : [];
689
- panel.events = panel.loadedEarlier ? mergeDelegationEvents(panel.events, events) : events;
905
+ panel.events = mergeDelegationEvents(events, panel.events.filter(event => Number(event.sequence) > Number(page && page.highWater || Infinity)));
906
+ if (panel.loadedEarlier) panel.events = mergeDelegationEvents(panel.olderEvents || [], panel.events);
907
+ panel.highWater = Number(page && page.highWater) || Math.max(0, ...events.map(event => Number(event.sequence) || 0));
908
+ 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
909
  panel.nextCursor = page && page.nextCursor || null;
691
910
  panel.hasMore = isId(panel.nextCursor);
692
911
  panel.loaded = true;
912
+ tab.connectionError = null;
693
913
  panel.rendered = false;
694
914
  panel.lastTurns = view.child && Number.isFinite(Number(view.child.turns)) ? Number(view.child.turns) : null;
695
915
  render();
@@ -721,68 +941,163 @@
721
941
  panel.events = [...older.filter(row => !known.has(delegationRowKey(row))), ...panel.events];
722
942
  panel.nextCursor = page && page.nextCursor || null;
723
943
  panel.hasMore = isId(panel.nextCursor);
724
- panel.loadedEarlier = true;
944
+ panel.loadedEarlier = true; panel.olderEvents = panel.events.slice();
725
945
  panel.rendered = false;
726
946
  render();
727
947
  }
728
948
 
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
- }
949
+ function buildReadOnlyTool(event) {
950
+ if (renderToolStep) return renderToolStep(event, event.input || {});
951
+ const node = el('details', 'step rd-toolstep');
952
+ node.append(el('summary', null, event.name || event.toolName || '工具'));
953
+ const body = el('div', 'step-body');
954
+ body.append(el('pre', 'rd-pre', JSON.stringify(event.input || {}, null, 2)));
955
+ node.append(body);
772
956
  return node;
773
957
  }
774
958
 
959
+ function finishReadOnlyTool(step, event) {
960
+ if (renderToolResult) { renderToolResult(step, event); return; }
961
+ const output = event.output == null ? '' : typeof event.output === 'string' ? event.output : JSON.stringify(event.output, null, 2);
962
+ step.append(el('pre', 'rd-pre', output));
963
+ if (event.isError) step.classList.add('failed');
964
+ }
965
+
966
+ // Normalize transport events into conversation turns. Internal identities,
967
+ // dispatch state and diagnostics never enter customer DOM.
968
+ function createConversation(scroll) {
969
+ let turn = null;
970
+ const tools = new Map();
971
+ function current() {
972
+ if (!turn) turn = { messages: [], changes: [], process: null, live: null, thinking: null };
973
+ return turn;
974
+ }
975
+ function mark(node, sequence) {
976
+ if (Number.isFinite(Number(sequence))) node.dataset.seq = String(sequence);
977
+ return node;
978
+ }
979
+ function message(role, text, sequence) {
980
+ const node = renderMessage ? renderMessage(role, text) : el('div', role === 'user' ? 'msg user' : 'msg ai');
981
+ let body = node.querySelector && node.querySelector(role === 'user' ? '.bubble' : '.md-body');
982
+ if (!body) {
983
+ body = el('div', role === 'user' ? 'bubble' : 'md-body');
984
+ if (role === 'user') body.textContent = text;
985
+ else renderMdInto(body, text);
986
+ node.append(body);
987
+ }
988
+ mark(node, sequence);
989
+ scroll.append(node);
990
+ return { node, body, text };
991
+ }
992
+ function process(sequence) {
993
+ const t = current();
994
+ if (!t.process) {
995
+ if (renderProcess) t.process = renderProcess();
996
+ else {
997
+ const node = el('details', 'proc');
998
+ const meta = el('summary', 'p-meta');
999
+ const steps = el('div', 'proc-steps');
1000
+ node.append(meta, steps);
1001
+ t.process = { node, meta, steps };
1002
+ }
1003
+ t.process.node.open = false;
1004
+ scroll.append(t.process.node);
1005
+ }
1006
+ for (const msg of t.messages.splice(0)) {
1007
+ if (typeof msg.node.remove === 'function') msg.node.remove();
1008
+ t.process.steps.append(msg.node);
1009
+ }
1010
+ t.live = null;
1011
+ mark(t.process.node, sequence);
1012
+ return t.process;
1013
+ }
1014
+ function updateProcess(p) { p.meta.textContent = p.steps.children.length + ' 个步骤'; }
1015
+ function finish(sequence) {
1016
+ if (!turn) return;
1017
+ if (renderChanges && turn.changes.length) {
1018
+ const changes = renderChanges(turn.changes);
1019
+ if (changes) scroll.append(mark(changes, sequence));
1020
+ }
1021
+ turn = null;
1022
+ }
1023
+ function accept(event) {
1024
+ if (!event) return;
1025
+ const seq = event.sequence, type = event.type;
1026
+ if (type === 'claude/user-prompt') {
1027
+ finish(seq);
1028
+ if (event.text) message('user', String(event.text), seq);
1029
+ current();
1030
+ } else if (type === 'claude/text-start') {
1031
+ current().live = null;
1032
+ } else if (type === 'claude/text-delta' || type === 'conversation/assistant') {
1033
+ const text = String(event.text || '');
1034
+ if (!text) return;
1035
+ const t = current();
1036
+ if (type === 'conversation/assistant' || !t.live || t.live.id !== event.id) {
1037
+ t.live = message('assistant', text, seq);
1038
+ t.live.id = event.id;
1039
+ t.messages.push(t.live);
1040
+ } else {
1041
+ t.live.text += text;
1042
+ renderMdInto(t.live.body, t.live.text);
1043
+ mark(t.live.node, seq);
1044
+ }
1045
+ t.thinking = null;
1046
+ } else if (type === 'claude/tool-use') {
1047
+ const p = process(seq);
1048
+ const step = buildReadOnlyTool(event);
1049
+ p.steps.append(step);
1050
+ tools.set(event.id, step);
1051
+ updateProcess(p);
1052
+ current().thinking = null;
1053
+ } else if (type === 'claude/tool-result') {
1054
+ const step = tools.get(event.id);
1055
+ const p = current().process || process(seq);
1056
+ const target = step || buildReadOnlyTool({ id: event.id, name: event.toolName || '工具', input: {} });
1057
+ if (!step) p.steps.append(target);
1058
+ finishReadOnlyTool(target, event);
1059
+ current().changes.push({ ...event, input: target._toolInput || {} });
1060
+ mark(p.node, seq);
1061
+ updateProcess(p);
1062
+ } else if (type === 'claude/thinking-delta') {
1063
+ const t = current();
1064
+ const p = t.process || process(seq);
1065
+ if (!t.thinking) {
1066
+ const node = el('div', 'think-seg');
1067
+ node.append(el('div', 'think-label', '思考'));
1068
+ const body = el('div', 'think-body');
1069
+ node.append(body);
1070
+ p.steps.append(node);
1071
+ t.thinking = body;
1072
+ }
1073
+ t.thinking.textContent += String(event.text || '');
1074
+ mark(p.node, seq);
1075
+ updateProcess(p);
1076
+ } else if (type === 'claude/result') {
1077
+ const t = current();
1078
+ if (event.isError) scroll.append(mark(el('div', 'turn-note', '回复失败,请稍后重试。'), seq));
1079
+ else if (!t.messages.length && event.result) message('assistant', String(event.result), seq);
1080
+ finish(seq);
1081
+ } else if (type === 'conversation/unavailable') {
1082
+ scroll.append(mark(el('div', 'turn-note', '任务记录暂不可用,请刷新重试。'), seq));
1083
+ } else if (type === 'claude/aborted' || type === 'claude/task-interrupted' || type === 'claude/error') {
1084
+ scroll.append(mark(el('div', 'turn-note', type === 'claude/error' ? '回复失败,请稍后重试。' : '回复已中断'), seq));
1085
+ finish(seq);
1086
+ }
1087
+ }
1088
+ return { accept, finish };
1089
+ }
1090
+
775
1091
  function renderDelegationTimeline(tab) {
776
1092
  const pane = tab.pane;
777
1093
  const panel = ensureDelegationPanelState(tab);
778
1094
  if (!pane) return;
779
1095
  const keepScroll = pane.follow === false ? pane.scroll.scrollTop : null;
780
1096
  pane.scroll.textContent = '';
1097
+ const conversation = createConversation(pane.scroll);
781
1098
  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
- }
1099
+ for (const event of ordered) conversation.accept(event);
1100
+ pane.conversation = conversation;
786
1101
  panel.rendered = true;
787
1102
  if (keepScroll !== null) pane.scroll.scrollTop = keepScroll;
788
1103
  else if (pane.follow !== false) pane.scroll.scrollTop = pane.scroll.scrollHeight;
@@ -798,104 +1113,62 @@
798
1113
  return null;
799
1114
  }
800
1115
 
1116
+ function taskTitle(entry) {
1117
+ const title = entry && entry.launcherToolUseId && deps && typeof deps.taskTitle === 'function' ? deps.taskTitle(entry.launcherToolUseId) : null;
1118
+ return String(title || entry && entry.description || '子代理任务').replace(/\s+/g, ' ').slice(0, 60);
1119
+ }
1120
+
1121
+ function delegationTitle(id) {
1122
+ if (delegationTitles.has(id)) return delegationTitles.get(id);
1123
+ const tab = tabs.find(item => item.kind === 'delegated' && item.delegationId === id && item.panel);
1124
+ if (tab && tab.title) return tab.title;
1125
+ const prompt = tab && tab.panel.events.find(event => event.type === 'claude/user-prompt' && event.text);
1126
+ return prompt ? String(prompt.text).replace(/\s+/g, ' ').slice(0, 60) : '子代理任务';
1127
+ }
1128
+
1129
+ function delegationStatus(view) {
1130
+ if (!view) return { status: 'pending', label: '状态待确认' };
1131
+ if (view.phase === 'dispatch-unknown' || view.outcome === 'unknown') return { status: 'pending', label: '状态待确认' };
1132
+ if (view.phase === 'child-create-failed') return { status: 'failed', label: '任务未能启动' };
1133
+ const child = view.child;
1134
+ if (!child.present) return { status: 'failed', label: '任务暂不可用' };
1135
+ if (child.state === 'failed') return { status: 'failed', label: '失败' };
1136
+ if (child.state === 'aborted') return { status: 'stopped', label: '已停止' };
1137
+ if (child.state === 'pending-permission') return { status: 'pending', label: '等待你确认' };
1138
+ if (child.active) return { status: 'running', label: '运行中' };
1139
+ if (['dispatched', 'completed'].includes(view.phase) && child.state === 'idle' && child.turns > 0) return { status: 'completed', label: '已完成' };
1140
+ return { status: 'pending', label: '待执行' };
1141
+ }
1142
+
801
1143
  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 };
1144
+ buildPane(tab);
1145
+ tab.pane.node.classList.add('rd-delegation-pane');
1146
+ tab.pane.composer = buildComposer(tab);
1147
+ tab.pane.permissions = el('div', 'rd-permissions');
1148
+ tab.pane.node.append(tab.pane.permissions, tab.pane.composer.node);
834
1149
  }
835
1150
 
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
1151
  function refreshDelegationPane(tab) {
840
1152
  const pane = tab.pane;
841
1153
  if (!pane) return;
842
1154
  const panel = ensureDelegationPanelState(tab);
843
1155
  const view = delegationIndex.get(tab.delegationId) || null;
1156
+ pane.title.textContent = delegationTitle(tab.delegationId);
1157
+ pane.status.textContent = delegationStatus(view).label;
1158
+ pane.errorBox.hidden = !tab.invalid && !panel.error;
1159
+ if (tab.invalid || panel.error) pane.errorBox.textContent = '暂时无法查看此任务,请刷新后重试。';
1160
+ pane.banner.hidden = true;
844
1161
  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
- }
1162
+ pane.scroll.textContent = '';
1163
+ pane.more.hidden = true;
854
1164
  return;
855
1165
  }
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
1166
  if (!panel.rendered) renderDelegationTimeline(tab);
1167
+ if (tab.error || tab.connectionError) { pane.errorBox.hidden = false; pane.errorBox.textContent = tab.error || tab.connectionError; }
1168
+ if (pane.composer) { pane.composer.setBusy(childBusy(tab)); pane.composer.send.disabled = Boolean(tab.invalid || tab.sending || !view || !view.child.present); }
1169
+ if (pane.composer && view) pane.composer.setLabel?.((view.child.model || '子代理') + ' · 默认防护');
1170
+ renderPermissions(tab);
1171
+ subscribeChild(tab);
899
1172
  pane.more.hidden = !(panel.hasMore && panel.nextCursor);
900
1173
  pane.more.disabled = panel.loading === true;
901
1174
  }
@@ -926,6 +1199,7 @@
926
1199
  }
927
1200
  renderTabBar();
928
1201
  renderToggleState();
1202
+ for (const tab of tabs) if (!open || tab.id !== activeTabId) releaseSubscription(tab);
929
1203
  // Live native state events move task status immediately and with it
930
1204
  // backgroundActivity; notify the host listener on the same beat.
931
1205
  notifyBackgroundActivity();
@@ -963,15 +1237,11 @@
963
1237
  panel.usageSummary = event.usage.summary;
964
1238
  }
965
1239
  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
- }
1240
+ if (!tab.pane.conversation) renderTimeline(tab);
1241
+ else appendNativeConversation(tab.pane.conversation, event);
972
1242
  if (tab.pane.follow !== false) tab.pane.scroll.scrollTop = tab.pane.scroll.scrollHeight;
973
1243
  else tab.pane.pendingNew = true;
974
- refreshFoot(tab);
1244
+
975
1245
  return true;
976
1246
  }
977
1247
 
@@ -1037,6 +1307,7 @@
1037
1307
  flushPersist();
1038
1308
  scopeEpoch += 1;
1039
1309
  scope = null;
1310
+ for (const tab of tabs) releaseSubscription(tab);
1040
1311
  tabs = [];
1041
1312
  activeTabId = null;
1042
1313
  pickerOpen = false;
@@ -1047,10 +1318,19 @@
1047
1318
  notifyPanelsChanged('scope');
1048
1319
  }
1049
1320
 
1050
- function openTask(refInput) {
1321
+ function openTask(refInput, options = {}) {
1051
1322
  const ref = sanitizeTaskRef(refInput);
1052
1323
  if (!ref) { toast('无效的任务引用,未打开面板。', true); return null; }
1053
1324
  if (!scope || !scopeRefMatches(scope, ref)) { toast('该任务不属于当前会话范围;请在所属会话中打开。', true); return null; }
1325
+ const existing = options.reuse && tabs.find(tab => tab.taskRef && taskRefKey(tab.taskRef) === taskRefKey(ref));
1326
+ if (existing) {
1327
+ activeTabId = existing.id;
1328
+ open = true;
1329
+ pickerOpen = false;
1330
+ render();
1331
+ schedulePersist();
1332
+ return existing.id;
1333
+ }
1054
1334
  const tab = {
1055
1335
  id: 'tab-' + Date.now().toString(36) + '-' + (++tabSeq),
1056
1336
  taskRef: ref, invalid: null, panel: null, pane: null, invalidRendered: null,
@@ -1077,10 +1357,10 @@
1077
1357
  }
1078
1358
 
1079
1359
  function openPanel(providerIdInput) {
1080
- if (!registry) { toast('面板 registry 不可用。', true); return null; }
1360
+ if (!registry) { toast('暂时无法打开此内容。', true); return null; }
1081
1361
  const providerId = isId(providerIdInput) ? String(providerIdInput).trim() : '';
1082
1362
  const descriptor = registry.get(providerId);
1083
- if (!descriptor) { toast('未注册的面板 provider,未打开。', true); return null; }
1363
+ if (!descriptor) { toast('此内容当前不可用。', true); return null; }
1084
1364
  if (!providerAppliesToScope(descriptor)) { toast('该面板不适用于当前项目范围。', true); return null; }
1085
1365
  if (descriptor.multiInstance !== 'multiple') {
1086
1366
  const existing = tabs.find(tab => tab.kind === 'host' && tab.providerId === providerId);
@@ -1137,8 +1417,10 @@
1137
1417
  const index = tabs.findIndex(tab => tab.id === tabId);
1138
1418
  if (index === -1) return;
1139
1419
  const [tab] = tabs.splice(index, 1);
1420
+ saveTabDraft(tab); releaseSubscription(tab);
1140
1421
  if (tab.pane && tab.pane.node && typeof tab.pane.node.remove === 'function') tab.pane.node.remove();
1141
1422
  if (activeTabId === tabId) activeTabId = (tabs[index] || tabs[index - 1] || {}).id || null;
1423
+ if (!tabs.length) { open = false; pickerOpen = false; }
1142
1424
  render();
1143
1425
  schedulePersist();
1144
1426
  if (tab.kind === 'host') {
@@ -1150,6 +1432,7 @@
1150
1432
 
1151
1433
  function toggle() {
1152
1434
  open = !open;
1435
+ if (open && !tabs.length) { openPicker(); return open; }
1153
1436
  if (open) pickerOpen = false;
1154
1437
  render();
1155
1438
  schedulePersist();
@@ -1223,13 +1506,23 @@
1223
1506
  root.append(resizer, body);
1224
1507
  }
1225
1508
  if (toggleBtn) toggleBtn.addEventListener('click', () => { toggle(); });
1509
+ if (typeof global.ResizeObserver === 'function' && root && root.parentNode) { const observer = new global.ResizeObserver(applyDockVisibility); observer.observe(root.parentNode); }
1226
1510
  domBound = true;
1227
1511
  }
1228
1512
 
1229
1513
  function applyDockVisibility() {
1230
1514
  if (!root) return;
1231
1515
  root.hidden = !open;
1232
- root.style.width = open ? width + 'px' : '';
1516
+ const parent = root.parentNode;
1517
+ const rect = parent && typeof parent.getBoundingClientRect === 'function' ? parent.getBoundingClientRect() : null;
1518
+ const sidebar = parent && parent.querySelector ? parent.querySelector('.sidebar') : null;
1519
+ const sideWidth = sidebar && sidebar.getBoundingClientRect ? sidebar.getBoundingClientRect().width : 0;
1520
+ const available = rect ? rect.width - sideWidth : 0;
1521
+ const drawer = available > 0 && available < 821;
1522
+ if (root.classList) { if (drawer) root.classList.add('rd-drawer'); else root.classList.remove('rd-drawer'); }
1523
+ const effective = available ? drawer ? Math.min(width, available - 24) : Math.min(width, available - 481) : width;
1524
+ root.style.width = open ? Math.max(0, effective) + 'px' : '';
1525
+ if (root.parentNode && root.parentNode.classList) root.parentNode.classList.toggle('dock-open', open);
1233
1526
  }
1234
1527
 
1235
1528
  function runningCount() {
@@ -1243,7 +1536,7 @@
1243
1536
  if (!toggleBtn) return;
1244
1537
  toggleBtn.hidden = false;
1245
1538
  toggleBtn.setAttribute('aria-expanded', String(open));
1246
- toggleBtn.title = open ? '收起右侧任务面板' : '展开右侧任务面板(后台任务与委派会话)';
1539
+ toggleBtn.title = open ? '收起右侧面板' : '展开右侧面板';
1247
1540
  const badge = toggleBtn.querySelector && toggleBtn.querySelector('.rd-toggle-count');
1248
1541
  if (badge) {
1249
1542
  const running = runningCount();
@@ -1257,68 +1550,87 @@
1257
1550
  function render() {
1258
1551
  ensureDom();
1259
1552
  applyDockVisibility();
1553
+ for (const tab of tabs) if (!open || tab.id !== activeTabId) releaseSubscription(tab);
1260
1554
  renderTabBar();
1261
1555
  renderActivePane();
1262
1556
  renderPicker();
1263
1557
  renderToggleState();
1264
1558
  }
1265
1559
 
1560
+ function collapseButton() {
1561
+ const button = el('button', 'rd-collapse', '收起');
1562
+ button.type = 'button';
1563
+ button.setAttribute('aria-label', '收起任务面板');
1564
+ button.addEventListener('click', () => { toggle(); });
1565
+ return button;
1566
+ }
1567
+
1266
1568
  function renderTabBar() {
1267
1569
  if (!tabBar) return;
1268
1570
  tabBar.textContent = '';
1269
- if (!scope) { tabBar.hidden = true; return; }
1571
+
1270
1572
  tabBar.hidden = false;
1573
+ const tabList = el('div', 'rd-tab-list');
1574
+ tabBar.append(tabList);
1271
1575
  for (const tab of tabs) {
1272
1576
  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' };
1577
+ const status = entry && !entry.typeConflict ? entry.status : null;
1578
+ const item = el('div', 'rd-tab' + (tab.id === activeTabId ? ' active' : ''));
1579
+ const tabOpen = el('button', 'rd-tab-open'); tabOpen.setAttribute('role', 'tab'); tabOpen.setAttribute('aria-selected', String(tab.id === activeTabId));
1580
+ const close = el('button', 'rd-tab-close'); close.type = 'button';
1581
+ if (tab.kind === 'draft' || tab.kind === 'launcher') {
1582
+ item.title = tab.kind === 'launcher' ? '新标签页' : '新子代理';
1583
+ close.textContent = '×'; close.setAttribute('aria-label', '关闭' + item.title);
1584
+ close.addEventListener('click', ev => { if (ev && ev.stopPropagation) ev.stopPropagation(); closeTab(tab.id); });
1585
+ tabOpen.append(el('span', 'rd-tab-label', item.title)); item.append(tabOpen, close);
1586
+ } else if (tab.kind === 'host') {
1587
+ const descriptor = tab.descriptor || (registry ? registry.get(tab.providerId) : null) || { title: '参考资料', panelType: 'knowledge' };
1278
1588
  item.title = descriptor.title;
1279
1589
  const dot = el('span', 'rd-dot host');
1280
1590
  const label = el('span', 'rd-tab-label', descriptor.title);
1281
- close.title = '关闭此视图(只销毁面板,不停止任务)';
1591
+ close.title = '关闭';
1282
1592
  close.setAttribute('aria-label', close.title);
1283
1593
  close.textContent = '×';
1284
1594
  close.addEventListener('click', ev => { if (ev && ev.stopPropagation) ev.stopPropagation(); closeTab(tab.id); });
1285
- item.append(dot, label, close);
1595
+ tabOpen.append(dot, label); item.append(tabOpen, close);
1286
1596
  } else if (tab.kind === 'delegated') {
1287
- item.title = tab.delegationId;
1597
+ item.title = delegationTitle(tab.delegationId);
1288
1598
  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';
1599
+ const dotClass = delegationStatus(entry).status;
1291
1600
  const dot = el('span', 'rd-dot ' + dotClass);
1292
- const label = el('span', 'rd-tab-label', '委派 ' + shortId(tab.delegationId));
1293
- close.title = '关闭此视图(只销毁面板,不停止子会话,不重发输入)';
1601
+ const label = el('span', 'rd-tab-label', delegationTitle(tab.delegationId));
1602
+ close.title = '关闭';
1294
1603
  close.setAttribute('aria-label', close.title);
1295
1604
  close.textContent = '×';
1296
1605
  close.addEventListener('click', ev => { if (ev && ev.stopPropagation) ev.stopPropagation(); closeTab(tab.id); });
1297
- item.append(dot, label, close);
1606
+ tabOpen.append(dot, label); item.append(tabOpen, close);
1298
1607
  } else {
1299
- item.title = tab.taskRef.taskId;
1608
+ item.title = taskTitle(entry);
1300
1609
  const dot = el('span', 'rd-dot ' + (status || 'unknown'));
1301
- const label = el('span', 'rd-tab-label', '#' + shortId(tab.taskRef.taskId));
1302
- close.title = '关闭此视图(只销毁面板,不停止任务)';
1610
+ const label = el('span', 'rd-tab-label', taskTitle(entry));
1611
+ close.title = '关闭';
1303
1612
  close.setAttribute('aria-label', close.title);
1304
1613
  close.textContent = '×';
1305
1614
  close.addEventListener('click', ev => { if (ev && ev.stopPropagation) ev.stopPropagation(); closeTab(tab.id); });
1306
- item.append(dot, label, close);
1615
+ tabOpen.append(dot, label); item.append(tabOpen, close);
1307
1616
  }
1617
+ close.setAttribute('aria-label', '关闭' + item.title);
1308
1618
  item.addEventListener('click', () => {
1309
1619
  activeTabId = tab.id;
1310
1620
  render();
1311
1621
  schedulePersist();
1312
1622
  if (tab.kind === 'host') notifyPanelsChanged('active');
1313
1623
  });
1314
- tabBar.append(item);
1624
+ tabList.append(item);
1315
1625
  }
1316
1626
  const plus = el('button', 'rd-plus');
1317
- plus.title = '选择原生后台任务(同一任务可开多个视图)';
1627
+ plus.title = '新建标签页';
1318
1628
  plus.setAttribute('aria-label', plus.title);
1319
1629
  plus.textContent = '+';
1320
- plus.addEventListener('click', () => { pickerOpen ? closePicker() : openPicker(); });
1321
- tabBar.append(plus);
1630
+ plus.addEventListener('click', () => { openPicker(); });
1631
+ tabBar.append(plus, collapseButton());
1632
+ const selected = tabList.querySelector && tabList.querySelector('[aria-selected="true"]');
1633
+ if (selected && selected.scrollIntoView) selected.scrollIntoView({ block: 'nearest', inline: 'nearest' });
1322
1634
  }
1323
1635
 
1324
1636
  function renderActivePane() {
@@ -1336,24 +1648,33 @@
1336
1648
  tab.pane.node.hidden = tab.id !== activeTabId;
1337
1649
  }
1338
1650
  const activeBeforeScope = tabs.find(tab => tab.id === activeTabId) || tabs[0];
1651
+ if (activeBeforeScope && ['draft', 'launcher'].includes(activeBeforeScope.kind)) {
1652
+ const tab = activeBeforeScope;
1653
+ if (!tab.pane) { if (tab.kind === 'draft') buildDraftPane(tab); else buildLauncherPane(tab); paneHost.append(tab.pane.node); }
1654
+ hideEmptyPane(); tab.pane.node.hidden = false;
1655
+ 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); }
1656
+ return;
1657
+ }
1339
1658
  if (!scope && (!activeBeforeScope || activeBeforeScope.kind !== 'host')) {
1340
- renderEmptyPane('尚未附加会话。打开一个会话后,这里显示它的原生后台任务。');
1659
+ renderEmptyPane('打开会话后,可在这里查看子代理任务。');
1341
1660
  return;
1342
1661
  }
1343
1662
  if (!tabs.length) {
1344
- renderEmptyPane(listState.loading ? '正在读取原生任务列表…' : '没有打开的任务视图。点击 + 选择原生后台任务。');
1663
+ renderEmptyPane('点击 + 添加页面。');
1345
1664
  return;
1346
1665
  }
1347
1666
  hideEmptyPane();
1348
1667
  const active = tabs.find(tab => tab.id === activeTabId) || tabs[0];
1349
1668
  if (active && active.id !== activeTabId) activeTabId = active.id;
1350
1669
  if (!active) return;
1670
+ if (active.kind === 'host' && !active.descriptor) { active.descriptor = registry && registry.get(active.providerId); if (!active.descriptor) { renderEmptyPane('正在准备知识引用…'); return; } }
1351
1671
  if (!active.pane) {
1352
1672
  if (active.kind === 'host') buildHostPane(active);
1353
1673
  else if (active.kind === 'delegated') buildDelegationPane(active);
1354
1674
  else buildPane(active);
1355
1675
  paneHost.append(active.pane.node);
1356
1676
  }
1677
+ active.pane.node.hidden = false;
1357
1678
  if (active.kind === 'host') refreshHostPane(active);
1358
1679
  else if (active.kind === 'delegated') refreshDelegationPane(active);
1359
1680
  else refreshPane(active);
@@ -1374,21 +1695,13 @@
1374
1695
  const node = el('section', 'rd-pane');
1375
1696
  node.hidden = true;
1376
1697
  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);
1698
+ const title = el('strong', 'rd-title', '子代理任务');
1699
+ const status = el('span', 'rd-task-status');
1700
+ head.append(el('span', 'rd-thread-note', '关联主对话'), status); title.hidden = true;
1388
1701
  const banner = el('div', 'rd-banner');
1389
1702
  banner.hidden = true;
1390
1703
  const bannerText = el('span', 'rd-banner-text');
1391
- const resyncBtn = el('button', 'rd-resync', '重新同步');
1704
+ const resyncBtn = el('button', 'rd-resync', '刷新');
1392
1705
  resyncBtn.addEventListener('click', () => { void validateScopeTasks(); });
1393
1706
  banner.append(bannerText, resyncBtn);
1394
1707
  const errorBox = el('div', 'rd-error');
@@ -1400,15 +1713,27 @@
1400
1713
  const distance = (scroll.scrollHeight || 0) - (scroll.scrollTop || 0) - (scroll.clientHeight || 0);
1401
1714
  const wasFollowing = pane.follow !== false;
1402
1715
  pane.follow = distance <= 2;
1403
- if (wasFollowing && !pane.follow) pane.anchorSeq = lastSequenceOf(tab);
1716
+ if (wasFollowing && !pane.follow) pane.anchorSeq = tab.kind === 'delegated' ? lastDelegationSequenceOf(tab) : lastSequenceOf(tab);
1404
1717
  schedulePersist();
1405
1718
  });
1406
1719
  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 };
1720
+ more.addEventListener('click', () => { void (tab.kind === 'delegated' ? loadEarlierDelegation(tab) : loadEarlier(tab)); });
1721
+ node.append(head, banner, errorBox, scroll, more);
1722
+ if (tab.kind !== 'delegated') {
1723
+ const continuation = el('button', 'rd-continue', '基于此结果继续');
1724
+ continuation.addEventListener('click', () => {
1725
+ const texts = [];
1726
+ for (const event of ensurePanelState(tab).events) for (const entry of event.entries || []) {
1727
+ if (entry.role !== 'assistant') continue;
1728
+ const text = (entry.blocks || []).filter(block => block.kind === 'text' && typeof block.text === 'string').map(block => block.text).join('\n');
1729
+ if (text.trim()) texts.push(text);
1730
+ }
1731
+ openNewAgent('请基于“' + taskTitle(taskIndex.get(taskRefKey(tab.taskRef))) + '”的结果继续:\n\n' + (texts.pop() || ''));
1732
+ });
1733
+ node.append(continuation);
1734
+ }
1735
+ tab.pane = { node, head, title, status, banner, bannerText, resyncBtn, errorBox, scroll, more,
1736
+ follow: true, anchorSeq: null, anchorNote: null, pendingNew: false };
1412
1737
  }
1413
1738
 
1414
1739
  // Host panels are validated iframe descriptors from the registry. The
@@ -1419,17 +1744,13 @@
1419
1744
  const descriptor = tab.descriptor || {};
1420
1745
  const node = el('section', 'rd-pane rd-host-pane');
1421
1746
  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', '正在加载宿主面板…');
1747
+ const status = el('div', 'rd-host-status', '正在加载…');
1427
1748
  const errorBox = el('div', 'rd-error');
1428
1749
  errorBox.hidden = true;
1429
1750
  const frameHolder = el('div', 'rd-host-frame-holder');
1430
1751
  const frame = el('iframe', 'rd-host-frame');
1431
1752
  frame.setAttribute('src', descriptor.url || '');
1432
- frame.setAttribute('title', descriptor.title || '宿主面板');
1753
+ frame.setAttribute('title', descriptor.title || '知识引用');
1433
1754
  frame.setAttribute('sandbox', 'allow-scripts');
1434
1755
  frame.setAttribute('referrerpolicy', 'no-referrer');
1435
1756
  frame.setAttribute('loading', 'eager');
@@ -1449,12 +1770,12 @@
1449
1770
  tab.pane.error = 'LOAD_FAILED';
1450
1771
  tab.pane.status.hidden = true;
1451
1772
  tab.pane.errorBox.hidden = false;
1452
- tab.pane.errorBox.textContent = '宿主面板加载失败(同源 URL 未成功载入)。';
1773
+ tab.pane.errorBox.textContent = '内容加载失败,请稍后重试。';
1453
1774
  notifyPanelsChanged('error');
1454
1775
  });
1455
1776
  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 };
1777
+ node.append(status, errorBox, frameHolder);
1778
+ tab.pane = { node, status, errorBox, frameHolder, frame, follow: true, anchorSeq: null, anchorNote: null, pendingNew: false, finalNode: null, error: null };
1458
1779
  }
1459
1780
 
1460
1781
  function refreshHostPane(tab) {
@@ -1462,12 +1783,9 @@
1462
1783
  if (!pane) return;
1463
1784
  const descriptor = tab.descriptor || (registry ? registry.get(tab.providerId) : null) || {};
1464
1785
  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
1786
  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);
1787
+ if (pane.error) pane.errorBox.textContent = '内容加载失败,请稍后重试。';
1788
+
1471
1789
  pane.status.hidden = pane.error != null || pane.loaded === true;
1472
1790
  }
1473
1791
 
@@ -1511,6 +1829,7 @@
1511
1829
  if (typeof data.version === 'number' && Number.isFinite(data.version)) version = data.version;
1512
1830
  else if (typeof data.version === 'string' && data.version.trim() && data.version.length <= 200) version = data.version.trim();
1513
1831
  if (version === null) return null;
1832
+ if (tab.pane.contentVersion == null || String(version) !== String(tab.pane.contentVersion)) return null;
1514
1833
  const ok = data.ok === true;
1515
1834
  const error = typeof data.error === 'string' && data.error.trim() ? data.error.slice(0, 300) : null;
1516
1835
  tab.pane.contentAck = { version, ok, error, at: Date.now() };
@@ -1532,19 +1851,7 @@
1532
1851
  const value = Number(children[i] && children[i].dataset ? children[i].dataset.seq : NaN);
1533
1852
  if (Number.isFinite(value)) return value;
1534
1853
  }
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;
1854
+ return null;
1548
1855
  }
1549
1856
 
1550
1857
  function refreshPane(tab) {
@@ -1552,52 +1859,21 @@
1552
1859
  if (!pane) return;
1553
1860
  const panel = ensurePanelState(tab);
1554
1861
  const entry = taskIndex.get(taskRefKey(tab.taskRef)) || null;
1862
+ pane.title.textContent = taskTitle(entry);
1863
+ pane.status.textContent = entry && !entry.typeConflict ? (STATUS_LABELS[entry.status] || '状态待确认') : '状态待确认';
1864
+ pane.errorBox.hidden = !tab.invalid && !panel.error;
1865
+ if (tab.invalid || panel.error) pane.errorBox.textContent = '暂时无法查看此任务,请刷新后重试。';
1866
+ pane.banner.hidden = !panel.resync;
1867
+ if (panel.resync) pane.bannerText.textContent = '记录已更新,请刷新查看。';
1555
1868
  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
- }
1869
+ pane.scroll.textContent = '';
1870
+ pane.more.hidden = true;
1871
+ pane.banner.hidden = true;
1567
1872
  return;
1568
1873
  }
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
1874
  if (!panel.rendered) renderTimeline(tab);
1598
1875
  pane.more.hidden = !(panel.hasMore && panel.nextCursor);
1599
1876
  pane.more.disabled = panel.loading === true;
1600
- refreshFoot(tab);
1601
1877
  }
1602
1878
 
1603
1879
  function renderTimeline(tab) {
@@ -1606,61 +1882,36 @@
1606
1882
  if (!pane) return;
1607
1883
  const keepScroll = pane.follow === false ? pane.scroll.scrollTop : null;
1608
1884
  pane.scroll.textContent = '';
1609
- pane.finalNode = null;
1610
- let lastTextNode = null;
1885
+ const conversation = createConversation(pane.scroll);
1611
1886
  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; }
1887
+ for (const event of ordered) appendNativeConversation(conversation, event);
1888
+ const entry = taskIndex.get(taskRefKey(tab.taskRef));
1889
+ if (entry && TERMINAL_STATUSES.includes(entry.status)) conversation.finish(ordered.length ? ordered[ordered.length - 1].sequence : null);
1890
+ pane.conversation = conversation;
1619
1891
  panel.rendered = true;
1620
1892
  if (keepScroll !== null) pane.scroll.scrollTop = keepScroll;
1621
1893
  else if (pane.follow !== false) pane.scroll.scrollTop = pane.scroll.scrollHeight;
1622
1894
  }
1623
1895
 
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)));
1896
+ function appendNativeConversation(conversation, event) {
1897
+ if (!event || typeof event !== 'object') return;
1898
+ if (event.type === 'claude/task-interrupted') { conversation.accept(event); return; }
1899
+ if (event.type !== 'claude/native-task-transcript') return;
1900
+ if (!event.read || event.read.status !== 'ok') {
1901
+ conversation.accept({ type: 'conversation/unavailable', sequence: event.sequence });
1902
+ return;
1903
+ }
1904
+ for (const entry of Array.isArray(event.entries) ? event.entries : []) {
1905
+ if (!entry || typeof entry !== 'object') continue;
1906
+ const blocks = Array.isArray(entry.blocks) ? entry.blocks : [];
1907
+ const text = blocks.filter(block => block && block.kind === 'text' && typeof block.text === 'string').map(block => block.text).join('\n');
1908
+ if (text) conversation.accept({ type: entry.role === 'assistant' ? 'conversation/assistant' : 'claude/user-prompt', text, sequence: event.sequence });
1909
+ for (const block of blocks) {
1910
+ if (!block || typeof block !== 'object') continue;
1911
+ 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 });
1912
+ 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
1913
  }
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
1914
  }
1661
- const seq = Number(event.sequence);
1662
- if (node && Number.isFinite(seq) && node.dataset) node.dataset.seq = String(seq);
1663
- return { node, textNode };
1664
1915
  }
1665
1916
 
1666
1917
  function renderMdInto(node, text) {
@@ -1671,99 +1922,10 @@
1671
1922
  node.textContent = value;
1672
1923
  }
1673
1924
 
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
1925
 
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
1926
 
1764
1927
  function renderAnchorNote(pane, text) {
1765
1928
  pane.anchorNote = text;
1766
- if (pane.note) { pane.note.textContent = text; pane.note.hidden = !text; }
1767
1929
  }
1768
1930
 
1769
1931
  function applyAnchor(tab) {
@@ -1779,6 +1941,7 @@
1779
1941
  renderAnchorNote(pane, '已定位到最新记录');
1780
1942
  return;
1781
1943
  }
1944
+ if (anchor && Number.isFinite(anchor.top)) { pane.scroll.scrollTop = anchor.top; renderAnchorNote(pane, '已恢复阅读位置'); return; }
1782
1945
  if (Number.isFinite(seq)) {
1783
1946
  let matched = null;
1784
1947
  for (const node of Array.from(pane.scroll.children || [])) {
@@ -1796,75 +1959,14 @@
1796
1959
  } else renderAnchorNote(pane, '已恢复读取位置(跟随关闭)');
1797
1960
  }
1798
1961
 
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));
1962
+ function openPicker() {
1963
+ let tab = tabs.find(item => item.kind === 'launcher');
1964
+ if (!tab) { tab = { id: nextTabId(), kind: 'launcher', pane: null }; tabs.push(tab); }
1965
+ else if (tab.pane) { tab.pane.node.remove(); tab.pane = null; }
1966
+ activeTabId = tab.id; open = true; pickerOpen = true; render(); schedulePersist();
1867
1967
  }
1968
+ function closePicker() { pickerOpen = false; }
1969
+ function renderPicker() { if (pickerHost) pickerHost.hidden = true; }
1868
1970
 
1869
1971
  function snapshot() {
1870
1972
  return {
@@ -1885,6 +1987,7 @@
1885
1987
  providerId: tab.kind === 'host' ? tab.providerId : null,
1886
1988
  panelType: tab.kind === 'host' && tab.descriptor ? tab.descriptor.panelType : null,
1887
1989
  hostError: tab.pane && tab.pane.error ? tab.pane.error : null,
1990
+ hostReady: Boolean(tab.kind === 'host' && tab.pane && tab.pane.loaded),
1888
1991
  delegation: tab.kind === 'delegated' ? delegationSnapshotOf(tab) : null,
1889
1992
  content: tab.kind === 'host' && tab.pane ? {
1890
1993
  version: tab.pane.contentVersion !== undefined ? tab.pane.contentVersion : null,
@@ -1909,12 +2012,40 @@
1909
2012
  return Array.from(tab.pane.scroll.children || []).map(node => String(node.textContent || '')).join('\n');
1910
2013
  }
1911
2014
 
1912
- function pickerText() { return pickerHost ? String(pickerHost.textContent || '') : ''; }
2015
+ function pickerText() { const tab = tabs.find(item => item.kind === 'launcher'); return tab && tab.pane ? String(tab.pane.node.textContent || '') : ''; }
2016
+
2017
+ function taskViews(parentSessionId) {
2018
+ if (parentSessionId !== undefined && (!scope || scope.parentSessionId !== parentSessionId)) return [];
2019
+ const native = [...taskIndex.values()].filter(entry => entry.taskRef && scopeRefMatches(scope, entry.taskRef)).map(entry => ({
2020
+ taskRef: { ...entry.taskRef }, taskId: entry.taskId, status: entry.status,
2021
+ description: taskTitle(entry),
2022
+ }));
2023
+ const delegated = [...delegationIndex.values()].map(view => ({
2024
+ delegationId: view.delegationId, taskId: view.childSessionId,
2025
+ description: delegationTitle(view.delegationId),
2026
+ status: delegationStatus(view).status, statusLabel: delegationStatus(view).label,
2027
+ }));
2028
+ return [...native, ...delegated];
2029
+ }
2030
+
2031
+ async function openTaskForToolUseId(toolUseId) {
2032
+ if (!isId(toolUseId) || !scope) return null;
2033
+ const epoch = scopeEpoch;
2034
+ await validateScopeTasks();
2035
+ if (epoch !== scopeEpoch) return null;
2036
+ const matches = [...taskIndex.values()].filter(entry => entry.launcherToolUseId === toolUseId && entry.taskRef && scopeRefMatches(scope, entry.taskRef));
2037
+ if (matches.length !== 1) {
2038
+ toast('此子代理暂未提供可读取的独立任务记录。', true);
2039
+ return null;
2040
+ }
2041
+ return openTask(matches[0].taskRef, { reuse: true });
2042
+ }
1913
2043
 
1914
2044
  const api = {
1915
2045
  attachSession, detachSession, handleSessionEvent, handleResync,
1916
2046
  refreshIdentity,
1917
- openTask, openDelegation, notifySessionChanged, backgroundActivity,
2047
+ refreshProviders() { for (const tab of tabs) if (tab.kind === 'launcher' && tab.pane) { tab.pane.node.remove(); tab.pane = null; } render(); },
2048
+ openTask, openTaskForToolUseId, taskViews, openNewAgent, openDelegation, notifySessionChanged, backgroundActivity,
1918
2049
  closeTab, toggle, setWidth, adjustWidth,
1919
2050
  openPanel, closeHostPanels, activateHostPanel,
1920
2051
  relayHostPanelContent, ackHostPanelContent,