@steerable/agent-shell-web 0.6.19 → 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 (52) hide show
  1. package/package.json +15 -8
  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 -791
  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/AskUserQuestionMenu.test.tsx +0 -190
  11. package/src/components/chat/AssistantMessage.test.tsx +0 -68
  12. package/src/components/chat/ChatInput.test.tsx +0 -609
  13. package/src/components/chat/ExecPolicyPicker.test.tsx +0 -26
  14. package/src/components/chat/Markdown.test.tsx +0 -170
  15. package/src/components/chat/ModelPicker.test.tsx +0 -149
  16. package/src/components/chat/SessionTreeModal.test.tsx +0 -161
  17. package/src/components/chat/TaskOutcomeCards.test.tsx +0 -97
  18. package/src/components/chat/TaskPanelModal.test.tsx +0 -198
  19. package/src/components/chat/TaskProcessPanel.test.tsx +0 -111
  20. package/src/components/chat/TurnProcessGroup.test.tsx +0 -161
  21. package/src/components/chat/elapsed.test.ts +0 -42
  22. package/src/components/chat/executed-actions-model.test.ts +0 -134
  23. package/src/components/chat/orchestration-children-model.test.ts +0 -115
  24. package/src/components/chat/turn-timeline.test.ts +0 -97
  25. package/src/components/chat/useChatTasks.test.ts +0 -136
  26. package/src/components/settings/AgentsSettingsPanel.test.tsx +0 -229
  27. package/src/components/settings/InsightsSettingsPanel.test.tsx +0 -132
  28. package/src/components/settings/LlmSettingsPanel.test.tsx +0 -461
  29. package/src/components/settings/ModelIdCombobox.test.tsx +0 -88
  30. package/src/components/settings/SecuritySettingsPanel.test.tsx +0 -122
  31. package/src/components/settings/SkillsSettingsPanel.test.tsx +0 -62
  32. package/src/components/settings/WebSearchSettingsPanel.test.tsx +0 -90
  33. package/src/components/settings/compat-flags-model.test.ts +0 -71
  34. package/src/components/settings/llm-vendors.test.ts +0 -171
  35. package/src/components/settings/model-capabilities.test.ts +0 -88
  36. package/src/components/settings/preset-choice-model.test.ts +0 -96
  37. package/src/hooks/useChatsAndAgents.test.ts +0 -165
  38. package/src/lib/attachments.test.ts +0 -127
  39. package/src/lib/chat-title.test.ts +0 -53
  40. package/src/lib/chat-transport.test.ts +0 -282
  41. package/src/lib/date-groups.test.ts +0 -62
  42. package/src/lib/demo-flag.test.ts +0 -17
  43. package/src/lib/electron-bridge.test.ts +0 -54
  44. package/src/lib/exec-policy.test.ts +0 -71
  45. package/src/lib/http-bridge.test.ts +0 -283
  46. package/src/lib/insights.test.ts +0 -56
  47. package/src/lib/local-api.test.ts +0 -353
  48. package/src/lib/pending-first-message.test.ts +0 -17
  49. package/src/lib/slash-sources.test.ts +0 -161
  50. package/src/pages/AgentPage.test.tsx +0 -627
  51. package/src/pages/SettingsPage.test.tsx +0 -102
  52. package/vite.base.ts +0 -94
@@ -1,176 +0,0 @@
1
- /**
2
- * AskUserModalHost 的接线契约(W8):
3
- * - `ask-user:request` 广播到达时渲染分步菜单卡片(intro + 问题菜单);
4
- * - 单选点击/Enter 直接提交并进入下一题;文本题 Enter 进入下一题;
5
- * - 「其他 / 自定义」展开输入框,输入直接作为该题答案(不产生新问题);
6
- * - 提交把 requestId + 答案映射经 bridge.askUser.answer 送回主进程;
7
- * - 「交给我决定」应答空映射(fail-open:模型自行推进);
8
- * - 应答后卡片关闭,队列中的下一组问题顶上来。
9
- */
10
- import { act, cleanup, fireEvent, render, screen } from '@testing-library/react';
11
- import { afterEach, describe, expect, it, vi } from 'vitest';
12
- import type { AskUserPromptRequest } from '@/lib/electron-bridge';
13
- import { AskUserModalHost } from './AskUserModalHost';
14
-
15
- afterEach(() => {
16
- cleanup();
17
- delete (window as { electron?: unknown }).electron;
18
- });
19
-
20
- function installBridge() {
21
- const listeners = new Set<(request: AskUserPromptRequest) => void>();
22
- const answer = vi.fn().mockResolvedValue(undefined);
23
- (window as { electron?: unknown }).electron = {
24
- askUser: {
25
- onRequest: (callback: (request: AskUserPromptRequest) => void) => {
26
- listeners.add(callback);
27
- return () => listeners.delete(callback);
28
- },
29
- answer,
30
- },
31
- };
32
- return {
33
- answer,
34
- emit: (request: AskUserPromptRequest) => {
35
- for (const callback of Array.from(listeners)) callback(request);
36
- },
37
- };
38
- }
39
-
40
- const REQUEST: AskUserPromptRequest = {
41
- requestId: 'req-1',
42
- intro: '部署前确认',
43
- questions: [
44
- { id: 'env', text: '目标环境?', type: 'select', options: ['staging', 'prod'], multiSelect: false },
45
- ],
46
- };
47
-
48
- const REQUEST_TWO: AskUserPromptRequest = {
49
- requestId: 'req-two',
50
- intro: '部署前确认',
51
- questions: [
52
- { id: 'env', text: '目标环境?', type: 'select', options: ['staging', 'prod'] },
53
- { id: 'note', text: '备注', type: 'text' },
54
- ],
55
- };
56
-
57
- describe('AskUserModalHost(W8)', () => {
58
- it('无请求时不渲染;请求到达后渲染问题卡片', () => {
59
- const bridge = installBridge();
60
- render(<AskUserModalHost />);
61
- expect(screen.queryByRole('dialog')).toBeNull();
62
-
63
- act(() => bridge.emit(REQUEST));
64
- expect(screen.getByRole('dialog', { name: 'Agent 提问' })).toBeTruthy();
65
- expect(screen.getByText('部署前确认')).toBeTruthy();
66
- expect(screen.getByText(/目标环境?/)).toBeTruthy();
67
- });
68
-
69
- it('单选点击选项即提交:answer 收到 requestId 与答案映射,卡片关闭', () => {
70
- const bridge = installBridge();
71
- render(<AskUserModalHost />);
72
- act(() => bridge.emit(REQUEST));
73
-
74
- fireEvent.click(screen.getByRole('radio', { name: /staging/ }));
75
-
76
- expect(bridge.answer).toHaveBeenCalledWith({
77
- requestId: 'req-1',
78
- answers: { env: 'staging' },
79
- });
80
- expect(screen.queryByRole('dialog')).toBeNull();
81
- });
82
-
83
- it('「交给我决定」应答空映射', () => {
84
- const bridge = installBridge();
85
- render(<AskUserModalHost />);
86
- act(() => bridge.emit(REQUEST));
87
-
88
- fireEvent.click(screen.getByRole('button', { name: /交给我决定/ }));
89
- expect(bridge.answer).toHaveBeenCalledWith({ requestId: 'req-1', answers: {} });
90
- });
91
-
92
- it('FIFO:应答第一组后队列中的第二组顶上来', () => {
93
- const bridge = installBridge();
94
- render(<AskUserModalHost />);
95
- act(() => {
96
- bridge.emit(REQUEST);
97
- bridge.emit({ ...REQUEST, requestId: 'req-2', intro: '第二组问题' });
98
- });
99
- expect(screen.getByText('部署前确认')).toBeTruthy();
100
-
101
- fireEvent.click(screen.getByRole('button', { name: /交给我决定/ }));
102
- expect(screen.getByText('第二组问题')).toBeTruthy();
103
- expect(bridge.answer).toHaveBeenCalledTimes(1);
104
- });
105
-
106
- it('bridge 没有 askUser 能力时不渲染也不报错', () => {
107
- (window as { electron?: unknown }).electron = {};
108
- render(<AskUserModalHost />);
109
- expect(screen.queryByRole('dialog')).toBeNull();
110
- });
111
- });
112
-
113
- describe('AskUserModalHost 分步菜单(一次一题)', () => {
114
- it('第一题选择后进入第二题,文本输入后一次性提交全部答案', () => {
115
- const bridge = installBridge();
116
- render(<AskUserModalHost />);
117
- act(() => bridge.emit(REQUEST_TWO));
118
-
119
- expect(screen.getByText(/问题 1 \/ 2/)).toBeTruthy();
120
- expect(screen.getByText(/目标环境?/)).toBeTruthy();
121
-
122
- fireEvent.click(screen.getByRole('radio', { name: /staging/ }));
123
-
124
- // 不提交,而是进入同一请求的下一题
125
- expect(bridge.answer).not.toHaveBeenCalled();
126
- expect(screen.getByText(/问题 2 \/ 2/)).toBeTruthy();
127
- expect(screen.getByText('备注')).toBeTruthy();
128
-
129
- const input = screen.getByPlaceholderText('输入你的回答...');
130
- fireEvent.change(input, { target: { value: '周三窗口' } });
131
- fireEvent.keyDown(input, { key: 'Enter' });
132
-
133
- expect(bridge.answer).toHaveBeenCalledWith({
134
- requestId: 'req-two',
135
- answers: { env: 'staging', note: '周三窗口' },
136
- });
137
- expect(screen.queryByRole('dialog')).toBeNull();
138
- });
139
-
140
- it('「其他 / 自定义」输入直接作为该题答案,不会生成新的问题', () => {
141
- const bridge = installBridge();
142
- render(<AskUserModalHost />);
143
- act(() => bridge.emit(REQUEST));
144
-
145
- fireEvent.click(screen.getByRole('radio', { name: /其他/ }));
146
- const input = screen.getByPlaceholderText('输入你的回答...');
147
- fireEvent.change(input, { target: { value: '蓝绿发布' } });
148
- fireEvent.keyDown(input, { key: 'Enter' });
149
-
150
- expect(bridge.answer).toHaveBeenCalledWith({
151
- requestId: 'req-1',
152
- answers: { env: '蓝绿发布' },
153
- });
154
- expect(screen.queryByRole('dialog')).toBeNull();
155
- });
156
-
157
- it('↑↓ 在菜单项间移动焦点,Enter 选中并提交', () => {
158
- const bridge = installBridge();
159
- render(<AskUserModalHost />);
160
- act(() => bridge.emit(REQUEST));
161
-
162
- const staging = screen.getByRole('radio', { name: /staging/ });
163
- const prod = screen.getByRole('radio', { name: /prod/ });
164
- const group = screen.getByRole('radiogroup');
165
-
166
- act(() => staging.focus());
167
- fireEvent.keyDown(group, { key: 'ArrowDown' });
168
- expect(document.activeElement).toBe(prod);
169
-
170
- fireEvent.keyDown(prod, { key: 'Enter' });
171
- expect(bridge.answer).toHaveBeenCalledWith({
172
- requestId: 'req-1',
173
- answers: { env: 'prod' },
174
- });
175
- });
176
- });
@@ -1,190 +0,0 @@
1
- /**
2
- * AskUserQuestionMenu 分步提问卡片契约:
3
- * - 一次一题 + 进度显示;单选点击即提交并推进,多选勾选后按按钮提交;
4
- * - 「其他 / 自定义」把用户输入直接作为该题答案(不创建新问题);
5
- * - 提交映射保持 { [questionId]: string | string[] } 契约;
6
- * - 上一题回退保留已答;空问题列表直接提交空映射;
7
- * - text/password 题型的输入与 Enter 提交。
8
- */
9
- import { cleanup, fireEvent, render, screen } from '@testing-library/react';
10
- import { afterEach, describe, expect, it, vi } from 'vitest';
11
- import { AskUserQuestionMenu } from './AskUserQuestionMenu';
12
-
13
- afterEach(cleanup);
14
-
15
- const TWO_SELECTS = [
16
- {
17
- id: 'q1',
18
- text: '选一个问题域',
19
- options: [{ label: '甲方案' }, { label: '乙方案', description: '带说明' }],
20
- },
21
- {
22
- id: 'q2',
23
- text: '选一个范围',
24
- options: ['仅本地', '全部'],
25
- },
26
- ];
27
-
28
- describe('AskUserQuestionMenu · 分步与提交契约', () => {
29
- it('空问题列表:不渲染卡片,直接提交空映射', () => {
30
- const onSubmit = vi.fn();
31
- const { container } = render(<AskUserQuestionMenu intro="" questions={[]} onSubmit={onSubmit} />);
32
- expect(container.firstChild).toBeNull();
33
- expect(onSubmit).toHaveBeenCalledWith({});
34
- });
35
-
36
- it('单选:点击即提交并推进到下一题,进度从 1/2 变 2/2', () => {
37
- const onSubmit = vi.fn();
38
- render(<AskUserQuestionMenu intro="需要你的输入" questions={TWO_SELECTS} onSubmit={onSubmit} />);
39
-
40
- expect(screen.getByText('问题 1 / 2')).toBeTruthy();
41
- expect(screen.getByText('选一个问题域')).toBeTruthy();
42
- // 上一题在第一题不出现
43
- expect(screen.queryByText('上一题')).toBeNull();
44
-
45
- fireEvent.click(screen.getByText('甲方案'));
46
- expect(screen.getByText('问题 2 / 2')).toBeTruthy();
47
- expect(screen.getByText('选一个范围')).toBeTruthy();
48
- // 还没到提交全部的时候
49
- expect(onSubmit).not.toHaveBeenCalled();
50
- });
51
-
52
- it('最后一题提交:answers 映射带齐两题', () => {
53
- const onSubmit = vi.fn();
54
- render(<AskUserQuestionMenu intro="" questions={TWO_SELECTS} onSubmit={onSubmit} />);
55
- fireEvent.click(screen.getByText('乙方案'));
56
- fireEvent.click(screen.getByText('全部'));
57
- expect(onSubmit).toHaveBeenCalledWith({ q1: '乙方案', q2: '全部' });
58
- });
59
-
60
- it('上一题回退:已选答案保留(aria-checked),可改选后重新推进', () => {
61
- const onSubmit = vi.fn();
62
- render(<AskUserQuestionMenu intro="" questions={TWO_SELECTS} onSubmit={onSubmit} />);
63
- fireEvent.click(screen.getByText('甲方案'));
64
- fireEvent.click(screen.getByText('上一题'));
65
-
66
- expect(screen.getByText('问题 1 / 2')).toBeTruthy();
67
- const prev = screen.getByText('甲方案').closest('button');
68
- expect(prev?.getAttribute('aria-checked')).toBe('true');
69
-
70
- fireEvent.click(screen.getByText('乙方案'));
71
- fireEvent.click(screen.getByText('仅本地'));
72
- expect(onSubmit).toHaveBeenCalledWith({ q1: '乙方案', q2: '仅本地' });
73
- });
74
-
75
- it('多选:勾选切换 aria-checked,空选时按钮禁用,提交数组合并', () => {
76
- const onSubmit = vi.fn();
77
- const questions = [
78
- {
79
- id: 'q1',
80
- text: '多选',
81
- multiSelect: true,
82
- options: ['红', '绿', '蓝'],
83
- },
84
- ];
85
- render(<AskUserQuestionMenu intro="" questions={questions} onSubmit={onSubmit} />);
86
-
87
- const submit = screen.getByText('提交').closest('button')!;
88
- expect(submit.disabled).toBe(true);
89
-
90
- const red = screen.getByText('红').closest('button')!;
91
- fireEvent.click(red);
92
- expect(red.getAttribute('aria-checked')).toBe('true');
93
- expect(submit.disabled).toBe(false);
94
-
95
- fireEvent.click(red);
96
- expect(red.getAttribute('aria-checked')).toBe('false');
97
-
98
- fireEvent.click(screen.getByText('红'));
99
- fireEvent.click(screen.getByText('蓝'));
100
- fireEvent.click(submit);
101
- expect(onSubmit).toHaveBeenCalledWith({ q1: ['红', '蓝'] });
102
- });
103
-
104
- it('「其他 / 自定义」:输入文本直接作为该题答案', () => {
105
- const onSubmit = vi.fn();
106
- render(<AskUserQuestionMenu intro="" questions={[TWO_SELECTS[0]]} onSubmit={onSubmit} />);
107
- fireEvent.click(screen.getByText('其他 / 自定义'));
108
- const input = screen.getByPlaceholderText('输入你的回答...');
109
- fireEvent.change(input, { target: { value: '丙方案' } });
110
- fireEvent.click(screen.getByText('提交'));
111
- expect(onSubmit).toHaveBeenCalledWith({ q1: '丙方案' });
112
- });
113
-
114
- it('自定义模式可返回选项列表(答案不丢)', () => {
115
- const onSubmit = vi.fn();
116
- render(<AskUserQuestionMenu intro="" questions={[TWO_SELECTS[0]]} onSubmit={onSubmit} />);
117
- fireEvent.click(screen.getByText('其他 / 自定义'));
118
- fireEvent.click(screen.getByText('← 返回选项'));
119
- expect(screen.getByText('甲方案')).toBeTruthy();
120
- });
121
-
122
- it('text 题型:Enter 提交;空输入按钮禁用', () => {
123
- const onSubmit = vi.fn();
124
- const questions = [{ id: 'q1', text: '叫什么名字', type: 'text', placeholder: '输入名字' }];
125
- render(<AskUserQuestionMenu intro="" questions={questions} onSubmit={onSubmit} />);
126
-
127
- const submit = screen.getByText('提交').closest('button')!;
128
- expect(submit.disabled).toBe(true);
129
-
130
- const input = screen.getByPlaceholderText('输入名字');
131
- fireEvent.change(input, { target: { value: ' 小测 ' } });
132
- fireEvent.keyDown(input, { key: 'Enter' });
133
- // 提交前去空白
134
- expect(onSubmit).toHaveBeenCalledWith({ q1: '小测' });
135
- });
136
-
137
- it('password 题型:input type=password 且角标为密码输入', () => {
138
- const questions = [{ id: 'q1', text: '口令', type: 'password' }];
139
- const { container } = render(
140
- <AskUserQuestionMenu intro="" questions={questions} onSubmit={vi.fn()} />,
141
- );
142
- expect(container.querySelector('input[type="password"]')).not.toBeNull();
143
- expect(screen.getByText('密码输入')).toBeTruthy();
144
- });
145
-
146
- it('「交给我决定」按钮透传 onAutoContinue', () => {
147
- const onAutoContinue = vi.fn();
148
- render(
149
- <AskUserQuestionMenu
150
- intro=""
151
- questions={[TWO_SELECTS[0]]}
152
- onSubmit={vi.fn()}
153
- onAutoContinue={onAutoContinue}
154
- />,
155
- );
156
- fireEvent.click(screen.getByText('交给我决定'));
157
- expect(onAutoContinue).toHaveBeenCalledOnce();
158
- });
159
- });
160
-
161
- describe('AskUserQuestionMenu · 归一化韧性', () => {
162
- it('question 字段别名、字符串/对象选项、坏条目过滤', () => {
163
- const onSubmit = vi.fn();
164
- const questions = [
165
- { id: 'bad-no-text' },
166
- { id: 'q1', question: '用别名的问题', options: ['x'] },
167
- 'not-an-object',
168
- ];
169
- render(<AskUserQuestionMenu intro="" questions={questions as never} onSubmit={onSubmit} />);
170
- expect(screen.getByText('用别名的问题')).toBeTruthy();
171
- expect(screen.getByText('问题 1 / 1')).toBeTruthy();
172
- });
173
-
174
- it('有选项的题强制 select 形态(即便声明了 text)', () => {
175
- const questions = [{ id: 'q1', text: 't', type: 'text', options: ['a'] }];
176
- render(<AskUserQuestionMenu intro="" questions={questions} onSubmit={vi.fn()} />);
177
- expect(screen.getByText('菜单选择')).toBeTruthy();
178
- });
179
-
180
- it('已有答案是选项外的自定义值:进入自定义模式并预填', () => {
181
- const onSubmit = vi.fn();
182
- // 第一题先答自定义值,再回退验证 initialDraft 的自定义恢复
183
- render(<AskUserQuestionMenu intro="" questions={TWO_SELECTS} onSubmit={onSubmit} />);
184
- fireEvent.click(screen.getByText('其他 / 自定义'));
185
- fireEvent.change(screen.getByPlaceholderText('输入你的回答...'), { target: { value: '自定义甲' } });
186
- fireEvent.click(screen.getByText('下一题'));
187
- fireEvent.click(screen.getByText('上一题'));
188
- expect(screen.getByDisplayValue('自定义甲')).toBeTruthy();
189
- });
190
- });
@@ -1,68 +0,0 @@
1
- /**
2
- * AssistantMessage 时间戳行:分享对话的入口从标题栏挪到最近一条助手回复
3
- * 时间戳行上的分享、复制、重新生成共用悬停显隐。
4
- */
5
- import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
6
- import { afterEach, describe, expect, it, vi } from 'vitest';
7
- import type { ChatMessage } from '@steerable/agent-protocol';
8
- import { AssistantMessage } from './AssistantMessage';
9
-
10
- afterEach(cleanup);
11
-
12
- const MESSAGE: ChatMessage = {
13
- id: 'm1',
14
- role: 'assistant',
15
- content: '问好完成。',
16
- createdAt: '2026-09-13T09:03:00.000Z',
17
- };
18
-
19
- describe('AssistantMessage 分享', () => {
20
- it('没有 onShare 时不画分享按钮', () => {
21
- render(
22
- <AssistantMessage
23
- message={MESSAGE}
24
- isStreaming={false}
25
- agents={[]}
26
- currentAgent={null}
27
- />,
28
- );
29
- expect(screen.queryByRole('button', { name: '分享对话截图' })).toBeNull();
30
- });
31
-
32
- it('流式中不画分享按钮', () => {
33
- render(
34
- <AssistantMessage
35
- message={MESSAGE}
36
- isStreaming
37
- agents={[]}
38
- currentAgent={null}
39
- onShare={vi.fn()}
40
- />,
41
- );
42
- expect(screen.queryByRole('button', { name: '分享对话截图' })).toBeNull();
43
- });
44
-
45
- it('点分享会调用 onShare,成功后提示已复制', async () => {
46
- const onShare = vi.fn().mockResolvedValue(true);
47
- render(
48
- <AssistantMessage
49
- message={MESSAGE}
50
- isStreaming={false}
51
- agents={[]}
52
- currentAgent={null}
53
- onShare={onShare}
54
- />,
55
- );
56
-
57
- fireEvent.click(screen.getByRole('button', { name: '分享对话截图' }));
58
- expect(screen.getByRole('button', { name: '分享对话截图' }).className).toContain(
59
- 'group-hover/message:opacity-100',
60
- );
61
- await waitFor(() => expect(onShare).toHaveBeenCalledTimes(1));
62
- await waitFor(() =>
63
- expect(screen.getByRole('button', { name: '分享对话截图' }).getAttribute('title')).toBe(
64
- '截图已复制到剪贴板',
65
- ),
66
- );
67
- });
68
- });