@steerable/agent-shell-web 0.6.15 → 0.6.17

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 +19 -12
  2. package/vite.base.d.ts +7 -0
  3. package/vite.base.js +83 -0
  4. package/src/brand.test.ts +0 -22
  5. package/src/components/AgentSidebar.test.tsx +0 -127
  6. package/src/components/ChatHeader.test.tsx +0 -230
  7. package/src/components/LocalLlmSettingsModal.test.tsx +0 -372
  8. package/src/components/chat/ApprovalModal.test.tsx +0 -83
  9. package/src/components/chat/AskUserModalHost.test.tsx +0 -176
  10. package/src/components/chat/AssistantMessage.test.tsx +0 -68
  11. package/src/components/chat/ChatInput.test.tsx +0 -609
  12. package/src/components/chat/ExecPolicyPicker.test.tsx +0 -26
  13. package/src/components/chat/Markdown.test.tsx +0 -170
  14. package/src/components/chat/ModelPicker.test.tsx +0 -149
  15. package/src/components/chat/SessionTreeModal.test.tsx +0 -161
  16. package/src/components/chat/TaskOutcomeCards.test.tsx +0 -97
  17. package/src/components/chat/TaskPanelModal.test.tsx +0 -198
  18. package/src/components/chat/TaskProcessPanel.test.tsx +0 -111
  19. package/src/components/chat/TurnProcessGroup.test.tsx +0 -161
  20. package/src/components/chat/elapsed.test.ts +0 -42
  21. package/src/components/chat/executed-actions-model.test.ts +0 -134
  22. package/src/components/chat/orchestration-children-model.test.ts +0 -115
  23. package/src/components/chat/turn-timeline.test.ts +0 -97
  24. package/src/components/chat/useChatTasks.test.ts +0 -136
  25. package/src/components/settings/AgentsSettingsPanel.test.tsx +0 -229
  26. package/src/components/settings/InsightsSettingsPanel.test.tsx +0 -132
  27. package/src/components/settings/ModelIdCombobox.test.tsx +0 -88
  28. package/src/components/settings/SecuritySettingsPanel.test.tsx +0 -122
  29. package/src/components/settings/SkillsSettingsPanel.test.tsx +0 -62
  30. package/src/components/settings/WebSearchSettingsPanel.test.tsx +0 -90
  31. package/src/components/settings/compat-flags-model.test.ts +0 -71
  32. package/src/components/settings/llm-vendors.test.ts +0 -171
  33. package/src/components/settings/model-capabilities.test.ts +0 -88
  34. package/src/components/settings/preset-choice-model.test.ts +0 -96
  35. package/src/lib/pending-first-message.test.ts +0 -17
  36. package/src/pages/SettingsPage.test.tsx +0 -102
  37. package/vite.base.ts +0 -94
@@ -1,171 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import {
3
- defaultModelForVendor,
4
- FALLBACK_VENDORS,
5
- inferVendorId,
6
- llmProviderFromWireKind,
7
- mergeVendorOptions,
8
- modelPickerOptions,
9
- modelPickerRows,
10
- vendorLabel,
11
- type VendorOption,
12
- } from './llm-vendors';
13
- import type { LlmSettings } from '@/lib/local-api';
14
-
15
- const vendors = mergeVendorOptions([
16
- {
17
- id: 'deepseek',
18
- apiBaseUrl: 'https://api.deepseek.com',
19
- envVars: [],
20
- wireKind: 'openai_compat',
21
- models: ['deepseek-v4-flash', 'deepseek-v4-pro'],
22
- },
23
- {
24
- id: 'anthropic',
25
- apiBaseUrl: 'https://api.anthropic.com',
26
- envVars: [],
27
- wireKind: 'anthropic',
28
- models: ['claude-sonnet-4-6'],
29
- },
30
- {
31
- id: 'azure',
32
- apiBaseUrl: null,
33
- envVars: ['AZURE_API_KEY'],
34
- wireKind: 'openai_compat',
35
- models: ['gpt-4o'],
36
- },
37
- ]);
38
-
39
- describe('mergeVendorOptions', () => {
40
- it('pins featured vendors and injects ollama / custom', () => {
41
- const ids = vendors.map((v) => v.id);
42
- expect(ids.slice(0, 2)).toEqual(['deepseek', 'anthropic']);
43
- expect(ids).toContain('ollama');
44
- expect(ids).toContain('custom');
45
- expect(ids).toContain('azure');
46
- expect(vendorLabel('alibaba-cn')).toBe('阿里云百炼');
47
- });
48
-
49
- it('orders the fallback featured list by common usage', () => {
50
- expect(
51
- mergeVendorOptions(FALLBACK_VENDORS)
52
- .filter((v) => v.featured)
53
- .map((v) => v.id),
54
- ).toEqual([
55
- 'deepseek',
56
- 'alibaba-cn',
57
- 'moonshotai-cn',
58
- 'zhipuai',
59
- 'openai',
60
- 'anthropic',
61
- 'google',
62
- 'openrouter',
63
- 'xai',
64
- 'groq',
65
- 'lmstudio',
66
- 'ollama',
67
- 'custom',
68
- ]);
69
- });
70
- });
71
-
72
- describe('inferVendorId', () => {
73
- it('prefers an explicit vendorId when that vendor is listed', () => {
74
- expect(
75
- inferVendorId({ provider: 'openai-compat', vendorId: 'anthropic', model: 'x' }, vendors),
76
- ).toBe('anthropic');
77
- });
78
-
79
- it('matches a saved DeepSeek URL back to the catalog vendor', () => {
80
- const settings: LlmSettings = {
81
- provider: 'openai-compat',
82
- model: 'deepseek-v4-flash',
83
- baseUrl: 'https://api.deepseek.com',
84
- };
85
- expect(inferVendorId(settings, vendors)).toBe('deepseek');
86
- });
87
-
88
- it('uses ollama for the local provider', () => {
89
- expect(
90
- inferVendorId(
91
- { provider: 'ollama', model: 'llama3.1:8b', baseUrl: 'http://127.0.0.1:11434' },
92
- vendors,
93
- ),
94
- ).toBe('ollama');
95
- });
96
- });
97
-
98
- describe('llmProviderFromWireKind', () => {
99
- it('maps sidecar kinds onto persisted provider ids', () => {
100
- expect(llmProviderFromWireKind('openai_compat')).toBe('openai-compat');
101
- expect(llmProviderFromWireKind('anthropic')).toBe('anthropic');
102
- expect(llmProviderFromWireKind('google')).toBe('google');
103
- expect(llmProviderFromWireKind('openai-responses')).toBe('openai-responses');
104
- expect(llmProviderFromWireKind('ollama')).toBe('ollama');
105
- });
106
- });
107
-
108
- describe('defaultModelForVendor', () => {
109
- it('keeps the current model when it is on the vendor list', () => {
110
- const vendor = vendors.find((v) => v.id === 'deepseek') as VendorOption;
111
- expect(defaultModelForVendor(vendor, 'deepseek-v4-pro')).toBe('deepseek-v4-pro');
112
- });
113
-
114
- it('falls back to the first catalog model', () => {
115
- const vendor = vendors.find((v) => v.id === 'deepseek') as VendorOption;
116
- expect(defaultModelForVendor(vendor, 'other')).toBe('deepseek-v4-flash');
117
- });
118
- });
119
-
120
- describe('modelPickerOptions', () => {
121
- const catalog = ['deepseek-chat', 'deepseek-v4-flash', 'deepseek-v4-pro'];
122
-
123
- it('uses the live gateway list and does not mix in catalog ids', () => {
124
- expect(
125
- modelPickerOptions(['deepseek-chat', 'deepseek-reasoner'], catalog, 'deepseek-chat', 'live'),
126
- ).toEqual(['deepseek-chat', 'deepseek-reasoner']);
127
- });
128
-
129
- it('keeps the currently selected id even if the gateway omitted it', () => {
130
- expect(
131
- modelPickerOptions(['deepseek-chat'], catalog, 'deepseek-v4-flash', 'live'),
132
- ).toEqual(['deepseek-chat', 'deepseek-v4-flash']);
133
- });
134
-
135
- it('falls back to the vendor catalog when the gateway listing is unavailable', () => {
136
- expect(modelPickerOptions([], catalog, 'deepseek-chat', 'offline')).toEqual(catalog);
137
- });
138
- });
139
-
140
- describe('modelPickerRows', () => {
141
- const catalog = ['deepseek-chat', 'deepseek-v4-flash'];
142
-
143
- it('keeps live capability rows and marks a missing current id as unknown', () => {
144
- const live = [
145
- {
146
- id: 'deepseek-v4-pro',
147
- name: 'DeepSeek V4 Pro',
148
- window: 1_000_000,
149
- modalities: ['text'],
150
- reasoningLevels: ['high', 'max'],
151
- pricing: null,
152
- joinedFrom: 'deepseek/deepseek-v4-pro',
153
- capabilities: 'known' as const,
154
- },
155
- ];
156
- expect(modelPickerRows(live, catalog, 'deepseek-v4-flash', 'live')).toEqual([
157
- { id: 'deepseek-v4-pro', entry: live[0] },
158
- {
159
- id: 'deepseek-v4-flash',
160
- entry: expect.objectContaining({ id: 'deepseek-v4-flash', capabilities: 'unknown' }),
161
- },
162
- ]);
163
- });
164
-
165
- it('does not invent capabilities for the vendor catalog fallback', () => {
166
- expect(modelPickerRows([], catalog, 'deepseek-chat', 'offline')).toEqual([
167
- { id: 'deepseek-chat', entry: null },
168
- { id: 'deepseek-v4-flash', entry: null },
169
- ]);
170
- });
171
- });
@@ -1,88 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import type { GatewayModelEntry } from '@/lib/local-api';
3
- import {
4
- formatContextWindow,
5
- modelCapabilityChips,
6
- } from './model-capabilities';
7
-
8
- function entry(overrides: Partial<GatewayModelEntry> & Pick<GatewayModelEntry, 'id'>): GatewayModelEntry {
9
- return {
10
- name: null,
11
- window: null,
12
- modalities: [],
13
- reasoningLevels: [],
14
- pricing: null,
15
- joinedFrom: null,
16
- capabilities: 'unknown',
17
- ...overrides,
18
- };
19
- }
20
-
21
- describe('formatContextWindow', () => {
22
- it('renders million- and thousand-token windows', () => {
23
- expect(formatContextWindow(1_000_000)).toBe('1M');
24
- expect(formatContextWindow(1_048_576)).toBe('1M');
25
- expect(formatContextWindow(131_072)).toBe('131K');
26
- expect(formatContextWindow(512)).toBe('512');
27
- expect(formatContextWindow(null)).toBeNull();
28
- });
29
- });
30
-
31
- describe('modelCapabilityChips', () => {
32
- it('shows verified thinking, multimodal, and window chips', () => {
33
- const chips = modelCapabilityChips(
34
- entry({
35
- id: 'gemini-3.5-flash',
36
- window: 1_048_576,
37
- modalities: ['audio', 'image', 'pdf', 'text', 'video'],
38
- reasoningLevels: ['minimal', 'low', 'medium', 'high'],
39
- joinedFrom: 'google/gemini-3.5-flash',
40
- capabilities: 'known',
41
- }),
42
- );
43
- expect(chips.map((chip) => chip.label)).toEqual([
44
- '思考',
45
- '图像',
46
- 'PDF',
47
- '音频',
48
- '视频',
49
- '1M',
50
- ]);
51
- });
52
-
53
- it('puts reasoning levels on the thinking chip in the selected summary', () => {
54
- const chips = modelCapabilityChips(
55
- entry({
56
- id: 'deepseek-v4-pro',
57
- window: 1_000_000,
58
- modalities: ['text'],
59
- reasoningLevels: ['high', 'max'],
60
- joinedFrom: 'deepseek/deepseek-v4-pro',
61
- capabilities: 'known',
62
- }),
63
- { detail: true },
64
- );
65
- expect(chips.map((chip) => chip.label)).toEqual(['思考(high / max)', '1M']);
66
- });
67
-
68
- it('does not invent thinking or image chips from heuristic unknown rows', () => {
69
- expect(
70
- modelCapabilityChips(
71
- entry({
72
- id: 'deepseek-flash',
73
- window: 131_072,
74
- modalities: ['text'],
75
- reasoningLevels: [],
76
- capabilities: 'unknown',
77
- }),
78
- ),
79
- ).toEqual([
80
- expect.objectContaining({ key: 'unknown', label: '未识别' }),
81
- ]);
82
- });
83
-
84
- it('omits chips when capabilities was not reported', () => {
85
- expect(modelCapabilityChips({ id: 'custom-model' } as GatewayModelEntry)).toEqual([]);
86
- expect(modelCapabilityChips(null)).toEqual([]);
87
- });
88
- });
@@ -1,96 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import {
3
- choiceFromPresetMode,
4
- descriptorLabel,
5
- overrideFromDescriptor,
6
- overridesEqual,
7
- presetModeFromChoice,
8
- summarizePreset,
9
- } from './preset-choice-model';
10
- import type { ProviderPresetDescriptor } from '@/lib/local-api';
11
-
12
- // 与框架 llm/presets.py `describe_provider_presets` 同形的注册表行夹具。
13
- const ROWS: ProviderPresetDescriptor[] = [
14
- {
15
- host: 'api.deepseek.com',
16
- modelPrefix: 'deepseek',
17
- temperature: 0.0,
18
- topP: null,
19
- maxTokens: null,
20
- reasoningEffort: null,
21
- extraBody: null,
22
- },
23
- {
24
- host: null,
25
- modelPrefix: 'qwen3',
26
- temperature: 0.6,
27
- topP: 0.95,
28
- maxTokens: null,
29
- reasoningEffort: null,
30
- extraBody: { top_k: 20 },
31
- },
32
- ];
33
-
34
- describe('overrideFromDescriptor', () => {
35
- it('drops null fields so they are not sent on the wire', () => {
36
- expect(overrideFromDescriptor(ROWS[0])).toEqual({ temperature: 0.0 });
37
- expect(overrideFromDescriptor(ROWS[1])).toEqual({
38
- temperature: 0.6,
39
- topP: 0.95,
40
- extraBody: { top_k: 20 },
41
- });
42
- });
43
- });
44
-
45
- describe('descriptorLabel', () => {
46
- it('joins host and model prefix, tolerating single-key rows', () => {
47
- expect(descriptorLabel(ROWS[0])).toBe('api.deepseek.com + deepseek*');
48
- expect(descriptorLabel(ROWS[1])).toBe('qwen3*');
49
- });
50
- });
51
-
52
- describe('summarizePreset', () => {
53
- it('renders a one-line parameter summary', () => {
54
- expect(summarizePreset(overrideFromDescriptor(ROWS[1]))).toBe(
55
- 'temperature 0.6 · top_p 0.95 · top_k 20',
56
- );
57
- });
58
-
59
- it('describes the empty preset honestly (Moonshot case)', () => {
60
- expect(summarizePreset(null)).toBe('不下发额外采样参数');
61
- expect(summarizePreset({})).toContain('不带采样字段');
62
- });
63
- });
64
-
65
- describe('presetModeFromChoice / choiceFromPresetMode', () => {
66
- it('round-trips the three modes', () => {
67
- expect(presetModeFromChoice(undefined)).toBe('auto');
68
- expect(presetModeFromChoice({ enabled: true })).toBe('auto');
69
- expect(presetModeFromChoice({ enabled: false })).toBe('off');
70
- expect(presetModeFromChoice({ override: { temperature: 0.6 } })).toBe('pinned');
71
-
72
- expect(choiceFromPresetMode('auto', -1, ROWS)).toBeUndefined();
73
- expect(choiceFromPresetMode('off', -1, ROWS)).toEqual({ enabled: false });
74
- expect(choiceFromPresetMode('pinned', 1, ROWS)).toEqual({
75
- override: { temperature: 0.6, topP: 0.95, extraBody: { top_k: 20 } },
76
- });
77
- });
78
-
79
- it('keeps a saved custom override when the registry is unavailable', () => {
80
- const existing = { override: { temperature: 0.42 } };
81
- expect(choiceFromPresetMode('pinned', -1, [], existing)).toEqual(existing);
82
- // 无注册表也无已保存覆盖:钉死无从谈起,回落为不持久化(自动)。
83
- expect(choiceFromPresetMode('pinned', -1, [])).toBeUndefined();
84
- });
85
- });
86
-
87
- describe('overridesEqual', () => {
88
- it('matches a saved override back to its registry row', () => {
89
- expect(overridesEqual(overrideFromDescriptor(ROWS[1]), {
90
- temperature: 0.6,
91
- topP: 0.95,
92
- extraBody: { top_k: 20 },
93
- })).toBe(true);
94
- expect(overridesEqual(overrideFromDescriptor(ROWS[0]), { temperature: 0.1 })).toBe(false);
95
- });
96
- });
@@ -1,17 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import {
3
- setPendingFirstMessage,
4
- takePendingFirstMessage,
5
- } from './pending-first-message';
6
-
7
- describe('pending-first-message(首页首条消息接力)', () => {
8
- it('按 chatId 取出一次后清空,错配的 chatId 不消费', () => {
9
- setPendingFirstMessage({ chatId: 'chat-a', content: '秋天到了,随便聊一句吧。' });
10
- expect(takePendingFirstMessage('chat-b')).toBeNull();
11
- expect(takePendingFirstMessage('chat-a')).toEqual({
12
- chatId: 'chat-a',
13
- content: '秋天到了,随便聊一句吧。',
14
- });
15
- expect(takePendingFirstMessage('chat-a')).toBeNull();
16
- });
17
- });
@@ -1,102 +0,0 @@
1
- import { cleanup, fireEvent, render, screen } from '@testing-library/react';
2
- import { MemoryRouter } from 'react-router-dom';
3
- import { afterEach, describe, expect, it, vi } from 'vitest';
4
-
5
- const { save } = vi.hoisted(() => ({ save: vi.fn(async () => {}) }));
6
-
7
- vi.mock('@/lib/electron-bridge', () => ({
8
- isElectron: () => true,
9
- }));
10
-
11
- vi.mock('@/components/settings/LlmSettingsPanel', async () => {
12
- const react = await import('react');
13
- return {
14
- LlmSettingsPanel: react.forwardRef(function MockLlm(
15
- props: { onSaveUiChange?: (ui: { saving: boolean; savedOk: boolean; loading: boolean }) => void },
16
- ref,
17
- ) {
18
- react.useImperativeHandle(ref, () => ({ save }));
19
- react.useEffect(() => {
20
- props.onSaveUiChange?.({ saving: false, savedOk: false, loading: false });
21
- }, [props.onSaveUiChange]);
22
- return <div data-testid="llm-panel" />;
23
- }),
24
- };
25
- });
26
-
27
- vi.mock('@/components/settings/InsightsSettingsPanel', () => ({
28
- InsightsSettingsPanel: () => null,
29
- }));
30
- vi.mock('@/components/settings/TelemetrySettingsPanel', () => ({
31
- TelemetrySettingsPanel: () => null,
32
- }));
33
- vi.mock('@/components/settings/UsagePanel', () => ({
34
- UsagePanel: () => null,
35
- }));
36
- vi.mock('@/components/settings/WebSearchSettingsPanel', () => ({
37
- WebSearchSettingsPanel: () => null,
38
- }));
39
- vi.mock('@/components/settings/SecuritySettingsPanel', () => ({
40
- SecuritySettingsPanel: () => null,
41
- }));
42
- vi.mock('@/components/settings/SkillsSettingsPanel', () => ({
43
- SkillsSettingsPanel: () => null,
44
- }));
45
- vi.mock('@/components/settings/McpSettingsPanel', () => ({
46
- McpSettingsPanel: () => null,
47
- }));
48
- vi.mock('@/components/settings/AgentsSettingsPanel', () => ({
49
- AgentsSettingsPanel: () => null,
50
- }));
51
-
52
- const { SettingsPage } = await import('./SettingsPage');
53
-
54
- function renderSettings(search = '') {
55
- return render(
56
- <MemoryRouter initialEntries={[`/settings${search}`]}>
57
- <SettingsPage />
58
- </MemoryRouter>,
59
- );
60
- }
61
-
62
- afterEach(() => {
63
- cleanup();
64
- save.mockClear();
65
- });
66
-
67
- describe('SettingsPage header save', () => {
68
- it('saves LLM settings from the general settings header', () => {
69
- renderSettings();
70
- fireEvent.click(screen.getByTestId('settings-header-save'));
71
- expect(save).toHaveBeenCalledTimes(1);
72
- });
73
-
74
- it('does not show the header save on skills, MCP, or agents pages', () => {
75
- const skills = renderSettings('?section=skills');
76
- expect(screen.queryByTestId('settings-header-save')).toBeNull();
77
- skills.unmount();
78
- const mcp = renderSettings('?section=mcp');
79
- expect(screen.queryByTestId('settings-header-save')).toBeNull();
80
- mcp.unmount();
81
- renderSettings('?section=agents');
82
- expect(screen.queryByTestId('settings-header-save')).toBeNull();
83
- });
84
-
85
- it('orders general sections by how often they are used', () => {
86
- renderSettings();
87
- expect(screen.getByRole('heading', { name: '帮助改进产品' })).toBeTruthy();
88
- expect(
89
- [...document.querySelectorAll('[data-testid^="settings-section-"]')].map(
90
- (el) => el.getAttribute('data-testid'),
91
- ),
92
- ).toEqual([
93
- 'settings-section-llm',
94
- 'settings-section-web-search',
95
- 'settings-section-usage',
96
- 'settings-section-diagnose',
97
- 'settings-section-security',
98
- 'settings-section-insights',
99
- 'settings-section-telemetry',
100
- ]);
101
- });
102
- });
package/vite.base.ts DELETED
@@ -1,94 +0,0 @@
1
- /**
2
- * 产品 web 构建的共享 vite 配置工厂(2.3)。
3
- *
4
- * 每个产品(products/<id>/web/)有自己的 vite 入口:index.html +
5
- * main.tsx(静态注册本产品的包渲染层贡献)+ vite.config.ts(调本工厂)。
6
- * 产品的编译单元只含自己的包——未激活包代码不进 bundle,不依赖树摇
7
- * 正确性(物理隔离与 node 侧对称)。
8
- *
9
- * Electron 加载产物时走 file:// 协议,必须用相对路径,否则资源 404。
10
- * dev server 跑在固定端口,主进程的 IS_DEV 分支 loadURL('http://localhost:5173')。
11
- */
12
- import { defineConfig } from 'vite';
13
- import react from '@vitejs/plugin-react';
14
- import tailwindcss from '@tailwindcss/vite';
15
- import path from 'node:path';
16
- import { fileURLToPath } from 'node:url';
17
- import { existsSync, readFileSync } from 'node:fs';
18
-
19
- const HERE = path.dirname(fileURLToPath(import.meta.url));
20
-
21
- export interface ProductViteConfigOptions {
22
- /** 产品目录(products/<id>/web 的上一级…即 products/<id>)。 */
23
- productDir: string;
24
- /** 产品 flavor(决定品牌 define 与激活包集合)。 */
25
- flavor: string;
26
- }
27
-
28
- export function createProductViteConfig(options: ProductViteConfigOptions) {
29
- const { productDir, flavor } = options;
30
- const repoRoot = path.resolve(productDir, '../..');
31
- // shell 渲染层源码锚到本包自身(3.2 起住在框架仓库
32
- // packages/agent-shell/web/src),产品经 `@/` 别名引用。
33
- const webSrcDir = path.resolve(HERE, 'src');
34
- const readJson = (p: string) => JSON.parse(readFileSync(p, 'utf8'));
35
-
36
- // 产品品牌(3.1):product.json 的 brand 字段优先(无包产品的品牌
37
- // 来源);否则取激活包 pack.json 的 brand(0.3g 机制,读
38
- // products/manifest.json 的 flavor→包映射,第一个带 brand 的包)。
39
- // 都没有则用 shell 中性默认(Agent Shell)。
40
- const productJsonPath = path.join(productDir, 'product.json');
41
- const product = existsSync(productJsonPath) ? readJson(productJsonPath) : {};
42
- const manifest = readJson(path.join(repoRoot, 'products/manifest.json'));
43
- let packBrand: Record<string, string> | null = null;
44
- for (const packId of manifest.flavors?.[flavor] ?? []) {
45
- const pack = readJson(path.join(repoRoot, 'packages', `pack-${packId}`, 'pack.json'));
46
- if (pack.brand) {
47
- packBrand = pack.brand;
48
- break;
49
- }
50
- }
51
- const brand = product.brand ?? packBrand;
52
-
53
- return defineConfig({
54
- // root 默认取 config 文件所在目录(products/<id>/web)。
55
- base: './',
56
- plugins: [react(), tailwindcss()],
57
- define: {
58
- // 显式 define 让 flavor 条件在构建期可静态折叠。
59
- 'import.meta.env.VITE_APP_FLAVOR': JSON.stringify(flavor),
60
- // 品牌 define:产品/包声明覆盖 shell 中性默认(Agent Shell)。
61
- // logo 不走 define——资产必须静态 import 才进 bundle,由包 web 模块
62
- // 在注册时经 setBrandLogoUrl 注入(本包 src/brand.ts)。
63
- 'import.meta.env.VITE_BRAND_NAME': JSON.stringify(brand?.displayName ?? 'Steerable Shell'),
64
- 'import.meta.env.VITE_BRAND_TAGLINE': JSON.stringify(brand?.tagline ?? '一款本地桌面 AI 伙伴'),
65
- 'import.meta.env.VITE_DEFAULT_AGENT_ID': JSON.stringify(brand?.defaultAgentId ?? 'local-assistant'),
66
- },
67
- resolve: {
68
- alias: {
69
- // shell 渲染层源码(产品入口与包 web 模块都以 `@/` 引用它)。
70
- '@': webSrcDir,
71
- },
72
- },
73
- // 静态资产(favicon 等中性默认)随 shell web 包分发;产品可在自己的
74
- // web root 放 public/ 覆盖(vite 不合并双 publicDir——需要产品级资产
75
- // 时在产品 vite.config 里显式覆盖本字段)。
76
- publicDir: path.resolve(HERE, 'public'),
77
- server: {
78
- port: 5173,
79
- strictPort: true,
80
- host: '127.0.0.1',
81
- // 产品 root 在 products/<id>/web;shell 渲染层源码在框架仓库
82
- // (3.2 起经 file: 依赖符号链接)——两个根都放行。
83
- fs: { allow: [repoRoot, path.resolve(webSrcDir, '..')] },
84
- },
85
- build: {
86
- // 产物落在产品目录:products/<id>/web/dist(打包期被 electron-builder
87
- // 映射为包内 web-dist/)。
88
- outDir: 'dist',
89
- emptyOutDir: true,
90
- sourcemap: true,
91
- target: 'chrome120',
92
- },
93
- });
94
- }