@co0ontty/wand 1.6.1 → 1.7.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.
@@ -78,13 +78,18 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
78
78
  });
79
79
  app.post("/api/structured-sessions", express.json(), async (req, res) => {
80
80
  const body = req.body;
81
- console.log("[WAND] POST /api/structured-sessions body:", JSON.stringify({ cwd: body.cwd, mode: body.mode, runner: body.runner, hasPrompt: !!body.prompt }));
81
+ 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 }));
82
82
  try {
83
+ if (body.provider && body.provider !== "claude") {
84
+ res.status(400).json({ error: "结构化会话当前仅支持 Claude provider。" });
85
+ return;
86
+ }
83
87
  const snapshot = structured.createSession({
84
88
  cwd: body.cwd?.trim() || process.cwd(),
85
89
  mode: normalizeMode(body.mode, defaultMode),
86
90
  prompt: body.prompt,
87
91
  runner: body.runner ?? "claude-cli-print",
92
+ worktreeEnabled: body.worktreeEnabled === true,
88
93
  });
89
94
  console.log("[WAND] structured session created:", JSON.stringify({ id: snapshot.id, sessionKind: snapshot.sessionKind, runner: snapshot.runner, status: snapshot.status }));
90
95
  res.status(201).json(snapshot);
@@ -148,17 +153,21 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
148
153
  res.status(404).json({ error: "未找到该会话,可能已被删除。" });
149
154
  return;
150
155
  }
156
+ const transcriptOutput = (snapshot.sessionKind ?? "pty") === "pty"
157
+ ? processes.getPtyTranscript(snapshot.id) ?? snapshot.output
158
+ : snapshot.output;
151
159
  if (req.query.format === "chat") {
152
160
  const allowFallback = (snapshot.sessionKind ?? "pty") === "pty";
161
+ const fallbackOutput = allowFallback ? transcriptOutput : "";
153
162
  const messages = snapshot.messages && snapshot.messages.length > 0
154
163
  ? snapshot.messages
155
164
  : allowFallback
156
- ? parseMessages(snapshot.output)
165
+ ? parseMessages(fallbackOutput, snapshot.command)
157
166
  : [];
158
- res.json({ ...snapshot, messages });
167
+ res.json({ ...snapshot, output: transcriptOutput, messages });
159
168
  }
160
169
  else {
161
- res.json(snapshot);
170
+ res.json({ ...snapshot, output: transcriptOutput });
162
171
  }
163
172
  });
164
173
  app.post("/api/sessions/:id/resume", (req, res) => {
@@ -176,6 +185,10 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
176
185
  res.status(400).json({ error: "结构化会话不支持 Claude CLI resume。" });
177
186
  return;
178
187
  }
188
+ if (existingSession.provider && existingSession.provider !== "claude") {
189
+ res.status(400).json({ error: "只有 Claude provider 支持恢复功能。" });
190
+ return;
191
+ }
179
192
  const claudeSessionId = existingSession.claudeSessionId;
180
193
  if (!claudeSessionId) {
181
194
  res.status(400).json({ error: "此会话没有 Claude 会话 ID,无法恢复。" });
@@ -207,6 +220,10 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
207
220
  }
208
221
  const existingSession = storage.getLatestSessionByClaudeSessionId(claudeSessionId);
209
222
  if (existingSession) {
223
+ if (existingSession.provider && existingSession.provider !== "claude") {
224
+ res.status(400).json({ error: "只有 Claude provider 支持按 Claude Session ID 恢复。" });
225
+ return;
226
+ }
210
227
  const command = existingSession.command.trim();
211
228
  if ((existingSession.sessionKind ?? "pty") !== "pty") {
212
229
  res.status(400).json({ error: "结构化会话不支持按 Claude Session ID 恢复。" });
@@ -284,7 +301,12 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
284
301
  app.post("/api/sessions/:id/approve-permission", (req, res) => {
285
302
  try {
286
303
  if (structured.get(req.params.id)) {
287
- res.json(structured.approvePermission(req.params.id));
304
+ res.status(400).json({ error: "结构化会话不需要终端权限操作。" });
305
+ return;
306
+ }
307
+ const snapshot = processes.get(req.params.id);
308
+ if (snapshot?.provider === "codex") {
309
+ res.status(400).json({ error: "Codex provider 不支持权限批准操作。" });
288
310
  return;
289
311
  }
290
312
  res.json(processes.approvePermission(req.params.id));
@@ -296,7 +318,12 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
296
318
  app.post("/api/sessions/:id/deny-permission", (req, res) => {
297
319
  try {
298
320
  if (structured.get(req.params.id)) {
299
- res.json(structured.denyPermission(req.params.id));
321
+ res.status(400).json({ error: "结构化会话不需要终端权限操作。" });
322
+ return;
323
+ }
324
+ const snapshot = processes.get(req.params.id);
325
+ if (snapshot?.provider === "codex") {
326
+ res.status(400).json({ error: "Codex provider 不支持权限拒绝操作。" });
300
327
  return;
301
328
  }
302
329
  res.json(processes.denyPermission(req.params.id));
@@ -308,7 +335,12 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
308
335
  app.post("/api/sessions/:id/toggle-auto-approve", (req, res) => {
309
336
  try {
310
337
  if (structured.get(req.params.id)) {
311
- res.json(structured.toggleAutoApprove(req.params.id));
338
+ res.status(400).json({ error: "结构化会话不需要切换终端自动批准。" });
339
+ return;
340
+ }
341
+ const snapshot = processes.get(req.params.id);
342
+ if (snapshot?.provider === "codex") {
343
+ res.status(400).json({ error: "Codex provider 不支持自动批准切换。" });
312
344
  return;
313
345
  }
314
346
  res.json(processes.toggleAutoApprove(req.params.id));
package/dist/server.js CHANGED
@@ -8,6 +8,19 @@ import { promisify } from "node:util";
8
8
  import path from "node:path";
9
9
  import process from "node:process";
10
10
  import { WebSocketServer } from "ws";
11
+ import { ensureAvatarSeed, getAvatarSvg } from "./avatar.js";
12
+ import { createSession, revokeSession, setAuthStorage, validateSession } from "./auth.js";
13
+ import { ensureCertificates } from "./cert.js";
14
+ import { isExecutionMode, resolveConfigDir, saveConfig } from "./config.js";
15
+ import { ProcessManager } from "./process-manager.js";
16
+ import { StructuredSessionManager } from "./structured-session-manager.js";
17
+ import { generatePwaManifest, generateServiceWorker } from "./pwa.js";
18
+ import { getErrorMessage, registerClaudeHistoryRoutes, registerSessionRoutes } from "./server-session-routes.js";
19
+ import { resolveDatabasePath, WandStorage } from "./storage.js";
20
+ import { renderApp } from "./web-ui/index.js";
21
+ import { WsBroadcastManager } from "./ws-broadcast.js";
22
+ import { checkRateLimit, recordFailedLogin, resetRateLimit } from "./middleware/rate-limit.js";
23
+ import { isPathWithinBase, isBlockedFolderPath, normalizeFolderPath } from "./middleware/path-safety.js";
11
24
  const execAsync = promisify(exec);
12
25
  const SERVER_MODULE_DIR = path.dirname(new URL(import.meta.url).pathname);
13
26
  const RUNTIME_ROOT_DIR = path.resolve(SERVER_MODULE_DIR, "..");
@@ -52,19 +65,83 @@ function compareSemver(a, b) {
52
65
  }
53
66
  return 0;
54
67
  }
55
- import { ensureAvatarSeed, getAvatarSvg } from "./avatar.js";
56
- import { createSession, revokeSession, setAuthStorage, validateSession } from "./auth.js";
57
- import { ensureCertificates } from "./cert.js";
58
- import { isExecutionMode, resolveConfigDir, saveConfig } from "./config.js";
59
- import { ProcessManager } from "./process-manager.js";
60
- import { StructuredSessionManager } from "./structured-session-manager.js";
61
- import { generatePwaManifest, generateServiceWorker } from "./pwa.js";
62
- import { getErrorMessage, registerClaudeHistoryRoutes, registerSessionRoutes } from "./server-session-routes.js";
63
- import { resolveDatabasePath, WandStorage } from "./storage.js";
64
- import { renderApp } from "./web-ui/index.js";
65
- import { WsBroadcastManager } from "./ws-broadcast.js";
66
- import { checkRateLimit, recordFailedLogin, resetRateLimit } from "./middleware/rate-limit.js";
67
- import { isPathWithinBase, isBlockedFolderPath, normalizeFolderPath } from "./middleware/path-safety.js";
68
+ function isExternalAvatarSource(value) {
69
+ return /^(https?:|data:)/i.test(value);
70
+ }
71
+ function normalizePersonaName(value) {
72
+ if (typeof value !== "string")
73
+ return undefined;
74
+ const trimmed = value.trim();
75
+ return trimmed || undefined;
76
+ }
77
+ function normalizePersonaAvatar(value) {
78
+ if (typeof value !== "string")
79
+ return undefined;
80
+ const trimmed = value.trim();
81
+ return trimmed || undefined;
82
+ }
83
+ function resolveStructuredChatPersona(config) {
84
+ const persona = config.structuredChatPersona;
85
+ if (!persona)
86
+ return undefined;
87
+ const userName = normalizePersonaName(persona.user?.name);
88
+ const userAvatar = normalizePersonaAvatar(persona.user?.avatar);
89
+ const assistantName = normalizePersonaName(persona.assistant?.name);
90
+ const assistantAvatar = normalizePersonaAvatar(persona.assistant?.avatar);
91
+ if (!userName && !userAvatar && !assistantName && !assistantAvatar) {
92
+ return undefined;
93
+ }
94
+ return {
95
+ user: userName || userAvatar ? { name: userName, avatar: userAvatar } : undefined,
96
+ assistant: assistantName || assistantAvatar ? { name: assistantName, avatar: assistantAvatar } : undefined,
97
+ };
98
+ }
99
+ function resolveStructuredChatAvatarPath(configPath, config, role) {
100
+ const avatar = role === "user"
101
+ ? config.structuredChatPersona?.user?.avatar
102
+ : config.structuredChatPersona?.assistant?.avatar;
103
+ if (!avatar || isExternalAvatarSource(avatar)) {
104
+ return null;
105
+ }
106
+ const configDir = resolveConfigDir(configPath);
107
+ return path.isAbsolute(avatar) ? avatar : path.resolve(configDir, avatar);
108
+ }
109
+ async function buildStructuredChatPersonaPayload(configPath, config) {
110
+ const persona = resolveStructuredChatPersona(config);
111
+ if (!persona)
112
+ return undefined;
113
+ const buildRole = async (role) => {
114
+ const roleConfig = role === "user" ? persona.user : persona.assistant;
115
+ if (!roleConfig)
116
+ return undefined;
117
+ let avatar = roleConfig.avatar;
118
+ if (avatar && !isExternalAvatarSource(avatar)) {
119
+ const resolvedPath = resolveStructuredChatAvatarPath(configPath, config, role);
120
+ if (!resolvedPath) {
121
+ avatar = undefined;
122
+ }
123
+ else {
124
+ try {
125
+ const fileStat = await stat(resolvedPath);
126
+ avatar = fileStat.isFile() ? `/api/structured-chat-avatar/${role}` : undefined;
127
+ }
128
+ catch {
129
+ avatar = undefined;
130
+ }
131
+ }
132
+ }
133
+ if (!roleConfig.name && !avatar)
134
+ return undefined;
135
+ return {
136
+ name: roleConfig.name,
137
+ avatar,
138
+ };
139
+ };
140
+ const [user, assistant] = await Promise.all([buildRole("user"), buildRole("assistant")]);
141
+ if (!user && !assistant)
142
+ return undefined;
143
+ return { user, assistant };
144
+ }
68
145
  // ── Git helpers ──
69
146
  async function isGitRepo(dirPath) {
70
147
  try {
@@ -266,6 +343,50 @@ export async function startServer(config, configPath) {
266
343
  res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
267
344
  res.type("html").send(renderApp(configPath));
268
345
  });
346
+ app.get("/api/structured-chat-avatar/:role", async (req, res) => {
347
+ const role = req.params.role === "user" || req.params.role === "assistant"
348
+ ? req.params.role
349
+ : null;
350
+ if (!role) {
351
+ res.status(404).end();
352
+ return;
353
+ }
354
+ const resolvedPath = resolveStructuredChatAvatarPath(configPath, config, role);
355
+ if (!resolvedPath) {
356
+ res.status(404).end();
357
+ return;
358
+ }
359
+ try {
360
+ const fileStat = await stat(resolvedPath);
361
+ if (!fileStat.isFile()) {
362
+ res.status(404).end();
363
+ return;
364
+ }
365
+ const ext = path.extname(resolvedPath).toLowerCase();
366
+ const contentType = ext === ".svg"
367
+ ? "image/svg+xml"
368
+ : ext === ".png"
369
+ ? "image/png"
370
+ : ext === ".jpg" || ext === ".jpeg"
371
+ ? "image/jpeg"
372
+ : ext === ".webp"
373
+ ? "image/webp"
374
+ : ext === ".gif"
375
+ ? "image/gif"
376
+ : ext === ".avif"
377
+ ? "image/avif"
378
+ : null;
379
+ if (!contentType) {
380
+ res.status(415).json({ error: "不支持的头像格式。" });
381
+ return;
382
+ }
383
+ res.setHeader("X-Content-Type-Options", "nosniff");
384
+ res.type(contentType).sendFile(resolvedPath);
385
+ }
386
+ catch {
387
+ res.status(404).end();
388
+ }
389
+ });
269
390
  app.get("/manifest.json", (_req, res) => {
270
391
  res.setHeader("Content-Type", "application/manifest+json");
271
392
  res.send(generatePwaManifest());
@@ -328,7 +449,8 @@ export async function startServer(config, configPath) {
328
449
  });
329
450
  app.use("/api", requireAuth);
330
451
  // ── Config & Session info ──
331
- app.get("/api/config", (_req, res) => {
452
+ app.get("/api/config", async (_req, res) => {
453
+ const structuredChatPersona = await buildStructuredChatPersonaPayload(configPath, config);
332
454
  res.json({
333
455
  host: config.host,
334
456
  port: config.port,
@@ -336,6 +458,7 @@ export async function startServer(config, configPath) {
336
458
  defaultCwd: config.defaultCwd,
337
459
  commandPresets: config.commandPresets,
338
460
  structuredRunners: [{ label: "Claude Structured", runner: "claude-cli-print" }],
461
+ structuredChatPersona,
339
462
  updateAvailable: cachedUpdateInfo?.updateAvailable ?? false,
340
463
  latestVersion: cachedUpdateInfo?.latest ?? null,
341
464
  currentVersion: PKG_VERSION,
@@ -735,7 +858,10 @@ export async function startServer(config, configPath) {
735
858
  }
736
859
  const initialInput = body.initialInput?.trim();
737
860
  try {
738
- const snapshot = processes.start(body.command, body.cwd, normalizeMode(body.mode, config.defaultMode), initialInput || undefined);
861
+ const snapshot = processes.start(body.command, body.cwd, normalizeMode(body.mode, config.defaultMode), initialInput || undefined, {
862
+ worktreeEnabled: body.worktreeEnabled === true,
863
+ provider: body.provider,
864
+ });
739
865
  res.status(201).json(snapshot);
740
866
  }
741
867
  catch (error) {
@@ -44,6 +44,8 @@ export declare class SessionLogger {
44
44
  private rotatePtyLog;
45
45
  /** Append raw PTY output chunk */
46
46
  appendPtyOutput(sessionId: string, chunk: string): void;
47
+ /** Read the full PTY transcript including rotated logs, oldest first. */
48
+ readPtyOutput(sessionId: string): string | null;
47
49
  /** Append a native mode NDJSON event */
48
50
  appendStreamEvent(sessionId: string, event: unknown): void;
49
51
  /** Save the current structured messages snapshot */
@@ -89,6 +89,29 @@ export class SessionLogger {
89
89
  // Non-critical — don't let logging failures affect main flow
90
90
  }
91
91
  }
92
+ /** Read the full PTY transcript including rotated logs, oldest first. */
93
+ readPtyOutput(sessionId) {
94
+ try {
95
+ const dir = this.ensureDir(sessionId);
96
+ const parts = [];
97
+ for (let index = PTY_LOG_MAX_ROTATIONS; index >= 1; index -= 1) {
98
+ const rotatedPath = path.join(dir, `pty-output.log.${index}`);
99
+ if (existsSync(rotatedPath)) {
100
+ parts.push(readFileSync(rotatedPath, "utf8"));
101
+ }
102
+ }
103
+ const currentPath = path.join(dir, "pty-output.log");
104
+ if (existsSync(currentPath)) {
105
+ parts.push(readFileSync(currentPath, "utf8"));
106
+ }
107
+ if (parts.length === 0)
108
+ return null;
109
+ return parts.join("");
110
+ }
111
+ catch {
112
+ return null;
113
+ }
114
+ }
92
115
  /** Append a native mode NDJSON event */
93
116
  appendStreamEvent(sessionId, event) {
94
117
  try {
package/dist/storage.js CHANGED
@@ -12,6 +12,18 @@ function parseStoredMessages(raw) {
12
12
  return undefined;
13
13
  }
14
14
  }
15
+ function parseQueuedMessages(raw) {
16
+ if (!raw) {
17
+ return undefined;
18
+ }
19
+ try {
20
+ const parsed = JSON.parse(raw);
21
+ return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string") : undefined;
22
+ }
23
+ catch {
24
+ return undefined;
25
+ }
26
+ }
15
27
  function parseStructuredState(raw) {
16
28
  if (!raw) {
17
29
  return undefined;
@@ -23,6 +35,33 @@ function parseStructuredState(raw) {
23
35
  return undefined;
24
36
  }
25
37
  }
38
+ function inferSessionProvider(row) {
39
+ if (row.provider === "claude" || row.provider === "codex") {
40
+ return row.provider;
41
+ }
42
+ if (row.runner === "claude-cli" || row.runner === "claude-cli-print") {
43
+ return "claude";
44
+ }
45
+ return /^claude\b/.test(row.command.trim()) ? "claude" : undefined;
46
+ }
47
+ function parseWorktreeInfo(raw) {
48
+ if (!raw) {
49
+ return undefined;
50
+ }
51
+ try {
52
+ const parsed = JSON.parse(raw);
53
+ if (parsed
54
+ && typeof parsed === "object"
55
+ && typeof parsed.branch === "string"
56
+ && typeof parsed.path === "string") {
57
+ return parsed;
58
+ }
59
+ }
60
+ catch {
61
+ return undefined;
62
+ }
63
+ return undefined;
64
+ }
26
65
  export const DEFAULT_DB_FILE = "wand.db";
27
66
  export function resolveDatabasePath(configPath) {
28
67
  return path.resolve(path.dirname(configPath), DEFAULT_DB_FILE);
@@ -46,13 +85,17 @@ const INIT_SQL = `
46
85
  archived INTEGER NOT NULL DEFAULT 0,
47
86
  archived_at TEXT,
48
87
  claude_session_id TEXT,
88
+ provider TEXT,
49
89
  session_kind TEXT NOT NULL DEFAULT 'pty',
50
90
  runner TEXT,
51
91
  messages TEXT,
92
+ queued_messages TEXT,
52
93
  structured_state TEXT,
53
94
  resumed_from_session_id TEXT,
54
95
  resumed_to_session_id TEXT,
55
- auto_recovered INTEGER NOT NULL DEFAULT 0
96
+ auto_recovered INTEGER NOT NULL DEFAULT 0,
97
+ worktree_enabled INTEGER NOT NULL DEFAULT 0,
98
+ worktree_info TEXT
56
99
  );
57
100
 
58
101
  CREATE TABLE IF NOT EXISTS app_config (
@@ -143,9 +186,9 @@ export class WandStorage {
143
186
  this.db
144
187
  .prepare(`INSERT INTO command_sessions (
145
188
  id, command, cwd, mode, status, exit_code, started_at, ended_at, output
146
- , archived, archived_at, claude_session_id, session_kind, runner, messages, structured_state
147
- , resumed_from_session_id, resumed_to_session_id, auto_recovered
148
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
189
+ , archived, archived_at, claude_session_id, provider, session_kind, runner, messages, queued_messages, structured_state
190
+ , resumed_from_session_id, resumed_to_session_id, auto_recovered, worktree_enabled, worktree_info
191
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
149
192
  ON CONFLICT(id) DO UPDATE SET
150
193
  command = excluded.command,
151
194
  cwd = excluded.cwd,
@@ -158,14 +201,18 @@ export class WandStorage {
158
201
  archived = excluded.archived,
159
202
  archived_at = excluded.archived_at,
160
203
  claude_session_id = excluded.claude_session_id,
204
+ provider = excluded.provider,
161
205
  session_kind = excluded.session_kind,
162
206
  runner = excluded.runner,
163
207
  messages = excluded.messages,
208
+ queued_messages = excluded.queued_messages,
164
209
  structured_state = excluded.structured_state,
165
210
  resumed_from_session_id = excluded.resumed_from_session_id,
166
211
  resumed_to_session_id = excluded.resumed_to_session_id,
167
- auto_recovered = excluded.auto_recovered`)
168
- .run(snapshot.id, snapshot.command, snapshot.cwd, snapshot.mode, snapshot.status, snapshot.exitCode, snapshot.startedAt, snapshot.endedAt, snapshot.output, snapshot.archived ? 1 : 0, snapshot.archivedAt, snapshot.claudeSessionId, snapshot.sessionKind ?? "pty", snapshot.runner ?? null, snapshot.messages ? JSON.stringify(snapshot.messages) : null, snapshot.structuredState ? JSON.stringify(snapshot.structuredState) : null, snapshot.resumedFromSessionId ?? null, snapshot.resumedToSessionId ?? null, snapshot.autoRecovered ? 1 : 0);
212
+ auto_recovered = excluded.auto_recovered,
213
+ worktree_enabled = excluded.worktree_enabled,
214
+ worktree_info = excluded.worktree_info`)
215
+ .run(snapshot.id, snapshot.command, snapshot.cwd, snapshot.mode, snapshot.status, snapshot.exitCode, snapshot.startedAt, snapshot.endedAt, snapshot.output, snapshot.archived ? 1 : 0, snapshot.archivedAt, snapshot.claudeSessionId, snapshot.provider ?? null, snapshot.sessionKind ?? "pty", snapshot.runner ?? null, snapshot.messages ? JSON.stringify(snapshot.messages) : null, snapshot.queuedMessages ? JSON.stringify(snapshot.queuedMessages) : null, snapshot.structuredState ? JSON.stringify(snapshot.structuredState) : null, snapshot.resumedFromSessionId ?? null, snapshot.resumedToSessionId ?? null, snapshot.autoRecovered ? 1 : 0, snapshot.worktreeEnabled ? 1 : 0, snapshot.worktree ? JSON.stringify(snapshot.worktree) : null);
169
216
  this.db.exec("COMMIT");
170
217
  }
171
218
  catch (error) {
@@ -184,15 +231,16 @@ export class WandStorage {
184
231
  command = ?, cwd = ?, mode = ?, status = ?, exit_code = ?,
185
232
  started_at = ?, ended_at = ?, output = ?,
186
233
  archived = ?, archived_at = ?, claude_session_id = ?,
187
- session_kind = ?, runner = ?, structured_state = ?,
188
- resumed_from_session_id = ?, resumed_to_session_id = ?, auto_recovered = ?
234
+ provider = ?, session_kind = ?, runner = ?, structured_state = ?,
235
+ resumed_from_session_id = ?, resumed_to_session_id = ?, auto_recovered = ?,
236
+ worktree_enabled = ?, worktree_info = ?
189
237
  WHERE id = ?`)
190
- .run(snapshot.command, snapshot.cwd, snapshot.mode, snapshot.status, snapshot.exitCode, snapshot.startedAt, snapshot.endedAt, snapshot.output, snapshot.archived ? 1 : 0, snapshot.archivedAt, snapshot.claudeSessionId, snapshot.sessionKind ?? "pty", snapshot.runner ?? null, snapshot.structuredState ? JSON.stringify(snapshot.structuredState) : null, snapshot.resumedFromSessionId ?? null, snapshot.resumedToSessionId ?? null, snapshot.autoRecovered ? 1 : 0, snapshot.id);
238
+ .run(snapshot.command, snapshot.cwd, snapshot.mode, snapshot.status, snapshot.exitCode, snapshot.startedAt, snapshot.endedAt, snapshot.output, snapshot.archived ? 1 : 0, snapshot.archivedAt, snapshot.claudeSessionId, snapshot.provider ?? null, snapshot.sessionKind ?? "pty", snapshot.runner ?? null, snapshot.structuredState ? JSON.stringify(snapshot.structuredState) : null, snapshot.resumedFromSessionId ?? null, snapshot.resumedToSessionId ?? null, snapshot.autoRecovered ? 1 : 0, snapshot.worktreeEnabled ? 1 : 0, snapshot.worktree ? JSON.stringify(snapshot.worktree) : null, snapshot.id);
191
239
  }
192
240
  getSession(id) {
193
241
  const row = this.db
194
- .prepare(`SELECT id, session_kind, runner, command, cwd, mode, status, exit_code, started_at, ended_at, output, archived, archived_at, claude_session_id, messages, structured_state
195
- , resumed_from_session_id, resumed_to_session_id, auto_recovered
242
+ .prepare(`SELECT id, provider, session_kind, runner, command, cwd, mode, status, exit_code, started_at, ended_at, output, archived, archived_at, claude_session_id, messages, queued_messages, structured_state
243
+ , resumed_from_session_id, resumed_to_session_id, auto_recovered, worktree_enabled, worktree_info
196
244
  FROM command_sessions
197
245
  WHERE id = ?`)
198
246
  .get(id);
@@ -200,8 +248,8 @@ export class WandStorage {
200
248
  }
201
249
  getLatestSessionByClaudeSessionId(claudeSessionId) {
202
250
  const row = this.db
203
- .prepare(`SELECT id, session_kind, runner, command, cwd, mode, status, exit_code, started_at, ended_at, output, archived, archived_at, claude_session_id, messages, structured_state
204
- , resumed_from_session_id, resumed_to_session_id, auto_recovered
251
+ .prepare(`SELECT id, provider, session_kind, runner, command, cwd, mode, status, exit_code, started_at, ended_at, output, archived, archived_at, claude_session_id, messages, queued_messages, structured_state
252
+ , resumed_from_session_id, resumed_to_session_id, auto_recovered, worktree_enabled, worktree_info
205
253
  FROM command_sessions
206
254
  WHERE claude_session_id = ?
207
255
  ORDER BY started_at DESC
@@ -211,17 +259,19 @@ export class WandStorage {
211
259
  }
212
260
  loadSessions() {
213
261
  const rows = this.db
214
- .prepare(`SELECT id, session_kind, runner, command, cwd, mode, status, exit_code, started_at, ended_at, output, archived, archived_at, claude_session_id, messages, structured_state
215
- , resumed_from_session_id, resumed_to_session_id, auto_recovered
262
+ .prepare(`SELECT id, provider, session_kind, runner, command, cwd, mode, status, exit_code, started_at, ended_at, output, archived, archived_at, claude_session_id, messages, queued_messages, structured_state
263
+ , resumed_from_session_id, resumed_to_session_id, auto_recovered, worktree_enabled, worktree_info
216
264
  FROM command_sessions
217
265
  ORDER BY started_at DESC`)
218
266
  .all();
219
267
  return rows.map((row) => this.mapSessionRow(row));
220
268
  }
221
269
  mapSessionRow(row) {
270
+ const provider = inferSessionProvider(row);
222
271
  return {
223
272
  id: row.id,
224
273
  sessionKind: row.session_kind ?? "pty",
274
+ provider,
225
275
  runner: row.runner ?? undefined,
226
276
  command: row.command,
227
277
  cwd: row.cwd,
@@ -235,10 +285,13 @@ export class WandStorage {
235
285
  archivedAt: row.archived_at,
236
286
  claudeSessionId: row.claude_session_id,
237
287
  messages: parseStoredMessages(row.messages),
288
+ queuedMessages: parseQueuedMessages(row.queued_messages),
238
289
  structuredState: parseStructuredState(row.structured_state),
239
290
  resumedFromSessionId: row.resumed_from_session_id ?? undefined,
240
291
  resumedToSessionId: row.resumed_to_session_id ?? undefined,
241
- autoRecovered: Boolean(row.auto_recovered)
292
+ autoRecovered: Boolean(row.auto_recovered),
293
+ worktreeEnabled: Boolean(row.worktree_enabled),
294
+ worktree: parseWorktreeInfo(row.worktree_info) ?? null
242
295
  };
243
296
  }
244
297
  deleteSession(id) {
@@ -257,6 +310,9 @@ function ensureCommandSessionSchema(db) {
257
310
  if (!names.has("claude_session_id")) {
258
311
  db.exec("ALTER TABLE command_sessions ADD COLUMN claude_session_id TEXT");
259
312
  }
313
+ if (!names.has("provider")) {
314
+ db.exec("ALTER TABLE command_sessions ADD COLUMN provider TEXT");
315
+ }
260
316
  if (!names.has("session_kind")) {
261
317
  db.exec("ALTER TABLE command_sessions ADD COLUMN session_kind TEXT NOT NULL DEFAULT 'pty'");
262
318
  }
@@ -266,6 +322,9 @@ function ensureCommandSessionSchema(db) {
266
322
  if (!names.has("messages")) {
267
323
  db.exec("ALTER TABLE command_sessions ADD COLUMN messages TEXT");
268
324
  }
325
+ if (!names.has("queued_messages")) {
326
+ db.exec("ALTER TABLE command_sessions ADD COLUMN queued_messages TEXT");
327
+ }
269
328
  if (!names.has("structured_state")) {
270
329
  db.exec("ALTER TABLE command_sessions ADD COLUMN structured_state TEXT");
271
330
  }
@@ -275,7 +334,10 @@ function ensureCommandSessionSchema(db) {
275
334
  if (!names.has("resumed_to_session_id")) {
276
335
  db.exec("ALTER TABLE command_sessions ADD COLUMN resumed_to_session_id TEXT");
277
336
  }
278
- if (!names.has("auto_recovered")) {
279
- db.exec("ALTER TABLE command_sessions ADD COLUMN auto_recovered INTEGER NOT NULL DEFAULT 0");
337
+ if (!names.has("worktree_enabled")) {
338
+ db.exec("ALTER TABLE command_sessions ADD COLUMN worktree_enabled INTEGER NOT NULL DEFAULT 0");
339
+ }
340
+ if (!names.has("worktree_info")) {
341
+ db.exec("ALTER TABLE command_sessions ADD COLUMN worktree_info TEXT");
280
342
  }
281
343
  }
@@ -5,6 +5,7 @@ interface CreateStructuredSessionOptions {
5
5
  mode: ExecutionMode;
6
6
  prompt?: string;
7
7
  runner?: SessionRunner;
8
+ worktreeEnabled?: boolean;
8
9
  }
9
10
  export declare class StructuredSessionManager {
10
11
  private readonly storage;
@@ -29,6 +30,10 @@ export declare class StructuredSessionManager {
29
30
  stop(id: string): SessionSnapshot;
30
31
  delete(id: string): void;
31
32
  private requireSession;
33
+ private buildQueuedPlaceholderTurns;
34
+ private buildRenderableMessages;
35
+ private emitStructuredSnapshot;
36
+ private flushNextQueuedMessage;
32
37
  private emit;
33
38
  private resolvePermission;
34
39
  private incrementApprovalStats;