@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,714 @@
1
+ /**
2
+ * Typed wrappers around `window.electron.localBackend.request` for the
3
+ * endpoints that apps/web actually consumes today. Shapes follow what
4
+ * `src/local-backend/router.ts` returns — see the corresponding route
5
+ * handlers there if you need to add fields.
6
+ *
7
+ * Conventions:
8
+ * - Every helper throws if the bridge is missing (caller should `isElectron()`
9
+ * check first if a graceful degradation is needed).
10
+ * - We deliberately keep the local return shapes (not Pydantic / protocol
11
+ * types) — they're the contract between local-backend and renderer, and
12
+ * may drift from any public cloud API. Aligning the two is out of scope.
13
+ */
14
+
15
+ import { getElectronBridge } from './electron-bridge';
16
+
17
+ export interface LocalChat {
18
+ id: string;
19
+ projectId: string | null;
20
+ userId: string;
21
+ title: string;
22
+ agentId: string | null;
23
+ createdAt: string;
24
+ updatedAt: string;
25
+ isPinned: boolean;
26
+ systemPrompt: string | null;
27
+ pinnedRefs: unknown;
28
+ }
29
+
30
+ /** 智能体的工具准入策略;`all` = 不限制。 */
31
+ export interface ChatAgentToolPolicy {
32
+ mode: 'all' | 'allowlist' | 'denylist';
33
+ tools: string[];
34
+ }
35
+
36
+ export interface LocalChatAgent {
37
+ id: string;
38
+ slug: string | null;
39
+ name: string;
40
+ icon: string | null;
41
+ color: string | null;
42
+ description: string | null;
43
+ rolePrompt: string | null;
44
+ /**
45
+ * 勾选的技能(技能 `dirName`):正文无条件常驻该智能体的系统提示词,
46
+ * 绕过技能自身的触发条件。
47
+ */
48
+ skillIds?: string[];
49
+ /** 模型可见工具的准入策略;每轮工具列表与分发层都执行。 */
50
+ toolPolicy?: ChatAgentToolPolicy;
51
+ /** false = 只允许 {@link skillIds} 里的技能(硬白名单)。缺省 true。 */
52
+ allowExternalSkills?: boolean;
53
+ /** true = 无视触发条件加载全部技能(内置「智能助手」如此)。 */
54
+ loadAllSkills?: boolean;
55
+ isBuiltin: boolean;
56
+ isArchived?: boolean;
57
+ /**
58
+ * Display order for the AgentSidebar's "专家团队" list. Local-backend
59
+ * persists this on every CRUD write (see router.ts:341 / 373). Lower
60
+ * numbers render first; same-value items fall back to alphabetic by name.
61
+ * Optional because the schema still tolerates legacy rows without it.
62
+ */
63
+ sortOrder?: number | null;
64
+ }
65
+
66
+ export interface LocalChatMessage {
67
+ id: string;
68
+ chatId: string;
69
+ role: 'system' | 'user' | 'assistant';
70
+ content: string;
71
+ createdAt: string;
72
+ messageMetadata?: string | null;
73
+ }
74
+
75
+ interface ChatListResponse {
76
+ chats: LocalChat[];
77
+ pagination: {
78
+ page: number;
79
+ limit: number;
80
+ total: number;
81
+ totalPages: number;
82
+ hasMore: boolean;
83
+ };
84
+ }
85
+
86
+ interface ChatAgentListResponse {
87
+ agents: LocalChatAgent[];
88
+ total: number;
89
+ }
90
+
91
+ interface CreateChatResponse {
92
+ success: boolean;
93
+ chatId: string;
94
+ projectId: string | null;
95
+ isTemporary: boolean;
96
+ }
97
+
98
+ function bridge() {
99
+ const b = getElectronBridge();
100
+ if (!b) {
101
+ throw new Error(
102
+ 'Electron bridge unavailable — local API can only be used inside the desktop shell.',
103
+ );
104
+ }
105
+ return b;
106
+ }
107
+
108
+ export async function listChats(params: { page?: number; limit?: number } = {}) {
109
+ const page = params.page ?? 1;
110
+ const limit = params.limit ?? 50;
111
+ return bridge().localBackend.request<ChatListResponse>({
112
+ method: 'GET',
113
+ path: `/api/v2/chats?page=${page}&limit=${limit}`,
114
+ });
115
+ }
116
+
117
+ export async function createChat(input: { agentId?: string; projectId?: string } = {}) {
118
+ return bridge().localBackend.request<CreateChatResponse>({
119
+ method: 'POST',
120
+ path: '/api/v2/chats/new',
121
+ body: input,
122
+ });
123
+ }
124
+
125
+ export async function deleteChat(chatId: string) {
126
+ return bridge().localBackend.request<{
127
+ success: boolean;
128
+ message: string;
129
+ chatId: string;
130
+ }>({
131
+ method: 'DELETE',
132
+ path: `/api/v2/chats/${encodeURIComponent(chatId)}`,
133
+ });
134
+ }
135
+
136
+ /** 没有消息的会话才删;已有内容或会话不存在都返回 `deleted: false`。 */
137
+ export async function deleteChatIfEmpty(chatId: string) {
138
+ return bridge().localBackend.request<{
139
+ success: boolean;
140
+ deleted: boolean;
141
+ chatId: string;
142
+ }>({
143
+ method: 'DELETE',
144
+ path: `/api/v2/chats/${encodeURIComponent(chatId)}?onlyIfEmpty=1`,
145
+ });
146
+ }
147
+
148
+ /** 清掉空会话;`exceptChatId` 是当前打开的对话,避免和首条发送抢跑。 */
149
+ export async function pruneEmptyChats(exceptChatId?: string | null) {
150
+ return bridge().localBackend.request<{ deletedChatIds: string[] }>({
151
+ method: 'POST',
152
+ path: '/api/v2/chats/prune-empty',
153
+ body: { exceptChatId: exceptChatId ?? null },
154
+ });
155
+ }
156
+
157
+ /** W1.2.1: one node in a chat's branch family (framework history record). */
158
+ export interface ChatBranchPoint {
159
+ recordId: string;
160
+ sourceRecordId: string | null;
161
+ sourceUntilSeq: number | null;
162
+ label: string;
163
+ depth?: number;
164
+ }
165
+
166
+ export interface ChatBranchesResponse {
167
+ activeRecordId: string;
168
+ lineage: ChatBranchPoint[];
169
+ children: ChatBranchPoint[];
170
+ }
171
+
172
+ /** W1.2.1: the chat's regenerate-fork branch family (empty when sidecar off). */
173
+ export async function getChatBranches(chatId: string) {
174
+ return bridge().localBackend.request<ChatBranchesResponse>({
175
+ method: 'GET',
176
+ path: `/api/v2/chats/${encodeURIComponent(chatId)}/branches`,
177
+ });
178
+ }
179
+
180
+ /**
181
+ * W1.2.1: switch the chat's active record to another branch. The backend
182
+ * re-projects the desktop message store from the framework record; the
183
+ * caller must re-hydrate the message list afterwards.
184
+ */
185
+ export async function activateChatBranch(chatId: string, recordId: string) {
186
+ return bridge().localBackend.request<{
187
+ activeRecordId: string;
188
+ messageCount: number;
189
+ }>({
190
+ method: 'POST',
191
+ path: `/api/v2/chats/${encodeURIComponent(chatId)}/branches/activate`,
192
+ body: { recordId },
193
+ });
194
+ }
195
+
196
+ /** Session tree: one node in the chat's full branch family (recursive). */
197
+ export interface ChatBranchTreeNode {
198
+ recordId: string;
199
+ sourceRecordId: string | null;
200
+ sourceUntilSeq: number | null;
201
+ label: string;
202
+ depth: number;
203
+ children: ChatBranchTreeNode[];
204
+ }
205
+
206
+ export interface ChatBranchTreeResponse {
207
+ activeRecordId: string;
208
+ /** Null when the sidecar is off or the record is unknown to it. */
209
+ tree: ChatBranchTreeNode | null;
210
+ nodeCount: number;
211
+ /** True when the sidecar's safety bounds (depth 32 / 500 nodes) cut the tree. */
212
+ truncated: boolean;
213
+ }
214
+
215
+ /**
216
+ * Session tree: the chat's full branch family from the root (cousins
217
+ * included) — the data behind the SessionTreeModal. Unlike
218
+ * `getChatBranches` (lineage + direct children), this sees every fork.
219
+ */
220
+ export async function getChatBranchTree(chatId: string) {
221
+ return bridge().localBackend.request<ChatBranchTreeResponse>({
222
+ method: 'GET',
223
+ path: `/api/v2/chats/${encodeURIComponent(chatId)}/branches/tree`,
224
+ });
225
+ }
226
+
227
+ export async function listChatAgents(includeArchived = false) {
228
+ return bridge().localBackend.request<ChatAgentListResponse>({
229
+ method: 'GET',
230
+ path: `/api/v2/chat-agents${includeArchived ? '?include_archived=true' : ''}`,
231
+ });
232
+ }
233
+
234
+ export interface ChatAgentWriteInput {
235
+ name: string;
236
+ color?: string | null;
237
+ description?: string | null;
238
+ rolePrompt?: string | null;
239
+ sortOrder?: number;
240
+ skillIds?: string[];
241
+ toolPolicy?: ChatAgentToolPolicy;
242
+ allowExternalSkills?: boolean;
243
+ loadAllSkills?: boolean;
244
+ }
245
+
246
+ /** 技能目录条目(`GET /api/v2/chat-agents/skills`)。 */
247
+ export interface ChatAgentSkillOption {
248
+ /** 技能 `dirName`,也是写入 `skillIds` 的值。 */
249
+ id: string;
250
+ name: string;
251
+ displayName: string;
252
+ description: string;
253
+ /** eager = 正文常驻;catalog = 模型按需加载。 */
254
+ layer: 'eager' | 'catalog';
255
+ origin?: 'builtin' | 'user' | 'workspace';
256
+ }
257
+
258
+ /** 工具目录条目(`GET /api/v2/chat-agents/tools`)。 */
259
+ export interface ChatAgentToolOption {
260
+ name: string;
261
+ description: string;
262
+ category: string;
263
+ }
264
+
265
+ export async function listChatAgentSkills() {
266
+ return bridge().localBackend.request<{ skills: ChatAgentSkillOption[] }>({
267
+ method: 'GET',
268
+ path: '/api/v2/chat-agents/skills',
269
+ });
270
+ }
271
+
272
+ export async function listChatAgentTools() {
273
+ return bridge().localBackend.request<{ tools: ChatAgentToolOption[] }>({
274
+ method: 'GET',
275
+ path: '/api/v2/chat-agents/tools',
276
+ });
277
+ }
278
+
279
+ export async function createChatAgent(input: ChatAgentWriteInput) {
280
+ return bridge().localBackend.request<{ agent: LocalChatAgent }>({
281
+ method: 'POST',
282
+ path: '/api/v2/chat-agents',
283
+ body: input,
284
+ });
285
+ }
286
+
287
+ export async function updateChatAgent(
288
+ agentId: string,
289
+ input: Partial<ChatAgentWriteInput>,
290
+ ) {
291
+ return bridge().localBackend.request<{ agent: LocalChatAgent }>({
292
+ method: 'PATCH',
293
+ path: `/api/v2/chat-agents/${encodeURIComponent(agentId)}`,
294
+ body: input,
295
+ });
296
+ }
297
+
298
+ export async function archiveChatAgent(agentId: string) {
299
+ return bridge().localBackend.request<{ id: string; status: string }>({
300
+ method: 'DELETE',
301
+ path: `/api/v2/chat-agents/${encodeURIComponent(agentId)}`,
302
+ });
303
+ }
304
+
305
+ /**
306
+ * 运行中回合的实时快照(`GET /api/v2/chats/:id/live-stream`)。用于切走再
307
+ * 切回时恢复「正在运行」的状态:active=true 时携带已产出的部分文本、工具
308
+ * 卡片、时间线与子代理事件;active=false 表示当前没有运行中的回合。
309
+ */
310
+ export interface ChatLiveStream {
311
+ active: boolean;
312
+ status?: string;
313
+ content?: string;
314
+ executedActions?: unknown[];
315
+ timeline?: unknown[];
316
+ children?: unknown[];
317
+ }
318
+
319
+ export async function getChatLiveStream(chatId: string) {
320
+ return bridge().localBackend.request<ChatLiveStream>({
321
+ method: 'GET',
322
+ path: `/api/v2/chats/${encodeURIComponent(chatId)}/live-stream`,
323
+ });
324
+ }
325
+
326
+ /** 按 chatId 取消运行中的回合(切回后新挂载的视图手里没有原 streamId)。 */
327
+ export async function cancelChatTurn(chatId: string) {
328
+ return bridge().localBackend.request<{ success: boolean; reason?: string }>({
329
+ method: 'POST',
330
+ path: `/api/v2/chats/${encodeURIComponent(chatId)}/cancel`,
331
+ body: {},
332
+ });
333
+ }
334
+
335
+ /* ---------------- Projects(项目模式) ---------------- */
336
+
337
+ export interface LocalProject {
338
+ id: string;
339
+ name: string;
340
+ folderPath: string;
341
+ /** W6-5: 信任后该项目目录里的规则文件才会注入模型上下文。缺省 false。 */
342
+ trusted?: boolean;
343
+ createdAt: string;
344
+ updatedAt: string;
345
+ }
346
+
347
+ /** W6-5: 会话的项目上下文状态(信任横幅用)。 */
348
+ export interface ChatProjectContext {
349
+ project: { id: string; name: string; folderPath: string; trusted: boolean } | null;
350
+ ruleFileCount?: number;
351
+ rulesActive?: boolean;
352
+ }
353
+
354
+ export async function getChatProjectContext(chatId: string) {
355
+ return bridge().localBackend.request<ChatProjectContext>({
356
+ method: 'GET',
357
+ path: `/api/v2/chats/${encodeURIComponent(chatId)}/project-context`,
358
+ });
359
+ }
360
+
361
+ export async function setProjectTrusted(projectId: string, trusted: boolean) {
362
+ return bridge().localBackend.request<{ success: boolean; project: LocalProject }>({
363
+ method: 'PUT',
364
+ path: `/api/v2/projects/${encodeURIComponent(projectId)}/trust`,
365
+ body: { trusted },
366
+ });
367
+ }
368
+
369
+ /* ---------------- Tasks(4.6 跨 turn 后台任务 + worktree) ---------------- */
370
+
371
+ /** 与后端 TaskRecord(src/storage/index.ts)同形。 */
372
+ export interface LocalTask {
373
+ id: string;
374
+ chatId: string;
375
+ task: string;
376
+ /** blocked=等依赖任务全部完成后自动点火(task_run dependsOn 编排)。 */
377
+ status: 'blocked' | 'running' | 'completed' | 'failed';
378
+ answer: string | null;
379
+ error: string | null;
380
+ worktreePath: string | null;
381
+ worktreeBranch: string | null;
382
+ /** pending=等用户决定合并/丢弃;null=非 worktree 任务。 */
383
+ worktreeState: 'pending' | 'merged' | 'discarded' | null;
384
+ /** 编排依赖:本任务等待的 taskId 列表(null=无依赖)。 */
385
+ dependsOn?: string[] | null;
386
+ createdAt: string;
387
+ updatedAt: string;
388
+ }
389
+
390
+ export async function listChatTasks(chatId: string) {
391
+ return bridge().localBackend.request<{ tasks: LocalTask[] }>({
392
+ method: 'GET',
393
+ path: `/api/v2/chats/${encodeURIComponent(chatId)}/tasks`,
394
+ });
395
+ }
396
+
397
+ export interface TaskProcessSnapshot {
398
+ task: LocalTask;
399
+ timeline: unknown[];
400
+ live: boolean;
401
+ stale: boolean;
402
+ }
403
+
404
+ export async function getTaskProcess(taskId: string) {
405
+ return bridge().localBackend.request<TaskProcessSnapshot>({
406
+ method: 'GET',
407
+ path: `/api/v2/tasks/${encodeURIComponent(taskId)}/process`,
408
+ });
409
+ }
410
+
411
+ /** 把已完成 worktree 任务的分支合并回主仓当前分支(4.6c)。 */
412
+ export async function mergeTaskWorktree(taskId: string) {
413
+ return bridge().localBackend.request<{ success: boolean; task: LocalTask }>({
414
+ method: 'POST',
415
+ path: `/api/v2/tasks/${encodeURIComponent(taskId)}/merge`,
416
+ });
417
+ }
418
+
419
+ /** 丢弃已完成 worktree 任务的改动并移除 worktree(4.6c)。 */
420
+ export async function discardTaskWorktree(taskId: string) {
421
+ return bridge().localBackend.request<{ success: boolean; task: LocalTask }>({
422
+ method: 'POST',
423
+ path: `/api/v2/tasks/${encodeURIComponent(taskId)}/discard`,
424
+ });
425
+ }
426
+
427
+ export async function listProjects() {
428
+ return bridge().localBackend.request<{ projects: LocalProject[] }>({
429
+ method: 'GET',
430
+ path: '/api/v2/projects',
431
+ });
432
+ }
433
+
434
+ export async function createProject(input: { name: string; folderPath: string }) {
435
+ return bridge().localBackend.request<{ success: boolean; project: LocalProject }>({
436
+ method: 'POST',
437
+ path: '/api/v2/projects',
438
+ body: input,
439
+ });
440
+ }
441
+
442
+ export async function updateProject(
443
+ projectId: string,
444
+ updates: { name?: string; folderPath?: string },
445
+ ) {
446
+ return bridge().localBackend.request<{ success: boolean; project: LocalProject }>({
447
+ method: 'PUT',
448
+ path: `/api/v2/projects/${encodeURIComponent(projectId)}`,
449
+ body: updates,
450
+ });
451
+ }
452
+
453
+ export async function deleteProject(projectId: string) {
454
+ return bridge().localBackend.request<{ success: boolean; detachedChats: number }>({
455
+ method: 'DELETE',
456
+ path: `/api/v2/projects/${encodeURIComponent(projectId)}`,
457
+ });
458
+ }
459
+
460
+ /**
461
+ * 改变会话的项目归属:projectId 为项目 id 表示关联过去,null 表示移出项目。
462
+ * 走通用的 chat settings PATCH(后端会校验项目存在性)。
463
+ */
464
+ export async function updateChatProject(chatId: string, projectId: string | null) {
465
+ return bridge().localBackend.request<{ id: string; projectId: string | null }>({
466
+ method: 'PATCH',
467
+ path: `/api/v2/chats/${encodeURIComponent(chatId)}/settings`,
468
+ body: { projectId },
469
+ });
470
+ }
471
+
472
+ /* ---------------- Local LLM settings ---------------- */
473
+
474
+ export type LlmProvider = 'ollama' | 'openai-compat' | 'anthropic' | 'google' | 'openai-responses';
475
+
476
+ /** 与 src/storage/llm-settings.ts 的同名接口镜像(主进程侧是真源)。 */
477
+ export interface OpenAICompatOverrides {
478
+ supportsUsageInStreaming?: boolean;
479
+ maxTokensField?: 'max_tokens' | 'max_completion_tokens';
480
+ supportsReasoningEffort?: boolean;
481
+ supportsTemperature?: boolean;
482
+ reasoningDeltaFields?: string[];
483
+ cachedTokensFields?: string[];
484
+ }
485
+
486
+ /** sidecar `compat.describe` 返回的旗标描述符——设置页按它渲染,键名不硬编码。 */
487
+ export interface CompatFlagDescriptor {
488
+ key: string;
489
+ field: string;
490
+ /** "bool" | "string-list" | "enum:<opt>,<opt>" */
491
+ kind: string;
492
+ default: unknown;
493
+ description: string;
494
+ }
495
+
496
+ /** 与 src/storage/llm-settings.ts 的同名接口镜像(主进程侧是真源)。 */
497
+ export interface ProviderPresetOverride {
498
+ temperature?: number;
499
+ topP?: number;
500
+ maxTokens?: number;
501
+ reasoningEffort?: string;
502
+ extraBody?: Record<string, unknown>;
503
+ }
504
+
505
+ /** 厂商参数预制选择:缺省/enabled=true = 自动匹配;false = 关闭;override = 钉死一条。 */
506
+ export interface ProviderPresetsChoice {
507
+ enabled?: boolean;
508
+ override?: ProviderPresetOverride;
509
+ }
510
+
511
+ /** sidecar `presets.describe` 返回的注册表行——设置页的预制选择器按它渲染。 */
512
+ export interface ProviderPresetDescriptor {
513
+ host: string | null;
514
+ modelPrefix: string | null;
515
+ temperature: number | null;
516
+ topP: number | null;
517
+ maxTokens: number | null;
518
+ reasoningEffort: string | null;
519
+ extraBody: Record<string, unknown> | null;
520
+ }
521
+
522
+ export interface LlmSettings {
523
+ provider: LlmProvider;
524
+ /** 设置页选中的服务商目录 id;缺省由 baseUrl 回推。 */
525
+ vendorId?: string;
526
+ model: string;
527
+ baseUrl?: string;
528
+ apiKey?: string;
529
+ /** 缺省 = 自动:命中厂商预制时用预制值,否则不下发(厂商服务端默认)。 */
530
+ temperature?: number;
531
+ systemPrompt?: string;
532
+ maxTotalTokens?: number;
533
+ /** 本地命令默认超时(秒)。留空 = 内置默认(headless 30s / 可见终端 60s)。 */
534
+ execTimeoutSeconds?: number;
535
+ /** OpenAI 兼容旗标覆盖;缺省 = 框架按 baseUrl 自动探测。 */
536
+ compat?: OpenAICompatOverrides;
537
+ /** 厂商参数预制选择;缺省 = 框架按 baseUrl+model 自动匹配。 */
538
+ presets?: ProviderPresetsChoice;
539
+ }
540
+
541
+ export async function getCompatFlags() {
542
+ return bridge().localBackend.request<{ flags: CompatFlagDescriptor[]; error?: string }>({
543
+ method: 'GET',
544
+ path: '/api/v2/compat/flags',
545
+ });
546
+ }
547
+
548
+ export async function getProviderPresets() {
549
+ return bridge().localBackend.request<{ presets: ProviderPresetDescriptor[]; error?: string }>({
550
+ method: 'GET',
551
+ path: '/api/v2/llm/presets',
552
+ });
553
+ }
554
+
555
+ /** 当前 baseUrl+model 自动匹配命中的预制(设置页「生效预览」);未命中 = null。 */
556
+ export async function resolveProviderPreset(baseUrl?: string, model?: string) {
557
+ const query = new URLSearchParams();
558
+ if (baseUrl) query.set('baseUrl', baseUrl);
559
+ if (model) query.set('model', model);
560
+ return bridge().localBackend.request<{ preset: ProviderPresetOverride | null; error?: string }>({
561
+ method: 'GET',
562
+ path: `/api/v2/llm/presets/resolve?${query.toString()}`,
563
+ });
564
+ }
565
+
566
+ /**
567
+ * 网关活模型目录的一行(sidecar `models.list`):网关真正接受的 id +
568
+ * models.dev 能力表 join 的结果。`capabilities: 'unknown'` 表示目录没
569
+ * join 上——reasoningLevels 不可用而非为空;选择仍可下发(发现而非
570
+ * 路由白名单)。
571
+ */
572
+ export interface GatewayModelEntry {
573
+ id: string;
574
+ name: string | null;
575
+ window: number | null;
576
+ modalities: string[];
577
+ reasoningLevels: string[];
578
+ pricing: {
579
+ promptPerMtok: number | null;
580
+ completionPerMtok: number | null;
581
+ } | null;
582
+ joinedFrom: string | null;
583
+ capabilities: 'known' | 'unknown';
584
+ }
585
+
586
+ /** `GET /api/v2/llm/models` 的响应。catalogStatus:live 刚拉取 /
587
+ * stale 缓存兜底 / offline 目录不可用(models 为空,error 带原因)。 */
588
+ export interface GatewayModelCatalog {
589
+ models: GatewayModelEntry[];
590
+ catalogStatus: 'live' | 'stale' | 'offline';
591
+ error?: string;
592
+ fetchedAt?: number;
593
+ current?: { model: string | null; reasoningEffort: string | null };
594
+ }
595
+
596
+ /** sidecar `catalog.describe`:服务商默认 URL、wire 种类、可对话模型 id。 */
597
+ export interface CatalogProviderDescriptor {
598
+ id: string;
599
+ apiBaseUrl: string | null;
600
+ envVars: string[];
601
+ wireKind: string;
602
+ models: string[];
603
+ }
604
+
605
+ export async function getCatalogProviders() {
606
+ return bridge().localBackend.request<{ providers: CatalogProviderDescriptor[]; error?: string }>({
607
+ method: 'GET',
608
+ path: '/api/v2/llm/catalog',
609
+ });
610
+ }
611
+
612
+ /** 聊天输入框 / 设置页的模型目录。设置页可带尚未保存的 draft 凭证。 */
613
+ export async function getLlmModels(draft?: {
614
+ baseUrl?: string;
615
+ apiKey?: string;
616
+ provider?: LlmProvider;
617
+ refresh?: boolean;
618
+ }) {
619
+ const query = new URLSearchParams();
620
+ if (draft?.baseUrl) query.set('baseUrl', draft.baseUrl);
621
+ if (draft?.apiKey) query.set('apiKey', draft.apiKey);
622
+ if (draft?.provider) query.set('provider', draft.provider);
623
+ if (draft?.refresh) query.set('refresh', '1');
624
+ const qs = query.toString();
625
+ const suffix = qs ? `?${qs}` : '';
626
+ return bridge().localBackend.request<GatewayModelCatalog>({
627
+ method: 'GET',
628
+ path: `/api/v2/llm/models${suffix}`,
629
+ });
630
+ }
631
+
632
+ /**
633
+ * W4-3: layer-1(sidecar 进程沙箱)态势——主进程在 spawn 时记录,与
634
+ * src/sidecar/types.ts 的 SidecarSandboxPosture 同形。enforcement 沿用
635
+ * layer-3 词汇(partial|none);reason 区分手动关闭与非自愿降级,
636
+ * 设置页「安全」区只对后者告警。
637
+ */
638
+ export interface SidecarSandboxPosture {
639
+ backend: 'seatbelt' | 'bwrap' | 'landlock' | 'windows-restricted-token' | 'none';
640
+ enforcement: 'partial' | 'none';
641
+ reason:
642
+ | 'active'
643
+ | 'disabled_by_option'
644
+ | 'disabled_by_env'
645
+ | 'platform_unsupported'
646
+ | 'seatbelt_missing'
647
+ | 'profile_failed'
648
+ | 'wrap_failed'
649
+ | 'helper_missing';
650
+ }
651
+
652
+ /**
653
+ * 出网管控态势(W-egress-posture):per-host 代理生效 / 端口级退回
654
+ * (reason 含原因,如检测到系统代理)/ 用户显式关闭。与
655
+ * src/sidecar/egress-proxy.ts 的 EgressPosture 同形。
656
+ */
657
+ export interface EgressPosture {
658
+ mode: 'per-host-proxy' | 'port-only-fallback' | 'disabled';
659
+ reason: string | null;
660
+ }
661
+
662
+ export async function getSidecarSandboxPosture() {
663
+ return bridge().localBackend.request<{
664
+ posture: SidecarSandboxPosture | null;
665
+ egress?: EgressPosture | null;
666
+ error?: string;
667
+ }>({
668
+ method: 'GET',
669
+ path: '/api/v2/sidecar/sandbox-posture',
670
+ });
671
+ }
672
+
673
+ export async function getLlmSettings() {
674
+ return bridge().localBackend.request<LlmSettings>({
675
+ method: 'GET',
676
+ path: '/api/v2/local-settings/llm',
677
+ });
678
+ }
679
+
680
+ export async function setLlmSettings(input: LlmSettings) {
681
+ return bridge().localBackend.request<LlmSettings>({
682
+ method: 'POST',
683
+ path: '/api/v2/local-settings/llm',
684
+ body: input,
685
+ });
686
+ }
687
+
688
+ /* ---------------- LLM link diagnosis ---------------- */
689
+
690
+ export interface DiagnoseStep {
691
+ name: string;
692
+ ok: boolean;
693
+ durationMs: number;
694
+ detail: string;
695
+ }
696
+
697
+ export interface DiagnoseResult {
698
+ ok: boolean;
699
+ steps: DiagnoseStep[];
700
+ ambientProxies: string[];
701
+ hint: string | null;
702
+ }
703
+
704
+ export async function diagnoseLlmConnection(input: {
705
+ baseUrl?: string;
706
+ apiKey?: string;
707
+ model?: string;
708
+ }) {
709
+ return bridge().localBackend.request<DiagnoseResult>({
710
+ method: 'POST',
711
+ path: '/api/v2/llm/diagnose',
712
+ body: input,
713
+ });
714
+ }