@co0ontty/wand 4.71.2 → 4.72.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.
@@ -18,3 +18,25 @@ export function scopedMilestoneId(storage, milestoneId, workspaceId) {
18
18
  return milestoneId;
19
19
  return milestone.workspaceId === workspaceId ? milestoneId : null;
20
20
  }
21
+ /**
22
+ * 写路径的迭代兑底:用户没选(或没传)迭代就落到默认迭代,
23
+ * 这样「每个任务都属于一个迭代」在前端和后续 commit 总结里都成立。
24
+ */
25
+ export function defaultMilestoneIdForWrite(storage) {
26
+ return storage.ensureDefaultWandMilestone().id;
27
+ }
28
+ /**
29
+ * 读出任务该显示的迭代:库里仍然用 NULL 表示「没单独指定」
30
+ * (历史数据不动,写入时才开始落默认迭代),读的时候统一兑成默认迭代。
31
+ */
32
+ export function resolvedMilestoneFields(storage, milestoneId) {
33
+ const milestone = milestoneId
34
+ ? storage.getWandMilestone(milestoneId)
35
+ : storage.ensureDefaultWandMilestone();
36
+ if (!milestone)
37
+ return { milestoneId: null, milestone: null };
38
+ return {
39
+ milestoneId: milestone.id,
40
+ milestone: { id: milestone.id, name: milestone.name, isDefault: milestone.isDefault },
41
+ };
42
+ }
package/dist/missions.js CHANGED
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
2
2
  import { existsSync, statSync } from "node:fs";
3
3
  import path from "node:path";
4
4
  import { buildMissionDiff } from "./mission-diff.js";
5
+ import { recordIterationPromptForTask } from "./iteration-log.js";
5
6
  import { isSessionProvider } from "./session-provider.js";
6
7
  import { syncWorkspaceTaskToBoard } from "./wand-task-sync.js";
7
8
  const MAX_ATTEMPTS = 6;
@@ -148,9 +149,11 @@ export class Missions {
148
149
  throw new Error(`请选择 1-${MAX_ATTEMPTS} 个有效 provider。`);
149
150
  }
150
151
  const createdAt = nowIso();
151
- const milestoneId = input.milestoneId?.trim() || null;
152
- if (milestoneId && !this.storage.getWandMilestone(milestoneId))
152
+ // 没选迭代就落到默认迭代:每个任务都属于一个迭代,后面按迭代做 commit 总结才成立。
153
+ const requestedMilestoneId = input.milestoneId?.trim() || null;
154
+ if (requestedMilestoneId && !this.storage.getWandMilestone(requestedMilestoneId))
153
155
  throw new Error("未找到该里程碑。");
156
+ const milestoneId = requestedMilestoneId ?? this.storage.ensureDefaultWandMilestone().id;
154
157
  const mission = {
155
158
  id: randomUUID(),
156
159
  title: input.title?.trim().slice(0, 120) || firstPromptLine(prompt),
@@ -168,6 +171,15 @@ export class Missions {
168
171
  updatedAt: createdAt,
169
172
  };
170
173
  this.storage.saveMission(mission);
174
+ // 派发即改动意图:记进迭代提示词,commit / 汇报总结时不必读 diff。
175
+ recordIterationPromptForTask(this.storage, {
176
+ cwd: mission.cwd,
177
+ milestoneId: mission.milestoneId,
178
+ taskId: mission.taskId,
179
+ title: mission.title,
180
+ detail: prompt,
181
+ source: "dispatch",
182
+ });
171
183
  for (const provider of providers)
172
184
  this.dispatchAttempt(mission, provider);
173
185
  this.refreshMissionStatus(mission.id);
@@ -14,8 +14,9 @@ import { buildLanguageDirective, buildManagedAutonomyDirective } from "./languag
14
14
  import { prepareSessionWorktree } from "./git-worktree.js";
15
15
  import { getProviderCommandSessionId, getProviderResumeCommandSessionId } from "./resume-policy.js";
16
16
  import { normalizeThinkingEffort, thinkingEffortToClaudeCliEffort, thinkingEffortToClaudeSlashEffort, thinkingEffortToCodexReasoningEffort, thinkingEffortToOpenCodeVariant, thinkingEffortToPiLevel } from "./structured-provider-common.js";
17
- import { consumePtyInputForTopic, createPtyTopicLineBuffer, provisionalSessionTopic, SessionTopicCoordinator, sessionTopicBlocklistForSnapshot, shouldAcceptGeneratedSessionTitle, } from "./session-topic.js";
17
+ import { consumePtyInputForTopic, createPtyTopicLineBuffer, provisionalSessionTopic, SessionTopicCoordinator, sessionTopicBlocklistForSnapshot, shouldAcceptGeneratedSessionTitle, shouldGenerateSessionTopicFromInput, } from "./session-topic.js";
18
18
  import { getErrorMessage } from "./error-utils.js";
19
+ import { recordIterationPrompt } from "./iteration-log.js";
19
20
  import { describePtySpawnFailure } from "./ensure-node-pty-helper.js";
20
21
  import { resolveSystemAiContext } from "./session-ai-context.js";
21
22
  import { resolveSessionCwd } from "./session-cwd.js";
@@ -1935,9 +1936,13 @@ export class ProcessManager extends EventEmitter {
1935
1936
  const record = this.sessions.get(id);
1936
1937
  if (this.disposed || !prompt || !record)
1937
1938
  return;
1939
+ // 迭代提示词记录:和会话标题共用同一套「有没有信息量」判断,用户不用多做一步。
1940
+ recordIterationPrompt(this.storage, record, prompt, "session");
1938
1941
  const blockedTitles = sessionTopicBlocklistForSnapshot(record, this.storage);
1939
1942
  const provisional = provisionalSessionTopic(prompt, blockedTitles);
1940
- if (provisional && (record.title !== provisional.title || record.description !== provisional.description)) {
1943
+ if (provisional
1944
+ && shouldGenerateSessionTopicFromInput(prompt)
1945
+ && (record.title !== provisional.title || record.description !== provisional.description)) {
1941
1946
  this.setSessionTopic(id, provisional.title, provisional.description);
1942
1947
  }
1943
1948
  this.topicCoordinator.request(id, {
@@ -8,6 +8,7 @@ import { resolveSessionCwd } from "./session-cwd.js";
8
8
  import { resolveCommitAiContext } from "./session-ai-context.js";
9
9
  import { getGitStatusAsync, QuickCommitError, runQuickCommitWithFallback, runTagHead, runPush, generateCommitMessageOnly, } from "./git-quick-commit.js";
10
10
  import { getErrorMessage } from "./error-utils.js";
11
+ import { buildIterationCommitContext, COMMIT_CONTEXT_MODE_PREF_KEY, isCommitContextMode, readCommitContextMode, resolveCommitContextInput, } from "./iteration-log.js";
11
12
  import { inferProviderFromCommand, isSessionProvider, providerCliCommand, SESSION_PROVIDERS } from "./session-provider.js";
12
13
  import { buildProviderResumeCommand, isProviderSessionId } from "./resume-policy.js";
13
14
  import { parseBoundedInteger } from "./request-limits.js";
@@ -27,6 +28,23 @@ export function parseExecutionMode(value, fallback) {
27
28
  }
28
29
  return value;
29
30
  }
31
+ /** 提交输入模式:query 覆盖 > 记住的偏好 > 默认(有提示词就优先用提示词)。 */
32
+ function commitModeForRead(storage, override) {
33
+ return readCommitContextMode(storage, override);
34
+ }
35
+ /**
36
+ * 生成 commit message 的输入:迭代模式下给「本轮提示词清单」,
37
+ * 切到 diff 模式、或本轮没记录到任何提示词时给空 digest(调用方自动读完整 diff)。
38
+ */
39
+ async function iterationCommitInput(storage, snapshot, body) {
40
+ const context = await buildIterationCommitContext(storage, { session: snapshot });
41
+ const input = resolveCommitContextInput(storage, context, body);
42
+ return { digest: input.digest, entryIds: input.entryIds, iteration: context.iteration };
43
+ }
44
+ /** 只有「让模型自己写 message」时才把迭代提示词交给它;用户手写的 message 不需要读任何输入。 */
45
+ function commitMessageFromIteration(body) {
46
+ return body.autoMessage !== false && !body.customMessage?.trim();
47
+ }
30
48
  export function parseSessionCreationOrigin(body) {
31
49
  const rawSource = body?.sessionSource;
32
50
  if (rawSource !== undefined && rawSource !== "interactive" && rawSource !== "automation" && rawSource !== "startup") {
@@ -451,6 +469,23 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
451
469
  syncWorkspaceTaskToBoard(storage, snapshot.workspaceTaskId);
452
470
  const prompt = body.prompt?.trim();
453
471
  if (prompt) {
472
+ if (body.respondImmediately === true) {
473
+ // 只创建、不等首轮:整轮可能是几分钟,调用方(新建任务对话框、worktree 合并)
474
+ // 等在 HTTP 上会把用户锁在「正在创建…」且关闭按钮被禁用。与
475
+ // /api/sessions/:id/input 的 respondImmediately、/api/wand-tasks/:id/dispatch
476
+ // 一致:先回会话快照,首轮进度继续走结构化事件。
477
+ structured.sendMessage(snapshot.id, prompt).catch((error) => {
478
+ console.error("[wand] Accepted structured prompt later failed", {
479
+ sessionId: snapshot.id,
480
+ error: getInputDebugMeta(error),
481
+ });
482
+ });
483
+ const accepted = structured.get(snapshot.id);
484
+ if (!accepted)
485
+ throw new Error("未找到该结构化会话。");
486
+ res.status(201).json(sessionResponseDTO(accepted));
487
+ return;
488
+ }
454
489
  const finished = await structured.sendMessage(snapshot.id, prompt);
455
490
  res.status(201).json(finished);
456
491
  return;
@@ -755,6 +790,8 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
755
790
  const body = (req.body ?? {});
756
791
  try {
757
792
  const ai = resolveCommitAiContext(snapshot, config);
793
+ // 自动 message + 迭代模式:拿本轮提示词当输入;提交成功后标记已用掉。
794
+ const context = await iterationCommitInput(storage, snapshot, body);
758
795
  const result = await runQuickCommitWithFallback({
759
796
  cwd: snapshot.cwd,
760
797
  language: config.language ?? "",
@@ -765,8 +802,21 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
765
802
  autoTag: !!body.autoTag,
766
803
  push: !!body.push,
767
804
  submodule: !!body.submodule,
805
+ iterationDigest: commitMessageFromIteration(body) ? context.digest : "",
806
+ includeDiff: !!body.includeDiff,
807
+ });
808
+ const consumed = result.ok && result.commit?.hash && context.entryIds.length > 0
809
+ ? storage.markIterationPromptsConsumed(context.entryIds, result.commit.hash)
810
+ : 0;
811
+ res.json({
812
+ ...result,
813
+ commitContext: {
814
+ source: commitMessageFromIteration(body) && context.digest ? "iteration" : "diff",
815
+ entryIds: context.entryIds,
816
+ iteration: context.iteration,
817
+ },
818
+ iterationEntriesConsumed: consumed,
768
819
  });
769
- res.json(result);
770
820
  }
771
821
  catch (error) {
772
822
  if (error instanceof QuickCommitError) {
@@ -777,6 +827,37 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
777
827
  sendRouteError(res, error, "快捷提交失败。");
778
828
  }
779
829
  }));
830
+ // 本轮迭代的提示词清单:快捷提交面板默认勾选「上次提交以来」的那些,
831
+ // 也可以往回勾已提交过的历史,或者整套切成读 diff。
832
+ app.get("/api/sessions/:id/iteration-context", asyncRoute(async (req, res) => {
833
+ const snapshot = sessions.getLatest(req.params.id);
834
+ if (!snapshot) {
835
+ res.status(404).json({ error: "未找到该会话。" });
836
+ return;
837
+ }
838
+ const milestoneId = typeof req.query.milestoneId === "string" ? req.query.milestoneId : null;
839
+ try {
840
+ const context = await buildIterationCommitContext(storage, { session: snapshot, milestoneId });
841
+ res.json({ ...context, mode: commitModeForRead(storage, req.query.mode) });
842
+ }
843
+ catch (error) {
844
+ sendRouteError(res, error, "无法读取本轮变更。");
845
+ }
846
+ }));
847
+ // 记住用户选的模式(只存偏好,不碰仓库)。
848
+ app.post("/api/sessions/:id/iteration-context", (req, res) => {
849
+ if (!sessions.getLatest(req.params.id)) {
850
+ res.status(404).json({ error: "未找到该会话。" });
851
+ return;
852
+ }
853
+ const mode = (req.body ?? {}).mode;
854
+ if (!isCommitContextMode(mode)) {
855
+ res.status(400).json({ error: "无效的提交输入模式。" });
856
+ return;
857
+ }
858
+ storage.setPreference(COMMIT_CONTEXT_MODE_PREF_KEY, mode);
859
+ res.json({ ok: true, mode });
860
+ });
780
861
  app.post("/api/sessions/:id/generate-commit-message", asyncRoute(async (req, res) => {
781
862
  const snapshot = sessions.getLatest(req.params.id);
782
863
  if (!snapshot) {
@@ -787,12 +868,24 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
787
868
  res.status(400).json({ error: "会话没有工作目录。" });
788
869
  return;
789
870
  }
871
+ const body = (req.body ?? {});
790
872
  try {
791
873
  const ai = resolveCommitAiContext(snapshot, config);
874
+ const context = await iterationCommitInput(storage, snapshot, body);
792
875
  const result = await generateCommitMessageOnly(snapshot.cwd, config.language ?? "", {
793
876
  ...ai,
877
+ }, {
878
+ iterationDigest: context.digest,
879
+ includeDiff: !!body.includeDiff,
880
+ });
881
+ res.json({
882
+ ...result,
883
+ commitContext: {
884
+ source: context.digest ? "iteration" : "diff",
885
+ entryIds: context.entryIds,
886
+ iteration: context.iteration,
887
+ },
794
888
  });
795
- res.json(result);
796
889
  }
797
890
  catch (error) {
798
891
  if (error instanceof QuickCommitError) {
@@ -3,8 +3,9 @@ import { asyncRoute } from "./express-async.js";
3
3
  import { getErrorMessage } from "./error-utils.js";
4
4
  import { bodyObject, sendRouteError, text } from "./server-request.js";
5
5
  import { parseWandTaskAgent } from "./storage.js";
6
- import { scopedMilestoneId } from "./milestone-scope.js";
6
+ import { defaultMilestoneIdForWrite, resolvedMilestoneFields, scopedMilestoneId } from "./milestone-scope.js";
7
7
  import { generateWandTaskTitle, provisionalTaskTitleFromDescription, TASK_TITLE_MAX_LENGTH } from "./task-title.js";
8
+ import { recordIterationPromptForTask } from "./iteration-log.js";
8
9
  import { resolveSystemAiContext } from "./session-ai-context.js";
9
10
  import { DEFAULT_WAND_TASK_AGENT_KIND, DEFAULT_WAND_TASK_AGENT_MODE, DEFAULT_WAND_TASK_PRIORITY, isWandTaskAgentKind, isWandTaskAgentMode, normalizeWandTaskAgentMode, WAND_MILESTONE_NAME_MAX_LENGTH } from "./task-types.js";
10
11
  import { archiveBoardTask, ensureWorkspaceTaskForBoardTask, isAutoNameableBoardTask, moveSessionToWorkspaceTask, syncClosedBoardTask, syncUngroupedSessionsToBoard, syncWorkspaceTaskToBoard, taskAutoNameSignature, taskAutoNameSourceText } from "./wand-task-sync.js";
@@ -117,7 +118,7 @@ function taskDto({ storage, sessions }, task) {
117
118
  return null;
118
119
  const workspace = task.workspaceId ? storage.getWorkspace(task.workspaceId) : null;
119
120
  const sessionIds = storage.listWandTaskSessionIds(task.id);
120
- const milestone = task.milestoneId ? storage.getWandMilestone(task.milestoneId) : null;
121
+ const milestone = resolvedMilestoneFields(storage, task.milestoneId);
121
122
  return {
122
123
  ...task,
123
124
  sessionIds,
@@ -128,8 +129,10 @@ function taskDto({ storage, sessions }, task) {
128
129
  workspace: workspace
129
130
  ? { id: workspace.id, name: workspace.name, cwd: workspace.cwd }
130
131
  : null,
131
- // 卡片上要显示里程碑名字,直接在这里解出,免去前端再查一次列表。
132
- milestone: milestone ? { id: milestone.id, name: milestone.name } : null,
132
+ // 卡片上要显示迭代名字,直接在这里解出,免去前端再查一次列表。
133
+ // 任务没有单独指定迭代时给的就是默认迭代(id 与名字成对返回)。
134
+ milestoneId: milestone.milestoneId,
135
+ milestone: milestone.milestone,
133
136
  };
134
137
  }
135
138
  function taskSessionSummary(session) {
@@ -254,10 +257,12 @@ export function registerTaskRoutes(app, deps) {
254
257
  app.get("/api/wand-task-agent-defaults", (_req, res) => {
255
258
  res.json(readTaskBoardLastAgent(storage));
256
259
  });
257
- // ── 里程碑(迭代):归属工作区,任务面板的「新建任务」按工作区过滤 ──
260
+ // ── 迭代(里程碑):归属工作区,任务面板的「新建任务」按工作区过滤 ──
258
261
  app.get("/api/wand-milestones", (req, res) => {
259
- // 不传 workspaceId 返回全部;传了就只给「该工作区 + 全局」的里程碑。
262
+ // 不传 workspaceId 返回全部;传了就只给「该工作区 + 全局」的迭代。
260
263
  const workspaceId = typeof req.query.workspaceId === "string" ? req.query.workspaceId : null;
264
+ // 默认迭代是惰性创建的:列表是它的第一个真实读取点,在这里保证它一定存在。
265
+ storage.ensureDefaultWandMilestone();
261
266
  const counts = storage.countWandTasksByMilestone();
262
267
  res.json({
263
268
  milestones: storage.listWandMilestones(workspaceId).map((milestone) => ({
@@ -319,6 +324,12 @@ export function registerTaskRoutes(app, deps) {
319
324
  }
320
325
  });
321
326
  app.delete("/api/wand-milestones/:id", (req, res) => {
327
+ const current = storage.getWandMilestone(req.params.id);
328
+ // 默认迭代是所有任务的兜底归属,删了就再没有地方接「没选迭代」的任务。
329
+ if (current?.isDefault) {
330
+ res.status(400).json({ error: "默认迭代不能删除,可以改成你自己习惯的名字。" });
331
+ return;
332
+ }
322
333
  // 只解绑任务,不删任务;已经删过的 id 幂等返回 ok。
323
334
  storage.deleteWandMilestone(req.params.id);
324
335
  res.json({ ok: true });
@@ -356,7 +367,10 @@ export function registerTaskRoutes(app, deps) {
356
367
  writeTaskBoardLastAgent(storage, agent);
357
368
  const labels = labelsFrom(body.labels);
358
369
  const dueDate = dateValue(body.dueDate) ?? null;
359
- const milestoneId = scopedMilestoneId(storage, milestoneIdFrom(storage, body.milestoneId), workspaceId);
370
+ // 没选迭代就落到默认迭代:每个任务都属于一个迭代。
371
+ const requestedMilestoneId = milestoneIdFrom(storage, body.milestoneId) ?? defaultMilestoneIdForWrite(storage);
372
+ const milestoneId = scopedMilestoneId(storage, requestedMilestoneId, workspaceId)
373
+ ?? defaultMilestoneIdForWrite(storage);
360
374
  const task = storage.transaction(() => {
361
375
  const card = storage.createWandTask({
362
376
  workspaceId,
@@ -414,18 +428,21 @@ export function registerTaskRoutes(app, deps) {
414
428
  patch.labels = labelsFrom(body.labels);
415
429
  if (body.dueDate !== undefined)
416
430
  patch.dueDate = dateValue(body.dueDate) ?? null;
417
- if (body.milestoneId !== undefined)
418
- patch.milestoneId = milestoneIdFrom(storage, body.milestoneId);
431
+ if (body.milestoneId !== undefined) {
432
+ // 清空迭代等于回到默认迭代(原生端的「清除」按钮也走这里)。
433
+ patch.milestoneId = milestoneIdFrom(storage, body.milestoneId) ?? defaultMilestoneIdForWrite(storage);
434
+ }
419
435
  if (body.workspaceId !== undefined) {
420
436
  const workspaceId = body.workspaceId === null ? null : text(body.workspaceId);
421
437
  if (workspaceId && !storage.getWorkspace(workspaceId))
422
438
  throw new Error("项目不存在。");
423
439
  patch.workspaceId = workspaceId || null;
424
- // 换项目后原迭代不再属于新工作区就一并摘掉;移动端只 PATCH workspaceId,不会自己清。
440
+ // 换项目后原迭代不再属于新工作区就一并收敛;移动端只 PATCH workspaceId,不会自己清。
425
441
  const milestoneId = body.milestoneId !== undefined
426
442
  ? patch.milestoneId
427
443
  : storage.getWandTask(req.params.id)?.milestoneId ?? null;
428
- patch.milestoneId = scopedMilestoneId(storage, milestoneId, patch.workspaceId);
444
+ patch.milestoneId = scopedMilestoneId(storage, milestoneId, patch.workspaceId)
445
+ ?? defaultMilestoneIdForWrite(storage);
429
446
  }
430
447
  if (body.agent !== undefined) {
431
448
  // 老客户端不传 mode / kind:沿用任务当前值,而不是复位成标准 / 结构化。
@@ -560,6 +577,17 @@ export function registerTaskRoutes(app, deps) {
560
577
  });
561
578
  storage.updateWandTask(task.id, { agent, status: task.status === "todo" ? "doing" : task.status });
562
579
  storage.bindWandTaskSession(task.id, session.id);
580
+ // 派发也算一轮迭代里的改动意图:直接把任务的标题 / 描述记进迭代记录。
581
+ recordIterationPromptForTask(storage, {
582
+ sessionId: session.id,
583
+ cwd,
584
+ workspaceId: group.workspaceId,
585
+ milestoneId: task.milestoneId,
586
+ taskId: task.id,
587
+ title: task.title,
588
+ detail: task.description || prompt,
589
+ source: "dispatch",
590
+ });
563
591
  if (agent.kind !== "pty") {
564
592
  const completion = structured.sendMessage(session.id, prompt);
565
593
  completion.catch((error) => console.error(`[WandTask] Agent dispatch failed for ${task.id}:`, error));
@@ -9,7 +9,7 @@ import { checkSessionWorktreeMergeabilityAsync, cleanupWorktreeSync, prepareSess
9
9
  import { collectSessionTopicBlocklist } from "./session-topic.js";
10
10
  import { resolveSessionDisplayTitle } from "./session-transport.js";
11
11
  import { attachUnboundSessionsToWorkspace, backfillSessionWorkspaces, normalizeProjectCwd, projectCwdForSession, isGlobalWorkspace, syncDirectoryNameForWorkspace } from "./workspace-binding.js";
12
- import { scopedMilestoneId } from "./milestone-scope.js";
12
+ import { defaultMilestoneIdForWrite, resolvedMilestoneFields, scopedMilestoneId } from "./milestone-scope.js";
13
13
  import { archiveBoardTaskForWorkspaceTask, archiveWorkspaceTask, ensureBoardTaskForWorkspaceTask, isUnnamedWorkspaceTaskName, moveSessionToWorkspaceTask, syncSidebarTasksFromBoard, UNNAMED_WORKSPACE_TASK_NAME } from "./wand-task-sync.js";
14
14
  import { isSessionProvider } from "./session-provider.js";
15
15
  import { firstLayoutTabId } from "./layout-tree.js";
@@ -124,7 +124,7 @@ function createTaskForWorkspace(storage, workspace, body) {
124
124
  if (requestedMilestoneId && !storage.getWandMilestone(requestedMilestoneId)) {
125
125
  throw new Error("未找到该里程碑。");
126
126
  }
127
- const milestoneId = scopedMilestoneId(storage, requestedMilestoneId || null, isGlobalWorkspace(workspace) ? null : workspace.id);
127
+ const milestoneId = scopedMilestoneId(storage, requestedMilestoneId || null, isGlobalWorkspace(workspace) ? null : workspace.id) ?? defaultMilestoneIdForWrite(storage);
128
128
  let mountedCwd;
129
129
  if (body.cwd !== undefined && body.cwd !== null && String(body.cwd).trim()) {
130
130
  mountedCwd = resolveWorkspaceCwd(body.cwd);
@@ -429,6 +429,7 @@ export function registerWorkspaceRoutes(app, storage, sessions, titleOptions = {
429
429
  refreshAutoBoardTaskTitles(storage, titleOptions);
430
430
  res.status(201).json({
431
431
  ...created.task,
432
+ ...resolvedMilestoneFields(storage, created.task.milestoneId),
432
433
  cwd: created.cwd,
433
434
  isolated: created.isolated,
434
435
  worktreeError: created.worktreeError,
@@ -539,6 +540,8 @@ export function registerWorkspaceRoutes(app, storage, sessions, titleOptions = {
539
540
  }));
540
541
  target.tasks.push({
541
542
  ...task,
543
+ // 没单独指定迭代的任务读作默认迭代(id 与名字成对)。
544
+ ...resolvedMilestoneFields(storage, task.milestoneId),
542
545
  cwd: taskRuntimeCwd(task, workspace),
543
546
  isolated: task.worktree !== null,
544
547
  sessions,
@@ -640,7 +643,10 @@ export function registerWorkspaceRoutes(app, storage, sessions, titleOptions = {
640
643
  res.status(404).json({ error: "未找到该项目。" });
641
644
  return;
642
645
  }
643
- res.json(storage.listWorkspaceTasks(workspace.id));
646
+ res.json(storage.listWorkspaceTasks(workspace.id).map((task) => ({
647
+ ...task,
648
+ ...resolvedMilestoneFields(storage, task.milestoneId),
649
+ })));
644
650
  });
645
651
  // 项目级 Worktree 总览:一次解析默认目标分支,再为每个任务读取可合并状态。
646
652
  app.get("/api/workspaces/:id/worktrees", asyncRoute(async (req, res) => {
@@ -730,6 +736,7 @@ export function registerWorkspaceRoutes(app, storage, sessions, titleOptions = {
730
736
  refreshAutoBoardTaskTitles(storage, titleOptions);
731
737
  res.status(201).json({
732
738
  ...created.task,
739
+ ...resolvedMilestoneFields(storage, created.task.milestoneId),
733
740
  cwd: created.cwd,
734
741
  isolated: created.isolated,
735
742
  worktreeError: created.worktreeError,
@@ -750,6 +757,7 @@ export function registerWorkspaceRoutes(app, storage, sessions, titleOptions = {
750
757
  const workspace = storage.getWorkspace(task.workspaceId);
751
758
  res.json({
752
759
  ...task,
760
+ ...resolvedMilestoneFields(storage, task.milestoneId),
753
761
  cwd: taskRuntimeCwd(task, workspace),
754
762
  sessions: storage.listSessionsByWorkspaceTaskSlim(task.id).map((session) => ({
755
763
  ...workspaceSessionSummary(session, {
@@ -795,12 +803,17 @@ export function registerWorkspaceRoutes(app, storage, sessions, titleOptions = {
795
803
  sendRouteError(res, new Error("未找到该里程碑。"), "无法更新任务。");
796
804
  return;
797
805
  }
798
- patch.milestoneId = milestoneId || null;
806
+ // 清空迭代等于回到默认迭代(老客户端不传时不会走到这里)。
807
+ patch.milestoneId = milestoneId || defaultMilestoneIdForWrite(storage);
799
808
  }
800
809
  storage.updateWorkspaceTask(existing.id, patch);
801
810
  if (patch.status === "done")
802
811
  archiveBoardTaskForWorkspaceTask(storage, existing.id);
803
- res.json(storage.getWorkspaceTask(existing.id));
812
+ const updated = storage.getWorkspaceTask(existing.id);
813
+ res.json({
814
+ ...updated,
815
+ ...resolvedMilestoneFields(storage, updated?.milestoneId),
816
+ });
804
817
  });
805
818
  // 归档任务:软删除。终端继续运行、worktree 与卡片历史都保留,只是侧栏不再显示。
806
819
  app.post("/api/workspace-tasks/:taskId/archive", (req, res) => {
@@ -810,7 +823,11 @@ export function registerWorkspaceRoutes(app, storage, sessions, titleOptions = {
810
823
  return;
811
824
  }
812
825
  archiveWorkspaceTask(storage, existing);
813
- res.json(storage.getWorkspaceTask(existing.id));
826
+ const archived = storage.getWorkspaceTask(existing.id);
827
+ res.json({
828
+ ...archived,
829
+ ...resolvedMilestoneFields(storage, archived?.milestoneId),
830
+ });
814
831
  });
815
832
  // 删除任务;cascade=true 连带删会话,否则仅解绑;尽力清理 worktree
816
833
  app.delete("/api/workspace-tasks/:taskId", (req, res) => {
@@ -39,6 +39,9 @@ export declare function collectSessionTopicBlocklist(input: {
39
39
  cwd?: string;
40
40
  }): string[];
41
41
  export declare function shouldAcceptGeneratedSessionTitle(title: string, blockedTitles?: readonly string[]): boolean;
42
+ export declare function measureSessionTopicInputWeight(input: string): number;
43
+ /** 太短的用户输入不值得生成标题,也不应该覆盖已有标题。 */
44
+ export declare function shouldGenerateSessionTopicFromInput(input: string): boolean;
42
45
  /** Immediate title from the user's command, without waiting for the model. */
43
46
  export declare function summarizeSessionTitleFromInput(input: string, options?: {
44
47
  blockedTitles?: readonly string[];
@@ -63,6 +66,7 @@ export declare function generateSessionTopic(userMessages: readonly string[], cw
63
66
  /**
64
67
  * Coalesces title refreshes per session. A newer user message invalidates an
65
68
  * in-flight result and is summarized together with every earlier user turn.
69
+ * Short inputs (single commands, choices) are dropped before any model call.
66
70
  */
67
71
  export declare class SessionTopicCoordinator {
68
72
  private readonly generate;
@@ -141,6 +141,24 @@ export function shouldAcceptGeneratedSessionTitle(title, blockedTitles = []) {
141
141
  return false;
142
142
  return !blockedTitles.some((blocked) => isSameSessionTitle(cleaned, blocked));
143
143
  }
144
+ const CJK_CHARACTER_PATTERN = /[\u3040-\u30ff\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff\uac00-\ud7af]/g;
145
+ /**
146
+ * 用户输入的「信息量」:CJK 字符按 2 计,其余非空白字符按 1 计。
147
+ * 单条命令(`pwd`、`git status`)、选择与确认(`y`、`1`、`好的`)、斜杠命令(`/compact`)
148
+ * 都低于阈值:让模型总结只会把输入复述一遍,还多花一次调用。
149
+ */
150
+ const SESSION_TOPIC_INPUT_MIN_WEIGHT = 12;
151
+ export function measureSessionTopicInputWeight(input) {
152
+ const text = input.replace(/\s+/g, "");
153
+ if (!text)
154
+ return 0;
155
+ const cjkCount = text.match(CJK_CHARACTER_PATTERN)?.length ?? 0;
156
+ return cjkCount * 2 + (text.length - cjkCount);
157
+ }
158
+ /** 太短的用户输入不值得生成标题,也不应该覆盖已有标题。 */
159
+ export function shouldGenerateSessionTopicFromInput(input) {
160
+ return measureSessionTopicInputWeight(input) >= SESSION_TOPIC_INPUT_MIN_WEIGHT;
161
+ }
144
162
  /** Immediate title from the user's command, without waiting for the model. */
145
163
  export function summarizeSessionTitleFromInput(input, options = {}) {
146
164
  const blockedTitles = options.blockedTitles ?? [];
@@ -248,6 +266,7 @@ export async function generateSessionTopic(userMessages, cwd, language, ai = {})
248
266
  /**
249
267
  * Coalesces title refreshes per session. A newer user message invalidates an
250
268
  * in-flight result and is summarized together with every earlier user turn.
269
+ * Short inputs (single commands, choices) are dropped before any model call.
251
270
  */
252
271
  export class SessionTopicCoordinator {
253
272
  generate;
@@ -260,7 +279,7 @@ export class SessionTopicCoordinator {
260
279
  if (this.disposed)
261
280
  return;
262
281
  const input = request.input.trim();
263
- if (!input)
282
+ if (!input || !shouldGenerateSessionTopicFromInput(input))
264
283
  return;
265
284
  let state = this.states.get(sessionId);
266
285
  if (!state) {
package/dist/storage.d.ts CHANGED
@@ -143,20 +143,61 @@ export declare class WandStorage {
143
143
  */
144
144
  listWandMilestones(workspaceId?: string | null): import("./task-types.js").WandTaskMilestone[];
145
145
  getWandMilestone(id: string): import("./task-types.js").WandTaskMilestone | null;
146
+ /** 已有的默认迭代;未创建过时返回 null(读路径用它,不写库)。 */
147
+ findDefaultWandMilestone(): import("./task-types.js").WandTaskMilestone | null;
148
+ /**
149
+ * 惰性拿到全局唯一的「默认迭代」:没有就建一个,返回值一定可写进任务 / 记录。
150
+ * 用户已经自己建过一个同名迭代时直接把它提升为默认,不再造第二个重名行。
151
+ */
152
+ ensureDefaultWandMilestone(): import("./task-types.js").WandTaskMilestone;
146
153
  createWandMilestone(input: {
147
154
  name: string;
148
155
  dueDate?: string | null;
149
156
  workspaceId?: string | null;
157
+ isDefault?: boolean;
150
158
  }): import("./task-types.js").WandTaskMilestone;
151
159
  updateWandMilestone(id: string, patch: {
152
160
  name?: string;
153
161
  dueDate?: string | null;
154
162
  workspaceId?: string | null;
155
163
  }): import("./task-types.js").WandTaskMilestone | null;
156
- /** 删除里程碑只解绑任务(milestone_id 置空),绝不删除任务。 */
164
+ /**
165
+ * 删除里程碑只解绑任务(milestone_id 置空)、并把提示词记录改挂到默认迭代,绝不删任务与记录。
166
+ * 默认迭代不可删除:返回 false,由调用方给出明确提示。
167
+ */
157
168
  deleteWandMilestone(id: string): boolean;
158
- /** 各里程碑下的任务数(看板卡片),用于下拉里的数量提示。 */
169
+ /** 各迭代下的任务数(看板卡片),用于下拉里的数量提示;历史 NULL 行算在默认迭代里。 */
159
170
  countWandTasksByMilestone(): Record<string, number>;
171
+ /** 同一会话最近一条记录;用来去重(同一提示词重复提交 / 占位与模型结果双写)。 */
172
+ latestIterationPromptForSession(sessionId: string): import("./task-types.js").WandIterationPrompt | null;
173
+ createIterationPrompt(input: {
174
+ milestoneId: string;
175
+ workspaceId?: string | null;
176
+ sessionId?: string | null;
177
+ taskId?: string | null;
178
+ repoKey?: string | null;
179
+ cwd?: string;
180
+ title: string;
181
+ detail?: string;
182
+ source?: import("./task-types.js").WandIterationPromptSource;
183
+ }): import("./task-types.js").WandIterationPrompt;
184
+ /**
185
+ * 迭代窗口里的提示词记录,按时间正序(模型读起来就是「先做什么、后做什么」)。
186
+ * `repoKey` 给值时按仓库隔离;`includeConsumed=false` 只看还没提交的部分。
187
+ */
188
+ listIterationPrompts(filter?: {
189
+ milestoneId?: string | null;
190
+ repoKey?: string | null;
191
+ sessionId?: string | null;
192
+ includeConsumed?: boolean;
193
+ limit?: number;
194
+ }): import("./task-types.js").WandIterationPrompt[];
195
+ /** 按 id 批量取记录(用户只勾选了一部分历史变更时用)。 */
196
+ listIterationPromptsByIds(ids: readonly string[]): import("./task-types.js").WandIterationPrompt[];
197
+ /** 标记这些记录已被某次提交用掉,下一轮「上次提交以来」从这里开始。 */
198
+ markIterationPromptsConsumed(ids: readonly string[], commit: string): number;
199
+ /** 会话绑定的看板任务 id;没绑或已删除时返回 null。 */
200
+ getWandTaskIdForSession(sessionId: string): string | null;
160
201
  listWandTaskSessionIds(taskId: string): string[];
161
202
  listBoundWandTaskSessionIds(): string[];
162
203
  bindWandTaskSession(taskId: string, sessionId: string): void;