@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.
- package/dist/build-info.json +3 -3
- package/dist/git-quick-commit.d.ts +12 -2
- package/dist/git-quick-commit.js +80 -12
- package/dist/iteration-log.d.ts +133 -0
- package/dist/iteration-log.js +293 -0
- package/dist/milestone-scope.d.ts +19 -0
- package/dist/milestone-scope.js +22 -0
- package/dist/missions.js +14 -2
- package/dist/process-manager.js +7 -2
- package/dist/server-session-routes.js +95 -2
- package/dist/server-task-routes.js +39 -11
- package/dist/server-workspace-routes.js +23 -6
- package/dist/session-topic.d.ts +4 -0
- package/dist/session-topic.js +20 -1
- package/dist/storage.d.ts +43 -2
- package/dist/storage.js +201 -12
- package/dist/structured-session-manager.js +7 -2
- package/dist/task-types.d.ts +47 -1
- package/dist/task-types.js +6 -0
- package/dist/web-ui/content/scripts.js +260 -116
- package/dist/web-ui/content/styles.css +1 -1
- package/dist/web-ui/embedded-assets.js +2 -2
- package/package.json +1 -1
package/dist/milestone-scope.js
CHANGED
|
@@ -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
|
-
|
|
152
|
-
|
|
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);
|
package/dist/process-manager.js
CHANGED
|
@@ -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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) => {
|
package/dist/session-topic.d.ts
CHANGED
|
@@ -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;
|
package/dist/session-topic.js
CHANGED
|
@@ -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
|
-
/**
|
|
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;
|