@quantiya/codevibe-antigravity-plugin 2.0.102 → 2.0.104

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/agy-hook.js CHANGED
@@ -100,8 +100,32 @@ function decodeBoolean(val) {
100
100
  if (typeof val === "number") return val !== 0;
101
101
  return false;
102
102
  }
103
+ function sliceToSerializedBudget(rawText, buildCandidate, maxChars) {
104
+ if (maxChars <= 0) return null;
105
+ const emptyCandidate = buildCandidate("");
106
+ if (emptyCandidate.length > maxChars) {
107
+ return "[]".length <= maxChars ? "[]" : null;
108
+ }
109
+ const fullCandidate = buildCandidate(rawText);
110
+ if (fullCandidate.length <= maxChars) return fullCandidate;
111
+ let low = 0;
112
+ let high = rawText.length;
113
+ let best = emptyCandidate;
114
+ while (low <= high) {
115
+ const mid = low + high >>> 1;
116
+ const candidate = buildCandidate(rawText.slice(0, mid));
117
+ if (candidate.length <= maxChars) {
118
+ best = candidate;
119
+ low = mid + 1;
120
+ } else {
121
+ high = mid - 1;
122
+ }
123
+ }
124
+ return best;
125
+ }
103
126
  function serializeStructuredArg(value, maxChars = MAX_STRUCTURED_ARG_CHARS, keyHint) {
104
127
  try {
128
+ if (maxChars <= 0) return null;
105
129
  let parsed = value;
106
130
  if (typeof value === "string") {
107
131
  const trimmed = value.trim();
@@ -120,57 +144,72 @@ function serializeStructuredArg(value, maxChars = MAX_STRUCTURED_ARG_CHARS, keyH
120
144
  (item) => typeof item === "string" || typeof item === "number" || item && typeof item === "object" && !("question" in item)
121
145
  );
122
146
  if (isOptionArray) {
147
+ const wasSlicedAtCap = parsed.length > 30;
123
148
  const boundedOptions = parsed.slice(0, 30).map((opt, idx) => {
124
- if (typeof opt === "string") return { number: String(idx + 1), text: opt.slice(0, 256) };
149
+ if (typeof opt === "string") return { number: String(idx + 1), text: opt };
125
150
  if (typeof opt === "number") return { number: String(idx + 1), text: String(opt) };
126
151
  if (opt && typeof opt === "object") {
127
152
  const optObj = opt;
128
153
  const num = typeof optObj.number !== "undefined" ? String(optObj.number).slice(0, 16) : String(idx + 1);
129
- const txt = typeof optObj.text === "string" ? optObj.text.slice(0, 256) : String(optObj.text ?? optObj.label ?? optObj.value ?? "").slice(0, 256);
154
+ const txt = typeof optObj.text === "string" ? optObj.text : String(optObj.text ?? optObj.label ?? optObj.value ?? "");
130
155
  return { number: num, text: txt };
131
156
  }
132
- return { number: String(idx + 1), text: String(opt ?? "").slice(0, 256) };
157
+ return { number: String(idx + 1), text: String(opt ?? "") };
133
158
  });
159
+ if (wasSlicedAtCap && boundedOptions.length > 0) {
160
+ boundedOptions[0].options_truncated = true;
161
+ }
134
162
  const str2 = JSON.stringify(boundedOptions);
135
163
  if (str2.length <= maxChars) return str2;
136
164
  for (let i = boundedOptions.length - 1; i >= 1; i--) {
137
- const sub = JSON.stringify(boundedOptions.slice(0, i));
165
+ const prefix = boundedOptions.slice(0, i).map(
166
+ (o, idx) => idx === 0 ? { ...o, options_truncated: true } : o
167
+ );
168
+ const sub = JSON.stringify(prefix);
138
169
  if (sub.length <= maxChars) return sub;
139
170
  }
140
171
  const firstOpt = boundedOptions[0];
141
172
  if (firstOpt) {
142
- const safeText = firstOpt.text.slice(0, Math.max(0, maxChars - 30));
143
- return JSON.stringify([{ number: firstOpt.number, text: safeText }]);
173
+ return sliceToSerializedBudget(
174
+ firstOpt.text,
175
+ (safeText) => JSON.stringify([{ number: firstOpt.number, text: safeText, options_truncated: true }]),
176
+ maxChars
177
+ );
144
178
  }
145
- return JSON.stringify(boundedOptions.slice(0, 1));
179
+ return "[]".length <= maxChars ? "[]" : null;
146
180
  }
147
181
  const bounded = parsed.slice(0, 20).map((item) => {
148
- if (typeof item === "string") return { question: item.slice(0, 1024), options: [] };
182
+ if (typeof item === "string") return { question: item, options: [] };
149
183
  if (item && typeof item === "object") {
150
184
  const res = {};
151
185
  const obj = item;
152
- if (typeof obj.question === "string") res.question = obj.question.slice(0, 1024);
186
+ if (typeof obj.question === "string") res.question = obj.question;
153
187
  if (typeof obj.is_multi_select !== "undefined") res.is_multi_select = decodeBoolean(obj.is_multi_select);
154
188
  if (typeof obj.isMultiSelect !== "undefined") res.isMultiSelect = decodeBoolean(obj.isMultiSelect);
189
+ if (typeof obj.options_truncated !== "undefined") res.options_truncated = decodeBoolean(obj.options_truncated);
190
+ if (typeof obj.optionsTruncated !== "undefined") res.optionsTruncated = decodeBoolean(obj.optionsTruncated);
155
191
  if (Array.isArray(obj.options)) {
156
- res.options = obj.options.slice(0, 25).map((opt, idx) => {
157
- if (typeof opt === "string") return opt.slice(0, 256);
192
+ if (obj.options.length > 25) {
193
+ res.options_truncated = true;
194
+ }
195
+ res.options = obj.options.slice(0, 25).map((opt) => {
196
+ if (typeof opt === "string") return opt;
158
197
  if (typeof opt === "number") return String(opt);
159
198
  if (opt && typeof opt === "object") {
160
199
  const optObj = opt;
161
200
  return {
162
201
  ...typeof optObj.number !== "undefined" ? { number: String(optObj.number).slice(0, 16) } : {},
163
- text: typeof optObj.text === "string" ? optObj.text.slice(0, 256) : String(optObj.text ?? optObj.label ?? optObj.value ?? "").slice(0, 256)
202
+ text: typeof optObj.text === "string" ? optObj.text : String(optObj.text ?? optObj.label ?? optObj.value ?? "")
164
203
  };
165
204
  }
166
- return String(opt ?? "").slice(0, 256);
205
+ return String(opt ?? "");
167
206
  });
168
207
  }
169
208
  return res;
170
209
  }
171
- return { question: String(item).slice(0, 1024), options: [] };
210
+ return { question: String(item), options: [] };
172
211
  });
173
- let str = JSON.stringify(bounded);
212
+ const str = JSON.stringify(bounded);
174
213
  if (str.length <= maxChars) return str;
175
214
  for (let i = bounded.length - 1; i >= 1; i--) {
176
215
  const sub = JSON.stringify(bounded.slice(0, i));
@@ -178,11 +217,26 @@ function serializeStructuredArg(value, maxChars = MAX_STRUCTURED_ARG_CHARS, keyH
178
217
  }
179
218
  const firstQ = bounded[0];
180
219
  if (firstQ && typeof firstQ === "object") {
181
- const qText = typeof firstQ.question === "string" ? firstQ.question : "";
182
- const safeQText = qText.slice(0, Math.max(0, maxChars - 30));
183
- return JSON.stringify([{ question: safeQText, options: [] }]);
220
+ const qObj = firstQ;
221
+ const qText = typeof qObj.question === "string" ? qObj.question : "";
222
+ const opts = Array.isArray(qObj.options) ? qObj.options : [];
223
+ for (let k = opts.length - 1; k >= 1; k--) {
224
+ const subQ = JSON.stringify([{ ...qObj, options: opts.slice(0, k), options_truncated: true }]);
225
+ if (subQ.length <= maxChars) return subQ;
226
+ }
227
+ return sliceToSerializedBudget(
228
+ qText,
229
+ (safeQText) => JSON.stringify([
230
+ {
231
+ question: safeQText,
232
+ options: [],
233
+ ...opts.length > 0 ? { options_truncated: true } : {}
234
+ }
235
+ ]),
236
+ maxChars
237
+ );
184
238
  }
185
- return JSON.stringify(bounded.slice(0, 1));
239
+ return "[]".length <= maxChars ? "[]" : null;
186
240
  }
187
241
  if (parsed && typeof parsed === "object") {
188
242
  const raw = JSON.stringify(parsed);
@@ -216,6 +270,8 @@ function buildBody(raw) {
216
270
  if (key === "questions" || key === "options") {
217
271
  const serialized = serializeStructuredArg(value, MAX_STRUCTURED_ARG_CHARS, key);
218
272
  if (serialized !== null) kept[key] = serialized;
273
+ } else if (key === "question" || key === "prompt") {
274
+ kept[key] = value.slice(0, MAX_STRUCTURED_ARG_CHARS);
219
275
  } else {
220
276
  kept[key] = value.slice(0, MAX_TOOL_ARG_CHARS);
221
277
  }
package/dist/server.js CHANGED
@@ -3556,8 +3556,9 @@ function looksLikePromptSnapshot(snapshot) {
3556
3556
  const allLines = stripped.trimEnd().split("\n");
3557
3557
  const fenceMask = computeFenceMask(allLines);
3558
3558
  const windowStart = Math.max(0, allLines.length - 30);
3559
+ const questionWindowStart = Math.max(0, allLines.length - 80);
3559
3560
  let headerIdx = -1;
3560
- for (let i = allLines.length - 1; i >= windowStart; i--) {
3561
+ for (let i = allLines.length - 1; i >= questionWindowStart; i--) {
3561
3562
  if (!fenceMask[i] && isQuestionHeaderLine(allLines[i])) {
3562
3563
  headerIdx = i;
3563
3564
  break;
@@ -3565,9 +3566,16 @@ function looksLikePromptSnapshot(snapshot) {
3565
3566
  }
3566
3567
  if (headerIdx < 0) return false;
3567
3568
  const headerLine = allLines[headerIdx].trim();
3569
+ const isTallHeader = headerIdx < windowStart;
3568
3570
  const optionIndices = [];
3569
3571
  for (let i = headerIdx + 1; i < allLines.length; i++) {
3570
- if (!fenceMask[i] && /^[>\s❯]*[1-9][.)]\s+\S/.test(allLines[i].trim())) {
3572
+ if (fenceMask[i]) continue;
3573
+ const trimmed = allLines[i].trim();
3574
+ if (!trimmed) continue;
3575
+ if ((isTallHeader || optionIndices.length > 0) && isResumedTurnSignal(trimmed) || isComposerBoxAt(allLines, i) || /^\s*(?:[❯>]\s*)?\?\s+for\s+(?:shortcuts|help|commands|options|keybindings|menu)\b/i.test(trimmed)) {
3576
+ return false;
3577
+ }
3578
+ if (/^(?:[>\s❯]{0,4})[1-9][.)]\s+\S/.test(allLines[i])) {
3571
3579
  optionIndices.push(i);
3572
3580
  }
3573
3581
  }
@@ -3591,7 +3599,7 @@ function looksLikePromptSnapshot(snapshot) {
3591
3599
  }
3592
3600
  }
3593
3601
  }
3594
- if (!hasQuestionFooter) return false;
3602
+ if (!hasQuestionFooter || footerIdx < windowStart) return false;
3595
3603
  if (hasTrailingAgyComposer(allLines, footerIdx, fenceMask)) {
3596
3604
  return false;
3597
3605
  }
@@ -4310,6 +4318,38 @@ function detectActivePromptKind(snapshot) {
4310
4318
  }
4311
4319
  if (lastApprovalLineIdx >= 0 && lastQuestionLineIdx >= 0 && lastFileAccessLineIdx >= 0 && lastSystemErrorLineIdx >= 0) break;
4312
4320
  }
4321
+ if (lastQuestionLineIdx < 0 && lastApprovalLineIdx < 0 && lastFileAccessLineIdx < 0) {
4322
+ const tallQuestionStart = Math.max(0, lines.length - 80);
4323
+ for (let i = windowStart - 1; i >= tallQuestionStart; i--) {
4324
+ if (lineInFence[i]) continue;
4325
+ if (isQuestionHeaderLine(lines[i])) {
4326
+ let optCount = 0;
4327
+ let footerInBottomWindow = false;
4328
+ let footerIdx = i;
4329
+ let interrupted = false;
4330
+ for (let j = i + 1; j < lines.length; j++) {
4331
+ if (lineInFence[j]) continue;
4332
+ const trimmed = lines[j].trim();
4333
+ if (!trimmed) continue;
4334
+ if (approvalRe.test(trimmed) || fileEditQuestionRe.test(trimmed) || fileAccessRe.test(trimmed) || fileAccessQuestionRe.test(trimmed) || /Do you want to proceed\?/i.test(trimmed) || isResumedTurnSignal(trimmed) || isComposerBoxAt(lines, j) || /^\s*(?:[❯>]\s*)?\?\s+for\s+(?:shortcuts|help|commands|options|keybindings|menu)\b/i.test(trimmed)) {
4335
+ interrupted = true;
4336
+ break;
4337
+ }
4338
+ if (/^(?:[>\s❯]{0,4})[1-9][.)]\s+\S/.test(lines[j])) {
4339
+ optCount += 1;
4340
+ } else if (isQuestionFooterLine(trimmed) && j >= windowStart) {
4341
+ footerInBottomWindow = true;
4342
+ footerIdx = j;
4343
+ break;
4344
+ }
4345
+ }
4346
+ if (!interrupted && optCount >= 2 && footerInBottomWindow && !hasTrailingAgyComposer(lines, footerIdx, lineInFence)) {
4347
+ lastQuestionLineIdx = i;
4348
+ }
4349
+ break;
4350
+ }
4351
+ }
4352
+ }
4313
4353
  if (lastApprovalLineIdx < 0 && lastQuestionLineIdx < 0) {
4314
4354
  for (let i = lines.length - 1; i >= windowStart; i--) {
4315
4355
  if (/Do you want to proceed\?/i.test(lines[i])) {
@@ -4335,7 +4375,7 @@ function detectActivePromptKind(snapshot) {
4335
4375
  for (let j = lastSystemErrorLineIdx + 1; j < lines.length; j++) {
4336
4376
  if (!lineInFence[j]) {
4337
4377
  const trimmed = lines[j].trim();
4338
- if (hasQuestionOptions && isResumedTurnSignal(trimmed)) {
4378
+ if ((hasQuestionOptions || lastQuestionLineIdx < windowStart) && isResumedTurnSignal(trimmed)) {
4339
4379
  break;
4340
4380
  }
4341
4381
  if (/^[>\s❯]*[1-9][.)]\s+\S/.test(trimmed)) {
@@ -4389,7 +4429,7 @@ function detectActivePromptKind(snapshot) {
4389
4429
  if (isAgyStatusFooterLine(trimmed) || /^\s*(?:[❯>]\s*)?\?\s+for\s+(?:shortcuts|help)\b/i.test(trimmed) || isComposerBoxAt(lines, j)) {
4390
4430
  break;
4391
4431
  }
4392
- if (hasFoundOptions && isResumedTurnSignal(trimmed)) {
4432
+ if ((hasFoundOptions || lastQuestionLineIdx < windowStart) && isResumedTurnSignal(trimmed)) {
4393
4433
  break;
4394
4434
  }
4395
4435
  if (/^[>\s❯]*[1-9][.)]\s+\S/.test(trimmed)) {
@@ -4882,7 +4922,8 @@ function buildApprovalSemanticKey(candidate) {
4882
4922
  body: isDiffBody ? rawBody.trim() : normalize(rawBody),
4883
4923
  options: (candidate.options ?? []).map((option) => ({
4884
4924
  number: option.number,
4885
- text: normalize(option.text)
4925
+ text: normalize(option.text),
4926
+ ...option.rawLines && option.rawLines.length > 1 ? { rawLines: option.rawLines.map((line) => normalize(line)) } : {}
4886
4927
  })),
4887
4928
  submitMap: Object.fromEntries(
4888
4929
  Object.entries(candidate.submitMap ?? {}).sort(([left], [right]) => Number(left) - Number(right)).map(([number, keys]) => [number, [...keys]])
@@ -5003,7 +5044,7 @@ function parseQuestionSnapshot(stripped, originalSnapshot, header) {
5003
5044
  }
5004
5045
  }
5005
5046
  const belowHeader = headerIdx >= 0 ? lines.slice(headerIdx + 1).join("\n") : stripped;
5006
- const options = parseOptions(belowHeader);
5047
+ const options = parseOptions(belowHeader, 80);
5007
5048
  if (options.length < 2) return null;
5008
5049
  const normalizedHeader = header.fullLine.replace(/^[❯>\s*•-]+\s*/, "").trim();
5009
5050
  const isMultiSelect = /\bselect\s+(?:all|multiple)\b/i.test(normalizedHeader) || /\bselect\s+(?:all|multiple)\b/i.test(belowHeader) || /(?:<space>|\bspace)\s+(?:to\s+)?(?:select|toggle)\b/i.test(belowHeader) || /\bctrl\+a\s+select\s+all\b/i.test(belowHeader);
@@ -5024,7 +5065,7 @@ function parseQuestionSnapshot(stripped, originalSnapshot, header) {
5024
5065
  }
5025
5066
  function extractBodyBetweenHeaderAndFirstOption(belowHeader) {
5026
5067
  const lines = belowHeader.split("\n");
5027
- const optionRegex = /^[>\s❯]*([1-9])[.)]\s+(.+?)\s*$/;
5068
+ const optionRegex = /^(?:[>\s❯]{0,4})([1-9])[.)]\s+(.+?)\s*$/;
5028
5069
  const lineInFence = computeFenceMask(lines);
5029
5070
  let firstOptionIdx = -1;
5030
5071
  for (let i = 0; i < lines.length; i++) {
@@ -5037,7 +5078,34 @@ function extractBodyBetweenHeaderAndFirstOption(belowHeader) {
5037
5078
  if (firstOptionIdx <= 0) return "";
5038
5079
  return lines.slice(0, firstOptionIdx).join("\n").trim();
5039
5080
  }
5040
- function parseOptions(snapshot) {
5081
+ function isOptionContinuationBoundary(trimmed, rawLine, nextTrimmedLine) {
5082
+ if (!trimmed) return true;
5083
+ if (/^[•‣◦▪▫]\s/.test(trimmed)) return true;
5084
+ if (/^\s{2,4}[-*]\s/.test(rawLine)) return true;
5085
+ if (isQuestionFooterLine(trimmed)) return true;
5086
+ if (isQuestionHeaderLine(trimmed)) return true;
5087
+ if (isResumedTurnSignal(trimmed)) return true;
5088
+ if (/^[─━═-]{4,}$/.test(trimmed)) return true;
5089
+ if (/^\s*esc to cancel\b/i.test(trimmed)) return true;
5090
+ if (/^(?:↑\/↓|↑|↓|\^|v)\s*(?:\/|\s+)?Navigate\b/i.test(trimmed)) return true;
5091
+ if (/^ctrl\+[a-z0-9]+\s+[a-zA-Z]+$/i.test(trimmed)) return true;
5092
+ if (/^\(?use\s+(?:arrow|\S+)\s+keys[^)]*\)?$/i.test(trimmed)) return true;
5093
+ if (!/^ {5,}/.test(rawLine) && /^Your answer:/i.test(trimmed) && (/^Your answer:\s*$/i.test(trimmed) || Boolean(
5094
+ nextTrimmedLine && (isQuestionFooterLine(nextTrimmedLine) || /^\(?Enter to submit(?:\s*,\s*esc to cancel)?\)?\s*$/i.test(nextTrimmedLine) || /^\s*esc to cancel\b/i.test(nextTrimmedLine))
5095
+ ))) {
5096
+ return true;
5097
+ }
5098
+ if (!/^ {5,}/.test(rawLine) && /^\(?Enter to submit(?:\s*,\s*esc to cancel)?\)?\s*$/i.test(trimmed)) {
5099
+ return true;
5100
+ }
5101
+ if (/^\s*(?:[❯>]\s*)?\?\s+for\s+(?:shortcuts|help|commands|options|keybindings|menu)\b/i.test(trimmed)) return true;
5102
+ if (/Requesting permission for:/i.test(trimmed)) return true;
5103
+ if (/Do you want to proceed\?/i.test(trimmed)) return true;
5104
+ if (/^\s*Allow access to this file\?\s*$/i.test(trimmed)) return true;
5105
+ if (/^\s*(?:Accept this file edit\?|Allow creation of this file\?)\s*$/i.test(trimmed)) return true;
5106
+ return false;
5107
+ }
5108
+ function parseOptions(snapshot, maxRecentLines = 30) {
5041
5109
  const lines = snapshot.split("\n");
5042
5110
  const lineInFence = computeFenceMask(lines);
5043
5111
  const filteredLines = [];
@@ -5049,16 +5117,72 @@ function parseOptions(snapshot) {
5049
5117
  while (filteredLines.length > 0 && filteredLines[filteredLines.length - 1].trim() === "") {
5050
5118
  filteredLines.pop();
5051
5119
  }
5052
- const recent = filteredLines.slice(-30);
5053
- const optionRegex = /^[>\s❯]*([1-9])[.)]\s+(.+?)\s*$/;
5120
+ const recent = filteredLines.slice(-Math.max(30, maxRecentLines));
5121
+ const optionRegex = /^(?:[>\s❯]{0,4})([1-9])[.)]\s+(.+?)\s*$/;
5054
5122
  const found = /* @__PURE__ */ new Map();
5055
- for (const line of recent) {
5123
+ let currentOptionNum = null;
5124
+ let currentOptionTextCol = 5;
5125
+ let prevOptionRawLen = 0;
5126
+ for (let idx = 0; idx < recent.length; idx++) {
5127
+ const line = recent[idx];
5056
5128
  const m = line.match(optionRegex);
5057
- if (!m) continue;
5058
- const num = m[1];
5059
- found.set(num, m[2].trim());
5129
+ if (m) {
5130
+ const num = m[1];
5131
+ const text = m[2].trim();
5132
+ const prefixMatch = line.match(/^(?:[>\s❯]{0,4})[1-9][.)]\s+/);
5133
+ currentOptionTextCol = prefixMatch ? prefixMatch[0].length : 5;
5134
+ prevOptionRawLen = line.trimEnd().length;
5135
+ found.set(num, {
5136
+ text,
5137
+ rawLines: [text],
5138
+ rawLineLengths: [prevOptionRawLen]
5139
+ });
5140
+ currentOptionNum = /^\s*write-?in(?:\s*(?:\.{3}|…))?\s*$/i.test(text) ? null : num;
5141
+ continue;
5142
+ }
5143
+ const trimmed = line.trim();
5144
+ if (currentOptionNum !== null) {
5145
+ let nextTrimmedLine;
5146
+ for (let k = idx + 1; k < recent.length; k++) {
5147
+ const nt = recent[k].trim();
5148
+ if (nt) {
5149
+ nextTrimmedLine = nt;
5150
+ break;
5151
+ }
5152
+ }
5153
+ const leadingSpaces = (line.match(/^\s*/)?.[0] ?? "").length;
5154
+ const isIndentedContinuation = leadingSpaces >= Math.max(4, currentOptionTextCol);
5155
+ const isColumnZeroWrapContinuation = leadingSpaces === 0;
5156
+ if (isOptionContinuationBoundary(trimmed, line, nextTrimmedLine) || !isIndentedContinuation && !isColumnZeroWrapContinuation) {
5157
+ currentOptionNum = null;
5158
+ } else {
5159
+ const entry = found.get(currentOptionNum);
5160
+ if (entry) {
5161
+ const existing = entry.text;
5162
+ entry.text = existing ? `${existing} ${trimmed}` : trimmed;
5163
+ entry.rawLines.push(trimmed);
5164
+ prevOptionRawLen = line.trimEnd().length;
5165
+ entry.rawLineLengths.push(prevOptionRawLen);
5166
+ }
5167
+ }
5168
+ }
5060
5169
  }
5061
- return Array.from(found.entries()).sort(([a], [b]) => Number(a) - Number(b)).map(([number, text]) => ({ number, text }));
5170
+ return Array.from(found.entries()).sort(([a], [b]) => Number(a) - Number(b)).map(([number, entry]) => {
5171
+ const opt = { number, text: entry.text };
5172
+ if (entry.rawLines.length > 1) {
5173
+ Object.defineProperty(opt, "rawLines", {
5174
+ value: entry.rawLines,
5175
+ enumerable: false,
5176
+ configurable: true
5177
+ });
5178
+ Object.defineProperty(opt, "rawLineLengths", {
5179
+ value: entry.rawLineLengths,
5180
+ enumerable: false,
5181
+ configurable: true
5182
+ });
5183
+ }
5184
+ return opt;
5185
+ });
5062
5186
  }
5063
5187
  function buildApprovalSubmitMap(options) {
5064
5188
  const map = {};
@@ -5079,10 +5203,11 @@ function isWriteInInputActive(paneText, expectedHeader, expectedStep, expectedBo
5079
5203
  if (detectActivePromptKind(paneText) !== "question") return false;
5080
5204
  const stripped = stripAnsiAndExpandRep(paneText);
5081
5205
  const lines = stripped.trimEnd().split("\n");
5082
- const windowStart = Math.max(0, lines.length - 30);
5206
+ const fenceMask = computeFenceMask(lines);
5207
+ const windowStart = Math.max(0, lines.length - 80);
5083
5208
  let headerIdx = -1;
5084
5209
  for (let i = lines.length - 1; i >= windowStart; i--) {
5085
- if (isQuestionHeaderLine(lines[i])) {
5210
+ if (!fenceMask[i] && isQuestionHeaderLine(lines[i])) {
5086
5211
  headerIdx = i;
5087
5212
  break;
5088
5213
  }
@@ -5586,28 +5711,61 @@ function extractAllQuestionsFromArgs(toolName, args) {
5586
5711
  const qObj = qItem;
5587
5712
  const qText = typeof qObj.question === "string" ? stripWrappingQuotes2(qObj.question.trim()) : "Question";
5588
5713
  const opts = [];
5714
+ let optItemTruncated = false;
5589
5715
  if (Array.isArray(qObj.options)) {
5590
5716
  for (let i = 0; i < qObj.options.length; i++) {
5591
5717
  const opt = qObj.options[i];
5592
5718
  if (typeof opt === "string" && opt.trim().length > 0) {
5593
5719
  opts.push({ number: String(i + 1), text: stripWrappingQuotes2(opt.trim()) });
5594
5720
  } else if (opt && typeof opt === "object" && typeof opt.text === "string") {
5721
+ const optRecord = opt;
5722
+ if (decodeBoolean(optRecord.options_truncated ?? optRecord.optionsTruncated)) {
5723
+ optItemTruncated = true;
5724
+ }
5595
5725
  opts.push({
5596
- number: String(opt.number ?? i + 1),
5597
- text: stripWrappingQuotes2(String(opt.text).trim())
5726
+ number: String(optRecord.number ?? i + 1),
5727
+ text: stripWrappingQuotes2(String(optRecord.text).trim())
5598
5728
  });
5599
5729
  }
5600
5730
  }
5601
5731
  }
5602
5732
  const isMulti = decodeBoolean(qObj.is_multi_select ?? qObj.isMultiSelect);
5603
- items.push({ question: qText, options: opts, isMultiSelect: isMulti });
5733
+ const isTruncated = optItemTruncated || decodeBoolean(qObj.options_truncated ?? qObj.optionsTruncated);
5734
+ items.push({
5735
+ question: qText,
5736
+ options: opts,
5737
+ isMultiSelect: isMulti,
5738
+ ...isTruncated ? { optionsTruncated: true } : {}
5739
+ });
5604
5740
  }
5605
5741
  }
5606
5742
  } else if (typeof a.question === "string" || typeof a.prompt === "string") {
5607
5743
  const qText = stripWrappingQuotes2(String(a.question ?? a.prompt).trim());
5608
5744
  const opts = extractQuestionOptionsFromArgs(toolName, args) ?? [];
5609
5745
  const isMulti = decodeBoolean(a.is_multi_select ?? a.isMultiSelect);
5610
- items.push({ question: qText, options: opts, isMultiSelect: isMulti });
5746
+ let topOptTruncated = decodeBoolean(a.options_truncated ?? a.optionsTruncated);
5747
+ if (!topOptTruncated && (Array.isArray(a.options) || typeof a.options === "string")) {
5748
+ let topOptions = a.options;
5749
+ if (typeof topOptions === "string") {
5750
+ try {
5751
+ topOptions = JSON.parse(topOptions.trim());
5752
+ } catch {
5753
+ }
5754
+ }
5755
+ if (Array.isArray(topOptions)) {
5756
+ topOptTruncated = topOptions.some(
5757
+ (o) => o !== null && typeof o === "object" && decodeBoolean(
5758
+ o.options_truncated ?? o.optionsTruncated
5759
+ )
5760
+ );
5761
+ }
5762
+ }
5763
+ items.push({
5764
+ question: qText,
5765
+ options: opts,
5766
+ isMultiSelect: isMulti,
5767
+ ...topOptTruncated ? { optionsTruncated: true } : {}
5768
+ });
5611
5769
  }
5612
5770
  return items.length > 0 ? items : void 0;
5613
5771
  }
@@ -5709,6 +5867,104 @@ function matchPaneQuestionLines(hookText, candidate) {
5709
5867
  function paneCandidateMatchesQuestion(hookText, candidate) {
5710
5868
  return matchPaneQuestionLines(hookText, candidate) !== null;
5711
5869
  }
5870
+ function buildHookQuestionOptionsAndSubmitMap(rawOptions, isMultiSelect, optionsTruncated = false) {
5871
+ if (!rawOptions || rawOptions.length === 0) {
5872
+ return { paneOptions: void 0, submitMap: {} };
5873
+ }
5874
+ const normalized = rawOptions.map((opt, idx) => {
5875
+ const entry = {
5876
+ number: String(opt.number ?? idx + 1),
5877
+ text: opt.text
5878
+ };
5879
+ if (opt.isGeneratedWriteIn) {
5880
+ Object.defineProperty(entry, "isGeneratedWriteIn", {
5881
+ value: true,
5882
+ enumerable: false,
5883
+ configurable: true
5884
+ });
5885
+ }
5886
+ return entry;
5887
+ });
5888
+ if (isMultiSelect || optionsTruncated || normalized.length >= 9) {
5889
+ return { paneOptions: normalized, submitMap: {} };
5890
+ }
5891
+ const lastOpt = normalized[normalized.length - 1];
5892
+ const hasTrailingGeneratedWriteIn = Boolean(lastOpt?.isGeneratedWriteIn);
5893
+ let paneOptions;
5894
+ if (hasTrailingGeneratedWriteIn) {
5895
+ paneOptions = normalized;
5896
+ } else {
5897
+ const writeInOpt = {
5898
+ number: String(normalized.length + 1),
5899
+ text: "Write-in..."
5900
+ };
5901
+ Object.defineProperty(writeInOpt, "isGeneratedWriteIn", {
5902
+ value: true,
5903
+ enumerable: false,
5904
+ configurable: true
5905
+ });
5906
+ paneOptions = [...normalized, writeInOpt];
5907
+ }
5908
+ const submitMap = {};
5909
+ for (const opt of paneOptions) {
5910
+ submitMap[opt.number] = [opt.number];
5911
+ }
5912
+ return { paneOptions, submitMap };
5913
+ }
5914
+ function hookOptionsMatchPaneOptions(hookOpts, paneOpts) {
5915
+ if (!hookOpts || hookOpts.length === 0 || !paneOpts || paneOpts.length === 0) {
5916
+ return false;
5917
+ }
5918
+ const normOpt = (s) => s.normalize("NFKC").toLowerCase().replace(/\s+/g, " ").trim();
5919
+ const hookNormalized = hookOpts.map((o, idx) => ({
5920
+ number: String(o.number ?? idx + 1).trim(),
5921
+ text: normOpt(o.text),
5922
+ isGeneratedWriteIn: Boolean(o.isGeneratedWriteIn)
5923
+ }));
5924
+ const hookEffective = hookNormalized.length > 0 && hookNormalized[hookNormalized.length - 1].isGeneratedWriteIn ? hookNormalized.slice(0, -1) : hookNormalized;
5925
+ const paneNormalized = paneOpts.map((o, idx) => ({
5926
+ number: String(o.number ?? idx + 1).trim(),
5927
+ text: normOpt(o.text),
5928
+ isGeneratedWriteIn: Boolean(o.isGeneratedWriteIn)
5929
+ }));
5930
+ const lastPane = paneNormalized[paneNormalized.length - 1];
5931
+ const paneHasTrailingWriteIn = Boolean(lastPane) && (lastPane.isGeneratedWriteIn || paneNormalized.length === hookEffective.length + 1 && /^\s*write-?in(?:\.{3}|…)?\s*$/i.test(lastPane.text));
5932
+ const paneEffective = paneHasTrailingWriteIn ? paneNormalized.slice(0, -1) : paneNormalized;
5933
+ if (hookEffective.length === 0 || hookEffective.length !== paneEffective.length) {
5934
+ return false;
5935
+ }
5936
+ return hookEffective.every(
5937
+ (h, i) => paneEffective[i].text === h.text && paneEffective[i].number === h.number
5938
+ );
5939
+ }
5940
+ function tagPaneGeneratedWriteInOption(paneOpts, hookStepOpts) {
5941
+ if (!paneOpts || paneOpts.length === 0) return paneOpts;
5942
+ const hookBaseOpts = hookStepOpts ? hookStepOpts.filter((o) => !o.isGeneratedWriteIn) : void 0;
5943
+ for (let i = 0; i < paneOpts.length - 1; i++) {
5944
+ const opt = paneOpts[i];
5945
+ if (opt?.isGeneratedWriteIn) {
5946
+ delete opt.isGeneratedWriteIn;
5947
+ }
5948
+ }
5949
+ const lastOpt = paneOpts[paneOpts.length - 1];
5950
+ if (!lastOpt || !/^\s*write-?in(?:\.{3}|…)?\s*$/i.test(lastOpt.text)) {
5951
+ if (lastOpt?.isGeneratedWriteIn) {
5952
+ delete lastOpt.isGeneratedWriteIn;
5953
+ }
5954
+ return paneOpts;
5955
+ }
5956
+ const isGenerated = !hookBaseOpts || hookBaseOpts.length === 0 || paneOpts.length === hookBaseOpts.length + 1;
5957
+ if (isGenerated) {
5958
+ Object.defineProperty(lastOpt, "isGeneratedWriteIn", {
5959
+ value: true,
5960
+ enumerable: false,
5961
+ configurable: true
5962
+ });
5963
+ } else if (lastOpt.isGeneratedWriteIn) {
5964
+ delete lastOpt.isGeneratedWriteIn;
5965
+ }
5966
+ return paneOpts;
5967
+ }
5712
5968
  function correlatesQuestionInvocation(call, delivery) {
5713
5969
  if (!call) return false;
5714
5970
  const tn = (delivery.toolName ?? "").toLowerCase().trim();
@@ -5898,6 +6154,9 @@ var ApprovalDetector = class extends import_events3.EventEmitter {
5898
6154
  return null;
5899
6155
  }
5900
6156
  }
6157
+ let replacedHookCall;
6158
+ let replacedWasHookPrompt = false;
6159
+ let replacedHookWasDisplayOnly = false;
5901
6160
  if (candidate.kind === "question") {
5902
6161
  const paneQuestionText = candidate.headerText || candidate.fullHeaderLine;
5903
6162
  const candidateSemanticKey = buildApprovalSemanticKey(candidate);
@@ -5914,7 +6173,7 @@ var ApprovalDetector = class extends import_events3.EventEmitter {
5914
6173
  const qOptTexts = (targetQ.options || []).map((o) => normOpt(typeof o === "string" ? o : o.text));
5915
6174
  const cOptTexts = (candidate.options || []).map((o) => normOpt(typeof o === "string" ? o : o.text));
5916
6175
  if (cOptTexts.length > 0 && qOptTexts.length > 0) {
5917
- const hasTrailingWriteIn = cOptTexts.length === qOptTexts.length + 1 && /^\s*write-?in\b/i.test(cOptTexts[cOptTexts.length - 1]);
6176
+ const hasTrailingWriteIn = cOptTexts.length === qOptTexts.length + 1 && /^\s*write-?in(?:\.{3}|…)?\s*$/i.test(cOptTexts[cOptTexts.length - 1]);
5918
6177
  const cEffectiveTexts = hasTrailingWriteIn ? cOptTexts.slice(0, -1) : cOptTexts;
5919
6178
  const matchedQCount = qOptTexts.filter((q) => cEffectiveTexts.includes(q)).length;
5920
6179
  const matchedCCount = cEffectiveTexts.filter((c) => qOptTexts.includes(c)).length;
@@ -5948,6 +6207,20 @@ var ApprovalDetector = class extends import_events3.EventEmitter {
5948
6207
  }
5949
6208
  }
5950
6209
  if (bestStepIdx === -1) {
6210
+ const currentStepEval = scoreStep(currentActiveIdx);
6211
+ if (currentStepEval.textMatch) {
6212
+ const existing = [...this.pendingPrompts.values()].find(
6213
+ (p) => p.conversationId === conversationId && p.kind === "question"
6214
+ );
6215
+ if (existing && Object.keys(existing.submitMap || {}).length > 0) {
6216
+ existing.submitMap = {};
6217
+ if (existing.paneOptions && existing.paneOptions.length > 0 && existing.paneOptions[existing.paneOptions.length - 1]?.isGeneratedWriteIn) {
6218
+ existing.paneOptions = existing.paneOptions.slice(0, -1);
6219
+ }
6220
+ existing.paneSemanticKey = candidateSemanticKey;
6221
+ this.emit("prompt-upgraded", existing);
6222
+ }
6223
+ }
5951
6224
  logger.debug("Pane candidate did not match any step of active live wizard", {
5952
6225
  paneQuestionText,
5953
6226
  currentActiveIdx,
@@ -5966,47 +6239,42 @@ var ApprovalDetector = class extends import_events3.EventEmitter {
5966
6239
  (p) => p.conversationId === conversationId && p.kind === "question"
5967
6240
  );
5968
6241
  if (existing) {
5969
- const targetQ = existing.pendingCall?.structuredQuestions?.[existing.wizardStepIndex ?? 0];
5970
- const isAuthoritativeMultiSelect = Boolean(targetQ?.isMultiSelect);
5971
- const isInitialUpgrade = existing.isHookPrompt && !isAuthoritativeMultiSelect && Object.keys(existing.submitMap || {}).length === 0 && Object.keys(candidate.submitMap || {}).length > 0;
5972
- if (isInitialUpgrade) {
5973
- existing.submitMap = candidate.submitMap;
5974
- if (candidate.options && candidate.options.length > 0) {
5975
- existing.paneOptions = candidate.options;
5976
- }
5977
- existing.paneSemanticKey = candidateSemanticKey;
5978
- existing.paneHash = candidate.paneHash;
5979
- existing.matchedPaneHeader = stepLineMatch?.stitchedHeaderText ?? candidate.headerText ?? existing.matchedPaneHeader;
5980
- existing.paneDisplayHeader = stepLineMatch?.stitchedFullHeaderLine ?? candidate.fullHeaderLine ?? existing.paneDisplayHeader;
5981
- existing.body = stepLineMatch ? stepLineMatch.remainingBody || void 0 : candidate.body;
5982
- if (candidate.paneHash) this.paneOnlyEmittedHashes.add(candidate.paneHash);
5983
- logger.info("Upgraded hook-driven question prompt to actionable from pane observer", {
5984
- promptId: existing.promptId,
5985
- optionsCount: existing.paneOptions?.length ?? 0
5986
- });
5987
- this.emit("prompt-upgraded", existing);
5988
- return existing;
5989
- }
5990
- const areControlsMutated = (existingOpts, candidateOpts) => {
5991
- if (!existingOpts || !candidateOpts) return false;
5992
- if (existingOpts.length !== candidateOpts.length) return true;
5993
- return !existingOpts.every((o, idx) => o.number === candidateOpts[idx].number && (o.text || "").trim() === (candidateOpts[idx].text || "").trim());
5994
- };
5995
- const isControlDelta = Boolean(
5996
- existing.paneOptions && existing.paneOptions.length > 0 && candidate.options && candidate.options.length > 0 && areControlsMutated(existing.paneOptions, candidate.options)
5997
- );
5998
- if (isControlDelta) {
5999
- logger.info("Control delta observed on active question step; resolving old prompt and re-emitting", {
6000
- oldPromptId: existing.promptId,
6001
- oldSemanticKey: existing.paneSemanticKey,
6002
- newSemanticKey: candidateSemanticKey
6003
- });
6242
+ const nextBody = stepLineMatch ? stepLineMatch.remainingBody || void 0 : candidate.body || void 0;
6243
+ const bodyChangedAfterObservation = Boolean(existing.paneBodyObserved) && existing.body !== nextBody;
6244
+ if (bodyChangedAfterObservation) {
6245
+ replacedHookCall = existing.pendingCall;
6246
+ replacedWasHookPrompt = Boolean(existing.isHookPrompt);
6247
+ replacedHookWasDisplayOnly = Object.keys(existing.submitMap || {}).length === 0;
6004
6248
  this.resolvePrompt(existing.promptId, { advanceWizard: false });
6005
6249
  this.emit("prompt-resolved", existing.promptId);
6006
6250
  } else {
6007
- if (!existing.paneSemanticKey) {
6008
- existing.paneSemanticKey = candidateSemanticKey;
6251
+ const targetQ = existing.pendingCall?.structuredQuestions?.[existing.wizardStepIndex ?? 0];
6252
+ const isAuthoritativeMultiSelect = Boolean(
6253
+ existing.pendingCall?.isMultiSelect || targetQ?.isMultiSelect
6254
+ );
6255
+ const isOptionsTruncated = Boolean(
6256
+ existing.pendingCall?.optionsTruncated || targetQ?.optionsTruncated
6257
+ );
6258
+ const hasExplicitEmptyOptions = Boolean(
6259
+ targetQ && Array.isArray(targetQ.options) && targetQ.options.length === 0
6260
+ );
6261
+ let adoptedFallbackOptions = false;
6262
+ if (!isAuthoritativeMultiSelect && !isOptionsTruncated && !hasExplicitEmptyOptions && Object.keys(existing.submitMap || {}).length === 0 && (!existing.paneOptions || existing.paneOptions.length === 0) && Object.keys(candidate.submitMap || {}).length > 0) {
6263
+ existing.submitMap = candidate.submitMap;
6264
+ if (candidate.options && candidate.options.length > 0) {
6265
+ existing.paneOptions = tagPaneGeneratedWriteInOption(candidate.options, targetQ?.options);
6266
+ }
6267
+ adoptedFallbackOptions = true;
6268
+ }
6269
+ const candidateIsActionable = Object.keys(candidate.submitMap || {}).length > 0;
6270
+ const optionsMatch = hookOptionsMatchPaneOptions(existing.paneOptions, candidate.options);
6271
+ const exactControlsMatch = adoptedFallbackOptions || candidateIsActionable && optionsMatch && (existing.paneOptions?.length ?? 0) === (candidate.options?.length ?? 0) && (existing.paneOptions ?? []).every(
6272
+ (o, i) => o.number === candidate.options[i]?.number && JSON.stringify(existing.submitMap?.[o.number]) === JSON.stringify(candidate.submitMap?.[o.number])
6273
+ );
6274
+ if (existing.paneHash && existing.paneHash !== candidate.paneHash) {
6275
+ this.paneOnlyEmittedHashes.delete(existing.paneHash);
6009
6276
  }
6277
+ existing.paneHash = candidate.paneHash;
6010
6278
  if (stepLineMatch) {
6011
6279
  existing.matchedPaneHeader = stepLineMatch.stitchedHeaderText;
6012
6280
  existing.paneDisplayHeader = stepLineMatch.stitchedFullHeaderLine;
@@ -6014,8 +6282,19 @@ var ApprovalDetector = class extends import_events3.EventEmitter {
6014
6282
  if (existing.pendingCall && targetStepQ?.question) {
6015
6283
  existing.pendingCall.questionText = stepLineMatch.stitchedHeaderText;
6016
6284
  }
6017
- } else if (candidate.body) {
6018
- existing.body = candidate.body;
6285
+ } else {
6286
+ existing.body = nextBody;
6287
+ }
6288
+ existing.paneBodyObserved = true;
6289
+ if (exactControlsMatch) {
6290
+ existing.paneSemanticKey = candidateSemanticKey;
6291
+ } else if ((!candidateIsActionable || !optionsMatch) && Object.keys(existing.submitMap || {}).length > 0) {
6292
+ existing.submitMap = {};
6293
+ if (existing.paneOptions && existing.paneOptions.length > 0 && existing.paneOptions[existing.paneOptions.length - 1]?.isGeneratedWriteIn) {
6294
+ existing.paneOptions = existing.paneOptions.slice(0, -1);
6295
+ }
6296
+ existing.paneSemanticKey = candidateSemanticKey;
6297
+ this.emit("prompt-upgraded", existing);
6019
6298
  }
6020
6299
  if (candidate.paneHash) this.paneOnlyEmittedHashes.add(candidate.paneHash);
6021
6300
  return null;
@@ -6042,44 +6321,86 @@ var ApprovalDetector = class extends import_events3.EventEmitter {
6042
6321
  const lineMatch = matchPaneQuestionLines(hookQuestionText, candidate);
6043
6322
  const isSameQuestion = lineMatch !== null;
6044
6323
  if (isSameQuestion) {
6045
- const targetQ = p.pendingCall?.structuredQuestions?.[p.wizardStepIndex ?? 0];
6046
- const isAuthoritativeMultiSelect = Boolean(targetQ?.isMultiSelect);
6047
- const isInitialUpgrade = p.isHookPrompt && !isAuthoritativeMultiSelect && Object.keys(p.submitMap || {}).length === 0 && Object.keys(candidate.submitMap || {}).length > 0;
6048
- const isControlDelta = !isInitialUpgrade && Boolean(
6049
- p.paneSemanticKey && candidateSemanticKey !== p.paneSemanticKey
6050
- );
6051
- if (!isControlDelta) {
6052
- if (isInitialUpgrade) {
6053
- p.submitMap = candidate.submitMap;
6054
- if (candidate.options && candidate.options.length > 0) {
6055
- p.paneOptions = candidate.options;
6324
+ if (p.isHookPrompt) {
6325
+ const nextBody = lineMatch ? lineMatch.remainingBody || void 0 : candidate.body || void 0;
6326
+ const bodyChangedAfterObservation = Boolean(p.paneBodyObserved) && p.body !== nextBody;
6327
+ if (!bodyChangedAfterObservation) {
6328
+ const targetQ = p.pendingCall?.structuredQuestions?.[p.wizardStepIndex ?? 0];
6329
+ const isAuthoritativeMultiSelect = Boolean(
6330
+ p.pendingCall?.isMultiSelect || targetQ?.isMultiSelect
6331
+ );
6332
+ const isOptionsTruncated = Boolean(
6333
+ p.pendingCall?.optionsTruncated || targetQ?.optionsTruncated
6334
+ );
6335
+ let adoptedFallbackOptions = false;
6336
+ if (!isAuthoritativeMultiSelect && !isOptionsTruncated && Object.keys(p.submitMap || {}).length === 0 && (!p.paneOptions || p.paneOptions.length === 0) && Object.keys(candidate.submitMap || {}).length > 0) {
6337
+ p.submitMap = candidate.submitMap;
6338
+ if (candidate.options && candidate.options.length > 0) {
6339
+ p.paneOptions = tagPaneGeneratedWriteInOption(
6340
+ candidate.options,
6341
+ targetQ?.options ?? p.pendingCall?.questionOptions
6342
+ );
6343
+ }
6344
+ adoptedFallbackOptions = true;
6345
+ }
6346
+ const candidateIsActionable = Object.keys(candidate.submitMap || {}).length > 0;
6347
+ const optionsMatch = hookOptionsMatchPaneOptions(p.paneOptions, candidate.options);
6348
+ const exactControlsMatch = adoptedFallbackOptions || candidateIsActionable && optionsMatch && (p.paneOptions?.length ?? 0) === (candidate.options?.length ?? 0) && (p.paneOptions ?? []).every(
6349
+ (o, i) => o.number === candidate.options[i]?.number && JSON.stringify(p.submitMap?.[o.number]) === JSON.stringify(candidate.submitMap?.[o.number])
6350
+ );
6351
+ if (p.paneHash && p.paneHash !== candidate.paneHash) {
6352
+ this.paneOnlyEmittedHashes.delete(p.paneHash);
6056
6353
  }
6057
- p.paneSemanticKey = candidateSemanticKey;
6058
6354
  p.paneHash = candidate.paneHash;
6059
- p.matchedPaneHeader = lineMatch?.stitchedHeaderText ?? candidate.headerText ?? p.matchedPaneHeader;
6060
- p.paneDisplayHeader = lineMatch?.stitchedFullHeaderLine ?? candidate.fullHeaderLine ?? p.paneDisplayHeader;
6061
- p.body = lineMatch ? lineMatch.remainingBody || void 0 : candidate.body;
6355
+ if (lineMatch) {
6356
+ p.matchedPaneHeader = lineMatch.stitchedHeaderText;
6357
+ p.paneDisplayHeader = lineMatch.stitchedFullHeaderLine;
6358
+ p.body = lineMatch.remainingBody || void 0;
6359
+ if (p.pendingCall) {
6360
+ p.pendingCall.questionText = lineMatch.stitchedHeaderText;
6361
+ }
6362
+ } else {
6363
+ p.body = nextBody;
6364
+ }
6365
+ p.paneBodyObserved = true;
6366
+ if (exactControlsMatch) {
6367
+ p.paneSemanticKey = candidateSemanticKey;
6368
+ } else if ((!candidateIsActionable || !optionsMatch) && Object.keys(p.submitMap || {}).length > 0) {
6369
+ p.submitMap = {};
6370
+ if (p.paneOptions && p.paneOptions.length > 0 && p.paneOptions[p.paneOptions.length - 1]?.isGeneratedWriteIn) {
6371
+ p.paneOptions = p.paneOptions.slice(0, -1);
6372
+ }
6373
+ p.paneSemanticKey = candidateSemanticKey;
6374
+ this.emit("prompt-upgraded", p);
6375
+ }
6062
6376
  if (candidate.paneHash) this.paneOnlyEmittedHashes.add(candidate.paneHash);
6063
- logger.info("Upgraded hook-driven question prompt to actionable from pane observer", {
6064
- promptId: p.promptId,
6065
- optionsCount: p.paneOptions?.length ?? 0
6066
- });
6067
- this.emit("prompt-upgraded", p);
6068
- return p;
6377
+ return null;
6069
6378
  }
6070
- if (!p.paneSemanticKey) {
6071
- p.paneSemanticKey = candidateSemanticKey;
6072
- }
6073
- if (lineMatch) {
6074
- p.matchedPaneHeader = lineMatch.stitchedHeaderText;
6075
- p.paneDisplayHeader = lineMatch.stitchedFullHeaderLine;
6076
- p.body = lineMatch.remainingBody || void 0;
6077
- if (p.pendingCall) {
6078
- p.pendingCall.questionText = lineMatch.stitchedHeaderText;
6379
+ }
6380
+ if (!p.isHookPrompt) {
6381
+ const isControlDelta = Boolean(
6382
+ p.paneSemanticKey && candidateSemanticKey !== p.paneSemanticKey
6383
+ );
6384
+ if (!isControlDelta) {
6385
+ if (!p.paneSemanticKey) {
6386
+ p.paneSemanticKey = candidateSemanticKey;
6079
6387
  }
6388
+ if (lineMatch) {
6389
+ p.matchedPaneHeader = lineMatch.stitchedHeaderText;
6390
+ p.paneDisplayHeader = lineMatch.stitchedFullHeaderLine;
6391
+ p.body = lineMatch.remainingBody || void 0;
6392
+ if (p.pendingCall) {
6393
+ p.pendingCall.questionText = lineMatch.stitchedHeaderText;
6394
+ }
6395
+ }
6396
+ p.paneBodyObserved = true;
6397
+ if (candidate.paneHash) this.paneOnlyEmittedHashes.add(candidate.paneHash);
6398
+ return null;
6080
6399
  }
6081
- if (candidate.paneHash) this.paneOnlyEmittedHashes.add(candidate.paneHash);
6082
- return null;
6400
+ } else {
6401
+ replacedHookCall = p.pendingCall;
6402
+ replacedWasHookPrompt = true;
6403
+ replacedHookWasDisplayOnly = Object.keys(p.submitMap || {}).length === 0;
6083
6404
  }
6084
6405
  }
6085
6406
  this.resolvePrompt(p.promptId);
@@ -6090,14 +6411,35 @@ var ApprovalDetector = class extends import_events3.EventEmitter {
6090
6411
  }
6091
6412
  const promptId = forcedPromptId ?? (0, import_uuid2.v4)();
6092
6413
  const liveWizard = candidate.kind === "question" ? this.liveWizardInvocations.get(conversationId) : void 0;
6093
- const matchingCall = liveWizard ? liveWizard.call : candidate.kind === "question" ? this.findMatchingPendingCall(candidate, conversationId) : null;
6414
+ const matchingCall = liveWizard ? liveWizard.call : candidate.kind === "question" ? this.findMatchingPendingCall(candidate, conversationId) ?? replacedHookCall ?? null : null;
6094
6415
  if (liveWizard) {
6095
6416
  liveWizard.promptId = promptId;
6096
6417
  } else if (matchingCall) {
6097
6418
  matchingCall.matchedPromptId = promptId;
6098
6419
  }
6099
- const matchedStepQuestionText = candidate.kind === "question" ? liveWizard?.call.structuredQuestions?.[liveWizard.activeStepIndex ?? 0]?.question ?? matchingCall?.structuredQuestions?.find((sq) => paneCandidateMatchesQuestion(sq.question, candidate))?.question ?? matchingCall?.questionText : void 0;
6420
+ const matchedStructuredQuestion = candidate.kind === "question" ? liveWizard?.call.structuredQuestions?.[liveWizard.activeStepIndex ?? 0] ?? matchingCall?.structuredQuestions?.find((sq) => paneCandidateMatchesQuestion(sq.question, candidate)) : void 0;
6421
+ const matchedStepQuestionText = candidate.kind === "question" ? matchedStructuredQuestion?.question ?? matchingCall?.questionText : void 0;
6100
6422
  const synthLineMatch = candidate.kind === "question" && matchedStepQuestionText ? matchPaneQuestionLines(matchedStepQuestionText, candidate) : null;
6423
+ const activeHookStepOpts = candidate.kind === "question" ? matchedStructuredQuestion?.options ?? matchingCall?.questionOptions : void 0;
6424
+ const isHookRestrictedMultiSelect = Boolean(
6425
+ matchingCall?.isMultiSelect || matchedStructuredQuestion?.isMultiSelect
6426
+ );
6427
+ const isHookRestrictedTruncated = Boolean(
6428
+ matchingCall?.optionsTruncated || matchedStructuredQuestion?.optionsTruncated
6429
+ );
6430
+ const hasExplicitEmptyHookStepOpts = Boolean(
6431
+ matchedStructuredQuestion && Array.isArray(matchedStructuredQuestion.options) && matchedStructuredQuestion.options.length === 0
6432
+ );
6433
+ const hookRawStepOpts = (activeHookStepOpts ?? []).map(
6434
+ (o, idx) => typeof o === "string" ? { number: String(idx + 1), text: o } : { number: String(o.number ?? idx + 1), text: o.text }
6435
+ );
6436
+ const hasHookStepOptionMismatch = candidate.kind === "question" && candidate.options.length > 0 && (hasExplicitEmptyHookStepOpts || hookRawStepOpts.length > 0 && !hookOptionsMatchPaneOptions(hookRawStepOpts, candidate.options));
6437
+ const isHookCall = Boolean(
6438
+ replacedWasHookPrompt || matchingCall?.structuredQuestions && matchingCall.structuredQuestions.length > 0
6439
+ );
6440
+ const isHookRestricted = candidate.kind === "question" && isHookCall && (replacedHookWasDisplayOnly || isHookRestrictedMultiSelect || isHookRestrictedTruncated || hasHookStepOptionMismatch);
6441
+ const effectiveSubmitMap = isHookRestricted ? {} : candidate.submitMap;
6442
+ const taggedCandidateOptions = candidate.kind === "question" ? isHookRestricted ? candidate.options : tagPaneGeneratedWriteInOption(candidate.options, activeHookStepOpts) : candidate.options;
6101
6443
  const syntheticCall = {
6102
6444
  conversationId,
6103
6445
  intentStepIndex: matchingCall?.intentStepIndex ?? -1,
@@ -6115,26 +6457,26 @@ var ApprovalDetector = class extends import_events3.EventEmitter {
6115
6457
  // NOTE (Codex R3-F1 / Claude R3-F2): Pane options are authoritative ground truth
6116
6458
  // that directly correspond to desktop keystrokes (submitMap). Transcript options
6117
6459
  // serve solely as fallback when pane options cannot be parsed.
6118
- questionOptions: candidate.kind === "question" && candidate.options && candidate.options.length > 0 ? candidate.options : matchingCall?.questionOptions,
6460
+ questionOptions: candidate.kind === "question" && taggedCandidateOptions && taggedCandidateOptions.length > 0 ? taggedCandidateOptions : matchingCall?.questionOptions,
6119
6461
  structuredQuestions: matchingCall?.structuredQuestions,
6462
+ isMultiSelect: matchingCall?.isMultiSelect ?? matchedStructuredQuestion?.isMultiSelect,
6463
+ optionsTruncated: matchingCall?.optionsTruncated ?? matchedStructuredQuestion?.optionsTruncated,
6120
6464
  promptId,
6121
6465
  startedAt: matchingCall?.startedAt ?? Date.now(),
6122
6466
  intentCreatedAt: matchingCall?.intentCreatedAt ?? (/* @__PURE__ */ new Date()).toISOString()
6123
6467
  };
6124
- const isHookCall = Boolean(
6125
- matchingCall?.structuredQuestions && matchingCall.structuredQuestions.length > 0
6126
- );
6127
6468
  const state = {
6128
6469
  promptId,
6129
6470
  conversationId,
6130
6471
  pendingCall: syntheticCall,
6131
- submitMap: candidate.submitMap,
6472
+ submitMap: effectiveSubmitMap,
6132
6473
  matchedPaneHeader: synthLineMatch?.stitchedHeaderText ?? candidate.headerText,
6133
6474
  paneDisplayHeader: synthLineMatch?.stitchedFullHeaderLine ?? candidate.fullHeaderLine,
6134
6475
  body: synthLineMatch ? synthLineMatch.remainingBody || void 0 : candidate.body,
6135
- paneOptions: candidate.options,
6136
- paneSemanticKey: buildApprovalSemanticKey(candidate),
6137
- paneRedrawSemanticKey: buildApprovalRedrawSemanticKey(candidate),
6476
+ paneBodyObserved: true,
6477
+ paneOptions: taggedCandidateOptions,
6478
+ paneSemanticKey: buildApprovalSemanticKey({ ...candidate, submitMap: effectiveSubmitMap }),
6479
+ paneRedrawSemanticKey: buildApprovalRedrawSemanticKey({ ...candidate, submitMap: effectiveSubmitMap }),
6138
6480
  kind: candidate.kind,
6139
6481
  isHookPrompt: isHookCall,
6140
6482
  // Carry hook ownership forward so pane redraws do not drop it! (Codex S2-Codex-R6-F15)
@@ -6184,6 +6526,15 @@ var ApprovalDetector = class extends import_events3.EventEmitter {
6184
6526
  if (p.pendingCall) {
6185
6527
  p.pendingCall.questionText = hookLineMatch?.stitchedHeaderText ?? call.questionText ?? p.pendingCall.questionText;
6186
6528
  p.pendingCall.structuredQuestions = call.structuredQuestions;
6529
+ if (call.questionOptions) {
6530
+ p.pendingCall.questionOptions = call.questionOptions;
6531
+ }
6532
+ if (call.isMultiSelect !== void 0) {
6533
+ p.pendingCall.isMultiSelect = call.isMultiSelect;
6534
+ }
6535
+ if (call.optionsTruncated !== void 0) {
6536
+ p.pendingCall.optionsTruncated = call.optionsTruncated;
6537
+ }
6187
6538
  if (call.intentStepIndex !== -1) {
6188
6539
  p.pendingCall.intentStepIndex = call.intentStepIndex;
6189
6540
  }
@@ -6192,25 +6543,38 @@ var ApprovalDetector = class extends import_events3.EventEmitter {
6192
6543
  } else {
6193
6544
  p.pendingCall = call;
6194
6545
  }
6195
- const isHookMultiSelect = Boolean(
6546
+ const isHookMultiSelect2 = Boolean(
6196
6547
  call.isMultiSelect || call.structuredQuestions && call.structuredQuestions[0]?.isMultiSelect
6197
6548
  );
6198
- const normOpt = (s) => s.normalize("NFKC").toLowerCase().replace(/\s+/g, " ").trim();
6199
- const hookRawOpts = call.structuredQuestions?.[0]?.options ?? call.questionOptions ?? [];
6200
- const qOptTexts = hookRawOpts.map((o) => normOpt(typeof o === "string" ? o : o.text));
6201
- const cOptTexts = (p.paneOptions || []).map((o) => normOpt(typeof o === "string" ? o : o.text));
6202
- const hasTrailingWriteIn = cOptTexts.length === qOptTexts.length + 1 && /^\s*write-?in\b/i.test(cOptTexts[cOptTexts.length - 1]);
6203
- const cEffectiveTexts = hasTrailingWriteIn ? cOptTexts.slice(0, -1) : cOptTexts;
6549
+ const isHookOptionsTruncated2 = Boolean(
6550
+ call.optionsTruncated || call.structuredQuestions && call.structuredQuestions[0]?.optionsTruncated
6551
+ );
6552
+ const hookRawOpts = (call.structuredQuestions?.[0]?.options ?? call.questionOptions ?? []).map(
6553
+ (o, idx) => typeof o === "string" ? { number: String(idx + 1), text: o } : { number: String(o.number ?? idx + 1), text: o.text }
6554
+ );
6555
+ const paneOptsNormalized = (p.paneOptions || []).map(
6556
+ (o, idx) => typeof o === "string" ? { number: String(idx + 1), text: o } : {
6557
+ number: String(o.number ?? idx + 1),
6558
+ text: o.text,
6559
+ isGeneratedWriteIn: Boolean(o.isGeneratedWriteIn)
6560
+ }
6561
+ );
6204
6562
  const hasStructuredQuestion = Boolean(call.structuredQuestions && call.structuredQuestions.length > 0);
6205
- const hasHookOptionMismatch = cOptTexts.length > 0 && (hasStructuredQuestion && qOptTexts.length === 0 || qOptTexts.length > 0 && (cEffectiveTexts.length !== qOptTexts.length || qOptTexts.some((q) => !cEffectiveTexts.includes(q)) || cEffectiveTexts.some((c) => !qOptTexts.includes(c))));
6206
- if ((isHookMultiSelect || hasHookOptionMismatch) && Object.keys(p.submitMap || {}).length > 0) {
6563
+ const hasHookOptionMismatch = paneOptsNormalized.length > 0 && (hasStructuredQuestion && hookRawOpts.length === 0 || hookRawOpts.length > 0 && !hookOptionsMatchPaneOptions(hookRawOpts, paneOptsNormalized));
6564
+ if ((isHookMultiSelect2 || isHookOptionsTruncated2 || hasHookOptionMismatch) && Object.keys(p.submitMap || {}).length > 0) {
6207
6565
  p.submitMap = {};
6208
- logger.info("emitHookQuestionPrompt: revoked actionable submitMap due to multi-select or option mismatch", {
6566
+ logger.info("emitHookQuestionPrompt: revoked actionable submitMap due to multi-select, truncation, or option mismatch", {
6209
6567
  promptId: p.promptId,
6210
- isHookMultiSelect,
6568
+ isHookMultiSelect: isHookMultiSelect2,
6569
+ isHookOptionsTruncated: isHookOptionsTruncated2,
6211
6570
  hasHookOptionMismatch
6212
6571
  });
6213
6572
  this.emit("prompt-upgraded", p);
6573
+ } else if (p.paneOptions && p.paneOptions.length > 0 && Object.keys(p.submitMap || {}).length > 0) {
6574
+ p.paneOptions = tagPaneGeneratedWriteInOption(
6575
+ p.paneOptions,
6576
+ hookRawOpts.length > 0 ? hookRawOpts : void 0
6577
+ );
6214
6578
  }
6215
6579
  p.isHookPrompt = true;
6216
6580
  call.matchedPromptId = p.promptId;
@@ -6241,21 +6605,38 @@ var ApprovalDetector = class extends import_events3.EventEmitter {
6241
6605
  const promptId = forcedPromptId ?? (0, import_uuid2.v4)();
6242
6606
  this.registerPendingCall(call);
6243
6607
  call.matchedPromptId = promptId;
6608
+ const firstStructured = call.structuredQuestions?.[0];
6609
+ const isHookMultiSelect = Boolean(
6610
+ call.isMultiSelect || firstStructured?.isMultiSelect
6611
+ );
6612
+ const isHookOptionsTruncated = Boolean(
6613
+ call.optionsTruncated || firstStructured?.optionsTruncated
6614
+ );
6615
+ const hasExplicitEmptyStructuredOptions = Boolean(
6616
+ firstStructured && Array.isArray(firstStructured.options) && firstStructured.options.length === 0
6617
+ );
6618
+ const rawOptions = hasExplicitEmptyStructuredOptions ? [] : firstStructured?.options && firstStructured.options.length > 0 ? firstStructured.options : call.questionOptions;
6619
+ const { paneOptions, submitMap } = buildHookQuestionOptionsAndSubmitMap(
6620
+ rawOptions,
6621
+ isHookMultiSelect,
6622
+ isHookOptionsTruncated
6623
+ );
6244
6624
  const state = {
6245
6625
  promptId,
6246
6626
  conversationId: call.conversationId,
6247
6627
  pendingCall: call,
6248
- submitMap: {},
6628
+ submitMap,
6249
6629
  matchedPaneHeader: call.questionText ?? "Question",
6250
6630
  paneDisplayHeader: call.questionText ?? "Question",
6251
6631
  body: void 0,
6252
- paneOptions: call.questionOptions,
6632
+ paneBodyObserved: false,
6633
+ paneOptions,
6253
6634
  paneSemanticKey: buildApprovalSemanticKey({
6254
6635
  kind: "question",
6255
6636
  headerText: call.questionText ?? "Question",
6256
6637
  fullHeaderLine: call.questionText ?? "Question",
6257
- options: call.questionOptions ?? [],
6258
- submitMap: {},
6638
+ options: paneOptions ?? [],
6639
+ submitMap,
6259
6640
  body: "",
6260
6641
  paneHash: ""
6261
6642
  }),
@@ -6278,7 +6659,8 @@ var ApprovalDetector = class extends import_events3.EventEmitter {
6278
6659
  promptId,
6279
6660
  conversationId: call.conversationId,
6280
6661
  questionText: call.questionText,
6281
- optionsCount: call.questionOptions?.length ?? 0
6662
+ optionsCount: paneOptions?.length ?? 0,
6663
+ mobileActionable: Object.keys(submitMap).length > 0
6282
6664
  });
6283
6665
  this.emit("pending-prompt", state);
6284
6666
  return state;
@@ -6789,9 +7171,13 @@ var PromptResponder = class {
6789
7171
  };
6790
7172
  }
6791
7173
  const isQuestionPrompt = state.kind === "question";
6792
- const expectedStep = (state.paneDisplayHeader || "").match(/\bQuestion\s+(\d+(?:[\/:of\s]+\d+)?):/i)?.[1];
7174
+ let expectedStep = (state.paneDisplayHeader || "").match(/\bQuestion\s+(\d+(?:[\/:of\s]+\d+)?):/i)?.[1];
6793
7175
  const matchingOpt = state.paneOptions?.find((o) => String(o.number) === String(optionNumber));
6794
- const isWriteInOption = matchingOpt ? /^\s*write-?in\b/i.test(matchingOpt.text) : false;
7176
+ const activeStepHookOpts = state.pendingCall?.structuredQuestions?.[state.wizardStepIndex ?? 0]?.options ?? state.pendingCall?.questionOptions;
7177
+ const hookBaseOptCount = activeStepHookOpts ? activeStepHookOpts.filter((o) => !o.isGeneratedWriteIn).length : 0;
7178
+ const isWriteInOption = matchingOpt ? Boolean(
7179
+ matchingOpt.isGeneratedWriteIn || matchingOpt === state.paneOptions?.[state.paneOptions.length - 1] && /^\s*write-?in(?:\.{3}|…)?\s*$/i.test(matchingOpt.text) && (!state.isHookPrompt || hookBaseOptCount === 0 || (state.paneOptions?.length ?? 0) === hookBaseOptCount + 1)
7180
+ ) : false;
6795
7181
  if (isQuestionPrompt) {
6796
7182
  if (state.paneOptions && state.paneOptions.length > 0 && !matchingOpt) {
6797
7183
  return {
@@ -6849,9 +7235,10 @@ var PromptResponder = class {
6849
7235
  };
6850
7236
  }
6851
7237
  let liveSemanticKey = null;
7238
+ let liveCandidate = null;
6852
7239
  try {
6853
- const snapshot = await this.paneObserver.captureSnapshot();
6854
- const postCapturePrompt = this.detector.getPendingPrompt(promptId);
7240
+ let snapshot = await this.paneObserver.captureSnapshot();
7241
+ let postCapturePrompt = this.detector.getPendingPrompt(promptId);
6855
7242
  if (postCapturePrompt !== state || !postCapturePrompt.submitMap || !postCapturePrompt.submitMap[optionNumber]) {
6856
7243
  return {
6857
7244
  ok: false,
@@ -6868,7 +7255,35 @@ var PromptResponder = class {
6868
7255
  deliveryPhase: "none"
6869
7256
  };
6870
7257
  }
6871
- const live = parseApprovalSnapshot(snapshot);
7258
+ let live = parseApprovalSnapshot(snapshot);
7259
+ if (!live && isQuestionPrompt && state.isHookPrompt && !state.paneHash && detectActivePromptKind(snapshot) === null) {
7260
+ const waitDeadline = Date.now() + 250;
7261
+ while (!live && Date.now() < waitDeadline) {
7262
+ await delay(50);
7263
+ snapshot = await this.paneObserver.captureSnapshot();
7264
+ postCapturePrompt = this.detector.getPendingPrompt(promptId);
7265
+ if (postCapturePrompt !== state || !postCapturePrompt.submitMap || !postCapturePrompt.submitMap[optionNumber]) {
7266
+ return {
7267
+ ok: false,
7268
+ reason: "prompt-expired",
7269
+ details: "option is no longer actionable on active prompt (controls revoked or updated)",
7270
+ deliveryPhase
7271
+ };
7272
+ }
7273
+ live = parseApprovalSnapshot(snapshot);
7274
+ if (!live && detectActivePromptKind(snapshot) !== null) {
7275
+ break;
7276
+ }
7277
+ }
7278
+ if (!live && detectActivePromptKind(snapshot) === null && !state.paneHash) {
7279
+ return {
7280
+ ok: false,
7281
+ reason: "prompt-probe-failed",
7282
+ details: "the question chooser has not rendered on the terminal yet; please retry in a moment",
7283
+ deliveryPhase
7284
+ };
7285
+ }
7286
+ }
6872
7287
  if (!live) {
6873
7288
  if (detectActivePromptKind(snapshot) === null) {
6874
7289
  this.detector.resolvePrompt(promptId);
@@ -6886,6 +7301,7 @@ var PromptResponder = class {
6886
7301
  deliveryPhase
6887
7302
  };
6888
7303
  }
7304
+ liveCandidate = live;
6889
7305
  liveSemanticKey = live ? buildApprovalSemanticKey(live) : null;
6890
7306
  } catch (error) {
6891
7307
  return {
@@ -6895,15 +7311,55 @@ var PromptResponder = class {
6895
7311
  deliveryPhase
6896
7312
  };
6897
7313
  }
6898
- if (liveSemanticKey !== state.paneSemanticKey) {
7314
+ if (!liveCandidate || !liveCandidate.submitMap?.[optionNumber]?.length) {
6899
7315
  this.detector.resolvePrompt(promptId);
6900
7316
  return {
6901
7317
  ok: false,
6902
7318
  reason: "prompt-superseded",
6903
- details: "active chooser semantics differ from the mobile prompt",
7319
+ details: "active chooser does not allow actionable submission for this option",
6904
7320
  deliveryPhase
6905
7321
  };
6906
7322
  }
7323
+ if (liveSemanticKey !== state.paneSemanticKey) {
7324
+ let hookQuestionMatchesLive = false;
7325
+ if (isQuestionPrompt && state.isHookPrompt && liveCandidate && liveCandidate.kind === "question") {
7326
+ const activeStepQ = state.pendingCall?.structuredQuestions?.[state.wizardStepIndex ?? 0];
7327
+ const hookQuestionText = activeStepQ?.question ?? state.pendingCall?.questionText ?? state.matchedPaneHeader;
7328
+ const lineMatch = matchPaneQuestionLines(hookQuestionText, liveCandidate);
7329
+ const liveRemainingBody = lineMatch ? lineMatch.remainingBody || void 0 : liveCandidate.body || void 0;
7330
+ const bodyConsistent = !state.paneBodyObserved || state.body === liveRemainingBody;
7331
+ const normOpt = (s) => s.normalize("NFKC").toLowerCase().replace(/\s+/g, " ").trim();
7332
+ const optionsMatch = hookOptionsMatchPaneOptions(state.paneOptions, liveCandidate.options);
7333
+ const selectedLiveOpt = (liveCandidate.options ?? []).find(
7334
+ (o) => String(o.number) === String(optionNumber)
7335
+ );
7336
+ const selectedOptMatches = isWriteInOption ? Boolean(selectedLiveOpt && /^\s*write-?in(?:\.{3}|…)?\s*$/i.test(selectedLiveOpt.text)) : Boolean(
7337
+ selectedLiveOpt && matchingOpt && normOpt(selectedLiveOpt.text) === normOpt(matchingOpt.text)
7338
+ );
7339
+ if (lineMatch !== null && bodyConsistent && optionsMatch && selectedOptMatches && Boolean(liveCandidate.submitMap[optionNumber]?.length)) {
7340
+ hookQuestionMatchesLive = true;
7341
+ state.matchedPaneHeader = lineMatch.stitchedHeaderText;
7342
+ state.paneDisplayHeader = lineMatch.stitchedFullHeaderLine;
7343
+ state.body = liveRemainingBody;
7344
+ state.paneBodyObserved = true;
7345
+ if (liveSemanticKey) {
7346
+ state.paneSemanticKey = liveSemanticKey;
7347
+ }
7348
+ expectedStep = (state.paneDisplayHeader || "").match(/\bQuestion\s+(\d+(?:[\/:of\s]+\d+)?):/i)?.[1];
7349
+ }
7350
+ }
7351
+ if (!hookQuestionMatchesLive) {
7352
+ this.detector.resolvePrompt(promptId);
7353
+ return {
7354
+ ok: false,
7355
+ reason: "prompt-superseded",
7356
+ details: "active chooser semantics differ from the mobile prompt",
7357
+ deliveryPhase
7358
+ };
7359
+ }
7360
+ } else if (!state.paneBodyObserved) {
7361
+ state.paneBodyObserved = true;
7362
+ }
6907
7363
  deliveryPhase = "option_attempted";
6908
7364
  if (isNamedKey(key)) {
6909
7365
  await this.sendKey(target, key);
@@ -8970,6 +9426,7 @@ var McpServer = class _McpServer {
8970
9426
  questionText,
8971
9427
  questionOptions,
8972
9428
  structuredQuestions,
9429
+ optionsTruncated: Boolean(structuredQuestions?.[0]?.optionsTruncated),
8973
9430
  startedAt: Date.now(),
8974
9431
  intentCreatedAt: (/* @__PURE__ */ new Date()).toISOString()
8975
9432
  };
@@ -8997,7 +9454,19 @@ var McpServer = class _McpServer {
8997
9454
  });
8998
9455
  return;
8999
9456
  }
9000
- const e1rec = this.newE1Raise(session.sessionId, false);
9457
+ const firstStructured = pendingCall.structuredQuestions?.[0];
9458
+ const isMultiSelect = Boolean(
9459
+ pendingCall.isMultiSelect || firstStructured?.isMultiSelect
9460
+ );
9461
+ const optionsTruncated = Boolean(
9462
+ pendingCall.optionsTruncated || firstStructured?.optionsTruncated
9463
+ );
9464
+ const hasExplicitEmptyStructuredOptions = Boolean(
9465
+ firstStructured && Array.isArray(firstStructured.options) && firstStructured.options.length === 0
9466
+ );
9467
+ const rawOpts = hasExplicitEmptyStructuredOptions ? [] : firstStructured?.options && firstStructured.options.length > 0 ? firstStructured.options : pendingCall.questionOptions;
9468
+ const isInitiallyActionable = !isMultiSelect && !optionsTruncated && Boolean(rawOpts && rawOpts.length > 0 && rawOpts.length <= 8);
9469
+ const e1rec = this.newE1Raise(session.sessionId, isInitiallyActionable);
9001
9470
  const emitted = this.approvalDetector.emitHookQuestionPrompt(pendingCall, e1rec.promptId);
9002
9471
  if (!emitted) {
9003
9472
  this.e1PromptRaises.delete(e1rec.promptId);
@@ -10437,7 +10906,7 @@ var McpServer = class _McpServer {
10437
10906
  } else if (optionResult.reason === "prompt-expired" || optionResult.reason === "prompt-superseded") {
10438
10907
  message = "The question prompt on desktop has expired or changed. Please review on desktop.";
10439
10908
  } else if (optionResult.reason === "prompt-probe-failed") {
10440
- message = "Write-in input field was not ready on desktop. Please try again or respond on desktop.";
10909
+ message = optionResult.details?.includes("has not rendered on the terminal yet") ? optionResult.details : intent.followUpText ? "Write-in input field was not ready on desktop. Please try again or respond on desktop." : "Question prompt was not ready on desktop. Please try again or respond on desktop.";
10441
10910
  } else if (optionResult.reason === "no-tmux-target" || optionResult.reason === "tmux-failed") {
10442
10911
  message = optionResult.details || "Desktop terminal session is unreachable. Please respond directly on desktop.";
10443
10912
  } else if (optionResult.reason === "display-only-question-active") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quantiya/codevibe-antigravity-plugin",
3
- "version": "2.0.102",
3
+ "version": "2.0.104",
4
4
  "description": "Control Antigravity CLI from your iPhone and Android — real-time sync, approve file edits, send prompts by voice. Part of CodeVibe.",
5
5
  "main": "dist/server.js",
6
6
  "codevibe": {