@co0ontty/wand 4.68.1 → 4.69.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/storage.js CHANGED
@@ -347,8 +347,8 @@ function sessionPersistAssignments() {
347
347
  title = excluded.title,
348
348
  description = excluded.description,
349
349
  session_options = excluded.session_options,
350
- workspace_id = excluded.workspace_id,
351
- workspace_task_id = excluded.workspace_task_id`;
350
+ workspace_id = command_sessions.workspace_id,
351
+ workspace_task_id = command_sessions.workspace_task_id`;
352
352
  }
353
353
  function sessionRuntimeMetadataAssignments() {
354
354
  return `session_source = ?, automation_id = ?,
@@ -708,6 +708,7 @@ const INIT_SQL = `
708
708
  agent_json TEXT,
709
709
  workspace_task_id TEXT,
710
710
  title_source TEXT,
711
+ auto_title_signature TEXT,
711
712
  created_at TEXT NOT NULL,
712
713
  updated_at TEXT NOT NULL,
713
714
  FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE SET NULL
@@ -735,9 +736,12 @@ function ensureWandTaskSchema(db) {
735
736
  db.exec("ALTER TABLE wand_tasks ADD COLUMN agent_json TEXT");
736
737
  if (columns.length > 0 && !names.has("workspace_task_id"))
737
738
  db.exec("ALTER TABLE wand_tasks ADD COLUMN workspace_task_id TEXT");
738
- // 标题来源:'user' = 用户手写,'auto' = 由描述自动生成。只加列,历史行保持 NULL(读取时视为 user)。
739
+ // 标题来源:'user' = 用户手写,'auto' = 由描述/会话内容自动生成。只加列,历史行保持 NULL(读取时视为 user)。
739
740
  if (columns.length > 0 && !names.has("title_source"))
740
741
  db.exec("ALTER TABLE wand_tasks ADD COLUMN title_source TEXT");
742
+ // 自动标题最近一次的输入指纹:内容没变就不再重复总结(也避免自动标题来回震荡)。只加列。
743
+ if (columns.length > 0 && !names.has("auto_title_signature"))
744
+ db.exec("ALTER TABLE wand_tasks ADD COLUMN auto_title_signature TEXT");
741
745
  // 里程碑:只加列,历史行保持 NULL;里程碑本体在 wand_milestones(INIT_SQL 建表)。
742
746
  if (columns.length > 0 && !names.has("milestone_id"))
743
747
  db.exec("ALTER TABLE wand_tasks ADD COLUMN milestone_id TEXT");
@@ -1028,6 +1032,11 @@ export class WandStorage {
1028
1032
  return rows.map((row) => this.mapSessionRow(row));
1029
1033
  }
1030
1034
  /** 显式更新某会话的工作空间归属(用于创建时绑定)。 */
1035
+ getSessionWorkspace(sessionId) {
1036
+ const row = this.db.prepare("SELECT workspace_id, workspace_task_id FROM command_sessions WHERE id = ?")
1037
+ .get(sessionId);
1038
+ return row ? { workspaceId: row.workspace_id ?? undefined, workspaceTaskId: row.workspace_task_id ?? undefined } : null;
1039
+ }
1031
1040
  setSessionWorkspaceId(sessionId, workspaceId) {
1032
1041
  this.db.prepare("UPDATE command_sessions SET workspace_id = ? WHERE id = ?").run(workspaceId, sessionId);
1033
1042
  }
@@ -1097,6 +1106,10 @@ export class WandStorage {
1097
1106
  updateWorkspaceTask(id, patch) {
1098
1107
  const assignments = [];
1099
1108
  const values = [];
1109
+ if (patch.workspaceId !== undefined) {
1110
+ assignments.push("workspace_id = ?");
1111
+ values.push(patch.workspaceId);
1112
+ }
1100
1113
  if (patch.name !== undefined) {
1101
1114
  assignments.push("name = ?");
1102
1115
  values.push(patch.name);
@@ -1116,6 +1129,19 @@ export class WandStorage {
1116
1129
  if (assignments.length === 0)
1117
1130
  return;
1118
1131
  this.db.prepare(`UPDATE workspace_tasks SET ${assignments.join(", ")} WHERE id = ?`).run(...values, id);
1132
+ const card = this.getWandTaskByWorkspaceTaskId(id);
1133
+ if (card) {
1134
+ // Reverse projection uses direct SQL, so these writers cannot recurse.
1135
+ this.updateWandTask(card.id, {
1136
+ ...(patch.name !== undefined ? { title: patch.name, titleSource: "user" } : {}),
1137
+ ...(patch.milestoneId !== undefined ? { milestoneId: patch.milestoneId } : {}),
1138
+ ...(patch.status !== undefined ? {
1139
+ status: patch.status === "done"
1140
+ ? card.status === "archived" ? "archived" : "done"
1141
+ : card.status === "todo" ? "todo" : "doing",
1142
+ } : {}),
1143
+ });
1144
+ }
1119
1145
  }
1120
1146
  saveWorkspaceTaskLayout(id, layout, expectedRevision) {
1121
1147
  const current = this.getWorkspaceTask(id);
@@ -1161,8 +1187,10 @@ export class WandStorage {
1161
1187
  FROM (SELECT id, name, cwd FROM workspaces ORDER BY id)), '') AS workspaceNames,
1162
1188
  COALESCE((SELECT GROUP_CONCAT(path || ':' || name || ':' || updated_at, '|')
1163
1189
  FROM (SELECT path, name, updated_at FROM session_directory_names ORDER BY path)), '') AS directoryNames`).get();
1190
+ const membership = this.db.prepare("SELECT id, workspace_id, workspace_task_id FROM command_sessions ORDER BY id").all();
1164
1191
  return JSON.stringify({
1165
1192
  sessions,
1193
+ membership,
1166
1194
  tasks,
1167
1195
  workspaces,
1168
1196
  names,
@@ -1170,7 +1198,7 @@ export class WandStorage {
1170
1198
  }
1171
1199
  /** GET /api/wand-tasks 的顺序:新创建在前。客户端按此顺序渲染,不再本地排序。 */
1172
1200
  listWandTasks(workspaceId) {
1173
- 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
1201
+ const rows = this.db.prepare(`SELECT id, identifier, workspace_id, workspace_task_id, title, title_source, auto_title_signature, description, status, priority, labels_json, due_date, milestone_id, sort_order, agent_json, created_at, updated_at
1174
1202
  FROM wand_tasks ${workspaceId === undefined ? "" : "WHERE workspace_id IS ?"}
1175
1203
  ORDER BY created_at DESC, rowid DESC`).all(...(workspaceId === undefined ? [] : [workspaceId]));
1176
1204
  return rows.map((row) => this.mapWandTaskRow(row));
@@ -1183,6 +1211,7 @@ export class WandStorage {
1183
1211
  workspaceTaskId: typeof row.workspace_task_id === "string" && row.workspace_task_id ? row.workspace_task_id : null,
1184
1212
  title: String(row.title),
1185
1213
  titleSource: row.title_source === "auto" ? "auto" : "user",
1214
+ autoTitleSignature: typeof row.auto_title_signature === "string" && row.auto_title_signature ? row.auto_title_signature : null,
1186
1215
  description: String(row.description ?? ""),
1187
1216
  status: row.status === "doing" || row.status === "done" || row.status === "archived" ? row.status : "todo",
1188
1217
  priority: row.priority === "low" || row.priority === "medium" || row.priority === "high" || row.priority === "urgent" ? row.priority : "none",
@@ -1199,17 +1228,10 @@ export class WandStorage {
1199
1228
  return this.listWandTasks().find((task) => task.id === id) ?? null;
1200
1229
  }
1201
1230
  getWandTaskByWorkspaceTaskId(workspaceTaskId) {
1202
- 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
1231
+ const row = this.db.prepare(`SELECT id, identifier, workspace_id, workspace_task_id, title, title_source, auto_title_signature, description, status, priority, labels_json, due_date, milestone_id, sort_order, agent_json, created_at, updated_at
1203
1232
  FROM wand_tasks WHERE workspace_task_id = ? ORDER BY updated_at DESC LIMIT 1`).get(workspaceTaskId);
1204
1233
  return row ? this.mapWandTaskRow(row) : null;
1205
1234
  }
1206
- findUnlinkedWandTask(workspaceId, title) {
1207
- 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
1208
- FROM wand_tasks
1209
- WHERE workspace_id IS ? AND title = ? AND (workspace_task_id IS NULL OR workspace_task_id = '')
1210
- ORDER BY updated_at DESC LIMIT 1`).get(workspaceId, title);
1211
- return row ? this.mapWandTaskRow(row) : null;
1212
- }
1213
1235
  createWandTask(input) {
1214
1236
  const id = crypto.randomUUID();
1215
1237
  const now = nowIso();
@@ -1218,7 +1240,7 @@ export class WandStorage {
1218
1240
  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);
1219
1241
  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();
1220
1242
  const identifier = `TASK-${(numberRow?.value ?? 0) + 1}`;
1221
- 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);
1243
+ this.db.prepare(`INSERT INTO wand_tasks (id, identifier, workspace_id, workspace_task_id, title, title_source, auto_title_signature, 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", null, 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);
1222
1244
  return this.getWandTask(id);
1223
1245
  }
1224
1246
  updateWandTask(id, patch) {
@@ -1226,7 +1248,14 @@ export class WandStorage {
1226
1248
  if (!current)
1227
1249
  return null;
1228
1250
  const next = { ...current, ...patch, updatedAt: nowIso() };
1229
- 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);
1251
+ this.db.prepare(`UPDATE wand_tasks SET workspace_id = ?, workspace_task_id = ?, title = ?, title_source = ?, auto_title_signature = ?, 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.autoTitleSignature ?? null, 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);
1252
+ if (next.workspaceTaskId) {
1253
+ const workspaceId = next.workspaceId ?? this.ensureGlobalWorkspace().id;
1254
+ this.db.prepare(`UPDATE workspace_tasks SET name = ?, status = ?, milestone_id = ?, workspace_id = ? WHERE id = ?`)
1255
+ .run(next.title, next.status === "done" || next.status === "archived" ? "done" : "active", next.milestoneId ?? null, workspaceId, next.workspaceTaskId);
1256
+ this.db.prepare("UPDATE command_sessions SET workspace_id = ? WHERE workspace_task_id = ?")
1257
+ .run(workspaceId, next.workspaceTaskId);
1258
+ }
1230
1259
  return next;
1231
1260
  }
1232
1261
  deleteWandTask(id) { this.db.prepare("DELETE FROM wand_tasks WHERE id = ?").run(id); }
@@ -1284,6 +1313,7 @@ export class WandStorage {
1284
1313
  throw new Error("未找到该任务。");
1285
1314
  if (!this.getSession(sessionId))
1286
1315
  throw new Error("未找到该会话。");
1316
+ this.db.prepare("DELETE FROM wand_task_sessions WHERE session_id = ? AND task_id != ?").run(sessionId, taskId);
1287
1317
  this.db.prepare("INSERT OR IGNORE INTO wand_task_sessions (task_id, session_id, bound_at) VALUES (?, ?, ?)").run(taskId, sessionId, nowIso());
1288
1318
  }
1289
1319
  unbindWandTaskSession(taskId, sessionId) { this.db.prepare("DELETE FROM wand_task_sessions WHERE task_id = ? AND session_id = ?").run(taskId, sessionId); }
@@ -129,6 +129,7 @@ export declare class StructuredSessionManager {
129
129
  /** Return lightweight snapshots for the session list (no output/messages). */
130
130
  listSlim(): SessionSnapshot[];
131
131
  get(id: string): SessionSnapshot | null;
132
+ setSessionWorkspace(id: string, membership: Pick<SessionSnapshot, "workspaceId" | "workspaceTaskId">): void;
132
133
  setSessionTopic(id: string, title: string, description: string): SessionSnapshot;
133
134
  private setSessionTopicGenerating;
134
135
  /**
@@ -931,6 +931,11 @@ export class StructuredSessionManager {
931
931
  const s = this.sessions.get(id);
932
932
  return s ? withSummary(s) : null;
933
933
  }
934
+ setSessionWorkspace(id, membership) {
935
+ const updated = { ...this.requireSession(id), ...membership };
936
+ this.sessions.set(id, updated);
937
+ this.emitStructuredSnapshot(updated);
938
+ }
934
939
  setSessionTopic(id, title, description) {
935
940
  const current = this.requireSession(id);
936
941
  const updated = { ...current, title, description, summary: description };
@@ -58,8 +58,13 @@ export interface WandTask {
58
58
  workspaceTaskId: string | null;
59
59
  identifier: string;
60
60
  title: string;
61
- /** 'user' = 用户自己填的标题;'auto' = 用户留空后按描述自动生成。 */
61
+ /** 'user' = 用户自己填的标题;'auto' = 用户留空后按描述/会话内容自动生成。 */
62
62
  titleSource: WandTaskTitleSource;
63
+ /**
64
+ * 最近一次自动命名用到的输入指纹(描述 + 所有会话摘要);内容没变就不再重复总结。
65
+ * 标题来源为 user 或从未自动命名过时为 null。
66
+ */
67
+ autoTitleSignature: string | null;
63
68
  description: string;
64
69
  status: WandTaskStatus;
65
70
  priority: WandTaskPriority;
@@ -1,19 +1,40 @@
1
1
  import type { WandStorage } from "./storage.js";
2
- import { type WandTask } from "./task-types.js";
2
+ import { type WandTask, type WandTaskTitleSource } from "./task-types.js";
3
3
  import type { SessionSnapshot, Workspace, WorkspaceTask } from "./types.js";
4
+ /** 未命名任务的占位名;服务端新建时写入,自动命名成功后被真实标题覆盖。 */
5
+ export declare const UNNAMED_WORKSPACE_TASK_NAME = "\u672A\u547D\u540D\u4EFB\u52A1";
4
6
  export declare function isUnnamedWorkspaceTaskName(name: string): boolean;
5
- /** 从未分组会话的标题 / 描述 / 首条用户消息里抽出看板标题。 */
7
+ /** Legacy title helper; prompts name sessions, not their containing task. */
6
8
  export declare function boardTitleFromSession(session: SessionSnapshot): string;
7
- /** 侧栏工作任务与看板卡片对账:已关联则复用,同项目同名未关联则挂上,否则新建。 */
8
- export declare function ensureBoardTaskForWorkspaceTask(storage: WandStorage, task: WorkspaceTask, workspace: Workspace): WandTask | null;
9
9
  /**
10
- * 把历史未分组会话补到看板:用会话标题/首条消息当标题,绑上会话,放进「进行中」。
11
- * 幂等,打开看板列表时调用。
10
+ * 自动命名只覆盖「还没有人为名字」的任务:标题来源是 auto,或仍是占位名的历史卡片
11
+ * (老版本把未命名任务写成了 titleSource=user)。用户在面板 / 侧栏改过名后此处为 false。
12
12
  */
13
+ export declare function isAutoNameableBoardTask(card: Pick<WandTask, "title" | "titleSource">): boolean;
14
+ /**
15
+ * 自动命名的输入:任务描述(去掉「项目:/ 目录:/ 分支:」同步元信息)+ 每个会话的可读摘要。
16
+ * 为空表示这个任务还没有可命名的内容,调用方应保留占位标题。
17
+ */
18
+ export declare function taskAutoNameSourceText(storage: WandStorage, card: WandTask): string;
19
+ /** 输入指纹:内容没变就没必要再总结一次,也避免自动标题与模型结果来回震荡。 */
20
+ export declare function taskAutoNameSignature(source: string): string;
21
+ /** Identity is the task id, never its title. Same-named tasks stay independent. */
22
+ export declare function ensureBoardTaskForWorkspaceTask(storage: WandStorage, task: WorkspaceTask, workspace: Workspace, options?: {
23
+ titleSource?: WandTaskTitleSource;
24
+ }): WandTask;
25
+ /** A board task always has a sidebar container, even before its first session. */
26
+ export declare function ensureWorkspaceTaskForBoardTask(storage: WandStorage, card: WandTask): WorkspaceTask;
27
+ /** Atomic exclusive ownership change. Never changes cwd, messages, or execution state. */
28
+ export declare function moveSessionToWorkspaceTask(storage: WandStorage, sessionId: string, taskId: string | null): void;
29
+ /** Reconcile legacy records by explicit ids. Unassigned sessions remain unassigned. */
30
+ export declare function syncSidebarTasksFromBoard(storage: WandStorage): void;
13
31
  export declare function syncUngroupedSessionsToBoard(storage: WandStorage): void;
14
- /** 看板确认/归档时,把关联的侧栏工作任务标成 done(不删会话)。 */
32
+ /** Status and title projection is centralized in the storage writer. */
15
33
  export declare function syncClosedBoardTask(storage: WandStorage, task: WandTask): void;
16
- /** 看板归档:卡片进入归档目录,并同步把关联的侧栏工作任务标成 done(不删会话)。 */
17
34
  export declare function archiveBoardTask(storage: WandStorage, id: string): WandTask | null;
18
- /** 侧栏工作任务完成/删除时,把对应看板卡片标成已完成;已归档的保持归档。 */
35
+ /**
36
+ * 归档侧栏任务是软删除:终端继续运行、worktree 与布局都保留,只有看板卡片进入
37
+ * 归档文件夹、侧栏不再显示。恢复方式是看板里把卡片拖回任一列(或右键恢复)。
38
+ */
39
+ export declare function archiveWorkspaceTask(storage: WandStorage, task: WorkspaceTask): WandTask | null;
19
40
  export declare function archiveBoardTaskForWorkspaceTask(storage: WandStorage, workspaceTaskId: string): void;