@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,343 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import { LuLoaderCircle, LuSearch } from 'react-icons/lu';
|
|
3
|
+
import { isElectron } from '@/lib/electron-bridge';
|
|
4
|
+
import {
|
|
5
|
+
listChatAgentSkills,
|
|
6
|
+
listChatAgentTools,
|
|
7
|
+
type ChatAgentSkillOption,
|
|
8
|
+
type ChatAgentToolOption,
|
|
9
|
+
type ChatAgentToolPolicy,
|
|
10
|
+
} from '@/lib/local-api';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* AgentCapabilityFields — 智能体表单里的「能力」部分:技能勾选、技能范围
|
|
14
|
+
* 开关、工具权限。
|
|
15
|
+
*
|
|
16
|
+
* 这些字段是真实生效的,不是展示用的偏好:
|
|
17
|
+
* - 勾选的技能正文无条件进该智能体的系统提示词(绕过技能自身的触发条件);
|
|
18
|
+
* - 关掉「允许其他技能」后,未勾选的技能在注入、按需加载目录、"/技能名"
|
|
19
|
+
* 三处同时消失;
|
|
20
|
+
* - 工具权限决定每轮给模型的工具列表、`tool_search` 能发现什么,以及
|
|
21
|
+
* 分发层是否接受调用。
|
|
22
|
+
*
|
|
23
|
+
* 技能与工具目录自己拉(挂载即取),父表单只持有草稿值。
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/** 表单里的能力面草稿。 */
|
|
27
|
+
export interface AgentCapabilityDraft {
|
|
28
|
+
skillIds: string[];
|
|
29
|
+
allowExternalSkills: boolean;
|
|
30
|
+
loadAllSkills: boolean;
|
|
31
|
+
toolPolicy: ChatAgentToolPolicy;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const DEFAULT_CAPABILITY_DRAFT: AgentCapabilityDraft = {
|
|
35
|
+
skillIds: [],
|
|
36
|
+
allowExternalSkills: true,
|
|
37
|
+
loadAllSkills: false,
|
|
38
|
+
toolPolicy: { mode: 'all', tools: [] },
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const TOOL_POLICY_MODES: Array<{
|
|
42
|
+
mode: ChatAgentToolPolicy['mode'];
|
|
43
|
+
label: string;
|
|
44
|
+
hint: string;
|
|
45
|
+
}> = [
|
|
46
|
+
{ mode: 'all', label: '全部工具', hint: '不限制,可调用所有已接线的工具。' },
|
|
47
|
+
{
|
|
48
|
+
mode: 'allowlist',
|
|
49
|
+
label: '仅允许勾选',
|
|
50
|
+
hint: '只有勾选的工具进入工具列表,其余连名字都看不到。',
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
mode: 'denylist',
|
|
54
|
+
label: '禁用勾选',
|
|
55
|
+
hint: '勾选的工具被拒绝,其余照常可用。',
|
|
56
|
+
},
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
function toggle(list: readonly string[], item: string): string[] {
|
|
60
|
+
return list.includes(item)
|
|
61
|
+
? list.filter((entry) => entry !== item)
|
|
62
|
+
: [...list, item];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function CheckRow({
|
|
66
|
+
checked,
|
|
67
|
+
onChange,
|
|
68
|
+
testId,
|
|
69
|
+
title,
|
|
70
|
+
badge,
|
|
71
|
+
description,
|
|
72
|
+
}: {
|
|
73
|
+
checked: boolean;
|
|
74
|
+
onChange: () => void;
|
|
75
|
+
testId: string;
|
|
76
|
+
title: string;
|
|
77
|
+
badge?: string;
|
|
78
|
+
description?: string;
|
|
79
|
+
}) {
|
|
80
|
+
return (
|
|
81
|
+
<label
|
|
82
|
+
className="flex cursor-pointer items-start gap-2 rounded px-1.5 py-1.5 transition-colors hover:bg-agent-foreground/5"
|
|
83
|
+
data-testid={testId}
|
|
84
|
+
>
|
|
85
|
+
<input
|
|
86
|
+
type="checkbox"
|
|
87
|
+
checked={checked}
|
|
88
|
+
onChange={onChange}
|
|
89
|
+
className="mt-0.5 h-3 w-3 shrink-0 accent-agent-foreground"
|
|
90
|
+
/>
|
|
91
|
+
<span className="min-w-0 flex-1">
|
|
92
|
+
<span className="flex items-center gap-1.5">
|
|
93
|
+
<span className="truncate text-[11px] font-medium text-agent-foreground">
|
|
94
|
+
{title}
|
|
95
|
+
</span>
|
|
96
|
+
{badge && (
|
|
97
|
+
<span className="shrink-0 rounded bg-agent-muted px-1 py-0.5 text-[9px] text-agent-muted-foreground">
|
|
98
|
+
{badge}
|
|
99
|
+
</span>
|
|
100
|
+
)}
|
|
101
|
+
</span>
|
|
102
|
+
{description && (
|
|
103
|
+
<span className="mt-0.5 line-clamp-2 block text-[10px] text-agent-muted-foreground">
|
|
104
|
+
{description}
|
|
105
|
+
</span>
|
|
106
|
+
)}
|
|
107
|
+
</span>
|
|
108
|
+
</label>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function SwitchRow({
|
|
113
|
+
checked,
|
|
114
|
+
onChange,
|
|
115
|
+
testId,
|
|
116
|
+
label,
|
|
117
|
+
hint,
|
|
118
|
+
}: {
|
|
119
|
+
checked: boolean;
|
|
120
|
+
onChange: () => void;
|
|
121
|
+
testId: string;
|
|
122
|
+
label: string;
|
|
123
|
+
hint: string;
|
|
124
|
+
}) {
|
|
125
|
+
return (
|
|
126
|
+
<label className="flex cursor-pointer items-start gap-2">
|
|
127
|
+
<input
|
|
128
|
+
type="checkbox"
|
|
129
|
+
checked={checked}
|
|
130
|
+
onChange={onChange}
|
|
131
|
+
data-testid={testId}
|
|
132
|
+
className="mt-0.5 h-3 w-3 shrink-0 accent-agent-foreground"
|
|
133
|
+
/>
|
|
134
|
+
<span className="min-w-0 flex-1">
|
|
135
|
+
<span className="block text-[11px] font-medium text-agent-foreground">{label}</span>
|
|
136
|
+
<span className="block text-[10px] text-agent-muted-foreground">{hint}</span>
|
|
137
|
+
</span>
|
|
138
|
+
</label>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function AgentCapabilityFields({
|
|
143
|
+
value,
|
|
144
|
+
onChange,
|
|
145
|
+
}: {
|
|
146
|
+
value: AgentCapabilityDraft;
|
|
147
|
+
onChange: (next: AgentCapabilityDraft) => void;
|
|
148
|
+
}) {
|
|
149
|
+
const [skills, setSkills] = useState<ChatAgentSkillOption[]>([]);
|
|
150
|
+
const [tools, setTools] = useState<ChatAgentToolOption[]>([]);
|
|
151
|
+
const [loading, setLoading] = useState(false);
|
|
152
|
+
const [skillFilter, setSkillFilter] = useState('');
|
|
153
|
+
const [toolFilter, setToolFilter] = useState('');
|
|
154
|
+
|
|
155
|
+
const fetchCatalogs = useCallback(async () => {
|
|
156
|
+
if (!isElectron()) return;
|
|
157
|
+
setLoading(true);
|
|
158
|
+
try {
|
|
159
|
+
const [skillRes, toolRes] = await Promise.all([
|
|
160
|
+
listChatAgentSkills(),
|
|
161
|
+
listChatAgentTools(),
|
|
162
|
+
]);
|
|
163
|
+
setSkills(skillRes.skills || []);
|
|
164
|
+
setTools(toolRes.tools || []);
|
|
165
|
+
} catch {
|
|
166
|
+
// 目录取不到时只降级为「没有可勾选项」——已保存的 skillIds /
|
|
167
|
+
// toolPolicy 不受影响,表单的其余字段照常可用。
|
|
168
|
+
setSkills([]);
|
|
169
|
+
setTools([]);
|
|
170
|
+
} finally {
|
|
171
|
+
setLoading(false);
|
|
172
|
+
}
|
|
173
|
+
}, []);
|
|
174
|
+
|
|
175
|
+
useEffect(() => {
|
|
176
|
+
void fetchCatalogs();
|
|
177
|
+
}, [fetchCatalogs]);
|
|
178
|
+
|
|
179
|
+
const visibleSkills = useMemo(() => {
|
|
180
|
+
const key = skillFilter.trim().toLowerCase();
|
|
181
|
+
if (!key) return skills;
|
|
182
|
+
return skills.filter((skill) =>
|
|
183
|
+
[skill.id, skill.name, skill.displayName, skill.description]
|
|
184
|
+
.filter(Boolean)
|
|
185
|
+
.some((field) => field.toLowerCase().includes(key)),
|
|
186
|
+
);
|
|
187
|
+
}, [skills, skillFilter]);
|
|
188
|
+
|
|
189
|
+
const visibleTools = useMemo(() => {
|
|
190
|
+
const key = toolFilter.trim().toLowerCase();
|
|
191
|
+
if (!key) return tools;
|
|
192
|
+
return tools.filter((tool) =>
|
|
193
|
+
[tool.name, tool.description]
|
|
194
|
+
.filter(Boolean)
|
|
195
|
+
.some((field) => field.toLowerCase().includes(key)),
|
|
196
|
+
);
|
|
197
|
+
}, [tools, toolFilter]);
|
|
198
|
+
|
|
199
|
+
const policy = value.toolPolicy;
|
|
200
|
+
const activeMode = TOOL_POLICY_MODES.find((item) => item.mode === policy.mode);
|
|
201
|
+
|
|
202
|
+
return (
|
|
203
|
+
<div className="space-y-3 rounded-agent-md border border-agent-border/50 bg-agent-canvas/60 p-3">
|
|
204
|
+
<section className="space-y-1.5">
|
|
205
|
+
<div className="flex items-center justify-between gap-2">
|
|
206
|
+
<h5 className="text-[11px] font-semibold text-agent-foreground">技能</h5>
|
|
207
|
+
{value.skillIds.length > 0 && (
|
|
208
|
+
<span className="text-[10px] text-agent-muted-foreground">
|
|
209
|
+
已选 {value.skillIds.length}
|
|
210
|
+
</span>
|
|
211
|
+
)}
|
|
212
|
+
</div>
|
|
213
|
+
<p className="text-[10px] text-agent-muted-foreground">
|
|
214
|
+
勾选的技能正文会常驻该智能体的系统提示词,不必等触发条件命中。
|
|
215
|
+
</p>
|
|
216
|
+
{loading ? (
|
|
217
|
+
<div className="flex items-center gap-2 py-3 text-[11px] text-agent-muted-foreground">
|
|
218
|
+
<LuLoaderCircle className="h-3 w-3 animate-spin" />
|
|
219
|
+
获取技能目录...
|
|
220
|
+
</div>
|
|
221
|
+
) : skills.length === 0 ? (
|
|
222
|
+
<p className="py-2 text-[11px] text-agent-muted-foreground">
|
|
223
|
+
暂无可勾选的技能,可在「Skill 设置」里导入。
|
|
224
|
+
</p>
|
|
225
|
+
) : (
|
|
226
|
+
<>
|
|
227
|
+
{skills.length > 8 && (
|
|
228
|
+
<div className="relative">
|
|
229
|
+
<LuSearch className="pointer-events-none absolute left-2 top-1/2 h-3 w-3 -translate-y-1/2 text-agent-muted-foreground" />
|
|
230
|
+
<input
|
|
231
|
+
type="text"
|
|
232
|
+
value={skillFilter}
|
|
233
|
+
onChange={(event) => setSkillFilter(event.target.value)}
|
|
234
|
+
placeholder="筛选技能"
|
|
235
|
+
className="h-7 w-full rounded-agent-md border border-agent-border bg-agent-canvas pl-7 pr-2 text-[11px] text-agent-foreground focus:outline-none focus:ring-2 focus:ring-agent-foreground/30"
|
|
236
|
+
data-testid="agent-form-skill-filter"
|
|
237
|
+
/>
|
|
238
|
+
</div>
|
|
239
|
+
)}
|
|
240
|
+
<div className="max-h-44 overflow-y-auto" data-testid="agent-form-skills">
|
|
241
|
+
{visibleSkills.map((skill) => (
|
|
242
|
+
<CheckRow
|
|
243
|
+
key={skill.id}
|
|
244
|
+
checked={value.skillIds.includes(skill.id)}
|
|
245
|
+
onChange={() =>
|
|
246
|
+
onChange({ ...value, skillIds: toggle(value.skillIds, skill.id) })
|
|
247
|
+
}
|
|
248
|
+
testId={`agent-form-skill-${skill.id}`}
|
|
249
|
+
title={skill.displayName || skill.name}
|
|
250
|
+
badge={skill.layer === 'eager' ? '常驻' : '按需'}
|
|
251
|
+
description={skill.description}
|
|
252
|
+
/>
|
|
253
|
+
))}
|
|
254
|
+
</div>
|
|
255
|
+
</>
|
|
256
|
+
)}
|
|
257
|
+
<div className="space-y-2 pt-1">
|
|
258
|
+
<SwitchRow
|
|
259
|
+
checked={value.allowExternalSkills}
|
|
260
|
+
onChange={() =>
|
|
261
|
+
onChange({ ...value, allowExternalSkills: !value.allowExternalSkills })
|
|
262
|
+
}
|
|
263
|
+
testId="agent-form-allow-external-skills"
|
|
264
|
+
label="允许使用未勾选的技能"
|
|
265
|
+
hint="关掉后这个智能体只能用上面勾选的技能,其余一律加载不到。"
|
|
266
|
+
/>
|
|
267
|
+
<SwitchRow
|
|
268
|
+
checked={value.loadAllSkills}
|
|
269
|
+
onChange={() => onChange({ ...value, loadAllSkills: !value.loadAllSkills })}
|
|
270
|
+
testId="agent-form-load-all-skills"
|
|
271
|
+
label="无视触发条件,加载全部技能"
|
|
272
|
+
hint="内置「智能助手」就是这个模式。会显著增加每轮提示词长度。"
|
|
273
|
+
/>
|
|
274
|
+
</div>
|
|
275
|
+
</section>
|
|
276
|
+
|
|
277
|
+
<section className="space-y-1.5 border-t border-agent-border/40 pt-3">
|
|
278
|
+
<h5 className="text-[11px] font-semibold text-agent-foreground">工具权限</h5>
|
|
279
|
+
<div className="flex flex-wrap gap-1" data-testid="agent-form-tool-modes">
|
|
280
|
+
{TOOL_POLICY_MODES.map((item) => (
|
|
281
|
+
<button
|
|
282
|
+
key={item.mode}
|
|
283
|
+
type="button"
|
|
284
|
+
onClick={() => onChange({ ...value, toolPolicy: { ...policy, mode: item.mode } })}
|
|
285
|
+
data-testid={`agent-form-tool-mode-${item.mode}`}
|
|
286
|
+
aria-pressed={policy.mode === item.mode}
|
|
287
|
+
className={`h-6 rounded-full px-2.5 text-[10px] font-medium transition-colors ${
|
|
288
|
+
policy.mode === item.mode
|
|
289
|
+
? 'bg-agent-foreground text-agent-canvas'
|
|
290
|
+
: 'bg-agent-muted text-agent-muted-foreground hover:text-agent-foreground'
|
|
291
|
+
}`}
|
|
292
|
+
>
|
|
293
|
+
{item.label}
|
|
294
|
+
</button>
|
|
295
|
+
))}
|
|
296
|
+
</div>
|
|
297
|
+
{activeMode && (
|
|
298
|
+
<p className="text-[10px] text-agent-muted-foreground">{activeMode.hint}</p>
|
|
299
|
+
)}
|
|
300
|
+
{policy.mode !== 'all' && (
|
|
301
|
+
<>
|
|
302
|
+
{policy.tools.length === 0 && (
|
|
303
|
+
<p
|
|
304
|
+
className="text-[10px] text-agent-destructive"
|
|
305
|
+
data-testid="agent-form-tool-policy-empty"
|
|
306
|
+
>
|
|
307
|
+
一个工具都没勾选——保存后按「全部工具」处理。
|
|
308
|
+
</p>
|
|
309
|
+
)}
|
|
310
|
+
<div className="relative">
|
|
311
|
+
<LuSearch className="pointer-events-none absolute left-2 top-1/2 h-3 w-3 -translate-y-1/2 text-agent-muted-foreground" />
|
|
312
|
+
<input
|
|
313
|
+
type="text"
|
|
314
|
+
value={toolFilter}
|
|
315
|
+
onChange={(event) => setToolFilter(event.target.value)}
|
|
316
|
+
placeholder="筛选工具"
|
|
317
|
+
className="h-7 w-full rounded-agent-md border border-agent-border bg-agent-canvas pl-7 pr-2 text-[11px] text-agent-foreground focus:outline-none focus:ring-2 focus:ring-agent-foreground/30"
|
|
318
|
+
data-testid="agent-form-tool-filter"
|
|
319
|
+
/>
|
|
320
|
+
</div>
|
|
321
|
+
<div className="max-h-44 overflow-y-auto" data-testid="agent-form-tools">
|
|
322
|
+
{visibleTools.map((tool) => (
|
|
323
|
+
<CheckRow
|
|
324
|
+
key={tool.name}
|
|
325
|
+
checked={policy.tools.includes(tool.name)}
|
|
326
|
+
onChange={() =>
|
|
327
|
+
onChange({
|
|
328
|
+
...value,
|
|
329
|
+
toolPolicy: { ...policy, tools: toggle(policy.tools, tool.name) },
|
|
330
|
+
})
|
|
331
|
+
}
|
|
332
|
+
testId={`agent-form-tool-${tool.name}`}
|
|
333
|
+
title={tool.name}
|
|
334
|
+
description={tool.description}
|
|
335
|
+
/>
|
|
336
|
+
))}
|
|
337
|
+
</div>
|
|
338
|
+
</>
|
|
339
|
+
)}
|
|
340
|
+
</section>
|
|
341
|
+
</div>
|
|
342
|
+
);
|
|
343
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import type { LocalChatAgent } from '@/lib/local-api';
|
|
4
|
+
|
|
5
|
+
const listChatAgents = vi.fn();
|
|
6
|
+
const createChatAgent = vi.fn();
|
|
7
|
+
const updateChatAgent = vi.fn();
|
|
8
|
+
const archiveChatAgent = vi.fn();
|
|
9
|
+
const listChatAgentSkills = vi.fn();
|
|
10
|
+
const listChatAgentTools = vi.fn();
|
|
11
|
+
|
|
12
|
+
vi.mock('@/lib/electron-bridge', () => ({
|
|
13
|
+
isElectron: () => true,
|
|
14
|
+
getElectronBridge: () => ({
|
|
15
|
+
localBackend: { request: vi.fn() },
|
|
16
|
+
local: {},
|
|
17
|
+
}),
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
vi.mock('@/lib/local-api', () => ({
|
|
21
|
+
listChatAgents: (...args: unknown[]) => listChatAgents(...args),
|
|
22
|
+
createChatAgent: (...args: unknown[]) => createChatAgent(...args),
|
|
23
|
+
updateChatAgent: (...args: unknown[]) => updateChatAgent(...args),
|
|
24
|
+
archiveChatAgent: (...args: unknown[]) => archiveChatAgent(...args),
|
|
25
|
+
listChatAgentSkills: (...args: unknown[]) => listChatAgentSkills(...args),
|
|
26
|
+
listChatAgentTools: (...args: unknown[]) => listChatAgentTools(...args),
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
const { AgentsSettingsPanel } = await import('./AgentsSettingsPanel');
|
|
30
|
+
|
|
31
|
+
const builtin: LocalChatAgent = {
|
|
32
|
+
id: 'local-assistant',
|
|
33
|
+
slug: 'local-assistant',
|
|
34
|
+
name: '电脑操作员',
|
|
35
|
+
icon: null,
|
|
36
|
+
color: '#4f46e5',
|
|
37
|
+
description: '默认本地助手',
|
|
38
|
+
rolePrompt: '你是本地离线助手。',
|
|
39
|
+
isBuiltin: true,
|
|
40
|
+
sortOrder: 0,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const custom: LocalChatAgent = {
|
|
44
|
+
id: 'geo-advisor',
|
|
45
|
+
slug: null,
|
|
46
|
+
name: '地质顾问',
|
|
47
|
+
icon: null,
|
|
48
|
+
color: '#16a34a',
|
|
49
|
+
description: '答地质问题',
|
|
50
|
+
rolePrompt: '你是地质顾问。',
|
|
51
|
+
isBuiltin: false,
|
|
52
|
+
sortOrder: 1,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
beforeEach(() => {
|
|
56
|
+
listChatAgents.mockReset();
|
|
57
|
+
createChatAgent.mockReset();
|
|
58
|
+
updateChatAgent.mockReset();
|
|
59
|
+
archiveChatAgent.mockReset();
|
|
60
|
+
listChatAgentSkills.mockReset();
|
|
61
|
+
listChatAgentTools.mockReset();
|
|
62
|
+
listChatAgents.mockResolvedValue({ agents: [builtin], total: 1 });
|
|
63
|
+
createChatAgent.mockResolvedValue({ agent: custom });
|
|
64
|
+
updateChatAgent.mockResolvedValue({ agent: custom });
|
|
65
|
+
archiveChatAgent.mockResolvedValue({ id: custom.id, status: 'archived' });
|
|
66
|
+
listChatAgentSkills.mockResolvedValue({
|
|
67
|
+
skills: [
|
|
68
|
+
{
|
|
69
|
+
id: '90-csv-tools',
|
|
70
|
+
name: 'csv-tools',
|
|
71
|
+
displayName: 'CSV 工具',
|
|
72
|
+
description: '处理本地 CSV 数据。',
|
|
73
|
+
layer: 'catalog',
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
id: '00-identity',
|
|
77
|
+
name: 'identity',
|
|
78
|
+
displayName: '',
|
|
79
|
+
description: '基础身份。',
|
|
80
|
+
layer: 'eager',
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
});
|
|
84
|
+
listChatAgentTools.mockResolvedValue({
|
|
85
|
+
tools: [
|
|
86
|
+
{ name: 'local_exec_shell', description: '执行本地命令。', category: 'local' },
|
|
87
|
+
{ name: 'local_read_file', description: '读取文件。', category: 'local' },
|
|
88
|
+
],
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
afterEach(cleanup);
|
|
93
|
+
|
|
94
|
+
/** 技能/工具勾选项的复选框(CheckRow 把 input 包在 label 里)。 */
|
|
95
|
+
function checkbox(row: HTMLElement): HTMLInputElement {
|
|
96
|
+
return row.querySelector('input') as HTMLInputElement;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
describe('AgentsSettingsPanel', () => {
|
|
100
|
+
it('lists builtin agents and hides archive on them', async () => {
|
|
101
|
+
render(<AgentsSettingsPanel />);
|
|
102
|
+
const row = await screen.findByTestId('agent-row-local-assistant');
|
|
103
|
+
expect(row.textContent).toContain('电脑操作员');
|
|
104
|
+
expect(row.textContent).toContain('内置');
|
|
105
|
+
expect(screen.queryByTestId('agent-archive-local-assistant')).toBeNull();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('creates a custom agent and refreshes the catalog', async () => {
|
|
109
|
+
const onCatalogChange = vi.fn(async () => {});
|
|
110
|
+
listChatAgents
|
|
111
|
+
.mockResolvedValueOnce({ agents: [builtin], total: 1 })
|
|
112
|
+
.mockResolvedValueOnce({ agents: [builtin, custom], total: 2 });
|
|
113
|
+
render(<AgentsSettingsPanel onCatalogChange={onCatalogChange} />);
|
|
114
|
+
await screen.findByTestId('agent-row-local-assistant');
|
|
115
|
+
fireEvent.click(screen.getByTestId('agent-add'));
|
|
116
|
+
fireEvent.change(screen.getByTestId('agent-form-name'), {
|
|
117
|
+
target: { value: '地质顾问' },
|
|
118
|
+
});
|
|
119
|
+
fireEvent.change(screen.getByTestId('agent-form-role'), {
|
|
120
|
+
target: { value: '你是地质顾问。' },
|
|
121
|
+
});
|
|
122
|
+
fireEvent.click(screen.getByTestId('agent-form-save'));
|
|
123
|
+
await waitFor(() => {
|
|
124
|
+
expect(createChatAgent).toHaveBeenCalledWith(
|
|
125
|
+
expect.objectContaining({
|
|
126
|
+
name: '地质顾问',
|
|
127
|
+
rolePrompt: '你是地质顾问。',
|
|
128
|
+
// 缺省能力面:不钉技能、不限制——新智能体不该一上手就少功能。
|
|
129
|
+
skillIds: [],
|
|
130
|
+
allowExternalSkills: true,
|
|
131
|
+
loadAllSkills: false,
|
|
132
|
+
toolPolicy: { mode: 'all', tools: [] },
|
|
133
|
+
}),
|
|
134
|
+
);
|
|
135
|
+
});
|
|
136
|
+
await screen.findByTestId('agent-row-geo-advisor');
|
|
137
|
+
expect(onCatalogChange).toHaveBeenCalled();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('保存勾选的技能与工具白名单', async () => {
|
|
141
|
+
render(<AgentsSettingsPanel />);
|
|
142
|
+
await screen.findByTestId('agent-row-local-assistant');
|
|
143
|
+
fireEvent.click(screen.getByTestId('agent-add'));
|
|
144
|
+
fireEvent.change(screen.getByTestId('agent-form-name'), {
|
|
145
|
+
target: { value: '数据助手' },
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const csvTools = await screen.findByTestId('agent-form-skill-90-csv-tools');
|
|
149
|
+
fireEvent.click(checkbox(csvTools));
|
|
150
|
+
fireEvent.click(screen.getByTestId('agent-form-allow-external-skills'));
|
|
151
|
+
fireEvent.click(screen.getByTestId('agent-form-tool-mode-allowlist'));
|
|
152
|
+
const readFile = await screen.findByTestId('agent-form-tool-local_read_file');
|
|
153
|
+
fireEvent.click(checkbox(readFile));
|
|
154
|
+
|
|
155
|
+
fireEvent.click(screen.getByTestId('agent-form-save'));
|
|
156
|
+
await waitFor(() => {
|
|
157
|
+
expect(createChatAgent).toHaveBeenCalledWith(
|
|
158
|
+
expect.objectContaining({
|
|
159
|
+
name: '数据助手',
|
|
160
|
+
skillIds: ['90-csv-tools'],
|
|
161
|
+
allowExternalSkills: false,
|
|
162
|
+
toolPolicy: { mode: 'allowlist', tools: ['local_read_file'] },
|
|
163
|
+
}),
|
|
164
|
+
);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('编辑时回填已保存的能力面', async () => {
|
|
169
|
+
const restricted: LocalChatAgent = {
|
|
170
|
+
...custom,
|
|
171
|
+
skillIds: ['90-csv-tools'],
|
|
172
|
+
allowExternalSkills: false,
|
|
173
|
+
loadAllSkills: true,
|
|
174
|
+
toolPolicy: { mode: 'denylist', tools: ['local_exec_shell'] },
|
|
175
|
+
};
|
|
176
|
+
listChatAgents.mockResolvedValue({ agents: [restricted], total: 1 });
|
|
177
|
+
render(<AgentsSettingsPanel />);
|
|
178
|
+
await screen.findByTestId('agent-row-geo-advisor');
|
|
179
|
+
fireEvent.click(screen.getByTestId('agent-edit-geo-advisor'));
|
|
180
|
+
|
|
181
|
+
const csvTools = await screen.findByTestId('agent-form-skill-90-csv-tools');
|
|
182
|
+
expect(checkbox(csvTools).checked).toBe(true);
|
|
183
|
+
expect(
|
|
184
|
+
(screen.getByTestId('agent-form-allow-external-skills') as HTMLInputElement).checked,
|
|
185
|
+
).toBe(false);
|
|
186
|
+
expect(
|
|
187
|
+
(screen.getByTestId('agent-form-load-all-skills') as HTMLInputElement).checked,
|
|
188
|
+
).toBe(true);
|
|
189
|
+
expect(
|
|
190
|
+
screen.getByTestId('agent-form-tool-mode-denylist').getAttribute('aria-pressed'),
|
|
191
|
+
).toBe('true');
|
|
192
|
+
const shell = await screen.findByTestId('agent-form-tool-local_exec_shell');
|
|
193
|
+
expect(checkbox(shell).checked).toBe(true);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('列表行摘要标出受限的智能体,缺省配置不占位', async () => {
|
|
197
|
+
const restricted: LocalChatAgent = {
|
|
198
|
+
...custom,
|
|
199
|
+
skillIds: ['90-csv-tools'],
|
|
200
|
+
allowExternalSkills: false,
|
|
201
|
+
toolPolicy: { mode: 'allowlist', tools: ['local_read_file'] },
|
|
202
|
+
};
|
|
203
|
+
listChatAgents.mockResolvedValue({ agents: [builtin, restricted], total: 2 });
|
|
204
|
+
render(<AgentsSettingsPanel />);
|
|
205
|
+
const summary = await screen.findByTestId('agent-capability-geo-advisor');
|
|
206
|
+
expect(summary.textContent).toContain('技能 1');
|
|
207
|
+
expect(summary.textContent).toContain('仅限所选技能');
|
|
208
|
+
expect(summary.textContent).toContain('仅 1 个工具');
|
|
209
|
+
expect(screen.queryByTestId('agent-capability-local-assistant')).toBeNull();
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('archives a custom agent after two-step confirm', async () => {
|
|
213
|
+
listChatAgents
|
|
214
|
+
.mockResolvedValueOnce({ agents: [builtin, custom], total: 2 })
|
|
215
|
+
.mockResolvedValueOnce({ agents: [builtin], total: 1 });
|
|
216
|
+
render(<AgentsSettingsPanel />);
|
|
217
|
+
await screen.findByTestId('agent-row-geo-advisor');
|
|
218
|
+
const archive = screen.getByTestId('agent-archive-geo-advisor');
|
|
219
|
+
fireEvent.click(archive);
|
|
220
|
+
expect(archiveChatAgent).not.toHaveBeenCalled();
|
|
221
|
+
fireEvent.click(archive);
|
|
222
|
+
await waitFor(() => {
|
|
223
|
+
expect(archiveChatAgent).toHaveBeenCalledWith('geo-advisor');
|
|
224
|
+
});
|
|
225
|
+
await waitFor(() => {
|
|
226
|
+
expect(screen.queryByTestId('agent-row-geo-advisor')).toBeNull();
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
});
|