hammoc 1.6.0 → 1.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (227) hide show
  1. package/README.md +6 -4
  2. package/package.json +2 -2
  3. package/packages/client/dist/assets/{agentExampleHighlight-ltj9ce0U.js → agentExampleHighlight-Du5I7v6o.js} +1 -1
  4. package/packages/client/dist/assets/{commandTokenHighlight-ji_ViMb4.js → commandTokenHighlight-Dfo51Aof.js} +1 -1
  5. package/packages/client/dist/assets/{index-B-DiRGuz.js → index-4l4YwxEs.js} +1 -1
  6. package/packages/client/dist/assets/index-CVy582lU.css +32 -0
  7. package/packages/client/dist/assets/{index-BT4RIi0U.js → index-CqAHxPRw.js} +556 -510
  8. package/packages/client/dist/assets/{snippetTokenHighlight-CP3v4o2g.js → snippetTokenHighlight-26DWljQA.js} +1 -1
  9. package/packages/client/dist/index.html +2 -2
  10. package/packages/client/dist/sw.js +2 -2
  11. package/packages/server/dist/_dbg_agent_rows.d.ts +2 -0
  12. package/packages/server/dist/_dbg_agent_rows.d.ts.map +1 -0
  13. package/packages/server/dist/_dbg_agent_rows.js +60 -0
  14. package/packages/server/dist/_dbg_agent_rows.js.map +1 -0
  15. package/packages/server/dist/_dbg_flicker.d.ts +2 -0
  16. package/packages/server/dist/_dbg_flicker.d.ts.map +1 -0
  17. package/packages/server/dist/_dbg_flicker.js +38 -0
  18. package/packages/server/dist/_dbg_flicker.js.map +1 -0
  19. package/packages/server/dist/_harness_cli.d.ts +2 -0
  20. package/packages/server/dist/_harness_cli.d.ts.map +1 -0
  21. package/packages/server/dist/_harness_cli.js +81 -0
  22. package/packages/server/dist/_harness_cli.js.map +1 -0
  23. package/packages/server/dist/_harness_provcard.d.ts +2 -0
  24. package/packages/server/dist/_harness_provcard.d.ts.map +1 -0
  25. package/packages/server/dist/_harness_provcard.js +67 -0
  26. package/packages/server/dist/_harness_provcard.js.map +1 -0
  27. package/packages/server/dist/_replay_dump.d.ts +2 -0
  28. package/packages/server/dist/_replay_dump.d.ts.map +1 -0
  29. package/packages/server/dist/_replay_dump.js +62 -0
  30. package/packages/server/dist/_replay_dump.js.map +1 -0
  31. package/packages/server/dist/app.d.ts.map +1 -1
  32. package/packages/server/dist/app.js +6 -0
  33. package/packages/server/dist/app.js.map +1 -1
  34. package/packages/server/dist/config/index.d.ts.map +1 -1
  35. package/packages/server/dist/config/index.js.map +1 -1
  36. package/packages/server/dist/controllers/projectController.d.ts.map +1 -1
  37. package/packages/server/dist/controllers/projectController.js +7 -1
  38. package/packages/server/dist/controllers/projectController.js.map +1 -1
  39. package/packages/server/dist/controllers/sessionController.d.ts.map +1 -1
  40. package/packages/server/dist/controllers/sessionController.js +16 -4
  41. package/packages/server/dist/controllers/sessionController.js.map +1 -1
  42. package/packages/server/dist/controllers/systemBrowseController.d.ts +33 -0
  43. package/packages/server/dist/controllers/systemBrowseController.d.ts.map +1 -0
  44. package/packages/server/dist/controllers/systemBrowseController.js +105 -0
  45. package/packages/server/dist/controllers/systemBrowseController.js.map +1 -0
  46. package/packages/server/dist/handlers/streamCallbacks.d.ts +4 -0
  47. package/packages/server/dist/handlers/streamCallbacks.d.ts.map +1 -1
  48. package/packages/server/dist/handlers/streamCallbacks.js +21 -8
  49. package/packages/server/dist/handlers/streamCallbacks.js.map +1 -1
  50. package/packages/server/dist/handlers/websocket.d.ts +45 -3
  51. package/packages/server/dist/handlers/websocket.d.ts.map +1 -1
  52. package/packages/server/dist/handlers/websocket.js +367 -95
  53. package/packages/server/dist/handlers/websocket.js.map +1 -1
  54. package/packages/server/dist/index.js +2 -0
  55. package/packages/server/dist/index.js.map +1 -1
  56. package/packages/server/dist/locales/en/server.json +17 -0
  57. package/packages/server/dist/locales/es/server.json +17 -0
  58. package/packages/server/dist/locales/ja/server.json +17 -0
  59. package/packages/server/dist/locales/ko/server.json +17 -0
  60. package/packages/server/dist/locales/pt/server.json +17 -0
  61. package/packages/server/dist/locales/zh-CN/server.json +17 -0
  62. package/packages/server/dist/routes/preferences.d.ts.map +1 -1
  63. package/packages/server/dist/routes/preferences.js +12 -2
  64. package/packages/server/dist/routes/preferences.js.map +1 -1
  65. package/packages/server/dist/routes/systemBrowse.d.ts +17 -0
  66. package/packages/server/dist/routes/systemBrowse.d.ts.map +1 -0
  67. package/packages/server/dist/routes/systemBrowse.js +22 -0
  68. package/packages/server/dist/routes/systemBrowse.js.map +1 -0
  69. package/packages/server/dist/services/bmadStatusService.d.ts.map +1 -1
  70. package/packages/server/dist/services/bmadStatusService.js +18 -7
  71. package/packages/server/dist/services/bmadStatusService.js.map +1 -1
  72. package/packages/server/dist/services/chatEngine.d.ts +81 -0
  73. package/packages/server/dist/services/chatEngine.d.ts.map +1 -0
  74. package/packages/server/dist/services/chatEngine.js +2 -0
  75. package/packages/server/dist/services/chatEngine.js.map +1 -0
  76. package/packages/server/dist/services/chatEngineFactory.d.ts +15 -0
  77. package/packages/server/dist/services/chatEngineFactory.d.ts.map +1 -0
  78. package/packages/server/dist/services/chatEngineFactory.js +28 -0
  79. package/packages/server/dist/services/chatEngineFactory.js.map +1 -0
  80. package/packages/server/dist/services/chatService.d.ts +34 -26
  81. package/packages/server/dist/services/chatService.d.ts.map +1 -1
  82. package/packages/server/dist/services/chatService.js +43 -103
  83. package/packages/server/dist/services/chatService.js.map +1 -1
  84. package/packages/server/dist/services/cliChatEngine.d.ts +198 -0
  85. package/packages/server/dist/services/cliChatEngine.d.ts.map +1 -0
  86. package/packages/server/dist/services/cliChatEngine.js +2977 -0
  87. package/packages/server/dist/services/cliChatEngine.js.map +1 -0
  88. package/packages/server/dist/services/cliGridCards.d.ts +87 -0
  89. package/packages/server/dist/services/cliGridCards.d.ts.map +1 -0
  90. package/packages/server/dist/services/cliGridCards.js +298 -0
  91. package/packages/server/dist/services/cliGridCards.js.map +1 -0
  92. package/packages/server/dist/services/cliGridRegion.d.ts +44 -0
  93. package/packages/server/dist/services/cliGridRegion.d.ts.map +1 -0
  94. package/packages/server/dist/services/cliGridRegion.js +94 -0
  95. package/packages/server/dist/services/cliGridRegion.js.map +1 -0
  96. package/packages/server/dist/services/cliModalDetect.d.ts +299 -0
  97. package/packages/server/dist/services/cliModalDetect.d.ts.map +1 -0
  98. package/packages/server/dist/services/cliModalDetect.js +583 -0
  99. package/packages/server/dist/services/cliModalDetect.js.map +1 -0
  100. package/packages/server/dist/services/cliScreenCache.d.ts +33 -0
  101. package/packages/server/dist/services/cliScreenCache.d.ts.map +1 -0
  102. package/packages/server/dist/services/cliScreenCache.js +48 -0
  103. package/packages/server/dist/services/cliScreenCache.js.map +1 -0
  104. package/packages/server/dist/services/cliScreenModel.d.ts +141 -0
  105. package/packages/server/dist/services/cliScreenModel.d.ts.map +1 -0
  106. package/packages/server/dist/services/cliScreenModel.js +196 -0
  107. package/packages/server/dist/services/cliScreenModel.js.map +1 -0
  108. package/packages/server/dist/services/cliScreenStallWatchdog.d.ts +41 -0
  109. package/packages/server/dist/services/cliScreenStallWatchdog.d.ts.map +1 -0
  110. package/packages/server/dist/services/cliScreenStallWatchdog.js +45 -0
  111. package/packages/server/dist/services/cliScreenStallWatchdog.js.map +1 -0
  112. package/packages/server/dist/services/cliSessionPool.d.ts +97 -0
  113. package/packages/server/dist/services/cliSessionPool.d.ts.map +1 -0
  114. package/packages/server/dist/services/cliSessionPool.js +278 -0
  115. package/packages/server/dist/services/cliSessionPool.js.map +1 -0
  116. package/packages/server/dist/services/cliSpinnerProgress.d.ts +41 -0
  117. package/packages/server/dist/services/cliSpinnerProgress.d.ts.map +1 -0
  118. package/packages/server/dist/services/cliSpinnerProgress.js +131 -0
  119. package/packages/server/dist/services/cliSpinnerProgress.js.map +1 -0
  120. package/packages/server/dist/services/fileRewind.d.ts +46 -0
  121. package/packages/server/dist/services/fileRewind.d.ts.map +1 -0
  122. package/packages/server/dist/services/fileRewind.js +77 -0
  123. package/packages/server/dist/services/fileRewind.js.map +1 -0
  124. package/packages/server/dist/services/historyParser.d.ts +11 -0
  125. package/packages/server/dist/services/historyParser.d.ts.map +1 -1
  126. package/packages/server/dist/services/historyParser.js +10 -8
  127. package/packages/server/dist/services/historyParser.js.map +1 -1
  128. package/packages/server/dist/services/imageStorageService.d.ts +12 -0
  129. package/packages/server/dist/services/imageStorageService.d.ts.map +1 -1
  130. package/packages/server/dist/services/imageStorageService.js +22 -0
  131. package/packages/server/dist/services/imageStorageService.js.map +1 -1
  132. package/packages/server/dist/services/projectService.d.ts +13 -1
  133. package/packages/server/dist/services/projectService.d.ts.map +1 -1
  134. package/packages/server/dist/services/projectService.js +32 -0
  135. package/packages/server/dist/services/projectService.js.map +1 -1
  136. package/packages/server/dist/services/queueService.d.ts +1 -0
  137. package/packages/server/dist/services/queueService.d.ts.map +1 -1
  138. package/packages/server/dist/services/queueService.js +13 -3
  139. package/packages/server/dist/services/queueService.js.map +1 -1
  140. package/packages/server/dist/services/rateLimitProbeService.d.ts +15 -0
  141. package/packages/server/dist/services/rateLimitProbeService.d.ts.map +1 -1
  142. package/packages/server/dist/services/rateLimitProbeService.js +48 -19
  143. package/packages/server/dist/services/rateLimitProbeService.js.map +1 -1
  144. package/packages/server/dist/services/streamHandler.d.ts +8 -1
  145. package/packages/server/dist/services/streamHandler.d.ts.map +1 -1
  146. package/packages/server/dist/services/streamHandler.js +12 -5
  147. package/packages/server/dist/services/streamHandler.js.map +1 -1
  148. package/packages/server/dist/services/systemBrowseService.d.ts +91 -0
  149. package/packages/server/dist/services/systemBrowseService.d.ts.map +1 -0
  150. package/packages/server/dist/services/systemBrowseService.js +372 -0
  151. package/packages/server/dist/services/systemBrowseService.js.map +1 -0
  152. package/packages/server/dist/services/workspaceContext.d.ts +24 -0
  153. package/packages/server/dist/services/workspaceContext.d.ts.map +1 -0
  154. package/packages/server/dist/services/workspaceContext.js +109 -0
  155. package/packages/server/dist/services/workspaceContext.js.map +1 -0
  156. package/packages/server/dist/snippets/qa-review +2 -0
  157. package/packages/server/dist/utils/backgroundBlock.d.ts +29 -0
  158. package/packages/server/dist/utils/backgroundBlock.d.ts.map +1 -0
  159. package/packages/server/dist/utils/backgroundBlock.js +33 -0
  160. package/packages/server/dist/utils/backgroundBlock.js.map +1 -0
  161. package/packages/server/dist/utils/backgroundTaskTracker.d.ts +20 -0
  162. package/packages/server/dist/utils/backgroundTaskTracker.d.ts.map +1 -0
  163. package/packages/server/dist/utils/backgroundTaskTracker.js +40 -0
  164. package/packages/server/dist/utils/backgroundTaskTracker.js.map +1 -0
  165. package/packages/server/dist/utils/bundledBinaryModelSupport.d.ts +7 -0
  166. package/packages/server/dist/utils/bundledBinaryModelSupport.d.ts.map +1 -1
  167. package/packages/server/dist/utils/bundledBinaryModelSupport.js +9 -4
  168. package/packages/server/dist/utils/bundledBinaryModelSupport.js.map +1 -1
  169. package/packages/server/dist/utils/cliDebugLog.d.ts +40 -0
  170. package/packages/server/dist/utils/cliDebugLog.d.ts.map +1 -0
  171. package/packages/server/dist/utils/cliDebugLog.js +71 -0
  172. package/packages/server/dist/utils/cliDebugLog.js.map +1 -0
  173. package/packages/server/dist/utils/cliEngineUtils.d.ts +35 -0
  174. package/packages/server/dist/utils/cliEngineUtils.d.ts.map +1 -0
  175. package/packages/server/dist/utils/cliEngineUtils.js +38 -0
  176. package/packages/server/dist/utils/cliEngineUtils.js.map +1 -0
  177. package/packages/server/dist/utils/effortUtils.d.ts.map +1 -1
  178. package/packages/server/dist/utils/effortUtils.js +5 -3
  179. package/packages/server/dist/utils/effortUtils.js.map +1 -1
  180. package/packages/server/dist/utils/trailingThrottle.d.ts +21 -0
  181. package/packages/server/dist/utils/trailingThrottle.d.ts.map +1 -0
  182. package/packages/server/dist/utils/trailingThrottle.js +36 -0
  183. package/packages/server/dist/utils/trailingThrottle.js.map +1 -0
  184. package/packages/server/package.json +61 -59
  185. package/packages/server/resources/internals/image-storage.md +9 -0
  186. package/packages/server/resources/manual/02-chat.md +22 -3
  187. package/packages/server/resources/manual/03-sessions.md +1 -0
  188. package/packages/server/resources/manual/05-projects.md +12 -1
  189. package/packages/server/resources/manual/11-bmad-method-integration.md +2 -0
  190. package/packages/server/resources/manual/13-settings.md +56 -12
  191. package/packages/shared/dist/index.d.ts +3 -0
  192. package/packages/shared/dist/index.d.ts.map +1 -1
  193. package/packages/shared/dist/index.js +6 -0
  194. package/packages/shared/dist/index.js.map +1 -1
  195. package/packages/shared/dist/types/bmadStatus.d.ts +16 -0
  196. package/packages/shared/dist/types/bmadStatus.d.ts.map +1 -1
  197. package/packages/shared/dist/types/bmadStatus.js.map +1 -1
  198. package/packages/shared/dist/types/preferences.d.ts +14 -2
  199. package/packages/shared/dist/types/preferences.d.ts.map +1 -1
  200. package/packages/shared/dist/types/preferences.js +1 -0
  201. package/packages/shared/dist/types/preferences.js.map +1 -1
  202. package/packages/shared/dist/types/project.d.ts +7 -1
  203. package/packages/shared/dist/types/project.d.ts.map +1 -1
  204. package/packages/shared/dist/types/project.js.map +1 -1
  205. package/packages/shared/dist/types/sdk.d.ts +123 -11
  206. package/packages/shared/dist/types/sdk.d.ts.map +1 -1
  207. package/packages/shared/dist/types/sdk.js +64 -19
  208. package/packages/shared/dist/types/sdk.js.map +1 -1
  209. package/packages/shared/dist/types/streaming.d.ts +2 -2
  210. package/packages/shared/dist/types/streaming.d.ts.map +1 -1
  211. package/packages/shared/dist/types/streaming.js.map +1 -1
  212. package/packages/shared/dist/types/systemBrowse.d.ts +120 -0
  213. package/packages/shared/dist/types/systemBrowse.d.ts.map +1 -0
  214. package/packages/shared/dist/types/systemBrowse.js +57 -0
  215. package/packages/shared/dist/types/systemBrowse.js.map +1 -0
  216. package/packages/shared/dist/types/websocket.d.ts +39 -0
  217. package/packages/shared/dist/types/websocket.d.ts.map +1 -1
  218. package/packages/shared/dist/utils/interruptFiller.d.ts +30 -0
  219. package/packages/shared/dist/utils/interruptFiller.d.ts.map +1 -0
  220. package/packages/shared/dist/utils/interruptFiller.js +38 -0
  221. package/packages/shared/dist/utils/interruptFiller.js.map +1 -0
  222. package/packages/shared/dist/utils/toolResultContent.d.ts +22 -0
  223. package/packages/shared/dist/utils/toolResultContent.d.ts.map +1 -0
  224. package/packages/shared/dist/utils/toolResultContent.js +29 -0
  225. package/packages/shared/dist/utils/toolResultContent.js.map +1 -0
  226. package/scripts/run-integration-test.mjs +362 -362
  227. package/packages/client/dist/assets/index-DyNJ5jEW.css +0 -32
@@ -0,0 +1,583 @@
1
+ /**
2
+ * CLI modal/usage-limit grid detectors (Epic 37 — Story 37.4)
3
+ *
4
+ * Pure readers for the permission dialog (Story 32.6), the AskUserQuestion selection
5
+ * modal (Story 32.8), and the subscription usage-limit notice — moved here, *input
6
+ * source switched from a linear ANSI-stripped buffer to the settled screen grid*
7
+ * produced by the headless screen model (Story 37.1). This is the final extraction
8
+ * surface to leave the linear path: Stories 37.2/37.3 already moved the spinner
9
+ * progress reader (`cliSpinnerProgress`), and once these modal/limit consumers read
10
+ * the grid, the linear `stripAnsiForDetect` + rolling-buffer skeleton is deleted (37.4).
11
+ *
12
+ * Why the grid matters here (the 32.8 box-chrome fix): claude paints the modal with
13
+ * *box-drawing borders* (┌─┐ │ └─┘) and *in-place overwrites*. A linear buffer has no
14
+ * 2D coordinates, so box glyphs fused into option labels ("│"-laden / "──────"-stretched
15
+ * rows) and same-cell redraws concatenated. The screen grid *applies* claude's cursor
16
+ * moves, so **each option lands on its own row** — a per-row read keeps every label
17
+ * intact and fusion is structurally impossible. The per-row `stripBoxChrome` still trims
18
+ * the left/right border cells, but rows no longer fuse, so the label body survives.
19
+ *
20
+ * Two input shapes, same settled grid:
21
+ * - *Existence* detectors (`detectPermissionDialog` / `detectUsageLimit` /
22
+ * `detectQuestionModal`) and the verb/sentence scrapes (`extractToolName` /
23
+ * `extractPromptSentence`) run line-spanning regex → they take `readScreenText()`
24
+ * (the grid joined with '\n').
25
+ * - *Row-structure* parsers (`parseQuestionModal` / `parsePrecedingText`) read the
26
+ * grid ROWS (`readGrid()` → string[]) directly — reading rows is the whole point of
27
+ * the box-chrome fix.
28
+ *
29
+ * Pure by construction (no node-pty / no engine state), so the screen-less unit tests
30
+ * exercise them with hand-built grid rows — the pattern Stories 37.2/37.3 established
31
+ * with `readSpinnerProgress`. Still version-fragile (a TUI revision could reword the
32
+ * footers/phrases or change the box layout); the grid removes the *fusion / box-chrome*
33
+ * failure mode, not the wording fragility.
34
+ *
35
+ * @see docs/stories/37.4.story.md
36
+ * [Source: docs/prd/epic-37-cli-terminal-emulation.md#story-374]
37
+ */
38
+ import { liveFooterRows, liveFooterText } from './cliGridRegion.js';
39
+ import { parseGridCards } from './cliGridCards.js';
40
+ /** "Chat about this" is auto-appended to every AskUserQuestion modal — a marker unique to
41
+ * it (absent from the permission dialog and other selection menus), used to disambiguate. */
42
+ const CLI_QUESTION_AFFORDANCE_RE = /Chat about this/i;
43
+ /** The terminal box-drawing / block range (U+2500–U+259F: ─ │ ┌ ▏ …). */
44
+ const BOX_CHROME_RE = /[─-▟]/g;
45
+ /**
46
+ * Conservative permission-dialog matcher. Requires a permission-specific phrase
47
+ * AND the fully-rendered footer ("Esc to cancel" / "Tab to amend") so a half-drawn
48
+ * dialog, a spinner, or the echoed prompt can never match — a false positive would
49
+ * inject a stray Enter/Esc and corrupt the session. On the grid a half-drawn dialog
50
+ * simply has no footer row yet, so the AND-of-footer naturally withholds detection
51
+ * until the modal is fully painted. The dialog chrome renders in English regardless of
52
+ * the model's reply language (observed); a future TUI revision could change these
53
+ * strings (documented version-fragility).
54
+ */
55
+ export function detectPermissionDialog(text) {
56
+ const hasPermPhrase = /Yes,\s*allow all edits/i.test(text) ||
57
+ /Do you want to (?:create|make|write|edit|update|apply|run|execute|read|proceed|allow)/i.test(text);
58
+ const hasFooter = /Esc\b[^\n]{0,16}\bcancel\b/i.test(text) || /Tab\b[^\n]{0,16}\bamend\b/i.test(text);
59
+ return hasPermPhrase && hasFooter;
60
+ }
61
+ /**
62
+ * Detect the subscription **usage-limit exhaustion** notice on the screen and return its
63
+ * scraped sentence (else null). Why this exists: the interactive TUI prints "You've hit your
64
+ * weekly limit · resets 1am (Asia/Seoul)" ONLY on screen — it is NEVER written to the session
65
+ * JSONL — so the JSONL watch (how every other turn-completion is detected) never sees it and the
66
+ * turn would otherwise hang forever waiting for an `end_turn` that never comes. Detecting it lets
67
+ * the engine fail fast with the exact message claude showed (including the reset time).
68
+ *
69
+ * Conservative by construction so it can never stop a healthy turn:
70
+ * - requires an exhaustion verb (hit/reached/exceeded) OR "<window> limit reached",
71
+ * - requires an explicit window qualifier (weekly / 5-hour / daily / usage / session),
72
+ * - requires a nearby "reset" clause (the real notice always has one),
73
+ * - and explicitly EXCLUDES the still-usable percentage warning ("used 97% of your weekly
74
+ * limit") — at 97% generation continues, so that must NOT stop the turn.
75
+ * The call site adds the OAuth-usage corroboration + POST-INJECTION gating (preserved in the
76
+ * engine dispatch). Version-fragile (a TUI revision could reword these) — the same documented
77
+ * constraint as the permission/question detectors, which also read screen *state*.
78
+ */
79
+ export function detectUsageLimit(text) {
80
+ const m = text.match(/(?:hit|reached|exceeded)\s+your\s+(?:weekly|5-hour|daily|usage|session)\s+limit\b[^\n]{0,60}|(?:weekly|usage|5-hour|daily|session)\s+limit\s+(?:reached|exceeded)\b[^\n]{0,60}/i);
81
+ if (!m)
82
+ return null;
83
+ if (/\bused\s+\d+\s*%/i.test(m[0]))
84
+ return null; // percentage warning — still usable, don't stop
85
+ if (!/\breset/i.test(m[0]))
86
+ return null; // require the reset clause for confidence
87
+ return m[0].replace(/\s{2,}/g, ' ').trim().slice(0, 160);
88
+ }
89
+ /**
90
+ * Detect the transient server-side **rate-limit** error on screen — DISTINCT from the weekly usage cap.
91
+ * claude prints "API Error: Server is temporarily limiting requests (not your usage limit) · Rate limited"
92
+ * ONLY on the PTY (never the JSONL), so without detection the turn hangs on the spinner waiting for an
93
+ * end_turn that never comes. Returns the scraped sentence (else null). No OAuth corroboration needed (this
94
+ * is an explicit error string, not the ambiguous usage-cap prose) — but require BOTH the distinctive phrase
95
+ * AND the "Rate limited" tag so a stray mention can't trip it. Version-fragile like the other screen-state
96
+ * detectors. The caller gates it POST-INJECTION (so a pre-injection repaint can't false-fire).
97
+ */
98
+ export function detectRateLimit(text) {
99
+ if (!/Server is temporarily limiting requests/i.test(text))
100
+ return null;
101
+ if (!/Rate limited/i.test(text))
102
+ return null;
103
+ const m = text.match(/(?:API Error:\s*)?Server is temporarily limiting requests[^\n]{0,80}/i);
104
+ return m ? m[0].replace(/\s{2,}/g, ' ').trim().slice(0, 160) : null;
105
+ }
106
+ /**
107
+ * Best-effort tool name from the dialog's question verb (screen scrape — low
108
+ * fidelity; the structured tool name is not in the JSONL until after approval).
109
+ */
110
+ export function extractToolName(text) {
111
+ const verb = (text.match(/Do you want to (\w+)/i)?.[1] ?? '').toLowerCase();
112
+ if (/^(create|write|make)$/.test(verb))
113
+ return 'Write';
114
+ if (/^(edit|update|apply|modify|change)$/.test(verb))
115
+ return 'Edit';
116
+ if (/^(run|execute)$/.test(verb))
117
+ return 'Bash';
118
+ if (/^(read|view)$/.test(verb))
119
+ return 'Read';
120
+ if (/^(fetch|access)$/.test(verb))
121
+ return 'WebFetch';
122
+ // Secondary hint: the tool header line "● Write(…)".
123
+ return text.match(/[●·]\s*([A-Z][a-zA-Z]+)\s*\(/)?.[1] ?? 'Tool';
124
+ }
125
+ /** Best-effort human-readable prompt sentence (the dialog's own words). */
126
+ export function extractPromptSentence(text) {
127
+ return (text.match(/Do you want to [^?\n]{1,160}\?/i)?.[0] ?? 'Claude is requesting tool permission').trim();
128
+ }
129
+ /**
130
+ * Conservative AskUserQuestion-modal matcher (Story 32.8). Requires the selection footer
131
+ * ("Enter to select" + "↑/↓ to navigate") AND the auto-appended "Chat about this"
132
+ * affordance — together unique to the question modal and absent from a permission dialog
133
+ * ("Do you want to…" / "Yes, allow all edits", which has no list navigation) and from
134
+ * ordinary output, so neither can false-trigger a stray keypress. `detectPermissionDialog`
135
+ * is checked first and is mutually exclusive (it needs a permission phrase this modal lacks;
136
+ * this needs "to navigate" the dialog lacks), so the two never cross-fire. Version-fragile
137
+ * (a TUI revision could reword these) — the documented constrained surface.
138
+ */
139
+ export function detectQuestionModal(text) {
140
+ const hasNavFooter = /Enter\b[^\n]{0,12}\bselect\b/i.test(text) && /to\s+navigate/i.test(text);
141
+ return hasNavFooter && CLI_QUESTION_AFFORDANCE_RE.test(text);
142
+ }
143
+ /** Index of the LAST grid row matching `re`, or -1. (The footer sits at the bottom of the modal.) */
144
+ function lastRowMatching(rows, re) {
145
+ for (let i = rows.length - 1; i >= 0; i--)
146
+ if (re.test(rows[i]))
147
+ return i;
148
+ return -1;
149
+ }
150
+ /** Strip box-drawing / block chrome from a row, collapse runs to one space, trim (a chrome-only
151
+ * row collapses to ''). Each option is its OWN grid row, so this only removes the border cells —
152
+ * the label body no longer fuses with a neighbour the way the linear buffer fused them (32.8). */
153
+ function stripBoxChrome(s) {
154
+ return s.replace(BOX_CHROME_RE, ' ').replace(/\s{2,}/g, ' ').trim();
155
+ }
156
+ /**
157
+ * The modal region (header ballot-box row down to just above the footer) and its header row, or
158
+ * null when no selection footer is on the grid. Shared by every question reader so they bound the
159
+ * scan to the rendered box identically — lead-in prose far above the box cannot leak into a scrape.
160
+ */
161
+ function questionModalRegion(rows) {
162
+ const footerIdx = lastRowMatching(rows, /to\s+navigate/i);
163
+ if (footerIdx < 0)
164
+ return null;
165
+ const region = rows.slice(0, footerIdx);
166
+ const headerIdx = region.findIndex((r) => /[☐☒]/.test(r));
167
+ const modalRows = headerIdx >= 0 ? region.slice(headerIdx) : region;
168
+ return { region, headerIdx, modalRows };
169
+ }
170
+ /**
171
+ * Scrape the question text + multiSelect flag + options from the modal region rows — the body
172
+ * common to BOTH the single-question modal (Story 32.8) and one tab of a multi-question modal
173
+ * (ISSUE-99). NO single-question guard here (a tabbed modal legitimately has >1 header ballot box);
174
+ * the guard lives in `parseQuestionModal`. `header` (when known) is excluded from the question-text
175
+ * candidates so the short tab label never masquerades as the full question.
176
+ *
177
+ * The grid renders **each option on its own row**, so the box-chrome that used to fuse into labels
178
+ * is confined to each row's border cells and `stripBoxChrome` removes it cleanly. Crucially the
179
+ * scrape order equals the grid row order (top-to-bottom), which equals the ↓-count used to drive
180
+ * the answer, so the option↔index mapping is self-consistent (row order IS the navigation index).
181
+ */
182
+ function scrapeQuestionBody(modalRows, header) {
183
+ const multiSelect = modalRows.some((r) => /\[\s*[✔x ]?\s*\]/.test(r)); // [ ] / [✔] ⇒ multiSelect
184
+ // A numbered row opens an option; the indented prose row(s) claude paints directly BELOW the label,
185
+ // up to the next numbered row, are that option's DESCRIPTION (실측 2026-06-17: the TUI renders
186
+ // "1. 빨강 / <indent>빨간색을 선호합니다." for EVERY option, not just the highlighted one). The earlier
187
+ // scrape read only the label row and silently dropped these description rows, so the web card showed
188
+ // bare labels while the richer text sat right there on screen — the "선택지가 단순" gap. Last
189
+ // occurrence wins per number, then ordered by number; one label per row so a per-row match suffices.
190
+ const byNum = new Map();
191
+ let currentNum = null;
192
+ for (const r of modalRows) {
193
+ const m = r.match(/(\d{1,2})\.\s+(?:\[[✔x ]?\s*\]\s*)?(.*\S)/);
194
+ if (m) {
195
+ currentNum = parseInt(m[1], 10);
196
+ byNum.set(currentNum, { label: stripBoxChrome(m[2]), desc: [] });
197
+ }
198
+ else if (currentNum !== null) {
199
+ // Continuation row under the open option → its description. Drop chrome-only rows (empty after
200
+ // stripBoxChrome) and the auto-appended affordance rows so neither leaks into the description.
201
+ const extra = stripBoxChrome(r);
202
+ if (extra && !/^(?:Type something|Chat about this)\.?$/i.test(extra))
203
+ byNum.get(currentNum).desc.push(extra);
204
+ }
205
+ }
206
+ const options = [...byNum.entries()]
207
+ .sort((a, b) => a[0] - b[0])
208
+ .map(([, v]) => ({ label: v.label, description: v.desc.join(' ').trim().slice(0, 500) }))
209
+ // Drop rows that were ONLY box-drawing chrome (empty label) and the auto-appended affordance rows
210
+ // — none of these are real answer options. Omit an empty description so the option stays `{label}`.
211
+ .filter((o) => o.label.length > 0)
212
+ .filter((o) => !/^Type something\.?$/i.test(o.label) && !/^Chat about this\.?$/i.test(o.label))
213
+ .map((o) => ({ label: o.label, ...(o.description ? { description: o.description } : {}) }));
214
+ if (options.length === 0)
215
+ return null;
216
+ // Question = the last meaningful row between the header tab and the first numbered option
217
+ // (best-effort; it may not end in '?' — e.g. "Which pets? Choose any."). Strip tab/cursor glyphs
218
+ // and drop the header line itself.
219
+ const firstOptOffset = modalRows.findIndex((r) => /\d{1,2}\.\s/.test(r));
220
+ const preOptionRows = firstOptOffset > 0 ? modalRows.slice(0, firstOptOffset) : [];
221
+ const question = (preOptionRows
222
+ .map((l) => stripBoxChrome(l.replace(/[←→✔☐☒❯]/g, '')))
223
+ .filter((l) => l && l !== header)
224
+ .pop() ??
225
+ header ??
226
+ 'Claude is asking a question').trim();
227
+ return { question, multiSelect, options };
228
+ }
229
+ /**
230
+ * Scrape one question + its options from a SINGLE-question modal's screen ROWS (Story 32.8 — low
231
+ * fidelity, the documented constraint; Story 37.4 — reads the settled grid rows instead of a linear
232
+ * buffer).
233
+ *
234
+ * Returns null when it cannot build a usable *single*-question structure — no options, or a
235
+ * **multi-question** tabbed modal (more than one header ballot-box tab) which this single-round-trip
236
+ * reader does not drive. ISSUE-99 added a separate per-tab path (`countQuestionTabs` +
237
+ * `parseQuestionTabBody`) for the multi-question case; THIS function and its guard are unchanged so
238
+ * the single-question path stays byte-for-byte identical.
239
+ */
240
+ export function parseQuestionModal(rows) {
241
+ const r = questionModalRegion(rows);
242
+ if (!r)
243
+ return null;
244
+ const { region, headerIdx, modalRows } = r;
245
+ // Multi-question modals show >1 ballot-box tab in the header row (☐ Q1 ☐ Q2 ✔ Submit) — not a
246
+ // single round-trip, so guard rather than half-answer (the multi-question driver handles those).
247
+ if ((modalRows.join('').match(/[☐☒]/g) || []).length > 1)
248
+ return null;
249
+ const headerRow = headerIdx >= 0 ? region[headerIdx] : undefined;
250
+ const headerMatch = headerRow?.match(/[☐☒]\s*([^✔→\n]+?)(?:\s{2,}|$)/);
251
+ const header = headerMatch ? headerMatch[1].trim() : undefined;
252
+ const body = scrapeQuestionBody(modalRows, header);
253
+ if (!body)
254
+ return null;
255
+ return { question: body.question, header, multiSelect: body.multiSelect, options: body.options };
256
+ }
257
+ /**
258
+ * Count the question tabs (header ballot boxes ☐/☒) in a detected AskUserQuestion modal (ISSUE-99).
259
+ * claude renders a single question as one ballot box (`☐ Color`) and a **multi-question** modal as a
260
+ * tab bar with one box per question plus a Submit tab (`← ☐ Color ☐ Size ✔ Submit →`). So the
261
+ * count is the branch signal: 1 ⇒ the single-round-trip `parseQuestionModal` path; >1 ⇒ the tabbed
262
+ * multi-question driver. 0 ⇒ no header (a confirm-style menu / not a question modal). Pure — counts
263
+ * the ballot glyphs on the header row only (option checkboxes are `[ ]`, a distinct glyph). */
264
+ export function countQuestionTabs(rows) {
265
+ const r = questionModalRegion(rows);
266
+ if (!r || r.headerIdx < 0)
267
+ return 0;
268
+ return (r.region[r.headerIdx].match(/[☐☒]/g) || []).length;
269
+ }
270
+ /**
271
+ * The ordered question header labels from a multi-question tab bar (ISSUE-99). For
272
+ * `← ☐ Color ☐ Size ✔ Submit →` this returns `['Color', 'Size']` — each label is the text after
273
+ * a ballot box up to the next box / `✔` Submit / arrow. The Submit tab is excluded (it is not a
274
+ * question). The labels are positional: the i-th label is the i-th tab the driver visits in order,
275
+ * so the engine attaches `headers[i]` to the question it scrapes on tab i. Pure. */
276
+ export function parseQuestionTabHeaders(rows) {
277
+ const r = questionModalRegion(rows);
278
+ if (!r || r.headerIdx < 0)
279
+ return [];
280
+ const headerRow = r.region[r.headerIdx];
281
+ const labels = [];
282
+ const re = /[☐☒]\s*([^☐☒✔→←\n]+)/g;
283
+ let m;
284
+ while ((m = re.exec(headerRow)) !== null) {
285
+ const label = stripBoxChrome(m[1]).trim();
286
+ if (label && !/^Submit$/i.test(label))
287
+ labels.push(label);
288
+ }
289
+ return labels;
290
+ }
291
+ /**
292
+ * Scrape ONE tab of a multi-question modal — the active question's text + multiSelect + options
293
+ * (ISSUE-99). The tabbed modal shows only the active question's body below the tab bar, so this is
294
+ * the same body scrape as the single-question path but WITHOUT the single-question guard (the tab
295
+ * bar legitimately carries >1 ballot box). The header label is NOT read here (it is supplied
296
+ * positionally by `parseQuestionTabHeaders`), so a multi-box tab bar row is harmless — it is not a
297
+ * numbered option and is never the last pre-option row (the question text is). Returns null when the
298
+ * tab has no real options (e.g. a half-painted frame). Pure. */
299
+ export function parseQuestionTabBody(rows) {
300
+ const r = questionModalRegion(rows);
301
+ if (!r)
302
+ return null;
303
+ return scrapeQuestionBody(r.modalRows, undefined);
304
+ }
305
+ /** A confirm-style choice menu — e.g. claude's "resume full session vs summary" prompt shown when
306
+ * resuming a large/old session. It differs from the AskUserQuestion modal in two ways: the footer
307
+ * reads "Enter to confirm · Esc to cancel" (not "↑↓ to navigate · Enter to select") and there is
308
+ * no ballot-box header tab. It is still a single-select numbered list, so it returns the SAME
309
+ * `ParsedQuestion` shape (multiSelect:false) and the existing web card round-trip + the
310
+ * `buildQuestionKeys` driver (↓×index + Enter) handle it verbatim — the boot-stage gate hands a
311
+ * detected one to the same card instead of Esc-cancelling it (Story 37.6 follow-up). Kept as a
312
+ * SEPARATE parser so the AskUserQuestion path stays byte-for-byte untouched (regression-0). */
313
+ const CLI_CONFIRM_FOOTER_RE = /Enter\b[^\n]{0,16}\bconfirm\b/i;
314
+ export function parseConfirmChoiceMenu(rows) {
315
+ // AND-gate: require the live confirm footer alongside the numbered rows (same conservative spirit
316
+ // as the other detectors — a quoted "Enter to confirm" in scrollback prose, with no numbered
317
+ // options below it, must not read as a live menu).
318
+ const footerIdx = lastRowMatching(rows, CLI_CONFIRM_FOOTER_RE);
319
+ if (footerIdx < 0)
320
+ return null;
321
+ // Live-menu gate (false-positive guard, 실측 2026-06-12): a REAL confirm menu occupies the input
322
+ // area at the bottom of the screen, so its footer is the LAST meaningful row — nothing renders
323
+ // below it. If ANY non-blank row follows the footer, the numbered rows are quoted SCROLLBACK:
324
+ // transcript prose/lists that merely contain a "1." "2." sequence and the literal "Enter to
325
+ // confirm" phrase (e.g. THIS feature discussed in the very session, repainted on resume). A
326
+ // footer-phrase match alone is far too weak — ordinary chat ("press Enter to confirm", numbered
327
+ // steps) trips it; requiring the footer to be the last painted row is what isolates a live menu.
328
+ const below = rows.slice(footerIdx + 1);
329
+ if (below.some((r) => r.trim().length > 0))
330
+ return null;
331
+ const region = rows.slice(0, footerIdx);
332
+ // Numbered option rows ("❯ 1. …" / " 2. …"), one per row; last label wins per number, ordered by
333
+ // number. The optional leading "❯" is the highlight cursor on the current row.
334
+ const byNum = new Map();
335
+ for (const r of region) {
336
+ const m = r.match(/^\s*[❯>]?\s*(\d{1,2})\.\s+(.*\S)/);
337
+ if (m)
338
+ byNum.set(parseInt(m[1], 10), stripBoxChrome(m[2]));
339
+ }
340
+ const options = [...byNum.entries()]
341
+ .sort((a, b) => a[0] - b[0])
342
+ .map((e) => e[1])
343
+ .filter((l) => l.length > 0)
344
+ .map((label) => ({ label }));
345
+ // A real choice needs at least two options (a lone "1." quoted in scrollback is not a live menu).
346
+ if (options.length < 2)
347
+ return null;
348
+ // Question = the trailing prose row above the first numbered option (best-effort, like
349
+ // parseQuestionModal). The card shows every option regardless, so a weak question is non-fatal.
350
+ const firstOptIdx = region.findIndex((r) => /^\s*[❯>]?\s*\d{1,2}\.\s/.test(r));
351
+ const preOption = firstOptIdx > 0 ? region.slice(0, firstOptIdx) : [];
352
+ const question = (preOption.map((l) => stripBoxChrome(l)).filter((l) => l).pop() ?? 'Claude is asking a question').trim();
353
+ return { question, header: undefined, multiSelect: false, options };
354
+ }
355
+ /**
356
+ * Scrape the assistant prose the TUI rendered ABOVE the question modal (the explanation that
357
+ * leads into the choices), else null. Why this exists (ordering fix): that prose and the
358
+ * AskUserQuestion are flushed to the session JSONL only AFTER the user answers (the whole
359
+ * assistant message lands post-selection — verified), while the question CARD is shown the moment
360
+ * the modal is detected on screen. So reading the prose from the file always puts it AFTER the
361
+ * card ("선택지 누른 뒤에 설명이 나온다"). The screen is the ONLY pre-answer source, so we scrape
362
+ * it and emit it first. Best-effort and lossy (collapsed to one line, may catch nothing) — the
363
+ * turn-end reload replaces it with the authoritative JSONL copy; the caller dedups the matching
364
+ * JSONL block's live re-emit.
365
+ *
366
+ * Reads the same grid rows as parseQuestionModal: it finds where the modal begins (the header
367
+ * ballot-box tab ☐/☒, or the row above the first numbered option) and returns the trailing
368
+ * contiguous block of prose rows above it (box chrome stripped, option/footer/affordance rows
369
+ * excluded). Returns null for a trivial fragment so a bare modal with no lead-in prose never
370
+ * emits scrape noise.
371
+ */
372
+ export function parsePrecedingText(rows) {
373
+ const footerIdx = lastRowMatching(rows, /to\s+navigate/i);
374
+ if (footerIdx < 0)
375
+ return null;
376
+ const region = rows.slice(0, footerIdx);
377
+ let modalStart = region.findIndex((r) => /[☐☒]/.test(r));
378
+ if (modalStart < 0)
379
+ modalStart = region.findIndex((r) => /^\s*1\.\s/.test(r));
380
+ if (modalStart <= 0)
381
+ return null;
382
+ // Use the card classifier so only recognized blocks (● text, ● tool, ⎿ result, ∴/Thought
383
+ // thinking) are considered — spinners, UI chrome, and any other non-block rows are
384
+ // automatically excluded. Only trailing contiguous TEXT cards are lead-in prose.
385
+ const cards = parseGridCards(region.slice(0, modalStart));
386
+ const prose = [];
387
+ for (let i = cards.length - 1; i >= 0; i--) {
388
+ if (cards[i].kind === 'text')
389
+ prose.unshift(cards[i].text);
390
+ else
391
+ break;
392
+ }
393
+ const text = prose.join(' ').trim();
394
+ return text.length < 4 ? null : text.slice(0, 2000);
395
+ }
396
+ /**
397
+ * Scrape the assistant prose rendered ABOVE a *permission* dialog (Story 32.6) — the sibling of
398
+ * `parsePrecedingText` for the permission path (Story 37.9, the same "[본문, 선택지]" ordering fix:
399
+ * claude writes the gated tool's whole assistant message — explanatory prose included — to the JSONL
400
+ * only AFTER the decision, so the screen is the only pre-decision source).
401
+ *
402
+ * The permission dialog has no "to navigate" footer; its gated tool renders as a `● Tool(…)` card
403
+ * with the "Create file / Do you want to… / 1. Yes / 2. … / Esc to cancel" chrome below it, and the
404
+ * explanatory prose sits ABOVE that tool card. So the region above the dialog footer is classified
405
+ * with the single-source `parseGridCards`, and the prose = the trailing contiguous run of TEXT cards
406
+ * immediately before the gated (last) tool card. Returns null when there is no lead-in prose.
407
+ * Best-effort / lossy like its sibling — the turn-end reload replaces it with the canonical block.
408
+ */
409
+ export function parsePrecedingPermissionText(rows) {
410
+ const footerIdx = lastRowMatching(rows, /Esc\b[^\n]{0,16}\bcancel\b|Tab\b[^\n]{0,16}\bamend\b/i);
411
+ const region = footerIdx >= 0 ? rows.slice(0, footerIdx) : rows;
412
+ const cards = parseGridCards(region);
413
+ // The gated tool is the LAST tool card (the dialog paints it just above the prompt). The prose is
414
+ // the contiguous TEXT cards right before it; with no tool card at all, consider every card.
415
+ let lastToolIdx = -1;
416
+ for (let i = cards.length - 1; i >= 0; i--) {
417
+ if (cards[i].kind === 'tool') {
418
+ lastToolIdx = i;
419
+ break;
420
+ }
421
+ }
422
+ const before = lastToolIdx >= 0 ? cards.slice(0, lastToolIdx) : cards;
423
+ const prose = [];
424
+ for (let i = before.length - 1; i >= 0; i--) {
425
+ if (before[i].kind === 'text')
426
+ prose.unshift(before[i].text);
427
+ else
428
+ break;
429
+ }
430
+ const text = prose.join(' ').trim();
431
+ // Sub-word floor: a real short lead-in must survive; only a stray glyph is dropped.
432
+ return text.length < 4 ? null : text.slice(0, 2000);
433
+ }
434
+ /**
435
+ * Story 37.5 — permission-mode control (the *write* application of the same settled grid).
436
+ *
437
+ * CLI permission modes that map 1:1 onto claude's Shift+Tab (`CSI Z`) cycle, **in cycle order**:
438
+ * a normal session cycles `default → accept edits on → plan mode on → auto mode on → (wrap)
439
+ * default` (empirically captured, claude v2.1.162). The forward step count from current→target
440
+ * is `(targetIdx - curIdx + N) % N`.
441
+ *
442
+ * `auto` is claude's classifier mode ("auto mode on"), a DISTINCT mode from `acceptEdits`
443
+ * ("accept edits on") — they are separate `--permission-mode` values. Earlier code wrongly read
444
+ * "auto mode on" as `bypassPermissions`; that conflation is fixed here.
445
+ *
446
+ * Two values sit OFF this cycle, so `permissionModeCycleIndex` returns -1 and the engine routes
447
+ * them to the store-only / next-spawn `--permission-mode` path instead of a live closed loop:
448
+ * - `bypassPermissions` — claude deliberately keeps it out of a normal session's cycle (it
449
+ * surfaces in the cycle only once a session is explicitly STARTED in it). We never drive to it
450
+ * live; selecting Bypass applies on the NEXT spawn via `--permission-mode bypassPermissions`.
451
+ * - `dontAsk` — headless-only, never on the cycle.
452
+ *
453
+ * version-fragile: label wording / cycle order can shift across claude versions (live-verify item).
454
+ */
455
+ export const CLI_PERMISSION_MODE_CYCLE = ['default', 'acceptEdits', 'plan', 'auto'];
456
+ /** A session STARTED in bypass renders `bypassPermissions` INSIDE its live Shift+Tab cycle —
457
+ * inserted between `plan` and `auto` (empirically captured: `bypass → auto → default → accept
458
+ * edits → plan → bypass`, claude v2.1.177). So for such a session bypass IS a live-drivable
459
+ * target; a session NOT started in bypass keeps it off the cycle (`CLI_PERMISSION_MODE_CYCLE`
460
+ * above) and bypass stays store-only / next-spawn. */
461
+ export const CLI_PERMISSION_MODE_CYCLE_WITH_BYPASS = ['default', 'acceptEdits', 'plan', 'bypassPermissions', 'auto'];
462
+ /** Cycle position of a mode, or -1 when off the Shift+Tab cycle. `includeBypass` selects the
463
+ * bypass-started session's cycle (where `bypassPermissions` is reachable live); the default
464
+ * (false) keeps `bypassPermissions`/`dontAsk` off-cycle → next-spawn `--permission-mode` path. */
465
+ export function permissionModeCycleIndex(mode, includeBypass = false) {
466
+ return (includeBypass ? CLI_PERMISSION_MODE_CYCLE_WITH_BYPASS : CLI_PERMISSION_MODE_CYCLE).indexOf(mode);
467
+ }
468
+ /** Status-row label → Hammoc mode. Each is anchored by its claude label phrase. `default`
469
+ * (normal) has NO label row, so it is read as the *absence* of any of these (see below). */
470
+ const CLI_MODE_LABELS = [
471
+ { re: /accept edits on/i, mode: 'acceptEdits' },
472
+ { re: /plan mode on/i, mode: 'plan' },
473
+ { re: /auto mode on/i, mode: 'auto' },
474
+ // bypassPermissions is off the normal cycle, but a session STARTED in bypass renders this row,
475
+ // so the reader must still recognize it (it just isn't a live-drivable target — see the cycle).
476
+ { re: /bypass permissions on/i, mode: 'bypassPermissions' },
477
+ ];
478
+ /** The mode status row renders the label together with this footer ("… (shift+tab to cycle) ·
479
+ * ← for agents"). Requiring the footer on the SAME row is the AND-gate that keeps a half-drawn
480
+ * frame or a quoted "plan mode on" in conversation prose from being read as the live mode (the
481
+ * same conservative AND-of-footer spirit the permission / question detectors use). */
482
+ const CLI_MODE_CYCLE_FOOTER_RE = /shift\s*\+?\s*tab\s+to\s+cycle/i;
483
+ /**
484
+ * Read claude's current permission mode from the settled screen grid's status row (Story 37.5).
485
+ * Pure: input is the grid rows, output is the Hammoc `PermissionMode`. A row is accepted as the
486
+ * mode status row ONLY when it carries the `shift+tab to cycle` footer (AND-gate); among such
487
+ * rows the bottom-most (freshest rendered) wins, matching `readSpinnerProgress`. When NO mode
488
+ * status row is present the mode is `default` (normal) — but note this is a *weak* signal (it is
489
+ * the absence of a row), so the caller MUST read it from a `flush()`-settled grid: a half-drawn
490
+ * frame whose label has not painted yet would otherwise read transiently as `default`.
491
+ */
492
+ export function readPermissionMode(grid) {
493
+ // Live region only: the mode status row renders in the bottom UI cluster, so a full mode row
494
+ // ("⏸ plan mode on (shift+tab to cycle) …") QUOTED far up in resume-repaint scrollback cannot be
495
+ // read as the live mode when the live screen is `default` (no row). Bottom-most (freshest) wins.
496
+ const region = liveFooterRows(grid);
497
+ for (let y = region.length - 1; y >= 0; y--) {
498
+ const row = region[y];
499
+ if (!CLI_MODE_CYCLE_FOOTER_RE.test(row))
500
+ continue; // AND-gate: only a real mode status row
501
+ for (const { re, mode } of CLI_MODE_LABELS) {
502
+ if (re.test(row))
503
+ return mode;
504
+ }
505
+ }
506
+ // No mode status row (or a footer with no recognized label) = normal = default.
507
+ return 'default';
508
+ }
509
+ /**
510
+ * Is the settled grid mid-GENERATION? The live footer carries claude's active-generation footer
511
+ * ("esc to interrupt") or a spinner token counter ("↓ N tokens"). Scanned over the LIVE FOOTER
512
+ * REGION only (`liveFooterText`) so scrollback prose that quotes these phrases can't poison the
513
+ * verdict (same discipline as `isIdleInputGrid`).
514
+ *
515
+ * Story 37.5 follow-up (owner-confirmed 2026-06-13): the mode status row renders at the very bottom
516
+ * of the reconstructed grid in BOTH idle and generating states (spinner above, input box + mode row
517
+ * below), and Shift+Tab cycles the permission mode live in either. So the permission-mode closed
518
+ * loop drives on this POSITIVE generation signal too — it distinguishes a real generating frame
519
+ * (safe to read the mode + cycle) from an UNKNOWN boot/loading screen (where blind keys stay
520
+ * forbidden). Version-fragile like the rest of this module.
521
+ */
522
+ export function isGeneratingGrid(grid) {
523
+ const footer = liveFooterText(grid);
524
+ // Either arrow direction (`[↑↓]`): claude renders the spinner counter as "↓ N tokens" or
525
+ // "↑ N tokens" depending on phase (실측 2026-06-14). The ↓-only test let a long "↑ N tokens"
526
+ // generation read as NOT generating, which (a) suppressed progress and (b) would let the
527
+ // post-injection modal guard fire mid-generation — both fixed by accepting both arrows.
528
+ return /esc to interrupt/i.test(footer) || /[↑↓]\s*[\d.,]+k?\s*tokens/i.test(footer);
529
+ }
530
+ /**
531
+ * Is the settled grid showing claude's idle INPUT BOX (ready to accept a keypress), as opposed to a
532
+ * mid-generation spinner frame? (Story 37.5 — distinguishes idle from generating; both render the
533
+ * mode status row, but the live Shift+Tab gate treats them separately where it needs to.)
534
+ *
535
+ * Heuristic on a settled grid (half-drawn frames are excluded upstream by `flush()`), scanned over
536
+ * the LIVE FOOTER REGION only (so scrollback prose that quotes these phrases can't poison it):
537
+ * - a generation footer/counter (`isGeneratingGrid`) ⇒ NOT idle;
538
+ * - else the input-box prompt glyph (❯) present ⇒ idle.
539
+ *
540
+ * Left as a shared named helper so Story 37.6's "pre-injection screen classification" (which draws
541
+ * the same input-box-vs-spinner line, and likewise treats `❯` alone as insufficient) can reuse it
542
+ * rather than re-deriving it. This story is self-sufficient and does NOT depend on 37.6.
543
+ */
544
+ export function isIdleInputGrid(grid) {
545
+ if (isGeneratingGrid(grid))
546
+ return false; // active generation footer / spinner counter ⇒ generating
547
+ return /❯/.test(liveFooterText(grid)); // idle input-box marker (live region only — not scrollback)
548
+ }
549
+ /** A LIVE selection footer (bottom-of-box nav/cancel affordance) — the AND-gate partner that keeps
550
+ * quoted scrollback ("❯ 1. Yes" in resume-repaint prose) from reading as a live menu. */
551
+ const CLI_SELECTION_FOOTER_RE = /to\s+navigate|Esc\b[^\n]{0,16}\bcancel\b|↑\s*\/?\s*↓/i;
552
+ /** A numbered option row ("1. …" / " 2. …"), anchored at the row start (one option per grid row). */
553
+ const CLI_NUMBERED_OPTION_RE = /^\s*\d{1,2}\.\s/;
554
+ /**
555
+ * Classify a SETTLED screen grid as input box / selection menu / unknown for the pre-injection
556
+ * readiness gate (Story 37.6). MUST be called on a `flush()`-settled grid (see the absence-signal
557
+ * note above). Pure — no node-pty / no engine state.
558
+ */
559
+ export function classifyPreInjectScreen(grid) {
560
+ // (1) Recognized selection menus/modals, OR a numbered list WITH a live footer (AND-gate). EVERY
561
+ // selection signal here is matched over the LIVE FOOTER region only (`liveFooterText` /
562
+ // `liveFooterRows`), NEVER the whole screen. A resume-repaint can QUOTE a modal's text — a
563
+ // nav/cancel footer, a permission phrase, an AskUserQuestion fixture in a transcript that merely
564
+ // *discusses* these menus — in the scrollback BODY; a whole-screen scan then mistook that for a
565
+ // live modal and WITHHELD injection (boot abort, surfaced to the user as a generic "timeout"). A
566
+ // genuinely live modal renders at the BOTTOM of the screen, so its signature lands in the live
567
+ // region; a quoted one sits up in the body with the idle input box below it. (Until ISSUE-99 only
568
+ // the generic numbered-list path was footer-anchored; the named permission/question detectors
569
+ // still scanned the whole screen — exactly the hole ISSUE-99's modal test fixtures fell through.
570
+ // Now all three are footer-anchored, the same discipline the idle/generating/mode readers use.)
571
+ const footer = liveFooterText(grid);
572
+ const hasNumberedOption = liveFooterRows(grid).some((row) => CLI_NUMBERED_OPTION_RE.test(row));
573
+ const hasSelectionFooter = CLI_SELECTION_FOOTER_RE.test(footer);
574
+ if (detectPermissionDialog(footer) || detectQuestionModal(footer) || (hasNumberedOption && hasSelectionFooter)) {
575
+ return 'selection';
576
+ }
577
+ // (2) A verified idle input box (no selection signature + `❯` present + not mid-generation).
578
+ if (isIdleInputGrid(grid))
579
+ return 'input-box';
580
+ // (3) Neither — do not press blind keys (AC3).
581
+ return 'unknown';
582
+ }
583
+ //# sourceMappingURL=cliModalDetect.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cliModalDetect.js","sourceRoot":"","sources":["../../src/services/cliModalDetect.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * CLI screen cache (Story 37.7, reworked in 37.8) — a session-lifetime snapshot of the
3
+ * current claude TUI screen, used to restore late-joining / refreshed / collapse-expanded
4
+ * browsers in CLI mode.
5
+ *
6
+ * The Story 37.1 headless emulator is per-TURN (spawned with the PTY, disposed at teardown).
7
+ * But the CLI engine is turn-per-process, so between turns there is no emulator and no live
8
+ * PTY. This cache lives at the SESSION scope: while a turn streams, the engine refreshes it
9
+ * with the CURRENT serialized screen on the same ~100ms throttle as the live `cli:screen-frame`
10
+ * (so a late-join mid-turn restores the live screen, not a stale one), and hands off the FINAL
11
+ * serialized screen at teardown. A `session:join` or `cli:request-screen-frame` arriving
12
+ * between turns then has a "current screen" to send.
13
+ *
14
+ * Story 37.8: the entry is now a single SERIALIZED frame string (ANSI/color intact, produced
15
+ * by the screen model's serialize addon), not a plain-text grid — the client restores it via
16
+ * reset()+write(frame), identical to a live frame, so restore and live share one render path.
17
+ *
18
+ * Pure module-level Map. CLI-only; SDK mode never writes it (no PTY / screen model), so `get`
19
+ * returns undefined for SDK sessions and the frame push is naturally a no-op.
20
+ */
21
+ /** Store (or replace) the current serialized screen frame for a session. `ts` records recency.
22
+ * Preserves the existing `stalled` flag — only the watchdog (setCliScreenStall) mutates that. */
23
+ export declare function setCliScreen(sessionId: string, frame: string): void;
24
+ /** Return the cached serialized screen frame for a session, or undefined on a cache miss. */
25
+ export declare function getCliScreen(sessionId: string): string | undefined;
26
+ /** Update the soft screen-stall flag on an EXISTING screen entry. No-op on a cache miss, so a
27
+ * frame-less (leak-prone) entry is never created — a cached screen frame is what defines a session. */
28
+ export declare function setCliScreenStall(sessionId: string, stalled: boolean): void;
29
+ /** Current cached stall flag for a session (false on a miss). Used to resync late joiners. */
30
+ export declare function getCliScreenStall(sessionId: string): boolean;
31
+ /** Drop a session's cached screen (called when its socket room empties — leak guard). */
32
+ export declare function deleteCliScreen(sessionId: string): void;
33
+ //# sourceMappingURL=cliScreenCache.d.ts.map