@steerable/agent-shell-web 0.6.27 → 0.6.29

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 (53) hide show
  1. package/package.json +4 -4
  2. package/src/components/AgentSidebar.tsx +27 -27
  3. package/src/components/BrandLockup.tsx +20 -4
  4. package/src/components/ChatHeader.tsx +5 -15
  5. package/src/components/LocalLlmSettingsModal.tsx +5 -5
  6. package/src/components/TerminalPanel.tsx +1 -1
  7. package/src/components/chat/ApprovalModal.tsx +5 -5
  8. package/src/components/chat/AskUserQuestionMenu.tsx +13 -13
  9. package/src/components/chat/AssistantMessage.tsx +122 -36
  10. package/src/components/chat/ChatInput.tsx +34 -26
  11. package/src/components/chat/ExecPolicyPicker.tsx +4 -4
  12. package/src/components/chat/ExecutedActionsCard.tsx +398 -29
  13. package/src/components/chat/FilePathCode.tsx +2 -1
  14. package/src/components/chat/LocalChatPanel.tsx +42 -11
  15. package/src/components/chat/Markdown.tsx +15 -14
  16. package/src/components/chat/MessageList.tsx +23 -5
  17. package/src/components/chat/ModelPicker.tsx +6 -6
  18. package/src/components/chat/OrchestrationChildrenCard.tsx +69 -5
  19. package/src/components/chat/SessionTodoList.tsx +234 -0
  20. package/src/components/chat/SessionTreeModal.tsx +3 -3
  21. package/src/components/chat/SuggestedReplies.tsx +14 -5
  22. package/src/components/chat/TaskPanelModal.tsx +3 -3
  23. package/src/components/chat/TaskProcessPanel.tsx +67 -21
  24. package/src/components/chat/TurnFilesCard.tsx +2 -2
  25. package/src/components/chat/TurnProcessGroup.tsx +252 -99
  26. package/src/components/chat/UserMessage.tsx +7 -7
  27. package/src/components/chat/executed-actions-model.ts +469 -0
  28. package/src/components/chat/next-steps-tags.ts +28 -0
  29. package/src/components/chat/orchestration-children-model.ts +120 -0
  30. package/src/components/chat/process-status.ts +125 -53
  31. package/src/components/chat/todo-list-model.ts +167 -0
  32. package/src/components/chat/turn-timeline.ts +123 -11
  33. package/src/components/settings/AgentsSettingsPanel.tsx +2 -2
  34. package/src/components/settings/AppearanceSettingsPanel.tsx +3 -3
  35. package/src/components/settings/DiagnoseSettingsPanel.tsx +2 -2
  36. package/src/components/settings/InsightsSettingsPanel.tsx +4 -4
  37. package/src/components/settings/LlmSettingsPanel.tsx +10 -10
  38. package/src/components/settings/McpSettingsPanel.tsx +3 -3
  39. package/src/components/settings/ModelIdCombobox.tsx +2 -2
  40. package/src/components/settings/SecuritySettingsPanel.tsx +1 -1
  41. package/src/components/settings/SettingsSaveButton.tsx +1 -1
  42. package/src/components/settings/SkillsSettingsPanel.tsx +2 -2
  43. package/src/components/settings/TelemetrySettingsPanel.tsx +2 -2
  44. package/src/components/settings/UsagePanel.tsx +14 -10
  45. package/src/components/settings/WebSearchSettingsPanel.tsx +2 -2
  46. package/src/layouts/AgentLayout.tsx +3 -3
  47. package/src/lib/chat-transport.ts +68 -12
  48. package/src/lib/local-api.ts +11 -0
  49. package/src/lib/show-thinking-content.ts +4 -4
  50. package/src/pages/AgentPage.tsx +82 -11
  51. package/src/pages/ChatPanelPreviewPage.tsx +26 -4
  52. package/src/pages/SettingsPage.tsx +39 -39
  53. package/src/styles/index.css +12 -2
@@ -397,9 +397,9 @@ export const LlmSettingsPanel = forwardRef<LlmSettingsPanelHandle, LlmSettingsPa
397
397
  }, [saving, savedOk, loading, onSaveUiChange]);
398
398
 
399
399
  return (
400
- <div className="space-y-4">
400
+ <div className="space-y-3">
401
401
  {loading ? (
402
- <div className="flex items-center gap-2 py-4 text-sm text-agent-muted-foreground">
402
+ <div className="flex items-center gap-2 py-2 text-xs text-agent-muted-foreground">
403
403
  <LuLoaderCircle className="h-4 w-4 animate-spin" />
404
404
  加载当前配置...
405
405
  </div>
@@ -413,7 +413,7 @@ export const LlmSettingsPanel = forwardRef<LlmSettingsPanelHandle, LlmSettingsPa
413
413
  data-testid="llm-vendor-select"
414
414
  value={selectedVendor?.id ?? 'custom'}
415
415
  onChange={(e) => handleSwitchVendor(e.target.value)}
416
- className="h-9 w-full rounded-agent-md border border-agent-border bg-agent-canvas px-2 text-sm text-agent-foreground focus:outline-none focus:ring-2 focus:ring-agent-foreground/30"
416
+ className="h-8 w-full rounded-agent-md border border-agent-border bg-agent-canvas px-2 text-xs text-agent-foreground focus:outline-none focus:ring-2 focus:ring-agent-foreground/30"
417
417
  >
418
418
  {featuredVendors.length > 0 && (
419
419
  <optgroup label="常用">
@@ -465,7 +465,7 @@ export const LlmSettingsPanel = forwardRef<LlmSettingsPanelHandle, LlmSettingsPa
465
465
  ? 'https://your-gateway.example/v1'
466
466
  : '该服务商没有默认地址,请手动填写')
467
467
  }
468
- className="h-9 w-full rounded-agent-md border border-agent-border bg-agent-canvas px-3 text-sm text-agent-foreground focus:outline-none focus:ring-2 focus:ring-agent-foreground/30"
468
+ className="h-8 w-full rounded-agent-md border border-agent-border bg-agent-canvas px-3 text-xs text-agent-foreground focus:outline-none focus:ring-2 focus:ring-agent-foreground/30"
469
469
  />
470
470
  <p className="mt-1.5 text-[11px] text-agent-muted-foreground">
471
471
  {selectedVendor?.apiBaseUrl
@@ -491,7 +491,7 @@ export const LlmSettingsPanel = forwardRef<LlmSettingsPanelHandle, LlmSettingsPa
491
491
  ? 'sk-... (可选,私有部署可留空)'
492
492
  : 'sk-... (必填,到服务商控制台创建)'
493
493
  }
494
- className="h-9 min-w-0 flex-1 rounded-agent-md border border-agent-border bg-agent-canvas px-3 text-sm text-agent-foreground focus:outline-none focus:ring-2 focus:ring-agent-foreground/30"
494
+ className="h-8 min-w-0 flex-1 rounded-agent-md border border-agent-border bg-agent-canvas px-3 text-xs text-agent-foreground focus:outline-none focus:ring-2 focus:ring-agent-foreground/30"
495
495
  />
496
496
  <button
497
497
  type="button"
@@ -499,7 +499,7 @@ export const LlmSettingsPanel = forwardRef<LlmSettingsPanelHandle, LlmSettingsPa
499
499
  title="用当前 URL 和 Key 向网关拉一次模型目录"
500
500
  disabled={!settings.baseUrl?.trim() || keyTest.status === 'testing'}
501
501
  onClick={() => void handleTestKey()}
502
- className="inline-flex h-9 shrink-0 items-center justify-center rounded-agent-md border border-agent-border bg-agent-canvas px-3 text-xs font-medium text-agent-foreground transition-colors hover:bg-agent-foreground/5 disabled:cursor-not-allowed disabled:opacity-50"
502
+ className="inline-flex h-8 shrink-0 items-center justify-center rounded-agent-md border border-agent-border bg-agent-canvas px-3 text-xs font-medium text-agent-foreground transition-colors hover:bg-agent-foreground/5 disabled:cursor-not-allowed disabled:opacity-50"
503
503
  >
504
504
  {keyTest.status === 'testing' ? (
505
505
  <LuLoaderCircle className="h-4 w-4 animate-spin" />
@@ -551,7 +551,7 @@ export const LlmSettingsPanel = forwardRef<LlmSettingsPanelHandle, LlmSettingsPa
551
551
  aria-label="刷新模型目录"
552
552
  disabled={!settings.baseUrl?.trim() || modelsRefreshing}
553
553
  onClick={() => void handleRefreshModels()}
554
- className="inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-agent-md border border-agent-border bg-agent-canvas text-agent-muted-foreground transition-colors hover:bg-agent-foreground/5 hover:text-agent-foreground disabled:cursor-not-allowed disabled:opacity-50"
554
+ className="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-agent-md border border-agent-border bg-agent-canvas text-agent-muted-foreground transition-colors hover:bg-agent-foreground/5 hover:text-agent-foreground disabled:cursor-not-allowed disabled:opacity-50"
555
555
  >
556
556
  <LuRefreshCw className={`h-4 w-4 ${modelsRefreshing ? 'animate-spin' : ''}`} />
557
557
  </button>
@@ -819,7 +819,7 @@ export const LlmSettingsPanel = forwardRef<LlmSettingsPanelHandle, LlmSettingsPa
819
819
  setSettings((prev) => ({ ...prev, maxTotalTokens: isNaN(val) ? undefined : val }));
820
820
  }}
821
821
  placeholder="60000"
822
- className="h-9 w-full rounded-agent-md border border-agent-border bg-agent-canvas px-3 text-sm text-agent-foreground focus:outline-none focus:ring-2 focus:ring-agent-foreground/30"
822
+ className="h-8 w-full rounded-agent-md border border-agent-border bg-agent-canvas px-3 text-xs text-agent-foreground focus:outline-none focus:ring-2 focus:ring-agent-foreground/30"
823
823
  />
824
824
  <p className="mt-1.5 text-[11px] text-agent-muted-foreground">
825
825
  单次对话中累积消耗的最大 Token 数量(默认 60,000)。超限后将自动停止,防止模型死循环或意外消耗过多 Token。
@@ -827,7 +827,7 @@ export const LlmSettingsPanel = forwardRef<LlmSettingsPanelHandle, LlmSettingsPa
827
827
  </div>
828
828
 
829
829
  {/* ───── 超时设置 ───── */}
830
- <div className="border-t border-agent-border/60 pt-4 space-y-4">
830
+ <div className="border-t border-agent-border/60 pt-3 space-y-3">
831
831
  <h4 className="text-xs font-semibold text-agent-muted-foreground uppercase tracking-wide">
832
832
  超时设置
833
833
  </h4>
@@ -842,7 +842,7 @@ export const LlmSettingsPanel = forwardRef<LlmSettingsPanelHandle, LlmSettingsPa
842
842
  value={execTimeoutInput}
843
843
  onChange={(e) => setExecTimeoutInput(e.target.value)}
844
844
  placeholder="留空 = 默认(后台 30s / 终端 60s)"
845
- className="h-9 w-full rounded-agent-md border border-agent-border bg-agent-canvas px-3 text-sm text-agent-foreground focus:outline-none focus:ring-2 focus:ring-agent-foreground/30"
845
+ className="h-8 w-full rounded-agent-md border border-agent-border bg-agent-canvas px-3 text-xs text-agent-foreground focus:outline-none focus:ring-2 focus:ring-agent-foreground/30"
846
846
  />
847
847
  <p className="mt-1.5 text-[11px] text-agent-muted-foreground">
848
848
  执行本地命令时未显式指定超时的默认等待时长。命令行带 “gui” 的启动命令不受此限制:超时后按“程序已启动、仍在运行”处理,不会被判为失败或重复启动。
@@ -230,9 +230,9 @@ export function McpSettingsPanel() {
230
230
  };
231
231
 
232
232
  return (
233
- <div className="space-y-4">
233
+ <div className="space-y-3">
234
234
  {/* JSON 导入 */}
235
- <div className="bg-agent-muted/30 border border-agent-border/60 rounded-agent-md p-3.5 space-y-2">
235
+ <div className="bg-agent-muted/30 border border-agent-border/60 rounded-agent-md p-2.5 space-y-2">
236
236
  <h4 className="text-xs font-semibold text-agent-foreground flex items-center gap-1.5">
237
237
  <LuPlug className="h-3.5 w-3.5 text-agent-muted-foreground" />
238
238
  粘贴 JSON 导入 (Claude Desktop 格式)
@@ -271,7 +271,7 @@ export function McpSettingsPanel() {
271
271
 
272
272
  {/* 手动添加 / 编辑表单 */}
273
273
  {mcpFormOpen ? (
274
- <div className="bg-agent-muted/30 border border-agent-border/60 rounded-agent-md p-3.5 space-y-2.5">
274
+ <div className="bg-agent-muted/30 border border-agent-border/60 rounded-agent-md p-2.5 space-y-2.5">
275
275
  <h4 className="text-xs font-semibold text-agent-foreground">
276
276
  {mcpEditingId ? '编辑 MCP 服务' : '手动添加 MCP 服务'}
277
277
  </h4>
@@ -119,7 +119,7 @@ export function ModelIdCombobox({
119
119
  setOpen(false);
120
120
  }}
121
121
  className={[
122
- 'flex w-full flex-col gap-1 px-3 py-1.5 text-left text-sm',
122
+ 'flex w-full flex-col gap-0.5 px-2.5 py-1 text-left text-xs',
123
123
  selected
124
124
  ? 'bg-agent-foreground/10 text-agent-foreground'
125
125
  : 'text-agent-foreground hover:bg-agent-foreground/5',
@@ -159,7 +159,7 @@ export function ModelIdCombobox({
159
159
  }}
160
160
  onFocus={show}
161
161
  placeholder={placeholder}
162
- className="h-9 w-full rounded-agent-md border border-agent-border bg-agent-canvas px-3 pr-8 text-sm text-agent-foreground focus:outline-none focus:ring-2 focus:ring-agent-foreground/30"
162
+ className="h-8 w-full rounded-agent-md border border-agent-border bg-agent-canvas px-2.5 pr-8 text-xs text-agent-foreground focus:outline-none focus:ring-2 focus:ring-agent-foreground/30"
163
163
  />
164
164
  <button
165
165
  type="button"
@@ -122,7 +122,7 @@ export function SecuritySettingsPanel() {
122
122
  const optOutReason = posture ? OPT_OUT_REASON_TEXT[posture.reason] : undefined;
123
123
 
124
124
  return (
125
- <div className="bg-agent-muted/30 border border-agent-border/60 rounded-agent-md p-3.5 space-y-3">
125
+ <div className="bg-agent-muted/30 border border-agent-border/60 rounded-agent-md p-2.5 space-y-2">
126
126
  <h4 className="text-xs font-semibold text-agent-foreground flex items-center gap-1.5">
127
127
  <LuShieldCheck className="h-3.5 w-3.5 text-agent-muted-foreground" />
128
128
  Sidecar 进程沙箱
@@ -22,7 +22,7 @@ export function SettingsSaveButton({
22
22
  data-testid={testId}
23
23
  onClick={onClick}
24
24
  disabled={disabled}
25
- className="flex h-8 items-center gap-1.5 rounded-full bg-agent-foreground px-5 text-xs font-medium text-agent-canvas transition-all hover:opacity-90 disabled:opacity-50"
25
+ className="flex h-7 items-center gap-1.5 rounded-full bg-agent-foreground px-3 text-xs font-medium text-agent-canvas transition-all hover:opacity-90 disabled:opacity-50"
26
26
  >
27
27
  {saving ? (
28
28
  <LuLoaderCircle className="h-3.5 w-3.5 animate-spin" />
@@ -153,9 +153,9 @@ export function SkillsSettingsPanel() {
153
153
  };
154
154
 
155
155
  return (
156
- <div className="space-y-4">
156
+ <div className="space-y-3">
157
157
  {/* Import Skill Bar */}
158
- <div className="bg-agent-muted/30 border border-agent-border/60 rounded-agent-md p-3.5 space-y-2">
158
+ <div className="bg-agent-muted/30 border border-agent-border/60 rounded-agent-md p-2.5 space-y-2">
159
159
  <h4 className="text-xs font-semibold text-agent-foreground flex items-center gap-1.5">
160
160
  <LuBlocks className="h-3.5 w-3.5 text-agent-muted-foreground" />
161
161
  导入本地技能目录 (Import Local Skill Directory)
@@ -81,8 +81,8 @@ export function TelemetrySettingsPanel() {
81
81
  };
82
82
 
83
83
  return (
84
- <div className="space-y-4">
85
- <div className="bg-agent-muted/30 border border-agent-border/60 rounded-agent-md p-3.5 space-y-3">
84
+ <div className="space-y-3">
85
+ <div className="bg-agent-muted/30 border border-agent-border/60 rounded-agent-md p-2.5 space-y-2">
86
86
  <h4 className="text-xs font-semibold text-agent-foreground flex items-center gap-1.5">
87
87
  <LuActivity className="h-3.5 w-3.5 text-agent-muted-foreground" />
88
88
  OTLP Collector
@@ -7,7 +7,7 @@ import { getElectronBridge, isElectron } from '@/lib/electron-bridge';
7
7
  *
8
8
  * 每轮 CoreLoop 结束后,local-backend 把该轮的累计 token 用量(以及按
9
9
  * framework pricing 单价表估算的 USD 成本)落进 `usage_events` 表;本面板
10
- * 按 model 分桶聚合展示近 N 天的用量与成本。
10
+ * 按 model 分桶聚合展示近 N 天的用量,成本按固定汇率换算为人民币。
11
11
  *
12
12
  * 成本口径:只统计有单价的模型(framework `MODEL_PRICES`);本地/未知模型
13
13
  * 无单价,token 照常计入但成本列渲染为 "—",不计入总成本。
@@ -52,10 +52,14 @@ function formatTokens(n: number): string {
52
52
  return String(n);
53
53
  }
54
54
 
55
+ /** 展示用估算汇率。底层仍存 USD,仅在面板换成人民币。 */
56
+ const USD_TO_CNY = 7.2;
57
+
55
58
  function formatCost(usd: number | null): string {
56
59
  if (usd === null) return '—';
57
- if (usd < 0.01) return `$${usd.toFixed(4)}`;
58
- return `$${usd.toFixed(2)}`;
60
+ const cny = usd * USD_TO_CNY;
61
+ if (cny < 0.01) return `¥${cny.toFixed(4)}`;
62
+ return `¥${cny.toFixed(2)}`;
59
63
  }
60
64
 
61
65
  export function UsagePanel() {
@@ -86,8 +90,8 @@ export function UsagePanel() {
86
90
  }, [fetchSummary, days]);
87
91
 
88
92
  return (
89
- <div className="space-y-4">
90
- <div className="bg-agent-muted/30 border border-agent-border/60 rounded-agent-md p-3.5 space-y-3">
93
+ <div className="space-y-3">
94
+ <div className="bg-agent-muted/30 border border-agent-border/60 rounded-agent-md p-2.5 space-y-2">
91
95
  <div className="flex items-center justify-between">
92
96
  <h4 className="text-xs font-semibold text-agent-foreground flex items-center gap-1.5">
93
97
  <LuChartBar className="h-3.5 w-3.5 text-agent-muted-foreground" />
@@ -135,19 +139,19 @@ export function UsagePanel() {
135
139
  <div className="grid grid-cols-4 gap-2">
136
140
  <div className="rounded-agent-md border border-agent-border bg-agent-canvas px-2.5 py-2">
137
141
  <div className="text-[10px] text-agent-muted-foreground">总轮次</div>
138
- <div className="text-sm font-semibold text-agent-foreground">{summary.totals.turns}</div>
142
+ <div className="text-xs font-semibold text-agent-foreground">{summary.totals.turns}</div>
139
143
  </div>
140
144
  <div className="rounded-agent-md border border-agent-border bg-agent-canvas px-2.5 py-2">
141
145
  <div className="text-[10px] text-agent-muted-foreground">总 Token</div>
142
- <div className="text-sm font-semibold text-agent-foreground">{formatTokens(summary.totals.totalTokens)}</div>
146
+ <div className="text-xs font-semibold text-agent-foreground">{formatTokens(summary.totals.totalTokens)}</div>
143
147
  </div>
144
148
  <div className="rounded-agent-md border border-agent-border bg-agent-canvas px-2.5 py-2">
145
149
  <div className="text-[10px] text-agent-muted-foreground">缓存命中</div>
146
- <div className="text-sm font-semibold text-agent-foreground">{formatTokens(summary.totals.cachedPromptTokens)}</div>
150
+ <div className="text-xs font-semibold text-agent-foreground">{formatTokens(summary.totals.cachedPromptTokens)}</div>
147
151
  </div>
148
152
  <div className="rounded-agent-md border border-agent-border bg-agent-canvas px-2.5 py-2">
149
153
  <div className="text-[10px] text-agent-muted-foreground">估算成本</div>
150
- <div className="text-sm font-semibold text-agent-foreground">{formatCost(summary.totals.costUsd)}</div>
154
+ <div className="text-xs font-semibold text-agent-foreground">{formatCost(summary.totals.costUsd)}</div>
151
155
  </div>
152
156
  </div>
153
157
 
@@ -180,7 +184,7 @@ export function UsagePanel() {
180
184
  </div>
181
185
 
182
186
  <p className="text-[10px] text-agent-muted-foreground">
183
- 成本按 framework 单价表估算,只统计有单价的模型;"—" 表示该模型无单价(本地/未知)。缓存命中为命中 prompt 缓存的 token 数。
187
+ 成本按 framework 单价表估算后按约 1 美元 = 7.2 元换算为人民币,只统计有单价的模型;"—" 表示该模型无单价(本地/未知)。缓存命中为命中 prompt 缓存的 token 数。
184
188
  </p>
185
189
  </>
186
190
  )}
@@ -85,8 +85,8 @@ export function WebSearchSettingsPanel() {
85
85
  };
86
86
 
87
87
  return (
88
- <div className="space-y-4">
89
- <div className="bg-agent-muted/30 border border-agent-border/60 rounded-agent-md p-3.5 space-y-3">
88
+ <div className="space-y-3">
89
+ <div className="bg-agent-muted/30 border border-agent-border/60 rounded-agent-md p-2.5 space-y-2">
90
90
  <h4 className="text-xs font-semibold text-agent-foreground flex items-center gap-1.5">
91
91
  <LuSearch className="h-3.5 w-3.5 text-agent-muted-foreground" />
92
92
  网络搜索
@@ -394,14 +394,14 @@ function AgentLayoutContent() {
394
394
  }, [chatId, persistRightPanel]);
395
395
 
396
396
  // Same drag ergonomics as the sidebar handle, mirrored: the terminal's
397
- // right edge is pinned to the window's right padding (p-2 = 8px), so the
397
+ // right edge is pinned to the window's right padding (p-1.5 = 6px), so the
398
398
  // width is the distance from the cursor to that edge.
399
399
  useEffect(() => {
400
400
  if (!isTerminalResizing) return;
401
401
  const onMove = (e: MouseEvent) => {
402
402
  const next = Math.min(
403
403
  MAX_TERMINAL_WIDTH,
404
- Math.max(MIN_TERMINAL_WIDTH, window.innerWidth - 8 - e.clientX),
404
+ Math.max(MIN_TERMINAL_WIDTH, window.innerWidth - 6 - e.clientX),
405
405
  );
406
406
  terminalWidthRef.current = next;
407
407
  setTerminalWidth(next);
@@ -484,7 +484,7 @@ function AgentLayoutContent() {
484
484
  />
485
485
  </>
486
486
  )}
487
- <div className="h-full min-w-0 flex-1 p-2">
487
+ <div className="h-full min-w-0 flex-1 p-1.5">
488
488
  <div className="flex h-full w-full">
489
489
  <div className="flex h-full min-w-0 flex-1 flex-col overflow-hidden rounded-agent-lg bg-agent-canvas shadow-sm">
490
490
  <div className="min-h-0 flex-1 overflow-hidden">
@@ -53,8 +53,18 @@ import {
53
53
  } from '@steerable/agent-ui/state';
54
54
  import type { SSEEvent } from '@steerable/agent-protocol';
55
55
  import { getElectronBridge } from './electron-bridge';
56
- import { appendDelta, syncTools, type TurnBlock } from '@/components/chat/turn-timeline';
56
+ import {
57
+ appendDelta,
58
+ freezeReasoningDurations,
59
+ sealLastBlock,
60
+ syncTools,
61
+ type TurnBlock,
62
+ } from '@/components/chat/turn-timeline';
57
63
  import type { ExecutedAction } from '@/components/chat/ExecutedActionsCard';
64
+ import {
65
+ LlmRequestSpeedTracker,
66
+ type LlmSpeedSnapshot,
67
+ } from '@/components/chat/process-status';
58
68
 
59
69
  // ---------------------------------------------------------------------------
60
70
  // Local-backend SSE adapter — feeds opaque IPC chunks into the framework's
@@ -68,6 +78,7 @@ class LocalBackendSseAdapter {
68
78
  private completed = false;
69
79
  private readonly parser: SSEParser;
70
80
  private timeline: TurnBlock[] = [];
81
+ private readonly speed = new LlmRequestSpeedTracker();
71
82
 
72
83
  constructor(private readonly onEvent: (event: SSEEvent) => void) {
73
84
  this.parser = new SSEParser({
@@ -78,10 +89,7 @@ class LocalBackendSseAdapter {
78
89
  if (ev) this.handleNormalised(ev);
79
90
  },
80
91
  onComplete: () => {
81
- if (!this.completed) {
82
- this.completed = true;
83
- this.onEvent({ type: 'done' });
84
- }
92
+ this.finishStream();
85
93
  },
86
94
  });
87
95
  }
@@ -92,10 +100,31 @@ class LocalBackendSseAdapter {
92
100
 
93
101
  end(): void {
94
102
  this.parser.end();
95
- if (!this.completed) {
96
- this.completed = true;
97
- this.onEvent({ type: 'done' });
103
+ this.finishStream();
104
+ }
105
+
106
+ private finishStream(): void {
107
+ if (this.completed) return;
108
+ this.completed = true;
109
+ const frozen = freezeReasoningDurations(this.timeline);
110
+ if (frozen !== this.timeline) {
111
+ this.timeline = frozen;
112
+ this.onEvent({
113
+ type: 'agent',
114
+ event: 'turn_timeline',
115
+ payload: { blocks: this.timeline },
116
+ });
98
117
  }
118
+ this.emitLlmSpeed(this.speed.endRequest());
119
+ this.onEvent({ type: 'done' });
120
+ }
121
+
122
+ private emitLlmSpeed(snapshot: LlmSpeedSnapshot): void {
123
+ this.onEvent({
124
+ type: 'agent',
125
+ event: 'llm_speed',
126
+ payload: snapshot,
127
+ });
99
128
  }
100
129
 
101
130
  private handleNormalised(event: SSEEvent): void {
@@ -108,13 +137,11 @@ class LocalBackendSseAdapter {
108
137
  event: 'budget_exhausted_suppressed',
109
138
  payload: { reason: event.message ?? 'budget_exhausted' },
110
139
  } as any);
111
- if (!this.completed) {
112
- this.completed = true;
113
- this.onEvent({ type: 'done' });
114
- }
140
+ this.finishStream();
115
141
  return;
116
142
  }
117
143
  const timelineChanged = this.applyToTimeline(event);
144
+ const speedSnapshot = this.applyLlmClock(event);
118
145
  this.onEvent(event);
119
146
  if (timelineChanged) {
120
147
  this.onEvent({
@@ -123,6 +150,28 @@ class LocalBackendSseAdapter {
123
150
  payload: { blocks: this.timeline },
124
151
  });
125
152
  }
153
+ if (speedSnapshot) this.emitLlmSpeed(speedSnapshot);
154
+ }
155
+
156
+ private applyLlmClock(event: SSEEvent): LlmSpeedSnapshot | null {
157
+ if (event.type === 'content' && typeof event.content === 'string' && event.content.length > 0) {
158
+ return this.speed.noteOutput(event.content);
159
+ }
160
+ if (event.type !== 'agent') return null;
161
+ if (event.event === 'reasoning') {
162
+ const delta =
163
+ typeof event.payload?.content === 'string'
164
+ ? event.payload.content
165
+ : typeof event.payload?.delta === 'string'
166
+ ? event.payload.delta
167
+ : '';
168
+ if (!delta) return null;
169
+ return this.speed.noteOutput(delta);
170
+ }
171
+ if (event.event === 'round_end') {
172
+ return this.speed.endRequest();
173
+ }
174
+ return null;
126
175
  }
127
176
 
128
177
  private applyToTimeline(event: SSEEvent): boolean {
@@ -148,6 +197,13 @@ class LocalBackendSseAdapter {
148
197
  this.timeline = syncTools(this.timeline, actions);
149
198
  return true;
150
199
  }
200
+ if (event.event === 'round_end') {
201
+ if (event.payload?.status === 'cancelled') return false;
202
+ const next = sealLastBlock(this.timeline);
203
+ if (next === this.timeline) return false;
204
+ this.timeline = next;
205
+ return true;
206
+ }
151
207
  return false;
152
208
  }
153
209
  }
@@ -439,6 +439,17 @@ export async function getTaskProcess(taskId: string) {
439
439
  });
440
440
  }
441
441
 
442
+ /**
443
+ * 子代理(`delegate_subagent`)的推理过程:子回合写自己的 durable record,
444
+ * 这里按 record 读回它的思考与工具调用。
445
+ */
446
+ export async function getChildProcess(recordId: string) {
447
+ return bridge().localBackend.request<{ recordId: string; timeline: unknown[] }>({
448
+ method: 'GET',
449
+ path: `/api/v2/child-process?recordId=${encodeURIComponent(recordId)}`,
450
+ });
451
+ }
452
+
442
453
  /** 把已完成 worktree 任务的分支合并回主仓当前分支(4.6c)。 */
443
454
  export async function mergeTaskWorktree(taskId: string) {
444
455
  return bridge().localBackend.request<{ success: boolean; task: LocalTask }>({
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * 聊天里「显示思考内容」的本机偏好:隐藏 / 显示 5 行 / 完整显示。
3
3
  *
4
- * 默认「显示 5 行」:流式中露出固定行数,结束后折叠,只留状态行。
4
+ * 默认「显示 5 行」:流式中按轮次各露出 5 行,结束后折叠工作区,只留状态行。
5
5
  * 存在 localStorage,设置页切换立即生效,不走后端。
6
6
  */
7
7
 
@@ -19,9 +19,9 @@ export const THINKING_DISPLAY_OPTIONS: ReadonlyArray<{
19
19
  label: string;
20
20
  hint: string;
21
21
  }> = [
22
- { mode: 'hidden', label: '隐藏', hint: '只保留状态行,不展示思考正文。' },
23
- { mode: 'peek', label: '显示5行', hint: '流式中露出 5 行思考,结束后自动折叠。' },
24
- { mode: 'full', label: '完整显示', hint: '流式中展开全部思考与工具过程,结束后可手动折叠。' },
22
+ { mode: 'hidden', label: '隐藏', hint: '不展示思考正文,只保留工作状态行。' },
23
+ { mode: 'peek', label: '显示5行', hint: '当前轮思考最多露出 5 行,不足不撑开;该轮结束后自动折叠。完整正文请用「完整显示」。' },
24
+ { mode: 'full', label: '完整显示', hint: '当前轮思考完整展开,该轮结束后自动折叠;工作行结束后仍自动折叠。' },
25
25
  ];
26
26
 
27
27
  export function isThinkingDisplayMode(value: unknown): value is ThinkingDisplayMode {