@co0ontty/wand 4.89.5 → 4.90.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 (197) hide show
  1. package/README.md +5 -3
  2. package/dist/agent-dispatch.js +4 -9
  3. package/dist/ai-team-employee-binding.js +10 -9
  4. package/dist/ai-team-runner.d.ts +2 -1
  5. package/dist/ai-team-runner.js +38 -2
  6. package/dist/ai-team-types.js +3 -2
  7. package/dist/ai-text-runner.d.ts +61 -0
  8. package/dist/ai-text-runner.js +653 -0
  9. package/dist/avatar-input.d.ts +2 -0
  10. package/dist/avatar-input.js +19 -0
  11. package/dist/bounded-process-output.d.ts +5 -0
  12. package/dist/bounded-process-output.js +9 -0
  13. package/dist/build-info.json +3 -3
  14. package/dist/command-error.d.ts +6 -0
  15. package/dist/command-error.js +8 -0
  16. package/dist/config.js +11 -42
  17. package/dist/conversation-service.d.ts +12 -2
  18. package/dist/conversation-service.js +104 -25
  19. package/dist/conversation-session-preview.d.ts +2 -0
  20. package/dist/conversation-session-preview.js +4 -0
  21. package/dist/decision-client.js +22 -6
  22. package/dist/decision-expert-employee.js +2 -2
  23. package/dist/decision-expert-identity.d.ts +1 -1
  24. package/dist/decision-expert-identity.js +1 -1
  25. package/dist/decision-expert-service.d.ts +1 -0
  26. package/dist/decision-expert-service.js +82 -76
  27. package/dist/decision-hardware.js +4 -3
  28. package/dist/decision-types.d.ts +4 -0
  29. package/dist/decision-types.js +4 -0
  30. package/dist/deployment-lock.d.ts +13 -0
  31. package/dist/deployment-lock.js +189 -0
  32. package/dist/employee-text.js +1 -1
  33. package/dist/fixed-employee-avatar.d.ts +7 -0
  34. package/dist/fixed-employee-avatar.js +13 -0
  35. package/dist/git-quick-commit.d.ts +5 -40
  36. package/dist/git-quick-commit.js +8 -636
  37. package/dist/hr-employee-identity.d.ts +5 -0
  38. package/dist/hr-employee-identity.js +5 -0
  39. package/dist/hr-employee.d.ts +5 -0
  40. package/dist/hr-employee.js +21 -0
  41. package/dist/middleware/path-safety.js +1 -1
  42. package/dist/milestone-scope.js +1 -1
  43. package/dist/model-auto-assign.d.ts +1 -1
  44. package/dist/model-auto-assign.js +1 -1
  45. package/dist/models.d.ts +4 -0
  46. package/dist/models.js +21 -1
  47. package/dist/npm-update-utils.js +5 -44
  48. package/dist/pi-auto-resources.js +6 -3
  49. package/dist/plush-avatar.d.ts +10 -1
  50. package/dist/plush-avatar.js +13 -1
  51. package/dist/plush-cat-appearance.d.ts +97 -0
  52. package/dist/plush-cat-appearance.js +118 -0
  53. package/dist/plush-cat-whiskers.d.ts +6 -0
  54. package/dist/plush-cat-whiskers.js +42 -0
  55. package/dist/process-manager.d.ts +22 -21
  56. package/dist/process-manager.js +49 -83
  57. package/dist/prompt-optimizer.d.ts +1 -1
  58. package/dist/prompt-optimizer.js +1 -1
  59. package/dist/render-host.js +1 -19
  60. package/dist/scheduled-task-service.d.ts +41 -0
  61. package/dist/scheduled-task-service.js +294 -0
  62. package/dist/scheduled-task-state.d.ts +11 -0
  63. package/dist/scheduled-task-state.js +65 -0
  64. package/dist/scheduled-task-time.d.ts +4 -0
  65. package/dist/scheduled-task-time.js +82 -0
  66. package/dist/scheduled-task-types.d.ts +71 -0
  67. package/dist/scheduled-task-types.js +1 -0
  68. package/dist/server-ai-team-routes.js +3 -17
  69. package/dist/server-employee-routes.d.ts +3 -0
  70. package/dist/server-employee-routes.js +20 -38
  71. package/dist/server-file-routes.js +44 -1
  72. package/dist/server-pi-recommendation-routes.js +13 -4
  73. package/dist/server-request.d.ts +2 -0
  74. package/dist/server-request.js +9 -0
  75. package/dist/server-resume-routes.js +1 -1
  76. package/dist/server-scheduled-task-routes.d.ts +4 -0
  77. package/dist/server-scheduled-task-routes.js +34 -0
  78. package/dist/server-session-routes.d.ts +5 -13
  79. package/dist/server-session-routes.js +35 -70
  80. package/dist/server-task-routes.d.ts +6 -21
  81. package/dist/server-task-routes.js +25 -120
  82. package/dist/server-team-dispatch-routes.js +12 -2
  83. package/dist/server-workspace-routes.d.ts +1 -1
  84. package/dist/server-workspace-routes.js +33 -78
  85. package/dist/server.js +55 -20
  86. package/dist/session-model.d.ts +15 -0
  87. package/dist/session-model.js +23 -0
  88. package/dist/session-registry.d.ts +5 -1
  89. package/dist/session-registry.js +20 -11
  90. package/dist/session-request.d.ts +9 -0
  91. package/dist/session-request.js +27 -0
  92. package/dist/session-topic.d.ts +1 -1
  93. package/dist/session-topic.js +1 -1
  94. package/dist/session-transport.d.ts +3 -1
  95. package/dist/session-transport.js +11 -5
  96. package/dist/silicon-employee-draft.d.ts +2 -2
  97. package/dist/silicon-employee-draft.js +3 -3
  98. package/dist/socket-probe.d.ts +2 -0
  99. package/dist/socket-probe.js +20 -0
  100. package/dist/storage-schedules.d.ts +20 -0
  101. package/dist/storage-schedules.js +78 -0
  102. package/dist/storage-schema.d.ts +3 -0
  103. package/dist/storage-schema.js +702 -0
  104. package/dist/storage.d.ts +42 -8
  105. package/dist/storage.js +182 -715
  106. package/dist/structured-claude-adapter.js +12 -0
  107. package/dist/structured-claude-protocol.d.ts +4 -0
  108. package/dist/structured-claude-protocol.js +18 -4
  109. package/dist/structured-codex-adapter.js +11 -0
  110. package/dist/structured-exec-pump.js +79 -38
  111. package/dist/structured-failure.d.ts +21 -0
  112. package/dist/structured-failure.js +14 -0
  113. package/dist/structured-gemini-adapter.js +12 -2
  114. package/dist/structured-grok-adapter.js +12 -2
  115. package/dist/structured-opencode-adapter.js +8 -1
  116. package/dist/structured-qoder-adapter.js +14 -0
  117. package/dist/structured-session-manager.d.ts +13 -18
  118. package/dist/structured-session-manager.js +147 -517
  119. package/dist/system-employee.d.ts +1 -1
  120. package/dist/system-employee.js +1 -1
  121. package/dist/task-archive-policy.js +1 -1
  122. package/dist/task-read-model.d.ts +27 -0
  123. package/dist/task-read-model.js +47 -0
  124. package/dist/task-retention.d.ts +1 -1
  125. package/dist/task-retention.js +1 -1
  126. package/dist/task-title-service.d.ts +30 -0
  127. package/dist/task-title-service.js +122 -0
  128. package/dist/task-title.d.ts +1 -1
  129. package/dist/task-title.js +1 -1
  130. package/dist/terminal-daemon-client.js +1 -18
  131. package/dist/user-memory.js +1 -1
  132. package/dist/web-ui/asset-runtime.d.ts +12 -0
  133. package/dist/web-ui/asset-runtime.js +6 -0
  134. package/dist/web-ui/composer-avatar-activity.d.ts +6 -0
  135. package/dist/web-ui/composer-avatar-activity.js +29 -0
  136. package/dist/web-ui/content/ai-teams.js +13 -13
  137. package/dist/web-ui/content/module-manifest.json +1 -0
  138. package/dist/web-ui/content/modules/5478be410b112154/chat-render-PY6DIXNZ.js +24 -0
  139. package/dist/web-ui/content/modules/5478be410b112154/chunk-2YZLCGYN.js +1 -0
  140. package/dist/web-ui/content/modules/5478be410b112154/chunk-6CFHW3FA.js +111 -0
  141. package/dist/web-ui/content/modules/5478be410b112154/chunk-6XQEBIEX.js +1 -0
  142. package/dist/web-ui/content/modules/5478be410b112154/chunk-DDSO7OX7.js +1 -0
  143. package/dist/web-ui/content/modules/5478be410b112154/chunk-G3CXNMA2.js +20 -0
  144. package/dist/web-ui/content/modules/5478be410b112154/chunk-LGVAPHRS.js +36 -0
  145. package/dist/web-ui/content/modules/5478be410b112154/chunk-NURCNMRU.js +1 -0
  146. package/dist/web-ui/content/modules/5478be410b112154/chunk-PCMFGZKX.js +1 -0
  147. package/dist/web-ui/content/modules/5478be410b112154/chunk-QRNA4D7E.js +1 -0
  148. package/dist/web-ui/content/modules/5478be410b112154/chunk-TMRPHNJO.js +1 -0
  149. package/dist/web-ui/content/modules/5478be410b112154/chunk-WC7RK7LP.js +1 -0
  150. package/dist/web-ui/content/modules/5478be410b112154/chunk-YOXXNXOX.js +2 -0
  151. package/dist/web-ui/content/modules/5478be410b112154/chunk-YX475LXO.js +1 -0
  152. package/dist/web-ui/content/modules/5478be410b112154/employee-card-MDWKU4V3.js +1 -0
  153. package/dist/web-ui/content/modules/5478be410b112154/host-56WPSON2.js +1 -0
  154. package/dist/web-ui/content/modules/5478be410b112154/host-5QOHKHNC.js +1 -0
  155. package/dist/web-ui/content/modules/5478be410b112154/host-AWIJ5L2K.js +1 -0
  156. package/dist/web-ui/content/modules/5478be410b112154/host-BOZLGIT5.js +69 -0
  157. package/dist/web-ui/content/modules/5478be410b112154/host-EZV22Q7Y.js +4 -0
  158. package/dist/web-ui/content/modules/5478be410b112154/host-IW43Z5ZF.js +1 -0
  159. package/dist/web-ui/content/modules/5478be410b112154/host-JT7NIUQL.js +1 -0
  160. package/dist/web-ui/content/modules/5478be410b112154/host-JWEIDB55.js +3 -0
  161. package/dist/web-ui/content/modules/5478be410b112154/host-OU5TRYH6.js +2 -0
  162. package/dist/web-ui/content/modules/5478be410b112154/host-PRSTCPVE.js +5 -0
  163. package/dist/web-ui/content/modules/5478be410b112154/host-TWPJI3HE.js +1 -0
  164. package/dist/web-ui/content/modules/5478be410b112154/host-WZGJSNML.js +1 -0
  165. package/dist/web-ui/content/modules/5478be410b112154/host-X6KQVFD6.js +1 -0
  166. package/dist/web-ui/content/modules/5478be410b112154/host-YKCQFMQR.js +1 -0
  167. package/dist/web-ui/content/modules/5478be410b112154/host-bridge-2LZX6UUZ.js +1 -0
  168. package/dist/web-ui/content/modules/5478be410b112154/library-bridge-MQFJGTGY.js +4 -0
  169. package/dist/web-ui/content/modules/5478be410b112154/page-T3VUCU2W.js +3 -0
  170. package/dist/web-ui/content/modules/5478be410b112154/panel-HJBGBBPY.js +1 -0
  171. package/dist/web-ui/content/modules/5478be410b112154/runtime.js +920 -0
  172. package/dist/web-ui/content/modules/5478be410b112154/sha256-ANXV72UC.js +1 -0
  173. package/dist/web-ui/content/modules/5478be410b112154/task-board-host-AVMDIHIV.js +2 -0
  174. package/dist/web-ui/content/modules/5478be410b112154/workspace-agent-picker-KI7QVSK3.js +1 -0
  175. package/dist/web-ui/content/modules/5478be410b112154/workspace-window-L542FGP2.js +1 -0
  176. package/dist/web-ui/content/plush-avatar.js +177 -177
  177. package/dist/web-ui/content/scripts.js +1 -994
  178. package/dist/web-ui/content/styles.css +1 -1
  179. package/dist/web-ui/content/tailwind.css +1 -1
  180. package/dist/web-ui/embedded-assets.d.ts +32 -0
  181. package/dist/web-ui/embedded-assets.js +10 -8
  182. package/dist/web-ui/markdown.d.ts +3 -2
  183. package/dist/web-ui/markdown.js +61 -48
  184. package/dist/web-ui/message-file-target.d.ts +13 -0
  185. package/dist/web-ui/message-file-target.js +85 -0
  186. package/dist/web-ui/provider-identity.d.ts +1 -1
  187. package/dist/web-ui/provider-identity.js +3 -4
  188. package/dist/web-ui/react/local-preview/controller.d.ts +2 -0
  189. package/dist/web-ui/react/local-preview/controller.js +12 -0
  190. package/dist/web-ui/scripts.d.ts +10 -0
  191. package/dist/web-ui/scripts.js +42 -5
  192. package/package.json +6 -3
  193. package/browser-extension/icons/wand-128.png +0 -0
  194. package/browser-extension/icons/wand-16.png +0 -0
  195. package/browser-extension/icons/wand-32.png +0 -0
  196. package/browser-extension/icons/wand-48.png +0 -0
  197. package/dist/web-ui/content/licenses/codex-LICENSE.txt +0 -21
@@ -1,22 +1,22 @@
1
- import { canArchiveTask } from "./task-archive-policy.js";
2
1
  import { dispatchAgentForTask, resolveTaskDispatchTarget } from "./agent-dispatch.js";
2
+ import { memberAgents } from "./ai-team-types.js";
3
+ import { defaultRoleForCli } from "./default-employee.js";
3
4
  import { asyncRoute } from "./express-async.js";
4
- import { getErrorMessage } from "./error-utils.js";
5
- import { bodyObject, sendRouteError, text } from "./server-request.js";
6
5
  import { cleanupWorktreeSync } from "./git-worktree.js";
7
- import { isRetentionBusy } from "./retention.js";
8
- import { parseWandTaskAgent } from "./storage.js";
9
- import { defaultMilestoneIdForWrite, resolvedMilestoneFields, scopedMilestoneId } from "./milestone-scope.js";
10
- import { generateWandTaskTitle, provisionalTaskTitleFromDescription, TASK_TITLE_MAX_LENGTH } from "./task-title.js";
11
6
  import { recordIterationPromptForTask } from "./iteration-log.js";
12
- import { resolveSystemAiContext } from "./session-ai-context.js";
13
- import { memberAgents } from "./ai-team-types.js";
14
- import { selectEmployeeCandidate } from "./silicon-employee-dispatch.js";
15
- import { defaultRoleForCli } from "./default-employee.js";
16
- import { DEFAULT_WAND_TASK_AGENT_KIND, DEFAULT_WAND_TASK_AGENT_MODE, DEFAULT_WAND_TASK_PRIORITY, isWandTaskAgentEngine, isWandTaskAgentKind, isWandTaskAgentMode, normalizeWandTaskAgentMode, WAND_MILESTONE_NAME_MAX_LENGTH } from "./task-types.js";
17
- import { isAutoNameableBoardTask, taskAutoNameSignature, taskAutoNameSourceText } from "./wand-task-sync.js";
7
+ import { defaultMilestoneIdForWrite, resolvedMilestoneFields, scopedMilestoneId } from "./milestone-scope.js";
8
+ import { isRetentionBusy } from "./retention.js";
9
+ import { bodyObject, sendRouteError, text } from "./server-request.js";
18
10
  import { isSessionProvider } from "./session-provider.js";
11
+ import { selectEmployeeCandidate } from "./silicon-employee-dispatch.js";
12
+ import { parseWandTaskAgent } from "./storage.js";
19
13
  import { isThinkingEffort } from "./structured-provider-common.js";
14
+ import { compactTransportAvatar } from "./fixed-employee-avatar.js";
15
+ import { canArchiveTask } from "./task-archive-policy.js";
16
+ import { TaskReadModel } from "./task-read-model.js";
17
+ import { refreshAutoBoardTaskTitles, taskTitleService } from "./task-title-service.js";
18
+ import { provisionalTaskTitleFromDescription } from "./task-title.js";
19
+ import { DEFAULT_WAND_TASK_AGENT_KIND, DEFAULT_WAND_TASK_AGENT_MODE, DEFAULT_WAND_TASK_PRIORITY, isWandTaskAgentEngine, isWandTaskAgentKind, isWandTaskAgentMode, normalizeWandTaskAgentMode, WAND_MILESTONE_NAME_MAX_LENGTH } from "./task-types.js";
20
20
  const STATUSES = new Set(["todo", "doing", "done", "archived"]);
21
21
  const PRIORITIES = new Set(["none", "low", "medium", "high", "urgent"]);
22
22
  const TASK_BOARD_LAST_AGENT_KEY = "pref:taskBoardLastAgent";
@@ -200,18 +200,18 @@ export function parseTaskAgent(value, fallbackMode = DEFAULT_WAND_TASK_AGENT_MOD
200
200
  const engine = rawEngine === "sdk" ? rawEngine : undefined;
201
201
  return { provider, model, thinkingEffort, mode, kind: rawKind, ...(engine ? { engine } : {}) };
202
202
  }
203
- function taskDto({ storage, sessions }, task) {
203
+ function taskDto({ storage, sessions }, task, model) {
204
204
  if (!task)
205
205
  return null;
206
- const workspace = task.workspaceId ? storage.getWorkspace(task.workspaceId) : null;
207
- const sessionIds = storage.listWandTaskSessionIds(task.id);
208
- const milestone = resolvedMilestoneFields(storage, task.milestoneId);
206
+ const workspace = task.workspaceId ? (model ? model.workspaces.get(task.workspaceId) : storage.getWorkspace(task.workspaceId)) : null;
207
+ const sessionIds = model ? model.taskSessions(task).map(session => session.id) : storage.listWandTaskSessionIds(task.id);
208
+ const milestone = model ? model.milestone(task.milestoneId) : resolvedMilestoneFields(storage, task.milestoneId);
209
209
  return {
210
210
  ...task,
211
- canArchive: canArchiveTask(storage, sessions, task.id),
211
+ canArchive: model ? model.canArchive(task) : canArchiveTask(storage, sessions, task.id),
212
212
  sessionIds,
213
213
  sessions: sessionIds.flatMap((id) => {
214
- const session = sessions?.getLatest(id) ?? storage.getSession(id);
214
+ const session = model ? model.sessionsById.get(id) : sessions?.getSlim(id) ?? storage.getSessionSlim(id);
215
215
  return session ? [taskSessionSummary(session)] : [];
216
216
  }),
217
217
  workspace: workspace
@@ -239,113 +239,20 @@ function taskSessionSummary(session) {
239
239
  engine: session.provider === "pi" ? (session.structuredState?.engine === "core" ? "sdk" : "cli") : undefined,
240
240
  employeeId: session.employeeId,
241
241
  employeeName: session.employeeName,
242
- employeeAvatar: session.employeeAvatar,
243
- };
244
- }
245
- /**
246
- * 用户没写标题时,先用描述/会话内容当占位标题,再由后台模型总结一个更好的。
247
- * 全程不阻塞创建请求:失败只是保留占位标题,不返回错误、也不覆盖用户后来改的标题。
248
- * 多次总结会排队而不是丢弃,保证每张卡片最终都会拿到标题。
249
- */
250
- let titleGenerationChain = Promise.resolve();
251
- function scheduleWandTaskTitleGeneration(storage, taskId, source, signature, options) {
252
- const generateTitle = options.generateTitle ?? generateWandTaskTitle;
253
- const cwd = options.cwd || options.config?.defaultCwd || process.cwd();
254
- const run = async () => {
255
- try {
256
- const pending = storage.getWandTask(taskId);
257
- // 排队期间可能移走 Agent,只剩空白终端;此时也不应再启动总结。
258
- if (!pending || !taskAutoNameSourceText(storage, pending))
259
- return;
260
- const title = await generateTitle(source, cwd, options.config?.language ?? "", taskTitleAiOptions(options.config, storage, options.free));
261
- const current = storage.getWandTask(taskId);
262
- // 用户已经自己写了标题(原生端 / 面板编辑)就不要覆盖。
263
- if (!current || current.titleSource !== "auto")
264
- return;
265
- // 命名输入已经变了(新增会话 / 会话内容更新):这次是过期结果,交给下一轮。
266
- if (current.autoTitleSignature !== signature)
267
- return;
268
- if (!taskAutoNameSourceText(storage, current))
269
- return;
270
- const clipped = title.slice(0, TASK_TITLE_MAX_LENGTH);
271
- if (!clipped || clipped === current.title)
272
- return;
273
- storage.updateWandTask(taskId, { title: clipped });
274
- }
275
- catch (error) {
276
- console.error(`[WandTask] Failed to auto-generate title for ${taskId}:`, getErrorMessage(error));
277
- }
278
- };
279
- titleGenerationChain = titleGenerationChain.then(run, run);
280
- }
281
- /** 等待排队中的标题生成全部结束;只有测试用,避免用例轮询。 */
282
- export function whenWandTaskTitlesSettled() {
283
- return titleGenerationChain;
284
- }
285
- /**
286
- * 扫描标题仍可自动命名的看板任务(titleSource=auto,或仍是占位名的历史卡片):
287
- * 先用任务描述 + 所有绑定会话的内容立即改掉占位名,再交给后台模型总结更贴切的标题。
288
- * 同一个输入指纹只处理一次:内容没变就跳过,也避免自动标题与模型结果来回震荡。
289
- * 用户建任务时写了名字、或事后改过名(titleSource=user 且不是占位名)的任务永不进入这里。
290
- */
291
- export function refreshAutoBoardTaskTitles(storage, options = {}) {
292
- for (const card of storage.listWandTasks()) {
293
- if (!isAutoNameableBoardTask(card))
294
- continue;
295
- const source = taskAutoNameSourceText(storage, card);
296
- if (!source)
297
- continue;
298
- const signature = taskAutoNameSignature(source);
299
- if (card.autoTitleSignature === signature)
300
- continue;
301
- const provisional = provisionalTaskTitleFromDescription(source);
302
- storage.updateWandTask(card.id, {
303
- titleSource: "auto",
304
- autoTitleSignature: signature,
305
- ...(provisional && provisional !== card.title ? { title: provisional } : {}),
306
- });
307
- // 未启用模型(如部分只验证占位改名的单测)时只保留立即生效的占位标题。
308
- if (!options.generateTitle && !options.config)
309
- continue;
310
- const cwd = options.cwd
311
- || (card.workspaceId ? storage.getWorkspace(card.workspaceId)?.cwd : undefined);
312
- scheduleWandTaskTitleGeneration(storage, card.id, source, signature, {
313
- cwd,
314
- config: options.config,
315
- generateTitle: options.generateTitle,
316
- free: options.free,
317
- });
318
- }
319
- }
320
- /** 自动生成标题没有会话上下文,按「默认 provider + 默认模型」解析,与提示词优化一致。 */
321
- function taskTitleAiOptions(config, storage, free) {
322
- if (!config)
323
- return {};
324
- const provider = config.defaultProvider ?? "claude";
325
- const defaultSession = {
326
- provider,
327
- structuredState: undefined,
328
- runner: undefined,
329
- command: provider === "qoder" ? "qodercli" : provider,
330
- selectedModel: null,
331
- thinkingEffort: config.defaultThinkingEffort,
242
+ employeeAvatar: compactTransportAvatar(session.employeeAvatar),
332
243
  };
333
- return resolveSystemAiContext(defaultSession, config, storage?.getSystemSiliconEmployee() ?? null, free);
334
244
  }
245
+ export { refreshAutoBoardTaskTitles, whenWandTaskTitlesSettled } from "./task-title-service.js";
335
246
  export function registerTaskRoutes(app, deps) {
336
247
  const { storage, sessions, structured, processes, config, aiTeams } = deps;
248
+ taskTitleService(storage, { config, generateTitle: deps.generateTitle, free: deps.free });
337
249
  const dto = (task) => taskDto(deps, task);
338
250
  app.get("/api/wand-tasks", (req, res) => {
339
- try {
340
- // 侧栏建的任务 / 后续新增的会话都会在这里补上自动标题。
341
- refreshAutoBoardTaskTitles(storage, { config, generateTitle: deps.generateTitle, free: deps.free });
342
- }
343
- catch (error) {
344
- console.error("[WandTask] Failed to refresh auto task titles:", getErrorMessage(error));
345
- }
346
251
  const workspaceId = typeof req.query.workspaceId === "string" ? req.query.workspaceId : undefined;
347
252
  // 数组顺序即展示顺序:created_at 倒序,新创建的在上面。
348
- res.json(storage.listWandTasks(workspaceId).map((task) => dto(task)));
253
+ const model = new TaskReadModel(storage, sessions);
254
+ res.json(model.tasks.filter(task => workspaceId === undefined || task.workspaceId === workspaceId)
255
+ .map(task => taskDto(deps, task, model)));
349
256
  });
350
257
  app.get("/api/wand-task-agent-defaults", (_req, res) => {
351
258
  res.json(readTaskBoardLastAgent(storage));
@@ -354,8 +261,6 @@ export function registerTaskRoutes(app, deps) {
354
261
  app.get("/api/wand-milestones", (req, res) => {
355
262
  // 不传 workspaceId 返回全部;传了就只给「该工作区 + 全局」的迭代。
356
263
  const workspaceId = typeof req.query.workspaceId === "string" ? req.query.workspaceId : null;
357
- // 默认迭代是惰性创建的:列表是它的第一个真实读取点,在这里保证它一定存在。
358
- storage.ensureDefaultWandMilestone();
359
264
  const counts = storage.countWandTasksByMilestone();
360
265
  res.json({
361
266
  milestones: storage.listWandMilestones(workspaceId).map((milestone) => ({
@@ -20,6 +20,10 @@ function sendDispatchError(res, error) {
20
20
  */
21
21
  export function registerTeamDispatchRoutes(app, deps) {
22
22
  app.post("/api/team-dispatch/plan", asyncRoute(async (req, res) => {
23
+ const abort = new AbortController();
24
+ const disconnected = () => { if (!res.writableEnded)
25
+ abort.abort(); };
26
+ res.once("close", disconnected);
23
27
  try {
24
28
  const body = bodyObject(req.body ?? {});
25
29
  const note = text(body.note);
@@ -32,11 +36,17 @@ export function registerTeamDispatchRoutes(app, deps) {
32
36
  ...(maxMembers === undefined ? {} : { maxMembers }),
33
37
  ...(threshold === undefined ? {} : { threshold }),
34
38
  evaluate: (value, caller, signal) => deps.decisions.evaluate(value, caller, signal),
39
+ signal: abort.signal,
35
40
  });
36
- res.json(plan);
41
+ if (!res.destroyed)
42
+ res.json(plan);
37
43
  }
38
44
  catch (error) {
39
- sendDispatchError(res, error);
45
+ if (!res.destroyed)
46
+ sendDispatchError(res, error);
47
+ }
48
+ finally {
49
+ res.off("close", disconnected);
40
50
  }
41
51
  }));
42
52
  app.post("/api/team-dispatch/start", asyncRoute(async (req, res) => {
@@ -2,7 +2,7 @@ import type { Express } from "express";
2
2
  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
- import { type AutoTaskTitleOptions } from "./server-task-routes.js";
5
+ import { type AutoTaskTitleOptions } from "./task-title-service.js";
6
6
  /** 校验并清洗前端提交的布局树;非法输入返回 null(空工作空间)。 */
7
7
  export declare function sanitizeLayout(value: unknown): LayoutNode | null;
8
8
  /** 校验任务级工作窗口集合;旧版单棵布局会兼容升级成一个 window。 */
@@ -1,4 +1,5 @@
1
1
  import { canArchiveTask } from "./task-archive-policy.js";
2
+ import { TaskReadModel } from "./task-read-model.js";
2
3
  import { isArchiveBlocked } from "./archive-policy.js";
3
4
  import path from "node:path";
4
5
  import crypto from "node:crypto";
@@ -9,13 +10,15 @@ import { getErrorMessage } from "./error-utils.js";
9
10
  import { expandHomePath } from "./middleware/path-safety.js";
10
11
  import { checkSessionWorktreeMergeabilityAsync, cleanupWorktreeSync, prepareSessionWorktree, resolveWorktreeTargetBranchAsync, } from "./git-worktree.js";
11
12
  import { collectSessionTopicBlocklist } from "./session-topic.js";
13
+ import { compactTransportAvatar } from "./fixed-employee-avatar.js";
12
14
  import { resolveSessionDisplayTitle } from "./session-transport.js";
13
- import { attachUnboundSessionsToWorkspace, backfillSessionWorkspaces, normalizeProjectCwd, projectCwdForSession, isGlobalWorkspace, syncDirectoryNameForWorkspace } from "./workspace-binding.js";
15
+ import { attachUnboundSessionsToWorkspace, normalizeProjectCwd, projectCwdForSession, isGlobalWorkspace, syncDirectoryNameForWorkspace } from "./workspace-binding.js";
14
16
  import { defaultMilestoneIdForWrite, resolvedMilestoneFields, scopedMilestoneId } from "./milestone-scope.js";
15
17
  import { isUnnamedWorkspaceTaskName, UNNAMED_WORKSPACE_TASK_NAME } from "./wand-task-sync.js";
16
18
  import { isSessionProvider } from "./session-provider.js";
17
19
  import { firstLayoutTabId } from "./layout-tree.js";
18
- import { parentTaskIdFrom, refreshAutoBoardTaskTitles } from "./server-task-routes.js";
20
+ import { parentTaskIdFrom } from "./server-task-routes.js";
21
+ import { taskTitleService, refreshAutoBoardTaskTitles } from "./task-title-service.js";
19
22
  import { provisionalTaskTitleFromDescription } from "./task-title.js";
20
23
  function workspaceSessionTitle(session, names = {}) {
21
24
  return resolveSessionDisplayTitle(session, collectSessionTopicBlocklist({
@@ -25,7 +28,7 @@ function workspaceSessionTitle(session, names = {}) {
25
28
  }));
26
29
  }
27
30
  function liveSession(session, registry) {
28
- return registry?.getLatest(session.id) ?? session;
31
+ return registry?.getSlim(session.id) ?? session;
29
32
  }
30
33
  function workspaceSessionSummary(session, names = {}, registry, team = {}) {
31
34
  const live = liveSession(session, registry);
@@ -51,7 +54,7 @@ function workspaceSessionSummary(session, names = {}, registry, team = {}) {
51
54
  startedAt: live.startedAt,
52
55
  employeeId: live.employeeId,
53
56
  employeeName: live.employeeName,
54
- employeeAvatar: live.employeeAvatar,
57
+ employeeAvatar: compactTransportAvatar(live.employeeAvatar),
55
58
  ...(team.chat ? { teamChat: team.chat } : {}),
56
59
  ...(team.step ? { teamStep: team.step } : {}),
57
60
  };
@@ -71,34 +74,6 @@ function teamMarkersFor(markers, sessionId) {
71
74
  const step = markers.step.get(sessionId);
72
75
  return step ? { step } : {};
73
76
  }
74
- function cheapTasksRevision(storage, registry, includeArchived = false) {
75
- const fingerprint = [
76
- // Invalidate pre-filter client caches even if no stored row has changed.
77
- "visible-sessions-v2",
78
- includeArchived,
79
- storage.tasksAggregateFingerprint(),
80
- // 团队标记(群聊入口、成员步骤与运行状态)也在列表里。
81
- storage.teamSessionFingerprint(),
82
- // 排序只更新偏好,不改变任务和会话;轮询 revision 也必须跟着变化。
83
- storage.getWorkspaceGroupOrder(),
84
- ...(registry?.listSlim() ?? []).map((session) => [
85
- session.id,
86
- session.status,
87
- session.completionRevision ?? 0,
88
- session.viewedCompletionRevision ?? 0,
89
- session.archived === true ? 1 : 0,
90
- session.ptyBusy === true ? 1 : 0,
91
- session.ptyCommandRunning === true ? 1 : 0,
92
- session.providerCliActive ? 1 : 0,
93
- session.structuredState?.inFlight === true ? 1 : 0,
94
- isArchiveBlocked(session) ? 1 : 0,
95
- session.title ?? "",
96
- session.workspaceId ?? "",
97
- session.workspaceTaskId ?? "",
98
- ]),
99
- ];
100
- return crypto.createHash("sha256").update(JSON.stringify(fingerprint)).digest("base64url");
101
- }
102
77
  /**
103
78
  * 目录组排序:先按用户拖动保存的顺序,再按默认规则(全局组在前、创建时间倒序、id 兜底)。
104
79
  *
@@ -351,9 +326,9 @@ export function sanitizeTaskLayout(value) {
351
326
  * - 布局树由前端 allotment 构造,PUT 时经 sanitizeLayout 校验/规整后落库。
352
327
  */
353
328
  export function registerWorkspaceRoutes(app, storage, sessions, titleOptions = {}) {
329
+ taskTitleService(storage, titleOptions);
354
330
  // 列出所有项目(按创建时间降序:新建在前,打开不会改位置)
355
331
  app.get("/api/workspaces", (req, res) => {
356
- backfillSessionWorkspaces(storage);
357
332
  const includeArchived = req.query.includeArchived === "1";
358
333
  const sessionCounts = storage.countSessionsByWorkspace({ includeArchived });
359
334
  res.json(storage.listWorkspaces()
@@ -383,19 +358,18 @@ export function registerWorkspaceRoutes(app, storage, sessions, titleOptions = {
383
358
  attachUnboundSessionsToWorkspace(storage, workspace);
384
359
  res.status(201).json(workspaceWithCounts(storage, workspace));
385
360
  }));
386
- // 项目详情:meta + 会话 + 布局;访问即更新 lastOpenedAt
361
+ // 项目详情:meta + 会话 + 布局;纯投影,不把读取当成用户已打开。
387
362
  app.get("/api/workspaces/:id", (req, res) => {
388
363
  const workspace = storage.getWorkspace(req.params.id);
389
364
  if (!workspace) {
390
365
  res.status(404).json({ error: "未找到该项目。" });
391
366
  return;
392
367
  }
393
- storage.touchWorkspace(workspace.id);
394
368
  const includeArchived = req.query.includeArchived === "1";
395
369
  const archivedTaskIds = new Set(storage.listWorkspaceTasks(workspace.id)
396
370
  .filter((task) => task.archived).map((task) => task.id));
397
371
  const teamMarkers = teamSessionMarkers(storage);
398
- const visibleSessions = storage.listSessionsByWorkspace(workspace.id).map((session) => ({
372
+ const visibleSessions = storage.listSessionsByWorkspaceSlim(workspace.id).map((session) => ({
399
373
  ...workspaceSessionSummary(session, { workspaceName: workspace.name }, sessions, teamMarkersFor(teamMarkers, session.id)),
400
374
  workspaceTaskId: session.workspaceTaskId,
401
375
  })).filter((session) => includeArchived || (!session.archived
@@ -520,25 +494,11 @@ export function registerWorkspaceRoutes(app, storage, sessions, titleOptions = {
520
494
  }));
521
495
  // 目录组为一级容器的任务聚合列表,供侧栏「任务」视图一次拉全。
522
496
  app.get("/api/tasks", (req, res) => {
523
- // 侧栏轮询也在补自动标题:不打开看板的任务(以及新增会话后的改名)也要吃到。
524
- try {
525
- refreshAutoBoardTaskTitles(storage, titleOptions);
526
- }
527
- catch (error) {
528
- console.error("[WandTask] Failed to refresh auto task titles:", getErrorMessage(error));
529
- }
530
497
  // 查询参数:workspaceId 过滤单目录;limit 截断每目录任务数;
531
498
  // maxSessions 截断每任务内嵌会话数(大数据量时控制响应体积)。
532
499
  const includeArchived = req.query.includeArchived === "1";
533
500
  const hasRevisionQuery = typeof req.query.revision === "string";
534
501
  const requestedRevision = hasRevisionQuery ? req.query.revision : "";
535
- if (hasRevisionQuery && requestedRevision) {
536
- const cheap = cheapTasksRevision(storage, sessions, includeArchived);
537
- if (requestedRevision === cheap) {
538
- res.json({ unchanged: true, revision: cheap, groups: [] });
539
- return;
540
- }
541
- }
542
502
  const workspaceFilter = typeof req.query.workspaceId === "string" ? req.query.workspaceId : "";
543
503
  const parseBoundedCount = (raw) => {
544
504
  const value = Number(raw);
@@ -546,17 +506,23 @@ export function registerWorkspaceRoutes(app, storage, sessions, titleOptions = {
546
506
  };
547
507
  const taskLimit = parseBoundedCount(req.query.limit);
548
508
  const sessionLimit = parseBoundedCount(req.query.maxSessions);
549
- backfillSessionWorkspaces(storage);
550
- // 目录组只需要会话元数据:用 slim 读,否则每次轮询都要把每个会话的
551
- // messages 大字段 JSON 解析一遍(单会话可达十几 MB,整表 ~30MB)。
552
- const allSessions = storage.loadSessionsSlim();
553
- const liveArchiveFlags = new Map((sessions?.listSlim() ?? []).map((session) => [session.id, session.archived]));
554
- const sessionVisible = (session) => includeArchived
555
- || !(liveArchiveFlags.get(session.id) ?? session.archived);
556
- const workspaces = storage.listWorkspaces();
509
+ const snapshots = sessions?.listSlim();
510
+ const revision = crypto.createHash("sha256").update(JSON.stringify({
511
+ storage: storage.projectionRevision(),
512
+ query: { includeArchived, workspaceFilter, taskLimit, sessionLimit },
513
+ runtime: snapshots,
514
+ })).digest("base64url");
515
+ if (hasRevisionQuery && requestedRevision === revision) {
516
+ res.json({ unchanged: true, revision, groups: [] });
517
+ return;
518
+ }
519
+ const model = new TaskReadModel(storage, sessions, snapshots);
520
+ const allSessions = model.sessions;
521
+ const sessionVisible = (session) => includeArchived || !session.archived;
522
+ const workspaces = [...model.workspaces.values()];
557
523
  // 群聊与成员派发标记一次查全:列表里每条会话只做 Map 命中,不逐个查库。
558
524
  const teamMarkers = teamSessionMarkers(storage);
559
- const summarize = (session, names = {}) => workspaceSessionSummary(session, names, sessions, teamMarkersFor(teamMarkers, session.id));
525
+ const summarize = (session, names = {}) => workspaceSessionSummary(session, names, undefined, teamMarkersFor(teamMarkers, session.id));
560
526
  // 目录自定义名(工作区名称):没有项目实体的合成目录也按这个名字显示。
561
527
  const directoryNames = storage.listSessionDirectoryNames();
562
528
  const customNameForCwd = (cwd) => {
@@ -602,19 +568,10 @@ export function registerWorkspaceRoutes(app, storage, sessions, titleOptions = {
602
568
  groups.set(id, created);
603
569
  return created;
604
570
  };
605
- const tasksByWorkspace = new Map(workspaces.map((workspace) => [
606
- workspace.id, storage.listWorkspaceTasks(workspace.id),
607
- ]));
608
- const sessionsByTask = new Map();
609
- const taskSessions = (taskId) => {
610
- let snapshots = sessionsByTask.get(taskId);
611
- if (!snapshots) {
612
- snapshots = storage.listSessionsByWorkspaceTaskSlim(taskId);
613
- sessionsByTask.set(taskId, snapshots);
614
- }
615
- return snapshots;
616
- };
617
- const sessionRegistry = sessions;
571
+ const tasksByWorkspace = new Map(workspaces.map(workspace => [workspace.id, []]));
572
+ for (const task of storage.listAllWorkspaceTasks())
573
+ tasksByWorkspace.get(task.workspaceId)?.push(task);
574
+ const taskSessions = (taskId) => model.sessionsByTask.get(taskId) ?? [];
618
575
  const appendTask = (target, workspace, task) => {
619
576
  const visibleSessions = taskSessions(task.id).filter(sessionVisible);
620
577
  const sessions = visibleSessions
@@ -625,9 +582,8 @@ export function registerWorkspaceRoutes(app, storage, sessions, titleOptions = {
625
582
  }));
626
583
  target.tasks.push({
627
584
  ...task,
628
- canArchive: (() => { const card = storage.getWandTaskByWorkspaceTaskId(task.id); return card ? canArchiveTask(storage, sessionRegistry, card.id) : true; })(),
629
- // 没单独指定迭代的任务读作默认迭代(id 与名字成对)。
630
- ...resolvedMilestoneFields(storage, task.milestoneId),
585
+ canArchive: (() => { const card = model.cardsByContainer.get(task.id); return card ? model.canArchive(card) : true; })(),
586
+ ...model.milestone(task.milestoneId),
631
587
  cwd: taskRuntimeCwd(task, workspace),
632
588
  isolated: task.worktree !== null,
633
589
  sessions,
@@ -713,7 +669,7 @@ export function registerWorkspaceRoutes(app, storage, sessions, titleOptions = {
713
669
  .filter((group) => !group.global || group.tasks.length > 0 || group.standaloneSessions.length > 0);
714
670
  const payload = sortTaskDirectoryGroups(orderedGroups, storage.getWorkspaceGroupOrder());
715
671
  if (hasRevisionQuery) {
716
- res.json({ unchanged: false, revision: cheapTasksRevision(storage, sessions, includeArchived), groups: payload });
672
+ res.json({ unchanged: false, revision, groups: payload });
717
673
  return;
718
674
  }
719
675
  res.json(payload);
@@ -855,14 +811,13 @@ export function registerWorkspaceRoutes(app, storage, sessions, titleOptions = {
855
811
  sendRouteError(res, error, "创建任务失败。");
856
812
  }
857
813
  }));
858
- // 任务详情:meta + 该任务下的会话;访问即更新 lastOpenedAt
814
+ // 任务详情:meta + 该任务下的会话;后台预取/轮询不改活动时间。
859
815
  app.get("/api/workspace-tasks/:taskId", (req, res) => {
860
816
  const task = storage.getWorkspaceTask(req.params.taskId);
861
817
  if (!task) {
862
818
  res.status(404).json({ error: "未找到该任务。" });
863
819
  return;
864
820
  }
865
- storage.touchWorkspaceTask(task.id);
866
821
  const workspace = storage.getWorkspace(task.workspaceId);
867
822
  const teamMarkers = teamSessionMarkers(storage);
868
823
  res.json({