@steerable/agent-shell-web 0.6.15
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/LICENSE +91 -0
- package/README.md +20 -0
- package/package.json +69 -0
- package/public/favicon.ico +0 -0
- package/public/favicon.png +0 -0
- package/src/AppShell.tsx +24 -0
- package/src/assets/logo-generic.png +0 -0
- package/src/brand.test.ts +22 -0
- package/src/brand.ts +48 -0
- package/src/components/AgentSidebar.test.tsx +127 -0
- package/src/components/AgentSidebar.tsx +1010 -0
- package/src/components/ChatHeader.test.tsx +230 -0
- package/src/components/ChatHeader.tsx +436 -0
- package/src/components/DockPanelHeader.tsx +69 -0
- package/src/components/LocalLlmSettingsModal.test.tsx +372 -0
- package/src/components/LocalLlmSettingsModal.tsx +72 -0
- package/src/components/TerminalPanel.tsx +68 -0
- package/src/components/TerminalView.tsx +228 -0
- package/src/components/chat/ApprovalModal.test.tsx +83 -0
- package/src/components/chat/ApprovalModal.tsx +229 -0
- package/src/components/chat/AskUserModalHost.test.tsx +176 -0
- package/src/components/chat/AskUserModalHost.tsx +61 -0
- package/src/components/chat/AskUserQuestionMenu.tsx +621 -0
- package/src/components/chat/AssistantMessage.test.tsx +68 -0
- package/src/components/chat/AssistantMessage.tsx +469 -0
- package/src/components/chat/ChatInput.test.tsx +609 -0
- package/src/components/chat/ChatInput.tsx +2068 -0
- package/src/components/chat/ChatProjectBadge.tsx +362 -0
- package/src/components/chat/EmptyChat.tsx +18 -0
- package/src/components/chat/ExecPolicyPicker.test.tsx +26 -0
- package/src/components/chat/ExecPolicyPicker.tsx +120 -0
- package/src/components/chat/ExecutedActionsCard.tsx +216 -0
- package/src/components/chat/InterruptedTurnCard.tsx +53 -0
- package/src/components/chat/LocalChatPanel.tsx +408 -0
- package/src/components/chat/Markdown.test.tsx +170 -0
- package/src/components/chat/Markdown.tsx +432 -0
- package/src/components/chat/MessageList.tsx +332 -0
- package/src/components/chat/ModelPicker.test.tsx +149 -0
- package/src/components/chat/ModelPicker.tsx +289 -0
- package/src/components/chat/OrchestrationChildrenCard.tsx +40 -0
- package/src/components/chat/PlanTodoList.tsx +121 -0
- package/src/components/chat/ProjectTrustBanner.tsx +91 -0
- package/src/components/chat/SessionTreeModal.test.tsx +161 -0
- package/src/components/chat/SessionTreeModal.tsx +221 -0
- package/src/components/chat/StreamingStatus.tsx +95 -0
- package/src/components/chat/TaskOutcomeCards.test.tsx +97 -0
- package/src/components/chat/TaskOutcomeCards.tsx +128 -0
- package/src/components/chat/TaskPanelModal.test.tsx +198 -0
- package/src/components/chat/TaskPanelModal.tsx +310 -0
- package/src/components/chat/TaskProcessPanel.test.tsx +111 -0
- package/src/components/chat/TaskProcessPanel.tsx +208 -0
- package/src/components/chat/TurnProcessGroup.test.tsx +161 -0
- package/src/components/chat/TurnProcessGroup.tsx +193 -0
- package/src/components/chat/UnifiedDiff.tsx +31 -0
- package/src/components/chat/UserMessage.tsx +155 -0
- package/src/components/chat/elapsed.test.ts +42 -0
- package/src/components/chat/elapsed.ts +45 -0
- package/src/components/chat/executed-actions-model.test.ts +134 -0
- package/src/components/chat/executed-actions-model.ts +143 -0
- package/src/components/chat/orchestration-children-model.test.ts +115 -0
- package/src/components/chat/orchestration-children-model.ts +104 -0
- package/src/components/chat/timestamp.ts +24 -0
- package/src/components/chat/turn-timeline.test.ts +97 -0
- package/src/components/chat/turn-timeline.ts +132 -0
- package/src/components/chat/useChatTasks.test.ts +136 -0
- package/src/components/chat/useChatTasks.ts +134 -0
- package/src/components/chat/useCopy.ts +62 -0
- package/src/components/settings/AgentCapabilityFields.tsx +343 -0
- package/src/components/settings/AgentsSettingsPanel.test.tsx +229 -0
- package/src/components/settings/AgentsSettingsPanel.tsx +412 -0
- package/src/components/settings/DiagnoseSettingsPanel.tsx +119 -0
- package/src/components/settings/InsightsSettingsPanel.test.tsx +132 -0
- package/src/components/settings/InsightsSettingsPanel.tsx +309 -0
- package/src/components/settings/LlmSettingsPanel.tsx +875 -0
- package/src/components/settings/McpSettingsPanel.tsx +466 -0
- package/src/components/settings/ModelCapabilityChips.tsx +46 -0
- package/src/components/settings/ModelIdCombobox.test.tsx +88 -0
- package/src/components/settings/ModelIdCombobox.tsx +185 -0
- package/src/components/settings/SecuritySettingsPanel.test.tsx +122 -0
- package/src/components/settings/SecuritySettingsPanel.tsx +169 -0
- package/src/components/settings/SettingsSaveButton.tsx +35 -0
- package/src/components/settings/SkillsSettingsPanel.test.tsx +62 -0
- package/src/components/settings/SkillsSettingsPanel.tsx +334 -0
- package/src/components/settings/TelemetrySettingsPanel.tsx +173 -0
- package/src/components/settings/UsagePanel.tsx +196 -0
- package/src/components/settings/WebSearchSettingsPanel.test.tsx +90 -0
- package/src/components/settings/WebSearchSettingsPanel.tsx +183 -0
- package/src/components/settings/compat-flags-model.test.ts +71 -0
- package/src/components/settings/compat-flags-model.ts +57 -0
- package/src/components/settings/llm-vendors.test.ts +171 -0
- package/src/components/settings/llm-vendors.ts +275 -0
- package/src/components/settings/model-capabilities.test.ts +88 -0
- package/src/components/settings/model-capabilities.ts +123 -0
- package/src/components/settings/preset-choice-model.test.ts +96 -0
- package/src/components/settings/preset-choice-model.ts +75 -0
- package/src/fixtures/browser-dev-data.json +315 -0
- package/src/hooks/useChatsAndAgents.ts +182 -0
- package/src/layouts/AgentLayout.tsx +574 -0
- package/src/lib/attachments.ts +78 -0
- package/src/lib/browser-dev-electron-mock.ts +640 -0
- package/src/lib/chat-title.ts +33 -0
- package/src/lib/chat-transport.ts +346 -0
- package/src/lib/date-groups.ts +34 -0
- package/src/lib/demo-flag.ts +15 -0
- package/src/lib/electron-bridge.ts +270 -0
- package/src/lib/exec-policy.ts +25 -0
- package/src/lib/http-bridge.ts +293 -0
- package/src/lib/insights.ts +17 -0
- package/src/lib/local-api.ts +714 -0
- package/src/lib/pending-first-message.test.ts +17 -0
- package/src/lib/pending-first-message.ts +36 -0
- package/src/lib/slash-sources.ts +191 -0
- package/src/main.tsx +103 -0
- package/src/packs/registry.ts +64 -0
- package/src/pages/AgentPage.tsx +1226 -0
- package/src/pages/ChatPanelPreviewPage.tsx +264 -0
- package/src/pages/SettingsPage.test.tsx +102 -0
- package/src/pages/SettingsPage.tsx +197 -0
- package/src/styles/index.css +156 -0
- package/src/vite-env.d.ts +1 -0
- package/vite.base.ts +94 -0
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
LuLoaderCircle,
|
|
4
|
+
LuPencil,
|
|
5
|
+
LuPlus,
|
|
6
|
+
LuTrash2,
|
|
7
|
+
} from 'react-icons/lu';
|
|
8
|
+
import { isElectron } from '@/lib/electron-bridge';
|
|
9
|
+
import {
|
|
10
|
+
archiveChatAgent,
|
|
11
|
+
createChatAgent,
|
|
12
|
+
listChatAgents,
|
|
13
|
+
updateChatAgent,
|
|
14
|
+
type LocalChatAgent,
|
|
15
|
+
} from '@/lib/local-api';
|
|
16
|
+
import {
|
|
17
|
+
AgentCapabilityFields,
|
|
18
|
+
DEFAULT_CAPABILITY_DRAFT,
|
|
19
|
+
type AgentCapabilityDraft,
|
|
20
|
+
} from './AgentCapabilityFields';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* AgentsSettingsPanel — 智能体目录(列表 / 新建 / 编辑 / 归档)。
|
|
24
|
+
*
|
|
25
|
+
* 渲染在 `/settings?section=agents` 独立页。自定义智能体出现在输入框
|
|
26
|
+
* 上方的专家选择器里。内置智能体可改文案与能力,不能归档。
|
|
27
|
+
*
|
|
28
|
+
* 除文案外,每个智能体还持有真实生效的能力面(勾选技能 / 技能范围 /
|
|
29
|
+
* 工具权限),编辑控件见 {@link AgentCapabilityFields}。
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
const AGENT_COLORS = [
|
|
33
|
+
'#0ea5e9',
|
|
34
|
+
'#4f46e5',
|
|
35
|
+
'#a855f7',
|
|
36
|
+
'#16a34a',
|
|
37
|
+
'#ea580c',
|
|
38
|
+
'#e11d48',
|
|
39
|
+
'#0891b2',
|
|
40
|
+
'#7c3aed',
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
const DEFAULT_COLOR = AGENT_COLORS[0];
|
|
44
|
+
|
|
45
|
+
function nextSortOrder(agents: LocalChatAgent[]): number {
|
|
46
|
+
return agents.reduce((max, agent) => Math.max(max, agent.sortOrder ?? 0), 0) + 1;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function nextColor(agents: LocalChatAgent[]): string {
|
|
50
|
+
const used = new Set(agents.map((agent) => agent.color).filter(Boolean));
|
|
51
|
+
return AGENT_COLORS.find((item) => !used.has(item)) ?? DEFAULT_COLOR;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function agentInitial(agent: LocalChatAgent): string {
|
|
55
|
+
const name = agent.name.trim();
|
|
56
|
+
return name ? name[0].toUpperCase() : 'A';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** 列表行上的能力摘要——只列出与缺省不同的项,缺省配置不占视觉空间。 */
|
|
60
|
+
function capabilitySummary(agent: LocalChatAgent): string[] {
|
|
61
|
+
const labels: string[] = [];
|
|
62
|
+
const skillCount = agent.skillIds?.length ?? 0;
|
|
63
|
+
if (skillCount > 0) labels.push(`技能 ${skillCount}`);
|
|
64
|
+
if (agent.allowExternalSkills === false) labels.push('仅限所选技能');
|
|
65
|
+
if (agent.loadAllSkills) labels.push('全部技能');
|
|
66
|
+
const policy = agent.toolPolicy;
|
|
67
|
+
if (policy && policy.mode !== 'all' && policy.tools.length > 0) {
|
|
68
|
+
labels.push(
|
|
69
|
+
policy.mode === 'allowlist'
|
|
70
|
+
? `仅 ${policy.tools.length} 个工具`
|
|
71
|
+
: `禁用 ${policy.tools.length} 个工具`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
return labels;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function AgentsSettingsPanel({
|
|
78
|
+
onCatalogChange,
|
|
79
|
+
}: {
|
|
80
|
+
onCatalogChange?: () => Promise<void> | void;
|
|
81
|
+
}) {
|
|
82
|
+
const [agents, setAgents] = useState<LocalChatAgent[]>([]);
|
|
83
|
+
const [loading, setLoading] = useState(false);
|
|
84
|
+
const [saving, setSaving] = useState(false);
|
|
85
|
+
const [formOpen, setFormOpen] = useState(false);
|
|
86
|
+
const [editingId, setEditingId] = useState<string | null>(null);
|
|
87
|
+
const [name, setName] = useState('');
|
|
88
|
+
const [description, setDescription] = useState('');
|
|
89
|
+
const [rolePrompt, setRolePrompt] = useState('');
|
|
90
|
+
const [color, setColor] = useState(DEFAULT_COLOR);
|
|
91
|
+
const [capability, setCapability] = useState<AgentCapabilityDraft>(
|
|
92
|
+
DEFAULT_CAPABILITY_DRAFT,
|
|
93
|
+
);
|
|
94
|
+
const [confirmArchiveId, setConfirmArchiveId] = useState<string | null>(null);
|
|
95
|
+
const [archivingId, setArchivingId] = useState<string | null>(null);
|
|
96
|
+
const [error, setError] = useState<string | null>(null);
|
|
97
|
+
|
|
98
|
+
const fetchAgents = useCallback(async () => {
|
|
99
|
+
if (!isElectron()) return;
|
|
100
|
+
setLoading(true);
|
|
101
|
+
setError(null);
|
|
102
|
+
try {
|
|
103
|
+
const res = await listChatAgents(false);
|
|
104
|
+
setAgents(res.agents || []);
|
|
105
|
+
} catch (err) {
|
|
106
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
107
|
+
} finally {
|
|
108
|
+
setLoading(false);
|
|
109
|
+
}
|
|
110
|
+
}, []);
|
|
111
|
+
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
void fetchAgents();
|
|
114
|
+
}, [fetchAgents]);
|
|
115
|
+
|
|
116
|
+
const resetForm = () => {
|
|
117
|
+
setEditingId(null);
|
|
118
|
+
setName('');
|
|
119
|
+
setDescription('');
|
|
120
|
+
setRolePrompt('');
|
|
121
|
+
setColor(nextColor(agents));
|
|
122
|
+
setCapability(DEFAULT_CAPABILITY_DRAFT);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const openCreate = () => {
|
|
126
|
+
resetForm();
|
|
127
|
+
setColor(nextColor(agents));
|
|
128
|
+
setFormOpen(true);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const openEdit = (agent: LocalChatAgent) => {
|
|
132
|
+
setEditingId(agent.id);
|
|
133
|
+
setName(agent.name);
|
|
134
|
+
setDescription(agent.description ?? '');
|
|
135
|
+
setRolePrompt(agent.rolePrompt ?? '');
|
|
136
|
+
setColor(agent.color || DEFAULT_COLOR);
|
|
137
|
+
setCapability({
|
|
138
|
+
skillIds: agent.skillIds ?? [],
|
|
139
|
+
// 旧库行没有这两列时按「不限制」读——与后端缺省一致。
|
|
140
|
+
allowExternalSkills: agent.allowExternalSkills ?? true,
|
|
141
|
+
loadAllSkills: agent.loadAllSkills ?? false,
|
|
142
|
+
toolPolicy: agent.toolPolicy ?? { mode: 'all', tools: [] },
|
|
143
|
+
});
|
|
144
|
+
setFormOpen(true);
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const handleSave = async () => {
|
|
148
|
+
const trimmedName = name.trim();
|
|
149
|
+
if (!trimmedName || !isElectron()) return;
|
|
150
|
+
setSaving(true);
|
|
151
|
+
setError(null);
|
|
152
|
+
try {
|
|
153
|
+
const body = {
|
|
154
|
+
name: trimmedName,
|
|
155
|
+
color,
|
|
156
|
+
description: description.trim() || null,
|
|
157
|
+
rolePrompt: rolePrompt.trim() || `你是 **${trimmedName}**。`,
|
|
158
|
+
skillIds: capability.skillIds,
|
|
159
|
+
allowExternalSkills: capability.allowExternalSkills,
|
|
160
|
+
loadAllSkills: capability.loadAllSkills,
|
|
161
|
+
toolPolicy: capability.toolPolicy,
|
|
162
|
+
};
|
|
163
|
+
if (editingId) {
|
|
164
|
+
await updateChatAgent(editingId, body);
|
|
165
|
+
} else {
|
|
166
|
+
await createChatAgent({
|
|
167
|
+
...body,
|
|
168
|
+
sortOrder: nextSortOrder(agents),
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
resetForm();
|
|
172
|
+
setFormOpen(false);
|
|
173
|
+
await fetchAgents();
|
|
174
|
+
await onCatalogChange?.();
|
|
175
|
+
} catch (err) {
|
|
176
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
177
|
+
} finally {
|
|
178
|
+
setSaving(false);
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const handleArchive = async (agent: LocalChatAgent) => {
|
|
183
|
+
if (agent.isBuiltin) return;
|
|
184
|
+
if (confirmArchiveId !== agent.id) {
|
|
185
|
+
setConfirmArchiveId(agent.id);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
setArchivingId(agent.id);
|
|
189
|
+
setError(null);
|
|
190
|
+
try {
|
|
191
|
+
await archiveChatAgent(agent.id);
|
|
192
|
+
setConfirmArchiveId(null);
|
|
193
|
+
await fetchAgents();
|
|
194
|
+
await onCatalogChange?.();
|
|
195
|
+
} catch (err) {
|
|
196
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
197
|
+
} finally {
|
|
198
|
+
setArchivingId(null);
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
return (
|
|
203
|
+
<div className="space-y-4">
|
|
204
|
+
<p className="text-[11px] text-agent-muted-foreground">
|
|
205
|
+
自定义智能体会出现在输入框上方的专家选择器里。角色设定会作为对话的人设前言,
|
|
206
|
+
勾选的技能与工具权限在每一轮真实生效。内置智能体可以改文案和能力,但不能删除。
|
|
207
|
+
</p>
|
|
208
|
+
|
|
209
|
+
{formOpen ? (
|
|
210
|
+
<div className="space-y-2.5 rounded-agent-md border border-agent-border/60 bg-agent-muted/30 p-3.5">
|
|
211
|
+
<h4 className="text-xs font-semibold text-agent-foreground">
|
|
212
|
+
{editingId ? '编辑智能体' : '新建智能体'}
|
|
213
|
+
</h4>
|
|
214
|
+
<input
|
|
215
|
+
type="text"
|
|
216
|
+
value={name}
|
|
217
|
+
onChange={(event) => setName(event.target.value)}
|
|
218
|
+
placeholder="名称,如 地质顾问"
|
|
219
|
+
maxLength={40}
|
|
220
|
+
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"
|
|
221
|
+
data-testid="agent-form-name"
|
|
222
|
+
/>
|
|
223
|
+
<input
|
|
224
|
+
type="text"
|
|
225
|
+
value={description}
|
|
226
|
+
onChange={(event) => setDescription(event.target.value)}
|
|
227
|
+
placeholder="一句话简介(可选)"
|
|
228
|
+
maxLength={200}
|
|
229
|
+
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"
|
|
230
|
+
data-testid="agent-form-description"
|
|
231
|
+
/>
|
|
232
|
+
<div>
|
|
233
|
+
<p className="mb-1.5 text-[10px] font-medium text-agent-muted-foreground">颜色</p>
|
|
234
|
+
<div className="flex flex-wrap gap-1.5" data-testid="agent-form-colors">
|
|
235
|
+
{AGENT_COLORS.map((item) => (
|
|
236
|
+
<button
|
|
237
|
+
key={item}
|
|
238
|
+
type="button"
|
|
239
|
+
onClick={() => setColor(item)}
|
|
240
|
+
className={`h-6 w-6 rounded-full border-2 transition-transform ${
|
|
241
|
+
color === item
|
|
242
|
+
? 'scale-110 border-agent-foreground'
|
|
243
|
+
: 'border-transparent hover:scale-105'
|
|
244
|
+
}`}
|
|
245
|
+
style={{ backgroundColor: item }}
|
|
246
|
+
title={item}
|
|
247
|
+
aria-label={`颜色 ${item}`}
|
|
248
|
+
/>
|
|
249
|
+
))}
|
|
250
|
+
</div>
|
|
251
|
+
</div>
|
|
252
|
+
<textarea
|
|
253
|
+
value={rolePrompt}
|
|
254
|
+
onChange={(event) => setRolePrompt(event.target.value)}
|
|
255
|
+
placeholder="角色设定。例如:你是地质顾问,回答简洁,先给结论再给依据。"
|
|
256
|
+
rows={6}
|
|
257
|
+
className="w-full rounded-agent-md border border-agent-border bg-agent-canvas px-3 py-2 text-xs text-agent-foreground focus:outline-none focus:ring-2 focus:ring-agent-foreground/30"
|
|
258
|
+
data-testid="agent-form-role"
|
|
259
|
+
/>
|
|
260
|
+
<AgentCapabilityFields value={capability} onChange={setCapability} />
|
|
261
|
+
<div className="flex justify-end gap-2">
|
|
262
|
+
<button
|
|
263
|
+
type="button"
|
|
264
|
+
onClick={() => {
|
|
265
|
+
resetForm();
|
|
266
|
+
setFormOpen(false);
|
|
267
|
+
}}
|
|
268
|
+
className="h-8 rounded-full px-4 text-xs font-medium text-agent-muted-foreground transition-colors hover:bg-agent-muted hover:text-agent-foreground"
|
|
269
|
+
>
|
|
270
|
+
取消
|
|
271
|
+
</button>
|
|
272
|
+
<button
|
|
273
|
+
type="button"
|
|
274
|
+
onClick={() => void handleSave()}
|
|
275
|
+
disabled={saving || !name.trim()}
|
|
276
|
+
data-testid="agent-form-save"
|
|
277
|
+
className={`h-8 rounded-full px-4 text-xs font-medium transition-all ${
|
|
278
|
+
saving || !name.trim()
|
|
279
|
+
? 'cursor-not-allowed bg-agent-muted text-agent-muted-foreground'
|
|
280
|
+
: 'bg-agent-foreground text-agent-canvas hover:opacity-90'
|
|
281
|
+
}`}
|
|
282
|
+
>
|
|
283
|
+
{saving ? <LuLoaderCircle className="h-3 w-3 animate-spin" /> : '保存'}
|
|
284
|
+
</button>
|
|
285
|
+
</div>
|
|
286
|
+
</div>
|
|
287
|
+
) : (
|
|
288
|
+
<button
|
|
289
|
+
type="button"
|
|
290
|
+
onClick={openCreate}
|
|
291
|
+
className="flex items-center gap-1.5 text-xs font-medium text-agent-muted-foreground transition-colors hover:text-agent-foreground"
|
|
292
|
+
data-testid="agent-add"
|
|
293
|
+
>
|
|
294
|
+
<LuPlus className="h-3.5 w-3.5" />
|
|
295
|
+
添加智能体
|
|
296
|
+
</button>
|
|
297
|
+
)}
|
|
298
|
+
|
|
299
|
+
<div className="space-y-2">
|
|
300
|
+
<h4 className="text-xs font-semibold uppercase tracking-wide text-agent-muted-foreground">
|
|
301
|
+
已有智能体
|
|
302
|
+
{agents.length > 0 && (
|
|
303
|
+
<span className="ml-1.5 font-normal normal-case">· {agents.length}</span>
|
|
304
|
+
)}
|
|
305
|
+
</h4>
|
|
306
|
+
{loading ? (
|
|
307
|
+
<div className="flex items-center gap-2 py-4 text-xs text-agent-muted-foreground">
|
|
308
|
+
<LuLoaderCircle className="h-3.5 w-3.5 animate-spin" />
|
|
309
|
+
获取智能体...
|
|
310
|
+
</div>
|
|
311
|
+
) : agents.length === 0 ? (
|
|
312
|
+
<div className="py-6 text-center text-xs text-agent-muted-foreground">
|
|
313
|
+
暂无智能体
|
|
314
|
+
</div>
|
|
315
|
+
) : (
|
|
316
|
+
<div className="divide-y divide-agent-border/40 pr-1">
|
|
317
|
+
{agents.map((agent) => (
|
|
318
|
+
<div
|
|
319
|
+
key={agent.id}
|
|
320
|
+
className="flex items-start justify-between gap-3 py-2.5"
|
|
321
|
+
data-testid={`agent-row-${agent.id}`}
|
|
322
|
+
>
|
|
323
|
+
<div className="min-w-0 flex-1 space-y-1">
|
|
324
|
+
<div className="flex flex-wrap items-center gap-1.5">
|
|
325
|
+
<span
|
|
326
|
+
className="inline-flex h-5 w-5 flex-shrink-0 items-center justify-center rounded-full text-[9px] font-semibold text-white"
|
|
327
|
+
style={{ backgroundColor: agent.color || DEFAULT_COLOR }}
|
|
328
|
+
>
|
|
329
|
+
{agentInitial(agent)}
|
|
330
|
+
</span>
|
|
331
|
+
<span className="truncate text-xs font-medium text-agent-foreground">
|
|
332
|
+
{agent.name}
|
|
333
|
+
</span>
|
|
334
|
+
{agent.isBuiltin && (
|
|
335
|
+
<span className="flex-shrink-0 rounded bg-agent-muted px-1 py-0.5 text-[9px] font-medium text-agent-muted-foreground">
|
|
336
|
+
内置
|
|
337
|
+
</span>
|
|
338
|
+
)}
|
|
339
|
+
</div>
|
|
340
|
+
{agent.description && (
|
|
341
|
+
<p className="line-clamp-2 text-[11px] text-agent-muted-foreground">
|
|
342
|
+
{agent.description}
|
|
343
|
+
</p>
|
|
344
|
+
)}
|
|
345
|
+
{capabilitySummary(agent).length > 0 && (
|
|
346
|
+
<div
|
|
347
|
+
className="flex flex-wrap gap-1"
|
|
348
|
+
data-testid={`agent-capability-${agent.id}`}
|
|
349
|
+
>
|
|
350
|
+
{capabilitySummary(agent).map((label) => (
|
|
351
|
+
<span
|
|
352
|
+
key={label}
|
|
353
|
+
className="rounded bg-agent-muted/70 px-1 py-0.5 text-[9px] text-agent-muted-foreground"
|
|
354
|
+
>
|
|
355
|
+
{label}
|
|
356
|
+
</span>
|
|
357
|
+
))}
|
|
358
|
+
</div>
|
|
359
|
+
)}
|
|
360
|
+
</div>
|
|
361
|
+
<div className="flex shrink-0 items-center gap-0.5">
|
|
362
|
+
<button
|
|
363
|
+
type="button"
|
|
364
|
+
onClick={() => openEdit(agent)}
|
|
365
|
+
className="rounded-full p-1 text-agent-muted-foreground transition-colors hover:bg-agent-muted hover:text-agent-foreground"
|
|
366
|
+
title="编辑"
|
|
367
|
+
data-testid={`agent-edit-${agent.id}`}
|
|
368
|
+
>
|
|
369
|
+
<LuPencil className="h-3.5 w-3.5" />
|
|
370
|
+
</button>
|
|
371
|
+
{!agent.isBuiltin && (
|
|
372
|
+
<button
|
|
373
|
+
type="button"
|
|
374
|
+
onClick={() => void handleArchive(agent)}
|
|
375
|
+
disabled={archivingId === agent.id}
|
|
376
|
+
className={`rounded-full p-1 transition-colors ${
|
|
377
|
+
confirmArchiveId === agent.id
|
|
378
|
+
? 'bg-agent-destructive/10 text-agent-destructive hover:bg-agent-destructive/20'
|
|
379
|
+
: 'text-agent-muted-foreground hover:bg-agent-muted hover:text-agent-destructive'
|
|
380
|
+
}`}
|
|
381
|
+
title={
|
|
382
|
+
confirmArchiveId === agent.id
|
|
383
|
+
? '再次点击确认删除'
|
|
384
|
+
: '删除'
|
|
385
|
+
}
|
|
386
|
+
data-testid={`agent-archive-${agent.id}`}
|
|
387
|
+
>
|
|
388
|
+
{archivingId === agent.id ? (
|
|
389
|
+
<LuLoaderCircle className="h-3 w-3 animate-spin" />
|
|
390
|
+
) : (
|
|
391
|
+
<LuTrash2 className="h-3.5 w-3.5" />
|
|
392
|
+
)}
|
|
393
|
+
</button>
|
|
394
|
+
)}
|
|
395
|
+
</div>
|
|
396
|
+
</div>
|
|
397
|
+
))}
|
|
398
|
+
</div>
|
|
399
|
+
)}
|
|
400
|
+
</div>
|
|
401
|
+
|
|
402
|
+
{error && (
|
|
403
|
+
<div
|
|
404
|
+
className="rounded-agent-md border border-agent-destructive/20 bg-agent-destructive/10 p-2.5 text-xs text-agent-destructive"
|
|
405
|
+
data-testid="agents-error"
|
|
406
|
+
>
|
|
407
|
+
{error}
|
|
408
|
+
</div>
|
|
409
|
+
)}
|
|
410
|
+
</div>
|
|
411
|
+
);
|
|
412
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { LuCircleCheck, LuCircleX, LuLoader, LuPlay } from 'react-icons/lu';
|
|
3
|
+
import { diagnoseLlmConnection, type DiagnoseResult } from '@/lib/local-api';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 设置页「链路诊断」面板:一键探测当前 LLM 配置的连通性
|
|
7
|
+
* (DNS → TCP → TLS → HTTP /models → chat completion),并报告
|
|
8
|
+
* 宿主机的 ambient 代理配置。
|
|
9
|
+
*/
|
|
10
|
+
export function DiagnoseSettingsPanel() {
|
|
11
|
+
const [running, setRunning] = useState(false);
|
|
12
|
+
const [result, setResult] = useState<DiagnoseResult | null>(null);
|
|
13
|
+
const [error, setError] = useState<string | null>(null);
|
|
14
|
+
|
|
15
|
+
const run = async () => {
|
|
16
|
+
setRunning(true);
|
|
17
|
+
setError(null);
|
|
18
|
+
setResult(null);
|
|
19
|
+
try {
|
|
20
|
+
const res = await diagnoseLlmConnection({});
|
|
21
|
+
setResult(res);
|
|
22
|
+
} catch (err) {
|
|
23
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
24
|
+
} finally {
|
|
25
|
+
setRunning(false);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div className="space-y-3 rounded-agent-md border border-agent-border bg-agent-card p-4">
|
|
31
|
+
<p className="text-xs text-agent-muted-foreground">
|
|
32
|
+
诊断当前模型服务的网络连通性,包括 DNS 解析、TCP 连接、TLS 握手、HTTP 接口与对话补全。
|
|
33
|
+
诊断在宿主进程内执行,会同时报告系统/环境代理配置。
|
|
34
|
+
</p>
|
|
35
|
+
|
|
36
|
+
<button
|
|
37
|
+
type="button"
|
|
38
|
+
onClick={() => void run()}
|
|
39
|
+
disabled={running}
|
|
40
|
+
className="inline-flex items-center gap-1.5 rounded-agent-md bg-agent-primary px-3 py-1.5 text-xs font-medium text-agent-primary-foreground transition hover:bg-agent-primary/90 disabled:opacity-50"
|
|
41
|
+
>
|
|
42
|
+
{running ? (
|
|
43
|
+
<>
|
|
44
|
+
<LuLoader className="h-3.5 w-3.5 animate-spin" />
|
|
45
|
+
诊断中…
|
|
46
|
+
</>
|
|
47
|
+
) : (
|
|
48
|
+
<>
|
|
49
|
+
<LuPlay className="h-3.5 w-3.5" />
|
|
50
|
+
开始诊断
|
|
51
|
+
</>
|
|
52
|
+
)}
|
|
53
|
+
</button>
|
|
54
|
+
|
|
55
|
+
{error && (
|
|
56
|
+
<p className="rounded-agent-md border border-agent-destructive/20 bg-agent-destructive/10 p-2.5 text-xs text-agent-destructive">
|
|
57
|
+
诊断请求失败:{error}
|
|
58
|
+
</p>
|
|
59
|
+
)}
|
|
60
|
+
|
|
61
|
+
{result && (
|
|
62
|
+
<div className="space-y-2">
|
|
63
|
+
<div className="flex items-center gap-2 text-sm">
|
|
64
|
+
{result.ok ? (
|
|
65
|
+
<span className="inline-flex items-center gap-1 text-agent-success">
|
|
66
|
+
<LuCircleCheck className="h-4 w-4" />
|
|
67
|
+
全部通过
|
|
68
|
+
</span>
|
|
69
|
+
) : (
|
|
70
|
+
<span className="inline-flex items-center gap-1 text-agent-destructive">
|
|
71
|
+
<LuCircleX className="h-4 w-4" />
|
|
72
|
+
诊断失败
|
|
73
|
+
</span>
|
|
74
|
+
)}
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<ol className="space-y-1.5">
|
|
78
|
+
{result.steps.map((step) => (
|
|
79
|
+
<li
|
|
80
|
+
key={step.name}
|
|
81
|
+
className="flex items-start gap-2 rounded-agent-sm bg-agent-muted/50 px-2.5 py-1.5 text-xs"
|
|
82
|
+
>
|
|
83
|
+
{step.ok ? (
|
|
84
|
+
<LuCircleCheck className="mt-0.5 h-3.5 w-3.5 flex-shrink-0 text-agent-success" />
|
|
85
|
+
) : (
|
|
86
|
+
<LuCircleX className="mt-0.5 h-3.5 w-3.5 flex-shrink-0 text-agent-destructive" />
|
|
87
|
+
)}
|
|
88
|
+
<div className="min-w-0 flex-1">
|
|
89
|
+
<div className="flex items-center justify-between gap-2">
|
|
90
|
+
<span className="font-medium text-agent-foreground">{step.name}</span>
|
|
91
|
+
<span className="text-agent-muted-foreground">{step.durationMs}ms</span>
|
|
92
|
+
</div>
|
|
93
|
+
<p className="mt-0.5 break-all text-agent-muted-foreground">{step.detail}</p>
|
|
94
|
+
</div>
|
|
95
|
+
</li>
|
|
96
|
+
))}
|
|
97
|
+
</ol>
|
|
98
|
+
|
|
99
|
+
{result.ambientProxies.length > 0 && (
|
|
100
|
+
<div className="rounded-agent-sm border border-agent-border bg-agent-muted/30 p-2.5 text-xs">
|
|
101
|
+
<p className="font-medium text-agent-foreground">检测到的代理配置</p>
|
|
102
|
+
<ul className="mt-1 list-inside list-disc space-y-0.5 text-agent-muted-foreground">
|
|
103
|
+
{result.ambientProxies.map((proxy) => (
|
|
104
|
+
<li key={proxy}>{proxy}</li>
|
|
105
|
+
))}
|
|
106
|
+
</ul>
|
|
107
|
+
</div>
|
|
108
|
+
)}
|
|
109
|
+
|
|
110
|
+
{result.hint && (
|
|
111
|
+
<p className="rounded-agent-md border border-agent-warning/20 bg-agent-warning/10 p-2.5 text-xs text-agent-warning">
|
|
112
|
+
{result.hint}
|
|
113
|
+
</p>
|
|
114
|
+
)}
|
|
115
|
+
</div>
|
|
116
|
+
)}
|
|
117
|
+
</div>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
const request = vi.fn();
|
|
5
|
+
|
|
6
|
+
vi.mock('@/lib/electron-bridge', () => ({
|
|
7
|
+
isElectron: () => true,
|
|
8
|
+
getElectronBridge: () => ({
|
|
9
|
+
localBackend: { request },
|
|
10
|
+
local: { saveTextFile: vi.fn() },
|
|
11
|
+
}),
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
const { InsightsConsentBanner, InsightsSettingsPanel } = await import('./InsightsSettingsPanel');
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
request.mockReset();
|
|
18
|
+
request.mockResolvedValue({
|
|
19
|
+
installId: '550e8400-e29b-41d4-a716-446655440000',
|
|
20
|
+
shareBehavior: false,
|
|
21
|
+
shareConversation: false,
|
|
22
|
+
shareProfile: false,
|
|
23
|
+
profile: { displayName: '', email: '', company: '', note: '' },
|
|
24
|
+
stats: { events: 2, turns: 1, profile: 0, pending: 3 },
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
afterEach(cleanup);
|
|
29
|
+
|
|
30
|
+
describe('InsightsConsentBanner', () => {
|
|
31
|
+
it('renders a simple upload-or-cancel first-run banner', async () => {
|
|
32
|
+
render(<InsightsConsentBanner />);
|
|
33
|
+
const banner = await screen.findByTestId('insights-consent-banner');
|
|
34
|
+
expect(banner.className).toContain('bg-agent-muted');
|
|
35
|
+
// 3.1 起品牌由产品注入;测试环境无注入 = shell 中性默认。
|
|
36
|
+
expect(screen.getByText('帮助改进Steerable Shell')).toBeTruthy();
|
|
37
|
+
expect(screen.getByText(/同意上传数据到服务器/)).toBeTruthy();
|
|
38
|
+
expect(screen.queryAllByRole('checkbox')).toHaveLength(0);
|
|
39
|
+
expect(screen.getByRole('button', { name: '保存' })).toBeTruthy();
|
|
40
|
+
expect(screen.getByRole('button', { name: '取消' })).toBeTruthy();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('save agrees to upload; cancel keeps data local', async () => {
|
|
44
|
+
render(<InsightsConsentBanner />);
|
|
45
|
+
await screen.findByTestId('insights-consent-banner');
|
|
46
|
+
fireEvent.click(screen.getByRole('button', { name: '保存' }));
|
|
47
|
+
await waitFor(() => {
|
|
48
|
+
expect(request).toHaveBeenCalledWith(
|
|
49
|
+
expect.objectContaining({
|
|
50
|
+
method: 'POST',
|
|
51
|
+
path: '/api/v2/local-settings/insights',
|
|
52
|
+
body: expect.objectContaining({
|
|
53
|
+
shareBehavior: true,
|
|
54
|
+
shareConversation: true,
|
|
55
|
+
shareProfile: true,
|
|
56
|
+
markPrompted: true,
|
|
57
|
+
}),
|
|
58
|
+
}),
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('cancel declines upload and still dismisses the prompt', async () => {
|
|
64
|
+
render(<InsightsConsentBanner />);
|
|
65
|
+
await screen.findByTestId('insights-consent-banner');
|
|
66
|
+
fireEvent.click(screen.getByRole('button', { name: '取消' }));
|
|
67
|
+
await waitFor(() => {
|
|
68
|
+
expect(request).toHaveBeenCalledWith(
|
|
69
|
+
expect.objectContaining({
|
|
70
|
+
method: 'POST',
|
|
71
|
+
path: '/api/v2/local-settings/insights',
|
|
72
|
+
body: expect.objectContaining({
|
|
73
|
+
shareBehavior: false,
|
|
74
|
+
shareConversation: false,
|
|
75
|
+
shareProfile: false,
|
|
76
|
+
markPrompted: true,
|
|
77
|
+
}),
|
|
78
|
+
}),
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('does not open after the prompt has been answered', async () => {
|
|
84
|
+
request.mockResolvedValueOnce({
|
|
85
|
+
installId: '550e8400-e29b-41d4-a716-446655440000',
|
|
86
|
+
shareBehavior: false,
|
|
87
|
+
shareConversation: false,
|
|
88
|
+
shareProfile: false,
|
|
89
|
+
promptedAt: '2026-09-12T00:00:00.000Z',
|
|
90
|
+
profile: { displayName: '', email: '', company: '', note: '' },
|
|
91
|
+
});
|
|
92
|
+
render(<InsightsConsentBanner />);
|
|
93
|
+
await waitFor(() => {
|
|
94
|
+
expect(request).toHaveBeenCalledWith(
|
|
95
|
+
expect.objectContaining({ method: 'GET', path: '/api/v2/local-settings/insights' }),
|
|
96
|
+
);
|
|
97
|
+
});
|
|
98
|
+
expect(screen.queryByTestId('insights-consent-banner')).toBeNull();
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe('InsightsSettingsPanel', () => {
|
|
103
|
+
it('shows separate toggles for behavior, conversation, and profile', async () => {
|
|
104
|
+
render(<InsightsSettingsPanel />);
|
|
105
|
+
expect(await screen.findByText(/帮助改进产品(行为 \/ 对话 \/ 用户信息 分开确认)/)).toBeTruthy();
|
|
106
|
+
expect(screen.getAllByText(/自动上传/).length).toBe(3);
|
|
107
|
+
expect(screen.getAllByRole('checkbox')).toHaveLength(3);
|
|
108
|
+
expect(screen.getByText(/导出文件发给开发者/)).toBeTruthy();
|
|
109
|
+
expect(screen.getByText(/现在上传本地记录/)).toBeTruthy();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('saves the three flags independently', async () => {
|
|
113
|
+
render(<InsightsSettingsPanel />);
|
|
114
|
+
await screen.findByText(/本机已记/);
|
|
115
|
+
const boxes = screen.getAllByRole('checkbox');
|
|
116
|
+
fireEvent.click(boxes[0]);
|
|
117
|
+
fireEvent.click(screen.getByRole('button', { name: '保存' }));
|
|
118
|
+
await waitFor(() => {
|
|
119
|
+
expect(request).toHaveBeenCalledWith(
|
|
120
|
+
expect.objectContaining({
|
|
121
|
+
method: 'POST',
|
|
122
|
+
path: '/api/v2/local-settings/insights',
|
|
123
|
+
body: expect.objectContaining({
|
|
124
|
+
shareBehavior: true,
|
|
125
|
+
shareConversation: false,
|
|
126
|
+
shareProfile: false,
|
|
127
|
+
}),
|
|
128
|
+
}),
|
|
129
|
+
);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
});
|