@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.
Files changed (121) hide show
  1. package/LICENSE +91 -0
  2. package/README.md +20 -0
  3. package/package.json +69 -0
  4. package/public/favicon.ico +0 -0
  5. package/public/favicon.png +0 -0
  6. package/src/AppShell.tsx +24 -0
  7. package/src/assets/logo-generic.png +0 -0
  8. package/src/brand.test.ts +22 -0
  9. package/src/brand.ts +48 -0
  10. package/src/components/AgentSidebar.test.tsx +127 -0
  11. package/src/components/AgentSidebar.tsx +1010 -0
  12. package/src/components/ChatHeader.test.tsx +230 -0
  13. package/src/components/ChatHeader.tsx +436 -0
  14. package/src/components/DockPanelHeader.tsx +69 -0
  15. package/src/components/LocalLlmSettingsModal.test.tsx +372 -0
  16. package/src/components/LocalLlmSettingsModal.tsx +72 -0
  17. package/src/components/TerminalPanel.tsx +68 -0
  18. package/src/components/TerminalView.tsx +228 -0
  19. package/src/components/chat/ApprovalModal.test.tsx +83 -0
  20. package/src/components/chat/ApprovalModal.tsx +229 -0
  21. package/src/components/chat/AskUserModalHost.test.tsx +176 -0
  22. package/src/components/chat/AskUserModalHost.tsx +61 -0
  23. package/src/components/chat/AskUserQuestionMenu.tsx +621 -0
  24. package/src/components/chat/AssistantMessage.test.tsx +68 -0
  25. package/src/components/chat/AssistantMessage.tsx +469 -0
  26. package/src/components/chat/ChatInput.test.tsx +609 -0
  27. package/src/components/chat/ChatInput.tsx +2068 -0
  28. package/src/components/chat/ChatProjectBadge.tsx +362 -0
  29. package/src/components/chat/EmptyChat.tsx +18 -0
  30. package/src/components/chat/ExecPolicyPicker.test.tsx +26 -0
  31. package/src/components/chat/ExecPolicyPicker.tsx +120 -0
  32. package/src/components/chat/ExecutedActionsCard.tsx +216 -0
  33. package/src/components/chat/InterruptedTurnCard.tsx +53 -0
  34. package/src/components/chat/LocalChatPanel.tsx +408 -0
  35. package/src/components/chat/Markdown.test.tsx +170 -0
  36. package/src/components/chat/Markdown.tsx +432 -0
  37. package/src/components/chat/MessageList.tsx +332 -0
  38. package/src/components/chat/ModelPicker.test.tsx +149 -0
  39. package/src/components/chat/ModelPicker.tsx +289 -0
  40. package/src/components/chat/OrchestrationChildrenCard.tsx +40 -0
  41. package/src/components/chat/PlanTodoList.tsx +121 -0
  42. package/src/components/chat/ProjectTrustBanner.tsx +91 -0
  43. package/src/components/chat/SessionTreeModal.test.tsx +161 -0
  44. package/src/components/chat/SessionTreeModal.tsx +221 -0
  45. package/src/components/chat/StreamingStatus.tsx +95 -0
  46. package/src/components/chat/TaskOutcomeCards.test.tsx +97 -0
  47. package/src/components/chat/TaskOutcomeCards.tsx +128 -0
  48. package/src/components/chat/TaskPanelModal.test.tsx +198 -0
  49. package/src/components/chat/TaskPanelModal.tsx +310 -0
  50. package/src/components/chat/TaskProcessPanel.test.tsx +111 -0
  51. package/src/components/chat/TaskProcessPanel.tsx +208 -0
  52. package/src/components/chat/TurnProcessGroup.test.tsx +161 -0
  53. package/src/components/chat/TurnProcessGroup.tsx +193 -0
  54. package/src/components/chat/UnifiedDiff.tsx +31 -0
  55. package/src/components/chat/UserMessage.tsx +155 -0
  56. package/src/components/chat/elapsed.test.ts +42 -0
  57. package/src/components/chat/elapsed.ts +45 -0
  58. package/src/components/chat/executed-actions-model.test.ts +134 -0
  59. package/src/components/chat/executed-actions-model.ts +143 -0
  60. package/src/components/chat/orchestration-children-model.test.ts +115 -0
  61. package/src/components/chat/orchestration-children-model.ts +104 -0
  62. package/src/components/chat/timestamp.ts +24 -0
  63. package/src/components/chat/turn-timeline.test.ts +97 -0
  64. package/src/components/chat/turn-timeline.ts +132 -0
  65. package/src/components/chat/useChatTasks.test.ts +136 -0
  66. package/src/components/chat/useChatTasks.ts +134 -0
  67. package/src/components/chat/useCopy.ts +62 -0
  68. package/src/components/settings/AgentCapabilityFields.tsx +343 -0
  69. package/src/components/settings/AgentsSettingsPanel.test.tsx +229 -0
  70. package/src/components/settings/AgentsSettingsPanel.tsx +412 -0
  71. package/src/components/settings/DiagnoseSettingsPanel.tsx +119 -0
  72. package/src/components/settings/InsightsSettingsPanel.test.tsx +132 -0
  73. package/src/components/settings/InsightsSettingsPanel.tsx +309 -0
  74. package/src/components/settings/LlmSettingsPanel.tsx +875 -0
  75. package/src/components/settings/McpSettingsPanel.tsx +466 -0
  76. package/src/components/settings/ModelCapabilityChips.tsx +46 -0
  77. package/src/components/settings/ModelIdCombobox.test.tsx +88 -0
  78. package/src/components/settings/ModelIdCombobox.tsx +185 -0
  79. package/src/components/settings/SecuritySettingsPanel.test.tsx +122 -0
  80. package/src/components/settings/SecuritySettingsPanel.tsx +169 -0
  81. package/src/components/settings/SettingsSaveButton.tsx +35 -0
  82. package/src/components/settings/SkillsSettingsPanel.test.tsx +62 -0
  83. package/src/components/settings/SkillsSettingsPanel.tsx +334 -0
  84. package/src/components/settings/TelemetrySettingsPanel.tsx +173 -0
  85. package/src/components/settings/UsagePanel.tsx +196 -0
  86. package/src/components/settings/WebSearchSettingsPanel.test.tsx +90 -0
  87. package/src/components/settings/WebSearchSettingsPanel.tsx +183 -0
  88. package/src/components/settings/compat-flags-model.test.ts +71 -0
  89. package/src/components/settings/compat-flags-model.ts +57 -0
  90. package/src/components/settings/llm-vendors.test.ts +171 -0
  91. package/src/components/settings/llm-vendors.ts +275 -0
  92. package/src/components/settings/model-capabilities.test.ts +88 -0
  93. package/src/components/settings/model-capabilities.ts +123 -0
  94. package/src/components/settings/preset-choice-model.test.ts +96 -0
  95. package/src/components/settings/preset-choice-model.ts +75 -0
  96. package/src/fixtures/browser-dev-data.json +315 -0
  97. package/src/hooks/useChatsAndAgents.ts +182 -0
  98. package/src/layouts/AgentLayout.tsx +574 -0
  99. package/src/lib/attachments.ts +78 -0
  100. package/src/lib/browser-dev-electron-mock.ts +640 -0
  101. package/src/lib/chat-title.ts +33 -0
  102. package/src/lib/chat-transport.ts +346 -0
  103. package/src/lib/date-groups.ts +34 -0
  104. package/src/lib/demo-flag.ts +15 -0
  105. package/src/lib/electron-bridge.ts +270 -0
  106. package/src/lib/exec-policy.ts +25 -0
  107. package/src/lib/http-bridge.ts +293 -0
  108. package/src/lib/insights.ts +17 -0
  109. package/src/lib/local-api.ts +714 -0
  110. package/src/lib/pending-first-message.test.ts +17 -0
  111. package/src/lib/pending-first-message.ts +36 -0
  112. package/src/lib/slash-sources.ts +191 -0
  113. package/src/main.tsx +103 -0
  114. package/src/packs/registry.ts +64 -0
  115. package/src/pages/AgentPage.tsx +1226 -0
  116. package/src/pages/ChatPanelPreviewPage.tsx +264 -0
  117. package/src/pages/SettingsPage.test.tsx +102 -0
  118. package/src/pages/SettingsPage.tsx +197 -0
  119. package/src/styles/index.css +156 -0
  120. package/src/vite-env.d.ts +1 -0
  121. 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;