hammoc 1.6.0 → 1.7.0

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 (226) hide show
  1. package/README.md +6 -4
  2. package/package.json +2 -2
  3. package/packages/client/dist/assets/{agentExampleHighlight-ltj9ce0U.js → agentExampleHighlight-7Olvqdpf.js} +1 -1
  4. package/packages/client/dist/assets/{commandTokenHighlight-ji_ViMb4.js → commandTokenHighlight-DtKwh4lx.js} +1 -1
  5. package/packages/client/dist/assets/{index-BT4RIi0U.js → index-Bee52CKG.js} +556 -510
  6. package/packages/client/dist/assets/index-CVy582lU.css +32 -0
  7. package/packages/client/dist/assets/{index-B-DiRGuz.js → index-DLHBUw8c.js} +1 -1
  8. package/packages/client/dist/assets/{snippetTokenHighlight-CP3v4o2g.js → snippetTokenHighlight-C8O688f4.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 +2 -0
  47. package/packages/server/dist/handlers/streamCallbacks.d.ts.map +1 -1
  48. package/packages/server/dist/handlers/streamCallbacks.js +17 -4
  49. package/packages/server/dist/handlers/streamCallbacks.js.map +1 -1
  50. package/packages/server/dist/handlers/websocket.d.ts +43 -3
  51. package/packages/server/dist/handlers/websocket.d.ts.map +1 -1
  52. package/packages/server/dist/handlers/websocket.js +362 -92
  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.map +1 -1
  137. package/packages/server/dist/services/queueService.js +9 -3
  138. package/packages/server/dist/services/queueService.js.map +1 -1
  139. package/packages/server/dist/services/rateLimitProbeService.d.ts +15 -0
  140. package/packages/server/dist/services/rateLimitProbeService.d.ts.map +1 -1
  141. package/packages/server/dist/services/rateLimitProbeService.js +48 -19
  142. package/packages/server/dist/services/rateLimitProbeService.js.map +1 -1
  143. package/packages/server/dist/services/streamHandler.d.ts +8 -1
  144. package/packages/server/dist/services/streamHandler.d.ts.map +1 -1
  145. package/packages/server/dist/services/streamHandler.js +12 -5
  146. package/packages/server/dist/services/streamHandler.js.map +1 -1
  147. package/packages/server/dist/services/systemBrowseService.d.ts +91 -0
  148. package/packages/server/dist/services/systemBrowseService.d.ts.map +1 -0
  149. package/packages/server/dist/services/systemBrowseService.js +372 -0
  150. package/packages/server/dist/services/systemBrowseService.js.map +1 -0
  151. package/packages/server/dist/services/workspaceContext.d.ts +24 -0
  152. package/packages/server/dist/services/workspaceContext.d.ts.map +1 -0
  153. package/packages/server/dist/services/workspaceContext.js +109 -0
  154. package/packages/server/dist/services/workspaceContext.js.map +1 -0
  155. package/packages/server/dist/snippets/qa-review +2 -0
  156. package/packages/server/dist/utils/backgroundBlock.d.ts +29 -0
  157. package/packages/server/dist/utils/backgroundBlock.d.ts.map +1 -0
  158. package/packages/server/dist/utils/backgroundBlock.js +33 -0
  159. package/packages/server/dist/utils/backgroundBlock.js.map +1 -0
  160. package/packages/server/dist/utils/backgroundTaskTracker.d.ts +20 -0
  161. package/packages/server/dist/utils/backgroundTaskTracker.d.ts.map +1 -0
  162. package/packages/server/dist/utils/backgroundTaskTracker.js +40 -0
  163. package/packages/server/dist/utils/backgroundTaskTracker.js.map +1 -0
  164. package/packages/server/dist/utils/bundledBinaryModelSupport.d.ts +7 -0
  165. package/packages/server/dist/utils/bundledBinaryModelSupport.d.ts.map +1 -1
  166. package/packages/server/dist/utils/bundledBinaryModelSupport.js +9 -4
  167. package/packages/server/dist/utils/bundledBinaryModelSupport.js.map +1 -1
  168. package/packages/server/dist/utils/cliDebugLog.d.ts +38 -0
  169. package/packages/server/dist/utils/cliDebugLog.d.ts.map +1 -0
  170. package/packages/server/dist/utils/cliDebugLog.js +65 -0
  171. package/packages/server/dist/utils/cliDebugLog.js.map +1 -0
  172. package/packages/server/dist/utils/cliEngineUtils.d.ts +35 -0
  173. package/packages/server/dist/utils/cliEngineUtils.d.ts.map +1 -0
  174. package/packages/server/dist/utils/cliEngineUtils.js +38 -0
  175. package/packages/server/dist/utils/cliEngineUtils.js.map +1 -0
  176. package/packages/server/dist/utils/effortUtils.d.ts.map +1 -1
  177. package/packages/server/dist/utils/effortUtils.js +5 -3
  178. package/packages/server/dist/utils/effortUtils.js.map +1 -1
  179. package/packages/server/dist/utils/trailingThrottle.d.ts +21 -0
  180. package/packages/server/dist/utils/trailingThrottle.d.ts.map +1 -0
  181. package/packages/server/dist/utils/trailingThrottle.js +36 -0
  182. package/packages/server/dist/utils/trailingThrottle.js.map +1 -0
  183. package/packages/server/package.json +61 -59
  184. package/packages/server/resources/internals/image-storage.md +9 -0
  185. package/packages/server/resources/manual/02-chat.md +22 -3
  186. package/packages/server/resources/manual/03-sessions.md +1 -0
  187. package/packages/server/resources/manual/05-projects.md +12 -1
  188. package/packages/server/resources/manual/11-bmad-method-integration.md +2 -0
  189. package/packages/server/resources/manual/13-settings.md +57 -5
  190. package/packages/shared/dist/index.d.ts +3 -0
  191. package/packages/shared/dist/index.d.ts.map +1 -1
  192. package/packages/shared/dist/index.js +6 -0
  193. package/packages/shared/dist/index.js.map +1 -1
  194. package/packages/shared/dist/types/bmadStatus.d.ts +16 -0
  195. package/packages/shared/dist/types/bmadStatus.d.ts.map +1 -1
  196. package/packages/shared/dist/types/bmadStatus.js.map +1 -1
  197. package/packages/shared/dist/types/preferences.d.ts +14 -2
  198. package/packages/shared/dist/types/preferences.d.ts.map +1 -1
  199. package/packages/shared/dist/types/preferences.js +1 -0
  200. package/packages/shared/dist/types/preferences.js.map +1 -1
  201. package/packages/shared/dist/types/project.d.ts +7 -1
  202. package/packages/shared/dist/types/project.d.ts.map +1 -1
  203. package/packages/shared/dist/types/project.js.map +1 -1
  204. package/packages/shared/dist/types/sdk.d.ts +123 -11
  205. package/packages/shared/dist/types/sdk.d.ts.map +1 -1
  206. package/packages/shared/dist/types/sdk.js +64 -19
  207. package/packages/shared/dist/types/sdk.js.map +1 -1
  208. package/packages/shared/dist/types/streaming.d.ts +2 -2
  209. package/packages/shared/dist/types/streaming.d.ts.map +1 -1
  210. package/packages/shared/dist/types/streaming.js.map +1 -1
  211. package/packages/shared/dist/types/systemBrowse.d.ts +120 -0
  212. package/packages/shared/dist/types/systemBrowse.d.ts.map +1 -0
  213. package/packages/shared/dist/types/systemBrowse.js +57 -0
  214. package/packages/shared/dist/types/systemBrowse.js.map +1 -0
  215. package/packages/shared/dist/types/websocket.d.ts +38 -0
  216. package/packages/shared/dist/types/websocket.d.ts.map +1 -1
  217. package/packages/shared/dist/utils/interruptFiller.d.ts +30 -0
  218. package/packages/shared/dist/utils/interruptFiller.d.ts.map +1 -0
  219. package/packages/shared/dist/utils/interruptFiller.js +38 -0
  220. package/packages/shared/dist/utils/interruptFiller.js.map +1 -0
  221. package/packages/shared/dist/utils/toolResultContent.d.ts +22 -0
  222. package/packages/shared/dist/utils/toolResultContent.d.ts.map +1 -0
  223. package/packages/shared/dist/utils/toolResultContent.js +29 -0
  224. package/packages/shared/dist/utils/toolResultContent.js.map +1 -0
  225. package/scripts/run-integration-test.mjs +362 -362
  226. package/packages/client/dist/assets/index-DyNJ5jEW.css +0 -32
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Shared "live footer region" helper for the CLI settled-grid readers (Epic 37).
3
+ *
4
+ * claude's interactive UI — the idle input box, the generation spinner + "↓ N tokens" / "esc to
5
+ * interrupt" footer, the mode status row, and a selection box's nav/cancel footer — all render flush
6
+ * at the BOTTOM of the screen. Restricting the live-state regexes to this region is the "anchor to
7
+ * the live footer, not the scrollback body" discipline shared by the grid readers: it stops a
8
+ * resume-repaint whose prior answer QUOTED any of those markers in the transcript body from
9
+ * masquerading as a live signal.
10
+ *
11
+ * 실측 2026-06-13: a long answer that *discussed* the CLI spinner ("esc to interrupt" / "↓N tokens")
12
+ * was repainted into the scrollback on resume; the old whole-screen scan matched the quoted phrases,
13
+ * read the idle input box as "generating", returned `unknown` from the pre-injection classifier, and
14
+ * the next turn's prompt was withheld → the turn was lost. Anchoring the readers to the bottom region
15
+ * removes that whole class (idle/generation gate, mode-row read, spinner counter).
16
+ *
17
+ * Uses the last NON-EMPTY rows so trailing blank rows below the box don't shrink the window.
18
+ * Version-fragile like the rest of the CLI screen readers (the bottom UI height can shift across
19
+ * claude TUI revisions), but a strict improvement over scanning the whole screen.
20
+ */
21
+ /** How many bottom non-empty rows count as the live UI cluster (box + spinner/footer + status row). */
22
+ export declare const CLI_LIVE_FOOTER_ROWS = 8;
23
+ /** The last non-empty rows of a settled grid — the live UI cluster. Empties are dropped first so a
24
+ * few blank rows below the input box don't push the box out of the window. */
25
+ export declare function liveFooterRows(grid: string[]): string[];
26
+ /** `liveFooterRows` joined with '\n', for the line-spanning existence regexes. */
27
+ export declare function liveFooterText(grid: string[]): string;
28
+ /**
29
+ * The SCROLLBACK BODY — the grid rows strictly ABOVE the live footer CLUSTER (the inverse of
30
+ * `liveFooterRows`). Story 37.9/37.10: the card parser (`parseGridCards`) folds a trailing non-glyph
31
+ * row into the open card, so feeding it the footer would pollute the last card — and, worse, fold the
32
+ * live spinner (whose `Ns`/`↓N tokens` counters change every frame) into a thinking card, making its
33
+ * text churn and re-emit endlessly (실측 2026-06-16).
34
+ *
35
+ * During generation claude renders the footer as a CLUSTER of contiguous rows — the spinner
36
+ * (`↓N tokens`), the input box (`────` / `❯` / `────`), the `esc to interrupt` row, then the mode/effort
37
+ * status row at the very bottom. Cutting at only the BOTTOM-most anchor leaves the spinner (the
38
+ * cluster's TOP) inside the body. So: find the bottom-most anchor, then walk UP through the contiguous
39
+ * cluster (anchor rows + box-rule chrome — claude draws no blank WITHIN it) to the cluster top, and cut
40
+ * there. Anything below (incl. the variable status row) is excluded; a blank or content row above the
41
+ * cluster ends the walk and stays in the body. No anchor at all ⇒ a pure scrollback frame ⇒ all body.
42
+ */
43
+ export declare function scrollbackBodyRows(grid: string[]): string[];
44
+ //# sourceMappingURL=cliGridRegion.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cliGridRegion.d.ts","sourceRoot":"","sources":["../../src/services/cliGridRegion.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,uGAAuG;AACvG,eAAO,MAAM,oBAAoB,IAAI,CAAC;AAEtC;+EAC+E;AAC/E,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAGvD;AAED,kFAAkF;AAClF,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,CAErD;AAcD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAsB3D"}
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Shared "live footer region" helper for the CLI settled-grid readers (Epic 37).
3
+ *
4
+ * claude's interactive UI — the idle input box, the generation spinner + "↓ N tokens" / "esc to
5
+ * interrupt" footer, the mode status row, and a selection box's nav/cancel footer — all render flush
6
+ * at the BOTTOM of the screen. Restricting the live-state regexes to this region is the "anchor to
7
+ * the live footer, not the scrollback body" discipline shared by the grid readers: it stops a
8
+ * resume-repaint whose prior answer QUOTED any of those markers in the transcript body from
9
+ * masquerading as a live signal.
10
+ *
11
+ * 실측 2026-06-13: a long answer that *discussed* the CLI spinner ("esc to interrupt" / "↓N tokens")
12
+ * was repainted into the scrollback on resume; the old whole-screen scan matched the quoted phrases,
13
+ * read the idle input box as "generating", returned `unknown` from the pre-injection classifier, and
14
+ * the next turn's prompt was withheld → the turn was lost. Anchoring the readers to the bottom region
15
+ * removes that whole class (idle/generation gate, mode-row read, spinner counter).
16
+ *
17
+ * Uses the last NON-EMPTY rows so trailing blank rows below the box don't shrink the window.
18
+ * Version-fragile like the rest of the CLI screen readers (the bottom UI height can shift across
19
+ * claude TUI revisions), but a strict improvement over scanning the whole screen.
20
+ */
21
+ /** How many bottom non-empty rows count as the live UI cluster (box + spinner/footer + status row). */
22
+ export const CLI_LIVE_FOOTER_ROWS = 8;
23
+ /** The last non-empty rows of a settled grid — the live UI cluster. Empties are dropped first so a
24
+ * few blank rows below the input box don't push the box out of the window. */
25
+ export function liveFooterRows(grid) {
26
+ const nonEmpty = grid.filter((row) => row.trim().length > 0);
27
+ return nonEmpty.slice(-CLI_LIVE_FOOTER_ROWS);
28
+ }
29
+ /** `liveFooterRows` joined with '\n', for the line-spanning existence regexes. */
30
+ export function liveFooterText(grid) {
31
+ return liveFooterRows(grid).join('\n');
32
+ }
33
+ /**
34
+ * The bottommost live-footer anchor: claude's generation spinner ("… esc to interrupt" / a
35
+ * "↓/↑ N tokens" counter) or the idle input-box prompt glyph (`❯`). The footer always renders flush
36
+ * at the BOTTOM, so the BOTTOMMOST match is the live footer (a scrollback line that merely *quotes*
37
+ * one of these markers sits above it — the same ISSUE-99 anti-poisoning discipline as the readers).
38
+ */
39
+ const LIVE_FOOTER_ANCHOR_RE = /esc to interrupt|[↑↓]\s*[\d.,]+\s*k?\s*tokens|❯/i;
40
+ /** A pure box-drawing rule row (the input-box top/bottom border `────`, corners, sides) — chrome that
41
+ * is part of the live-footer cluster, never a content card. */
42
+ const BOX_RULE_RE = /^[─╭╮╰╯│]+$/;
43
+ /**
44
+ * The SCROLLBACK BODY — the grid rows strictly ABOVE the live footer CLUSTER (the inverse of
45
+ * `liveFooterRows`). Story 37.9/37.10: the card parser (`parseGridCards`) folds a trailing non-glyph
46
+ * row into the open card, so feeding it the footer would pollute the last card — and, worse, fold the
47
+ * live spinner (whose `Ns`/`↓N tokens` counters change every frame) into a thinking card, making its
48
+ * text churn and re-emit endlessly (실측 2026-06-16).
49
+ *
50
+ * During generation claude renders the footer as a CLUSTER of contiguous rows — the spinner
51
+ * (`↓N tokens`), the input box (`────` / `❯` / `────`), the `esc to interrupt` row, then the mode/effort
52
+ * status row at the very bottom. Cutting at only the BOTTOM-most anchor leaves the spinner (the
53
+ * cluster's TOP) inside the body. So: find the bottom-most anchor, then walk UP through the contiguous
54
+ * cluster (anchor rows + box-rule chrome — claude draws no blank WITHIN it) to the cluster top, and cut
55
+ * there. Anything below (incl. the variable status row) is excluded; a blank or content row above the
56
+ * cluster ends the walk and stays in the body. No anchor at all ⇒ a pure scrollback frame ⇒ all body.
57
+ */
58
+ export function scrollbackBodyRows(grid) {
59
+ let bottom = -1;
60
+ for (let i = grid.length - 1; i >= 0; i--) {
61
+ if (LIVE_FOOTER_ANCHOR_RE.test(grid[i])) {
62
+ bottom = i;
63
+ break;
64
+ }
65
+ }
66
+ if (bottom < 0)
67
+ return grid;
68
+ const isCluster = (r) => LIVE_FOOTER_ANCHOR_RE.test(r) || BOX_RULE_RE.test(r.trim());
69
+ let top = bottom;
70
+ let i = bottom - 1;
71
+ while (i >= 0) {
72
+ if (isCluster(grid[i])) {
73
+ top = i;
74
+ i--;
75
+ continue;
76
+ }
77
+ if (grid[i].trim() === '') {
78
+ // A blank belongs to the cluster only if footer chrome continues ABOVE it (claude pads between
79
+ // the spinner and the input box); a blank with CONTENT above it is a body↔footer separator and
80
+ // stays in the body. Peek past the blank run.
81
+ let j = i - 1;
82
+ while (j >= 0 && grid[j].trim() === '')
83
+ j--;
84
+ if (j >= 0 && isCluster(grid[j])) {
85
+ top = j;
86
+ i = j - 1;
87
+ continue;
88
+ }
89
+ }
90
+ break; // content row (or a separator blank) → the cluster ends here
91
+ }
92
+ return grid.slice(0, top);
93
+ }
94
+ //# sourceMappingURL=cliGridRegion.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cliGridRegion.js","sourceRoot":"","sources":["../../src/services/cliGridRegion.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,uGAAuG;AACvG,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,CAAC;AAEtC;+EAC+E;AAC/E,MAAM,UAAU,cAAc,CAAC,IAAc;IAC3C,MAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAC7D,OAAO,QAAQ,CAAC,KAAK,CAAC,CAAC,oBAAoB,CAAC,CAAC;AAC/C,CAAC;AAED,kFAAkF;AAClF,MAAM,UAAU,cAAc,CAAC,IAAc;IAC3C,OAAO,cAAc,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACzC,CAAC;AAED;;;;;GAKG;AACH,MAAM,qBAAqB,GAAG,kDAAkD,CAAC;AAEjF;gEACgE;AAChE,MAAM,WAAW,GAAG,aAAa,CAAC;AAElC;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,kBAAkB,CAAC,IAAc;IAC/C,IAAI,MAAM,GAAG,CAAC,CAAC,CAAC;IAChB,KAAK,IAAI,CAAC,GAAG,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QAC1C,IAAI,qBAAqB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;YAAC,MAAM,GAAG,CAAC,CAAC;YAAC,MAAM;QAAC,CAAC;IACjE,CAAC;IACD,IAAI,MAAM,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IAC5B,MAAM,SAAS,GAAG,CAAC,CAAS,EAAE,EAAE,CAAC,qBAAqB,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;IAC7F,IAAI,GAAG,GAAG,MAAM,CAAC;IACjB,IAAI,CAAC,GAAG,MAAM,GAAG,CAAC,CAAC;IACnB,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QACd,IAAI,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;YAAC,GAAG,GAAG,CAAC,CAAC;YAAC,CAAC,EAAE,CAAC;YAAC,SAAS;QAAC,CAAC;QACnD,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;YAC1B,+FAA+F;YAC/F,+FAA+F;YAC/F,8CAA8C;YAC9C,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YACd,OAAO,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE;gBAAE,CAAC,EAAE,CAAC;YAC5C,IAAI,CAAC,IAAI,CAAC,IAAI,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;gBAAC,GAAG,GAAG,CAAC,CAAC;gBAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;gBAAC,SAAS;YAAC,CAAC;QACrE,CAAC;QACD,MAAM,CAAC,6DAA6D;IACtE,CAAC;IACD,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;AAC5B,CAAC"}
@@ -0,0 +1,299 @@
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 type { PermissionMode } from '@hammoc/shared';
39
+ /** A single scraped AskUserQuestion choice. Story 32.8 scraped one of these from a single-question
40
+ * modal; ISSUE-99 reuses the same shape per-tab to reconstruct a **multi-question** (tabbed) modal,
41
+ * one ParsedQuestion per tab. */
42
+ export interface ParsedQuestion {
43
+ question: string;
44
+ header?: string;
45
+ multiSelect: boolean;
46
+ /** Each option's label plus the per-option `description` claude paints on the indented row(s)
47
+ * directly below its label (실측 2026-06-17). The description is optional — a confirm-style menu
48
+ * or a bare option carries none. The web card already renders `description` when present. */
49
+ options: Array<{
50
+ label: string;
51
+ description?: string;
52
+ }>;
53
+ }
54
+ /**
55
+ * Conservative permission-dialog matcher. Requires a permission-specific phrase
56
+ * AND the fully-rendered footer ("Esc to cancel" / "Tab to amend") so a half-drawn
57
+ * dialog, a spinner, or the echoed prompt can never match — a false positive would
58
+ * inject a stray Enter/Esc and corrupt the session. On the grid a half-drawn dialog
59
+ * simply has no footer row yet, so the AND-of-footer naturally withholds detection
60
+ * until the modal is fully painted. The dialog chrome renders in English regardless of
61
+ * the model's reply language (observed); a future TUI revision could change these
62
+ * strings (documented version-fragility).
63
+ */
64
+ export declare function detectPermissionDialog(text: string): boolean;
65
+ /**
66
+ * Detect the subscription **usage-limit exhaustion** notice on the screen and return its
67
+ * scraped sentence (else null). Why this exists: the interactive TUI prints "You've hit your
68
+ * weekly limit · resets 1am (Asia/Seoul)" ONLY on screen — it is NEVER written to the session
69
+ * JSONL — so the JSONL watch (how every other turn-completion is detected) never sees it and the
70
+ * turn would otherwise hang forever waiting for an `end_turn` that never comes. Detecting it lets
71
+ * the engine fail fast with the exact message claude showed (including the reset time).
72
+ *
73
+ * Conservative by construction so it can never stop a healthy turn:
74
+ * - requires an exhaustion verb (hit/reached/exceeded) OR "<window> limit reached",
75
+ * - requires an explicit window qualifier (weekly / 5-hour / daily / usage / session),
76
+ * - requires a nearby "reset" clause (the real notice always has one),
77
+ * - and explicitly EXCLUDES the still-usable percentage warning ("used 97% of your weekly
78
+ * limit") — at 97% generation continues, so that must NOT stop the turn.
79
+ * The call site adds the OAuth-usage corroboration + POST-INJECTION gating (preserved in the
80
+ * engine dispatch). Version-fragile (a TUI revision could reword these) — the same documented
81
+ * constraint as the permission/question detectors, which also read screen *state*.
82
+ */
83
+ export declare function detectUsageLimit(text: string): string | null;
84
+ /**
85
+ * Detect the transient server-side **rate-limit** error on screen — DISTINCT from the weekly usage cap.
86
+ * claude prints "API Error: Server is temporarily limiting requests (not your usage limit) · Rate limited"
87
+ * ONLY on the PTY (never the JSONL), so without detection the turn hangs on the spinner waiting for an
88
+ * end_turn that never comes. Returns the scraped sentence (else null). No OAuth corroboration needed (this
89
+ * is an explicit error string, not the ambiguous usage-cap prose) — but require BOTH the distinctive phrase
90
+ * AND the "Rate limited" tag so a stray mention can't trip it. Version-fragile like the other screen-state
91
+ * detectors. The caller gates it POST-INJECTION (so a pre-injection repaint can't false-fire).
92
+ */
93
+ export declare function detectRateLimit(text: string): string | null;
94
+ /**
95
+ * Best-effort tool name from the dialog's question verb (screen scrape — low
96
+ * fidelity; the structured tool name is not in the JSONL until after approval).
97
+ */
98
+ export declare function extractToolName(text: string): string;
99
+ /** Best-effort human-readable prompt sentence (the dialog's own words). */
100
+ export declare function extractPromptSentence(text: string): string;
101
+ /**
102
+ * Conservative AskUserQuestion-modal matcher (Story 32.8). Requires the selection footer
103
+ * ("Enter to select" + "↑/↓ to navigate") AND the auto-appended "Chat about this"
104
+ * affordance — together unique to the question modal and absent from a permission dialog
105
+ * ("Do you want to…" / "Yes, allow all edits", which has no list navigation) and from
106
+ * ordinary output, so neither can false-trigger a stray keypress. `detectPermissionDialog`
107
+ * is checked first and is mutually exclusive (it needs a permission phrase this modal lacks;
108
+ * this needs "to navigate" the dialog lacks), so the two never cross-fire. Version-fragile
109
+ * (a TUI revision could reword these) — the documented constrained surface.
110
+ */
111
+ export declare function detectQuestionModal(text: string): boolean;
112
+ /**
113
+ * Scrape one question + its options from a SINGLE-question modal's screen ROWS (Story 32.8 — low
114
+ * fidelity, the documented constraint; Story 37.4 — reads the settled grid rows instead of a linear
115
+ * buffer).
116
+ *
117
+ * Returns null when it cannot build a usable *single*-question structure — no options, or a
118
+ * **multi-question** tabbed modal (more than one header ballot-box tab) which this single-round-trip
119
+ * reader does not drive. ISSUE-99 added a separate per-tab path (`countQuestionTabs` +
120
+ * `parseQuestionTabBody`) for the multi-question case; THIS function and its guard are unchanged so
121
+ * the single-question path stays byte-for-byte identical.
122
+ */
123
+ export declare function parseQuestionModal(rows: string[]): ParsedQuestion | null;
124
+ /**
125
+ * Count the question tabs (header ballot boxes ☐/☒) in a detected AskUserQuestion modal (ISSUE-99).
126
+ * claude renders a single question as one ballot box (`☐ Color`) and a **multi-question** modal as a
127
+ * tab bar with one box per question plus a Submit tab (`← ☐ Color ☐ Size ✔ Submit →`). So the
128
+ * count is the branch signal: 1 ⇒ the single-round-trip `parseQuestionModal` path; >1 ⇒ the tabbed
129
+ * multi-question driver. 0 ⇒ no header (a confirm-style menu / not a question modal). Pure — counts
130
+ * the ballot glyphs on the header row only (option checkboxes are `[ ]`, a distinct glyph). */
131
+ export declare function countQuestionTabs(rows: string[]): number;
132
+ /**
133
+ * The ordered question header labels from a multi-question tab bar (ISSUE-99). For
134
+ * `← ☐ Color ☐ Size ✔ Submit →` this returns `['Color', 'Size']` — each label is the text after
135
+ * a ballot box up to the next box / `✔` Submit / arrow. The Submit tab is excluded (it is not a
136
+ * question). The labels are positional: the i-th label is the i-th tab the driver visits in order,
137
+ * so the engine attaches `headers[i]` to the question it scrapes on tab i. Pure. */
138
+ export declare function parseQuestionTabHeaders(rows: string[]): string[];
139
+ /**
140
+ * Scrape ONE tab of a multi-question modal — the active question's text + multiSelect + options
141
+ * (ISSUE-99). The tabbed modal shows only the active question's body below the tab bar, so this is
142
+ * the same body scrape as the single-question path but WITHOUT the single-question guard (the tab
143
+ * bar legitimately carries >1 ballot box). The header label is NOT read here (it is supplied
144
+ * positionally by `parseQuestionTabHeaders`), so a multi-box tab bar row is harmless — it is not a
145
+ * numbered option and is never the last pre-option row (the question text is). Returns null when the
146
+ * tab has no real options (e.g. a half-painted frame). Pure. */
147
+ export declare function parseQuestionTabBody(rows: string[]): {
148
+ question: string;
149
+ multiSelect: boolean;
150
+ options: Array<{
151
+ label: string;
152
+ }>;
153
+ } | null;
154
+ export declare function parseConfirmChoiceMenu(rows: string[]): ParsedQuestion | null;
155
+ /**
156
+ * Scrape the assistant prose the TUI rendered ABOVE the question modal (the explanation that
157
+ * leads into the choices), else null. Why this exists (ordering fix): that prose and the
158
+ * AskUserQuestion are flushed to the session JSONL only AFTER the user answers (the whole
159
+ * assistant message lands post-selection — verified), while the question CARD is shown the moment
160
+ * the modal is detected on screen. So reading the prose from the file always puts it AFTER the
161
+ * card ("선택지 누른 뒤에 설명이 나온다"). The screen is the ONLY pre-answer source, so we scrape
162
+ * it and emit it first. Best-effort and lossy (collapsed to one line, may catch nothing) — the
163
+ * turn-end reload replaces it with the authoritative JSONL copy; the caller dedups the matching
164
+ * JSONL block's live re-emit.
165
+ *
166
+ * Reads the same grid rows as parseQuestionModal: it finds where the modal begins (the header
167
+ * ballot-box tab ☐/☒, or the row above the first numbered option) and returns the trailing
168
+ * contiguous block of prose rows above it (box chrome stripped, option/footer/affordance rows
169
+ * excluded). Returns null for a trivial fragment so a bare modal with no lead-in prose never
170
+ * emits scrape noise.
171
+ */
172
+ export declare function parsePrecedingText(rows: string[]): string | null;
173
+ /**
174
+ * Scrape the assistant prose rendered ABOVE a *permission* dialog (Story 32.6) — the sibling of
175
+ * `parsePrecedingText` for the permission path (Story 37.9, the same "[본문, 선택지]" ordering fix:
176
+ * claude writes the gated tool's whole assistant message — explanatory prose included — to the JSONL
177
+ * only AFTER the decision, so the screen is the only pre-decision source).
178
+ *
179
+ * The permission dialog has no "to navigate" footer; its gated tool renders as a `● Tool(…)` card
180
+ * with the "Create file / Do you want to… / 1. Yes / 2. … / Esc to cancel" chrome below it, and the
181
+ * explanatory prose sits ABOVE that tool card. So the region above the dialog footer is classified
182
+ * with the single-source `parseGridCards`, and the prose = the trailing contiguous run of TEXT cards
183
+ * immediately before the gated (last) tool card. Returns null when there is no lead-in prose.
184
+ * Best-effort / lossy like its sibling — the turn-end reload replaces it with the canonical block.
185
+ */
186
+ export declare function parsePrecedingPermissionText(rows: string[]): string | null;
187
+ /**
188
+ * Story 37.5 — permission-mode control (the *write* application of the same settled grid).
189
+ *
190
+ * CLI permission modes that map 1:1 onto claude's Shift+Tab (`CSI Z`) cycle, **in cycle order**:
191
+ * a normal session cycles `default → accept edits on → plan mode on → auto mode on → (wrap)
192
+ * default` (empirically captured, claude v2.1.162). The forward step count from current→target
193
+ * is `(targetIdx - curIdx + N) % N`.
194
+ *
195
+ * `auto` is claude's classifier mode ("auto mode on"), a DISTINCT mode from `acceptEdits`
196
+ * ("accept edits on") — they are separate `--permission-mode` values. Earlier code wrongly read
197
+ * "auto mode on" as `bypassPermissions`; that conflation is fixed here.
198
+ *
199
+ * Two values sit OFF this cycle, so `permissionModeCycleIndex` returns -1 and the engine routes
200
+ * them to the store-only / next-spawn `--permission-mode` path instead of a live closed loop:
201
+ * - `bypassPermissions` — claude deliberately keeps it out of a normal session's cycle (it
202
+ * surfaces in the cycle only once a session is explicitly STARTED in it). We never drive to it
203
+ * live; selecting Bypass applies on the NEXT spawn via `--permission-mode bypassPermissions`.
204
+ * - `dontAsk` — headless-only, never on the cycle.
205
+ *
206
+ * version-fragile: label wording / cycle order can shift across claude versions (live-verify item).
207
+ */
208
+ export declare const CLI_PERMISSION_MODE_CYCLE: PermissionMode[];
209
+ /** A session STARTED in bypass renders `bypassPermissions` INSIDE its live Shift+Tab cycle —
210
+ * inserted between `plan` and `auto` (empirically captured: `bypass → auto → default → accept
211
+ * edits → plan → bypass`, claude v2.1.177). So for such a session bypass IS a live-drivable
212
+ * target; a session NOT started in bypass keeps it off the cycle (`CLI_PERMISSION_MODE_CYCLE`
213
+ * above) and bypass stays store-only / next-spawn. */
214
+ export declare const CLI_PERMISSION_MODE_CYCLE_WITH_BYPASS: PermissionMode[];
215
+ /** Cycle position of a mode, or -1 when off the Shift+Tab cycle. `includeBypass` selects the
216
+ * bypass-started session's cycle (where `bypassPermissions` is reachable live); the default
217
+ * (false) keeps `bypassPermissions`/`dontAsk` off-cycle → next-spawn `--permission-mode` path. */
218
+ export declare function permissionModeCycleIndex(mode: PermissionMode, includeBypass?: boolean): number;
219
+ /**
220
+ * Read claude's current permission mode from the settled screen grid's status row (Story 37.5).
221
+ * Pure: input is the grid rows, output is the Hammoc `PermissionMode`. A row is accepted as the
222
+ * mode status row ONLY when it carries the `shift+tab to cycle` footer (AND-gate); among such
223
+ * rows the bottom-most (freshest rendered) wins, matching `readSpinnerProgress`. When NO mode
224
+ * status row is present the mode is `default` (normal) — but note this is a *weak* signal (it is
225
+ * the absence of a row), so the caller MUST read it from a `flush()`-settled grid: a half-drawn
226
+ * frame whose label has not painted yet would otherwise read transiently as `default`.
227
+ */
228
+ export declare function readPermissionMode(grid: string[]): PermissionMode;
229
+ /**
230
+ * Is the settled grid mid-GENERATION? The live footer carries claude's active-generation footer
231
+ * ("esc to interrupt") or a spinner token counter ("↓ N tokens"). Scanned over the LIVE FOOTER
232
+ * REGION only (`liveFooterText`) so scrollback prose that quotes these phrases can't poison the
233
+ * verdict (same discipline as `isIdleInputGrid`).
234
+ *
235
+ * Story 37.5 follow-up (owner-confirmed 2026-06-13): the mode status row renders at the very bottom
236
+ * of the reconstructed grid in BOTH idle and generating states (spinner above, input box + mode row
237
+ * below), and Shift+Tab cycles the permission mode live in either. So the permission-mode closed
238
+ * loop drives on this POSITIVE generation signal too — it distinguishes a real generating frame
239
+ * (safe to read the mode + cycle) from an UNKNOWN boot/loading screen (where blind keys stay
240
+ * forbidden). Version-fragile like the rest of this module.
241
+ */
242
+ export declare function isGeneratingGrid(grid: string[]): boolean;
243
+ /**
244
+ * Is the settled grid showing claude's idle INPUT BOX (ready to accept a keypress), as opposed to a
245
+ * mid-generation spinner frame? (Story 37.5 — distinguishes idle from generating; both render the
246
+ * mode status row, but the live Shift+Tab gate treats them separately where it needs to.)
247
+ *
248
+ * Heuristic on a settled grid (half-drawn frames are excluded upstream by `flush()`), scanned over
249
+ * the LIVE FOOTER REGION only (so scrollback prose that quotes these phrases can't poison it):
250
+ * - a generation footer/counter (`isGeneratingGrid`) ⇒ NOT idle;
251
+ * - else the input-box prompt glyph (❯) present ⇒ idle.
252
+ *
253
+ * Left as a shared named helper so Story 37.6's "pre-injection screen classification" (which draws
254
+ * the same input-box-vs-spinner line, and likewise treats `❯` alone as insufficient) can reuse it
255
+ * rather than re-deriving it. This story is self-sufficient and does NOT depend on 37.6.
256
+ */
257
+ export declare function isIdleInputGrid(grid: string[]): boolean;
258
+ /**
259
+ * Story 37.6 — PRE-INJECTION screen classification (the *read* sibling of 37.4's post-injection
260
+ * detectors). The boot/resume readiness check used to be a single linear test —
261
+ * `bootBuffer.includes('❯')` → inject. But `❯` is a *shared* glyph: claude paints it for the idle
262
+ * input box, for the highlighted row of a selection menu, AND inside the permission dialog. So the
263
+ * marker is *necessary but not sufficient*; a resume frame that lands on a selection menu reads `❯`
264
+ * and the old path injected an Enter into the first option (e.g. `/compact`), losing the prompt and
265
+ * compacting the conversation (실측 2026-06-11).
266
+ *
267
+ * This 3-way classifier of the SETTLED grid removes that ambiguity:
268
+ * - `selection` — a recognized selection menu/modal: the 32.6 permission dialog OR the 32.8
269
+ * question modal (both AND-gated detectors, reused verbatim), OR a generic
270
+ * numbered-option list THAT ALSO carries a live selection footer.
271
+ * - `input-box` — no selection signature AND `isIdleInputGrid` holds (an `❯` is present with no
272
+ * mid-generation spinner). This is the ONLY class that injects (AC1).
273
+ * - `unknown` — neither. The caller presses NO blind key and (at the decisive checkpoint) ends
274
+ * the turn with an explicit error ("모르면 치지 않는다", AC3).
275
+ *
276
+ * The **footer AND-gate is essential** (AC2): a resume-repaint can quote a prior turn's "❯ 1. Yes"
277
+ * / numbered list / table in the scrollback BODY, but the *live* nav/cancel footer renders only at
278
+ * the bottom of an actually-live selection box. Requiring the footer alongside the numbered rows
279
+ * stops quoted scrollback from being mistaken for a live menu — the same half-drawn / quoted-text
280
+ * defense the permission/question detectors already use.
281
+ *
282
+ * Pure by construction (grid rows in, union out) so the screen-less unit tests drive it with
283
+ * hand-built rows. **Settled-grid precondition:** the `input-box`/`selection` distinction partly
284
+ * rests on the *absence* of a row (no footer ⇒ not a live menu), a weak signal that a half-drawn
285
+ * frame can transiently violate, so the CALLER must classify only an `await flush()`-settled grid
286
+ * (37.5 weak-signal discipline). Still version-fragile (the option/footer wording can shift across
287
+ * claude TUI revisions) — the grid removes the *fusion* failure mode, not the wording fragility.
288
+ *
289
+ * @see docs/stories/37.6.story.md
290
+ * [Source: docs/prd/epic-37-cli-terminal-emulation.md#story-376]
291
+ */
292
+ export type PreInjectScreen = 'input-box' | 'selection' | 'unknown';
293
+ /**
294
+ * Classify a SETTLED screen grid as input box / selection menu / unknown for the pre-injection
295
+ * readiness gate (Story 37.6). MUST be called on a `flush()`-settled grid (see the absence-signal
296
+ * note above). Pure — no node-pty / no engine state.
297
+ */
298
+ export declare function classifyPreInjectScreen(grid: string[]): PreInjectScreen;
299
+ //# sourceMappingURL=cliModalDetect.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cliModalDetect.d.ts","sourceRoot":"","sources":["../../src/services/cliModalDetect.ts"],"names":[],"mappings":"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"}