@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.
Files changed (49) hide show
  1. package/package.json +4 -4
  2. package/src/components/AgentSidebar.tsx +24 -24
  3. package/src/components/BrandLockup.tsx +1 -1
  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 +41 -13
  10. package/src/components/chat/ChatInput.tsx +24 -24
  11. package/src/components/chat/ExecPolicyPicker.tsx +4 -4
  12. package/src/components/chat/ExecutedActionsCard.tsx +10 -3
  13. package/src/components/chat/FilePathCode.tsx +2 -1
  14. package/src/components/chat/LocalChatPanel.tsx +39 -4
  15. package/src/components/chat/Markdown.tsx +15 -14
  16. package/src/components/chat/MessageList.tsx +16 -3
  17. package/src/components/chat/ModelPicker.tsx +6 -6
  18. package/src/components/chat/SessionTodoList.tsx +137 -0
  19. package/src/components/chat/SessionTreeModal.tsx +3 -3
  20. package/src/components/chat/SuggestedReplies.tsx +5 -4
  21. package/src/components/chat/TaskPanelModal.tsx +3 -3
  22. package/src/components/chat/TaskProcessPanel.tsx +5 -4
  23. package/src/components/chat/TurnFilesCard.tsx +2 -2
  24. package/src/components/chat/TurnProcessGroup.tsx +226 -99
  25. package/src/components/chat/UserMessage.tsx +4 -4
  26. package/src/components/chat/next-steps-tags.ts +28 -0
  27. package/src/components/chat/process-status.ts +125 -53
  28. package/src/components/chat/todo-list-model.ts +167 -0
  29. package/src/components/chat/turn-timeline.ts +100 -11
  30. package/src/components/settings/AgentsSettingsPanel.tsx +2 -2
  31. package/src/components/settings/AppearanceSettingsPanel.tsx +3 -3
  32. package/src/components/settings/DiagnoseSettingsPanel.tsx +2 -2
  33. package/src/components/settings/InsightsSettingsPanel.tsx +4 -4
  34. package/src/components/settings/LlmSettingsPanel.tsx +10 -10
  35. package/src/components/settings/McpSettingsPanel.tsx +3 -3
  36. package/src/components/settings/ModelIdCombobox.tsx +2 -2
  37. package/src/components/settings/SecuritySettingsPanel.tsx +1 -1
  38. package/src/components/settings/SettingsSaveButton.tsx +1 -1
  39. package/src/components/settings/SkillsSettingsPanel.tsx +2 -2
  40. package/src/components/settings/TelemetrySettingsPanel.tsx +2 -2
  41. package/src/components/settings/UsagePanel.tsx +14 -10
  42. package/src/components/settings/WebSearchSettingsPanel.tsx +2 -2
  43. package/src/layouts/AgentLayout.tsx +3 -3
  44. package/src/lib/chat-transport.ts +68 -12
  45. package/src/lib/show-thinking-content.ts +4 -4
  46. package/src/pages/AgentPage.tsx +56 -6
  47. package/src/pages/ChatPanelPreviewPage.tsx +26 -4
  48. package/src/pages/SettingsPage.tsx +39 -39
  49. package/src/styles/index.css +12 -2
@@ -0,0 +1,137 @@
1
+ import { useMemo, useState } from 'react';
2
+ import {
3
+ LuCircle,
4
+ LuCircleCheck,
5
+ LuCircleDot,
6
+ LuChevronDown,
7
+ LuChevronRight,
8
+ LuListChecks,
9
+ } from 'react-icons/lu';
10
+ import {
11
+ summarizeTodos,
12
+ type SessionTodo,
13
+ type TodoStatus,
14
+ } from './todo-list-model';
15
+
16
+ function StatusIcon({
17
+ status,
18
+ className,
19
+ }: {
20
+ status: TodoStatus;
21
+ className?: string;
22
+ }) {
23
+ const cls = ['h-3.5 w-3.5 shrink-0', className].filter(Boolean).join(' ');
24
+ if (status === 'completed') {
25
+ return <LuCircleCheck className={`${cls} text-emerald-600 dark:text-emerald-400`} />;
26
+ }
27
+ if (status === 'in_progress') {
28
+ return <LuCircleDot className={`${cls} text-blue-600 dark:text-blue-400`} />;
29
+ }
30
+ return <LuCircle className={`${cls} text-agent-muted-foreground/70`} />;
31
+ }
32
+
33
+ export function currentTodoStep(todos: SessionTodo[]): SessionTodo | null {
34
+ return (
35
+ todos.find((todo) => todo.status === 'in_progress') ??
36
+ todos.find((todo) => todo.status === 'pending') ??
37
+ todos[todos.length - 1] ??
38
+ null
39
+ );
40
+ }
41
+
42
+ export function todoProgressCopy(todos: SessionTodo[]): string {
43
+ const summary = summarizeTodos(todos);
44
+ if (summary.total === 0) return '';
45
+ if (summary.completed === summary.total) {
46
+ return `${summary.completed}/${summary.total} 已完成`;
47
+ }
48
+ const currentIndex = todos.findIndex((todo) => todo.status === 'in_progress');
49
+ const step =
50
+ currentIndex >= 0 ? currentIndex + 1 : Math.min(summary.completed + 1, summary.total);
51
+ return `执行到 ${step}/${summary.total}`;
52
+ }
53
+
54
+ export function TodoItems({
55
+ todos,
56
+ compact = false,
57
+ }: {
58
+ todos: SessionTodo[];
59
+ compact?: boolean;
60
+ }) {
61
+ return (
62
+ <ol
63
+ className={`m-0 list-none ${compact ? 'max-h-40 space-y-1 overflow-y-auto px-2.5 py-1.5' : 'space-y-1 px-2.5 py-1.5'}`}
64
+ >
65
+ {todos.map((todo) => {
66
+ const done = todo.status === 'completed';
67
+ const active = todo.status === 'in_progress';
68
+ return (
69
+ <li
70
+ key={todo.id}
71
+ data-testid={`todo-item-${todo.id}`}
72
+ data-status={todo.status}
73
+ className="flex items-start gap-2 text-[12px] leading-relaxed"
74
+ >
75
+ <StatusIcon status={todo.status} className="mt-[2px]" />
76
+ <span
77
+ className={
78
+ done
79
+ ? 'text-agent-muted-foreground line-through'
80
+ : active
81
+ ? 'font-medium text-agent-foreground'
82
+ : 'text-agent-foreground'
83
+ }
84
+ >
85
+ {todo.content}
86
+ </span>
87
+ </li>
88
+ );
89
+ })}
90
+ </ol>
91
+ );
92
+ }
93
+
94
+ export function SessionTodoList({ todos }: { todos: SessionTodo[] }) {
95
+ const [expanded, setExpanded] = useState(false);
96
+ const current = useMemo(() => currentTodoStep(todos), [todos]);
97
+ const progressLabel = useMemo(() => todoProgressCopy(todos), [todos]);
98
+
99
+ return (
100
+ <div
101
+ className="mb-1 overflow-hidden rounded-agent-lg border border-agent-border bg-agent-canvas shadow-sm"
102
+ data-testid="session-todo-list"
103
+ >
104
+ <button
105
+ type="button"
106
+ onClick={() => setExpanded((value) => !value)}
107
+ className="flex w-full items-center gap-1.5 px-2.5 py-1.5 text-left"
108
+ aria-expanded={expanded}
109
+ >
110
+ {expanded ? (
111
+ <LuChevronDown className="h-3.5 w-3.5 shrink-0 text-agent-muted-foreground" />
112
+ ) : (
113
+ <LuChevronRight className="h-3.5 w-3.5 shrink-0 text-agent-muted-foreground" />
114
+ )}
115
+ {expanded || !current ? (
116
+ <LuListChecks className="h-3.5 w-3.5 shrink-0 text-agent-foreground" />
117
+ ) : (
118
+ <StatusIcon status={current.status} />
119
+ )}
120
+ <span className="min-w-0 flex-1 truncate text-[12px] font-medium text-agent-foreground">
121
+ {expanded ? '任务清单' : (current?.content ?? '任务清单')}
122
+ </span>
123
+ {progressLabel ? (
124
+ <span
125
+ className="shrink-0 rounded-full bg-agent-muted px-1.5 py-0.5 text-[10px] font-medium text-agent-muted-foreground"
126
+ data-testid="todo-progress"
127
+ >
128
+ {progressLabel}
129
+ </span>
130
+ ) : null}
131
+ </button>
132
+ {expanded ? <TodoItems todos={todos} compact /> : null}
133
+ </div>
134
+ );
135
+ }
136
+
137
+ export default SessionTodoList;
@@ -136,9 +136,9 @@ export function SessionTreeModal({ chatId, onClose, onBranchSwitched }: SessionT
136
136
  data-session-tree-modal
137
137
  >
138
138
  <div className="flex max-h-[85vh] w-full max-w-lg flex-col overflow-hidden rounded-agent-lg border border-agent-border bg-agent-canvas shadow-xl">
139
- <div className="flex items-center gap-2 border-b border-agent-border px-4 py-2.5">
139
+ <div className="flex items-center gap-2 border-b border-agent-border px-3 py-2">
140
140
  <LuGitBranch className="h-4 w-4 shrink-0 text-agent-muted-foreground" />
141
- <span className="min-w-0 flex-1 truncate text-sm font-medium text-agent-foreground">
141
+ <span className="min-w-0 flex-1 truncate text-xs font-medium text-agent-foreground">
142
142
  会话分支树
143
143
  {data && data.nodeCount > 1 && (
144
144
  <span className="ml-1.5 text-xs font-normal text-agent-muted-foreground">
@@ -206,7 +206,7 @@ export function SessionTreeModal({ chatId, onClose, onBranchSwitched }: SessionT
206
206
  </div>
207
207
 
208
208
  {!loading && !isEmpty && (
209
- <div className="border-t border-agent-border px-4 py-2 text-[10px] text-agent-muted-foreground/80">
209
+ <div className="border-t border-agent-border px-3 py-1.5 text-[10px] text-agent-muted-foreground/80">
210
210
  {data?.truncated
211
211
  ? '分支过多,树已被截断(仅显示部分节点)。'
212
212
  : '点击切换分支;↑↓ 移动,Enter 切换,Esc 关闭。'}
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * 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.
3
+ * latest assistant reply. Source is the model's judgment of `[next_steps]`
4
+ * or the last paragraph — count follows that, not a fixed three. Clicking
5
+ * one sends that text immediately.
5
6
  */
6
7
  export function SuggestedReplies({
7
8
  suggestions,
@@ -14,7 +15,7 @@ export function SuggestedReplies({
14
15
 
15
16
  return (
16
17
  <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"
18
+ className="mx-auto flex w-full max-w-[var(--chat-input-box-width)] flex-col items-start gap-1 px-1 pt-0.5 text-[12px]"
18
19
  data-testid="suggested-replies"
19
20
  >
20
21
  {suggestions.map((text) => (
@@ -23,7 +24,7 @@ export function SuggestedReplies({
23
24
  type="button"
24
25
  data-testid="suggested-reply"
25
26
  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"
27
+ className="max-w-full rounded-full border border-agent-border bg-agent-muted/40 px-2 py-0.5 text-left text-[12px] leading-[1.45] text-agent-muted-foreground transition-colors hover:bg-agent-foreground/5 hover:text-agent-foreground"
27
28
  >
28
29
  {text}
29
30
  </button>
@@ -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>
@@ -149,7 +149,7 @@ export function TaskProcessPanel({
149
149
  )
150
150
  }
151
151
  />
152
- <div className="border-b border-agent-border/60 px-3 py-2">
152
+ <div className="border-b border-agent-border/60 px-2.5 py-1.5">
153
153
  <div className="line-clamp-3 text-xs leading-relaxed text-agent-foreground">
154
154
  {inspected.title}
155
155
  </div>
@@ -157,7 +157,7 @@ export function TaskProcessPanel({
157
157
  <div className="relative min-h-0 flex-1 overflow-hidden">
158
158
  <div
159
159
  ref={containerRef}
160
- className="h-full overflow-y-auto overflow-anchor-none p-3"
160
+ className="h-full overflow-y-auto overflow-anchor-none p-2.5"
161
161
  data-testid="task-process-scroll"
162
162
  >
163
163
  {loading ? (
@@ -172,6 +172,7 @@ export function TaskProcessPanel({
172
172
  blocks={blocks}
173
173
  isStreaming={live}
174
174
  showThinkingContent
175
+ collapseWhenFinished={false}
175
176
  agents={[]}
176
177
  chats={[]}
177
178
  emptyFallback={
@@ -184,7 +185,7 @@ export function TaskProcessPanel({
184
185
  </div>
185
186
  }
186
187
  renderAnswer={(block) => (
187
- <div className="text-sm leading-relaxed text-agent-foreground">
188
+ <div className="text-xs leading-relaxed text-agent-foreground">
188
189
  <Markdown agents={[]} chats={[]}>{block.content}</Markdown>
189
190
  </div>
190
191
  )}
@@ -199,7 +200,7 @@ export function TaskProcessPanel({
199
200
  setIsAtBottom(true);
200
201
  scrollToBottom('smooth');
201
202
  }}
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"
203
+ 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
204
  title="回到底部"
204
205
  aria-label="回到底部"
205
206
  >
@@ -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
  >