@steerable/agent-shell-web 0.6.15
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.
- package/LICENSE +91 -0
- package/README.md +20 -0
- package/package.json +69 -0
- package/public/favicon.ico +0 -0
- package/public/favicon.png +0 -0
- package/src/AppShell.tsx +24 -0
- package/src/assets/logo-generic.png +0 -0
- package/src/brand.test.ts +22 -0
- package/src/brand.ts +48 -0
- package/src/components/AgentSidebar.test.tsx +127 -0
- package/src/components/AgentSidebar.tsx +1010 -0
- package/src/components/ChatHeader.test.tsx +230 -0
- package/src/components/ChatHeader.tsx +436 -0
- package/src/components/DockPanelHeader.tsx +69 -0
- package/src/components/LocalLlmSettingsModal.test.tsx +372 -0
- package/src/components/LocalLlmSettingsModal.tsx +72 -0
- package/src/components/TerminalPanel.tsx +68 -0
- package/src/components/TerminalView.tsx +228 -0
- package/src/components/chat/ApprovalModal.test.tsx +83 -0
- package/src/components/chat/ApprovalModal.tsx +229 -0
- package/src/components/chat/AskUserModalHost.test.tsx +176 -0
- package/src/components/chat/AskUserModalHost.tsx +61 -0
- package/src/components/chat/AskUserQuestionMenu.tsx +621 -0
- package/src/components/chat/AssistantMessage.test.tsx +68 -0
- package/src/components/chat/AssistantMessage.tsx +469 -0
- package/src/components/chat/ChatInput.test.tsx +609 -0
- package/src/components/chat/ChatInput.tsx +2068 -0
- package/src/components/chat/ChatProjectBadge.tsx +362 -0
- package/src/components/chat/EmptyChat.tsx +18 -0
- package/src/components/chat/ExecPolicyPicker.test.tsx +26 -0
- package/src/components/chat/ExecPolicyPicker.tsx +120 -0
- package/src/components/chat/ExecutedActionsCard.tsx +216 -0
- package/src/components/chat/InterruptedTurnCard.tsx +53 -0
- package/src/components/chat/LocalChatPanel.tsx +408 -0
- package/src/components/chat/Markdown.test.tsx +170 -0
- package/src/components/chat/Markdown.tsx +432 -0
- package/src/components/chat/MessageList.tsx +332 -0
- package/src/components/chat/ModelPicker.test.tsx +149 -0
- package/src/components/chat/ModelPicker.tsx +289 -0
- package/src/components/chat/OrchestrationChildrenCard.tsx +40 -0
- package/src/components/chat/PlanTodoList.tsx +121 -0
- package/src/components/chat/ProjectTrustBanner.tsx +91 -0
- package/src/components/chat/SessionTreeModal.test.tsx +161 -0
- package/src/components/chat/SessionTreeModal.tsx +221 -0
- package/src/components/chat/StreamingStatus.tsx +95 -0
- package/src/components/chat/TaskOutcomeCards.test.tsx +97 -0
- package/src/components/chat/TaskOutcomeCards.tsx +128 -0
- package/src/components/chat/TaskPanelModal.test.tsx +198 -0
- package/src/components/chat/TaskPanelModal.tsx +310 -0
- package/src/components/chat/TaskProcessPanel.test.tsx +111 -0
- package/src/components/chat/TaskProcessPanel.tsx +208 -0
- package/src/components/chat/TurnProcessGroup.test.tsx +161 -0
- package/src/components/chat/TurnProcessGroup.tsx +193 -0
- package/src/components/chat/UnifiedDiff.tsx +31 -0
- package/src/components/chat/UserMessage.tsx +155 -0
- package/src/components/chat/elapsed.test.ts +42 -0
- package/src/components/chat/elapsed.ts +45 -0
- package/src/components/chat/executed-actions-model.test.ts +134 -0
- package/src/components/chat/executed-actions-model.ts +143 -0
- package/src/components/chat/orchestration-children-model.test.ts +115 -0
- package/src/components/chat/orchestration-children-model.ts +104 -0
- package/src/components/chat/timestamp.ts +24 -0
- package/src/components/chat/turn-timeline.test.ts +97 -0
- package/src/components/chat/turn-timeline.ts +132 -0
- package/src/components/chat/useChatTasks.test.ts +136 -0
- package/src/components/chat/useChatTasks.ts +134 -0
- package/src/components/chat/useCopy.ts +62 -0
- package/src/components/settings/AgentCapabilityFields.tsx +343 -0
- package/src/components/settings/AgentsSettingsPanel.test.tsx +229 -0
- package/src/components/settings/AgentsSettingsPanel.tsx +412 -0
- package/src/components/settings/DiagnoseSettingsPanel.tsx +119 -0
- package/src/components/settings/InsightsSettingsPanel.test.tsx +132 -0
- package/src/components/settings/InsightsSettingsPanel.tsx +309 -0
- package/src/components/settings/LlmSettingsPanel.tsx +875 -0
- package/src/components/settings/McpSettingsPanel.tsx +466 -0
- package/src/components/settings/ModelCapabilityChips.tsx +46 -0
- package/src/components/settings/ModelIdCombobox.test.tsx +88 -0
- package/src/components/settings/ModelIdCombobox.tsx +185 -0
- package/src/components/settings/SecuritySettingsPanel.test.tsx +122 -0
- package/src/components/settings/SecuritySettingsPanel.tsx +169 -0
- package/src/components/settings/SettingsSaveButton.tsx +35 -0
- package/src/components/settings/SkillsSettingsPanel.test.tsx +62 -0
- package/src/components/settings/SkillsSettingsPanel.tsx +334 -0
- package/src/components/settings/TelemetrySettingsPanel.tsx +173 -0
- package/src/components/settings/UsagePanel.tsx +196 -0
- package/src/components/settings/WebSearchSettingsPanel.test.tsx +90 -0
- package/src/components/settings/WebSearchSettingsPanel.tsx +183 -0
- package/src/components/settings/compat-flags-model.test.ts +71 -0
- package/src/components/settings/compat-flags-model.ts +57 -0
- package/src/components/settings/llm-vendors.test.ts +171 -0
- package/src/components/settings/llm-vendors.ts +275 -0
- package/src/components/settings/model-capabilities.test.ts +88 -0
- package/src/components/settings/model-capabilities.ts +123 -0
- package/src/components/settings/preset-choice-model.test.ts +96 -0
- package/src/components/settings/preset-choice-model.ts +75 -0
- package/src/fixtures/browser-dev-data.json +315 -0
- package/src/hooks/useChatsAndAgents.ts +182 -0
- package/src/layouts/AgentLayout.tsx +574 -0
- package/src/lib/attachments.ts +78 -0
- package/src/lib/browser-dev-electron-mock.ts +640 -0
- package/src/lib/chat-title.ts +33 -0
- package/src/lib/chat-transport.ts +346 -0
- package/src/lib/date-groups.ts +34 -0
- package/src/lib/demo-flag.ts +15 -0
- package/src/lib/electron-bridge.ts +270 -0
- package/src/lib/exec-policy.ts +25 -0
- package/src/lib/http-bridge.ts +293 -0
- package/src/lib/insights.ts +17 -0
- package/src/lib/local-api.ts +714 -0
- package/src/lib/pending-first-message.test.ts +17 -0
- package/src/lib/pending-first-message.ts +36 -0
- package/src/lib/slash-sources.ts +191 -0
- package/src/main.tsx +103 -0
- package/src/packs/registry.ts +64 -0
- package/src/pages/AgentPage.tsx +1226 -0
- package/src/pages/ChatPanelPreviewPage.tsx +264 -0
- package/src/pages/SettingsPage.test.tsx +102 -0
- package/src/pages/SettingsPage.tsx +197 -0
- package/src/styles/index.css +156 -0
- package/src/vite-env.d.ts +1 -0
- package/vite.base.ts +94 -0
|
@@ -0,0 +1,176 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { getElectronBridge, type AskUserPromptRequest } from '@/lib/electron-bridge';
|
|
3
|
+
import { AskUserQuestionMenu } from './AskUserQuestionMenu';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* AskUserModalHost — W8 结构化提问的桌面 UI 半侧。
|
|
7
|
+
*
|
|
8
|
+
* 挂载一次(AgentLayout),订阅主进程/BS 服务器广播的 `ask-user:request`,
|
|
9
|
+
* 以模态渲染自研的 `<AskUserQuestionMenu />`(分步菜单选择框:模型建议
|
|
10
|
+
* 作为选项、底部追加「其他 / 自定义」输入)。答案经 `ask-user:answer`
|
|
11
|
+
* (Electron IPC / BS HTTP)回到主进程,再应答 sidecar 的反向调用。
|
|
12
|
+
*
|
|
13
|
+
* 队列为 FIFO:ask_user 标记了 concurrency_safe=False,sidecar 串行等待
|
|
14
|
+
* 每个应答。「交给我决定」应答空映射——模型收到「用户未作答」后自行
|
|
15
|
+
* 推进(对齐 HostAskUserHandler 的 fail-open 语义)。
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export function AskUserModalHost() {
|
|
19
|
+
const [queue, setQueue] = useState<AskUserPromptRequest[]>([]);
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
const bridge = getElectronBridge();
|
|
23
|
+
if (!bridge?.askUser) return;
|
|
24
|
+
return bridge.askUser.onRequest((request) => {
|
|
25
|
+
setQueue((prev) => [...prev, request]);
|
|
26
|
+
});
|
|
27
|
+
}, []);
|
|
28
|
+
|
|
29
|
+
const current = queue[0] ?? null;
|
|
30
|
+
if (!current) return null;
|
|
31
|
+
|
|
32
|
+
const answer = (answers: Record<string, string | string[]>) => {
|
|
33
|
+
const bridge = getElectronBridge();
|
|
34
|
+
setQueue((prev) => prev.slice(1));
|
|
35
|
+
void bridge?.askUser?.answer({ requestId: current.requestId, answers });
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div
|
|
40
|
+
className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4"
|
|
41
|
+
role="dialog"
|
|
42
|
+
aria-modal="true"
|
|
43
|
+
aria-label="Agent 提问"
|
|
44
|
+
>
|
|
45
|
+
<div className="max-h-[85vh] w-full max-w-lg overflow-auto rounded-agent-lg">
|
|
46
|
+
<AskUserQuestionMenu
|
|
47
|
+
key={current.requestId}
|
|
48
|
+
intro={current.intro}
|
|
49
|
+
questions={current.questions}
|
|
50
|
+
onSubmit={(answers) => answer(answers)}
|
|
51
|
+
onAutoContinue={() => answer({})}
|
|
52
|
+
bottomHint={
|
|
53
|
+
queue.length > 1 ? `还有 ${queue.length - 1} 组问题待回答` : undefined
|
|
54
|
+
}
|
|
55
|
+
/>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export default AskUserModalHost;
|