commandmate 0.3.2 → 0.3.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.
Files changed (113) 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 +4 -4
  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/required-server-files.json +1 -1
  19. package/.next/routes-manifest.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/ollama/models/route.js +1 -0
  23. package/.next/server/app/api/ollama/models/route.js.nft.json +1 -0
  24. package/.next/server/app/api/ollama/models.body +1 -0
  25. package/.next/server/app/api/ollama/models.meta +1 -0
  26. package/.next/server/app/api/repositories/route.js +3 -3
  27. package/.next/server/app/api/repositories/route.js.nft.json +1 -1
  28. package/.next/server/app/api/worktrees/[id]/auto-yes/route.js +1 -1
  29. package/.next/server/app/api/worktrees/[id]/auto-yes/route.js.nft.json +1 -1
  30. package/.next/server/app/api/worktrees/[id]/cli-tool/route.js +1 -1
  31. package/.next/server/app/api/worktrees/[id]/current-output/route.js +1 -1
  32. package/.next/server/app/api/worktrees/[id]/current-output/route.js.nft.json +1 -1
  33. package/.next/server/app/api/worktrees/[id]/execution-logs/[logId]/route.js +1 -1
  34. package/.next/server/app/api/worktrees/[id]/execution-logs/[logId]/route.js.nft.json +1 -1
  35. package/.next/server/app/api/worktrees/[id]/execution-logs/route.js +1 -1
  36. package/.next/server/app/api/worktrees/[id]/execution-logs/route.js.nft.json +1 -1
  37. package/.next/server/app/api/worktrees/[id]/interrupt/route.js +1 -1
  38. package/.next/server/app/api/worktrees/[id]/interrupt/route.js.nft.json +1 -1
  39. package/.next/server/app/api/worktrees/[id]/kill-session/route.js +1 -1
  40. package/.next/server/app/api/worktrees/[id]/kill-session/route.js.nft.json +1 -1
  41. package/.next/server/app/api/worktrees/[id]/messages/route.js +1 -1
  42. package/.next/server/app/api/worktrees/[id]/prompt-response/route.js +1 -1
  43. package/.next/server/app/api/worktrees/[id]/prompt-response/route.js.nft.json +1 -1
  44. package/.next/server/app/api/worktrees/[id]/respond/route.js +1 -1
  45. package/.next/server/app/api/worktrees/[id]/respond/route.js.nft.json +1 -1
  46. package/.next/server/app/api/worktrees/[id]/route.js +1 -1
  47. package/.next/server/app/api/worktrees/[id]/route.js.nft.json +1 -1
  48. package/.next/server/app/api/worktrees/[id]/schedules/[scheduleId]/route.js +1 -1
  49. package/.next/server/app/api/worktrees/[id]/schedules/[scheduleId]/route.js.nft.json +1 -1
  50. package/.next/server/app/api/worktrees/[id]/schedules/route.js +2 -2
  51. package/.next/server/app/api/worktrees/[id]/schedules/route.js.nft.json +1 -1
  52. package/.next/server/app/api/worktrees/[id]/send/route.js +1 -1
  53. package/.next/server/app/api/worktrees/[id]/send/route.js.nft.json +1 -1
  54. package/.next/server/app/api/worktrees/[id]/slash-commands/route.js +1 -1
  55. package/.next/server/app/api/worktrees/[id]/start-polling/route.js +1 -1
  56. package/.next/server/app/api/worktrees/[id]/start-polling/route.js.nft.json +1 -1
  57. package/.next/server/app/api/worktrees/route.js +1 -1
  58. package/.next/server/app/api/worktrees/route.js.nft.json +1 -1
  59. package/.next/server/app/login/page_client-reference-manifest.js +1 -1
  60. package/.next/server/app/page.js +1 -1
  61. package/.next/server/app/page_client-reference-manifest.js +1 -1
  62. package/.next/server/app/proxy/[...path]/route.js +1 -1
  63. package/.next/server/app/worktrees/[id]/files/[...path]/page_client-reference-manifest.js +1 -1
  64. package/.next/server/app/worktrees/[id]/page.js +4 -4
  65. package/.next/server/app/worktrees/[id]/page_client-reference-manifest.js +1 -1
  66. package/.next/server/app/worktrees/[id]/terminal/page_client-reference-manifest.js +1 -1
  67. package/.next/server/app-paths-manifest.json +11 -10
  68. package/.next/server/chunks/2314.js +1 -1
  69. package/.next/server/chunks/4559.js +1 -1
  70. package/.next/server/chunks/539.js +10 -10
  71. package/.next/server/chunks/5853.js +1 -1
  72. package/.next/server/chunks/6228.js +1 -1
  73. package/.next/server/chunks/7425.js +67 -41
  74. package/.next/server/chunks/7566.js +1 -1
  75. package/.next/server/chunks/8693.js +1 -1
  76. package/.next/server/chunks/9446.js +1 -0
  77. package/.next/server/functions-config-manifest.json +1 -1
  78. package/.next/server/middleware-build-manifest.js +1 -1
  79. package/.next/server/middleware-manifest.json +5 -5
  80. package/.next/server/pages/500.html +1 -1
  81. package/.next/server/pages-manifest.json +1 -1
  82. package/.next/server/server-reference-manifest.json +1 -1
  83. package/.next/static/chunks/8091-c0e955616dd86f82.js +1 -0
  84. package/.next/static/chunks/app/page-9e523a8f415bc707.js +1 -0
  85. package/.next/static/chunks/app/worktrees/[id]/page-9c0c64488c17db3c.js +1 -0
  86. package/.next/static/chunks/{main-db79434ee4a6c931.js → main-2feda12a4d321111.js} +1 -1
  87. package/.next/static/css/{bd6065b03ddb3efd.css → fa3df0e6f437f2ba.css} +1 -1
  88. package/.next/trace +5 -5
  89. package/.next/types/app/api/ollama/models/route.ts +343 -0
  90. package/README.md +74 -76
  91. package/dist/server/src/config/schedule-config.js +7 -1
  92. package/dist/server/src/lib/auto-yes-manager.js +2 -2
  93. package/dist/server/src/lib/claude-executor.js +15 -4
  94. package/dist/server/src/lib/cli-patterns.js +73 -9
  95. package/dist/server/src/lib/cli-tools/gemini.js +81 -22
  96. package/dist/server/src/lib/cli-tools/manager.js +4 -2
  97. package/dist/server/src/lib/cli-tools/types.js +98 -2
  98. package/dist/server/src/lib/cli-tools/vibe-local.js +172 -0
  99. package/dist/server/src/lib/cmate-parser.js +25 -3
  100. package/dist/server/src/lib/db-migrations.js +66 -1
  101. package/dist/server/src/lib/db.js +70 -1
  102. package/dist/server/src/lib/prompt-detector.js +23 -3
  103. package/dist/server/src/lib/response-poller.js +50 -23
  104. package/dist/server/src/lib/schedule-manager.js +6 -2
  105. package/dist/server/src/lib/selected-agents-validator.js +99 -0
  106. package/dist/server/src/types/sidebar.js +9 -4
  107. package/package.json +4 -4
  108. package/.next/server/chunks/7536.js +0 -1
  109. package/.next/static/chunks/8091-925542bdfc843dce.js +0 -1
  110. package/.next/static/chunks/app/page-238b5a70d8c101e9.js +0 -1
  111. package/.next/static/chunks/app/worktrees/[id]/page-0c889ab3f30d5af7.js +0 -1
  112. /package/.next/static/{j8HFvzDZj7tHjAnhpXUno → BiyH3zkbySg7ZWTeZuXqj}/_buildManifest.js +0 -0
  113. /package/.next/static/{j8HFvzDZj7tHjAnhpXUno → BiyH3zkbySg7ZWTeZuXqj}/_ssgManifest.js +0 -0
@@ -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 = 17;
22
+ exports.CURRENT_SCHEMA_VERSION = 20;
23
23
  /**
24
24
  * Migration registry
25
25
  * All migrations should be added to this array in order
@@ -811,6 +811,71 @@ const migrations = [
811
811
  db.exec('DROP TABLE IF EXISTS scheduled_executions');
812
812
  console.log('✓ Dropped scheduled_executions and execution_logs tables');
813
813
  }
814
+ },
815
+ {
816
+ version: 18,
817
+ name: 'add-selected-agents-column',
818
+ up: (db) => {
819
+ // Issue #368: Add selected_agents column for agent selection persistence
820
+ // NOTE (R1-010): The literal values 'claude', 'codex' in the SQL CASE below
821
+ // are fixed at migration time and do NOT sync with TypeScript CLI_TOOL_IDS.
822
+ // Changes to CLI_TOOL_IDS will not retroactively affect already-migrated data.
823
+ // Migration tests cover all CLIToolType values to catch sync issues.
824
+ // Step 1: Add column
825
+ db.exec(`
826
+ ALTER TABLE worktrees ADD COLUMN selected_agents TEXT;
827
+ `);
828
+ // Step 2: Initialize existing data based on cli_tool_id
829
+ // - If cli_tool_id is 'claude' or 'codex' -> default ["claude","codex"]
830
+ // - Otherwise (e.g. 'gemini', 'vibe-local') -> [cli_tool_id, "claude"]
831
+ db.exec(`
832
+ UPDATE worktrees SET selected_agents =
833
+ CASE
834
+ WHEN cli_tool_id NOT IN ('claude', 'codex')
835
+ THEN json_array(cli_tool_id, 'claude')
836
+ ELSE '["claude","codex"]'
837
+ END;
838
+ `);
839
+ console.log('✓ Added selected_agents column to worktrees table');
840
+ console.log('✓ Initialized selected_agents based on cli_tool_id');
841
+ },
842
+ down: () => {
843
+ // selected_agents is a nullable TEXT column; dropping it requires table recreation
844
+ // which is disproportionate for a rollback. The column is harmless if unused.
845
+ console.log('No rollback for selected_agents column (SQLite limitation)');
846
+ }
847
+ },
848
+ {
849
+ version: 19,
850
+ name: 'add-vibe-local-model-column',
851
+ up: (db) => {
852
+ // Issue #368: Add vibe_local_model column for Ollama model selection
853
+ // NULL means use the default model (vibe-local decides)
854
+ db.exec(`
855
+ ALTER TABLE worktrees ADD COLUMN vibe_local_model TEXT DEFAULT NULL;
856
+ `);
857
+ console.log('✓ Added vibe_local_model column to worktrees table');
858
+ },
859
+ down: () => {
860
+ // vibe_local_model is a nullable TEXT column; harmless if unused
861
+ console.log('No rollback for vibe_local_model column (SQLite limitation)');
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
+ }
814
879
  }
815
880
  ];
816
881
  /**
@@ -32,6 +32,9 @@ exports.markPendingPromptsAsAnswered = markPendingPromptsAsAnswered;
32
32
  exports.updateFavorite = updateFavorite;
33
33
  exports.updateStatus = updateStatus;
34
34
  exports.updateCliToolId = updateCliToolId;
35
+ exports.updateSelectedAgents = updateSelectedAgents;
36
+ exports.updateVibeLocalModel = updateVibeLocalModel;
37
+ exports.updateVibeLocalContextWindow = updateVibeLocalContextWindow;
35
38
  exports.getMemosByWorktreeId = getMemosByWorktreeId;
36
39
  exports.getMemoById = getMemoById;
37
40
  exports.createMemo = createMemo;
@@ -44,6 +47,7 @@ exports.getWorktreeIdsByRepository = getWorktreeIdsByRepository;
44
47
  exports.deleteRepositoryWorktrees = deleteRepositoryWorktrees;
45
48
  exports.deleteWorktreesByIds = deleteWorktreesByIds;
46
49
  const crypto_1 = require("crypto");
50
+ const selected_agents_validator_1 = require("../lib/selected-agents-validator");
47
51
  function mapChatMessage(row) {
48
52
  return {
49
53
  id: row.id,
@@ -129,6 +133,16 @@ function initDatabase(db) {
129
133
  * Get latest user message per CLI tool for multiple worktrees (batch query)
130
134
  * Optimized to avoid N+1 query problem
131
135
  */
136
+ /**
137
+ * Get latest user message per CLI tool for multiple worktrees (batch query)
138
+ * Optimized to avoid N+1 query problem
139
+ *
140
+ * R4-001: SQL IN clause for cli_tool_id removed to eliminate SQLインジェクション risk.
141
+ * All cli_tool_id values are fetched; filtering happens at application layer.
142
+ * Tool count is at most 4-5, so the performance impact is negligible.
143
+ *
144
+ * R2-002: Return type changed to Partial<Record<CLIToolType, string>>
145
+ */
132
146
  function getLastMessagesByCliBatch(db, worktreeIds) {
133
147
  if (worktreeIds.length === 0) {
134
148
  return new Map();
@@ -149,7 +163,6 @@ function getLastMessagesByCliBatch(db, worktreeIds) {
149
163
  FROM chat_messages
150
164
  WHERE worktree_id IN (${placeholders})
151
165
  AND role = 'user'
152
- AND cli_tool_id IN ('claude', 'codex', 'gemini')
153
166
  )
154
167
  SELECT worktree_id, cli_tool_id, content
155
168
  FROM ranked_messages
@@ -181,6 +194,7 @@ function getWorktrees(db, repositoryPath) {
181
194
  w.id, w.name, w.path, w.repository_path, w.repository_name, w.description,
182
195
  w.last_user_message, w.last_user_message_at, w.last_message_summary,
183
196
  w.updated_at, w.favorite, w.status, w.link, w.cli_tool_id, w.last_viewed_at,
197
+ w.selected_agents, w.vibe_local_model, w.vibe_local_context_window,
184
198
  (SELECT MAX(timestamp) FROM chat_messages
185
199
  WHERE worktree_id = w.id AND role = 'assistant') as last_assistant_message_at
186
200
  FROM worktrees w
@@ -216,6 +230,9 @@ function getWorktrees(db, repositoryPath) {
216
230
  status: row.status || null,
217
231
  link: row.link || undefined,
218
232
  cliToolId: row.cli_tool_id ?? 'claude',
233
+ selectedAgents: (0, selected_agents_validator_1.parseSelectedAgents)(row.selected_agents),
234
+ vibeLocalModel: row.vibe_local_model ?? null,
235
+ vibeLocalContextWindow: row.vibe_local_context_window ?? null,
219
236
  };
220
237
  });
221
238
  }
@@ -250,6 +267,7 @@ function getWorktreeById(db, id) {
250
267
  w.id, w.name, w.path, w.repository_path, w.repository_name, w.description,
251
268
  w.last_user_message, w.last_user_message_at, w.last_message_summary,
252
269
  w.updated_at, w.favorite, w.status, w.link, w.cli_tool_id, w.last_viewed_at,
270
+ w.selected_agents, w.vibe_local_model, w.vibe_local_context_window,
253
271
  (SELECT MAX(timestamp) FROM chat_messages
254
272
  WHERE worktree_id = w.id AND role = 'assistant') as last_assistant_message_at
255
273
  FROM worktrees w
@@ -276,6 +294,9 @@ function getWorktreeById(db, id) {
276
294
  status: row.status || null,
277
295
  link: row.link || undefined,
278
296
  cliToolId: row.cli_tool_id ?? 'claude',
297
+ selectedAgents: (0, selected_agents_validator_1.parseSelectedAgents)(row.selected_agents),
298
+ vibeLocalModel: row.vibe_local_model ?? null,
299
+ vibeLocalContextWindow: row.vibe_local_context_window ?? null,
279
300
  };
280
301
  }
281
302
  /**
@@ -678,6 +699,54 @@ function updateCliToolId(db, id, cliToolId) {
678
699
  `);
679
700
  stmt.run(cliToolId, id);
680
701
  }
702
+ /**
703
+ * Update selected_agents for a worktree
704
+ * Issue #368: Persists the user's choice of 2 display agents
705
+ *
706
+ * @param db - Database instance
707
+ * @param id - Worktree ID
708
+ * @param selectedAgents - Tuple of 2 CLIToolType values
709
+ */
710
+ function updateSelectedAgents(db, id, selectedAgents) {
711
+ const stmt = db.prepare(`
712
+ UPDATE worktrees
713
+ SET selected_agents = ?
714
+ WHERE id = ?
715
+ `);
716
+ stmt.run(JSON.stringify(selectedAgents), id);
717
+ }
718
+ /**
719
+ * Update vibe_local_model for a worktree
720
+ * Issue #368: Persists the user's Ollama model selection for vibe-local
721
+ *
722
+ * @param db - Database instance
723
+ * @param id - Worktree ID
724
+ * @param model - Model name or null for default
725
+ */
726
+ function updateVibeLocalModel(db, id, model) {
727
+ const stmt = db.prepare(`
728
+ UPDATE worktrees
729
+ SET vibe_local_model = ?
730
+ WHERE id = ?
731
+ `);
732
+ stmt.run(model, id);
733
+ }
734
+ /**
735
+ * Update vibe_local_context_window for a worktree
736
+ * Issue #374: Persists the user's Ollama context window size for vibe-local
737
+ *
738
+ * @param db - Database instance
739
+ * @param id - Worktree ID
740
+ * @param contextWindow - Context window size or null for default
741
+ */
742
+ function updateVibeLocalContextWindow(db, id, contextWindow) {
743
+ const stmt = db.prepare(`
744
+ UPDATE worktrees
745
+ SET vibe_local_context_window = ?
746
+ WHERE id = ?
747
+ `);
748
+ stmt.run(contextWindow, id);
749
+ }
681
750
  /**
682
751
  * Map database row to WorktreeMemo model
683
752
  */
@@ -166,11 +166,12 @@ const TEXT_INPUT_PATTERNS = [
166
166
  /differently/i,
167
167
  ];
168
168
  /**
169
- * Pattern for ❯ (U+276F) indicator lines used by Claude CLI to mark the default selection.
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).
170
171
  * Used in Pass 1 (existence check) and Pass 2 (option collection) of the 2-pass detection.
171
172
  * Anchored at both ends -- ReDoS safe (S4-001).
172
173
  */
173
- const DEFAULT_OPTION_PATTERN = /^\s*\u276F\s*(\d+)\.\s*(.+)$/;
174
+ const DEFAULT_OPTION_PATTERN = /^\s*[\u276F\u25CF\u203A]\s*(\d+)\.\s*(.+)$/;
174
175
  /**
175
176
  * Pattern for normal option lines (no ❯ indicator, just leading whitespace + number).
176
177
  * Only applied in Pass 2 when ❯ indicator existence is confirmed by Pass 1.
@@ -206,6 +207,13 @@ const SEPARATOR_LINE_PATTERN = /^[-─]+$/;
206
207
  * @see Issue #256: multiple_choice prompt detection improvement
207
208
  */
208
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;
209
217
  /**
210
218
  * Creates a "no prompt detected" result.
211
219
  * Centralizes the repeated pattern of returning isPrompt: false with trimmed content.
@@ -543,6 +551,7 @@ function detectMultipleChoicePrompt(output, options) {
543
551
  // ==========================================================================
544
552
  const collectedOptions = [];
545
553
  let questionEndIndex = -1;
554
+ let continuationLineCount = 0;
546
555
  for (let i = effectiveEnd - 1; i >= scanStart; i--) {
547
556
  const line = lines[i].trim();
548
557
  // Try DEFAULT_OPTION_PATTERN first (❯ indicator)
@@ -551,6 +560,7 @@ function detectMultipleChoicePrompt(output, options) {
551
560
  const number = parseInt(defaultMatch[1], 10);
552
561
  const label = defaultMatch[2].trim();
553
562
  collectedOptions.unshift({ number, label, isDefault: true });
563
+ continuationLineCount = 0;
554
564
  continue;
555
565
  }
556
566
  // Try NORMAL_OPTION_PATTERN (no ❯ indicator)
@@ -559,6 +569,7 @@ function detectMultipleChoicePrompt(output, options) {
559
569
  const number = parseInt(normalMatch[1], 10);
560
570
  const label = normalMatch[2].trim();
561
571
  collectedOptions.unshift({ number, label, isDefault: false });
572
+ continuationLineCount = 0;
562
573
  continue;
563
574
  }
564
575
  // [Issue #287 Bug3] User input prompt barrier:
@@ -567,7 +578,7 @@ function detectMultipleChoicePrompt(output, options) {
567
578
  // user input prompt (e.g., "❯ 1", "❯ /command") or idle prompt ("❯").
568
579
  // Anything above this line in the scrollback is historical conversation text,
569
580
  // not an active prompt. Stop scanning to prevent false positives.
570
- if (collectedOptions.length === 0 && line.startsWith('\u276F')) {
581
+ if (collectedOptions.length === 0 && (line.startsWith('\u276F') || line.startsWith('\u25CF') || line.startsWith('\u203A'))) {
571
582
  return noPromptResult(output);
572
583
  }
573
584
  // Non-option line handling
@@ -590,6 +601,15 @@ function detectMultipleChoicePrompt(output, options) {
590
601
  // or path/filename fragments from terminal width wrapping - Issue #181)
591
602
  const rawLine = lines[i]; // Original line with indentation preserved
592
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
+ }
593
613
  // Skip continuation lines and continue scanning for more options
594
614
  continue;
595
615
  }
@@ -90,13 +90,15 @@ function incompleteResult(lineCount) {
90
90
  * @param findRecentUserPromptIndex - Callback to locate the most recent user prompt
91
91
  * @returns ExtractionResult with isComplete: true and ANSI-stripped response
92
92
  */
93
- function buildPromptExtractionResult(lines, lastCapturedLine, totalLines, bufferReset, cliToolId, findRecentUserPromptIndex) {
93
+ function buildPromptExtractionResult(lines, lastCapturedLine, totalLines, bufferReset, cliToolId, findRecentUserPromptIndex, promptDetection) {
94
94
  const startIndex = resolveExtractionStartIndex(lastCapturedLine, totalLines, bufferReset, cliToolId, findRecentUserPromptIndex);
95
95
  const extractedLines = lines.slice(startIndex);
96
96
  return {
97
97
  response: (0, cli_patterns_1.stripAnsi)(extractedLines.join('\n')),
98
98
  isComplete: true,
99
99
  lineCount: totalLines,
100
+ promptDetection,
101
+ bufferReset,
100
102
  };
101
103
  }
102
104
  /**
@@ -131,7 +133,7 @@ function getPollerKey(worktreeId, cliToolId) {
131
133
  */
132
134
  function detectPromptWithOptions(output, cliToolId) {
133
135
  const promptOptions = (0, cli_patterns_1.buildDetectPromptOptions)(cliToolId);
134
- return (0, prompt_detector_1.detectPrompt)((0, cli_patterns_1.stripAnsi)(output), promptOptions);
136
+ return (0, prompt_detector_1.detectPrompt)((0, cli_patterns_1.stripBoxDrawing)((0, cli_patterns_1.stripAnsi)(output)), promptOptions);
135
137
  }
136
138
  /**
137
139
  * Clean up Claude response by removing shell setup commands, environment exports, ANSI codes, and banner
@@ -357,14 +359,20 @@ function extractResponse(output, lastCapturedLine, cliToolId) {
357
359
  }
358
360
  return -1;
359
361
  };
360
- // Early check for Claude permission prompts (before extraction logic)
361
- // Permission prompts appear after normal responses and need special handling
362
- if (cliToolId === 'claude') {
362
+ // Early check for interactive prompts (before extraction logic)
363
+ // Permission prompts appear after normal responses and need special handling.
364
+ // Issue #372: Codex command confirmation prompts ( 1. Yes, proceed) match
365
+ // CODEX_PROMPT_PATTERN, causing isCodexOrGeminiComplete to fire prematurely.
366
+ // Early detection ensures prompt options are preserved in the extraction result.
367
+ if (cliToolId === 'claude' || cliToolId === 'codex') {
363
368
  const fullOutput = lines.join('\n');
364
369
  const promptDetection = detectPromptWithOptions(fullOutput, cliToolId);
365
370
  if (promptDetection.isPrompt) {
366
- // Prompt detection uses full buffer for accuracy, but return only lastCapturedLine onwards
367
- return buildPromptExtractionResult(lines, lastCapturedLine, totalLines, bufferReset, cliToolId, findRecentUserPromptIndex);
371
+ // Prompt detection uses full buffer for accuracy, but return only lastCapturedLine onwards.
372
+ // Issue #372: Carry promptDetection through ExtractionResult so checkForResponse()
373
+ // can use it directly, avoiding a second detection on the (potentially truncated)
374
+ // extracted portion which may miss the › indicator line.
375
+ return buildPromptExtractionResult(lines, lastCapturedLine, totalLines, bufferReset, cliToolId, findRecentUserPromptIndex, promptDetection);
368
376
  }
369
377
  }
370
378
  // Strip ANSI codes before pattern matching
@@ -372,11 +380,12 @@ function extractResponse(output, lastCapturedLine, cliToolId) {
372
380
  const hasPrompt = promptPattern.test(cleanOutputToCheck);
373
381
  const hasSeparator = separatorPattern.test(cleanOutputToCheck);
374
382
  const isThinking = thinkingPattern.test(cleanOutputToCheck);
375
- // Codex/Gemini completion logic: prompt detected and not thinking (separator optional)
383
+ // Codex/Gemini/Vibe-Local completion logic: prompt detected and not thinking (separator optional)
376
384
  // - Codex: Interactive TUI, detects › prompt
377
- // - Gemini: Non-interactive one-shot, detects shell prompt (%, $)
385
+ // - Gemini: Interactive REPL, detects > / prompt
386
+ // - Vibe-Local: Interactive REPL, detects > prompt
378
387
  // Claude: require both prompt and separator
379
- const isCodexOrGeminiComplete = (cliToolId === 'codex' || cliToolId === 'gemini') && hasPrompt && !isThinking;
388
+ const isCodexOrGeminiComplete = (cliToolId === 'codex' || cliToolId === 'gemini' || cliToolId === 'vibe-local') && hasPrompt && !isThinking;
380
389
  const isClaudeComplete = cliToolId === 'claude' && hasPrompt && hasSeparator && !isThinking;
381
390
  if (isCodexOrGeminiComplete || isClaudeComplete) {
382
391
  // CLI tool has completed response
@@ -465,6 +474,7 @@ function extractResponse(output, lastCapturedLine, cliToolId) {
465
474
  response,
466
475
  isComplete: true,
467
476
  lineCount: endIndex, // Use endIndex instead of totalLines to track where we actually stopped
477
+ bufferReset,
468
478
  };
469
479
  }
470
480
  // Check if this is an interactive prompt (yes/no or multiple choice)
@@ -474,7 +484,7 @@ function extractResponse(output, lastCapturedLine, cliToolId) {
474
484
  if (promptDetection.isPrompt) {
475
485
  // Prompt detection uses full buffer for accuracy, but return only lastCapturedLine onwards
476
486
  // stripAnsi is applied inside buildPromptExtractionResult (Stage 4 MF-001: XSS risk mitigation)
477
- return buildPromptExtractionResult(lines, lastCapturedLine, totalLines, bufferReset, cliToolId, findRecentUserPromptIndex);
487
+ return buildPromptExtractionResult(lines, lastCapturedLine, totalLines, bufferReset, cliToolId, findRecentUserPromptIndex, promptDetection);
478
488
  }
479
489
  // Not a prompt, but we may have a partial response in progress (even if Claude shows a spinner)
480
490
  const responseLines = [];
@@ -524,6 +534,7 @@ async function checkForResponse(worktreeId, cliToolId) {
524
534
  // Check if CLI tool session is running
525
535
  const running = await (0, cli_session_1.isSessionRunning)(worktreeId, cliToolId);
526
536
  if (!running) {
537
+ console.log(`[checkForResponse] Session not running for ${worktreeId} (${cliToolId}), stopping poller`);
527
538
  stopPolling(worktreeId, cliToolId);
528
539
  return false;
529
540
  }
@@ -555,17 +566,23 @@ async function checkForResponse(worktreeId, cliToolId) {
555
566
  }
556
567
  // CRITICAL FIX: If lineCount == lastCapturedLine AND there's no in-progress message,
557
568
  // this response has already been saved. Skip to prevent duplicates.
558
- if (result.lineCount === lastCapturedLine && !sessionState?.inProgressMessageId) {
569
+ // Issue #372: Skip when buffer reset detected (TUI redraw may coincidentally match lineCount).
570
+ if (!result.bufferReset && result.lineCount === lastCapturedLine && !sessionState?.inProgressMessageId) {
559
571
  return false;
560
572
  }
561
573
  // Additional duplicate prevention: check if savePendingAssistantResponse
562
- // already saved this content by comparing line counts
563
- if (result.lineCount <= lastCapturedLine) {
574
+ // already saved this content by comparing line counts.
575
+ // Issue #372: Skip this check when buffer reset is detected (TUI redraw, screen clear).
576
+ // Codex TUI redraws cause totalLines to shrink, making lineCount < lastCapturedLine.
577
+ if (!result.bufferReset && result.lineCount <= lastCapturedLine) {
564
578
  console.log(`[checkForResponse] Already saved up to line ${lastCapturedLine}, skipping (result: ${result.lineCount})`);
565
579
  return false;
566
580
  }
567
- // Response is complete! Check if it's a prompt
568
- const promptDetection = detectPromptWithOptions(result.response, cliToolId);
581
+ // Response is complete! Check if it's a prompt.
582
+ // Issue #372: Prefer the prompt detection carried from extractResponse() early check,
583
+ // which uses the full tmux output for accuracy. The extracted portion (result.response)
584
+ // may be truncated and miss the › indicator line when lastCapturedLine falls just before it.
585
+ const promptDetection = result.promptDetection ?? detectPromptWithOptions(result.response, cliToolId);
569
586
  if (promptDetection.isPrompt) {
570
587
  // This is a prompt - save as prompt message
571
588
  (0, db_1.clearInProgressMessageId)(db, worktreeId, cliToolId);
@@ -667,10 +684,15 @@ function startPolling(worktreeId, cliToolId) {
667
684
  stopPolling(worktreeId, cliToolId);
668
685
  // Record start time
669
686
  pollingStartTimes.set(pollerKey, Date.now());
670
- // Start polling
671
- const interval = setInterval(async () => {
672
- const startTime = pollingStartTimes.get(pollerKey);
687
+ // Start polling with setTimeout chain to prevent race conditions
688
+ scheduleNextResponsePoll(worktreeId, cliToolId);
689
+ }
690
+ /** Schedule next checkForResponse() after current one completes (setTimeout chain) */
691
+ function scheduleNextResponsePoll(worktreeId, cliToolId) {
692
+ const pollerKey = getPollerKey(worktreeId, cliToolId);
693
+ const timerId = setTimeout(async () => {
673
694
  // Check if max duration exceeded
695
+ const startTime = pollingStartTimes.get(pollerKey);
674
696
  if (startTime && Date.now() - startTime > MAX_POLLING_DURATION) {
675
697
  stopPolling(worktreeId, cliToolId);
676
698
  return;
@@ -682,8 +704,13 @@ function startPolling(worktreeId, cliToolId) {
682
704
  catch (error) {
683
705
  console.error(`[Poller] Error:`, error);
684
706
  }
707
+ // Schedule next poll ONLY after current one completes
708
+ // Guard: only if poller is still active (not stopped during checkForResponse)
709
+ if (activePollers.has(pollerKey)) {
710
+ scheduleNextResponsePoll(worktreeId, cliToolId);
711
+ }
685
712
  }, POLLING_INTERVAL);
686
- activePollers.set(pollerKey, interval);
713
+ activePollers.set(pollerKey, timerId);
687
714
  }
688
715
  /**
689
716
  * Stop polling for a worktree and CLI tool combination
@@ -698,9 +725,9 @@ function startPolling(worktreeId, cliToolId) {
698
725
  */
699
726
  function stopPolling(worktreeId, cliToolId) {
700
727
  const pollerKey = getPollerKey(worktreeId, cliToolId);
701
- const interval = activePollers.get(pollerKey);
702
- if (interval) {
703
- clearInterval(interval);
728
+ const timerId = activePollers.get(pollerKey);
729
+ if (timerId) {
730
+ clearTimeout(timerId);
704
731
  activePollers.delete(pollerKey);
705
732
  pollingStartTimes.delete(pollerKey);
706
733
  }
@@ -220,12 +220,16 @@ async function executeSchedule(state) {
220
220
  const logId = createExecutionLog(state.scheduleId, state.worktreeId, state.entry.message);
221
221
  try {
222
222
  const db = getLazyDbInstance();
223
- const worktree = db.prepare('SELECT path FROM worktrees WHERE id = ?').get(state.worktreeId);
223
+ const worktree = db.prepare('SELECT path, vibe_local_model FROM worktrees WHERE id = ?').get(state.worktreeId);
224
224
  if (!worktree) {
225
225
  updateExecutionLog(logId, 'failed', 'Worktree not found', null);
226
226
  return;
227
227
  }
228
- const result = await (0, claude_executor_1.executeClaudeCommand)(state.entry.message, worktree.path, state.entry.cliToolId, state.entry.permission);
228
+ // Build options for vibe-local model
229
+ const options = state.entry.cliToolId === 'vibe-local' && worktree.vibe_local_model
230
+ ? { model: worktree.vibe_local_model }
231
+ : undefined;
232
+ const result = await (0, claude_executor_1.executeClaudeCommand)(state.entry.message, worktree.path, state.entry.cliToolId, state.entry.permission, options);
229
233
  updateExecutionLog(logId, result.status, result.output, result.exitCode);
230
234
  updateScheduleLastExecuted(state.scheduleId);
231
235
  console.log(`[schedule-manager] Executed ${state.entry.name}: ${result.status}`);
@@ -0,0 +1,99 @@
1
+ "use strict";
2
+ /**
3
+ * Selected Agents Validator
4
+ * Issue #368: Validation and parsing for worktree selected_agents field
5
+ *
6
+ * Provides:
7
+ * - validateAgentsPair(): Core validation logic (R1-001)
8
+ * - parseSelectedAgents(): DB read with fallback + console.warn (R4-005 log sanitization)
9
+ * - validateSelectedAgentsInput(): API input validation
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.DEFAULT_SELECTED_AGENTS = void 0;
13
+ exports.validateAgentsPair = validateAgentsPair;
14
+ exports.parseSelectedAgents = parseSelectedAgents;
15
+ exports.validateSelectedAgentsInput = validateSelectedAgentsInput;
16
+ const types_1 = require("./cli-tools/types");
17
+ /**
18
+ * ANSI escape code pattern for log sanitization (R4-005)
19
+ * Duplicated from cli-patterns.ts to avoid importing server-side logger chain
20
+ */
21
+ const ANSI_PATTERN = /\x1b\[[0-9;]*[a-zA-Z]|\x1b\][^\x07]*\x07|\[[0-9;]*m/g;
22
+ /**
23
+ * Strip ANSI escape codes from a string
24
+ */
25
+ function stripAnsi(str) {
26
+ return str.replace(ANSI_PATTERN, '');
27
+ }
28
+ /**
29
+ * Sanitize raw DB value for safe console.warn output (R4-005)
30
+ * Removes ANSI escapes, newlines, and truncates to 100 chars
31
+ */
32
+ function sanitizeRawForLog(raw) {
33
+ return stripAnsi(raw).replace(/[\n\r]/g, ' ').substring(0, 100);
34
+ }
35
+ /** Default selected agents when DB value is missing or invalid */
36
+ exports.DEFAULT_SELECTED_AGENTS = ['claude', 'codex'];
37
+ /**
38
+ * Core validation function for a pair of CLI tool IDs (R1-001)
39
+ * Shared between parseSelectedAgents() and validateSelectedAgentsInput()
40
+ *
41
+ * @param input - Array of values to validate
42
+ * @returns Validation result with optional typed value or error message
43
+ */
44
+ function validateAgentsPair(input) {
45
+ if (input.length !== 2) {
46
+ return { valid: false, error: 'Must be 2 elements' };
47
+ }
48
+ if (!input.every(id => typeof id === 'string' && types_1.CLI_TOOL_IDS.includes(id))) {
49
+ return { valid: false, error: 'Invalid CLI tool ID' };
50
+ }
51
+ if (input[0] === input[1]) {
52
+ return { valid: false, error: 'Duplicate tool IDs not allowed' };
53
+ }
54
+ return { valid: true, value: input };
55
+ }
56
+ /**
57
+ * Parse selected_agents JSON from DB with safe fallback
58
+ * Returns default value for any invalid input (never throws)
59
+ *
60
+ * Logs a warning when fallback is triggered to help detect DB data issues.
61
+ * Log output is sanitized (R4-005): ANSI stripped, newlines removed, truncated.
62
+ *
63
+ * @param raw - Raw JSON string from DB (or null)
64
+ * @returns Validated tuple of 2 CLIToolType values
65
+ */
66
+ function parseSelectedAgents(raw) {
67
+ if (!raw)
68
+ return exports.DEFAULT_SELECTED_AGENTS;
69
+ try {
70
+ const parsed = JSON.parse(raw);
71
+ if (!Array.isArray(parsed)) {
72
+ console.warn(`[selected-agents] Invalid format in DB, falling back to default: ${sanitizeRawForLog(raw)}`);
73
+ return exports.DEFAULT_SELECTED_AGENTS;
74
+ }
75
+ const result = validateAgentsPair(parsed);
76
+ if (!result.valid) {
77
+ console.warn(`[selected-agents] Invalid data in DB (${result.error}), falling back to default: ${sanitizeRawForLog(raw)}`);
78
+ return exports.DEFAULT_SELECTED_AGENTS;
79
+ }
80
+ return result.value;
81
+ }
82
+ catch {
83
+ console.warn(`[selected-agents] JSON parse error in DB, falling back to default: ${sanitizeRawForLog(raw)}`);
84
+ return exports.DEFAULT_SELECTED_AGENTS;
85
+ }
86
+ }
87
+ /**
88
+ * Validate selectedAgents input from API request body
89
+ * Returns structured error for API error responses (does not fallback)
90
+ *
91
+ * @param input - Raw input from request body (unknown type for safety)
92
+ * @returns Validation result with typed value or error string
93
+ */
94
+ function validateSelectedAgentsInput(input) {
95
+ if (!Array.isArray(input) || input.length !== 2) {
96
+ return { valid: false, error: 'selected_agents must be an array of 2 elements' };
97
+ }
98
+ return validateAgentsPair(input);
99
+ }
@@ -8,6 +8,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
8
8
  exports.deriveCliStatus = deriveCliStatus;
9
9
  exports.calculateHasUnread = calculateHasUnread;
10
10
  exports.toBranchItem = toBranchItem;
11
+ const selected_agents_validator_1 = require("../lib/selected-agents-validator");
11
12
  /**
12
13
  * Derive BranchStatus from per-CLI tool session status flags.
13
14
  * Shared by sidebar (toBranchItem) and WorktreeDetailRefactored tab dots.
@@ -58,6 +59,13 @@ function toBranchItem(worktree) {
58
59
  const status = 'idle';
59
60
  // Use new hasUnread logic based on lastAssistantMessageAt and lastViewedAt
60
61
  const hasUnread = calculateHasUnread(worktree);
62
+ // Issue #368: Use selectedAgents to determine which tools to show status for
63
+ // Falls back to DEFAULT_SELECTED_AGENTS when selectedAgents is not set
64
+ const agents = worktree.selectedAgents ?? selected_agents_validator_1.DEFAULT_SELECTED_AGENTS;
65
+ const cliStatus = {};
66
+ for (const agent of agents) {
67
+ cliStatus[agent] = deriveCliStatus(worktree.sessionStatusByCli?.[agent]);
68
+ }
61
69
  return {
62
70
  id: worktree.id,
63
71
  name: worktree.name,
@@ -66,9 +74,6 @@ function toBranchItem(worktree) {
66
74
  hasUnread,
67
75
  lastActivity: worktree.updatedAt,
68
76
  description: worktree.description,
69
- cliStatus: {
70
- claude: deriveCliStatus(worktree.sessionStatusByCli?.claude),
71
- codex: deriveCliStatus(worktree.sessionStatusByCli?.codex),
72
- },
77
+ cliStatus,
73
78
  };
74
79
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "commandmate",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "description": "Git worktree management with Claude CLI and tmux sessions",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -88,14 +88,14 @@
88
88
  "@types/uuid": "^10.0.0",
89
89
  "@types/ws": "^8.18.1",
90
90
  "@vitejs/plugin-react": "^5.1.1",
91
- "@vitest/coverage-v8": "^4.0.16",
92
- "@vitest/ui": "^4.0.9",
91
+ "@vitest/coverage-v8": "^4.0.18",
92
+ "@vitest/ui": "^4.0.18",
93
93
  "eslint": "^8.57.0",
94
94
  "eslint-config-next": "^14.2.35",
95
95
  "tailwindcss": "^3.4.18",
96
96
  "tsc-alias": "~1.8.16",
97
97
  "tsx": "^4.20.6",
98
98
  "typescript": "^5.5.0",
99
- "vitest": "^4.0.16"
99
+ "vitest": "^4.0.18"
100
100
  }
101
101
  }