@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.
- package/package.json +4 -4
- package/src/components/AgentSidebar.tsx +27 -27
- package/src/components/BrandLockup.tsx +20 -4
- package/src/components/ChatHeader.tsx +5 -15
- package/src/components/LocalLlmSettingsModal.tsx +5 -5
- package/src/components/TerminalPanel.tsx +1 -1
- package/src/components/chat/ApprovalModal.tsx +5 -5
- package/src/components/chat/AskUserQuestionMenu.tsx +13 -13
- package/src/components/chat/AssistantMessage.tsx +122 -36
- package/src/components/chat/ChatInput.tsx +34 -26
- package/src/components/chat/ExecPolicyPicker.tsx +4 -4
- package/src/components/chat/ExecutedActionsCard.tsx +398 -29
- package/src/components/chat/FilePathCode.tsx +2 -1
- package/src/components/chat/LocalChatPanel.tsx +42 -11
- package/src/components/chat/Markdown.tsx +15 -14
- package/src/components/chat/MessageList.tsx +23 -5
- package/src/components/chat/ModelPicker.tsx +6 -6
- package/src/components/chat/OrchestrationChildrenCard.tsx +69 -5
- package/src/components/chat/SessionTodoList.tsx +234 -0
- package/src/components/chat/SessionTreeModal.tsx +3 -3
- package/src/components/chat/SuggestedReplies.tsx +14 -5
- package/src/components/chat/TaskPanelModal.tsx +3 -3
- package/src/components/chat/TaskProcessPanel.tsx +67 -21
- package/src/components/chat/TurnFilesCard.tsx +2 -2
- package/src/components/chat/TurnProcessGroup.tsx +252 -99
- package/src/components/chat/UserMessage.tsx +7 -7
- package/src/components/chat/executed-actions-model.ts +469 -0
- package/src/components/chat/next-steps-tags.ts +28 -0
- package/src/components/chat/orchestration-children-model.ts +120 -0
- package/src/components/chat/process-status.ts +125 -53
- package/src/components/chat/todo-list-model.ts +167 -0
- package/src/components/chat/turn-timeline.ts +123 -11
- package/src/components/settings/AgentsSettingsPanel.tsx +2 -2
- package/src/components/settings/AppearanceSettingsPanel.tsx +3 -3
- package/src/components/settings/DiagnoseSettingsPanel.tsx +2 -2
- package/src/components/settings/InsightsSettingsPanel.tsx +4 -4
- package/src/components/settings/LlmSettingsPanel.tsx +10 -10
- package/src/components/settings/McpSettingsPanel.tsx +3 -3
- package/src/components/settings/ModelIdCombobox.tsx +2 -2
- package/src/components/settings/SecuritySettingsPanel.tsx +1 -1
- package/src/components/settings/SettingsSaveButton.tsx +1 -1
- package/src/components/settings/SkillsSettingsPanel.tsx +2 -2
- package/src/components/settings/TelemetrySettingsPanel.tsx +2 -2
- package/src/components/settings/UsagePanel.tsx +14 -10
- package/src/components/settings/WebSearchSettingsPanel.tsx +2 -2
- package/src/layouts/AgentLayout.tsx +3 -3
- package/src/lib/chat-transport.ts +68 -12
- package/src/lib/local-api.ts +11 -0
- package/src/lib/show-thinking-content.ts +4 -4
- package/src/pages/AgentPage.tsx +82 -11
- package/src/pages/ChatPanelPreviewPage.tsx +26 -4
- package/src/pages/SettingsPage.tsx +39 -39
- 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-
|
|
400
|
+
<div className="space-y-3">
|
|
401
401
|
{loading ? (
|
|
402
|
-
<div className="flex items-center gap-2 py-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
85
|
-
<div className="bg-agent-muted/30 border border-agent-border/60 rounded-agent-md p-
|
|
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
|
-
|
|
58
|
-
return
|
|
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-
|
|
90
|
-
<div className="bg-agent-muted/30 border border-agent-border/60 rounded-agent-md p-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
|
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-
|
|
89
|
-
<div className="bg-agent-muted/30 border border-agent-border/60 rounded-agent-md p-
|
|
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-
|
|
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 -
|
|
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-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/lib/local-api.ts
CHANGED
|
@@ -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: '
|
|
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 {
|