@agfpd/iapeer 0.4.73 → 0.4.74

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agfpd/iapeer",
3
- "version": "0.4.73",
3
+ "version": "0.4.74",
4
4
  "description": "Run Claude Code and Codex CLI as local background agents that message each other, wake on demand, and share one memory.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "agfpd",
@@ -23,6 +23,23 @@ export interface NumberedModal {
23
23
 
24
24
  const CAP_LINES = 24
25
25
  const CAP_BYTES = 8192
26
+ // How far above the footer to scan for the modal's options — the real AskUserQuestion box (question +
27
+ // pill + N options each with a DESCRIPTION line + an inter-option separator) is a dozen-plus rows.
28
+ const MODAL_MAX_LINES = 40
29
+
30
+ /**
31
+ * Is `line` a claude/codex interactive-SELECT footer — the stable signature of a live numbered picker?
32
+ * AskUserQuestion: "Enter to select · ↑/↓ to navigate · Esc to cancel"; a resume/boot select: "Enter to
33
+ * confirm · Esc to cancel"; codex: "Press enter to confirm or esc to go back". Anchoring on this footer
34
+ * (boris's guidance — устойчивее чем «❯ внизу») is layout-robust: it survives the cursor sitting on ANY
35
+ * option and options being interleaved with description lines. The command-approval breaker's footer
36
+ * ("Esc to cancel · Tab to amend · ctrl+e to explain") deliberately does NOT match (no select/navigate
37
+ * hint) — it is a KNOWN class handled by its own taxonomy, and the structural guards below (a live
38
+ * glyph-selected option + ≥2 options + nothing below the footer) keep this from firing on prose.
39
+ */
40
+ export function isSelectFooter(line: string): boolean {
41
+ return /esc to (?:cancel|go back)/i.test(line) && /(?:enter to |to navigate|to select|to confirm|press enter)/i.test(line)
42
+ }
26
43
  // Unicode box-drawing block (U+2500–U+257F) — modal borders/separators; stripped for PARSING only
27
44
  // (the verbatim `content` keeps them).
28
45
  const BOX_DRAWING = /[─-╿]/g
@@ -65,37 +82,42 @@ function capBlock(block: string[]): string {
65
82
  */
66
83
  export function detectNumberedModal(pane: string, glyph: string): NumberedModal | null {
67
84
  const lines = pane.split(/\r?\n/)
68
- // bottom-most row carrying the select glyph.
69
- let selected = -1
70
- for (let i = 0; i < lines.length; i++) if (lines[i].includes(glyph)) selected = i
71
- if (selected < 0) return null
72
- // it must be a SELECTED numbered option (`glyph … N. …`) — the composer (`glyph <text>`) carries no
73
- // leading digit-dot, so an idle/ready pane and a mere quote-with-composer-below both fail here.
85
+ // 1. FOOTER anchor — the stable signature of a live interactive select (boris: устойчивее «❯ внизу»).
86
+ // The LAST footer line is THIS modal's (a scrolled-off older one sits above). Anchoring here is
87
+ // robust to the cursor sitting on ANY option and to options interleaved with DESCRIPTION lines /
88
+ // separators (the real AskUserQuestion layout — verified against a live 2.1.202 render).
89
+ let footerIdx = -1
90
+ for (let i = 0; i < lines.length; i++) if (isSelectFooter(lines[i])) footerIdx = i
91
+ if (footerIdx < 0) return null
92
+ // 2. COMPOSER-REPLACED guard — a LIVE blocking modal has NOTHING below its footer (the composer is
93
+ // gone). A mere QUOTE of a modal pasted into chat has the ready composer glyph rendered BELOW it, so
94
+ // ANY select glyph after the footer means "not a live block" → reject (the В40 quote class).
95
+ for (let i = footerIdx + 1; i < lines.length; i++) if (lines[i].includes(glyph)) return null
96
+ // 3. OPTIONS — scan a bounded WINDOW above the footer (NOT a contiguous run: the real modal interleaves
97
+ // each option with a wrapped description line and even an inter-option separator, and a blank can sit
98
+ // between the last option and the footer). Require a glyph-SELECTED option (a live cursor is on one)
99
+ // and ≥2 distinct numbered options. option1 = the "1." row CLOSEST to the footer (the modal's own —
100
+ // a stray transcript "1." further up loses to it).
101
+ const windowTop = Math.max(0, footerIdx - MODAL_MAX_LINES)
74
102
  const selectedRe = new RegExp(`${escapeForRegex(glyph)}\\s*\\d+[.)]\\s`)
75
- if (!selectedRe.test(lines[selected])) return null
76
- // The select glyph sits only on the SELECTED option; siblings render WITHOUT it (above and below). Grow
77
- // the contiguous OPTION RUN in BOTH directions from the selected row while each line parses as a
78
- // numbered option (an interleaved blank / non-option line ends the run). This captures every option,
79
- // not just the ones above the cursor.
80
- let optTop = selected
81
- let optBottom = selected
82
- while (optTop - 1 >= 0 && parseOption(lines[optTop - 1], glyph)) optTop--
83
- while (optBottom + 1 < lines.length && parseOption(lines[optBottom + 1], glyph)) optBottom++
84
- // need ≥2 DISTINCT numbered options (a real select, not a lone printed `N.` line).
85
103
  const nums = new Set<number>()
86
104
  let option1 = ''
87
- for (let i = optTop; i <= optBottom; i++) {
105
+ let hasSelected = false
106
+ let firstOptIdx = -1
107
+ for (let i = windowTop; i < footerIdx; i++) {
108
+ if (selectedRe.test(lines[i])) hasSelected = true
88
109
  const opt = parseOption(lines[i], glyph)
89
110
  if (!opt) continue
111
+ if (firstOptIdx < 0) firstOptIdx = i
90
112
  nums.add(opt.num)
91
- if (opt.num === 1 && !option1) option1 = opt.label
113
+ if (opt.num === 1) option1 = opt.label // last (closest-to-footer) wins
92
114
  }
93
- if (nums.size < 2) return null
94
- // Verbatim block shown to the human: the option run + a little context (the question above / a footer
95
- // below), then leading/trailing blank lines trimmed. Bounded by the caps.
96
- const start = Math.max(0, optTop - 3)
97
- const end = Math.min(lines.length - 1, optBottom + 2)
98
- const block = lines.slice(start, end + 1)
115
+ if (!hasSelected) return null // no live cursor on an option → a printed list, not a live select
116
+ if (nums.size < 2) return null // ≥2 options → a real select
117
+ // 4. Verbatim block shown to the human: from a couple of lines above the first option (the question) to
118
+ // the footer, blanks trimmed + capped.
119
+ const cTop = Math.max(windowTop, (firstOptIdx < 0 ? footerIdx : firstOptIdx) - 2)
120
+ const block = lines.slice(cTop, footerIdx + 1)
99
121
  while (block.length && block[0].trim() === '') block.shift()
100
122
  while (block.length && block[block.length - 1].trim() === '') block.pop()
101
123
  return { content: capBlock(block), option1: option1 || '(option 1)' }