tmux-ide 2.6.1 → 2.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -14
- package/bin/cli.js +4174 -1235
- package/bin/cli.ts +426 -72
- package/package.json +10 -6
- package/packages/contracts/src/__tests__/control.test.ts +154 -0
- package/packages/contracts/src/control.ts +217 -0
- package/packages/contracts/src/index.ts +1 -0
- package/packages/daemon/dist/agent-explain.d.ts +8 -1
- package/packages/daemon/dist/agent-explain.js +19 -3
- package/packages/daemon/dist/control/client.d.ts +23 -0
- package/packages/daemon/dist/control/client.js +105 -0
- package/packages/daemon/dist/control/dispatch.d.ts +34 -0
- package/packages/daemon/dist/control/dispatch.js +83 -0
- package/packages/daemon/dist/control/fanout.d.ts +19 -0
- package/packages/daemon/dist/control/fanout.js +37 -0
- package/packages/daemon/dist/control/frames.d.ts +23 -0
- package/packages/daemon/dist/control/frames.js +37 -0
- package/packages/daemon/dist/control/lifecycle.d.ts +45 -0
- package/packages/daemon/dist/control/lifecycle.js +114 -0
- package/packages/daemon/dist/control/server.d.ts +16 -0
- package/packages/daemon/dist/control/server.js +214 -0
- package/packages/daemon/dist/control/verbs.d.ts +11 -0
- package/packages/daemon/dist/control/verbs.js +91 -0
- package/packages/daemon/dist/doctor.d.ts +18 -0
- package/packages/daemon/dist/doctor.js +105 -15
- package/packages/daemon/dist/lib/agent-discovery.d.ts +27 -2
- package/packages/daemon/dist/lib/agent-discovery.js +29 -14
- package/packages/daemon/dist/lib/app-config.d.ts +106 -0
- package/packages/daemon/dist/lib/app-config.js +104 -5
- package/packages/daemon/dist/lib/manifest-pack.d.ts +79 -0
- package/packages/daemon/dist/lib/manifest-pack.js +232 -0
- package/packages/daemon/dist/lib/state-home.d.ts +2 -0
- package/packages/daemon/dist/lib/state-home.js +12 -0
- package/packages/daemon/dist/lib/tui-binary.d.ts +57 -0
- package/packages/daemon/dist/lib/tui-binary.js +130 -0
- package/packages/daemon/dist/lib/update-check.js +5 -0
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Info.plist +34 -0
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/MacOS/tmux-ide-notifier +0 -0
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/PkgInfo +1 -0
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Resources/AppIcon.icns +0 -0
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Resources/Assets.car +0 -0
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/_CodeSignature/CodeResources +139 -0
- package/packages/daemon/dist/restore.d.ts +35 -8
- package/packages/daemon/dist/restore.js +52 -15
- package/packages/daemon/dist/send.d.ts +33 -1
- package/packages/daemon/dist/send.js +32 -19
- package/packages/daemon/dist/widgets/explorer/breadcrumbs.d.ts +1 -1
- package/packages/daemon/dist/widgets/explorer/footer.d.ts +1 -1
- package/packages/daemon/dist/widgets/explorer/tree.d.ts +1 -1
- package/packages/daemon/dist/widgets/lib/help-overlay.d.ts +1 -1
- package/packages/daemon/dist/widgets/setup/agent-naming.d.ts +1 -1
- package/packages/daemon/dist/widgets/setup/config-tree.d.ts +1 -1
- package/packages/daemon/dist/widgets/setup/detect-panel.d.ts +1 -1
- package/packages/daemon/dist/widgets/setup/field-editor.d.ts +1 -1
- package/packages/daemon/dist/widgets/setup/footer.d.ts +1 -1
- package/packages/daemon/dist/widgets/setup/layout-picker.d.ts +1 -1
- package/packages/daemon/src/agent-explain.ts +34 -6
- package/packages/daemon/src/control/client.ts +128 -0
- package/packages/daemon/src/control/dispatch.ts +107 -0
- package/packages/daemon/src/control/fanout.ts +44 -0
- package/packages/daemon/src/control/frames.ts +40 -0
- package/packages/daemon/src/control/lifecycle.ts +151 -0
- package/packages/daemon/src/control/server.ts +237 -0
- package/packages/daemon/src/control/verbs.ts +118 -0
- package/packages/daemon/src/doctor.ts +113 -28
- package/packages/daemon/src/lib/agent-discovery.ts +53 -13
- package/packages/daemon/src/lib/app-config.ts +194 -5
- package/packages/daemon/src/lib/manifest-pack.ts +255 -0
- package/packages/daemon/src/lib/state-home.ts +13 -0
- package/packages/daemon/src/lib/tui-binary.ts +165 -0
- package/packages/daemon/src/lib/update-check.ts +5 -0
- package/packages/daemon/src/restore.ts +53 -15
- package/packages/daemon/src/send.ts +55 -21
- package/packages/daemon/src/tui/chrome/events.ts +4 -4
- package/packages/daemon/src/tui/chrome/front-door.ts +39 -0
- package/packages/daemon/src/tui/chrome/notify-prefs.ts +58 -0
- package/packages/daemon/src/tui/chrome/notify-state.ts +76 -0
- package/packages/daemon/src/tui/chrome/notify.ts +737 -52
- package/packages/daemon/src/tui/chrome/updater.ts +318 -30
- package/packages/daemon/src/tui/compiled.ts +11 -3
- package/packages/daemon/src/tui/detect/classify.ts +49 -0
- package/packages/daemon/src/tui/detect/manifest-loader.ts +56 -6
- package/packages/daemon/src/tui/detect/manifest.ts +39 -3
- package/packages/daemon/src/tui/detect/manifests.ts +395 -33
- package/packages/daemon/src/tui/detect/process-tree.ts +33 -3
- package/packages/daemon/src/tui/detect/session-id.ts +503 -0
- package/packages/daemon/src/tui/integrations/opencode.ts +121 -0
- package/packages/daemon/src/tui/main.ts +13 -1
- package/packages/daemon/src/tui/mirror/ack-writer.ts +77 -0
- package/packages/daemon/src/tui/mirror/agent-chip.ts +126 -0
- package/packages/daemon/src/tui/mirror/agent-lifecycle.ts +437 -0
- package/packages/daemon/src/tui/mirror/agent-rows.ts +155 -0
- package/packages/daemon/src/tui/mirror/app-state.ts +342 -0
- package/packages/daemon/src/tui/mirror/app.tsx +7048 -0
- package/packages/daemon/src/tui/mirror/attention.ts +110 -0
- package/packages/daemon/src/tui/mirror/blit.ts +186 -0
- package/packages/daemon/src/tui/mirror/control-client.ts +80 -9
- package/packages/daemon/src/tui/mirror/dialog-model.ts +298 -0
- package/packages/daemon/src/tui/mirror/dialog-stack.ts +367 -0
- package/packages/daemon/src/tui/mirror/diff-model.ts +387 -0
- package/packages/daemon/src/tui/mirror/editor-buffer.ts +117 -0
- package/packages/daemon/src/tui/mirror/file-tree.ts +322 -0
- package/packages/daemon/src/tui/mirror/focus-border.ts +57 -0
- package/packages/daemon/src/tui/mirror/folder-picker.ts +124 -0
- package/packages/daemon/src/tui/mirror/home-model.ts +174 -0
- package/packages/daemon/src/tui/mirror/host-terminal.ts +49 -0
- package/packages/daemon/src/tui/mirror/hosted.ts +205 -0
- package/packages/daemon/src/tui/mirror/input-coalescer.ts +105 -0
- package/packages/daemon/src/tui/mirror/layout-parse.ts +154 -0
- package/packages/daemon/src/tui/mirror/menu-model.ts +210 -0
- package/packages/daemon/src/tui/mirror/palette.ts +564 -0
- package/packages/daemon/src/tui/mirror/pane-mirror.ts +578 -20
- package/packages/daemon/src/tui/mirror/pane-surface.tsx +422 -0
- package/packages/daemon/src/tui/mirror/perf-tap.ts +186 -0
- package/packages/daemon/src/tui/mirror/resize-model.ts +85 -0
- package/packages/daemon/src/tui/mirror/scrollbar-model.ts +88 -0
- package/packages/daemon/src/tui/mirror/search-model.ts +70 -0
- package/packages/daemon/src/tui/mirror/selection.ts +376 -0
- package/packages/daemon/src/tui/mirror/session-mirror.ts +724 -0
- package/packages/daemon/src/tui/mirror/settings-model.ts +425 -0
- package/packages/daemon/src/tui/mirror/sidebar.tsx +218 -0
- package/packages/daemon/src/tui/mirror/size-truth.ts +130 -0
- package/packages/daemon/src/tui/mirror/spans.ts +46 -0
- package/packages/daemon/src/tui/mirror/status-grammar.ts +32 -0
- package/packages/daemon/src/tui/mirror/theme.ts +45 -0
- package/packages/daemon/src/tui/team/entry.ts +34 -7
- package/packages/daemon/src/tui/team/fuzzy.ts +20 -0
- package/packages/daemon/src/tui/team/report.ts +11 -1
- package/packages/daemon/src/tui/team/sessions.ts +184 -17
- package/packages/daemon/src/tui/team/wait.ts +144 -0
- package/scripts/build-macos-notifier.mjs +160 -0
- package/scripts/build-tui.mjs +11 -4
- package/scripts/perf-mirror.mjs +313 -0
- package/scripts/postinstall.js +8 -1
- package/scripts/prepublish-check.mjs +37 -1
- package/scripts/publish-tap.sh +55 -0
- package/skill/SKILL.md +110 -2
- package/templates/AGENTS.md +14 -7
- package/templates/agent-team-monorepo.yml +8 -0
- package/templates/agent-team-nextjs.yml +8 -0
- package/templates/agent-team.yml +10 -0
- package/templates/convex.yml +2 -0
- package/templates/default.yml +11 -5
- package/templates/go.yml +4 -0
- package/templates/missions.yml +6 -0
- package/templates/nextjs.yml +4 -0
- package/templates/python.yml +4 -0
- package/templates/skills/backend.md +5 -12
- package/templates/skills/frontend.md +5 -12
- package/templates/skills/general-worker.md +5 -12
- package/templates/skills/researcher.md +7 -12
- package/templates/skills/reviewer.md +7 -16
- package/templates/vite.yml +4 -0
- package/packages/daemon/src/tui/mirror/viewer.tsx +0 -166
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure scrollbar geometry (M19.5) — shared by the editor viewport, the diff
|
|
3
|
+
* pane, and any scrolled mirror pane.
|
|
4
|
+
*
|
|
5
|
+
* A 1-col track at the right edge of an always-present container; the thumb's
|
|
6
|
+
* position and size are pure functions of (viewportTop, contentLen, viewH).
|
|
7
|
+
* Both the RENDER (draws the track cells) and the ROUTER (classifies a press as
|
|
8
|
+
* thumb vs track, maps a thumb drag to an absolute scroll top, pages a track
|
|
9
|
+
* click) read the SAME math from here — the surface-bar discipline applied to a
|
|
10
|
+
* vertical strip. Deliberately opentui-free so it unit-tests under Node.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export interface ScrollThumb {
|
|
14
|
+
/** Whether the content overflows the viewport (nothing to scroll → hidden). */
|
|
15
|
+
overflow: boolean;
|
|
16
|
+
/** First track row (0-based, within [0, viewH)) the thumb occupies. */
|
|
17
|
+
start: number;
|
|
18
|
+
/** Thumb height in rows (≥1, ≤viewH). */
|
|
19
|
+
size: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The thumb size for a content/viewport pair — a viewport-fraction of the
|
|
23
|
+
* track, floored at one cell so a thumb always exists. Shared by every
|
|
24
|
+
* function below so their maths never drift. */
|
|
25
|
+
function thumbSize(contentLen: number, viewH: number): number {
|
|
26
|
+
return Math.max(1, Math.min(viewH, Math.round((viewH * viewH) / contentLen)));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The thumb geometry for a scroll position. `viewportTop` is the first visible
|
|
30
|
+
* content line; `contentLen` the total lines; `viewH` the visible rows. When
|
|
31
|
+
* the content fits, `overflow` is false and the caller draws no track. */
|
|
32
|
+
export function scrollThumb(viewportTop: number, contentLen: number, viewH: number): ScrollThumb {
|
|
33
|
+
if (viewH <= 0 || contentLen <= viewH) {
|
|
34
|
+
return { overflow: false, start: 0, size: Math.max(1, viewH) };
|
|
35
|
+
}
|
|
36
|
+
const size = thumbSize(contentLen, viewH);
|
|
37
|
+
const maxTop = contentLen - viewH;
|
|
38
|
+
const maxStart = viewH - size;
|
|
39
|
+
const top = Math.max(0, Math.min(maxTop, viewportTop));
|
|
40
|
+
const start = maxStart <= 0 ? 0 : Math.round((top / maxTop) * maxStart);
|
|
41
|
+
return { overflow: true, start: Math.max(0, Math.min(maxStart, start)), size };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Where a track row falls relative to the thumb — the fourth drag-origin's
|
|
45
|
+
* press classifier and the page-click direction source. */
|
|
46
|
+
export type TrackZone = "above" | "thumb" | "below";
|
|
47
|
+
|
|
48
|
+
export function trackZone(row: number, thumb: ScrollThumb): TrackZone {
|
|
49
|
+
if (row < thumb.start) return "above";
|
|
50
|
+
if (row >= thumb.start + thumb.size) return "below";
|
|
51
|
+
return "thumb";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Page the viewport toward a click at track `row` (above the thumb → up one
|
|
55
|
+
* viewport, below → down one; on the thumb → unchanged), clamped to range. */
|
|
56
|
+
export function pageTop(
|
|
57
|
+
row: number,
|
|
58
|
+
viewportTop: number,
|
|
59
|
+
contentLen: number,
|
|
60
|
+
viewH: number,
|
|
61
|
+
): number {
|
|
62
|
+
const thumb = scrollThumb(viewportTop, contentLen, viewH);
|
|
63
|
+
if (!thumb.overflow) return viewportTop;
|
|
64
|
+
const maxTop = contentLen - viewH;
|
|
65
|
+
const zone = trackZone(row, thumb);
|
|
66
|
+
const next =
|
|
67
|
+
zone === "above" ? viewportTop - viewH : zone === "below" ? viewportTop + viewH : viewportTop;
|
|
68
|
+
return Math.max(0, Math.min(maxTop, next));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Map a dragged thumb to an absolute scroll top. `trackRow` is the current
|
|
72
|
+
* pointer row within the track; `grabOffset` is the row within the thumb where
|
|
73
|
+
* the drag began (so the thumb doesn't jump under the cursor). Returns the new
|
|
74
|
+
* first-visible line, clamped. */
|
|
75
|
+
export function dragTop(
|
|
76
|
+
trackRow: number,
|
|
77
|
+
grabOffset: number,
|
|
78
|
+
contentLen: number,
|
|
79
|
+
viewH: number,
|
|
80
|
+
): number {
|
|
81
|
+
if (viewH <= 0 || contentLen <= viewH) return 0;
|
|
82
|
+
const size = thumbSize(contentLen, viewH);
|
|
83
|
+
const maxStart = viewH - size;
|
|
84
|
+
const maxTop = contentLen - viewH;
|
|
85
|
+
if (maxStart <= 0) return 0;
|
|
86
|
+
const start = Math.max(0, Math.min(maxStart, trackRow - grabOffset));
|
|
87
|
+
return Math.round((start / maxStart) * maxTop);
|
|
88
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure, io-free math for scrollback search (M20.3) — the copy-mode `/` finder,
|
|
3
|
+
* app-native. app.tsx owns the search-session state machine + the io (reading a
|
|
4
|
+
* pane's `bufferLines`, driving scrollOffset); this module owns the arithmetic,
|
|
5
|
+
* unit-tested without a terminal exactly like selection.ts / diff-model.ts.
|
|
6
|
+
*
|
|
7
|
+
* A match is a (line, col) into the pane's full buffer (0 = oldest scrollback
|
|
8
|
+
* line). The render maps a match line to a visible row and inverse/accent-tints
|
|
9
|
+
* its [col, col+len) span; a jump converts a match line to a scrollOffset.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** One substring hit: `line` is the absolute buffer line, `col` the 0-based
|
|
13
|
+
* character column of the match start on that line. */
|
|
14
|
+
export interface SearchMatch {
|
|
15
|
+
line: number;
|
|
16
|
+
col: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Every case-insensitive substring occurrence of `query` across `lines`, in
|
|
21
|
+
* buffer order (top→bottom, left→right), one entry per occurrence (multiple per
|
|
22
|
+
* line allowed; non-overlapping — the scan advances past each hit). An empty
|
|
23
|
+
* query yields no matches.
|
|
24
|
+
*/
|
|
25
|
+
export function findMatches(lines: readonly string[], query: string): SearchMatch[] {
|
|
26
|
+
const out: SearchMatch[] = [];
|
|
27
|
+
if (query.length === 0) return out;
|
|
28
|
+
const needle = query.toLowerCase();
|
|
29
|
+
for (let line = 0; line < lines.length; line++) {
|
|
30
|
+
const hay = (lines[line] ?? "").toLowerCase();
|
|
31
|
+
let from = 0;
|
|
32
|
+
for (;;) {
|
|
33
|
+
const idx = hay.indexOf(needle, from);
|
|
34
|
+
if (idx === -1) break;
|
|
35
|
+
out.push({ line, col: idx });
|
|
36
|
+
from = idx + needle.length;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Matches in VISIT order — bottom-up (nearest the live viewport first), the
|
|
44
|
+
* order `/`-then-Enter lands on and n/N cycle through. So visit index 0 is the
|
|
45
|
+
* bottom-most match (shown as "1/N"), and `n` walks upward through the buffer.
|
|
46
|
+
* A fresh copy (does not mutate the input).
|
|
47
|
+
*/
|
|
48
|
+
export function visitOrder(matches: readonly SearchMatch[]): SearchMatch[] {
|
|
49
|
+
return [...matches].reverse();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Cycle a match index by `dir` (+1 next / -1 prev) with wraparound; -1 stays
|
|
53
|
+
* -1 when there are no matches. */
|
|
54
|
+
export function stepMatch(current: number, dir: number, total: number): number {
|
|
55
|
+
if (total <= 0) return -1;
|
|
56
|
+
return (((current + dir) % total) + total) % total;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The scrollOffset (lines above the live viewport) that brings buffer `line`
|
|
61
|
+
* into view, positioned roughly at the middle of a `viewH`-row viewport, clamped
|
|
62
|
+
* to [0, depth]. `depth` is the scrollback budget (lines above the live top);
|
|
63
|
+
* the pane snapshot renders visible row `r` from buffer line `depth - offset + r`,
|
|
64
|
+
* so putting `line` at row `target` needs `offset = depth + target - line`.
|
|
65
|
+
*/
|
|
66
|
+
export function offsetForMatch(line: number, depth: number, viewH: number): number {
|
|
67
|
+
const target = Math.floor(Math.max(0, viewH) / 2);
|
|
68
|
+
const raw = depth + target - line;
|
|
69
|
+
return Math.max(0, Math.min(depth, raw));
|
|
70
|
+
}
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure, io-free geometry + text math for mouse selection & clipboard (M19.4).
|
|
3
|
+
*
|
|
4
|
+
* Two surfaces select the same way — the session MIRROR (pane snapshot rows)
|
|
5
|
+
* and the built-in EDITOR (buffer lines) — so the coordinate math lives here,
|
|
6
|
+
* unit-tested without a terminal, exactly like spans.ts / diff-model.ts. app.tsx
|
|
7
|
+
* owns the mouse-gesture state machine and the io (OSC52 to stdout, tmux
|
|
8
|
+
* passthrough, send-keys); this module owns the arithmetic.
|
|
9
|
+
*
|
|
10
|
+
* The selection is LINEAR (terminal-style, reading order), not rectangular: a
|
|
11
|
+
* cell is selected if it falls between the anchor and head in row-major order.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** A cell in surface-local coordinates. Mirror (M25.6): ABSOLUTE xterm buffer
|
|
15
|
+
* line (0 = oldest scrollback line) + pane column — anchored to CONTENT, so
|
|
16
|
+
* the selection survives scrolling mid-drag and can span many screens. Editor:
|
|
17
|
+
* buffer line + buffer column (already absolute by nature). A visible pane row
|
|
18
|
+
* `r` at scroll offset `off` maps to absolute line `baseY + r` where
|
|
19
|
+
* `baseY = scrollbackDepth − off` (the search path's existing arithmetic —
|
|
20
|
+
* see PaneMirror.bufferLines). */
|
|
21
|
+
export interface Cell {
|
|
22
|
+
row: number;
|
|
23
|
+
col: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** An active selection on one surface. `anchor` is where the drag began, `head`
|
|
27
|
+
* the current/last pointer cell; either may precede the other. Mirror cells
|
|
28
|
+
* are in ABSOLUTE buffer coordinates (see {@link Cell}). */
|
|
29
|
+
export type Selection =
|
|
30
|
+
| { surface: "mirror"; paneId: string; anchor: Cell; head: Cell }
|
|
31
|
+
| { surface: "editor"; anchor: Cell; head: Cell };
|
|
32
|
+
|
|
33
|
+
/** OpenTUI TextAttributes bit for inverse video (matches pane-mirror.ts). */
|
|
34
|
+
export const ATTR_INVERSE = 32;
|
|
35
|
+
|
|
36
|
+
/** Order two cells into reading order (row-major); both endpoints inclusive. */
|
|
37
|
+
export function orderCells(a: Cell, b: Cell): { start: Cell; end: Cell } {
|
|
38
|
+
if (a.row < b.row || (a.row === b.row && a.col <= b.col)) return { start: a, end: b };
|
|
39
|
+
return { start: b, end: a };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The inclusive [from,to] column interval selected on `row` for a linear
|
|
44
|
+
* selection between ordered endpoints, or null when the row is outside the
|
|
45
|
+
* selection or has no cells. `rowLen` is the row's character count.
|
|
46
|
+
*/
|
|
47
|
+
export function rowSelectionRange(
|
|
48
|
+
row: number,
|
|
49
|
+
rowLen: number,
|
|
50
|
+
start: Cell,
|
|
51
|
+
end: Cell,
|
|
52
|
+
): { from: number; to: number } | null {
|
|
53
|
+
if (row < start.row || row > end.row) return null;
|
|
54
|
+
if (rowLen <= 0) return null;
|
|
55
|
+
const rawFrom = row === start.row ? start.col : 0;
|
|
56
|
+
const rawTo = row === end.row ? end.col : rowLen - 1;
|
|
57
|
+
const from = Math.max(0, Math.min(rawFrom, rowLen - 1));
|
|
58
|
+
const to = Math.max(0, Math.min(rawTo, rowLen - 1));
|
|
59
|
+
if (to < from) return null;
|
|
60
|
+
return { from, to };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Extract the selected text from per-row plain strings between ordered
|
|
65
|
+
* endpoints. Rows join with "\n". When `trimTrailing` (mirror snapshot rows are
|
|
66
|
+
* space-padded to the pane width), trailing whitespace on each visual row is
|
|
67
|
+
* stripped — the terminal-copy convention; the editor passes false to keep
|
|
68
|
+
* buffer text exact.
|
|
69
|
+
*/
|
|
70
|
+
export function extractSelection(
|
|
71
|
+
rowTexts: string[],
|
|
72
|
+
start: Cell,
|
|
73
|
+
end: Cell,
|
|
74
|
+
trimTrailing = true,
|
|
75
|
+
): string {
|
|
76
|
+
const out: string[] = [];
|
|
77
|
+
for (let r = start.row; r <= end.row; r++) {
|
|
78
|
+
const text = rowTexts[r] ?? "";
|
|
79
|
+
const range = rowSelectionRange(r, text.length, start, end);
|
|
80
|
+
let seg = range ? text.slice(range.from, range.to + 1) : "";
|
|
81
|
+
if (trimTrailing) seg = seg.replace(/\s+$/u, "");
|
|
82
|
+
out.push(seg);
|
|
83
|
+
}
|
|
84
|
+
return out.join("\n");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ── Absolute-space mapping (M25.6) ──────────────────────────────────────────
|
|
88
|
+
// The selection model is anchored to ABSOLUTE buffer lines; the render works in
|
|
89
|
+
// visible pane rows. These mappers are the one bridge: given the pane's current
|
|
90
|
+
// baseY (scrollbackDepth − scrollOffset) and height, an absolute range resolves
|
|
91
|
+
// to the visible rows it covers and the per-row column spans to highlight.
|
|
92
|
+
|
|
93
|
+
/** A highlighted span on one VISIBLE pane row (row is viewport-relative). */
|
|
94
|
+
export interface VisibleSpan {
|
|
95
|
+
row: number;
|
|
96
|
+
from: number;
|
|
97
|
+
to: number;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Visible pane rows [0,height) covered by an ORDERED absolute range at the
|
|
101
|
+
* current view (`baseY` = scrollbackDepth − scrollOffset). Empty when the
|
|
102
|
+
* whole range is scrolled off either edge. */
|
|
103
|
+
export function visibleSelRows(start: Cell, end: Cell, baseY: number, height: number): number[] {
|
|
104
|
+
const lo = Math.max(0, start.row - baseY);
|
|
105
|
+
const hi = Math.min(height - 1, end.row - baseY);
|
|
106
|
+
const out: number[] = [];
|
|
107
|
+
for (let r = lo; r <= hi; r++) out.push(r);
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Map an ORDERED absolute range to the visible per-row column spans at the
|
|
113
|
+
* current view. `rowLen(viewRow)` supplies each visible row's selectable cell
|
|
114
|
+
* count (grid width for the framebuffer swap; the row's char count for run
|
|
115
|
+
* tinting — wide glyphs collapse to one char there, so the two callers pass
|
|
116
|
+
* different lengths and each gets spans clamped to ITS space). Rows whose
|
|
117
|
+
* span is empty (zero-length row, or a first/last-row column interval that
|
|
118
|
+
* falls outside it) are omitted.
|
|
119
|
+
*/
|
|
120
|
+
export function visibleSelectionSpans(
|
|
121
|
+
start: Cell,
|
|
122
|
+
end: Cell,
|
|
123
|
+
baseY: number,
|
|
124
|
+
height: number,
|
|
125
|
+
rowLen: (viewRow: number) => number,
|
|
126
|
+
): VisibleSpan[] {
|
|
127
|
+
const out: VisibleSpan[] = [];
|
|
128
|
+
for (const r of visibleSelRows(start, end, baseY, height)) {
|
|
129
|
+
const range = rowSelectionRange(baseY + r, rowLen(r), start, end);
|
|
130
|
+
if (range) out.push({ row: r, from: range.from, to: range.to });
|
|
131
|
+
}
|
|
132
|
+
return out;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Re-anchor an absolute cell after the buffer trimmed `trimDelta` lines off its
|
|
137
|
+
* top (the scrollback cap rotating mid-drag): every retained line's index
|
|
138
|
+
* dropped by the trim, so the cell follows its content down. A cell whose line
|
|
139
|
+
* was trimmed away clamps to the oldest retained line (row 0), col 0 — the
|
|
140
|
+
* selection top pins to the oldest text still in the buffer.
|
|
141
|
+
*/
|
|
142
|
+
export function trimAdjustCell(cell: Cell, trimDelta: number): Cell {
|
|
143
|
+
if (trimDelta <= 0) return cell;
|
|
144
|
+
const row = cell.row - trimDelta;
|
|
145
|
+
if (row < 0) return { row: 0, col: 0 };
|
|
146
|
+
return { row, col: cell.col };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* The inclusive [from,to] char interval of the word at `col` in `text`,
|
|
151
|
+
* splitting on /\W/ (word chars are [A-Za-z0-9_]). A click on a non-word char
|
|
152
|
+
* selects just that char; an empty line selects {0,0}.
|
|
153
|
+
*/
|
|
154
|
+
export function wordRangeAt(text: string, col: number): { from: number; to: number } {
|
|
155
|
+
const n = text.length;
|
|
156
|
+
if (n === 0) return { from: 0, to: 0 };
|
|
157
|
+
const c = Math.max(0, Math.min(col, n - 1));
|
|
158
|
+
const isWord = (ch: string): boolean => /\w/u.test(ch);
|
|
159
|
+
if (!isWord(text[c]!)) return { from: c, to: c };
|
|
160
|
+
let from = c;
|
|
161
|
+
let to = c;
|
|
162
|
+
while (from > 0 && isWord(text[from - 1]!)) from--;
|
|
163
|
+
while (to < n - 1 && isWord(text[to + 1]!)) to++;
|
|
164
|
+
return { from, to };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** The inclusive [from,to] interval covering the line up to its last non-blank
|
|
168
|
+
* char (a blank line → {0,0}). */
|
|
169
|
+
export function lineRangeAt(text: string): { from: number; to: number } {
|
|
170
|
+
const trimmed = text.replace(/\s+$/u, "");
|
|
171
|
+
if (trimmed.length === 0) return { from: 0, to: 0 };
|
|
172
|
+
return { from: 0, to: trimmed.length - 1 };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* The resulting click count (1/2/3, cycling to 1 after 3) for double/triple
|
|
177
|
+
* detection: a click at the same cell within `windowMs` of the previous one
|
|
178
|
+
* increments; anything else resets to 1.
|
|
179
|
+
*/
|
|
180
|
+
export function clickCount(
|
|
181
|
+
prev: { row: number; col: number; ts: number; count: number } | null,
|
|
182
|
+
cell: Cell,
|
|
183
|
+
ts: number,
|
|
184
|
+
windowMs: number,
|
|
185
|
+
): number {
|
|
186
|
+
if (prev && prev.row === cell.row && prev.col === cell.col && ts - prev.ts <= windowMs) {
|
|
187
|
+
return prev.count >= 3 ? 1 : prev.count + 1;
|
|
188
|
+
}
|
|
189
|
+
return 1;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Toggle the inverse attribute across chars [from,to] (inclusive, in row-char
|
|
194
|
+
* columns) of a row's styled runs, splitting runs at the boundaries so the
|
|
195
|
+
* selected span renders highlighted while every other run keeps its colors.
|
|
196
|
+
* XOR (not OR) so an already-inverse cell — e.g. the painted cursor — flips
|
|
197
|
+
* back, matching pane-mirror's cursor treatment.
|
|
198
|
+
*/
|
|
199
|
+
export function tintRunsInverse<T extends { text: string; attributes: number }>(
|
|
200
|
+
runs: T[],
|
|
201
|
+
from: number,
|
|
202
|
+
to: number,
|
|
203
|
+
): T[] {
|
|
204
|
+
if (to < from) return runs;
|
|
205
|
+
const out: T[] = [];
|
|
206
|
+
let col = 0;
|
|
207
|
+
for (const run of runs) {
|
|
208
|
+
const len = run.text.length;
|
|
209
|
+
const runStart = col;
|
|
210
|
+
const runEnd = col + len - 1;
|
|
211
|
+
col += len;
|
|
212
|
+
if (len === 0 || runEnd < from || runStart > to) {
|
|
213
|
+
out.push(run);
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
const a = Math.max(from, runStart) - runStart;
|
|
217
|
+
const b = Math.min(to, runEnd) - runStart;
|
|
218
|
+
if (a > 0) out.push({ ...run, text: run.text.slice(0, a) });
|
|
219
|
+
out.push({ ...run, text: run.text.slice(a, b + 1), attributes: run.attributes ^ ATTR_INVERSE });
|
|
220
|
+
if (b + 1 < len) out.push({ ...run, text: run.text.slice(b + 1) });
|
|
221
|
+
}
|
|
222
|
+
return out;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Set the background color across chars [from,to] (inclusive, in row-char
|
|
227
|
+
* columns) of a row's styled runs, splitting runs at the boundaries so the
|
|
228
|
+
* spanned cells render with `bg` while every other run keeps its colors. The
|
|
229
|
+
* twin of {@link tintRunsInverse} — search-match highlighting paints an accent
|
|
230
|
+
* background (a distinct treatment from the inverse-video selection) so the two
|
|
231
|
+
* can coexist on one row. `bg` is packed 0xRRGGBB (the run bg convention).
|
|
232
|
+
*/
|
|
233
|
+
export function tintRunsBg<T extends { text: string; bg: number | null }>(
|
|
234
|
+
runs: T[],
|
|
235
|
+
from: number,
|
|
236
|
+
to: number,
|
|
237
|
+
bg: number,
|
|
238
|
+
): T[] {
|
|
239
|
+
if (to < from) return runs;
|
|
240
|
+
const out: T[] = [];
|
|
241
|
+
let col = 0;
|
|
242
|
+
for (const run of runs) {
|
|
243
|
+
const len = run.text.length;
|
|
244
|
+
const runStart = col;
|
|
245
|
+
const runEnd = col + len - 1;
|
|
246
|
+
col += len;
|
|
247
|
+
if (len === 0 || runEnd < from || runStart > to) {
|
|
248
|
+
out.push(run);
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
const a = Math.max(from, runStart) - runStart;
|
|
252
|
+
const b = Math.min(to, runEnd) - runStart;
|
|
253
|
+
if (a > 0) out.push({ ...run, text: run.text.slice(0, a) });
|
|
254
|
+
out.push({ ...run, text: run.text.slice(a, b + 1), bg });
|
|
255
|
+
if (b + 1 < len) out.push({ ...run, text: run.text.slice(b + 1) });
|
|
256
|
+
}
|
|
257
|
+
return out;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ── Select mode on app-mouse panes (M22.9) ──────────────────────────────────
|
|
261
|
+
// Panes whose app turned mouse reporting on (`appMouse`) normally get every
|
|
262
|
+
// press FORWARDED, so a drag can never start a local selection there. Two
|
|
263
|
+
// entries reopen selection: an explicit per-pane SELECT MODE (right-click →
|
|
264
|
+
// "Select text…", paused forwarding until esc / a completed copy / focus
|
|
265
|
+
// leaving the pane) and a SHIFT-modified press when the terminal passes shift
|
|
266
|
+
// through (SGR encodes it as +4 on the button code; many terminals keep
|
|
267
|
+
// shift+drag for native selection instead — then only select mode applies).
|
|
268
|
+
|
|
269
|
+
// ── Implicit drag-select default (M24.2) ─────────────────────────────────────
|
|
270
|
+
// Every agent pane (claude/codex) is app-mouse, so "forwarding wins" meant a
|
|
271
|
+
// user's whole fleet couldn't drag-select. The DEFAULT now follows the pane:
|
|
272
|
+
// where our own detection says an agent runs there, a plain drag selects and
|
|
273
|
+
// only a genuine click (press+release in one cell) is forwarded — deferred
|
|
274
|
+
// until motion or release decides. Shift INVERTS the pane's default (so on an
|
|
275
|
+
// agent pane shift+drag forwards; on a vim pane it selects, as in M22.9), the
|
|
276
|
+
// right-click toggle overrides per pane for the session, and `app.dragSelect`
|
|
277
|
+
// ("agents"|"always"|"never") sets the policy.
|
|
278
|
+
|
|
279
|
+
/** Where a plain left drag on a pane goes by default. */
|
|
280
|
+
export type PaneDragDefault = "select" | "forward";
|
|
281
|
+
/** The `app.dragSelect` policy values (app-config). */
|
|
282
|
+
export type DragSelectSetting = "agents" | "always" | "never";
|
|
283
|
+
|
|
284
|
+
/** PURE — a pane's drag default. Precedence: the session's per-pane override
|
|
285
|
+
* (the right-click toggle) > the config policy ("always"/"never") > the agent
|
|
286
|
+
* join ("agents": a pane matching a fleet agent entry selects; every other
|
|
287
|
+
* app-mouse pane forwards). */
|
|
288
|
+
export function paneDragDefault(
|
|
289
|
+
agentEntry: { paneId: string } | undefined,
|
|
290
|
+
config: DragSelectSetting,
|
|
291
|
+
override: PaneDragDefault | null,
|
|
292
|
+
): PaneDragDefault {
|
|
293
|
+
if (override !== null) return override;
|
|
294
|
+
if (config === "always") return "select";
|
|
295
|
+
if (config === "never") return "forward";
|
|
296
|
+
return agentEntry !== undefined ? "select" : "forward";
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** What a left press on a pane does: run the selection machine NOW, forward
|
|
300
|
+
* the SGR press NOW, or DEFER (withhold the press until motion starts a
|
|
301
|
+
* selection or a release-in-place forwards the owed click pair). */
|
|
302
|
+
export type PressRouting = "select" | "forward" | "defer";
|
|
303
|
+
|
|
304
|
+
/** PURE — route a left press. Plain panes and select mode keep the immediate
|
|
305
|
+
* selection machine. Otherwise the pane's drag default decides, with shift
|
|
306
|
+
* inverting it: a shift press that lands on "select" stays IMMEDIATE (the
|
|
307
|
+
* M22.9 behavior — a shift-click was never forwarded), while an unshifted
|
|
308
|
+
* "select" defers so the pane's app still gets genuine clicks. */
|
|
309
|
+
export function routePanePress(
|
|
310
|
+
appMouse: boolean,
|
|
311
|
+
selectModeOn: boolean,
|
|
312
|
+
shift: boolean,
|
|
313
|
+
dragDefault: PaneDragDefault,
|
|
314
|
+
): PressRouting {
|
|
315
|
+
if (!appMouse || selectModeOn) return "select";
|
|
316
|
+
const effective: PaneDragDefault = shift
|
|
317
|
+
? dragDefault === "select"
|
|
318
|
+
? "forward"
|
|
319
|
+
: "select"
|
|
320
|
+
: dragDefault;
|
|
321
|
+
if (effective === "forward") return "forward";
|
|
322
|
+
return shift ? "select" : "defer";
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** PURE — whether the wheel scrolls the LOCAL mirror scrollback (vs. being
|
|
326
|
+
* forwarded as SGR wheel events). Select mode reclaims the wheel so older
|
|
327
|
+
* output can be scrolled into view and selected. */
|
|
328
|
+
export function wheelScrollsLocal(appMouse: boolean, selectModeOn: boolean): boolean {
|
|
329
|
+
return !appMouse || selectModeOn;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** PURE — the select-mode badge text for a pane width (rendered in the pane's
|
|
333
|
+
* top-right badge family, next to the scroll badge), degrading label → glyph
|
|
334
|
+
* → hidden on narrow panes exactly like the agent chip's budget. */
|
|
335
|
+
export function selectBadgeLabel(paneWidth: number): string | null {
|
|
336
|
+
if (paneWidth >= 16) return " ⧉ select ";
|
|
337
|
+
if (paneWidth >= 5) return " ⧉ ";
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** The raw OSC52 clipboard-set escape for a base64 payload (BEL-terminated). */
|
|
342
|
+
export function osc52Sequence(base64: string): string {
|
|
343
|
+
return `\x1b]52;c;${base64}\x07`;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Wrap a sequence in the tmux passthrough envelope (DCS `tmux;` … ST) with inner
|
|
348
|
+
* ESC bytes doubled, so it rides through an outer tmux (with allow-passthrough
|
|
349
|
+
* on) verbatim to the real terminal. Kept tested + available; the app prefers
|
|
350
|
+
* raw OSC52 so `set-clipboard on` captures it into tmux's paste buffer.
|
|
351
|
+
*/
|
|
352
|
+
export function tmuxPassthrough(seq: string): string {
|
|
353
|
+
const doubled = seq.split("\x1b").join("\x1b\x1b");
|
|
354
|
+
return `\x1bPtmux;${doubled}\x1b\\`;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/** Split `text` into chunks each at most `maxBytes` UTF-8 bytes, never breaking
|
|
358
|
+
* a code point — for send-keys -H, whose per-command length tmux caps. */
|
|
359
|
+
export function chunkByBytes(text: string, maxBytes: number): string[] {
|
|
360
|
+
const enc = new TextEncoder();
|
|
361
|
+
const chunks: string[] = [];
|
|
362
|
+
let cur = "";
|
|
363
|
+
let curBytes = 0;
|
|
364
|
+
for (const ch of text) {
|
|
365
|
+
const b = enc.encode(ch).length;
|
|
366
|
+
if (curBytes + b > maxBytes && cur.length > 0) {
|
|
367
|
+
chunks.push(cur);
|
|
368
|
+
cur = "";
|
|
369
|
+
curBytes = 0;
|
|
370
|
+
}
|
|
371
|
+
cur += ch;
|
|
372
|
+
curBytes += b;
|
|
373
|
+
}
|
|
374
|
+
if (cur.length > 0) chunks.push(cur);
|
|
375
|
+
return chunks;
|
|
376
|
+
}
|