@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.
Files changed (37) hide show
  1. package/package.json +4 -4
  2. package/src/brand.ts +7 -0
  3. package/src/components/AgentSidebar.tsx +284 -188
  4. package/src/components/ConfirmDialog.tsx +90 -0
  5. package/src/components/CreateProjectModal.tsx +32 -16
  6. package/src/components/chat/ApprovalModal.tsx +2 -5
  7. package/src/components/chat/ApprovalPromptProvider.tsx +38 -22
  8. package/src/components/chat/AskUserPromptProvider.tsx +1 -32
  9. package/src/components/chat/ChatInput.tsx +47 -57
  10. package/src/components/chat/ChatProjectBadge.tsx +1 -1
  11. package/src/components/chat/EmptyChatHero.tsx +22 -0
  12. package/src/components/chat/ExecPolicyPicker.tsx +2 -2
  13. package/src/components/chat/ExecutedActionsCard.tsx +23 -13
  14. package/src/components/chat/LocalChatPanel.tsx +61 -18
  15. package/src/components/chat/MessageList.tsx +77 -5
  16. package/src/components/chat/ModelPicker.tsx +21 -1
  17. package/src/components/chat/OrchestrationFlowCard.tsx +484 -0
  18. package/src/components/chat/SessionOrchestrationFlow.tsx +573 -0
  19. package/src/components/chat/SessionTodoList.tsx +12 -3
  20. package/src/components/chat/UserMessage.tsx +116 -25
  21. package/src/components/chat/composer-status-stack.tsx +79 -0
  22. package/src/components/chat/orchestration-flow-model.ts +401 -0
  23. package/src/components/chat/prompt-chat-scope.ts +33 -0
  24. package/src/components/settings/AgentsSettingsPanel.tsx +33 -22
  25. package/src/components/settings/McpSettingsPanel.tsx +1 -1
  26. package/src/components/settings/OrchestrationSettingsPanel.tsx +67 -0
  27. package/src/components/settings/SettingsNavMenu.tsx +55 -0
  28. package/src/components/settings/SkillsSettingsPanel.tsx +1 -1
  29. package/src/hooks/useProjects.ts +82 -0
  30. package/src/lib/electron-bridge.ts +1 -0
  31. package/src/lib/exec-policy.ts +1 -1
  32. package/src/lib/local-api.ts +1 -1
  33. package/src/lib/orchestration-settings.ts +58 -0
  34. package/src/pages/AgentPage.tsx +116 -64
  35. package/src/pages/SettingsPage.tsx +338 -55
  36. package/src/vite-env.d.ts +2 -0
  37. package/vite.base.js +1 -0
@@ -1,9 +1,26 @@
1
- import { useRef, useState } from 'react';
2
- import { LuActivity, LuBlocks, LuBot, LuChartBar, LuDownload, LuMonitor, LuNetwork, LuPlug, LuSearch, LuSettings, LuShieldCheck } from 'react-icons/lu';
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 = 'skills' | 'mcp' | 'agents' | 'general';
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 === 'skills' && settingsChrome('skills')) return 'skills';
33
- if (raw === 'mcp' && settingsChrome('mcp')) return 'mcp';
34
- if (raw === 'agents' && settingsChrome('agents')) return 'agents';
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
- * - `skills` 侧栏「Skill 设置」
41
- * - `mcp` 侧栏「MCP 设置」
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 section = resolveSection(searchParams.get('section'));
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
- const title =
59
- section === 'mcp'
60
- ? 'MCP 设置'
61
- : section === 'skills'
62
- ? 'Skill 设置'
63
- : section === 'agents'
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
- <h1 className="text-xs font-semibold text-agent-foreground">{title}</h1>
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 className="min-h-0 flex-1 overflow-y-auto">
83
- <div className="mx-auto max-w-3xl space-y-4 px-3 py-3">
84
- {!isElectron() && (
85
- <p className="rounded-agent-md border border-agent-destructive/20 bg-agent-destructive/10 p-2.5 text-xs text-agent-destructive">
86
- 浏览器预览模式 — 没有 Electron IPC 桥接,部分设置不可用。
87
- </p>
88
- )}
89
-
90
- {section === 'skills' && (
91
- <section className="space-y-2" data-testid="settings-section-skills">
92
- <h2 className="flex items-center gap-1.5 text-xs font-semibold text-agent-foreground">
93
- <LuBlocks className="h-3.5 w-3.5 text-agent-muted-foreground" />
94
- 本地技能
95
- </h2>
96
- <SkillsSettingsPanel />
97
- </section>
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
- {section === 'mcp' && (
101
- <section className="space-y-2" data-testid="settings-section-mcp">
102
- <h2 className="flex items-center gap-1.5 text-xs font-semibold text-agent-foreground">
103
- <LuPlug className="h-3.5 w-3.5 text-agent-muted-foreground" />
104
- MCP 服务
105
- </h2>
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 === 'agents' && (
111
- <section className="space-y-2" data-testid="settings-section-agents">
112
- <h2 className="flex items-center gap-1.5 text-xs font-semibold text-agent-foreground">
113
- <LuBot className="h-3.5 w-3.5 text-agent-muted-foreground" />
114
- 智能体
115
- </h2>
116
- <AgentsSettingsPanel onCatalogChange={catalog?.refreshAgents} />
117
- </section>
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 ?? {})),