@steerable/agent-shell-web 0.6.31 → 0.6.33

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.
@@ -0,0 +1,206 @@
1
+ /**
2
+ * 渲染层读构建期注入的宿主工具族 / 安全询问。
3
+ * 解析规则与 ts/src/host-tools.ts 对齐:缺省全开;对象必须显式 chrome:true。
4
+ */
5
+
6
+ export type HostToolFamilyId =
7
+ | 'terminal'
8
+ | 'local-fs'
9
+ | 'projects'
10
+ | 'background-tasks'
11
+ | 'mcp'
12
+ | 'web'
13
+ | 'plugins';
14
+
15
+ export interface HostToolFamilySurface {
16
+ capability: boolean;
17
+ chrome: boolean;
18
+ }
19
+
20
+ export type HostToolsConfig = Partial<
21
+ Record<HostToolFamilyId, boolean | { capability?: boolean; chrome?: boolean }>
22
+ >;
23
+
24
+ export type ResolvedHostTools = Record<HostToolFamilyId, HostToolFamilySurface>;
25
+
26
+ const FAMILY_IDS: readonly HostToolFamilyId[] = [
27
+ 'terminal',
28
+ 'local-fs',
29
+ 'projects',
30
+ 'background-tasks',
31
+ 'mcp',
32
+ 'web',
33
+ 'plugins',
34
+ ];
35
+
36
+ function resolveSurface(
37
+ value: boolean | { capability?: boolean; chrome?: boolean } | undefined,
38
+ ): HostToolFamilySurface {
39
+ if (value === undefined || value === true) return { capability: true, chrome: true };
40
+ if (value === false) return { capability: false, chrome: false };
41
+ return {
42
+ capability: value.capability !== false,
43
+ chrome: value.chrome === true,
44
+ };
45
+ }
46
+
47
+ export function resolveWebHostTools(config?: HostToolsConfig | null): ResolvedHostTools {
48
+ const out = {} as ResolvedHostTools;
49
+ for (const id of FAMILY_IDS) {
50
+ out[id] = resolveSurface(config?.[id]);
51
+ }
52
+ return out;
53
+ }
54
+
55
+ function readHostToolsConfig(): HostToolsConfig {
56
+ const raw = import.meta.env.VITE_HOST_TOOLS;
57
+ if (!raw) return {};
58
+ try {
59
+ const parsed = typeof raw === 'string' ? (JSON.parse(raw) as unknown) : raw;
60
+ return parsed && typeof parsed === 'object' ? (parsed as HostToolsConfig) : {};
61
+ } catch {
62
+ return {};
63
+ }
64
+ }
65
+
66
+ let cached: ResolvedHostTools | null = null;
67
+
68
+ export function getWebHostTools(): ResolvedHostTools {
69
+ cached ??= resolveWebHostTools(readHostToolsConfig());
70
+ return cached;
71
+ }
72
+
73
+ export function hostToolChrome(id: HostToolFamilyId): boolean {
74
+ return getWebHostTools()[id].chrome;
75
+ }
76
+
77
+ export function hostToolCapability(id: HostToolFamilyId): boolean {
78
+ return getWebHostTools()[id].capability;
79
+ }
80
+
81
+ export function isWebApprovalEnabled(): boolean {
82
+ return import.meta.env.VITE_APPROVAL !== 'off';
83
+ }
84
+
85
+ export type ChatModeId = 'agent' | 'plan';
86
+
87
+ const DEFAULT_CHAT_MODES: ChatModeId[] = ['agent', 'plan'];
88
+
89
+ export function resolveWebChatModes(value?: unknown): ChatModeId[] {
90
+ if (!Array.isArray(value)) return [...DEFAULT_CHAT_MODES];
91
+ const allowed = DEFAULT_CHAT_MODES.filter((id) => value.includes(id));
92
+ return allowed.length > 0 ? allowed : ['agent'];
93
+ }
94
+
95
+ function readChatModesConfig(): unknown {
96
+ const raw = import.meta.env.VITE_CHAT_MODES;
97
+ if (!raw) return undefined;
98
+ try {
99
+ return typeof raw === 'string' ? JSON.parse(raw) : raw;
100
+ } catch {
101
+ return undefined;
102
+ }
103
+ }
104
+
105
+ let cachedModes: ChatModeId[] | null = null;
106
+
107
+ export function getWebChatModes(): ChatModeId[] {
108
+ cachedModes ??= resolveWebChatModes(readChatModesConfig());
109
+ return cachedModes;
110
+ }
111
+
112
+ export function clampWebChatMode(requested: unknown): ChatModeId {
113
+ return requested === 'plan' && getWebChatModes().includes('plan') ? 'plan' : 'agent';
114
+ }
115
+
116
+ export const SETTINGS_ITEM_IDS = [
117
+ 'agents',
118
+ 'skills',
119
+ 'mcp',
120
+ 'appearance',
121
+ 'llm',
122
+ 'web-search',
123
+ 'usage',
124
+ 'diagnose',
125
+ 'security',
126
+ 'insights',
127
+ 'telemetry',
128
+ ] as const;
129
+
130
+ export type SettingsItemId = (typeof SETTINGS_ITEM_IDS)[number];
131
+
132
+ export const GENERAL_SETTINGS_ITEM_IDS = [
133
+ 'appearance',
134
+ 'llm',
135
+ 'web-search',
136
+ 'usage',
137
+ 'diagnose',
138
+ 'security',
139
+ 'insights',
140
+ 'telemetry',
141
+ ] as const;
142
+
143
+ export type SettingsChromeConfig = Partial<Record<SettingsItemId, boolean>>;
144
+
145
+ export type ResolvedSettingsChrome = Record<SettingsItemId, boolean>;
146
+
147
+ function settingsFollowsHostTool(id: SettingsItemId, tools: ResolvedHostTools): boolean {
148
+ if (id === 'skills') return tools.plugins.chrome;
149
+ if (id === 'mcp') return tools.mcp.chrome;
150
+ if (id === 'web-search') return tools.web.chrome;
151
+ return true;
152
+ }
153
+
154
+ export function resolveWebSettingsChrome(
155
+ value?: unknown,
156
+ tools: ResolvedHostTools = resolveWebHostTools(),
157
+ ): ResolvedSettingsChrome {
158
+ const config =
159
+ value && typeof value === 'object' && !Array.isArray(value)
160
+ ? (value as Record<string, unknown>)
161
+ : {};
162
+ const out = {} as ResolvedSettingsChrome;
163
+ for (const id of SETTINGS_ITEM_IDS) {
164
+ out[id] = config[id] !== false && settingsFollowsHostTool(id, tools);
165
+ }
166
+ return out;
167
+ }
168
+
169
+ function readSettingsConfig(): unknown {
170
+ const raw = import.meta.env.VITE_SETTINGS;
171
+ if (!raw) return undefined;
172
+ try {
173
+ return typeof raw === 'string' ? JSON.parse(raw) : raw;
174
+ } catch {
175
+ return undefined;
176
+ }
177
+ }
178
+
179
+ let cachedSettings: ResolvedSettingsChrome | null = null;
180
+
181
+ export function getWebSettingsChrome(): ResolvedSettingsChrome {
182
+ cachedSettings ??= resolveWebSettingsChrome(readSettingsConfig(), getWebHostTools());
183
+ return cachedSettings;
184
+ }
185
+
186
+ export function settingsChrome(id: SettingsItemId): boolean {
187
+ return getWebSettingsChrome()[id];
188
+ }
189
+
190
+ export function hasGeneralSettingsChrome(): boolean {
191
+ const chrome = getWebSettingsChrome();
192
+ return GENERAL_SETTINGS_ITEM_IDS.some((id) => chrome[id]);
193
+ }
194
+
195
+ export function sanitizeRightPanelKind(kind: string | null | undefined): string | null {
196
+ if (!kind) return null;
197
+ if (kind === 'terminal') return hostToolChrome('terminal') ? 'terminal' : null;
198
+ return kind;
199
+ }
200
+
201
+ /** 测试用:清掉缓存,让下次按新 env 重读。 */
202
+ export function resetWebHostToolsForTests(): void {
203
+ cached = null;
204
+ cachedModes = null;
205
+ cachedSettings = null;
206
+ }
@@ -44,6 +44,15 @@ function authHeaders(): Record<string, string> {
44
44
  return token ? { Authorization: `Bearer ${token}` } : {};
45
45
  }
46
46
 
47
+ function httpErrorMessage(data: unknown, status: number): string {
48
+ if (data && typeof data === 'object') {
49
+ const rec = data as { error?: unknown; detail?: unknown };
50
+ if (typeof rec.error === 'string' && rec.error.trim()) return rec.error;
51
+ if (typeof rec.detail === 'string' && rec.detail.trim()) return rec.detail;
52
+ }
53
+ return `Request failed (${status})`;
54
+ }
55
+
47
56
  async function http<T>(method: string, path: string, body?: unknown): Promise<T> {
48
57
  const res = await fetch(path, {
49
58
  method,
@@ -53,11 +62,9 @@ async function http<T>(method: string, path: string, body?: unknown): Promise<T>
53
62
  },
54
63
  body: body !== undefined ? JSON.stringify(body) : undefined,
55
64
  });
56
- const data = (await res.json().catch(() => null)) as T & { detail?: string };
65
+ const data = (await res.json().catch(() => null)) as T;
57
66
  if (!res.ok) {
58
- const err = new Error(
59
- (data as { detail?: string } | null)?.detail || `Request failed (${res.status})`,
60
- ) as Error & { status?: number };
67
+ const err = new Error(httpErrorMessage(data, res.status)) as Error & { status?: number };
61
68
  err.status = res.status;
62
69
  throw err;
63
70
  }
@@ -197,9 +204,15 @@ export function createHttpBridge(): ElectronBridge {
197
204
  platform: boot.platform,
198
205
 
199
206
  local: {
200
- // 浏览器没有系统目录选择器(showDirectoryPicker 不给路径)。返回
201
- // canceled 让 UI 走"手动输入路径"的既有分支。
202
- selectDirectory: async () => ({ canceled: true, filePaths: [] }),
207
+ // 浏览器 File System Access API 不给真实路径。改走宿主
208
+ // `/host/local/select-directory`(本机系统选择器);取消或无 GUI
209
+ // 时返回 canceled,UI 再走手动输入路径。
210
+ selectDirectory: async (options) =>
211
+ http<{ canceled: boolean; filePaths: string[] }>(
212
+ 'POST',
213
+ '/host/local/select-directory',
214
+ options ?? {},
215
+ ),
203
216
  saveTextFile: async (options) => {
204
217
  const blob = new Blob([options.content], { type: 'application/json' });
205
218
  const url = URL.createObjectURL(blob);
@@ -353,7 +353,7 @@ export interface ResolvedLocalPath {
353
353
 
354
354
  /**
355
355
  * 批量确认正文里提到的路径是否真实存在(用于决定行内代码要不要变成可点击)。
356
- * 相对路径由后端按对话绑定的项目根解析,无项目时按 home;不存在的候选不回。
356
+ * 相对路径由后端按对话绑定的项目根解析,无项目时按对话工作区;不存在的候选不回。
357
357
  */
358
358
  export async function resolveLocalPaths(candidates: string[], chatId?: string | null) {
359
359
  return bridge().localBackend.request<{ resolved: ResolvedLocalPath[] }>({
@@ -489,7 +489,7 @@ export async function createProject(input: {
489
489
 
490
490
  export async function updateProject(
491
491
  projectId: string,
492
- updates: { name?: string; folderPath?: string },
492
+ updates: { name?: string; folderPath?: string; sourceFolders?: string[] },
493
493
  ) {
494
494
  return bridge().localBackend.request<{ success: boolean; project: LocalProject }>({
495
495
  method: 'PUT',
@@ -22,6 +22,7 @@ import {
22
22
  readStoredExecPolicy,
23
23
  type ExecPolicy,
24
24
  } from "@/lib/exec-policy";
25
+ import { clampWebChatMode, hostToolChrome, settingsChrome } from "@/lib/host-tools";
25
26
  import type { ExecutedAction } from "@/components/chat/ExecutedActionsCard";
26
27
  import { inspectTaskTitle, type InspectTaskInput } from "@/components/chat/executed-actions-model";
27
28
  import type { ChildInfo } from "@/components/chat/OrchestrationChildrenCard";
@@ -85,10 +86,8 @@ type ChatMessageWithMetadata = ChatMessage & LocalChatMessage;
85
86
  const CHAT_MODE_STORAGE_KEY = "agent-chat-mode";
86
87
 
87
88
  function readStoredMode(): ChatMode {
88
- if (typeof localStorage === "undefined") return "agent";
89
- return localStorage.getItem(CHAT_MODE_STORAGE_KEY) === "plan"
90
- ? "plan"
91
- : "agent";
89
+ if (typeof localStorage === "undefined") return clampWebChatMode("agent");
90
+ return clampWebChatMode(localStorage.getItem(CHAT_MODE_STORAGE_KEY));
92
91
  }
93
92
 
94
93
  function extractPersistedActions(
@@ -374,8 +373,9 @@ function AgentChatView({
374
373
  // 当前会话绑定的项目 — Codex 式 cwd 指示:输入框上方显示项目名徽章,
375
374
  // 点击可关联到其他项目 / 修改项目目录 / 移出项目。无项目会话不显示。
376
375
  const [projects, setProjects] = useState<LocalProject[]>([]);
376
+ const showProjectsChrome = hostToolChrome("projects");
377
377
  const fetchProjects = useCallback(async () => {
378
- if (!isElectron()) return;
378
+ if (!isElectron() || !hostToolChrome("projects")) return;
379
379
  try {
380
380
  const res = await listProjects();
381
381
  setProjects(res.projects ?? []);
@@ -516,9 +516,10 @@ function AgentChatView({
516
516
  );
517
517
 
518
518
  const handleModeChange = useCallback((next: ChatMode) => {
519
- setMode(next);
519
+ const clamped = clampWebChatMode(next);
520
+ setMode(clamped);
520
521
  if (typeof localStorage !== "undefined") {
521
- localStorage.setItem(CHAT_MODE_STORAGE_KEY, next);
522
+ localStorage.setItem(CHAT_MODE_STORAGE_KEY, clamped);
522
523
  }
523
524
  }, []);
524
525
 
@@ -1121,7 +1122,9 @@ function AgentChatView({
1121
1122
  suggestedReplies={visibleSuggestedReplies}
1122
1123
  onSelectSuggestion={handleSelectSuggestion}
1123
1124
  onOpenSettings={
1124
- isElectron() ? () => setLlmSettingsOpen(true) : undefined
1125
+ isElectron() && settingsChrome("llm")
1126
+ ? () => setLlmSettingsOpen(true)
1127
+ : undefined
1125
1128
  }
1126
1129
  inputToolbarExtras={
1127
1130
  <ModelPicker
@@ -1134,25 +1137,31 @@ function AgentChatView({
1134
1137
  }
1135
1138
  mode={mode}
1136
1139
  onModeChange={handleModeChange}
1137
- execPolicy={execPolicy}
1138
- onExecPolicyChange={handleExecPolicyChange}
1140
+ execPolicy={hostToolChrome("local-fs") ? execPolicy : "workspace"}
1141
+ onExecPolicyChange={
1142
+ hostToolChrome("local-fs") ? handleExecPolicyChange : undefined
1143
+ }
1139
1144
  inputLeadingChrome={
1140
- <ChatProjectBadge
1141
- chatId={chatId}
1142
- project={chatProject}
1143
- projects={projects}
1144
- onProjectsChanged={fetchProjects}
1145
- onChatProjectChanged={ctx.refreshChats}
1146
- />
1145
+ showProjectsChrome ? (
1146
+ <ChatProjectBadge
1147
+ chatId={chatId}
1148
+ project={chatProject}
1149
+ projects={projects}
1150
+ onProjectsChanged={fetchProjects}
1151
+ onChatProjectChanged={ctx.refreshChats}
1152
+ />
1153
+ ) : undefined
1147
1154
  }
1148
1155
  inputBanner={
1149
1156
  <>
1150
1157
  {/* W6-5 项目信任门控:项目含规则文件但未信任时提示授权。 */}
1158
+ {showProjectsChrome ? (
1151
1159
  <ProjectTrustBanner
1152
1160
  chatId={chatId}
1153
1161
  project={chatProject}
1154
1162
  onTrustChanged={fetchProjects}
1155
1163
  />
1164
+ ) : null}
1156
1165
  {planReady && !isStreaming ? (
1157
1166
  <div className="mx-2.5 mb-1 flex items-center justify-between gap-2 rounded-agent-md border border-amber-400/50 bg-amber-400/10 px-2.5 py-1.5 text-xs">
1158
1167
  <div className="flex items-center gap-2 text-amber-700 dark:text-amber-300">
@@ -1183,7 +1192,7 @@ function AgentChatView({
1183
1192
  </>
1184
1193
  }
1185
1194
  />
1186
- {isElectron() && (
1195
+ {isElectron() && settingsChrome("llm") && (
1187
1196
  <LocalLlmSettingsModal
1188
1197
  open={llmSettingsOpen}
1189
1198
  onClose={() => setLlmSettingsOpen(false)}
@@ -1234,8 +1243,9 @@ function EmptyChatGate() {
1234
1243
  useEffect(() => {
1235
1244
  setSelectedProjectId(projectIdFromUrl);
1236
1245
  }, [projectIdFromUrl]);
1246
+ const showProjectsChrome = hostToolChrome("projects");
1237
1247
  const fetchProjects = useCallback(async () => {
1238
- if (!isElectron()) return;
1248
+ if (!isElectron() || !hostToolChrome("projects")) return;
1239
1249
  try {
1240
1250
  const res = await listProjects();
1241
1251
  setProjects(res.projects ?? []);
@@ -1258,9 +1268,10 @@ function EmptyChatGate() {
1258
1268
  null;
1259
1269
 
1260
1270
  const handleModeChange = useCallback((next: ChatMode) => {
1261
- setMode(next);
1271
+ const clamped = clampWebChatMode(next);
1272
+ setMode(clamped);
1262
1273
  if (typeof localStorage !== "undefined") {
1263
- localStorage.setItem(CHAT_MODE_STORAGE_KEY, next);
1274
+ localStorage.setItem(CHAT_MODE_STORAGE_KEY, clamped);
1264
1275
  }
1265
1276
  }, []);
1266
1277
 
@@ -1316,7 +1327,7 @@ function EmptyChatGate() {
1316
1327
  setCreateError(null);
1317
1328
  try {
1318
1329
  const id = await ctx.createChat({
1319
- ...(selectedProjectId ? { projectId: selectedProjectId } : {}),
1330
+ ...(selectedProjectId && showProjectsChrome ? { projectId: selectedProjectId } : {}),
1320
1331
  ...(ctx.selectedAgentId ? { agentId: ctx.selectedAgentId } : {}),
1321
1332
  });
1322
1333
  if (!id) throw new Error("创建对话失败,请重试");
@@ -1365,7 +1376,9 @@ function EmptyChatGate() {
1365
1376
  selectedAgentId={ctx.selectedAgentId}
1366
1377
  onSelectAgent={handleSelectAgent}
1367
1378
  onOpenSettings={
1368
- isElectron() ? () => setLlmSettingsOpen(true) : undefined
1379
+ isElectron() && settingsChrome("llm")
1380
+ ? () => setLlmSettingsOpen(true)
1381
+ : undefined
1369
1382
  }
1370
1383
  toolbarExtras={
1371
1384
  <ModelPicker
@@ -1377,7 +1390,7 @@ function EmptyChatGate() {
1377
1390
  />
1378
1391
  }
1379
1392
  leadingChrome={
1380
- isElectron() ? (
1393
+ isElectron() && showProjectsChrome ? (
1381
1394
  <ProjectPickerButton
1382
1395
  projects={projects}
1383
1396
  value={selectedProjectId}
@@ -1388,8 +1401,10 @@ function EmptyChatGate() {
1388
1401
  }
1389
1402
  mode={mode}
1390
1403
  onModeChange={handleModeChange}
1391
- execPolicy={execPolicy}
1392
- onExecPolicyChange={handleExecPolicyChange}
1404
+ execPolicy={hostToolChrome("local-fs") ? execPolicy : "workspace"}
1405
+ onExecPolicyChange={
1406
+ hostToolChrome("local-fs") ? handleExecPolicyChange : undefined
1407
+ }
1393
1408
  files={files}
1394
1409
  onFilesChange={setFiles}
1395
1410
  onMentionReferencesChange={setMentionReferences}
@@ -1411,7 +1426,7 @@ function EmptyChatGate() {
1411
1426
  </p>
1412
1427
  )}
1413
1428
  </div>
1414
- {isElectron() && (
1429
+ {isElectron() && settingsChrome("llm") && (
1415
1430
  <LocalLlmSettingsModal
1416
1431
  open={llmSettingsOpen}
1417
1432
  onClose={() => setLlmSettingsOpen(false)}
@@ -19,12 +19,15 @@ import { TelemetrySettingsPanel } from '@/components/settings/TelemetrySettingsP
19
19
  import { UsagePanel } from '@/components/settings/UsagePanel';
20
20
  import { WebSearchSettingsPanel } from '@/components/settings/WebSearchSettingsPanel';
21
21
  import { getPackSettingsPanels } from '@/packs/registry';
22
+ import { settingsChrome } from '@/lib/host-tools';
22
23
  import type { AgentOutletContext } from '@/layouts/AgentLayout';
23
24
 
24
25
  type SettingsSection = 'skills' | 'mcp' | 'agents' | 'general';
25
26
 
26
27
  function resolveSection(raw: string | null): SettingsSection {
27
- if (raw === 'skills' || raw === 'mcp' || raw === 'agents') return raw;
28
+ if (raw === 'skills' && settingsChrome('skills')) return 'skills';
29
+ if (raw === 'mcp' && settingsChrome('mcp')) return 'mcp';
30
+ if (raw === 'agents' && settingsChrome('agents')) return 'agents';
28
31
  return 'general';
29
32
  }
30
33
 
@@ -61,7 +64,7 @@ export function SettingsPage() {
61
64
  <div className="flex h-full w-full flex-col overflow-hidden">
62
65
  <header className="flex h-9 flex-shrink-0 items-center justify-between gap-2 border-b border-agent-border px-2.5">
63
66
  <h1 className="text-xs font-semibold text-agent-foreground">{title}</h1>
64
- {section === 'general' && (
67
+ {section === 'general' && settingsChrome('llm') && (
65
68
  <SettingsSaveButton
66
69
  testId="settings-header-save"
67
70
  saving={llmSaveUi.saving}
@@ -112,6 +115,7 @@ export function SettingsPage() {
112
115
 
113
116
  {section === 'general' && (
114
117
  <>
118
+ {settingsChrome('appearance') && (
115
119
  <section className="space-y-2" data-testid="settings-section-appearance">
116
120
  <h2 className="flex items-center gap-1.5 text-xs font-semibold text-agent-foreground">
117
121
  <LuMonitor className="h-3.5 w-3.5 text-agent-muted-foreground" />
@@ -119,7 +123,9 @@ export function SettingsPage() {
119
123
  </h2>
120
124
  <AppearanceSettingsPanel />
121
125
  </section>
126
+ )}
122
127
 
128
+ {settingsChrome('llm') && (
123
129
  <section className="space-y-2" data-testid="settings-section-llm">
124
130
  <h2 className="flex items-center gap-1.5 text-xs font-semibold text-agent-foreground">
125
131
  <LuSettings className="h-3.5 w-3.5 text-agent-muted-foreground" />
@@ -131,7 +137,9 @@ export function SettingsPage() {
131
137
  onSaveUiChange={setLlmSaveUi}
132
138
  />
133
139
  </section>
140
+ )}
134
141
 
142
+ {settingsChrome('web-search') && (
135
143
  <section className="space-y-2" data-testid="settings-section-web-search">
136
144
  <h2 className="flex items-center gap-1.5 text-xs font-semibold text-agent-foreground">
137
145
  <LuSearch className="h-3.5 w-3.5 text-agent-muted-foreground" />
@@ -139,7 +147,9 @@ export function SettingsPage() {
139
147
  </h2>
140
148
  <WebSearchSettingsPanel />
141
149
  </section>
150
+ )}
142
151
 
152
+ {settingsChrome('usage') && (
143
153
  <section className="space-y-2" data-testid="settings-section-usage">
144
154
  <h2 className="flex items-center gap-1.5 text-xs font-semibold text-agent-foreground">
145
155
  <LuChartBar className="h-3.5 w-3.5 text-agent-muted-foreground" />
@@ -147,7 +157,9 @@ export function SettingsPage() {
147
157
  </h2>
148
158
  <UsagePanel />
149
159
  </section>
160
+ )}
150
161
 
162
+ {settingsChrome('diagnose') && (
151
163
  <section className="space-y-2" data-testid="settings-section-diagnose">
152
164
  <h2 className="flex items-center gap-1.5 text-xs font-semibold text-agent-foreground">
153
165
  <LuNetwork className="h-3.5 w-3.5 text-agent-muted-foreground" />
@@ -155,7 +167,9 @@ export function SettingsPage() {
155
167
  </h2>
156
168
  <DiagnoseSettingsPanel />
157
169
  </section>
170
+ )}
158
171
 
172
+ {settingsChrome('security') && (
159
173
  <section className="space-y-2" data-testid="settings-section-security">
160
174
  <h2 className="flex items-center gap-1.5 text-xs font-semibold text-agent-foreground">
161
175
  <LuShieldCheck className="h-3.5 w-3.5 text-agent-muted-foreground" />
@@ -163,7 +177,9 @@ export function SettingsPage() {
163
177
  </h2>
164
178
  <SecuritySettingsPanel />
165
179
  </section>
180
+ )}
166
181
 
182
+ {settingsChrome('insights') && (
167
183
  <section className="space-y-2" data-testid="settings-section-insights">
168
184
  <h2 className="flex items-center gap-1.5 text-xs font-semibold text-agent-foreground">
169
185
  <LuChartBar className="h-3.5 w-3.5 text-agent-muted-foreground" />
@@ -171,7 +187,9 @@ export function SettingsPage() {
171
187
  </h2>
172
188
  <InsightsSettingsPanel />
173
189
  </section>
190
+ )}
174
191
 
192
+ {settingsChrome('telemetry') && (
175
193
  <section className="space-y-2" data-testid="settings-section-telemetry">
176
194
  <h2 className="flex items-center gap-1.5 text-xs font-semibold text-agent-foreground">
177
195
  <LuActivity className="h-3.5 w-3.5 text-agent-muted-foreground" />
@@ -179,6 +197,7 @@ export function SettingsPage() {
179
197
  </h2>
180
198
  <TelemetrySettingsPanel />
181
199
  </section>
200
+ )}
182
201
 
183
202
  {/* 场景包设置面板(1.2 起由包渲染层贡献)。
184
203
  包经 packs/registry 注册,未激活 flavor 的产物里没有包组件。 */}
package/src/vite-env.d.ts CHANGED
@@ -1 +1,17 @@
1
1
  /// <reference types="vite/client" />
2
+
3
+ interface ImportMetaEnv {
4
+ readonly VITE_APP_FLAVOR?: string;
5
+ readonly VITE_BRAND_NAME?: string;
6
+ readonly VITE_BRAND_TITLE?: string;
7
+ readonly VITE_BRAND_TAGLINE?: string;
8
+ readonly VITE_DEFAULT_AGENT_ID?: string;
9
+ readonly VITE_HOST_TOOLS?: string;
10
+ readonly VITE_APPROVAL?: string;
11
+ readonly VITE_CHAT_MODES?: string;
12
+ readonly VITE_SETTINGS?: string;
13
+ }
14
+
15
+ interface ImportMeta {
16
+ readonly env: ImportMetaEnv;
17
+ }
package/vite.base.js CHANGED
@@ -53,6 +53,11 @@ export function createProductViteConfig(options) {
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
55
  'import.meta.env.VITE_DEFAULT_AGENT_ID': JSON.stringify(brand?.defaultAgentId ?? 'local-assistant'),
56
+ // 宿主工具族 / 安全询问:与 node 侧 setProductConfig 读同一份 product.json。
57
+ 'import.meta.env.VITE_HOST_TOOLS': JSON.stringify(JSON.stringify(product.hostTools ?? {})),
58
+ 'import.meta.env.VITE_APPROVAL': JSON.stringify(product.approval ?? 'host'),
59
+ 'import.meta.env.VITE_CHAT_MODES': JSON.stringify(JSON.stringify(product.chatModes ?? ['agent', 'plan'])),
60
+ 'import.meta.env.VITE_SETTINGS': JSON.stringify(JSON.stringify(product.settings ?? {})),
56
61
  },
57
62
  resolve: {
58
63
  alias: {