@co0ontty/wand 1.18.1 → 1.20.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.
@@ -6,12 +6,11 @@ import process from "node:process";
6
6
  import os from "node:os";
7
7
  import pty from "node-pty";
8
8
  import { SessionLogger } from "./session-logger.js";
9
- import { SessionLifecycleManager } from "./session-lifecycle.js";
10
9
  import { ClaudePtyBridge } from "./claude-pty-bridge.js";
11
10
  import { truncateMessagesForTransport } from "./message-truncator.js";
12
11
  import { appendWindow, hasExplicitConfirmSyntax, hasPermissionActionContext, normalizePromptText } from "./pty-text-utils.js";
13
12
  import { prepareSessionWorktree } from "./git-worktree.js";
14
- import { getResumeCommandSessionId, hasRealConversationMessages, } from "./resume-policy.js";
13
+ import { getResumeCommandSessionId } from "./resume-policy.js";
15
14
  function resolveProviderFromCommand(command) {
16
15
  return /^codex\b/.test(command.trim()) ? "codex" : "claude";
17
16
  }
@@ -128,52 +127,8 @@ function selectClaudeProjectSessionForRecord(record) {
128
127
  }
129
128
  return candidates[0] ?? null;
130
129
  }
131
- /**
132
- * Broader fallback: find a JSONL file by mtime proximity when strict
133
- * mtime-correlation fails (e.g., file existed before session but Claude
134
- * wrote conversation content during this session).
135
- * Looks for the most recently modified file that was active near the
136
- * session's start time and has real conversation content.
137
- */
138
- function selectClaudeProjectSessionByProximity(record) {
139
- const hasUserTurn = record.messages.some((turn) => turn.role === "user"
140
- && turn.content.some((block) => block.type === "text" && block.text.trim().length > 0));
141
- if (!hasUserTurn) {
142
- return null;
143
- }
144
- const startedAtMs = Date.parse(record.startedAt);
145
- const now = Date.now();
146
- // Look for files modified from ~60s before session start up to now
147
- const proximityWindowMs = 60 * 1000;
148
- const candidates = listClaudeProjectSessionCandidates(record.cwd)
149
- .filter((candidate) => {
150
- if (!Number.isFinite(startedAtMs))
151
- return true;
152
- return candidate.mtimeMs >= startedAtMs - proximityWindowMs
153
- && candidate.mtimeMs <= now + DISCOVERY_RECENT_WINDOW_MS;
154
- })
155
- .map((candidate) => readClaudeProjectSessionDetails(candidate.filePath, candidate.id))
156
- .filter((candidate) => Boolean(candidate?.hasConversation))
157
- .sort((a, b) => b.mtimeMs - a.mtimeMs);
158
- return candidates[0] ?? null;
159
- }
160
- function getResumeEligibility(record) {
161
- const hasClaudeSessionId = Boolean(record.claudeSessionId);
162
- const hasRealConversation = hasRealConversationMessages(record.messages);
163
- return {
164
- hasClaudeSessionId,
165
- hasRealConversation,
166
- eligible: hasClaudeSessionId && hasRealConversation
167
- };
168
- }
169
- function hasResumeEligibleConversation(record) {
170
- return getResumeEligibility(record).eligible;
171
- }
172
130
  function getLatestClaudeProjectSessionId(record) {
173
- // Try strict mtime-correlation first, then fall back to mtime proximity
174
- return selectClaudeProjectSessionForRecord(record)?.id
175
- ?? selectClaudeProjectSessionByProximity(record)?.id
176
- ?? null;
131
+ return selectClaudeProjectSessionForRecord(record)?.id ?? null;
177
132
  }
178
133
  function listRecentClaudeProjectSessionIds(cwd, startedAt) {
179
134
  return listClaudeProjectSessionCandidates(cwd)
@@ -181,33 +136,6 @@ function listRecentClaudeProjectSessionIds(cwd, startedAt) {
181
136
  .sort((a, b) => b.mtimeMs - a.mtimeMs)
182
137
  .map((candidate) => candidate.id);
183
138
  }
184
- function findRealClaudeProjectSessionId(cwd, startedAt) {
185
- // Strict mtime-based discovery first
186
- const candidates = listRecentClaudeProjectSessionIds(cwd, startedAt)
187
- .map((id) => {
188
- const filePath = path.join(getClaudeProjectDir(cwd), `${id}.jsonl`);
189
- return readClaudeProjectSessionDetails(filePath, id);
190
- })
191
- .filter((candidate) => Boolean(candidate?.hasConversation))
192
- .sort((a, b) => b.mtimeMs - a.mtimeMs);
193
- if (candidates.length > 0)
194
- return candidates[0].id;
195
- // Fallback: broader proximity search for files with conversation content
196
- const startedAtMs = Date.parse(startedAt);
197
- const now = Date.now();
198
- const proximityWindowMs = 60 * 1000;
199
- const proximityCandidates = listClaudeProjectSessionCandidates(cwd)
200
- .filter((candidate) => {
201
- if (!Number.isFinite(startedAtMs))
202
- return true;
203
- return candidate.mtimeMs >= startedAtMs - proximityWindowMs
204
- && candidate.mtimeMs <= now + DISCOVERY_RECENT_WINDOW_MS;
205
- })
206
- .map((candidate) => readClaudeProjectSessionDetails(candidate.filePath, candidate.id))
207
- .filter((candidate) => Boolean(candidate?.hasConversation))
208
- .sort((a, b) => b.mtimeMs - a.mtimeMs);
209
- return proximityCandidates[0]?.id ?? null;
210
- }
211
139
  function isClaudeSessionFileAvailable(cwd, claudeSessionId) {
212
140
  const filePath = path.join(getClaudeProjectDir(cwd), `${claudeSessionId}.jsonl`);
213
141
  return Boolean(readClaudeProjectSessionDetails(filePath, claudeSessionId));
@@ -349,19 +277,6 @@ function listAllClaudeHistorySessions() {
349
277
  return [];
350
278
  }
351
279
  }
352
- function shouldAutoResumeSession(record) {
353
- return record.status === "exited"
354
- && !record.archived
355
- && !record.resumedToSessionId
356
- && record.ptyProcess === null
357
- && hasResumeEligibleConversation(record);
358
- }
359
- function shouldBackfillClaudeSessionId(record) {
360
- return record.status === "exited"
361
- && !record.claudeSessionId
362
- && /^claude\b/.test(record.command.trim())
363
- && hasRealConversationMessages(record.messages);
364
- }
365
280
  function snapshotMessages(record) {
366
281
  return record.ptyBridge?.getMessages() ?? record.messages;
367
282
  }
@@ -451,26 +366,19 @@ export class ProcessManager extends EventEmitter {
451
366
  storage;
452
367
  sessions = new Map();
453
368
  logger;
454
- lifecycleManager;
369
+ /** 24h archive scan timer */
370
+ archiveTimer = null;
455
371
  /** Per-session debounce timers for throttled persist calls */
456
372
  persistDebounceTimers = new Map();
457
373
  /** Last persisted message state per session — used to skip redundant message writes */
458
374
  lastPersistedMessageState = new Map();
375
+ /** 启动时被识别为孤儿 PTY 并标记为 exited 的旧会话数(旧服务器进程已死) */
376
+ orphanRecoveredCount = 0;
459
377
  constructor(config, storage, configDir) {
460
378
  super();
461
379
  this.config = config;
462
380
  this.storage = storage;
463
381
  this.logger = new SessionLogger(configDir || path.join(process.env.HOME || process.cwd(), ".wand"), config.shortcutLogMaxBytes);
464
- // Initialize lifecycle manager
465
- this.lifecycleManager = new SessionLifecycleManager({
466
- onStateChange: (sessionId, oldState, newState) => {
467
- this.emitEvent({ type: "status", sessionId, data: { oldState, newState } });
468
- },
469
- onIdle: (_sessionId) => { },
470
- onArchived: (sessionId, reason) => {
471
- console.error(`[ProcessManager] Session ${sessionId} archived: ${reason}`);
472
- },
473
- });
474
382
  for (const snapshot of this.storage.loadSessions()) {
475
383
  if ((snapshot.sessionKind ?? "pty") !== "pty") {
476
384
  continue;
@@ -520,8 +428,7 @@ export class ProcessManager extends EventEmitter {
520
428
  claudeSessionId: resumeCommandSessionId ?? updated.claudeSessionId,
521
429
  approvalStats: { tool: 0, command: 0, file: 0, total: 0 }
522
430
  });
523
- this.lifecycleManager.register(snapshot.id, "idle");
524
- console.error(`[ProcessManager] Restored session ${snapshot.id} marked as exited (PTY orphaned)`);
431
+ this.orphanRecoveredCount += 1;
525
432
  }
526
433
  else {
527
434
  this.sessions.set(snapshot.id, {
@@ -554,20 +461,26 @@ export class ProcessManager extends EventEmitter {
554
461
  claudeSessionId: resumeCommandSessionId ?? snapshot.claudeSessionId,
555
462
  approvalStats: { tool: 0, command: 0, file: 0, total: 0 }
556
463
  });
557
- this.lifecycleManager.register(snapshot.id, "archived");
558
464
  }
559
465
  }
560
- // Defer expensive file-system scanning and auto-recovery so the server
561
- // can start responding to requests immediately.
562
- setImmediate(() => {
563
- this.backfillExitedClaudeSessionIds();
564
- this.autoRecoverExitedSessions();
565
- });
566
466
  this.archiveExpiredSessions();
467
+ this.archiveTimer = setInterval(() => {
468
+ try {
469
+ this.archiveExpiredSessions();
470
+ }
471
+ catch (err) {
472
+ console.error(`[ProcessManager] archive scan failed: ${String(err)}`);
473
+ }
474
+ }, 60 * 1000);
475
+ this.archiveTimer.unref?.();
567
476
  }
568
477
  on(event, listener) {
569
478
  return super.on("process", listener);
570
479
  }
480
+ /** 启动时被识别为孤儿 PTY 并标记为 exited 的旧会话数量(仅用于启动摘要展示)。 */
481
+ getOrphanRecoveredCount() {
482
+ return this.orphanRecoveredCount;
483
+ }
571
484
  emitEvent(event) {
572
485
  this.emit("process", event);
573
486
  }
@@ -670,7 +583,7 @@ export class ProcessManager extends EventEmitter {
670
583
  ptyPermissionBlocked: false,
671
584
  lastAutoConfirmAt: 0,
672
585
  autoApprovePermissions: this.shouldAutoApprovePermissions(command, effectiveMode, provider),
673
- resumedFromSessionId: opts?.resumedFromSessionId ?? null,
586
+ resumedFromSessionId: opts?.resumedFromSessionId ?? (opts?.reuseId ? opts.reuseId : null),
674
587
  autoRecovered: opts?.autoRecovered ?? false,
675
588
  rememberedEscalationScopes: new Set(),
676
589
  rememberedEscalationTargets: new Set(),
@@ -704,7 +617,6 @@ export class ProcessManager extends EventEmitter {
704
617
  this.claudeHistoryCache = null;
705
618
  }
706
619
  this.cleanupOldSessions();
707
- this.lifecycleManager.register(id, "initializing");
708
620
  const shellArgs = this.buildShellArgs(processedCommand);
709
621
  let child;
710
622
  try {
@@ -727,14 +639,12 @@ export class ProcessManager extends EventEmitter {
727
639
  record.exitCode = -1;
728
640
  record.endedAt = new Date().toISOString();
729
641
  record.ptyProcess = null;
730
- this.lifecycleManager.archive(id, "Session spawn failed", "error");
731
642
  this.persist(record);
732
643
  return this.snapshot(record);
733
644
  }
734
645
  record.processId = child.pid;
735
646
  record.ptyProcess = child;
736
647
  record.status = "running";
737
- this.lifecycleManager.setState(id, "running");
738
648
  child.onExit(({ exitCode }) => {
739
649
  const current = this.sessions.get(id);
740
650
  if (!current)
@@ -757,7 +667,6 @@ export class ProcessManager extends EventEmitter {
757
667
  current.exitCode = current.stopRequested ? null : exitCode;
758
668
  current.endedAt = new Date().toISOString();
759
669
  current.ptyProcess = null;
760
- this.lifecycleManager.archive(id, `Session ${current.status}`, current.stopRequested ? "user" : "error");
761
670
  this.flushPersist(current);
762
671
  this.storage.saveSession(this.snapshot(current));
763
672
  this.emitEvent({ type: "ended", sessionId: id, data: this.snapshot(current) });
@@ -877,14 +786,12 @@ export class ProcessManager extends EventEmitter {
877
786
  return this.snapshot(record);
878
787
  }
879
788
  list() {
880
- this.archiveExpiredSessions();
881
789
  return Array.from(this.sessions.values())
882
790
  .sort((a, b) => b.startedAt.localeCompare(a.startedAt))
883
791
  .map((session) => this.snapshot(session));
884
792
  }
885
793
  /** Return lightweight snapshots for the session list (no output/messages). */
886
794
  listSlim() {
887
- this.archiveExpiredSessions();
888
795
  return Array.from(this.sessions.values())
889
796
  .sort((a, b) => b.startedAt.localeCompare(a.startedAt))
890
797
  .map((session) => this.snapshotSlim(session));
@@ -947,18 +854,15 @@ export class ProcessManager extends EventEmitter {
947
854
  return deleted;
948
855
  }
949
856
  get(id) {
950
- this.archiveExpiredSessions();
951
857
  const record = this.sessions.get(id);
952
858
  if (!record) {
953
- // Fallback: check SQLite for sessions that were evicted from memory
954
859
  return this.storage.getSession(id) ?? null;
955
860
  }
956
- // For sessions loaded from storage on startup, in-memory output starts empty.
957
- // Prefer in-memory output (live PTY data), fall back to stored output.
861
+ const result = this.snapshot(record);
958
862
  if (!record.output && record.storedOutput) {
959
- record.output = record.storedOutput;
863
+ result.output = record.storedOutput;
960
864
  }
961
- return this.snapshot(record);
865
+ return result;
962
866
  }
963
867
  getPtyTranscript(id) {
964
868
  return this.logger.readPtyOutput(id);
@@ -990,8 +894,6 @@ export class ProcessManager extends EventEmitter {
990
894
  throw new SessionInputError("Session is not running.", "SESSION_NOT_RUNNING", id, record.status);
991
895
  }
992
896
  // Update lifecycle
993
- this.lifecycleManager.touch(id);
994
- this.lifecycleManager.startThinking(id);
995
897
  if (!record.ptyProcess) {
996
898
  console.error(`[ProcessManager] Rejecting input: session ${id} has no PTY`);
997
899
  throw new SessionInputError("Session is not running.", "SESSION_NO_PTY", id, record.status);
@@ -1003,7 +905,7 @@ export class ProcessManager extends EventEmitter {
1003
905
  const ctx = {
1004
906
  mode: record.mode,
1005
907
  autoApprove: record.autoApprovePermissions,
1006
- permissionBlocked: record.ptyPermissionBlocked || !!record.pendingEscalation,
908
+ permissionBlocked: this.isPermissionBlocked(record),
1007
909
  input,
1008
910
  };
1009
911
  this.logger.appendShortcutLog(id, shortcutKey, tailLines, ctx);
@@ -1093,7 +995,6 @@ export class ProcessManager extends EventEmitter {
1093
995
  record.ptyBridge = null;
1094
996
  }
1095
997
  // Update lifecycle
1096
- this.lifecycleManager.archive(id, "Session stopped by user", "user");
1097
998
  this.persist(record);
1098
999
  return this.snapshot(record);
1099
1000
  }
@@ -1130,7 +1031,6 @@ export class ProcessManager extends EventEmitter {
1130
1031
  record.ptyBridge.removeAllListeners();
1131
1032
  record.ptyBridge = null;
1132
1033
  }
1133
- this.lifecycleManager.unregister(record.id);
1134
1034
  }
1135
1035
  delete(id) {
1136
1036
  const record = this.mustGet(id);
@@ -1183,7 +1083,6 @@ export class ProcessManager extends EventEmitter {
1183
1083
  this.deleteClaudeCache(record);
1184
1084
  this.sessions.delete(id);
1185
1085
  this.lastPersistedMessageState.delete(id);
1186
- this.lifecycleManager.unregister(id);
1187
1086
  if (record.claudeSessionId) {
1188
1087
  this.claudeHistoryCache = null;
1189
1088
  }
@@ -1246,7 +1145,6 @@ export class ProcessManager extends EventEmitter {
1246
1145
  claudeSessionId: record.claudeSessionId || null,
1247
1146
  messages: messages.length > 0 ? messages : undefined,
1248
1147
  resumedFromSessionId: record.resumedFromSessionId ?? undefined,
1249
- resumedToSessionId: record.resumedToSessionId ?? undefined,
1250
1148
  autoRecovered: record.autoRecovered ?? false,
1251
1149
  autoApprovePermissions: record.autoApprovePermissions || undefined,
1252
1150
  approvalStats: record.approvalStats.total > 0 ? record.approvalStats : undefined,
@@ -1265,7 +1163,7 @@ export class ProcessManager extends EventEmitter {
1265
1163
  };
1266
1164
  }
1267
1165
  isPermissionBlocked(record) {
1268
- return record.ptyBridge?.isPermissionBlocked() ?? record.pendingEscalation !== null;
1166
+ return record.ptyPermissionBlocked || record.pendingEscalation !== null;
1269
1167
  }
1270
1168
  defaultAutonomyPolicy(mode) {
1271
1169
  if (mode === "agent" || mode === "agent-max" || mode === "managed" || mode === "native" || mode === "full-access") {
@@ -1357,7 +1255,6 @@ export class ProcessManager extends EventEmitter {
1357
1255
  endedAt: record.endedAt,
1358
1256
  claudeSessionId: record.claudeSessionId,
1359
1257
  resumedFromSessionId: record.resumedFromSessionId ?? null,
1360
- resumedToSessionId: record.resumedToSessionId ?? null,
1361
1258
  autoRecovered: record.autoRecovered ?? false,
1362
1259
  });
1363
1260
  if (shouldSaveMessages) {
@@ -1392,70 +1289,6 @@ export class ProcessManager extends EventEmitter {
1392
1289
  }
1393
1290
  this.persist(record);
1394
1291
  }
1395
- backfillExitedClaudeSessionIds() {
1396
- for (const record of this.sessions.values()) {
1397
- record.messages = snapshotMessages(record);
1398
- if (!shouldBackfillClaudeSessionId(record)) {
1399
- continue;
1400
- }
1401
- const discoveredSessionId = findRealClaudeProjectSessionId(record.cwd, record.startedAt);
1402
- if (!discoveredSessionId) {
1403
- continue;
1404
- }
1405
- record.claudeSessionId = discoveredSessionId;
1406
- this.persist(record);
1407
- }
1408
- }
1409
- /**
1410
- * Auto-recover the most recent exited session that has a Claude session ID.
1411
- * Only resumes one session per server start, using the most recent eligible
1412
- * session. Reuses the original session ID (in-place resume) and sets
1413
- * `autoRecovered: true`.
1414
- */
1415
- autoRecoverExitedSessions() {
1416
- // Find eligible exited sessions
1417
- const eligibleSessions = [];
1418
- for (const record of this.sessions.values()) {
1419
- record.messages = snapshotMessages(record);
1420
- if (shouldAutoResumeSession(record)) {
1421
- eligibleSessions.push(record);
1422
- }
1423
- }
1424
- if (eligibleSessions.length === 0)
1425
- return;
1426
- // Sort by startedAt descending (most recent first)
1427
- eligibleSessions.sort((a, b) => b.startedAt.localeCompare(a.startedAt));
1428
- // Only auto-recover the single most recent session
1429
- const original = eligibleSessions[0];
1430
- const isClaude = /^claude\b/.test(original.command.trim());
1431
- if (!isClaude)
1432
- return;
1433
- // If no claudeSessionId is bound yet, try to discover it via proximity search
1434
- if (!original.claudeSessionId) {
1435
- const discovered = findRealClaudeProjectSessionId(original.cwd, original.startedAt);
1436
- if (discovered) {
1437
- original.claudeSessionId = discovered;
1438
- process.stderr.write(`[wand] Backfilled Claude session ID for auto-recovery: ${discovered}\n`);
1439
- this.persist(original);
1440
- }
1441
- }
1442
- if (!original.claudeSessionId) {
1443
- console.error(`[ProcessManager] Skipping auto-recovery: no Claude session ID for session ${original.id}`);
1444
- return;
1445
- }
1446
- console.error(`[ProcessManager] Auto-recovering session ${original.id} with Claude session ID ${original.claudeSessionId}`);
1447
- const resumeCommand = `${original.command.trim()} --resume ${original.claudeSessionId}`;
1448
- try {
1449
- const snapshot = this.start(resumeCommand, original.cwd, original.mode, undefined, {
1450
- reuseId: original.id,
1451
- autoRecovered: true
1452
- });
1453
- console.error(`[ProcessManager] Auto-recovered session ${snapshot.id} (in-place)`);
1454
- }
1455
- catch (err) {
1456
- console.error(`[ProcessManager] Auto-recovery failed: ${String(err)}`);
1457
- }
1458
- }
1459
1292
  archiveExpiredSessions() {
1460
1293
  const now = Date.now();
1461
1294
  for (const record of this.sessions.values()) {
@@ -1637,8 +1470,6 @@ export class ProcessManager extends EventEmitter {
1637
1470
  record.ptyBridge?.clearRememberedPermissions();
1638
1471
  record.rememberedEscalationScopes.clear();
1639
1472
  record.rememberedEscalationTargets.clear();
1640
- this.lifecycleManager.stopThinking(record.id);
1641
- this.lifecycleManager.waitingInput(record.id);
1642
1473
  this.persist(record);
1643
1474
  this.storage.saveSession(this.snapshot(record));
1644
1475
  break;
@@ -0,0 +1,5 @@
1
+ export declare class PromptOptimizeError extends Error {
2
+ readonly code: string;
3
+ constructor(message: string, code: string);
4
+ }
5
+ export declare function optimizePrompt(rawText: string, language: string, cwd?: string): Promise<string>;
@@ -0,0 +1,72 @@
1
+ import { execFile } from "node:child_process";
2
+ const CLAUDE_TIMEOUT_MS = 60_000;
3
+ const MAX_INPUT_LENGTH = 8000;
4
+ export class PromptOptimizeError extends Error {
5
+ code;
6
+ constructor(message, code) {
7
+ super(message);
8
+ this.code = code;
9
+ this.name = "PromptOptimizeError";
10
+ }
11
+ }
12
+ function callClaudeText(prompt, cwd) {
13
+ return new Promise((resolve, reject) => {
14
+ const child = execFile("claude", ["-p", "--output-format", "text"], {
15
+ cwd: cwd && cwd.length > 0 ? cwd : undefined,
16
+ encoding: "utf8",
17
+ maxBuffer: 4 * 1024 * 1024,
18
+ timeout: CLAUDE_TIMEOUT_MS,
19
+ }, (error, stdout, stderr) => {
20
+ if (error) {
21
+ const e = error;
22
+ if (e.code === "ENOENT") {
23
+ reject(new PromptOptimizeError("未找到 claude CLI。", "CLAUDE_CLI_MISSING"));
24
+ return;
25
+ }
26
+ if (e.code === "ETIMEDOUT") {
27
+ reject(new PromptOptimizeError("Claude 优化超时,请稍后重试。", "CLAUDE_TIMEOUT"));
28
+ return;
29
+ }
30
+ const msg = (stderr || "").trim() || e.message || "claude 调用失败";
31
+ reject(new PromptOptimizeError(`Claude CLI 失败:${msg}`, "CLAUDE_CLI_FAILED"));
32
+ return;
33
+ }
34
+ resolve((stdout || "").trim());
35
+ });
36
+ child.stdin?.end(prompt);
37
+ });
38
+ }
39
+ function buildOptimizePrompt(userInput, language) {
40
+ const lang = (language || "").trim() || "中文";
41
+ return [
42
+ `你是一名提示词优化助手。请把用户写给编码 AI 的「原始提示词」改写得更清晰、结构化、可执行,便于 AI 理解并完成任务。`,
43
+ `要求:`,
44
+ `1. 保留用户原意和所有关键信息(文件路径、变量名、技术名词、数字、约束等),不要删减事实,也不要新增臆测的需求。`,
45
+ `2. 必要时拆分为「目标 / 上下文 / 约束 / 验收标准」几个部分;如果原文很短或很简单,则只做语句润色,不要硬塞结构。`,
46
+ `3. 用${lang}输出。语气克制专业,不寒暄、不解释你做了什么。`,
47
+ `4. 只输出优化后的提示词正文,不要包裹在代码块或引号里,不要加任何前后缀(比如「优化后:」之类)。`,
48
+ ``,
49
+ `原始提示词:`,
50
+ userInput,
51
+ ].join("\n");
52
+ }
53
+ export async function optimizePrompt(rawText, language, cwd) {
54
+ const text = (rawText || "").trim();
55
+ if (!text) {
56
+ throw new PromptOptimizeError("请先输入要优化的内容。", "EMPTY_INPUT");
57
+ }
58
+ if (text.length > MAX_INPUT_LENGTH) {
59
+ throw new PromptOptimizeError(`输入过长(${text.length} 字符),请缩短到 ${MAX_INPUT_LENGTH} 以内。`, "INPUT_TOO_LONG");
60
+ }
61
+ const prompt = buildOptimizePrompt(text, language);
62
+ const raw = await callClaudeText(prompt, cwd);
63
+ const cleaned = raw
64
+ .replace(/^```[a-zA-Z]*\n?/, "")
65
+ .replace(/\n?```$/, "")
66
+ .replace(/^["'`]+|["'`]+$/g, "")
67
+ .trim();
68
+ if (!cleaned) {
69
+ throw new PromptOptimizeError("Claude 返回了空结果。", "EMPTY_RESULT");
70
+ }
71
+ return cleaned;
72
+ }
@@ -1,7 +1,5 @@
1
1
  /**
2
- * Shared PTY text processing utilities.
3
- * Used by both claude-pty-bridge.ts and message-parser.ts to ensure
4
- * consistent ANSI stripping and noise filtering.
2
+ * Shared PTY text processing utilities for consistent ANSI stripping and noise filtering.
5
3
  */
6
4
  /** Strip ANSI escape sequences and control characters from raw PTY output. */
7
5
  export declare function stripAnsi(text: string): string;
@@ -1,7 +1,5 @@
1
1
  /**
2
- * Shared PTY text processing utilities.
3
- * Used by both claude-pty-bridge.ts and message-parser.ts to ensure
4
- * consistent ANSI stripping and noise filtering.
2
+ * Shared PTY text processing utilities for consistent ANSI stripping and noise filtering.
5
3
  */
6
4
  /** Strip ANSI escape sequences and control characters from raw PTY output. */
7
5
  export function stripAnsi(text) {
@@ -2,7 +2,7 @@ import { Express } from "express";
2
2
  import { ProcessManager } from "./process-manager.js";
3
3
  import { StructuredSessionManager } from "./structured-session-manager.js";
4
4
  import { WandStorage } from "./storage.js";
5
- import { ExecutionMode } from "./types.js";
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): void;
7
+ export declare function registerSessionRoutes(app: Express, processes: ProcessManager, structured: StructuredSessionManager, storage: WandStorage, defaultMode: ExecutionMode, config: WandConfig): void;
8
8
  export declare function registerClaudeHistoryRoutes(app: Express, processes: ProcessManager, storage: WandStorage): void;
@@ -1,7 +1,7 @@
1
1
  import express from "express";
2
- import { parseMessages } from "./message-parser.js";
3
2
  import { SessionInputError } from "./process-manager.js";
4
3
  import { checkSessionWorktreeMergeability, cleanupSessionWorktree, getWorktreeMergeErrorCode, mergeSessionWorktree, WorktreeMergeError } from "./git-worktree.js";
4
+ import { getGitStatus, QuickCommitError, runQuickCommit, generateCommitMessageOnly } from "./git-quick-commit.js";
5
5
  export function getErrorMessage(error, fallback) {
6
6
  return error instanceof Error ? error.message : fallback;
7
7
  }
@@ -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) {
139
+ export function registerSessionRoutes(app, processes, structured, storage, defaultMode, config) {
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 })));
@@ -198,9 +198,10 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
198
198
  });
199
199
  app.post("/api/structured-sessions/:id/messages", express.json(), async (req, res) => {
200
200
  const input = String(req.body?.input ?? "");
201
- console.log("[WAND] POST /api/structured-sessions/:id/messages id:", req.params.id, "input:", input.substring(0, 50));
201
+ 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);
202
203
  try {
203
- const snapshot = await structured.sendMessage(req.params.id, input);
204
+ const snapshot = await structured.sendMessage(req.params.id, input, { interrupt });
204
205
  res.json(snapshot);
205
206
  }
206
207
  catch (error) {
@@ -302,6 +303,77 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
302
303
  res.status(getWorktreeMergeResponseStatus(error)).json(getWorktreeMergePayload(error, "无法合并 worktree。"));
303
304
  }
304
305
  });
306
+ app.get("/api/sessions/:id/git-status", (req, res) => {
307
+ const snapshot = getLatestSessionSnapshot(processes, structured, storage, req.params.id);
308
+ if (!snapshot) {
309
+ res.status(404).json({ error: "未找到该会话。" });
310
+ return;
311
+ }
312
+ if (!snapshot.cwd) {
313
+ res.json({ isGit: false });
314
+ return;
315
+ }
316
+ try {
317
+ res.json(getGitStatus(snapshot.cwd));
318
+ }
319
+ catch (error) {
320
+ res.json({ isGit: false, error: getErrorMessage(error, "无法读取 git 状态。") });
321
+ }
322
+ });
323
+ app.post("/api/sessions/:id/quick-commit", express.json(), async (req, res) => {
324
+ const snapshot = getLatestSessionSnapshot(processes, structured, storage, req.params.id);
325
+ if (!snapshot) {
326
+ res.status(404).json({ error: "未找到该会话。" });
327
+ return;
328
+ }
329
+ if (!snapshot.cwd) {
330
+ res.status(400).json({ error: "会话没有工作目录。", errorCode: "NO_CWD" });
331
+ return;
332
+ }
333
+ const body = (req.body ?? {});
334
+ try {
335
+ const result = await runQuickCommit({
336
+ cwd: snapshot.cwd,
337
+ language: config.language ?? "",
338
+ autoMessage: body.autoMessage !== false,
339
+ customMessage: typeof body.customMessage === "string" ? body.customMessage : undefined,
340
+ tag: typeof body.tag === "string" ? body.tag : undefined,
341
+ autoTag: !!body.autoTag,
342
+ push: !!body.push,
343
+ });
344
+ res.json(result);
345
+ }
346
+ catch (error) {
347
+ if (error instanceof QuickCommitError) {
348
+ const status = error.code === "NOTHING_TO_COMMIT" || error.code === "TAG_EXISTS" ? 409 : 400;
349
+ res.status(status).json({ error: error.message, errorCode: error.code });
350
+ return;
351
+ }
352
+ res.status(400).json({ error: getErrorMessage(error, "快捷提交失败。") });
353
+ }
354
+ });
355
+ app.post("/api/sessions/:id/generate-commit-message", express.json(), async (req, res) => {
356
+ const snapshot = getLatestSessionSnapshot(processes, structured, storage, req.params.id);
357
+ if (!snapshot) {
358
+ res.status(404).json({ error: "未找到该会话。" });
359
+ return;
360
+ }
361
+ if (!snapshot.cwd) {
362
+ res.status(400).json({ error: "会话没有工作目录。" });
363
+ return;
364
+ }
365
+ try {
366
+ const message = await generateCommitMessageOnly(snapshot.cwd, config.language ?? "");
367
+ res.json({ message });
368
+ }
369
+ catch (error) {
370
+ if (error instanceof QuickCommitError) {
371
+ res.status(400).json({ error: error.message, errorCode: error.code });
372
+ return;
373
+ }
374
+ res.status(400).json({ error: getErrorMessage(error, "生成 commit message 失败。") });
375
+ }
376
+ });
305
377
  app.post("/api/sessions/:id/worktree/cleanup", (req, res) => {
306
378
  try {
307
379
  const current = requireWorktreeSession(getLatestSessionSnapshot(processes, structured, storage, req.params.id));
@@ -362,13 +434,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
362
434
  ? processes.getPtyTranscript(snapshot.id) ?? snapshot.output
363
435
  : snapshot.output;
364
436
  if (req.query.format === "chat") {
365
- const allowFallback = (snapshot.sessionKind ?? "pty") === "pty";
366
- const fallbackOutput = allowFallback ? transcriptOutput : "";
367
- const messages = snapshot.messages && snapshot.messages.length > 0
368
- ? snapshot.messages
369
- : allowFallback
370
- ? parseMessages(fallbackOutput, snapshot.command)
371
- : [];
437
+ const messages = snapshot.messages ?? [];
372
438
  res.json({ ...snapshot, output: transcriptOutput, messages });
373
439
  }
374
440
  else {
@@ -407,7 +473,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
407
473
  const newMode = body.mode ? normalizeMode(body.mode, defaultMode) : normalizeMode(existingSession.mode, defaultMode);
408
474
  const resumeCommand = `${command} --resume ${claudeSessionId}`;
409
475
  const newSnapshot = processes.start(resumeCommand, existingSession.cwd, newMode, undefined, { reuseId: sessionId });
410
- res.status(201).json({ resumedFromSessionId: sessionId, ...newSnapshot });
476
+ res.status(201).json(newSnapshot);
411
477
  }
412
478
  catch (error) {
413
479
  res.status(400).json({ error: getErrorMessage(error, "无法恢复会话。") });
@@ -444,7 +510,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
444
510
  const newMode = body.mode ? normalizeMode(body.mode, defaultMode) : normalizeMode(existingSession.mode, defaultMode);
445
511
  const resumeCommand = `${command} --resume ${claudeSessionId}`;
446
512
  const newSnapshot = processes.start(resumeCommand, existingSession.cwd, newMode, undefined, { reuseId: existingSession.id });
447
- res.status(201).json({ resumedFromSessionId: existingSession.id, resumedClaudeSessionId: claudeSessionId, ...newSnapshot });
513
+ res.status(201).json({ resumedClaudeSessionId: claudeSessionId, ...newSnapshot });
448
514
  }
449
515
  else {
450
516
  const cwd = body.cwd?.trim();