@co0ontty/wand 1.6.2 → 1.9.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.
@@ -1,5 +1,6 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { spawn } from "node:child_process";
3
+ import { prepareSessionWorktree } from "./git-worktree.js";
3
4
  const STREAM_EMIT_DEBOUNCE_MS = 16;
4
5
  function isRunningAsRoot() {
5
6
  return process.getuid?.() === 0 || process.geteuid?.() === 0;
@@ -25,6 +26,15 @@ function withSummary(snapshot) {
25
26
  function shouldAutoApproveForMode(mode) {
26
27
  return mode === "full-access" || mode === "managed" || mode === "auto-edit";
27
28
  }
29
+ function buildStructuredOutputPayload(snapshot) {
30
+ return {
31
+ output: snapshot.output,
32
+ messages: snapshot.messages,
33
+ queuedMessages: snapshot.queuedMessages,
34
+ sessionKind: "structured",
35
+ structuredState: snapshot.structuredState,
36
+ };
37
+ }
28
38
  export class StructuredSessionManager {
29
39
  storage;
30
40
  config;
@@ -41,13 +51,16 @@ export class StructuredSessionManager {
41
51
  const restored = {
42
52
  ...snapshot,
43
53
  sessionKind: "structured",
54
+ provider: snapshot.provider ?? "claude",
44
55
  runner: snapshot.runner ?? "claude-cli-print",
45
56
  status: restoredStatus,
46
57
  autoApprovePermissions: snapshot.autoApprovePermissions ?? shouldAutoApproveForMode(snapshot.mode),
47
58
  approvalStats: snapshot.approvalStats ?? { tool: 0, command: 0, file: 0, total: 0 },
59
+ queuedMessages: snapshot.queuedMessages ?? [],
48
60
  pendingEscalation: null,
49
61
  permissionBlocked: false,
50
62
  structuredState: {
63
+ provider: snapshot.structuredState?.provider ?? snapshot.provider ?? "claude",
51
64
  runner: snapshot.runner ?? "claude-cli-print",
52
65
  model: snapshot.structuredState?.model,
53
66
  lastError: snapshot.structuredState?.lastError ?? null,
@@ -75,23 +88,31 @@ export class StructuredSessionManager {
75
88
  const id = randomUUID();
76
89
  const startedAt = new Date().toISOString();
77
90
  const prompt = options.prompt?.trim();
91
+ const worktreeSetup = options.worktreeEnabled
92
+ ? prepareSessionWorktree({ cwd: options.cwd, sessionId: id })
93
+ : null;
78
94
  const snapshot = {
79
95
  id,
80
96
  sessionKind: "structured",
97
+ provider: "claude",
81
98
  runner: options.runner ?? "claude-cli-print",
82
99
  command: "claude -p --output-format stream-json",
83
- cwd: options.cwd,
100
+ cwd: worktreeSetup?.cwd ?? options.cwd,
84
101
  mode: options.mode,
85
- status: prompt ? "running" : "stopped",
102
+ worktreeEnabled: Boolean(worktreeSetup),
103
+ worktree: worktreeSetup?.worktree ?? null,
104
+ status: "running",
86
105
  exitCode: null,
87
106
  startedAt,
88
- endedAt: prompt ? null : startedAt,
107
+ endedAt: null,
89
108
  output: "",
90
109
  archived: false,
91
110
  archivedAt: null,
92
111
  claudeSessionId: null,
93
112
  messages: [],
113
+ queuedMessages: [],
94
114
  structuredState: {
115
+ provider: "claude",
95
116
  runner: options.runner ?? "claude-cli-print",
96
117
  inFlight: false,
97
118
  activeRequestId: null,
@@ -119,7 +140,6 @@ export class StructuredSessionManager {
119
140
  const child = this.pendingChildren.get(id);
120
141
  const childAlive = child && !child.killed && child.exitCode === null;
121
142
  if (!childAlive) {
122
- // Auto-recover: inFlight is stuck but child process is gone or dead
123
143
  if (child)
124
144
  this.pendingChildren.delete(id);
125
145
  const recovered = {
@@ -137,7 +157,18 @@ export class StructuredSessionManager {
137
157
  session = recovered;
138
158
  }
139
159
  else {
140
- throw new Error("结构化会话正在处理中,请稍后再试。");
160
+ const queue = [...(session.queuedMessages ?? [])];
161
+ if (queue.length >= 10) {
162
+ throw new Error("排队消息已满(最多 10 条),请等待当前消息处理完成。");
163
+ }
164
+ const queued = {
165
+ ...session,
166
+ queuedMessages: [...queue, prompt],
167
+ };
168
+ this.sessions.set(id, queued);
169
+ this.storage.saveSession(queued);
170
+ this.emitStructuredSnapshot(queued);
171
+ return queued;
141
172
  }
142
173
  }
143
174
  const userTurn = {
@@ -152,7 +183,7 @@ export class StructuredSessionManager {
152
183
  endedAt: null,
153
184
  messages: [...(session.messages ?? []), userTurn],
154
185
  structuredState: {
155
- ...(session.structuredState ?? { runner: "claude-cli-print", lastError: null, inFlight: false, activeRequestId: null }),
186
+ ...(session.structuredState ?? { provider: "claude", runner: "claude-cli-print", lastError: null, inFlight: false, activeRequestId: null }),
156
187
  inFlight: true,
157
188
  activeRequestId: requestId,
158
189
  lastError: null,
@@ -160,15 +191,11 @@ export class StructuredSessionManager {
160
191
  };
161
192
  this.sessions.set(id, updated);
162
193
  this.storage.saveSession(updated);
163
- this.emit({
164
- type: "output",
165
- sessionId: id,
166
- data: { output: updated.output, messages: updated.messages, sessionKind: "structured" },
167
- });
194
+ this.emitStructuredSnapshot(updated);
168
195
  this.emit({
169
196
  type: "status",
170
197
  sessionId: id,
171
- data: { status: "running", sessionKind: "structured" },
198
+ data: { status: "running", sessionKind: "structured", queuedMessages: updated.queuedMessages, structuredState: updated.structuredState },
172
199
  });
173
200
  try {
174
201
  await this.runClaudeStreaming(id, updated, prompt);
@@ -197,13 +224,9 @@ export class StructuredSessionManager {
197
224
  this.emit({
198
225
  type: "status",
199
226
  sessionId: id,
200
- data: { status: failed.status, error: message, sessionKind: "structured" },
201
- });
202
- this.emit({
203
- type: "ended",
204
- sessionId: id,
205
- data: { status: failed.status, exitCode: 1, messages: failed.messages, sessionKind: "structured", structuredState: failed.structuredState },
227
+ data: { status: failed.status, error: message, sessionKind: "structured", queuedMessages: failed.queuedMessages, structuredState: failed.structuredState },
206
228
  });
229
+ this.emitStructuredSnapshot(failed, "ended");
207
230
  throw error;
208
231
  }
209
232
  }
@@ -268,14 +291,14 @@ export class StructuredSessionManager {
268
291
  pendingEscalation: null,
269
292
  permissionBlocked: false,
270
293
  structuredState: {
271
- ...(session.structuredState ?? { runner: "claude-cli-print", lastError: null, inFlight: false, activeRequestId: null }),
294
+ ...(session.structuredState ?? { provider: "claude", runner: "claude-cli-print", lastError: null, inFlight: false, activeRequestId: null }),
272
295
  inFlight: false,
273
296
  activeRequestId: null,
274
297
  },
275
298
  };
276
299
  this.sessions.set(id, stopped);
277
300
  this.storage.saveSession(stopped);
278
- this.emit({ type: "ended", sessionId: id, data: { status: stopped.status, exitCode: null, messages: stopped.messages, sessionKind: "structured" } });
301
+ this.emitStructuredSnapshot(stopped, "ended");
279
302
  return stopped;
280
303
  }
281
304
  delete(id) {
@@ -297,6 +320,58 @@ export class StructuredSessionManager {
297
320
  }
298
321
  return session;
299
322
  }
323
+ buildQueuedPlaceholderTurns(session) {
324
+ return (session.queuedMessages ?? []).map((text) => ({
325
+ role: "user",
326
+ content: [{ type: "text", text, __queued: true }],
327
+ }));
328
+ }
329
+ buildRenderableMessages(session) {
330
+ return [
331
+ ...(session.messages ?? []),
332
+ ...this.buildQueuedPlaceholderTurns(session),
333
+ ];
334
+ }
335
+ emitStructuredSnapshot(session, eventType = "output") {
336
+ const payload = buildStructuredOutputPayload(session);
337
+ const data = {
338
+ ...payload,
339
+ messages: this.buildRenderableMessages(session),
340
+ status: session.status,
341
+ exitCode: session.exitCode,
342
+ };
343
+ this.emit({
344
+ type: eventType,
345
+ sessionId: session.id,
346
+ data,
347
+ });
348
+ }
349
+ async flushNextQueuedMessage(sessionId) {
350
+ const current = this.sessions.get(sessionId);
351
+ if (!current || (current.queuedMessages?.length ?? 0) === 0) {
352
+ return;
353
+ }
354
+ if (current.structuredState?.inFlight) {
355
+ return;
356
+ }
357
+ const [nextInput, ...restQueue] = current.queuedMessages ?? [];
358
+ if (!nextInput) {
359
+ return;
360
+ }
361
+ const nextSession = {
362
+ ...current,
363
+ queuedMessages: restQueue,
364
+ };
365
+ this.sessions.set(sessionId, nextSession);
366
+ this.storage.saveSession(nextSession);
367
+ this.emitStructuredSnapshot(nextSession);
368
+ try {
369
+ await this.sendMessage(sessionId, nextInput);
370
+ }
371
+ catch (error) {
372
+ console.error("[WAND] flushNextQueuedMessage failed:", error);
373
+ }
374
+ }
300
375
  emit(event) {
301
376
  if (this.emitEvent) {
302
377
  this.emitEvent(event);
@@ -431,7 +506,7 @@ export class StructuredSessionManager {
431
506
  this.emit({
432
507
  type: "output",
433
508
  sessionId,
434
- data: { output: current.output, messages: current.messages, sessionKind: "structured" },
509
+ data: buildStructuredOutputPayload(current),
435
510
  });
436
511
  };
437
512
  const scheduleEmit = () => {
@@ -462,12 +537,16 @@ export class StructuredSessionManager {
462
537
  ...current,
463
538
  claudeSessionId: turnState.sessionId ?? current.claudeSessionId,
464
539
  messages: msgs,
540
+ output: turnState.result || current.output,
465
541
  structuredState: {
466
542
  ...current.structuredState,
467
543
  model: turnState.model ?? current.structuredState?.model,
468
544
  },
469
545
  };
470
546
  this.sessions.set(sessionId, patched);
547
+ // Persist streaming progress so a server restart does not roll back the
548
+ // latest assistant turn to the pre-stream snapshot.
549
+ this.storage.saveSession(patched);
471
550
  };
472
551
  const processLine = (line) => {
473
552
  const trimmed = line.trim();
@@ -590,16 +669,8 @@ export class StructuredSessionManager {
590
669
  };
591
670
  this.sessions.set(sessionId, failed);
592
671
  this.storage.saveSession(failed);
593
- this.emit({
594
- type: "output",
595
- sessionId,
596
- data: { output: failed.output, messages: failed.messages, sessionKind: "structured" },
597
- });
598
- this.emit({
599
- type: "ended",
600
- sessionId,
601
- data: { status: failed.status, exitCode: failed.exitCode, messages: failed.messages, sessionKind: "structured", structuredState: failed.structuredState },
602
- });
672
+ this.emitStructuredSnapshot(failed);
673
+ this.emitStructuredSnapshot(failed, "ended");
603
674
  reject(new Error(errorText));
604
675
  return;
605
676
  }
@@ -639,16 +710,8 @@ export class StructuredSessionManager {
639
710
  };
640
711
  this.sessions.set(sessionId, finished);
641
712
  this.storage.saveSession(finished);
642
- this.emit({
643
- type: "output",
644
- sessionId,
645
- data: { output: finished.output, messages: finished.messages, sessionKind: "structured" },
646
- });
647
- this.emit({
648
- type: "ended",
649
- sessionId,
650
- data: { status: finished.status, exitCode: 0, messages: finished.messages, sessionKind: "structured", structuredState: finished.structuredState },
651
- });
713
+ this.emitStructuredSnapshot(finished);
714
+ this.emitStructuredSnapshot(finished, "ended");
652
715
  // Auto-continue after plan mode exit: when Claude calls ExitPlanMode,
653
716
  // the `-p` process exits because stdin is "ignore" and it cannot get
654
717
  // user confirmation. Detect this and automatically resume execution
@@ -657,8 +720,6 @@ export class StructuredSessionManager {
657
720
  if (lastToolUse && lastToolUse.name === "ExitPlanMode" && turnState.sessionId) {
658
721
  console.log("[WAND] ExitPlanMode detected – auto-continuing plan execution for session:", sessionId);
659
722
  resolve();
660
- // Schedule the continuation outside the current promise chain so it
661
- // does not block the close handler.
662
723
  setImmediate(() => {
663
724
  this.sendMessage(sessionId, "Plan approved. Proceed with the implementation.").catch((err) => {
664
725
  console.error("[WAND] Auto-continue after ExitPlanMode failed:", err);
@@ -667,6 +728,9 @@ export class StructuredSessionManager {
667
728
  return;
668
729
  }
669
730
  resolve();
731
+ setImmediate(() => {
732
+ void this.flushNextQueuedMessage(sessionId);
733
+ });
670
734
  });
671
735
  });
672
736
  }
package/dist/types.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export type SessionKind = "pty" | "structured";
2
2
  export type SessionCreateKind = "pty" | "structured";
3
+ export type SessionProvider = "claude" | "codex";
3
4
  export type SessionRunner = "claude-cli" | "claude-cli-print" | "pty";
4
5
  export type ExecutionMode = "assist" | "agent" | "agent-max" | "default" | "auto-edit" | "full-access" | "native" | "managed";
5
6
  export type AutonomyPolicy = "assist" | "agent" | "agent-max";
@@ -46,6 +47,21 @@ export interface StructuredChatPersonaConfig {
46
47
  user?: StructuredChatPersonaRoleConfig;
47
48
  assistant?: StructuredChatPersonaRoleConfig;
48
49
  }
50
+ export interface DefaultPanelStateConfig {
51
+ sessionsDrawerOpen?: boolean;
52
+ filePanelOpen?: boolean;
53
+ shortcutsExpanded?: boolean;
54
+ claudeHistoryExpanded?: boolean;
55
+ chatMessageExpanded?: boolean;
56
+ structuredThinkingExpanded?: boolean;
57
+ structuredToolGroupExpanded?: boolean;
58
+ structuredInlineToolExpanded?: boolean;
59
+ structuredTerminalExpanded?: boolean;
60
+ structuredToolCardExpanded?: boolean;
61
+ }
62
+ export interface UiPreferencesConfig {
63
+ defaultPanelState?: DefaultPanelStateConfig;
64
+ }
49
65
  export interface WandConfig {
50
66
  host: string;
51
67
  port: number;
@@ -63,12 +79,52 @@ export interface WandConfig {
63
79
  shortcutLogMaxBytes?: number;
64
80
  /** Preferred response language for Claude (e.g. "中文", "English"). Empty string means no override. */
65
81
  language?: string;
82
+ /** UI defaults used for initializing panel open/expanded state. */
83
+ uiPreferences?: UiPreferencesConfig;
84
+ }
85
+ interface WorktreeInfo {
86
+ branch: string;
87
+ path: string;
88
+ }
89
+ export interface WorktreeMergeInfo {
90
+ targetBranch?: string;
91
+ mergedAt?: string;
92
+ mergeCommit?: string;
93
+ cleanupDone?: boolean;
94
+ lastError?: string;
95
+ conflict?: boolean;
96
+ }
97
+ export interface WorktreeMergeCheckResult {
98
+ ok: boolean;
99
+ sourceBranch: string;
100
+ targetBranch: string;
101
+ worktreePath: string;
102
+ repoRoot: string;
103
+ hasUncommittedChanges: boolean;
104
+ aheadCount: number;
105
+ hasConflicts: boolean;
106
+ recommendedAction: "merge" | "noop" | "resolve-conflict";
107
+ reason?: string;
108
+ }
109
+ export interface WorktreeMergeResult {
110
+ ok: boolean;
111
+ sourceBranch: string;
112
+ targetBranch: string;
113
+ repoRoot: string;
114
+ mergeCommit?: string;
115
+ mergedAt?: string;
116
+ cleanupDone: boolean;
117
+ conflict: boolean;
118
+ errorCode?: string;
119
+ reason?: string;
66
120
  }
67
121
  export interface CommandRequest {
68
122
  command: string;
123
+ provider?: SessionProvider;
69
124
  cwd?: string;
70
125
  mode?: ExecutionMode;
71
126
  initialInput?: string;
127
+ worktreeEnabled?: boolean;
72
128
  }
73
129
  export interface InputRequest {
74
130
  input?: string;
@@ -143,6 +199,7 @@ export interface ConversationTurn {
143
199
  };
144
200
  }
145
201
  export interface StructuredSessionState {
202
+ provider?: SessionProvider;
146
203
  runner: SessionRunner;
147
204
  model?: string;
148
205
  lastError: string | null;
@@ -152,10 +209,15 @@ export interface StructuredSessionState {
152
209
  export interface SessionSnapshot {
153
210
  id: string;
154
211
  sessionKind?: SessionKind;
212
+ provider?: SessionProvider;
155
213
  runner?: SessionRunner;
156
214
  command: string;
157
215
  cwd: string;
158
216
  mode: ExecutionMode;
217
+ worktreeEnabled?: boolean;
218
+ worktree?: WorktreeInfo | null;
219
+ worktreeMergeStatus?: "ready" | "checking" | "merging" | "merged" | "failed";
220
+ worktreeMergeInfo?: WorktreeMergeInfo | null;
159
221
  autonomyPolicy?: AutonomyPolicy;
160
222
  approvalPolicy?: ApprovalPolicy;
161
223
  allowedScopes?: EscalationScope[];
@@ -178,6 +240,8 @@ export interface SessionSnapshot {
178
240
  claudeSessionId: string | null;
179
241
  /** Structured conversation messages derived from PTY output. */
180
242
  messages?: ConversationTurn[];
243
+ /** Pending structured user inputs queued while an assistant response is in flight. */
244
+ queuedMessages?: string[];
181
245
  structuredState?: StructuredSessionState;
182
246
  /** Session lifecycle state */
183
247
  lifecycleState?: "running" | "idle" | "archived";
@@ -255,3 +319,4 @@ export interface TaskData {
255
319
  export interface SessionEndData {
256
320
  exitCode: number | null;
257
321
  }
322
+ export {};