@co0ontty/wand 4.58.0 → 4.59.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.
@@ -1,3 +1,5 @@
1
+ import { provisionalTaskTitleFromDescription } from "./task-title.js";
2
+ const UNNAMED_TASK_NAME = "未命名任务";
1
3
  function boardDescriptionFor(task, workspace) {
2
4
  const lines = [`项目:${workspace.name}`];
3
5
  const cwd = task.worktree?.path || task.cwd || workspace.cwd;
@@ -7,11 +9,102 @@ function boardDescriptionFor(task, workspace) {
7
9
  lines.push(`分支:${task.worktree.branch}`);
8
10
  return lines.join("\n");
9
11
  }
12
+ export function isUnnamedWorkspaceTaskName(name) {
13
+ const trimmed = name.trim();
14
+ return !trimmed || trimmed === UNNAMED_TASK_NAME;
15
+ }
16
+ function normalizeTitle(title) {
17
+ return title.trim().replace(/[。..!?!?]+$/g, "").replace(/\s+/g, " ");
18
+ }
19
+ function firstUserMessageText(session) {
20
+ for (const turn of session.messages ?? []) {
21
+ if (turn.role !== "user")
22
+ continue;
23
+ const text = turn.content
24
+ .flatMap((block) => block.type === "text" ? [block.text.trim()] : [])
25
+ .filter(Boolean)
26
+ .join("\n");
27
+ if (text)
28
+ return text;
29
+ }
30
+ return "";
31
+ }
32
+ /** 从未分组会话的标题 / 描述 / 首条用户消息里抽出看板标题。 */
33
+ export function boardTitleFromSession(session) {
34
+ const title = session.title?.trim() ?? "";
35
+ if (title && !isUnnamedWorkspaceTaskName(title)) {
36
+ return provisionalTaskTitleFromDescription(title);
37
+ }
38
+ const description = session.description?.trim() ?? "";
39
+ if (description)
40
+ return provisionalTaskTitleFromDescription(description);
41
+ const message = firstUserMessageText(session);
42
+ if (message)
43
+ return provisionalTaskTitleFromDescription(message);
44
+ return "";
45
+ }
46
+ function boardDescriptionFromSession(session) {
47
+ const description = session.description?.trim() ?? "";
48
+ if (description)
49
+ return description;
50
+ const title = session.title?.trim() ?? "";
51
+ if (title && !isUnnamedWorkspaceTaskName(title))
52
+ return title;
53
+ return firstUserMessageText(session);
54
+ }
55
+ function pickBestSession(sessions) {
56
+ if (sessions.length === 0)
57
+ return null;
58
+ return sessions.find((session) => boardTitleFromSession(session)) ?? sessions[0] ?? null;
59
+ }
60
+ function isSyncedWorkspaceDescription(description) {
61
+ const lines = description.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
62
+ if (lines.length === 0)
63
+ return true;
64
+ return lines.every((line) => line.startsWith("项目:") || line.startsWith("目录:") || line.startsWith("分支:"));
65
+ }
66
+ function bindSessions(storage, taskId, sessionIds) {
67
+ for (const sessionId of sessionIds) {
68
+ try {
69
+ storage.bindWandTaskSession(taskId, sessionId);
70
+ }
71
+ catch {
72
+ // 会话可能已被删;跳过单条,不中断整次对账。
73
+ }
74
+ }
75
+ }
76
+ function findBoardTaskByTitle(tasks, title, workspaceId) {
77
+ const needle = normalizeTitle(title);
78
+ if (!needle)
79
+ return null;
80
+ const sameWorkspace = tasks.filter((task) => (task.workspaceId ?? "") === (workspaceId ?? "")
81
+ && normalizeTitle(task.title) === needle);
82
+ if (sameWorkspace[0])
83
+ return sameWorkspace[0];
84
+ return tasks.find((task) => normalizeTitle(task.title) === needle) ?? null;
85
+ }
86
+ function promoteUngroupedCard(storage, card, patch) {
87
+ const nextTitle = patch.title?.trim() && isUnnamedWorkspaceTaskName(card.title) ? patch.title.trim() : card.title;
88
+ const nextDescription = patch.description?.trim() && isSyncedWorkspaceDescription(card.description)
89
+ ? patch.description.trim()
90
+ : card.description;
91
+ return storage.updateWandTask(card.id, {
92
+ title: nextTitle,
93
+ titleSource: nextTitle !== card.title ? "auto" : card.titleSource,
94
+ description: nextDescription,
95
+ status: card.status === "done" ? "done" : "doing",
96
+ workspaceId: patch.workspaceId !== undefined ? patch.workspaceId : card.workspaceId,
97
+ workspaceTaskId: patch.workspaceTaskId !== undefined ? patch.workspaceTaskId : card.workspaceTaskId,
98
+ }) ?? card;
99
+ }
10
100
  /** 侧栏工作任务与看板卡片对账:已关联则复用,同项目同名未关联则挂上,否则新建。 */
11
101
  export function ensureBoardTaskForWorkspaceTask(storage, task, workspace) {
12
102
  const linked = storage.getWandTaskByWorkspaceTaskId(task.id);
13
103
  if (linked)
14
104
  return linked;
105
+ // 未命名侧栏任务在会话树里并进「未分组终端」;等同步函数根据会话内容补卡片。
106
+ if (isUnnamedWorkspaceTaskName(task.name))
107
+ return null;
15
108
  const reusable = storage.findUnlinkedWandTask(workspace.id, task.name);
16
109
  if (reusable) {
17
110
  return storage.updateWandTask(reusable.id, {
@@ -27,6 +120,79 @@ export function ensureBoardTaskForWorkspaceTask(storage, task, workspace) {
27
120
  status: task.status === "done" ? "done" : "todo",
28
121
  });
29
122
  }
123
+ /**
124
+ * 把历史未分组会话补到看板:用会话标题/首条消息当标题,绑上会话,放进「进行中」。
125
+ * 幂等,打开看板列表时调用。
126
+ */
127
+ export function syncUngroupedSessionsToBoard(storage) {
128
+ const boardTasks = storage.listWandTasks();
129
+ const bound = new Set(storage.listBoundWandTaskSessionIds());
130
+ for (const workspace of storage.listWorkspaces()) {
131
+ for (const task of storage.listWorkspaceTasks(workspace.id)) {
132
+ if (!isUnnamedWorkspaceTaskName(task.name))
133
+ continue;
134
+ const sessions = storage.listSessionsByWorkspaceTask(task.id);
135
+ const best = pickBestSession(sessions);
136
+ const title = best ? boardTitleFromSession(best) : "";
137
+ if (!best || !title)
138
+ continue;
139
+ const description = boardDescriptionFromSession(best);
140
+ let card = storage.getWandTaskByWorkspaceTaskId(task.id)
141
+ ?? findBoardTaskByTitle(boardTasks, title, workspace.id);
142
+ if (!card) {
143
+ card = storage.createWandTask({
144
+ workspaceId: workspace.id,
145
+ workspaceTaskId: task.id,
146
+ title,
147
+ titleSource: "auto",
148
+ description,
149
+ status: "doing",
150
+ });
151
+ boardTasks.push(card);
152
+ }
153
+ else {
154
+ card = promoteUngroupedCard(storage, card, {
155
+ title,
156
+ description,
157
+ workspaceId: workspace.id,
158
+ workspaceTaskId: task.id,
159
+ });
160
+ }
161
+ bindSessions(storage, card.id, sessions.map((session) => session.id));
162
+ for (const session of sessions)
163
+ bound.add(session.id);
164
+ }
165
+ }
166
+ for (const session of storage.loadSessions()) {
167
+ if (bound.has(session.id))
168
+ continue;
169
+ if (session.workspaceTaskId)
170
+ continue;
171
+ const title = boardTitleFromSession(session);
172
+ if (!title)
173
+ continue;
174
+ let card = findBoardTaskByTitle(boardTasks, title, session.workspaceId ?? null);
175
+ if (!card) {
176
+ card = storage.createWandTask({
177
+ workspaceId: session.workspaceId ?? null,
178
+ title,
179
+ titleSource: "auto",
180
+ description: boardDescriptionFromSession(session),
181
+ status: "doing",
182
+ });
183
+ boardTasks.push(card);
184
+ }
185
+ else if (card.status !== "done") {
186
+ card = promoteUngroupedCard(storage, card, {
187
+ title,
188
+ description: boardDescriptionFromSession(session),
189
+ workspaceId: session.workspaceId ?? card.workspaceId,
190
+ });
191
+ }
192
+ bindSessions(storage, card.id, [session.id]);
193
+ bound.add(session.id);
194
+ }
195
+ }
30
196
  /** 看板归档:卡片进入已完成,并同步把关联的侧栏工作任务标成 done(不删会话)。 */
31
197
  export function archiveBoardTask(storage, id) {
32
198
  const current = storage.getWandTask(id);