@steerable/agent-shell-web 0.6.15 → 0.6.16

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 (34) hide show
  1. package/package.json +11 -9
  2. package/src/brand.test.ts +0 -22
  3. package/src/components/AgentSidebar.test.tsx +0 -127
  4. package/src/components/ChatHeader.test.tsx +0 -230
  5. package/src/components/LocalLlmSettingsModal.test.tsx +0 -372
  6. package/src/components/chat/ApprovalModal.test.tsx +0 -83
  7. package/src/components/chat/AskUserModalHost.test.tsx +0 -176
  8. package/src/components/chat/AssistantMessage.test.tsx +0 -68
  9. package/src/components/chat/ChatInput.test.tsx +0 -609
  10. package/src/components/chat/ExecPolicyPicker.test.tsx +0 -26
  11. package/src/components/chat/Markdown.test.tsx +0 -170
  12. package/src/components/chat/ModelPicker.test.tsx +0 -149
  13. package/src/components/chat/SessionTreeModal.test.tsx +0 -161
  14. package/src/components/chat/TaskOutcomeCards.test.tsx +0 -97
  15. package/src/components/chat/TaskPanelModal.test.tsx +0 -198
  16. package/src/components/chat/TaskProcessPanel.test.tsx +0 -111
  17. package/src/components/chat/TurnProcessGroup.test.tsx +0 -161
  18. package/src/components/chat/elapsed.test.ts +0 -42
  19. package/src/components/chat/executed-actions-model.test.ts +0 -134
  20. package/src/components/chat/orchestration-children-model.test.ts +0 -115
  21. package/src/components/chat/turn-timeline.test.ts +0 -97
  22. package/src/components/chat/useChatTasks.test.ts +0 -136
  23. package/src/components/settings/AgentsSettingsPanel.test.tsx +0 -229
  24. package/src/components/settings/InsightsSettingsPanel.test.tsx +0 -132
  25. package/src/components/settings/ModelIdCombobox.test.tsx +0 -88
  26. package/src/components/settings/SecuritySettingsPanel.test.tsx +0 -122
  27. package/src/components/settings/SkillsSettingsPanel.test.tsx +0 -62
  28. package/src/components/settings/WebSearchSettingsPanel.test.tsx +0 -90
  29. package/src/components/settings/compat-flags-model.test.ts +0 -71
  30. package/src/components/settings/llm-vendors.test.ts +0 -171
  31. package/src/components/settings/model-capabilities.test.ts +0 -88
  32. package/src/components/settings/preset-choice-model.test.ts +0 -96
  33. package/src/lib/pending-first-message.test.ts +0 -17
  34. package/src/pages/SettingsPage.test.tsx +0 -102
@@ -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
- });