@huseynsnmz/pi-suite 0.0.0-stage → 0.2.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 (274) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/LICENSE +21 -0
  3. package/README.md +88 -2
  4. package/SECURITY.md +5 -0
  5. package/package.json +112 -4
  6. package/packages/board/CHANGELOG.md +340 -0
  7. package/packages/board/CONTRIBUTING.md +7 -0
  8. package/packages/board/LICENSE +21 -0
  9. package/packages/board/README.md +88 -0
  10. package/packages/board/SECURITY.md +19 -0
  11. package/packages/board/docs/how-it-works.md +31 -0
  12. package/packages/board/docs/reference.md +278 -0
  13. package/packages/board/package.json +81 -0
  14. package/packages/board/runner/job-runner-legacy.mjs +68 -0
  15. package/packages/board/runner/job-runner.mjs +834 -0
  16. package/packages/board/runner/pty-runner-legacy.mjs +50 -0
  17. package/packages/board/runner/pty-runner.mjs +2175 -0
  18. package/packages/board/runner/state-coordinator.mjs +673 -0
  19. package/packages/board/runner/state-runner.mjs +203 -0
  20. package/packages/board/runner/title-runner.mjs +80 -0
  21. package/packages/board/src/commands/agent-board.ts +394 -0
  22. package/packages/board/src/commands/attach-decision.mjs +66 -0
  23. package/packages/board/src/commands/attach-flow.ts +251 -0
  24. package/packages/board/src/commands/bg.ts +103 -0
  25. package/packages/board/src/commands/board-model.ts +21 -0
  26. package/packages/board/src/core/atomic.mjs +184 -0
  27. package/packages/board/src/core/attach-delete.mjs +24 -0
  28. package/packages/board/src/core/attach-frame-proof.mjs +870 -0
  29. package/packages/board/src/core/attach-frame-reporter.mjs +648 -0
  30. package/packages/board/src/core/attach-frame-stream.mjs +266 -0
  31. package/packages/board/src/core/attach-view-proof.mjs +709 -0
  32. package/packages/board/src/core/auto-state.mjs +377 -0
  33. package/packages/board/src/core/background-runs.mjs +273 -0
  34. package/packages/board/src/core/code-refs-store.mjs +315 -0
  35. package/packages/board/src/core/code-refs.mjs +913 -0
  36. package/packages/board/src/core/config.mjs +77 -0
  37. package/packages/board/src/core/control-protocol.mjs +482 -0
  38. package/packages/board/src/core/coordinator-client.mjs +633 -0
  39. package/packages/board/src/core/coordinator-journal.mjs +282 -0
  40. package/packages/board/src/core/coordinator-protocol.mjs +14 -0
  41. package/packages/board/src/core/cwd-stats.mjs +114 -0
  42. package/packages/board/src/core/dashboard-render.mjs +10 -0
  43. package/packages/board/src/core/derive.mjs +117 -0
  44. package/packages/board/src/core/diagnostics.mjs +133 -0
  45. package/packages/board/src/core/double-press.mjs +26 -0
  46. package/packages/board/src/core/editor-state-reporter.mjs +112 -0
  47. package/packages/board/src/core/events.mjs +286 -0
  48. package/packages/board/src/core/evidence.mjs +245 -0
  49. package/packages/board/src/core/follow-up-queue.mjs +211 -0
  50. package/packages/board/src/core/foreground-preview-cache.mjs +117 -0
  51. package/packages/board/src/core/foreground-projection-cache.mjs +99 -0
  52. package/packages/board/src/core/frame-witness-update.mjs +35 -0
  53. package/packages/board/src/core/heuristics.mjs +313 -0
  54. package/packages/board/src/core/hold-input.mjs +27 -0
  55. package/packages/board/src/core/host-coordination.mjs +300 -0
  56. package/packages/board/src/core/host-crash.mjs +79 -0
  57. package/packages/board/src/core/host-probe.mjs +207 -0
  58. package/packages/board/src/core/host-protocol.mjs +73 -0
  59. package/packages/board/src/core/ids.mjs +35 -0
  60. package/packages/board/src/core/ime-cursor-coalesce.mjs +193 -0
  61. package/packages/board/src/core/invocation.mjs +43 -0
  62. package/packages/board/src/core/launch-options.mjs +418 -0
  63. package/packages/board/src/core/launch.mjs +157 -0
  64. package/packages/board/src/core/load-config.mjs +4 -0
  65. package/packages/board/src/core/locks.mjs +530 -0
  66. package/packages/board/src/core/paths.mjs +183 -0
  67. package/packages/board/src/core/pid.mjs +75 -0
  68. package/packages/board/src/core/prewarm-schedule.mjs +41 -0
  69. package/packages/board/src/core/process-identity-probe.mjs +228 -0
  70. package/packages/board/src/core/prompt-transport.mjs +13 -0
  71. package/packages/board/src/core/pty-attach-jiggle-controller.mjs +356 -0
  72. package/packages/board/src/core/pty-attach-jiggle-retry.mjs +109 -0
  73. package/packages/board/src/core/pty-attach-reconnect.mjs +61 -0
  74. package/packages/board/src/core/pty-attach-render.mjs +229 -0
  75. package/packages/board/src/core/pty-links.mjs +81 -0
  76. package/packages/board/src/core/pty-scroll.mjs +522 -0
  77. package/packages/board/src/core/pty-support.mjs +327 -0
  78. package/packages/board/src/core/repo.mjs +103 -0
  79. package/packages/board/src/core/rows.mjs +414 -0
  80. package/packages/board/src/core/screen-log-gc.mjs +198 -0
  81. package/packages/board/src/core/screen-log.mjs +160 -0
  82. package/packages/board/src/core/session-view.mjs +174 -0
  83. package/packages/board/src/core/slash-commands.mjs +52 -0
  84. package/packages/board/src/core/state-commands.mjs +757 -0
  85. package/packages/board/src/core/status-consistency.mjs +98 -0
  86. package/packages/board/src/core/steering-prompts.mjs +34 -0
  87. package/packages/board/src/core/steering.mjs +133 -0
  88. package/packages/board/src/core/stop-latch.mjs +41 -0
  89. package/packages/board/src/core/store.mjs +523 -0
  90. package/packages/board/src/core/suspend.mjs +36 -0
  91. package/packages/board/src/core/terminal-attach-client.mjs +981 -0
  92. package/packages/board/src/core/terminal-attach-protocol.mjs +460 -0
  93. package/packages/board/src/core/terminal-model.mjs +222 -0
  94. package/packages/board/src/core/terminal-query-sequences.mjs +239 -0
  95. package/packages/board/src/core/terminal-reply-guard.mjs +222 -0
  96. package/packages/board/src/core/terminal-snapshot.mjs +440 -0
  97. package/packages/board/src/core/test-knobs.mjs +28 -0
  98. package/packages/board/src/core/title.mjs +43 -0
  99. package/packages/board/src/core/types.mjs +414 -0
  100. package/packages/board/src/core/warm-host-sweeper.mjs +168 -0
  101. package/packages/board/src/core/worktree.mjs +65 -0
  102. package/packages/board/src/index.ts +303 -0
  103. package/packages/board/src/runtime/service.mjs +3222 -0
  104. package/packages/board/src/ui/chrome.ts +20 -0
  105. package/packages/board/src/ui/dashboard-decisions.mjs +55 -0
  106. package/packages/board/src/ui/dashboard-evidence.mjs +85 -0
  107. package/packages/board/src/ui/dashboard.ts +2237 -0
  108. package/packages/board/src/ui/double-left.ts +30 -0
  109. package/packages/board/src/ui/external-editor.ts +25 -0
  110. package/packages/board/src/ui/focus.ts +32 -0
  111. package/packages/board/src/ui/pty-attach.ts +3184 -0
  112. package/packages/board/src/ui/waiting.ts +30 -0
  113. package/packages/commands/LICENSE +21 -0
  114. package/packages/commands/README.md +19 -0
  115. package/packages/commands/package.json +22 -0
  116. package/packages/commands/src/index.ts +43 -0
  117. package/packages/commands/src/security-review.ts +109 -0
  118. package/packages/frame/CHANGELOG.md +113 -0
  119. package/packages/frame/CONTRIBUTING.md +7 -0
  120. package/packages/frame/LICENSE +22 -0
  121. package/packages/frame/README.md +105 -0
  122. package/packages/frame/SECURITY.md +16 -0
  123. package/packages/frame/docs/how-it-works.md +53 -0
  124. package/packages/frame/docs/reference.md +78 -0
  125. package/packages/frame/package.json +72 -0
  126. package/packages/frame/src/data/git.ts +90 -0
  127. package/packages/frame/src/data/github.ts +140 -0
  128. package/packages/frame/src/data/gitlab.ts +63 -0
  129. package/packages/frame/src/data/mode.ts +49 -0
  130. package/packages/frame/src/data/past-prompts.ts +57 -0
  131. package/packages/frame/src/data/runner.ts +32 -0
  132. package/packages/frame/src/data/snapshot.ts +223 -0
  133. package/packages/frame/src/data/start-notes.ts +104 -0
  134. package/packages/frame/src/data/telemetry.ts +337 -0
  135. package/packages/frame/src/data/todos.ts +162 -0
  136. package/packages/frame/src/data/totals.ts +94 -0
  137. package/packages/frame/src/data/visits.ts +45 -0
  138. package/packages/frame/src/index.ts +696 -0
  139. package/packages/frame/src/kit/LICENSE +21 -0
  140. package/packages/frame/src/kit/app/attribution.ts +30 -0
  141. package/packages/frame/src/kit/app/builder.ts +186 -0
  142. package/packages/frame/src/kit/app/claim.ts +27 -0
  143. package/packages/frame/src/kit/app/feature.ts +91 -0
  144. package/packages/frame/src/kit/app/host.ts +214 -0
  145. package/packages/frame/src/kit/app/renderers.ts +62 -0
  146. package/packages/frame/src/kit/app/scope.ts +125 -0
  147. package/packages/frame/src/kit/board/model.ts +337 -0
  148. package/packages/frame/src/kit/contract.ts +79 -0
  149. package/packages/frame/src/kit/contracts/accounts.ts +33 -0
  150. package/packages/frame/src/kit/contracts/ask.ts +120 -0
  151. package/packages/frame/src/kit/contracts/board.ts +6 -0
  152. package/packages/frame/src/kit/contracts/by.ts +22 -0
  153. package/packages/frame/src/kit/contracts/calls.ts +46 -0
  154. package/packages/frame/src/kit/contracts/events.ts +89 -0
  155. package/packages/frame/src/kit/contracts/mode.ts +26 -0
  156. package/packages/frame/src/kit/contracts/names.ts +12 -0
  157. package/packages/frame/src/kit/contracts/peers.ts +77 -0
  158. package/packages/frame/src/kit/contracts/prompts.ts +8 -0
  159. package/packages/frame/src/kit/contracts/screen.ts +75 -0
  160. package/packages/frame/src/kit/contracts/skills.ts +81 -0
  161. package/packages/frame/src/kit/contracts/sources.ts +81 -0
  162. package/packages/frame/src/kit/contracts/views.ts +42 -0
  163. package/packages/frame/src/kit/control.ts +59 -0
  164. package/packages/frame/src/kit/dark-theme.ts +41 -0
  165. package/packages/frame/src/kit/decode.ts +210 -0
  166. package/packages/frame/src/kit/events/log.ts +313 -0
  167. package/packages/frame/src/kit/format.ts +25 -0
  168. package/packages/frame/src/kit/host.ts +4 -0
  169. package/packages/frame/src/kit/index.ts +25 -0
  170. package/packages/frame/src/kit/legacy-tasks.ts +39 -0
  171. package/packages/frame/src/kit/overlay-images.ts +36 -0
  172. package/packages/frame/src/kit/paths.ts +16 -0
  173. package/packages/frame/src/kit/screen/client.ts +74 -0
  174. package/packages/frame/src/kit/screen/feature.ts +64 -0
  175. package/packages/frame/src/kit/screen/model.ts +182 -0
  176. package/packages/frame/src/kit/screen/view.ts +838 -0
  177. package/packages/frame/src/kit/settings/files.ts +90 -0
  178. package/packages/frame/src/kit/settings/setting.ts +57 -0
  179. package/packages/frame/src/kit/settings/store.ts +192 -0
  180. package/packages/frame/src/kit/sources/file.ts +129 -0
  181. package/packages/frame/src/kit/sources/follow.ts +155 -0
  182. package/packages/frame/src/kit/testing.ts +260 -0
  183. package/packages/frame/src/kit/totals.ts +72 -0
  184. package/packages/frame/src/kit/ui/entry.ts +75 -0
  185. package/packages/frame/src/kit/ui/frame.ts +212 -0
  186. package/packages/frame/src/kit/ui/glyphs.ts +132 -0
  187. package/packages/frame/src/kit/ui/scroll.ts +14 -0
  188. package/packages/frame/src/render/diff.ts +157 -0
  189. package/packages/frame/src/render/fit.ts +86 -0
  190. package/packages/frame/src/render/format.ts +108 -0
  191. package/packages/frame/src/render/frame.ts +106 -0
  192. package/packages/frame/src/render/glyphs.ts +189 -0
  193. package/packages/frame/src/render/keys.ts +43 -0
  194. package/packages/frame/src/render/lead.ts +37 -0
  195. package/packages/frame/src/render/paint.ts +135 -0
  196. package/packages/frame/src/render/safe.ts +19 -0
  197. package/packages/frame/src/render/segments.ts +735 -0
  198. package/packages/frame/src/render/streaming.ts +223 -0
  199. package/packages/frame/src/render/timing.ts +106 -0
  200. package/packages/frame/src/render/tools.ts +167 -0
  201. package/packages/frame/src/settings.ts +105 -0
  202. package/packages/frame/src/ui/editor.ts +272 -0
  203. package/packages/frame/src/ui/effort.ts +36 -0
  204. package/packages/frame/src/ui/footer.ts +165 -0
  205. package/packages/frame/src/ui/header.ts +362 -0
  206. package/packages/frame/src/ui/history-search.ts +23 -0
  207. package/packages/frame/src/ui/interrupt.ts +21 -0
  208. package/packages/frame/src/ui/outputs.ts +170 -0
  209. package/packages/frame/src/ui/pi-entries.ts +298 -0
  210. package/packages/frame/src/ui/previews.ts +72 -0
  211. package/packages/frame/src/ui/queue.ts +75 -0
  212. package/packages/frame/src/ui/screen.ts +59 -0
  213. package/packages/frame/src/ui/timings.ts +48 -0
  214. package/packages/frame/src/ui/todos.ts +205 -0
  215. package/packages/frame/src/ui/tool-frame.ts +595 -0
  216. package/packages/frame/src/ui/working.ts +25 -0
  217. package/packages/frame/src/view/state.ts +97 -0
  218. package/packages/frame/src/view/transcript.ts +175 -0
  219. package/packages/frame/src/view/view.ts +162 -0
  220. package/packages/memory/CONTRIBUTING.md +7 -0
  221. package/packages/memory/LICENSE +21 -0
  222. package/packages/memory/README.md +44 -0
  223. package/packages/memory/SECURITY.md +20 -0
  224. package/packages/memory/package.json +55 -0
  225. package/packages/memory/skills/review-memory/SKILL.md +48 -0
  226. package/packages/memory/src/index.ts +163 -0
  227. package/packages/memory/src/prompt.ts +25 -0
  228. package/packages/memory/src/review.ts +150 -0
  229. package/packages/memory/src/secrets.ts +22 -0
  230. package/packages/memory/src/store.ts +173 -0
  231. package/packages/modes/CHANGELOG.md +104 -0
  232. package/packages/modes/CONTRIBUTING.md +7 -0
  233. package/packages/modes/LICENSE +21 -0
  234. package/packages/modes/README.md +174 -0
  235. package/packages/modes/SECURITY.md +17 -0
  236. package/packages/modes/package.json +55 -0
  237. package/packages/modes/skills/review-permissions/SKILL.md +54 -0
  238. package/packages/modes/src/bash.ts +218 -0
  239. package/packages/modes/src/deny-off.ts +62 -0
  240. package/packages/modes/src/git-safety.ts +72 -0
  241. package/packages/modes/src/index.ts +338 -0
  242. package/packages/modes/src/interview.ts +206 -0
  243. package/packages/modes/src/judge-gate.ts +113 -0
  244. package/packages/modes/src/modes.ts +121 -0
  245. package/packages/modes/src/review/apply.mjs +156 -0
  246. package/packages/modes/src/review/collect.mjs +208 -0
  247. package/packages/modes/src/review/tools.ts +71 -0
  248. package/packages/modes/src/sandbox.ts +37 -0
  249. package/packages/router/CHANGELOG.md +8 -0
  250. package/packages/router/LICENSE +21 -0
  251. package/packages/router/README.md +90 -0
  252. package/packages/router/package.json +51 -0
  253. package/packages/router/src/config.ts +65 -0
  254. package/packages/router/src/index.ts +172 -0
  255. package/packages/router/src/judge.ts +51 -0
  256. package/packages/router/src/route.ts +67 -0
  257. package/packages/web/CHANGELOG.md +36 -0
  258. package/packages/web/LICENSE +21 -0
  259. package/packages/web/README.md +87 -0
  260. package/packages/web/SECURITY.md +13 -0
  261. package/packages/web/package.json +59 -0
  262. package/packages/web/src/config.ts +109 -0
  263. package/packages/web/src/dnsfilter.ts +97 -0
  264. package/packages/web/src/extract.ts +65 -0
  265. package/packages/web/src/index.ts +217 -0
  266. package/packages/web/src/markdown.ts +122 -0
  267. package/packages/web/src/net.ts +109 -0
  268. package/packages/web/src/reader.ts +73 -0
  269. package/packages/web/src/sanitize.ts +50 -0
  270. package/packages/web/src/search.ts +76 -0
  271. package/packages/web/src/secrets.ts +22 -0
  272. package/packages/web/src/validate.ts +186 -0
  273. package/src/index.ts +156 -0
  274. package/src/settings.ts +79 -0
@@ -0,0 +1,239 @@
1
+ /**
2
+ * Terminal query sequence extraction and color-scheme report mapping for the
3
+ * PTY attach client (issue #128).
4
+ *
5
+ * The attach client rebuilds the child screen in a headless terminal model, so
6
+ * the child's terminal capability queries (OSC 11 background color, color
7
+ * scheme change notifications) are consumed locally and never reach the real
8
+ * terminal. extractOscQuerySequences pulls exactly those query/notify forms
9
+ * out of the child output stream so the attach client can forward them to the
10
+ * real terminal; the replies travel back through the existing input
11
+ * passthrough (handleInput fallback). Setting forms (e.g. `\x1b]11;rgb:...`)
12
+ * are deliberately NOT extracted: forwarding those would repaint the local
13
+ * terminal's own colors.
14
+ *
15
+ * Kitty keyboard protocol / DA1 negotiation (`\x1b[>7u`, `\x1b[?u`, `\x1b[c`)
16
+ * is intentionally out of scope: the child negotiates once at spawn, before
17
+ * any attach client exists, and pushing kitty flags would retune the shared
18
+ * real-terminal keyboard stack under the local Pi's feet.
19
+ *
20
+ * Pure functions only — no I/O, no pi-tui import (the report format below is
21
+ * pinned by unit tests that round-trip it through pi-tui's parser).
22
+ */
23
+
24
+ // OSC 11 background-color QUERY. The `?` payload is what makes it a query;
25
+ // the SET form `\x1b]11;rgb:...` never matches this prefix (`;r` vs `;?`).
26
+ const OSC11_QUERY_PREFIX = "\x1b]11;?";
27
+ const OSC11_QUERY_BEL = OSC11_QUERY_PREFIX + "\x07";
28
+ const OSC11_QUERY_ST = OSC11_QUERY_PREFIX + "\x1b\\";
29
+
30
+ // Color-scheme change notification switch (host Pi toggles it at startup and
31
+ // when its own setting changes; the real terminal then reports scheme changes
32
+ // as `\x1b[?997;{1|2}n` reports). Fixed-form CSI, no variadic fields.
33
+ const COLOR_SCHEME_NOTIFY_ON = "\x1b[?2031h";
34
+ const COLOR_SCHEME_NOTIFY_OFF = "\x1b[?2031l";
35
+
36
+ // Carry slack aligned with the extractor family in pty-attach.ts (payload cap
37
+ // + 4096). Our longest target is 8 bytes, so 4096 is ample even for a
38
+ // malformed no-terminator tail, and slice(-N) keeps the carry bounded.
39
+ export const OSC_QUERY_CARRY_MAX_BYTES = 4096;
40
+
41
+ const TARGETS = [OSC11_QUERY_PREFIX, COLOR_SCHEME_NOTIFY_ON, COLOR_SCHEME_NOTIFY_OFF];
42
+ const MAX_TARGET_LENGTH = Math.max(...TARGETS.map((t) => t.length));
43
+
44
+ /**
45
+ * Extract forwardable terminal query / notify sequences from a child output
46
+ * chunk. Returns the sequences (in order) and the carry: the unmatched tail
47
+ * to prepend to the next chunk. The carry is either a target prefix suffix
48
+ * (sequence split across chunks) or an unterminated OSC 11 query waiting for
49
+ * its BEL/ST terminator.
50
+ */
51
+ export function extractOscQuerySequences(input) {
52
+ const sequences = [];
53
+ let scanFrom = 0;
54
+ let carryStart = -1;
55
+ while (scanFrom < input.length) {
56
+ const osc = input.indexOf(OSC11_QUERY_PREFIX, scanFrom);
57
+ const notifyOn = input.indexOf(COLOR_SCHEME_NOTIFY_ON, scanFrom);
58
+ const notifyOff = input.indexOf(COLOR_SCHEME_NOTIFY_OFF, scanFrom);
59
+ const start = firstIndex(firstIndex(osc, notifyOn), notifyOff);
60
+ if (start < 0) break;
61
+ if (start === osc) {
62
+ const after = start + OSC11_QUERY_PREFIX.length;
63
+ const bel = input.indexOf("\x07", after);
64
+ const st = input.indexOf("\x1b\\", after);
65
+ const end = firstTerminator(bel, st);
66
+ if (!end) {
67
+ carryStart = start;
68
+ break;
69
+ }
70
+ const [endIndex, terminatorLength] = end;
71
+ const seq = input.slice(start, endIndex + terminatorLength);
72
+ // Strict form: an OSC 11 query has no payload between `?` and the
73
+ // terminator. Anything else (e.g. `\x1b]11;?<junk>\x07`) is not a
74
+ // query we forward.
75
+ if (seq === OSC11_QUERY_BEL || seq === OSC11_QUERY_ST) sequences.push(seq);
76
+ scanFrom = endIndex + terminatorLength;
77
+ } else {
78
+ // Fixed-form CSI notify switch: an indexOf hit is always complete.
79
+ const seq = start === notifyOn ? COLOR_SCHEME_NOTIFY_ON : COLOR_SCHEME_NOTIFY_OFF;
80
+ sequences.push(seq);
81
+ scanFrom = start + seq.length;
82
+ }
83
+ }
84
+ const carry = carryStart >= 0
85
+ ? input.slice(carryStart).slice(-OSC_QUERY_CARRY_MAX_BYTES)
86
+ : queryPrefixSuffix(input);
87
+ return { sequences, carry };
88
+ }
89
+
90
+ /**
91
+ * Longest suffix of `input` that is a strict prefix of one of our target
92
+ * sequences (empty when none matches). Used as the carry when every target
93
+ * occurrence in this chunk was complete.
94
+ */
95
+ function queryPrefixSuffix(input) {
96
+ const max = Math.min(input.length, MAX_TARGET_LENGTH - 1);
97
+ for (let len = max; len > 0; len--) {
98
+ const suffix = input.slice(-len);
99
+ if (TARGETS.some((t) => t.startsWith(suffix))) return suffix;
100
+ }
101
+ return "";
102
+ }
103
+
104
+ function firstIndex(a, b) {
105
+ if (a < 0) return b;
106
+ if (b < 0) return a;
107
+ return Math.min(a, b);
108
+ }
109
+
110
+ function firstTerminator(bel, st) {
111
+ if (bel < 0 && st < 0) return null;
112
+ if (bel >= 0 && (st < 0 || bel < st)) return [bel, 1];
113
+ return [st, 2];
114
+ }
115
+
116
+ // Color-scheme report expected by pi-tui's parseTerminalColorSchemeReport
117
+ // (node_modules/@earendil-works/pi-tui/dist/terminal-colors.js:19):
118
+ // /^\x1b\[\?997;(1|2)n$/ with "2" => light, "1" => dark (line 57). Note the
119
+ // report code point is 997 — the QUERY the host sends is `\x1b[?996n`.
120
+ const COLOR_SCHEME_REPORTS = {
121
+ light: "\x1b[?997;2n",
122
+ dark: "\x1b[?997;1n",
123
+ };
124
+
125
+ /**
126
+ * Map a color scheme ("light" | "dark") to the report sequence the host Pi's
127
+ * pi-tui parser consumes. Returns "" for anything else; callers skip sending
128
+ * on empty.
129
+ */
130
+ export function toColorSchemeReport(scheme) {
131
+ return Object.hasOwn(COLOR_SCHEME_REPORTS, scheme) ? COLOR_SCHEME_REPORTS[scheme] : "";
132
+ }
133
+
134
+ /**
135
+ * Issue #148: derive the color scheme from an OSC 11 background reply, so the
136
+ * attach client can answer the scheme question itself instead of relying on
137
+ * the child's own probe being resolved (Pi's leaked pending state swallows the
138
+ * first late reply — see the issue for the exact swallow path).
139
+ *
140
+ * Mirrors Pi's own threshold (`getRgbColorLuminance` + `>= 0.5` in the theme
141
+ * detection code) so a client-side answer matches what Pi would have
142
+ * concluded from the same reply. Relative luminance per WCAG/sRGB.
143
+ *
144
+ * @param rgb - parsed reply channels ({r,g,b} in 0-255), or undefined.
145
+ * @returns "light" / "dark", or undefined when the input is not a usable rgb.
146
+ */
147
+ export function colorSchemeForBackgroundRgb(rgb) {
148
+ if (!rgb || typeof rgb !== "object") return undefined;
149
+ const { r, g, b } = rgb;
150
+ if (![r, g, b].every((channel) => Number.isFinite(channel))) return undefined;
151
+ const luminance = 0.2126 * toLinearChannel(r) + 0.7152 * toLinearChannel(g) + 0.0722 * toLinearChannel(b);
152
+ return luminance >= 0.5 ? "light" : "dark";
153
+ }
154
+
155
+ function toLinearChannel(channel) {
156
+ const value = channel / 255;
157
+ return value <= 0.03928 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
158
+ }
159
+
160
+ // --- Attach settle scheme probe (issue #161) -------------------------------
161
+ // pi-tui >= 0.99 renamed queryTerminalBackgroundColor -> queryTerminalColors.
162
+ // This repo compiles against pi-tui 0.79.8 but the extension runs against the
163
+ // host pi's pi-tui (peerDependencies "*"), so the probe feature-detects at
164
+ // runtime. Local structural typedefs only — TerminalColors does not exist on
165
+ // the 0.79.8 compile face and must not be imported.
166
+
167
+ /** @typedef {{ r: number, g: number, b: number }} SettleRgb */
168
+ /** @typedef {{ background?: SettleRgb, foreground?: SettleRgb, palette?: SettleRgb[] }} TerminalColorsLike */
169
+ /** @typedef {{ queryTerminalColors?: (opts: { timeoutMs: number, onLateReply?: (colors: TerminalColorsLike) => void }) => Promise<TerminalColorsLike>, queryTerminalBackgroundColor?: (opts: { timeoutMs: number }) => Promise<SettleRgb|undefined> }} SettleProbeSurface */
170
+ /** @typedef {{ rgb?: SettleRgb, anyColors: boolean }} SettleProbeResult */
171
+
172
+ /**
173
+ * Issue #161: detect which terminal color query API a TUI surface exposes.
174
+ * Detection is pure; the returned adapter only calls the passed-in surface.
175
+ * @param {SettleProbeSurface} tuiLike
176
+ * @returns {{ api: "colors"|"background"|"none", invoke: (tui: SettleProbeSurface, timeoutMs: number, onLateReply?: (colors: TerminalColorsLike) => void) => Promise<SettleProbeResult> }}
177
+ */
178
+ export function resolveProbeApi(tuiLike) {
179
+ if (tuiLike && typeof tuiLike.queryTerminalColors === "function") {
180
+ return {
181
+ api: "colors",
182
+ invoke: async (tui, timeoutMs, onLateReply) => {
183
+ const colors = await tui.queryTerminalColors({ timeoutMs, onLateReply });
184
+ return { rgb: backgroundRgbFromTerminalColors(colors), anyColors: hasAnyColor(colors) };
185
+ },
186
+ };
187
+ }
188
+ if (tuiLike && typeof tuiLike.queryTerminalBackgroundColor === "function") {
189
+ return {
190
+ api: "background",
191
+ invoke: async (tui, timeoutMs) => {
192
+ const rgb = await tui.queryTerminalBackgroundColor({ timeoutMs });
193
+ return { rgb, anyColors: rgb !== undefined };
194
+ },
195
+ };
196
+ }
197
+ return { api: "none", invoke: async () => ({ rgb: undefined, anyColors: false }) };
198
+ }
199
+
200
+ /** @param {TerminalColorsLike} colors @returns {SettleRgb|undefined} */
201
+ export function backgroundRgbFromTerminalColors(colors) {
202
+ return colors && typeof colors === "object" ? colors.background : undefined;
203
+ }
204
+
205
+ /** @param {TerminalColorsLike} colors @returns {boolean} */
206
+ function hasAnyColor(colors) {
207
+ if (!colors || typeof colors !== "object") return false;
208
+ if (colors.background !== undefined || colors.foreground !== undefined) return true;
209
+ return Array.isArray(colors.palette) && colors.palette.some((c) => c !== undefined);
210
+ }
211
+
212
+ const SETTLE_SCHEME_WARN_OUTCOMES = new Set(["timeout", "error", "no_background", "no_probe_api", "dropped_disconnected"]);
213
+
214
+ /**
215
+ * Issue #161: build the DiagnosticEvent-compatible patch for the settle scheme
216
+ * flow. Field names MUST stay inside normalizeDiagnostic's whitelist
217
+ * (level/code/runId/source/message/details) — anything else is silently
218
+ * dropped. Warn only when delivery was expected but failed; routine outcomes
219
+ * (dedup, quick detach, kill switch) stay info so warningCount stays meaningful.
220
+ * @param {{ probeApi?: "colors"|"background"|"none", outcome: "reported"|"timeout"|"error"|"no_background"|"no_probe_api"|"dropped_closed"|"dropped_disconnected"|"duplicate_skipped"|"suppressed", report?: string, late?: boolean }} details
221
+ * @returns {{ source: string, level: "info"|"warn", code: string, message: string, details: { probeApi?: string, outcome: string, report?: string, late?: boolean } }}
222
+ */
223
+ export function buildSettleSchemePatch(details) {
224
+ const parts = [details.outcome];
225
+ if (details.probeApi) parts.push(`via ${details.probeApi}`);
226
+ if (details.report) parts.push(details.report);
227
+ if (details.late) parts.push("late");
228
+ const patch = {
229
+ source: "attach",
230
+ level: SETTLE_SCHEME_WARN_OUTCOMES.has(details.outcome) ? "warn" : "info",
231
+ code: "attach_settle_scheme",
232
+ message: `attach settle scheme: ${parts.join(" ")}`,
233
+ details: { outcome: details.outcome },
234
+ };
235
+ if (details.probeApi !== undefined) patch.details.probeApi = details.probeApi;
236
+ if (details.report !== undefined) patch.details.report = details.report;
237
+ if (details.late !== undefined) patch.details.late = details.late;
238
+ return patch;
239
+ }
@@ -0,0 +1,222 @@
1
+ /**
2
+ * Inbound terminal reply guard for the PTY attach client (issue #170).
3
+ *
4
+ * pi-tui hands replies to forwarded terminal queries to this overlay as input
5
+ * fragments: the prefix arms a 50ms sequence timeout in StdinBuffer and a
6
+ * 150ms negotiation flush in ProcessTerminal, so a CSI-shaped reply head is
7
+ * emitted as an input event and the remaining fragments arrive as plain text.
8
+ * The overlay's passthrough then feeds that garbage into the child's editor
9
+ * (upstream earendil-works/pi#10657 — a DA1 reply arrives as four reads and
10
+ * shows up in the child as a stable `c7c`).
11
+ *
12
+ * This module classifies every inbound event and the state machine below
13
+ * swallows reply fragments until the sequence completes. Color replies (OSC
14
+ * 4/10/11/12 and the 997 scheme report) are forwarded as ONE whole sequence,
15
+ * because the child's pi-tui parsers anchor `^...$`; everything else falls
16
+ * through untouched (issue #128 decision: the child negotiates at spawn, a late
17
+ * DA/kitty reply is meaningless to it, and forwarding kitty flags would retune
18
+ * the shared real-terminal keyboard stack).
19
+ *
20
+ * Issue #213: `\x1b[6;` is not proof of a reply — kitty's functional-key encoding puts
21
+ * PageDown at `CSI 6;<mods>u` (and modifyOtherKeys at `\x1b[6;<mods>~`). The rule is
22
+ * therefore key-first: a complete CSI key sequence (`u`/`~` terminated) beats the head
23
+ * prefixes, so the head list is a heuristic rather than an invariant.
24
+ * Fragments without a terminator still arm.
25
+ *
26
+ * Pure only: no I/O, no timers, no `process.env`, no pi-tui import. Time is
27
+ * injected by the caller (see `armReplyGuardTimer` in src/ui/pty-attach.ts).
28
+ *
29
+ * Known boundary: a FRAGMENTED DA3 reply (`\x1b[=...c`) is not armed — this
30
+ * module's head list deliberately omits `\x1b[=` because the attach client
31
+ * never queries DA3 — so such a fragment passes through as it did before this
32
+ * guard. The complete DA3 form is still dropped (it is a DROP_SHAPES entry).
33
+ *
34
+ * Known trade-offs in the head list (raised in review): `\x1bP` is also the
35
+ * legacy xterm encoding of Alt+Shift+P on terminals without the kitty
36
+ * protocol, so that chord can arm a window and absorb up to the capped bytes
37
+ * of plain text typed right after it. It is kept because a fragmented DCS
38
+ * reply (XTVersion) is otherwise unprotected, and the absorption is bounded
39
+ * (64 bytes / 250ms) and kill-switchable. `\x1b]1` covers the whole OSC 1xx
40
+ * range, so a fragmented reply outside the forwarded 4/10/11/12 set is
41
+ * absorbed rather than leaked — the safe direction for an unrecognized OSC.
42
+ */
43
+
44
+ /** How long an open reply window stays open for trailing fragments (spec §3.3). */
45
+ export const REPLY_GUARD_WINDOW_MS = 250;
46
+
47
+ /** Bound on how much of a user's fast typing an open window may absorb. */
48
+ export const REPLY_GUARD_MAX_ABSORB_BYTES = 64;
49
+
50
+ // Complete reply shapes that are dropped. The child's negotiation happened at
51
+ // spawn, before any attach client existed, so a late reply can only pollute its
52
+ // input or retune keyboards that no longer asked.
53
+ const DROP_SHAPES = [
54
+ // DA1 / DA3 (`\x1b[?...c`, `\x1b[=...c`) and DA2 (`\x1b[>...c`).
55
+ { family: "device-attributes", pattern: /^\x1b\[[?>=][0-9;]*c$/ },
56
+ // kitty keyboard flags reply.
57
+ { family: "kitty-flags", pattern: /^\x1b\[\?[0-9;]*u$/ },
58
+ // XTWINOPS cell-size report (`\x1b[6;<rows>;<cols>t`).
59
+ { family: "cell-size", pattern: /^\x1b\[6;[0-9]+;[0-9]+t$/ },
60
+ // DCS replies such as XTVersion.
61
+ { family: "dcs", pattern: /^\x1bP[\s\S]*\x1b\\$/ },
62
+ ];
63
+
64
+ // Complete reply shapes that are forwarded whole: the child's pi-tui parsers
65
+ // consume them (automatic light/dark theme) and only as one sequence.
66
+ const FORWARD_SHAPES = [
67
+ // `\x1b[?997;<n>n` color-scheme report. Any numeric value (one or more digits)
68
+ // is forwarded — value validation belongs to the child's parser, not to this
69
+ // guard.
70
+ { family: "color-scheme", pattern: /^\x1b\[\?997;[0-9]+n$/ },
71
+ // OSC 4 palette / 10 foreground / 11 background / 12 cursor color replies,
72
+ // BEL- or ST-terminated.
73
+ { family: "osc-color", pattern: /^\x1b\](?:4;[0-9]+|10|11|12);[\s\S]*(?:\x07|\x1b\\)$/ },
74
+ ];
75
+
76
+ // Prefixes that arm an absorption window for a possibly-fragmented reply. These are
77
+ // heuristics, not proofs: `\x1b[6;` also prefixes kitty's PageDown (`CSI 6;<mods>u`,
78
+ // see CSI_KEY_SEQUENCE) and modifyOtherKeys' `\x1b[6;<mods>~`, which is why a COMPLETE
79
+ // CSI key is classified before this list is consulted. Bare `\x1b`, `\x1b[`, `\x1b[<`
80
+ // (SGR mouse) and `\x1bO` (SS3) are deliberately absent.
81
+ const REPLY_HEADS = [
82
+ { family: "device-attributes", prefix: "\x1b[?" },
83
+ { family: "device-attributes", prefix: "\x1b[>" },
84
+ { family: "cell-size", prefix: "\x1b[6;" },
85
+ { family: "dcs", prefix: "\x1bP" },
86
+ { family: "osc-color", prefix: "\x1b]1" },
87
+ { family: "osc-color", prefix: "\x1b]4" },
88
+ ];
89
+
90
+ // Issue #213: kitty keyboard protocol keys that carry a modifier use the legacy
91
+ // functional-key encoding `CSI <num>;<mods>u`, and PageDown's legacy number IS 6
92
+ // (`\x1b[6;1u` = no modifier, the mods field is bitmask+1). modifyOtherKeys uses the
93
+ // same prefix with `~` (`\x1b[6;5~`). Both therefore share the cell-size head prefix
94
+ // (`\x1b[6;`), which used to arm the guard and swallow the key — asymmetric with
95
+ // PageUp (key number 5) and invisible to the previous tests.
96
+ //
97
+ // A COMPLETE CSI key is user input, never a reply fragment: check it before the head
98
+ // prefixes. Only the terminator decides — genuine cell-size fragments (`\x1b[6;`,
99
+ // `\x1b[6;24`) carry no `u`/`~` and still arm below. `(?:[;:]\d+)*` also covers the
100
+ // alternate-key form (`\x1b[6;1:2;65u`), which a narrower `(?:;\d+)?(?::\d+)?` misses.
101
+ const CSI_KEY_SEQUENCE = /^\x1b\[\d+(?:[;:]\d+)*[u~]$/;
102
+
103
+ /**
104
+ * Classify one inbound input event. Complete reply shapes win over head
105
+ * prefixes (a complete kitty reply also starts with `\x1b[?`); escape-led
106
+ * events that are neither are keys, mouse or paste data.
107
+ */
108
+ export function classifyReplyEvent(data) {
109
+ if (typeof data !== "string" || data.length === 0) return { kind: "plain" };
110
+ for (const shape of DROP_SHAPES) {
111
+ if (shape.pattern.test(data)) return { kind: "reply-complete", family: shape.family, action: "drop" };
112
+ }
113
+ for (const shape of FORWARD_SHAPES) {
114
+ if (shape.pattern.test(data)) return { kind: "reply-complete", family: shape.family, action: "forward" };
115
+ }
116
+ // Issue #213: a complete CSI key wins over the head prefixes (see CSI_KEY_SEQUENCE).
117
+ if (CSI_KEY_SEQUENCE.test(data)) return { kind: "other" };
118
+ for (const head of REPLY_HEADS) {
119
+ if (data.startsWith(head.prefix)) return { kind: "reply-head", family: head.family };
120
+ }
121
+ // Everything else that starts with ESC is a key/mouse/paste encoding; plain
122
+ // text may be a reply tail fragment (those never start with ESC after the
123
+ // upstream split, see the field evidence in the issue).
124
+ return data.startsWith("\x1b") ? { kind: "other" } : { kind: "plain" };
125
+ }
126
+
127
+ const SWALLOW = Object.freeze({ action: "swallow" });
128
+ const PASS = Object.freeze({ action: "pass" });
129
+
130
+ /**
131
+ * Issue #170 kill switch: `0`, `off` or `false` (case/space-insensitive)
132
+ * disable the guard; unset or anything else keeps it enabled.
133
+ */
134
+ export function isReplyGuardEnabled(value) {
135
+ if (typeof value !== "string") return true;
136
+ return !["0", "off", "false"].includes(value.trim().toLowerCase());
137
+ }
138
+
139
+ /**
140
+ * Pure reply-fragment state machine (spec §3.3). Two states only — idle and
141
+ * armed — and no timer of its own: the caller feeds `now` and arms a host
142
+ * timer from `pendingSince()`.
143
+ *
144
+ * idle + head → armed (buffer = event, since = now) → swallow
145
+ * idle + complete → idle → drop/forward
146
+ * idle + plain | other → idle → pass
147
+ * armed + plain → armed, buffer += event → swallow
148
+ * armed + plain over cap → idle (buffer and event are dropped) → swallow
149
+ * armed + joined complete → idle → drop/forward joined
150
+ * armed + head → armed (stale buffer dropped) → swallow
151
+ * armed + complete → idle (stale buffer dropped) → handle event alone
152
+ * armed + other → idle (stale buffer dropped) → pass
153
+ * armed + expire in time → idle → swallow
154
+ */
155
+ export function createReplyGuard({
156
+ windowMs = REPLY_GUARD_WINDOW_MS,
157
+ maxAbsorbBytes = REPLY_GUARD_MAX_ABSORB_BYTES,
158
+ } = {}) {
159
+ let armed = false;
160
+ let buffer = "";
161
+ let since = null;
162
+
163
+ const reset = () => {
164
+ armed = false;
165
+ buffer = "";
166
+ since = null;
167
+ };
168
+ const decisionFor = (event, data) => (event.action === "forward" ? { action: "forward", data } : SWALLOW);
169
+
170
+ return {
171
+ feed(data, now) {
172
+ const event = classifyReplyEvent(data);
173
+ if (!armed) {
174
+ if (event.kind === "reply-head") {
175
+ armed = true;
176
+ buffer = data;
177
+ since = now;
178
+ return SWALLOW;
179
+ }
180
+ if (event.kind === "reply-complete") return decisionFor(event, data);
181
+ return PASS;
182
+ }
183
+ if (event.kind === "other") {
184
+ // Keys, mouse and paste win over absorbing one more fragment.
185
+ reset();
186
+ return PASS;
187
+ }
188
+ if (event.kind === "reply-head") {
189
+ // A fresh head means the previous sequence will never complete.
190
+ buffer = data;
191
+ since = now;
192
+ return SWALLOW;
193
+ }
194
+ if (event.kind === "reply-complete") {
195
+ reset();
196
+ return decisionFor(event, data);
197
+ }
198
+ const joined = buffer + data;
199
+ // Bound in UTF-16 code units — byte-exact for the ASCII reply corpus.
200
+ if (joined.length > maxAbsorbBytes) {
201
+ reset();
202
+ return SWALLOW;
203
+ }
204
+ const joinedEvent = classifyReplyEvent(joined);
205
+ if (joinedEvent.kind === "reply-complete") {
206
+ reset();
207
+ return decisionFor(joinedEvent, joined);
208
+ }
209
+ buffer = joined;
210
+ return SWALLOW;
211
+ },
212
+ expire(now) {
213
+ if (!armed || now - since < windowMs) return null;
214
+ reset();
215
+ return SWALLOW;
216
+ },
217
+ pendingSince() {
218
+ return armed ? since : null;
219
+ },
220
+ reset,
221
+ };
222
+ }