commandmate 0.3.3 → 0.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-build-manifest.json +13 -13
  3. package/.next/app-path-routes-manifest.json +1 -1
  4. package/.next/build-manifest.json +5 -5
  5. package/.next/cache/.tsbuildinfo +1 -1
  6. package/.next/cache/config.json +3 -3
  7. package/.next/cache/webpack/client-production/0.pack +0 -0
  8. package/.next/cache/webpack/client-production/1.pack +0 -0
  9. package/.next/cache/webpack/client-production/2.pack +0 -0
  10. package/.next/cache/webpack/client-production/index.pack +0 -0
  11. package/.next/cache/webpack/client-production/index.pack.old +0 -0
  12. package/.next/cache/webpack/edge-server-production/0.pack +0 -0
  13. package/.next/cache/webpack/edge-server-production/index.pack +0 -0
  14. package/.next/cache/webpack/server-production/0.pack +0 -0
  15. package/.next/cache/webpack/server-production/index.pack +0 -0
  16. package/.next/next-server.js.nft.json +1 -1
  17. package/.next/prerender-manifest.json +1 -1
  18. package/.next/react-loadable-manifest.json +13 -7
  19. package/.next/required-server-files.json +1 -1
  20. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  21. package/.next/server/app/api/app/update-check/route.js +1 -1
  22. package/.next/server/app/api/repositories/route.js +2 -2
  23. package/.next/server/app/api/repositories/route.js.nft.json +1 -1
  24. package/.next/server/app/api/worktrees/[id]/auto-yes/route.js +1 -1
  25. package/.next/server/app/api/worktrees/[id]/auto-yes/route.js.nft.json +1 -1
  26. package/.next/server/app/api/worktrees/[id]/current-output/route.js +1 -1
  27. package/.next/server/app/api/worktrees/[id]/current-output/route.js.nft.json +1 -1
  28. package/.next/server/app/api/worktrees/[id]/execution-logs/[logId]/route.js +1 -1
  29. package/.next/server/app/api/worktrees/[id]/execution-logs/[logId]/route.js.nft.json +1 -1
  30. package/.next/server/app/api/worktrees/[id]/execution-logs/route.js +1 -1
  31. package/.next/server/app/api/worktrees/[id]/execution-logs/route.js.nft.json +1 -1
  32. package/.next/server/app/api/worktrees/[id]/interrupt/route.js +1 -1
  33. package/.next/server/app/api/worktrees/[id]/interrupt/route.js.nft.json +1 -1
  34. package/.next/server/app/api/worktrees/[id]/kill-session/route.js +1 -1
  35. package/.next/server/app/api/worktrees/[id]/kill-session/route.js.nft.json +1 -1
  36. package/.next/server/app/api/worktrees/[id]/prompt-response/route.js +1 -1
  37. package/.next/server/app/api/worktrees/[id]/prompt-response/route.js.nft.json +1 -1
  38. package/.next/server/app/api/worktrees/[id]/respond/route.js +1 -1
  39. package/.next/server/app/api/worktrees/[id]/respond/route.js.nft.json +1 -1
  40. package/.next/server/app/api/worktrees/[id]/route.js +1 -1
  41. package/.next/server/app/api/worktrees/[id]/route.js.nft.json +1 -1
  42. package/.next/server/app/api/worktrees/[id]/schedules/[scheduleId]/route.js +1 -1
  43. package/.next/server/app/api/worktrees/[id]/schedules/[scheduleId]/route.js.nft.json +1 -1
  44. package/.next/server/app/api/worktrees/[id]/schedules/route.js +1 -1
  45. package/.next/server/app/api/worktrees/[id]/schedules/route.js.nft.json +1 -1
  46. package/.next/server/app/api/worktrees/[id]/send/route.js +1 -1
  47. package/.next/server/app/api/worktrees/[id]/send/route.js.nft.json +1 -1
  48. package/.next/server/app/api/worktrees/[id]/slash-commands/route.js +1 -1
  49. package/.next/server/app/api/worktrees/[id]/start-polling/route.js +1 -1
  50. package/.next/server/app/api/worktrees/[id]/start-polling/route.js.nft.json +1 -1
  51. package/.next/server/app/api/worktrees/route.js +1 -1
  52. package/.next/server/app/api/worktrees/route.js.nft.json +1 -1
  53. package/.next/server/app/login/page.js +1 -1
  54. package/.next/server/app/login/page_client-reference-manifest.js +1 -1
  55. package/.next/server/app/page_client-reference-manifest.js +1 -1
  56. package/.next/server/app/proxy/[...path]/route.js +1 -1
  57. package/.next/server/app/worktrees/[id]/files/[...path]/page_client-reference-manifest.js +1 -1
  58. package/.next/server/app/worktrees/[id]/page.js +4 -4
  59. package/.next/server/app/worktrees/[id]/page_client-reference-manifest.js +1 -1
  60. package/.next/server/app/worktrees/[id]/terminal/page_client-reference-manifest.js +1 -1
  61. package/.next/server/app-paths-manifest.json +4 -4
  62. package/.next/server/chunks/2314.js +1 -1
  63. package/.next/server/chunks/3074.js +1 -1
  64. package/.next/server/chunks/4952.js +1 -0
  65. package/.next/server/chunks/539.js +3 -3
  66. package/.next/server/chunks/5795.js +1 -1
  67. package/.next/server/chunks/6228.js +1 -1
  68. package/.next/server/chunks/7425.js +52 -43
  69. package/.next/server/chunks/7566.js +1 -1
  70. package/.next/server/chunks/8693.js +1 -1
  71. package/.next/server/middleware-build-manifest.js +1 -1
  72. package/.next/server/middleware-manifest.json +5 -5
  73. package/.next/server/middleware-react-loadable-manifest.js +1 -1
  74. package/.next/server/pages/500.html +1 -1
  75. package/.next/server/server-reference-manifest.json +1 -1
  76. package/.next/static/chunks/{4327.740cc7fe2d0b5049.js → 4327.157a4c226d919531.js} +14 -14
  77. package/.next/static/chunks/5970.0df906ad5a9c9147.js +1 -0
  78. package/.next/static/chunks/{8091-274bc0716106e7fc.js → 8091-d65d2ab6daed23c6.js} +1 -1
  79. package/.next/static/chunks/app/login/page-010f02fd4b0dbc48.js +1 -0
  80. package/.next/static/chunks/app/worktrees/[id]/page-8fb4dc30b58a5681.js +1 -0
  81. package/.next/static/chunks/webpack-81c97591dd5567ac.js +1 -0
  82. package/.next/static/css/45b3a41370668314.css +3 -0
  83. package/.next/trace +5 -5
  84. package/dist/server/src/lib/claude-executor.js +14 -3
  85. package/dist/server/src/lib/cli-patterns.js +99 -20
  86. package/dist/server/src/lib/cli-tools/manager.js +5 -3
  87. package/dist/server/src/lib/cli-tools/opencode-config.js +236 -0
  88. package/dist/server/src/lib/cli-tools/opencode.js +188 -0
  89. package/dist/server/src/lib/cli-tools/types.js +47 -6
  90. package/dist/server/src/lib/cli-tools/vibe-local.js +12 -3
  91. package/dist/server/src/lib/db-migrations.js +17 -1
  92. package/dist/server/src/lib/db.js +39 -2
  93. package/dist/server/src/lib/prompt-detector.js +23 -4
  94. package/dist/server/src/lib/response-poller.js +392 -28
  95. package/package.json +5 -4
  96. package/.next/server/chunks/9446.js +0 -1
  97. package/.next/static/chunks/app/login/page-2d42204ba87cd136.js +0 -1
  98. package/.next/static/chunks/app/worktrees/[id]/page-78580947c201d698.js +0 -1
  99. package/.next/static/chunks/webpack-3c0ee3ce5b546818.js +0 -1
  100. package/.next/static/css/e85de230ef5ddc40.css +0 -3
  101. /package/.next/static/chunks/app/{page-060057e02b841125.js → page-9e523a8f415bc707.js} +0 -0
  102. /package/.next/static/{O7EDFfAYQNe_HRbORxQAC → p3hosTZoJ22r35fWwUoLr}/_buildManifest.js +0 -0
  103. /package/.next/static/{O7EDFfAYQNe_HRbORxQAC → p3hosTZoJ22r35fWwUoLr}/_ssgManifest.js +0 -0
@@ -3,16 +3,17 @@
3
3
  * Type definitions and interfaces for CLI tools
4
4
  */
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.OLLAMA_MODEL_PATTERN = exports.CLI_TOOL_DISPLAY_NAMES = exports.CLI_TOOL_IDS = void 0;
6
+ exports.OLLAMA_MODEL_PATTERN = exports.VIBE_LOCAL_CONTEXT_WINDOW_MAX = exports.VIBE_LOCAL_CONTEXT_WINDOW_MIN = exports.CLI_TOOL_DISPLAY_NAMES = exports.CLI_TOOL_IDS = void 0;
7
7
  exports.isCliToolType = isCliToolType;
8
8
  exports.getCliToolDisplayName = getCliToolDisplayName;
9
9
  exports.getCliToolDisplayNameSafe = getCliToolDisplayNameSafe;
10
+ exports.isValidVibeLocalContextWindow = isValidVibeLocalContextWindow;
10
11
  /**
11
12
  * CLI Tool IDs constant array
12
13
  * T2.1: Single source of truth for CLI tool IDs
13
14
  * CLIToolType is derived from this constant (DRY principle)
14
15
  */
15
- exports.CLI_TOOL_IDS = ['claude', 'codex', 'gemini', 'vibe-local'];
16
+ exports.CLI_TOOL_IDS = ['claude', 'codex', 'gemini', 'vibe-local', 'opencode'];
16
17
  /**
17
18
  * CLI tool display names for UI rendering
18
19
  * Issue #368: Centralized display name mapping
@@ -25,6 +26,7 @@ exports.CLI_TOOL_DISPLAY_NAMES = {
25
26
  codex: 'Codex',
26
27
  gemini: 'Gemini',
27
28
  'vibe-local': 'Vibe Local',
29
+ opencode: 'OpenCode',
28
30
  };
29
31
  /**
30
32
  * Check if a string is a valid CLIToolType
@@ -65,10 +67,49 @@ function getCliToolDisplayNameSafe(cliToolId, fallback = 'Assistant') {
65
67
  return fallback;
66
68
  }
67
69
  /**
68
- * Ollama model name validation pattern.
69
- * Allows: alphanumeric start, followed by alphanumeric, dots, underscores, colons, slashes, hyphens.
70
- * Max 100 characters. Used for defense-in-depth validation at point of use.
70
+ * Minimum context window size for vibe-local.
71
+ * [S1-007] Lower bound rationale: Ollama's actual minimum context window is
72
+ * typically 2048+, but 128 is set as a permissive lower bound to accommodate
73
+ * custom models or future models with smaller contexts. Users are recommended
74
+ * to use practical values (e.g., 2048+).
75
+ * [S1-004] vibe-local specific constant. If more vibe-local constants are added,
76
+ * consider extracting to src/lib/cli-tools/vibe-local-config.ts.
77
+ * [SEC-002] Used to prevent unreasonable values in CLI arguments.
78
+ */
79
+ exports.VIBE_LOCAL_CONTEXT_WINDOW_MIN = 128;
80
+ /**
81
+ * Maximum context window size for vibe-local (2M tokens).
82
+ * Shared between API validation and defense-in-depth (DRY principle).
83
+ * [S1-004] vibe-local specific constant. If more vibe-local constants are added,
84
+ * consider extracting to src/lib/cli-tools/vibe-local-config.ts.
85
+ * [SEC-002] Used to prevent unreasonable values in CLI arguments.
86
+ */
87
+ exports.VIBE_LOCAL_CONTEXT_WINDOW_MAX = 2097152;
88
+ /**
89
+ * Validate vibe-local context window value.
90
+ * Shared between API layer and CLI layer (defense-in-depth).
91
+ * [S1-001] DRY: Single source of truth for context window validation.
92
+ *
93
+ * @param value - Value to validate (accepts unknown for type guard usage)
94
+ * @returns True if value is a valid context window size (integer between MIN and MAX)
95
+ */
96
+ function isValidVibeLocalContextWindow(value) {
97
+ return (typeof value === 'number' &&
98
+ Number.isInteger(value) &&
99
+ value >= exports.VIBE_LOCAL_CONTEXT_WINDOW_MIN &&
100
+ value <= exports.VIBE_LOCAL_CONTEXT_WINDOW_MAX);
101
+ }
102
+ /**
103
+ * Ollama model name validation pattern (API/DB layer).
104
+ * Requires alphanumeric first character, followed by alphanumeric, dots, underscores,
105
+ * colons, slashes, hyphens. No explicit length limit (DB schema handles storage limits).
106
+ *
107
+ * [SEC-001] Shared between API route validation and CLI command construction.
71
108
  *
72
- * [SEC-001] Shared between API route validation and CLI command construction
109
+ * Note: opencode-config.ts has a separate OLLAMA_MODEL_PATTERN with a 100-character
110
+ * length limit (`{1,100}`) for DoS protection when parsing Ollama API responses.
111
+ * The patterns are intentionally different: this one enforces first-character constraints
112
+ * for user-facing validation, while the opencode-config version adds length limits
113
+ * for untrusted external API data.
73
114
  */
74
115
  exports.OLLAMA_MODEL_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._:/-]*$/;
@@ -69,18 +69,27 @@ class VibeLocalTool extends base_1.BaseCLITool {
69
69
  });
70
70
  // Wait a moment for the session to be created
71
71
  await new Promise((resolve) => setTimeout(resolve, 100));
72
- // Read Ollama model preference from DB
72
+ // Read Ollama model and context window preferences from DB
73
73
  // [SEC-001] Re-validate model name at point of use (defense-in-depth)
74
+ // [S1-005] DB direct access follows existing vibeLocalModel pattern;
75
+ // future DIP refactoring should pass these as startSession() arguments.
74
76
  let vibeLocalCommand = 'vibe-local -y';
75
77
  try {
76
78
  const db = (0, db_instance_1.getDbInstance)();
77
79
  const wt = (0, db_1.getWorktreeById)(db, worktreeId);
78
80
  if (wt?.vibeLocalModel && types_1.OLLAMA_MODEL_PATTERN.test(wt.vibeLocalModel)) {
79
- vibeLocalCommand = `vibe-local -y -m ${wt.vibeLocalModel}`;
81
+ vibeLocalCommand += ` -m ${wt.vibeLocalModel}`;
82
+ }
83
+ // [C2-008] contextWindow from the same wt object (no additional DB call)
84
+ const ctxWindow = wt?.vibeLocalContextWindow;
85
+ // [SEC-002] Defense-in-depth: re-validate at point of use
86
+ // [S4-001] Number() cast for additional safety in template literal
87
+ if ((0, types_1.isValidVibeLocalContextWindow)(ctxWindow)) {
88
+ vibeLocalCommand += ` --context-window ${Number(ctxWindow)}`;
80
89
  }
81
90
  }
82
91
  catch {
83
- // DB read failure is non-fatal; use default model
92
+ // DB read failure is non-fatal; use defaults
84
93
  }
85
94
  // Start vibe-local in interactive mode with auto-approve (-y)
86
95
  // -y flag skips the permission confirmation prompt
@@ -19,7 +19,7 @@ const db_1 = require("./db");
19
19
  * Current schema version
20
20
  * Increment this when adding new migrations
21
21
  */
22
- exports.CURRENT_SCHEMA_VERSION = 19;
22
+ exports.CURRENT_SCHEMA_VERSION = 20;
23
23
  /**
24
24
  * Migration registry
25
25
  * All migrations should be added to this array in order
@@ -860,6 +860,22 @@ const migrations = [
860
860
  // vibe_local_model is a nullable TEXT column; harmless if unused
861
861
  console.log('No rollback for vibe_local_model column (SQLite limitation)');
862
862
  }
863
+ },
864
+ {
865
+ version: 20,
866
+ name: 'add-vibe-local-context-window-column',
867
+ up: (db) => {
868
+ // Issue #374: Add vibe_local_context_window column for Ollama context window size
869
+ // NULL means use the default (vibe-local CLI decides)
870
+ db.exec(`
871
+ ALTER TABLE worktrees ADD COLUMN vibe_local_context_window INTEGER DEFAULT NULL;
872
+ `);
873
+ console.log('✓ Added vibe_local_context_window column to worktrees table');
874
+ },
875
+ down: () => {
876
+ // vibe_local_context_window is a nullable INTEGER column; harmless if unused
877
+ console.log('No rollback for vibe_local_context_window column (SQLite limitation)');
878
+ }
863
879
  }
864
880
  ];
865
881
  /**
@@ -19,6 +19,7 @@ exports.getMessages = getMessages;
19
19
  exports.getLastUserMessage = getLastUserMessage;
20
20
  exports.getLastMessage = getLastMessage;
21
21
  exports.deleteAllMessages = deleteAllMessages;
22
+ exports.deleteMessageById = deleteMessageById;
22
23
  exports.deleteMessagesByCliTool = deleteMessagesByCliTool;
23
24
  exports.getSessionState = getSessionState;
24
25
  exports.updateSessionState = updateSessionState;
@@ -34,6 +35,7 @@ exports.updateStatus = updateStatus;
34
35
  exports.updateCliToolId = updateCliToolId;
35
36
  exports.updateSelectedAgents = updateSelectedAgents;
36
37
  exports.updateVibeLocalModel = updateVibeLocalModel;
38
+ exports.updateVibeLocalContextWindow = updateVibeLocalContextWindow;
37
39
  exports.getMemosByWorktreeId = getMemosByWorktreeId;
38
40
  exports.getMemoById = getMemoById;
39
41
  exports.createMemo = createMemo;
@@ -193,7 +195,7 @@ function getWorktrees(db, repositoryPath) {
193
195
  w.id, w.name, w.path, w.repository_path, w.repository_name, w.description,
194
196
  w.last_user_message, w.last_user_message_at, w.last_message_summary,
195
197
  w.updated_at, w.favorite, w.status, w.link, w.cli_tool_id, w.last_viewed_at,
196
- w.selected_agents, w.vibe_local_model,
198
+ w.selected_agents, w.vibe_local_model, w.vibe_local_context_window,
197
199
  (SELECT MAX(timestamp) FROM chat_messages
198
200
  WHERE worktree_id = w.id AND role = 'assistant') as last_assistant_message_at
199
201
  FROM worktrees w
@@ -231,6 +233,7 @@ function getWorktrees(db, repositoryPath) {
231
233
  cliToolId: row.cli_tool_id ?? 'claude',
232
234
  selectedAgents: (0, selected_agents_validator_1.parseSelectedAgents)(row.selected_agents),
233
235
  vibeLocalModel: row.vibe_local_model ?? null,
236
+ vibeLocalContextWindow: row.vibe_local_context_window ?? null,
234
237
  };
235
238
  });
236
239
  }
@@ -265,7 +268,7 @@ function getWorktreeById(db, id) {
265
268
  w.id, w.name, w.path, w.repository_path, w.repository_name, w.description,
266
269
  w.last_user_message, w.last_user_message_at, w.last_message_summary,
267
270
  w.updated_at, w.favorite, w.status, w.link, w.cli_tool_id, w.last_viewed_at,
268
- w.selected_agents, w.vibe_local_model,
271
+ w.selected_agents, w.vibe_local_model, w.vibe_local_context_window,
269
272
  (SELECT MAX(timestamp) FROM chat_messages
270
273
  WHERE worktree_id = w.id AND role = 'assistant') as last_assistant_message_at
271
274
  FROM worktrees w
@@ -294,6 +297,7 @@ function getWorktreeById(db, id) {
294
297
  cliToolId: row.cli_tool_id ?? 'claude',
295
298
  selectedAgents: (0, selected_agents_validator_1.parseSelectedAgents)(row.selected_agents),
296
299
  vibeLocalModel: row.vibe_local_model ?? null,
300
+ vibeLocalContextWindow: row.vibe_local_context_window ?? null,
297
301
  };
298
302
  }
299
303
  /**
@@ -477,6 +481,23 @@ function deleteAllMessages(db, worktreeId) {
477
481
  stmt.run(worktreeId);
478
482
  console.log(`[deleteAllMessages] Deleted all messages for worktree: ${worktreeId}`);
479
483
  }
484
+ /**
485
+ * Delete a single message by its ID
486
+ * Used to clean up orphaned user messages (e.g., when a user re-sends a message
487
+ * after the previous one received no response).
488
+ *
489
+ * @param db - Database instance
490
+ * @param messageId - ID of the message to delete
491
+ * @returns True if a message was deleted, false otherwise
492
+ */
493
+ function deleteMessageById(db, messageId) {
494
+ const stmt = db.prepare(`
495
+ DELETE FROM chat_messages
496
+ WHERE id = ?
497
+ `);
498
+ const result = stmt.run(messageId);
499
+ return result.changes > 0;
500
+ }
480
501
  /**
481
502
  * Delete messages for a specific CLI tool in a worktree
482
503
  * Issue #4: T4.2 - Individual CLI tool session termination (MF3-001)
@@ -728,6 +749,22 @@ function updateVibeLocalModel(db, id, model) {
728
749
  `);
729
750
  stmt.run(model, id);
730
751
  }
752
+ /**
753
+ * Update vibe_local_context_window for a worktree
754
+ * Issue #374: Persists the user's Ollama context window size for vibe-local
755
+ *
756
+ * @param db - Database instance
757
+ * @param id - Worktree ID
758
+ * @param contextWindow - Context window size or null for default
759
+ */
760
+ function updateVibeLocalContextWindow(db, id, contextWindow) {
761
+ const stmt = db.prepare(`
762
+ UPDATE worktrees
763
+ SET vibe_local_context_window = ?
764
+ WHERE id = ?
765
+ `);
766
+ stmt.run(contextWindow, id);
767
+ }
731
768
  /**
732
769
  * Map database row to WorktreeMemo model
733
770
  */
@@ -166,12 +166,12 @@ const TEXT_INPUT_PATTERNS = [
166
166
  /differently/i,
167
167
  ];
168
168
  /**
169
- * Pattern for ❯ (U+276F) / ● (U+25CF) indicator lines used by CLI tools to mark the default selection.
170
- * Claude CLI uses ❯, Gemini CLI uses ●.
169
+ * Pattern for ❯ (U+276F) / ● (U+25CF) / › (U+203A) indicator lines used by CLI tools to mark the default selection.
170
+ * Claude CLI uses ❯, Gemini CLI uses ●, Codex CLI uses › (Issue #372).
171
171
  * Used in Pass 1 (existence check) and Pass 2 (option collection) of the 2-pass detection.
172
172
  * Anchored at both ends -- ReDoS safe (S4-001).
173
173
  */
174
- const DEFAULT_OPTION_PATTERN = /^\s*[\u276F\u25CF]\s*(\d+)\.\s*(.+)$/;
174
+ const DEFAULT_OPTION_PATTERN = /^\s*[\u276F\u25CF\u203A]\s*(\d+)\.\s*(.+)$/;
175
175
  /**
176
176
  * Pattern for normal option lines (no ❯ indicator, just leading whitespace + number).
177
177
  * Only applied in Pass 2 when ❯ indicator existence is confirmed by Pass 1.
@@ -207,6 +207,13 @@ const SEPARATOR_LINE_PATTERN = /^[-─]+$/;
207
207
  * @see Issue #256: multiple_choice prompt detection improvement
208
208
  */
209
209
  const QUESTION_SCAN_RANGE = 3;
210
+ /**
211
+ * Maximum consecutive continuation lines allowed between options and question.
212
+ * Issue #372: Codex TUI indents all output with 2 spaces, causing isContinuationLine()
213
+ * to match body text lines indefinitely. Without this limit, the scanner would traverse
214
+ * through the entire command output, picking up numbered lists as false options.
215
+ */
216
+ const MAX_CONTINUATION_LINES = 5;
210
217
  /**
211
218
  * Creates a "no prompt detected" result.
212
219
  * Centralizes the repeated pattern of returning isPrompt: false with trimmed content.
@@ -544,6 +551,7 @@ function detectMultipleChoicePrompt(output, options) {
544
551
  // ==========================================================================
545
552
  const collectedOptions = [];
546
553
  let questionEndIndex = -1;
554
+ let continuationLineCount = 0;
547
555
  for (let i = effectiveEnd - 1; i >= scanStart; i--) {
548
556
  const line = lines[i].trim();
549
557
  // Try DEFAULT_OPTION_PATTERN first (❯ indicator)
@@ -552,6 +560,7 @@ function detectMultipleChoicePrompt(output, options) {
552
560
  const number = parseInt(defaultMatch[1], 10);
553
561
  const label = defaultMatch[2].trim();
554
562
  collectedOptions.unshift({ number, label, isDefault: true });
563
+ continuationLineCount = 0;
555
564
  continue;
556
565
  }
557
566
  // Try NORMAL_OPTION_PATTERN (no ❯ indicator)
@@ -560,6 +569,7 @@ function detectMultipleChoicePrompt(output, options) {
560
569
  const number = parseInt(normalMatch[1], 10);
561
570
  const label = normalMatch[2].trim();
562
571
  collectedOptions.unshift({ number, label, isDefault: false });
572
+ continuationLineCount = 0;
563
573
  continue;
564
574
  }
565
575
  // [Issue #287 Bug3] User input prompt barrier:
@@ -568,7 +578,7 @@ function detectMultipleChoicePrompt(output, options) {
568
578
  // user input prompt (e.g., "❯ 1", "❯ /command") or idle prompt ("❯").
569
579
  // Anything above this line in the scrollback is historical conversation text,
570
580
  // not an active prompt. Stop scanning to prevent false positives.
571
- if (collectedOptions.length === 0 && (line.startsWith('\u276F') || line.startsWith('\u25CF'))) {
581
+ if (collectedOptions.length === 0 && (line.startsWith('\u276F') || line.startsWith('\u25CF') || line.startsWith('\u203A'))) {
572
582
  return noPromptResult(output);
573
583
  }
574
584
  // Non-option line handling
@@ -591,6 +601,15 @@ function detectMultipleChoicePrompt(output, options) {
591
601
  // or path/filename fragments from terminal width wrapping - Issue #181)
592
602
  const rawLine = lines[i]; // Original line with indentation preserved
593
603
  if (isContinuationLine(rawLine, line)) {
604
+ continuationLineCount++;
605
+ // Issue #372: Codex TUI indents all output with 2 spaces, causing
606
+ // every line to match isContinuationLine(). Limit the scan distance
607
+ // to prevent traversing into body text where numbered lists would be
608
+ // collected as false options.
609
+ if (continuationLineCount > MAX_CONTINUATION_LINES) {
610
+ questionEndIndex = i;
611
+ break;
612
+ }
594
613
  // Skip continuation lines and continue scanning for more options
595
614
  continue;
596
615
  }