@co0ontty/wand 4.42.0 → 4.42.1-beta.g6a621ff
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 +4 -4
- package/dist/cli.js +0 -3
- package/dist/missions.d.ts +4 -7
- package/dist/missions.js +12 -61
- package/dist/process-manager.js +8 -2
- package/dist/server-mission-routes.js +2 -4
- package/dist/server-session-routes.js +17 -5
- package/dist/server-workspace-routes.js +17 -7
- package/dist/server.js +7 -4
- package/dist/storage.d.ts +8 -0
- package/dist/storage.js +24 -1
- package/dist/web-ui/content/scripts.js +66 -83
- package/dist/web-ui/content/styles.css +1 -1
- package/dist/web-ui/embedded-assets.d.ts +1 -1
- package/dist/web-ui/embedded-assets.js +3 -3
- package/dist/workspace-binding.d.ts +29 -0
- package/dist/workspace-binding.js +111 -0
- package/package.json +1 -1
package/dist/build-info.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"commit": "
|
|
3
|
-
"builtAt": "2026-08-
|
|
4
|
-
"version": "4.42.
|
|
5
|
-
"channel": "
|
|
2
|
+
"commit": "6a621ff17512424111dd00205861d1eeea439387",
|
|
3
|
+
"builtAt": "2026-08-14T01:28:27.019Z",
|
|
4
|
+
"version": "4.42.1-beta.g6a621ff",
|
|
5
|
+
"channel": "beta"
|
|
6
6
|
}
|
package/dist/cli.js
CHANGED
|
@@ -177,7 +177,6 @@ async function main() {
|
|
|
177
177
|
case "session:read":
|
|
178
178
|
case "session:send":
|
|
179
179
|
case "session:wait":
|
|
180
|
-
case "inbox:list":
|
|
181
180
|
case "mission:list":
|
|
182
181
|
case "mission:create":
|
|
183
182
|
case "mission:diff":
|
|
@@ -228,7 +227,6 @@ Agent runtime:
|
|
|
228
227
|
wand session:read <id> Read a complete session snapshot
|
|
229
228
|
wand session:send <id> <text>
|
|
230
229
|
wand session:wait <id> [--timeout 900]
|
|
231
|
-
wand inbox:list List global Agent Inbox items
|
|
232
230
|
wand mission:list List parallel tasks
|
|
233
231
|
wand mission:create --prompt <text> --cwd <path> --providers claude,codex
|
|
234
232
|
wand mission:diff <mission-id> <attempt-id>
|
|
@@ -286,7 +284,6 @@ async function runAgentCliCommand(command, args, configPath) {
|
|
|
286
284
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
287
285
|
}
|
|
288
286
|
}
|
|
289
|
-
case "inbox:list": return output(await api.get("/api/inbox"));
|
|
290
287
|
case "mission:list": return output(await api.get("/api/missions"));
|
|
291
288
|
case "mission:create": {
|
|
292
289
|
const prompt = required(readFlagValue(args, "--prompt"), "wand mission:create --prompt <text> --cwd <path> --providers claude,codex");
|
package/dist/missions.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { CreateMissionInput, CreateReviewCommentInput, MissionDetails, MissionDiff, MissionReviewComment } from "./mission-types.js";
|
|
2
2
|
import type { SessionRegistry } from "./session-registry.js";
|
|
3
3
|
import type { StructuredSessionManager } from "./structured-session-manager.js";
|
|
4
4
|
import type { WandStorage } from "./storage.js";
|
|
5
5
|
import type { ProcessEvent } from "./types.js";
|
|
6
6
|
/**
|
|
7
|
-
* Deep task-orchestration module. Callers create missions
|
|
8
|
-
*
|
|
7
|
+
* Deep task-orchestration module. Callers create missions and review diffs
|
|
8
|
+
* without coordinating session/worktree/storage details.
|
|
9
9
|
*/
|
|
10
10
|
export declare class Missions {
|
|
11
11
|
private readonly storage;
|
|
@@ -14,17 +14,14 @@ export declare class Missions {
|
|
|
14
14
|
constructor(storage: WandStorage, structured: StructuredSessionManager, sessions: SessionRegistry);
|
|
15
15
|
list(): MissionDetails[];
|
|
16
16
|
get(id: string): MissionDetails | null;
|
|
17
|
-
inbox(): AgentActivityItem[];
|
|
18
|
-
markInboxRead(sessionId?: string): void;
|
|
19
17
|
create(input: CreateMissionInput): MissionDetails;
|
|
20
|
-
ingest(event: ProcessEvent):
|
|
18
|
+
ingest(event: ProcessEvent): void;
|
|
21
19
|
diff(missionId: string, attemptId: string): MissionDiff;
|
|
22
20
|
addReviewComment(missionId: string, attemptId: string, input: CreateReviewCommentInput): MissionReviewComment;
|
|
23
21
|
sendReview(missionId: string, attemptId: string, commentIds?: string[]): MissionReviewComment[];
|
|
24
22
|
resolveReview(missionId: string, attemptId: string, commentIds: string[]): MissionReviewComment[];
|
|
25
23
|
archive(id: string): MissionDetails;
|
|
26
24
|
private dispatchAttempt;
|
|
27
|
-
private seedInbox;
|
|
28
25
|
private details;
|
|
29
26
|
private getIncludingArchived;
|
|
30
27
|
private requireAttempt;
|
package/dist/missions.js
CHANGED
|
@@ -94,8 +94,8 @@ function reviewPrompt(comments) {
|
|
|
94
94
|
].join("\n");
|
|
95
95
|
}
|
|
96
96
|
/**
|
|
97
|
-
* Deep task-orchestration module. Callers create missions
|
|
98
|
-
*
|
|
97
|
+
* Deep task-orchestration module. Callers create missions and review diffs
|
|
98
|
+
* without coordinating session/worktree/storage details.
|
|
99
99
|
*/
|
|
100
100
|
export class Missions {
|
|
101
101
|
storage;
|
|
@@ -113,13 +113,6 @@ export class Missions {
|
|
|
113
113
|
const mission = this.storage.getMission(id);
|
|
114
114
|
return mission ? this.details(mission) : null;
|
|
115
115
|
}
|
|
116
|
-
inbox() {
|
|
117
|
-
this.seedInbox();
|
|
118
|
-
return this.storage.listAgentActivity();
|
|
119
|
-
}
|
|
120
|
-
markInboxRead(sessionId) {
|
|
121
|
-
this.storage.markAgentActivityRead(sessionId);
|
|
122
|
-
}
|
|
123
116
|
create(input) {
|
|
124
117
|
const prompt = input.prompt?.trim();
|
|
125
118
|
if (!prompt)
|
|
@@ -156,38 +149,23 @@ export class Missions {
|
|
|
156
149
|
}
|
|
157
150
|
ingest(event) {
|
|
158
151
|
if (!event.sessionId || event.sessionId === "__system__")
|
|
159
|
-
return
|
|
152
|
+
return;
|
|
160
153
|
const snapshot = this.sessions.getLatest(event.sessionId);
|
|
161
154
|
if (!snapshot)
|
|
162
|
-
return
|
|
155
|
+
return;
|
|
163
156
|
const attempt = this.storage.getMissionAttemptBySession(event.sessionId);
|
|
164
|
-
|
|
157
|
+
if (!attempt)
|
|
158
|
+
return;
|
|
165
159
|
const state = activityState(snapshot, event);
|
|
166
160
|
const updatedAt = nowIso();
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
attemptId: attempt?.id ?? null,
|
|
171
|
-
state,
|
|
172
|
-
title: mission?.title || snapshot.title || `${snapshot.provider ?? "agent"} 会话`,
|
|
161
|
+
this.storage.saveMissionAttempt({
|
|
162
|
+
...attempt,
|
|
163
|
+
state: state,
|
|
173
164
|
summary: sessionSummary(snapshot),
|
|
174
|
-
|
|
175
|
-
cwd: snapshot.cwd || null,
|
|
165
|
+
error: state === "failed" ? snapshot.structuredState?.lastError ?? "任务执行失败" : null,
|
|
176
166
|
updatedAt,
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
this.storage.upsertAgentActivity(item);
|
|
180
|
-
if (attempt) {
|
|
181
|
-
this.storage.saveMissionAttempt({
|
|
182
|
-
...attempt,
|
|
183
|
-
state: state,
|
|
184
|
-
summary: item.summary,
|
|
185
|
-
error: state === "failed" ? snapshot.structuredState?.lastError ?? "任务执行失败" : null,
|
|
186
|
-
updatedAt,
|
|
187
|
-
});
|
|
188
|
-
this.refreshMissionStatus(attempt.missionId);
|
|
189
|
-
}
|
|
190
|
-
return item;
|
|
167
|
+
});
|
|
168
|
+
this.refreshMissionStatus(attempt.missionId);
|
|
191
169
|
}
|
|
192
170
|
diff(missionId, attemptId) {
|
|
193
171
|
const attempt = this.requireAttempt(missionId, attemptId);
|
|
@@ -277,11 +255,6 @@ export class Missions {
|
|
|
277
255
|
updatedAt: nowIso(),
|
|
278
256
|
};
|
|
279
257
|
this.storage.saveMissionAttempt(attempt);
|
|
280
|
-
this.storage.upsertAgentActivity({
|
|
281
|
-
sessionId: session.id, missionId: mission.id, attemptId, state: "working",
|
|
282
|
-
title: mission.title, summary: `已分派给 ${provider}`, provider, cwd: session.cwd,
|
|
283
|
-
updatedAt: attempt.updatedAt, readAt: null,
|
|
284
|
-
});
|
|
285
258
|
const completion = this.structured.sendMessage(session.id, mission.prompt);
|
|
286
259
|
completion.catch((error) => {
|
|
287
260
|
console.error(`[Missions] Attempt ${attemptId} failed after dispatch:`, error);
|
|
@@ -296,28 +269,6 @@ export class Missions {
|
|
|
296
269
|
});
|
|
297
270
|
}
|
|
298
271
|
}
|
|
299
|
-
seedInbox() {
|
|
300
|
-
const existing = new Set(this.storage.listAgentActivity().map((item) => item.sessionId));
|
|
301
|
-
for (const snapshot of this.sessions.listSlim()) {
|
|
302
|
-
if (existing.has(snapshot.id))
|
|
303
|
-
continue;
|
|
304
|
-
const attempt = this.storage.getMissionAttemptBySession(snapshot.id);
|
|
305
|
-
const mission = attempt ? this.storage.getMission(attempt.missionId) : null;
|
|
306
|
-
const at = nowIso();
|
|
307
|
-
this.storage.upsertAgentActivity({
|
|
308
|
-
sessionId: snapshot.id,
|
|
309
|
-
missionId: attempt?.missionId ?? null,
|
|
310
|
-
attemptId: attempt?.id ?? null,
|
|
311
|
-
state: activityState(snapshot),
|
|
312
|
-
title: mission?.title || snapshot.title || `${snapshot.provider ?? "agent"} 会话`,
|
|
313
|
-
summary: sessionSummary(snapshot),
|
|
314
|
-
provider: snapshot.provider ?? snapshot.structuredState?.provider ?? null,
|
|
315
|
-
cwd: snapshot.cwd || null,
|
|
316
|
-
updatedAt: snapshot.endedAt || snapshot.startedAt || at,
|
|
317
|
-
readAt: at,
|
|
318
|
-
});
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
272
|
details(mission) {
|
|
322
273
|
return {
|
|
323
274
|
...mission,
|
package/dist/process-manager.js
CHANGED
|
@@ -968,12 +968,16 @@ export class ProcessManager extends EventEmitter {
|
|
|
968
968
|
let priorMessages = [];
|
|
969
969
|
let inheritedSessionSource;
|
|
970
970
|
let inheritedAutomationId;
|
|
971
|
+
let inheritedWorkspaceId;
|
|
972
|
+
let inheritedWorkspaceTaskId;
|
|
971
973
|
if (opts?.reuseId) {
|
|
972
974
|
const oldRecord = this.sessions.get(id);
|
|
973
975
|
if (oldRecord) {
|
|
974
976
|
priorMessages = oldRecord.ptyBridge?.getMessages() ?? oldRecord.messages ?? [];
|
|
975
977
|
inheritedSessionSource = oldRecord.sessionSource;
|
|
976
978
|
inheritedAutomationId = oldRecord.automationId;
|
|
979
|
+
inheritedWorkspaceId = oldRecord.workspaceId;
|
|
980
|
+
inheritedWorkspaceTaskId = oldRecord.workspaceTaskId;
|
|
977
981
|
this.cleanupRecord(oldRecord);
|
|
978
982
|
this.sessions.delete(id);
|
|
979
983
|
}
|
|
@@ -982,6 +986,8 @@ export class ProcessManager extends EventEmitter {
|
|
|
982
986
|
priorMessages = stored?.messages ?? [];
|
|
983
987
|
inheritedSessionSource = stored?.sessionSource;
|
|
984
988
|
inheritedAutomationId = stored?.automationId;
|
|
989
|
+
inheritedWorkspaceId = stored?.workspaceId;
|
|
990
|
+
inheritedWorkspaceTaskId = stored?.workspaceTaskId;
|
|
985
991
|
}
|
|
986
992
|
this.terminalHost.forget(id);
|
|
987
993
|
}
|
|
@@ -1032,8 +1038,8 @@ export class ProcessManager extends EventEmitter {
|
|
|
1032
1038
|
id,
|
|
1033
1039
|
sessionSource: opts?.sessionSource ?? inheritedSessionSource ?? "interactive",
|
|
1034
1040
|
automationId: opts?.automationId ?? inheritedAutomationId,
|
|
1035
|
-
workspaceId: opts?.workspaceId,
|
|
1036
|
-
workspaceTaskId: opts?.workspaceTaskId,
|
|
1041
|
+
workspaceId: opts?.workspaceId ?? inheritedWorkspaceId,
|
|
1042
|
+
workspaceTaskId: opts?.workspaceTaskId ?? inheritedWorkspaceTaskId,
|
|
1037
1043
|
provider,
|
|
1038
1044
|
command,
|
|
1039
1045
|
cwd: resolvedCwd,
|
|
@@ -6,11 +6,9 @@ function sendMissionError(res, error) {
|
|
|
6
6
|
}
|
|
7
7
|
export function registerMissionRoutes(app, missions) {
|
|
8
8
|
app.get("/api/inbox", (_req, res) => {
|
|
9
|
-
res.json({ items:
|
|
9
|
+
res.json({ items: [] });
|
|
10
10
|
});
|
|
11
|
-
app.post("/api/inbox/read", (
|
|
12
|
-
const sessionId = typeof req.body?.sessionId === "string" ? req.body.sessionId : undefined;
|
|
13
|
-
missions.markInboxRead(sessionId);
|
|
11
|
+
app.post("/api/inbox/read", (_req, res) => {
|
|
14
12
|
res.json({ ok: true });
|
|
15
13
|
});
|
|
16
14
|
app.get("/api/missions", (_req, res) => {
|
|
@@ -13,6 +13,7 @@ import { parseBoundedInteger } from "./request-limits.js";
|
|
|
13
13
|
import { asyncRoute } from "./express-async.js";
|
|
14
14
|
import { enrichStructuredMessages, WAND_PROTOCOL_VERSION } from "./structured-client-protocol.js";
|
|
15
15
|
import { buildDirectoryTree, normalizeSessionDirectory, } from "./session-directory-tree.js";
|
|
16
|
+
import { projectCwdForSession, resolveWorkspaceIdForNewSession, } from "./workspace-binding.js";
|
|
16
17
|
export function parseExecutionMode(value, fallback) {
|
|
17
18
|
if (value === undefined)
|
|
18
19
|
return fallback;
|
|
@@ -367,7 +368,7 @@ function isPtyProviderCommand(provider, command) {
|
|
|
367
368
|
const executable = provider === "qoder" ? "qodercli" : provider;
|
|
368
369
|
return new RegExp(`^${executable}\\b`, "i").test(command.trim());
|
|
369
370
|
}
|
|
370
|
-
async function startResumedPtySession(processes, existingSession, sessionId, defaultMode, body, initialInput) {
|
|
371
|
+
async function startResumedPtySession(processes, storage, existingSession, sessionId, defaultMode, body, initialInput) {
|
|
371
372
|
if ((existingSession.sessionKind ?? "pty") !== "pty") {
|
|
372
373
|
throw new Error("结构化会话不支持 PTY resume。");
|
|
373
374
|
}
|
|
@@ -396,6 +397,9 @@ async function startResumedPtySession(processes, existingSession, sessionId, def
|
|
|
396
397
|
provider,
|
|
397
398
|
model: existingSession.selectedModel ?? undefined,
|
|
398
399
|
thinkingEffort: existingSession.thinkingEffort ?? undefined,
|
|
400
|
+
workspaceId: existingSession.workspaceId
|
|
401
|
+
?? resolveWorkspaceIdForNewSession(storage, projectCwdForSession(existingSession) || existingSession.cwd),
|
|
402
|
+
workspaceTaskId: existingSession.workspaceTaskId,
|
|
399
403
|
});
|
|
400
404
|
}
|
|
401
405
|
function getAutoResumeInitialInput(snapshot, input, view, shortcutKey) {
|
|
@@ -513,8 +517,9 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
513
517
|
const provider = body.provider === "codex" || body.provider === "opencode" || body.provider === "grok" || body.provider === "qoder" || body.provider === "pi" ? body.provider : "claude";
|
|
514
518
|
const rawModel = typeof body.model === "string" ? body.model.trim() : "";
|
|
515
519
|
const origin = parseSessionCreationOrigin(body);
|
|
520
|
+
const cwd = resolveSessionCwd(body.cwd, config.defaultCwd);
|
|
516
521
|
const snapshot = structured.createSession({
|
|
517
|
-
cwd
|
|
522
|
+
cwd,
|
|
518
523
|
mode: parseExecutionMode(body.mode, defaultMode),
|
|
519
524
|
provider,
|
|
520
525
|
// Omit runner to let StructuredSessionManager apply the configured
|
|
@@ -525,7 +530,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
525
530
|
thinkingEffort: typeof body.thinkingEffort === "string"
|
|
526
531
|
? body.thinkingEffort
|
|
527
532
|
: config.defaultThinkingEffort,
|
|
528
|
-
workspaceId: body.workspaceId,
|
|
533
|
+
workspaceId: resolveWorkspaceIdForNewSession(storage, cwd, body.workspaceId),
|
|
529
534
|
workspaceTaskId: body.workspaceTaskId,
|
|
530
535
|
...origin,
|
|
531
536
|
});
|
|
@@ -1081,7 +1086,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
1081
1086
|
res.status(400).json({ error: "结构化会话不支持 PTY resume。" });
|
|
1082
1087
|
return;
|
|
1083
1088
|
}
|
|
1084
|
-
const newSnapshot = await startResumedPtySession(processes, existingSession, sessionId, defaultMode, body);
|
|
1089
|
+
const newSnapshot = await startResumedPtySession(processes, storage, existingSession, sessionId, defaultMode, body);
|
|
1085
1090
|
res.status(201).json(newSnapshot);
|
|
1086
1091
|
}
|
|
1087
1092
|
catch (error) {
|
|
@@ -1128,6 +1133,9 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
1128
1133
|
reuseId: existingSession.id,
|
|
1129
1134
|
cols: reqCols,
|
|
1130
1135
|
rows: reqRows,
|
|
1136
|
+
workspaceId: existingSession.workspaceId
|
|
1137
|
+
?? resolveWorkspaceIdForNewSession(storage, projectCwdForSession(existingSession) || existingSession.cwd),
|
|
1138
|
+
workspaceTaskId: existingSession.workspaceTaskId,
|
|
1131
1139
|
...(requestedOrigin ?? {}),
|
|
1132
1140
|
});
|
|
1133
1141
|
res.status(201).json({ resumedClaudeSessionId: claudeSessionId, ...sessionResponseDTO(newSnapshot) });
|
|
@@ -1145,6 +1153,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
1145
1153
|
const newSnapshot = await processes.start(resumeCommand, cwd, newMode, undefined, {
|
|
1146
1154
|
cols: reqCols,
|
|
1147
1155
|
rows: reqRows,
|
|
1156
|
+
workspaceId: resolveWorkspaceIdForNewSession(storage, cwd),
|
|
1148
1157
|
...(requestedOrigin ?? {}),
|
|
1149
1158
|
});
|
|
1150
1159
|
res.status(201).json({ resumedClaudeSessionId: claudeSessionId, ...sessionResponseDTO(newSnapshot) });
|
|
@@ -1189,7 +1198,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
1189
1198
|
const existingSession = processes.get(sessionId) || storage.getSession(sessionId);
|
|
1190
1199
|
const autoResumeInput = getAutoResumeInitialInput(existingSession, input, view, shortcutKey);
|
|
1191
1200
|
if (autoResumeInput !== null && canAutoResumePtyForInput(existingSession, autoResumeInput)) {
|
|
1192
|
-
const snapshot = await startResumedPtySession(processes, existingSession, sessionId, defaultMode, {}, autoResumeInput);
|
|
1201
|
+
const snapshot = await startResumedPtySession(processes, storage, existingSession, sessionId, defaultMode, {}, autoResumeInput);
|
|
1193
1202
|
res.json(sessionResponseDTO(snapshot));
|
|
1194
1203
|
return;
|
|
1195
1204
|
}
|
|
@@ -1233,6 +1242,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
1233
1242
|
runner: "codex-cli-exec",
|
|
1234
1243
|
worktreeEnabled: body.worktreeEnabled === true,
|
|
1235
1244
|
claudeSessionId: threadId,
|
|
1245
|
+
workspaceId: resolveWorkspaceIdForNewSession(storage, cwd),
|
|
1236
1246
|
...origin,
|
|
1237
1247
|
});
|
|
1238
1248
|
onSessionCreated?.(cwd);
|
|
@@ -1273,6 +1283,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
1273
1283
|
runner: "opencode-cli-run",
|
|
1274
1284
|
worktreeEnabled: body.worktreeEnabled === true,
|
|
1275
1285
|
claudeSessionId: sessionId,
|
|
1286
|
+
workspaceId: resolveWorkspaceIdForNewSession(storage, cwd),
|
|
1276
1287
|
...parseSessionCreationOrigin(body),
|
|
1277
1288
|
});
|
|
1278
1289
|
onSessionCreated?.(cwd);
|
|
@@ -1307,6 +1318,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
1307
1318
|
runner: "qoder-cli-print",
|
|
1308
1319
|
worktreeEnabled: body.worktreeEnabled === true,
|
|
1309
1320
|
claudeSessionId: sessionId,
|
|
1321
|
+
workspaceId: resolveWorkspaceIdForNewSession(storage, cwd),
|
|
1310
1322
|
...parseSessionCreationOrigin(body),
|
|
1311
1323
|
});
|
|
1312
1324
|
onSessionCreated?.(cwd);
|
|
@@ -5,6 +5,8 @@ import { asyncRoute } from "./express-async.js";
|
|
|
5
5
|
import { getErrorMessage } from "./error-utils.js";
|
|
6
6
|
import { expandHomePath } from "./middleware/path-safety.js";
|
|
7
7
|
import { checkSessionWorktreeMergeabilityAsync, cleanupWorktreeSync, prepareSessionWorktree, resolveWorktreeTargetBranchAsync, } from "./git-worktree.js";
|
|
8
|
+
import { resolveSessionDisplayTitle } from "./session-transport.js";
|
|
9
|
+
import { attachUnboundSessionsToWorkspace, backfillSessionWorkspaces, } from "./workspace-binding.js";
|
|
8
10
|
const PROVIDERS = new Set(["claude", "codex", "opencode", "grok", "qoder", "pi"]);
|
|
9
11
|
function parseDefaultProvider(value) {
|
|
10
12
|
return typeof value === "string" && PROVIDERS.has(value) ? value : undefined;
|
|
@@ -29,11 +31,13 @@ function deleteSessions(storage, sessions, sessionIds) {
|
|
|
29
31
|
storage.deleteSession(sessionId);
|
|
30
32
|
}
|
|
31
33
|
}
|
|
32
|
-
function
|
|
34
|
+
function workspaceWithCounts(storage, workspace, sessionCounts) {
|
|
33
35
|
const worktreeCount = storage.listWorkspaceTasks(workspace.id)
|
|
34
36
|
.filter((task) => task.worktree !== null)
|
|
35
37
|
.length;
|
|
36
|
-
|
|
38
|
+
const sessionCount = sessionCounts?.get(workspace.id)
|
|
39
|
+
?? storage.listSessionsByWorkspace(workspace.id).length;
|
|
40
|
+
return { ...workspace, worktreeCount, sessionCount };
|
|
37
41
|
}
|
|
38
42
|
// ── Layout validation / sanitization(前端 PUT 与测试共用)──
|
|
39
43
|
function sanitizePaneTab(value) {
|
|
@@ -148,7 +152,9 @@ export function sanitizeTaskLayout(value) {
|
|
|
148
152
|
export function registerWorkspaceRoutes(app, storage, sessions) {
|
|
149
153
|
// 列出所有项目(按最近打开排序)
|
|
150
154
|
app.get("/api/workspaces", (_req, res) => {
|
|
151
|
-
|
|
155
|
+
backfillSessionWorkspaces(storage);
|
|
156
|
+
const sessionCounts = storage.countSessionsByWorkspace();
|
|
157
|
+
res.json(storage.listWorkspaces().map((workspace) => workspaceWithCounts(storage, workspace, sessionCounts)));
|
|
152
158
|
});
|
|
153
159
|
// 新建项目:名称 + 目录 + 默认 IDE,不启动会话
|
|
154
160
|
app.post("/api/workspaces", asyncRoute(async (req, res) => {
|
|
@@ -168,7 +174,8 @@ export function registerWorkspaceRoutes(app, storage, sessions) {
|
|
|
168
174
|
}
|
|
169
175
|
const defaultProvider = parseDefaultProvider(body.defaultProvider);
|
|
170
176
|
const workspace = storage.createWorkspace({ name, cwd, defaultProvider });
|
|
171
|
-
|
|
177
|
+
const attached = attachUnboundSessionsToWorkspace(storage, workspace);
|
|
178
|
+
res.status(201).json({ ...workspace, worktreeCount: 0, sessionCount: attached });
|
|
172
179
|
}));
|
|
173
180
|
// 项目详情:meta + 会话 + 布局;访问即更新 lastOpenedAt
|
|
174
181
|
app.get("/api/workspaces/:id", (req, res) => {
|
|
@@ -179,8 +186,11 @@ export function registerWorkspaceRoutes(app, storage, sessions) {
|
|
|
179
186
|
}
|
|
180
187
|
storage.touchWorkspace(workspace.id);
|
|
181
188
|
res.json({
|
|
182
|
-
...
|
|
183
|
-
sessions: storage.listSessionsByWorkspace(workspace.id)
|
|
189
|
+
...workspaceWithCounts(storage, workspace),
|
|
190
|
+
sessions: storage.listSessionsByWorkspace(workspace.id).map((session) => ({
|
|
191
|
+
...session,
|
|
192
|
+
title: resolveSessionDisplayTitle(session),
|
|
193
|
+
})),
|
|
184
194
|
});
|
|
185
195
|
});
|
|
186
196
|
// 改名 / 目录 / 默认 IDE
|
|
@@ -213,7 +223,7 @@ export function registerWorkspaceRoutes(app, storage, sessions) {
|
|
|
213
223
|
}
|
|
214
224
|
storage.updateWorkspace(existing.id, patch);
|
|
215
225
|
const updated = storage.getWorkspace(existing.id);
|
|
216
|
-
res.json(updated ?
|
|
226
|
+
res.json(updated ? workspaceWithCounts(storage, updated) : null);
|
|
217
227
|
});
|
|
218
228
|
// 删除项目;cascade=true 连带删会话,否则仅解绑
|
|
219
229
|
app.delete("/api/workspaces/:id", (req, res) => {
|
package/dist/server.js
CHANGED
|
@@ -28,6 +28,7 @@ import { refreshProviderCliUpdateState, registerAdminUpdateRoutes, registerPubli
|
|
|
28
28
|
import { parseSessionCreationOrigin, registerClaudeHistoryRoutes, registerSessionRoutes } from "./server-session-routes.js";
|
|
29
29
|
import { registerWorkspaceRoutes } from "./server-workspace-routes.js";
|
|
30
30
|
import { resolveSessionCwd } from "./session-cwd.js";
|
|
31
|
+
import { resolveWorkspaceIdForNewSession } from "./workspace-binding.js";
|
|
31
32
|
import { getErrorMessage } from "./error-utils.js";
|
|
32
33
|
import { asyncRoute, jsonErrorHandler } from "./express-async.js";
|
|
33
34
|
import { checkPackageUpdateAsync, installPackageGloballyAsync, normalizeUpdateChannel, resolveGlobalWandCli, } from "./npm-update-utils.js";
|
|
@@ -1020,23 +1021,25 @@ export async function startServer(config, configPath, options = {}) {
|
|
|
1020
1021
|
: undefined;
|
|
1021
1022
|
const reqCols = typeof body.cols === "number" && Number.isFinite(body.cols) ? body.cols : undefined;
|
|
1022
1023
|
const reqRows = typeof body.rows === "number" && Number.isFinite(body.rows) ? body.rows : undefined;
|
|
1024
|
+
const sessionCwd = resolveSessionCwd(body.cwd, config.defaultCwd);
|
|
1025
|
+
const workspaceId = resolveWorkspaceIdForNewSession(storage, sessionCwd, body.workspaceId);
|
|
1023
1026
|
const snapshot = await (interactiveShell
|
|
1024
|
-
? processes.startShell(
|
|
1027
|
+
? processes.startShell(sessionCwd, body.mode ?? "default", {
|
|
1025
1028
|
worktreeEnabled: body.worktreeEnabled === true,
|
|
1026
1029
|
cols: reqCols,
|
|
1027
1030
|
rows: reqRows,
|
|
1028
|
-
workspaceId
|
|
1031
|
+
workspaceId,
|
|
1029
1032
|
workspaceTaskId: body.workspaceTaskId,
|
|
1030
1033
|
...origin,
|
|
1031
1034
|
})
|
|
1032
|
-
: processes.start(command,
|
|
1035
|
+
: processes.start(command, sessionCwd, body.mode ?? config.defaultMode, initialInput || undefined, {
|
|
1033
1036
|
worktreeEnabled: body.worktreeEnabled === true,
|
|
1034
1037
|
provider,
|
|
1035
1038
|
model: effectiveModel,
|
|
1036
1039
|
cols: reqCols,
|
|
1037
1040
|
rows: reqRows,
|
|
1038
1041
|
thinkingEffort: body.thinkingEffort ?? config.defaultThinkingEffort,
|
|
1039
|
-
workspaceId
|
|
1042
|
+
workspaceId,
|
|
1040
1043
|
workspaceTaskId: body.workspaceTaskId,
|
|
1041
1044
|
...origin,
|
|
1042
1045
|
}));
|
package/dist/storage.d.ts
CHANGED
|
@@ -60,6 +60,14 @@ export declare class WandStorage {
|
|
|
60
60
|
listSessionsByWorkspace(workspaceId: string): SessionSnapshot[];
|
|
61
61
|
/** 显式更新某会话的工作空间归属(用于创建时绑定)。 */
|
|
62
62
|
setSessionWorkspaceId(sessionId: string, workspaceId: string | null): void;
|
|
63
|
+
/** Lightweight count of persisted sessions grouped by workspace. */
|
|
64
|
+
countSessionsByWorkspace(): Map<string, number>;
|
|
65
|
+
/** Sessions not yet attached to a project. Omits messages/output. */
|
|
66
|
+
listUnboundSessionBindings(): Array<{
|
|
67
|
+
id: string;
|
|
68
|
+
cwd: string;
|
|
69
|
+
worktree: SessionSnapshot["worktree"];
|
|
70
|
+
}>;
|
|
63
71
|
listWorkspaceTasks(workspaceId: string): WorkspaceTask[];
|
|
64
72
|
getWorkspaceTask(id: string): WorkspaceTask | null;
|
|
65
73
|
createWorkspaceTask(input: {
|
package/dist/storage.js
CHANGED
|
@@ -861,6 +861,29 @@ export class WandStorage {
|
|
|
861
861
|
setSessionWorkspaceId(sessionId, workspaceId) {
|
|
862
862
|
this.db.prepare("UPDATE command_sessions SET workspace_id = ? WHERE id = ?").run(workspaceId, sessionId);
|
|
863
863
|
}
|
|
864
|
+
/** Lightweight count of persisted sessions grouped by workspace. */
|
|
865
|
+
countSessionsByWorkspace() {
|
|
866
|
+
const rows = this.db
|
|
867
|
+
.prepare(`SELECT workspace_id AS id, COUNT(*) AS n
|
|
868
|
+
FROM command_sessions
|
|
869
|
+
WHERE workspace_id IS NOT NULL AND workspace_id != ''
|
|
870
|
+
GROUP BY workspace_id`)
|
|
871
|
+
.all();
|
|
872
|
+
return new Map(rows.map((row) => [row.id, Number(row.n) || 0]));
|
|
873
|
+
}
|
|
874
|
+
/** Sessions not yet attached to a project. Omits messages/output. */
|
|
875
|
+
listUnboundSessionBindings() {
|
|
876
|
+
const rows = this.db
|
|
877
|
+
.prepare(`SELECT id, cwd, worktree_info
|
|
878
|
+
FROM command_sessions
|
|
879
|
+
WHERE workspace_id IS NULL OR workspace_id = ''`)
|
|
880
|
+
.all();
|
|
881
|
+
return rows.map((row) => ({
|
|
882
|
+
id: row.id,
|
|
883
|
+
cwd: row.cwd,
|
|
884
|
+
worktree: parseWorktreeInfo(row.worktree_info) ?? null,
|
|
885
|
+
}));
|
|
886
|
+
}
|
|
864
887
|
// ── Workspace tasks(任务 = 命名 + 独立 worktree + 一组标签)──
|
|
865
888
|
listWorkspaceTasks(workspaceId) {
|
|
866
889
|
const rows = this.db
|
|
@@ -1111,7 +1134,7 @@ export class WandStorage {
|
|
|
1111
1134
|
deleteExpiredAuthSessions(now) {
|
|
1112
1135
|
this.db.prepare("DELETE FROM auth_sessions WHERE expires_at < ?").run(now);
|
|
1113
1136
|
}
|
|
1114
|
-
// ============ Missions
|
|
1137
|
+
// ============ Missions ============
|
|
1115
1138
|
saveMission(mission) {
|
|
1116
1139
|
this.db.prepare(`INSERT INTO missions (
|
|
1117
1140
|
id, title, prompt, cwd, status, base_ref, shared_directories, copy_paths, created_at, updated_at
|