@steerable/agent-shell-web 0.6.18 → 0.6.20

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 (55) hide show
  1. package/package.json +15 -8
  2. package/src/lib/chat-transport.ts +1 -2
  3. package/src/lib/electron-bridge.ts +2 -2
  4. package/src/lib/http-bridge.ts +18 -4
  5. package/vite.base.d.ts +7 -0
  6. package/vite.base.js +83 -0
  7. package/src/brand.test.ts +0 -22
  8. package/src/components/AgentSidebar.test.tsx +0 -791
  9. package/src/components/ChatHeader.test.tsx +0 -230
  10. package/src/components/LocalLlmSettingsModal.test.tsx +0 -372
  11. package/src/components/chat/ApprovalModal.test.tsx +0 -83
  12. package/src/components/chat/AskUserModalHost.test.tsx +0 -176
  13. package/src/components/chat/AskUserQuestionMenu.test.tsx +0 -190
  14. package/src/components/chat/AssistantMessage.test.tsx +0 -68
  15. package/src/components/chat/ChatInput.test.tsx +0 -609
  16. package/src/components/chat/ExecPolicyPicker.test.tsx +0 -26
  17. package/src/components/chat/Markdown.test.tsx +0 -170
  18. package/src/components/chat/ModelPicker.test.tsx +0 -149
  19. package/src/components/chat/SessionTreeModal.test.tsx +0 -161
  20. package/src/components/chat/TaskOutcomeCards.test.tsx +0 -97
  21. package/src/components/chat/TaskPanelModal.test.tsx +0 -198
  22. package/src/components/chat/TaskProcessPanel.test.tsx +0 -111
  23. package/src/components/chat/TurnProcessGroup.test.tsx +0 -161
  24. package/src/components/chat/elapsed.test.ts +0 -42
  25. package/src/components/chat/executed-actions-model.test.ts +0 -134
  26. package/src/components/chat/orchestration-children-model.test.ts +0 -115
  27. package/src/components/chat/turn-timeline.test.ts +0 -97
  28. package/src/components/chat/useChatTasks.test.ts +0 -136
  29. package/src/components/settings/AgentsSettingsPanel.test.tsx +0 -229
  30. package/src/components/settings/InsightsSettingsPanel.test.tsx +0 -132
  31. package/src/components/settings/LlmSettingsPanel.test.tsx +0 -461
  32. package/src/components/settings/ModelIdCombobox.test.tsx +0 -88
  33. package/src/components/settings/SecuritySettingsPanel.test.tsx +0 -122
  34. package/src/components/settings/SkillsSettingsPanel.test.tsx +0 -62
  35. package/src/components/settings/WebSearchSettingsPanel.test.tsx +0 -90
  36. package/src/components/settings/compat-flags-model.test.ts +0 -71
  37. package/src/components/settings/llm-vendors.test.ts +0 -171
  38. package/src/components/settings/model-capabilities.test.ts +0 -88
  39. package/src/components/settings/preset-choice-model.test.ts +0 -96
  40. package/src/hooks/useChatsAndAgents.test.ts +0 -165
  41. package/src/lib/attachments.test.ts +0 -127
  42. package/src/lib/chat-title.test.ts +0 -53
  43. package/src/lib/chat-transport.test.ts +0 -277
  44. package/src/lib/date-groups.test.ts +0 -62
  45. package/src/lib/demo-flag.test.ts +0 -17
  46. package/src/lib/electron-bridge.test.ts +0 -54
  47. package/src/lib/exec-policy.test.ts +0 -71
  48. package/src/lib/http-bridge.test.ts +0 -246
  49. package/src/lib/insights.test.ts +0 -56
  50. package/src/lib/local-api.test.ts +0 -353
  51. package/src/lib/pending-first-message.test.ts +0 -17
  52. package/src/lib/slash-sources.test.ts +0 -161
  53. package/src/pages/AgentPage.test.tsx +0 -627
  54. package/src/pages/SettingsPage.test.tsx +0 -102
  55. package/vite.base.ts +0 -94
@@ -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,165 +0,0 @@
1
- /**
2
- * useChatsAndAgents:侧栏状态钩子(分页会话列表 + 智能体目录 + 新建默认选中)。
3
- * 它是 useChatList 的 Electron 适配层,这里锁定适配层自己的契约:
4
- * - 非 Electron 环境完全不发请求(skipInitialLoad + 传输层短路),
5
- * create/delete 软失败并给出错误文案;
6
- * - Electron 下首屏拉 page 1 与目录,目录到达后默认选中 local-assistant;
7
- * - createChat 成功返回 id、失败返回 null 且错误上浮为字符串;
8
- * - deleteChat 成功后重置回 page 1 重新拉取;
9
- * - patchChatTitle 只改内存、不打后端(SSE 标题推送的配套路径)。
10
- * 桥走真实的 window.electron 路径,不 mock 模块。
11
- */
12
- import { act, renderHook, waitFor } from '@testing-library/react';
13
- import { afterEach, describe, expect, it, vi } from 'vitest';
14
- import { useChatsAndAgents } from './useChatsAndAgents';
15
-
16
- type RequestInput = { method: string; path: string; body?: unknown };
17
-
18
- const PAGE_1 = {
19
- chats: [
20
- { id: 'c1', title: '会话一' },
21
- { id: 'c2', title: '会话二' },
22
- ],
23
- pagination: { page: 1, limit: 50, total: 3, totalPages: 2, hasMore: true },
24
- };
25
- const PAGE_2 = {
26
- chats: [{ id: 'c3', title: '会话三' }],
27
- pagination: { page: 2, limit: 50, total: 3, totalPages: 2, hasMore: false },
28
- };
29
- const AGENTS = [{ id: 'agent-x' }, { id: 'local-assistant' }];
30
-
31
- /** 装一个按路径路由的 request 替身,返回调用记录便于断言。 */
32
- function installBridge(overrides: {
33
- onCreate?: () => Promise<unknown>;
34
- onDelete?: () => Promise<unknown>;
35
- } = {}) {
36
- const calls: RequestInput[] = [];
37
- const request = vi.fn((input: RequestInput): Promise<unknown> => {
38
- calls.push(input);
39
- if (input.path.startsWith('/api/v2/chats?')) {
40
- return Promise.resolve(input.path.includes('page=2') ? PAGE_2 : PAGE_1);
41
- }
42
- if (input.path === '/api/v2/chat-agents') return Promise.resolve({ agents: AGENTS });
43
- if (input.path === '/api/v2/chats/new') {
44
- return overrides.onCreate ? overrides.onCreate() : Promise.resolve({ chatId: 'c-new' });
45
- }
46
- if (input.method === 'DELETE' && input.path.startsWith('/api/v2/chats/')) {
47
- return overrides.onDelete
48
- ? overrides.onDelete()
49
- : Promise.resolve({ success: true, message: '', chatId: 'c1' });
50
- }
51
- return Promise.reject(new Error(`unexpected ${input.method} ${input.path}`));
52
- });
53
- (window as { electron?: unknown }).electron = { localBackend: { request } };
54
- return { calls, request };
55
- }
56
-
57
- afterEach(() => {
58
- delete (window as { electron?: unknown }).electron;
59
- });
60
-
61
- describe('非 Electron 环境', () => {
62
- it('不发任何请求,列表为空', async () => {
63
- const { result } = renderHook(() => useChatsAndAgents());
64
- await waitFor(() => expect(result.current.isLoading).toBe(false));
65
- expect(result.current.chats).toEqual([]);
66
- expect(result.current.agents).toEqual([]);
67
- expect(result.current.error).toBeNull();
68
- });
69
-
70
- it('createChat / deleteChat 软失败并上浮错误', async () => {
71
- const { result } = renderHook(() => useChatsAndAgents());
72
- let created: string | null = 'unset';
73
- await act(async () => {
74
- created = await result.current.createChat();
75
- });
76
- expect(created).toBeNull();
77
- expect(result.current.error).toBe('not in electron');
78
- let deleted = true;
79
- await act(async () => {
80
- deleted = await result.current.deleteChat('c1');
81
- });
82
- expect(deleted).toBe(false);
83
- });
84
- });
85
-
86
- describe('Electron 环境', () => {
87
- it('首屏拉取会话 page 1 与智能体目录,默认选中 local-assistant', async () => {
88
- installBridge();
89
- const { result } = renderHook(() => useChatsAndAgents());
90
- await waitFor(() => expect(result.current.chats).toHaveLength(2));
91
- expect(result.current.chats.map((c) => c.id)).toEqual(['c1', 'c2']);
92
- expect(result.current.hasMoreChats).toBe(true);
93
- await waitFor(() => expect(result.current.selectedAgentId).toBe('local-assistant'));
94
- });
95
-
96
- it('loadMoreChats 追加 page 2,hasMore 随响应翻转为 false', async () => {
97
- installBridge();
98
- const { result } = renderHook(() => useChatsAndAgents());
99
- await waitFor(() => expect(result.current.chats).toHaveLength(2));
100
- await act(async () => {
101
- await result.current.loadMoreChats();
102
- });
103
- expect(result.current.chats.map((c) => c.id)).toEqual(['c1', 'c2', 'c3']);
104
- expect(result.current.hasMoreChats).toBe(false);
105
- });
106
-
107
- it('createChat 成功返回新 id', async () => {
108
- installBridge();
109
- const { result } = renderHook(() => useChatsAndAgents());
110
- await waitFor(() => expect(result.current.isLoading).toBe(false));
111
- let id: string | null = null;
112
- await act(async () => {
113
- id = await result.current.createChat({ agentId: 'local-assistant' });
114
- });
115
- expect(id).toBe('c-new');
116
- expect(result.current.error).toBeNull();
117
- });
118
-
119
- it('createChat 失败返回 null 且错误上浮为字符串', async () => {
120
- installBridge({ onCreate: () => Promise.reject(new Error('创建失败')) });
121
- const { result } = renderHook(() => useChatsAndAgents());
122
- await waitFor(() => expect(result.current.isLoading).toBe(false));
123
- let id: string | null = 'unset';
124
- await act(async () => {
125
- id = await result.current.createChat();
126
- });
127
- expect(id).toBeNull();
128
- expect(result.current.error).toBe('创建失败');
129
- });
130
-
131
- it('deleteChat 成功后重置回 page 1 重新拉取', async () => {
132
- const { calls } = installBridge();
133
- const { result } = renderHook(() => useChatsAndAgents());
134
- await waitFor(() => expect(result.current.chats).toHaveLength(2));
135
- await act(async () => {
136
- await result.current.loadMoreChats();
137
- });
138
- expect(result.current.chats).toHaveLength(3);
139
- let ok = false;
140
- await act(async () => {
141
- ok = await result.current.deleteChat('c3');
142
- });
143
- expect(ok).toBe(true);
144
- expect(calls.some((c) => c.method === 'DELETE' && c.path === '/api/v2/chats/c3')).toBe(true);
145
- // 删除后 refresh 重置分页:重新拉了 page 1,列表回到两条。
146
- await waitFor(() => expect(result.current.chats.map((c) => c.id)).toEqual(['c1', 'c2']));
147
- });
148
-
149
- it('patchChatTitle 只改内存,不发请求', async () => {
150
- const { calls } = installBridge();
151
- const { result } = renderHook(() => useChatsAndAgents());
152
- await waitFor(() => expect(result.current.chats).toHaveLength(2));
153
- const before = calls.length;
154
- act(() => {
155
- result.current.patchChatTitle('c1', '新标题');
156
- });
157
- expect(result.current.chats[0].title).toBe('新标题');
158
- expect(calls.length).toBe(before);
159
- // 找不到的 id 静默忽略。
160
- act(() => {
161
- result.current.patchChatTitle('nope', '无');
162
- });
163
- expect(result.current.chats).toHaveLength(2);
164
- });
165
- });
@@ -1,127 +0,0 @@
1
- /**
2
- * attachments:会话附件的 renderer 侧助手。
3
- * 锁定:图像扩展名判定(大小写不敏感、无扩展名不算)、File→base64 的
4
- * 分块编码(跨块边界与空文件)、saveChatAttachments 的降级阶梯——
5
- * 无 chatId / 无桥 / 桥无 attachments 能力时原样返回;逐文件失败只退回
6
- * 该文件,整体失败退回整批(用户的选择不丢)。
7
- * 桥走真实的 window.electron 路径,不 mock 模块。
8
- */
9
- import { afterEach, describe, expect, it, vi } from 'vitest';
10
- import {
11
- fileToBase64,
12
- isImageFile,
13
- saveChatAttachments,
14
- type AttachmentFile,
15
- } from './attachments';
16
-
17
- afterEach(() => {
18
- delete (window as { electron?: unknown }).electron;
19
- vi.restoreAllMocks();
20
- });
21
-
22
- function installBridge(save?: unknown) {
23
- (window as { electron?: unknown }).electron = {
24
- localBackend: { request: vi.fn() },
25
- ...(save ? { attachments: { save } } : {}),
26
- };
27
- }
28
-
29
- describe('isImageFile', () => {
30
- it('常见图像扩展名命中,大小写不敏感', () => {
31
- for (const p of ['a.png', 'b.JPG', 'c.jpeg', 'd.gif', 'e.webp', 'f.bmp']) {
32
- expect(isImageFile(p)).toBe(true);
33
- }
34
- });
35
-
36
- it('非图像与无扩展名不命中', () => {
37
- for (const p of ['a.txt', 'archive.tar.gz', 'noext', 'dot.']) {
38
- expect(isImageFile(p)).toBe(false);
39
- }
40
- });
41
- });
42
-
43
- describe('fileToBase64', () => {
44
- it('小文件逐字节编码', async () => {
45
- const file = new File([new Uint8Array([1, 2, 3, 255])], 'x.bin');
46
- expect(await fileToBase64(file)).toBe('AQID/w==');
47
- });
48
-
49
- it('空文件编码为空串', async () => {
50
- expect(await fileToBase64(new File([], 'empty'))).toBe('');
51
- });
52
-
53
- it('跨 0x8000 分块边界的编码与一次性编码一致', async () => {
54
- const bytes = new Uint8Array(0x8000 + 100).fill(0x61);
55
- const file = new File([bytes], 'big.bin');
56
- const expected = Buffer.from(bytes).toString('base64');
57
- expect(await fileToBase64(file)).toBe(expected);
58
- });
59
- });
60
-
61
- describe('saveChatAttachments 降级阶梯', () => {
62
- const files: AttachmentFile[] = [{ name: 'a.txt', path: '/src/a.txt' }];
63
-
64
- it('无 chatId / 空列表时原样返回(同一引用)', async () => {
65
- expect(await saveChatAttachments(null, files)).toBe(files);
66
- expect(await saveChatAttachments('', files)).toBe(files);
67
- expect(await saveChatAttachments('chat-1', [])).toEqual([]);
68
- });
69
-
70
- it('无桥或桥无 attachments 能力时原样返回', async () => {
71
- expect(await saveChatAttachments('chat-1', files)).toBe(files);
72
- installBridge();
73
- expect(await saveChatAttachments('chat-1', files)).toBe(files);
74
- });
75
-
76
- it('有路径走 path 拷贝,无路径读字节走 data 上传', async () => {
77
- const save = vi.fn().mockResolvedValue({
78
- files: [
79
- { name: 'a.txt', path: '/stored/a.txt', size: 3 },
80
- { name: 'b.txt', path: '/stored/b.txt', size: 2 },
81
- ],
82
- });
83
- installBridge(save);
84
- const input: AttachmentFile[] = [
85
- { name: 'a.txt', path: '/src/a.txt' },
86
- { name: 'b.txt', path: '', file: new File([new Uint8Array([72, 105])], 'b.txt') },
87
- ];
88
- const result = await saveChatAttachments('chat-1', input);
89
- expect(save).toHaveBeenCalledWith({
90
- chatId: 'chat-1',
91
- files: [
92
- { name: 'a.txt', path: '/src/a.txt' },
93
- { name: 'b.txt', data: 'SGk=' },
94
- ],
95
- });
96
- expect(result).toEqual([
97
- { name: 'a.txt', path: '/stored/a.txt' },
98
- { name: 'b.txt', path: '/stored/b.txt' },
99
- ]);
100
- });
101
-
102
- it('逐文件失败只退回该文件,其余仍用落盘路径', async () => {
103
- const save = vi.fn().mockResolvedValue({
104
- files: [
105
- { name: 'a.txt', path: '/stored/a.txt', size: 3 },
106
- { name: 'b.txt', path: '', error: 'too big' },
107
- ],
108
- });
109
- installBridge(save);
110
- const input: AttachmentFile[] = [
111
- { name: 'a.txt', path: '/src/a.txt' },
112
- { name: 'b.txt', path: '/src/b.txt' },
113
- ];
114
- const result = await saveChatAttachments('chat-1', input);
115
- expect(result[0]).toEqual({ name: 'a.txt', path: '/stored/a.txt' });
116
- expect(result[1]).toBe(input[1]);
117
- });
118
-
119
- it('整体保存失败时 warn 并退回整批原始文件', async () => {
120
- const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
121
- const save = vi.fn().mockRejectedValue(new Error('disk full'));
122
- installBridge(save);
123
- const result = await saveChatAttachments('chat-1', files);
124
- expect(result).toBe(files);
125
- expect(warn).toHaveBeenCalledOnce();
126
- });
127
- });
@@ -1,53 +0,0 @@
1
- /**
2
- * chat-title 的 `[自动化] ` 前缀解析:本地后端与云端后端共用同一前缀约定,
3
- * 解析器必须同时接受「带空格前缀」与「裸括号」两种写法,且只有裸括号写法
4
- * 会 trimStart——带空格分支保留剩余前导空白(此处逐字锁定这一不对称,
5
- * 改动它属于行为变更)。
6
- */
7
- import { describe, expect, it } from 'vitest';
8
- import { AUTOMATION_TITLE_PREFIX, parseChatTitle } from './chat-title';
9
-
10
- describe('parseChatTitle', () => {
11
- it('null / undefined / 空串 → 空标题且非自动化', () => {
12
- for (const raw of [null, undefined, '']) {
13
- expect(parseChatTitle(raw)).toEqual({ displayTitle: '', isAutomation: false });
14
- }
15
- });
16
-
17
- it('带空格前缀 → 去掉前缀、标记自动化', () => {
18
- expect(parseChatTitle(`${AUTOMATION_TITLE_PREFIX}每日报表`)).toEqual({
19
- displayTitle: '每日报表',
20
- isAutomation: true,
21
- });
22
- });
23
-
24
- it('裸括号(无空格)→ 去掉括号并 trimStart', () => {
25
- expect(parseChatTitle('[自动化]每日报表')).toEqual({
26
- displayTitle: '每日报表',
27
- isAutomation: true,
28
- });
29
- expect(parseChatTitle('[自动化] 每日报表')).toEqual({
30
- displayTitle: ' 每日报表',
31
- isAutomation: true,
32
- });
33
- });
34
-
35
- it('仅剩前缀本身 → 空标题但仍标记自动化', () => {
36
- expect(parseChatTitle('[自动化] ')).toEqual({ displayTitle: '', isAutomation: true });
37
- expect(parseChatTitle('[自动化]')).toEqual({ displayTitle: '', isAutomation: true });
38
- });
39
-
40
- it('普通标题原样透传', () => {
41
- expect(parseChatTitle('聊聊架构')).toEqual({
42
- displayTitle: '聊聊架构',
43
- isAutomation: false,
44
- });
45
- });
46
-
47
- it('前缀出现在中段不算自动化', () => {
48
- expect(parseChatTitle('笔记:[自动化] 方案')).toEqual({
49
- displayTitle: '笔记:[自动化] 方案',
50
- isAutomation: false,
51
- });
52
- });
53
- });