@steerable/agent-shell-web 0.6.50 → 0.6.52
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/brand.ts +7 -0
- package/src/components/AgentSidebar.tsx +284 -188
- package/src/components/ConfirmDialog.tsx +90 -0
- package/src/components/CreateProjectModal.tsx +32 -16
- package/src/components/chat/ApprovalModal.tsx +2 -5
- package/src/components/chat/ApprovalPromptProvider.tsx +38 -22
- package/src/components/chat/AskUserPromptProvider.tsx +1 -32
- package/src/components/chat/ChatInput.tsx +47 -57
- package/src/components/chat/ChatProjectBadge.tsx +1 -1
- package/src/components/chat/EmptyChatHero.tsx +22 -0
- package/src/components/chat/ExecPolicyPicker.tsx +2 -2
- package/src/components/chat/ExecutedActionsCard.tsx +23 -13
- package/src/components/chat/LocalChatPanel.tsx +61 -18
- package/src/components/chat/MessageList.tsx +77 -5
- package/src/components/chat/ModelPicker.tsx +21 -1
- package/src/components/chat/OrchestrationFlowCard.tsx +484 -0
- package/src/components/chat/SessionOrchestrationFlow.tsx +573 -0
- package/src/components/chat/SessionTodoList.tsx +12 -3
- package/src/components/chat/UserMessage.tsx +116 -25
- package/src/components/chat/composer-status-stack.tsx +79 -0
- package/src/components/chat/orchestration-flow-model.ts +401 -0
- package/src/components/chat/prompt-chat-scope.ts +33 -0
- package/src/components/settings/AgentsSettingsPanel.tsx +33 -22
- package/src/components/settings/McpSettingsPanel.tsx +1 -1
- package/src/components/settings/OrchestrationSettingsPanel.tsx +67 -0
- package/src/components/settings/SettingsNavMenu.tsx +55 -0
- package/src/components/settings/SkillsSettingsPanel.tsx +1 -1
- package/src/hooks/useProjects.ts +82 -0
- package/src/lib/electron-bridge.ts +1 -0
- package/src/lib/exec-policy.ts +1 -1
- package/src/lib/local-api.ts +1 -1
- package/src/lib/orchestration-settings.ts +58 -0
- package/src/pages/AgentPage.tsx +116 -64
- package/src/pages/SettingsPage.tsx +338 -55
- package/src/vite-env.d.ts +2 -0
- package/vite.base.js +1 -0
|
@@ -1,9 +1,26 @@
|
|
|
1
|
-
import { useRef, useState } from 'react';
|
|
2
|
-
import {
|
|
1
|
+
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
LuActivity,
|
|
4
|
+
LuBlocks,
|
|
5
|
+
LuBot,
|
|
6
|
+
LuChartBar,
|
|
7
|
+
LuCodeXml,
|
|
8
|
+
LuDownload,
|
|
9
|
+
LuGitFork,
|
|
10
|
+
LuInfo,
|
|
11
|
+
LuMonitor,
|
|
12
|
+
LuNetwork,
|
|
13
|
+
LuPlug,
|
|
14
|
+
LuSearch,
|
|
15
|
+
LuSettings,
|
|
16
|
+
LuShieldCheck,
|
|
17
|
+
} from 'react-icons/lu';
|
|
3
18
|
import { useOutletContext, useSearchParams } from 'react-router-dom';
|
|
4
19
|
import { isElectron } from '@/lib/electron-bridge';
|
|
20
|
+
import { useAppRelease } from '@/components/SidebarRelease';
|
|
5
21
|
import { AgentsSettingsPanel } from '@/components/settings/AgentsSettingsPanel';
|
|
6
22
|
import { AppearanceSettingsPanel } from '@/components/settings/AppearanceSettingsPanel';
|
|
23
|
+
import { OrchestrationSettingsPanel } from '@/components/settings/OrchestrationSettingsPanel';
|
|
7
24
|
import { AppUpdateSettingsPanel } from '@/components/settings/AppUpdateSettingsPanel';
|
|
8
25
|
import { PortableSettingsPanel } from '@/components/settings/PortableSettingsPanel';
|
|
9
26
|
import { PythonRunnerSettingsPanel } from '@/components/settings/PythonRunnerSettingsPanel';
|
|
@@ -21,53 +38,279 @@ import { SettingsSaveButton } from '@/components/settings/SettingsSaveButton';
|
|
|
21
38
|
import { TelemetrySettingsPanel } from '@/components/settings/TelemetrySettingsPanel';
|
|
22
39
|
import { UsagePanel } from '@/components/settings/UsagePanel';
|
|
23
40
|
import { WebSearchSettingsPanel } from '@/components/settings/WebSearchSettingsPanel';
|
|
41
|
+
import { SettingsNavMenu, type SettingsNavItem } from '@/components/settings/SettingsNavMenu';
|
|
24
42
|
import { getPackSettingsPanels } from '@/packs/registry';
|
|
25
43
|
import { settingsChrome } from '@/lib/host-tools';
|
|
26
44
|
import { isPortableEnabled } from '@/lib/portable';
|
|
27
45
|
import type { AgentOutletContext } from '@/layouts/AgentLayout';
|
|
28
46
|
|
|
29
|
-
type SettingsSection = '
|
|
47
|
+
type SettingsSection = 'plugins' | 'general';
|
|
48
|
+
type PluginTab = 'agents' | 'skills' | 'mcp' | 'web-search';
|
|
49
|
+
|
|
50
|
+
const PLUGIN_TABS: readonly PluginTab[] = ['agents', 'skills', 'mcp', 'web-search'];
|
|
51
|
+
|
|
52
|
+
const PLUGIN_TAB_META: Record<
|
|
53
|
+
PluginTab,
|
|
54
|
+
{ label: string; noun: string; Icon: typeof LuBot }
|
|
55
|
+
> = {
|
|
56
|
+
agents: { label: '智能体', noun: '智能体', Icon: LuBot },
|
|
57
|
+
skills: { label: 'Skills', noun: '技能', Icon: LuBlocks },
|
|
58
|
+
mcp: { label: 'MCP', noun: 'MCP 服务', Icon: LuPlug },
|
|
59
|
+
'web-search': { label: '网络搜索', noun: '网络搜索', Icon: LuSearch },
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
function isPluginTab(value: string | null): value is PluginTab {
|
|
63
|
+
return (PLUGIN_TABS as readonly string[]).includes(value ?? '');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function pluginsSummary(tabs: readonly PluginTab[]): string {
|
|
67
|
+
const nouns = tabs.map((id) => PLUGIN_TAB_META[id].noun);
|
|
68
|
+
if (nouns.length <= 1) return `管理${nouns[0] ?? '插件'}。`;
|
|
69
|
+
return `管理${nouns.slice(0, -1).join('、')}和${nouns[nouns.length - 1]}。`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function enabledPluginTabs(): PluginTab[] {
|
|
73
|
+
return PLUGIN_TABS.filter((id) => settingsChrome(id));
|
|
74
|
+
}
|
|
30
75
|
|
|
31
76
|
function resolveSection(raw: string | null): SettingsSection {
|
|
32
|
-
if (raw === '
|
|
33
|
-
if (raw === 'mcp' && settingsChrome(
|
|
34
|
-
|
|
77
|
+
if (raw === 'plugins') return enabledPluginTabs().length > 0 ? 'plugins' : 'general';
|
|
78
|
+
if ((raw === 'agents' || raw === 'skills' || raw === 'mcp') && settingsChrome(raw)) {
|
|
79
|
+
return 'plugins';
|
|
80
|
+
}
|
|
35
81
|
return 'general';
|
|
36
82
|
}
|
|
37
83
|
|
|
84
|
+
/** `?section=agents|skills|mcp` 是旧深链,仍打开插件页上对应分类。 */
|
|
85
|
+
function resolvePluginTab(section: string | null, tab: string | null): PluginTab | null {
|
|
86
|
+
const enabled = enabledPluginTabs();
|
|
87
|
+
const requested =
|
|
88
|
+
section === 'agents' || section === 'skills' || section === 'mcp' ? section : tab;
|
|
89
|
+
if (isPluginTab(requested) && enabled.includes(requested)) {
|
|
90
|
+
return requested;
|
|
91
|
+
}
|
|
92
|
+
return enabled[0] ?? null;
|
|
93
|
+
}
|
|
94
|
+
|
|
38
95
|
/**
|
|
39
96
|
* `/settings` — 右侧内容区的设置页,按 `?section=` 分成独立页面:
|
|
40
|
-
* - `
|
|
41
|
-
*
|
|
42
|
-
* - `agents` 侧栏「智能体管理」
|
|
97
|
+
* - `plugins` 侧栏「插件」。顶栏分类切换智能体 / Skills / MCP / 网络搜索
|
|
98
|
+
* (`?tab=`,旧的 `?section=agents|skills|mcp` 仍落到对应分类)
|
|
43
99
|
* - 缺省/`general` 侧栏底「设置」(界面 / 模型 / 搜索 / 用量 / 安全 / 洞察 / 遥测 / 关于)
|
|
44
100
|
*
|
|
45
101
|
* Panel 数据自管理(挂载即拉取),页面本身不持有后端状态。
|
|
46
102
|
*/
|
|
47
103
|
export function SettingsPage() {
|
|
48
|
-
const [searchParams] = useSearchParams();
|
|
49
|
-
const
|
|
104
|
+
const [searchParams, setSearchParams] = useSearchParams();
|
|
105
|
+
const rawSection = searchParams.get('section');
|
|
106
|
+
const section = resolveSection(rawSection);
|
|
107
|
+
const pluginTab = section === 'plugins' ? resolvePluginTab(rawSection, searchParams.get('tab')) : null;
|
|
108
|
+
const pluginTabs = section === 'plugins' ? enabledPluginTabs() : [];
|
|
109
|
+
const selectPluginTab = useCallback((id: PluginTab) => {
|
|
110
|
+
setSearchParams({ section: 'plugins', tab: id });
|
|
111
|
+
}, [setSearchParams]);
|
|
50
112
|
const catalog = useOutletContext<AgentOutletContext | null>();
|
|
113
|
+
const release = useAppRelease();
|
|
51
114
|
const llmPanelRef = useRef<LlmSettingsPanelHandle>(null);
|
|
115
|
+
const pageContainerRef = useRef<HTMLDivElement>(null);
|
|
116
|
+
const scrollRef = useRef<HTMLDivElement>(null);
|
|
117
|
+
const contentRef = useRef<HTMLDivElement>(null);
|
|
118
|
+
const [containerWidth, setContainerWidth] = useState<number>(0);
|
|
119
|
+
const [activeSectionId, setActiveSectionId] = useState<string | null>(null);
|
|
120
|
+
const [renderedSectionIds, setRenderedSectionIds] = useState<Set<string> | null>(null);
|
|
52
121
|
const [llmSaveUi, setLlmSaveUi] = useState<LlmSaveUi>({
|
|
53
122
|
saving: false,
|
|
54
123
|
savedOk: false,
|
|
55
124
|
loading: true,
|
|
56
125
|
});
|
|
57
126
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
127
|
+
useEffect(() => {
|
|
128
|
+
const el = pageContainerRef.current;
|
|
129
|
+
if (!el || typeof ResizeObserver === 'undefined') return;
|
|
130
|
+
const ro = new ResizeObserver((entries) => {
|
|
131
|
+
for (const entry of entries) {
|
|
132
|
+
setContainerWidth(entry.contentRect.width);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
ro.observe(el);
|
|
136
|
+
return () => ro.disconnect();
|
|
137
|
+
}, []);
|
|
138
|
+
|
|
139
|
+
// 扫描右侧实际挂载且未被隐藏 (display !== 'none') 的设置分段
|
|
140
|
+
const updateRenderedSections = useCallback(() => {
|
|
141
|
+
const container = contentRef.current;
|
|
142
|
+
if (!container) return;
|
|
143
|
+
const sectionEls = container.querySelectorAll<HTMLElement>('[data-testid^="settings-section-"]');
|
|
144
|
+
const ids = new Set<string>();
|
|
145
|
+
sectionEls.forEach((el) => {
|
|
146
|
+
const tid = el.getAttribute('data-testid');
|
|
147
|
+
if (!tid) return;
|
|
148
|
+
const id = tid.replace('settings-section-', '');
|
|
149
|
+
if (typeof window !== 'undefined' && window.getComputedStyle) {
|
|
150
|
+
const style = window.getComputedStyle(el);
|
|
151
|
+
if (style.display === 'none' || style.visibility === 'hidden') return;
|
|
152
|
+
}
|
|
153
|
+
ids.add(id);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
setRenderedSectionIds((prev) => {
|
|
157
|
+
if (prev && prev.size === ids.size && [...ids].every((id) => prev.has(id))) {
|
|
158
|
+
return prev;
|
|
159
|
+
}
|
|
160
|
+
return ids;
|
|
161
|
+
});
|
|
162
|
+
}, []);
|
|
163
|
+
|
|
164
|
+
useLayoutEffect(() => {
|
|
165
|
+
updateRenderedSections();
|
|
166
|
+
const container = contentRef.current;
|
|
167
|
+
if (!container || typeof MutationObserver === 'undefined') return;
|
|
168
|
+
const observer = new MutationObserver(() => {
|
|
169
|
+
updateRenderedSections();
|
|
170
|
+
});
|
|
171
|
+
observer.observe(container, {
|
|
172
|
+
childList: true,
|
|
173
|
+
subtree: true,
|
|
174
|
+
attributes: true,
|
|
175
|
+
attributeFilter: ['style', 'class', 'hidden'],
|
|
176
|
+
});
|
|
177
|
+
return () => observer.disconnect();
|
|
178
|
+
}, [updateRenderedSections]);
|
|
179
|
+
|
|
180
|
+
// 页面宽度 >= 920px 视为宽屏(有足够空间并排容纳左侧边栏目录与主设置表单)
|
|
181
|
+
const isWide = containerWidth >= 920;
|
|
182
|
+
|
|
183
|
+
const allCandidates = useMemo<SettingsNavItem[]>(() => {
|
|
184
|
+
if (section !== 'general') return [];
|
|
185
|
+
const list: SettingsNavItem[] = [];
|
|
186
|
+
|
|
187
|
+
if (settingsChrome('appearance')) {
|
|
188
|
+
list.push({ id: 'appearance', label: '界面', Icon: LuMonitor });
|
|
189
|
+
}
|
|
190
|
+
list.push({ id: 'orchestration', label: '协同编排', Icon: LuGitFork });
|
|
191
|
+
|
|
192
|
+
if (settingsChrome('llm')) {
|
|
193
|
+
list.push({ id: 'llm', label: '本地模型设置', Icon: LuSettings });
|
|
194
|
+
}
|
|
195
|
+
if (settingsChrome('web-search')) {
|
|
196
|
+
list.push({ id: 'web-search', label: '网络搜索', Icon: LuSearch });
|
|
197
|
+
}
|
|
198
|
+
if (settingsChrome('usage')) {
|
|
199
|
+
list.push({ id: 'usage', label: '用量与成本', Icon: LuChartBar });
|
|
200
|
+
}
|
|
201
|
+
if (settingsChrome('diagnose')) {
|
|
202
|
+
list.push({ id: 'diagnose', label: '链路诊断', Icon: LuNetwork });
|
|
203
|
+
}
|
|
204
|
+
if (settingsChrome('security')) {
|
|
205
|
+
list.push({ id: 'security', label: '安全', Icon: LuShieldCheck });
|
|
206
|
+
}
|
|
207
|
+
if (settingsChrome('insights')) {
|
|
208
|
+
list.push({ id: 'insights', label: '帮助改进产品', Icon: LuChartBar });
|
|
209
|
+
}
|
|
210
|
+
if (settingsChrome('telemetry')) {
|
|
211
|
+
list.push({ id: 'telemetry', label: '遥测(OTLP)', Icon: LuActivity });
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const packPanels = getPackSettingsPanels();
|
|
215
|
+
for (const panel of packPanels) {
|
|
216
|
+
list.push({
|
|
217
|
+
id: panel.panelId,
|
|
218
|
+
label: panel.title,
|
|
219
|
+
Icon: panel.Icon ?? LuSettings,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (isPortableEnabled()) {
|
|
224
|
+
list.push({ id: 'portable', label: '备份与迁移', Icon: LuDownload });
|
|
225
|
+
}
|
|
226
|
+
if (isElectron()) {
|
|
227
|
+
list.push({ id: 'python-runner', label: 'Python 运行环境', Icon: LuCodeXml });
|
|
228
|
+
}
|
|
229
|
+
if (release.version) {
|
|
230
|
+
list.push({ id: 'update', label: '软件更新', Icon: LuInfo });
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return list;
|
|
234
|
+
}, [section, release.version]);
|
|
235
|
+
|
|
236
|
+
// 严格过滤:右侧实际未挂载或隐藏的分段,左侧菜单一律不显示
|
|
237
|
+
const menuItems = useMemo<SettingsNavItem[]>(() => {
|
|
238
|
+
if (section !== 'general') return [];
|
|
239
|
+
if (renderedSectionIds !== null) {
|
|
240
|
+
return allCandidates.filter((item) => renderedSectionIds.has(item.id));
|
|
241
|
+
}
|
|
242
|
+
return allCandidates;
|
|
243
|
+
}, [section, allCandidates, renderedSectionIds]);
|
|
244
|
+
|
|
245
|
+
const handleSelectSection = useCallback((id: string) => {
|
|
246
|
+
setActiveSectionId(id);
|
|
247
|
+
const target = document.querySelector(`[data-testid="settings-section-${id}"]`);
|
|
248
|
+
if (target) {
|
|
249
|
+
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
250
|
+
}
|
|
251
|
+
}, []);
|
|
252
|
+
|
|
253
|
+
const handleScroll = useCallback(() => {
|
|
254
|
+
if (!scrollRef.current || menuItems.length === 0) return;
|
|
255
|
+
const containerTop = scrollRef.current.getBoundingClientRect().top;
|
|
256
|
+
let closestId: string | null = null;
|
|
257
|
+
let minDistance = Infinity;
|
|
258
|
+
|
|
259
|
+
for (const item of menuItems) {
|
|
260
|
+
const el = document.querySelector(`[data-testid="settings-section-${item.id}"]`);
|
|
261
|
+
if (el) {
|
|
262
|
+
const top = el.getBoundingClientRect().top - containerTop;
|
|
263
|
+
if (top <= 120 && Math.abs(top) < minDistance) {
|
|
264
|
+
minDistance = Math.abs(top);
|
|
265
|
+
closestId = item.id;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (closestId) {
|
|
270
|
+
setActiveSectionId(closestId);
|
|
271
|
+
}
|
|
272
|
+
}, [menuItems]);
|
|
273
|
+
|
|
274
|
+
const title = section === 'plugins' ? '插件' : '设置';
|
|
66
275
|
|
|
67
276
|
return (
|
|
68
|
-
<div className="flex h-full w-full flex-col overflow-hidden">
|
|
277
|
+
<div ref={pageContainerRef} className="flex h-full w-full flex-col overflow-hidden">
|
|
69
278
|
<header className="flex h-9 flex-shrink-0 items-center justify-between gap-2 border-b border-agent-border px-2.5">
|
|
70
|
-
<
|
|
279
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
280
|
+
<h1 className="shrink-0 text-xs font-semibold text-agent-foreground">{title}</h1>
|
|
281
|
+
{section === 'plugins' && pluginTabs.length > 1 && (
|
|
282
|
+
<div
|
|
283
|
+
role="tablist"
|
|
284
|
+
aria-label="插件"
|
|
285
|
+
data-testid="plugins-tabs"
|
|
286
|
+
className="flex min-w-0 items-center gap-1"
|
|
287
|
+
>
|
|
288
|
+
{pluginTabs.map((id) => {
|
|
289
|
+
const meta = PLUGIN_TAB_META[id];
|
|
290
|
+
const active = pluginTab === id;
|
|
291
|
+
return (
|
|
292
|
+
<button
|
|
293
|
+
key={id}
|
|
294
|
+
type="button"
|
|
295
|
+
role="tab"
|
|
296
|
+
aria-selected={active}
|
|
297
|
+
data-testid={`plugins-tab-${id}`}
|
|
298
|
+
onClick={() => selectPluginTab(id)}
|
|
299
|
+
className={[
|
|
300
|
+
'flex h-7 items-center gap-1.5 rounded-full px-2.5 text-xs transition-colors',
|
|
301
|
+
active
|
|
302
|
+
? 'bg-agent-foreground/10 font-medium text-agent-foreground'
|
|
303
|
+
: 'text-agent-muted-foreground hover:bg-agent-foreground/5 hover:text-agent-foreground',
|
|
304
|
+
].join(' ')}
|
|
305
|
+
>
|
|
306
|
+
<meta.Icon className="h-3.5 w-3.5" />
|
|
307
|
+
{meta.label}
|
|
308
|
+
</button>
|
|
309
|
+
);
|
|
310
|
+
})}
|
|
311
|
+
</div>
|
|
312
|
+
)}
|
|
313
|
+
</div>
|
|
71
314
|
{section === 'general' && settingsChrome('llm') && (
|
|
72
315
|
<SettingsSaveButton
|
|
73
316
|
testId="settings-header-save"
|
|
@@ -79,42 +322,73 @@ export function SettingsPage() {
|
|
|
79
322
|
)}
|
|
80
323
|
</header>
|
|
81
324
|
|
|
82
|
-
<div
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
)}
|
|
325
|
+
<div
|
|
326
|
+
ref={scrollRef}
|
|
327
|
+
onScroll={handleScroll}
|
|
328
|
+
className="min-h-0 flex-1 overflow-y-auto"
|
|
329
|
+
>
|
|
330
|
+
<div
|
|
331
|
+
className={`mx-auto px-4 py-3 ${
|
|
332
|
+
isWide && menuItems.length > 0 ? 'flex max-w-5xl justify-center gap-6' : 'max-w-3xl'
|
|
333
|
+
}`}
|
|
334
|
+
>
|
|
335
|
+
{isWide && menuItems.length > 0 ? (
|
|
336
|
+
<SettingsNavMenu
|
|
337
|
+
items={menuItems}
|
|
338
|
+
activeId={activeSectionId ?? menuItems[0]?.id ?? null}
|
|
339
|
+
onSelect={handleSelectSection}
|
|
340
|
+
/>
|
|
341
|
+
) : null}
|
|
99
342
|
|
|
100
|
-
{
|
|
101
|
-
|
|
102
|
-
<
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
<McpSettingsPanel />
|
|
107
|
-
</section>
|
|
108
|
-
)}
|
|
343
|
+
<div ref={contentRef} className="min-w-0 flex-1 max-w-3xl space-y-4">
|
|
344
|
+
{!isElectron() && (
|
|
345
|
+
<p className="rounded-agent-md border border-agent-destructive/20 bg-agent-destructive/10 p-2.5 text-xs text-agent-destructive">
|
|
346
|
+
浏览器预览模式 — 没有 Electron IPC 桥接,部分设置不可用。
|
|
347
|
+
</p>
|
|
348
|
+
)}
|
|
109
349
|
|
|
110
|
-
{section === '
|
|
111
|
-
|
|
112
|
-
<
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
350
|
+
{section === 'plugins' && (
|
|
351
|
+
<>
|
|
352
|
+
<p className="text-xs text-agent-muted-foreground" data-testid="plugins-summary">
|
|
353
|
+
{pluginsSummary(pluginTabs)}
|
|
354
|
+
</p>
|
|
355
|
+
{pluginTab === 'skills' && (
|
|
356
|
+
<section className="space-y-2" data-testid="settings-section-skills">
|
|
357
|
+
<h2 className="flex items-center gap-1.5 text-xs font-semibold text-agent-foreground">
|
|
358
|
+
<LuBlocks className="h-3.5 w-3.5 text-agent-muted-foreground" />
|
|
359
|
+
本地技能
|
|
360
|
+
</h2>
|
|
361
|
+
<SkillsSettingsPanel />
|
|
362
|
+
</section>
|
|
363
|
+
)}
|
|
364
|
+
{pluginTab === 'mcp' && (
|
|
365
|
+
<section className="space-y-2" data-testid="settings-section-mcp">
|
|
366
|
+
<h2 className="flex items-center gap-1.5 text-xs font-semibold text-agent-foreground">
|
|
367
|
+
<LuPlug className="h-3.5 w-3.5 text-agent-muted-foreground" />
|
|
368
|
+
MCP 服务
|
|
369
|
+
</h2>
|
|
370
|
+
<McpSettingsPanel />
|
|
371
|
+
</section>
|
|
372
|
+
)}
|
|
373
|
+
{pluginTab === 'agents' && (
|
|
374
|
+
<section className="space-y-2" data-testid="settings-section-agents">
|
|
375
|
+
<h2 className="flex items-center gap-1.5 text-xs font-semibold text-agent-foreground">
|
|
376
|
+
<LuBot className="h-3.5 w-3.5 text-agent-muted-foreground" />
|
|
377
|
+
智能体
|
|
378
|
+
</h2>
|
|
379
|
+
<AgentsSettingsPanel onCatalogChange={catalog?.refreshAgents} />
|
|
380
|
+
</section>
|
|
381
|
+
)}
|
|
382
|
+
{pluginTab === 'web-search' && settingsChrome('web-search') && (
|
|
383
|
+
<section className="space-y-2" data-testid="settings-section-web-search">
|
|
384
|
+
<h2 className="flex items-center gap-1.5 text-xs font-semibold text-agent-foreground">
|
|
385
|
+
<LuSearch className="h-3.5 w-3.5 text-agent-muted-foreground" />
|
|
386
|
+
网络搜索
|
|
387
|
+
</h2>
|
|
388
|
+
<WebSearchSettingsPanel />
|
|
389
|
+
</section>
|
|
390
|
+
)}
|
|
391
|
+
</>
|
|
118
392
|
)}
|
|
119
393
|
|
|
120
394
|
{section === 'general' && (
|
|
@@ -129,6 +403,14 @@ export function SettingsPage() {
|
|
|
129
403
|
</section>
|
|
130
404
|
)}
|
|
131
405
|
|
|
406
|
+
<section className="space-y-2" data-testid="settings-section-orchestration">
|
|
407
|
+
<h2 className="flex items-center gap-1.5 text-xs font-semibold text-agent-foreground">
|
|
408
|
+
<LuSettings className="h-3.5 w-3.5 text-agent-muted-foreground" />
|
|
409
|
+
协同编排
|
|
410
|
+
</h2>
|
|
411
|
+
<OrchestrationSettingsPanel />
|
|
412
|
+
</section>
|
|
413
|
+
|
|
132
414
|
{settingsChrome('llm') && (
|
|
133
415
|
<section className="space-y-2" data-testid="settings-section-llm">
|
|
134
416
|
<h2 className="flex items-center gap-1.5 text-xs font-semibold text-agent-foreground">
|
|
@@ -235,6 +517,7 @@ export function SettingsPage() {
|
|
|
235
517
|
<AppUpdateSettingsPanel />
|
|
236
518
|
</>
|
|
237
519
|
)}
|
|
520
|
+
</div>
|
|
238
521
|
</div>
|
|
239
522
|
</div>
|
|
240
523
|
</div>
|
package/src/vite-env.d.ts
CHANGED
|
@@ -5,6 +5,8 @@ interface ImportMetaEnv {
|
|
|
5
5
|
readonly VITE_BRAND_NAME?: string;
|
|
6
6
|
readonly VITE_BRAND_TITLE?: string;
|
|
7
7
|
readonly VITE_BRAND_TAGLINE?: string;
|
|
8
|
+
/** 新对话落地页副文案。空字符串表示不显示。 */
|
|
9
|
+
readonly VITE_BRAND_HOME_HINT?: string;
|
|
8
10
|
readonly VITE_DEFAULT_AGENT_ID?: string;
|
|
9
11
|
readonly VITE_HOST_TOOLS?: string;
|
|
10
12
|
readonly VITE_APPROVAL?: string;
|
package/vite.base.js
CHANGED
|
@@ -52,6 +52,7 @@ export function createProductViteConfig(options) {
|
|
|
52
52
|
'import.meta.env.VITE_BRAND_NAME': JSON.stringify(brand?.displayName ?? 'Steerable Shell'),
|
|
53
53
|
'import.meta.env.VITE_BRAND_TITLE': JSON.stringify(brand?.title ?? ''),
|
|
54
54
|
'import.meta.env.VITE_BRAND_TAGLINE': JSON.stringify(brand?.tagline ?? '一款本地桌面 AI 伙伴'),
|
|
55
|
+
'import.meta.env.VITE_BRAND_HOME_HINT': JSON.stringify(brand?.homeHint ?? '输入消息,直接开始一段新对话。'),
|
|
55
56
|
'import.meta.env.VITE_DEFAULT_AGENT_ID': JSON.stringify(brand?.defaultAgentId ?? 'local-assistant'),
|
|
56
57
|
// 宿主工具族 / 安全询问:与 node 侧 setProductConfig 读同一份 product.json。
|
|
57
58
|
'import.meta.env.VITE_HOST_TOOLS': JSON.stringify(JSON.stringify(product.hostTools ?? {})),
|