@co0ontty/wand 4.93.0 → 4.94.0

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 (172) hide show
  1. package/README.md +25 -30
  2. package/dist/ai-text-runner.d.ts +2 -10
  3. package/dist/ai-text-runner.js +13 -74
  4. package/dist/build-info.json +3 -3
  5. package/dist/cli.js +47 -9
  6. package/dist/config.d.ts +2 -3
  7. package/dist/config.js +26 -5
  8. package/dist/core-status-cli.d.ts +1 -1
  9. package/dist/core-status-cli.js +3 -1
  10. package/dist/daemon-connection.d.ts +10 -0
  11. package/dist/daemon-connection.js +28 -0
  12. package/dist/diagnostic-logs.d.ts +10 -0
  13. package/dist/diagnostic-logs.js +121 -0
  14. package/dist/employee-text.js +1 -3
  15. package/dist/git-quick-commit.d.ts +1 -1
  16. package/dist/git-quick-commit.js +2 -9
  17. package/dist/layout-tree.d.ts +3 -1
  18. package/dist/layout-tree.js +8 -0
  19. package/dist/local-model-process.js +20 -0
  20. package/dist/local-model-setup.js +5 -3
  21. package/dist/managed-temp.d.ts +32 -0
  22. package/dist/managed-temp.js +176 -0
  23. package/dist/middleware/api-scope.js +1 -0
  24. package/dist/models/shared.d.ts +0 -6
  25. package/dist/models/shared.js +0 -12
  26. package/dist/models/thinking.d.ts +0 -2
  27. package/dist/models/thinking.js +0 -2
  28. package/dist/models.d.ts +0 -1
  29. package/dist/models.js +3 -11
  30. package/dist/native-session-title.d.ts +1 -1
  31. package/dist/native-session-title.js +1 -1
  32. package/dist/pi-model-discovery.js +5 -17
  33. package/dist/pi-resource-recommendation.d.ts +1 -1
  34. package/dist/pi-resource-recommendation.js +129 -42
  35. package/dist/process-manager-native-history.js +1 -6
  36. package/dist/process-manager.d.ts +0 -1
  37. package/dist/process-manager.js +5 -18
  38. package/dist/provider-catalog.d.ts +1 -2
  39. package/dist/provider-catalog.js +1 -6
  40. package/dist/provider-cli-updater.js +1 -9
  41. package/dist/provider-history-scanner.d.ts +0 -3
  42. package/dist/provider-history-scanner.js +0 -26
  43. package/dist/render-daemon-client.js +6 -29
  44. package/dist/render-structured-client.js +2 -7
  45. package/dist/retention.js +1 -6
  46. package/dist/run-command.d.ts +5 -0
  47. package/dist/run-command.js +14 -1
  48. package/dist/server-session-lifecycle-routes.js +1 -1
  49. package/dist/server-storage-cleanup-routes.d.ts +3 -0
  50. package/dist/server-storage-cleanup-routes.js +19 -0
  51. package/dist/server-workspace-routes.d.ts +4 -1
  52. package/dist/server-workspace-routes.js +75 -9
  53. package/dist/server.js +21 -16
  54. package/dist/service-command-privilege.d.ts +17 -0
  55. package/dist/service-command-privilege.js +55 -0
  56. package/dist/service-manager/index.d.ts +3 -0
  57. package/dist/service-manager/index.js +3 -0
  58. package/dist/service-manager/launchd.d.ts +1 -0
  59. package/dist/service-manager/launchd.js +15 -0
  60. package/dist/service-manager/systemd.d.ts +1 -0
  61. package/dist/service-manager/systemd.js +11 -0
  62. package/dist/session-ai-context.js +17 -6
  63. package/dist/session-completion-state.d.ts +2 -0
  64. package/dist/session-completion-state.js +6 -2
  65. package/dist/session-logger.js +7 -1
  66. package/dist/session-model.d.ts +4 -0
  67. package/dist/session-model.js +13 -4
  68. package/dist/session-registry.d.ts +1 -1
  69. package/dist/session-registry.js +0 -3
  70. package/dist/silicon-employee-draft.js +0 -1
  71. package/dist/speech-service.js +18 -8
  72. package/dist/storage/mappers.js +6 -10
  73. package/dist/storage/migrations.js +14 -0
  74. package/dist/storage/uploads-repository.d.ts +36 -0
  75. package/dist/storage/uploads-repository.js +98 -0
  76. package/dist/storage-cleanup.d.ts +50 -0
  77. package/dist/storage-cleanup.js +344 -0
  78. package/dist/storage.d.ts +3 -1
  79. package/dist/storage.js +4 -0
  80. package/dist/structured-failure.d.ts +2 -0
  81. package/dist/structured-failure.js +9 -0
  82. package/dist/structured-grok-adapter.js +4 -4
  83. package/dist/structured-opencode-adapter.js +4 -9
  84. package/dist/structured-provider-common.d.ts +1 -2
  85. package/dist/structured-provider-common.js +3 -6
  86. package/dist/structured-session-manager-replay.js +2 -6
  87. package/dist/structured-session-manager.d.ts +1 -2
  88. package/dist/structured-session-manager.js +32 -25
  89. package/dist/task-organize-contract.d.ts +41 -0
  90. package/dist/task-organize-contract.js +4 -0
  91. package/dist/task-title-organize-service.d.ts +21 -0
  92. package/dist/task-title-organize-service.js +168 -0
  93. package/dist/task-title-organize.d.ts +77 -0
  94. package/dist/task-title-organize.js +346 -0
  95. package/dist/terminal-daemon-client.js +10 -48
  96. package/dist/time-utils.d.ts +2 -0
  97. package/dist/time-utils.js +7 -0
  98. package/dist/tui/attach.js +6 -5
  99. package/dist/tui/commands.d.ts +4 -1
  100. package/dist/tui/commands.js +36 -25
  101. package/dist/tui/index.js +2 -2
  102. package/dist/tui/service-actions.js +5 -4
  103. package/dist/tui/service-command.d.ts +6 -0
  104. package/dist/tui/service-command.js +40 -0
  105. package/dist/tui/service-panel.js +9 -8
  106. package/dist/types.d.ts +4 -4
  107. package/dist/upload-reference-guard.d.ts +4 -0
  108. package/dist/upload-reference-guard.js +29 -0
  109. package/dist/upload-routes.d.ts +4 -0
  110. package/dist/upload-routes.js +121 -21
  111. package/dist/user-profile.d.ts +1 -1
  112. package/dist/user-profile.js +8 -3
  113. package/dist/web-ui/content/ai-teams.js +9 -9
  114. package/dist/web-ui/content/module-manifest.json +1 -1
  115. package/dist/web-ui/content/modules/{2548dbe0d0783577/chat-render-EODYMQF6.js → 0051bd3d55f10ab7/chat-render-RY2I2CF3.js} +1 -1
  116. package/dist/web-ui/content/modules/{2548dbe0d0783577/chunk-ZTEQSXPA.js → 0051bd3d55f10ab7/chunk-2F6J2MKX.js} +1 -1
  117. package/dist/web-ui/content/modules/{2548dbe0d0783577/chunk-A33YPCY6.js → 0051bd3d55f10ab7/chunk-7AFM245W.js} +1 -1
  118. package/dist/web-ui/content/modules/{2548dbe0d0783577/chunk-QNUWUMKW.js → 0051bd3d55f10ab7/chunk-MHX2QXIS.js} +1 -1
  119. package/dist/web-ui/content/modules/{2548dbe0d0783577/chunk-WGADUEIT.js → 0051bd3d55f10ab7/chunk-RAAPETNH.js} +1 -1
  120. package/dist/web-ui/content/modules/{2548dbe0d0783577/chunk-I3UYDQLS.js → 0051bd3d55f10ab7/chunk-RXKBHYUR.js} +2 -2
  121. package/dist/web-ui/content/modules/{2548dbe0d0783577/chunk-3TCUG766.js → 0051bd3d55f10ab7/chunk-U4EDP4TI.js} +1 -1
  122. package/dist/web-ui/content/modules/{2548dbe0d0783577/chunk-HK7RBU3M.js → 0051bd3d55f10ab7/chunk-WHXQR7JT.js} +1 -1
  123. package/dist/web-ui/content/modules/0051bd3d55f10ab7/employee-card-NV34TZIH.js +1 -0
  124. package/dist/web-ui/content/modules/{2548dbe0d0783577/host-2PU6TV2I.js → 0051bd3d55f10ab7/host-2IVIZ2YC.js} +1 -1
  125. package/dist/web-ui/content/modules/{2548dbe0d0783577/host-BPYNMKFC.js → 0051bd3d55f10ab7/host-AZXNFCRW.js} +1 -1
  126. package/dist/web-ui/content/modules/{2548dbe0d0783577/host-HZIKPWYY.js → 0051bd3d55f10ab7/host-EPEQX22R.js} +1 -1
  127. package/dist/web-ui/content/modules/{2548dbe0d0783577/host-YELXZID4.js → 0051bd3d55f10ab7/host-FZYIMOLC.js} +1 -1
  128. package/dist/web-ui/content/modules/{2548dbe0d0783577/host-OGD6AWHL.js → 0051bd3d55f10ab7/host-G6GL4SNL.js} +1 -1
  129. package/dist/web-ui/content/modules/{2548dbe0d0783577/host-E3SCPOIU.js → 0051bd3d55f10ab7/host-PEILC5NU.js} +1 -1
  130. package/dist/web-ui/content/modules/{2548dbe0d0783577/host-OWD32FZ3.js → 0051bd3d55f10ab7/host-PHHNGVNL.js} +2 -2
  131. package/dist/web-ui/content/modules/{2548dbe0d0783577/host-CXDAFMXX.js → 0051bd3d55f10ab7/host-Q524TMSR.js} +1 -1
  132. package/dist/web-ui/content/modules/0051bd3d55f10ab7/host-RO5MB6RB.js +69 -0
  133. package/dist/web-ui/content/modules/{2548dbe0d0783577/host-J5XRAQFP.js → 0051bd3d55f10ab7/host-WOQRK6NG.js} +1 -1
  134. package/dist/web-ui/content/modules/{2548dbe0d0783577/host-HKXE2DOT.js → 0051bd3d55f10ab7/host-XNIR3NTJ.js} +1 -1
  135. package/dist/web-ui/content/modules/{2548dbe0d0783577/host-4E4H4JZJ.js → 0051bd3d55f10ab7/host-XRVOIEP6.js} +1 -1
  136. package/dist/web-ui/content/modules/0051bd3d55f10ab7/host-ZDKOQEMS.js +1 -0
  137. package/dist/web-ui/content/modules/0051bd3d55f10ab7/host-bridge-PMQZRG5G.js +1 -0
  138. package/dist/web-ui/content/modules/{2548dbe0d0783577/library-bridge-Y3Y24CPQ.js → 0051bd3d55f10ab7/library-bridge-DTMQIHVJ.js} +1 -1
  139. package/dist/web-ui/content/modules/{2548dbe0d0783577/page-GQBUU7T7.js → 0051bd3d55f10ab7/page-PY4JUL4W.js} +1 -1
  140. package/dist/web-ui/content/modules/{2548dbe0d0783577/panel-PG662OC5.js → 0051bd3d55f10ab7/panel-BBQFC55Q.js} +1 -1
  141. package/dist/web-ui/content/modules/0051bd3d55f10ab7/runtime.js +974 -0
  142. package/dist/web-ui/content/modules/0051bd3d55f10ab7/task-board-host-PXZSWBBG.js +2 -0
  143. package/dist/web-ui/content/modules/0051bd3d55f10ab7/workspace-agent-picker-DJSECSY6.js +1 -0
  144. package/dist/web-ui/content/modules/{2548dbe0d0783577/workspace-window-HV4T3WKR.js → 0051bd3d55f10ab7/workspace-window-MNMKSOBG.js} +1 -1
  145. package/dist/web-ui/content/styles.css +1 -1
  146. package/dist/web-ui/embedded-assets.d.ts +27 -27
  147. package/dist/web-ui/embedded-assets.js +4 -4
  148. package/dist/web-ui/provider-identity.d.ts +4 -5
  149. package/dist/web-ui/provider-identity.js +4 -10
  150. package/dist/web-ui/react/workspaces/sidebar-manage.d.ts +39 -0
  151. package/dist/web-ui/react/workspaces/sidebar-manage.js +149 -0
  152. package/dist/web-ui/react/workspaces/types.d.ts +408 -0
  153. package/dist/web-ui/react/workspaces/types.js +16 -0
  154. package/dist/web-ui/session-activity.js +1 -1
  155. package/package.json +2 -2
  156. package/dist/structured-gemini-adapter.d.ts +0 -40
  157. package/dist/structured-gemini-adapter.js +0 -208
  158. package/dist/web-ui/content/modules/2548dbe0d0783577/chunk-XGVT4ZWT.js +0 -1
  159. package/dist/web-ui/content/modules/2548dbe0d0783577/employee-card-LJ3TX72B.js +0 -1
  160. package/dist/web-ui/content/modules/2548dbe0d0783577/host-ITV2QZ4A.js +0 -1
  161. package/dist/web-ui/content/modules/2548dbe0d0783577/host-XNWC7KPR.js +0 -69
  162. package/dist/web-ui/content/modules/2548dbe0d0783577/host-bridge-G5HEVADT.js +0 -1
  163. package/dist/web-ui/content/modules/2548dbe0d0783577/runtime.js +0 -946
  164. package/dist/web-ui/content/modules/2548dbe0d0783577/task-board-host-RAN2Z2KR.js +0 -2
  165. package/dist/web-ui/content/modules/2548dbe0d0783577/workspace-agent-picker-FXLH6N5K.js +0 -1
  166. /package/dist/web-ui/content/modules/{2548dbe0d0783577/chunk-YX475LXO.js → 0051bd3d55f10ab7/chunk-6C6A2RHG.js} +0 -0
  167. /package/dist/web-ui/content/modules/{2548dbe0d0783577/chunk-NP35GDQ3.js → 0051bd3d55f10ab7/chunk-ECWYA3U5.js} +0 -0
  168. /package/dist/web-ui/content/modules/{2548dbe0d0783577/chunk-47EVOSJS.js → 0051bd3d55f10ab7/chunk-T35D57FW.js} +0 -0
  169. /package/dist/web-ui/content/modules/{2548dbe0d0783577 → 0051bd3d55f10ab7}/chunk-YOXXNXOX.js +0 -0
  170. /package/dist/web-ui/content/modules/{2548dbe0d0783577/chunk-C3K34WJD.js → 0051bd3d55f10ab7/chunk-YQJ6XYV7.js} +0 -0
  171. /package/dist/web-ui/content/modules/{2548dbe0d0783577/host-J6HBDK4A.js → 0051bd3d55f10ab7/host-2AQAIUNO.js} +0 -0
  172. /package/dist/web-ui/content/modules/{2548dbe0d0783577 → 0051bd3d55f10ab7}/sha256-ANXV72UC.js +0 -0
@@ -1,4 +1,4 @@
1
- import { execFile, spawnSync, } from "node:child_process";
1
+ import { execFile, spawn, spawnSync, } from "node:child_process";
2
2
  import { buildChildEnv } from "./env-utils.js";
3
3
  /** Default cap for captured stdout/stderr of one-shot CLI probes (1 MiB). */
4
4
  export const RUN_COMMAND_MAX_BUFFER = 1024 * 1024;
@@ -30,6 +30,19 @@ export function runCommandSync(file, args, options = {}) {
30
30
  shell: false,
31
31
  });
32
32
  }
33
+ /** Foreground operations such as sudo: the terminal owns input (including passwords).
34
+ * No capture, shell, or timeout: cancellation belongs to the user, and Core drain
35
+ * may legitimately wait longer than the bounded probe timeout.
36
+ */
37
+ export function runCommandInteractive(file, args) {
38
+ return new Promise((resolve, reject) => {
39
+ const child = spawn(file, [...args], {
40
+ env: commandEnv({}), stdio: "inherit", shell: false,
41
+ });
42
+ child.once("error", reject);
43
+ child.once("exit", (code, signal) => resolve(code ?? (signal === "SIGINT" ? 130 : 1)));
44
+ });
45
+ }
33
46
  /** Async variant with the same env/timeout/output-cap contract; never rejects. */
34
47
  export function runCommand(file, args, options = {}) {
35
48
  const execOptions = {
@@ -38,7 +38,7 @@ export function registerSessionLifecycleRoutes(app, ctx) {
38
38
  const selectedEmployeeCandidate = employee ? selectEmployeeCandidate(employee) : null;
39
39
  const employeeAgent = selectedEmployeeCandidate?.agent;
40
40
  if (body.provider && !isSessionProvider(body.provider)) {
41
- res.status(400).json({ error: "结构化会话当前仅支持 Claude、Codex、OpenCode、Grok、Qoder、Pi 或 Gemini provider。" });
41
+ res.status(400).json({ error: "结构化会话当前仅支持 Claude、Codex、OpenCode、Grok、Qoder 或 Pi provider。" });
42
42
  return;
43
43
  }
44
44
  const overrideCli = body.overrideCli === true;
@@ -0,0 +1,3 @@
1
+ import type { Express, RequestHandler } from "express";
2
+ import type { StorageCleanup } from "./storage-cleanup.js";
3
+ export declare function registerStorageCleanupRoutes(app: Express, cleanup: StorageCleanup, requireAdmin: RequestHandler): void;
@@ -0,0 +1,19 @@
1
+ import { asyncRoute } from "./express-async.js";
2
+ import { HttpError } from "./http-error.js";
3
+ export function registerStorageCleanupRoutes(app, cleanup, requireAdmin) {
4
+ app.use("/api/storage-cleanup", requireAdmin, (_req, res, next) => { res.set("Cache-Control", "no-store"); next(); });
5
+ app.get("/api/storage-cleanup/status", (_req, res) => { res.json(cleanup.status()); });
6
+ app.get("/api/storage-cleanup/preview", asyncRoute(async (_req, res) => {
7
+ if (cleanup.status().running)
8
+ throw new HttpError(409, "cleanup_busy", "清理正在进行,请稍后重试。");
9
+ res.json(await cleanup.run());
10
+ }));
11
+ app.post("/api/storage-cleanup/run", asyncRoute(async (req, res) => {
12
+ if (req.body !== undefined && (!req.body || typeof req.body !== "object" || Array.isArray(req.body) || Object.keys(req.body).length)) {
13
+ throw new HttpError(400, "bad_request", "清理范围和保留期由服务端固定,不接受路径或命令参数。");
14
+ }
15
+ if (cleanup.status().running)
16
+ throw new HttpError(409, "cleanup_busy", "清理正在进行,请稍后重试。");
17
+ res.json(await cleanup.run(true));
18
+ }));
19
+ }
@@ -3,6 +3,7 @@ import type { SessionRegistry } from "./session-registry.js";
3
3
  import type { WandStorage } from "./storage.js";
4
4
  import { type LayoutNode, type TaskWindowLayout } from "./types.js";
5
5
  import { type AutoTaskTitleOptions } from "./task-title-service.js";
6
+ import { type TitleJudge } from "./task-title-organize.js";
6
7
  /** 校验并清洗前端提交的布局树;非法输入返回 null(空工作空间)。 */
7
8
  export declare function sanitizeLayout(value: unknown): LayoutNode | null;
8
9
  /** 校验任务级工作窗口集合;旧版单棵布局会兼容升级成一个 window。 */
@@ -14,4 +15,6 @@ export declare function sanitizeTaskLayout(value: unknown): TaskWindowLayout | n
14
15
  * - POST 只建项目实体,**不启动任何会话**;会话由工作空间内「+」标签按需创建并绑定 workspaceId。
15
16
  * - 布局树由前端 allotment 构造,PUT 时经 sanitizeLayout 校验/规整后落库。
16
17
  */
17
- export declare function registerWorkspaceRoutes(app: Express, storage: WandStorage, sessions?: SessionRegistry, titleOptions?: AutoTaskTitleOptions): void;
18
+ export declare function registerWorkspaceRoutes(app: Express, storage: WandStorage, sessions?: SessionRegistry, titleOptions?: AutoTaskTitleOptions, organize?: {
19
+ judge?: TitleJudge;
20
+ }): void;
@@ -16,9 +16,11 @@ import { attachUnboundSessionsToWorkspace, normalizeProjectCwd, projectCwdForSes
16
16
  import { defaultMilestoneIdForWrite, resolvedMilestoneFields, scopedMilestoneId } from "./milestone-scope.js";
17
17
  import { isUnnamedWorkspaceTaskName, UNNAMED_WORKSPACE_TASK_NAME } from "./wand-task-sync.js";
18
18
  import { isSessionProvider } from "./session-provider.js";
19
- import { firstLayoutTabId } from "./layout-tree.js";
19
+ import { firstLayoutTabId, wrapLegacyTaskLayout } from "./layout-tree.js";
20
20
  import { parentTaskIdFrom } from "./task-agent-parsing.js";
21
21
  import { taskTitleService, refreshAutoBoardTaskTitles } from "./task-title-service.js";
22
+ import { organizeSelectedByTitle, titleJudgeForStorage } from "./task-title-organize-service.js";
23
+ import { TITLE_ORGANIZE_MAX_SESSIONS, TITLE_ORGANIZE_MAX_TASKS } from "./task-title-organize.js";
22
24
  import { provisionalTaskTitleFromDescription } from "./task-title.js";
23
25
  import { badRequest, surfaceDomainError } from "./http-error.js";
24
26
  function workspaceSessionTitle(session, names = {}) {
@@ -286,13 +288,8 @@ function layoutHasTab(node, tabId) {
286
288
  /** 校验任务级工作窗口集合;旧版单棵布局会兼容升级成一个 window。 */
287
289
  export function sanitizeTaskLayout(value) {
288
290
  const legacy = sanitizeLayoutNode(value);
289
- if (legacy) {
290
- return {
291
- type: "windows",
292
- windows: [{ id: "window-legacy", layout: legacy, activeTabId: firstLayoutTabId(legacy) }],
293
- activeWindowId: "window-legacy",
294
- };
295
- }
291
+ if (legacy)
292
+ return wrapLegacyTaskLayout(legacy);
296
293
  if (!value || typeof value !== "object")
297
294
  return null;
298
295
  const record = value;
@@ -327,7 +324,7 @@ export function sanitizeTaskLayout(value) {
327
324
  * - POST 只建项目实体,**不启动任何会话**;会话由工作空间内「+」标签按需创建并绑定 workspaceId。
328
325
  * - 布局树由前端 allotment 构造,PUT 时经 sanitizeLayout 校验/规整后落库。
329
326
  */
330
- export function registerWorkspaceRoutes(app, storage, sessions, titleOptions = {}) {
327
+ export function registerWorkspaceRoutes(app, storage, sessions, titleOptions = {}, organize = {}) {
331
328
  taskTitleService(storage, titleOptions);
332
329
  // 列出所有项目(按创建时间降序:新建在前,打开不会改位置)
333
330
  app.get("/api/workspaces", (req, res) => {
@@ -475,6 +472,75 @@ export function registerWorkspaceRoutes(app, storage, sessions, titleOptions = {
475
472
  res.json({ ok: true, layout });
476
473
  });
477
474
  // ── 任务(Task = 命名 + 独立 worktree + 一组标签)──
475
+ // 侧边栏多选后的按标题归类:任务归入同类父任务,会话归入标题对应的任务。
476
+ // 不确定的不移动。选中任务时,其下会话即使没有被逐条勾选也在范围内。
477
+ app.post("/api/tasks/organize-by-title", asyncRoute(async (req, res) => {
478
+ const body = req.body;
479
+ if (body?.mode !== undefined && body.mode !== "auto" && body.mode !== "all") {
480
+ res.status(400).json({ error: "归类范围必须是 auto 或 all。" });
481
+ return;
482
+ }
483
+ const ids = (value, limit) => {
484
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string"))
485
+ return null;
486
+ return [...new Set(value.map((item) => item.trim()).filter(Boolean))].slice(0, limit + 1);
487
+ };
488
+ let taskIds = ids(body?.taskIds, TITLE_ORGANIZE_MAX_TASKS);
489
+ let sessionIds = body?.sessionIds === undefined ? [] : ids(body.sessionIds, TITLE_ORGANIZE_MAX_SESSIONS);
490
+ if (body?.target !== undefined) {
491
+ const target = body.target;
492
+ if (!target || typeof target !== "object" || Array.isArray(target)
493
+ || typeof target.kind !== "string" || !["all", "workspace", "directory"].includes(target.kind)
494
+ || (target.kind === "workspace" && (typeof target.workspaceId !== "string" || !storage.getWorkspace(target.workspaceId)))
495
+ || (target.kind === "directory" && (typeof target.cwd !== "string" || !target.cwd.trim()))
496
+ || body.taskIds !== undefined || body.sessionIds !== undefined) {
497
+ res.status(400).json({ error: "归类目标无效。" });
498
+ return;
499
+ }
500
+ const cwd = typeof target.cwd === "string" ? normalizeProjectCwd(target.cwd) : "";
501
+ taskIds = storage.listAllWorkspaceTasks().filter(task => {
502
+ if (task.archived)
503
+ return false;
504
+ if (target.kind === "all")
505
+ return true;
506
+ if (target.kind === "workspace")
507
+ return task.workspaceId === target.workspaceId;
508
+ const workspace = storage.getWorkspace(task.workspaceId);
509
+ // Match the same grouping as GET /api/tasks, including synthetic global directories.
510
+ return normalizeProjectCwd(workspace && !isGlobalWorkspace(workspace)
511
+ ? workspace.cwd : taskRuntimeCwd(task, workspace)) === cwd;
512
+ }).map(task => task.id);
513
+ sessionIds = storage.loadSessionsSlim().filter(session => {
514
+ if (session.archived || session.workspaceTaskId)
515
+ return false;
516
+ if (target.kind === "all")
517
+ return true;
518
+ const directory = target.kind === "workspace" ? storage.getWorkspace(String(target.workspaceId))?.cwd : cwd;
519
+ return projectCwdForSession(session) === normalizeProjectCwd(directory || "");
520
+ }).map(session => session.id);
521
+ }
522
+ if (!taskIds || !sessionIds) {
523
+ res.status(400).json({ error: "需要任务 id 列表,会话 id 列表可选。" });
524
+ return;
525
+ }
526
+ if (!taskIds.length && body?.target === undefined) {
527
+ res.status(400).json({ error: "请选择要归类的任务。" });
528
+ return;
529
+ }
530
+ if (body?.target === undefined && (taskIds.length > TITLE_ORGANIZE_MAX_TASKS || sessionIds.length > TITLE_ORGANIZE_MAX_SESSIONS)) {
531
+ res.status(400).json({ error: `一次最多归类 ${TITLE_ORGANIZE_MAX_TASKS} 个任务、${TITLE_ORGANIZE_MAX_SESSIONS} 个会话。` });
532
+ return;
533
+ }
534
+ const result = await organizeSelectedByTitle(storage, { taskIds, sessionIds, mode: body?.mode === "all" ? "all" : "auto" }, {
535
+ judge: organize.judge ?? titleJudgeForStorage(storage, titleOptions),
536
+ refreshSession: (sessionId) => sessions?.refreshSessionWorkspace(sessionId),
537
+ });
538
+ if (!result.foundTasks && body?.target === undefined) {
539
+ res.status(404).json({ error: "未找到所选任务。" });
540
+ return;
541
+ }
542
+ res.json(result);
543
+ }));
478
544
  // 不依赖项目的独立任务:挂到隐藏的全局空间,目录可省略(使用全局临时目录)。
479
545
  app.post("/api/tasks", asyncRoute(async (req, res) => {
480
546
  const workspace = storage.ensureGlobalWorkspace();
package/dist/server.js CHANGED
@@ -16,7 +16,7 @@ import { createExtensionCors } from "./middleware/cors.js";
16
16
  import { ensureCertificates } from "./cert.js";
17
17
  import { getDefaultModelForProvider, getProviderDefaultModels, isExecutionMode, resolveConfigDir, writePreferenceToStorage, } from "./config.js";
18
18
  import { ModelCatalogService } from "./models.js";
19
- import { fallbackProviderModel, sessionEmployeeFields } from "./session-model.js";
19
+ import { fallbackProviderModel, readProviderModelPreferences, sessionEmployeeFields } from "./session-model.js";
20
20
  import { ProcessManager } from "./process-manager.js";
21
21
  import { SessionLogger } from "./session-logger.js";
22
22
  import { coreHarnessAgentDir } from "./harness-engine.js";
@@ -35,6 +35,8 @@ import { registerSpeechRoutes } from "./server-speech-routes.js";
35
35
  import { SpeechPolisherService } from "./speech-polisher-service.js";
36
36
  import { SPEECH_POLISHER_KEY } from "./speech-polisher-identity.js";
37
37
  import { registerLocalModelRoutes } from "./server-local-model-routes.js";
38
+ import { createStorageCleanup } from "./storage-cleanup.js";
39
+ import { registerStorageCleanupRoutes } from "./server-storage-cleanup-routes.js";
38
40
  import { LocalModelSetupService } from "./local-model-setup.js";
39
41
  import { DecisionExpertService } from "./decision-expert-service.js";
40
42
  import { DECISION_EXPERT_KEY } from "./decision-expert-identity.js";
@@ -80,7 +82,9 @@ import { safeServiceInstalled } from "./tui/runtime-utils.js";
80
82
  import { toSessionDetailDTO } from "./session-transport.js";
81
83
  import { compactToolMessagesForTransport, windowMessagesForTransport } from "./message-truncator.js";
82
84
  import { enrichStructuredMessages } from "./structured-client-protocol.js";
83
- import { registerUploadRoutes } from "./upload-routes.js";
85
+ import { registerUploadRoutes, invalidateUploadUsage } from "./upload-routes.js";
86
+ import { uploadReferenceGuard } from "./upload-reference-guard.js";
87
+ import { configTemporaryRoot } from "./managed-temp.js";
84
88
  import { optimizePrompt, PromptOptimizeError } from "./prompt-optimizer.js";
85
89
  import { resolveDatabasePath, WandStorage } from "./storage.js";
86
90
  import { deepRepairRuntimePath, formatPathRepairSummary, repairRuntimePath } from "./path-repair.js";
@@ -250,19 +254,9 @@ export async function startServer(config, configPath, options = {}) {
250
254
  const decisions = new DecisionService(config.localDecision);
251
255
  const decisionExpert = new DecisionExpertService({ local: decisions, free: openRouter, config,
252
256
  employee: () => storage.getSystemSiliconEmployee(DECISION_EXPERT_KEY) });
253
- // 默认模型优先读存储(UI 改设置后实时生效),未设置时由 getPreference 回落到 config。
254
- const getCurrentDefaultModels = () => ({
255
- claude: storage.getPreference("pref:defaultModel", config.defaultModel ?? ""),
256
- codex: storage.getPreference("pref:defaultCodexModel", config.defaultCodexModel ?? ""),
257
- opencode: storage.getPreference("pref:defaultOpenCodeModel", config.defaultOpenCodeModel ?? ""),
258
- grok: storage.getPreference("pref:defaultGrokModel", config.defaultGrokModel ?? ""),
259
- qoder: storage.getPreference("pref:defaultQoderModel", config.defaultQoderModel ?? ""),
260
- pi: storage.getPreference("pref:defaultPiModel", config.defaultPiModel ?? ""),
261
- gemini: storage.getPreference("pref:defaultGeminiModel", config.defaultGeminiModel ?? ""),
262
- });
263
257
  const getModelRefreshOptions = () => {
264
258
  const injected = options.modelRefreshOptions?.() ?? {};
265
- const currentDefaults = getCurrentDefaultModels();
259
+ const currentDefaults = readProviderModelPreferences(config, storage);
266
260
  return {
267
261
  storage,
268
262
  managedPiModels: () => openRouter.catalog(),
@@ -284,6 +278,9 @@ export async function startServer(config, configPath, options = {}) {
284
278
  // Clients receive its persisted snapshot via GET /api/models.
285
279
  const modelCatalog = new ModelCatalogService(getModelRefreshOptions);
286
280
  const configDir = resolveConfigDir(configPath);
281
+ const storageCleanup = createStorageCleanup(configDir, { uploads: storage.uploads,
282
+ uploadsActive: () => sessionRegistry.listSlim().some(session => (session.sessionKind === "structured" && session.status === "running") || (session.queuedMessages?.length ?? 0) > 0),
283
+ onUploadRemoved: invalidateUploadUsage });
287
284
  const speech = new SpeechService(storage, configDir);
288
285
  const localModels = new LocalModelSetupService({ configDir, speech, decisions,
289
286
  decisionConfig: () => config.localDecision ?? { enabled: false, pythonPath: "", modelPath: "" },
@@ -348,7 +345,6 @@ export async function startServer(config, configPath, options = {}) {
348
345
  grok: "grokModels",
349
346
  qoder: "qoderModels",
350
347
  pi: "piModels",
351
- gemini: "geminiModels",
352
348
  }[provider];
353
349
  return cache[key].map((entry) => entry.id);
354
350
  };
@@ -454,6 +450,7 @@ export async function startServer(config, configPath, options = {}) {
454
450
  // Every /api family declares its scope in src/middleware/api-scope.ts; unlisted
455
451
  // paths are admin-only for connected apps (default-deny).
456
452
  app.use("/api", createApiScopeMiddleware());
453
+ app.use("/api", uploadReferenceGuard(storage.uploads));
457
454
  app.use(userMemoryOperationLog(storage));
458
455
  const userMemory = new UserMemoryService({
459
456
  storage, config, free: openRouter,
@@ -583,11 +580,12 @@ export async function startServer(config, configPath, options = {}) {
583
580
  free: openRouter,
584
581
  });
585
582
  registerClaudeHistoryRoutes(app, { processes, storage });
586
- registerWorkspaceRoutes(app, storage, sessionRegistry, { config });
583
+ registerWorkspaceRoutes(app, storage, sessionRegistry, { config, free: openRouter });
587
584
  registerMissionRoutes(app, missions);
588
- registerUploadRoutes(app, sessionRegistry);
585
+ registerUploadRoutes(app, sessionRegistry, { uploads: storage.uploads, temporaryRoot: configTemporaryRoot(configDir) });
589
586
  registerSpeechRoutes(app, { speech, requireSessions, requireAdmin, polisher: speechPolisher });
590
587
  registerLocalModelRoutes(app, { models: localModels, requireSessions, requireAdmin });
588
+ registerStorageCleanupRoutes(app, storageCleanup, requireAdmin);
591
589
  app.post("/api/optimize-prompt", asyncRoute(async (req, res) => {
592
590
  const body = (req.body ?? {});
593
591
  const text = typeof body.text === "string" ? body.text : "";
@@ -633,6 +631,10 @@ export async function startServer(config, configPath, options = {}) {
633
631
  res.status(400).json({ error: "PTY 只能选择 CLI 工具。" });
634
632
  return;
635
633
  }
634
+ if (body.provider !== undefined && !isSessionProvider(body.provider)) {
635
+ res.status(400).json({ error: "请选择有效的执行工具。" });
636
+ return;
637
+ }
636
638
  const interactiveShell = body.shell === true;
637
639
  // 连接应用只能启动 Provider CLI 会话(provider 字段 + 该 CLI 的标准可执行名)。
638
640
  // 任意自定义命令字符串需要 admin。shell:true 是原生客户端“空白终端”(服务端配置的
@@ -1012,6 +1014,8 @@ export async function startServer(config, configPath, options = {}) {
1012
1014
  // preferences, or stop a live execution. Startup recovery is already attached.
1013
1015
  if (!testMode)
1014
1016
  daemonMaintenance.start();
1017
+ if (!testMode && config.autoCleanupTemporaryFiles !== false)
1018
+ storageCleanup.start();
1015
1019
  // ── Auto-update logic ──
1016
1020
  // 同一目标版本自动安装失败后指数退避(30 分钟起,最长 6 小时),避免每轮轮询都重试
1017
1021
  // 同一个必失败的安装并刷屏;用户主动触发(切通道/打开开关)时清零重试。
@@ -1136,6 +1140,7 @@ export async function startServer(config, configPath, options = {}) {
1136
1140
  const teamShutdown = aiTeams.dispose();
1137
1141
  await scheduledTasks.dispose();
1138
1142
  await daemonMaintenance.stop();
1143
+ await storageCleanup.stop();
1139
1144
  decisions.dispose();
1140
1145
  openRouter.dispose();
1141
1146
  await userMemory.dispose();
@@ -0,0 +1,17 @@
1
+ import { type ServiceScope } from "./service-manager/index.js";
2
+ export declare function resolveServiceCommandScope(command: string, args: string[]): ServiceScope;
3
+ export interface ServicePrivilegeRuntime {
4
+ platform: NodeJS.Platform;
5
+ uid: number | undefined;
6
+ interactive: boolean;
7
+ node: string;
8
+ cli: string;
9
+ execArgv: string[];
10
+ run(file: string, args: readonly string[]): Promise<number>;
11
+ report(message: string): void;
12
+ }
13
+ /** null means continue in this process; a number is the elevated child's exit code.
14
+ * Re-enter the same CLI (not a PATH-resolved wand) before loading service code.
15
+ * Scope/config are pinned so sudo's HOME/PATH cannot target a different service.
16
+ */
17
+ export declare function elevateServiceCommand(command: string, args: string[], configPath: string, scope: ServiceScope, runtime?: ServicePrivilegeRuntime): Promise<number | null>;
@@ -0,0 +1,55 @@
1
+ import path from "node:path";
2
+ import process from "node:process";
3
+ import { DEFAULT_SERVICE_SCOPE, detectInstalledScope } from "./service-manager/index.js";
4
+ import { runCommandInteractive } from "./run-command.js";
5
+ const MUTATING_SERVICE_COMMANDS = new Set([
6
+ "service:install", "service:uninstall", "service:start", "service:stop", "service:restart",
7
+ ]);
8
+ export function resolveServiceCommandScope(command, args) {
9
+ if (args.includes("--user"))
10
+ return "user";
11
+ if (args.includes("--system"))
12
+ return "system";
13
+ return command === "service:install" ? DEFAULT_SERVICE_SCOPE : detectInstalledScope() ?? DEFAULT_SERVICE_SCOPE;
14
+ }
15
+ /** null means continue in this process; a number is the elevated child's exit code.
16
+ * Re-enter the same CLI (not a PATH-resolved wand) before loading service code.
17
+ * Scope/config are pinned so sudo's HOME/PATH cannot target a different service.
18
+ */
19
+ export async function elevateServiceCommand(command, args, configPath, scope, runtime = {
20
+ platform: process.platform, uid: process.getuid?.(),
21
+ interactive: Boolean(process.stdin.isTTY && process.stderr.isTTY),
22
+ node: process.execPath, cli: process.argv[1], execArgv: process.execArgv,
23
+ run: runCommandInteractive, report: (message) => process.stderr.write(`[wand] ${message}\n`),
24
+ }) {
25
+ if (!MUTATING_SERVICE_COMMANDS.has(command) || scope !== "system" || runtime.uid === 0
26
+ || !["darwin", "linux"].includes(runtime.platform))
27
+ return null;
28
+ const forwarded = [];
29
+ for (let i = 0; i < args.length; i++) {
30
+ if (args[i] === "--user" || args[i] === "--system")
31
+ continue;
32
+ if (args[i] === "-c" || args[i] === "--config") {
33
+ i++;
34
+ continue;
35
+ }
36
+ forwarded.push(args[i]);
37
+ }
38
+ if (runtime.interactive)
39
+ runtime.report("系统服务操作需要管理员权限,正在申请 sudo;请在终端输入密码(输入不会显示)。");
40
+ try {
41
+ const code = await runtime.run("sudo", [
42
+ ...(runtime.interactive ? [] : ["-n"]), "--", runtime.node, ...runtime.execArgv,
43
+ path.resolve(runtime.cli), ...forwarded, "--system", "-c", path.resolve(configPath),
44
+ ]);
45
+ if (code !== 0)
46
+ runtime.report(runtime.interactive
47
+ ? "管理员授权或服务操作未完成;服务命令已停止,请查看上方错误。"
48
+ : "当前没有交互终端,无法输入 sudo 密码。请在终端运行同一命令以自动申请授权。");
49
+ return code;
50
+ }
51
+ catch (error) {
52
+ runtime.report(`无法申请 sudo:${error instanceof Error ? error.message : String(error)}`);
53
+ return 1;
54
+ }
55
+ }
@@ -11,6 +11,8 @@ export interface ServiceManager {
11
11
  unitPath(scope: ServiceScope): string;
12
12
  /** Command the installed unit runs, or null when absent/unreadable. */
13
13
  readEntrypoint(scope: ServiceScope): string | null;
14
+ /** Config used by the installed Server; restart must drain this exact process. */
15
+ readConfigPath(scope: ServiceScope): string | null;
14
16
  }
15
17
  /** The manager for the current platform, or null where Wand has no service integration. */
16
18
  export declare function currentServiceManager(platform?: NodeJS.Platform): ServiceManager | null;
@@ -20,3 +22,4 @@ export declare function detectInstalledServiceScopes(): ServiceScope[];
20
22
  /** Auto-detect an installed scope; system preferred. */
21
23
  export declare function detectInstalledScope(): ServiceScope | null;
22
24
  export declare function readServiceEntrypoint(scope: ServiceScope): string | null;
25
+ export declare function readServiceConfigPath(scope: ServiceScope): string | null;
@@ -34,3 +34,6 @@ export function detectInstalledScope() {
34
34
  export function readServiceEntrypoint(scope) {
35
35
  return currentServiceManager()?.readEntrypoint(scope) ?? null;
36
36
  }
37
+ export function readServiceConfigPath(scope) {
38
+ return currentServiceManager()?.readConfigPath(scope) ?? null;
39
+ }
@@ -2,6 +2,7 @@ import type { ServiceManager, ServiceScope } from "./index.js";
2
2
  export declare class LaunchdServiceManager implements ServiceManager {
3
3
  readonly platform: "darwin";
4
4
  unitPath(scope: ServiceScope): string;
5
+ readConfigPath(scope: ServiceScope): string | null;
5
6
  readEntrypoint(scope: ServiceScope): string | null;
6
7
  }
7
8
  /** PID of the loaded com.wand.web job in a launchd domain ("system" or "gui/<uid>"), or null. */
@@ -8,6 +8,21 @@ export class LaunchdServiceManager {
8
8
  ? path.join(os.homedir(), "Library/LaunchAgents/com.wand.web.plist")
9
9
  : "/Library/LaunchDaemons/com.wand.web.plist";
10
10
  }
11
+ readConfigPath(scope) {
12
+ const result = runCommandSync("plutil", ["-extract", "ProgramArguments", "json", "-o", "-", this.unitPath(scope)], { timeout: 5_000 });
13
+ if (result.status !== 0)
14
+ return null;
15
+ try {
16
+ const args = JSON.parse(result.stdout);
17
+ if (!Array.isArray(args) || !args.every((arg) => typeof arg === "string"))
18
+ return null;
19
+ const at = args.findIndex((arg) => arg === "-c" || arg === "--config");
20
+ return at >= 0 && args[at + 1] ? path.resolve(args[at + 1]) : null;
21
+ }
22
+ catch {
23
+ return null;
24
+ }
25
+ }
11
26
  readEntrypoint(scope) {
12
27
  const result = runCommandSync("plutil", ["-extract", "ProgramArguments.1", "raw", "-o", "-", this.unitPath(scope)], { timeout: 5_000 });
13
28
  return result.status === 0 ? (result.stdout || "").trim() || null : null;
@@ -2,5 +2,6 @@ import type { ServiceManager, ServiceScope } from "./index.js";
2
2
  export declare class SystemdServiceManager implements ServiceManager {
3
3
  readonly platform: "linux";
4
4
  unitPath(scope: ServiceScope): string;
5
+ readConfigPath(scope: ServiceScope): string | null;
5
6
  readEntrypoint(scope: ServiceScope): string | null;
6
7
  }
@@ -8,6 +8,17 @@ export class SystemdServiceManager {
8
8
  ? path.join(os.homedir(), ".config/systemd/user/wand.service")
9
9
  : "/etc/systemd/system/wand.service";
10
10
  }
11
+ readConfigPath(scope) {
12
+ try {
13
+ const execStart = readFileSync(this.unitPath(scope), "utf8").split("\n").find((line) => line.startsWith("ExecStart="));
14
+ const match = execStart?.match(/(?:^|\s)(?:-c|--config)(?:=|\s+)(?:"([^"\\]+)"|'([^'\\]+)'|([^\s"'\\]+))(?=\s|$)/);
15
+ const config = match?.[1] ?? match?.[2] ?? match?.[3];
16
+ return config ? path.resolve(config) : null;
17
+ }
18
+ catch {
19
+ return null;
20
+ }
21
+ }
11
22
  readEntrypoint(scope) {
12
23
  try {
13
24
  const execStart = readFileSync(this.unitPath(scope), "utf8").split("\n").find((line) => line.startsWith("ExecStart="));
@@ -8,13 +8,24 @@ import { inferProviderFromCommand, inferProviderFromRunner } from "./session-pro
8
8
  * still identify Codex through structuredState, runner, or command.
9
9
  */
10
10
  export function resolveSessionProvider(snapshot) {
11
- if (isSessionProvider(snapshot.provider))
12
- return snapshot.provider;
13
- const structuredProvider = snapshot.structuredState?.provider;
14
- if (isSessionProvider(structuredProvider))
15
- return structuredProvider;
11
+ for (const provider of [snapshot.provider, snapshot.structuredState?.provider]) {
12
+ if (provider == null)
13
+ continue;
14
+ if (!isSessionProvider(provider))
15
+ throw new Error(`不支持的会话 provider:${String(provider)}`);
16
+ return provider;
17
+ }
16
18
  const runner = snapshot.runner ?? snapshot.structuredState?.runner;
17
- return inferProviderFromRunner(runner) ?? inferProviderFromCommand(snapshot.command) ?? "claude";
19
+ if (runner && runner !== "pty") {
20
+ const provider = inferProviderFromRunner(runner);
21
+ if (!provider)
22
+ throw new Error(`不支持的会话 runner:${String(runner)}`);
23
+ return provider;
24
+ }
25
+ const provider = inferProviderFromCommand(snapshot.command);
26
+ if (!provider)
27
+ throw new Error("不支持的会话命令,无法选择 AI provider。");
28
+ return provider;
18
29
  }
19
30
  function normalizeModel(value) {
20
31
  const model = value?.trim();
@@ -7,7 +7,9 @@ export interface SessionCompletionState {
7
7
  export declare function createSessionCompletionViewIntent(): {
8
8
  open(id: string): void;
9
9
  clear(): void;
10
+ loaded(id: string): void;
10
11
  isOpen(id: string): boolean;
12
+ isLoaded(id: string): boolean;
11
13
  };
12
14
  /** Delayed HTTP/WS projections may advance generations, but must never move them backwards. */
13
15
  export declare function mergeSessionCompletionState(current: SessionCompletionState | null | undefined, incoming: SessionCompletionState): SessionCompletionState;
@@ -1,10 +1,14 @@
1
1
  /** Explicit navigation, not a list refresh choosing its preferred/default session. */
2
2
  export function createSessionCompletionViewIntent() {
3
3
  let openedId = null;
4
+ let loaded = false;
4
5
  return {
5
- open(id) { openedId = id; },
6
- clear() { openedId = null; },
6
+ open(id) { openedId = id; loaded = false; },
7
+ clear() { openedId = null; loaded = false; },
8
+ loaded(id) { if (openedId === id)
9
+ loaded = true; },
7
10
  isOpen(id) { return openedId === id; },
11
+ isLoaded(id) { return openedId === id && loaded; },
8
12
  };
9
13
  }
10
14
  /** Delayed HTTP/WS projections may advance generations, but must never move them backwards. */
@@ -1,6 +1,7 @@
1
1
  import { mkdirSync, rmSync, appendFileSync, writeFileSync, readFileSync, existsSync, statSync, renameSync, unlinkSync, openSync, readSync, closeSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  import process from "node:process";
4
+ import { appendBoundedDiagnostic } from "./diagnostic-logs.js";
4
5
  // ── Constants ──
5
6
  /** Max size for a single PTY log file before rotation (50 MB) */
6
7
  const PTY_LOG_MAX_SIZE = 50 * 1024 * 1024;
@@ -52,7 +53,12 @@ export class SessionLogger {
52
53
  this.totalBufferMaxBytes = positiveInteger(options.totalBufferMaxBytes, DEFAULT_TOTAL_BUFFER_MAX_BYTES);
53
54
  this.ptyLogMaxBytes = positiveInteger(options.ptyLogMaxBytes, PTY_LOG_MAX_SIZE);
54
55
  this.ptyLogMaxRotations = positiveInteger(options.ptyLogMaxRotations, PTY_LOG_MAX_ROTATIONS);
55
- this.appendFile = options.appendFile ?? ((filePath, data) => appendFileSync(filePath, data));
56
+ this.appendFile = options.appendFile ?? ((filePath, data) => {
57
+ if (["structured-stdout.log", "structured-stderr.log", "structured-spawns.jsonl"].includes(path.basename(filePath)))
58
+ appendBoundedDiagnostic(filePath, data);
59
+ else
60
+ appendFileSync(filePath, data);
61
+ });
56
62
  try {
57
63
  mkdirSync(this.baseDir, { recursive: true });
58
64
  }
@@ -1,4 +1,8 @@
1
+ import { type ProviderModelField } from "./config.js";
1
2
  import type { SessionProvider, WandConfig } from "./types.js";
3
+ import type { WandStorage } from "./storage.js";
4
+ /** Read raw live preferences without the configured-model trim/default projection. */
5
+ export declare function readProviderModelPreferences(config: Pick<WandConfig, ProviderModelField>, storage: Pick<WandStorage, "getPreference">): Record<SessionProvider, string>;
2
6
  /** The repeated `group selector || provider default` tail. Callers keep their own explicit-model precedence. */
3
7
  export declare function fallbackProviderModel(config: WandConfig, provider: SessionProvider): string | undefined;
4
8
  /** Employee and conversation creates treat blank and `default` as the configured fallback. */
@@ -1,10 +1,19 @@
1
- import { getDefaultModelForProvider } from "./config.js";
1
+ import { getDefaultModelForProvider, PROVIDER_MODEL_PREFERENCE_FIELDS } from "./config.js";
2
2
  import { defaultModelGroupSelector } from "./model-groups.js";
3
+ import { SESSION_PROVIDERS } from "./session-provider.js";
4
+ /** Read raw live preferences without the configured-model trim/default projection. */
5
+ export function readProviderModelPreferences(config, storage) {
6
+ const models = {};
7
+ for (const provider of SESSION_PROVIDERS) {
8
+ const field = PROVIDER_MODEL_PREFERENCE_FIELDS[provider];
9
+ models[provider] = storage.getPreference(`pref:${field}`, config[field] ?? "");
10
+ }
11
+ return models;
12
+ }
3
13
  /** The repeated `group selector || provider default` tail. Callers keep their own explicit-model precedence. */
4
14
  export function fallbackProviderModel(config, provider) {
5
- return defaultModelGroupSelector(config.modelGroups, provider, getDefaultModelForProvider(config, provider))
6
- || getDefaultModelForProvider(config, provider)
7
- || undefined;
15
+ const defaultModel = getDefaultModelForProvider(config, provider);
16
+ return defaultModelGroupSelector(config.modelGroups, provider, defaultModel) || defaultModel || undefined;
8
17
  }
9
18
  /** Employee and conversation creates treat blank and `default` as the configured fallback. */
10
19
  export function resolveExplicitOrFallbackModel(config, provider, explicit) {
@@ -3,7 +3,7 @@ import type { WandStorage } from "./storage.js";
3
3
  import type { StructuredSessionManager } from "./structured-session-manager.js";
4
4
  import type { ExecutionMode, SessionSnapshot } from "./types.js";
5
5
  export type SessionOwner = "structured" | "pty" | "storage";
6
- export type ProviderHistoryDeleter = Pick<ProcessManager, "deleteClaudeHistoryFiles" | "deleteCodexHistoryFiles" | "deleteOpenCodeHistorySessions" | "deleteQoderHistoryFiles" | "deletePiHistoryFiles" | "deleteGrokHistoryFiles" | "deleteGeminiHistoryFiles">;
6
+ export type ProviderHistoryDeleter = Pick<ProcessManager, "deleteClaudeHistoryFiles" | "deleteCodexHistoryFiles" | "deleteOpenCodeHistorySessions" | "deleteQoderHistoryFiles" | "deletePiHistoryFiles" | "deleteGrokHistoryFiles">;
7
7
  export declare function deleteProviderNativeHistory(deleter: ProviderHistoryDeleter, provider: string, providerSessionId: string, cwd: string): void;
8
8
  type HiddenSessionStore = Pick<WandStorage, "getConfigValue" | "setConfigValue">;
9
9
  /** The owner deletes the managed session first; native history/tombstones have one implementation. */
@@ -24,9 +24,6 @@ export function deleteProviderNativeHistory(deleter, provider, providerSessionId
24
24
  case "grok":
25
25
  deleter.deleteGrokHistoryFiles([id]);
26
26
  break;
27
- case "gemini":
28
- deleter.deleteGeminiHistoryFiles([id]);
29
- break;
30
27
  default:
31
28
  break;
32
29
  }
@@ -27,7 +27,6 @@ const PROVIDER_LABELS = {
27
27
  grok: "Grok Build CLI",
28
28
  qoder: "Qoder CLI",
29
29
  pi: "Pi coding agent",
30
- gemini: "Gemini CLI",
31
30
  };
32
31
  /** 硅基员工只能跑结构化会话;候选沿用看板的「跟随服务端默认」。 */
33
32
  export function employeeAgentFor(provider) {