@co0ontty/wand 4.60.0 → 4.61.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/claude-pty-bridge.js +4 -0
- package/dist/mission-types.d.ts +4 -0
- package/dist/missions.js +4 -0
- package/dist/server-app-connect.d.ts +40 -0
- package/dist/server-app-connect.js +241 -0
- package/dist/server-file-routes.js +11 -10
- package/dist/server-github-routes.js +12 -30
- package/dist/server-mission-routes.js +4 -2
- package/dist/server-request.d.ts +34 -0
- package/dist/server-request.js +61 -0
- package/dist/server-resume-routes.d.ts +25 -0
- package/dist/server-resume-routes.js +124 -0
- package/dist/server-session-routes.js +37 -182
- package/dist/server-settings-routes.js +6 -5
- package/dist/server-task-routes.js +93 -15
- package/dist/server-update-routes.js +23 -16
- package/dist/server-vault-routes.d.ts +16 -0
- package/dist/server-vault-routes.js +125 -0
- package/dist/server-workspace-routes.js +44 -8
- package/dist/server.js +12 -367
- package/dist/storage.d.ts +20 -1
- package/dist/storage.js +112 -16
- package/dist/structured-session-manager.js +33 -44
- package/dist/task-types.d.ts +19 -1
- package/dist/task-types.js +6 -1
- package/dist/types.d.ts +6 -0
- package/dist/wand-task-sync.d.ts +5 -3
- package/dist/wand-task-sync.js +30 -12
- package/dist/web-ui/content/scripts.js +76 -73
- package/dist/web-ui/content/styles.css +1 -1
- package/dist/web-ui/embedded-assets.js +2 -2
- package/dist/workspace-binding.d.ts +8 -1
- package/dist/workspace-binding.js +38 -0
- package/package.json +1 -1
package/dist/storage.js
CHANGED
|
@@ -34,6 +34,19 @@ export function parseWandTaskAgent(raw) {
|
|
|
34
34
|
return null;
|
|
35
35
|
return { provider, model: model.trim(), thinkingEffort };
|
|
36
36
|
}
|
|
37
|
+
/** `wand_milestones` 行 → 领域对象;名字必须非空,脏行直接跳过。 */
|
|
38
|
+
function mapWandMilestoneRow(row) {
|
|
39
|
+
const name = typeof row.name === "string" ? row.name.trim() : "";
|
|
40
|
+
if (!name)
|
|
41
|
+
return null;
|
|
42
|
+
return {
|
|
43
|
+
id: String(row.id),
|
|
44
|
+
name,
|
|
45
|
+
dueDate: typeof row.due_date === "string" && row.due_date ? row.due_date : null,
|
|
46
|
+
createdAt: String(row.created_at),
|
|
47
|
+
updatedAt: String(row.updated_at),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
37
50
|
const SESSION_OPTIONS_SCHEMA_VERSION = 1;
|
|
38
51
|
function isRecord(value) {
|
|
39
52
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -330,6 +343,7 @@ function mapWorkspaceTaskRow(row) {
|
|
|
330
343
|
name: row.name,
|
|
331
344
|
worktree: mapWorkspaceTaskWorktree(row.worktree_json),
|
|
332
345
|
...(cwd ? { cwd } : {}),
|
|
346
|
+
milestoneId: typeof row.milestone_id === "string" && row.milestone_id ? row.milestone_id : null,
|
|
333
347
|
layout: mapWorkspaceTaskLayout(row.layout_json),
|
|
334
348
|
status: (row.status === "done" ? "done" : "active"),
|
|
335
349
|
createdAt: row.created_at,
|
|
@@ -606,6 +620,7 @@ const INIT_SQL = `
|
|
|
606
620
|
cwd TEXT NOT NULL,
|
|
607
621
|
status TEXT NOT NULL,
|
|
608
622
|
task_id TEXT,
|
|
623
|
+
milestone_id TEXT,
|
|
609
624
|
base_ref TEXT,
|
|
610
625
|
shared_directories TEXT NOT NULL DEFAULT '[]',
|
|
611
626
|
copy_paths TEXT NOT NULL DEFAULT '[]',
|
|
@@ -686,6 +701,7 @@ const INIT_SQL = `
|
|
|
686
701
|
layout_json TEXT,
|
|
687
702
|
status TEXT NOT NULL DEFAULT 'active',
|
|
688
703
|
cwd TEXT,
|
|
704
|
+
milestone_id TEXT,
|
|
689
705
|
created_at TEXT NOT NULL,
|
|
690
706
|
last_opened_at TEXT,
|
|
691
707
|
layout_revision INTEGER NOT NULL DEFAULT 0,
|
|
@@ -706,6 +722,15 @@ const INIT_SQL = `
|
|
|
706
722
|
);
|
|
707
723
|
CREATE INDEX IF NOT EXISTS idx_github_issue_bindings_session ON github_issue_bindings(session_id);
|
|
708
724
|
|
|
725
|
+
CREATE TABLE IF NOT EXISTS wand_milestones (
|
|
726
|
+
id TEXT PRIMARY KEY,
|
|
727
|
+
name TEXT NOT NULL,
|
|
728
|
+
due_date TEXT,
|
|
729
|
+
created_at TEXT NOT NULL,
|
|
730
|
+
updated_at TEXT NOT NULL
|
|
731
|
+
);
|
|
732
|
+
CREATE INDEX IF NOT EXISTS idx_wand_milestones_created ON wand_milestones(created_at DESC);
|
|
733
|
+
|
|
709
734
|
CREATE TABLE IF NOT EXISTS wand_tasks (
|
|
710
735
|
id TEXT PRIMARY KEY,
|
|
711
736
|
identifier TEXT,
|
|
@@ -716,6 +741,7 @@ const INIT_SQL = `
|
|
|
716
741
|
priority TEXT NOT NULL DEFAULT 'none',
|
|
717
742
|
labels_json TEXT NOT NULL DEFAULT '[]',
|
|
718
743
|
due_date TEXT,
|
|
744
|
+
milestone_id TEXT,
|
|
719
745
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
720
746
|
agent_json TEXT,
|
|
721
747
|
workspace_task_id TEXT,
|
|
@@ -750,6 +776,9 @@ function ensureWandTaskSchema(db) {
|
|
|
750
776
|
// 标题来源:'user' = 用户手写,'auto' = 由描述自动生成。只加列,历史行保持 NULL(读取时视为 user)。
|
|
751
777
|
if (columns.length > 0 && !names.has("title_source"))
|
|
752
778
|
db.exec("ALTER TABLE wand_tasks ADD COLUMN title_source TEXT");
|
|
779
|
+
// 里程碑:只加列,历史行保持 NULL;里程碑本体在 wand_milestones(INIT_SQL 建表)。
|
|
780
|
+
if (columns.length > 0 && !names.has("milestone_id"))
|
|
781
|
+
db.exec("ALTER TABLE wand_tasks ADD COLUMN milestone_id TEXT");
|
|
753
782
|
if (columns.length > 0) {
|
|
754
783
|
// Index creation must wait until the column exists on legacy databases.
|
|
755
784
|
// INIT_SQL cannot create it: CREATE TABLE IF NOT EXISTS is a no-op on old
|
|
@@ -774,6 +803,10 @@ function ensureWorkspaceSchema(db) {
|
|
|
774
803
|
if (taskColumns.length > 0 && !taskNames.has("layout_revision")) {
|
|
775
804
|
db.exec("ALTER TABLE workspace_tasks ADD COLUMN layout_revision INTEGER NOT NULL DEFAULT 0");
|
|
776
805
|
}
|
|
806
|
+
// 侧栏任务也记里程碑:建任务时把它带到看板卡片上,命名任务与未命名任务保持一致。
|
|
807
|
+
if (taskColumns.length > 0 && !taskNames.has("milestone_id")) {
|
|
808
|
+
db.exec("ALTER TABLE workspace_tasks ADD COLUMN milestone_id TEXT");
|
|
809
|
+
}
|
|
777
810
|
}
|
|
778
811
|
function ensureConnectorSchema(db) {
|
|
779
812
|
// The table is created by INIT_SQL for new databases; this repairs an
|
|
@@ -809,6 +842,10 @@ export function ensureDatabaseFile(dbPath) {
|
|
|
809
842
|
if (missionColumns.length > 0 && !missionColumns.some((column) => column.name === "task_id")) {
|
|
810
843
|
db.exec("ALTER TABLE missions ADD COLUMN task_id TEXT");
|
|
811
844
|
}
|
|
845
|
+
// 并行任务的里程碑与看板共用同一份列表;只加列,历史行保持 NULL。
|
|
846
|
+
if (missionColumns.length > 0 && !missionColumns.some((column) => column.name === "milestone_id")) {
|
|
847
|
+
db.exec("ALTER TABLE missions ADD COLUMN milestone_id TEXT");
|
|
848
|
+
}
|
|
812
849
|
}
|
|
813
850
|
db.close();
|
|
814
851
|
chmodSync(dbPath, 0o600);
|
|
@@ -1056,17 +1093,18 @@ export class WandStorage {
|
|
|
1056
1093
|
}));
|
|
1057
1094
|
}
|
|
1058
1095
|
// ── Workspace tasks(任务 = 命名 + 独立 worktree + 一组标签)──
|
|
1096
|
+
/** 侧栏任务顺序:新创建在前。GET /api/tasks 按此顺序填充目录组。 */
|
|
1059
1097
|
listWorkspaceTasks(workspaceId) {
|
|
1060
1098
|
const rows = this.db
|
|
1061
|
-
.prepare(`SELECT id, workspace_id, name, worktree_json, layout_json, status, cwd, created_at, last_opened_at, layout_revision
|
|
1099
|
+
.prepare(`SELECT id, workspace_id, name, worktree_json, layout_json, status, cwd, milestone_id, created_at, last_opened_at, layout_revision
|
|
1062
1100
|
FROM workspace_tasks WHERE workspace_id = ?
|
|
1063
|
-
ORDER BY created_at DESC,
|
|
1101
|
+
ORDER BY created_at DESC, rowid DESC`)
|
|
1064
1102
|
.all(workspaceId);
|
|
1065
1103
|
return rows.map(mapWorkspaceTaskRow);
|
|
1066
1104
|
}
|
|
1067
1105
|
getWorkspaceTask(id) {
|
|
1068
1106
|
const row = this.db
|
|
1069
|
-
.prepare(`SELECT id, workspace_id, name, worktree_json, layout_json, status, cwd, created_at, last_opened_at, layout_revision
|
|
1107
|
+
.prepare(`SELECT id, workspace_id, name, worktree_json, layout_json, status, cwd, milestone_id, created_at, last_opened_at, layout_revision
|
|
1070
1108
|
FROM workspace_tasks WHERE id = ?`)
|
|
1071
1109
|
.get(id);
|
|
1072
1110
|
return row ? mapWorkspaceTaskRow(row) : null;
|
|
@@ -1076,16 +1114,18 @@ export class WandStorage {
|
|
|
1076
1114
|
const createdAt = nowIso();
|
|
1077
1115
|
const status = input.status ?? "active";
|
|
1078
1116
|
const cwd = typeof input.cwd === "string" && input.cwd.trim() ? input.cwd.trim() : null;
|
|
1117
|
+
const milestoneId = typeof input.milestoneId === "string" && input.milestoneId ? input.milestoneId : null;
|
|
1079
1118
|
this.db
|
|
1080
|
-
.prepare(`INSERT INTO workspace_tasks (id, workspace_id, name, worktree_json, layout_json, status, cwd, created_at, last_opened_at)
|
|
1081
|
-
VALUES (?, ?, ?, ?, NULL, ?, ?, ?, NULL)`)
|
|
1082
|
-
.run(id, input.workspaceId, input.name, input.worktree ? JSON.stringify(input.worktree) : null, status, cwd, createdAt);
|
|
1119
|
+
.prepare(`INSERT INTO workspace_tasks (id, workspace_id, name, worktree_json, layout_json, status, cwd, milestone_id, created_at, last_opened_at)
|
|
1120
|
+
VALUES (?, ?, ?, ?, NULL, ?, ?, ?, ?, NULL)`)
|
|
1121
|
+
.run(id, input.workspaceId, input.name, input.worktree ? JSON.stringify(input.worktree) : null, status, cwd, milestoneId, createdAt);
|
|
1083
1122
|
return {
|
|
1084
1123
|
id,
|
|
1085
1124
|
workspaceId: input.workspaceId,
|
|
1086
1125
|
name: input.name,
|
|
1087
1126
|
worktree: input.worktree ?? null,
|
|
1088
1127
|
...(cwd ? { cwd } : {}),
|
|
1128
|
+
milestoneId,
|
|
1089
1129
|
layout: null,
|
|
1090
1130
|
status,
|
|
1091
1131
|
createdAt,
|
|
@@ -1107,6 +1147,10 @@ export class WandStorage {
|
|
|
1107
1147
|
assignments.push("worktree_json = ?");
|
|
1108
1148
|
values.push(patch.worktree ? JSON.stringify(patch.worktree) : null);
|
|
1109
1149
|
}
|
|
1150
|
+
if (patch.milestoneId !== undefined) {
|
|
1151
|
+
assignments.push("milestone_id = ?");
|
|
1152
|
+
values.push(patch.milestoneId || null);
|
|
1153
|
+
}
|
|
1110
1154
|
if (assignments.length === 0)
|
|
1111
1155
|
return;
|
|
1112
1156
|
this.db.prepare(`UPDATE workspace_tasks SET ${assignments.join(", ")} WHERE id = ?`).run(...values, id);
|
|
@@ -1149,16 +1193,24 @@ export class WandStorage {
|
|
|
1149
1193
|
COALESCE(MAX(layout_revision), 0) AS revision
|
|
1150
1194
|
FROM workspace_tasks`).get();
|
|
1151
1195
|
const workspaces = this.db.prepare(`SELECT COUNT(*) AS count, COALESCE(MAX(last_opened_at), '') AS opened FROM workspaces`).get();
|
|
1196
|
+
// 名称也进指纹:改名(含工作区/项目名与目录自定义名)后客户端才肯重拉任务列表。
|
|
1197
|
+
const names = this.db.prepare(`SELECT
|
|
1198
|
+
COALESCE((SELECT GROUP_CONCAT(id || ':' || name || ':' || cwd, '|')
|
|
1199
|
+
FROM (SELECT id, name, cwd FROM workspaces ORDER BY id)), '') AS workspaceNames,
|
|
1200
|
+
COALESCE((SELECT GROUP_CONCAT(path || ':' || name || ':' || updated_at, '|')
|
|
1201
|
+
FROM (SELECT path, name, updated_at FROM session_directory_names ORDER BY path)), '') AS directoryNames`).get();
|
|
1152
1202
|
return JSON.stringify({
|
|
1153
1203
|
sessions,
|
|
1154
1204
|
tasks,
|
|
1155
1205
|
workspaces,
|
|
1206
|
+
names,
|
|
1156
1207
|
});
|
|
1157
1208
|
}
|
|
1209
|
+
/** GET /api/wand-tasks 的顺序:新创建在前。客户端按此顺序渲染,不再本地排序。 */
|
|
1158
1210
|
listWandTasks(workspaceId) {
|
|
1159
|
-
const rows = this.db.prepare(`SELECT id, identifier, workspace_id, workspace_task_id, title, title_source, description, status, priority, labels_json, due_date, sort_order, agent_json, created_at, updated_at
|
|
1211
|
+
const rows = this.db.prepare(`SELECT id, identifier, workspace_id, workspace_task_id, title, title_source, description, status, priority, labels_json, due_date, milestone_id, sort_order, agent_json, created_at, updated_at
|
|
1160
1212
|
FROM wand_tasks ${workspaceId === undefined ? "" : "WHERE workspace_id IS ?"}
|
|
1161
|
-
ORDER BY
|
|
1213
|
+
ORDER BY created_at DESC, rowid DESC`).all(...(workspaceId === undefined ? [] : [workspaceId]));
|
|
1162
1214
|
return rows.map((row) => this.mapWandTaskRow(row));
|
|
1163
1215
|
}
|
|
1164
1216
|
mapWandTaskRow(row) {
|
|
@@ -1170,10 +1222,11 @@ export class WandStorage {
|
|
|
1170
1222
|
title: String(row.title),
|
|
1171
1223
|
titleSource: row.title_source === "auto" ? "auto" : "user",
|
|
1172
1224
|
description: String(row.description ?? ""),
|
|
1173
|
-
status: row.status === "doing" || row.status === "done" ? row.status : "todo",
|
|
1225
|
+
status: row.status === "doing" || row.status === "done" || row.status === "archived" ? row.status : "todo",
|
|
1174
1226
|
priority: row.priority === "low" || row.priority === "medium" || row.priority === "high" || row.priority === "urgent" ? row.priority : "none",
|
|
1175
1227
|
labels: (() => { const parsed = safeJsonParse(String(row.labels_json ?? "[]")); return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string") : []; })(),
|
|
1176
1228
|
dueDate: typeof row.due_date === "string" ? row.due_date : null,
|
|
1229
|
+
milestoneId: typeof row.milestone_id === "string" && row.milestone_id ? row.milestone_id : null,
|
|
1177
1230
|
sortOrder: Number(row.sort_order) || 0,
|
|
1178
1231
|
agent: parseWandTaskAgent(row.agent_json),
|
|
1179
1232
|
createdAt: String(row.created_at),
|
|
@@ -1184,12 +1237,12 @@ export class WandStorage {
|
|
|
1184
1237
|
return this.listWandTasks().find((task) => task.id === id) ?? null;
|
|
1185
1238
|
}
|
|
1186
1239
|
getWandTaskByWorkspaceTaskId(workspaceTaskId) {
|
|
1187
|
-
const row = this.db.prepare(`SELECT id, identifier, workspace_id, workspace_task_id, title, title_source, description, status, priority, labels_json, due_date, sort_order, agent_json, created_at, updated_at
|
|
1240
|
+
const row = this.db.prepare(`SELECT id, identifier, workspace_id, workspace_task_id, title, title_source, description, status, priority, labels_json, due_date, milestone_id, sort_order, agent_json, created_at, updated_at
|
|
1188
1241
|
FROM wand_tasks WHERE workspace_task_id = ? ORDER BY updated_at DESC LIMIT 1`).get(workspaceTaskId);
|
|
1189
1242
|
return row ? this.mapWandTaskRow(row) : null;
|
|
1190
1243
|
}
|
|
1191
1244
|
findUnlinkedWandTask(workspaceId, title) {
|
|
1192
|
-
const row = this.db.prepare(`SELECT id, identifier, workspace_id, workspace_task_id, title, title_source, description, status, priority, labels_json, due_date, sort_order, agent_json, created_at, updated_at
|
|
1245
|
+
const row = this.db.prepare(`SELECT id, identifier, workspace_id, workspace_task_id, title, title_source, description, status, priority, labels_json, due_date, milestone_id, sort_order, agent_json, created_at, updated_at
|
|
1193
1246
|
FROM wand_tasks
|
|
1194
1247
|
WHERE workspace_id IS ? AND title = ? AND (workspace_task_id IS NULL OR workspace_task_id = '')
|
|
1195
1248
|
ORDER BY updated_at DESC LIMIT 1`).get(workspaceId, title);
|
|
@@ -1203,7 +1256,7 @@ export class WandStorage {
|
|
|
1203
1256
|
const max = this.db.prepare("SELECT COALESCE(MAX(sort_order), -1) AS value FROM wand_tasks WHERE workspace_id IS ? AND status = ?").get(input.workspaceId ?? null, status);
|
|
1204
1257
|
const numberRow = this.db.prepare("SELECT COALESCE(MAX(CAST(substr(identifier, 6) AS INTEGER)), 0) AS value FROM wand_tasks WHERE identifier GLOB 'TASK-[0-9]*'").get();
|
|
1205
1258
|
const identifier = `TASK-${(numberRow?.value ?? 0) + 1}`;
|
|
1206
|
-
this.db.prepare(`INSERT INTO wand_tasks (id, identifier, workspace_id, workspace_task_id, title, title_source, description, status, priority, labels_json, due_date, sort_order, agent_json, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, identifier, input.workspaceId ?? null, input.workspaceTaskId ?? null, input.title, input.titleSource ?? "user", input.description ?? "", status, priority, JSON.stringify(input.labels ?? []), input.dueDate ?? null, (max?.value ?? -1) + 1, input.agent ? JSON.stringify(input.agent) : null, now, now);
|
|
1259
|
+
this.db.prepare(`INSERT INTO wand_tasks (id, identifier, workspace_id, workspace_task_id, title, title_source, description, status, priority, labels_json, due_date, milestone_id, sort_order, agent_json, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, identifier, input.workspaceId ?? null, input.workspaceTaskId ?? null, input.title, input.titleSource ?? "user", input.description ?? "", status, priority, JSON.stringify(input.labels ?? []), input.dueDate ?? null, input.milestoneId ?? null, (max?.value ?? -1) + 1, input.agent ? JSON.stringify(input.agent) : null, now, now);
|
|
1207
1260
|
return this.getWandTask(id);
|
|
1208
1261
|
}
|
|
1209
1262
|
updateWandTask(id, patch) {
|
|
@@ -1211,10 +1264,51 @@ export class WandStorage {
|
|
|
1211
1264
|
if (!current)
|
|
1212
1265
|
return null;
|
|
1213
1266
|
const next = { ...current, ...patch, updatedAt: nowIso() };
|
|
1214
|
-
this.db.prepare(`UPDATE wand_tasks SET workspace_id = ?, workspace_task_id = ?, title = ?, title_source = ?, description = ?, status = ?, priority = ?, labels_json = ?, due_date = ?, sort_order = ?, agent_json = ?, updated_at = ? WHERE id = ?`).run(next.workspaceId, next.workspaceTaskId, next.title, next.titleSource, next.description, next.status, next.priority, JSON.stringify(next.labels), next.dueDate, next.sortOrder, next.agent ? JSON.stringify(next.agent) : null, next.updatedAt, id);
|
|
1267
|
+
this.db.prepare(`UPDATE wand_tasks SET workspace_id = ?, workspace_task_id = ?, title = ?, title_source = ?, description = ?, status = ?, priority = ?, labels_json = ?, due_date = ?, milestone_id = ?, sort_order = ?, agent_json = ?, updated_at = ? WHERE id = ?`).run(next.workspaceId, next.workspaceTaskId, next.title, next.titleSource, next.description, next.status, next.priority, JSON.stringify(next.labels), next.dueDate, next.milestoneId ?? null, next.sortOrder, next.agent ? JSON.stringify(next.agent) : null, next.updatedAt, id);
|
|
1215
1268
|
return next;
|
|
1216
1269
|
}
|
|
1217
1270
|
deleteWandTask(id) { this.db.prepare("DELETE FROM wand_tasks WHERE id = ?").run(id); }
|
|
1271
|
+
// ── 里程碑(跨项目全局列表;任务只存 milestoneId)──
|
|
1272
|
+
listWandMilestones() {
|
|
1273
|
+
const rows = this.db.prepare(`SELECT id, name, due_date, created_at, updated_at FROM wand_milestones ORDER BY created_at DESC, rowid DESC`).all();
|
|
1274
|
+
return rows.map(mapWandMilestoneRow).filter((item) => item !== null);
|
|
1275
|
+
}
|
|
1276
|
+
getWandMilestone(id) {
|
|
1277
|
+
const row = this.db.prepare(`SELECT id, name, due_date, created_at, updated_at FROM wand_milestones WHERE id = ?`).get(id);
|
|
1278
|
+
return row ? mapWandMilestoneRow(row) : null;
|
|
1279
|
+
}
|
|
1280
|
+
createWandMilestone(input) {
|
|
1281
|
+
const id = crypto.randomUUID();
|
|
1282
|
+
const now = nowIso();
|
|
1283
|
+
this.db.prepare(`INSERT INTO wand_milestones (id, name, due_date, created_at, updated_at) VALUES (?, ?, ?, ?, ?)`).run(id, input.name, input.dueDate ?? null, now, now);
|
|
1284
|
+
return this.getWandMilestone(id);
|
|
1285
|
+
}
|
|
1286
|
+
updateWandMilestone(id, patch) {
|
|
1287
|
+
const current = this.getWandMilestone(id);
|
|
1288
|
+
if (!current)
|
|
1289
|
+
return null;
|
|
1290
|
+
const next = {
|
|
1291
|
+
name: patch.name ?? current.name,
|
|
1292
|
+
dueDate: patch.dueDate === undefined ? current.dueDate : patch.dueDate,
|
|
1293
|
+
updatedAt: nowIso(),
|
|
1294
|
+
};
|
|
1295
|
+
this.db.prepare(`UPDATE wand_milestones SET name = ?, due_date = ?, updated_at = ? WHERE id = ?`).run(next.name, next.dueDate, next.updatedAt, id);
|
|
1296
|
+
return this.getWandMilestone(id);
|
|
1297
|
+
}
|
|
1298
|
+
/** 删除里程碑只解绑任务(milestone_id 置空),绝不删除任务。 */
|
|
1299
|
+
deleteWandMilestone(id) {
|
|
1300
|
+
const removed = this.db.prepare("DELETE FROM wand_milestones WHERE id = ?").run(id);
|
|
1301
|
+
if (Number(removed.changes) === 0)
|
|
1302
|
+
return false;
|
|
1303
|
+
this.db.prepare("UPDATE wand_tasks SET milestone_id = NULL WHERE milestone_id = ?").run(id);
|
|
1304
|
+
this.db.prepare("UPDATE workspace_tasks SET milestone_id = NULL WHERE milestone_id = ?").run(id);
|
|
1305
|
+
return true;
|
|
1306
|
+
}
|
|
1307
|
+
/** 各里程碑下的任务数(看板卡片),用于下拉里的数量提示。 */
|
|
1308
|
+
countWandTasksByMilestone() {
|
|
1309
|
+
const rows = this.db.prepare(`SELECT milestone_id, COUNT(*) AS count FROM wand_tasks WHERE milestone_id IS NOT NULL GROUP BY milestone_id`).all();
|
|
1310
|
+
return Object.fromEntries(rows.map((row) => [row.milestone_id, Number(row.count) || 0]));
|
|
1311
|
+
}
|
|
1218
1312
|
listWandTaskSessionIds(taskId) {
|
|
1219
1313
|
const rows = this.db.prepare("SELECT session_id FROM wand_task_sessions WHERE task_id = ? ORDER BY rowid ASC, session_id ASC").all(taskId);
|
|
1220
1314
|
return rows.map((row) => row.session_id);
|
|
@@ -1498,13 +1592,14 @@ export class WandStorage {
|
|
|
1498
1592
|
// ============ Missions ============
|
|
1499
1593
|
saveMission(mission) {
|
|
1500
1594
|
this.db.prepare(`INSERT INTO missions (
|
|
1501
|
-
id, title, prompt, cwd, status, base_ref, shared_directories, copy_paths, created_at, updated_at, task_id
|
|
1502
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1595
|
+
id, title, prompt, cwd, status, base_ref, shared_directories, copy_paths, created_at, updated_at, task_id, milestone_id
|
|
1596
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1503
1597
|
ON CONFLICT(id) DO UPDATE SET
|
|
1504
1598
|
title = excluded.title, prompt = excluded.prompt, cwd = excluded.cwd,
|
|
1505
1599
|
status = excluded.status, base_ref = excluded.base_ref,
|
|
1506
1600
|
shared_directories = excluded.shared_directories, copy_paths = excluded.copy_paths,
|
|
1507
|
-
updated_at = excluded.updated_at, task_id = excluded.task_id
|
|
1601
|
+
updated_at = excluded.updated_at, task_id = excluded.task_id,
|
|
1602
|
+
milestone_id = excluded.milestone_id`).run(mission.id, mission.title, mission.prompt, mission.cwd, mission.status, mission.worktree.baseRef ?? null, JSON.stringify(mission.worktree.sharedDirectories ?? []), JSON.stringify(mission.worktree.copyPaths ?? []), mission.createdAt, mission.updatedAt, mission.taskId ?? null, mission.milestoneId ?? null);
|
|
1508
1603
|
}
|
|
1509
1604
|
getMission(id) {
|
|
1510
1605
|
const row = this.db.prepare("SELECT * FROM missions WHERE id = ?").get(id);
|
|
@@ -1688,6 +1783,7 @@ function mapMissionRow(row) {
|
|
|
1688
1783
|
copyPaths: safeJsonParse(typeof row.copy_paths === "string" ? row.copy_paths : null) ?? [],
|
|
1689
1784
|
},
|
|
1690
1785
|
taskId: typeof row.task_id === "string" ? row.task_id : null,
|
|
1786
|
+
milestoneId: typeof row.milestone_id === "string" && row.milestone_id ? row.milestone_id : null,
|
|
1691
1787
|
createdAt: String(row.created_at),
|
|
1692
1788
|
updatedAt: String(row.updated_at),
|
|
1693
1789
|
};
|
|
@@ -283,6 +283,27 @@ function buildIncrementalStructuredPayload(snapshot, cardDefaults) {
|
|
|
283
283
|
messageCount: messages.length,
|
|
284
284
|
};
|
|
285
285
|
}
|
|
286
|
+
function isoNow() {
|
|
287
|
+
return new Date().toISOString();
|
|
288
|
+
}
|
|
289
|
+
function upsertAssistantMessage(messages, turn, complete = false) {
|
|
290
|
+
const msgs = [...(messages ?? [])];
|
|
291
|
+
const last = msgs[msgs.length - 1];
|
|
292
|
+
const createdAt = (last?.role === "assistant" ? last.createdAt : undefined) ?? turn.createdAt ?? isoNow();
|
|
293
|
+
const next = {
|
|
294
|
+
...turn,
|
|
295
|
+
createdAt,
|
|
296
|
+
};
|
|
297
|
+
if (complete)
|
|
298
|
+
next.completedAt = isoNow();
|
|
299
|
+
else if (last?.role === "assistant" && last.completedAt)
|
|
300
|
+
next.completedAt = last.completedAt;
|
|
301
|
+
if (last?.role === "assistant")
|
|
302
|
+
msgs[msgs.length - 1] = next;
|
|
303
|
+
else
|
|
304
|
+
msgs.push(next);
|
|
305
|
+
return msgs;
|
|
306
|
+
}
|
|
286
307
|
export class StructuredSessionManager {
|
|
287
308
|
storage;
|
|
288
309
|
config;
|
|
@@ -570,11 +591,7 @@ export class StructuredSessionManager {
|
|
|
570
591
|
content: this.compactContentBlocks([...turnState.blocks], turnState.result),
|
|
571
592
|
usage: turnState.usage,
|
|
572
593
|
};
|
|
573
|
-
const messages =
|
|
574
|
-
if (messages[messages.length - 1]?.role === "assistant")
|
|
575
|
-
messages[messages.length - 1] = turn;
|
|
576
|
-
else
|
|
577
|
-
messages.push(turn);
|
|
594
|
+
const messages = upsertAssistantMessage(current.messages, turn);
|
|
578
595
|
const patched = {
|
|
579
596
|
...current,
|
|
580
597
|
claudeSessionId: turnState.sessionId ?? current.claudeSessionId,
|
|
@@ -1111,9 +1128,11 @@ export class StructuredSessionManager {
|
|
|
1111
1128
|
// child 是被 SIGTERM 主动 kill 的,正在等用户回答)。如果有,把这次的输入打包
|
|
1112
1129
|
// 成 tool_result 注入到 messages,让 UI 把卡片渲染为 answered。
|
|
1113
1130
|
const pendingAsk = findUnpairedAskUserQuestion(session.messages ?? []);
|
|
1131
|
+
const userTurnAt = isoNow();
|
|
1114
1132
|
const userTurn = pendingAsk
|
|
1115
1133
|
? {
|
|
1116
1134
|
role: "user",
|
|
1135
|
+
createdAt: userTurnAt,
|
|
1117
1136
|
content: [
|
|
1118
1137
|
{
|
|
1119
1138
|
type: "tool_result",
|
|
@@ -1125,6 +1144,7 @@ export class StructuredSessionManager {
|
|
|
1125
1144
|
}
|
|
1126
1145
|
: {
|
|
1127
1146
|
role: "user",
|
|
1147
|
+
createdAt: userTurnAt,
|
|
1128
1148
|
content: [{ type: "text", text: prompt }],
|
|
1129
1149
|
};
|
|
1130
1150
|
const requestId = randomUUID();
|
|
@@ -1827,11 +1847,7 @@ export class StructuredSessionManager {
|
|
|
1827
1847
|
content: this.compactContentBlocks([...turnState.blocks], turnState.result),
|
|
1828
1848
|
usage: turnState.usage,
|
|
1829
1849
|
};
|
|
1830
|
-
const messages =
|
|
1831
|
-
if (messages[messages.length - 1]?.role === "assistant")
|
|
1832
|
-
messages[messages.length - 1] = turn;
|
|
1833
|
-
else
|
|
1834
|
-
messages.push(turn);
|
|
1850
|
+
const messages = upsertAssistantMessage(current.messages, turn);
|
|
1835
1851
|
const patched = {
|
|
1836
1852
|
...current,
|
|
1837
1853
|
claudeSessionId: turnState.sessionId ?? current.claudeSessionId,
|
|
@@ -1986,11 +2002,7 @@ export class StructuredSessionManager {
|
|
|
1986
2002
|
content: this.compactContentBlocks([...turnState.blocks], turnState.result),
|
|
1987
2003
|
usage: turnState.usage,
|
|
1988
2004
|
};
|
|
1989
|
-
const messages =
|
|
1990
|
-
if (messages.at(-1)?.role === "assistant")
|
|
1991
|
-
messages[messages.length - 1] = turn;
|
|
1992
|
-
else
|
|
1993
|
-
messages.push(turn);
|
|
2005
|
+
const messages = upsertAssistantMessage(current.messages, turn);
|
|
1994
2006
|
const patched = {
|
|
1995
2007
|
...current,
|
|
1996
2008
|
claudeSessionId: turnState.sessionId ?? current.claudeSessionId,
|
|
@@ -2133,11 +2145,7 @@ export class StructuredSessionManager {
|
|
|
2133
2145
|
content: this.compactContentBlocks([...turnState.blocks], turnState.result),
|
|
2134
2146
|
usage: turnState.usage,
|
|
2135
2147
|
};
|
|
2136
|
-
const messages =
|
|
2137
|
-
if (messages[messages.length - 1]?.role === "assistant")
|
|
2138
|
-
messages[messages.length - 1] = turn;
|
|
2139
|
-
else
|
|
2140
|
-
messages.push(turn);
|
|
2148
|
+
const messages = upsertAssistantMessage(current.messages, turn);
|
|
2141
2149
|
const patched = {
|
|
2142
2150
|
...current,
|
|
2143
2151
|
claudeSessionId: turnState.sessionId ?? current.claudeSessionId,
|
|
@@ -2300,17 +2308,14 @@ export class StructuredSessionManager {
|
|
|
2300
2308
|
if (!current)
|
|
2301
2309
|
return;
|
|
2302
2310
|
const hasAssistantContent = turnState.blocks.length > 0 || !!turnState.result;
|
|
2303
|
-
|
|
2311
|
+
let messages = [...(current.messages ?? [])];
|
|
2304
2312
|
if (hasAssistantContent) {
|
|
2305
2313
|
const turn = {
|
|
2306
2314
|
role: "assistant",
|
|
2307
2315
|
content: this.compactContentBlocks([...turnState.blocks], turnState.result),
|
|
2308
2316
|
usage: turnState.usage,
|
|
2309
2317
|
};
|
|
2310
|
-
|
|
2311
|
-
messages[messages.length - 1] = turn;
|
|
2312
|
-
else
|
|
2313
|
-
messages.push(turn);
|
|
2318
|
+
messages = upsertAssistantMessage(current.messages, turn);
|
|
2314
2319
|
}
|
|
2315
2320
|
const patched = {
|
|
2316
2321
|
...current,
|
|
@@ -2666,12 +2671,7 @@ export class StructuredSessionManager {
|
|
|
2666
2671
|
content: this.compactContentBlocks([...turnState.blocks], turnState.result),
|
|
2667
2672
|
usage: turnState.usage,
|
|
2668
2673
|
};
|
|
2669
|
-
const msgs =
|
|
2670
|
-
const lastMsg = msgs[msgs.length - 1];
|
|
2671
|
-
if (lastMsg && lastMsg.role === "assistant")
|
|
2672
|
-
msgs[msgs.length - 1] = inProgressTurn;
|
|
2673
|
-
else
|
|
2674
|
-
msgs.push(inProgressTurn);
|
|
2674
|
+
const msgs = upsertAssistantMessage(current.messages, inProgressTurn);
|
|
2675
2675
|
const patched = {
|
|
2676
2676
|
...current,
|
|
2677
2677
|
claudeSessionId: turnState.sessionId ?? current.claudeSessionId,
|
|
@@ -3009,13 +3009,7 @@ export class StructuredSessionManager {
|
|
|
3009
3009
|
content: this.compactContentBlocks([...turnState.blocks], turnState.result),
|
|
3010
3010
|
usage: turnState.usage,
|
|
3011
3011
|
};
|
|
3012
|
-
|
|
3013
|
-
const lastMsg = msgs[msgs.length - 1];
|
|
3014
|
-
if (lastMsg && lastMsg.role === "assistant")
|
|
3015
|
-
msgs[msgs.length - 1] = assistantTurn;
|
|
3016
|
-
else
|
|
3017
|
-
msgs.push(assistantTurn);
|
|
3018
|
-
return msgs;
|
|
3012
|
+
return upsertAssistantMessage(current.messages, assistantTurn, true);
|
|
3019
3013
|
}
|
|
3020
3014
|
resolveQueuedMessagesAfterInterrupt(sessionId, current, interruptPrompt) {
|
|
3021
3015
|
if (interruptPrompt && !this.preserveQueueOnInterrupt.has(sessionId))
|
|
@@ -3066,12 +3060,7 @@ export class StructuredSessionManager {
|
|
|
3066
3060
|
role: "assistant",
|
|
3067
3061
|
content: [{ type: "text", text: `结构化会话执行失败:${errorText}` }],
|
|
3068
3062
|
};
|
|
3069
|
-
const msgs =
|
|
3070
|
-
const lastMsg = msgs[msgs.length - 1];
|
|
3071
|
-
if (lastMsg && lastMsg.role === "assistant")
|
|
3072
|
-
msgs[msgs.length - 1] = failureTurn;
|
|
3073
|
-
else
|
|
3074
|
-
msgs.push(failureTurn);
|
|
3063
|
+
const msgs = upsertAssistantMessage(current.messages, failureTurn, true);
|
|
3075
3064
|
return {
|
|
3076
3065
|
...current,
|
|
3077
3066
|
status: "failed",
|
package/dist/task-types.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
export type WandTaskStatus = "todo" | "doing" | "done";
|
|
1
|
+
export type WandTaskStatus = "todo" | "doing" | "done" | "archived";
|
|
2
|
+
/** 已确认或已归档:不再算未完成,也不会被未分组会话同步重新打开。 */
|
|
3
|
+
export declare function isClosedWandTaskStatus(status: WandTaskStatus): boolean;
|
|
2
4
|
export type WandTaskPriority = "none" | "low" | "medium" | "high" | "urgent";
|
|
3
5
|
/** 任务派发时选择的 CLI 工具;空串表示尚未指定。 */
|
|
4
6
|
export type WandTaskAgentProvider = "claude" | "codex" | "opencode" | "grok" | "qoder" | "pi";
|
|
@@ -13,6 +15,20 @@ export interface WandTaskAgent {
|
|
|
13
15
|
}
|
|
14
16
|
/** 任务标题来源;标题是可选字段,留空时由服务端按描述自动生成。 */
|
|
15
17
|
export type WandTaskTitleSource = "user" | "auto";
|
|
18
|
+
/** 里程碑名上限;所有任务面板的新增入口共用同一约束。 */
|
|
19
|
+
export declare const WAND_MILESTONE_NAME_MAX_LENGTH = 60;
|
|
20
|
+
/**
|
|
21
|
+
* 里程碑:跨项目的全局列表(不是每个目录一份),创建任务时从中选择或新增。
|
|
22
|
+
* 任务只保存 milestoneId,删除里程碑时只解绑任务、不删除任务。
|
|
23
|
+
*/
|
|
24
|
+
export interface WandTaskMilestone {
|
|
25
|
+
id: string;
|
|
26
|
+
name: string;
|
|
27
|
+
/** YYYY-MM-DD,可为空。 */
|
|
28
|
+
dueDate: string | null;
|
|
29
|
+
createdAt: string;
|
|
30
|
+
updatedAt: string;
|
|
31
|
+
}
|
|
16
32
|
export interface WandTask {
|
|
17
33
|
id: string;
|
|
18
34
|
workspaceId: string | null;
|
|
@@ -27,6 +43,8 @@ export interface WandTask {
|
|
|
27
43
|
priority: WandTaskPriority;
|
|
28
44
|
labels: string[];
|
|
29
45
|
dueDate: string | null;
|
|
46
|
+
/** 所属里程碑;null 表示未归入任何里程碑。 */
|
|
47
|
+
milestoneId: string | null;
|
|
30
48
|
sortOrder: number;
|
|
31
49
|
/** 该任务绑定的执行 Agent;null 表示还没指定。 */
|
|
32
50
|
agent: WandTaskAgent | null;
|
package/dist/task-types.js
CHANGED
package/dist/types.d.ts
CHANGED
|
@@ -495,6 +495,10 @@ export type ContentBlock = TextBlock | ThinkingBlock | ToolUseBlock | ToolResult
|
|
|
495
495
|
export interface ConversationTurn {
|
|
496
496
|
role: "user" | "assistant";
|
|
497
497
|
content: ContentBlock[];
|
|
498
|
+
/** ISO time when this turn was first recorded (user send / assistant start). */
|
|
499
|
+
createdAt?: string;
|
|
500
|
+
/** ISO time when an assistant turn finished streaming. */
|
|
501
|
+
completedAt?: string;
|
|
498
502
|
/** Optional usage metadata when available from the underlying tool. */
|
|
499
503
|
usage?: {
|
|
500
504
|
inputTokens?: number;
|
|
@@ -691,6 +695,8 @@ export interface WorkspaceTask {
|
|
|
691
695
|
worktree: WorkspaceTaskWorktree | null;
|
|
692
696
|
/** Optional directory override; empty/undefined falls back to the workspace cwd. */
|
|
693
697
|
cwd?: string;
|
|
698
|
+
/** 归属里程碑(全局列表);null = 未归入。建任务时同步到看板卡片。 */
|
|
699
|
+
milestoneId?: string | null;
|
|
694
700
|
/** 该任务的工作窗口 Tabs;每个窗口内部可含一棵分屏树。 */
|
|
695
701
|
layout: TaskWindowLayout | null;
|
|
696
702
|
status: WorkspaceTaskStatus;
|
package/dist/wand-task-sync.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { WandStorage } from "./storage.js";
|
|
2
|
-
import type
|
|
2
|
+
import { type WandTask } from "./task-types.js";
|
|
3
3
|
import type { SessionSnapshot, Workspace, WorkspaceTask } from "./types.js";
|
|
4
4
|
export declare function isUnnamedWorkspaceTaskName(name: string): boolean;
|
|
5
5
|
/** 从未分组会话的标题 / 描述 / 首条用户消息里抽出看板标题。 */
|
|
@@ -11,7 +11,9 @@ export declare function ensureBoardTaskForWorkspaceTask(storage: WandStorage, ta
|
|
|
11
11
|
* 幂等,打开看板列表时调用。
|
|
12
12
|
*/
|
|
13
13
|
export declare function syncUngroupedSessionsToBoard(storage: WandStorage): void;
|
|
14
|
-
/**
|
|
14
|
+
/** 看板确认/归档时,把关联的侧栏工作任务标成 done(不删会话)。 */
|
|
15
|
+
export declare function syncClosedBoardTask(storage: WandStorage, task: WandTask): void;
|
|
16
|
+
/** 看板归档:卡片进入归档目录,并同步把关联的侧栏工作任务标成 done(不删会话)。 */
|
|
15
17
|
export declare function archiveBoardTask(storage: WandStorage, id: string): WandTask | null;
|
|
16
|
-
/**
|
|
18
|
+
/** 侧栏工作任务完成/删除时,把对应看板卡片标成已完成;已归档的保持归档。 */
|
|
17
19
|
export declare function archiveBoardTaskForWorkspaceTask(storage: WandStorage, workspaceTaskId: string): void;
|
package/dist/wand-task-sync.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { provisionalTaskTitleFromDescription } from "./task-title.js";
|
|
2
|
+
import { isClosedWandTaskStatus } from "./task-types.js";
|
|
2
3
|
const UNNAMED_TASK_NAME = "未命名任务";
|
|
3
4
|
function boardDescriptionFor(task, workspace) {
|
|
4
5
|
const lines = [`项目:${workspace.name}`];
|
|
@@ -92,7 +93,7 @@ function promoteUngroupedCard(storage, card, patch) {
|
|
|
92
93
|
title: nextTitle,
|
|
93
94
|
titleSource: nextTitle !== card.title ? "auto" : card.titleSource,
|
|
94
95
|
description: nextDescription,
|
|
95
|
-
status: card.status
|
|
96
|
+
status: isClosedWandTaskStatus(card.status) ? card.status : "doing",
|
|
96
97
|
workspaceId: patch.workspaceId !== undefined ? patch.workspaceId : card.workspaceId,
|
|
97
98
|
workspaceTaskId: patch.workspaceTaskId !== undefined ? patch.workspaceTaskId : card.workspaceTaskId,
|
|
98
99
|
}) ?? card;
|
|
@@ -110,6 +111,7 @@ export function ensureBoardTaskForWorkspaceTask(storage, task, workspace) {
|
|
|
110
111
|
return storage.updateWandTask(reusable.id, {
|
|
111
112
|
workspaceTaskId: task.id,
|
|
112
113
|
workspaceId: workspace.id,
|
|
114
|
+
...(task.milestoneId ? { milestoneId: task.milestoneId } : {}),
|
|
113
115
|
}) ?? reusable;
|
|
114
116
|
}
|
|
115
117
|
return storage.createWandTask({
|
|
@@ -118,6 +120,7 @@ export function ensureBoardTaskForWorkspaceTask(storage, task, workspace) {
|
|
|
118
120
|
title: task.name,
|
|
119
121
|
description: boardDescriptionFor(task, workspace),
|
|
120
122
|
status: task.status === "done" ? "done" : "todo",
|
|
123
|
+
milestoneId: task.milestoneId ?? null,
|
|
121
124
|
});
|
|
122
125
|
}
|
|
123
126
|
/**
|
|
@@ -147,6 +150,8 @@ export function syncUngroupedSessionsToBoard(storage) {
|
|
|
147
150
|
titleSource: "auto",
|
|
148
151
|
description,
|
|
149
152
|
status: "doing",
|
|
153
|
+
// 未命名任务建立板卡时才补上:建任务时选的里程碑不能丢。
|
|
154
|
+
milestoneId: task.milestoneId ?? null,
|
|
150
155
|
});
|
|
151
156
|
boardTasks.push(card);
|
|
152
157
|
}
|
|
@@ -157,6 +162,9 @@ export function syncUngroupedSessionsToBoard(storage) {
|
|
|
157
162
|
workspaceId: workspace.id,
|
|
158
163
|
workspaceTaskId: task.id,
|
|
159
164
|
});
|
|
165
|
+
if (!card.milestoneId && task.milestoneId) {
|
|
166
|
+
card = storage.updateWandTask(card.id, { milestoneId: task.milestoneId }) ?? card;
|
|
167
|
+
}
|
|
160
168
|
}
|
|
161
169
|
bindSessions(storage, card.id, sessions.map((session) => session.id));
|
|
162
170
|
for (const session of sessions)
|
|
@@ -182,7 +190,7 @@ export function syncUngroupedSessionsToBoard(storage) {
|
|
|
182
190
|
});
|
|
183
191
|
boardTasks.push(card);
|
|
184
192
|
}
|
|
185
|
-
else if (card.status
|
|
193
|
+
else if (!isClosedWandTaskStatus(card.status)) {
|
|
186
194
|
card = promoteUngroupedCard(storage, card, {
|
|
187
195
|
title,
|
|
188
196
|
description: boardDescriptionFromSession(session),
|
|
@@ -193,24 +201,34 @@ export function syncUngroupedSessionsToBoard(storage) {
|
|
|
193
201
|
bound.add(session.id);
|
|
194
202
|
}
|
|
195
203
|
}
|
|
196
|
-
|
|
204
|
+
function closeLinkedWorkspaceTask(storage, workspaceTaskId) {
|
|
205
|
+
if (!workspaceTaskId)
|
|
206
|
+
return;
|
|
207
|
+
const workspaceTask = storage.getWorkspaceTask(workspaceTaskId);
|
|
208
|
+
if (workspaceTask && workspaceTask.status !== "done") {
|
|
209
|
+
storage.updateWorkspaceTask(workspaceTask.id, { status: "done" });
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
/** 看板确认/归档时,把关联的侧栏工作任务标成 done(不删会话)。 */
|
|
213
|
+
export function syncClosedBoardTask(storage, task) {
|
|
214
|
+
if (!isClosedWandTaskStatus(task.status))
|
|
215
|
+
return;
|
|
216
|
+
closeLinkedWorkspaceTask(storage, task.workspaceTaskId);
|
|
217
|
+
}
|
|
218
|
+
/** 看板归档:卡片进入归档目录,并同步把关联的侧栏工作任务标成 done(不删会话)。 */
|
|
197
219
|
export function archiveBoardTask(storage, id) {
|
|
198
220
|
const current = storage.getWandTask(id);
|
|
199
221
|
if (!current)
|
|
200
222
|
return null;
|
|
201
|
-
const archived = storage.updateWandTask(id, { status: "
|
|
202
|
-
if (
|
|
203
|
-
|
|
204
|
-
if (workspaceTask && workspaceTask.status !== "done") {
|
|
205
|
-
storage.updateWorkspaceTask(workspaceTask.id, { status: "done" });
|
|
206
|
-
}
|
|
207
|
-
}
|
|
223
|
+
const archived = storage.updateWandTask(id, { status: "archived" });
|
|
224
|
+
if (archived)
|
|
225
|
+
closeLinkedWorkspaceTask(storage, archived.workspaceTaskId);
|
|
208
226
|
return archived;
|
|
209
227
|
}
|
|
210
|
-
/**
|
|
228
|
+
/** 侧栏工作任务完成/删除时,把对应看板卡片标成已完成;已归档的保持归档。 */
|
|
211
229
|
export function archiveBoardTaskForWorkspaceTask(storage, workspaceTaskId) {
|
|
212
230
|
const linked = storage.getWandTaskByWorkspaceTaskId(workspaceTaskId);
|
|
213
|
-
if (!linked || linked.status
|
|
231
|
+
if (!linked || isClosedWandTaskStatus(linked.status))
|
|
214
232
|
return;
|
|
215
233
|
storage.updateWandTask(linked.id, { status: "done" });
|
|
216
234
|
}
|