@co0ontty/wand 1.18.12 → 1.21.4
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/claude-pty-bridge.d.ts +8 -0
- package/dist/claude-pty-bridge.js +34 -11
- package/dist/cli.js +72 -5
- package/dist/ensure-node-pty-helper.d.ts +1 -0
- package/dist/ensure-node-pty-helper.js +51 -0
- package/dist/git-quick-commit.d.ts +18 -0
- package/dist/git-quick-commit.js +381 -0
- package/dist/models.d.ts +3 -1
- package/dist/models.js +45 -7
- package/dist/process-manager.d.ts +6 -8
- package/dist/process-manager.js +90 -176
- package/dist/prompt-optimizer.d.ts +5 -0
- package/dist/prompt-optimizer.js +72 -0
- package/dist/pty-text-utils.d.ts +25 -1
- package/dist/pty-text-utils.js +158 -2
- package/dist/server-session-routes.d.ts +2 -2
- package/dist/server-session-routes.js +94 -8
- package/dist/server.d.ts +22 -1
- package/dist/server.js +138 -16
- package/dist/session-logger.d.ts +15 -4
- package/dist/session-logger.js +52 -4
- package/dist/structured-session-manager.d.ts +12 -2
- package/dist/structured-session-manager.js +465 -22
- package/dist/tui/index.d.ts +24 -0
- package/dist/tui/index.js +138 -0
- package/dist/tui/layout.d.ts +25 -0
- package/dist/tui/layout.js +198 -0
- package/dist/tui/log-bus.d.ts +23 -0
- package/dist/tui/log-bus.js +111 -0
- package/dist/tui/relative-time.d.ts +4 -0
- package/dist/tui/relative-time.js +27 -0
- package/dist/tui/session-formatter.d.ts +17 -0
- package/dist/tui/session-formatter.js +111 -0
- package/dist/types.d.ts +55 -2
- package/dist/web-ui/content/scripts.js +1371 -261
- package/dist/web-ui/content/styles.css +436 -9
- package/dist/web-ui/content/vendor/wterm/wterm.bundle.js +1 -1
- package/dist/ws-broadcast.js +74 -12
- package/package.json +3 -1
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import { prepareSessionWorktree } from "./git-worktree.js";
|
|
4
|
+
function defaultStructuredRunner(provider) {
|
|
5
|
+
return provider === "codex" ? "codex-cli-exec" : "claude-cli-print";
|
|
6
|
+
}
|
|
7
|
+
function defaultStructuredState(provider, runner = defaultStructuredRunner(provider)) {
|
|
8
|
+
return {
|
|
9
|
+
provider,
|
|
10
|
+
runner,
|
|
11
|
+
lastError: null,
|
|
12
|
+
inFlight: false,
|
|
13
|
+
activeRequestId: null,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
4
16
|
const STREAM_EMIT_DEBOUNCE_MS = 16;
|
|
5
17
|
const ARCHIVE_AFTER_MS = 1000 * 60 * 60 * 24;
|
|
6
18
|
function isRunningAsRoot() {
|
|
@@ -84,23 +96,25 @@ function buildIncrementalStructuredPayload(snapshot) {
|
|
|
84
96
|
export class StructuredSessionManager {
|
|
85
97
|
storage;
|
|
86
98
|
config;
|
|
99
|
+
logger;
|
|
87
100
|
sessions = new Map();
|
|
88
101
|
pendingChildren = new Map();
|
|
89
102
|
interruptedWith = new Map();
|
|
90
103
|
emitEvent = null;
|
|
91
104
|
archiveTimer = null;
|
|
92
|
-
constructor(storage, config) {
|
|
105
|
+
constructor(storage, config, logger = null) {
|
|
93
106
|
this.storage = storage;
|
|
94
107
|
this.config = config;
|
|
108
|
+
this.logger = logger;
|
|
95
109
|
for (const snapshot of this.storage.loadSessions()) {
|
|
96
110
|
if ((snapshot.sessionKind ?? "pty") !== "structured")
|
|
97
111
|
continue;
|
|
98
|
-
const restoredStatus = snapshot.status === "running" ? "
|
|
112
|
+
const restoredStatus = snapshot.status === "running" ? "idle" : snapshot.status;
|
|
99
113
|
const restored = {
|
|
100
114
|
...snapshot,
|
|
101
115
|
sessionKind: "structured",
|
|
102
|
-
provider: snapshot.provider ?? "claude",
|
|
103
|
-
runner: snapshot.runner ?? "claude
|
|
116
|
+
provider: snapshot.provider ?? snapshot.structuredState?.provider ?? "claude",
|
|
117
|
+
runner: snapshot.runner ?? snapshot.structuredState?.runner ?? defaultStructuredRunner(snapshot.provider ?? snapshot.structuredState?.provider ?? "claude"),
|
|
104
118
|
status: restoredStatus,
|
|
105
119
|
autoApprovePermissions: snapshot.autoApprovePermissions ?? shouldAutoApproveForMode(snapshot.mode),
|
|
106
120
|
approvalStats: snapshot.approvalStats ?? { tool: 0, command: 0, file: 0, total: 0 },
|
|
@@ -109,7 +123,7 @@ export class StructuredSessionManager {
|
|
|
109
123
|
permissionBlocked: false,
|
|
110
124
|
structuredState: {
|
|
111
125
|
provider: snapshot.structuredState?.provider ?? snapshot.provider ?? "claude",
|
|
112
|
-
runner: snapshot.runner ?? "claude
|
|
126
|
+
runner: snapshot.runner ?? snapshot.structuredState?.runner ?? defaultStructuredRunner(snapshot.structuredState?.provider ?? snapshot.provider ?? "claude"),
|
|
113
127
|
model: snapshot.structuredState?.model ?? snapshot.selectedModel ?? undefined,
|
|
114
128
|
lastError: snapshot.structuredState?.lastError ?? null,
|
|
115
129
|
inFlight: false,
|
|
@@ -171,6 +185,8 @@ export class StructuredSessionManager {
|
|
|
171
185
|
const id = randomUUID();
|
|
172
186
|
const startedAt = new Date().toISOString();
|
|
173
187
|
const prompt = options.prompt?.trim();
|
|
188
|
+
const provider = options.provider === "codex" ? "codex" : "claude";
|
|
189
|
+
const runner = options.runner ?? defaultStructuredRunner(provider);
|
|
174
190
|
const worktreeSetup = options.worktreeEnabled
|
|
175
191
|
? prepareSessionWorktree({ cwd: options.cwd, sessionId: id })
|
|
176
192
|
: null;
|
|
@@ -178,14 +194,14 @@ export class StructuredSessionManager {
|
|
|
178
194
|
const snapshot = {
|
|
179
195
|
id,
|
|
180
196
|
sessionKind: "structured",
|
|
181
|
-
provider
|
|
182
|
-
runner
|
|
183
|
-
command: "claude -p --output-format stream-json",
|
|
197
|
+
provider,
|
|
198
|
+
runner,
|
|
199
|
+
command: provider === "codex" ? "codex exec --json" : "claude -p --output-format stream-json",
|
|
184
200
|
cwd: worktreeSetup?.cwd ?? options.cwd,
|
|
185
201
|
mode: options.mode,
|
|
186
202
|
worktreeEnabled: Boolean(worktreeSetup),
|
|
187
203
|
worktree: worktreeSetup?.worktree ?? null,
|
|
188
|
-
status: "
|
|
204
|
+
status: "idle",
|
|
189
205
|
exitCode: null,
|
|
190
206
|
startedAt,
|
|
191
207
|
endedAt: null,
|
|
@@ -196,8 +212,8 @@ export class StructuredSessionManager {
|
|
|
196
212
|
messages: [],
|
|
197
213
|
queuedMessages: [],
|
|
198
214
|
structuredState: {
|
|
199
|
-
provider
|
|
200
|
-
runner
|
|
215
|
+
provider,
|
|
216
|
+
runner,
|
|
201
217
|
model: selectedModel ?? undefined,
|
|
202
218
|
inFlight: false,
|
|
203
219
|
activeRequestId: null,
|
|
@@ -211,9 +227,6 @@ export class StructuredSessionManager {
|
|
|
211
227
|
this.sessions.set(id, snapshot);
|
|
212
228
|
this.storage.saveSession(snapshot);
|
|
213
229
|
this.emit({ type: "started", sessionId: id, data: { sessionKind: "structured" } });
|
|
214
|
-
if (prompt) {
|
|
215
|
-
void this.sendMessage(id, prompt);
|
|
216
|
-
}
|
|
217
230
|
return snapshot;
|
|
218
231
|
}
|
|
219
232
|
async sendMessage(id, input, opts) {
|
|
@@ -230,7 +243,7 @@ export class StructuredSessionManager {
|
|
|
230
243
|
this.pendingChildren.delete(id);
|
|
231
244
|
const recovered = {
|
|
232
245
|
...session,
|
|
233
|
-
status: "
|
|
246
|
+
status: "idle",
|
|
234
247
|
endedAt: session.endedAt ?? new Date().toISOString(),
|
|
235
248
|
structuredState: {
|
|
236
249
|
...session.structuredState,
|
|
@@ -293,7 +306,7 @@ export class StructuredSessionManager {
|
|
|
293
306
|
endedAt: null,
|
|
294
307
|
messages: [...(session.messages ?? []), userTurn],
|
|
295
308
|
structuredState: {
|
|
296
|
-
...(session.structuredState ??
|
|
309
|
+
...(session.structuredState ?? defaultStructuredState(session.provider ?? "claude", session.runner)),
|
|
297
310
|
inFlight: true,
|
|
298
311
|
activeRequestId: requestId,
|
|
299
312
|
lastError: null,
|
|
@@ -313,7 +326,12 @@ export class StructuredSessionManager {
|
|
|
313
326
|
? `[对刚才 AskUserQuestion 工具的回答 — 结构化模式不支持工具结果回传,下面是用户从选项中的选择]\n${prompt}`
|
|
314
327
|
: prompt;
|
|
315
328
|
try {
|
|
316
|
-
|
|
329
|
+
if ((updated.provider ?? "claude") === "codex") {
|
|
330
|
+
await this.runCodexStreaming(id, updated, prompt);
|
|
331
|
+
}
|
|
332
|
+
else {
|
|
333
|
+
await this.runClaudeStreaming(id, updated, claudePrompt);
|
|
334
|
+
}
|
|
317
335
|
const finished = this.requireSession(id);
|
|
318
336
|
return finished;
|
|
319
337
|
}
|
|
@@ -364,7 +382,7 @@ export class StructuredSessionManager {
|
|
|
364
382
|
...session,
|
|
365
383
|
selectedModel: normalized,
|
|
366
384
|
structuredState: {
|
|
367
|
-
...(session.structuredState ??
|
|
385
|
+
...(session.structuredState ?? defaultStructuredState(session.provider ?? "claude", session.runner)),
|
|
368
386
|
model: normalized ?? undefined,
|
|
369
387
|
},
|
|
370
388
|
};
|
|
@@ -428,7 +446,7 @@ export class StructuredSessionManager {
|
|
|
428
446
|
pendingEscalation: null,
|
|
429
447
|
permissionBlocked: false,
|
|
430
448
|
structuredState: {
|
|
431
|
-
...(session.structuredState ??
|
|
449
|
+
...(session.structuredState ?? defaultStructuredState(session.provider ?? "claude", session.runner)),
|
|
432
450
|
inFlight: false,
|
|
433
451
|
activeRequestId: null,
|
|
434
452
|
},
|
|
@@ -446,6 +464,7 @@ export class StructuredSessionManager {
|
|
|
446
464
|
}
|
|
447
465
|
this.sessions.delete(id);
|
|
448
466
|
this.storage.deleteSession(id);
|
|
467
|
+
this.logger?.deleteSession(id);
|
|
449
468
|
}
|
|
450
469
|
// ---------------------------------------------------------------------------
|
|
451
470
|
// Private helpers
|
|
@@ -580,6 +599,285 @@ export class StructuredSessionManager {
|
|
|
580
599
|
}
|
|
581
600
|
return [];
|
|
582
601
|
}
|
|
602
|
+
buildCodexArgs(session) {
|
|
603
|
+
const args = ["exec", "--json", "--color", "never"];
|
|
604
|
+
const shouldBypass = session.autoApprovePermissions === true || session.mode === "full-access" || session.mode === "managed";
|
|
605
|
+
if (shouldBypass) {
|
|
606
|
+
args.push("--dangerously-bypass-approvals-and-sandbox");
|
|
607
|
+
}
|
|
608
|
+
else if (session.mode === "auto-edit" || session.mode === "agent" || session.mode === "agent-max") {
|
|
609
|
+
args.push("--sandbox", "workspace-write");
|
|
610
|
+
}
|
|
611
|
+
else {
|
|
612
|
+
args.push("--sandbox", "read-only");
|
|
613
|
+
}
|
|
614
|
+
args.push("--skip-git-repo-check");
|
|
615
|
+
const modelChoice = session.selectedModel?.trim();
|
|
616
|
+
if (modelChoice && modelChoice !== "default") {
|
|
617
|
+
args.push("--model", modelChoice);
|
|
618
|
+
}
|
|
619
|
+
if (session.claudeSessionId) {
|
|
620
|
+
args.push("resume", session.claudeSessionId, "-");
|
|
621
|
+
}
|
|
622
|
+
else {
|
|
623
|
+
args.push("-");
|
|
624
|
+
}
|
|
625
|
+
return args;
|
|
626
|
+
}
|
|
627
|
+
// ---------------------------------------------------------------------------
|
|
628
|
+
// Streaming codex exec --json execution
|
|
629
|
+
// ---------------------------------------------------------------------------
|
|
630
|
+
runCodexStreaming(sessionId, session, prompt) {
|
|
631
|
+
return new Promise((resolve, reject) => {
|
|
632
|
+
const args = this.buildCodexArgs(session);
|
|
633
|
+
console.log("[WAND] runCodexStreaming sessionId:", sessionId, "mode:", session.mode, "threadId:", session.claudeSessionId);
|
|
634
|
+
const spawnedAt = new Date().toISOString();
|
|
635
|
+
const child = spawn("codex", args, {
|
|
636
|
+
cwd: session.cwd,
|
|
637
|
+
env: process.env,
|
|
638
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
639
|
+
});
|
|
640
|
+
console.log("[WAND] spawned codex exec pid:", child.pid, "args:", args.join(" "));
|
|
641
|
+
this.logger?.appendStructuredSpawn(sessionId, {
|
|
642
|
+
kind: "codex-exec",
|
|
643
|
+
provider: "codex",
|
|
644
|
+
pid: child.pid ?? null,
|
|
645
|
+
cwd: session.cwd,
|
|
646
|
+
args,
|
|
647
|
+
prompt: prompt.slice(0, 2048),
|
|
648
|
+
promptLength: prompt.length,
|
|
649
|
+
threadId: session.claudeSessionId,
|
|
650
|
+
spawnedAt,
|
|
651
|
+
});
|
|
652
|
+
this.pendingChildren.set(sessionId, child);
|
|
653
|
+
child.stdin?.end(prompt);
|
|
654
|
+
const turnState = {
|
|
655
|
+
blocks: [],
|
|
656
|
+
result: "",
|
|
657
|
+
sessionId: session.claudeSessionId,
|
|
658
|
+
model: session.selectedModel ?? session.structuredState?.model,
|
|
659
|
+
usage: undefined,
|
|
660
|
+
};
|
|
661
|
+
let lineBuf = "";
|
|
662
|
+
let stderr = "";
|
|
663
|
+
let emitTimer = null;
|
|
664
|
+
// codex 把所有错误(包括重试日志和最终失败原因)都通过 stdout 的 NDJSON 事件
|
|
665
|
+
// 输出,stderr 通常是空的。我们在 processLine 里收集这些,然后在 close 中
|
|
666
|
+
// 决定真正的报错文本。
|
|
667
|
+
const codexErrors = [];
|
|
668
|
+
let codexTurnFailed = null;
|
|
669
|
+
const flushEmit = () => {
|
|
670
|
+
if (emitTimer) {
|
|
671
|
+
clearTimeout(emitTimer);
|
|
672
|
+
emitTimer = null;
|
|
673
|
+
}
|
|
674
|
+
const current = this.sessions.get(sessionId);
|
|
675
|
+
if (!current)
|
|
676
|
+
return;
|
|
677
|
+
this.emit({ type: "output", sessionId, data: buildIncrementalStructuredPayload(current) });
|
|
678
|
+
};
|
|
679
|
+
const scheduleEmit = () => {
|
|
680
|
+
if (!emitTimer)
|
|
681
|
+
emitTimer = setTimeout(flushEmit, STREAM_EMIT_DEBOUNCE_MS);
|
|
682
|
+
};
|
|
683
|
+
const syncSnapshot = () => {
|
|
684
|
+
const current = this.sessions.get(sessionId);
|
|
685
|
+
if (!current)
|
|
686
|
+
return;
|
|
687
|
+
const inProgressTurn = {
|
|
688
|
+
role: "assistant",
|
|
689
|
+
content: this.compactContentBlocks([...turnState.blocks], turnState.result),
|
|
690
|
+
usage: turnState.usage,
|
|
691
|
+
};
|
|
692
|
+
const msgs = [...(current.messages ?? [])];
|
|
693
|
+
const lastMsg = msgs[msgs.length - 1];
|
|
694
|
+
if (lastMsg && lastMsg.role === "assistant") {
|
|
695
|
+
msgs[msgs.length - 1] = inProgressTurn;
|
|
696
|
+
}
|
|
697
|
+
else {
|
|
698
|
+
msgs.push(inProgressTurn);
|
|
699
|
+
}
|
|
700
|
+
const patched = {
|
|
701
|
+
...current,
|
|
702
|
+
claudeSessionId: turnState.sessionId ?? current.claudeSessionId,
|
|
703
|
+
messages: msgs,
|
|
704
|
+
output: turnState.result || current.output,
|
|
705
|
+
structuredState: {
|
|
706
|
+
...current.structuredState,
|
|
707
|
+
model: turnState.model ?? current.structuredState?.model,
|
|
708
|
+
},
|
|
709
|
+
};
|
|
710
|
+
this.sessions.set(sessionId, patched);
|
|
711
|
+
this.storage.saveSession(patched);
|
|
712
|
+
};
|
|
713
|
+
const processLine = (line) => {
|
|
714
|
+
const trimmed = line.trim();
|
|
715
|
+
if (!trimmed)
|
|
716
|
+
return;
|
|
717
|
+
let parsed;
|
|
718
|
+
try {
|
|
719
|
+
parsed = JSON.parse(trimmed);
|
|
720
|
+
}
|
|
721
|
+
catch {
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
this.logger?.appendStreamEvent(sessionId, parsed);
|
|
725
|
+
if (parsed?.type === "thread.started" && typeof parsed.thread_id === "string") {
|
|
726
|
+
turnState.sessionId = parsed.thread_id;
|
|
727
|
+
syncSnapshot();
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
if (parsed?.type === "item.started" && parsed.item) {
|
|
731
|
+
const block = this.extractCodexItemBlock(parsed.item, false);
|
|
732
|
+
if (block) {
|
|
733
|
+
turnState.blocks.push(block);
|
|
734
|
+
syncSnapshot();
|
|
735
|
+
scheduleEmit();
|
|
736
|
+
}
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
if (parsed?.type === "item.completed" && parsed.item) {
|
|
740
|
+
const block = this.extractCodexItemBlock(parsed.item, true);
|
|
741
|
+
if (block) {
|
|
742
|
+
if (block.type === "text")
|
|
743
|
+
turnState.result = block.text;
|
|
744
|
+
this.upsertCodexBlock(turnState.blocks, block);
|
|
745
|
+
syncSnapshot();
|
|
746
|
+
scheduleEmit();
|
|
747
|
+
}
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
if (parsed?.type === "turn.completed") {
|
|
751
|
+
turnState.usage = this.extractCodexUsage(parsed.usage) ?? turnState.usage;
|
|
752
|
+
syncSnapshot();
|
|
753
|
+
scheduleEmit();
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
if (parsed?.type === "error") {
|
|
757
|
+
const message = typeof parsed.message === "string" ? parsed.message : "";
|
|
758
|
+
if (message) {
|
|
759
|
+
console.log("[WAND] codex error event:", message.slice(0, 300));
|
|
760
|
+
codexErrors.push(message);
|
|
761
|
+
}
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
if (parsed?.type === "turn.failed") {
|
|
765
|
+
const errObj = (parsed.error && typeof parsed.error === "object") ? parsed.error : null;
|
|
766
|
+
const message = (errObj && typeof errObj.message === "string" && errObj.message)
|
|
767
|
+
|| (typeof parsed.message === "string" ? parsed.message : "")
|
|
768
|
+
|| "codex turn failed";
|
|
769
|
+
console.log("[WAND] codex turn.failed:", message.slice(0, 300));
|
|
770
|
+
codexTurnFailed = message;
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
};
|
|
774
|
+
child.stdout?.on("data", (chunk) => {
|
|
775
|
+
const text = chunk.toString();
|
|
776
|
+
this.logger?.appendStructuredStdout(sessionId, text);
|
|
777
|
+
lineBuf += text;
|
|
778
|
+
const lines = lineBuf.split("\n");
|
|
779
|
+
lineBuf = lines.pop() ?? "";
|
|
780
|
+
for (const line of lines)
|
|
781
|
+
processLine(line);
|
|
782
|
+
});
|
|
783
|
+
child.stderr?.on("data", (chunk) => {
|
|
784
|
+
const text = chunk.toString();
|
|
785
|
+
this.logger?.appendStructuredStderr(sessionId, text);
|
|
786
|
+
stderr += text;
|
|
787
|
+
});
|
|
788
|
+
child.on("error", (error) => {
|
|
789
|
+
console.log("[WAND] codex exec child error:", error.message);
|
|
790
|
+
this.pendingChildren.delete(sessionId);
|
|
791
|
+
if (emitTimer)
|
|
792
|
+
clearTimeout(emitTimer);
|
|
793
|
+
this.logger?.appendStructuredSpawn(sessionId, {
|
|
794
|
+
kind: "codex-exec-error",
|
|
795
|
+
pid: child.pid ?? null,
|
|
796
|
+
spawnedAt,
|
|
797
|
+
closedAt: new Date().toISOString(),
|
|
798
|
+
spawnError: error.message,
|
|
799
|
+
});
|
|
800
|
+
reject(error);
|
|
801
|
+
});
|
|
802
|
+
child.on("close", (code) => {
|
|
803
|
+
console.log("[WAND] codex exec child close code:", code, "stderr:", stderr.substring(0, 200), "errors:", codexErrors.length, "turnFailed:", codexTurnFailed?.slice(0, 100));
|
|
804
|
+
this.pendingChildren.delete(sessionId);
|
|
805
|
+
if (lineBuf.trim()) {
|
|
806
|
+
processLine(lineBuf);
|
|
807
|
+
lineBuf = "";
|
|
808
|
+
}
|
|
809
|
+
flushEmit();
|
|
810
|
+
const closedAt = new Date().toISOString();
|
|
811
|
+
this.logger?.appendStructuredSpawn(sessionId, {
|
|
812
|
+
kind: "codex-exec-close",
|
|
813
|
+
pid: child.pid ?? null,
|
|
814
|
+
spawnedAt,
|
|
815
|
+
closedAt,
|
|
816
|
+
exitCode: code,
|
|
817
|
+
stderrTail: stderr.slice(-2048),
|
|
818
|
+
codexErrors,
|
|
819
|
+
codexTurnFailed,
|
|
820
|
+
});
|
|
821
|
+
const current = this.sessions.get(sessionId);
|
|
822
|
+
if (!current) {
|
|
823
|
+
reject(new Error("Session removed during execution."));
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
// codex 把模型/网络/沙箱等错误写到 stdout 的 NDJSON 流(type: error / turn.failed),
|
|
827
|
+
// 而不是 stderr。我们以 turn.failed 的 message 为准,其次是最后一个 error 事件。
|
|
828
|
+
const codexFailed = codexTurnFailed !== null;
|
|
829
|
+
if (codexFailed || (code !== 0 && code !== null)) {
|
|
830
|
+
const errorText = (codexTurnFailed && codexTurnFailed.trim())
|
|
831
|
+
|| (codexErrors.length > 0 ? codexErrors[codexErrors.length - 1] : "")
|
|
832
|
+
|| stderr.trim()
|
|
833
|
+
|| `codex exec exited with code ${code}`;
|
|
834
|
+
const exitForSnapshot = typeof code === "number" ? code : 1;
|
|
835
|
+
const failed = this.finishStructuredFailure(current, exitForSnapshot, errorText, turnState);
|
|
836
|
+
this.sessions.set(sessionId, failed);
|
|
837
|
+
this.storage.saveSession(failed);
|
|
838
|
+
this.emitStructuredSnapshot(failed);
|
|
839
|
+
this.emitStructuredSnapshot(failed, "ended");
|
|
840
|
+
reject(new Error(errorText));
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
const assistantTurn = {
|
|
844
|
+
role: "assistant",
|
|
845
|
+
content: this.compactContentBlocks([...turnState.blocks], turnState.result),
|
|
846
|
+
usage: turnState.usage,
|
|
847
|
+
};
|
|
848
|
+
const msgs = [...(current.messages ?? [])];
|
|
849
|
+
const lastMsg = msgs[msgs.length - 1];
|
|
850
|
+
if (lastMsg && lastMsg.role === "assistant")
|
|
851
|
+
msgs[msgs.length - 1] = assistantTurn;
|
|
852
|
+
else
|
|
853
|
+
msgs.push(assistantTurn);
|
|
854
|
+
const finished = {
|
|
855
|
+
...current,
|
|
856
|
+
status: "idle",
|
|
857
|
+
exitCode: 0,
|
|
858
|
+
endedAt: new Date().toISOString(),
|
|
859
|
+
output: turnState.result,
|
|
860
|
+
claudeSessionId: turnState.sessionId ?? current.claudeSessionId,
|
|
861
|
+
messages: msgs,
|
|
862
|
+
pendingEscalation: null,
|
|
863
|
+
permissionBlocked: false,
|
|
864
|
+
structuredState: {
|
|
865
|
+
...current.structuredState,
|
|
866
|
+
model: turnState.model ?? current.structuredState?.model,
|
|
867
|
+
inFlight: false,
|
|
868
|
+
activeRequestId: null,
|
|
869
|
+
lastError: null,
|
|
870
|
+
},
|
|
871
|
+
};
|
|
872
|
+
this.sessions.set(sessionId, finished);
|
|
873
|
+
this.storage.saveSession(finished);
|
|
874
|
+
this.emitStructuredSnapshot(finished);
|
|
875
|
+
this.emitStructuredSnapshot(finished, "ended");
|
|
876
|
+
resolve();
|
|
877
|
+
setImmediate(() => { void this.flushNextQueuedMessage(sessionId); });
|
|
878
|
+
});
|
|
879
|
+
});
|
|
880
|
+
}
|
|
583
881
|
// ---------------------------------------------------------------------------
|
|
584
882
|
// Streaming claude -p execution
|
|
585
883
|
// ---------------------------------------------------------------------------
|
|
@@ -633,12 +931,24 @@ export class StructuredSessionManager {
|
|
|
633
931
|
// variadic 参数贪婪吞掉(commander 的 <tools...> 会一直吃 positional 直到
|
|
634
932
|
// 下一个 flag)。表现为 claude 报 "Input must be provided either through
|
|
635
933
|
// stdin or as a prompt argument when using --print"。
|
|
934
|
+
const spawnedAt = new Date().toISOString();
|
|
636
935
|
const child = spawn("claude", args, {
|
|
637
936
|
cwd: session.cwd,
|
|
638
937
|
env: process.env,
|
|
639
938
|
stdio: ["pipe", "pipe", "pipe"],
|
|
640
939
|
});
|
|
641
940
|
console.log("[WAND] spawned claude -p pid:", child.pid, "args:", args.join(" "));
|
|
941
|
+
this.logger?.appendStructuredSpawn(sessionId, {
|
|
942
|
+
kind: "claude-print",
|
|
943
|
+
provider: "claude",
|
|
944
|
+
pid: child.pid ?? null,
|
|
945
|
+
cwd: session.cwd,
|
|
946
|
+
args,
|
|
947
|
+
prompt: prompt.slice(0, 2048),
|
|
948
|
+
promptLength: prompt.length,
|
|
949
|
+
claudeSessionId: session.claudeSessionId,
|
|
950
|
+
spawnedAt,
|
|
951
|
+
});
|
|
642
952
|
this.pendingChildren.set(sessionId, child);
|
|
643
953
|
child.stdin?.end(prompt);
|
|
644
954
|
const turnState = {
|
|
@@ -720,6 +1030,7 @@ export class StructuredSessionManager {
|
|
|
720
1030
|
catch {
|
|
721
1031
|
return;
|
|
722
1032
|
}
|
|
1033
|
+
this.logger?.appendStreamEvent(sessionId, parsed);
|
|
723
1034
|
if (parsed && parsed.type === "assistant" && parsed.message) {
|
|
724
1035
|
const extracted = this.extractAssistantMessage(parsed.message);
|
|
725
1036
|
if (extracted.content.length > 0) {
|
|
@@ -776,7 +1087,9 @@ export class StructuredSessionManager {
|
|
|
776
1087
|
};
|
|
777
1088
|
let stderr = "";
|
|
778
1089
|
child.stdout?.on("data", (chunk) => {
|
|
779
|
-
|
|
1090
|
+
const text = chunk.toString();
|
|
1091
|
+
this.logger?.appendStructuredStdout(sessionId, text);
|
|
1092
|
+
lineBuf += text;
|
|
780
1093
|
const lines = lineBuf.split("\n");
|
|
781
1094
|
// Keep the last (possibly incomplete) segment in the buffer.
|
|
782
1095
|
lineBuf = lines.pop() ?? "";
|
|
@@ -785,18 +1098,35 @@ export class StructuredSessionManager {
|
|
|
785
1098
|
}
|
|
786
1099
|
});
|
|
787
1100
|
child.stderr?.on("data", (chunk) => {
|
|
788
|
-
|
|
1101
|
+
const text = chunk.toString();
|
|
1102
|
+
this.logger?.appendStructuredStderr(sessionId, text);
|
|
1103
|
+
stderr += text;
|
|
789
1104
|
});
|
|
790
1105
|
child.on("error", (error) => {
|
|
791
1106
|
console.log("[WAND] claude -p child error:", error.message);
|
|
792
1107
|
this.pendingChildren.delete(sessionId);
|
|
793
1108
|
if (emitTimer)
|
|
794
1109
|
clearTimeout(emitTimer);
|
|
1110
|
+
this.logger?.appendStructuredSpawn(sessionId, {
|
|
1111
|
+
kind: "claude-print-error",
|
|
1112
|
+
pid: child.pid ?? null,
|
|
1113
|
+
spawnedAt,
|
|
1114
|
+
closedAt: new Date().toISOString(),
|
|
1115
|
+
spawnError: error.message,
|
|
1116
|
+
});
|
|
795
1117
|
reject(error);
|
|
796
1118
|
});
|
|
797
1119
|
child.on("close", (code) => {
|
|
798
1120
|
console.log("[WAND] claude -p child close code:", code, "stderr:", stderr.substring(0, 200));
|
|
799
1121
|
this.pendingChildren.delete(sessionId);
|
|
1122
|
+
this.logger?.appendStructuredSpawn(sessionId, {
|
|
1123
|
+
kind: "claude-print-close",
|
|
1124
|
+
pid: child.pid ?? null,
|
|
1125
|
+
spawnedAt,
|
|
1126
|
+
closedAt: new Date().toISOString(),
|
|
1127
|
+
exitCode: code,
|
|
1128
|
+
stderrTail: stderr.slice(-2048),
|
|
1129
|
+
});
|
|
800
1130
|
// Process any remaining data in the line buffer.
|
|
801
1131
|
if (lineBuf.trim()) {
|
|
802
1132
|
processLine(lineBuf);
|
|
@@ -871,7 +1201,7 @@ export class StructuredSessionManager {
|
|
|
871
1201
|
const keepRunning = killedForAskUserQuestion || !!interruptPrompt;
|
|
872
1202
|
const finished = {
|
|
873
1203
|
...current,
|
|
874
|
-
status: keepRunning ? "running" : "
|
|
1204
|
+
status: keepRunning ? "running" : "idle",
|
|
875
1205
|
exitCode: keepRunning ? null : 0,
|
|
876
1206
|
endedAt: keepRunning ? null : new Date().toISOString(),
|
|
877
1207
|
output: turnState.result,
|
|
@@ -1002,6 +1332,106 @@ export class StructuredSessionManager {
|
|
|
1002
1332
|
}
|
|
1003
1333
|
return typeof content === "undefined" || content === null ? "" : String(content);
|
|
1004
1334
|
}
|
|
1335
|
+
extractCodexText(value) {
|
|
1336
|
+
if (typeof value === "string")
|
|
1337
|
+
return value;
|
|
1338
|
+
if (!value || typeof value !== "object")
|
|
1339
|
+
return "";
|
|
1340
|
+
if (Array.isArray(value)) {
|
|
1341
|
+
return value.map((item) => this.extractCodexText(item)).filter(Boolean).join("");
|
|
1342
|
+
}
|
|
1343
|
+
const record = value;
|
|
1344
|
+
for (const key of ["text", "output_text", "message", "content", "summary"]) {
|
|
1345
|
+
const extracted = this.extractCodexText(record[key]);
|
|
1346
|
+
if (extracted)
|
|
1347
|
+
return extracted;
|
|
1348
|
+
}
|
|
1349
|
+
return "";
|
|
1350
|
+
}
|
|
1351
|
+
extractCodexItemBlock(item, completed) {
|
|
1352
|
+
const id = typeof item.id === "string" ? item.id : randomUUID();
|
|
1353
|
+
const type = typeof item.type === "string" ? item.type : "unknown";
|
|
1354
|
+
if (type === "agent_message") {
|
|
1355
|
+
const text = this.extractCodexText(item);
|
|
1356
|
+
return text ? { type: "text", text } : null;
|
|
1357
|
+
}
|
|
1358
|
+
if (type === "reasoning") {
|
|
1359
|
+
const text = this.extractCodexText(item);
|
|
1360
|
+
return text ? { type: "thinking", thinking: text } : null;
|
|
1361
|
+
}
|
|
1362
|
+
if (type === "command_execution") {
|
|
1363
|
+
const command = typeof item.command === "string" ? item.command : "";
|
|
1364
|
+
const aggregatedOutput = typeof item.aggregated_output === "string" ? item.aggregated_output : "";
|
|
1365
|
+
const exitCode = typeof item.exit_code === "number" ? item.exit_code : null;
|
|
1366
|
+
const status = typeof item.status === "string" ? item.status : completed ? "completed" : "in_progress";
|
|
1367
|
+
if (!completed) {
|
|
1368
|
+
return {
|
|
1369
|
+
type: "tool_use",
|
|
1370
|
+
id,
|
|
1371
|
+
name: "Bash",
|
|
1372
|
+
input: { command, status },
|
|
1373
|
+
};
|
|
1374
|
+
}
|
|
1375
|
+
return {
|
|
1376
|
+
type: "tool_result",
|
|
1377
|
+
tool_use_id: id,
|
|
1378
|
+
content: aggregatedOutput || (exitCode === null ? "" : `exit_code: ${exitCode}`),
|
|
1379
|
+
is_error: typeof exitCode === "number" && exitCode !== 0,
|
|
1380
|
+
};
|
|
1381
|
+
}
|
|
1382
|
+
if (completed) {
|
|
1383
|
+
const text = this.extractCodexText(item);
|
|
1384
|
+
if (text)
|
|
1385
|
+
return { type: "text", text };
|
|
1386
|
+
}
|
|
1387
|
+
return null;
|
|
1388
|
+
}
|
|
1389
|
+
upsertCodexBlock(blocks, block) {
|
|
1390
|
+
if (block.type === "tool_result") {
|
|
1391
|
+
const toolUseIndex = blocks.findIndex((existing) => existing.type === "tool_use" && existing.id === block.tool_use_id);
|
|
1392
|
+
if (toolUseIndex >= 0) {
|
|
1393
|
+
const nextIndex = toolUseIndex + 1;
|
|
1394
|
+
if (blocks[nextIndex]?.type === "tool_result" && blocks[nextIndex].tool_use_id === block.tool_use_id) {
|
|
1395
|
+
blocks[nextIndex] = block;
|
|
1396
|
+
}
|
|
1397
|
+
else {
|
|
1398
|
+
blocks.splice(nextIndex, 0, block);
|
|
1399
|
+
}
|
|
1400
|
+
return;
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
blocks.push(block);
|
|
1404
|
+
}
|
|
1405
|
+
finishStructuredFailure(current, code, errorText, turnState) {
|
|
1406
|
+
const failureTurn = {
|
|
1407
|
+
role: "assistant",
|
|
1408
|
+
content: [{ type: "text", text: `结构化会话执行失败:${errorText}` }],
|
|
1409
|
+
};
|
|
1410
|
+
const msgs = [...(current.messages ?? [])];
|
|
1411
|
+
const lastMsg = msgs[msgs.length - 1];
|
|
1412
|
+
if (lastMsg && lastMsg.role === "assistant")
|
|
1413
|
+
msgs[msgs.length - 1] = failureTurn;
|
|
1414
|
+
else
|
|
1415
|
+
msgs.push(failureTurn);
|
|
1416
|
+
return {
|
|
1417
|
+
...current,
|
|
1418
|
+
status: "failed",
|
|
1419
|
+
exitCode: code,
|
|
1420
|
+
endedAt: new Date().toISOString(),
|
|
1421
|
+
output: errorText,
|
|
1422
|
+
claudeSessionId: turnState.sessionId ?? current.claudeSessionId,
|
|
1423
|
+
messages: msgs,
|
|
1424
|
+
pendingEscalation: null,
|
|
1425
|
+
permissionBlocked: false,
|
|
1426
|
+
structuredState: {
|
|
1427
|
+
...current.structuredState,
|
|
1428
|
+
model: turnState.model ?? current.structuredState?.model,
|
|
1429
|
+
inFlight: false,
|
|
1430
|
+
activeRequestId: null,
|
|
1431
|
+
lastError: errorText,
|
|
1432
|
+
},
|
|
1433
|
+
};
|
|
1434
|
+
}
|
|
1005
1435
|
extractModelName(modelUsage) {
|
|
1006
1436
|
if (!modelUsage)
|
|
1007
1437
|
return undefined;
|
|
@@ -1029,4 +1459,17 @@ export class StructuredSessionManager {
|
|
|
1029
1459
|
}
|
|
1030
1460
|
return value;
|
|
1031
1461
|
}
|
|
1462
|
+
extractCodexUsage(source) {
|
|
1463
|
+
if (!source || typeof source !== "object")
|
|
1464
|
+
return undefined;
|
|
1465
|
+
const value = {
|
|
1466
|
+
inputTokens: typeof source.input_tokens === "number" ? source.input_tokens : undefined,
|
|
1467
|
+
outputTokens: typeof source.output_tokens === "number" ? source.output_tokens : undefined,
|
|
1468
|
+
cacheReadInputTokens: typeof source.cached_input_tokens === "number" ? source.cached_input_tokens : undefined,
|
|
1469
|
+
};
|
|
1470
|
+
if (value.inputTokens === undefined && value.outputTokens === undefined && value.cacheReadInputTokens === undefined) {
|
|
1471
|
+
return undefined;
|
|
1472
|
+
}
|
|
1473
|
+
return value;
|
|
1474
|
+
}
|
|
1032
1475
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ProcessManager } from "../process-manager.js";
|
|
2
|
+
import { StructuredSessionManager } from "../structured-session-manager.js";
|
|
3
|
+
export interface TuiDeps {
|
|
4
|
+
processManager: ProcessManager;
|
|
5
|
+
structuredSessions: StructuredSessionManager;
|
|
6
|
+
version: string;
|
|
7
|
+
configPath: string;
|
|
8
|
+
dbPath: string;
|
|
9
|
+
bindAddr: string;
|
|
10
|
+
httpsEnabled: boolean;
|
|
11
|
+
urls: Array<{
|
|
12
|
+
url: string;
|
|
13
|
+
scheme: "HTTP" | "HTTPS";
|
|
14
|
+
}>;
|
|
15
|
+
orphanRecoveredCount: number;
|
|
16
|
+
/** 退出 TUI 时调用。返回的 Promise resolve 后 cli 才会 process.exit。 */
|
|
17
|
+
onExit: (reason: ExitReason) => void | Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
export type ExitReason = "user" | "signal" | "error";
|
|
20
|
+
export interface TuiHandle {
|
|
21
|
+
isActive: boolean;
|
|
22
|
+
stop(reason: ExitReason): Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
export declare function startTui(deps: TuiDeps): TuiHandle;
|