@co0ontty/wand 4.42.1 → 4.43.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/cli.js +0 -3
- package/dist/distribution-manager.d.ts +8 -0
- package/dist/distribution-manager.js +41 -2
- package/dist/missions.d.ts +4 -7
- package/dist/missions.js +12 -61
- package/dist/process-manager.js +15 -7
- package/dist/provider-history-scanner.js +5 -0
- package/dist/server-mission-routes.js +2 -4
- package/dist/server-session-routes.d.ts +4 -11
- package/dist/server-session-routes.js +41 -66
- package/dist/server-update-routes.d.ts +1 -0
- package/dist/server-update-routes.js +2 -0
- 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/terminal-daemon-client.d.ts +20 -0
- package/dist/terminal-daemon-client.js +112 -7
- package/dist/types.d.ts +2 -0
- package/dist/web-ui/content/scripts.js +72 -89
- package/dist/web-ui/content/styles.css +1 -1
- package/dist/web-ui/content/vendor/xterm/xterm.bundle.js +10 -10
- package/dist/web-ui/embedded-assets.d.ts +2 -2
- package/dist/web-ui/embedded-assets.js +5 -5
- 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.
|
|
2
|
+
"commit": "c05b4f15f592f2c4f335df60c46208a7e0ea82fc",
|
|
3
|
+
"builtAt": "2026-08-22T00:47:50.129Z",
|
|
4
|
+
"version": "4.43.0",
|
|
5
5
|
"channel": "stable"
|
|
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");
|
|
@@ -16,6 +16,8 @@ export interface ResolvedDistributionAsset {
|
|
|
16
16
|
size: number;
|
|
17
17
|
source: "local" | "github";
|
|
18
18
|
releaseNotes?: string;
|
|
19
|
+
/** 本地分发文件的 SHA-256(hex);GitHub 来源不提供,客户端据此跳过校验。 */
|
|
20
|
+
sha256?: string;
|
|
19
21
|
}
|
|
20
22
|
export interface DistributionSettings {
|
|
21
23
|
androidApk: Record<string, unknown>;
|
|
@@ -45,6 +47,12 @@ export declare class DistributionManager {
|
|
|
45
47
|
resolveLatestDmg(): Promise<ResolvedDistributionAsset | null>;
|
|
46
48
|
resolveMacosDownload(): Promise<LocalDistributionAsset | null>;
|
|
47
49
|
getSettings(): Promise<DistributionSettings>;
|
|
50
|
+
private readonly sha256Cache;
|
|
51
|
+
/**
|
|
52
|
+
* 计算本地分发文件的 SHA-256(hex)。按 path + size + mtime 缓存:更新检查
|
|
53
|
+
* 可能被多个客户端频繁触发,避免每次都全量读盘。
|
|
54
|
+
*/
|
|
55
|
+
private computeLocalFileSha256;
|
|
48
56
|
private refreshConfig;
|
|
49
57
|
private resolveLocalAsset;
|
|
50
58
|
private readLocalAsset;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { createReadStream } from "node:fs";
|
|
1
3
|
import { mkdir, readdir, readFile, stat } from "node:fs/promises";
|
|
2
4
|
import path from "node:path";
|
|
3
5
|
import { compareApkInstallOrder, compareSemver, extractSemver } from "./version-utils.js";
|
|
@@ -48,10 +50,27 @@ export class DistributionManager {
|
|
|
48
50
|
source: "local",
|
|
49
51
|
} : null;
|
|
50
52
|
const github = githubApk ? { ...githubApk, source: "github" } : null;
|
|
53
|
+
let winner;
|
|
51
54
|
if (local && github) {
|
|
52
|
-
|
|
55
|
+
winner = compareApkInstallOrder(github.version, local.version) > 0 ? github : local;
|
|
53
56
|
}
|
|
54
|
-
|
|
57
|
+
else {
|
|
58
|
+
winner = local ?? github;
|
|
59
|
+
}
|
|
60
|
+
// 本地分发的 APK 附带 SHA-256,客户端下载后校验(GitHub 来源走正规 TLS,不提供)。
|
|
61
|
+
if (winner?.source === "local" && localApk) {
|
|
62
|
+
try {
|
|
63
|
+
const fileStat = await stat(localApk.filePath);
|
|
64
|
+
winner = {
|
|
65
|
+
...winner,
|
|
66
|
+
sha256: await this.computeLocalFileSha256(localApk.filePath, fileStat.size, fileStat.mtimeMs),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// 哈希计算失败不阻断更新检查;客户端拿到空 sha256 会跳过校验。
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return winner;
|
|
55
74
|
}
|
|
56
75
|
async resolveAndroidDownload(channel = "beta") {
|
|
57
76
|
await this.refreshConfig();
|
|
@@ -109,6 +128,26 @@ export class DistributionManager {
|
|
|
109
128
|
macosDmg: this.buildSettings("dmg", dmgDir, this.options.config.macos?.enabled === true, localDmg, githubDmg),
|
|
110
129
|
};
|
|
111
130
|
}
|
|
131
|
+
sha256Cache = new Map();
|
|
132
|
+
/**
|
|
133
|
+
* 计算本地分发文件的 SHA-256(hex)。按 path + size + mtime 缓存:更新检查
|
|
134
|
+
* 可能被多个客户端频繁触发,避免每次都全量读盘。
|
|
135
|
+
*/
|
|
136
|
+
async computeLocalFileSha256(filePath, size, mtimeMs) {
|
|
137
|
+
const key = `${size}:${mtimeMs}`;
|
|
138
|
+
const cached = this.sha256Cache.get(filePath);
|
|
139
|
+
if (cached && cached.key === key)
|
|
140
|
+
return cached.digest;
|
|
141
|
+
const digest = await new Promise((resolve, reject) => {
|
|
142
|
+
const hash = createHash("sha256");
|
|
143
|
+
const stream = createReadStream(filePath);
|
|
144
|
+
stream.on("error", reject);
|
|
145
|
+
stream.on("data", (chunk) => hash.update(chunk));
|
|
146
|
+
stream.on("end", () => resolve(hash.digest("hex")));
|
|
147
|
+
});
|
|
148
|
+
this.sha256Cache.set(filePath, { key, digest });
|
|
149
|
+
return digest;
|
|
150
|
+
}
|
|
112
151
|
async refreshConfig() {
|
|
113
152
|
let raw;
|
|
114
153
|
try {
|
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
|
@@ -369,7 +369,11 @@ function listOpenCodeSessionCandidates() {
|
|
|
369
369
|
let db = null;
|
|
370
370
|
try {
|
|
371
371
|
db = new DatabaseSync(dbPath, { readOnly: true });
|
|
372
|
-
const
|
|
372
|
+
const sessionColumns = db.prepare("PRAGMA table_info(session)").all();
|
|
373
|
+
const rootSessionFilter = sessionColumns.some((column) => column.name === "parent_id")
|
|
374
|
+
? "WHERE parent_id IS NULL"
|
|
375
|
+
: "";
|
|
376
|
+
const rows = db.prepare(`SELECT id, directory, time_created, time_updated FROM session ${rootSessionFilter} ORDER BY time_updated DESC LIMIT 500`).all();
|
|
373
377
|
return rows.flatMap((row) => {
|
|
374
378
|
if (typeof row.id !== "string" || typeof row.directory !== "string")
|
|
375
379
|
return [];
|
|
@@ -968,12 +972,16 @@ export class ProcessManager extends EventEmitter {
|
|
|
968
972
|
let priorMessages = [];
|
|
969
973
|
let inheritedSessionSource;
|
|
970
974
|
let inheritedAutomationId;
|
|
975
|
+
let inheritedWorkspaceId;
|
|
976
|
+
let inheritedWorkspaceTaskId;
|
|
971
977
|
if (opts?.reuseId) {
|
|
972
978
|
const oldRecord = this.sessions.get(id);
|
|
973
979
|
if (oldRecord) {
|
|
974
980
|
priorMessages = oldRecord.ptyBridge?.getMessages() ?? oldRecord.messages ?? [];
|
|
975
981
|
inheritedSessionSource = oldRecord.sessionSource;
|
|
976
982
|
inheritedAutomationId = oldRecord.automationId;
|
|
983
|
+
inheritedWorkspaceId = oldRecord.workspaceId;
|
|
984
|
+
inheritedWorkspaceTaskId = oldRecord.workspaceTaskId;
|
|
977
985
|
this.cleanupRecord(oldRecord);
|
|
978
986
|
this.sessions.delete(id);
|
|
979
987
|
}
|
|
@@ -982,6 +990,8 @@ export class ProcessManager extends EventEmitter {
|
|
|
982
990
|
priorMessages = stored?.messages ?? [];
|
|
983
991
|
inheritedSessionSource = stored?.sessionSource;
|
|
984
992
|
inheritedAutomationId = stored?.automationId;
|
|
993
|
+
inheritedWorkspaceId = stored?.workspaceId;
|
|
994
|
+
inheritedWorkspaceTaskId = stored?.workspaceTaskId;
|
|
985
995
|
}
|
|
986
996
|
this.terminalHost.forget(id);
|
|
987
997
|
}
|
|
@@ -1032,8 +1042,8 @@ export class ProcessManager extends EventEmitter {
|
|
|
1032
1042
|
id,
|
|
1033
1043
|
sessionSource: opts?.sessionSource ?? inheritedSessionSource ?? "interactive",
|
|
1034
1044
|
automationId: opts?.automationId ?? inheritedAutomationId,
|
|
1035
|
-
workspaceId: opts?.workspaceId,
|
|
1036
|
-
workspaceTaskId: opts?.workspaceTaskId,
|
|
1045
|
+
workspaceId: opts?.workspaceId ?? inheritedWorkspaceId,
|
|
1046
|
+
workspaceTaskId: opts?.workspaceTaskId ?? inheritedWorkspaceTaskId,
|
|
1037
1047
|
provider,
|
|
1038
1048
|
command,
|
|
1039
1049
|
cwd: resolvedCwd,
|
|
@@ -2352,10 +2362,8 @@ export class ProcessManager extends EventEmitter {
|
|
|
2352
2362
|
const escapedModel = trimmedModel.replace(/'/g, "'\\''");
|
|
2353
2363
|
result += ` --model '${escapedModel}'`;
|
|
2354
2364
|
}
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
result += ` --variant '${variant.replace(/'/g, "'\\''")}'`;
|
|
2358
|
-
}
|
|
2365
|
+
// thinkingEffort → --variant 仅适用于 `opencode run`(结构化 runner);
|
|
2366
|
+
// 交互 TUI(裸 `opencode`)没有该选项,注入会导致 CLI 直接报错退出。
|
|
2359
2367
|
if ((mode === "managed" || mode === "full-access" || mode === "auto-edit") && !/--auto(?:\s|$)/.test(result)) {
|
|
2360
2368
|
result += " --auto";
|
|
2361
2369
|
}
|
|
@@ -434,6 +434,10 @@ export class ProviderHistoryScanner {
|
|
|
434
434
|
let database = null;
|
|
435
435
|
try {
|
|
436
436
|
database = new DatabaseSync(this.openCodeDatabasePath, { readOnly: true });
|
|
437
|
+
const sessionColumns = database.prepare("PRAGMA table_info(session)").all();
|
|
438
|
+
const rootSessionFilter = sessionColumns.some((column) => column.name === "parent_id")
|
|
439
|
+
? "WHERE session.parent_id IS NULL"
|
|
440
|
+
: "";
|
|
437
441
|
const rows = database.prepare(`
|
|
438
442
|
SELECT
|
|
439
443
|
session.id AS id,
|
|
@@ -445,6 +449,7 @@ export class ProviderHistoryScanner {
|
|
|
445
449
|
SUM(CASE WHEN json_extract(message.data, '$.role') = 'assistant' THEN 1 ELSE 0 END) AS assistant_count
|
|
446
450
|
FROM session
|
|
447
451
|
LEFT JOIN message ON message.session_id = session.id
|
|
452
|
+
${rootSessionFilter}
|
|
448
453
|
GROUP BY session.id
|
|
449
454
|
ORDER BY session.time_updated DESC
|
|
450
455
|
LIMIT 1000
|
|
@@ -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) => {
|
|
@@ -37,13 +37,6 @@ export type SessionListPageEntry = {
|
|
|
37
37
|
key: string;
|
|
38
38
|
sortTimestamp: number;
|
|
39
39
|
session: ReturnType<typeof toSessionListItemDTO>;
|
|
40
|
-
} | {
|
|
41
|
-
type: "recoverable";
|
|
42
|
-
key: string;
|
|
43
|
-
sortTimestamp: number;
|
|
44
|
-
history: ProviderHistorySession & {
|
|
45
|
-
provider: "claude" | "codex" | "opencode" | "qoder";
|
|
46
|
-
};
|
|
47
40
|
};
|
|
48
41
|
export interface SessionListPage {
|
|
49
42
|
entries: SessionListPageEntry[];
|
|
@@ -60,9 +53,9 @@ export interface SessionDirectoryTreeResponse {
|
|
|
60
53
|
/** Full directory-tree revision, including custom workspace names. */
|
|
61
54
|
treeRevision: string;
|
|
62
55
|
}
|
|
63
|
-
export declare function buildSessionListEntries(sessions: SessionSnapshot[]
|
|
64
|
-
export declare function buildSessionListPage(sessions: SessionSnapshot[],
|
|
65
|
-
export declare function buildSessionDirectoryTree(sessions: SessionSnapshot[],
|
|
56
|
+
export declare function buildSessionListEntries(sessions: SessionSnapshot[]): SessionListPageEntry[];
|
|
57
|
+
export declare function buildSessionListPage(sessions: SessionSnapshot[], offset: number, limit: number): SessionListPage;
|
|
58
|
+
export declare function buildSessionDirectoryTree(sessions: SessionSnapshot[], customNames?: ReadonlyMap<string, string>): SessionDirectoryTreeResponse;
|
|
66
59
|
/**
|
|
67
60
|
* Provider history is scanned by ProcessManager, but structured sessions live
|
|
68
61
|
* in StructuredSessionManager. Annotate against the combined session list so a
|
|
@@ -71,5 +64,5 @@ export declare function buildSessionDirectoryTree(sessions: SessionSnapshot[], c
|
|
|
71
64
|
*/
|
|
72
65
|
export declare function markManagedProviderHistory<T extends ProviderHistorySession>(history: T[], sessions: SessionSnapshot[], provider: "claude" | "codex" | "opencode" | "qoder"): T[];
|
|
73
66
|
export declare function registerSessionRoutes(app: Express, processes: ProcessManager, structured: StructuredSessionManager, storage: WandStorage, defaultMode: ExecutionMode, config: WandConfig, sessions: SessionRegistry, onSessionCreated?: (cwd: string | undefined | null) => void): void;
|
|
74
|
-
export declare function registerClaudeHistoryRoutes(app: Express, processes: ProcessManager, _structured: StructuredSessionManager, storage: WandStorage,
|
|
67
|
+
export declare function registerClaudeHistoryRoutes(app: Express, processes: ProcessManager, _structured: StructuredSessionManager, storage: WandStorage, _sessionRegistry: SessionRegistry): void;
|
|
75
68
|
export {};
|
|
@@ -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;
|
|
@@ -197,33 +198,13 @@ function sessionSortTimestamp(snapshot) {
|
|
|
197
198
|
const timestamp = Date.parse(snapshot.startedAt);
|
|
198
199
|
return Number.isFinite(timestamp) ? timestamp : 0;
|
|
199
200
|
}
|
|
200
|
-
export function buildSessionListEntries(sessions
|
|
201
|
-
|
|
201
|
+
export function buildSessionListEntries(sessions) {
|
|
202
|
+
return sessions.map((session) => ({
|
|
202
203
|
type: "managed",
|
|
203
204
|
key: `session-${session.id}`,
|
|
204
205
|
sortTimestamp: sessionSortTimestamp(session),
|
|
205
206
|
session: toSessionListItemDTO(session),
|
|
206
|
-
}))
|
|
207
|
-
const recoverable = [
|
|
208
|
-
...markManagedProviderHistory(claudeHistory, sessions, "claude"),
|
|
209
|
-
...markManagedProviderHistory(codexHistory, sessions, "codex"),
|
|
210
|
-
...markManagedProviderHistory(openCodeHistory, sessions, "opencode"),
|
|
211
|
-
...markManagedProviderHistory(qoderHistory, sessions, "qoder"),
|
|
212
|
-
].flatMap((history) => {
|
|
213
|
-
const provider = history.provider === "codex" || history.provider === "opencode" || history.provider === "qoder"
|
|
214
|
-
? history.provider
|
|
215
|
-
: "claude";
|
|
216
|
-
if (!history.hasConversation || history.managedByWand || hiddenHistoryIds.has(history.claudeSessionId)) {
|
|
217
|
-
return [];
|
|
218
|
-
}
|
|
219
|
-
return [{
|
|
220
|
-
type: "recoverable",
|
|
221
|
-
key: `recoverable-${provider}-${history.claudeSessionId}`,
|
|
222
|
-
sortTimestamp: history.mtimeMs,
|
|
223
|
-
history: { ...history, provider },
|
|
224
|
-
}];
|
|
225
|
-
});
|
|
226
|
-
return [...managed, ...recoverable].sort((left, right) => {
|
|
207
|
+
})).sort((left, right) => {
|
|
227
208
|
const timestampOrder = right.sortTimestamp - left.sortTimestamp;
|
|
228
209
|
return timestampOrder || left.key.localeCompare(right.key);
|
|
229
210
|
});
|
|
@@ -240,8 +221,8 @@ function sessionListRevision(entries, directoryNames = []) {
|
|
|
240
221
|
.update(JSON.stringify(revisionState))
|
|
241
222
|
.digest("base64url");
|
|
242
223
|
}
|
|
243
|
-
export function buildSessionListPage(sessions,
|
|
244
|
-
const entries = buildSessionListEntries(sessions
|
|
224
|
+
export function buildSessionListPage(sessions, offset, limit) {
|
|
225
|
+
const entries = buildSessionListEntries(sessions);
|
|
245
226
|
const boundedOffset = Math.min(Math.max(offset, 0), entries.length);
|
|
246
227
|
const revision = sessionListRevision(entries);
|
|
247
228
|
return {
|
|
@@ -251,11 +232,11 @@ export function buildSessionListPage(sessions, claudeHistory, codexHistory, hidd
|
|
|
251
232
|
revision,
|
|
252
233
|
};
|
|
253
234
|
}
|
|
254
|
-
export function buildSessionDirectoryTree(sessions,
|
|
255
|
-
const entries = buildSessionListEntries(sessions
|
|
235
|
+
export function buildSessionDirectoryTree(sessions, customNames = new Map()) {
|
|
236
|
+
const entries = buildSessionListEntries(sessions);
|
|
256
237
|
const tree = buildDirectoryTree(entries.map((entry) => ({
|
|
257
238
|
entry,
|
|
258
|
-
cwd: entry.
|
|
239
|
+
cwd: entry.session.cwd,
|
|
259
240
|
sortTimestamp: entry.sortTimestamp,
|
|
260
241
|
})), "未知目录", customNames);
|
|
261
242
|
const appliedCustomNames = [];
|
|
@@ -367,7 +348,7 @@ function isPtyProviderCommand(provider, command) {
|
|
|
367
348
|
const executable = provider === "qoder" ? "qodercli" : provider;
|
|
368
349
|
return new RegExp(`^${executable}\\b`, "i").test(command.trim());
|
|
369
350
|
}
|
|
370
|
-
async function startResumedPtySession(processes, existingSession, sessionId, defaultMode, body, initialInput) {
|
|
351
|
+
async function startResumedPtySession(processes, storage, existingSession, sessionId, defaultMode, body, initialInput) {
|
|
371
352
|
if ((existingSession.sessionKind ?? "pty") !== "pty") {
|
|
372
353
|
throw new Error("结构化会话不支持 PTY resume。");
|
|
373
354
|
}
|
|
@@ -396,6 +377,9 @@ async function startResumedPtySession(processes, existingSession, sessionId, def
|
|
|
396
377
|
provider,
|
|
397
378
|
model: existingSession.selectedModel ?? undefined,
|
|
398
379
|
thinkingEffort: existingSession.thinkingEffort ?? undefined,
|
|
380
|
+
workspaceId: existingSession.workspaceId
|
|
381
|
+
?? resolveWorkspaceIdForNewSession(storage, projectCwdForSession(existingSession) || existingSession.cwd),
|
|
382
|
+
workspaceTaskId: existingSession.workspaceTaskId,
|
|
399
383
|
});
|
|
400
384
|
}
|
|
401
385
|
function getAutoResumeInitialInput(snapshot, input, view, shortcutKey) {
|
|
@@ -437,14 +421,14 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
437
421
|
messageTotal: windowed.messageTotal,
|
|
438
422
|
});
|
|
439
423
|
};
|
|
440
|
-
const currentDirectoryTree = () => buildSessionDirectoryTree(sessions.listSlim(),
|
|
424
|
+
const currentDirectoryTree = () => buildSessionDirectoryTree(sessions.listSlim(), storage.listSessionDirectoryNames());
|
|
441
425
|
app.get("/api/session-list", (req, res) => {
|
|
442
426
|
try {
|
|
443
427
|
const offset = parseBoundedInteger(req.query.offset, 0, 0, Number.MAX_SAFE_INTEGER);
|
|
444
428
|
const limit = parseBoundedInteger(req.query.limit, 40, 1, 200);
|
|
445
429
|
const requestedRevision = typeof req.query.revision === "string" ? req.query.revision : "";
|
|
446
430
|
const currentSessions = sessions.listSlim();
|
|
447
|
-
const page = buildSessionListPage(currentSessions,
|
|
431
|
+
const page = buildSessionListPage(currentSessions, offset, limit);
|
|
448
432
|
if (offset > 0 && requestedRevision !== page.revision) {
|
|
449
433
|
res.status(409).json({ error: "会话列表已更新,请重新加载。", revision: page.revision });
|
|
450
434
|
return;
|
|
@@ -513,8 +497,9 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
513
497
|
const provider = body.provider === "codex" || body.provider === "opencode" || body.provider === "grok" || body.provider === "qoder" || body.provider === "pi" ? body.provider : "claude";
|
|
514
498
|
const rawModel = typeof body.model === "string" ? body.model.trim() : "";
|
|
515
499
|
const origin = parseSessionCreationOrigin(body);
|
|
500
|
+
const cwd = resolveSessionCwd(body.cwd, config.defaultCwd);
|
|
516
501
|
const snapshot = structured.createSession({
|
|
517
|
-
cwd
|
|
502
|
+
cwd,
|
|
518
503
|
mode: parseExecutionMode(body.mode, defaultMode),
|
|
519
504
|
provider,
|
|
520
505
|
// Omit runner to let StructuredSessionManager apply the configured
|
|
@@ -525,7 +510,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
525
510
|
thinkingEffort: typeof body.thinkingEffort === "string"
|
|
526
511
|
? body.thinkingEffort
|
|
527
512
|
: config.defaultThinkingEffort,
|
|
528
|
-
workspaceId: body.workspaceId,
|
|
513
|
+
workspaceId: resolveWorkspaceIdForNewSession(storage, cwd, body.workspaceId),
|
|
529
514
|
workspaceTaskId: body.workspaceTaskId,
|
|
530
515
|
...origin,
|
|
531
516
|
});
|
|
@@ -1081,7 +1066,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
1081
1066
|
res.status(400).json({ error: "结构化会话不支持 PTY resume。" });
|
|
1082
1067
|
return;
|
|
1083
1068
|
}
|
|
1084
|
-
const newSnapshot = await startResumedPtySession(processes, existingSession, sessionId, defaultMode, body);
|
|
1069
|
+
const newSnapshot = await startResumedPtySession(processes, storage, existingSession, sessionId, defaultMode, body);
|
|
1085
1070
|
res.status(201).json(newSnapshot);
|
|
1086
1071
|
}
|
|
1087
1072
|
catch (error) {
|
|
@@ -1128,6 +1113,9 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
1128
1113
|
reuseId: existingSession.id,
|
|
1129
1114
|
cols: reqCols,
|
|
1130
1115
|
rows: reqRows,
|
|
1116
|
+
workspaceId: existingSession.workspaceId
|
|
1117
|
+
?? resolveWorkspaceIdForNewSession(storage, projectCwdForSession(existingSession) || existingSession.cwd),
|
|
1118
|
+
workspaceTaskId: existingSession.workspaceTaskId,
|
|
1131
1119
|
...(requestedOrigin ?? {}),
|
|
1132
1120
|
});
|
|
1133
1121
|
res.status(201).json({ resumedClaudeSessionId: claudeSessionId, ...sessionResponseDTO(newSnapshot) });
|
|
@@ -1145,6 +1133,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
1145
1133
|
const newSnapshot = await processes.start(resumeCommand, cwd, newMode, undefined, {
|
|
1146
1134
|
cols: reqCols,
|
|
1147
1135
|
rows: reqRows,
|
|
1136
|
+
workspaceId: resolveWorkspaceIdForNewSession(storage, cwd),
|
|
1148
1137
|
...(requestedOrigin ?? {}),
|
|
1149
1138
|
});
|
|
1150
1139
|
res.status(201).json({ resumedClaudeSessionId: claudeSessionId, ...sessionResponseDTO(newSnapshot) });
|
|
@@ -1189,11 +1178,19 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
1189
1178
|
const existingSession = processes.get(sessionId) || storage.getSession(sessionId);
|
|
1190
1179
|
const autoResumeInput = getAutoResumeInitialInput(existingSession, input, view, shortcutKey);
|
|
1191
1180
|
if (autoResumeInput !== null && canAutoResumePtyForInput(existingSession, autoResumeInput)) {
|
|
1192
|
-
const snapshot = await startResumedPtySession(processes, existingSession, sessionId, defaultMode, {}, autoResumeInput);
|
|
1181
|
+
const snapshot = await startResumedPtySession(processes, storage, existingSession, sessionId, defaultMode, {}, autoResumeInput);
|
|
1182
|
+
if (body.responseMode === "accepted") {
|
|
1183
|
+
res.status(202).json({ accepted: true });
|
|
1184
|
+
return;
|
|
1185
|
+
}
|
|
1193
1186
|
res.json(sessionResponseDTO(snapshot));
|
|
1194
1187
|
return;
|
|
1195
1188
|
}
|
|
1196
1189
|
const snapshot = processes.sendInput(sessionId, input, view, shortcutKey);
|
|
1190
|
+
if (body.responseMode === "accepted") {
|
|
1191
|
+
res.status(202).json({ accepted: true });
|
|
1192
|
+
return;
|
|
1193
|
+
}
|
|
1197
1194
|
res.json(sessionResponseDTO(snapshot));
|
|
1198
1195
|
}
|
|
1199
1196
|
catch (error) {
|
|
@@ -1233,6 +1230,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
1233
1230
|
runner: "codex-cli-exec",
|
|
1234
1231
|
worktreeEnabled: body.worktreeEnabled === true,
|
|
1235
1232
|
claudeSessionId: threadId,
|
|
1233
|
+
workspaceId: resolveWorkspaceIdForNewSession(storage, cwd),
|
|
1236
1234
|
...origin,
|
|
1237
1235
|
});
|
|
1238
1236
|
onSessionCreated?.(cwd);
|
|
@@ -1273,6 +1271,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
1273
1271
|
runner: "opencode-cli-run",
|
|
1274
1272
|
worktreeEnabled: body.worktreeEnabled === true,
|
|
1275
1273
|
claudeSessionId: sessionId,
|
|
1274
|
+
workspaceId: resolveWorkspaceIdForNewSession(storage, cwd),
|
|
1276
1275
|
...parseSessionCreationOrigin(body),
|
|
1277
1276
|
});
|
|
1278
1277
|
onSessionCreated?.(cwd);
|
|
@@ -1307,6 +1306,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
1307
1306
|
runner: "qoder-cli-print",
|
|
1308
1307
|
worktreeEnabled: body.worktreeEnabled === true,
|
|
1309
1308
|
claudeSessionId: sessionId,
|
|
1309
|
+
workspaceId: resolveWorkspaceIdForNewSession(storage, cwd),
|
|
1310
1310
|
...parseSessionCreationOrigin(body),
|
|
1311
1311
|
});
|
|
1312
1312
|
onSessionCreated?.(cwd);
|
|
@@ -1422,19 +1422,11 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
1422
1422
|
}
|
|
1423
1423
|
});
|
|
1424
1424
|
}
|
|
1425
|
-
export function registerClaudeHistoryRoutes(app, processes, _structured, storage,
|
|
1425
|
+
export function registerClaudeHistoryRoutes(app, processes, _structured, storage, _sessionRegistry) {
|
|
1426
|
+
// Kept as an empty compatibility endpoint for older native clients. Wand no
|
|
1427
|
+
// longer imports provider-native history into its session list.
|
|
1426
1428
|
app.get("/api/claude-history", (_req, res) => {
|
|
1427
|
-
|
|
1428
|
-
const history = markManagedProviderHistory(processes.listClaudeHistorySessions(), sessionRegistry.listSlim(), "claude");
|
|
1429
|
-
const hidden = getHiddenClaudeSessionIds(storage);
|
|
1430
|
-
const filtered = hidden.size > 0
|
|
1431
|
-
? history.filter((s) => !s.claudeSessionId || !hidden.has(s.claudeSessionId))
|
|
1432
|
-
: history;
|
|
1433
|
-
res.json(filtered);
|
|
1434
|
-
}
|
|
1435
|
-
catch (error) {
|
|
1436
|
-
res.status(500).json({ error: getErrorMessage(error, "无法扫描 Claude 历史会话。") });
|
|
1437
|
-
}
|
|
1429
|
+
res.json([]);
|
|
1438
1430
|
});
|
|
1439
1431
|
app.delete("/api/claude-history/:claudeSessionId", (req, res) => {
|
|
1440
1432
|
const claudeSessionId = req.params.claudeSessionId?.trim();
|
|
@@ -1529,17 +1521,7 @@ export function registerClaudeHistoryRoutes(app, processes, _structured, storage
|
|
|
1529
1521
|
// 字段),用户发第一条消息时 buildCodexArgs 自动拼 `codex exec ... resume <thread_id>`。
|
|
1530
1522
|
// hidden 集合与 claude 共用(id 全局唯一,不会冲突)。
|
|
1531
1523
|
app.get("/api/codex-history", (_req, res) => {
|
|
1532
|
-
|
|
1533
|
-
const history = markManagedProviderHistory(processes.listCodexHistorySessions(), sessionRegistry.listSlim(), "codex");
|
|
1534
|
-
const hidden = getHiddenClaudeSessionIds(storage);
|
|
1535
|
-
const filtered = hidden.size > 0
|
|
1536
|
-
? history.filter((s) => !hidden.has(s.claudeSessionId))
|
|
1537
|
-
: history;
|
|
1538
|
-
res.json(filtered);
|
|
1539
|
-
}
|
|
1540
|
-
catch (error) {
|
|
1541
|
-
res.status(500).json({ error: getErrorMessage(error, "无法扫描 Codex 历史会话。") });
|
|
1542
|
-
}
|
|
1524
|
+
res.json([]);
|
|
1543
1525
|
});
|
|
1544
1526
|
app.delete("/api/codex-history/:threadId", (req, res) => {
|
|
1545
1527
|
const threadId = req.params.threadId?.trim();
|
|
@@ -1615,14 +1597,7 @@ export function registerClaudeHistoryRoutes(app, processes, _structured, storage
|
|
|
1615
1597
|
];
|
|
1616
1598
|
for (const config of externalHistoryProviders) {
|
|
1617
1599
|
app.get(`/api/${config.provider}-history`, (_req, res) => {
|
|
1618
|
-
|
|
1619
|
-
const history = markManagedProviderHistory(config.list(), sessionRegistry.listSlim(), config.provider);
|
|
1620
|
-
const hidden = getHiddenClaudeSessionIds(storage);
|
|
1621
|
-
res.json(hidden.size > 0 ? history.filter((session) => !hidden.has(session.claudeSessionId)) : history);
|
|
1622
|
-
}
|
|
1623
|
-
catch (error) {
|
|
1624
|
-
res.status(500).json({ error: getErrorMessage(error, `无法扫描 ${config.label} 历史会话。`) });
|
|
1625
|
-
}
|
|
1600
|
+
res.json([]);
|
|
1626
1601
|
});
|
|
1627
1602
|
app.delete(`/api/${config.provider}-history/:sessionId`, (req, res) => {
|
|
1628
1603
|
const sessionId = req.params.sessionId?.trim();
|