@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
@@ -115,9 +115,29 @@ const dock = window.createRightWorkspaceDock && client
115
115
  client, apiBase, document,
116
116
  storage: typeof localStorage !== 'undefined' ? localStorage : null,
117
117
  renderMarkdown, toast,
118
+ renderToolStep: (event, input) => buildToolStep(event, input, { taskLinks: false }),
119
+ renderToolResult: (step, event) => finishToolStep(step, event, true),
120
+ renderMessage: (role, text) => buildConversationMessage(role, text),
121
+ renderProcess: () => buildConversationProcess(),
122
+ renderPageIcon: name => icon(name || 'file', 16),
123
+ createComposer: options => window.createConversationComposer({ ...options, document, icon }),
124
+ getSelection: () => {
125
+ const selected = state.sessionSummary || {};
126
+ return { agentId: selected.agentId || state.currentAgent, aiProfileId: selected.aiProfileId === undefined ? state.currentProfileId : selected.aiProfileId,
127
+ ...(state.currentModel ? { model: state.currentModel } : {}) };
128
+ },
129
+ taskTitle: toolUseId => {
130
+ const step = $('stream-col')?.querySelector(`.step[data-tool-id="${cssEscape(toolUseId || '')}"]`);
131
+ return step?._toolInput?.description || '';
132
+ },
133
+ renderChanges: events => {
134
+ const ctx = { changes: new Map() };
135
+ for (const event of events) collectTurnChanges(ctx, event, event.input || {});
136
+ return ctx.changes.size ? renderTurnDiffs(ctx.changes) : null;
137
+ },
118
138
  registry: panelRegistry,
119
139
  onPanelsChanged: reason => postPanelState(reason),
120
- onBackgroundActivityChanged: activity => renderSessionStatus(activity),
140
+ onBackgroundActivityChanged: activity => { renderSessionStatus(activity); renderBackgroundTasks(); },
121
141
  })
122
142
  : null;
123
143
 
@@ -971,6 +991,11 @@ async function openSession(sessionId, options = {}) {
971
991
  try {
972
992
  for (const event of historyView.events) { renderEvent(event); if (!page) last = Math.max(last, event.sequence || 0); }
973
993
  } finally { state.replaying = false; }
994
+ if (snapshot.permission?.kind === 'question') renderEvent({ type: 'claude/permission-request', ...snapshot.permission });
995
+ for (const card of state.questionCards?.values() || []) {
996
+ if (!card.classList.contains('question-settled') && card.dataset.permId !== snapshot.permission?.requestId) card.settle({ cancelled: true });
997
+ }
998
+ renderBackgroundTasks();
974
999
  rememberTitleFromEvents(historyView.events);
975
1000
  // The SSE replay window can overlap the snapshot (a slow turn raced the
976
1001
  // open, or the server answered a stale cursor with a full replay). Late or
@@ -994,6 +1019,7 @@ async function openSession(sessionId, options = {}) {
994
1019
  replayThrough = Number(event.highWater);
995
1020
  if (!Number.isFinite(replayThrough) || replayThrough < 0) replayThrough = 0;
996
1021
  resetStream();
1022
+ renderBackgroundTasks();
997
1023
  state.replaying = true;
998
1024
  return;
999
1025
  }
@@ -1052,6 +1078,7 @@ function rebuildHistory(view, events, prepend = true) {
1052
1078
  state.replaying = true;
1053
1079
  try { for (const event of events) renderEvent(event); }
1054
1080
  finally { state.replaying = false; state.follow = following; state.scrollIntent = intent; }
1081
+ renderBackgroundTasks();
1055
1082
  const rebuiltFolds = [...($('stream-col').querySelectorAll?.('details') || [])];
1056
1083
  const offset = rebuiltFolds.length - folds.length;
1057
1084
  if (offset >= 0) for (let i = 0; i < folds.length; i++) rebuiltFolds[offset + i].open = folds[i];
@@ -1148,6 +1175,7 @@ function rememberTitleFromEvents(events) {
1148
1175
  }
1149
1176
 
1150
1177
  function resetStream() {
1178
+ state.questionCards?.clear();
1151
1179
  endTurnVisuals();
1152
1180
  const col = $('stream-col');
1153
1181
  col.textContent = '';
@@ -1234,17 +1262,12 @@ function startTurn() {
1234
1262
 
1235
1263
  function ensureProc(t) {
1236
1264
  if (!(t.proc && t.proc.isConnected)) {
1237
- const details = el('details', 'proc');
1265
+ const process = buildConversationProcess();
1266
+ const details = process.node;
1238
1267
  details.open = true; // live process output stays visible while running; folds when the turn ends
1239
- const summary = el('summary', null, '');
1240
- const procCaret = el('span', 'caret');
1241
- procCaret.append(icon('caret', 12));
1242
- summary.append(procCaret);
1243
- t.meta = el('span', 'p-meta', '工作中 0 秒');
1244
- summary.append(t.meta);
1245
- details.append(summary);
1246
- t.steps = el('div', 'proc-steps');
1247
- details.append(t.steps);
1268
+ t.meta = process.meta;
1269
+ t.meta.textContent = '工作中 0 秒';
1270
+ t.steps = process.steps;
1248
1271
  $('stream-col').append(details);
1249
1272
  t.proc = details;
1250
1273
  if (!state.replaying) {
@@ -1411,32 +1434,32 @@ function restoreBackgroundTasks(tasks) {
1411
1434
  renderBackgroundTasks();
1412
1435
  }
1413
1436
  function renderBackgroundTasks() {
1414
- const panel = $('background-tasks');
1437
+ let panel = $('background-tasks');
1438
+ if (!panel && $('stream-col')) { panel = el('section', 'background-tasks'); panel.id = 'background-tasks'; panel.setAttribute('aria-label', '子代理任务'); $('stream-col').append(panel); }
1415
1439
  if (!panel || !panel.dataset) return;
1416
- const signature = JSON.stringify([state.caps?.backgroundTasks, [...backgroundTasks.values()]]);
1440
+ const indexed = state.activeSessionId && dock && typeof dock.taskViews === 'function' ? dock.taskViews(state.activeSessionId) : [];
1441
+ const tasks = [...backgroundTasks.values()].filter(task => !indexed.some(view => view.taskId && view.taskId === task.taskId));
1442
+ tasks.push(...indexed);
1443
+ const signature = JSON.stringify([state.caps?.backgroundTasks, tasks]);
1417
1444
  if (panel.dataset?.signature === signature) return;
1418
1445
  if (panel.dataset) panel.dataset.signature = signature;
1419
1446
  panel.textContent = '';
1420
- const unsupported = state.caps?.backgroundTasks === false;
1421
- panel.hidden = !unsupported && !backgroundTasks.size;
1422
- if (unsupported) { panel.append(el('p', 'task-detail', '当前 Agent 未提供后台任务状态;文字声明不能证明任务已启动。')); return; }
1423
- const running = [...backgroundTasks.values()].filter(t => ['running', 'pending'].includes(t.status)).length;
1424
- panel.append(el('strong', null, `后台任务 · ${running} 个运行中`));
1447
+ const unsupported = state.caps?.backgroundTasks === false && !indexed.length;
1448
+ panel.hidden = !tasks.length;
1449
+ if (unsupported) return;
1450
+ const running = tasks.filter(t => ['running', 'pending'].includes(t.status)).length;
1451
+ panel.setAttribute('aria-label', '子代理任务');
1425
1452
  // 单个后台任务的精确停止 API 未交付:本切片没有任何停止入口,也不以
1426
1453
  // 主会话 abort 冒充(stopCurrent 仅属于前台发送按钮)。
1427
- for (const task of backgroundTasks.values()) {
1428
- const card = el('article', 'task-card');
1429
- card.dataset.taskId = task.taskId;
1430
- card.append(el('strong', null, task.description || task.taskId));
1431
- card.append(el('div', null, ({ pending: '待执行', running: '运行中', completed: '已完成', failed: '失败', stopped: '已停止' })[task.status] || task.status));
1432
- const detail = [task.lastToolName && `最近工具:${task.lastToolName}`, task.summary,
1433
- task.usage && `工具调用 ${task.usage.tool_uses ?? '未知'} · 用时 ${task.usage.duration_ms ?? '未知'}ms`].filter(Boolean).join('\n');
1434
- if (detail) card.append(el('div', 'task-detail', detail));
1435
- if (task.taskRef && dock) {
1436
- const open = el('button', 'linklike', '在右侧任务面板查看');
1437
- open.addEventListener('click', () => { dock.openTask(task.taskRef); });
1438
- card.append(open);
1439
- }
1454
+ for (const task of tasks) {
1455
+ const card = el('button', 'task-card task-row');
1456
+ card.type = 'button'; card.dataset.taskId = task.taskId || task.delegationId || '';
1457
+ card.append(icon('agent', 14), el('span', 'task-row-title', task.description || '子代理任务'));
1458
+ card.append(el('span', 'task-row-status', task.statusLabel || ({ pending: '待执行', running: '运行中', completed: '已完成', failed: '失败', stopped: '已停止' })[task.status] || '状态待确认'));
1459
+ card.append(icon('arrowLeft', 13));
1460
+ card.setAttribute('aria-label', '打开子代理:' + (task.description || '子代理任务'));
1461
+ card.disabled = !(task.taskRef || task.delegationId);
1462
+ card.addEventListener('click', () => { if (!dock) return; if (task.delegationId) dock.openDelegation(task.delegationId); else dock.openTask(task.taskRef, { reuse: true }); });
1440
1463
  panel.append(card);
1441
1464
  }
1442
1465
  }
@@ -1487,15 +1510,8 @@ function renderEvent(event) {
1487
1510
  state.completedReply = null;
1488
1511
  rememberTitleFromEvents([event]);
1489
1512
  if (state.activeSessionId && event.text) state.sessionTitles.set(state.activeSessionId, event.text.replace(/\s+/g, ' ').slice(0, 24));
1490
- const wrap = el('div', 'msg user');
1513
+ const wrap = buildConversationMessage('user', event.text || '');
1491
1514
  wrap.dataset.historyKey = historyEventKey(event);
1492
- const bubble = el('div', 'bubble', event.text || '');
1493
- wrap.append(bubble);
1494
- const copy = el('button', 'bubble-copy');
1495
- copy.title = '复制';
1496
- copy.append(icon('copy', 13));
1497
- copy.addEventListener('click', () => copyText(event.text || '', copy));
1498
- wrap.append(copy);
1499
1515
  if (Array.isArray(event.attachments) && event.attachments.length) {
1500
1516
  const tray = el('div', 'attach-tray');
1501
1517
  for (const item of event.attachments) {
@@ -1522,9 +1538,9 @@ function renderEvent(event) {
1522
1538
  stampThinking(t);
1523
1539
  const ctx = t || startTurn();
1524
1540
  if (!ctx.liveText || !ctx.liveText.isConnected) {
1525
- const msg = el('div', 'msg ai');
1526
- const body = el('div', 'md-body streaming');
1527
- msg.append(body);
1541
+ const msg = buildConversationMessage('assistant', '');
1542
+ const body = msg.querySelector('.md-body');
1543
+ body.classList.add('streaming');
1528
1544
  col.append(msg);
1529
1545
  ctx.liveText = msg;
1530
1546
  ctx.liveBody = body;
@@ -1570,26 +1586,21 @@ function renderEvent(event) {
1570
1586
  case 'claude/tool-result': {
1571
1587
  const target = col.querySelector(`.step[data-tool-id="${cssEscape(event.id || '')}"]`);
1572
1588
  const step = target || col.appendChild(buildToolStep({ id: event.id, name: event.toolName || '' }, {}));
1573
- const body = step.querySelector('.step-body');
1574
- if (body) {
1575
- const output = event.output && typeof event.output === 'object' ? JSON.stringify(event.output, null, 2) : String(event.output || '');
1576
- body.append(ioCard('text', output.slice(0, 4000), '输出'));
1577
- }
1578
- renderFileChanges(step, event, step._toolInput || {});
1579
- const meta = step.querySelector('.s-meta');
1580
- if (meta) {
1581
- const done = event.isError ? '失败' : '完成';
1582
- const seconds = state.replaying ? null : Math.round((Date.now() - (step._startedAt || Date.now())) / 1000);
1583
- meta.textContent = seconds == null ? done : `${done} · ${fmtElapsed(seconds * 1000)}`;
1584
- meta.classList.remove('running');
1585
- meta.classList.toggle('failed', Boolean(event.isError));
1586
- }
1587
- step.classList.add('done');
1588
- if (event.isError) step.classList.add('failed');
1589
+ finishToolStep(step, event, state.replaying);
1589
1590
  if (t) collectTurnChanges(t, event, step._toolInput || {});
1590
1591
  break;
1591
1592
  }
1592
1593
  case 'claude/permission-request': {
1594
+ if (col.querySelector(`[data-perm-id="${cssEscape(event.requestId || '')}"]`)) break;
1595
+ if (event.kind === 'question' && Array.isArray(event.questions)) {
1596
+ const sessionId = state.activeSessionId;
1597
+ const card = window.createUserQuestionCard({ document, event,
1598
+ storageKey: `kb.workbench.questions:${apiBase}:${sessionId}:${event.requestId}`,
1599
+ onSubmit: answers => client.resolvePermission({ sessionId, requestId: event.requestId, allow: true, answers }),
1600
+ });
1601
+ state.questionCards ||= new Map(); state.questionCards.set(event.requestId, card);
1602
+ col.append(card); setBusyState('pending-permission'); break;
1603
+ }
1593
1604
  const card = el('div', 'perm');
1594
1605
  card.dataset.permId = event.requestId || '';
1595
1606
  const pIcon = el('span', 'p-icon');
@@ -1612,7 +1623,8 @@ function renderEvent(event) {
1612
1623
  }
1613
1624
  case 'claude/permission-resolved': {
1614
1625
  const card = col.querySelector(`.perm[data-perm-id="${cssEscape(event.requestId || '')}"]`);
1615
- if (card) settlePermissionCard(card, event.allow === true);
1626
+ if (card?.settle) card.settle(event);
1627
+ else if (card) settlePermissionCard(card, event.allow === true);
1616
1628
  setBusyState('running');
1617
1629
  break;
1618
1630
  }
@@ -1639,11 +1651,7 @@ function renderEvent(event) {
1639
1651
  col.append(err);
1640
1652
  } else {
1641
1653
  if (!hadLiveText && event.result) {
1642
- const msg = el('div', 'msg ai');
1643
- const body = el('div', 'md-body');
1644
- msg.append(body);
1645
- col.append(msg);
1646
- setMarkdown(body, String(event.result));
1654
+ col.append(buildConversationMessage('assistant', String(event.result)));
1647
1655
  }
1648
1656
  const lastMsg = [...col.children].reverse().find(node => node.classList && node.classList.contains('ai'));
1649
1657
  if (lastMsg) lastMsg.append(msgFooter(String(event.result || lastMsg.textContent), lastPrompt, { model: turnModel, at: event.at }));
@@ -1749,7 +1757,37 @@ function renderEvent(event) {
1749
1757
  if (stream && state.follow) stream.scrollTop = stream.scrollHeight;
1750
1758
  }
1751
1759
 
1752
- function buildToolStep(event, input) {
1760
+ // Shared conversation primitives keep task views in the same product language.
1761
+ function buildConversationMessage(role, text) {
1762
+ const user = role === 'user';
1763
+ const node = el('div', user ? 'msg user' : 'msg ai');
1764
+ const body = el('div', user ? 'bubble' : 'md-body');
1765
+ if (user) body.textContent = text;
1766
+ else setMarkdown(body, text);
1767
+ node.append(body);
1768
+ if (user) {
1769
+ const copy = el('button', 'bubble-copy');
1770
+ copy.title = '复制';
1771
+ copy.append(icon('copy', 13));
1772
+ copy.addEventListener('click', () => copyText(text, copy));
1773
+ node.append(copy);
1774
+ }
1775
+ return node;
1776
+ }
1777
+
1778
+ function buildConversationProcess() {
1779
+ const node = el('details', 'proc');
1780
+ const summary = el('summary');
1781
+ const caret = el('span', 'caret');
1782
+ caret.append(icon('caret', 12));
1783
+ const meta = el('span', 'p-meta');
1784
+ const steps = el('div', 'proc-steps');
1785
+ summary.append(caret, meta);
1786
+ node.append(summary, steps);
1787
+ return { node, meta, steps };
1788
+ }
1789
+
1790
+ function buildToolStep(event, input, options = {}) {
1753
1791
  const step = el('details', 'step');
1754
1792
  step.dataset.toolId = event.id || '';
1755
1793
  step._toolInput = input;
@@ -1760,6 +1798,18 @@ function buildToolStep(event, input) {
1760
1798
  const caret = el('span', 's-caret');
1761
1799
  caret.append(icon('caret', 12));
1762
1800
  summary.append(caret, icon(TOOL_ICONS[verb] || 'dot', 13), el('span', 's-name', verb), el('span', 's-desc', toolDetail(event.name, input)), buildToolArgs(input), el('span', 's-meta running', '运行中'));
1801
+ if (options.taskLinks !== false && verb === '子任务' && event.id && dock && typeof dock.openTaskForToolUseId === 'function') {
1802
+ const open = el('button', 'task-open', '打开子代理');
1803
+ open.type = 'button';
1804
+ open.addEventListener('click', async ev => {
1805
+ ev.preventDefault();
1806
+ ev.stopPropagation();
1807
+ open.disabled = true;
1808
+ try { await dock.openTaskForToolUseId(event.id); }
1809
+ finally { open.disabled = false; }
1810
+ });
1811
+ summary.append(open);
1812
+ }
1763
1813
  step.append(summary);
1764
1814
  // Edit/write steps change the workspace; keep them visible when the fold opens.
1765
1815
  if (verb === '编辑' || verb === '写入') step.open = true;
@@ -1770,6 +1820,27 @@ function buildToolStep(event, input) {
1770
1820
  return step;
1771
1821
  }
1772
1822
 
1823
+ // Both the main conversation and task panels use the same tool/result and
1824
+ // file-change components, so read-only task views retain the product UI.
1825
+ function finishToolStep(step, event, replaying = true) {
1826
+ const body = step.querySelector('.step-body');
1827
+ if (body) {
1828
+ const output = event.output && typeof event.output === 'object' ? JSON.stringify(event.output, null, 2) : String(event.output ?? '');
1829
+ body.append(ioCard('text', output, '输出'));
1830
+ }
1831
+ renderFileChanges(step, event, step._toolInput || {});
1832
+ const meta = step.querySelector('.s-meta');
1833
+ if (meta) {
1834
+ const done = event.isError ? '失败' : '完成';
1835
+ const seconds = replaying ? null : Math.round((Date.now() - (step._startedAt || Date.now())) / 1000);
1836
+ meta.textContent = seconds == null ? done : `${done} · ${fmtElapsed(seconds * 1000)}`;
1837
+ meta.classList.remove('running');
1838
+ meta.classList.toggle('failed', Boolean(event.isError));
1839
+ }
1840
+ step.classList.add('done');
1841
+ if (event.isError) step.classList.add('failed');
1842
+ }
1843
+
1773
1844
  const TOOL_ICONS = {
1774
1845
  '命令': 'terminal', '读取': 'file', '写入': 'pencil', '编辑': 'pencil',
1775
1846
  '搜索': 'search', '网络': 'globe', '子任务': 'agent',
@@ -1800,6 +1871,7 @@ function toolVerb(name) {
1800
1871
 
1801
1872
  function toolDetail(name, input) {
1802
1873
  const tool = String(name || '').toLowerCase();
1874
+ if (tool === 'agent' || tool === 'task') return String(input.description || input.prompt || name).replace(/\s+/g, ' ').slice(0, 90);
1803
1875
  const file = input.file_path || input.path || input.filePath || '';
1804
1876
  if (tool === 'bash' && input.command) return String(input.command).replace(/\s+/g, ' ').slice(0, 90);
1805
1877
  if (file) return String(file).slice(0, 90);
@@ -2161,7 +2233,7 @@ function renderSessionStatus(activity) {
2161
2233
  status.hidden = false;
2162
2234
  text.textContent = ({
2163
2235
  running: '工作中', thinking: '思考中', 'tool-running': '执行工具', spawning: '启动中',
2164
- 'pending-permission': '等待授权', aborted: '已停止', failed: '失败',
2236
+ 'pending-permission': '等待你确认', aborted: '已停止', failed: '失败',
2165
2237
  }[sessionState] || sessionState) + (background > 0 ? ' · 后台运行中' : '');
2166
2238
  return;
2167
2239
  }
@@ -2216,6 +2288,8 @@ async function refreshSessionRow() { await renderProjectList(); }
2216
2288
 
2217
2289
  function wireComposer() {
2218
2290
  const input = $('chat-input');
2291
+ if (window.bindConversationComposer) window.bindConversationComposer({ input, send: $('btn-send'), onSend: sendCurrent, onStop: stopCurrent, isBusy: () => state.busy, onInput: saveCurrentDraft });
2292
+ else {
2219
2293
  input.addEventListener('input', () => {
2220
2294
  input.style.height = 'auto';
2221
2295
  input.style.height = `${Math.min(150, input.scrollHeight)}px`;
@@ -2227,6 +2301,7 @@ function wireComposer() {
2227
2301
  $('btn-send').addEventListener('click', () => {
2228
2302
  if (state.busy) { stopCurrent(); } else { sendCurrent(); }
2229
2303
  });
2304
+ }
2230
2305
  $('btn-attach').addEventListener('click', () => $('file-input').click());
2231
2306
  $('file-input').addEventListener('change', async event => {
2232
2307
  for (const file of event.target.files || []) await addAttachment(file);
@@ -3073,6 +3148,7 @@ function panelStatePayload(reason) {
3073
3148
  open: true,
3074
3149
  active: tab.id === snap.activeTabId,
3075
3150
  error: tab.hostError || null,
3151
+ ready: tab.hostReady === true,
3076
3152
  content: tab.content || null,
3077
3153
  scope: snap.scope ? { projectId: snap.scope.contextId, sessionId: snap.scope.parentSessionId } : null,
3078
3154
  })) : [];
@@ -3168,7 +3244,7 @@ window.addEventListener('message', async ev => {
3168
3244
  // hosts may only ever register same-origin iframe panels.
3169
3245
  const descriptor = Object.assign({}, data.panel, { kind: 'iframe', origin: ev.origin });
3170
3246
  const result = panelRegistry.register(descriptor, hostPanelOwner);
3171
- if (result.ok) postPanelState('register');
3247
+ if (result.ok) { dock.refreshProviders?.(); postPanelState('register'); }
3172
3248
  reply({ type: 'kb:panel/registered', ok: result.ok, code: result.code || null, error: result.message || null, providerId: result.ok ? result.providerId : null });
3173
3249
  return;
3174
3250
  }
@@ -0,0 +1,44 @@
1
+ (function (global) {
2
+ 'use strict';
3
+ function bindConversationComposer(options) {
4
+ const { input, send, onSend, onStop, onInput } = options;
5
+ input.addEventListener('input', () => {
6
+ input.style.height = 'auto';
7
+ input.style.height = Math.min(150, input.scrollHeight || 48) + 'px';
8
+ if (onInput) onInput(input.value);
9
+ });
10
+ input.addEventListener('keydown', event => {
11
+ if (event.key === 'Enter' && !event.shiftKey && !event.isComposing) {
12
+ event.preventDefault();
13
+ if (!options.isBusy || !options.isBusy()) onSend();
14
+ }
15
+ });
16
+ send.addEventListener('click', () => {
17
+ if (options.isBusy && options.isBusy()) { if (onStop) onStop(); }
18
+ else onSend();
19
+ });
20
+ }
21
+ function createConversationComposer(options) {
22
+ const doc = options.document;
23
+ const node = doc.createElement('div'); node.className = 'composer-wrap rd-composer-wrap';
24
+ const box = doc.createElement('div'); box.className = 'composer';
25
+ const input = doc.createElement('textarea'); input.rows = 1;
26
+ input.placeholder = options.placeholder || '给子代理发送消息';
27
+ input.setAttribute('aria-label', input.placeholder); input.value = options.value || '';
28
+ const row = doc.createElement('div'); row.className = 'cmdrow';
29
+ const label = doc.createElement('span'); label.className = 'rd-composer-label'; label.textContent = '跟随主对话 · 默认防护';
30
+ const send = doc.createElement('button'); send.type = 'button'; send.className = 'send';
31
+ if (options.icon) send.append(options.icon('send', 16)); else send.textContent = '↑';
32
+ send.setAttribute('aria-label', '发送子代理消息');
33
+ row.append(label, send); box.append(input, row); node.append(box);
34
+ bindConversationComposer({ ...options, input, send });
35
+ return { node, input, send, setLabel(text) { label.textContent = text; }, setBusy(busy) {
36
+ send.setAttribute('aria-label', busy ? '停止子代理' : '发送子代理消息');
37
+ send.textContent = '';
38
+ if (options.icon) send.append(options.icon(busy ? 'stop' : 'send', 16)); else send.textContent = busy ? '■' : '↑';
39
+ input.disabled = Boolean(options.disabled && options.disabled());
40
+ } };
41
+ }
42
+ global.bindConversationComposer = bindConversationComposer;
43
+ global.createConversationComposer = createConversationComposer;
44
+ })(typeof window === 'undefined' ? globalThis : window);