@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
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import { LuSparkles } from 'react-icons/lu';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* WorkBuddy-style follow-up chips: next-turn user inputs rendered under the
|
|
3
|
-
* latest assistant reply.
|
|
4
|
-
*
|
|
5
|
+
* latest assistant reply. Source is the model's judgment of `[next_steps]`
|
|
6
|
+
* or the last paragraph — count follows that, not a fixed three. Clicking
|
|
7
|
+
* one sends that text immediately.
|
|
5
8
|
*/
|
|
6
9
|
export function SuggestedReplies({
|
|
7
10
|
suggestions,
|
|
@@ -14,18 +17,23 @@ export function SuggestedReplies({
|
|
|
14
17
|
|
|
15
18
|
return (
|
|
16
19
|
<div
|
|
17
|
-
className="mx-auto flex w-full max-w-[var(--chat-input-box-width)] flex-
|
|
20
|
+
className="mx-auto mt-2.5 flex w-full max-w-[var(--chat-input-box-width)] flex-wrap items-center gap-1.5 px-1 pb-0.5"
|
|
18
21
|
data-testid="suggested-replies"
|
|
19
22
|
>
|
|
23
|
+
<div className="mr-0.5 inline-flex items-center gap-1 text-[11px] font-medium text-agent-muted-foreground/70 select-none">
|
|
24
|
+
<LuSparkles className="h-3 w-3 text-agent-muted-foreground/80" />
|
|
25
|
+
<span>建议回复</span>
|
|
26
|
+
</div>
|
|
20
27
|
{suggestions.map((text) => (
|
|
21
28
|
<button
|
|
22
29
|
key={text}
|
|
23
30
|
type="button"
|
|
24
31
|
data-testid="suggested-reply"
|
|
25
32
|
onClick={() => onSelect(text)}
|
|
26
|
-
|
|
33
|
+
title={text}
|
|
34
|
+
className="inline-flex max-w-[280px] sm:max-w-[340px] items-center rounded-full border border-agent-border/80 bg-agent-canvas px-2.5 py-1 text-left text-[12px] leading-[1.4] text-agent-muted-foreground transition-all duration-150 hover:border-agent-foreground/30 hover:bg-agent-foreground/5 hover:text-agent-foreground shadow-2xs active:scale-[0.98]"
|
|
27
35
|
>
|
|
28
|
-
{text}
|
|
36
|
+
<span className="truncate">{text}</span>
|
|
29
37
|
</button>
|
|
30
38
|
))}
|
|
31
39
|
</div>
|
|
@@ -33,3 +41,4 @@ export function SuggestedReplies({
|
|
|
33
41
|
}
|
|
34
42
|
|
|
35
43
|
export default SuggestedReplies;
|
|
44
|
+
|
|
@@ -135,9 +135,9 @@ export function TaskPanelModal({
|
|
|
135
135
|
data-task-panel-modal
|
|
136
136
|
>
|
|
137
137
|
<div className="flex max-h-[85vh] w-full max-w-xl flex-col overflow-hidden rounded-agent-lg border border-agent-border bg-agent-canvas shadow-xl">
|
|
138
|
-
<div className="flex items-center gap-2 border-b border-agent-border px-
|
|
138
|
+
<div className="flex items-center gap-2 border-b border-agent-border px-3 py-2">
|
|
139
139
|
<LuListTodo className="h-4 w-4 shrink-0 text-agent-muted-foreground" />
|
|
140
|
-
<span className="min-w-0 flex-1 truncate text-
|
|
140
|
+
<span className="min-w-0 flex-1 truncate text-xs font-medium text-agent-foreground">
|
|
141
141
|
后台任务
|
|
142
142
|
{tasks && tasks.length > 0 && (
|
|
143
143
|
<span className="ml-1.5 text-xs font-normal text-agent-muted-foreground">
|
|
@@ -298,7 +298,7 @@ export function TaskPanelModal({
|
|
|
298
298
|
)}
|
|
299
299
|
</div>
|
|
300
300
|
|
|
301
|
-
<div className="border-t border-agent-border px-
|
|
301
|
+
<div className="border-t border-agent-border px-3 py-1.5 text-[10px] text-agent-muted-foreground/80">
|
|
302
302
|
任务由后台智能体独立执行。点任务名可在右侧看推理过程;Esc 关闭。
|
|
303
303
|
</div>
|
|
304
304
|
</div>
|
|
@@ -1,18 +1,34 @@
|
|
|
1
1
|
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
2
2
|
import { LuArrowDown, LuLoaderCircle, LuTerminal } from 'react-icons/lu';
|
|
3
3
|
import { DockHeaderButton, DockPanelHeader } from '@/components/DockPanelHeader';
|
|
4
|
-
import { getTaskProcess } from '@/lib/local-api';
|
|
4
|
+
import { getChildProcess, getTaskProcess } from '@/lib/local-api';
|
|
5
5
|
import { getElectronBridge } from '@/lib/electron-bridge';
|
|
6
6
|
import { Markdown } from '@/components/chat/Markdown';
|
|
7
7
|
import { TurnProcessGroup } from '@/components/chat/TurnProcessGroup';
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
parseTurnBlocks,
|
|
10
|
+
timelineContentSignature,
|
|
11
|
+
type TurnBlock,
|
|
12
|
+
} from '@/components/chat/turn-timeline';
|
|
9
13
|
|
|
10
14
|
export interface InspectedTask {
|
|
11
15
|
id: string;
|
|
12
16
|
chatId: string;
|
|
13
17
|
title: string;
|
|
18
|
+
/**
|
|
19
|
+
* 子代理过程:按它自己的 durable record 读,而不是任务表。设了它就走
|
|
20
|
+
* `GET /child-process`,`live` 期间轮询(子回合边跑边写 record)。
|
|
21
|
+
*/
|
|
22
|
+
recordId?: string;
|
|
23
|
+
/** 目标仍在运行——决定轮询与标题状态。 */
|
|
24
|
+
live?: boolean;
|
|
14
25
|
}
|
|
15
26
|
|
|
27
|
+
const CHILD_POLL_INTERVAL_MS = 1500;
|
|
28
|
+
|
|
29
|
+
/** 连续这么多次轮询读到同一份内容就认为子代理跑完了(≈60s)。 */
|
|
30
|
+
const CHILD_IDLE_POLLS_UNTIL_DONE = 40;
|
|
31
|
+
|
|
16
32
|
const NEAR_BOTTOM_THRESHOLD_PX = 100;
|
|
17
33
|
|
|
18
34
|
/**
|
|
@@ -74,27 +90,46 @@ export function TaskProcessPanel({
|
|
|
74
90
|
|
|
75
91
|
// live 增量在 paint 前钉住底部。smooth scroll 会先露出一帧半截内容再往下
|
|
76
92
|
// 滑,Windows 经典滚动条就会上下跳。
|
|
77
|
-
const timelineSig = blocks
|
|
78
|
-
.map((block) =>
|
|
79
|
-
block.type === 'tools' ? `t${block.actions.length}` : `c${block.content.length}`,
|
|
80
|
-
)
|
|
81
|
-
.join('|');
|
|
93
|
+
const timelineSig = timelineContentSignature(blocks);
|
|
82
94
|
useLayoutEffect(() => {
|
|
83
95
|
if (!isAtBottomRef.current) return;
|
|
84
96
|
scrollToBottom('auto');
|
|
85
97
|
}, [timelineSig, live, scrollToBottom]);
|
|
86
98
|
|
|
99
|
+
const childRecordId = inspected.recordId;
|
|
100
|
+
|
|
87
101
|
useEffect(() => {
|
|
88
102
|
let cancelled = false;
|
|
103
|
+
let timer: number | undefined;
|
|
104
|
+
let lastSig: string | null = null;
|
|
105
|
+
let idlePolls = 0;
|
|
89
106
|
setLoading(true);
|
|
90
107
|
setError(null);
|
|
91
|
-
|
|
108
|
+
const load = async () => {
|
|
92
109
|
try {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
110
|
+
if (childRecordId) {
|
|
111
|
+
const snapshot = await getChildProcess(childRecordId);
|
|
112
|
+
if (cancelled) return;
|
|
113
|
+
const next = parseTurnBlocks(snapshot.timeline) ?? [];
|
|
114
|
+
const sig = timelineContentSignature(next);
|
|
115
|
+
idlePolls = sig === lastSig ? idlePolls + 1 : 0;
|
|
116
|
+
lastSig = sig;
|
|
117
|
+
setBlocks(next);
|
|
118
|
+
// 子代理跑完不会再有事件通知这个面板(它的 live 是点开那刻的快照),
|
|
119
|
+
// 所以用「record 不再变化」当结束信号:停轮询、标题转「已结束」。
|
|
120
|
+
if (timer !== undefined && idlePolls >= CHILD_IDLE_POLLS_UNTIL_DONE) {
|
|
121
|
+
window.clearInterval(timer);
|
|
122
|
+
timer = undefined;
|
|
123
|
+
setLive(false);
|
|
124
|
+
}
|
|
125
|
+
setStale(false);
|
|
126
|
+
} else {
|
|
127
|
+
const snapshot = await getTaskProcess(inspected.id);
|
|
128
|
+
if (cancelled) return;
|
|
129
|
+
setBlocks(parseTurnBlocks(snapshot.timeline) ?? []);
|
|
130
|
+
setLive(snapshot.live);
|
|
131
|
+
setStale(snapshot.stale);
|
|
132
|
+
}
|
|
98
133
|
} catch (err) {
|
|
99
134
|
if (cancelled) return;
|
|
100
135
|
setError(err instanceof Error ? err.message : String(err));
|
|
@@ -102,13 +137,22 @@ export function TaskProcessPanel({
|
|
|
102
137
|
} finally {
|
|
103
138
|
if (!cancelled) setLoading(false);
|
|
104
139
|
}
|
|
105
|
-
}
|
|
140
|
+
};
|
|
141
|
+
if (childRecordId) setLive(Boolean(inspected.live));
|
|
142
|
+
void load();
|
|
143
|
+
// 子代理没有 task-process 广播通道(它不是任务),运行中靠轮询它的
|
|
144
|
+
// record 追进度;终态打开只读一次。
|
|
145
|
+
if (childRecordId && inspected.live) {
|
|
146
|
+
timer = window.setInterval(() => void load(), CHILD_POLL_INTERVAL_MS);
|
|
147
|
+
}
|
|
106
148
|
return () => {
|
|
107
149
|
cancelled = true;
|
|
150
|
+
if (timer !== undefined) window.clearInterval(timer);
|
|
108
151
|
};
|
|
109
|
-
}, [inspected.id]);
|
|
152
|
+
}, [inspected.id, childRecordId, inspected.live]);
|
|
110
153
|
|
|
111
154
|
useEffect(() => {
|
|
155
|
+
if (childRecordId) return;
|
|
112
156
|
const bridge = getElectronBridge();
|
|
113
157
|
if (!bridge?.onTaskProcess) return;
|
|
114
158
|
return bridge.onTaskProcess((payload) => {
|
|
@@ -121,13 +165,14 @@ export function TaskProcessPanel({
|
|
|
121
165
|
setLive(payload.live);
|
|
122
166
|
if (payload.live) setStale(false);
|
|
123
167
|
});
|
|
124
|
-
}, [inspected.id]);
|
|
168
|
+
}, [inspected.id, childRecordId]);
|
|
125
169
|
|
|
126
170
|
const statusLabel = live
|
|
127
171
|
? '正在推理'
|
|
128
172
|
: stale
|
|
129
173
|
? '进程已中断(表上仍是运行中)'
|
|
130
174
|
: '已结束';
|
|
175
|
+
const panelTitle = `${childRecordId ? '子代理推理' : '后台推理'} · ${statusLabel}`;
|
|
131
176
|
|
|
132
177
|
return (
|
|
133
178
|
<div
|
|
@@ -135,7 +180,7 @@ export function TaskProcessPanel({
|
|
|
135
180
|
data-testid="task-process-panel"
|
|
136
181
|
>
|
|
137
182
|
<DockPanelHeader
|
|
138
|
-
title={
|
|
183
|
+
title={panelTitle}
|
|
139
184
|
onClose={onClose}
|
|
140
185
|
closeLabel="关闭过程面板"
|
|
141
186
|
actions={
|
|
@@ -149,7 +194,7 @@ export function TaskProcessPanel({
|
|
|
149
194
|
)
|
|
150
195
|
}
|
|
151
196
|
/>
|
|
152
|
-
<div className="border-b border-agent-border/60 px-
|
|
197
|
+
<div className="border-b border-agent-border/60 px-2.5 py-1.5">
|
|
153
198
|
<div className="line-clamp-3 text-xs leading-relaxed text-agent-foreground">
|
|
154
199
|
{inspected.title}
|
|
155
200
|
</div>
|
|
@@ -157,7 +202,7 @@ export function TaskProcessPanel({
|
|
|
157
202
|
<div className="relative min-h-0 flex-1 overflow-hidden">
|
|
158
203
|
<div
|
|
159
204
|
ref={containerRef}
|
|
160
|
-
className="h-full overflow-y-auto overflow-anchor-none p-
|
|
205
|
+
className="h-full overflow-y-auto overflow-anchor-none p-2.5"
|
|
161
206
|
data-testid="task-process-scroll"
|
|
162
207
|
>
|
|
163
208
|
{loading ? (
|
|
@@ -172,6 +217,7 @@ export function TaskProcessPanel({
|
|
|
172
217
|
blocks={blocks}
|
|
173
218
|
isStreaming={live}
|
|
174
219
|
showThinkingContent
|
|
220
|
+
collapseWhenFinished={false}
|
|
175
221
|
agents={[]}
|
|
176
222
|
chats={[]}
|
|
177
223
|
emptyFallback={
|
|
@@ -184,7 +230,7 @@ export function TaskProcessPanel({
|
|
|
184
230
|
</div>
|
|
185
231
|
}
|
|
186
232
|
renderAnswer={(block) => (
|
|
187
|
-
<div className="text-
|
|
233
|
+
<div className="text-xs leading-relaxed text-agent-foreground">
|
|
188
234
|
<Markdown agents={[]} chats={[]}>{block.content}</Markdown>
|
|
189
235
|
</div>
|
|
190
236
|
)}
|
|
@@ -199,7 +245,7 @@ export function TaskProcessPanel({
|
|
|
199
245
|
setIsAtBottom(true);
|
|
200
246
|
scrollToBottom('smooth');
|
|
201
247
|
}}
|
|
202
|
-
className="absolute bottom-
|
|
248
|
+
className="absolute bottom-2 right-2 flex h-7 w-7 items-center justify-center rounded-full border border-agent-border bg-agent-canvas text-agent-foreground shadow-md transition-colors hover:bg-agent-foreground/5"
|
|
203
249
|
title="回到底部"
|
|
204
250
|
aria-label="回到底部"
|
|
205
251
|
>
|
|
@@ -57,7 +57,7 @@ export function TurnFilesCard({ files }: TurnFilesCardProps) {
|
|
|
57
57
|
className="rounded-agent-lg border border-agent-border bg-agent-canvas shadow-sm"
|
|
58
58
|
data-turn-files=""
|
|
59
59
|
>
|
|
60
|
-
<div className="flex items-center gap-1.5 border-b border-agent-border/60 px-
|
|
60
|
+
<div className="flex items-center gap-1.5 border-b border-agent-border/60 px-2.5 py-1 text-[11px] text-agent-muted-foreground">
|
|
61
61
|
<LuFiles className="h-3.5 w-3.5" />
|
|
62
62
|
<span>本轮产生了 {files.length} 个文件,点击打开</span>
|
|
63
63
|
</div>
|
|
@@ -74,7 +74,7 @@ export function TurnFilesCard({ files }: TurnFilesCardProps) {
|
|
|
74
74
|
onClick={() => void handleOpen(file)}
|
|
75
75
|
disabled={openingPath !== null}
|
|
76
76
|
title={file.path}
|
|
77
|
-
className="flex w-full items-center gap-2 px-
|
|
77
|
+
className="flex w-full items-center gap-2 px-2.5 py-1 text-left text-xs transition-colors hover:bg-agent-foreground/5 disabled:cursor-wait"
|
|
78
78
|
data-turn-file=""
|
|
79
79
|
data-kind={file.kind}
|
|
80
80
|
>
|