tmux-ide 2.7.0 → 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 +22 -5
- package/bin/cli.js +3532 -1090
- package/bin/cli.ts +368 -71
- package/package.json +2 -1
- 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/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/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/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 +103 -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/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 +582 -84
- package/packages/daemon/src/tui/chrome/updater.ts +268 -62
- package/packages/daemon/src/tui/detect/classify.ts +34 -0
- package/packages/daemon/src/tui/detect/manifest-loader.ts +54 -5
- package/packages/daemon/src/tui/detect/manifest.ts +24 -3
- package/packages/daemon/src/tui/detect/manifests.ts +240 -6
- package/packages/daemon/src/tui/detect/process-tree.ts +13 -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/mirror/agent-chip.ts +40 -11
- package/packages/daemon/src/tui/mirror/agent-lifecycle.ts +437 -0
- package/packages/daemon/src/tui/mirror/agent-rows.ts +27 -5
- package/packages/daemon/src/tui/mirror/app-state.ts +171 -8
- package/packages/daemon/src/tui/mirror/app.tsx +2182 -399
- package/packages/daemon/src/tui/mirror/attention.ts +110 -0
- package/packages/daemon/src/tui/mirror/dialog-stack.ts +17 -4
- package/packages/daemon/src/tui/mirror/diff-model.ts +279 -4
- package/packages/daemon/src/tui/mirror/file-tree.ts +231 -6
- 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/layout-parse.ts +154 -0
- package/packages/daemon/src/tui/mirror/menu-model.ts +27 -4
- package/packages/daemon/src/tui/mirror/palette.ts +299 -9
- package/packages/daemon/src/tui/mirror/pane-mirror.ts +82 -4
- package/packages/daemon/src/tui/mirror/pane-surface.tsx +18 -11
- package/packages/daemon/src/tui/mirror/perf-tap.ts +29 -3
- package/packages/daemon/src/tui/mirror/selection.ts +122 -8
- package/packages/daemon/src/tui/mirror/session-mirror.ts +349 -68
- package/packages/daemon/src/tui/mirror/settings-model.ts +96 -16
- package/packages/daemon/src/tui/mirror/sidebar.tsx +218 -0
- package/packages/daemon/src/tui/mirror/size-truth.ts +53 -0
- package/packages/daemon/src/tui/mirror/theme.ts +45 -0
- package/packages/daemon/src/tui/team/fuzzy.ts +20 -0
- package/packages/daemon/src/tui/team/sessions.ts +85 -7
- package/packages/daemon/src/tui/team/wait.ts +144 -0
- package/scripts/build-macos-notifier.mjs +160 -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 +88 -2
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
/** The surfaces a right-click can target. Each maps to a fixed item list; the
|
|
17
17
|
* concrete payload (session name, file path, pane id) rides in the app's menu
|
|
18
18
|
* state, not here. */
|
|
19
|
-
export type MenuRegion = "session" | "file" | "difffile" | "pane" | "window";
|
|
19
|
+
export type MenuRegion = "session" | "agent" | "file" | "difffile" | "pane" | "window";
|
|
20
20
|
|
|
21
21
|
/** One menu entry. `id` is what the app dispatches on; `danger` items rearm to
|
|
22
22
|
* a "confirm: y" state instead of firing immediately; `input` items open an
|
|
@@ -36,9 +36,21 @@ export interface MenuItem {
|
|
|
36
36
|
export const MENU_ITEMS: Record<MenuRegion, MenuItem[]> = {
|
|
37
37
|
session: [
|
|
38
38
|
{ id: "attach", label: "Attach" },
|
|
39
|
+
{ id: "new-agent", label: "New agent…" },
|
|
39
40
|
{ id: "rename", label: "Rename", input: "rename to" },
|
|
40
41
|
{ id: "kill", label: "Kill session", danger: true },
|
|
41
42
|
],
|
|
43
|
+
// Sidebar AGENT rows (M23.1): a left-click jumps; the context menu carries
|
|
44
|
+
// the lifecycle verbs. "Stop agent" only interrupts (the pane survives);
|
|
45
|
+
// "Close pane" is the destructive twin that also kills the pane. restart/
|
|
46
|
+
// stop confirm via DialogConfirm in the app (plain-language body), so only
|
|
47
|
+
// close — where there is no dialog — uses the inline confirm rearm.
|
|
48
|
+
agent: [
|
|
49
|
+
{ id: "jump", label: "Go to agent" },
|
|
50
|
+
{ id: "restart", label: "Restart agent" },
|
|
51
|
+
{ id: "stop", label: "Stop agent" },
|
|
52
|
+
{ id: "close", label: "Close pane", danger: true },
|
|
53
|
+
],
|
|
42
54
|
file: [
|
|
43
55
|
{ id: "open", label: "Open" },
|
|
44
56
|
{ id: "newfile", label: "New file", input: "new file" },
|
|
@@ -81,13 +93,24 @@ export const MENU_ITEMS: Record<MenuRegion, MenuItem[]> = {
|
|
|
81
93
|
* panes (the app turned mouse reporting on — presses are forwarded, so a drag
|
|
82
94
|
* can't start a selection) get a leading "Select text…" verb that pauses
|
|
83
95
|
* forwarding for that pane; while its select mode is active the entry flips to
|
|
84
|
-
* the exit verb.
|
|
85
|
-
|
|
96
|
+
* the exit verb. They also get the drag-default toggle (M24.2): the label is
|
|
97
|
+
* always the OTHER default (`dragDefault` is the pane's current one, override
|
|
98
|
+
* and config applied), so picking it flips the pane for the session. Ordinary
|
|
99
|
+
* panes keep the fixed list untouched. */
|
|
100
|
+
export function paneMenuItems(
|
|
101
|
+
appMouse: boolean,
|
|
102
|
+
selectModeOn: boolean,
|
|
103
|
+
dragDefault: "select" | "forward",
|
|
104
|
+
): MenuItem[] {
|
|
86
105
|
if (!appMouse) return MENU_ITEMS.pane;
|
|
87
106
|
const entry: MenuItem = selectModeOn
|
|
88
107
|
? { id: "select-text-off", label: "Stop selecting" }
|
|
89
108
|
: { id: "select-text", label: "Select text…" };
|
|
90
|
-
|
|
109
|
+
const dragToggle: MenuItem =
|
|
110
|
+
dragDefault === "select"
|
|
111
|
+
? { id: "drag-forward", label: "Forward mouse drags" }
|
|
112
|
+
: { id: "drag-select", label: "Select on drag" };
|
|
113
|
+
return [entry, dragToggle, ...MENU_ITEMS.pane];
|
|
91
114
|
}
|
|
92
115
|
|
|
93
116
|
/** The overlay's border (1) + horizontal padding (1 each side). The header row
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* to the top when the query looks like a path.
|
|
13
13
|
*/
|
|
14
14
|
import { fuzzyFilter } from "../team/fuzzy.ts";
|
|
15
|
-
import type { Tab } from "./app-state.ts";
|
|
15
|
+
import type { PaletteUsageEntry, Tab } from "./app-state.ts";
|
|
16
16
|
import type { AgentRowInput } from "./agent-rows.ts";
|
|
17
17
|
import { SETTINGS_PALETTE_COMMANDS, type SettingsCommandId } from "./settings-model.ts";
|
|
18
18
|
|
|
@@ -23,10 +23,26 @@ export type PaletteAction =
|
|
|
23
23
|
| { kind: "open-folder"; label: string }
|
|
24
24
|
| { kind: "attach"; session: string; label: string }
|
|
25
25
|
| { kind: "jump-agent"; paneId: string; session: string; windowIndex: number; label: string }
|
|
26
|
+
// The lifecycle verbs (M23.1) — `agentKind` (not `kind`, taken by the action
|
|
27
|
+
// discriminant) is the manifest id the restart flow resolves a launch
|
|
28
|
+
// command from. Each fleet agent gets one restart + one stop row; "New
|
|
29
|
+
// agent…" opens the spawn flow (kind → placement → run) on the dialog stack.
|
|
30
|
+
| { kind: "new-agent"; label: string }
|
|
31
|
+
// Repeat the current context's remembered spawn directly — no dialog (M24.1).
|
|
32
|
+
| { kind: "new-agent-again"; label: string }
|
|
33
|
+
// The one-surface team console (M24.1): jump/restart/stop + "+ new agent".
|
|
34
|
+
| { kind: "manage-team"; label: string }
|
|
35
|
+
| { kind: "restart-agent"; paneId: string; agentKind: string; session: string; label: string }
|
|
36
|
+
| { kind: "stop-agent"; paneId: string; agentKind: string; session: string; label: string }
|
|
26
37
|
| { kind: "open-file"; path: string; label: string }
|
|
38
|
+
// "Go to file:" (M24.6) — one row per fuzzy-matched REPO file (workspace-
|
|
39
|
+
// relative, ignore-respecting, fed by the app via ctx.repoFiles), unlike
|
|
40
|
+
// open-file whose path is whatever the user typed.
|
|
41
|
+
| { kind: "go-file"; path: string; label: string }
|
|
27
42
|
| { kind: "save"; label: string }
|
|
28
43
|
| { kind: "refresh-diff"; label: string }
|
|
29
44
|
| { kind: "paste-buffer"; label: string }
|
|
45
|
+
| { kind: "search-scrollback"; label: string }
|
|
30
46
|
| { kind: "new-window"; label: string }
|
|
31
47
|
| { kind: "rename-window"; name: string; label: string }
|
|
32
48
|
| { kind: "kill-window"; label: string }
|
|
@@ -69,6 +85,27 @@ export interface PaletteContext {
|
|
|
69
85
|
* "Select text in pane" offered (ordinary panes drag-select directly, so the
|
|
70
86
|
* action would be a no-op that clutters the list). */
|
|
71
87
|
appMousePane?: boolean;
|
|
88
|
+
/** What the current context's remembered spawn is called (`claude`, a custom
|
|
89
|
+
* argv, …) — when set, a direct "New agent: <name> (again)" action is PINNED
|
|
90
|
+
* FIRST, so a repeat spawn is F5 → Enter (M24.1's ≤2-Enters bar). */
|
|
91
|
+
againName?: string | null;
|
|
92
|
+
/** Palette usage history (M24.4) keyed by {@link paletteActionKey}: drives
|
|
93
|
+
* the empty-query "recent" group and the frequency/recency tie-break on a
|
|
94
|
+
* typed query. Persisted in app-state. */
|
|
95
|
+
usage?: Readonly<Record<string, PaletteUsageEntry>>;
|
|
96
|
+
/** The active surface tab (M24.4) — steers the empty-query "suggested"
|
|
97
|
+
* group (Terminal → window/pane verbs + the again spawn; Files → save +
|
|
98
|
+
* open-folder). `terminal` above stays THE gate for the window/pane verbs
|
|
99
|
+
* so existing callers/tests are untouched. */
|
|
100
|
+
surface?: Tab;
|
|
101
|
+
/** Action key → keycap ({@link ../mirror/settings-model.ts}'s
|
|
102
|
+
* PALETTE_KEYCAPS) — rows whose action has one right-align it (M24.4).
|
|
103
|
+
* app.tsx drops `quit` in HOSTED mode, where ^q detaches instead. */
|
|
104
|
+
keycaps?: Readonly<Record<string, string>>;
|
|
105
|
+
/** The workspace's file list (M24.6) — repo-relative paths, gitignore-
|
|
106
|
+
* respecting, capped by the caller. A non-empty query offers fuzzy-matched
|
|
107
|
+
* "Go to file: <path>" rows via {@link goToFileActions}. */
|
|
108
|
+
repoFiles?: readonly string[];
|
|
72
109
|
}
|
|
73
110
|
|
|
74
111
|
const TAB_LABELS: { tab: Tab; label: string }[] = [
|
|
@@ -85,14 +122,24 @@ export function staticPaletteActions(
|
|
|
85
122
|
sessions: string[],
|
|
86
123
|
ctx: PaletteContext = {},
|
|
87
124
|
): PaletteAction[] {
|
|
88
|
-
const actions: PaletteAction[] =
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
125
|
+
const actions: PaletteAction[] = [];
|
|
126
|
+
// The repeat spawn (M24.1) — FIRST, above everything: with memory for the
|
|
127
|
+
// current context, an empty-query Enter repeats the last spawn, making
|
|
128
|
+
// F5 → Enter the whole gesture (the card's ≤2-Enters acceptance bar).
|
|
129
|
+
if (ctx.againName) {
|
|
130
|
+
actions.push({ kind: "new-agent-again", label: `New agent: ${ctx.againName} (again)` });
|
|
131
|
+
}
|
|
132
|
+
for (const t of TAB_LABELS) actions.push({ kind: "tab", tab: t.tab, label: t.label });
|
|
93
133
|
// The non-technicals' front door (M22.5): a filesystem picker → create-or-
|
|
94
134
|
// attach a session in the chosen folder. Offered on every surface.
|
|
95
135
|
actions.push({ kind: "open-folder", label: "Open folder…" });
|
|
136
|
+
// Spawn an agent (M23.1/M24.1) — offered on every surface; ONE dialog whose
|
|
137
|
+
// default placement is contextual (split beside a focused pane, else a new
|
|
138
|
+
// window in the session, else a fresh session).
|
|
139
|
+
actions.push({ kind: "new-agent", label: "New agent…" });
|
|
140
|
+
// The team console (M24.1): every fleet agent in one dialog — jump, restart,
|
|
141
|
+
// stop, spawn. The sidebar agents-header click's keyboard twin.
|
|
142
|
+
actions.push({ kind: "manage-team", label: "Manage team…" });
|
|
96
143
|
for (const s of sessions) {
|
|
97
144
|
actions.push({ kind: "attach", session: s, label: `Attach session: ${s}` });
|
|
98
145
|
}
|
|
@@ -108,6 +155,25 @@ export function staticPaletteActions(
|
|
|
108
155
|
label: `Agent: ${a.kind} · ${a.session} (${a.state})`,
|
|
109
156
|
});
|
|
110
157
|
}
|
|
158
|
+
// The per-agent lifecycle verbs (M23.1), grouped after the jumps so an empty
|
|
159
|
+
// query reads jump-first; the fuzzy filter finds "restart"/"stop" directly.
|
|
160
|
+
for (const a of ctx.agents ?? []) {
|
|
161
|
+
const who = `${a.kind} · ${a.session}`;
|
|
162
|
+
actions.push({
|
|
163
|
+
kind: "restart-agent",
|
|
164
|
+
paneId: a.paneId,
|
|
165
|
+
agentKind: a.kind,
|
|
166
|
+
session: a.session,
|
|
167
|
+
label: `Restart agent: ${who}`,
|
|
168
|
+
});
|
|
169
|
+
actions.push({
|
|
170
|
+
kind: "stop-agent",
|
|
171
|
+
paneId: a.paneId,
|
|
172
|
+
agentKind: a.kind,
|
|
173
|
+
session: a.session,
|
|
174
|
+
label: `Stop agent: ${who}`,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
111
177
|
actions.push({ kind: "save", label: "Save file" });
|
|
112
178
|
actions.push({ kind: "refresh-diff", label: "Refresh diff" });
|
|
113
179
|
// Paste target is the focused surface (a pane, or the editor buffer), so this
|
|
@@ -120,6 +186,10 @@ export function staticPaletteActions(
|
|
|
120
186
|
actions.push({ kind: "settings", id: c.id, label: c.label });
|
|
121
187
|
}
|
|
122
188
|
if (ctx.terminal) {
|
|
189
|
+
// `/` only opens search while scrolled into history (at the live prompt it
|
|
190
|
+
// belongs to the pane — agents' slash commands); this is the always-there
|
|
191
|
+
// entry.
|
|
192
|
+
actions.push({ kind: "search-scrollback", label: "Search scrollback" });
|
|
123
193
|
actions.push({ kind: "new-window", label: "New window" });
|
|
124
194
|
actions.push({ kind: "kill-window", label: "Kill window" });
|
|
125
195
|
actions.push({ kind: "zoom-pane", label: "Zoom pane" });
|
|
@@ -246,9 +316,76 @@ function looksLikePath(q: string): boolean {
|
|
|
246
316
|
return /[/.~]/.test(q);
|
|
247
317
|
}
|
|
248
318
|
|
|
319
|
+
/** Cap on "Go to file:" rows so a big repo never floods the palette. */
|
|
320
|
+
export const GO_FILE_CAP = 8;
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* PURE (M24.6) — the dynamic "Go to file:" rows for `query`: the repo's
|
|
324
|
+
* (ignore-respecting) file list fuzzy-filtered against the query, one open
|
|
325
|
+
* action per match, capped at `cap`. An empty query offers none — the static
|
|
326
|
+
* list stays uncluttered. Strictly ADDITIVE to the palette: this only CALLS
|
|
327
|
+
* the shared fuzzy filter, and its rows are appended after the existing
|
|
328
|
+
* results (see {@link filterPaletteActions}).
|
|
329
|
+
*/
|
|
330
|
+
export function goToFileActions(
|
|
331
|
+
query: string,
|
|
332
|
+
repoFiles: readonly string[],
|
|
333
|
+
cap: number = GO_FILE_CAP,
|
|
334
|
+
): PaletteAction[] {
|
|
335
|
+
const q = query.trim();
|
|
336
|
+
if (q.length === 0 || repoFiles.length === 0) return [];
|
|
337
|
+
return fuzzyFilter(q, [...repoFiles], (p) => p)
|
|
338
|
+
.slice(0, cap)
|
|
339
|
+
.map((m) => ({ kind: "go-file" as const, path: m.item, label: `Go to file: ${m.item}` }));
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* PURE — the STABLE identity of an action for usage history (M24.4): the kind
|
|
344
|
+
* plus the payload fields that survive restarts (session/layout/settings id/
|
|
345
|
+
* path — NOT the label, so relabeling never orphans history, and NOT pane ids,
|
|
346
|
+
* which tmux renumbers). Kinds without a stable payload are keyed by kind
|
|
347
|
+
* alone (rename-window's typed name is query-dependent, not identity).
|
|
348
|
+
*/
|
|
349
|
+
export function paletteActionKey(a: PaletteAction): string {
|
|
350
|
+
switch (a.kind) {
|
|
351
|
+
case "tab":
|
|
352
|
+
return `tab:${a.tab}`;
|
|
353
|
+
case "attach":
|
|
354
|
+
return `attach:${a.session}`;
|
|
355
|
+
case "jump-agent":
|
|
356
|
+
return `jump-agent:${a.session}`;
|
|
357
|
+
case "restart-agent":
|
|
358
|
+
return `restart-agent:${a.session}:${a.agentKind}`;
|
|
359
|
+
case "stop-agent":
|
|
360
|
+
return `stop-agent:${a.session}:${a.agentKind}`;
|
|
361
|
+
case "select-layout":
|
|
362
|
+
return `select-layout:${a.layout}`;
|
|
363
|
+
case "settings":
|
|
364
|
+
return `settings:${a.id}`;
|
|
365
|
+
case "open-file":
|
|
366
|
+
return `open-file:${a.path}`;
|
|
367
|
+
default:
|
|
368
|
+
return a.kind;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/** PURE — the usage comparator (frequency first, then recency): negative when
|
|
373
|
+
* `a` should rank before `b`. Unused actions rank last, mutually tied. */
|
|
374
|
+
function compareUsage(
|
|
375
|
+
a: PaletteAction,
|
|
376
|
+
b: PaletteAction,
|
|
377
|
+
usage: Readonly<Record<string, PaletteUsageEntry>> | undefined,
|
|
378
|
+
): number {
|
|
379
|
+
const ua = usage?.[paletteActionKey(a)];
|
|
380
|
+
const ub = usage?.[paletteActionKey(b)];
|
|
381
|
+
return (ub?.count ?? 0) - (ua?.count ?? 0) || (ub?.lastUsed ?? 0) - (ua?.lastUsed ?? 0);
|
|
382
|
+
}
|
|
383
|
+
|
|
249
384
|
/**
|
|
250
385
|
* PURE — the palette result list for `query`: the static actions fuzzy-filtered
|
|
251
|
-
* and score-sorted
|
|
386
|
+
* and score-sorted (exact/prefix matches outrank mid-word via the scorer's
|
|
387
|
+
* label-start weighting; score TIES break by usage — frequency then recency —
|
|
388
|
+
* M24.4), plus (when the query is non-empty) a dynamic
|
|
252
389
|
* "Open file: <query>" action. That open-file entry is pinned FIRST when the
|
|
253
390
|
* query looks like a path, otherwise appended LAST so it never buries a real
|
|
254
391
|
* match. An empty query returns every static action in natural order.
|
|
@@ -261,7 +398,11 @@ export function filterPaletteActions(
|
|
|
261
398
|
const statics = staticPaletteActions(sessions, ctx);
|
|
262
399
|
const q = query.trim();
|
|
263
400
|
const matched =
|
|
264
|
-
q.length === 0
|
|
401
|
+
q.length === 0
|
|
402
|
+
? statics
|
|
403
|
+
: fuzzyFilter(query, statics, (a) => a.label)
|
|
404
|
+
.sort((x, y) => y.score - x.score || compareUsage(x.item, y.item, ctx.usage))
|
|
405
|
+
.map((m) => m.item);
|
|
265
406
|
if (q.length === 0) return matched;
|
|
266
407
|
const dynamic: PaletteAction[] = [{ kind: "open-file", path: q, label: `Open file: ${q}` }];
|
|
267
408
|
// On the Terminal surface a non-empty query also offers a rename-to-<query>
|
|
@@ -270,5 +411,154 @@ export function filterPaletteActions(
|
|
|
270
411
|
if (ctx.terminal) {
|
|
271
412
|
dynamic.push({ kind: "rename-window", name: q, label: `Rename window: ${q}` });
|
|
272
413
|
}
|
|
273
|
-
|
|
414
|
+
// "Go to file:" rows (M24.6) are APPENDED after everything so this addition
|
|
415
|
+
// cannot disturb the existing result ranking.
|
|
416
|
+
const goFiles = goToFileActions(q, ctx.repoFiles ?? []);
|
|
417
|
+
return looksLikePath(q)
|
|
418
|
+
? [...dynamic, ...matched, ...goFiles]
|
|
419
|
+
: [...matched, ...dynamic, ...goFiles];
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// ── Grouped rows (M24.4 — the palette's list model) ─────────────────────────
|
|
423
|
+
// The overlay renders ROWS, not bare actions: on an EMPTY query the list opens
|
|
424
|
+
// with a "recent" group (top used actions), a contextual "suggested" group,
|
|
425
|
+
// then everything else under "commands"; a typed query is one flat ranked list
|
|
426
|
+
// (no headers). Headers are real rows — they occupy a screen line, scroll with
|
|
427
|
+
// the list, and are NOT selectable/clickable — so the selection helpers below
|
|
428
|
+
// are what the keyboard/router use to move between action rows.
|
|
429
|
+
|
|
430
|
+
/** One rendered palette line: a group header, or an action with its optional
|
|
431
|
+
* right-aligned keycap (from ctx.keycaps). */
|
|
432
|
+
export type PaletteRow =
|
|
433
|
+
| { type: "header"; label: string }
|
|
434
|
+
| { type: "action"; action: PaletteAction; shortcut: string | null };
|
|
435
|
+
|
|
436
|
+
/** How many actions the empty-query "recent" group shows at most. */
|
|
437
|
+
export const PALETTE_RECENT_LIMIT = 5;
|
|
438
|
+
|
|
439
|
+
/** The Terminal surface's suggested window/pane verbs, in offer order. */
|
|
440
|
+
const TERMINAL_SUGGESTED_KINDS: ReadonlyArray<PaletteAction["kind"]> = [
|
|
441
|
+
"new-window",
|
|
442
|
+
"kill-window",
|
|
443
|
+
"zoom-pane",
|
|
444
|
+
"swap-pane",
|
|
445
|
+
"break-pane",
|
|
446
|
+
"rotate-window",
|
|
447
|
+
];
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* PURE — the palette rows for `query` (M24.4). A non-empty query is the flat
|
|
451
|
+
* {@link filterPaletteActions} ranking. An empty query groups:
|
|
452
|
+
* 1. "recent" — up to {@link PALETTE_RECENT_LIMIT} previously-run actions
|
|
453
|
+
* still offerable now (usage keys matched against the current statics),
|
|
454
|
+
* most-frequently-used first, recency breaking ties;
|
|
455
|
+
* 2. "suggested" — BLOCKED agents' jump actions first (ctx.agents arrives
|
|
456
|
+
* attention-sorted, so their relative order is the sidebar's), then the
|
|
457
|
+
* surface's verbs: Terminal → the again-spawn + window/pane verbs, Files
|
|
458
|
+
* → save + open-folder;
|
|
459
|
+
* 3. "commands" — every remaining action in today's natural order.
|
|
460
|
+
* With nothing to group (no usage, no suggestions) the list is exactly the
|
|
461
|
+
* ungrouped statics — no headers, same as before this card.
|
|
462
|
+
*/
|
|
463
|
+
export function paletteRows(
|
|
464
|
+
query: string,
|
|
465
|
+
sessions: string[],
|
|
466
|
+
ctx: PaletteContext = {},
|
|
467
|
+
): PaletteRow[] {
|
|
468
|
+
const row = (action: PaletteAction): PaletteRow => ({
|
|
469
|
+
type: "action",
|
|
470
|
+
action,
|
|
471
|
+
shortcut: ctx.keycaps?.[paletteActionKey(action)] ?? null,
|
|
472
|
+
});
|
|
473
|
+
if (query.trim().length > 0) return filterPaletteActions(query, sessions, ctx).map(row);
|
|
474
|
+
|
|
475
|
+
const statics = staticPaletteActions(sessions, ctx);
|
|
476
|
+
// First static per key — recents resolve through this so a persisted key
|
|
477
|
+
// finds the action's CURRENT incarnation (relabels don't orphan history).
|
|
478
|
+
const byKey = new Map<string, PaletteAction>();
|
|
479
|
+
for (const a of statics) {
|
|
480
|
+
const k = paletteActionKey(a);
|
|
481
|
+
if (!byKey.has(k)) byKey.set(k, a);
|
|
482
|
+
}
|
|
483
|
+
const taken = new Set<PaletteAction>();
|
|
484
|
+
|
|
485
|
+
const recent: PaletteAction[] = [];
|
|
486
|
+
const usedKeys = Object.entries(ctx.usage ?? {})
|
|
487
|
+
.sort(([, a], [, b]) => b.count - a.count || b.lastUsed - a.lastUsed)
|
|
488
|
+
.map(([k]) => k);
|
|
489
|
+
for (const k of usedKeys) {
|
|
490
|
+
if (recent.length >= PALETTE_RECENT_LIMIT) break;
|
|
491
|
+
const a = byKey.get(k);
|
|
492
|
+
if (a && !taken.has(a)) {
|
|
493
|
+
recent.push(a);
|
|
494
|
+
taken.add(a);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const suggested: PaletteAction[] = [];
|
|
499
|
+
const suggest = (pred: (a: PaletteAction) => boolean): void => {
|
|
500
|
+
for (const a of statics) {
|
|
501
|
+
if (!taken.has(a) && pred(a)) {
|
|
502
|
+
suggested.push(a);
|
|
503
|
+
taken.add(a);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
// Blocked agents outrank every other suggestion — they are why the user is
|
|
508
|
+
// opening the palette (the sidebar's attention-first law, applied here).
|
|
509
|
+
const blockedPanes = new Set(
|
|
510
|
+
(ctx.agents ?? []).filter((x) => x.state === "blocked").map((x) => x.paneId),
|
|
511
|
+
);
|
|
512
|
+
suggest((a) => a.kind === "jump-agent" && blockedPanes.has(a.paneId));
|
|
513
|
+
if (ctx.surface === "terminal") {
|
|
514
|
+
suggest((a) => a.kind === "new-agent-again");
|
|
515
|
+
suggest((a) => TERMINAL_SUGGESTED_KINDS.includes(a.kind));
|
|
516
|
+
} else if (ctx.surface === "files") {
|
|
517
|
+
suggest((a) => a.kind === "save" || a.kind === "open-folder");
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if (recent.length === 0 && suggested.length === 0) return statics.map(row);
|
|
521
|
+
const rows: PaletteRow[] = [];
|
|
522
|
+
if (recent.length > 0) {
|
|
523
|
+
rows.push({ type: "header", label: "recent" });
|
|
524
|
+
for (const a of recent) rows.push(row(a));
|
|
525
|
+
}
|
|
526
|
+
if (suggested.length > 0) {
|
|
527
|
+
rows.push({ type: "header", label: "suggested" });
|
|
528
|
+
for (const a of suggested) rows.push(row(a));
|
|
529
|
+
}
|
|
530
|
+
rows.push({ type: "header", label: "commands" });
|
|
531
|
+
for (const a of statics) if (!taken.has(a)) rows.push(row(a));
|
|
532
|
+
return rows;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/** PURE — the first selectable (action) row index, or -1 when none. Where the
|
|
536
|
+
* selection starts on open/query edits (headers are never selected). */
|
|
537
|
+
export function firstPaletteAction(rows: readonly PaletteRow[]): number {
|
|
538
|
+
return rows.findIndex((r) => r.type === "action");
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/** PURE — the next action row from `cur` in `dir` (±1), skipping headers;
|
|
542
|
+
* stays put at the list's ends. The keyboard's up/down. */
|
|
543
|
+
export function stepPaletteRow(rows: readonly PaletteRow[], cur: number, dir: 1 | -1): number {
|
|
544
|
+
for (let i = cur + dir; i >= 0 && i < rows.length; i += dir) {
|
|
545
|
+
if (rows[i]!.type === "action") return i;
|
|
546
|
+
}
|
|
547
|
+
return cur;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* PURE — one action row's text after the selection prefix: the label, then —
|
|
552
|
+
* when the row has a keycap — right-aligned padding + the keycap inside
|
|
553
|
+
* `width` cells. The label truncates with an ellipsis so the keycap ALWAYS
|
|
554
|
+
* fits (min one space between); without a keycap the label just truncates.
|
|
555
|
+
*/
|
|
556
|
+
export function paletteRowText(label: string, shortcut: string | null, width: number): string {
|
|
557
|
+
if (width <= 0) return "";
|
|
558
|
+
if (!shortcut || shortcut.length + 2 > width) {
|
|
559
|
+
return label.length <= width ? label : label.slice(0, Math.max(0, width - 1)) + "…";
|
|
560
|
+
}
|
|
561
|
+
const labelMax = width - shortcut.length - 1;
|
|
562
|
+
const shown = label.length <= labelMax ? label : label.slice(0, Math.max(0, labelMax - 1)) + "…";
|
|
563
|
+
return shown + " ".repeat(width - shown.length - shortcut.length) + shortcut;
|
|
274
564
|
}
|
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
type GraphemeOverride,
|
|
41
41
|
} from "./blit.ts";
|
|
42
42
|
import { AckWriter } from "./ack-writer.ts";
|
|
43
|
+
import { extractSelection, type Cell } from "./selection.ts";
|
|
43
44
|
|
|
44
45
|
/** OpenTUI TextAttributes bit values (kept literal to avoid the dep here). */
|
|
45
46
|
const ATTR_BOLD = 1;
|
|
@@ -163,6 +164,18 @@ export class PaneMirror {
|
|
|
163
164
|
* fast path. Counted from xterm's onScroll (which fires once per scrolled
|
|
164
165
|
* line and keeps firing at the scrollback cap, unlike the saturating payload). */
|
|
165
166
|
private _pendingScroll = 0;
|
|
167
|
+
// ── Absolute-line stability (M25.6) ────────────────────────────────────────
|
|
168
|
+
/** Lines TRIMMED off the top of the scrollback (monotonic): once viewportY
|
|
169
|
+
* saturates at the scrollback cap, each further scroll rotates one line out
|
|
170
|
+
* and every retained line's absolute index drops by one. An absolute-space
|
|
171
|
+
* consumer (the drag selection) records `lineTrim()` when it anchors and
|
|
172
|
+
* subtracts the delta later, so its anchor keeps naming the same content.
|
|
173
|
+
* Detected as "onScroll fired but viewportY did not grow". A scrollback
|
|
174
|
+
* clear (`clear-history` / ED3) collapses viewportY instead — that index
|
|
175
|
+
* shift is NOT counted; stale absolute cells then clamp into the buffer
|
|
176
|
+
* (the selection was over content that no longer exists anyway). */
|
|
177
|
+
private _trimmed = 0;
|
|
178
|
+
private _lastViewportY = 0;
|
|
166
179
|
/** The alt/normal buffer swapped (`?1049h/l`) — the whole grid is new. */
|
|
167
180
|
private _bufferSwapped = false;
|
|
168
181
|
/** Shadow of the last-blitted rows' raw xterm cell data (`_line._data`,
|
|
@@ -175,12 +188,19 @@ export class PaneMirror {
|
|
|
175
188
|
* incremental. */
|
|
176
189
|
private _incremental = true;
|
|
177
190
|
|
|
178
|
-
constructor(cols: number, rows: number) {
|
|
191
|
+
constructor(cols: number, rows: number, scrollback = 5000) {
|
|
179
192
|
this.cols = cols;
|
|
180
193
|
this.rows = rows;
|
|
181
|
-
this.term = new Terminal({ cols, rows, allowProposedApi: true, scrollback
|
|
194
|
+
this.term = new Terminal({ cols, rows, allowProposedApi: true, scrollback });
|
|
182
195
|
this.term.onWriteParsed(() => this._version++);
|
|
183
|
-
this.term.onScroll(() =>
|
|
196
|
+
this.term.onScroll(() => {
|
|
197
|
+
this._pendingScroll++;
|
|
198
|
+
// Trim detection (M25.6): below the cap each scroll grows viewportY by
|
|
199
|
+
// one; at the cap it stays put and the oldest line rotated out.
|
|
200
|
+
const vy = this.term.buffer.active.viewportY;
|
|
201
|
+
if (vy === this._lastViewportY) this._trimmed++;
|
|
202
|
+
else this._lastViewportY = vy;
|
|
203
|
+
});
|
|
184
204
|
this.term.buffer.onBufferChange(() => {
|
|
185
205
|
this._bufferSwapped = true;
|
|
186
206
|
this._version++;
|
|
@@ -234,9 +254,20 @@ export class PaneMirror {
|
|
|
234
254
|
this._shadow = null;
|
|
235
255
|
this._pendingScroll = 0;
|
|
236
256
|
this._bufferSwapped = false;
|
|
257
|
+
// Reflow moved viewportY without scrolls — re-baseline the trim detector so
|
|
258
|
+
// the first post-resize scroll isn't miscounted as a cap rotation. (Reflow
|
|
259
|
+
// also re-indexes wrapped lines; a mid-resize drag selection is approximate
|
|
260
|
+
// by nature and clamps safely.)
|
|
261
|
+
this._lastViewportY = this.term.buffer.active.viewportY;
|
|
237
262
|
this._version++;
|
|
238
263
|
}
|
|
239
264
|
|
|
265
|
+
/** Monotonic count of lines trimmed off the scrollback top (M25.6) — the
|
|
266
|
+
* absolute-line drift an anchored consumer must subtract. See {@link _trimmed}. */
|
|
267
|
+
lineTrim(): number {
|
|
268
|
+
return this._trimmed;
|
|
269
|
+
}
|
|
270
|
+
|
|
240
271
|
/** Lines available above the live viewport (how far back scroll can go). */
|
|
241
272
|
scrollbackDepth(): number {
|
|
242
273
|
return this.term.buffer.active.viewportY;
|
|
@@ -420,7 +451,12 @@ export class PaneMirror {
|
|
|
420
451
|
const forceRows = opts.forceRows && opts.forceRows.length ? opts.forceRows : null;
|
|
421
452
|
|
|
422
453
|
for (let y = 0; y < height; y++) {
|
|
423
|
-
const
|
|
454
|
+
const raw = this._incremental ? this.rowData(baseY, y) : null;
|
|
455
|
+
// Bounds guard (M23.5, D5): after a shrink xterm's reflow can leave a
|
|
456
|
+
// line's `_data` at a length ≠ cols×3; comparing or `set`ting it against
|
|
457
|
+
// the rowLen-strided shadow would read garbage / throw RangeError. Treat
|
|
458
|
+
// such a row as shadow-less: always repaint it, never write the shadow.
|
|
459
|
+
const data = raw !== null && raw.length === rowLen ? raw : null;
|
|
424
460
|
let dirty = full || y >= bottomDirtyFrom || data === null; // no shadow info for this row → always repaint
|
|
425
461
|
if (!dirty && this._shadow) dirty = !shadowMatches(this._shadow, y * rowLen, data!);
|
|
426
462
|
if (!dirty && forceRows) {
|
|
@@ -555,6 +591,48 @@ export class PaneMirror {
|
|
|
555
591
|
return out;
|
|
556
592
|
}
|
|
557
593
|
|
|
594
|
+
/**
|
|
595
|
+
* Extract the text of an ORDERED absolute-line range (M25.6) — the release
|
|
596
|
+
* path of a selection that may span far beyond the viewport. Each buffer
|
|
597
|
+
* line reads via `translateToString(true)` — the SAME trim/collapse as
|
|
598
|
+
* {@link visibleRowTexts} (wide-glyph spacers collapse, trailing blanks trim)
|
|
599
|
+
* — and the columns/joining go through {@link extractSelection}, so a
|
|
600
|
+
* selection that happens to fit one screen extracts byte-identically to the
|
|
601
|
+
* old visible-rows path. Wrapped lines stay separate visual rows joined with
|
|
602
|
+
* "\n", exactly as the on-screen copy always has.
|
|
603
|
+
*
|
|
604
|
+
* `maxBytes` bounds the string BUILT here: accumulation stops once the raw
|
|
605
|
+
* line total strictly exceeds it, so a runaway span never materializes tens
|
|
606
|
+
* of MB — the caller's clipboard cap (which refuses anything over the limit)
|
|
607
|
+
* still sees an over-limit byte count and refuses honestly.
|
|
608
|
+
*/
|
|
609
|
+
extractAbsoluteText(start: Cell, end: Cell, maxBytes: number): string {
|
|
610
|
+
const buf = this.term.buffer.active;
|
|
611
|
+
const last = buf.length - 1;
|
|
612
|
+
if (last < 0 || end.row < 0 || start.row > last) return "";
|
|
613
|
+
const lo = Math.max(0, Math.min(start.row, last));
|
|
614
|
+
const hi = Math.max(0, Math.min(end.row, last));
|
|
615
|
+
const rowTexts: string[] = [];
|
|
616
|
+
let bytes = 0;
|
|
617
|
+
for (let y = lo; y <= hi; y++) {
|
|
618
|
+
const line = buf.getLine(y);
|
|
619
|
+
const text = line ? line.translateToString(true) : "";
|
|
620
|
+
rowTexts.push(text);
|
|
621
|
+
bytes += Buffer.byteLength(text, "utf8") + 1; // +1 for the joining "\n"
|
|
622
|
+
if (bytes > maxBytes) break;
|
|
623
|
+
}
|
|
624
|
+
// Shift the range to the accumulated slice. A clamped start selects from
|
|
625
|
+
// column 0 (its true start line is gone/off-buffer); a clamped or
|
|
626
|
+
// byte-truncated end extends to its last line's end (Infinity clamps to
|
|
627
|
+
// the row length inside rowSelectionRange).
|
|
628
|
+
const lastRowAbs = lo + rowTexts.length - 1;
|
|
629
|
+
return extractSelection(
|
|
630
|
+
rowTexts,
|
|
631
|
+
{ row: 0, col: start.row === lo ? start.col : 0 },
|
|
632
|
+
{ row: rowTexts.length - 1, col: lastRowAbs === end.row ? end.col : Infinity },
|
|
633
|
+
);
|
|
634
|
+
}
|
|
635
|
+
|
|
558
636
|
dispose(): void {
|
|
559
637
|
this.term.dispose();
|
|
560
638
|
}
|
|
@@ -31,7 +31,7 @@ import { appendFileSync } from "node:fs";
|
|
|
31
31
|
import type { SessionMirror } from "./session-mirror.ts";
|
|
32
32
|
import type { CursorState } from "./pane-mirror.ts";
|
|
33
33
|
import { swapCells, paintBg, type GraphemeOverride } from "./blit.ts";
|
|
34
|
-
import { rowSelectionRange, type Cell } from "./selection.ts";
|
|
34
|
+
import { rowSelectionRange, visibleSelRows, type Cell } from "./selection.ts";
|
|
35
35
|
import type { SearchMatch } from "./search-model.ts";
|
|
36
36
|
|
|
37
37
|
/** The scrollback-search highlight payload for one pane: matches keyed by
|
|
@@ -60,7 +60,10 @@ export interface PaneSurfaceOptions extends RenderableOptions<FrameBufferRendera
|
|
|
60
60
|
paneFocused?: boolean;
|
|
61
61
|
/** Bumps (coalesced, once per state tick) when this pane's content changed. */
|
|
62
62
|
contentVersion?: number;
|
|
63
|
-
/** The drag selection on THIS pane (already surface/pane-filtered
|
|
63
|
+
/** The drag selection on THIS pane (already surface/pane-filtered and
|
|
64
|
+
* ordered), or null. ABSOLUTE buffer lines (M25.6): the walk maps them to
|
|
65
|
+
* visible rows per-frame against the pane's live baseY (depth − offset), so
|
|
66
|
+
* the highlight stays glued to its content while the view scrolls. */
|
|
64
67
|
selRange?: { start: Cell; end: Cell } | null;
|
|
65
68
|
search?: PaneSearchHighlight | null;
|
|
66
69
|
}
|
|
@@ -234,7 +237,14 @@ class PaneSurfaceRenderable extends FrameBufferRenderable {
|
|
|
234
237
|
this._lastScroll = this._scrollOffset;
|
|
235
238
|
this._lastFocused = this._focusedPane;
|
|
236
239
|
|
|
237
|
-
|
|
240
|
+
// The view's absolute→visible mapping (M25.6), re-read every walk: both the
|
|
241
|
+
// selection (absolute cells) and the search matches (absolute lines) resolve
|
|
242
|
+
// to visible rows against the CURRENT baseY, so a scroll — or new content
|
|
243
|
+
// pushing the depth — moves the highlights with their text. Same clamping as
|
|
244
|
+
// the blit's own offset math (depth here == viewportY there).
|
|
245
|
+
const depth = this._mirror.scrollbackDepth(this._paneId);
|
|
246
|
+
const baseY = depth - Math.min(Math.max(0, this._scrollOffset), depth);
|
|
247
|
+
const newSelRows = this.selRows(h, baseY);
|
|
238
248
|
const newSearchRows = this.searchRows(h);
|
|
239
249
|
// The live cursor (M21.6): the focused pane drives the hardware cursor; an
|
|
240
250
|
// unfocused pane paints a quiet marker on its cursor cell. Read once.
|
|
@@ -309,7 +319,7 @@ class PaneSurfaceRenderable extends FrameBufferRenderable {
|
|
|
309
319
|
if (sel) {
|
|
310
320
|
for (let i = 0; i < newSelRows.length; i++) {
|
|
311
321
|
const y = newSelRows[i]!;
|
|
312
|
-
const r = rowSelectionRange(y, w, sel.start, sel.end);
|
|
322
|
+
const r = rowSelectionRange(baseY + y, w, sel.start, sel.end);
|
|
313
323
|
if (r) swapCells(buffers, w, y, r.from, r.to);
|
|
314
324
|
}
|
|
315
325
|
}
|
|
@@ -370,15 +380,12 @@ class PaneSurfaceRenderable extends FrameBufferRenderable {
|
|
|
370
380
|
}
|
|
371
381
|
|
|
372
382
|
/** Visible rows covered by the current drag selection (clamped), or []. The
|
|
373
|
-
* range arrives ordered (app.tsx passes `orderCells`)
|
|
374
|
-
|
|
383
|
+
* range arrives ordered (app.tsx passes `orderCells`) in ABSOLUTE buffer
|
|
384
|
+
* lines; `baseY` is the view's current absolute top (M25.6). */
|
|
385
|
+
private selRows(height: number, baseY: number): number[] {
|
|
375
386
|
const sel = this._sel;
|
|
376
387
|
if (!sel) return [];
|
|
377
|
-
|
|
378
|
-
const hi = Math.min(height - 1, sel.end.row);
|
|
379
|
-
const out: number[] = [];
|
|
380
|
-
for (let y = lo; y <= hi; y++) out.push(y);
|
|
381
|
-
return out;
|
|
388
|
+
return visibleSelRows(sel.start, sel.end, baseY, height);
|
|
382
389
|
}
|
|
383
390
|
|
|
384
391
|
/** Distinct visible rows carrying a search match (clamped), or []. */
|