@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.
- package/CHANGELOG.md +28 -0
- package/LICENSE +21 -0
- package/README.md +88 -2
- package/SECURITY.md +5 -0
- package/package.json +112 -4
- package/packages/board/CHANGELOG.md +340 -0
- package/packages/board/CONTRIBUTING.md +7 -0
- package/packages/board/LICENSE +21 -0
- package/packages/board/README.md +88 -0
- package/packages/board/SECURITY.md +19 -0
- package/packages/board/docs/how-it-works.md +31 -0
- package/packages/board/docs/reference.md +278 -0
- package/packages/board/package.json +81 -0
- package/packages/board/runner/job-runner-legacy.mjs +68 -0
- package/packages/board/runner/job-runner.mjs +834 -0
- package/packages/board/runner/pty-runner-legacy.mjs +50 -0
- package/packages/board/runner/pty-runner.mjs +2175 -0
- package/packages/board/runner/state-coordinator.mjs +673 -0
- package/packages/board/runner/state-runner.mjs +203 -0
- package/packages/board/runner/title-runner.mjs +80 -0
- package/packages/board/src/commands/agent-board.ts +394 -0
- package/packages/board/src/commands/attach-decision.mjs +66 -0
- package/packages/board/src/commands/attach-flow.ts +251 -0
- package/packages/board/src/commands/bg.ts +103 -0
- package/packages/board/src/commands/board-model.ts +21 -0
- package/packages/board/src/core/atomic.mjs +184 -0
- package/packages/board/src/core/attach-delete.mjs +24 -0
- package/packages/board/src/core/attach-frame-proof.mjs +870 -0
- package/packages/board/src/core/attach-frame-reporter.mjs +648 -0
- package/packages/board/src/core/attach-frame-stream.mjs +266 -0
- package/packages/board/src/core/attach-view-proof.mjs +709 -0
- package/packages/board/src/core/auto-state.mjs +377 -0
- package/packages/board/src/core/background-runs.mjs +273 -0
- package/packages/board/src/core/code-refs-store.mjs +315 -0
- package/packages/board/src/core/code-refs.mjs +913 -0
- package/packages/board/src/core/config.mjs +77 -0
- package/packages/board/src/core/control-protocol.mjs +482 -0
- package/packages/board/src/core/coordinator-client.mjs +633 -0
- package/packages/board/src/core/coordinator-journal.mjs +282 -0
- package/packages/board/src/core/coordinator-protocol.mjs +14 -0
- package/packages/board/src/core/cwd-stats.mjs +114 -0
- package/packages/board/src/core/dashboard-render.mjs +10 -0
- package/packages/board/src/core/derive.mjs +117 -0
- package/packages/board/src/core/diagnostics.mjs +133 -0
- package/packages/board/src/core/double-press.mjs +26 -0
- package/packages/board/src/core/editor-state-reporter.mjs +112 -0
- package/packages/board/src/core/events.mjs +286 -0
- package/packages/board/src/core/evidence.mjs +245 -0
- package/packages/board/src/core/follow-up-queue.mjs +211 -0
- package/packages/board/src/core/foreground-preview-cache.mjs +117 -0
- package/packages/board/src/core/foreground-projection-cache.mjs +99 -0
- package/packages/board/src/core/frame-witness-update.mjs +35 -0
- package/packages/board/src/core/heuristics.mjs +313 -0
- package/packages/board/src/core/hold-input.mjs +27 -0
- package/packages/board/src/core/host-coordination.mjs +300 -0
- package/packages/board/src/core/host-crash.mjs +79 -0
- package/packages/board/src/core/host-probe.mjs +207 -0
- package/packages/board/src/core/host-protocol.mjs +73 -0
- package/packages/board/src/core/ids.mjs +35 -0
- package/packages/board/src/core/ime-cursor-coalesce.mjs +193 -0
- package/packages/board/src/core/invocation.mjs +43 -0
- package/packages/board/src/core/launch-options.mjs +418 -0
- package/packages/board/src/core/launch.mjs +157 -0
- package/packages/board/src/core/load-config.mjs +4 -0
- package/packages/board/src/core/locks.mjs +530 -0
- package/packages/board/src/core/paths.mjs +183 -0
- package/packages/board/src/core/pid.mjs +75 -0
- package/packages/board/src/core/prewarm-schedule.mjs +41 -0
- package/packages/board/src/core/process-identity-probe.mjs +228 -0
- package/packages/board/src/core/prompt-transport.mjs +13 -0
- package/packages/board/src/core/pty-attach-jiggle-controller.mjs +356 -0
- package/packages/board/src/core/pty-attach-jiggle-retry.mjs +109 -0
- package/packages/board/src/core/pty-attach-reconnect.mjs +61 -0
- package/packages/board/src/core/pty-attach-render.mjs +229 -0
- package/packages/board/src/core/pty-links.mjs +81 -0
- package/packages/board/src/core/pty-scroll.mjs +522 -0
- package/packages/board/src/core/pty-support.mjs +327 -0
- package/packages/board/src/core/repo.mjs +103 -0
- package/packages/board/src/core/rows.mjs +414 -0
- package/packages/board/src/core/screen-log-gc.mjs +198 -0
- package/packages/board/src/core/screen-log.mjs +160 -0
- package/packages/board/src/core/session-view.mjs +174 -0
- package/packages/board/src/core/slash-commands.mjs +52 -0
- package/packages/board/src/core/state-commands.mjs +757 -0
- package/packages/board/src/core/status-consistency.mjs +98 -0
- package/packages/board/src/core/steering-prompts.mjs +34 -0
- package/packages/board/src/core/steering.mjs +133 -0
- package/packages/board/src/core/stop-latch.mjs +41 -0
- package/packages/board/src/core/store.mjs +523 -0
- package/packages/board/src/core/suspend.mjs +36 -0
- package/packages/board/src/core/terminal-attach-client.mjs +981 -0
- package/packages/board/src/core/terminal-attach-protocol.mjs +460 -0
- package/packages/board/src/core/terminal-model.mjs +222 -0
- package/packages/board/src/core/terminal-query-sequences.mjs +239 -0
- package/packages/board/src/core/terminal-reply-guard.mjs +222 -0
- package/packages/board/src/core/terminal-snapshot.mjs +440 -0
- package/packages/board/src/core/test-knobs.mjs +28 -0
- package/packages/board/src/core/title.mjs +43 -0
- package/packages/board/src/core/types.mjs +414 -0
- package/packages/board/src/core/warm-host-sweeper.mjs +168 -0
- package/packages/board/src/core/worktree.mjs +65 -0
- package/packages/board/src/index.ts +303 -0
- package/packages/board/src/runtime/service.mjs +3222 -0
- package/packages/board/src/ui/chrome.ts +20 -0
- package/packages/board/src/ui/dashboard-decisions.mjs +55 -0
- package/packages/board/src/ui/dashboard-evidence.mjs +85 -0
- package/packages/board/src/ui/dashboard.ts +2237 -0
- package/packages/board/src/ui/double-left.ts +30 -0
- package/packages/board/src/ui/external-editor.ts +25 -0
- package/packages/board/src/ui/focus.ts +32 -0
- package/packages/board/src/ui/pty-attach.ts +3184 -0
- package/packages/board/src/ui/waiting.ts +30 -0
- package/packages/commands/LICENSE +21 -0
- package/packages/commands/README.md +19 -0
- package/packages/commands/package.json +22 -0
- package/packages/commands/src/index.ts +43 -0
- package/packages/commands/src/security-review.ts +109 -0
- package/packages/frame/CHANGELOG.md +113 -0
- package/packages/frame/CONTRIBUTING.md +7 -0
- package/packages/frame/LICENSE +22 -0
- package/packages/frame/README.md +105 -0
- package/packages/frame/SECURITY.md +16 -0
- package/packages/frame/docs/how-it-works.md +53 -0
- package/packages/frame/docs/reference.md +78 -0
- package/packages/frame/package.json +72 -0
- package/packages/frame/src/data/git.ts +90 -0
- package/packages/frame/src/data/github.ts +140 -0
- package/packages/frame/src/data/gitlab.ts +63 -0
- package/packages/frame/src/data/mode.ts +49 -0
- package/packages/frame/src/data/past-prompts.ts +57 -0
- package/packages/frame/src/data/runner.ts +32 -0
- package/packages/frame/src/data/snapshot.ts +223 -0
- package/packages/frame/src/data/start-notes.ts +104 -0
- package/packages/frame/src/data/telemetry.ts +337 -0
- package/packages/frame/src/data/todos.ts +162 -0
- package/packages/frame/src/data/totals.ts +94 -0
- package/packages/frame/src/data/visits.ts +45 -0
- package/packages/frame/src/index.ts +696 -0
- package/packages/frame/src/kit/LICENSE +21 -0
- package/packages/frame/src/kit/app/attribution.ts +30 -0
- package/packages/frame/src/kit/app/builder.ts +186 -0
- package/packages/frame/src/kit/app/claim.ts +27 -0
- package/packages/frame/src/kit/app/feature.ts +91 -0
- package/packages/frame/src/kit/app/host.ts +214 -0
- package/packages/frame/src/kit/app/renderers.ts +62 -0
- package/packages/frame/src/kit/app/scope.ts +125 -0
- package/packages/frame/src/kit/board/model.ts +337 -0
- package/packages/frame/src/kit/contract.ts +79 -0
- package/packages/frame/src/kit/contracts/accounts.ts +33 -0
- package/packages/frame/src/kit/contracts/ask.ts +120 -0
- package/packages/frame/src/kit/contracts/board.ts +6 -0
- package/packages/frame/src/kit/contracts/by.ts +22 -0
- package/packages/frame/src/kit/contracts/calls.ts +46 -0
- package/packages/frame/src/kit/contracts/events.ts +89 -0
- package/packages/frame/src/kit/contracts/mode.ts +26 -0
- package/packages/frame/src/kit/contracts/names.ts +12 -0
- package/packages/frame/src/kit/contracts/peers.ts +77 -0
- package/packages/frame/src/kit/contracts/prompts.ts +8 -0
- package/packages/frame/src/kit/contracts/screen.ts +75 -0
- package/packages/frame/src/kit/contracts/skills.ts +81 -0
- package/packages/frame/src/kit/contracts/sources.ts +81 -0
- package/packages/frame/src/kit/contracts/views.ts +42 -0
- package/packages/frame/src/kit/control.ts +59 -0
- package/packages/frame/src/kit/dark-theme.ts +41 -0
- package/packages/frame/src/kit/decode.ts +210 -0
- package/packages/frame/src/kit/events/log.ts +313 -0
- package/packages/frame/src/kit/format.ts +25 -0
- package/packages/frame/src/kit/host.ts +4 -0
- package/packages/frame/src/kit/index.ts +25 -0
- package/packages/frame/src/kit/legacy-tasks.ts +39 -0
- package/packages/frame/src/kit/overlay-images.ts +36 -0
- package/packages/frame/src/kit/paths.ts +16 -0
- package/packages/frame/src/kit/screen/client.ts +74 -0
- package/packages/frame/src/kit/screen/feature.ts +64 -0
- package/packages/frame/src/kit/screen/model.ts +182 -0
- package/packages/frame/src/kit/screen/view.ts +838 -0
- package/packages/frame/src/kit/settings/files.ts +90 -0
- package/packages/frame/src/kit/settings/setting.ts +57 -0
- package/packages/frame/src/kit/settings/store.ts +192 -0
- package/packages/frame/src/kit/sources/file.ts +129 -0
- package/packages/frame/src/kit/sources/follow.ts +155 -0
- package/packages/frame/src/kit/testing.ts +260 -0
- package/packages/frame/src/kit/totals.ts +72 -0
- package/packages/frame/src/kit/ui/entry.ts +75 -0
- package/packages/frame/src/kit/ui/frame.ts +212 -0
- package/packages/frame/src/kit/ui/glyphs.ts +132 -0
- package/packages/frame/src/kit/ui/scroll.ts +14 -0
- package/packages/frame/src/render/diff.ts +157 -0
- package/packages/frame/src/render/fit.ts +86 -0
- package/packages/frame/src/render/format.ts +108 -0
- package/packages/frame/src/render/frame.ts +106 -0
- package/packages/frame/src/render/glyphs.ts +189 -0
- package/packages/frame/src/render/keys.ts +43 -0
- package/packages/frame/src/render/lead.ts +37 -0
- package/packages/frame/src/render/paint.ts +135 -0
- package/packages/frame/src/render/safe.ts +19 -0
- package/packages/frame/src/render/segments.ts +735 -0
- package/packages/frame/src/render/streaming.ts +223 -0
- package/packages/frame/src/render/timing.ts +106 -0
- package/packages/frame/src/render/tools.ts +167 -0
- package/packages/frame/src/settings.ts +105 -0
- package/packages/frame/src/ui/editor.ts +272 -0
- package/packages/frame/src/ui/effort.ts +36 -0
- package/packages/frame/src/ui/footer.ts +165 -0
- package/packages/frame/src/ui/header.ts +362 -0
- package/packages/frame/src/ui/history-search.ts +23 -0
- package/packages/frame/src/ui/interrupt.ts +21 -0
- package/packages/frame/src/ui/outputs.ts +170 -0
- package/packages/frame/src/ui/pi-entries.ts +298 -0
- package/packages/frame/src/ui/previews.ts +72 -0
- package/packages/frame/src/ui/queue.ts +75 -0
- package/packages/frame/src/ui/screen.ts +59 -0
- package/packages/frame/src/ui/timings.ts +48 -0
- package/packages/frame/src/ui/todos.ts +205 -0
- package/packages/frame/src/ui/tool-frame.ts +595 -0
- package/packages/frame/src/ui/working.ts +25 -0
- package/packages/frame/src/view/state.ts +97 -0
- package/packages/frame/src/view/transcript.ts +175 -0
- package/packages/frame/src/view/view.ts +162 -0
- package/packages/memory/CONTRIBUTING.md +7 -0
- package/packages/memory/LICENSE +21 -0
- package/packages/memory/README.md +44 -0
- package/packages/memory/SECURITY.md +20 -0
- package/packages/memory/package.json +55 -0
- package/packages/memory/skills/review-memory/SKILL.md +48 -0
- package/packages/memory/src/index.ts +163 -0
- package/packages/memory/src/prompt.ts +25 -0
- package/packages/memory/src/review.ts +150 -0
- package/packages/memory/src/secrets.ts +22 -0
- package/packages/memory/src/store.ts +173 -0
- package/packages/modes/CHANGELOG.md +104 -0
- package/packages/modes/CONTRIBUTING.md +7 -0
- package/packages/modes/LICENSE +21 -0
- package/packages/modes/README.md +174 -0
- package/packages/modes/SECURITY.md +17 -0
- package/packages/modes/package.json +55 -0
- package/packages/modes/skills/review-permissions/SKILL.md +54 -0
- package/packages/modes/src/bash.ts +218 -0
- package/packages/modes/src/deny-off.ts +62 -0
- package/packages/modes/src/git-safety.ts +72 -0
- package/packages/modes/src/index.ts +338 -0
- package/packages/modes/src/interview.ts +206 -0
- package/packages/modes/src/judge-gate.ts +113 -0
- package/packages/modes/src/modes.ts +121 -0
- package/packages/modes/src/review/apply.mjs +156 -0
- package/packages/modes/src/review/collect.mjs +208 -0
- package/packages/modes/src/review/tools.ts +71 -0
- package/packages/modes/src/sandbox.ts +37 -0
- package/packages/router/CHANGELOG.md +8 -0
- package/packages/router/LICENSE +21 -0
- package/packages/router/README.md +90 -0
- package/packages/router/package.json +51 -0
- package/packages/router/src/config.ts +65 -0
- package/packages/router/src/index.ts +172 -0
- package/packages/router/src/judge.ts +51 -0
- package/packages/router/src/route.ts +67 -0
- package/packages/web/CHANGELOG.md +36 -0
- package/packages/web/LICENSE +21 -0
- package/packages/web/README.md +87 -0
- package/packages/web/SECURITY.md +13 -0
- package/packages/web/package.json +59 -0
- package/packages/web/src/config.ts +109 -0
- package/packages/web/src/dnsfilter.ts +97 -0
- package/packages/web/src/extract.ts +65 -0
- package/packages/web/src/index.ts +217 -0
- package/packages/web/src/markdown.ts +122 -0
- package/packages/web/src/net.ts +109 -0
- package/packages/web/src/reader.ts +73 -0
- package/packages/web/src/sanitize.ts +50 -0
- package/packages/web/src/search.ts +76 -0
- package/packages/web/src/secrets.ts +22 -0
- package/packages/web/src/validate.ts +186 -0
- package/src/index.ts +156 -0
- 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
|
+
}
|