@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
@@ -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. Count follows the turn (skill next steps), not a
4
- * fixed three. Clicking one sends that text immediately.
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-col items-start gap-1.5 px-1 pt-1"
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
- className="max-w-full rounded-full border border-agent-border bg-agent-muted/40 px-3 py-1.5 text-left text-[13px] leading-snug text-agent-foreground transition-colors hover:bg-agent-foreground/5"
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-4 py-2.5">
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-sm font-medium text-agent-foreground">
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-4 py-2 text-[10px] text-agent-muted-foreground/80">
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 { parseTurnBlocks, type TurnBlock } from '@/components/chat/turn-timeline';
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
- void (async () => {
108
+ const load = async () => {
92
109
  try {
93
- const snapshot = await getTaskProcess(inspected.id);
94
- if (cancelled) return;
95
- setBlocks(parseTurnBlocks(snapshot.timeline) ?? []);
96
- setLive(snapshot.live);
97
- setStale(snapshot.stale);
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={`后台推理 · ${statusLabel}`}
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-3 py-2">
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-3"
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-sm leading-relaxed text-agent-foreground">
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-3 right-3 flex h-8 w-8 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"
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-3 py-1.5 text-[11px] text-agent-muted-foreground">
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-3 py-1 text-left text-xs transition-colors hover:bg-agent-foreground/5 disabled:cursor-wait"
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
  >