@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,229 @@
1
+ import { validateFrameMetadata } from "./attach-frame-proof.mjs";
2
+
3
+ /**
4
+ * Decide whether the next attach repaint should force a full clear.
5
+ *
6
+ * The first attach paint always forces once so the previous dashboard/session surface
7
+ * cannot ghost behind the overlay. Later paints stay differential unless a caller
8
+ * explicitly requests a hard reset.
9
+ */
10
+ export function nextAttachRender(firstPaint, force = false) {
11
+ return {
12
+ force: Boolean(force || firstPaint),
13
+ firstPaint: false,
14
+ };
15
+ }
16
+
17
+ /**
18
+ * Attach output should repaint only after @xterm/headless finishes parsing the chunk.
19
+ * Status/control messages can repaint immediately because they mutate header/overlay state.
20
+ */
21
+ export function shouldScheduleAttachRenderForMessage(type) {
22
+ return type === "hello" || type === "status" || type === "exit" || type === "error";
23
+ }
24
+
25
+ export const ATTACH_OUTPUT_RENDER_INTERVAL_MS = 40;
26
+
27
+ /**
28
+ * Resolve the PTY cursor to an ABSOLUTE buffer row for the projected window
29
+ * [start, start + height). xterm's buffer cursorY is relative to baseY (the child
30
+ * terminal's viewport top when scrolled to bottom), so the absolute row is
31
+ * baseY + cursorY. Returns null when the cursor is outside the projected window
32
+ * (e.g. the user scrolled up into history).
33
+ *
34
+ * The returned row must stay absolute: projection loops pass absolute buffer
35
+ * indices (buf.getLine(i) for i in [start, end)) and mouse selection points are
36
+ * absolute too. Returning a start-relative row only matches while start === 0;
37
+ * once scrollback exists the cursor cell never matches, which silently drops the
38
+ * visible PTY-cursor block AND the CURSOR_MARKER used to position the hardware
39
+ * cursor for IME candidate windows.
40
+ */
41
+ export function projectPtyCursor(buf, start, height) {
42
+ if (typeof buf.cursorX !== "number" || typeof buf.cursorY !== "number" || buf.cursorX < 0 || buf.cursorY < 0) return null;
43
+ const row = buf.baseY + buf.cursorY;
44
+ if (row < start || row >= start + height) return null;
45
+ return { row, col: buf.cursorX };
46
+ }
47
+
48
+ /**
49
+ * Duck-typed read of the child terminal's DECTCEM visibility state.
50
+ *
51
+ * pi-tui hides the hardware cursor (ESC[?25l) on essentially every frame and
52
+ * still parks it for IME positioning, so the xterm cursor position outlives its
53
+ * visibility: it is a rendering byproduct, not a request to show a cursor. The
54
+ * attach projection must not resurrect that parked cell as a visible block, and
55
+ * must equally not hide a cursor the child wants shown (shells, vim,
56
+ * PI_HARDWARE_CURSOR=1). Only an explicit `true` from xterm's cursor service
57
+ * counts as hidden; anything unknown (renamed internals, another @xterm build)
58
+ * falls back to visible, which is the pre-#102 behavior.
59
+ */
60
+ export function isPtyCursorHidden(term) {
61
+ try {
62
+ return term?._core?.coreService?.isCursorHidden === true;
63
+ } catch {
64
+ return false;
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Coalesce PTY parser callbacks into a bounded stream of repaint requests.
70
+ *
71
+ * node-pty commonly splits one child-TUI update across many small chunks. Rendering
72
+ * after each @xterm/headless write callback exposes those intermediate frames and can
73
+ * drive the outer TUI near its 60 fps limit. A trailing throttle keeps output live
74
+ * while presenting only parsed snapshots at roughly 25 fps.
75
+ */
76
+ export function createAttachOutputRenderScheduler(requestRender, delayMs = ATTACH_OUTPUT_RENDER_INTERVAL_MS) {
77
+ let timer = null;
78
+ let disposed = false;
79
+ return {
80
+ request() {
81
+ if (disposed || timer) return;
82
+ timer = setTimeout(() => {
83
+ timer = null;
84
+ if (!disposed) requestRender();
85
+ }, delayMs);
86
+ timer.unref?.();
87
+ },
88
+ cancel() {
89
+ if (timer) clearTimeout(timer);
90
+ timer = null;
91
+ },
92
+ dispose() {
93
+ disposed = true;
94
+ if (timer) clearTimeout(timer);
95
+ timer = null;
96
+ },
97
+ };
98
+ }
99
+
100
+ /**
101
+ * Classify the PTY cursor's alignment for runtime desync detection (issue #11).
102
+ *
103
+ * Healthy idle pi: the child pi-tui parks the hardware cursor on the editor
104
+ * marker, whose cell is the inverse-video "fake cursor" — so the cursor cell
105
+ * itself is inverse. A desynced buffer leaves the cursor parked elsewhere
106
+ * (typically where the last differential write ended), on a non-inverse cell.
107
+ * Width-0 cells are CJK continuation cells and out-of-range columns sit past
108
+ * the line's cells; in both cases the meaningful attribute lives on the
109
+ * preceding cell, so we look left. Returns:
110
+ * "aligned" — cursor resolves to an inverse cell (healthy);
111
+ * "misaligned" — cursor resolves to a non-inverse cell (candidate desync;
112
+ * callers gate this with an output-quietness window);
113
+ * "unknown" — no cursor (scrolled out of the projected viewport) or no
114
+ * buffer line (defensive); never treat these as desync.
115
+ */
116
+ export function detectCursorDesync(buf, cursor) {
117
+ if (!cursor) return "unknown";
118
+ const line = buf.getLine(cursor.row);
119
+ if (!line) return "unknown";
120
+ let x = cursor.col;
121
+ let cell = line.getCell(x);
122
+ while ((!cell || cell.getWidth() === 0) && x > 0) {
123
+ x--;
124
+ cell = line.getCell(x);
125
+ }
126
+ if (!cell) return "misaligned";
127
+ return cell.isInverse() ? "aligned" : "misaligned";
128
+ }
129
+
130
+ /**
131
+ * Source-comparison gate for a committed view projection (spec §4.2/§4.3).
132
+ * A projection may be shown only while every identity axis still matches the
133
+ * current connection/baseline/parser/geometry target. Missing/unknown facts and
134
+ * a closed component always fail closed.
135
+ */
136
+ export function canPublishAttachView(viewIdentity, currentIdentity) {
137
+ if (viewIdentity === null || typeof viewIdentity !== "object" || Array.isArray(viewIdentity)) return false;
138
+ if (currentIdentity === null || typeof currentIdentity !== "object" || Array.isArray(currentIdentity)) return false;
139
+ if (viewIdentity.closed === true || currentIdentity.closed === true) return false;
140
+ const known = (value) => value !== null && value !== undefined && !(typeof value === "string" && value.length === 0) && !(typeof value === "number" && !Number.isFinite(value));
141
+ for (const key of ["connectionToken", "baselineToken", "generation", "parser", "geometryRevision", "targetCols", "targetRows"]) {
142
+ if (!known(viewIdentity[key]) || !known(currentIdentity[key])) return false;
143
+ if (viewIdentity[key] !== currentIdentity[key]) return false;
144
+ }
145
+ if (!Number.isInteger(viewIdentity.targetCols) || viewIdentity.targetCols < 1) return false;
146
+ if (!Number.isInteger(viewIdentity.targetRows) || viewIdentity.targetRows < 1) return false;
147
+ if (!Number.isInteger(viewIdentity.geometryRevision) || viewIdentity.geometryRevision < 0) return false;
148
+ return true;
149
+ }
150
+
151
+ /**
152
+ * Pure §4 presentation decision. It consumes only immutable facts — no clock,
153
+ * timer, terminal or I/O. `deadlineState:"expired"` can only change a WAIT
154
+ * reason; it can never upgrade an incomplete view to publish. Legacy decisions
155
+ * stay independent of proof facts.
156
+ *
157
+ * @param {{
158
+ * closed?: boolean,
159
+ * mode?: "undecided" | "protocol" | "legacy" | "raw" | string,
160
+ * baselineState?: "unverified" | "hydrating" | "valid-empty" | "valid-content" | "undecided" | string,
161
+ * localParseComplete?: boolean,
162
+ * frameState?: "unknown" | "open" | "complete" | string,
163
+ * openSyncBlock?: boolean,
164
+ * openCandidate?: boolean,
165
+ * contentAvailable?: boolean,
166
+ * contentKind?: "complete" | "boot" | "raw" | string,
167
+ * localSeq?: number,
168
+ * viewProof?: any,
169
+ * viewIdentity?: any,
170
+ * currentIdentity?: any,
171
+ * targetCols?: number,
172
+ * targetRows?: number,
173
+ * geometryRevision?: number,
174
+ * requiredGeometryRevision?: number,
175
+ * resizeIntentRequired?: boolean,
176
+ * resizeIntentSatisfied?: boolean,
177
+ * intentId?: number,
178
+ * deadlineState?: "pending" | "expired",
179
+ * }} state
180
+ * @returns {{ action: "closed" | "legacy" | "wait" | "publish", reason: string }}
181
+ */
182
+ export function decideAttachPresentation(state) {
183
+ if (state === null || typeof state !== "object" || Array.isArray(state)) return { action: "wait", reason: "state-missing" };
184
+ if (state.closed === true) return { action: "closed", reason: "closed" };
185
+ if (state.mode === "closed") return { action: "closed", reason: "closed" };
186
+ if (state.mode === "legacy") return { action: "legacy", reason: "legacy-mode" };
187
+ const wait = (reason) => ({
188
+ action: "wait",
189
+ reason: state.deadlineState === "expired" ? `deadline-expired:${reason}` : reason,
190
+ });
191
+ if (state.mode !== "protocol") return wait(`mode-${state.mode ?? "unknown"}`);
192
+ if (state.baselineState !== "valid-content" && state.baselineState !== "valid-empty") {
193
+ if (state.baselineState === "hydrating") return wait("baseline-hydrating");
194
+ if (state.baselineState === "unverified") return wait("baseline-unverified");
195
+ return wait("baseline-unknown");
196
+ }
197
+ if (state.localParseComplete !== true) return wait("local-parse-incomplete");
198
+ if (state.frameState === "open") return wait("sync-open");
199
+ if (state.frameState !== "complete") return wait("frame-incomplete");
200
+ if (state.openSyncBlock === true) return wait("sync-open");
201
+ if (state.openCandidate === true) return wait("candidate-open");
202
+ if (state.baselineState === "valid-empty") {
203
+ if (!Number.isInteger(state.localSeq) || state.localSeq < 1) return wait("empty-baseline-needs-live-complete");
204
+ }
205
+ if (state.contentKind === "boot") return wait("boot-content");
206
+ if (state.contentKind === "raw") return wait("raw-content");
207
+ if (state.contentKind !== "complete") return wait("content-kind-unknown");
208
+ if (state.contentAvailable !== true) return wait("content-missing");
209
+ if (!Number.isInteger(state.targetCols) || state.targetCols < 1) return wait("geometry-unknown");
210
+ if (!Number.isInteger(state.targetRows) || state.targetRows < 1) return wait("geometry-unknown");
211
+ const coverage = state.viewProof;
212
+ if (coverage === null || typeof coverage !== "object" || Array.isArray(coverage)) return wait("view-proof-missing");
213
+ if (coverage.status !== "covered") return wait(`view-proof-${coverage.status ?? "unknown"}`);
214
+ if (!Number.isInteger(coverage.throughSeq)) return wait("view-proof-invalid");
215
+ if (!Number.isInteger(state.localSeq) || coverage.throughSeq < state.localSeq) return wait("view-proof-behind");
216
+ if (coverage.throughSeq > state.localSeq) return wait("view-proof-future");
217
+ if (coverage.proof === null || typeof coverage.proof !== "object" || Array.isArray(coverage.proof)) return wait("view-proof-missing");
218
+ if (coverage.proof.marker !== "F" || !Number.isInteger(coverage.proof.seq) || coverage.proof.seq > coverage.throughSeq) return wait("view-proof-invalid");
219
+ const metadata = validateFrameMetadata(coverage.proof.metadata);
220
+ if (!metadata.ok) return wait("view-proof-invalid");
221
+ if (metadata.value.cols !== state.targetCols || metadata.value.rows !== state.targetRows) return wait("proof-geometry-mismatch");
222
+ if (state.requiredGeometryRevision !== undefined && metadata.value.geometryRevision !== state.requiredGeometryRevision) return wait("geometry-revision");
223
+ if (state.resizeIntentRequired === true) {
224
+ if (state.resizeIntentSatisfied !== true) return wait("resize-intent");
225
+ if (metadata.value.intentId !== state.intentId || metadata.value.intentId === 0) return wait("resize-intent");
226
+ }
227
+ if (!canPublishAttachView(state.viewIdentity, state.currentIdentity)) return wait("identity-mismatch");
228
+ return { action: "publish", reason: "complete" };
229
+ }
@@ -0,0 +1,81 @@
1
+ const HTTP_URL_RE = /https?:\/\/[^\s<>"'`]+/g;
2
+ const WORD_CHAR_RE = /[A-Za-z0-9_]/;
3
+
4
+ /**
5
+ * Whether a link in a session's output may be handed to the system's opener. The text comes from
6
+ * whatever the session printed, and `open` would just as well start an app from a `file:` or custom
7
+ * scheme link, or read a leading `-` as an option.
8
+ * @param {string} target
9
+ */
10
+ export function isOpenableLink(target) {
11
+ return /^https?:\/\/[^\s]/i.test(target);
12
+ }
13
+
14
+ export function trimTrailingUrlPunctuation(value) {
15
+ let url = value;
16
+ while (url.length > 0) {
17
+ if (/[.,;:!?]$/.test(url)) {
18
+ url = url.slice(0, -1);
19
+ continue;
20
+ }
21
+ const last = url.at(-1);
22
+ if (last === ")" && countChar(url, "(") < countChar(url, ")")) {
23
+ url = url.slice(0, -1);
24
+ continue;
25
+ }
26
+ if (last === "]" && countChar(url, "[") < countChar(url, "]")) {
27
+ url = url.slice(0, -1);
28
+ continue;
29
+ }
30
+ if (last === "}" && countChar(url, "{") < countChar(url, "}")) {
31
+ url = url.slice(0, -1);
32
+ continue;
33
+ }
34
+ break;
35
+ }
36
+ return url;
37
+ }
38
+
39
+ export function findHttpUrlRangeAtCells(cells, col) {
40
+ if (!Array.isArray(cells) || col < 0 || col >= cells.length) return null;
41
+ const text = cells.map(normalizeAsciiCell).join("");
42
+ HTTP_URL_RE.lastIndex = 0;
43
+ let match;
44
+ while ((match = HTTP_URL_RE.exec(text))) {
45
+ const url = trimTrailingUrlPunctuation(match[0]);
46
+ const start = match.index;
47
+ const end = start + url.length - 1;
48
+ if (col >= start && col <= end) return { text: url, start, end };
49
+ }
50
+ return null;
51
+ }
52
+
53
+ export function findHttpUrlAtCells(cells, col) {
54
+ return findHttpUrlRangeAtCells(cells, col)?.text ?? null;
55
+ }
56
+
57
+ export function findWordRangeAtCells(cells, col) {
58
+ if (!Array.isArray(cells) || col < 0 || col >= cells.length) return null;
59
+ const url = findHttpUrlRangeAtCells(cells, col);
60
+ if (url) return { ...url, kind: "url" };
61
+ const text = cells.map(normalizeAsciiCell);
62
+ if (!WORD_CHAR_RE.test(text[col] ?? "")) return null;
63
+ let start = col;
64
+ let end = col;
65
+ while (start > 0 && WORD_CHAR_RE.test(text[start - 1] ?? "")) start -= 1;
66
+ while (end + 1 < text.length && WORD_CHAR_RE.test(text[end + 1] ?? "")) end += 1;
67
+ return { text: text.slice(start, end + 1).join(""), start, end, kind: "word" };
68
+ }
69
+
70
+ function normalizeAsciiCell(cell) {
71
+ if (typeof cell !== "string" || cell.length !== 1) return " ";
72
+ return cell >= " " && cell <= "~" ? cell : " ";
73
+ }
74
+
75
+ function countChar(text, char) {
76
+ let count = 0;
77
+ for (const ch of text) {
78
+ if (ch === char) count += 1;
79
+ }
80
+ return count;
81
+ }