@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,522 @@
|
|
|
1
|
+
export function clampInt(value, min, max) {
|
|
2
|
+
return Math.max(min, Math.min(max, Math.floor(value)));
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
/** Lines scrolled per mouse-wheel event in the attach viewport when unset/invalid. */
|
|
6
|
+
export const DEFAULT_WHEEL_LINES = 1;
|
|
7
|
+
/** Lower bound for $AGENT_BOARD_WHEEL_LINES. */
|
|
8
|
+
export const MIN_WHEEL_LINES = 1;
|
|
9
|
+
/** Upper bound for $AGENT_BOARD_WHEEL_LINES. */
|
|
10
|
+
export const MAX_WHEEL_LINES = 50;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Issue #207: the full-screen mouse enable sequence to re-assert, matching pi-tui's own
|
|
14
|
+
* choice. Multiplexers get button-motion only (1000/1002/1004/1006) because any-motion
|
|
15
|
+
* (1003) makes them lag; plain terminals get the full set. Re-asserting the wrong form
|
|
16
|
+
* would either reintroduce multiplexer lag or silently drop hover reporting (1003).
|
|
17
|
+
* Pure by contract: the environment is passed in, never read here.
|
|
18
|
+
*
|
|
19
|
+
* @param {NodeJS.ProcessEnv|Record<string, string|undefined>} [env]
|
|
20
|
+
* @returns {string}
|
|
21
|
+
*/
|
|
22
|
+
export function fullscreenMouseEnableSequence(env = process.env) {
|
|
23
|
+
const term = String(env.TERM ?? "").toLowerCase();
|
|
24
|
+
const multiplexer =
|
|
25
|
+
env.TMUX !== undefined ||
|
|
26
|
+
env.ZELLIJ !== undefined ||
|
|
27
|
+
env.STY !== undefined ||
|
|
28
|
+
term.startsWith("tmux") ||
|
|
29
|
+
term.startsWith("screen");
|
|
30
|
+
return multiplexer
|
|
31
|
+
? "\x1b[?1000h\x1b[?1002h\x1b[?1004h\x1b[?1006h"
|
|
32
|
+
: "\x1b[?1000h\x1b[?1002h\x1b[?1003h\x1b[?1004h\x1b[?1006h";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Resolve how many lines a single mouse-wheel event scrolls in the attach viewport.
|
|
37
|
+
*
|
|
38
|
+
* Reads `AGENT_BOARD_WHEEL_LINES`, clamps it to `[MIN_WHEEL_LINES, MAX_WHEEL_LINES]`,
|
|
39
|
+
* and falls back to `DEFAULT_WHEEL_LINES` when unset, empty, or non-numeric. macOS
|
|
40
|
+
* inertial/momentum scrolling fires a *burst* of wheel events per gesture, so a small
|
|
41
|
+
* value here already scrolls quickly; the previous fixed `5` overshot badly.
|
|
42
|
+
*
|
|
43
|
+
* @param {Record<string, string|undefined>} [env]
|
|
44
|
+
* @returns {number}
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
export function resolveWheelLines(env = process.env) {
|
|
48
|
+
const raw = String(env.AGENT_BOARD_WHEEL_LINES ?? "").trim();
|
|
49
|
+
if (!raw) return DEFAULT_WHEEL_LINES;
|
|
50
|
+
const parsed = Number.parseInt(raw, 10);
|
|
51
|
+
if (!Number.isFinite(parsed)) return DEFAULT_WHEEL_LINES;
|
|
52
|
+
return clampInt(parsed, MIN_WHEEL_LINES, MAX_WHEEL_LINES);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function parseMouseEventPrefix(data, offset = 0) {
|
|
56
|
+
const input = data.slice(offset);
|
|
57
|
+
const sgr = /^\x1b\[(<|\?)(\d+);(\d+);(\d+)([Mm])/.exec(input);
|
|
58
|
+
if (sgr) {
|
|
59
|
+
const button = Number(sgr[2]);
|
|
60
|
+
const col = Number(sgr[3]);
|
|
61
|
+
const row = Number(sgr[4]);
|
|
62
|
+
if (!Number.isFinite(button) || !Number.isFinite(col) || !Number.isFinite(row)) return null;
|
|
63
|
+
return {
|
|
64
|
+
length: sgr[0].length,
|
|
65
|
+
raw: sgr[0],
|
|
66
|
+
mouse: {
|
|
67
|
+
encoding: sgr[1] === "?" ? "passive" : "sgr",
|
|
68
|
+
button,
|
|
69
|
+
col,
|
|
70
|
+
row,
|
|
71
|
+
action: sgr[5] === "m" ? "release" : button & 32 ? "move" : "press",
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// X10/normal mouse: ESC [ M Cb Cx Cy. Cb is encoded as button + 32.
|
|
77
|
+
if (input.startsWith("\x1b[M") && input.length >= 6) {
|
|
78
|
+
// Cb packs the button (bits 0-1), modifiers (2-4), motion (5) and wheel (6),
|
|
79
|
+
// except bit value 3 in bits 0-1: the protocol's release code, which carries
|
|
80
|
+
// no button identity. Decoding it as a press made the attach surface clear
|
|
81
|
+
// the drag selection on release instead of copying it (issue #188). Wheel
|
|
82
|
+
// reports never carry a release, so bit 6 keeps the press classification.
|
|
83
|
+
const button = input.charCodeAt(3) - 32;
|
|
84
|
+
const motion = (button & 32) !== 0;
|
|
85
|
+
const wheel = (button & 64) !== 0;
|
|
86
|
+
const release = !motion && !wheel && (button & 3) === 3;
|
|
87
|
+
return {
|
|
88
|
+
length: 6,
|
|
89
|
+
raw: input.slice(0, 6),
|
|
90
|
+
mouse: {
|
|
91
|
+
encoding: "x10",
|
|
92
|
+
button,
|
|
93
|
+
col: input.charCodeAt(4) - 32,
|
|
94
|
+
row: input.charCodeAt(5) - 32,
|
|
95
|
+
action: release ? "release" : motion ? "move" : "press",
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Parse a terminal mouse report.
|
|
104
|
+
* Supports standard SGR (`CSI < ...`), passive SGR (`CSI ? ...`), and X10 mouse encodings.
|
|
105
|
+
*/
|
|
106
|
+
export function parseMouseEvent(data) {
|
|
107
|
+
const parsed = parseMouseEventPrefix(data);
|
|
108
|
+
return parsed && parsed.length === data.length ? parsed.mouse : null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Read a maximal run of concatenated mouse reports starting at `offset`. Returns the decoded events
|
|
113
|
+
* with their exact raw byte sequences plus the consumed length, or null when no complete report
|
|
114
|
+
* starts there.
|
|
115
|
+
*/
|
|
116
|
+
function parseMouseRun(data, offset = 0) {
|
|
117
|
+
const events = [];
|
|
118
|
+
let position = offset;
|
|
119
|
+
for (;;) {
|
|
120
|
+
const parsed = parseMouseEventPrefix(data, position);
|
|
121
|
+
if (!parsed) break;
|
|
122
|
+
events.push(parsed);
|
|
123
|
+
position += parsed.length;
|
|
124
|
+
}
|
|
125
|
+
return events.length > 0 ? { events, length: position - offset } : null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Parse an input chunk that consists entirely of one or more concatenated mouse reports.
|
|
130
|
+
* Returns each decoded event with its exact raw byte sequence, or null if any non-mouse
|
|
131
|
+
* bytes are present in the chunk (all-or-nothing contract, pinned by unit tests and by the
|
|
132
|
+
* X10 acceptance of issue #188 — do not relax it).
|
|
133
|
+
*/
|
|
134
|
+
export function parseMouseInputChunk(data) {
|
|
135
|
+
if (!data) return null;
|
|
136
|
+
const run = parseMouseRun(data);
|
|
137
|
+
return run && run.length === data.length ? run.events : null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Issue #189: terminal *report* sequences, i.e. bytes the real terminal sends in answer to a query
|
|
142
|
+
* instead of in answer to the user. Every query this stack forwards to the real terminal (child
|
|
143
|
+
* OSC 11 probes, the host's colour batch, kitty protocol negotiation) is answered on the same stdin
|
|
144
|
+
* stream the user types on, so a report is routinely coalesced with keys or mouse reports into one
|
|
145
|
+
* read. Reports carry no user intent: they must reach the child unchanged, but they must not be
|
|
146
|
+
* treated as typing (no selection clear, no viewport reset).
|
|
147
|
+
*
|
|
148
|
+
* Grounding of each pattern (pi-tui 1.x, the face this extension runs against):
|
|
149
|
+
* - focus in/out: DECSET 1004, enabled by pi's fullscreen TUI; reachable in regular mode only
|
|
150
|
+
* through leaked terminal state.
|
|
151
|
+
* - `CSI ? ... c`: DA1 reply — the host sends `\x1b[c` as the trailing sentinel of its colour
|
|
152
|
+
* batch (`pi-tui tui.js:57`).
|
|
153
|
+
* - `CSI ? ... u`: kitty keyboard flag reply to `\x1b[?u` (`pi-tui terminal.js:210`).
|
|
154
|
+
* - `CSI ? ... n`: mode/status reports, including the colour-scheme report `\x1b[?997;1n` that Pi
|
|
155
|
+
* parses (`pi-tui terminal-colors.js:19`).
|
|
156
|
+
* - `CSI ... t`: XTWINOPS replies — the host asks for the cell size with `\x1b[16t`
|
|
157
|
+
* (`pi-tui tui.js:603`) and consumes `CSI 6;h;wt`.
|
|
158
|
+
* - `OSC ... BEL|ST`: colour replies (OSC 10/11/12, OSC 4) and other reply bodies such as the
|
|
159
|
+
* program-status reply (OSC 7501); bodies are capped at 4096 bytes like the query carry.
|
|
160
|
+
* - `CSI > ... c` (DA2) has **no** source in this stack — kept as a defensive extra only, because
|
|
161
|
+
* no keystroke encodes as a `>`-prefixed CSI.
|
|
162
|
+
*
|
|
163
|
+
* Deliberately NOT matched, because a keystroke can look like them: `CSI ... R` (cursor-position
|
|
164
|
+
* report but also modifier-F3), any `u` final without the `?` prefix (kitty key events),
|
|
165
|
+
* `CSI ... $ y` (DECRPM — nothing in this stack queries modes), and DCS replies (no in-repo DCS
|
|
166
|
+
* query). Unrecognised bytes keep today's keyboard semantics.
|
|
167
|
+
*/
|
|
168
|
+
const REPORT_PREFIXES = [
|
|
169
|
+
/^\x1b\[[IO]/,
|
|
170
|
+
/^\x1b\[\?\d+(?:;\d+)*c/,
|
|
171
|
+
/^\x1b\[>\d+(?:;\d+)*c/,
|
|
172
|
+
/^\x1b\[\?\d+(?:;\d+)*u/,
|
|
173
|
+
/^\x1b\[\?\d+(?:;\d+)*n/,
|
|
174
|
+
/^\x1b\[\d+(?:;\d+)*t/,
|
|
175
|
+
/^\x1b\][^\x07\x1b]{0,4096}(?:\x07|\x1b\\)/,
|
|
176
|
+
];
|
|
177
|
+
|
|
178
|
+
/** Issue #189: the complete report sequence starting at `offset`, or null. */
|
|
179
|
+
function parseReportPrefix(data, offset = 0) {
|
|
180
|
+
const input = data.slice(offset);
|
|
181
|
+
for (const pattern of REPORT_PREFIXES) {
|
|
182
|
+
const match = pattern.exec(input);
|
|
183
|
+
if (match) return match[0];
|
|
184
|
+
}
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Issue #189: the maximal run of concatenated report sequences at `offset`, or null. */
|
|
189
|
+
function parseReportRun(data, offset = 0) {
|
|
190
|
+
let position = offset;
|
|
191
|
+
for (;;) {
|
|
192
|
+
const report = parseReportPrefix(data, position);
|
|
193
|
+
if (!report) break;
|
|
194
|
+
position += report.length;
|
|
195
|
+
}
|
|
196
|
+
return position > offset ? data.slice(offset, position) : null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Merge a segment into the previous one when it has the same kind, else append it. */
|
|
200
|
+
function pushSegment(segments, segment) {
|
|
201
|
+
const last = segments[segments.length - 1];
|
|
202
|
+
if (last && last.kind === segment.kind) {
|
|
203
|
+
if (last.kind === "mouse") last.events.push(...segment.events);
|
|
204
|
+
else last.data += segment.data;
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
segments.push(segment);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Issue #189: split one input read into ordered segments so the attach surface can dispatch per
|
|
212
|
+
* kind instead of all-or-nothing. Mouse reports, terminal reports and everything else are returned
|
|
213
|
+
* in arrival order, consecutive segments of the same kind merged.
|
|
214
|
+
*
|
|
215
|
+
* A partial escape sequence (a sequence split across reads) is not recognised and becomes keyboard
|
|
216
|
+
* text — the same bytes the all-or-nothing parser used to forward.
|
|
217
|
+
*
|
|
218
|
+
* @param {string} data
|
|
219
|
+
* @returns {Array<{kind: "mouse", events: Array<{length: number, raw: string, mouse: {encoding: string, button: number, col: number, row: number, action: string}}>} | {kind: "report", data: string} | {kind: "key", data: string}>}
|
|
220
|
+
*/
|
|
221
|
+
export function splitAttachInput(data) {
|
|
222
|
+
if (!data) return [];
|
|
223
|
+
const segments = [];
|
|
224
|
+
let offset = 0;
|
|
225
|
+
while (offset < data.length) {
|
|
226
|
+
const mouse = parseMouseRun(data, offset);
|
|
227
|
+
if (mouse) {
|
|
228
|
+
pushSegment(segments, { kind: "mouse", events: mouse.events });
|
|
229
|
+
offset += mouse.length;
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
const report = parseReportRun(data, offset);
|
|
233
|
+
if (report) {
|
|
234
|
+
pushSegment(segments, { kind: "report", data: report });
|
|
235
|
+
offset += report.length;
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
// Neither a mouse report nor a terminal report: keyboard text up to the next escape.
|
|
239
|
+
const next = data.indexOf("\x1b", offset + 1);
|
|
240
|
+
const end = next === -1 ? data.length : next;
|
|
241
|
+
pushSegment(segments, { kind: "key", data: data.slice(offset, end) });
|
|
242
|
+
offset = end;
|
|
243
|
+
}
|
|
244
|
+
return segments;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Issue #167: map a normalized pi-tui mouse event on the attach surface to an
|
|
249
|
+
* action. "paste-primary" = middle-click paste (read X11 PRIMARY, forward as
|
|
250
|
+
* input); null = not ours, the event falls through untouched (pi's fullscreen
|
|
251
|
+
* selection, the wheel defer path, and everything else keep working). Pure by
|
|
252
|
+
* contract: the env flag is resolved at the call site, never read here.
|
|
253
|
+
*/
|
|
254
|
+
export function resolveAttachMouseAction(event, { nativePasteEnabled }) {
|
|
255
|
+
if (!nativePasteEnabled) return null;
|
|
256
|
+
if (event.type === "press" && event.button === "middle") return "paste-primary";
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Issue #174 (C): which X11 selection buffer middle-click paste reads.
|
|
262
|
+
* "primary" = classic X11 middle-click semantics (default); "clipboard" =
|
|
263
|
+
* escape hatch for environments where PRIMARY never updates (e.g. WSLg).
|
|
264
|
+
* Unknown or unset values fall back to "primary". Pure: the raw env value is
|
|
265
|
+
* read at the call site.
|
|
266
|
+
*/
|
|
267
|
+
export function resolvePasteSelectionSource(value) {
|
|
268
|
+
return typeof value === "string" && value.trim().toLowerCase() === "clipboard" ? "clipboard" : "primary";
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Issue #174 (B2) / #172: whether a normalized mouse event ends a fullscreen text
|
|
273
|
+
* selection gesture the attach overlay must settle — mirror the selection into X11
|
|
274
|
+
* PRIMARY (#174) and drop pi's highlight once copy-on-select has run (#172). Only
|
|
275
|
+
* release/click of the left button ends a selection in TuiAltScreen; pi owns the
|
|
276
|
+
* gesture, so the overlay observes without consuming. Pure: the native-paste kill
|
|
277
|
+
* switch is applied by writePrimarySelection, not here.
|
|
278
|
+
*/
|
|
279
|
+
export function isFullscreenSelectionEnd(event) {
|
|
280
|
+
return (event.type === "release" || event.type === "click") && event.button === "left";
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Issue #169: whether the attach surface should manage the OUTER terminal's
|
|
285
|
+
* mouse mode. Only in regular TUI mode — in fullscreen, TuiAltScreen owns
|
|
286
|
+
* mouse reporting (asserted once at startup, never re-asserted), so any write
|
|
287
|
+
* from the attach surface can only downgrade or kill it. Old runtimes without
|
|
288
|
+
* tui.mode behave as regular. Pure: both arguments are resolved at the call
|
|
289
|
+
* site (mode from the TUI, the flag from the env-derived helper).
|
|
290
|
+
*/
|
|
291
|
+
export function shouldOwnOuterMouseMode(mode, mouseEnabled) {
|
|
292
|
+
return mode !== "fullscreen" && !!mouseEnabled;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Return +1 for wheel-up (scroll back), -1 for wheel-down, 0 for non-wheel input.
|
|
297
|
+
* Supports standard/passive SGR and X10/normal mouse encodings.
|
|
298
|
+
*/
|
|
299
|
+
export function mouseWheelDirection(data) {
|
|
300
|
+
const mouse = parseMouseEvent(data);
|
|
301
|
+
if (!mouse || (mouse.button & 64) === 0) return 0;
|
|
302
|
+
const wheelButton = mouse.button & 3;
|
|
303
|
+
if (wheelButton === 0) return 1;
|
|
304
|
+
if (wheelButton === 1) return -1;
|
|
305
|
+
return 0; // horizontal wheel: ignore
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Issue #173 follow-up: translate a normalized pi-tui wheel event into the attach
|
|
310
|
+
* viewport's line convention.
|
|
311
|
+
*
|
|
312
|
+
* pi-tui reports `wheelDelta` as logical lines where *negative scrolls up* (the SGR
|
|
313
|
+
* wheel-up button decodes to direction -1), while `scrollViewportTop`/`tryScrollBy`
|
|
314
|
+
* take a positive `linesUp`. The renderer's `routeWheel` fallback only moves real
|
|
315
|
+
* `ScrollView` components, and the attach viewport paints its own xterm scrollback
|
|
316
|
+
* instead — so a wheel event we hand back unconsumed scrolls nothing at all and the
|
|
317
|
+
* fullscreen wheel goes dead.
|
|
318
|
+
*
|
|
319
|
+
* Returns 0 for non-wheel events and for a missing, non-finite, or zero delta, so
|
|
320
|
+
* callers can fall through to pi's own handling.
|
|
321
|
+
*
|
|
322
|
+
* @param {{type?: string, wheelDelta?: number}|null|undefined} event
|
|
323
|
+
* @returns {number}
|
|
324
|
+
*/
|
|
325
|
+
export function wheelScrollLinesUp(event) {
|
|
326
|
+
if (!event || event.type !== "wheel") return 0;
|
|
327
|
+
const delta = Number(event.wheelDelta);
|
|
328
|
+
if (!Number.isFinite(delta) || delta === 0) return 0;
|
|
329
|
+
return -delta;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Issue #185: route a normalized wheel event (fullscreen handleMouse path) to the
|
|
334
|
+
* surface that should scroll.
|
|
335
|
+
*
|
|
336
|
+
* "local" — scroll the attach's own xterm scrollback (normal buffer, where the
|
|
337
|
+
* scrollback actually holds history). The caller still downgrades to "none"
|
|
338
|
+
* itself when `tryScrollBy` cannot move: that decision needs the live viewport.
|
|
339
|
+
* "child" — forward one synthesized SGR wheel report instead (alternate
|
|
340
|
+
* buffer: xterm's alternate buffer keeps no scrollback, so the hosted child's
|
|
341
|
+
* own viewport is the only thing that can scroll).
|
|
342
|
+
* "none" — leave the event to pi (kill switches, or an unusable delta).
|
|
343
|
+
*
|
|
344
|
+
* Pure by contract: `bufferType` is `this.term.buffer.active.type` at the call
|
|
345
|
+
* site, `wheelLinesUp` comes from `wheelScrollLinesUp()`, and the switch state
|
|
346
|
+
* is resolved by the caller.
|
|
347
|
+
*
|
|
348
|
+
* @param {{bufferType?: string, wheelLinesUp?: number, mouseEnabled?: boolean}|null|undefined} input
|
|
349
|
+
* @returns {"local"|"child"|"none"}
|
|
350
|
+
*/
|
|
351
|
+
export function resolveAttachWheelRoute({ bufferType, wheelLinesUp, mouseEnabled } = {}) {
|
|
352
|
+
if (!mouseEnabled || wheelLinesUp === 0) return "none";
|
|
353
|
+
return bufferType === "alternate" ? "child" : "local";
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Issue #185: encode one synthesized SGR wheel report for the hosted child.
|
|
358
|
+
*
|
|
359
|
+
* `"up"` maps to button 64 and `"down"` to 65 — the plain report a terminal
|
|
360
|
+
* sends for a wheel notch. Coordinates are 1-based child-screen cells (the
|
|
361
|
+
* attach body rendered 1:1), floored and clamped to >= 1 so a malformed
|
|
362
|
+
* coordinate can never produce an invalid report. One report per received
|
|
363
|
+
* event: the child applies its own wheel acceleration, exactly as it would for
|
|
364
|
+
* a real terminal.
|
|
365
|
+
*
|
|
366
|
+
* `alt` sets SGR bit 3 (value 8) so the child keeps its Alt+wheel accelerator.
|
|
367
|
+
*
|
|
368
|
+
* @param {"up"|"down"} direction
|
|
369
|
+
* @param {{col?: number, row?: number, alt?: boolean}} coords
|
|
370
|
+
* @returns {string}
|
|
371
|
+
*/
|
|
372
|
+
export function encodeWheelReport(direction, { col, row, alt = false } = {}) {
|
|
373
|
+
const base = direction === "up" ? 64 : 65;
|
|
374
|
+
// Issue #214: SGR bit 3 (value 8) is Alt. pi-tui multiplies by 5 when it is set,
|
|
375
|
+
// so the forward hop must carry it. Strict comparison: a truthy non-boolean must
|
|
376
|
+
// not flip the bit.
|
|
377
|
+
const button = alt === true ? base + 8 : base;
|
|
378
|
+
const cell = (v) => (Number.isFinite(v) ? Math.max(1, Math.floor(v)) : 1);
|
|
379
|
+
return `\x1b[<${button};${cell(col)};${cell(row)}M`;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Issue #205: history floor left behind by snapshot hydration.
|
|
384
|
+
*
|
|
385
|
+
* Hydrating a frame taller than the local terminal scrolls its top rows into the
|
|
386
|
+
* local emulator's scrollback. The frame writes genuine scrollback rows first and the
|
|
387
|
+
* viewport rows after them, so an overflowing hydrate lays the local buffer out as
|
|
388
|
+
* `[genuine history][manufactured rows][viewport]`: the manufactured block does not sit
|
|
389
|
+
* below the real history but between it and the viewport, and every local scroll
|
|
390
|
+
* traverses it. No single floor can skip that block while keeping the older genuine
|
|
391
|
+
* rows, so an overflow disables local history entirely rather than hiding the oldest
|
|
392
|
+
* real rows. A hydrate that did not overflow leaves only real history above the
|
|
393
|
+
* viewport, so the base is the floor. Unvalidated input (an older runner that sends no
|
|
394
|
+
* `scrollbackLines`) degrades to the safe floor, never NaN. Pure by contract.
|
|
395
|
+
*
|
|
396
|
+
* @param {number} baseYAfter
|
|
397
|
+
* @param {number|undefined} scrollbackLines
|
|
398
|
+
* @returns {number}
|
|
399
|
+
*/
|
|
400
|
+
export function historyFloorAfterHydrate(baseYAfter, scrollbackLines) {
|
|
401
|
+
const safeBase = Number.isFinite(baseYAfter) ? Math.max(0, Math.floor(baseYAfter)) : 0;
|
|
402
|
+
const safeScrollback = Number.isFinite(scrollbackLines) ? Math.max(0, Math.floor(scrollbackLines)) : 0;
|
|
403
|
+
// Issue #205: the frame writes genuine scrollback rows first and viewport rows after
|
|
404
|
+
// them, so hydration that overflows puts the manufactured rows BETWEEN the genuine
|
|
405
|
+
// history and the viewport — any local scroll necessarily traverses them. Disable
|
|
406
|
+
// local history for that case rather than sacrificing the oldest genuine rows.
|
|
407
|
+
// No overflow means every row above the viewport is real history.
|
|
408
|
+
return safeBase > safeScrollback ? safeBase : 0;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Issue #205: how many rows of the local buffer are genuine history.
|
|
413
|
+
*
|
|
414
|
+
* Everything below `historyFloor` is pseudo scrollback produced by hydration (see
|
|
415
|
+
* `historyFloorAfterHydrate`); only the rows above it may answer a wheel notch. A
|
|
416
|
+
* floor at or past the base clamps to no history. Pure by contract.
|
|
417
|
+
*
|
|
418
|
+
* @param {number} baseY
|
|
419
|
+
* @param {number} historyFloor
|
|
420
|
+
* @returns {number}
|
|
421
|
+
*/
|
|
422
|
+
export function scrollableHistoryLines(baseY, historyFloor) {
|
|
423
|
+
const safeBase = Number.isFinite(baseY) ? Math.max(0, Math.floor(baseY)) : 0;
|
|
424
|
+
const safeFloor = Number.isFinite(historyFloor) ? Math.max(0, Math.floor(historyFloor)) : 0;
|
|
425
|
+
return Math.max(0, safeBase - Math.min(safeBase, safeFloor));
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Compute the next local PTY viewport after a scroll gesture.
|
|
430
|
+
*
|
|
431
|
+
* `viewportTop === null` means follow bottom. `changed` reports whether the gesture
|
|
432
|
+
* would visibly move the local scrollback viewport; callers can forward unconsumed
|
|
433
|
+
* scroll keys/wheel events to the hosted TUI instead. `floor` is the lowest reachable
|
|
434
|
+
* row (issue #205): hydration can manufacture pseudo scrollback below it, which the
|
|
435
|
+
* gesture must neither scroll to nor report as consumed. The default 0 keeps the
|
|
436
|
+
* pre-floor behaviour for callers that have no hydration floor to enforce.
|
|
437
|
+
*/
|
|
438
|
+
export function scrollViewportTop(viewportTop, bottom, linesUp, floor = 0) {
|
|
439
|
+
const safeBottom = Math.max(0, Math.floor(bottom));
|
|
440
|
+
const safeFloor = clampInt(floor, 0, safeBottom);
|
|
441
|
+
const current = viewportTop == null ? safeBottom : clampInt(viewportTop, safeFloor, safeBottom);
|
|
442
|
+
const next = clampInt(current - linesUp, safeFloor, safeBottom);
|
|
443
|
+
return {
|
|
444
|
+
viewportTop: next >= safeBottom ? null : next,
|
|
445
|
+
changed: next !== current,
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Decide whether drag-selecting at/just beyond the viewport edge should auto-scroll.
|
|
451
|
+
* Positive values scroll up into older output; negative values scroll down.
|
|
452
|
+
*/
|
|
453
|
+
export function selectionDragScrollLines(row, bodyHeight) {
|
|
454
|
+
const safeHeight = Math.max(1, Math.floor(bodyHeight));
|
|
455
|
+
const topRow = 2;
|
|
456
|
+
const bottomRow = topRow + safeHeight - 1;
|
|
457
|
+
const pointerRow = Math.floor(row);
|
|
458
|
+
if (pointerRow < topRow) return 2;
|
|
459
|
+
if (pointerRow === topRow) return 1;
|
|
460
|
+
if (pointerRow > bottomRow) return -2;
|
|
461
|
+
if (pointerRow === bottomRow) return -1;
|
|
462
|
+
return 0;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Return a safe temporary PTY size for an attach jiggle hold. The attach controller
|
|
467
|
+
* keeps the child at this size until a clear or guard restores the original size;
|
|
468
|
+
* near the minimum supported dimensions, only one safe dimension is changed, and
|
|
469
|
+
* at 20x5 there is no valid alternate size.
|
|
470
|
+
*/
|
|
471
|
+
export function resizeJiggleSize(cols, rows) {
|
|
472
|
+
const c = Math.max(1, Math.floor(cols));
|
|
473
|
+
const r = Math.max(1, Math.floor(rows));
|
|
474
|
+
if (c > 21 && r > 6) return { cols: c - 1, rows: r - 1 };
|
|
475
|
+
if (r > 6) return { cols: c, rows: r - 1 };
|
|
476
|
+
if (c > 21) return { cols: c - 1, rows: r };
|
|
477
|
+
return null;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Issue #187: two mouse points are "the same" within the 1-cell coordinate slack
|
|
482
|
+
* terminals report. Buffer-space comparison ({line, col}) — shared by double-click
|
|
483
|
+
* detection. Moved here from pty-attach.ts so both comparisons have one definition.
|
|
484
|
+
* Pure by contract.
|
|
485
|
+
*
|
|
486
|
+
* @param {{line: number, col: number}} a
|
|
487
|
+
* @param {{line: number, col: number}} b
|
|
488
|
+
* @returns {boolean}
|
|
489
|
+
*/
|
|
490
|
+
export function sameMousePoint(a, b) {
|
|
491
|
+
return a.line === b.line && Math.abs(a.col - b.col) <= 1;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Issue #187: two SCREEN cells are "the same" within the same 1-cell slack. Screen
|
|
496
|
+
* space ({row, col}) is what the pointer actually did; buffer space drifts while a
|
|
497
|
+
* session streams (the viewport base moves), which is why the drag/click decision must
|
|
498
|
+
* not compare buffer points captured at two different moments. Pure by contract.
|
|
499
|
+
*
|
|
500
|
+
* @param {{row: number, col: number}} a
|
|
501
|
+
* @param {{row: number, col: number}} b
|
|
502
|
+
* @returns {boolean}
|
|
503
|
+
*/
|
|
504
|
+
export function sameScreenCell(a, b) {
|
|
505
|
+
return a.row === b.row && Math.abs(a.col - b.col) <= 1;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Issue #187: whether a mouse release ends a real selection drag. Terminals such as
|
|
510
|
+
* Warp deliver a fast drag as press + release with zero motion events, so the move flag
|
|
511
|
+
* alone is not sufficient — the gesture geometry decides. Both points are SCREEN cells
|
|
512
|
+
* as reported by the terminal (never the clamped buffer point). Missing geometry (no
|
|
513
|
+
* screen anchor recorded) degrades to the pre-fix answer: not a drag. Pure by contract.
|
|
514
|
+
*
|
|
515
|
+
* @param {{dragging: boolean, pressScreen: ({row: number, col: number}|undefined|null), releaseScreen: ({row: number, col: number}|undefined|null)}} input
|
|
516
|
+
* @returns {boolean}
|
|
517
|
+
*/
|
|
518
|
+
export function selectionReleaseIsDrag({ dragging, pressScreen, releaseScreen }) {
|
|
519
|
+
if (dragging) return true;
|
|
520
|
+
if (!pressScreen || !releaseScreen) return false;
|
|
521
|
+
return !sameScreenCell(pressScreen, releaseScreen);
|
|
522
|
+
}
|