commandmate 0.14.0 → 0.14.1

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 (161) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-build-manifest.json +279 -279
  3. package/.next/app-path-routes-manifest.json +30 -30
  4. package/.next/build-manifest.json +5 -5
  5. package/.next/prerender-manifest.json +9 -9
  6. package/.next/react-loadable-manifest.json +4 -4
  7. package/.next/required-server-files.json +1 -1
  8. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  9. package/.next/server/app/api/app/update/route_client-reference-manifest.js +1 -1
  10. package/.next/server/app/api/app/update-check/route.js +1 -1
  11. package/.next/server/app/api/app/update-check/route_client-reference-manifest.js +1 -1
  12. package/.next/server/app/api/assistant/conversation/route_client-reference-manifest.js +1 -1
  13. package/.next/server/app/api/assistant/current-output/route_client-reference-manifest.js +1 -1
  14. package/.next/server/app/api/assistant/messages/route_client-reference-manifest.js +1 -1
  15. package/.next/server/app/api/assistant/session/route_client-reference-manifest.js +1 -1
  16. package/.next/server/app/api/assistant/start/route_client-reference-manifest.js +1 -1
  17. package/.next/server/app/api/assistant/terminal/route_client-reference-manifest.js +1 -1
  18. package/.next/server/app/api/assistant/tools/route_client-reference-manifest.js +1 -1
  19. package/.next/server/app/api/auth/login/route_client-reference-manifest.js +1 -1
  20. package/.next/server/app/api/auth/logout/route_client-reference-manifest.js +1 -1
  21. package/.next/server/app/api/auth/status/route_client-reference-manifest.js +1 -1
  22. package/.next/server/app/api/daily-summary/route_client-reference-manifest.js +1 -1
  23. package/.next/server/app/api/daily-summary/status/route_client-reference-manifest.js +1 -1
  24. package/.next/server/app/api/external-apps/[id]/health/route_client-reference-manifest.js +1 -1
  25. package/.next/server/app/api/external-apps/[id]/route_client-reference-manifest.js +1 -1
  26. package/.next/server/app/api/external-apps/route_client-reference-manifest.js +1 -1
  27. package/.next/server/app/api/hooks/claude-done/route_client-reference-manifest.js +1 -1
  28. package/.next/server/app/api/ollama/models/route_client-reference-manifest.js +1 -1
  29. package/.next/server/app/api/push/subscriptions/route_client-reference-manifest.js +1 -1
  30. package/.next/server/app/api/push/vapid/route_client-reference-manifest.js +1 -1
  31. package/.next/server/app/api/repositories/[id]/route_client-reference-manifest.js +1 -1
  32. package/.next/server/app/api/repositories/[id]/todos/[todoId]/route_client-reference-manifest.js +1 -1
  33. package/.next/server/app/api/repositories/[id]/todos/route_client-reference-manifest.js +1 -1
  34. package/.next/server/app/api/repositories/clone/[jobId]/route_client-reference-manifest.js +1 -1
  35. package/.next/server/app/api/repositories/clone/route_client-reference-manifest.js +1 -1
  36. package/.next/server/app/api/repositories/excluded/route_client-reference-manifest.js +1 -1
  37. package/.next/server/app/api/repositories/restore/route_client-reference-manifest.js +1 -1
  38. package/.next/server/app/api/repositories/route_client-reference-manifest.js +1 -1
  39. package/.next/server/app/api/repositories/scan/route_client-reference-manifest.js +1 -1
  40. package/.next/server/app/api/repositories/sync/route_client-reference-manifest.js +1 -1
  41. package/.next/server/app/api/sidebar/group-order/route_client-reference-manifest.js +1 -1
  42. package/.next/server/app/api/skills/[id]/route_client-reference-manifest.js +1 -1
  43. package/.next/server/app/api/skills/route_client-reference-manifest.js +1 -1
  44. package/.next/server/app/api/slash-commands/route_client-reference-manifest.js +1 -1
  45. package/.next/server/app/api/templates/[id]/route_client-reference-manifest.js +1 -1
  46. package/.next/server/app/api/templates/route_client-reference-manifest.js +1 -1
  47. package/.next/server/app/api/todos/route_client-reference-manifest.js +1 -1
  48. package/.next/server/app/api/worktrees/[id]/auto-yes/route_client-reference-manifest.js +1 -1
  49. package/.next/server/app/api/worktrees/[id]/capture/route_client-reference-manifest.js +1 -1
  50. package/.next/server/app/api/worktrees/[id]/cli-tool/route_client-reference-manifest.js +1 -1
  51. package/.next/server/app/api/worktrees/[id]/cmate/schedules/route_client-reference-manifest.js +1 -1
  52. package/.next/server/app/api/worktrees/[id]/current-output/route_client-reference-manifest.js +1 -1
  53. package/.next/server/app/api/worktrees/[id]/execution-logs/[logId]/route_client-reference-manifest.js +1 -1
  54. package/.next/server/app/api/worktrees/[id]/execution-logs/route_client-reference-manifest.js +1 -1
  55. package/.next/server/app/api/worktrees/[id]/files/[...path]/route_client-reference-manifest.js +1 -1
  56. package/.next/server/app/api/worktrees/[id]/git/branch/create/route_client-reference-manifest.js +1 -1
  57. package/.next/server/app/api/worktrees/[id]/git/branch/delete/route_client-reference-manifest.js +1 -1
  58. package/.next/server/app/api/worktrees/[id]/git/branches/route_client-reference-manifest.js +1 -1
  59. package/.next/server/app/api/worktrees/[id]/git/checkout/route_client-reference-manifest.js +1 -1
  60. package/.next/server/app/api/worktrees/[id]/git/commit/route_client-reference-manifest.js +1 -1
  61. package/.next/server/app/api/worktrees/[id]/git/diff/route_client-reference-manifest.js +1 -1
  62. package/.next/server/app/api/worktrees/[id]/git/fetch/route_client-reference-manifest.js +1 -1
  63. package/.next/server/app/api/worktrees/[id]/git/log/route_client-reference-manifest.js +1 -1
  64. package/.next/server/app/api/worktrees/[id]/git/pull/route_client-reference-manifest.js +1 -1
  65. package/.next/server/app/api/worktrees/[id]/git/push/route_client-reference-manifest.js +1 -1
  66. package/.next/server/app/api/worktrees/[id]/git/reset/route_client-reference-manifest.js +1 -1
  67. package/.next/server/app/api/worktrees/[id]/git/revert/route_client-reference-manifest.js +1 -1
  68. package/.next/server/app/api/worktrees/[id]/git/show/[commitHash]/route_client-reference-manifest.js +1 -1
  69. package/.next/server/app/api/worktrees/[id]/git/stage/route_client-reference-manifest.js +1 -1
  70. package/.next/server/app/api/worktrees/[id]/git/staged/route_client-reference-manifest.js +1 -1
  71. package/.next/server/app/api/worktrees/[id]/git/stash/[index]/route_client-reference-manifest.js +1 -1
  72. package/.next/server/app/api/worktrees/[id]/git/stash/apply/route_client-reference-manifest.js +1 -1
  73. package/.next/server/app/api/worktrees/[id]/git/stash/pop/route_client-reference-manifest.js +1 -1
  74. package/.next/server/app/api/worktrees/[id]/git/stash/push/route_client-reference-manifest.js +1 -1
  75. package/.next/server/app/api/worktrees/[id]/git/stash/route_client-reference-manifest.js +1 -1
  76. package/.next/server/app/api/worktrees/[id]/git/status/route_client-reference-manifest.js +1 -1
  77. package/.next/server/app/api/worktrees/[id]/git/unstage/route_client-reference-manifest.js +1 -1
  78. package/.next/server/app/api/worktrees/[id]/git/working-diff/route_client-reference-manifest.js +1 -1
  79. package/.next/server/app/api/worktrees/[id]/interrupt/route_client-reference-manifest.js +1 -1
  80. package/.next/server/app/api/worktrees/[id]/kill-session/route_client-reference-manifest.js +1 -1
  81. package/.next/server/app/api/worktrees/[id]/logs/[filename]/route_client-reference-manifest.js +1 -1
  82. package/.next/server/app/api/worktrees/[id]/logs/route_client-reference-manifest.js +1 -1
  83. package/.next/server/app/api/worktrees/[id]/marp-render/route_client-reference-manifest.js +1 -1
  84. package/.next/server/app/api/worktrees/[id]/memos/[memoId]/route_client-reference-manifest.js +1 -1
  85. package/.next/server/app/api/worktrees/[id]/memos/route_client-reference-manifest.js +1 -1
  86. package/.next/server/app/api/worktrees/[id]/messages/route_client-reference-manifest.js +1 -1
  87. package/.next/server/app/api/worktrees/[id]/prompt-response/route_client-reference-manifest.js +1 -1
  88. package/.next/server/app/api/worktrees/[id]/respond/route_client-reference-manifest.js +1 -1
  89. package/.next/server/app/api/worktrees/[id]/route_client-reference-manifest.js +1 -1
  90. package/.next/server/app/api/worktrees/[id]/schedules/[scheduleId]/route_client-reference-manifest.js +1 -1
  91. package/.next/server/app/api/worktrees/[id]/schedules/active/route_client-reference-manifest.js +1 -1
  92. package/.next/server/app/api/worktrees/[id]/schedules/route_client-reference-manifest.js +1 -1
  93. package/.next/server/app/api/worktrees/[id]/search/route_client-reference-manifest.js +1 -1
  94. package/.next/server/app/api/worktrees/[id]/send/route_client-reference-manifest.js +1 -1
  95. package/.next/server/app/api/worktrees/[id]/skills/[skillId]/install/route_client-reference-manifest.js +1 -1
  96. package/.next/server/app/api/worktrees/[id]/skills/[skillId]/plan/route_client-reference-manifest.js +1 -1
  97. package/.next/server/app/api/worktrees/[id]/skills/[skillId]/uninstall/route_client-reference-manifest.js +1 -1
  98. package/.next/server/app/api/worktrees/[id]/skills/[skillId]/uninstall-plan/route_client-reference-manifest.js +1 -1
  99. package/.next/server/app/api/worktrees/[id]/skills/route_client-reference-manifest.js +1 -1
  100. package/.next/server/app/api/worktrees/[id]/slash-commands/route.js +1 -1
  101. package/.next/server/app/api/worktrees/[id]/slash-commands/route_client-reference-manifest.js +1 -1
  102. package/.next/server/app/api/worktrees/[id]/special-keys/route_client-reference-manifest.js +1 -1
  103. package/.next/server/app/api/worktrees/[id]/start-polling/route_client-reference-manifest.js +1 -1
  104. package/.next/server/app/api/worktrees/[id]/terminal/route.js +1 -1
  105. package/.next/server/app/api/worktrees/[id]/terminal/route_client-reference-manifest.js +1 -1
  106. package/.next/server/app/api/worktrees/[id]/timers/history/route_client-reference-manifest.js +1 -1
  107. package/.next/server/app/api/worktrees/[id]/timers/route_client-reference-manifest.js +1 -1
  108. package/.next/server/app/api/worktrees/[id]/todos/[todoId]/route_client-reference-manifest.js +1 -1
  109. package/.next/server/app/api/worktrees/[id]/todos/route_client-reference-manifest.js +1 -1
  110. package/.next/server/app/api/worktrees/[id]/tree/[...path]/route_client-reference-manifest.js +1 -1
  111. package/.next/server/app/api/worktrees/[id]/tree/route_client-reference-manifest.js +1 -1
  112. package/.next/server/app/api/worktrees/[id]/upload/[...path]/route_client-reference-manifest.js +1 -1
  113. package/.next/server/app/api/worktrees/[id]/viewed/route_client-reference-manifest.js +1 -1
  114. package/.next/server/app/api/worktrees/route_client-reference-manifest.js +1 -1
  115. package/.next/server/app/chat/page_client-reference-manifest.js +1 -1
  116. package/.next/server/app/login/page_client-reference-manifest.js +1 -1
  117. package/.next/server/app/manifest.webmanifest/route_client-reference-manifest.js +1 -1
  118. package/.next/server/app/more/page_client-reference-manifest.js +1 -1
  119. package/.next/server/app/offline/page_client-reference-manifest.js +1 -1
  120. package/.next/server/app/page_client-reference-manifest.js +1 -1
  121. package/.next/server/app/proxy/[...path]/route_client-reference-manifest.js +1 -1
  122. package/.next/server/app/repositories/page_client-reference-manifest.js +1 -1
  123. package/.next/server/app/review/page_client-reference-manifest.js +1 -1
  124. package/.next/server/app/sessions/page_client-reference-manifest.js +1 -1
  125. package/.next/server/app/skills/[skillId]/page_client-reference-manifest.js +1 -1
  126. package/.next/server/app/skills/page_client-reference-manifest.js +1 -1
  127. package/.next/server/app/worktrees/[id]/files/[...path]/page_client-reference-manifest.js +1 -1
  128. package/.next/server/app/worktrees/[id]/page.js +10 -10
  129. package/.next/server/app/worktrees/[id]/page_client-reference-manifest.js +1 -1
  130. package/.next/server/app/worktrees/[id]/terminal/page_client-reference-manifest.js +1 -1
  131. package/.next/server/app-paths-manifest.json +30 -30
  132. package/.next/server/chunks/2119.js +1 -1
  133. package/.next/server/chunks/2404.js +1 -1
  134. package/.next/server/chunks/2445.js +1 -1
  135. package/.next/server/chunks/5959.js +3 -3
  136. package/.next/server/chunks/6130.js +1 -1
  137. package/.next/server/chunks/6283.js +1 -1
  138. package/.next/server/middleware-build-manifest.js +1 -1
  139. package/.next/server/middleware-manifest.json +5 -5
  140. package/.next/server/middleware-react-loadable-manifest.js +1 -1
  141. package/.next/server/pages/500.html +1 -1
  142. package/.next/server/server-reference-manifest.json +1 -1
  143. package/.next/static/chunks/{4027-0c529b322be7d4d8.js → 4027-000402aee1e3520b.js} +1 -1
  144. package/.next/static/chunks/{6488-1a37ccee1d444c10.js → 6488-74d2f443e43c07b7.js} +1 -1
  145. package/.next/static/chunks/{6497-dc26c6c6b0b15adf.js → 6497-ea466b22a6c1ae89.js} +1 -1
  146. package/.next/static/chunks/{8566-b8bbcfd837cc338b.js → 8566-9da32e46c9d3fd54.js} +1 -1
  147. package/.next/static/chunks/app/chat/{page-9cf4ba62e21d9074.js → page-623f5cb9040de639.js} +1 -1
  148. package/.next/static/chunks/app/worktrees/[id]/files/[...path]/{page-9ff4d065483cdfe9.js → page-6a45f3744da50d95.js} +1 -1
  149. package/.next/static/chunks/app/worktrees/[id]/page-983a80ece6289026.js +1 -0
  150. package/.next/static/chunks/webpack-e655374fed175528.js +1 -0
  151. package/.next/trace +11 -11
  152. package/dist/server/src/lib/cli-tools/submit-verified-sender.js +120 -34
  153. package/dist/server/src/lib/detection/cli-patterns.js +27 -3
  154. package/dist/server/src/lib/detection/prompt-detect-multiple-choice.js +19 -0
  155. package/dist/server/src/lib/session/current-output-builder.js +10 -1
  156. package/dist/server/src/lib/tmux/tmux.js +26 -0
  157. package/package.json +1 -1
  158. package/.next/static/chunks/app/worktrees/[id]/page-f2801662a721faab.js +0 -1
  159. package/.next/static/chunks/webpack-0f9275e279c058eb.js +0 -1
  160. /package/.next/static/{Etea8LidpB8eWOTac-R8b → 1WyelLMRgPnhvA2HmhZmm}/_buildManifest.js +0 -0
  161. /package/.next/static/{Etea8LidpB8eWOTac-R8b → 1WyelLMRgPnhvA2HmhZmm}/_ssgManifest.js +0 -0
@@ -29,8 +29,18 @@
29
29
  * `[Pasted text #\d+` placeholder. That placeholder is used only as one
30
30
  * "still pending" positive signal (broadened to be version-resilient); the
31
31
  * primary decision is "is the message still sitting on the input line?".
32
+ *
33
+ * Issue #1501 hardens the "still pending" branch. A TUI completion popup can
34
+ * REPLACE the typed body with a different command (`/status` -> `/statusline`,
35
+ * `/review` -> `/teamwork-preview`) when Enter selects a highlighted suggestion.
36
+ * The old substring check misread the replacement as "still typed" and resent
37
+ * Enter (executing the wrong command), or as "submitted" and left the residual
38
+ * behind (detonating on the next send). The decision is now three-valued —
39
+ * submitted / pending / replaced — and a `replaced` verdict clears the input
40
+ * line and THROWS instead of resending Enter (see classifySubmit).
32
41
  */
33
42
  Object.defineProperty(exports, "__esModule", { value: true });
43
+ exports.classifySubmit = classifySubmit;
34
44
  exports.isSubmitted = isSubmitted;
35
45
  exports.sendMessageWithSubmitVerification = sendMessageWithSubmitVerification;
36
46
  const tmux_1 = require("../tmux/tmux");
@@ -49,6 +59,18 @@ const logger = (0, logger_1.createLogger)('cli-tools/submit-verified-sender');
49
59
  * a tool that never renders it simply never matches.
50
60
  */
51
61
  const PASTE_PLACEHOLDER_PATTERN = /\[Pasted text[\s#]/;
62
+ /**
63
+ * A slash-command sitting on the input line (Issue #1501).
64
+ *
65
+ * TUI completion popups replace a typed slash command with a highlighted menu
66
+ * item (`/status` -> `/statusline`, `/review` -> `/teamwork-preview`); the
67
+ * result is always another slash command. Scoping the "replaced" verdict to
68
+ * `/…` text keeps idle-prompt placeholders that some TUIs paint on an empty
69
+ * composer (gemini's "Type your message or @path", claude's hints, "? for
70
+ * shortcuts") — none of which start with `/` — from being mistaken for a
71
+ * substitution, so a genuinely-submitted message never fails spuriously.
72
+ */
73
+ const REPLACEMENT_COMMAND_PATTERN = /^\/[A-Za-z]/;
52
74
  /**
53
75
  * Prompt input-line markers across the supported TUIs.
54
76
  * claude/gemini/copilot: `>` or `❯`; codex: `›`; antigravity: `>`;
@@ -59,22 +81,18 @@ const INPUT_LINE_MARKER = /^\s*(?:ctx:\d+%\s*)?[>❯›]/;
59
81
  const VERIFY_WINDOW_LINES = 12;
60
82
  /** Default bounded read-back attempts before giving up (throwing). */
61
83
  const DEFAULT_VERIFY_ATTEMPTS = 4;
62
- /** Minimum fragment length used to decide the body is still on the input line. */
63
- const MIN_FRAGMENT_LENGTH = 3;
64
- const MAX_FRAGMENT_LENGTH = 24;
65
84
  /**
66
- * First non-blank fragment of the message, used to decide whether the body is
67
- * still sitting on the input line. Only the first line matters because that is
68
- * what a TUI shows on the prompt line before folding into a paste placeholder.
85
+ * First non-blank line of the message, trimmed. This is what a TUI shows on the
86
+ * prompt line before the body folds into a paste placeholder. NOT truncated:
87
+ * the replacement check (inputMatchesBody) needs the full first line so that a
88
+ * completion suffix (`/status` -> `/statusline`) is not mistaken for the body.
69
89
  */
70
- function comparableFragment(message) {
71
- const firstLine = message
90
+ function firstNonBlankLine(message) {
91
+ const line = message
72
92
  .split('\n')
73
93
  .map((l) => l.trim())
74
94
  .find((l) => l.length > 0);
75
- if (!firstLine)
76
- return '';
77
- return firstLine.slice(0, MAX_FRAGMENT_LENGTH);
95
+ return line ?? '';
78
96
  }
79
97
  /**
80
98
  * Locate the active input line within the pane tail: the last line that begins
@@ -90,40 +108,87 @@ function findInputLine(windowLines) {
90
108
  }
91
109
  return null;
92
110
  }
111
+ /** Input line text with the prompt marker stripped and surrounding space trimmed. */
112
+ function stripInputMarker(inputLine) {
113
+ return inputLine.replace(INPUT_LINE_MARKER, '').trim();
114
+ }
93
115
  /**
94
- * Decide whether a captured pane shows the message as submitted.
116
+ * Whether the (marker-stripped, non-empty) input-line text is still our
117
+ * unsent body rather than a TUI-substituted command.
118
+ *
119
+ * The still-unsent body appears verbatim on the input line. Line wrapping can
120
+ * visually truncate it to a PREFIX of the first line, but a completion popup
121
+ * always produces a DIFFERENT string — the body plus a completion suffix
122
+ * (`/status` -> `/statusline`) or an unrelated command (`/review` ->
123
+ * `/teamwork-preview`) — which is never a prefix of the body. So the body is
124
+ * "still there" iff the input text is a prefix of (or equals) the body's first
125
+ * line. This deliberately rejects `/statusline` for a `/status` body: the safe
126
+ * rule is "input text that is not the body (or a prefix of it) is NOT resent".
127
+ */
128
+ function inputMatchesBody(strippedInput, message) {
129
+ const bodyFirstLine = firstNonBlankLine(message);
130
+ if (bodyFirstLine.length === 0)
131
+ return false;
132
+ return bodyFirstLine.startsWith(strippedInput);
133
+ }
134
+ /**
135
+ * Classify a captured pane into submitted / pending / replaced (Issue #1501).
95
136
  *
96
137
  * Version-independent by design — does NOT require the paste placeholder:
97
- * A. The tool is generating a response -> submitted.
98
- * B. A paste placeholder is on the input line -> NOT submitted (Enter eaten).
99
- * C. The body fragment is still on the input line -> NOT submitted.
100
- * D. Otherwise (empty input line / moved to history) -> submitted.
138
+ * A. The tool is generating a response -> submitted.
139
+ * B. No input line, or the input line is empty -> submitted.
140
+ * C. A paste placeholder is folded on the input line -> pending (Enter eaten).
141
+ * D. The body is still verbatim on the input line -> pending (resend Enter).
142
+ * E. A DIFFERENT slash command is on the input line -> replaced (TUI popup
143
+ * autocompleted the command; clear the line and throw, never resend Enter).
144
+ * F. Any other non-empty text (idle placeholder/hint) -> submitted (unchanged
145
+ * pre-#1501 permissive default, so normal sends never spuriously fail).
101
146
  *
102
- * B and C are scoped to the input line only, so the user-message echo that a
103
- * TUI prints into its history above the prompt never causes a false "pending".
147
+ * C–F are scoped to the input line only, so the user-message echo that a TUI
148
+ * prints into its history above the prompt never causes a false verdict.
104
149
  */
105
- function isSubmitted(output, cliToolId, message) {
150
+ function classifySubmit(output, cliToolId, message) {
106
151
  const clean = (0, cli_patterns_1.stripAnsi)(output);
107
152
  const windowLines = clean.split('\n').slice(-VERIFY_WINDOW_LINES);
108
153
  const windowStr = windowLines.join('\n');
109
154
  // A. Actively generating a response => the message was accepted.
110
155
  if ((0, cli_patterns_1.detectThinking)(cliToolId, windowStr)) {
111
- return true;
156
+ return 'submitted';
112
157
  }
113
158
  const inputLine = findInputLine(windowLines);
114
- if (inputLine) {
115
- // B. A paste placeholder is still folded on the input line => not submitted.
116
- if (PASTE_PLACEHOLDER_PATTERN.test(inputLine)) {
117
- return false;
118
- }
119
- // C. The typed body is still sitting on the input line => not submitted.
120
- const fragment = comparableFragment(message);
121
- if (fragment.length >= MIN_FRAGMENT_LENGTH && inputLine.includes(fragment)) {
122
- return false;
123
- }
159
+ // B. No input line visible => the prompt scrolled off / moved on => submitted.
160
+ if (!inputLine) {
161
+ return 'submitted';
162
+ }
163
+ const strippedInput = stripInputMarker(inputLine);
164
+ // B. Empty input line => the message left the box => submitted.
165
+ if (strippedInput.length === 0) {
166
+ return 'submitted';
167
+ }
168
+ // C. A paste placeholder is still folded on the input line => body is there.
169
+ if (PASTE_PLACEHOLDER_PATTERN.test(inputLine)) {
170
+ return 'pending';
124
171
  }
125
- // D. Input line is clear (or absent) and nothing is generating => submitted.
126
- return true;
172
+ // D. The typed body is still sitting on the input line => resend Enter.
173
+ if (inputMatchesBody(strippedInput, message)) {
174
+ return 'pending';
175
+ }
176
+ // E. A different slash command is on the input line => a completion popup
177
+ // replaced what we typed. (Scoped to `/…` so idle-prompt placeholders that
178
+ // some TUIs paint on an empty composer are never mistaken for this.)
179
+ if (REPLACEMENT_COMMAND_PATTERN.test(strippedInput)) {
180
+ return 'replaced';
181
+ }
182
+ // F. Non-empty, non-command steady-state text (idle placeholder / hint) =>
183
+ // submitted, preserving the pre-#1501 permissive default.
184
+ return 'submitted';
185
+ }
186
+ /**
187
+ * Backward-compatible boolean view of {@link classifySubmit}: submitted vs not.
188
+ * A `replaced` verdict is NOT "submitted", so this returns false for it too.
189
+ */
190
+ function isSubmitted(output, cliToolId, message) {
191
+ return classifySubmit(output, cliToolId, message) === 'submitted';
127
192
  }
128
193
  /**
129
194
  * Type a message body and submit it, then verify the submit actually happened.
@@ -156,11 +221,32 @@ async function sendMessageWithSubmitVerification(params) {
156
221
  for (let attempt = 0; attempt < attempts; attempt++) {
157
222
  await new Promise((resolve) => setTimeout(resolve, verifyDelayMs));
158
223
  const output = await (0, tmux_1.capturePane)(sessionName, { startLine: -VERIFY_WINDOW_LINES });
159
- if (isSubmitted(output, cliToolId, message)) {
224
+ const state = classifySubmit(output, cliToolId, message);
225
+ if (state === 'submitted') {
160
226
  (0, tmux_capture_cache_1.invalidateCache)(sessionName);
161
227
  return;
162
228
  }
163
- // Still typed-but-unsent resend a single Enter and re-check.
229
+ if (state === 'replaced') {
230
+ // A TUI completion popup replaced our command with a different one.
231
+ // Resending Enter would EXECUTE that command (Issue #1501 flavor A);
232
+ // leaving it in place lets the residual detonate on the next send
233
+ // (flavor B). Clear the input line (best-effort) and surface the failure
234
+ // to the caller instead of ever resending Enter.
235
+ try {
236
+ await (0, tmux_1.clearInputLine)(sessionName);
237
+ }
238
+ catch (clearError) {
239
+ logger.error('submit-clear-input-failed', {
240
+ sessionName,
241
+ cliToolId,
242
+ error: clearError instanceof Error ? clearError.message : String(clearError),
243
+ });
244
+ }
245
+ (0, tmux_capture_cache_1.invalidateCache)(sessionName);
246
+ logger.error('submit-replaced-by-tui-completion', { sessionName, cliToolId, attempt });
247
+ throw new Error(`Message was replaced by a TUI autocompletion for session ${sessionName}; the input no longer matches the sent text. Cleared the input line without submitting to avoid executing a different command.`);
248
+ }
249
+ // state === 'pending' — still typed-but-unsent, resend a single Enter and re-check.
164
250
  logger.warn('submit-not-confirmed:resending-enter', {
165
251
  sessionName,
166
252
  cliToolId,
@@ -4,7 +4,7 @@
4
4
  * Shared between response-poller.ts and API routes
5
5
  */
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.CLAUDE_SESSION_ERROR_REGEX_PATTERNS = exports.CLAUDE_SESSION_ERROR_PATTERNS = exports.extractAnsiSequences = exports.stripAnsi = exports.ANTIGRAVITY_SKIP_PATTERNS = exports.ANTIGRAVITY_SELECTION_LIST_PATTERN = exports.ANTIGRAVITY_SEPARATOR_PATTERN = exports.ANTIGRAVITY_THINKING_PATTERN = exports.ANTIGRAVITY_PROMPT_PATTERN = exports.VIBE_LOCAL_THINKING_PATTERN = exports.VIBE_LOCAL_PROMPT_PATTERN = exports.COPILOT_SKIP_PATTERNS = exports.COPILOT_SELECTION_LIST_PATTERN = exports.COPILOT_SEPARATOR_PATTERN = exports.COPILOT_THINKING_PATTERN = exports.COPILOT_PROMPT_PATTERN = exports.OPENCODE_SKIP_PATTERNS = exports.OPENCODE_SEPARATOR_PATTERN = exports.CLAUDE_SELECTION_LIST_FOOTER = exports.OPENCODE_SELECTION_LIST_PATTERN = exports.OPENCODE_PROCESSING_INDICATOR = exports.OPENCODE_RESPONSE_COMPLETE = exports.OPENCODE_LOADING_PATTERN = exports.OPENCODE_THINKING_PATTERN = exports.OPENCODE_PROMPT_AFTER_RESPONSE = exports.OPENCODE_PROMPT_PATTERN = exports.GEMINI_THINKING_PATTERN = exports.GEMINI_PROMPT_PATTERN = exports.MAX_PASTED_TEXT_RETRIES = exports.PASTED_TEXT_DETECT_DELAY = exports.PASTED_TEXT_PATTERN = exports.CODEX_STATUS_BAR_PATTERN = exports.CODEX_PAGER_FOOTER_PATTERN = exports.CODEX_SELECTION_LIST_PATTERN = exports.CODEX_SEPARATOR_PATTERN = exports.CODEX_DIALOG_PATTERN = exports.CODEX_PROMPT_PATTERN = exports.CLAUDE_TRUST_DIALOG_PATTERN = exports.CLAUDE_SEPARATOR_PATTERN = exports.CLAUDE_PROMPT_PATTERN = exports.CODEX_THINKING_PATTERN = exports.CLAUDE_INTERRUPT_HINT_PATTERN = exports.CLAUDE_THINKING_PATTERN = exports.CLAUDE_SPINNER_CHARS = void 0;
7
+ exports.CLAUDE_SESSION_ERROR_REGEX_PATTERNS = exports.CLAUDE_SESSION_ERROR_PATTERNS = exports.extractAnsiSequences = exports.stripAnsi = exports.ANTIGRAVITY_SKIP_PATTERNS = exports.ANTIGRAVITY_SELECTION_LIST_PATTERN = exports.ANTIGRAVITY_SEPARATOR_PATTERN = exports.ANTIGRAVITY_THINKING_PATTERN = exports.ANTIGRAVITY_PROMPT_PATTERN = exports.VIBE_LOCAL_THINKING_PATTERN = exports.VIBE_LOCAL_PROMPT_PATTERN = exports.COPILOT_SKIP_PATTERNS = exports.COPILOT_SELECTION_LIST_PATTERN = exports.COPILOT_SEPARATOR_PATTERN = exports.COPILOT_THINKING_PATTERN = exports.COPILOT_PROMPT_PATTERN = exports.OPENCODE_SKIP_PATTERNS = exports.OPENCODE_SEPARATOR_PATTERN = exports.CLAUDE_SELECTION_LIST_FOOTER = exports.CLAUDE_MODEL_OVERLAY_FOOTER_PATTERN = exports.OPENCODE_SELECTION_LIST_PATTERN = exports.OPENCODE_PROCESSING_INDICATOR = exports.OPENCODE_RESPONSE_COMPLETE = exports.OPENCODE_LOADING_PATTERN = exports.OPENCODE_THINKING_PATTERN = exports.OPENCODE_PROMPT_AFTER_RESPONSE = exports.OPENCODE_PROMPT_PATTERN = exports.GEMINI_THINKING_PATTERN = exports.GEMINI_PROMPT_PATTERN = exports.MAX_PASTED_TEXT_RETRIES = exports.PASTED_TEXT_DETECT_DELAY = exports.PASTED_TEXT_PATTERN = exports.CODEX_STATUS_BAR_PATTERN = exports.CODEX_PAGER_FOOTER_PATTERN = exports.CODEX_SELECTION_LIST_PATTERN = exports.CODEX_SEPARATOR_PATTERN = exports.CODEX_DIALOG_PATTERN = exports.CODEX_PROMPT_PATTERN = exports.CLAUDE_TRUST_DIALOG_PATTERN = exports.CLAUDE_SEPARATOR_PATTERN = exports.CLAUDE_PROMPT_PATTERN = exports.CODEX_THINKING_PATTERN = exports.CLAUDE_INTERRUPT_HINT_PATTERN = exports.CLAUDE_THINKING_PATTERN = exports.CLAUDE_SPINNER_CHARS = void 0;
8
8
  exports.findClaudeChromeStart = findClaudeChromeStart;
9
9
  exports.isCodexPromptReady = isCodexPromptReady;
10
10
  exports.getCodexActiveDialog = getCodexActiveDialog;
@@ -464,6 +464,23 @@ exports.OPENCODE_PROCESSING_INDICATOR = /esc interrupt/;
464
464
  * " Connect a provider esc"
465
465
  */
466
466
  exports.OPENCODE_SELECTION_LIST_PATTERN = /^\s*(Select\s+(model|provider)|Connect\s+a\s+provider)/m;
467
+ /**
468
+ * [Issue #1495] Footer signature of Claude Code's `/model` local-settings overlay.
469
+ * Verified against a real Claude Code v2.1.218 capture, the footer reads:
470
+ * "Enter to set as default · s to use this session only · Esc to cancel"
471
+ *
472
+ * The overlay renders a ❯-marked numbered model list ("1. Default … 5. Haiku")
473
+ * under a "Select model" header that detectMultipleChoicePrompt() otherwise
474
+ * matches as a genuine multiple_choice prompt — which let Auto-Yes Enter-confirm
475
+ * a selection and silently change the user's default model. This "set as default"
476
+ * phrasing is unique to the model picker: genuine confirmation prompts never
477
+ * contain it (the trust dialog uses "Enter to confirm · Esc to cancel", Bash-tool
478
+ * approvals use "Esc to cancel · Tab to amend", AskUserQuestion uses
479
+ * "Enter to select · … to navigate"), so it is a safe exclusion signal.
480
+ *
481
+ * Linear pattern, no nested quantifiers — ReDoS safe (S4-001).
482
+ */
483
+ exports.CLAUDE_MODEL_OVERLAY_FOOTER_PATTERN = /Enter\s+to\s+set\s+as\s+default\b/i;
467
484
  /**
468
485
  * Claude CLI selection list footer pattern
469
486
  * Detects Claude CLI's interactive selection prompts that require
@@ -472,9 +489,16 @@ exports.OPENCODE_SELECTION_LIST_PATTERN = /^\s*(Select\s+(model|provider)|Connec
472
489
  * Matches footer instruction lines (known variants):
473
490
  * "Enter to select · Tab/Arrow keys to navigate · Esc to cancel"
474
491
  * "Enter to select · ↑/↓ to navigate · n to add notes · Esc to cancel"
475
- * "Enter to confirm · Esc to exit" (/model command)
492
+ * "Enter to confirm · Esc to exit" (legacy /model command footer)
493
+ * "Enter to set as default · s to use this session only · Esc to cancel"
494
+ * (/model command footer as of Claude Code v2.1.218 — Issue #1495)
495
+ *
496
+ * The "set as default" branch lets status-detector classify the `/model` overlay
497
+ * as a Claude selection list (NavigationButtons + ESC hatch, hasActivePrompt=false)
498
+ * once detectPrompt() no longer reports it as a prompt (see
499
+ * CLAUDE_MODEL_OVERLAY_FOOTER_PATTERN).
476
500
  */
477
- exports.CLAUDE_SELECTION_LIST_FOOTER = /Enter\s+to\s+(?:select\s+.*to\s+navigate|confirm\s+·\s+Esc)/;
501
+ exports.CLAUDE_SELECTION_LIST_FOOTER = /Enter\s+to\s+(?:select\s+.*to\s+navigate|confirm\s+·\s+Esc|set\s+as\s+default)/;
478
502
  /**
479
503
  * OpenCode TUI separator pattern (Issue #379)
480
504
  * Matches lines composed entirely of box-drawing / TUI decoration characters.
@@ -4,6 +4,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.buildMultipleChoiceResult = buildMultipleChoiceResult;
5
5
  exports.detectMultipleChoicePrompt = detectMultipleChoicePrompt;
6
6
  const tui_detection_frame_1 = require("./tui-detection-frame");
7
+ const cli_patterns_1 = require("./cli-patterns");
7
8
  // ============================================================================
8
9
  // Constants
9
10
  // ============================================================================
@@ -590,6 +591,24 @@ function detectMultipleChoicePrompt(output, options, truncateRawContentFn) {
590
591
  }
591
592
  }
592
593
  const scanWindow = lines.slice(scanStart, effectiveEnd);
594
+ // [Issue #1495] Claude `/model` local-settings overlay guard.
595
+ // The `/model` overlay renders a ❯-marked numbered model list ("1. Default …
596
+ // 5. Haiku") under a "Select model" header, which the option-collection logic
597
+ // below would otherwise return as a genuine multiple_choice prompt. Auto-Yes
598
+ // (detectAndRespondToPrompt) calls detectPrompt() directly and would resolve +
599
+ // Enter-confirm the default option — silently changing the user's default model.
600
+ // The overlay-unique "Enter to set as default …" footer never appears in real
601
+ // confirmation prompts (permission dialogs, the trust dialog, AskUserQuestion),
602
+ // so bailing out here stops Auto-Yes without touching any real-prompt path. The
603
+ // overlay is modal — no genuine prompt can be active at the same time, and the
604
+ // Claude TUI repaints the overlay away on close, so a stale footer cannot linger
605
+ // in the captured pane. status-detector then reclassifies it as a Claude
606
+ // selection list via CLAUDE_SELECTION_LIST_FOOTER (NavigationButtons + ESC hatch).
607
+ for (const rawLine of scanWindow) {
608
+ if (cli_patterns_1.CLAUDE_MODEL_OVERLAY_FOOTER_PATTERN.test(rawLine)) {
609
+ return noPromptResult(output);
610
+ }
611
+ }
593
612
  // Single-pass footer detection: identify both confirmation footer presence and
594
613
  // specific "press number to confirm" variant in one iteration (Issue #616).
595
614
  let hasConfirmationFooter = false;
@@ -53,7 +53,16 @@ async function buildCurrentOutput(db, worktreeId, cliToolId, instanceId) {
53
53
  const isPromptWaiting = statusResult.hasActivePrompt;
54
54
  const isSelectionListActive = statusResult.status === 'waiting' && status_detector_1.SELECTION_LIST_REASONS.has(statusResult.reason);
55
55
  const isPagerActive = statusResult.reason === status_detector_1.STATUS_REASON.CODEX_PAGER;
56
- const isUnclassifiedActive = statusResult.status === 'running' && statusResult.reason === status_detector_1.STATUS_REASON.DEFAULT;
56
+ // Issue #1497: the detection-independent nav hatch (#1017/#1494) is gated on
57
+ // isUnclassifiedActive. A static, unrecognized TUI overlay (e.g. Claude `/help`)
58
+ // whose frame stops changing degrades from `running`/`default` to `ready`/
59
+ // `no_recent_output` once the Auto-Yes poller has stamped lastOutputTimestamp
60
+ // (its sole writer, auto-yes-poller.ts). That is still an interactive-but-
61
+ // unclassified frame — a real idle prompt (`❯`) is classified earlier as
62
+ // `input_prompt`, never as `no_recent_output` — so treat the timed-out fallback
63
+ // as unclassified too and keep the hatch open instead of stranding the user.
64
+ const isUnclassifiedActive = (statusResult.status === 'running' && statusResult.reason === status_detector_1.STATUS_REASON.DEFAULT) ||
65
+ (statusResult.status === 'ready' && statusResult.reason === status_detector_1.STATUS_REASON.NO_RECENT_OUTPUT);
57
66
  const realtimeSnippet = lines.slice(-100).join('\n');
58
67
  const autoYesState = (0, auto_yes_manager_1.getAutoYesState)(worktreeId, cliToolId, instanceId);
59
68
  return {
@@ -17,6 +17,7 @@ exports.capturePane = capturePane;
17
17
  exports.killSession = killSession;
18
18
  exports.ensureSession = ensureSession;
19
19
  exports.sendSpecialKey = sendSpecialKey;
20
+ exports.clearInputLine = clearInputLine;
20
21
  exports.isAllowedSpecialKey = isAllowedSpecialKey;
21
22
  exports.sendSpecialKeysAndInvalidate = sendSpecialKeysAndInvalidate;
22
23
  const child_process_1 = require("child_process");
@@ -477,6 +478,31 @@ async function sendSpecialKey(sessionName, key) {
477
478
  throw new Error(`Failed to send special key: ${errorMessage}`);
478
479
  }
479
480
  }
481
+ /**
482
+ * Clear whatever text is currently sitting on the TUI input line (Issue #1501).
483
+ *
484
+ * Sends a single `C-u` (kill-line-before-cursor) so the prompt is wiped. Used by
485
+ * the submit-verified sender when it detects that a TUI popup autocompleted the
486
+ * typed command into a DIFFERENT one (e.g. `/status` -> `/statusline`): the
487
+ * residual text must be removed so it can neither be executed by a stray Enter
488
+ * nor detonate on the next send.
489
+ *
490
+ * `C-u` is a fixed literal passed via execFile (no shell, no injection) and is
491
+ * intentionally NOT added to ALLOWED_SPECIAL_KEYS / NAVIGATION_KEY_VALUES — it is
492
+ * an internal recovery primitive, never exposed through the special-keys API.
493
+ *
494
+ * @param sessionName - Target session name
495
+ * @throws {Error} If the tmux command fails
496
+ */
497
+ async function clearInputLine(sessionName) {
498
+ try {
499
+ await execFileAsync('tmux', ['send-keys', '-t', exactTarget(sessionName), 'C-u'], { timeout: DEFAULT_TIMEOUT });
500
+ }
501
+ catch (error) {
502
+ const errorMessage = error instanceof Error ? error.message : String(error);
503
+ throw new Error(`Failed to clear tmux input line: ${errorMessage}`);
504
+ }
505
+ }
480
506
  /**
481
507
  * Allowed navigation key names for special-keys API validation.
482
508
  * Used for TUI navigation sequences (e.g., Up/Down cursor, Enter/Escape selection).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "commandmate",
3
- "version": "0.14.0",
3
+ "version": "0.14.1",
4
4
  "description": "A local control plane for agent CLIs — orchestration and visibility for Claude Code, Codex, Gemini CLI, and more across Git worktrees",
5
5
  "keywords": [
6
6
  "claude-code",