@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/build-info.json +3 -3
- package/dist/process-manager.d.ts +1 -0
- package/dist/process-manager.js +8 -0
- package/dist/server-task-routes.d.ts +14 -0
- package/dist/server-task-routes.js +90 -24
- package/dist/server-workspace-routes.d.ts +2 -1
- package/dist/server-workspace-routes.js +71 -26
- package/dist/server.js +1 -1
- package/dist/session-registry.d.ts +2 -0
- package/dist/session-registry.js +18 -3
- package/dist/storage.d.ts +3 -2
- package/dist/storage.js +44 -14
- package/dist/structured-session-manager.d.ts +1 -0
- package/dist/structured-session-manager.js +5 -0
- package/dist/task-types.d.ts +6 -1
- package/dist/wand-task-sync.d.ts +30 -9
- package/dist/wand-task-sync.js +198 -209
- package/dist/web-ui/content/scripts.js +88 -68
- package/dist/web-ui/content/styles.css +1 -1
- package/dist/web-ui/content/tailwind.css +3 -0
- package/dist/web-ui/embedded-assets.js +2 -2
- package/package.json +1 -1
package/dist/build-info.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"commit": "
|
|
3
|
-
"builtAt": "2026-09-
|
|
4
|
-
"version": "4.
|
|
2
|
+
"commit": "249c98c050191ae67009c35602ab9c133caf0378",
|
|
3
|
+
"builtAt": "2026-09-17T11:29:15.968Z",
|
|
4
|
+
"version": "4.69.0",
|
|
5
5
|
"channel": "stable"
|
|
6
6
|
}
|
|
@@ -125,6 +125,7 @@ export declare class ProcessManager extends EventEmitter {
|
|
|
125
125
|
/** Lightweight snapshot for list views — omits output and messages. */
|
|
126
126
|
private snapshotSlim;
|
|
127
127
|
private isPermissionBlocked;
|
|
128
|
+
setSessionWorkspace(id: string, membership: Pick<SessionSnapshot, "workspaceId" | "workspaceTaskId">): void;
|
|
128
129
|
setSessionTopic(id: string, title: string, description: string): SessionSnapshot;
|
|
129
130
|
private setSessionTopicGenerating;
|
|
130
131
|
/**
|
package/dist/process-manager.js
CHANGED
|
@@ -1669,6 +1669,10 @@ export class ProcessManager extends EventEmitter {
|
|
|
1669
1669
|
record.ptyBridge = null;
|
|
1670
1670
|
}
|
|
1671
1671
|
this.flushPersist(record, true);
|
|
1672
|
+
// handleTerminalExit 会因为 ptyProcess 已被清空而把这次异步退出判定为过期回调并
|
|
1673
|
+
// return,所以主动停止必须自己广播 ended。少了这一步,各端会一直以为会话还在跑:
|
|
1674
|
+
// 直通输入框不关闭,附件/发送全都打进已经死掉的 PTY。结构化 stop() 同样发 ended。
|
|
1675
|
+
this.emitEvent({ type: "ended", sessionId: record.id, data: this.snapshot(record) });
|
|
1672
1676
|
return this.snapshot(record);
|
|
1673
1677
|
}
|
|
1674
1678
|
cleanupRecord(record, terminateTerminal = true) {
|
|
@@ -1880,6 +1884,10 @@ export class ProcessManager extends EventEmitter {
|
|
|
1880
1884
|
isPermissionBlocked(record) {
|
|
1881
1885
|
return record.ptyPermissionBlocked || record.pendingEscalation !== null;
|
|
1882
1886
|
}
|
|
1887
|
+
setSessionWorkspace(id, membership) {
|
|
1888
|
+
Object.assign(this.mustGet(id), membership);
|
|
1889
|
+
this.emitEvent({ type: "status", sessionId: id, data: membership });
|
|
1890
|
+
}
|
|
1883
1891
|
setSessionTopic(id, title, description) {
|
|
1884
1892
|
const record = this.mustGet(id);
|
|
1885
1893
|
record.title = title;
|
|
@@ -24,4 +24,18 @@ export interface TaskRouteDependencies {
|
|
|
24
24
|
export declare function parseTaskAgent(value: unknown, fallbackMode?: WandTaskAgentMode): WandTaskAgent | null;
|
|
25
25
|
/** 等待排队中的标题生成全部结束;只有测试用,避免用例轮询。 */
|
|
26
26
|
export declare function whenWandTaskTitlesSettled(): Promise<void>;
|
|
27
|
+
export interface AutoTaskTitleOptions {
|
|
28
|
+
config?: WandConfig;
|
|
29
|
+
/** 可注入的标题生成器;测试里换成同步桩,避免真的起 CLI。 */
|
|
30
|
+
generateTitle?: typeof generateWandTaskTitle;
|
|
31
|
+
/** 未从任务所属工作区解析到目录时的兜底 cwd。 */
|
|
32
|
+
cwd?: string;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* 扫描标题仍可自动命名的看板任务(titleSource=auto,或仍是占位名的历史卡片):
|
|
36
|
+
* 先用任务描述 + 所有绑定会话的内容立即改掉占位名,再交给后台模型总结更贴切的标题。
|
|
37
|
+
* 同一个输入指纹只处理一次:内容没变就跳过,也避免自动标题与模型结果来回震荡。
|
|
38
|
+
* 用户建任务时写了名字、或事后改过名(titleSource=user 且不是占位名)的任务永不进入这里。
|
|
39
|
+
*/
|
|
40
|
+
export declare function refreshAutoBoardTaskTitles(storage: WandStorage, options?: AutoTaskTitleOptions): void;
|
|
27
41
|
export declare function registerTaskRoutes(app: Express, deps: TaskRouteDependencies): void;
|
|
@@ -6,7 +6,7 @@ import { parseWandTaskAgent } from "./storage.js";
|
|
|
6
6
|
import { generateWandTaskTitle, provisionalTaskTitleFromDescription, TASK_TITLE_MAX_LENGTH } from "./task-title.js";
|
|
7
7
|
import { resolveSystemAiContext } from "./session-ai-context.js";
|
|
8
8
|
import { DEFAULT_WAND_TASK_AGENT_MODE, isWandTaskAgentMode, normalizeWandTaskAgentMode, WAND_MILESTONE_NAME_MAX_LENGTH } from "./task-types.js";
|
|
9
|
-
import { archiveBoardTask, syncClosedBoardTask, syncUngroupedSessionsToBoard } from "./wand-task-sync.js";
|
|
9
|
+
import { archiveBoardTask, ensureWorkspaceTaskForBoardTask, isAutoNameableBoardTask, moveSessionToWorkspaceTask, syncClosedBoardTask, syncUngroupedSessionsToBoard, taskAutoNameSignature, taskAutoNameSourceText } from "./wand-task-sync.js";
|
|
10
10
|
import { isSessionProvider } from "./session-provider.js";
|
|
11
11
|
const STATUSES = new Set(["todo", "doing", "done", "archived"]);
|
|
12
12
|
const PRIORITIES = new Set(["none", "low", "medium", "high", "urgent"]);
|
|
@@ -130,20 +130,24 @@ function taskSessionSummary(session) {
|
|
|
130
130
|
};
|
|
131
131
|
}
|
|
132
132
|
/**
|
|
133
|
-
*
|
|
133
|
+
* 用户没写标题时,先用描述/会话内容当占位标题,再由后台模型总结一个更好的。
|
|
134
134
|
* 全程不阻塞创建请求:失败只是保留占位标题,不返回错误、也不覆盖用户后来改的标题。
|
|
135
|
-
*
|
|
135
|
+
* 多次总结会排队而不是丢弃,保证每张卡片最终都会拿到标题。
|
|
136
136
|
*/
|
|
137
137
|
let titleGenerationChain = Promise.resolve();
|
|
138
|
-
function scheduleWandTaskTitleGeneration(storage, taskId,
|
|
138
|
+
function scheduleWandTaskTitleGeneration(storage, taskId, source, signature, options) {
|
|
139
|
+
const generateTitle = options.generateTitle ?? generateWandTaskTitle;
|
|
139
140
|
const cwd = options.cwd || options.config?.defaultCwd || process.cwd();
|
|
140
141
|
const run = async () => {
|
|
141
142
|
try {
|
|
142
|
-
const title = await
|
|
143
|
+
const title = await generateTitle(source, cwd, options.config?.language ?? "", taskTitleAiOptions(options.config));
|
|
143
144
|
const current = storage.getWandTask(taskId);
|
|
144
145
|
// 用户已经自己写了标题(原生端 / 面板编辑)就不要覆盖。
|
|
145
146
|
if (!current || current.titleSource !== "auto")
|
|
146
147
|
return;
|
|
148
|
+
// 命名输入已经变了(新增会话 / 会话内容更新):这次是过期结果,交给下一轮。
|
|
149
|
+
if (current.autoTitleSignature !== signature)
|
|
150
|
+
return;
|
|
147
151
|
const clipped = title.slice(0, TASK_TITLE_MAX_LENGTH);
|
|
148
152
|
if (!clipped || clipped === current.title)
|
|
149
153
|
return;
|
|
@@ -159,6 +163,40 @@ function scheduleWandTaskTitleGeneration(storage, taskId, description, options)
|
|
|
159
163
|
export function whenWandTaskTitlesSettled() {
|
|
160
164
|
return titleGenerationChain;
|
|
161
165
|
}
|
|
166
|
+
/**
|
|
167
|
+
* 扫描标题仍可自动命名的看板任务(titleSource=auto,或仍是占位名的历史卡片):
|
|
168
|
+
* 先用任务描述 + 所有绑定会话的内容立即改掉占位名,再交给后台模型总结更贴切的标题。
|
|
169
|
+
* 同一个输入指纹只处理一次:内容没变就跳过,也避免自动标题与模型结果来回震荡。
|
|
170
|
+
* 用户建任务时写了名字、或事后改过名(titleSource=user 且不是占位名)的任务永不进入这里。
|
|
171
|
+
*/
|
|
172
|
+
export function refreshAutoBoardTaskTitles(storage, options = {}) {
|
|
173
|
+
for (const card of storage.listWandTasks()) {
|
|
174
|
+
if (!isAutoNameableBoardTask(card))
|
|
175
|
+
continue;
|
|
176
|
+
const source = taskAutoNameSourceText(storage, card);
|
|
177
|
+
if (!source)
|
|
178
|
+
continue;
|
|
179
|
+
const signature = taskAutoNameSignature(source);
|
|
180
|
+
if (card.autoTitleSignature === signature)
|
|
181
|
+
continue;
|
|
182
|
+
const provisional = provisionalTaskTitleFromDescription(source);
|
|
183
|
+
storage.updateWandTask(card.id, {
|
|
184
|
+
titleSource: "auto",
|
|
185
|
+
autoTitleSignature: signature,
|
|
186
|
+
...(provisional && provisional !== card.title ? { title: provisional } : {}),
|
|
187
|
+
});
|
|
188
|
+
// 未启用模型(如部分只验证占位改名的单测)时只保留立即生效的占位标题。
|
|
189
|
+
if (!options.generateTitle && !options.config)
|
|
190
|
+
continue;
|
|
191
|
+
const cwd = options.cwd
|
|
192
|
+
|| (card.workspaceId ? storage.getWorkspace(card.workspaceId)?.cwd : undefined);
|
|
193
|
+
scheduleWandTaskTitleGeneration(storage, card.id, source, signature, {
|
|
194
|
+
cwd,
|
|
195
|
+
config: options.config,
|
|
196
|
+
generateTitle: options.generateTitle,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
162
200
|
/** 自动生成标题没有会话上下文,按「默认 provider + 默认模型」解析,与提示词优化一致。 */
|
|
163
201
|
function taskTitleAiOptions(config) {
|
|
164
202
|
if (!config)
|
|
@@ -185,6 +223,13 @@ export function registerTaskRoutes(app, deps) {
|
|
|
185
223
|
catch (error) {
|
|
186
224
|
console.error("[WandTask] Failed to sync ungrouped sessions onto the board:", getErrorMessage(error));
|
|
187
225
|
}
|
|
226
|
+
try {
|
|
227
|
+
// 侧栏建的任务 / 后续新增的会话都会在这里补上自动标题。
|
|
228
|
+
refreshAutoBoardTaskTitles(storage, { config, generateTitle: deps.generateTitle });
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
console.error("[WandTask] Failed to refresh auto task titles:", getErrorMessage(error));
|
|
232
|
+
}
|
|
188
233
|
const workspaceId = typeof req.query.workspaceId === "string" ? req.query.workspaceId : undefined;
|
|
189
234
|
// 数组顺序即展示顺序:created_at 倒序,新创建的在上面。
|
|
190
235
|
res.json(storage.listWandTasks(workspaceId).map((task) => dto(task)));
|
|
@@ -282,26 +327,30 @@ export function registerTaskRoutes(app, deps) {
|
|
|
282
327
|
const labels = labelsFrom(body.labels);
|
|
283
328
|
const dueDate = dateValue(body.dueDate) ?? null;
|
|
284
329
|
const milestoneId = milestoneIdFrom(storage, body.milestoneId);
|
|
285
|
-
const task = storage.
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
330
|
+
const task = storage.transaction(() => {
|
|
331
|
+
const card = storage.createWandTask({
|
|
332
|
+
workspaceId,
|
|
333
|
+
title,
|
|
334
|
+
titleSource,
|
|
335
|
+
description,
|
|
336
|
+
status,
|
|
337
|
+
priority,
|
|
338
|
+
labels,
|
|
339
|
+
dueDate,
|
|
340
|
+
milestoneId,
|
|
341
|
+
agent,
|
|
342
|
+
});
|
|
343
|
+
ensureWorkspaceTaskForBoardTask(storage, card);
|
|
344
|
+
return storage.getWandTask(card.id);
|
|
296
345
|
});
|
|
297
346
|
if (titleSource === "auto") {
|
|
298
|
-
|
|
347
|
+
refreshAutoBoardTaskTitles(storage, {
|
|
299
348
|
cwd: workspaceId ? storage.getWorkspace(workspaceId)?.cwd : undefined,
|
|
300
349
|
config,
|
|
301
|
-
generateTitle: deps.generateTitle
|
|
350
|
+
generateTitle: deps.generateTitle,
|
|
302
351
|
});
|
|
303
352
|
}
|
|
304
|
-
res.status(201).json(dto(task));
|
|
353
|
+
res.status(201).json(dto(storage.getWandTask(task.id) ?? task));
|
|
305
354
|
}
|
|
306
355
|
catch (error) {
|
|
307
356
|
sendRouteError(res, error, "无法创建任务。");
|
|
@@ -381,7 +430,14 @@ export function registerTaskRoutes(app, deps) {
|
|
|
381
430
|
throw new Error("缺少 sessionId。");
|
|
382
431
|
if (sessions && !sessions.get(sessionId))
|
|
383
432
|
throw new Error("未找到该会话。");
|
|
384
|
-
storage.
|
|
433
|
+
const card = storage.getWandTask(req.params.id);
|
|
434
|
+
if (!card) {
|
|
435
|
+
res.status(404).json({ error: "未找到该任务。" });
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
const task = ensureWorkspaceTaskForBoardTask(storage, card);
|
|
439
|
+
moveSessionToWorkspaceTask(storage, sessionId, task.id);
|
|
440
|
+
sessions?.refreshSessionWorkspace(sessionId);
|
|
385
441
|
res.status(201).json(dto(storage.getWandTask(req.params.id)));
|
|
386
442
|
}
|
|
387
443
|
catch (error) {
|
|
@@ -389,7 +445,15 @@ export function registerTaskRoutes(app, deps) {
|
|
|
389
445
|
}
|
|
390
446
|
});
|
|
391
447
|
app.delete("/api/wand-tasks/:id/sessions/:sessionId", (req, res) => {
|
|
392
|
-
storage.
|
|
448
|
+
const card = storage.getWandTask(req.params.id);
|
|
449
|
+
const session = storage.getSession(req.params.sessionId);
|
|
450
|
+
if (card?.workspaceTaskId && session?.workspaceTaskId === card.workspaceTaskId) {
|
|
451
|
+
moveSessionToWorkspaceTask(storage, session.id, null);
|
|
452
|
+
sessions?.refreshSessionWorkspace(session.id);
|
|
453
|
+
}
|
|
454
|
+
else {
|
|
455
|
+
storage.unbindWandTaskSession(req.params.id, req.params.sessionId);
|
|
456
|
+
}
|
|
393
457
|
res.json({ ok: true });
|
|
394
458
|
});
|
|
395
459
|
/**
|
|
@@ -421,7 +485,9 @@ export function registerTaskRoutes(app, deps) {
|
|
|
421
485
|
const workspace = task.workspaceId ? storage.getWorkspace(task.workspaceId) : null;
|
|
422
486
|
if (task.workspaceId && !workspace)
|
|
423
487
|
throw new Error("任务所属项目已被删除,请重新指定。");
|
|
424
|
-
const
|
|
488
|
+
const group = ensureWorkspaceTaskForBoardTask(storage, task);
|
|
489
|
+
task = storage.getWandTask(task.id);
|
|
490
|
+
const cwd = group.worktree?.path || group.cwd || workspace?.cwd || config.defaultCwd;
|
|
425
491
|
const requestedPrompt = text(body.prompt);
|
|
426
492
|
const existingSessions = storage.listWandTaskSessionIds(task.id).length;
|
|
427
493
|
if (existingSessions > 0 && !requestedPrompt)
|
|
@@ -437,8 +503,8 @@ export function registerTaskRoutes(app, deps) {
|
|
|
437
503
|
worktreeEnabled: false,
|
|
438
504
|
sessionSource: "automation",
|
|
439
505
|
automationId: `wand-task:${task.id}`,
|
|
440
|
-
workspaceId:
|
|
441
|
-
workspaceTaskId:
|
|
506
|
+
workspaceId: group.workspaceId,
|
|
507
|
+
workspaceTaskId: group.id,
|
|
442
508
|
});
|
|
443
509
|
storage.updateWandTask(task.id, { agent, status: task.status === "todo" ? "doing" : task.status });
|
|
444
510
|
storage.bindWandTaskSession(task.id, session.id);
|
|
@@ -2,6 +2,7 @@ import type { Express } from "express";
|
|
|
2
2
|
import type { SessionRegistry } from "./session-registry.js";
|
|
3
3
|
import type { WandStorage } from "./storage.js";
|
|
4
4
|
import { type LayoutNode, type TaskWindowLayout } from "./types.js";
|
|
5
|
+
import { type AutoTaskTitleOptions } from "./server-task-routes.js";
|
|
5
6
|
/** 校验并清洗前端提交的布局树;非法输入返回 null(空工作空间)。 */
|
|
6
7
|
export declare function sanitizeLayout(value: unknown): LayoutNode | null;
|
|
7
8
|
/** 校验任务级工作窗口集合;旧版单棵布局会兼容升级成一个 window。 */
|
|
@@ -13,4 +14,4 @@ export declare function sanitizeTaskLayout(value: unknown): TaskWindowLayout | n
|
|
|
13
14
|
* - POST 只建项目实体,**不启动任何会话**;会话由工作空间内「+」标签按需创建并绑定 workspaceId。
|
|
14
15
|
* - 布局树由前端 allotment 构造,PUT 时经 sanitizeLayout 校验/规整后落库。
|
|
15
16
|
*/
|
|
16
|
-
export declare function registerWorkspaceRoutes(app: Express, storage: WandStorage, sessions?: SessionRegistry): void;
|
|
17
|
+
export declare function registerWorkspaceRoutes(app: Express, storage: WandStorage, sessions?: SessionRegistry, titleOptions?: AutoTaskTitleOptions): void;
|
|
@@ -9,9 +9,10 @@ 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 { archiveBoardTaskForWorkspaceTask, ensureBoardTaskForWorkspaceTask } from "./wand-task-sync.js";
|
|
12
|
+
import { archiveBoardTaskForWorkspaceTask, archiveWorkspaceTask, ensureBoardTaskForWorkspaceTask, isUnnamedWorkspaceTaskName, moveSessionToWorkspaceTask, syncSidebarTasksFromBoard, UNNAMED_WORKSPACE_TASK_NAME } from "./wand-task-sync.js";
|
|
13
13
|
import { isSessionProvider } from "./session-provider.js";
|
|
14
14
|
import { firstLayoutTabId } from "./layout-tree.js";
|
|
15
|
+
import { refreshAutoBoardTaskTitles } from "./server-task-routes.js";
|
|
15
16
|
function workspaceSessionTitle(session, names = {}) {
|
|
16
17
|
return resolveSessionDisplayTitle(session, collectSessionTopicBlocklist({
|
|
17
18
|
taskName: names.taskName,
|
|
@@ -96,12 +97,24 @@ function deleteSessions(storage, sessions, sessionIds) {
|
|
|
96
97
|
storage.deleteSession(sessionId);
|
|
97
98
|
}
|
|
98
99
|
}
|
|
100
|
+
function worktreeUsedOutsideTasks(storage, tasks) {
|
|
101
|
+
const deleting = new Set(tasks.map((task) => task.id));
|
|
102
|
+
return storage.loadSessionsSlim().some((session) => !deleting.has(session.workspaceTaskId ?? "")
|
|
103
|
+
&& tasks.some((task) => {
|
|
104
|
+
if (!task.worktree)
|
|
105
|
+
return false;
|
|
106
|
+
const relative = path.relative(task.worktree.path, session.cwd);
|
|
107
|
+
return !relative || (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative));
|
|
108
|
+
}));
|
|
109
|
+
}
|
|
99
110
|
function taskRuntimeCwd(task, workspace) {
|
|
100
111
|
return task.worktree?.path ?? task.cwd ?? workspace?.cwd ?? "";
|
|
101
112
|
}
|
|
102
113
|
function createTaskForWorkspace(storage, workspace, body) {
|
|
103
114
|
const name = typeof body.name === "string" ? body.name.trim() : "";
|
|
104
|
-
|
|
115
|
+
// 留空(或老客户端回传的占位名)走自动命名:先用占位名建组,会话内容一到位就改名。
|
|
116
|
+
const named = name.length > 0 && !isUnnamedWorkspaceTaskName(name);
|
|
117
|
+
const taskName = named ? name : UNNAMED_WORKSPACE_TASK_NAME;
|
|
105
118
|
// 里程碑是全局列表;建任务时可选,非法 id 直接报错而不是静默丢掉。
|
|
106
119
|
const requestedMilestoneId = typeof body.milestoneId === "string" ? body.milestoneId.trim() : "";
|
|
107
120
|
if (requestedMilestoneId && !storage.getWandMilestone(requestedMilestoneId)) {
|
|
@@ -118,25 +131,14 @@ function createTaskForWorkspace(storage, workspace, body) {
|
|
|
118
131
|
}
|
|
119
132
|
const baseRef = typeof body.baseRef === "string" && body.baseRef.trim() ? body.baseRef.trim() : undefined;
|
|
120
133
|
const runCwd = mountedCwd ?? workspace.cwd;
|
|
121
|
-
const requireWorktree = body.worktree === true;
|
|
122
|
-
const wantWorktree = requireWorktree || (body.worktree !== false && !isGlobalWorkspace(workspace));
|
|
123
134
|
let worktree = null;
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
});
|
|
132
|
-
worktree = setup.worktree;
|
|
133
|
-
}
|
|
134
|
-
catch (error) {
|
|
135
|
-
if (requireWorktree) {
|
|
136
|
-
throw new Error(getErrorMessage(error, "无法创建隔离 worktree。"));
|
|
137
|
-
}
|
|
138
|
-
worktreeError = getErrorMessage(error, "无法创建 worktree,将在项目目录直接运行。");
|
|
139
|
-
}
|
|
135
|
+
if (body.worktree === true) {
|
|
136
|
+
const setup = prepareSessionWorktree({
|
|
137
|
+
cwd: runCwd,
|
|
138
|
+
sessionId: crypto.randomUUID(),
|
|
139
|
+
spec: { taskName, baseRef },
|
|
140
|
+
});
|
|
141
|
+
worktree = setup.worktree;
|
|
140
142
|
}
|
|
141
143
|
const storedCwd = mountedCwd && mountedCwd !== workspace.cwd ? mountedCwd : undefined;
|
|
142
144
|
const task = storage.createWorkspaceTask({
|
|
@@ -146,12 +148,11 @@ function createTaskForWorkspace(storage, workspace, body) {
|
|
|
146
148
|
worktree,
|
|
147
149
|
milestoneId,
|
|
148
150
|
});
|
|
149
|
-
ensureBoardTaskForWorkspaceTask(storage, task, workspace);
|
|
151
|
+
ensureBoardTaskForWorkspaceTask(storage, task, workspace, { titleSource: named ? "user" : "auto" });
|
|
150
152
|
return {
|
|
151
153
|
task,
|
|
152
154
|
cwd: taskRuntimeCwd(task, workspace),
|
|
153
155
|
isolated: worktree !== null,
|
|
154
|
-
worktreeError,
|
|
155
156
|
};
|
|
156
157
|
}
|
|
157
158
|
function workspaceWithCounts(storage, workspace, sessionCounts) {
|
|
@@ -267,7 +268,7 @@ export function sanitizeTaskLayout(value) {
|
|
|
267
268
|
* - POST 只建项目实体,**不启动任何会话**;会话由工作空间内「+」标签按需创建并绑定 workspaceId。
|
|
268
269
|
* - 布局树由前端 allotment 构造,PUT 时经 sanitizeLayout 校验/规整后落库。
|
|
269
270
|
*/
|
|
270
|
-
export function registerWorkspaceRoutes(app, storage, sessions) {
|
|
271
|
+
export function registerWorkspaceRoutes(app, storage, sessions, titleOptions = {}) {
|
|
271
272
|
// 列出所有项目(按创建时间降序:新建在前,打开不会改位置)
|
|
272
273
|
app.get("/api/workspaces", (_req, res) => {
|
|
273
274
|
backfillSessionWorkspaces(storage);
|
|
@@ -368,6 +369,10 @@ export function registerWorkspaceRoutes(app, storage, sessions) {
|
|
|
368
369
|
return;
|
|
369
370
|
}
|
|
370
371
|
const tasks = storage.listWorkspaceTasks(existing.id);
|
|
372
|
+
if (worktreeUsedOutsideTasks(storage, tasks)) {
|
|
373
|
+
res.status(409).json({ error: "该 Worktree 仍被已移出的会话使用,请先结束并删除这些会话。" });
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
371
376
|
if (cascade) {
|
|
372
377
|
deleteSessions(storage, sessions, [
|
|
373
378
|
...storage.listSessionsByWorkspace(existing.id).map((session) => session.id),
|
|
@@ -384,6 +389,9 @@ export function registerWorkspaceRoutes(app, storage, sessions) {
|
|
|
384
389
|
for (const task of tasks) {
|
|
385
390
|
if (cascade || task.worktree)
|
|
386
391
|
cleanupWorktreeSync(task.worktree);
|
|
392
|
+
const card = storage.getWandTaskByWorkspaceTaskId(task.id);
|
|
393
|
+
if (card)
|
|
394
|
+
storage.updateWandTask(card.id, { status: "archived", workspaceTaskId: null });
|
|
387
395
|
}
|
|
388
396
|
storage.deleteWorkspace(existing.id, { cascade });
|
|
389
397
|
res.json({ ok: true });
|
|
@@ -419,6 +427,14 @@ export function registerWorkspaceRoutes(app, storage, sessions) {
|
|
|
419
427
|
}));
|
|
420
428
|
// 目录组为一级容器的任务聚合列表,供侧栏「任务」视图一次拉全。
|
|
421
429
|
app.get("/api/tasks", (req, res) => {
|
|
430
|
+
syncSidebarTasksFromBoard(storage);
|
|
431
|
+
// 侧栏轮询也在补自动标题:不打开看板的任务(以及新增会话后的改名)也要吃到。
|
|
432
|
+
try {
|
|
433
|
+
refreshAutoBoardTaskTitles(storage, titleOptions);
|
|
434
|
+
}
|
|
435
|
+
catch (error) {
|
|
436
|
+
console.error("[WandTask] Failed to refresh auto task titles:", getErrorMessage(error));
|
|
437
|
+
}
|
|
422
438
|
// 查询参数:workspaceId 过滤单目录;limit 截断每目录任务数;
|
|
423
439
|
// maxSessions 截断每任务内嵌会话数(大数据量时控制响应体积)。
|
|
424
440
|
const hasRevisionQuery = typeof req.query.revision === "string";
|
|
@@ -686,8 +702,7 @@ export function registerWorkspaceRoutes(app, storage, sessions) {
|
|
|
686
702
|
worktrees,
|
|
687
703
|
});
|
|
688
704
|
}));
|
|
689
|
-
//
|
|
690
|
-
// 显式 worktree:false 跳过隔离;显式 worktree:true 失败则 400,不静默降级)。
|
|
705
|
+
// 默认只创建逻辑分组;仅显式 worktree:true 才创建隔离目录。
|
|
691
706
|
app.post("/api/workspaces/:id/tasks", asyncRoute(async (req, res) => {
|
|
692
707
|
const workspace = storage.getWorkspace(req.params.id);
|
|
693
708
|
if (!workspace) {
|
|
@@ -728,6 +743,22 @@ export function registerWorkspaceRoutes(app, storage, sessions) {
|
|
|
728
743
|
})),
|
|
729
744
|
});
|
|
730
745
|
});
|
|
746
|
+
// 移动会话只改变归属,不修改运行目录,也不重启 CLI。
|
|
747
|
+
app.post("/api/workspace-tasks/:taskId/sessions", (req, res) => {
|
|
748
|
+
const sessionId = typeof req.body?.sessionId === "string" ? req.body.sessionId.trim() : "";
|
|
749
|
+
if (!storage.getWorkspaceTask(req.params.taskId) || !storage.getSession(sessionId)) {
|
|
750
|
+
res.status(404).json({ error: "未找到任务或会话。" });
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
try {
|
|
754
|
+
moveSessionToWorkspaceTask(storage, sessionId, req.params.taskId);
|
|
755
|
+
sessions?.refreshSessionWorkspace(sessionId);
|
|
756
|
+
res.json({ ok: true });
|
|
757
|
+
}
|
|
758
|
+
catch (error) {
|
|
759
|
+
sendRouteError(res, error, "无法移动会话。");
|
|
760
|
+
}
|
|
761
|
+
});
|
|
731
762
|
// 改名 / 状态
|
|
732
763
|
app.patch("/api/workspace-tasks/:taskId", (req, res) => {
|
|
733
764
|
const existing = storage.getWorkspaceTask(req.params.taskId);
|
|
@@ -754,6 +785,16 @@ export function registerWorkspaceRoutes(app, storage, sessions) {
|
|
|
754
785
|
archiveBoardTaskForWorkspaceTask(storage, existing.id);
|
|
755
786
|
res.json(storage.getWorkspaceTask(existing.id));
|
|
756
787
|
});
|
|
788
|
+
// 归档任务:软删除。终端继续运行、worktree 与卡片历史都保留,只是侧栏不再显示。
|
|
789
|
+
app.post("/api/workspace-tasks/:taskId/archive", (req, res) => {
|
|
790
|
+
const existing = storage.getWorkspaceTask(req.params.taskId);
|
|
791
|
+
if (!existing) {
|
|
792
|
+
res.status(404).json({ error: "未找到该任务。" });
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
archiveWorkspaceTask(storage, existing);
|
|
796
|
+
res.json(storage.getWorkspaceTask(existing.id));
|
|
797
|
+
});
|
|
757
798
|
// 删除任务;cascade=true 连带删会话,否则仅解绑;尽力清理 worktree
|
|
758
799
|
app.delete("/api/workspace-tasks/:taskId", (req, res) => {
|
|
759
800
|
const existing = storage.getWorkspaceTask(req.params.taskId);
|
|
@@ -761,6 +802,10 @@ export function registerWorkspaceRoutes(app, storage, sessions) {
|
|
|
761
802
|
res.status(404).json({ error: "未找到该任务。" });
|
|
762
803
|
return;
|
|
763
804
|
}
|
|
805
|
+
if (worktreeUsedOutsideTasks(storage, [existing])) {
|
|
806
|
+
res.status(409).json({ error: "该 Worktree 仍被已移出的会话使用,请先结束并删除这些会话。" });
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
764
809
|
// 删除隔离任务必然会移除其 cwd,因此即使调用方没有显式传 cascade,
|
|
765
810
|
// 也必须同步删除会话;非隔离任务仍保留旧的“默认解绑”API 语义。
|
|
766
811
|
const cascade = req.query.cascade === "1"
|
|
@@ -775,7 +820,7 @@ export function registerWorkspaceRoutes(app, storage, sessions) {
|
|
|
775
820
|
archiveBoardTaskForWorkspaceTask(storage, existing.id);
|
|
776
821
|
const board = storage.getWandTaskByWorkspaceTaskId(existing.id);
|
|
777
822
|
if (board)
|
|
778
|
-
storage.updateWandTask(board.id, { workspaceTaskId: null });
|
|
823
|
+
storage.updateWandTask(board.id, { workspaceTaskId: null, status: "archived" });
|
|
779
824
|
storage.deleteWorkspaceTask(existing.id, { cascade });
|
|
780
825
|
res.json({ ok: true });
|
|
781
826
|
});
|
package/dist/server.js
CHANGED
|
@@ -654,7 +654,7 @@ export async function startServer(config, configPath, options = {}) {
|
|
|
654
654
|
recordRecentPath(storage, cwd);
|
|
655
655
|
});
|
|
656
656
|
registerClaudeHistoryRoutes(app, processes, storage);
|
|
657
|
-
registerWorkspaceRoutes(app, storage, sessionRegistry);
|
|
657
|
+
registerWorkspaceRoutes(app, storage, sessionRegistry, { config });
|
|
658
658
|
registerMissionRoutes(app, missions);
|
|
659
659
|
registerUploadRoutes(app, processes);
|
|
660
660
|
app.post("/api/optimize-prompt", asyncRoute(async (req, res) => {
|
|
@@ -25,6 +25,8 @@ export declare class SessionRegistry {
|
|
|
25
25
|
get(id: string): SessionSnapshot | null;
|
|
26
26
|
getLatest(id: string): SessionSnapshot | null;
|
|
27
27
|
listSlim(): SessionSnapshot[];
|
|
28
|
+
/** Membership belongs to storage; both runners only refresh their live projection. */
|
|
29
|
+
refreshSessionWorkspace(id: string): void;
|
|
28
30
|
setSessionModel(id: string, model: string | null): SessionSnapshot | null;
|
|
29
31
|
setSessionThinkingEffort(id: string, effort: SessionSnapshot["thinkingEffort"]): SessionSnapshot | null;
|
|
30
32
|
setSessionMode(id: string, mode: ExecutionMode): SessionSnapshot | null;
|
package/dist/session-registry.js
CHANGED
|
@@ -65,7 +65,9 @@ export class SessionRegistry {
|
|
|
65
65
|
return this.storage.getSession(id) ? "storage" : null;
|
|
66
66
|
}
|
|
67
67
|
get(id) {
|
|
68
|
-
|
|
68
|
+
const snapshot = this.structured.get(id) ?? this.processes.getOwned(id) ?? this.storage.getSession(id);
|
|
69
|
+
const membership = this.storage.getSessionWorkspace(id);
|
|
70
|
+
return snapshot && membership ? { ...snapshot, ...membership } : snapshot;
|
|
69
71
|
}
|
|
70
72
|
getLatest(id) {
|
|
71
73
|
return this.get(id);
|
|
@@ -79,11 +81,24 @@ export class SessionRegistry {
|
|
|
79
81
|
byId.set(snapshot.id, snapshot);
|
|
80
82
|
}
|
|
81
83
|
for (const snapshot of this.storage.loadSessionsSlim()) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
+
const live = byId.get(snapshot.id);
|
|
85
|
+
byId.set(snapshot.id, live ? {
|
|
86
|
+
...live, workspaceId: snapshot.workspaceId, workspaceTaskId: snapshot.workspaceTaskId,
|
|
87
|
+
} : snapshot);
|
|
84
88
|
}
|
|
85
89
|
return Array.from(byId.values()).sort((a, b) => b.startedAt.localeCompare(a.startedAt));
|
|
86
90
|
}
|
|
91
|
+
/** Membership belongs to storage; both runners only refresh their live projection. */
|
|
92
|
+
refreshSessionWorkspace(id) {
|
|
93
|
+
const membership = this.storage.getSessionWorkspace(id);
|
|
94
|
+
if (!membership)
|
|
95
|
+
return;
|
|
96
|
+
const owner = this.ownerOf(id);
|
|
97
|
+
if (owner === "structured")
|
|
98
|
+
this.structured.setSessionWorkspace(id, membership);
|
|
99
|
+
else if (owner === "pty")
|
|
100
|
+
this.processes.setSessionWorkspace(id, membership);
|
|
101
|
+
}
|
|
87
102
|
setSessionModel(id, model) {
|
|
88
103
|
const owner = this.ownerOf(id);
|
|
89
104
|
if (owner === "structured")
|
package/dist/storage.d.ts
CHANGED
|
@@ -75,6 +75,7 @@ export declare class WandStorage {
|
|
|
75
75
|
}): void;
|
|
76
76
|
listSessionsByWorkspace(workspaceId: string): SessionSnapshot[];
|
|
77
77
|
/** 显式更新某会话的工作空间归属(用于创建时绑定)。 */
|
|
78
|
+
getSessionWorkspace(sessionId: string): Pick<SessionSnapshot, "workspaceId" | "workspaceTaskId"> | null;
|
|
78
79
|
setSessionWorkspaceId(sessionId: string, workspaceId: string | null): void;
|
|
79
80
|
/** Lightweight count of persisted sessions grouped by workspace. */
|
|
80
81
|
countSessionsByWorkspace(): Map<string, number>;
|
|
@@ -100,6 +101,7 @@ export declare class WandStorage {
|
|
|
100
101
|
status?: WorkspaceTaskStatus;
|
|
101
102
|
worktree?: WorkspaceTaskWorktree | null;
|
|
102
103
|
milestoneId?: string | null;
|
|
104
|
+
workspaceId?: string;
|
|
103
105
|
}): void;
|
|
104
106
|
saveWorkspaceTaskLayout(id: string, layout: TaskWindowLayout | null, expectedRevision?: number): {
|
|
105
107
|
ok: true;
|
|
@@ -120,7 +122,6 @@ export declare class WandStorage {
|
|
|
120
122
|
private mapWandTaskRow;
|
|
121
123
|
getWandTask(id: string): import("./task-types.js").WandTask | null;
|
|
122
124
|
getWandTaskByWorkspaceTaskId(workspaceTaskId: string): import("./task-types.js").WandTask | null;
|
|
123
|
-
findUnlinkedWandTask(workspaceId: string, title: string): import("./task-types.js").WandTask | null;
|
|
124
125
|
createWandTask(input: {
|
|
125
126
|
workspaceId?: string | null;
|
|
126
127
|
workspaceTaskId?: string | null;
|
|
@@ -134,7 +135,7 @@ export declare class WandStorage {
|
|
|
134
135
|
milestoneId?: string | null;
|
|
135
136
|
agent?: import("./task-types.js").WandTaskAgent | null;
|
|
136
137
|
}): import("./task-types.js").WandTask;
|
|
137
|
-
updateWandTask(id: string, patch: Partial<Pick<import("./task-types.js").WandTask, "workspaceId" | "workspaceTaskId" | "title" | "titleSource" | "description" | "status" | "priority" | "labels" | "dueDate" | "milestoneId" | "sortOrder" | "agent">>): import("./task-types.js").WandTask | null;
|
|
138
|
+
updateWandTask(id: string, patch: Partial<Pick<import("./task-types.js").WandTask, "workspaceId" | "workspaceTaskId" | "title" | "titleSource" | "autoTitleSignature" | "description" | "status" | "priority" | "labels" | "dueDate" | "milestoneId" | "sortOrder" | "agent">>): import("./task-types.js").WandTask | null;
|
|
138
139
|
deleteWandTask(id: string): void;
|
|
139
140
|
listWandMilestones(): import("./task-types.js").WandTaskMilestone[];
|
|
140
141
|
getWandMilestone(id: string): import("./task-types.js").WandTaskMilestone | null;
|