@co0ontty/wand 1.39.1 → 1.40.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.
package/dist/cli.js CHANGED
File without changes
@@ -64,6 +64,20 @@ function resolvePushRemote(cwd) {
64
64
  }
65
65
  return "origin";
66
66
  }
67
+ /**
68
+ * Derive a default next-version tag from the latest existing tag by bumping the
69
+ * patch component (preserving an optional `v` prefix). Returns a sane starting
70
+ * version when there's no tag yet, or `undefined` if the latest tag isn't semver-ish.
71
+ */
72
+ function computeSuggestedTag(latestTag) {
73
+ if (!latestTag)
74
+ return "v0.1.0";
75
+ const m = latestTag.match(/^(v?)(\d+)\.(\d+)\.(\d+)$/);
76
+ if (!m)
77
+ return undefined;
78
+ const [, prefix, major, minor, patch] = m;
79
+ return `${prefix}${major}.${minor}.${Number.parseInt(patch, 10) + 1}`;
80
+ }
67
81
  function unquotePath(raw) {
68
82
  if (raw.startsWith("\"") && raw.endsWith("\"")) {
69
83
  return raw.slice(1, -1).replace(/\\"/g, "\"").replace(/\\\\/g, "\\");
@@ -217,6 +231,17 @@ export function getGitStatus(cwd) {
217
231
  // `ls-remote` is a synchronous network call that can block the event loop
218
232
  // for seconds. The "unpushed tag" UI chip is best-effort and a separate
219
233
  // async endpoint should compute it on demand if reintroduced.
234
+ // Latest tag + a locally-derived next-version suggestion (both git-local, fast).
235
+ let latestTag;
236
+ if (!initialCommit) {
237
+ try {
238
+ latestTag = runGit(["describe", "--tags", "--abbrev=0"], cwd) || undefined;
239
+ }
240
+ catch {
241
+ latestTag = undefined;
242
+ }
243
+ }
244
+ const suggestedTag = computeSuggestedTag(latestTag);
220
245
  return {
221
246
  isGit: true,
222
247
  branch,
@@ -229,6 +254,8 @@ export function getGitStatus(cwd) {
229
254
  ahead,
230
255
  behind,
231
256
  lastCommit,
257
+ latestTag,
258
+ suggestedTag,
232
259
  };
233
260
  }
234
261
  export class QuickCommitError extends Error {
@@ -81,6 +81,7 @@ export declare class ProcessManager extends EventEmitter {
81
81
  listCodexHistorySessions(): CodexHistorySession[];
82
82
  hasCodexSessionFile(threadId: string): boolean;
83
83
  deleteCodexHistoryFiles(threadIds: string[]): number;
84
+ private captureCodexSessionId;
84
85
  get(id: string): SessionSnapshot | null;
85
86
  getPtyTranscript(id: string): string | null;
86
87
  /**
@@ -12,7 +12,7 @@ import { appendWindow, hasExplicitConfirmSyntax, hasPermissionActionContext, nor
12
12
  import { buildChildEnv } from "./env-utils.js";
13
13
  import { buildLanguageDirective } from "./language-prompt.js";
14
14
  import { prepareSessionWorktree } from "./git-worktree.js";
15
- import { getResumeCommandSessionId } from "./resume-policy.js";
15
+ import { getCodexResumeCommandSessionId, getResumeCommandSessionId } from "./resume-policy.js";
16
16
  import { applyThinkingEffortToPrompt, normalizeThinkingEffort } from "./structured-session-manager.js";
17
17
  function resolveProviderFromCommand(command) {
18
18
  return /^codex\b/.test(command.trim()) ? "codex" : "claude";
@@ -421,6 +421,40 @@ function listAllCodexHistorySessions() {
421
421
  }
422
422
  return Array.from(byThread.values()).sort((a, b) => b.mtimeMs - a.mtimeMs);
423
423
  }
424
+ function listCodexSessionMtimes() {
425
+ return new Map(listAllCodexHistorySessions().map((session) => [session.claudeSessionId, session.mtimeMs]));
426
+ }
427
+ function isSameResolvedPath(left, right) {
428
+ if (!left || !right)
429
+ return false;
430
+ return path.resolve(left) === path.resolve(right);
431
+ }
432
+ function selectCodexSessionForRecord(record) {
433
+ const knownMtimes = record.knownCodexSessionMtimes ?? new Map();
434
+ const candidates = listAllCodexHistorySessions()
435
+ .filter((session) => session.hasConversation)
436
+ .filter((session) => isSameResolvedPath(session.cwd, record.cwd))
437
+ .filter((session) => {
438
+ const previousMtime = knownMtimes.get(session.claudeSessionId);
439
+ return previousMtime === undefined || session.mtimeMs > previousMtime;
440
+ })
441
+ .filter((session) => hasRecentProjectActivity(session, record.startedAt))
442
+ .sort((a, b) => b.mtimeMs - a.mtimeMs);
443
+ if (candidates.length === 0) {
444
+ return null;
445
+ }
446
+ const fresh = candidates.filter((session) => !knownMtimes.has(session.claudeSessionId));
447
+ if (fresh.length === 1) {
448
+ return fresh[0];
449
+ }
450
+ if (fresh.length > 1) {
451
+ return null;
452
+ }
453
+ return candidates.length === 1 ? candidates[0] : null;
454
+ }
455
+ function getLatestCodexSessionId(record) {
456
+ return selectCodexSessionForRecord(record)?.claudeSessionId ?? null;
457
+ }
424
458
  /** Delete every rollout file belonging to the given codex thread ids. */
425
459
  function deleteCodexRolloutFiles(threadIds) {
426
460
  if (threadIds.size === 0)
@@ -548,7 +582,12 @@ export class ProcessManager extends EventEmitter {
548
582
  }
549
583
  const provider = snapshot.provider ?? resolveProviderFromCommand(snapshot.command);
550
584
  const isClaudeCmd = provider === "claude";
551
- const resumeCommandSessionId = isClaudeCmd ? getResumeCommandSessionId(snapshot.command) : null;
585
+ const isCodexCmd = provider === "codex";
586
+ const resumeCommandSessionId = isClaudeCmd
587
+ ? getResumeCommandSessionId(snapshot.command)
588
+ : isCodexCmd
589
+ ? getCodexResumeCommandSessionId(snapshot.command)
590
+ : null;
552
591
  // Sessions restored from storage have ptyProcess: null — the old server's PTY
553
592
  // belongs to a dead process. Mark running sessions as exited so the UI
554
593
  // reflects reality and users can start fresh sessions.
@@ -591,6 +630,8 @@ export class ProcessManager extends EventEmitter {
591
630
  knownClaudeTaskIds: undefined,
592
631
  claudeTaskDiscoveryTimer: null,
593
632
  knownClaudeProjectMtimes: isClaudeCmd ? listClaudeProjectSessionMtimes(updated.cwd) : undefined,
633
+ knownCodexSessionMtimes: isCodexCmd ? listCodexSessionMtimes() : undefined,
634
+ codexSessionDiscoveryTimer: null,
594
635
  claudeSessionId: resumeCommandSessionId ?? updated.claudeSessionId,
595
636
  approvalStats: { tool: 0, command: 0, file: 0, total: 0 },
596
637
  ptyCols: snapshot.ptyCols ?? 120,
@@ -627,6 +668,8 @@ export class ProcessManager extends EventEmitter {
627
668
  knownClaudeTaskIds: undefined,
628
669
  claudeTaskDiscoveryTimer: null,
629
670
  knownClaudeProjectMtimes: isClaudeCmd ? listClaudeProjectSessionMtimes(snapshot.cwd) : undefined,
671
+ knownCodexSessionMtimes: isCodexCmd ? listCodexSessionMtimes() : undefined,
672
+ codexSessionDiscoveryTimer: null,
630
673
  claudeSessionId: resumeCommandSessionId ?? snapshot.claudeSessionId,
631
674
  approvalStats: { tool: 0, command: 0, file: 0, total: 0 },
632
675
  ptyCols: snapshot.ptyCols ?? 120,
@@ -686,7 +729,9 @@ export class ProcessManager extends EventEmitter {
686
729
  const record = this.sessions.get(id);
687
730
  if (record) {
688
731
  this.logger.deleteSession(id);
689
- this.deleteClaudeCache(record);
732
+ if (record.provider === "claude") {
733
+ this.deleteClaudeCache(record);
734
+ }
690
735
  }
691
736
  this.sessions.delete(id);
692
737
  this.lastPersistedMessageState.delete(id);
@@ -694,6 +739,7 @@ export class ProcessManager extends EventEmitter {
694
739
  }
695
740
  if (toRemove.length > 0) {
696
741
  this.claudeHistoryCache = null;
742
+ this.codexHistoryCache = null;
697
743
  }
698
744
  }
699
745
  start(command, cwd, mode, initialInput, opts) {
@@ -731,9 +777,16 @@ export class ProcessManager extends EventEmitter {
731
777
  const resumeCommandSessionId = isClaudeProvider
732
778
  ? getResumeCommandSessionId(processedCommand) ?? getResumeCommandSessionId(command)
733
779
  : null;
780
+ const isCodexProvider = provider === "codex";
781
+ const codexResumeCommandSessionId = isCodexProvider
782
+ ? getCodexResumeCommandSessionId(processedCommand) ?? getCodexResumeCommandSessionId(command)
783
+ : null;
734
784
  const knownClaudeTaskIds = isClaudeProvider ? new Set(listRecentClaudeProjectSessionIds(resolvedCwd, new Date().toISOString())) : null;
735
785
  const knownClaudeProjectMtimes = isClaudeProvider ? listClaudeProjectSessionMtimes(resolvedCwd) : null;
736
- const initialClaudeSessionId = isClaudeProvider ? resumeCommandSessionId ?? null : null;
786
+ const knownCodexSessionMtimes = isCodexProvider && !codexResumeCommandSessionId ? listCodexSessionMtimes() : null;
787
+ const initialClaudeSessionId = isClaudeProvider
788
+ ? resumeCommandSessionId ?? null
789
+ : codexResumeCommandSessionId ?? null;
737
790
  const startedAt = new Date().toISOString();
738
791
  const record = {
739
792
  id,
@@ -778,6 +831,8 @@ export class ProcessManager extends EventEmitter {
778
831
  knownClaudeTaskIds: knownClaudeTaskIds ?? undefined,
779
832
  claudeTaskDiscoveryTimer: null,
780
833
  knownClaudeProjectMtimes: knownClaudeProjectMtimes ?? undefined,
834
+ knownCodexSessionMtimes: knownCodexSessionMtimes ?? undefined,
835
+ codexSessionDiscoveryTimer: null,
781
836
  approvalStats: { tool: 0, command: 0, file: 0, total: 0 },
782
837
  selectedModel: selectedModel ?? null,
783
838
  thinkingEffort: normalizeThinkingEffort(opts?.thinkingEffort),
@@ -800,9 +855,14 @@ export class ProcessManager extends EventEmitter {
800
855
  });
801
856
  }
802
857
  this.sessions.set(id, record);
803
- this.persist(record);
858
+ this.persist(record, { forceFullSave: true });
804
859
  if (initialClaudeSessionId) {
805
- this.claudeHistoryCache = null;
860
+ if (provider === "codex") {
861
+ this.codexHistoryCache = null;
862
+ }
863
+ else {
864
+ this.claudeHistoryCache = null;
865
+ }
806
866
  }
807
867
  this.cleanupOldSessions();
808
868
  const shellArgs = this.buildShellArgs(processedCommand);
@@ -842,6 +902,10 @@ export class ProcessManager extends EventEmitter {
842
902
  clearTimeout(current.claudeTaskDiscoveryTimer);
843
903
  current.claudeTaskDiscoveryTimer = null;
844
904
  }
905
+ if (current.codexSessionDiscoveryTimer) {
906
+ clearTimeout(current.codexSessionDiscoveryTimer);
907
+ current.codexSessionDiscoveryTimer = null;
908
+ }
845
909
  if (current.initialInputTimer) {
846
910
  clearTimeout(current.initialInputTimer);
847
911
  current.initialInputTimer = null;
@@ -852,6 +916,7 @@ export class ProcessManager extends EventEmitter {
852
916
  }
853
917
  current.pendingEscalation = null;
854
918
  current.ptyPermissionBlocked = false;
919
+ this.captureCodexSessionId(current);
855
920
  current.status = current.stopRequested ? "stopped" : exitCode === 0 ? "exited" : "failed";
856
921
  current.exitCode = current.stopRequested ? null : exitCode;
857
922
  current.endedAt = new Date().toISOString();
@@ -972,6 +1037,25 @@ export class ProcessManager extends EventEmitter {
972
1037
  };
973
1038
  record.claudeTaskDiscoveryTimer = setTimeout(tryDiscoverClaudeTaskId, 500);
974
1039
  }
1040
+ if (record.knownCodexSessionMtimes) {
1041
+ const tryDiscoverCodexSessionId = () => {
1042
+ const current = this.sessions.get(id);
1043
+ if (!current || current.status !== "running" || current.claudeSessionId || !current.knownCodexSessionMtimes) {
1044
+ return;
1045
+ }
1046
+ if (getCodexResumeCommandSessionId(current.command)) {
1047
+ current.codexSessionDiscoveryTimer = null;
1048
+ return;
1049
+ }
1050
+ if (this.captureCodexSessionId(current)) {
1051
+ current.codexSessionDiscoveryTimer = null;
1052
+ this.persist(current);
1053
+ return;
1054
+ }
1055
+ current.codexSessionDiscoveryTimer = setTimeout(tryDiscoverCodexSessionId, 1000);
1056
+ };
1057
+ record.codexSessionDiscoveryTimer = setTimeout(tryDiscoverCodexSessionId, 500);
1058
+ }
975
1059
  return this.snapshot(record);
976
1060
  }
977
1061
  list() {
@@ -999,7 +1083,7 @@ export class ProcessManager extends EventEmitter {
999
1083
  // Cross-reference with wand-managed sessions
1000
1084
  const managedClaudeIds = new Set();
1001
1085
  for (const record of this.sessions.values()) {
1002
- if (record.claudeSessionId) {
1086
+ if (record.provider === "claude" && record.claudeSessionId) {
1003
1087
  managedClaudeIds.add(record.claudeSessionId);
1004
1088
  }
1005
1089
  }
@@ -1077,6 +1161,24 @@ export class ProcessManager extends EventEmitter {
1077
1161
  }
1078
1162
  return deleted;
1079
1163
  }
1164
+ captureCodexSessionId(record) {
1165
+ if (record.provider !== "codex" || record.claudeSessionId || !record.knownCodexSessionMtimes) {
1166
+ return false;
1167
+ }
1168
+ const discoveredThreadId = getLatestCodexSessionId({
1169
+ cwd: record.cwd,
1170
+ startedAt: record.startedAt,
1171
+ knownCodexSessionMtimes: record.knownCodexSessionMtimes,
1172
+ });
1173
+ if (!discoveredThreadId) {
1174
+ return false;
1175
+ }
1176
+ record.claudeSessionId = discoveredThreadId;
1177
+ record.knownCodexSessionMtimes.set(discoveredThreadId, Date.now());
1178
+ this.codexHistoryCache = null;
1179
+ process.stderr.write(`[wand] Captured Codex thread ID: ${discoveredThreadId}\n`);
1180
+ return true;
1181
+ }
1080
1182
  get(id) {
1081
1183
  const record = this.sessions.get(id);
1082
1184
  if (!record) {
@@ -1224,6 +1326,10 @@ export class ProcessManager extends EventEmitter {
1224
1326
  clearTimeout(record.claudeTaskDiscoveryTimer);
1225
1327
  record.claudeTaskDiscoveryTimer = null;
1226
1328
  }
1329
+ if (record.codexSessionDiscoveryTimer) {
1330
+ clearTimeout(record.codexSessionDiscoveryTimer);
1331
+ record.codexSessionDiscoveryTimer = null;
1332
+ }
1227
1333
  if (record.initialInputTimer) {
1228
1334
  clearTimeout(record.initialInputTimer);
1229
1335
  record.initialInputTimer = null;
@@ -1250,6 +1356,7 @@ export class ProcessManager extends EventEmitter {
1250
1356
  record.ptyBridge = null;
1251
1357
  }
1252
1358
  // Update lifecycle
1359
+ this.captureCodexSessionId(record);
1253
1360
  this.persist(record);
1254
1361
  return this.snapshot(record);
1255
1362
  }
@@ -1262,6 +1369,10 @@ export class ProcessManager extends EventEmitter {
1262
1369
  clearTimeout(record.claudeTaskDiscoveryTimer);
1263
1370
  record.claudeTaskDiscoveryTimer = null;
1264
1371
  }
1372
+ if (record.codexSessionDiscoveryTimer) {
1373
+ clearTimeout(record.codexSessionDiscoveryTimer);
1374
+ record.codexSessionDiscoveryTimer = null;
1375
+ }
1265
1376
  if (record.initialInputTimer) {
1266
1377
  clearTimeout(record.initialInputTimer);
1267
1378
  record.initialInputTimer = null;
@@ -1298,6 +1409,10 @@ export class ProcessManager extends EventEmitter {
1298
1409
  clearTimeout(record.claudeTaskDiscoveryTimer);
1299
1410
  record.claudeTaskDiscoveryTimer = null;
1300
1411
  }
1412
+ if (record.codexSessionDiscoveryTimer) {
1413
+ clearTimeout(record.codexSessionDiscoveryTimer);
1414
+ record.codexSessionDiscoveryTimer = null;
1415
+ }
1301
1416
  if (record.initialInputTimer) {
1302
1417
  clearTimeout(record.initialInputTimer);
1303
1418
  record.initialInputTimer = null;
@@ -1335,11 +1450,18 @@ export class ProcessManager extends EventEmitter {
1335
1450
  // so a storage failure doesn't leave orphan records in the database.
1336
1451
  this.storage.deleteSession(id);
1337
1452
  this.logger.deleteSession(id);
1338
- this.deleteClaudeCache(record);
1453
+ if (record.provider === "claude") {
1454
+ this.deleteClaudeCache(record);
1455
+ }
1339
1456
  this.sessions.delete(id);
1340
1457
  this.lastPersistedMessageState.delete(id);
1341
1458
  if (record.claudeSessionId) {
1342
- this.claudeHistoryCache = null;
1459
+ if (record.provider === "codex") {
1460
+ this.codexHistoryCache = null;
1461
+ }
1462
+ else {
1463
+ this.claudeHistoryCache = null;
1464
+ }
1343
1465
  }
1344
1466
  }
1345
1467
  deleteClaudeCache(record) {
@@ -1488,14 +1610,15 @@ export class ProcessManager extends EventEmitter {
1488
1610
  this.persist(record);
1489
1611
  return this.snapshot(record);
1490
1612
  }
1491
- persist(record) {
1613
+ persist(record, options) {
1492
1614
  // Update messages from bridge before persisting
1493
1615
  const messages = record.ptyBridge?.getMessages() ?? record.messages;
1494
1616
  if (messages !== record.messages) {
1495
1617
  record.messages = messages;
1496
1618
  }
1497
1619
  const snapshot = this.snapshot(record);
1498
- const shouldSaveMessages = shouldPersistMessages(this.lastPersistedMessageState.get(record.id), messages);
1620
+ const shouldSaveMessages = options?.forceFullSave === true
1621
+ || shouldPersistMessages(this.lastPersistedMessageState.get(record.id), messages);
1499
1622
  // Persist full messages as soon as the structured conversation changes so
1500
1623
  // service restarts cannot roll the session back to an older tail message.
1501
1624
  if (shouldSaveMessages) {
@@ -1,3 +1,4 @@
1
1
  import { ConversationTurn } from "./types.js";
2
2
  export declare function hasRealConversationMessages(messages: ConversationTurn[] | undefined): boolean;
3
3
  export declare function getResumeCommandSessionId(command: string): string | null;
4
+ export declare function getCodexResumeCommandSessionId(command: string): string | null;
@@ -1,5 +1,7 @@
1
1
  const REAL_CONVERSATION_MIN_MESSAGES = 2;
2
- const RESUME_COMMAND_ID_PATTERN = /(?:^|\s)--resume\s+([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(?:\s|$)/i;
2
+ const UUID_PATTERN = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
3
+ const RESUME_COMMAND_ID_PATTERN = new RegExp(`(?:^|\\s)--resume\\s+(${UUID_PATTERN})(?:\\s|$)`, "i");
4
+ const CODEX_RESUME_COMMAND_ID_PATTERN = new RegExp(`(?:^|\\s)resume\\s+(${UUID_PATTERN})(?:\\s|$)`, "i");
3
5
  export function hasRealConversationMessages(messages) {
4
6
  if (!messages || messages.length < REAL_CONVERSATION_MIN_MESSAGES) {
5
7
  return false;
@@ -14,3 +16,7 @@ export function getResumeCommandSessionId(command) {
14
16
  const match = RESUME_COMMAND_ID_PATTERN.exec(command);
15
17
  return match?.[1] ?? null;
16
18
  }
19
+ export function getCodexResumeCommandSessionId(command) {
20
+ const match = CODEX_RESUME_COMMAND_ID_PATTERN.exec(command);
21
+ return match?.[1] ?? null;
22
+ }
@@ -130,6 +130,13 @@ function getWorktreeMergePayload(error, fallback) {
130
130
  function getLatestSessionSnapshot(processes, structured, storage, id) {
131
131
  return getSessionById(processes, structured, id) ?? storage.getSession(id);
132
132
  }
133
+ function buildCodexResumeCommand(command, threadId) {
134
+ const trimmed = command.trim();
135
+ const withoutExistingResume = trimmed
136
+ .replace(/\s+resume\s+[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}(?:\s|$)/i, " ")
137
+ .trim();
138
+ return `${withoutExistingResume || "codex"} resume ${threadId}`;
139
+ }
133
140
  function canMergeSession(snapshot) {
134
141
  return Boolean(snapshot.worktreeEnabled && snapshot.worktree?.branch && snapshot.worktree?.path);
135
142
  }
@@ -593,28 +600,41 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
593
600
  return;
594
601
  }
595
602
  if ((existingSession.sessionKind ?? "pty") !== "pty") {
596
- res.status(400).json({ error: "结构化会话不支持 Claude CLI resume。" });
603
+ res.status(400).json({ error: "结构化会话不支持 PTY resume。" });
597
604
  return;
598
605
  }
599
- if (existingSession.provider && existingSession.provider !== "claude") {
600
- res.status(400).json({ error: "只有 Claude provider 支持恢复功能。" });
606
+ const command = existingSession.command.trim();
607
+ const provider = existingSession.provider ?? (/^codex\b/.test(command) ? "codex" : "claude");
608
+ if (provider !== "claude" && provider !== "codex") {
609
+ res.status(400).json({ error: "只有 Claude 或 Codex provider 支持恢复功能。" });
601
610
  return;
602
611
  }
603
- const claudeSessionId = existingSession.claudeSessionId;
604
- if (!claudeSessionId) {
605
- res.status(400).json({ error: "此会话没有 Claude 会话 ID,无法恢复。" });
612
+ const resumeSessionId = existingSession.claudeSessionId;
613
+ if (!resumeSessionId) {
614
+ res.status(400).json({ error: provider === "codex" ? "此会话没有 Codex thread ID,无法恢复。" : "此会话没有 Claude 会话 ID,无法恢复。" });
606
615
  return;
607
616
  }
608
- const command = existingSession.command.trim();
609
- if (!/^claude\b/.test(command)) {
617
+ if (provider === "claude" && !/^claude\b/.test(command)) {
610
618
  res.status(400).json({ error: "只有 Claude 命令支持恢复功能。" });
611
619
  return;
612
620
  }
621
+ if (provider === "codex") {
622
+ if (!/^codex\b/.test(command)) {
623
+ res.status(400).json({ error: "只有 Codex 命令支持恢复功能。" });
624
+ return;
625
+ }
626
+ if (!processes.hasCodexSessionFile(resumeSessionId)) {
627
+ res.status(400).json({ error: "对应的 Codex 历史会话不存在,无法恢复。" });
628
+ return;
629
+ }
630
+ }
613
631
  const newMode = body.mode ? normalizeMode(body.mode, defaultMode) : normalizeMode(existingSession.mode, defaultMode);
614
- const resumeCommand = `${command} --resume ${claudeSessionId}`;
632
+ const resumeCommand = provider === "codex"
633
+ ? buildCodexResumeCommand(command, resumeSessionId)
634
+ : `${command} --resume ${resumeSessionId}`;
615
635
  const reqCols = typeof body.cols === "number" && Number.isFinite(body.cols) ? body.cols : undefined;
616
636
  const reqRows = typeof body.rows === "number" && Number.isFinite(body.rows) ? body.rows : undefined;
617
- const newSnapshot = processes.start(resumeCommand, existingSession.cwd, newMode, undefined, { reuseId: sessionId, cols: reqCols, rows: reqRows });
637
+ const newSnapshot = processes.start(resumeCommand, existingSession.cwd, newMode, undefined, { reuseId: sessionId, cols: reqCols, rows: reqRows, provider });
618
638
  res.status(201).json(newSnapshot);
619
639
  }
620
640
  catch (error) {
package/dist/types.d.ts CHANGED
@@ -196,6 +196,10 @@ export interface GitStatusResult {
196
196
  shortHash: string;
197
197
  subject: string;
198
198
  };
199
+ /** Most recent tag reachable from HEAD (`git describe --tags --abbrev=0`), if any. */
200
+ latestTag?: string;
201
+ /** Locally-derived next-version suggestion (patch bump of `latestTag`, or a sane default). */
202
+ suggestedTag?: string;
199
203
  error?: string;
200
204
  }
201
205
  export interface QuickCommitResult {