@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.
Files changed (39) hide show
  1. package/dist/claude-pty-bridge.d.ts +8 -0
  2. package/dist/claude-pty-bridge.js +34 -11
  3. package/dist/cli.js +72 -5
  4. package/dist/ensure-node-pty-helper.d.ts +1 -0
  5. package/dist/ensure-node-pty-helper.js +51 -0
  6. package/dist/git-quick-commit.d.ts +18 -0
  7. package/dist/git-quick-commit.js +381 -0
  8. package/dist/models.d.ts +3 -1
  9. package/dist/models.js +45 -7
  10. package/dist/process-manager.d.ts +6 -8
  11. package/dist/process-manager.js +90 -176
  12. package/dist/prompt-optimizer.d.ts +5 -0
  13. package/dist/prompt-optimizer.js +72 -0
  14. package/dist/pty-text-utils.d.ts +25 -1
  15. package/dist/pty-text-utils.js +158 -2
  16. package/dist/server-session-routes.d.ts +2 -2
  17. package/dist/server-session-routes.js +94 -8
  18. package/dist/server.d.ts +22 -1
  19. package/dist/server.js +138 -16
  20. package/dist/session-logger.d.ts +15 -4
  21. package/dist/session-logger.js +52 -4
  22. package/dist/structured-session-manager.d.ts +12 -2
  23. package/dist/structured-session-manager.js +465 -22
  24. package/dist/tui/index.d.ts +24 -0
  25. package/dist/tui/index.js +138 -0
  26. package/dist/tui/layout.d.ts +25 -0
  27. package/dist/tui/layout.js +198 -0
  28. package/dist/tui/log-bus.d.ts +23 -0
  29. package/dist/tui/log-bus.js +111 -0
  30. package/dist/tui/relative-time.d.ts +4 -0
  31. package/dist/tui/relative-time.js +27 -0
  32. package/dist/tui/session-formatter.d.ts +17 -0
  33. package/dist/tui/session-formatter.js +111 -0
  34. package/dist/types.d.ts +55 -2
  35. package/dist/web-ui/content/scripts.js +1371 -261
  36. package/dist/web-ui/content/styles.css +436 -9
  37. package/dist/web-ui/content/vendor/wterm/wterm.bundle.js +1 -1
  38. package/dist/ws-broadcast.js +74 -12
  39. 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" ? "stopped" : snapshot.status;
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-cli-print",
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-cli-print",
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: "claude",
182
- runner: options.runner ?? "claude-cli-print",
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: "running",
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: "claude",
200
- runner: options.runner ?? "claude-cli-print",
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: "stopped",
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 ?? { provider: "claude", runner: "claude-cli-print", lastError: null, inFlight: false, activeRequestId: null }),
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
- await this.runClaudeStreaming(id, updated, claudePrompt);
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 ?? { provider: "claude", runner: "claude-cli-print", inFlight: false, activeRequestId: null, lastError: null }),
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 ?? { provider: "claude", runner: "claude-cli-print", lastError: null, inFlight: false, activeRequestId: null }),
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
- lineBuf += chunk.toString();
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
- stderr += chunk.toString();
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" : "stopped",
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;