@quantiya/codevibe-antigravity-plugin 2.0.103 → 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
@@ -144,6 +144,7 @@ function serializeStructuredArg(value, maxChars = MAX_STRUCTURED_ARG_CHARS, keyH
144
144
  (item) => typeof item === "string" || typeof item === "number" || item && typeof item === "object" && !("question" in item)
145
145
  );
146
146
  if (isOptionArray) {
147
+ const wasSlicedAtCap = parsed.length > 30;
147
148
  const boundedOptions = parsed.slice(0, 30).map((opt, idx) => {
148
149
  if (typeof opt === "string") return { number: String(idx + 1), text: opt };
149
150
  if (typeof opt === "number") return { number: String(idx + 1), text: String(opt) };
@@ -155,17 +156,23 @@ function serializeStructuredArg(value, maxChars = MAX_STRUCTURED_ARG_CHARS, keyH
155
156
  }
156
157
  return { number: String(idx + 1), text: String(opt ?? "") };
157
158
  });
159
+ if (wasSlicedAtCap && boundedOptions.length > 0) {
160
+ boundedOptions[0].options_truncated = true;
161
+ }
158
162
  const str2 = JSON.stringify(boundedOptions);
159
163
  if (str2.length <= maxChars) return str2;
160
164
  for (let i = boundedOptions.length - 1; i >= 1; i--) {
161
- 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);
162
169
  if (sub.length <= maxChars) return sub;
163
170
  }
164
171
  const firstOpt = boundedOptions[0];
165
172
  if (firstOpt) {
166
173
  return sliceToSerializedBudget(
167
174
  firstOpt.text,
168
- (safeText) => JSON.stringify([{ number: firstOpt.number, text: safeText }]),
175
+ (safeText) => JSON.stringify([{ number: firstOpt.number, text: safeText, options_truncated: true }]),
169
176
  maxChars
170
177
  );
171
178
  }
@@ -179,7 +186,12 @@ function serializeStructuredArg(value, maxChars = MAX_STRUCTURED_ARG_CHARS, keyH
179
186
  if (typeof obj.question === "string") res.question = obj.question;
180
187
  if (typeof obj.is_multi_select !== "undefined") res.is_multi_select = decodeBoolean(obj.is_multi_select);
181
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);
182
191
  if (Array.isArray(obj.options)) {
192
+ if (obj.options.length > 25) {
193
+ res.options_truncated = true;
194
+ }
183
195
  res.options = obj.options.slice(0, 25).map((opt) => {
184
196
  if (typeof opt === "string") return opt;
185
197
  if (typeof opt === "number") return String(opt);
@@ -209,12 +221,18 @@ function serializeStructuredArg(value, maxChars = MAX_STRUCTURED_ARG_CHARS, keyH
209
221
  const qText = typeof qObj.question === "string" ? qObj.question : "";
210
222
  const opts = Array.isArray(qObj.options) ? qObj.options : [];
211
223
  for (let k = opts.length - 1; k >= 1; k--) {
212
- const subQ = JSON.stringify([{ ...qObj, options: opts.slice(0, k) }]);
224
+ const subQ = JSON.stringify([{ ...qObj, options: opts.slice(0, k), options_truncated: true }]);
213
225
  if (subQ.length <= maxChars) return subQ;
214
226
  }
215
227
  return sliceToSerializedBudget(
216
228
  qText,
217
- (safeQText) => JSON.stringify([{ question: safeQText, options: [] }]),
229
+ (safeQText) => JSON.stringify([
230
+ {
231
+ question: safeQText,
232
+ options: [],
233
+ ...opts.length > 0 ? { options_truncated: true } : {}
234
+ }
235
+ ]),
218
236
  maxChars
219
237
  );
220
238
  }
package/dist/server.js CHANGED
@@ -5711,28 +5711,61 @@ function extractAllQuestionsFromArgs(toolName, args) {
5711
5711
  const qObj = qItem;
5712
5712
  const qText = typeof qObj.question === "string" ? stripWrappingQuotes2(qObj.question.trim()) : "Question";
5713
5713
  const opts = [];
5714
+ let optItemTruncated = false;
5714
5715
  if (Array.isArray(qObj.options)) {
5715
5716
  for (let i = 0; i < qObj.options.length; i++) {
5716
5717
  const opt = qObj.options[i];
5717
5718
  if (typeof opt === "string" && opt.trim().length > 0) {
5718
5719
  opts.push({ number: String(i + 1), text: stripWrappingQuotes2(opt.trim()) });
5719
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
+ }
5720
5725
  opts.push({
5721
- number: String(opt.number ?? i + 1),
5722
- text: stripWrappingQuotes2(String(opt.text).trim())
5726
+ number: String(optRecord.number ?? i + 1),
5727
+ text: stripWrappingQuotes2(String(optRecord.text).trim())
5723
5728
  });
5724
5729
  }
5725
5730
  }
5726
5731
  }
5727
5732
  const isMulti = decodeBoolean(qObj.is_multi_select ?? qObj.isMultiSelect);
5728
- 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
+ });
5729
5740
  }
5730
5741
  }
5731
5742
  } else if (typeof a.question === "string" || typeof a.prompt === "string") {
5732
5743
  const qText = stripWrappingQuotes2(String(a.question ?? a.prompt).trim());
5733
5744
  const opts = extractQuestionOptionsFromArgs(toolName, args) ?? [];
5734
5745
  const isMulti = decodeBoolean(a.is_multi_select ?? a.isMultiSelect);
5735
- 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
+ });
5736
5769
  }
5737
5770
  return items.length > 0 ? items : void 0;
5738
5771
  }
@@ -5834,6 +5867,104 @@ function matchPaneQuestionLines(hookText, candidate) {
5834
5867
  function paneCandidateMatchesQuestion(hookText, candidate) {
5835
5868
  return matchPaneQuestionLines(hookText, candidate) !== null;
5836
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
+ }
5837
5968
  function correlatesQuestionInvocation(call, delivery) {
5838
5969
  if (!call) return false;
5839
5970
  const tn = (delivery.toolName ?? "").toLowerCase().trim();
@@ -6023,6 +6154,9 @@ var ApprovalDetector = class extends import_events3.EventEmitter {
6023
6154
  return null;
6024
6155
  }
6025
6156
  }
6157
+ let replacedHookCall;
6158
+ let replacedWasHookPrompt = false;
6159
+ let replacedHookWasDisplayOnly = false;
6026
6160
  if (candidate.kind === "question") {
6027
6161
  const paneQuestionText = candidate.headerText || candidate.fullHeaderLine;
6028
6162
  const candidateSemanticKey = buildApprovalSemanticKey(candidate);
@@ -6039,7 +6173,7 @@ var ApprovalDetector = class extends import_events3.EventEmitter {
6039
6173
  const qOptTexts = (targetQ.options || []).map((o) => normOpt(typeof o === "string" ? o : o.text));
6040
6174
  const cOptTexts = (candidate.options || []).map((o) => normOpt(typeof o === "string" ? o : o.text));
6041
6175
  if (cOptTexts.length > 0 && qOptTexts.length > 0) {
6042
- 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]);
6043
6177
  const cEffectiveTexts = hasTrailingWriteIn ? cOptTexts.slice(0, -1) : cOptTexts;
6044
6178
  const matchedQCount = qOptTexts.filter((q) => cEffectiveTexts.includes(q)).length;
6045
6179
  const matchedCCount = cEffectiveTexts.filter((c) => qOptTexts.includes(c)).length;
@@ -6073,6 +6207,20 @@ var ApprovalDetector = class extends import_events3.EventEmitter {
6073
6207
  }
6074
6208
  }
6075
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
+ }
6076
6224
  logger.debug("Pane candidate did not match any step of active live wizard", {
6077
6225
  paneQuestionText,
6078
6226
  currentActiveIdx,
@@ -6091,53 +6239,42 @@ var ApprovalDetector = class extends import_events3.EventEmitter {
6091
6239
  (p) => p.conversationId === conversationId && p.kind === "question"
6092
6240
  );
6093
6241
  if (existing) {
6094
- const targetQ = existing.pendingCall?.structuredQuestions?.[existing.wizardStepIndex ?? 0];
6095
- const isAuthoritativeMultiSelect = Boolean(targetQ?.isMultiSelect);
6096
- const isInitialUpgrade = existing.isHookPrompt && !isAuthoritativeMultiSelect && Object.keys(existing.submitMap || {}).length === 0 && Object.keys(candidate.submitMap || {}).length > 0;
6097
- if (isInitialUpgrade) {
6098
- existing.submitMap = candidate.submitMap;
6099
- if (candidate.options && candidate.options.length > 0) {
6100
- existing.paneOptions = candidate.options;
6101
- }
6102
- existing.paneSemanticKey = candidateSemanticKey;
6103
- existing.paneHash = candidate.paneHash;
6104
- existing.matchedPaneHeader = stepLineMatch?.stitchedHeaderText ?? candidate.headerText ?? existing.matchedPaneHeader;
6105
- existing.paneDisplayHeader = stepLineMatch?.stitchedFullHeaderLine ?? candidate.fullHeaderLine ?? existing.paneDisplayHeader;
6106
- existing.body = stepLineMatch ? stepLineMatch.remainingBody || void 0 : candidate.body;
6107
- if (candidate.paneHash) this.paneOnlyEmittedHashes.add(candidate.paneHash);
6108
- logger.info("Upgraded hook-driven question prompt to actionable from pane observer", {
6109
- promptId: existing.promptId,
6110
- optionsCount: existing.paneOptions?.length ?? 0
6111
- });
6112
- this.emit("prompt-upgraded", existing);
6113
- return existing;
6114
- }
6115
- const hasPriorPaneObservation = Boolean(existing.paneSemanticKey);
6116
- const areControlsMutated = (existingOpts, candidateOpts) => {
6117
- if (!existingOpts || !candidateOpts) return false;
6118
- if (existingOpts.length !== candidateOpts.length) return true;
6119
- return !existingOpts.every(
6120
- (o, idx) => o.number === candidateOpts[idx].number && (o.text || "").trim() === (candidateOpts[idx].text || "").trim() && (!hasPriorPaneObservation || JSON.stringify(o.rawLines ?? []) === JSON.stringify(candidateOpts[idx].rawLines ?? []))
6121
- );
6122
- };
6123
- const isControlDelta = Boolean(
6124
- existing.paneOptions && existing.paneOptions.length > 0 && candidate.options && candidate.options.length > 0 && areControlsMutated(existing.paneOptions, candidate.options)
6125
- );
6126
- if (isControlDelta) {
6127
- logger.info("Control delta observed on active question step; resolving old prompt and re-emitting", {
6128
- oldPromptId: existing.promptId,
6129
- oldSemanticKey: existing.paneSemanticKey,
6130
- newSemanticKey: candidateSemanticKey
6131
- });
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;
6132
6248
  this.resolvePrompt(existing.promptId, { advanceWizard: false });
6133
6249
  this.emit("prompt-resolved", existing.promptId);
6134
6250
  } else {
6135
- if (!existing.paneSemanticKey) {
6136
- 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;
6137
6264
  if (candidate.options && candidate.options.length > 0) {
6138
- existing.paneOptions = candidate.options;
6265
+ existing.paneOptions = tagPaneGeneratedWriteInOption(candidate.options, targetQ?.options);
6139
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);
6140
6276
  }
6277
+ existing.paneHash = candidate.paneHash;
6141
6278
  if (stepLineMatch) {
6142
6279
  existing.matchedPaneHeader = stepLineMatch.stitchedHeaderText;
6143
6280
  existing.paneDisplayHeader = stepLineMatch.stitchedFullHeaderLine;
@@ -6145,8 +6282,19 @@ var ApprovalDetector = class extends import_events3.EventEmitter {
6145
6282
  if (existing.pendingCall && targetStepQ?.question) {
6146
6283
  existing.pendingCall.questionText = stepLineMatch.stitchedHeaderText;
6147
6284
  }
6148
- } else if (candidate.body) {
6149
- 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);
6150
6298
  }
6151
6299
  if (candidate.paneHash) this.paneOnlyEmittedHashes.add(candidate.paneHash);
6152
6300
  return null;
@@ -6173,44 +6321,86 @@ var ApprovalDetector = class extends import_events3.EventEmitter {
6173
6321
  const lineMatch = matchPaneQuestionLines(hookQuestionText, candidate);
6174
6322
  const isSameQuestion = lineMatch !== null;
6175
6323
  if (isSameQuestion) {
6176
- const targetQ = p.pendingCall?.structuredQuestions?.[p.wizardStepIndex ?? 0];
6177
- const isAuthoritativeMultiSelect = Boolean(targetQ?.isMultiSelect);
6178
- const isInitialUpgrade = p.isHookPrompt && !isAuthoritativeMultiSelect && Object.keys(p.submitMap || {}).length === 0 && Object.keys(candidate.submitMap || {}).length > 0;
6179
- const isControlDelta = !isInitialUpgrade && Boolean(
6180
- p.paneSemanticKey && candidateSemanticKey !== p.paneSemanticKey
6181
- );
6182
- if (!isControlDelta) {
6183
- if (isInitialUpgrade) {
6184
- p.submitMap = candidate.submitMap;
6185
- if (candidate.options && candidate.options.length > 0) {
6186
- 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);
6187
6353
  }
6188
- p.paneSemanticKey = candidateSemanticKey;
6189
6354
  p.paneHash = candidate.paneHash;
6190
- p.matchedPaneHeader = lineMatch?.stitchedHeaderText ?? candidate.headerText ?? p.matchedPaneHeader;
6191
- p.paneDisplayHeader = lineMatch?.stitchedFullHeaderLine ?? candidate.fullHeaderLine ?? p.paneDisplayHeader;
6192
- 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
+ }
6193
6376
  if (candidate.paneHash) this.paneOnlyEmittedHashes.add(candidate.paneHash);
6194
- logger.info("Upgraded hook-driven question prompt to actionable from pane observer", {
6195
- promptId: p.promptId,
6196
- optionsCount: p.paneOptions?.length ?? 0
6197
- });
6198
- this.emit("prompt-upgraded", p);
6199
- return p;
6377
+ return null;
6200
6378
  }
6201
- if (!p.paneSemanticKey) {
6202
- p.paneSemanticKey = candidateSemanticKey;
6203
- }
6204
- if (lineMatch) {
6205
- p.matchedPaneHeader = lineMatch.stitchedHeaderText;
6206
- p.paneDisplayHeader = lineMatch.stitchedFullHeaderLine;
6207
- p.body = lineMatch.remainingBody || void 0;
6208
- if (p.pendingCall) {
6209
- 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;
6210
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;
6211
6399
  }
6212
- if (candidate.paneHash) this.paneOnlyEmittedHashes.add(candidate.paneHash);
6213
- return null;
6400
+ } else {
6401
+ replacedHookCall = p.pendingCall;
6402
+ replacedWasHookPrompt = true;
6403
+ replacedHookWasDisplayOnly = Object.keys(p.submitMap || {}).length === 0;
6214
6404
  }
6215
6405
  }
6216
6406
  this.resolvePrompt(p.promptId);
@@ -6221,14 +6411,35 @@ var ApprovalDetector = class extends import_events3.EventEmitter {
6221
6411
  }
6222
6412
  const promptId = forcedPromptId ?? (0, import_uuid2.v4)();
6223
6413
  const liveWizard = candidate.kind === "question" ? this.liveWizardInvocations.get(conversationId) : void 0;
6224
- 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;
6225
6415
  if (liveWizard) {
6226
6416
  liveWizard.promptId = promptId;
6227
6417
  } else if (matchingCall) {
6228
6418
  matchingCall.matchedPromptId = promptId;
6229
6419
  }
6230
- 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;
6231
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;
6232
6443
  const syntheticCall = {
6233
6444
  conversationId,
6234
6445
  intentStepIndex: matchingCall?.intentStepIndex ?? -1,
@@ -6246,26 +6457,26 @@ var ApprovalDetector = class extends import_events3.EventEmitter {
6246
6457
  // NOTE (Codex R3-F1 / Claude R3-F2): Pane options are authoritative ground truth
6247
6458
  // that directly correspond to desktop keystrokes (submitMap). Transcript options
6248
6459
  // serve solely as fallback when pane options cannot be parsed.
6249
- 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,
6250
6461
  structuredQuestions: matchingCall?.structuredQuestions,
6462
+ isMultiSelect: matchingCall?.isMultiSelect ?? matchedStructuredQuestion?.isMultiSelect,
6463
+ optionsTruncated: matchingCall?.optionsTruncated ?? matchedStructuredQuestion?.optionsTruncated,
6251
6464
  promptId,
6252
6465
  startedAt: matchingCall?.startedAt ?? Date.now(),
6253
6466
  intentCreatedAt: matchingCall?.intentCreatedAt ?? (/* @__PURE__ */ new Date()).toISOString()
6254
6467
  };
6255
- const isHookCall = Boolean(
6256
- matchingCall?.structuredQuestions && matchingCall.structuredQuestions.length > 0
6257
- );
6258
6468
  const state = {
6259
6469
  promptId,
6260
6470
  conversationId,
6261
6471
  pendingCall: syntheticCall,
6262
- submitMap: candidate.submitMap,
6472
+ submitMap: effectiveSubmitMap,
6263
6473
  matchedPaneHeader: synthLineMatch?.stitchedHeaderText ?? candidate.headerText,
6264
6474
  paneDisplayHeader: synthLineMatch?.stitchedFullHeaderLine ?? candidate.fullHeaderLine,
6265
6475
  body: synthLineMatch ? synthLineMatch.remainingBody || void 0 : candidate.body,
6266
- paneOptions: candidate.options,
6267
- paneSemanticKey: buildApprovalSemanticKey(candidate),
6268
- paneRedrawSemanticKey: buildApprovalRedrawSemanticKey(candidate),
6476
+ paneBodyObserved: true,
6477
+ paneOptions: taggedCandidateOptions,
6478
+ paneSemanticKey: buildApprovalSemanticKey({ ...candidate, submitMap: effectiveSubmitMap }),
6479
+ paneRedrawSemanticKey: buildApprovalRedrawSemanticKey({ ...candidate, submitMap: effectiveSubmitMap }),
6269
6480
  kind: candidate.kind,
6270
6481
  isHookPrompt: isHookCall,
6271
6482
  // Carry hook ownership forward so pane redraws do not drop it! (Codex S2-Codex-R6-F15)
@@ -6315,6 +6526,15 @@ var ApprovalDetector = class extends import_events3.EventEmitter {
6315
6526
  if (p.pendingCall) {
6316
6527
  p.pendingCall.questionText = hookLineMatch?.stitchedHeaderText ?? call.questionText ?? p.pendingCall.questionText;
6317
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
+ }
6318
6538
  if (call.intentStepIndex !== -1) {
6319
6539
  p.pendingCall.intentStepIndex = call.intentStepIndex;
6320
6540
  }
@@ -6323,25 +6543,38 @@ var ApprovalDetector = class extends import_events3.EventEmitter {
6323
6543
  } else {
6324
6544
  p.pendingCall = call;
6325
6545
  }
6326
- const isHookMultiSelect = Boolean(
6546
+ const isHookMultiSelect2 = Boolean(
6327
6547
  call.isMultiSelect || call.structuredQuestions && call.structuredQuestions[0]?.isMultiSelect
6328
6548
  );
6329
- const normOpt = (s) => s.normalize("NFKC").toLowerCase().replace(/\s+/g, " ").trim();
6330
- const hookRawOpts = call.structuredQuestions?.[0]?.options ?? call.questionOptions ?? [];
6331
- const qOptTexts = hookRawOpts.map((o) => normOpt(typeof o === "string" ? o : o.text));
6332
- const cOptTexts = (p.paneOptions || []).map((o) => normOpt(typeof o === "string" ? o : o.text));
6333
- const hasTrailingWriteIn = cOptTexts.length === qOptTexts.length + 1 && /^\s*write-?in\b/i.test(cOptTexts[cOptTexts.length - 1]);
6334
- 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
+ );
6335
6562
  const hasStructuredQuestion = Boolean(call.structuredQuestions && call.structuredQuestions.length > 0);
6336
- 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))));
6337
- 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) {
6338
6565
  p.submitMap = {};
6339
- 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", {
6340
6567
  promptId: p.promptId,
6341
- isHookMultiSelect,
6568
+ isHookMultiSelect: isHookMultiSelect2,
6569
+ isHookOptionsTruncated: isHookOptionsTruncated2,
6342
6570
  hasHookOptionMismatch
6343
6571
  });
6344
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
+ );
6345
6578
  }
6346
6579
  p.isHookPrompt = true;
6347
6580
  call.matchedPromptId = p.promptId;
@@ -6372,21 +6605,38 @@ var ApprovalDetector = class extends import_events3.EventEmitter {
6372
6605
  const promptId = forcedPromptId ?? (0, import_uuid2.v4)();
6373
6606
  this.registerPendingCall(call);
6374
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
+ );
6375
6624
  const state = {
6376
6625
  promptId,
6377
6626
  conversationId: call.conversationId,
6378
6627
  pendingCall: call,
6379
- submitMap: {},
6628
+ submitMap,
6380
6629
  matchedPaneHeader: call.questionText ?? "Question",
6381
6630
  paneDisplayHeader: call.questionText ?? "Question",
6382
6631
  body: void 0,
6383
- paneOptions: call.questionOptions,
6632
+ paneBodyObserved: false,
6633
+ paneOptions,
6384
6634
  paneSemanticKey: buildApprovalSemanticKey({
6385
6635
  kind: "question",
6386
6636
  headerText: call.questionText ?? "Question",
6387
6637
  fullHeaderLine: call.questionText ?? "Question",
6388
- options: call.questionOptions ?? [],
6389
- submitMap: {},
6638
+ options: paneOptions ?? [],
6639
+ submitMap,
6390
6640
  body: "",
6391
6641
  paneHash: ""
6392
6642
  }),
@@ -6409,7 +6659,8 @@ var ApprovalDetector = class extends import_events3.EventEmitter {
6409
6659
  promptId,
6410
6660
  conversationId: call.conversationId,
6411
6661
  questionText: call.questionText,
6412
- optionsCount: call.questionOptions?.length ?? 0
6662
+ optionsCount: paneOptions?.length ?? 0,
6663
+ mobileActionable: Object.keys(submitMap).length > 0
6413
6664
  });
6414
6665
  this.emit("pending-prompt", state);
6415
6666
  return state;
@@ -6920,9 +7171,13 @@ var PromptResponder = class {
6920
7171
  };
6921
7172
  }
6922
7173
  const isQuestionPrompt = state.kind === "question";
6923
- 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];
6924
7175
  const matchingOpt = state.paneOptions?.find((o) => String(o.number) === String(optionNumber));
6925
- 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;
6926
7181
  if (isQuestionPrompt) {
6927
7182
  if (state.paneOptions && state.paneOptions.length > 0 && !matchingOpt) {
6928
7183
  return {
@@ -6980,9 +7235,10 @@ var PromptResponder = class {
6980
7235
  };
6981
7236
  }
6982
7237
  let liveSemanticKey = null;
7238
+ let liveCandidate = null;
6983
7239
  try {
6984
- const snapshot = await this.paneObserver.captureSnapshot();
6985
- const postCapturePrompt = this.detector.getPendingPrompt(promptId);
7240
+ let snapshot = await this.paneObserver.captureSnapshot();
7241
+ let postCapturePrompt = this.detector.getPendingPrompt(promptId);
6986
7242
  if (postCapturePrompt !== state || !postCapturePrompt.submitMap || !postCapturePrompt.submitMap[optionNumber]) {
6987
7243
  return {
6988
7244
  ok: false,
@@ -6999,7 +7255,35 @@ var PromptResponder = class {
6999
7255
  deliveryPhase: "none"
7000
7256
  };
7001
7257
  }
7002
- 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
+ }
7003
7287
  if (!live) {
7004
7288
  if (detectActivePromptKind(snapshot) === null) {
7005
7289
  this.detector.resolvePrompt(promptId);
@@ -7017,6 +7301,7 @@ var PromptResponder = class {
7017
7301
  deliveryPhase
7018
7302
  };
7019
7303
  }
7304
+ liveCandidate = live;
7020
7305
  liveSemanticKey = live ? buildApprovalSemanticKey(live) : null;
7021
7306
  } catch (error) {
7022
7307
  return {
@@ -7026,15 +7311,55 @@ var PromptResponder = class {
7026
7311
  deliveryPhase
7027
7312
  };
7028
7313
  }
7029
- if (liveSemanticKey !== state.paneSemanticKey) {
7314
+ if (!liveCandidate || !liveCandidate.submitMap?.[optionNumber]?.length) {
7030
7315
  this.detector.resolvePrompt(promptId);
7031
7316
  return {
7032
7317
  ok: false,
7033
7318
  reason: "prompt-superseded",
7034
- details: "active chooser semantics differ from the mobile prompt",
7319
+ details: "active chooser does not allow actionable submission for this option",
7035
7320
  deliveryPhase
7036
7321
  };
7037
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
+ }
7038
7363
  deliveryPhase = "option_attempted";
7039
7364
  if (isNamedKey(key)) {
7040
7365
  await this.sendKey(target, key);
@@ -9101,6 +9426,7 @@ var McpServer = class _McpServer {
9101
9426
  questionText,
9102
9427
  questionOptions,
9103
9428
  structuredQuestions,
9429
+ optionsTruncated: Boolean(structuredQuestions?.[0]?.optionsTruncated),
9104
9430
  startedAt: Date.now(),
9105
9431
  intentCreatedAt: (/* @__PURE__ */ new Date()).toISOString()
9106
9432
  };
@@ -9128,7 +9454,19 @@ var McpServer = class _McpServer {
9128
9454
  });
9129
9455
  return;
9130
9456
  }
9131
- 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);
9132
9470
  const emitted = this.approvalDetector.emitHookQuestionPrompt(pendingCall, e1rec.promptId);
9133
9471
  if (!emitted) {
9134
9472
  this.e1PromptRaises.delete(e1rec.promptId);
@@ -10568,7 +10906,7 @@ var McpServer = class _McpServer {
10568
10906
  } else if (optionResult.reason === "prompt-expired" || optionResult.reason === "prompt-superseded") {
10569
10907
  message = "The question prompt on desktop has expired or changed. Please review on desktop.";
10570
10908
  } else if (optionResult.reason === "prompt-probe-failed") {
10571
- 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.";
10572
10910
  } else if (optionResult.reason === "no-tmux-target" || optionResult.reason === "tmux-failed") {
10573
10911
  message = optionResult.details || "Desktop terminal session is unreachable. Please respond directly on desktop.";
10574
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.103",
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": {