@co0ontty/wand 1.20.4 → 1.21.5

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.
@@ -1,6 +1,12 @@
1
1
  /**
2
2
  * Shared PTY text processing utilities for consistent ANSI stripping and noise filtering.
3
3
  */
4
+ /**
5
+ * Hard cap on the in-memory PTY replay buffer. Shared between ProcessManager
6
+ * and ClaudePtyBridge so a session keeps the same amount of history regardless
7
+ * of which capture path is active.
8
+ */
9
+ export const PTY_OUTPUT_MAX_SIZE = 200_000;
4
10
  /** Strip ANSI escape sequences and control characters from raw PTY output. */
5
11
  export function stripAnsi(text) {
6
12
  return text
@@ -115,10 +121,166 @@ export function isNoiseLine(line) {
115
121
  return true;
116
122
  return false;
117
123
  }
118
- /** Append text to a windowed buffer, trimming from start if over max size. */
124
+ /**
125
+ * Append text to a windowed buffer, trimming from start if over max size.
126
+ *
127
+ * The cut point is chosen so it never lands inside:
128
+ * - a UTF-16 surrogate pair (would corrupt the leading codepoint)
129
+ * - an unterminated ANSI escape sequence (would feed orphan "[31m..."
130
+ * text to a downstream terminal renderer)
131
+ *
132
+ * The returned buffer may be slightly shorter than maxSize.
133
+ */
119
134
  export function appendWindow(buffer, chunk, maxSize) {
120
135
  const next = buffer + chunk;
121
- return next.length > maxSize ? next.slice(-maxSize) : next;
136
+ if (next.length <= maxSize)
137
+ return next;
138
+ return safeSliceTail(next, maxSize);
139
+ }
140
+ /** Slice keeping the last ~maxSize chars on a safe boundary. Exported for tests. */
141
+ export function safeSliceTail(text, maxSize) {
142
+ if (text.length <= maxSize)
143
+ return text;
144
+ let start = text.length - maxSize;
145
+ // 1. Skip UTF-16 low surrogate half so we don't strand a high surrogate.
146
+ if (start > 0 && start < text.length) {
147
+ const code = text.charCodeAt(start);
148
+ if (code >= 0xdc00 && code <= 0xdfff)
149
+ start++;
150
+ }
151
+ // 2. Prefer cutting at the next newline within a small lookahead window.
152
+ // Newlines are always safe boundaries (no ANSI sequence spans a newline
153
+ // in well-formed terminal output) and keep lines aligned for replay.
154
+ const LOOKAHEAD = 4096;
155
+ const upper = Math.min(start + LOOKAHEAD, text.length);
156
+ for (let i = start; i < upper; i++) {
157
+ if (text.charCodeAt(i) === 0x0a)
158
+ return text.slice(i + 1);
159
+ }
160
+ // 3. No nearby newline. Detect whether `start` lands inside an open ANSI
161
+ // escape sequence by scanning backward for an ESC (0x1b). If we find one
162
+ // that is not yet terminated, advance past the sequence's final byte.
163
+ const lookback = Math.max(0, start - 256);
164
+ let escAt = -1;
165
+ for (let i = start - 1; i >= lookback; i--) {
166
+ const code = text.charCodeAt(i);
167
+ if (code === 0x1b) {
168
+ escAt = i;
169
+ break;
170
+ }
171
+ // If we hit a terminator before an ESC, the previous sequence is closed.
172
+ if (code === 0x07)
173
+ break;
174
+ if (code >= 0x40 && code <= 0x7e && i > 0 && isLikelyAnsiBody(text, i - 1))
175
+ break;
176
+ }
177
+ if (escAt !== -1) {
178
+ // OSC 序列以 `ESC ]` (0x1b 0x5d) 开头,必须用 BEL (0x07) 或
179
+ // ST (`ESC \\` = 0x1b 0x5c) 终止。其它范围内字节(包括裸 `\`)
180
+ // 都属于 payload,不能当终止符。CSI 等序列才用 0x40-0x7e final byte。
181
+ const isOsc = escAt + 1 < text.length && text.charCodeAt(escAt + 1) === 0x5d;
182
+ let terminated = false;
183
+ for (let i = escAt + 1; i < start; i++) {
184
+ const code = text.charCodeAt(i);
185
+ if (code === 0x07) {
186
+ terminated = true;
187
+ break;
188
+ }
189
+ if (isOsc) {
190
+ if (code === 0x1b && i + 1 < start && text.charCodeAt(i + 1) === 0x5c) {
191
+ terminated = true;
192
+ break;
193
+ }
194
+ continue;
195
+ }
196
+ if (code >= 0x40 && code <= 0x7e) {
197
+ terminated = true;
198
+ break;
199
+ }
200
+ }
201
+ if (!terminated) {
202
+ const ansiUpper = Math.min(start + 256, text.length);
203
+ for (let i = start; i < ansiUpper; i++) {
204
+ const code = text.charCodeAt(i);
205
+ if (code === 0x07)
206
+ return text.slice(i + 1);
207
+ if (isOsc) {
208
+ if (code === 0x1b && i + 1 < ansiUpper && text.charCodeAt(i + 1) === 0x5c) {
209
+ return text.slice(i + 2);
210
+ }
211
+ continue;
212
+ }
213
+ if (code >= 0x40 && code <= 0x7e)
214
+ return text.slice(i + 1);
215
+ }
216
+ }
217
+ }
218
+ return text.slice(start);
219
+ }
220
+ function isLikelyAnsiBody(text, idx) {
221
+ // CSI parameter/intermediate range covers most common ANSI bodies.
222
+ const code = text.charCodeAt(idx);
223
+ return code === 0x5b /* [ */ || code === 0x3f /* ? */ || (code >= 0x30 && code <= 0x3f);
224
+ }
225
+ /**
226
+ * Strip a string down to the printable codepoints used for echo matching.
227
+ * Removes control characters, whitespace and ANSI escapes; keeps all other
228
+ * visible characters (including `/`, `()`, `:`, CJK, emoji, etc.) so that
229
+ * echo alignment works for any user input.
230
+ */
231
+ export function stripForEchoMatch(input) {
232
+ let out = "";
233
+ for (let i = 0; i < input.length; i++) {
234
+ const code = input.charCodeAt(i);
235
+ if (code === 0x1b) {
236
+ i = skipAnsiSequence(input, i) - 1;
237
+ continue;
238
+ }
239
+ if (code < 0x20 || code === 0x7f)
240
+ continue;
241
+ if (code === 0x20)
242
+ continue;
243
+ out += input[i];
244
+ }
245
+ return out;
246
+ }
247
+ /**
248
+ * Given an index pointing at ESC (0x1b), return the index of the first
249
+ * character AFTER the escape sequence. Handles CSI, OSC and simple ESC-
250
+ * letter forms. Returns idx+1 if nothing matches (best-effort skip).
251
+ */
252
+ export function skipAnsiSequence(text, idx) {
253
+ if (text.charCodeAt(idx) !== 0x1b)
254
+ return idx;
255
+ const next = text.charCodeAt(idx + 1);
256
+ if (Number.isNaN(next))
257
+ return idx + 1;
258
+ // CSI: ESC [ ... final-byte (0x40-0x7E)
259
+ if (next === 0x5b /* [ */) {
260
+ let i = idx + 2;
261
+ while (i < text.length) {
262
+ const code = text.charCodeAt(i);
263
+ if (code >= 0x40 && code <= 0x7e)
264
+ return i + 1;
265
+ i++;
266
+ }
267
+ return text.length;
268
+ }
269
+ // OSC: ESC ] ... terminator (BEL or ESC \)
270
+ if (next === 0x5d /* ] */) {
271
+ let i = idx + 2;
272
+ while (i < text.length) {
273
+ const code = text.charCodeAt(i);
274
+ if (code === 0x07)
275
+ return i + 1;
276
+ if (code === 0x1b && text.charCodeAt(i + 1) === 0x5c)
277
+ return i + 2;
278
+ i++;
279
+ }
280
+ return text.length;
281
+ }
282
+ // Two-character ESC sequences (ESC = / ESC > / ESC M / etc.)
283
+ return idx + 2;
122
284
  }
123
285
  const EXPLICIT_CONFIRM_PATTERNS = [
124
286
  /(?:^|\b)(?:press\s+)?(?:y|yes)\s*(?:\/|\bor\b)\s*(?:n|no)(?:\b|$)/i,
@@ -4,5 +4,5 @@ import { StructuredSessionManager } from "./structured-session-manager.js";
4
4
  import { WandStorage } from "./storage.js";
5
5
  import { ExecutionMode, WandConfig } from "./types.js";
6
6
  export declare function getErrorMessage(error: unknown, fallback: string): string;
7
- export declare function registerSessionRoutes(app: Express, processes: ProcessManager, structured: StructuredSessionManager, storage: WandStorage, defaultMode: ExecutionMode, config: WandConfig): void;
7
+ export declare function registerSessionRoutes(app: Express, processes: ProcessManager, structured: StructuredSessionManager, storage: WandStorage, defaultMode: ExecutionMode, config: WandConfig, onSessionCreated?: (cwd: string | undefined | null) => void): void;
8
8
  export declare function registerClaudeHistoryRoutes(app: Express, processes: ProcessManager, storage: WandStorage): void;
@@ -136,7 +136,7 @@ function canMergeSession(snapshot) {
136
136
  function isMergeActionAllowed(snapshot) {
137
137
  return snapshot.status !== "running";
138
138
  }
139
- export function registerSessionRoutes(app, processes, structured, storage, defaultMode, config) {
139
+ export function registerSessionRoutes(app, processes, structured, storage, defaultMode, config, onSessionCreated) {
140
140
  app.get("/api/sessions", (_req, res) => {
141
141
  const all = listAllSessionsSlim(processes, structured);
142
142
  console.log("[WAND] GET /api/sessions count:", all.length, "sessions:", all.map(s => ({ id: s.id.substring(0, 8), kind: s.sessionKind, runner: s.runner, status: s.status })));
@@ -146,19 +146,27 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
146
146
  const body = req.body;
147
147
  console.log("[WAND] POST /api/structured-sessions body:", JSON.stringify({ cwd: body.cwd, mode: body.mode, runner: body.runner, provider: body.provider, worktreeEnabled: body.worktreeEnabled === true, hasPrompt: !!body.prompt, model: body.model }));
148
148
  try {
149
- if (body.provider && body.provider !== "claude") {
150
- res.status(400).json({ error: "结构化会话当前仅支持 Claude provider。" });
149
+ if (body.provider && body.provider !== "claude" && body.provider !== "codex") {
150
+ res.status(400).json({ error: "结构化会话当前仅支持 Claude 或 Codex provider。" });
151
151
  return;
152
152
  }
153
+ const provider = body.provider === "codex" ? "codex" : "claude";
153
154
  const snapshot = structured.createSession({
154
155
  cwd: body.cwd?.trim() || process.cwd(),
155
156
  mode: normalizeMode(body.mode, defaultMode),
156
- prompt: body.prompt,
157
- runner: body.runner ?? "claude-cli-print",
157
+ provider,
158
+ runner: body.runner ?? (provider === "codex" ? "codex-cli-exec" : "claude-cli-print"),
158
159
  worktreeEnabled: body.worktreeEnabled === true,
159
160
  model: typeof body.model === "string" ? body.model.trim() : undefined,
160
161
  });
161
162
  console.log("[WAND] structured session created:", JSON.stringify({ id: snapshot.id, sessionKind: snapshot.sessionKind, runner: snapshot.runner, status: snapshot.status }));
163
+ onSessionCreated?.(body.cwd ?? snapshot.cwd);
164
+ const prompt = body.prompt?.trim();
165
+ if (prompt) {
166
+ const finished = await structured.sendMessage(snapshot.id, prompt);
167
+ res.status(201).json(finished);
168
+ return;
169
+ }
162
170
  res.status(201).json(snapshot);
163
171
  }
164
172
  catch (error) {
@@ -199,13 +207,19 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
199
207
  app.post("/api/structured-sessions/:id/messages", express.json(), async (req, res) => {
200
208
  const input = String(req.body?.input ?? "");
201
209
  const interrupt = !!req.body?.interrupt;
202
- console.log("[WAND] POST /api/structured-sessions/:id/messages id:", req.params.id, "input:", input.substring(0, 50), "interrupt:", interrupt);
210
+ const idempotencyKey = typeof req.body?.idempotencyKey === "string" ? req.body.idempotencyKey : undefined;
211
+ console.log("[WAND] POST /api/structured-sessions/:id/messages id:", req.params.id, "input:", input.substring(0, 50), "interrupt:", interrupt, "idempotencyKey:", idempotencyKey);
203
212
  try {
204
- const snapshot = await structured.sendMessage(req.params.id, input, { interrupt });
213
+ const snapshot = await structured.sendMessage(req.params.id, input, { interrupt, idempotencyKey });
205
214
  res.json(snapshot);
206
215
  }
207
216
  catch (error) {
208
- res.status(400).json({ error: getErrorMessage(error, "无法发送结构化消息。") });
217
+ const errorCode = error?.code;
218
+ const status = errorCode === "duplicate_idempotency_key" ? 409 : 400;
219
+ res.status(status).json({
220
+ error: getErrorMessage(error, "无法发送结构化消息。"),
221
+ errorCode,
222
+ });
209
223
  }
210
224
  });
211
225
  // ── Tool content lazy-load endpoint ──
@@ -472,7 +486,9 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
472
486
  }
473
487
  const newMode = body.mode ? normalizeMode(body.mode, defaultMode) : normalizeMode(existingSession.mode, defaultMode);
474
488
  const resumeCommand = `${command} --resume ${claudeSessionId}`;
475
- const newSnapshot = processes.start(resumeCommand, existingSession.cwd, newMode, undefined, { reuseId: sessionId });
489
+ const reqCols = typeof body.cols === "number" && Number.isFinite(body.cols) ? body.cols : undefined;
490
+ const reqRows = typeof body.rows === "number" && Number.isFinite(body.rows) ? body.rows : undefined;
491
+ const newSnapshot = processes.start(resumeCommand, existingSession.cwd, newMode, undefined, { reuseId: sessionId, cols: reqCols, rows: reqRows });
476
492
  res.status(201).json(newSnapshot);
477
493
  }
478
494
  catch (error) {
@@ -509,7 +525,9 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
509
525
  }
510
526
  const newMode = body.mode ? normalizeMode(body.mode, defaultMode) : normalizeMode(existingSession.mode, defaultMode);
511
527
  const resumeCommand = `${command} --resume ${claudeSessionId}`;
512
- const newSnapshot = processes.start(resumeCommand, existingSession.cwd, newMode, undefined, { reuseId: existingSession.id });
528
+ const reqCols = typeof body.cols === "number" && Number.isFinite(body.cols) ? body.cols : undefined;
529
+ const reqRows = typeof body.rows === "number" && Number.isFinite(body.rows) ? body.rows : undefined;
530
+ const newSnapshot = processes.start(resumeCommand, existingSession.cwd, newMode, undefined, { reuseId: existingSession.id, cols: reqCols, rows: reqRows });
513
531
  res.status(201).json({ resumedClaudeSessionId: claudeSessionId, ...newSnapshot });
514
532
  }
515
533
  else {
@@ -520,7 +538,9 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
520
538
  }
521
539
  const newMode = normalizeMode(body.mode, defaultMode);
522
540
  const resumeCommand = `claude --resume ${claudeSessionId}`;
523
- const newSnapshot = processes.start(resumeCommand, cwd, newMode);
541
+ const reqCols = typeof body.cols === "number" && Number.isFinite(body.cols) ? body.cols : undefined;
542
+ const reqRows = typeof body.rows === "number" && Number.isFinite(body.rows) ? body.rows : undefined;
543
+ const newSnapshot = processes.start(resumeCommand, cwd, newMode, undefined, { cols: reqCols, rows: reqRows });
524
544
  res.status(201).json({ resumedClaudeSessionId: claudeSessionId, ...newSnapshot });
525
545
  }
526
546
  }
package/dist/server.d.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  import { ProcessManager } from "./process-manager.js";
2
2
  import { StructuredSessionManager } from "./structured-session-manager.js";
3
+ import { WandStorage } from "./storage.js";
3
4
  import { WandConfig } from "./types.js";
5
+ /** Persist a cwd to recent paths. Used by both REST and session creation hooks. */
6
+ export declare function recordRecentPath(storage: WandStorage, cwd: string | undefined | null): void;
4
7
  export interface ServerUrl {
5
8
  url: string;
6
9
  scheme: "HTTP" | "HTTPS";
package/dist/server.js CHANGED
@@ -16,6 +16,7 @@ import { ensureCertificates } from "./cert.js";
16
16
  import { isExecutionMode, normalizeCardDefaults, resolveConfigDir, saveConfig } from "./config.js";
17
17
  import { getCachedModels, refreshModels } from "./models.js";
18
18
  import { ProcessManager } from "./process-manager.js";
19
+ import { SessionLogger } from "./session-logger.js";
19
20
  import { StructuredSessionManager } from "./structured-session-manager.js";
20
21
  import { generatePwaManifest, generateServiceWorker } from "./pwa.js";
21
22
  import { getErrorMessage, registerClaudeHistoryRoutes, registerSessionRoutes } from "./server-session-routes.js";
@@ -481,6 +482,33 @@ function parseStoredPathList(raw) {
481
482
  }
482
483
  }
483
484
  const MAX_RECENT_PATHS = 10;
485
+ /** Persist a cwd to recent paths. Used by both REST and session creation hooks. */
486
+ export function recordRecentPath(storage, cwd) {
487
+ if (!cwd)
488
+ return;
489
+ const trimmed = cwd.trim();
490
+ if (!trimmed)
491
+ return;
492
+ let resolved;
493
+ try {
494
+ resolved = normalizeFolderPath(trimmed);
495
+ }
496
+ catch {
497
+ return;
498
+ }
499
+ if (isBlockedFolderPath(resolved))
500
+ return;
501
+ const stored = storage.getConfigValue("recent_paths");
502
+ let recent = parseStoredPathList(stored);
503
+ recent = recent.filter((r) => normalizeFolderPath(r.path) !== resolved);
504
+ recent.unshift({
505
+ path: resolved,
506
+ name: path.basename(resolved),
507
+ lastUsedAt: new Date().toISOString(),
508
+ });
509
+ recent = recent.slice(0, MAX_RECENT_PATHS);
510
+ storage.setConfigValue("recent_paths", JSON.stringify(recent));
511
+ }
484
512
  // ── File language detection ──
485
513
  function getLanguageFromExt(ext, filePath) {
486
514
  const map = {
@@ -514,7 +542,8 @@ export async function startServer(config, configPath) {
514
542
  const configDir = resolveConfigDir(configPath);
515
543
  const avatarSeed = await ensureAvatarSeed(configDir);
516
544
  const processes = new ProcessManager(config, storage, configDir);
517
- const structuredSessions = new StructuredSessionManager(storage, config);
545
+ const structuredLogger = new SessionLogger(configDir, config.shortcutLogMaxBytes);
546
+ const structuredSessions = new StructuredSessionManager(storage, config, structuredLogger);
518
547
  const useHttps = config.https === true;
519
548
  const protocol = useHttps ? "https" : "http";
520
549
  const nodeModulesDir = path.join(RUNTIME_ROOT_DIR, "node_modules");
@@ -695,7 +724,11 @@ export async function startServer(config, configPath) {
695
724
  defaultMode: config.defaultMode,
696
725
  defaultCwd: config.defaultCwd,
697
726
  commandPresets: config.commandPresets,
698
- structuredRunners: [{ label: "Claude Structured", runner: "claude-cli-print" }],
727
+ structuredRunner: config.structuredRunner ?? "cli",
728
+ structuredRunners: [
729
+ { label: "Claude Structured", runner: "claude-cli-print" },
730
+ { label: "Codex Structured", runner: "codex-cli-exec" },
731
+ ],
699
732
  structuredChatPersona,
700
733
  cardDefaults: config.cardDefaults,
701
734
  updateAvailable: cachedUpdateInfo?.updateAvailable ?? false,
@@ -783,7 +816,7 @@ export async function startServer(config, configPath) {
783
816
  });
784
817
  app.post("/api/settings/config", async (req, res) => {
785
818
  const body = req.body;
786
- const allowedFields = ["host", "port", "https", "defaultMode", "defaultCwd", "shell", "language", "defaultModel"];
819
+ const allowedFields = ["host", "port", "https", "defaultMode", "defaultCwd", "shell", "language", "defaultModel", "structuredRunner"];
787
820
  let changed = false;
788
821
  for (const field of allowedFields) {
789
822
  if (field in body && body[field] !== undefined) {
@@ -820,6 +853,9 @@ export async function startServer(config, configPath) {
820
853
  else if (field === "defaultModel") {
821
854
  config.defaultModel = typeof body.defaultModel === "string" ? body.defaultModel.trim() : "";
822
855
  }
856
+ else if (field === "structuredRunner") {
857
+ config.structuredRunner = body.structuredRunner === "sdk" ? "sdk" : "cli";
858
+ }
823
859
  changed = true;
824
860
  }
825
861
  }
@@ -847,6 +883,7 @@ export async function startServer(config, configPath) {
847
883
  const cached = getCachedModels();
848
884
  res.json({
849
885
  models: cached.models,
886
+ codexModels: cached.codexModels,
850
887
  claudeVersion: cached.claudeVersion,
851
888
  refreshedAt: cached.refreshedAt,
852
889
  defaultModel: config.defaultModel ?? "",
@@ -857,6 +894,7 @@ export async function startServer(config, configPath) {
857
894
  const refreshed = await refreshModels();
858
895
  res.json({
859
896
  models: refreshed.models,
897
+ codexModels: refreshed.codexModels,
860
898
  claudeVersion: refreshed.claudeVersion,
861
899
  refreshedAt: refreshed.refreshedAt,
862
900
  defaultModel: config.defaultModel ?? "",
@@ -919,7 +957,9 @@ export async function startServer(config, configPath) {
919
957
  updateInFlight = false;
920
958
  }
921
959
  });
922
- registerSessionRoutes(app, processes, structuredSessions, storage, config.defaultMode, config);
960
+ registerSessionRoutes(app, processes, structuredSessions, storage, config.defaultMode, config, (cwd) => {
961
+ recordRecentPath(storage, cwd);
962
+ });
923
963
  registerClaudeHistoryRoutes(app, processes, storage);
924
964
  registerUploadRoutes(app, processes);
925
965
  app.post("/api/optimize-prompt", express.json({ limit: "256kb" }), async (req, res) => {
@@ -1098,18 +1138,12 @@ export async function startServer(config, configPath) {
1098
1138
  res.status(403).json({ error: "访问被拒绝:无法保存系统敏感目录。" });
1099
1139
  return;
1100
1140
  }
1101
- const stored = storage.getConfigValue("recent_paths");
1102
- let recent = parseStoredPathList(stored);
1103
- recent = recent.filter((r) => normalizeFolderPath(r.path) !== resolvedRecentPath);
1104
- const newRecent = {
1141
+ recordRecentPath(storage, resolvedRecentPath);
1142
+ res.json({
1105
1143
  path: resolvedRecentPath,
1106
1144
  name: path.basename(resolvedRecentPath),
1107
1145
  lastUsedAt: new Date().toISOString(),
1108
- };
1109
- recent.unshift(newRecent);
1110
- recent = recent.slice(0, MAX_RECENT_PATHS);
1111
- storage.setConfigValue("recent_paths", JSON.stringify(recent));
1112
- res.json(newRecent);
1146
+ });
1113
1147
  });
1114
1148
  app.get("/api/validate-path", async (req, res) => {
1115
1149
  const inputPath = typeof req.query.path === "string" ? req.query.path : "";
@@ -1215,11 +1249,16 @@ export async function startServer(config, configPath) {
1215
1249
  try {
1216
1250
  const rawModel = typeof body.model === "string" ? body.model.trim() : "";
1217
1251
  const effectiveModel = rawModel || (config.defaultModel ?? "").trim() || undefined;
1252
+ const reqCols = typeof body.cols === "number" && Number.isFinite(body.cols) ? body.cols : undefined;
1253
+ const reqRows = typeof body.rows === "number" && Number.isFinite(body.rows) ? body.rows : undefined;
1218
1254
  const snapshot = processes.start(body.command, body.cwd, normalizeMode(body.mode, config.defaultMode), initialInput || undefined, {
1219
1255
  worktreeEnabled: body.worktreeEnabled === true,
1220
1256
  provider: body.provider,
1221
1257
  model: effectiveModel,
1258
+ cols: reqCols,
1259
+ rows: reqRows,
1222
1260
  });
1261
+ recordRecentPath(storage, body.cwd ?? snapshot.cwd);
1223
1262
  res.status(201).json(snapshot);
1224
1263
  }
1225
1264
  catch (error) {
@@ -1304,6 +1343,8 @@ export async function startServer(config, configPath) {
1304
1343
  }
1305
1344
  // Start configured background sessions after the server is already reachable.
1306
1345
  processes.runStartupCommands();
1346
+ // Pre-warm model cache (probes claude --version + codex debug models).
1347
+ refreshModels().catch(() => { });
1307
1348
  // ── Auto-update endpoints ──
1308
1349
  app.get("/api/auto-update", (_req, res) => {
1309
1350
  const web = storage.getConfigValue("autoUpdateWeb") === "true";
@@ -24,14 +24,19 @@ export interface ShortcutLogContext {
24
24
  * SessionLogger saves raw session content to local files for debugging and analysis.
25
25
  *
26
26
  * Directory structure: .wand/sessions/{sessionId}/
27
- * - pty-output.log Raw PTY output (current, rotated when > 50 MB)
28
- * - pty-output.log.1..3 Rotated PTY output backups
29
- * - stream-events.jsonl NDJSON events from native mode (append-only)
30
- * - messages.json Final structured messages (overwritten on each update)
27
+ * - pty-output.log Raw PTY output (current, rotated when > 50 MB)
28
+ * - pty-output.log.1..3 Rotated PTY output backups
29
+ * - stream-events.jsonl NDJSON events from native mode (append-only)
30
+ * - messages.json Final structured messages (overwritten on each update)
31
+ * - structured-stdout.log Raw stdout from `codex exec` / `claude -p` child (append-only)
32
+ * - structured-stderr.log Raw stderr from the same child (append-only)
33
+ * - structured-spawns.jsonl One line per spawn: args/pid/cwd/exit/error metadata
31
34
  */
32
35
  export declare class SessionLogger {
33
36
  private readonly baseDir;
34
37
  private readonly dirs;
38
+ /** Cached on-disk size of hot-path log files so we can rotate without stat'ing on every chunk. */
39
+ private readonly logSizes;
35
40
  private readonly shortcutLogMaxBytes;
36
41
  constructor(configDir: string, shortcutLogMaxBytes?: number);
37
42
  private ensureDir;
@@ -48,6 +53,14 @@ export declare class SessionLogger {
48
53
  readPtyOutput(sessionId: string): string | null;
49
54
  /** Append a native mode NDJSON event */
50
55
  appendStreamEvent(sessionId: string, event: unknown): void;
56
+ /** Append raw stdout chunk from a structured-mode child process. */
57
+ appendStructuredStdout(sessionId: string, chunk: string): void;
58
+ /** Append raw stderr chunk from a structured-mode child process. */
59
+ appendStructuredStderr(sessionId: string, chunk: string): void;
60
+ /** Append a spawn metadata record (args, pid, cwd, exit, errors, …) for a structured run. */
61
+ appendStructuredSpawn(sessionId: string, meta: Record<string, unknown>): void;
62
+ /** Read recent stderr tail (for surfacing in failure messages). */
63
+ readStructuredStderrTail(sessionId: string, maxBytes?: number): string;
51
64
  /** Save the current structured messages snapshot */
52
65
  saveMessages(sessionId: string, messages: ConversationTurn[]): void;
53
66
  /** Save session metadata */
@@ -56,6 +69,6 @@ export declare class SessionLogger {
56
69
  deleteSession(sessionId: string): void;
57
70
  /** Append a shortcut key interaction log entry (for analyzing auto-confirm gaps) */
58
71
  appendShortcutLog(sessionId: string, shortcutKey: string, tailLines: string, ctx?: ShortcutLogContext): void;
59
- /** Truncate shortcut log by keeping only the most recent half of entries */
72
+ /** Truncate shortcut log by keeping only the most recent half of entries. Returns the new on-disk size. */
60
73
  private truncateShortcutLog;
61
74
  }