@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,264 @@
1
+ import { useMemo, useState } from 'react';
2
+ import type { ChatMessage } from '@steerable/agent-protocol';
3
+ import { LocalChatPanel } from '@/components/chat/LocalChatPanel';
4
+ import { ChatHeader } from '@/components/ChatHeader';
5
+ import type { ExecutedAction } from '@/components/chat/ExecutedActionsCard';
6
+ import type { TurnBlock } from '@/components/chat/turn-timeline';
7
+ import type { LocalChat, LocalChatAgent } from '@/lib/local-api';
8
+
9
+ /**
10
+ * ChatPanelPreviewPage — dev-only visual harness for `LocalChatPanel`.
11
+ *
12
+ * Mounted at `/preview/chat` in dev. Produces deterministic mock chat data so
13
+ * we can iterate on the message renderer, executed-actions card, copy buttons
14
+ * etc. without needing local-backend to be wired up.
15
+ *
16
+ * Production builds keep the route around (tree-shake'd only when truly dead),
17
+ * but the dataset is small enough (a few hundred lines of mock content) that
18
+ * it's not worth gating behind `import.meta.env.DEV`. If we ever need to,
19
+ * wrap the router entry in `main.tsx` instead of guarding here.
20
+ */
21
+
22
+ const MOCK_AGENT: LocalChatAgent = {
23
+ id: 'agent_local_default',
24
+ slug: 'local-coder',
25
+ name: '本地 Coder',
26
+ icon: '🤖',
27
+ color: '#0ea5e9',
28
+ description: '本地代码助手 — 跑 shell / 工具演示 / 简单读写',
29
+ rolePrompt: null,
30
+ isBuiltin: true,
31
+ };
32
+
33
+ const MOCK_CHAT: LocalChat = {
34
+ id: 'preview-chat',
35
+ projectId: null,
36
+ userId: 'preview-user',
37
+ title: '预览:检查服务连通性 + 跑测试套件',
38
+ agentId: MOCK_AGENT.id,
39
+ createdAt: new Date().toISOString(),
40
+ updatedAt: new Date().toISOString(),
41
+ isPinned: false,
42
+ systemPrompt: null,
43
+ pinnedRefs: null,
44
+ };
45
+
46
+ const MOCK_MESSAGES: ChatMessage[] = [
47
+ {
48
+ id: 'm1',
49
+ role: 'user',
50
+ content: '帮我看下本地服务是否连通,再跑一下本目录的测试套件。',
51
+ createdAt: new Date(Date.now() - 1000 * 60 * 5).toISOString(),
52
+ },
53
+ {
54
+ id: 'm2',
55
+ role: 'assistant',
56
+ agentId: MOCK_AGENT.id,
57
+ content: `好的,先检查服务连通性,然后跑测试。
58
+
59
+ \`\`\`bash
60
+ $ check_connection
61
+ ✓ Mock service @ 127.0.0.1:8765 reachable (latency 4ms)
62
+ \`\`\`
63
+
64
+ 连接正常。接下来跑测试套件:
65
+
66
+ \`\`\`bash
67
+ $ npm test
68
+ PASS src/components/chat/ChatInput.test.tsx
69
+ PASS src/components/chat/MessageList.test.tsx
70
+ FAIL src/components/chat/AssistantMessage.test.tsx
71
+ ✗ renders agent badge when agentId present (5 ms)
72
+ Expected color "#0ea5e9", got "#7c3aed"
73
+
74
+ Tests: 1 failed, 2 passed, 3 total
75
+ \`\`\`
76
+
77
+ 有 1 个失败:\`AssistantMessage\` 的默认 agent 色没有从 props 传过来。建议在
78
+ \`agentInitial\` 之前先 \`a.color ?? '#7c3aed'\`,让 fallback 优先于硬编码。
79
+
80
+ 要我帮你直接改这个文件吗?`,
81
+ createdAt: new Date(Date.now() - 1000 * 60 * 3).toISOString(),
82
+ },
83
+ ];
84
+
85
+ const MOCK_ACTIONS: ExecutedAction[] = [
86
+ {
87
+ tool: 'check_connection',
88
+ arguments: { host: '127.0.0.1', port: 8765 },
89
+ result: { success: true, latencyMs: 4 },
90
+ },
91
+ {
92
+ tool: 'shell_run',
93
+ arguments: { command: 'npm test', cwd: '/home/me/proj' },
94
+ result: {
95
+ success: false,
96
+ exitCode: 1,
97
+ stdout: '...test output truncated...',
98
+ stderr: 'Tests: 1 failed, 2 passed, 3 total',
99
+ },
100
+ },
101
+ {
102
+ tool: 'fs_read_file',
103
+ arguments: { path: 'src/components/chat/AssistantMessage.tsx' },
104
+ result: { success: true, bytes: 4096 },
105
+ },
106
+ ];
107
+
108
+ const MOCK_TIMELINE: TurnBlock[] = [
109
+ { type: 'reasoning', content: '先检查服务连通性,再跑测试套件。' },
110
+ { type: 'tools', actions: [MOCK_ACTIONS[0]] },
111
+ { type: 'text', content: '连接正常。接下来跑测试套件。' },
112
+ { type: 'tools', actions: [MOCK_ACTIONS[1]] },
113
+ { type: 'reasoning', content: '测试失败,读一下失败文件再给建议。' },
114
+ { type: 'tools', actions: [MOCK_ACTIONS[2]] },
115
+ { type: 'text', content: MOCK_MESSAGES[1].content ?? '' },
116
+ ];
117
+
118
+ /**
119
+ * Three scenarios to flip between with the top-right pill buttons:
120
+ * • `static` — finished conversation; verifies copy buttons, action
121
+ * cards, markdown rendering on history.
122
+ * • `thinking` — in-flight assistant bubble before any content; verifies
123
+ * StreamingStatus's "正在思考..." baseline.
124
+ * • `tools-run` — in-flight bubble, round 2, 2 tools just ran; verifies
125
+ * StreamingStatus's "已调用 N 个工具" + "Round N" badge interaction.
126
+ */
127
+ type PreviewScene = 'static' | 'thinking' | 'tools-run';
128
+
129
+ const SCENES: { id: PreviewScene; label: string }[] = [
130
+ { id: 'static', label: '静态历史' },
131
+ { id: 'thinking', label: '流式 · 空内容' },
132
+ { id: 'tools-run', label: '流式 · 工具已跑 · round 2' },
133
+ ];
134
+
135
+ export function ChatPanelPreviewPage() {
136
+ const [scene, setScene] = useState<PreviewScene>('static');
137
+
138
+ // Explicit struct type so TS doesn't try to narrow `actionsByMsgId` to the
139
+ // union of {} | { m2: ... } — both shapes are valid `Record<string, ...>`
140
+ // but the inferred type rejects each side mutually.
141
+ interface SceneConfig {
142
+ messages: ChatMessage[];
143
+ isStreaming: boolean;
144
+ actionsByMsgId: Record<string, ExecutedAction[]>;
145
+ currentTurnActions: ExecutedAction[];
146
+ timelineByMsgId: Record<string, TurnBlock[]>;
147
+ currentTurnTimeline?: TurnBlock[];
148
+ currentTurnStartedAtMs?: number;
149
+ durationByMessageId?: Record<string, number>;
150
+ currentRound: number;
151
+ }
152
+ const config = useMemo<SceneConfig>(() => {
153
+ switch (scene) {
154
+ case 'thinking':
155
+ return {
156
+ messages: [
157
+ MOCK_MESSAGES[0],
158
+ {
159
+ id: 'm-streaming',
160
+ role: 'assistant',
161
+ agentId: MOCK_AGENT.id,
162
+ content: '',
163
+ createdAt: new Date().toISOString(),
164
+ },
165
+ ],
166
+ isStreaming: true,
167
+ actionsByMsgId: {} as Record<string, ExecutedAction[]>,
168
+ currentTurnActions: [],
169
+ timelineByMsgId: {} as Record<string, TurnBlock[]>,
170
+ currentTurnTimeline: [],
171
+ currentRound: 1,
172
+ };
173
+ case 'tools-run':
174
+ return {
175
+ messages: [
176
+ MOCK_MESSAGES[0],
177
+ {
178
+ id: 'm-streaming',
179
+ role: 'assistant',
180
+ agentId: MOCK_AGENT.id,
181
+ content: '',
182
+ createdAt: new Date().toISOString(),
183
+ },
184
+ ],
185
+ isStreaming: true,
186
+ actionsByMsgId: {} as Record<string, ExecutedAction[]>,
187
+ currentTurnActions: MOCK_ACTIONS.slice(0, 2),
188
+ timelineByMsgId: {} as Record<string, TurnBlock[]>,
189
+ currentTurnTimeline: [
190
+ { type: 'reasoning', content: '先检查连通性,再跑测试。' },
191
+ { type: 'tools', actions: MOCK_ACTIONS.slice(0, 2) },
192
+ ],
193
+ currentTurnStartedAtMs: Date.now() - 12_000,
194
+ currentRound: 2,
195
+ };
196
+ case 'static':
197
+ default:
198
+ return {
199
+ messages: MOCK_MESSAGES,
200
+ isStreaming: false,
201
+ actionsByMsgId: { m2: MOCK_ACTIONS },
202
+ currentTurnActions: [],
203
+ timelineByMsgId: { m2: MOCK_TIMELINE },
204
+ durationByMessageId: { m2: 83_000 },
205
+ currentRound: 1,
206
+ };
207
+ }
208
+ }, [scene]);
209
+
210
+ return (
211
+ <div className="flex h-full w-full flex-col bg-agent-muted/30">
212
+ <div className="flex shrink-0 items-center gap-2 border-b border-agent-border bg-agent-canvas px-3 py-2 text-xs">
213
+ <span className="text-agent-muted-foreground">
214
+ /preview/chat (dev harness)
215
+ </span>
216
+ <div className="flex gap-1">
217
+ {SCENES.map((s) => (
218
+ <button
219
+ key={s.id}
220
+ type="button"
221
+ onClick={() => setScene(s.id)}
222
+ className={`rounded-full border px-2 py-0.5 text-[11px] transition-colors ${
223
+ scene === s.id
224
+ ? 'border-agent-foreground/40 bg-agent-foreground/10 text-agent-foreground'
225
+ : 'border-agent-border bg-agent-canvas text-agent-muted-foreground hover:bg-agent-foreground/5'
226
+ }`}
227
+ >
228
+ {s.label}
229
+ </button>
230
+ ))}
231
+ </div>
232
+ </div>
233
+ <div className="m-2 flex flex-1 overflow-hidden rounded-agent-lg bg-agent-canvas shadow-sm">
234
+ <LocalChatPanel
235
+ messages={config.messages}
236
+ isStreaming={config.isStreaming}
237
+ onSubmit={async ({ content }) => {
238
+ console.log('[preview] submit:', content);
239
+ }}
240
+ onCancel={() => {}}
241
+ className="flex-1"
242
+ header={
243
+ <ChatHeader chat={MOCK_CHAT} agent={MOCK_AGENT} />
244
+ }
245
+ inputPlaceholder="预览模式 — 输入不会真的发送…"
246
+ agents={[MOCK_AGENT]}
247
+ currentAgent={MOCK_AGENT}
248
+ executedActionsByMessageId={config.actionsByMsgId}
249
+ currentTurnActions={config.currentTurnActions}
250
+ timelineByMessageId={config.timelineByMsgId}
251
+ currentTurnTimeline={config.currentTurnTimeline}
252
+ currentTurnStartedAtMs={config.currentTurnStartedAtMs}
253
+ durationByMessageId={config.durationByMessageId}
254
+ currentRound={config.currentRound}
255
+ onOpenSettings={() =>
256
+ console.log('[preview] open llm settings (noop in harness)')
257
+ }
258
+ />
259
+ </div>
260
+ </div>
261
+ );
262
+ }
263
+
264
+ export default ChatPanelPreviewPage;
@@ -0,0 +1,102 @@
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
+ });
@@ -0,0 +1,197 @@
1
+ import { useRef, useState } from 'react';
2
+ import { LuActivity, LuBlocks, LuBot, LuChartBar, LuNetwork, LuPlug, LuSearch, LuSettings, LuShieldCheck } from 'react-icons/lu';
3
+ import { useOutletContext, useSearchParams } from 'react-router-dom';
4
+ import { isElectron } from '@/lib/electron-bridge';
5
+ import { AgentsSettingsPanel } from '@/components/settings/AgentsSettingsPanel';
6
+ import { DiagnoseSettingsPanel } from '@/components/settings/DiagnoseSettingsPanel';
7
+ import { InsightsSettingsPanel } from '@/components/settings/InsightsSettingsPanel';
8
+ import {
9
+ LlmSettingsPanel,
10
+ type LlmSaveUi,
11
+ type LlmSettingsPanelHandle,
12
+ } from '@/components/settings/LlmSettingsPanel';
13
+ import { McpSettingsPanel } from '@/components/settings/McpSettingsPanel';
14
+ import { SecuritySettingsPanel } from '@/components/settings/SecuritySettingsPanel';
15
+ import { SkillsSettingsPanel } from '@/components/settings/SkillsSettingsPanel';
16
+ import { SettingsSaveButton } from '@/components/settings/SettingsSaveButton';
17
+ import { TelemetrySettingsPanel } from '@/components/settings/TelemetrySettingsPanel';
18
+ import { UsagePanel } from '@/components/settings/UsagePanel';
19
+ import { WebSearchSettingsPanel } from '@/components/settings/WebSearchSettingsPanel';
20
+ import { getPackSettingsPanels } from '@/packs/registry';
21
+ import type { AgentOutletContext } from '@/layouts/AgentLayout';
22
+
23
+ type SettingsSection = 'skills' | 'mcp' | 'agents' | 'general';
24
+
25
+ function resolveSection(raw: string | null): SettingsSection {
26
+ if (raw === 'skills' || raw === 'mcp' || raw === 'agents') return raw;
27
+ return 'general';
28
+ }
29
+
30
+ /**
31
+ * `/settings` — 右侧内容区的设置页,按 `?section=` 分成独立页面:
32
+ * - `skills` 侧栏「Skill 设置」
33
+ * - `mcp` 侧栏「MCP 设置」
34
+ * - `agents` 侧栏「智能体管理」
35
+ * - 缺省/`general` 侧栏底「设置」(模型 / 搜索 / 用量 / 安全 / 洞察 / 遥测)
36
+ *
37
+ * Panel 数据自管理(挂载即拉取),页面本身不持有后端状态。
38
+ */
39
+ export function SettingsPage() {
40
+ const [searchParams] = useSearchParams();
41
+ const section = resolveSection(searchParams.get('section'));
42
+ const catalog = useOutletContext<AgentOutletContext | null>();
43
+ const llmPanelRef = useRef<LlmSettingsPanelHandle>(null);
44
+ const [llmSaveUi, setLlmSaveUi] = useState<LlmSaveUi>({
45
+ saving: false,
46
+ savedOk: false,
47
+ loading: true,
48
+ });
49
+
50
+ const title =
51
+ section === 'mcp'
52
+ ? 'MCP 设置'
53
+ : section === 'skills'
54
+ ? 'Skill 设置'
55
+ : section === 'agents'
56
+ ? '智能体管理'
57
+ : '设置';
58
+
59
+ return (
60
+ <div className="flex h-full w-full flex-col overflow-hidden">
61
+ <header className="flex h-12 flex-shrink-0 items-center justify-between gap-3 border-b border-agent-border px-5">
62
+ <h1 className="text-sm font-semibold text-agent-foreground">{title}</h1>
63
+ {section === 'general' && (
64
+ <SettingsSaveButton
65
+ testId="settings-header-save"
66
+ saving={llmSaveUi.saving}
67
+ savedOk={llmSaveUi.savedOk}
68
+ disabled={llmSaveUi.saving || llmSaveUi.loading}
69
+ onClick={() => void llmPanelRef.current?.save()}
70
+ />
71
+ )}
72
+ </header>
73
+
74
+ <div className="min-h-0 flex-1 overflow-y-auto">
75
+ <div className="mx-auto max-w-3xl space-y-8 px-6 py-6">
76
+ {!isElectron() && (
77
+ <p className="rounded-agent-md border border-agent-destructive/20 bg-agent-destructive/10 p-2.5 text-xs text-agent-destructive">
78
+ 浏览器预览模式 — 没有 Electron IPC 桥接,部分设置不可用。
79
+ </p>
80
+ )}
81
+
82
+ {section === 'skills' && (
83
+ <section className="space-y-3" data-testid="settings-section-skills">
84
+ <h2 className="flex items-center gap-1.5 text-sm font-semibold text-agent-foreground">
85
+ <LuBlocks className="h-4 w-4 text-agent-muted-foreground" />
86
+ 本地技能
87
+ </h2>
88
+ <SkillsSettingsPanel />
89
+ </section>
90
+ )}
91
+
92
+ {section === 'mcp' && (
93
+ <section className="space-y-3" data-testid="settings-section-mcp">
94
+ <h2 className="flex items-center gap-1.5 text-sm font-semibold text-agent-foreground">
95
+ <LuPlug className="h-4 w-4 text-agent-muted-foreground" />
96
+ MCP 服务
97
+ </h2>
98
+ <McpSettingsPanel />
99
+ </section>
100
+ )}
101
+
102
+ {section === 'agents' && (
103
+ <section className="space-y-3" data-testid="settings-section-agents">
104
+ <h2 className="flex items-center gap-1.5 text-sm font-semibold text-agent-foreground">
105
+ <LuBot className="h-4 w-4 text-agent-muted-foreground" />
106
+ 智能体
107
+ </h2>
108
+ <AgentsSettingsPanel onCatalogChange={catalog?.refreshAgents} />
109
+ </section>
110
+ )}
111
+
112
+ {section === 'general' && (
113
+ <>
114
+ <section className="space-y-3" data-testid="settings-section-llm">
115
+ <h2 className="flex items-center gap-1.5 text-sm font-semibold text-agent-foreground">
116
+ <LuSettings className="h-4 w-4 text-agent-muted-foreground" />
117
+ 本地模型设置
118
+ </h2>
119
+ <LlmSettingsPanel
120
+ ref={llmPanelRef}
121
+ showFooterSave={false}
122
+ onSaveUiChange={setLlmSaveUi}
123
+ />
124
+ </section>
125
+
126
+ <section className="space-y-3" data-testid="settings-section-web-search">
127
+ <h2 className="flex items-center gap-1.5 text-sm font-semibold text-agent-foreground">
128
+ <LuSearch className="h-4 w-4 text-agent-muted-foreground" />
129
+ 网络搜索
130
+ </h2>
131
+ <WebSearchSettingsPanel />
132
+ </section>
133
+
134
+ <section className="space-y-3" data-testid="settings-section-usage">
135
+ <h2 className="flex items-center gap-1.5 text-sm font-semibold text-agent-foreground">
136
+ <LuChartBar className="h-4 w-4 text-agent-muted-foreground" />
137
+ 用量与成本
138
+ </h2>
139
+ <UsagePanel />
140
+ </section>
141
+
142
+ <section className="space-y-3" data-testid="settings-section-diagnose">
143
+ <h2 className="flex items-center gap-1.5 text-sm font-semibold text-agent-foreground">
144
+ <LuNetwork className="h-4 w-4 text-agent-muted-foreground" />
145
+ 链路诊断
146
+ </h2>
147
+ <DiagnoseSettingsPanel />
148
+ </section>
149
+
150
+ <section className="space-y-3" data-testid="settings-section-security">
151
+ <h2 className="flex items-center gap-1.5 text-sm font-semibold text-agent-foreground">
152
+ <LuShieldCheck className="h-4 w-4 text-agent-muted-foreground" />
153
+ 安全
154
+ </h2>
155
+ <SecuritySettingsPanel />
156
+ </section>
157
+
158
+ <section className="space-y-3" data-testid="settings-section-insights">
159
+ <h2 className="flex items-center gap-1.5 text-sm font-semibold text-agent-foreground">
160
+ <LuChartBar className="h-4 w-4 text-agent-muted-foreground" />
161
+ 帮助改进产品
162
+ </h2>
163
+ <InsightsSettingsPanel />
164
+ </section>
165
+
166
+ <section className="space-y-3" data-testid="settings-section-telemetry">
167
+ <h2 className="flex items-center gap-1.5 text-sm font-semibold text-agent-foreground">
168
+ <LuActivity className="h-4 w-4 text-agent-muted-foreground" />
169
+ 遥测(OTLP)
170
+ </h2>
171
+ <TelemetrySettingsPanel />
172
+ </section>
173
+
174
+ {/* 场景包设置面板(1.2 起由包渲染层贡献)。
175
+ 包经 packs/registry 注册,未激活 flavor 的产物里没有包组件。 */}
176
+ {getPackSettingsPanels().map((panel) => (
177
+ <section
178
+ key={panel.panelId}
179
+ className="space-y-3"
180
+ data-testid={`settings-section-${panel.panelId}`}
181
+ >
182
+ <h2 className="flex items-center gap-1.5 text-sm font-semibold text-agent-foreground">
183
+ {panel.Icon ? (
184
+ <panel.Icon className="h-4 w-4 text-agent-muted-foreground" />
185
+ ) : null}
186
+ {panel.title}
187
+ </h2>
188
+ <panel.Component />
189
+ </section>
190
+ ))}
191
+ </>
192
+ )}
193
+ </div>
194
+ </div>
195
+ </div>
196
+ );
197
+ }