@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,96 @@
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
+ });
@@ -0,0 +1,75 @@
1
+ /**
2
+ * 厂商参数预制(框架 llm.presets)设置区的纯表单模型,从
3
+ * LocalLlmSettingsModal 抽出以便单测。注册表与匹配规则都在框架侧
4
+ * (presets.describe / presets.resolve RPC),这里只做表单态与 wire
5
+ * 载荷之间的转换,不硬编码任何厂商参数。
6
+ */
7
+
8
+ import type {
9
+ ProviderPresetDescriptor,
10
+ ProviderPresetOverride,
11
+ ProviderPresetsChoice,
12
+ } from '@/lib/local-api';
13
+
14
+ /** 三态:自动(框架按 baseUrl+model 匹配)/ 关闭 / 钉死一条注册表行。 */
15
+ export type PresetMode = 'auto' | 'off' | 'pinned';
16
+
17
+ /** 注册表行 → 钉死时随流下发的 override 载荷(null 字段不下发)。 */
18
+ export function overrideFromDescriptor(d: ProviderPresetDescriptor): ProviderPresetOverride {
19
+ return {
20
+ ...(d.temperature != null && { temperature: d.temperature }),
21
+ ...(d.topP != null && { topP: d.topP }),
22
+ ...(d.maxTokens != null && { maxTokens: d.maxTokens }),
23
+ ...(d.reasoningEffort != null && { reasoningEffort: d.reasoningEffort }),
24
+ ...(d.extraBody != null && { extraBody: d.extraBody }),
25
+ };
26
+ }
27
+
28
+ /** 选择器选项标签:`host + 前缀*`,缺省键名退化为「未命名」。 */
29
+ export function descriptorLabel(d: ProviderPresetDescriptor): string {
30
+ const parts = [d.host, d.modelPrefix ? `${d.modelPrefix}*` : null].filter(Boolean);
31
+ return parts.join(' + ') || '(未命名预制)';
32
+ }
33
+
34
+ /** 预制参数的一行摘要,用于生效预览。 */
35
+ export function summarizePreset(p: ProviderPresetOverride | null): string {
36
+ if (!p) return '不下发额外采样参数';
37
+ const parts: string[] = [];
38
+ if (p.temperature != null) parts.push(`temperature ${p.temperature}`);
39
+ if (p.topP != null) parts.push(`top_p ${p.topP}`);
40
+ if (p.maxTokens != null) parts.push(`max_tokens ${p.maxTokens}`);
41
+ if (p.reasoningEffort) parts.push(`reasoning_effort ${p.reasoningEffort}`);
42
+ if (p.extraBody) {
43
+ for (const [k, v] of Object.entries(p.extraBody)) parts.push(`${k} ${JSON.stringify(v)}`);
44
+ }
45
+ return parts.length > 0 ? parts.join(' · ') : '不下发额外采样参数(该厂商最优请求即不带采样字段)';
46
+ }
47
+
48
+ export function overridesEqual(a: ProviderPresetOverride, b: ProviderPresetOverride): boolean {
49
+ return (
50
+ JSON.stringify({ ...a, extraBody: a.extraBody ?? null }) ===
51
+ JSON.stringify({ ...b, extraBody: b.extraBody ?? null })
52
+ );
53
+ }
54
+
55
+ /** 已持久化的选择 → 表单三态。 */
56
+ export function presetModeFromChoice(choice: ProviderPresetsChoice | undefined): PresetMode {
57
+ if (!choice) return 'auto';
58
+ if (choice.override) return 'pinned';
59
+ return choice.enabled === false ? 'off' : 'auto';
60
+ }
61
+
62
+ /** 表单三态 → 待持久化的选择(自动 = 不持久化,框架自动匹配兜底)。 */
63
+ export function choiceFromPresetMode(
64
+ mode: PresetMode,
65
+ pinnedIdx: number,
66
+ list: ProviderPresetDescriptor[],
67
+ existing?: ProviderPresetsChoice,
68
+ ): ProviderPresetsChoice | undefined {
69
+ if (mode === 'auto') return undefined;
70
+ if (mode === 'off') return { enabled: false };
71
+ const pinned = pinnedIdx >= 0 ? list[pinnedIdx] : undefined;
72
+ if (pinned) return { override: overrideFromDescriptor(pinned) };
73
+ // 钉死但注册表不可用(sidecar 未就绪):保留已保存的自定义覆盖。
74
+ return existing?.override ? { override: existing.override } : undefined;
75
+ }
@@ -0,0 +1,315 @@
1
+ {
2
+ "version": 1,
3
+ "exportedAt": null,
4
+ "source": "hand-curated neutral fixtures",
5
+ "agents": [
6
+ {
7
+ "id": "local-assistant",
8
+ "slug": "local-assistant",
9
+ "name": "电脑操作员",
10
+ "icon": "Cpu",
11
+ "color": "#4f46e5",
12
+ "description": "默认本地助手,可调用 shell、文件与 MCP 工具。",
13
+ "rolePrompt": "你是本地离线助手,回答时清晰、可执行。",
14
+ "forbiddenPrompt": null,
15
+ "skillIds": [],
16
+ "toolPolicy": {
17
+ "mode": "all",
18
+ "tools": []
19
+ },
20
+ "allowExternalSkills": true,
21
+ "isBuiltin": true,
22
+ "isArchived": false,
23
+ "sortOrder": 0,
24
+ "createdAt": "2026-06-22T03:32:30.256Z",
25
+ "updatedAt": "2026-06-22T03:32:30.256Z"
26
+ },
27
+ {
28
+ "id": "data-analyst",
29
+ "slug": "data-analyst",
30
+ "name": "数据分析助手",
31
+ "icon": "Activity",
32
+ "color": "#0ea5e9",
33
+ "description": "处理本地 CSV 与日志文件:清洗、统计、出图与汇总报告。",
34
+ "rolePrompt": "你是 **数据分析助手**,专注本地数据文件的清洗、统计与可视化。\n工作语言:中文。回答简洁、结论先行。\n\n工作纪律:\n1) 先探查文件结构再动手;\n2) 每一步都说明做了什么;\n3) 结果用表格或要点呈现。",
35
+ "forbiddenPrompt": null,
36
+ "skillIds": [],
37
+ "toolPolicy": {
38
+ "mode": "all",
39
+ "tools": []
40
+ },
41
+ "allowExternalSkills": true,
42
+ "isBuiltin": true,
43
+ "isArchived": false,
44
+ "sortOrder": 1,
45
+ "createdAt": "2026-06-22T03:32:30.256Z",
46
+ "updatedAt": "2026-06-22T03:32:30.256Z"
47
+ },
48
+ {
49
+ "id": "all-round-assistant",
50
+ "slug": "all-round-assistant",
51
+ "name": "智能助手",
52
+ "icon": "🔮",
53
+ "color": "#a855f7",
54
+ "description": "全能智能体,自动加载全部已安装本地技能和工具,无需限定条件。",
55
+ "rolePrompt": "你是 **智能助手**,一个具备高权限且集成了全部本地技能(Skills)的行动体。\n你具备完整的本地命令执行(local_exec_shell)、文件读写与数据分析能力。\n你的系统提示词中已经无条件加载了所有已安装的本地技能与工具。\n请根据用户的需求,直接、高效地调用最合适的本地工具或执行脚本来解决任务。",
56
+ "forbiddenPrompt": null,
57
+ "skillIds": [],
58
+ "toolPolicy": {
59
+ "mode": "all",
60
+ "tools": []
61
+ },
62
+ "allowExternalSkills": true,
63
+ "isBuiltin": true,
64
+ "isArchived": false,
65
+ "sortOrder": 2,
66
+ "createdAt": "2026-06-22T03:32:30.256Z",
67
+ "updatedAt": "2026-06-22T03:32:30.256Z"
68
+ }
69
+ ],
70
+ "chats": [
71
+ {
72
+ "id": "8286e3c8-2d83-409e-930a-3e99b2004d41",
73
+ "projectId": null,
74
+ "userId": "local",
75
+ "title": "你好",
76
+ "agentId": "local-assistant",
77
+ "createdAt": "2026-07-01T10:05:58.604Z",
78
+ "updatedAt": "2026-07-01T10:06:06.736Z",
79
+ "isPinned": false,
80
+ "systemPrompt": null,
81
+ "pinnedRefs": null
82
+ },
83
+ {
84
+ "id": "8b13a92e-4b3e-4bac-885f-879ddd9e4835",
85
+ "projectId": null,
86
+ "userId": "local",
87
+ "title": "启用 csv-data-tools 技能",
88
+ "agentId": "all-round-assistant",
89
+ "createdAt": "2026-06-22T04:30:54.662Z",
90
+ "updatedAt": "2026-06-22T04:32:12.657Z",
91
+ "isPinned": false,
92
+ "systemPrompt": null,
93
+ "pinnedRefs": null
94
+ },
95
+ {
96
+ "id": "d536659c-6492-424d-97e8-2f701a9ce5d8",
97
+ "projectId": null,
98
+ "userId": "local",
99
+ "title": "新对话",
100
+ "agentId": "all-round-assistant",
101
+ "createdAt": "2026-06-22T04:19:52.607Z",
102
+ "updatedAt": "2026-06-22T04:20:00.151Z",
103
+ "isPinned": false,
104
+ "systemPrompt": null,
105
+ "pinnedRefs": null
106
+ },
107
+ {
108
+ "id": "4845b527-c9c9-467c-9d72-5fc5f8763aa5",
109
+ "projectId": null,
110
+ "userId": "local",
111
+ "title": "新对话",
112
+ "agentId": "all-round-assistant",
113
+ "createdAt": "2026-06-22T04:12:17.151Z",
114
+ "updatedAt": "2026-06-22T04:12:22.038Z",
115
+ "isPinned": false,
116
+ "systemPrompt": null,
117
+ "pinnedRefs": null
118
+ },
119
+ {
120
+ "id": "405f5964-f01c-4ece-9c8f-02a73d539469",
121
+ "projectId": null,
122
+ "userId": "local",
123
+ "title": "启用 csv-data-tools 技能",
124
+ "agentId": "all-round-assistant",
125
+ "createdAt": "2026-06-22T04:06:21.745Z",
126
+ "updatedAt": "2026-06-22T04:11:42.782Z",
127
+ "isPinned": false,
128
+ "systemPrompt": null,
129
+ "pinnedRefs": null
130
+ },
131
+ {
132
+ "id": "19a1d8e7-7e9c-425b-8509-0fe9dd292971",
133
+ "projectId": null,
134
+ "userId": "local",
135
+ "title": "启用 csv-data-tools 技能",
136
+ "agentId": "all-round-assistant",
137
+ "createdAt": "2026-06-22T04:01:21.158Z",
138
+ "updatedAt": "2026-06-22T04:05:08.801Z",
139
+ "isPinned": false,
140
+ "systemPrompt": null,
141
+ "pinnedRefs": null
142
+ },
143
+ {
144
+ "id": "492c1a80-85b2-486a-b365-3d668c9ad7e6",
145
+ "projectId": null,
146
+ "userId": "local",
147
+ "title": "新对话",
148
+ "agentId": "all-round-assistant",
149
+ "createdAt": "2026-06-22T03:44:47.381Z",
150
+ "updatedAt": "2026-06-22T03:44:54.479Z",
151
+ "isPinned": false,
152
+ "systemPrompt": null,
153
+ "pinnedRefs": null
154
+ },
155
+ {
156
+ "id": "4e997d62-20ef-4937-a866-27778dee3a17",
157
+ "projectId": null,
158
+ "userId": "local",
159
+ "title": "hi",
160
+ "agentId": "all-round-assistant",
161
+ "createdAt": "2026-06-22T03:34:36.892Z",
162
+ "updatedAt": "2026-06-22T03:37:37.601Z",
163
+ "isPinned": false,
164
+ "systemPrompt": null,
165
+ "pinnedRefs": null
166
+ }
167
+ ],
168
+ "messagesByChatId": {
169
+ "8286e3c8-2d83-409e-930a-3e99b2004d41": [
170
+ {
171
+ "id": "ea36a5de-e2e0-428b-b882-f5e67ca326b0",
172
+ "chatId": "8286e3c8-2d83-409e-930a-3e99b2004d41",
173
+ "role": "assistant",
174
+ "content": "你好!我是**本地智能助手**,运行在你的本地机器上。\n\n我可以帮你做这些事情:\n\n**💻 本地终端操作**\n- 执行 shell 命令、读写文件\n- 搜索代码库、批量替换\n\n**📊 数据处理**\n- 清洗 CSV / 日志文件\n- 生成统计汇总\n\n直接输入需求即可开始。",
175
+ "createdAt": "2026-07-01T10:06:06.733Z",
176
+ "messageMetadata": "{\"traceId\":\"tr_269aa1b0f5244962b3279304d6f26323\",\"completionStatus\":\"completed\",\"trace\":{\"traceId\":\"tr_269aa1b0f5244962b3279304d6f26323\",\"sessionId\":\"8286e3c8-2d83-409e-930a-3e99b2004d41\",\"chatId\":\"8286e3c8-2d83-409e-930a-3e99b2004d41\",\"startedAtMs\":1782900364396,\"eventCount\":3,\"spanCount\":1,\"events\":[{\"name\":\"stream_start\",\"timestampMs\":1782900364396,\"data\":{\"path\":\"/api/v2/chats/8286e3c8-2d83-409e-930a-3e99b2004d41/run\",\"method\":\"POST\"}},{\"name\":\"completion_decision\",\"timestampMs\":1782900366733,\"data\":{\"status\":\"completed\",\"reason\":\"assistant produced final response with no pending tools\",\"confidence\":0.85,\"evidence\":[{\"textLength\":218,\"toolCalls\":[],\"toolErrorCount\":0}]}},{\"name\":\"stream_complete\",\"timestampMs\":1782900366733,\"data\":{\"usedTool\":false,\"completionStatus\":\"completed\",\"assistantLength\":218}}],\"spans\":[{\"name\":\"llm_call\",\"stepId\":\"step_1\",\"startMs\":1782900364396,\"endMs\":1782900366733,\"status\":\"ok\",\"data\":{\"turn\":0,\"model\":\"default\",\"tokensUsed\":{\"prompt\":8596,\"completion\":130,\"total\":8726},\"toolCalls\":0},\"durationMs\":2337}]},\"executedActions\":[]}"
177
+ },
178
+ {
179
+ "id": "ab1387a6-5c43-4d4b-986d-073d98b3f5e2",
180
+ "chatId": "8286e3c8-2d83-409e-930a-3e99b2004d41",
181
+ "role": "user",
182
+ "content": "你好",
183
+ "createdAt": "2026-07-01T10:06:04.392Z",
184
+ "messageMetadata": null
185
+ }
186
+ ],
187
+ "8b13a92e-4b3e-4bac-885f-879ddd9e4835": [
188
+ {
189
+ "id": "6599eb84-515c-4f58-bec8-0f37fcfbb234",
190
+ "chatId": "8b13a92e-4b3e-4bac-885f-879ddd9e4835",
191
+ "role": "assistant",
192
+ "content": "好的,我先查看 `csv-data-tools` 技能的可用模块。\n\n**`csv-data-tools`** 技能已就绪。以下是它的能力概览:\n\n---\n\n## 📦 csv-data-tools\n\n- `csv_stats` — 列级统计(行数、缺失、分布)\n- `csv_filter` — 条件过滤与导出\n- `csv_plot` — 快速出图\n\n告诉我文件路径即可开始。",
193
+ "createdAt": "2026-06-22T04:32:11.576Z",
194
+ "messageMetadata": null
195
+ },
196
+ {
197
+ "id": "a6d5f510-e26c-4fd5-878b-74ea98a977f9",
198
+ "chatId": "8b13a92e-4b3e-4bac-885f-879ddd9e4835",
199
+ "role": "user",
200
+ "content": "/csv-data-tools",
201
+ "createdAt": "2026-06-22T04:31:01.151Z",
202
+ "messageMetadata": null
203
+ }
204
+ ],
205
+ "d536659c-6492-424d-97e8-2f701a9ce5d8": [
206
+ {
207
+ "id": "9b04bf0c-2d90-4e27-b891-9871207d177e",
208
+ "chatId": "d536659c-6492-424d-97e8-2f701a9ce5d8",
209
+ "role": "user",
210
+ "content": "/csv-data-tools",
211
+ "createdAt": "2026-06-22T04:20:00.151Z",
212
+ "messageMetadata": null
213
+ }
214
+ ],
215
+ "4845b527-c9c9-467c-9d72-5fc5f8763aa5": [
216
+ {
217
+ "id": "abb7c501-67d1-4ca4-8d94-708bbd55646f",
218
+ "chatId": "4845b527-c9c9-467c-9d72-5fc5f8763aa5",
219
+ "role": "user",
220
+ "content": "/csv-data-tools",
221
+ "createdAt": "2026-06-22T04:12:22.038Z",
222
+ "messageMetadata": null
223
+ }
224
+ ],
225
+ "405f5964-f01c-4ece-9c8f-02a73d539469": [
226
+ {
227
+ "id": "c8dc9e96-642b-4232-8c3c-c8abc32a615c",
228
+ "chatId": "405f5964-f01c-4ece-9c8f-02a73d539469",
229
+ "role": "assistant",
230
+ "content": "我来查看当前可用的 MCP 工具,找到 `csv-data-tools` 的相关信息。\n\n看起来 `csv-data-tools` 没有作为独立的 MCP 服务器注册。换个方式,先扫描本地脚本和可执行工具。\n\n结论:该技能当前未安装,可通过技能设置页导入。",
231
+ "createdAt": "2026-06-22T04:11:42.077Z",
232
+ "messageMetadata": null
233
+ },
234
+ {
235
+ "id": "d4c21f8f-b4ab-4dc0-bc48-611bc66847a4",
236
+ "chatId": "405f5964-f01c-4ece-9c8f-02a73d539469",
237
+ "role": "user",
238
+ "content": "/csv-data-tools",
239
+ "createdAt": "2026-06-22T04:06:27.797Z",
240
+ "messageMetadata": null
241
+ }
242
+ ],
243
+ "19a1d8e7-7e9c-425b-8509-0fe9dd292971": [
244
+ {
245
+ "id": "cfb0f03c-7faa-4594-b29f-f4c11fb09105",
246
+ "chatId": "19a1d8e7-7e9c-425b-8509-0fe9dd292971",
247
+ "role": "assistant",
248
+ "content": "本地模型调用失败:无法连接 OpenAI 兼容网关 (https://api.deepseek.com),请检查 baseUrl / 网络。原始错误: fetch failed",
249
+ "createdAt": "2026-06-22T04:05:06.751Z",
250
+ "messageMetadata": null
251
+ },
252
+ {
253
+ "id": "2d7469fc-1a58-488b-beb8-c372e54e3def",
254
+ "chatId": "19a1d8e7-7e9c-425b-8509-0fe9dd292971",
255
+ "role": "user",
256
+ "content": "/csv-data-tools",
257
+ "createdAt": "2026-06-22T04:01:55.866Z",
258
+ "messageMetadata": null
259
+ }
260
+ ],
261
+ "492c1a80-85b2-486a-b365-3d668c9ad7e6": [
262
+ {
263
+ "id": "6ca59f0c-6df7-4747-9c96-80812d459a96",
264
+ "chatId": "492c1a80-85b2-486a-b365-3d668c9ad7e6",
265
+ "role": "user",
266
+ "content": "/csv-data-tools",
267
+ "createdAt": "2026-06-22T03:44:54.479Z",
268
+ "messageMetadata": null
269
+ }
270
+ ],
271
+ "4e997d62-20ef-4937-a866-27778dee3a17": [
272
+ {
273
+ "id": "c5d6136b-eaa4-4cc7-878b-450099b7d226",
274
+ "chatId": "4e997d62-20ef-4937-a866-27778dee3a17",
275
+ "role": "assistant",
276
+ "content": "我来查看当前可用的 MCP 工具,找到 `csv-data-tools` 的相关信息。\n\n看起来 `csv-data-tools` 没有作为独立的 MCP 服务器注册。换个方式,先扫描本地脚本和可执行工具。\n\n结论:该技能当前未安装,可通过技能设置页导入。",
277
+ "createdAt": "2026-06-22T03:37:37.601Z",
278
+ "messageMetadata": null
279
+ },
280
+ {
281
+ "id": "68f12c35-7de5-49f3-af14-ca5deae13dee",
282
+ "chatId": "4e997d62-20ef-4937-a866-27778dee3a17",
283
+ "role": "user",
284
+ "content": "/csv-data-tools",
285
+ "createdAt": "2026-06-22T03:36:49.529Z",
286
+ "messageMetadata": null
287
+ },
288
+ {
289
+ "id": "fa89030e-5db1-4d04-9320-e796baeb89e2",
290
+ "chatId": "4e997d62-20ef-4937-a866-27778dee3a17",
291
+ "role": "assistant",
292
+ "content": "你好!我是**本地智能助手**,运行在你的本地机器上。\n\n我可以帮你做这些事情:\n\n**💻 本地终端操作**\n- 执行 shell 命令、读写文件\n- 搜索代码库、批量替换\n\n**📊 数据处理**\n- 清洗 CSV / 日志文件\n- 生成统计汇总\n\n直接输入需求即可开始。",
293
+ "createdAt": "2026-06-22T03:34:48.778Z",
294
+ "messageMetadata": "{\"traceId\":\"tr_f53c2142bbd347b5888f9d50c5444478\",\"completionStatus\":\"completed\",\"trace\":{\"traceId\":\"tr_f53c2142bbd347b5888f9d50c5444478\",\"sessionId\":\"4e997d62-20ef-4937-a866-27778dee3a17\",\"chatId\":\"4e997d62-20ef-4937-a866-27778dee3a17\",\"startedAtMs\":1782099286405,\"eventCount\":3,\"spanCount\":1,\"events\":[{\"name\":\"stream_start\",\"timestampMs\":1782099286405,\"data\":{\"path\":\"/api/v2/chats/4e997d62-20ef-4937-a866-27778dee3a17/run\",\"method\":\"POST\"}},{\"name\":\"completion_decision\",\"timestampMs\":1782099288778,\"data\":{\"status\":\"completed\",\"reason\":\"assistant produced final response with no pending tools\",\"confidence\":0.85,\"evidence\":[{\"textLength\":224,\"toolCalls\":[],\"toolErrorCount\":0}]}},{\"name\":\"stream_complete\",\"timestampMs\":1782099288778,\"data\":{\"usedTool\":false,\"completionStatus\":\"completed\",\"assistantLength\":224}}],\"spans\":[{\"name\":\"llm_call\",\"stepId\":\"step_1\",\"startMs\":1782099286405,\"endMs\":1782099288777,\"status\":\"ok\",\"data\":{\"turn\":0,\"model\":\"default\",\"tokensUsed\":{\"prompt\":2234,\"completion\":130,\"total\":2364},\"toolCalls\":0},\"durationMs\":2372}]},\"executedActions\":[]}"
295
+ },
296
+ {
297
+ "id": "82c95340-1d74-49fe-abe7-2184aeab9f8c",
298
+ "chatId": "4e997d62-20ef-4937-a866-27778dee3a17",
299
+ "role": "user",
300
+ "content": "hi",
301
+ "createdAt": "2026-06-22T03:34:46.403Z",
302
+ "messageMetadata": null
303
+ }
304
+ ]
305
+ },
306
+ "llmSettings": {
307
+ "provider": "openai-compat",
308
+ "model": "deepseek-chat",
309
+ "baseUrl": "https://api.deepseek.com",
310
+ "apiKey": "",
311
+ "temperature": 0.3,
312
+ "systemPrompt": "你是一款本地行动助手,优先给出可执行建议并在需要时调用本地工具。"
313
+ },
314
+ "skills": []
315
+ }
@@ -0,0 +1,182 @@
1
+ /**
2
+ * `useChatsAndAgents` — sidebar state (paginated chats + agent catalog + the
3
+ * "agent picked for next new chat" selection) for the Agent shell.
4
+ *
5
+ * Wave 1 of the steerable-framework refactor moved the generic pagination /
6
+ * agent-selection / title-patching machinery into `@steerable/agent-ui/state`
7
+ * as `useChatList<TChat, TAgent>`. This file is now an Electron-flavoured
8
+ * adapter:
9
+ * - It wires the framework hook to the local-backend transport
10
+ * (`@/lib/local-api`).
11
+ * - It gates everything behind `isElectron()` so a renderer running in a
12
+ * plain browser doesn't fire requests.
13
+ * - It keeps the legacy `UseChatsAndAgentsResult` shape so callers
14
+ * (`AgentLayout`, `useChatsAndAgents()` consumers) don't change.
15
+ *
16
+ * Pagination contract (mirrors `deeppath/apps/web` `useChatUI`):
17
+ * - First mount loads page 1 (50 items).
18
+ * - AgentSidebar scrolls within its own container and calls `loadMoreChats`
19
+ * when the user nears the bottom. `isLoadingMoreChats` gates re-entry.
20
+ * - After a `refreshChats` (e.g. post create/delete), we reset to page 1
21
+ * and drop any already-loaded pages — keeping the list consistent is
22
+ * simpler than trying to splice individual pages.
23
+ */
24
+
25
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
26
+ import {
27
+ useChatList,
28
+ type ChatListTransport,
29
+ } from '@steerable/agent-ui/state';
30
+ import {
31
+ createChat as apiCreateChat,
32
+ deleteChat as apiDeleteChat,
33
+ listChatAgents,
34
+ listChats,
35
+ type LocalChat,
36
+ type LocalChatAgent,
37
+ } from '@/lib/local-api';
38
+ import { isElectron } from '@/lib/electron-bridge';
39
+ import { pickDefaultAgentId } from '@/brand';
40
+
41
+ const CHAT_PAGE_SIZE = 50;
42
+
43
+ export interface UseChatsAndAgentsResult {
44
+ chats: LocalChat[];
45
+ agents: LocalChatAgent[];
46
+ isLoading: boolean;
47
+ error: string | null;
48
+ selectedAgentId: string | null;
49
+ setSelectedAgentId: (id: string | null) => void;
50
+ refreshChats: () => Promise<void>;
51
+ refreshAgents: () => Promise<void>;
52
+ createChat: (input?: { agentId?: string; projectId?: string }) => Promise<string | null>;
53
+ deleteChat: (chatId: string) => Promise<boolean>;
54
+ /**
55
+ * 就地把某个 chat 的 title 改掉(不打后端、不重新拉列表)。
56
+ * 用于 SSE `chat_title_updated` 事件——后端已经写好库了,前端只要
57
+ * 同步内存里的 sidebar 视图就行,免得 refreshChats() 把翻页状态打回 page 1。
58
+ * 找不到 id 时静默忽略。
59
+ */
60
+ patchChatTitle: (chatId: string, title: string) => void;
61
+ /** True while a `loadMoreChats` call is in flight. */
62
+ isLoadingMoreChats: boolean;
63
+ /** True iff the last list-chats response reported more pages available. */
64
+ hasMoreChats: boolean;
65
+ loadMoreChats: () => Promise<void>;
66
+ }
67
+
68
+ export function useChatsAndAgents(): UseChatsAndAgentsResult {
69
+ const isElectronRef = useRef(isElectron());
70
+
71
+ const transport = useMemo<ChatListTransport<LocalChat, LocalChatAgent>>(
72
+ () => ({
73
+ listChats: async ({ page, pageSize }) => {
74
+ if (!isElectronRef.current) return { chats: [], hasMore: false };
75
+ const data = await listChats({ page, limit: pageSize });
76
+ return {
77
+ chats: data.chats,
78
+ hasMore: Boolean(data.pagination?.hasMore),
79
+ };
80
+ },
81
+ listAgents: async () => {
82
+ if (!isElectronRef.current) return [];
83
+ const data = await listChatAgents(false);
84
+ return data.agents;
85
+ },
86
+ createChat: async ({ agentId, projectId }) => {
87
+ if (!isElectronRef.current) {
88
+ throw new Error('not in electron');
89
+ }
90
+ const res = await apiCreateChat({
91
+ ...(agentId ? { agentId } : {}),
92
+ ...(projectId ? { projectId } : {}),
93
+ });
94
+ return res.chatId;
95
+ },
96
+ deleteChat: async (chatId) => {
97
+ if (!isElectronRef.current) return false;
98
+ await apiDeleteChat(chatId);
99
+ return true;
100
+ },
101
+ getChatId: (c) => c.id,
102
+ setChatTitle: (c, title) => (c.title === title ? c : { ...c, title }),
103
+ getAgentId: (a) => a.id,
104
+ }),
105
+ [],
106
+ );
107
+
108
+ const list = useChatList<LocalChat, LocalChatAgent>({
109
+ transport,
110
+ pageSize: CHAT_PAGE_SIZE,
111
+ // The renderer mounts before electron decides whether the bridge is live;
112
+ // skip the auto-fetch and trigger it manually after the SSR no-op check.
113
+ skipInitialLoad: !isElectronRef.current,
114
+ });
115
+
116
+ // Default-select this flavor's builtin expert once the catalog loads.
117
+ // 场景产品默认包的主打专家;无包产品默认 shell 内置智能体。
118
+ useEffect(() => {
119
+ if (list.agents.length === 0) return;
120
+ const current = list.selectedAgentId;
121
+ if (current && list.agents.some((a) => a.id === current)) return;
122
+ const next = pickDefaultAgentId(list.agents);
123
+ if (next) list.setSelectedAgentId(next);
124
+ // We only want this to fire when the catalog identity changes.
125
+ // eslint-disable-next-line react-hooks/exhaustive-deps
126
+ }, [list.agents]);
127
+
128
+ // Translate the framework's `Error | null` into the legacy `string | null`.
129
+ const errorStr = useMemo(
130
+ () => (list.error ? list.error.message : null),
131
+ [list.error],
132
+ );
133
+ // Some app-side flows (createChat/deleteChat below) also need to surface a
134
+ // string error without round-tripping through the framework hook. We keep a
135
+ // local override that takes precedence.
136
+ const [localError, setLocalError] = useState<string | null>(null);
137
+ const mergedError = localError ?? errorStr;
138
+
139
+ const createChat = useCallback(
140
+ async (input?: { agentId?: string; projectId?: string }) => {
141
+ try {
142
+ const id = await list.createChat(input);
143
+ return id;
144
+ } catch (err) {
145
+ setLocalError(err instanceof Error ? err.message : String(err));
146
+ return null;
147
+ }
148
+ },
149
+ [list],
150
+ );
151
+
152
+ const deleteChat = useCallback(
153
+ async (chatId: string) => {
154
+ try {
155
+ const ok = await list.deleteChat(chatId);
156
+ if (ok) await list.refreshChats();
157
+ return ok;
158
+ } catch (err) {
159
+ setLocalError(err instanceof Error ? err.message : String(err));
160
+ return false;
161
+ }
162
+ },
163
+ [list],
164
+ );
165
+
166
+ return {
167
+ chats: list.chats,
168
+ agents: list.agents,
169
+ isLoading: list.isLoading,
170
+ error: mergedError,
171
+ selectedAgentId: list.selectedAgentId,
172
+ setSelectedAgentId: list.setSelectedAgentId,
173
+ refreshChats: list.refreshChats,
174
+ refreshAgents: list.refreshAgents,
175
+ createChat,
176
+ deleteChat,
177
+ patchChatTitle: list.patchChatTitle,
178
+ isLoadingMoreChats: list.isLoadingMoreChats,
179
+ hasMoreChats: list.hasMoreChats,
180
+ loadMoreChats: list.loadMoreChats,
181
+ };
182
+ }