@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
|
@@ -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-
|
|
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-
|
|
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-
|
|
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.
|
|
4
|
-
*
|
|
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
|
|
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-
|
|
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-
|
|
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>
|
|
@@ -149,7 +149,7 @@ export function TaskProcessPanel({
|
|
|
149
149
|
)
|
|
150
150
|
}
|
|
151
151
|
/>
|
|
152
|
-
<div className="border-b border-agent-border/60 px-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
>
|