@steerable/agent-shell-web 0.6.27 → 0.6.28
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 +24 -24
- package/src/components/BrandLockup.tsx +1 -1
- 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 +41 -13
- package/src/components/chat/ChatInput.tsx +24 -24
- package/src/components/chat/ExecPolicyPicker.tsx +4 -4
- package/src/components/chat/ExecutedActionsCard.tsx +10 -3
- package/src/components/chat/FilePathCode.tsx +2 -1
- package/src/components/chat/LocalChatPanel.tsx +39 -4
- package/src/components/chat/Markdown.tsx +15 -14
- package/src/components/chat/MessageList.tsx +16 -3
- package/src/components/chat/ModelPicker.tsx +6 -6
- package/src/components/chat/SessionTodoList.tsx +137 -0
- package/src/components/chat/SessionTreeModal.tsx +3 -3
- package/src/components/chat/SuggestedReplies.tsx +5 -4
- package/src/components/chat/TaskPanelModal.tsx +3 -3
- package/src/components/chat/TaskProcessPanel.tsx +5 -4
- package/src/components/chat/TurnFilesCard.tsx +2 -2
- package/src/components/chat/TurnProcessGroup.tsx +226 -99
- package/src/components/chat/UserMessage.tsx +4 -4
- package/src/components/chat/next-steps-tags.ts +28 -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 +100 -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/show-thinking-content.ts +4 -4
- package/src/pages/AgentPage.tsx +56 -6
- 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
|
}
|
|
@@ -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 {
|
package/src/pages/AgentPage.tsx
CHANGED
|
@@ -26,6 +26,10 @@ import type { ExecutedAction } from "@/components/chat/ExecutedActionsCard";
|
|
|
26
26
|
import type { ChildInfo } from "@/components/chat/OrchestrationChildrenCard";
|
|
27
27
|
import { foldOrchestrationChildEvents } from "@/components/chat/orchestration-children-model";
|
|
28
28
|
import { parseTurnBlocks, type TurnBlock } from "@/components/chat/turn-timeline";
|
|
29
|
+
import {
|
|
30
|
+
parseLlmSpeedPayload,
|
|
31
|
+
type LlmSpeedSnapshot,
|
|
32
|
+
} from "@/components/chat/process-status";
|
|
29
33
|
import { parseTurnFiles, type TurnFile } from "@/components/chat/turn-files";
|
|
30
34
|
import { readPersistedDurationMs } from "@/components/chat/elapsed";
|
|
31
35
|
import {
|
|
@@ -297,7 +301,7 @@ function AgentChatLoader({
|
|
|
297
301
|
|
|
298
302
|
if (initialMessages === null) {
|
|
299
303
|
return (
|
|
300
|
-
<div className="flex h-full w-full items-center justify-center text-
|
|
304
|
+
<div className="flex h-full w-full items-center justify-center text-xs text-agent-muted-foreground">
|
|
301
305
|
加载对话历史…
|
|
302
306
|
</div>
|
|
303
307
|
);
|
|
@@ -449,6 +453,13 @@ function AgentChatView({
|
|
|
449
453
|
number | undefined
|
|
450
454
|
>(undefined);
|
|
451
455
|
const pendingDurationMessageIdRef = useRef<string | null>(null);
|
|
456
|
+
const [currentLlmSpeed, setCurrentLlmSpeed] = useState<LlmSpeedSnapshot | undefined>(
|
|
457
|
+
undefined,
|
|
458
|
+
);
|
|
459
|
+
const currentLlmSpeedRef = useRef<LlmSpeedSnapshot | null>(null);
|
|
460
|
+
const [llmSpeedByMessageId, setLlmSpeedByMessageId] = useState<
|
|
461
|
+
Record<string, LlmSpeedSnapshot>
|
|
462
|
+
>({});
|
|
452
463
|
// P3.1 编排:本轮子代理生命周期(orchestration_child SSE 事件累积),
|
|
453
464
|
// 与 currentTurnActions 同款 reconcile 模式(message_id 落库后按键归档)。
|
|
454
465
|
const [currentTurnChildren, setCurrentTurnChildren] = useState<ChildInfo[]>([]);
|
|
@@ -519,6 +530,13 @@ function AgentChatView({
|
|
|
519
530
|
}
|
|
520
531
|
return;
|
|
521
532
|
}
|
|
533
|
+
if (ev.event === "llm_speed") {
|
|
534
|
+
const snap = parseLlmSpeedPayload(ev.payload);
|
|
535
|
+
if (!snap) return;
|
|
536
|
+
currentLlmSpeedRef.current = snap;
|
|
537
|
+
setCurrentLlmSpeed(snap);
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
522
540
|
if (ev.event === "turn_files") {
|
|
523
541
|
// 回合收尾时后端发一次(在 message_id 之前);先挂 pending,
|
|
524
542
|
// message_id 到达时随其他队列一起归档。
|
|
@@ -583,6 +601,13 @@ function AgentChatView({
|
|
|
583
601
|
const messageId = ev.payload?.messageId as string | undefined;
|
|
584
602
|
if (!messageId) return;
|
|
585
603
|
pendingDurationMessageIdRef.current = messageId;
|
|
604
|
+
const queuedSpeed = currentLlmSpeedRef.current;
|
|
605
|
+
if (queuedSpeed) {
|
|
606
|
+
setLlmSpeedByMessageId((prev) => ({
|
|
607
|
+
...prev,
|
|
608
|
+
[messageId]: queuedSpeed,
|
|
609
|
+
}));
|
|
610
|
+
}
|
|
586
611
|
const queued = pendingActionsRef.current;
|
|
587
612
|
if (queued.length > 0) {
|
|
588
613
|
setExecutedActionsByMessageId((prev) => ({
|
|
@@ -727,6 +752,25 @@ function AgentChatView({
|
|
|
727
752
|
}
|
|
728
753
|
return next;
|
|
729
754
|
});
|
|
755
|
+
const snap = currentLlmSpeedRef.current;
|
|
756
|
+
if (snap) {
|
|
757
|
+
const frozen: LlmSpeedSnapshot = {
|
|
758
|
+
...snap,
|
|
759
|
+
live: false,
|
|
760
|
+
requestStartedAt: null,
|
|
761
|
+
elapsedMs: snap.elapsedMs,
|
|
762
|
+
};
|
|
763
|
+
currentLlmSpeedRef.current = frozen;
|
|
764
|
+
setCurrentLlmSpeed(frozen);
|
|
765
|
+
setLlmSpeedByMessageId((prev) => {
|
|
766
|
+
const next = { ...prev };
|
|
767
|
+
if (lastAssistant) next[lastAssistant.id] = frozen;
|
|
768
|
+
if (pendingDurationMessageIdRef.current) {
|
|
769
|
+
next[pendingDurationMessageIdRef.current] = frozen;
|
|
770
|
+
}
|
|
771
|
+
return next;
|
|
772
|
+
});
|
|
773
|
+
}
|
|
730
774
|
}
|
|
731
775
|
}
|
|
732
776
|
wasStreamingRef.current = isStreaming;
|
|
@@ -770,6 +814,8 @@ function AgentChatView({
|
|
|
770
814
|
turnStartedAtRef.current = started;
|
|
771
815
|
setCurrentTurnStartedAtMs(started);
|
|
772
816
|
pendingDurationMessageIdRef.current = null;
|
|
817
|
+
currentLlmSpeedRef.current = null;
|
|
818
|
+
setCurrentLlmSpeed(undefined);
|
|
773
819
|
// 新一轮开始:记录本轮是否为 plan 模式,并隐藏上一份计划的操作条。
|
|
774
820
|
planTurnRef.current = input.metadata?.mode === "plan";
|
|
775
821
|
setPlanReady(false);
|
|
@@ -842,6 +888,8 @@ function AgentChatView({
|
|
|
842
888
|
turnStartedAtRef.current = started;
|
|
843
889
|
setCurrentTurnStartedAtMs(started);
|
|
844
890
|
pendingDurationMessageIdRef.current = null;
|
|
891
|
+
currentLlmSpeedRef.current = null;
|
|
892
|
+
setCurrentLlmSpeed(undefined);
|
|
845
893
|
planTurnRef.current = mode === "plan";
|
|
846
894
|
setPlanReady(false);
|
|
847
895
|
const metadata = {
|
|
@@ -987,7 +1035,7 @@ function AgentChatView({
|
|
|
987
1035
|
return (
|
|
988
1036
|
<div className="flex h-full w-full flex-col">
|
|
989
1037
|
{hydrationError && (
|
|
990
|
-
<div className="border-b border-agent-border bg-agent-muted/60 px-
|
|
1038
|
+
<div className="border-b border-agent-border bg-agent-muted/60 px-2.5 py-1 text-xs text-agent-destructive">
|
|
991
1039
|
加载对话历史失败:{hydrationError}
|
|
992
1040
|
</div>
|
|
993
1041
|
)}
|
|
@@ -1035,6 +1083,8 @@ function AgentChatView({
|
|
|
1035
1083
|
currentTurnTimeline={effectiveCurrentTurnTimeline}
|
|
1036
1084
|
currentTurnStartedAtMs={currentTurnStartedAtMs}
|
|
1037
1085
|
durationByMessageId={durationByMessageId}
|
|
1086
|
+
currentLlmSpeed={currentLlmSpeed}
|
|
1087
|
+
llmSpeedByMessageId={llmSpeedByMessageId}
|
|
1038
1088
|
turnFilesByMessageId={turnFilesByMessageId}
|
|
1039
1089
|
currentTurnFiles={currentTurnFiles}
|
|
1040
1090
|
currentTurnChildren={effectiveCurrentTurnChildren}
|
|
@@ -1083,7 +1133,7 @@ function AgentChatView({
|
|
|
1083
1133
|
onTrustChanged={fetchProjects}
|
|
1084
1134
|
/>
|
|
1085
1135
|
{planReady && !isStreaming ? (
|
|
1086
|
-
<div className="mx-
|
|
1136
|
+
<div className="mx-2.5 mb-1 flex items-center justify-between gap-2 rounded-agent-md border border-amber-400/50 bg-amber-400/10 px-2.5 py-1.5 text-xs">
|
|
1087
1137
|
<div className="flex items-center gap-2 text-amber-700 dark:text-amber-300">
|
|
1088
1138
|
<LuListChecks className="h-4 w-4 shrink-0" />
|
|
1089
1139
|
<span>计划已生成。确认无误后可切换到 Agent 模式开始执行。</span>
|
|
@@ -1264,15 +1314,15 @@ function EmptyChatGate() {
|
|
|
1264
1314
|
|
|
1265
1315
|
return (
|
|
1266
1316
|
<div
|
|
1267
|
-
className="flex h-full w-full items-center justify-center p-
|
|
1317
|
+
className="flex h-full w-full items-center justify-center p-3 sm:p-5"
|
|
1268
1318
|
data-testid="empty-chat-home"
|
|
1269
1319
|
>
|
|
1270
|
-
<div className="flex w-full max-w-2xl flex-col items-center gap-
|
|
1320
|
+
<div className="flex w-full max-w-2xl flex-col items-center gap-4">
|
|
1271
1321
|
<div className="text-center">
|
|
1272
1322
|
<h1 className="text-xl font-semibold tracking-tight text-agent-foreground">
|
|
1273
1323
|
{BRAND_NAME}
|
|
1274
1324
|
</h1>
|
|
1275
|
-
<p className="mt-1.5 text-
|
|
1325
|
+
<p className="mt-1.5 text-xs text-agent-muted-foreground">
|
|
1276
1326
|
输入消息,直接开始一段新对话。
|
|
1277
1327
|
</p>
|
|
1278
1328
|
</div>
|
|
@@ -82,7 +82,28 @@ Tests: 1 failed, 2 passed, 3 total
|
|
|
82
82
|
},
|
|
83
83
|
];
|
|
84
84
|
|
|
85
|
+
const MOCK_TODOS = [
|
|
86
|
+
{ id: 'survey', content: '检查服务连通性', status: 'completed' as const },
|
|
87
|
+
{ id: 'test', content: '跑本目录测试套件', status: 'in_progress' as const },
|
|
88
|
+
{ id: 'fix', content: '修失败的断言颜色', status: 'pending' as const },
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
const MOCK_TODO_ACTION: ExecutedAction = {
|
|
92
|
+
tool: 'todo_write',
|
|
93
|
+
arguments: { todos: MOCK_TODOS },
|
|
94
|
+
result: {
|
|
95
|
+
success: true,
|
|
96
|
+
data: {
|
|
97
|
+
value: {
|
|
98
|
+
todos: MOCK_TODOS,
|
|
99
|
+
summary: { total: 3, pending: 1, inProgress: 1, completed: 1 },
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
|
|
85
105
|
const MOCK_ACTIONS: ExecutedAction[] = [
|
|
106
|
+
MOCK_TODO_ACTION,
|
|
86
107
|
{
|
|
87
108
|
tool: 'check_connection',
|
|
88
109
|
arguments: { host: '127.0.0.1', port: 8765 },
|
|
@@ -107,11 +128,12 @@ const MOCK_ACTIONS: ExecutedAction[] = [
|
|
|
107
128
|
|
|
108
129
|
const MOCK_TIMELINE: TurnBlock[] = [
|
|
109
130
|
{ type: 'reasoning', content: '先检查服务连通性,再跑测试套件。' },
|
|
110
|
-
{ type: 'tools', actions: [
|
|
111
|
-
{ type: 'text', content: '连接正常。接下来跑测试套件。' },
|
|
131
|
+
{ type: 'tools', actions: [MOCK_TODO_ACTION] },
|
|
112
132
|
{ type: 'tools', actions: [MOCK_ACTIONS[1]] },
|
|
113
|
-
{ type: '
|
|
133
|
+
{ type: 'text', content: '连接正常。接下来跑测试套件。' },
|
|
114
134
|
{ type: 'tools', actions: [MOCK_ACTIONS[2]] },
|
|
135
|
+
{ type: 'reasoning', content: '测试失败,读一下失败文件再给建议。' },
|
|
136
|
+
{ type: 'tools', actions: [MOCK_ACTIONS[3]] },
|
|
115
137
|
{ type: 'text', content: MOCK_MESSAGES[1].content ?? '' },
|
|
116
138
|
];
|
|
117
139
|
|
|
@@ -284,7 +306,7 @@ export function ChatPanelPreviewPage() {
|
|
|
284
306
|
|
|
285
307
|
return (
|
|
286
308
|
<div className="flex h-full w-full flex-col bg-agent-muted/30">
|
|
287
|
-
<div className="flex shrink-0 items-center gap-2 border-b border-agent-border bg-agent-canvas px-
|
|
309
|
+
<div className="flex shrink-0 items-center gap-2 border-b border-agent-border bg-agent-canvas px-2.5 py-1.5 text-xs">
|
|
288
310
|
<span className="text-agent-muted-foreground">
|
|
289
311
|
/preview/chat (dev harness)
|
|
290
312
|
</span>
|