poly-weaver 0.7.1 → 0.8.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 (177) hide show
  1. package/dist/ansi.d.ts +7 -0
  2. package/dist/ansi.d.ts.map +1 -1
  3. package/dist/ansi.js +30 -0
  4. package/dist/ansi.js.map +1 -1
  5. package/dist/app-header.d.ts +16 -0
  6. package/dist/app-header.d.ts.map +1 -0
  7. package/dist/app-header.js +74 -0
  8. package/dist/app-header.js.map +1 -0
  9. package/dist/cli.js +81 -59
  10. package/dist/cli.js.map +1 -1
  11. package/dist/config.d.ts +1 -0
  12. package/dist/config.d.ts.map +1 -1
  13. package/dist/config.js +6 -0
  14. package/dist/config.js.map +1 -1
  15. package/dist/dump/collector.d.ts +2 -1
  16. package/dist/dump/collector.d.ts.map +1 -1
  17. package/dist/dump/collector.js +1 -0
  18. package/dist/dump/collector.js.map +1 -1
  19. package/dist/dump/hotkey.d.ts +9 -30
  20. package/dist/dump/hotkey.d.ts.map +1 -1
  21. package/dist/dump/hotkey.js +15 -158
  22. package/dist/dump/hotkey.js.map +1 -1
  23. package/dist/dump/service.d.ts +3 -2
  24. package/dist/dump/service.d.ts.map +1 -1
  25. package/dist/dump/service.js +4 -1
  26. package/dist/dump/service.js.map +1 -1
  27. package/dist/dump/types.d.ts +5 -0
  28. package/dist/dump/types.d.ts.map +1 -1
  29. package/dist/flow/built-in/default-factory.d.ts +21 -0
  30. package/dist/flow/built-in/default-factory.d.ts.map +1 -0
  31. package/dist/flow/built-in/default-factory.js +155 -0
  32. package/dist/flow/built-in/default-factory.js.map +1 -0
  33. package/dist/flow/built-in/default.d.ts +4 -4
  34. package/dist/flow/built-in/default.d.ts.map +1 -1
  35. package/dist/flow/built-in/default.js +3 -141
  36. package/dist/flow/built-in/default.js.map +1 -1
  37. package/dist/flow/built-in/default.ts +5 -169
  38. package/dist/flow/built-in/why-so-serious-factory.d.ts +16 -0
  39. package/dist/flow/built-in/why-so-serious-factory.d.ts.map +1 -0
  40. package/dist/flow/built-in/why-so-serious-factory.js +187 -0
  41. package/dist/flow/built-in/why-so-serious-factory.js.map +1 -0
  42. package/dist/flow/built-in/why-so-serious.d.ts +3 -2
  43. package/dist/flow/built-in/why-so-serious.d.ts.map +1 -1
  44. package/dist/flow/built-in/why-so-serious.js +3 -164
  45. package/dist/flow/built-in/why-so-serious.js.map +1 -1
  46. package/dist/flow/custom/AUTHORING.md +13 -0
  47. package/dist/flow/custom/load.d.ts +16 -1
  48. package/dist/flow/custom/load.d.ts.map +1 -1
  49. package/dist/flow/custom/load.js +69 -18
  50. package/dist/flow/custom/load.js.map +1 -1
  51. package/dist/flow/dsl.js +1 -1
  52. package/dist/flow/dsl.js.map +1 -1
  53. package/dist/flow/executor.d.ts.map +1 -1
  54. package/dist/flow/executor.js +44 -8
  55. package/dist/flow/executor.js.map +1 -1
  56. package/dist/flow/types.d.ts +20 -2
  57. package/dist/flow/types.d.ts.map +1 -1
  58. package/dist/flow/types.js.map +1 -1
  59. package/dist/flow-editor/tui.d.ts.map +1 -1
  60. package/dist/flow-editor/tui.js +9 -18
  61. package/dist/flow-editor/tui.js.map +1 -1
  62. package/dist/orchestrator.d.ts +43 -0
  63. package/dist/orchestrator.d.ts.map +1 -1
  64. package/dist/orchestrator.js +230 -92
  65. package/dist/orchestrator.js.map +1 -1
  66. package/dist/providers/claude/completion-plan-mode.d.ts.map +1 -1
  67. package/dist/providers/claude/completion-plan-mode.js +5 -2
  68. package/dist/providers/claude/completion-plan-mode.js.map +1 -1
  69. package/dist/providers/claude/tool-tracking.d.ts +1 -0
  70. package/dist/providers/claude/tool-tracking.d.ts.map +1 -1
  71. package/dist/providers/claude/tool-tracking.js +3 -0
  72. package/dist/providers/claude/tool-tracking.js.map +1 -1
  73. package/dist/pty/spawn.d.ts.map +1 -1
  74. package/dist/pty/spawn.js +6 -1
  75. package/dist/pty/spawn.js.map +1 -1
  76. package/dist/pty/types.d.ts +7 -2
  77. package/dist/pty/types.d.ts.map +1 -1
  78. package/dist/resume-tui.d.ts +52 -0
  79. package/dist/resume-tui.d.ts.map +1 -0
  80. package/dist/resume-tui.js +344 -0
  81. package/dist/resume-tui.js.map +1 -0
  82. package/dist/session/flow-snapshot.d.ts +14 -0
  83. package/dist/session/flow-snapshot.d.ts.map +1 -0
  84. package/dist/session/flow-snapshot.js +28 -0
  85. package/dist/session/flow-snapshot.js.map +1 -0
  86. package/dist/session/list.d.ts +16 -0
  87. package/dist/session/list.d.ts.map +1 -0
  88. package/dist/session/list.js +89 -0
  89. package/dist/session/list.js.map +1 -0
  90. package/dist/session/lock.d.ts +26 -0
  91. package/dist/session/lock.d.ts.map +1 -0
  92. package/dist/session/lock.js +95 -0
  93. package/dist/session/lock.js.map +1 -0
  94. package/dist/session/manifest.d.ts +80 -0
  95. package/dist/session/manifest.d.ts.map +1 -0
  96. package/dist/session/manifest.js +2 -0
  97. package/dist/session/manifest.js.map +1 -0
  98. package/dist/session/persistence.d.ts +19 -0
  99. package/dist/session/persistence.d.ts.map +1 -0
  100. package/dist/session/persistence.js +68 -0
  101. package/dist/session/persistence.js.map +1 -0
  102. package/dist/session/process-start-time.d.ts +17 -0
  103. package/dist/session/process-start-time.d.ts.map +1 -0
  104. package/dist/session/process-start-time.js +99 -0
  105. package/dist/session/process-start-time.js.map +1 -0
  106. package/dist/session/restore.d.ts +31 -0
  107. package/dist/session/restore.d.ts.map +1 -0
  108. package/dist/session/restore.js +111 -0
  109. package/dist/session/restore.js.map +1 -0
  110. package/dist/session/resume.d.ts +5 -0
  111. package/dist/session/resume.d.ts.map +1 -0
  112. package/dist/session/resume.js +109 -0
  113. package/dist/session/resume.js.map +1 -0
  114. package/dist/session/session-store.d.ts +43 -0
  115. package/dist/session/session-store.d.ts.map +1 -0
  116. package/dist/session/session-store.js +134 -0
  117. package/dist/session/session-store.js.map +1 -0
  118. package/dist/startup-tui.d.ts +10 -11
  119. package/dist/startup-tui.d.ts.map +1 -1
  120. package/dist/startup-tui.js +25 -108
  121. package/dist/startup-tui.js.map +1 -1
  122. package/dist/status-bar.js +1 -26
  123. package/dist/status-bar.js.map +1 -1
  124. package/dist/terminal/host.d.ts +22 -9
  125. package/dist/terminal/host.d.ts.map +1 -1
  126. package/dist/terminal/host.js +67 -78
  127. package/dist/terminal/host.js.map +1 -1
  128. package/dist/terminal/input-router.d.ts +24 -103
  129. package/dist/terminal/input-router.d.ts.map +1 -1
  130. package/dist/terminal/input-router.js +63 -364
  131. package/dist/terminal/input-router.js.map +1 -1
  132. package/dist/terminal/key-event-source.d.ts +52 -0
  133. package/dist/terminal/key-event-source.d.ts.map +1 -0
  134. package/dist/terminal/key-event-source.js +31 -0
  135. package/dist/terminal/key-event-source.js.map +1 -0
  136. package/dist/terminal/key-to-bytes.d.ts +11 -0
  137. package/dist/terminal/key-to-bytes.d.ts.map +1 -0
  138. package/dist/terminal/key-to-bytes.js +81 -0
  139. package/dist/terminal/key-to-bytes.js.map +1 -0
  140. package/dist/terminal/koffi-loader.d.ts +14 -0
  141. package/dist/terminal/koffi-loader.d.ts.map +1 -1
  142. package/dist/terminal/koffi-loader.js.map +1 -1
  143. package/dist/terminal/render.d.ts +1 -2
  144. package/dist/terminal/render.d.ts.map +1 -1
  145. package/dist/terminal/render.js +1 -30
  146. package/dist/terminal/render.js.map +1 -1
  147. package/dist/terminal/stdin-byte-source.d.ts +27 -0
  148. package/dist/terminal/stdin-byte-source.d.ts.map +1 -0
  149. package/dist/terminal/stdin-byte-source.js +92 -0
  150. package/dist/terminal/stdin-byte-source.js.map +1 -0
  151. package/dist/terminal/win32-console-mode.d.ts.map +1 -1
  152. package/dist/terminal/win32-console-mode.js.map +1 -1
  153. package/dist/terminal/win32-console-source.d.ts +26 -0
  154. package/dist/terminal/win32-console-source.d.ts.map +1 -0
  155. package/dist/terminal/win32-console-source.js +275 -0
  156. package/dist/terminal/win32-console-source.js.map +1 -0
  157. package/dist/terminal/win32-key-translator.d.ts +26 -0
  158. package/dist/terminal/win32-key-translator.d.ts.map +1 -0
  159. package/dist/terminal/win32-key-translator.js +87 -0
  160. package/dist/terminal/win32-key-translator.js.map +1 -0
  161. package/dist/terminal-input.d.ts +10 -0
  162. package/dist/terminal-input.d.ts.map +1 -1
  163. package/dist/terminal-input.js +132 -113
  164. package/dist/terminal-input.js.map +1 -1
  165. package/dist/user/host-curate-prompt.d.ts +2 -1
  166. package/dist/user/host-curate-prompt.d.ts.map +1 -1
  167. package/dist/user/host-curate-prompt.js +13 -11
  168. package/dist/user/host-curate-prompt.js.map +1 -1
  169. package/dist/user/host-prompt.d.ts +4 -1
  170. package/dist/user/host-prompt.d.ts.map +1 -1
  171. package/dist/user/host-prompt.js +24 -6
  172. package/dist/user/host-prompt.js.map +1 -1
  173. package/dist/user/prompt.d.ts +7 -9
  174. package/dist/user/prompt.d.ts.map +1 -1
  175. package/dist/user/prompt.js +14 -23
  176. package/dist/user/prompt.js.map +1 -1
  177. package/package.json +1 -1
@@ -1,14 +1,19 @@
1
+ import type { TerminalKey } from "../terminal-input.js";
1
2
  import type { PtyHandle } from "../pty/types.js";
2
3
  /**
3
- * Active parent UI mode (e.g. user-review prompt). When attached, raw
4
- * stdin chunks are routed to it BEFORE host shortcuts / mouse / interrupt
5
- * (but AFTER the global Ctrl+] dump detector). A host-backed prompt that
6
- * wants to own wheel/mouse input simply consumes the chunk by returning
4
+ * Active parent UI mode (e.g. user-review prompt). When attached, event
5
+ * arrays are routed to it BEFORE host shortcuts / wheel scroll (but
6
+ * AFTER the global dump hotkey check). A host-backed prompt that wants
7
+ * to own wheel/mouse input simply consumes the events by returning
7
8
  * `true` (the default).
8
9
  */
9
10
  export interface ParentUiMode {
10
- /** Receive a chunk of raw input. Return true if consumed (don't forward). */
11
- onInput(chunk: Buffer): boolean | void;
11
+ /**
12
+ * Receive a batch of `TerminalKey` events. Return `true` (or `void`)
13
+ * to consume the entire batch; return `false` to let the router fall
14
+ * through to its own shortcut/child routing for the same events.
15
+ */
16
+ onEvents(events: TerminalKey[]): boolean | void;
12
17
  }
13
18
  /**
14
19
  * Number of lines to scroll for a single mouse-wheel event. Small enough
@@ -18,7 +23,7 @@ export declare const WHEEL_LINES_PER_EVENT = 3;
18
23
  export interface InputRouterOptions {
19
24
  /** Trigger a dump capture when Ctrl+] is observed. */
20
25
  onDump?: () => void;
21
- /** Host interrupt: invoked when raw Ctrl+C (byte 0x03) is observed and no parent UI consumes it. */
26
+ /** Host interrupt: invoked when Ctrl+C is observed and no parent UI consumes it. */
22
27
  onInterrupt?: () => void;
23
28
  /** Scroll back by N lines (older content). */
24
29
  onScrollBackLines?: (lines: number) => void;
@@ -32,115 +37,31 @@ export interface InputRouterOptions {
32
37
  onScrollToBottom?: () => void;
33
38
  }
34
39
  /**
35
- * Centralizes stdin ownership while the orchestration host is active.
36
- *
37
- * Routing order on each chunk:
40
+ * Event-based input router. Operates on `TerminalKey[]` produced by a
41
+ * `KeyEventSource`; the byte-level state machine is internal to
42
+ * `StdinByteSource`'s parser.
38
43
  *
39
- * 1. Dump hotkey detector (Ctrl+]) strips dump bytes from forward.
40
- * The detector also reassembles split CSI sequences for kitty /
41
- * win32 dump variants, so when configured, downstream stages see
42
- * whole sequences for those paths.
44
+ * Routing order on each event batch:
45
+ * 1. Dump hotkey check (Ctrl+]) strips matching events before they
46
+ * reach the parent UI or child.
43
47
  * 2. Active parent UI mode (if attached) — fully consumes by default.
44
- * Parent UI receives Ctrl+C, mouse, and nav keys first and may
45
- * handle them. Only when there is no parent UI, or it returns
46
- * `false`, does routing fall through to the host's own handlers.
47
- * 3. SGR mouse-wheel parsing — consumes `CSI < 64/65 ; … M` for
48
- * vertical wheel events and dispatches scroll callbacks. Non-wheel
49
- * SGR mouse events flow through unchanged. A small first-class
50
- * buffer reassembles SGR mouse sequences split across chunks so
51
- * the routing works regardless of whether the dump detector is
52
- * configured.
53
- * 4. Host shortcut routing on remaining bytes:
54
- * - Ctrl+C (0x03) → `onInterrupt()`
55
- * - PageUp (CSI 5~) → `onScrollPages(-1)`
56
- * - PageDown (CSI 6~) → `onScrollPages(+1)`
57
- * - Home (CSI 1~/H) → `onScrollToTop()`
58
- * - End (CSI 4~/F) → `onScrollToBottom()`
59
- * 5. Attached child PTY when `gateOpen` is true — `handle.write(forward)`.
60
- * 6. Otherwise the chunk is dropped.
48
+ * 3. Wheel events scrollback callbacks (consumed; never reach child).
49
+ * 4. Host shortcuts: Ctrl+C onInterrupt; page/home/end scrollback.
50
+ * 5. Remaining events child PTY when `gateOpen` is true, encoded via
51
+ * `encodeKeysToBytes`. Otherwise dropped.
61
52
  */
62
53
  export declare class InputRouter {
63
54
  private readonly options;
64
55
  private parentUiMode;
65
56
  private childHandle;
66
- private dumpDetector;
67
- /** Buffered head of a split SGR mouse sequence (`ESC [ < … `). */
68
- private sgrPartial;
69
- private sgrPartialTimer;
70
57
  constructor(options?: InputRouterOptions);
71
58
  attachChild(handle: PtyHandle): void;
72
59
  detachChild(handle?: PtyHandle): void;
73
60
  attachParentUi(mode: ParentUiMode): void;
74
61
  detachParentUi(mode?: ParentUiMode): void;
75
- /** Process a stdin chunk. Returns true when at least one byte was consumed. */
76
- handleChunk(chunk: Buffer): boolean;
62
+ /** Process a batch of `TerminalKey` events. */
63
+ handleEvents(events: TerminalKey[]): void;
77
64
  private forwardToChild;
78
- /**
79
- * Strip SGR mouse wheel events (`CSI < 64;…M` and `CSI < 65;…M`) from
80
- * the chunk, dispatching scrollback callbacks. Non-wheel SGR mouse
81
- * events (clicks, motion, releases) and other CSI sequences (Home,
82
- * PageUp, etc.) are left in the chunk so callers can still forward
83
- * them or run them through host shortcut routing.
84
- *
85
- * Sequences split across chunk boundaries are first-class. The buffer
86
- * holds any incomplete CSI prefix — bare ESC, ESC `[`, ESC `[ < …` —
87
- * and prepends it to the next chunk. This works even when the dump
88
- * detector is not configured (the detector reassembles split CSI for
89
- * its own purposes when it is configured, but mouse-wheel scrollback
90
- * MUST NOT depend on it). A 50 ms safety timer flushes a stale
91
- * partial through `consumeHostShortcuts` and the child gate so a
92
- * bare Esc keypress eventually reaches the child even when there is
93
- * no continuation, and so a stray ESC byte cannot permanently block
94
- * future input.
95
- */
96
- private consumeSgrMouseWheel;
97
- private savePartialSgr;
98
- /**
99
- * Stale-partial flush. Two distinct cases:
100
- *
101
- * - Bare `ESC` (1 byte): flush through host shortcuts and the child
102
- * gate. The user pressed Esc and there is no continuation; the
103
- * child must still receive the byte after the safety window
104
- * (matching the dump detector's bare-Esc deferred-flush).
105
- *
106
- * - Truncated CSI (`ESC [ …`, 2+ bytes): drop. The user pressed
107
- * Home/PageUp/wheel/etc. and the terminal failed to deliver the
108
- * rest within the window. Forwarding raw `ESC [ < 6 4 ;` would
109
- * inject random bytes into the child agent — much worse than
110
- * losing one stray nav event.
111
- */
112
- private flushPartialSgr;
113
- private resetSgrPartialTimer;
114
- private resetSgrPartial;
115
- /**
116
- * Consume host scrollback / interrupt shortcuts from the chunk. Returns
117
- * the chunk with any consumed bytes removed.
118
- *
119
- * Recognized sequences (single-chunk only — split CSI across two
120
- * chunks falls through to the child unchanged, which is acceptable
121
- * because real terminals deliver these in one chunk):
122
- *
123
- * - 0x03 Ctrl+C → onInterrupt()
124
- * - ESC [ 5 ~ PageUp → onScrollPages(-1)
125
- * - ESC [ 6 ~ PageDown → onScrollPages(+1)
126
- * - ESC [ 1 ~ / ESC [ H Home → onScrollToTop()
127
- * - ESC [ 4 ~ / ESC [ F End → onScrollToBottom()
128
- *
129
- * Modifier-prefixed variants (e.g. `ESC [ 1;5 H` for Ctrl+Home) match
130
- * the same callback as their unmodified form.
131
- */
132
- private consumeHostShortcuts;
133
- private dispatchCsi;
134
- /**
135
- * Route a deferred buffer (produced by `DumpHotkeyDetector` when a
136
- * pending escape sequence times out). The deferred bytes have already
137
- * been screened for the dump hotkey — they must NOT pass through the
138
- * detector again. They DO continue through the parent UI mode, the
139
- * SGR mouse parser, host shortcuts, and the child gate so a bare Esc
140
- * reaches host-backed prompts and a deferred mouse sequence is still
141
- * correctly classified.
142
- */
143
- private routeDeferred;
144
65
  destroy(): void;
145
66
  }
146
67
  //# sourceMappingURL=input-router.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"input-router.d.ts","sourceRoot":"","sources":["../../src/terminal/input-router.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAEjD;;;;;;GAMG;AACH,MAAM,WAAW,YAAY;IAC3B,6EAA6E;IAC7E,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CAAC;CACxC;AAED;;;GAGG;AACH,eAAO,MAAM,qBAAqB,IAAI,CAAC;AAEvC,MAAM,WAAW,kBAAkB;IACjC,sDAAsD;IACtD,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,oGAAoG;IACpG,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;IACzB,8CAA8C;IAC9C,iBAAiB,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5C,iDAAiD;IACjD,oBAAoB,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/C,8EAA8E;IAC9E,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,qCAAqC;IACrC,aAAa,CAAC,EAAE,MAAM,IAAI,CAAC;IAC3B,sCAAsC;IACtC,gBAAgB,CAAC,EAAE,MAAM,IAAI,CAAC;CAC/B;AAWD;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,qBAAa,WAAW;IAQV,OAAO,CAAC,QAAQ,CAAC,OAAO;IAPpC,OAAO,CAAC,YAAY,CAA2B;IAC/C,OAAO,CAAC,WAAW,CAAwB;IAC3C,OAAO,CAAC,YAAY,CAAiC;IACrD,kEAAkE;IAClE,OAAO,CAAC,UAAU,CAAgB;IAClC,OAAO,CAAC,eAAe,CAA4C;gBAEtC,OAAO,GAAE,kBAAuB;IAmB7D,WAAW,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI;IAIpC,WAAW,CAAC,MAAM,CAAC,EAAE,SAAS,GAAG,IAAI;IAKrC,cAAc,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI;IAIxC,cAAc,CAAC,IAAI,CAAC,EAAE,YAAY,GAAG,IAAI;IAKzC,+EAA+E;IAC/E,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO;IAmCnC,OAAO,CAAC,cAAc;IAOtB;;;;;;;;;;;;;;;;;OAiBG;IACH,OAAO,CAAC,oBAAoB;IAsF5B,OAAO,CAAC,cAAc;IAMtB;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,eAAe;IAgBvB,OAAO,CAAC,oBAAoB;IAO5B,OAAO,CAAC,eAAe;IAOvB;;;;;;;;;;;;;;;;OAgBG;IACH,OAAO,CAAC,oBAAoB;IAmD5B,OAAO,CAAC,WAAW;IAqDnB;;;;;;;;OAQG;IACH,OAAO,CAAC,aAAa;IAgBrB,OAAO,IAAI,IAAI;CAOhB"}
1
+ {"version":3,"file":"input-router.d.ts","sourceRoot":"","sources":["../../src/terminal/input-router.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACxD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAEjD;;;;;;GAMG;AACH,MAAM,WAAW,YAAY;IAC3B;;;;OAIG;IACH,QAAQ,CAAC,MAAM,EAAE,WAAW,EAAE,GAAG,OAAO,GAAG,IAAI,CAAC;CACjD;AAED;;;GAGG;AACH,eAAO,MAAM,qBAAqB,IAAI,CAAC;AAEvC,MAAM,WAAW,kBAAkB;IACjC,sDAAsD;IACtD,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,oFAAoF;IACpF,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;IACzB,8CAA8C;IAC9C,iBAAiB,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5C,iDAAiD;IACjD,oBAAoB,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/C,8EAA8E;IAC9E,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,qCAAqC;IACrC,aAAa,CAAC,EAAE,MAAM,IAAI,CAAC;IAC3B,sCAAsC;IACtC,gBAAgB,CAAC,EAAE,MAAM,IAAI,CAAC;CAC/B;AAED;;;;;;;;;;;;;GAaG;AACH,qBAAa,WAAW;IAIV,OAAO,CAAC,QAAQ,CAAC,OAAO;IAHpC,OAAO,CAAC,YAAY,CAA2B;IAC/C,OAAO,CAAC,WAAW,CAAwB;gBAEd,OAAO,GAAE,kBAAuB;IAE7D,WAAW,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI;IAIpC,WAAW,CAAC,MAAM,CAAC,EAAE,SAAS,GAAG,IAAI;IAKrC,cAAc,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI;IAIxC,cAAc,CAAC,IAAI,CAAC,EAAE,YAAY,GAAG,IAAI;IAKzC,+CAA+C;IAC/C,YAAY,CAAC,MAAM,EAAE,WAAW,EAAE,GAAG,IAAI;IA4DzC,OAAO,CAAC,cAAc;IAatB,OAAO,IAAI,IAAI;CAIhB"}
@@ -1,67 +1,30 @@
1
- import { DumpHotkeyDetector } from "../dump/hotkey.js";
1
+ import { isDumpHotkey } from "../dump/hotkey.js";
2
+ import { encodeKeysToBytes } from "./key-to-bytes.js";
2
3
  /**
3
4
  * Number of lines to scroll for a single mouse-wheel event. Small enough
4
5
  * to feel precise while still moving meaningfully on every notch.
5
6
  */
6
7
  export const WHEEL_LINES_PER_EVENT = 3;
7
- const ETX = 0x03; // Ctrl+C
8
- const ESC = 0x1b;
9
- const LBRACKET = 0x5b; // '['
10
- const LT = 0x3c; // '<'
11
- const TILDE = 0x7e; // '~'
12
- /** Maximum dwell for a buffered partial SGR mouse sequence before we drop it. */
13
- const SGR_MOUSE_PARTIAL_TIMEOUT_MS = 50;
14
8
  /**
15
- * Centralizes stdin ownership while the orchestration host is active.
9
+ * Event-based input router. Operates on `TerminalKey[]` produced by a
10
+ * `KeyEventSource`; the byte-level state machine is internal to
11
+ * `StdinByteSource`'s parser.
16
12
  *
17
- * Routing order on each chunk:
18
- *
19
- * 1. Dump hotkey detector (Ctrl+]) — strips dump bytes from forward.
20
- * The detector also reassembles split CSI sequences for kitty /
21
- * win32 dump variants, so when configured, downstream stages see
22
- * whole sequences for those paths.
13
+ * Routing order on each event batch:
14
+ * 1. Dump hotkey check (Ctrl+]) — strips matching events before they
15
+ * reach the parent UI or child.
23
16
  * 2. Active parent UI mode (if attached) — fully consumes by default.
24
- * Parent UI receives Ctrl+C, mouse, and nav keys first and may
25
- * handle them. Only when there is no parent UI, or it returns
26
- * `false`, does routing fall through to the host's own handlers.
27
- * 3. SGR mouse-wheel parsing — consumes `CSI < 64/65 ; … M` for
28
- * vertical wheel events and dispatches scroll callbacks. Non-wheel
29
- * SGR mouse events flow through unchanged. A small first-class
30
- * buffer reassembles SGR mouse sequences split across chunks so
31
- * the routing works regardless of whether the dump detector is
32
- * configured.
33
- * 4. Host shortcut routing on remaining bytes:
34
- * - Ctrl+C (0x03) → `onInterrupt()`
35
- * - PageUp (CSI 5~) → `onScrollPages(-1)`
36
- * - PageDown (CSI 6~) → `onScrollPages(+1)`
37
- * - Home (CSI 1~/H) → `onScrollToTop()`
38
- * - End (CSI 4~/F) → `onScrollToBottom()`
39
- * 5. Attached child PTY when `gateOpen` is true — `handle.write(forward)`.
40
- * 6. Otherwise the chunk is dropped.
17
+ * 3. Wheel events scrollback callbacks (consumed; never reach child).
18
+ * 4. Host shortcuts: Ctrl+C onInterrupt; page/home/end scrollback.
19
+ * 5. Remaining events child PTY when `gateOpen` is true, encoded via
20
+ * `encodeKeysToBytes`. Otherwise dropped.
41
21
  */
42
22
  export class InputRouter {
43
23
  options;
44
24
  parentUiMode;
45
25
  childHandle;
46
- dumpDetector;
47
- /** Buffered head of a split SGR mouse sequence (`ESC [ < … `). */
48
- sgrPartial = [];
49
- sgrPartialTimer;
50
26
  constructor(options = {}) {
51
27
  this.options = options;
52
- if (this.options.onDump) {
53
- const onDump = this.options.onDump;
54
- this.dumpDetector = new DumpHotkeyDetector(() => onDump(), (deferred) => {
55
- // Deferred flush — fires when DumpHotkeyDetector buffered an
56
- // ESC (or partial CSI) and the timeout expired without a final
57
- // byte arriving. Re-enter the routing pipeline so a bare ESC
58
- // reaches an active parent UI mode (HostBackedCuratePrompt
59
- // uses Esc to cancel comment editing). Only when there is no
60
- // parent UI and the child gate is open does the bytes reach
61
- // the child.
62
- this.routeDeferred(deferred);
63
- });
64
- }
65
28
  }
66
29
  attachChild(handle) {
67
30
  this.childHandle = handle;
@@ -79,346 +42,82 @@ export class InputRouter {
79
42
  return;
80
43
  this.parentUiMode = undefined;
81
44
  }
82
- /** Process a stdin chunk. Returns true when at least one byte was consumed. */
83
- handleChunk(chunk) {
84
- // Global parent shortcuts run FIRST so Ctrl+] reliably triggers a
85
- // dump regardless of which UI mode currently owns input. The detector
86
- // strips the hotkey bytes from the forward buffer; everything else
87
- // continues down the routing pipeline.
88
- let forward = chunk;
89
- if (this.dumpDetector) {
90
- forward = this.dumpDetector.process(chunk);
91
- }
92
- if (forward.length === 0)
93
- return true;
94
- // Parent UI gets first crack at every byte that isn't the dump
95
- // hotkey — including mouse events and nav keys. A host-backed
96
- // prompt that wants to own wheel input simply consumes the chunk.
97
- if (this.parentUiMode) {
98
- const consumed = this.parentUiMode.onInput(forward);
99
- // Default behaviour: parent UI swallows everything until detached.
100
- if (consumed !== false) {
101
- // The parent UI took ownership. Any partial SGR mouse sequence
102
- // we previously buffered no longer applies — clear the buffer.
103
- this.resetSgrPartial();
104
- return true;
45
+ /** Process a batch of `TerminalKey` events. */
46
+ handleEvents(events) {
47
+ if (events.length === 0)
48
+ return;
49
+ // Pre-filter: dump hotkey runs ahead of everything and is stripped.
50
+ const filtered = [];
51
+ for (const ev of events) {
52
+ if (isDumpHotkey(ev)) {
53
+ this.options.onDump?.();
54
+ continue;
105
55
  }
56
+ filtered.push(ev);
106
57
  }
107
- // Host owns the bytes. SGR mouse-wheel parsing comes BEFORE host
108
- // shortcuts so wheel events don't accidentally match nav keys.
109
- forward = this.consumeSgrMouseWheel(forward);
110
- if (forward.length === 0)
111
- return true;
112
- const afterShortcuts = this.consumeHostShortcuts(forward);
113
- if (afterShortcuts.length === 0)
114
- return true;
115
- return this.forwardToChild(afterShortcuts);
116
- }
117
- forwardToChild(buf) {
118
- const child = this.childHandle;
119
- if (!child || !child.gateOpen)
120
- return false;
121
- child.write(buf.toString());
122
- return true;
123
- }
124
- /**
125
- * Strip SGR mouse wheel events (`CSI < 64;…M` and `CSI < 65;…M`) from
126
- * the chunk, dispatching scrollback callbacks. Non-wheel SGR mouse
127
- * events (clicks, motion, releases) and other CSI sequences (Home,
128
- * PageUp, etc.) are left in the chunk so callers can still forward
129
- * them or run them through host shortcut routing.
130
- *
131
- * Sequences split across chunk boundaries are first-class. The buffer
132
- * holds any incomplete CSI prefix — bare ESC, ESC `[`, ESC `[ < …` —
133
- * and prepends it to the next chunk. This works even when the dump
134
- * detector is not configured (the detector reassembles split CSI for
135
- * its own purposes when it is configured, but mouse-wheel scrollback
136
- * MUST NOT depend on it). A 50 ms safety timer flushes a stale
137
- * partial through `consumeHostShortcuts` and the child gate so a
138
- * bare Esc keypress eventually reaches the child even when there is
139
- * no continuation, and so a stray ESC byte cannot permanently block
140
- * future input.
141
- */
142
- consumeSgrMouseWheel(buf) {
143
- // Prepend any buffered partial to this chunk before parsing.
144
- if (this.sgrPartial.length > 0) {
145
- const merged = Buffer.allocUnsafe(this.sgrPartial.length + buf.length);
146
- for (let k = 0; k < this.sgrPartial.length; k++)
147
- merged[k] = this.sgrPartial[k];
148
- buf.copy(merged, this.sgrPartial.length);
149
- buf = merged;
150
- this.resetSgrPartialTimer();
151
- this.sgrPartial = [];
58
+ if (filtered.length === 0)
59
+ return;
60
+ // Parent UI gets the remainder first.
61
+ if (this.parentUiMode) {
62
+ const consumed = this.parentUiMode.onEvents(filtered);
63
+ if (consumed !== false)
64
+ return;
152
65
  }
153
- if (buf.length === 0)
154
- return buf;
155
- const out = [];
156
- let i = 0;
157
- while (i < buf.length) {
158
- const b = buf[i];
159
- if (b !== ESC) {
160
- out.push(b);
161
- i++;
162
- continue;
163
- }
164
- // ESC byte. The chunk ends here, after `[`, or after `[ < …`
165
- // — buffer the partial in every case so a split mouse sequence
166
- // never leaks to the child as half-bytes.
167
- if (i + 1 >= buf.length) {
168
- // Bare ESC at end-of-chunk. Could be (a) the Esc key, (b) the
169
- // start of an Alt+key combo, or (c) the start of any CSI
170
- // including SGR mouse. Buffering all three for 50 ms costs a
171
- // small delay on the Esc key (matching the dump detector's
172
- // own delay) but is the price of not forwarding partial mouse
173
- // sequences to the child.
174
- this.savePartialSgr(buf.subarray(i));
175
- return Buffer.from(out);
176
- }
177
- if (buf[i + 1] !== LBRACKET) {
178
- // ESC + non-`[` — Alt+key or another ESC-prefixed combo. Emit
179
- // both bytes immediately so Alt+key combos are not delayed.
180
- out.push(b);
181
- i++;
182
- continue;
183
- }
184
- // We have ESC `[`. Find the CSI final byte in the range 0x40-0x7e.
185
- let j = i + 2;
186
- while (j < buf.length) {
187
- const t = buf[j];
188
- if (t >= 0x40 && t <= 0x7e)
189
- break;
190
- j++;
191
- }
192
- if (j >= buf.length) {
193
- // Unterminated CSI — buffer the partial for the next chunk to
194
- // complete. This covers all reviewer-flagged splits: ESC alone,
195
- // `ESC [`, `ESC [ <`, `ESC [ < 64 ;`, etc.
196
- this.savePartialSgr(buf.subarray(i));
197
- return Buffer.from(out);
198
- }
199
- const params = buf.subarray(i + 2, j).toString("ascii");
200
- const final = buf[j];
201
- // SGR mouse-wheel sequence: ESC `[ < cb ; cx ; cy M` where
202
- // cb=64 (wheel up) or cb=65 (wheel down). Anything else (clicks,
203
- // releases, non-mouse CSI) is passed through unchanged.
204
- if (params.startsWith("<") && final === 0x4d /* M */) {
205
- const cb = parseInt(params.slice(1).split(";")[0] ?? "", 10);
206
- if (cb === 64) {
66
+ // Host owns the batch — pull wheel events into scrollback, ETX into
67
+ // interrupt, page/home/end into scroll callbacks; forward whatever
68
+ // remains to the child via the encoder.
69
+ const forward = [];
70
+ for (const ev of filtered) {
71
+ if (ev.kind === "wheel") {
72
+ if (ev.direction === "up") {
207
73
  this.options.onScrollBackLines?.(WHEEL_LINES_PER_EVENT);
208
- i = j + 1;
209
- continue;
210
74
  }
211
- if (cb === 65) {
75
+ else {
212
76
  this.options.onScrollForwardLines?.(WHEEL_LINES_PER_EVENT);
213
- i = j + 1;
214
- continue;
215
77
  }
78
+ continue;
216
79
  }
217
- // Not a mouse wheel emit the entire CSI sequence intact.
218
- for (let k = i; k <= j; k++)
219
- out.push(buf[k]);
220
- i = j + 1;
221
- }
222
- return Buffer.from(out);
223
- }
224
- savePartialSgr(partial) {
225
- this.sgrPartial = Array.from(partial);
226
- if (this.sgrPartialTimer)
227
- clearTimeout(this.sgrPartialTimer);
228
- this.sgrPartialTimer = setTimeout(() => this.flushPartialSgr(), SGR_MOUSE_PARTIAL_TIMEOUT_MS);
229
- }
230
- /**
231
- * Stale-partial flush. Two distinct cases:
232
- *
233
- * - Bare `ESC` (1 byte): flush through host shortcuts and the child
234
- * gate. The user pressed Esc and there is no continuation; the
235
- * child must still receive the byte after the safety window
236
- * (matching the dump detector's bare-Esc deferred-flush).
237
- *
238
- * - Truncated CSI (`ESC [ …`, 2+ bytes): drop. The user pressed
239
- * Home/PageUp/wheel/etc. and the terminal failed to deliver the
240
- * rest within the window. Forwarding raw `ESC [ < 6 4 ;` would
241
- * inject random bytes into the child agent — much worse than
242
- * losing one stray nav event.
243
- */
244
- flushPartialSgr() {
245
- if (this.sgrPartial.length === 0) {
246
- this.sgrPartialTimer = undefined;
247
- return;
248
- }
249
- const buf = Buffer.from(this.sgrPartial);
250
- this.sgrPartial = [];
251
- this.sgrPartialTimer = undefined;
252
- if (buf.length === 1 && buf[0] === ESC) {
253
- const afterShortcuts = this.consumeHostShortcuts(buf);
254
- if (afterShortcuts.length > 0)
255
- this.forwardToChild(afterShortcuts);
256
- }
257
- // Else: drop the truncated CSI prefix.
258
- }
259
- resetSgrPartialTimer() {
260
- if (this.sgrPartialTimer) {
261
- clearTimeout(this.sgrPartialTimer);
262
- this.sgrPartialTimer = undefined;
263
- }
264
- }
265
- resetSgrPartial() {
266
- this.resetSgrPartialTimer();
267
- if (this.sgrPartial.length > 0) {
268
- this.sgrPartial = [];
269
- }
270
- }
271
- /**
272
- * Consume host scrollback / interrupt shortcuts from the chunk. Returns
273
- * the chunk with any consumed bytes removed.
274
- *
275
- * Recognized sequences (single-chunk only — split CSI across two
276
- * chunks falls through to the child unchanged, which is acceptable
277
- * because real terminals deliver these in one chunk):
278
- *
279
- * - 0x03 Ctrl+C → onInterrupt()
280
- * - ESC [ 5 ~ PageUp → onScrollPages(-1)
281
- * - ESC [ 6 ~ PageDown → onScrollPages(+1)
282
- * - ESC [ 1 ~ / ESC [ H Home → onScrollToTop()
283
- * - ESC [ 4 ~ / ESC [ F End → onScrollToBottom()
284
- *
285
- * Modifier-prefixed variants (e.g. `ESC [ 1;5 H` for Ctrl+Home) match
286
- * the same callback as their unmodified form.
287
- */
288
- consumeHostShortcuts(buf) {
289
- if (buf.length === 0)
290
- return buf;
291
- const out = [];
292
- let i = 0;
293
- while (i < buf.length) {
294
- const b = buf[i];
295
- if (b === ETX) {
80
+ if (ev.kind === "control" && ev.byte === 0x03) {
296
81
  if (this.options.onInterrupt) {
297
82
  this.options.onInterrupt();
298
- i++;
299
83
  continue;
300
84
  }
301
- // No interrupt handler — fall through to child.
302
- out.push(b);
303
- i++;
304
- continue;
305
85
  }
306
- if (b === ESC && i + 1 < buf.length && buf[i + 1] === LBRACKET) {
307
- // Find the CSI final byte.
308
- let j = i + 2;
309
- while (j < buf.length) {
310
- const t = buf[j];
311
- if (t >= 0x40 && t <= 0x7e)
312
- break;
313
- j++;
314
- }
315
- if (j >= buf.length) {
316
- // Unterminated — emit remainder unchanged.
317
- for (let k = i; k < buf.length; k++)
318
- out.push(buf[k]);
319
- return Buffer.from(out);
320
- }
321
- const params = buf.subarray(i + 2, j).toString("ascii");
322
- const final = buf[j];
323
- const handled = this.dispatchCsi(params, final);
324
- if (handled) {
325
- i = j + 1;
326
- continue;
327
- }
328
- // Not a host shortcut — pass through unchanged.
329
- for (let k = i; k <= j; k++)
330
- out.push(buf[k]);
331
- i = j + 1;
332
- continue;
333
- }
334
- out.push(b);
335
- i++;
336
- }
337
- return Buffer.from(out);
338
- }
339
- dispatchCsi(params, final) {
340
- // PageUp = ESC [ 5 ~ ; PageDown = ESC [ 6 ~ ; Home = ESC [ 1 ~ or H ;
341
- // End = ESC [ 4 ~ or F. Modifier params (e.g. "1;5") are accepted so
342
- // Ctrl/Shift+nav routes through the same scroll callback.
343
- if (final === TILDE) {
344
- const code = parseInt(params.split(";")[0] ?? "", 10);
345
- if (code === 5) {
346
- if (this.options.onScrollPages) {
347
- this.options.onScrollPages(-1);
348
- return true;
349
- }
350
- return false;
351
- }
352
- if (code === 6) {
86
+ if (ev.kind === "page") {
353
87
  if (this.options.onScrollPages) {
354
- this.options.onScrollPages(1);
355
- return true;
356
- }
357
- return false;
358
- }
359
- if (code === 1 || code === 7) {
360
- if (this.options.onScrollToTop) {
361
- this.options.onScrollToTop();
362
- return true;
363
- }
364
- return false;
365
- }
366
- if (code === 4 || code === 8) {
367
- if (this.options.onScrollToBottom) {
368
- this.options.onScrollToBottom();
369
- return true;
88
+ this.options.onScrollPages(ev.direction === "up" ? -1 : 1);
89
+ continue;
370
90
  }
371
- return false;
372
91
  }
373
- return false;
374
- }
375
- if (final === 0x48 /* H */) {
376
- if (this.options.onScrollToTop) {
92
+ if (ev.kind === "home" && this.options.onScrollToTop) {
377
93
  this.options.onScrollToTop();
378
- return true;
94
+ continue;
379
95
  }
380
- return false;
381
- }
382
- if (final === 0x46 /* F */) {
383
- if (this.options.onScrollToBottom) {
96
+ if (ev.kind === "end" && this.options.onScrollToBottom) {
384
97
  this.options.onScrollToBottom();
385
- return true;
98
+ continue;
386
99
  }
387
- return false;
100
+ forward.push(ev);
388
101
  }
389
- return false;
102
+ if (forward.length === 0)
103
+ return;
104
+ this.forwardToChild(forward);
390
105
  }
391
- /**
392
- * Route a deferred buffer (produced by `DumpHotkeyDetector` when a
393
- * pending escape sequence times out). The deferred bytes have already
394
- * been screened for the dump hotkey — they must NOT pass through the
395
- * detector again. They DO continue through the parent UI mode, the
396
- * SGR mouse parser, host shortcuts, and the child gate so a bare Esc
397
- * reaches host-backed prompts and a deferred mouse sequence is still
398
- * correctly classified.
399
- */
400
- routeDeferred(buf) {
106
+ forwardToChild(events) {
107
+ const child = this.childHandle;
108
+ if (!child || !child.gateOpen)
109
+ return;
110
+ const buf = encodeKeysToBytes(events);
401
111
  if (buf.length === 0)
402
- return false;
403
- if (this.parentUiMode) {
404
- const consumed = this.parentUiMode.onInput(buf);
405
- if (consumed !== false) {
406
- this.resetSgrPartial();
407
- return true;
408
- }
409
- }
410
- const afterMouse = this.consumeSgrMouseWheel(buf);
411
- if (afterMouse.length === 0)
412
- return true;
413
- const afterShortcuts = this.consumeHostShortcuts(afterMouse);
414
- if (afterShortcuts.length === 0)
415
- return true;
416
- return this.forwardToChild(afterShortcuts);
112
+ return;
113
+ // Forward the encoded Buffer verbatim — the PtyHandle adapter is
114
+ // responsible for handing it to node-pty without lossy decoding.
115
+ // Going through a latin1 string here would corrupt multibyte UTF-8
116
+ // printable chars (e.g. "中" → "中") because node-pty re-encodes
117
+ // the inbound string as UTF-8.
118
+ child.write(buf);
417
119
  }
418
120
  destroy() {
419
- this.dumpDetector?.destroy();
420
- this.dumpDetector = undefined;
421
- this.resetSgrPartial();
422
121
  this.childHandle = undefined;
423
122
  this.parentUiMode = undefined;
424
123
  }