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,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-app attention surfacing (M25.1) — the PURE half of "an agent you can't
|
|
3
|
+
* see needs you". The app's 3s fleet poll diffs the previous per-pane agent
|
|
4
|
+
* states against the fresh payload; blocked/done transitions for agents NOT on
|
|
5
|
+
* the current screen (another workspace, another window, or a non-Terminal
|
|
6
|
+
* tab) become a status-strip note plus a brief sidebar flash. The diff, the
|
|
7
|
+
* visibility test, and the note text live here so they unit-test without
|
|
8
|
+
* OpenTUI; app.tsx supplies the signals and timers.
|
|
9
|
+
*
|
|
10
|
+
* First sight is graced exactly like the chrome updater's notification path
|
|
11
|
+
* (`from: null` never notes) — an app boot must not re-announce every agent
|
|
12
|
+
* that was already blocked.
|
|
13
|
+
*/
|
|
14
|
+
import type { AgentStatus } from "../detect/classify.ts";
|
|
15
|
+
|
|
16
|
+
/** The per-agent slice of the fleet payload the diff needs. */
|
|
17
|
+
export interface AttentionAgent {
|
|
18
|
+
paneId: string;
|
|
19
|
+
session: string;
|
|
20
|
+
kind: string;
|
|
21
|
+
state: AgentStatus;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** One agent's state change between two fleet polls. */
|
|
25
|
+
export interface AttentionTransition {
|
|
26
|
+
paneId: string;
|
|
27
|
+
session: string;
|
|
28
|
+
kind: string;
|
|
29
|
+
from: AgentStatus | null;
|
|
30
|
+
to: AgentStatus;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** What the app is currently showing — the visibility side of the test. */
|
|
34
|
+
export interface AttentionView {
|
|
35
|
+
/** The active surface tab (only `terminal` shows panes at all). */
|
|
36
|
+
tab: string;
|
|
37
|
+
/** Pane ids on screen right now (the mirrored window's panes). */
|
|
38
|
+
visiblePaneIds: readonly string[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The two states worth interrupting the user over (mirrors the chrome
|
|
42
|
+
* updater's NOTIFY_STATES). */
|
|
43
|
+
const NOTE_STATES: ReadonlySet<AgentStatus> = new Set<AgentStatus>(["blocked", "done"]);
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* PURE — diff the previous per-pane states against this poll's agents.
|
|
47
|
+
* Returns the transitions plus the fresh state map to thread into the next
|
|
48
|
+
* poll (the input map is NOT mutated). A pane that vanished emits nothing and
|
|
49
|
+
* drops out of the state.
|
|
50
|
+
*/
|
|
51
|
+
export function diffAttention(
|
|
52
|
+
prev: ReadonlyMap<string, AgentStatus>,
|
|
53
|
+
agents: readonly AttentionAgent[],
|
|
54
|
+
): { transitions: AttentionTransition[]; next: Map<string, AgentStatus> } {
|
|
55
|
+
const next = new Map<string, AgentStatus>();
|
|
56
|
+
const transitions: AttentionTransition[] = [];
|
|
57
|
+
for (const a of agents) {
|
|
58
|
+
if (next.has(a.paneId)) continue; // deduped: a session can surface under two projects
|
|
59
|
+
const before = prev.has(a.paneId) ? prev.get(a.paneId)! : null;
|
|
60
|
+
next.set(a.paneId, a.state);
|
|
61
|
+
if (before === a.state) continue;
|
|
62
|
+
transitions.push({
|
|
63
|
+
paneId: a.paneId,
|
|
64
|
+
session: a.session,
|
|
65
|
+
kind: a.kind,
|
|
66
|
+
from: before,
|
|
67
|
+
to: a.state,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
return { transitions, next };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** PURE — is this pane on the app's screen right now? Only the Terminal tab
|
|
74
|
+
* shows panes; there, exactly the mirrored window's panes are visible. */
|
|
75
|
+
export function isPaneVisible(paneId: string, view: AttentionView): boolean {
|
|
76
|
+
return view.tab === "terminal" && view.visiblePaneIds.includes(paneId);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* PURE — the transitions worth a note: blocked/done, not first-sight, and not
|
|
81
|
+
* currently visible (the user can see a visible pane flip themselves).
|
|
82
|
+
*/
|
|
83
|
+
export function noteworthyTransitions(
|
|
84
|
+
transitions: readonly AttentionTransition[],
|
|
85
|
+
view: AttentionView,
|
|
86
|
+
): AttentionTransition[] {
|
|
87
|
+
return transitions.filter(
|
|
88
|
+
(t) => t.from !== null && NOTE_STATES.has(t.to) && !isPaneVisible(t.paneId, view),
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** PURE — the status-strip note for one transition, e.g.
|
|
93
|
+
* `● claude blocked · zz-api — click agents`. Done reads `✓ … — click agents`. */
|
|
94
|
+
export function attentionNote(t: AttentionTransition): string {
|
|
95
|
+
const glyph = t.to === "blocked" ? "●" : "✓";
|
|
96
|
+
return `${glyph} ${t.kind} ${t.to} · ${t.session} — click agents`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** PURE — the note line for a whole poll's noteworthy set: the first (most
|
|
100
|
+
* recent diff order) transition's note, with a `(+N)` tail when several agents
|
|
101
|
+
* flipped in the same poll. Null for an empty set. */
|
|
102
|
+
export function attentionNoteLine(worthy: readonly AttentionTransition[]): string | null {
|
|
103
|
+
const first = worthy[0];
|
|
104
|
+
if (!first) return null;
|
|
105
|
+
const extra = worthy.length > 1 ? ` (+${worthy.length - 1})` : "";
|
|
106
|
+
return `${attentionNote(first)}${extra}`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** How long the sidebar attention flash lasts. */
|
|
110
|
+
export const ATTENTION_FLASH_MS = 2500;
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure cell → framebuffer writers for the framebuffer pane-blit path (M21.3).
|
|
3
|
+
*
|
|
4
|
+
* These map a mirror grid straight into an OpenTUI `OptimizedBuffer`'s typed
|
|
5
|
+
* arrays — no `StyledRun[]` intermediary, no per-run `RGBA` allocation, no Solid
|
|
6
|
+
* subtree. One packed cell is `char[i]` (a codepoint), four `fg`/`bg` channels
|
|
7
|
+
* (`r,g,b,a`, 0–255) at `i*4`, and one `attributes[i]` bitmask — the exact layout
|
|
8
|
+
* `OptimizedBuffer.buffers` exposes.
|
|
9
|
+
*
|
|
10
|
+
* Kept dependency-free (plain typed arrays) so the packing is unit-testable
|
|
11
|
+
* without the native render lib; {@link PaneMirror.blit} does the xterm walk and
|
|
12
|
+
* feeds these.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** The spacer (second) half of a wide glyph: char `0`. The native renderer knows
|
|
16
|
+
* a cell is wide by MEASURING the lead cell's codepoint width and skips the
|
|
17
|
+
* spacer, so the spacer's char just needs to be empty — this is exactly what
|
|
18
|
+
* `OptimizedBuffer.setCell` writes for the trailing half (verified: a wide
|
|
19
|
+
* glyph → `char[i]=codepoint, char[i+1]=0`). NB the `0xC0000000`
|
|
20
|
+
* `CHAR_FLAG_CONTINUATION` seen in OptimizedBuffer is a DIFFERENT consumer (the
|
|
21
|
+
* JS `getSpanLines`/`getRealCharBytes` text reader, off our render path);
|
|
22
|
+
* writing it here blanks wide glyphs on screen (measured). */
|
|
23
|
+
export const CHAR_CONTINUATION = 0;
|
|
24
|
+
|
|
25
|
+
/** The space codepoint — the fill for blank / out-of-range cells. */
|
|
26
|
+
export const SPACE_CODE = 0x20;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* INVERSE is rendered by SWAPPING fg/bg, not by the OpenTUI INVERSE attribute
|
|
30
|
+
* bit: a framebuffer cell carrying that bit composites (drawFrameBuffer preserves
|
|
31
|
+
* it) but does NOT flush as reverse-video — the direct `<text>` path does, the
|
|
32
|
+
* framebuffer path doesn't (measured). Swapping colors always renders (truecolor
|
|
33
|
+
* channels propagate cleanly), and covers app-driven reverse (vim status line),
|
|
34
|
+
* the cursor cell, and the drag selection uniformly. Swaps compose like the old
|
|
35
|
+
* XOR: two swaps on a cell (selection over the cursor) return it to normal.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
/** The four packed arrays behind an `OptimizedBuffer` (or a plain-array stand-in
|
|
39
|
+
* for tests). `fg`/`bg` hold four `u16` channels (0–255) per cell at `i*4`. */
|
|
40
|
+
export interface CellArrays {
|
|
41
|
+
char: Uint32Array;
|
|
42
|
+
fg: Uint16Array;
|
|
43
|
+
bg: Uint16Array;
|
|
44
|
+
attributes: Uint32Array;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* A cell whose grapheme is more than one codepoint (ZWJ/flag emoji, combining
|
|
49
|
+
* marks) — a single `char[i]` u32 can't hold it. The fast blit writes its base
|
|
50
|
+
* codepoint; the caller (which owns the native `OptimizedBuffer`) re-writes these
|
|
51
|
+
* few cells with the full string via `setCell`, keeping full parity for the rare
|
|
52
|
+
* case without an allocation on the common path.
|
|
53
|
+
*/
|
|
54
|
+
export interface GraphemeOverride {
|
|
55
|
+
x: number;
|
|
56
|
+
y: number;
|
|
57
|
+
chars: string;
|
|
58
|
+
/** Packed `0xRRGGBB` or null (terminal default). */
|
|
59
|
+
fg: number | null;
|
|
60
|
+
bg: number | null;
|
|
61
|
+
attrs: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Write one styled cell at flat index `idx`. `fg`/`bg` are packed `0xRRGGBB` or
|
|
66
|
+
* `null` for the terminal default (→ the supplied `d*` channels); alpha is always
|
|
67
|
+
* opaque (panes never blend). No allocation — just eight typed-array stores.
|
|
68
|
+
*/
|
|
69
|
+
export function writeCell(
|
|
70
|
+
buf: CellArrays,
|
|
71
|
+
idx: number,
|
|
72
|
+
codepoint: number,
|
|
73
|
+
fg: number | null,
|
|
74
|
+
bg: number | null,
|
|
75
|
+
attrs: number,
|
|
76
|
+
dfR: number,
|
|
77
|
+
dfG: number,
|
|
78
|
+
dfB: number,
|
|
79
|
+
dbR: number,
|
|
80
|
+
dbG: number,
|
|
81
|
+
dbB: number,
|
|
82
|
+
): void {
|
|
83
|
+
buf.char[idx] = codepoint;
|
|
84
|
+
const o = idx * 4;
|
|
85
|
+
if (fg === null) {
|
|
86
|
+
buf.fg[o] = dfR;
|
|
87
|
+
buf.fg[o + 1] = dfG;
|
|
88
|
+
buf.fg[o + 2] = dfB;
|
|
89
|
+
} else {
|
|
90
|
+
buf.fg[o] = (fg >> 16) & 0xff;
|
|
91
|
+
buf.fg[o + 1] = (fg >> 8) & 0xff;
|
|
92
|
+
buf.fg[o + 2] = fg & 0xff;
|
|
93
|
+
}
|
|
94
|
+
buf.fg[o + 3] = 255;
|
|
95
|
+
if (bg === null) {
|
|
96
|
+
buf.bg[o] = dbR;
|
|
97
|
+
buf.bg[o + 1] = dbG;
|
|
98
|
+
buf.bg[o + 2] = dbB;
|
|
99
|
+
} else {
|
|
100
|
+
buf.bg[o] = (bg >> 16) & 0xff;
|
|
101
|
+
buf.bg[o + 1] = (bg >> 8) & 0xff;
|
|
102
|
+
buf.bg[o + 2] = bg & 0xff;
|
|
103
|
+
}
|
|
104
|
+
buf.bg[o + 3] = 255;
|
|
105
|
+
buf.attributes[idx] = attrs;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Mark cell `idx` as the continuation (spacer) half of the wide glyph at `idx-1`,
|
|
110
|
+
* inheriting the glyph's colors so a colored wide glyph's background spans both
|
|
111
|
+
* cells. The renderer emits nothing for a continuation cell but still paints its
|
|
112
|
+
* background.
|
|
113
|
+
*/
|
|
114
|
+
export function writeContinuation(buf: CellArrays, idx: number): void {
|
|
115
|
+
buf.char[idx] = CHAR_CONTINUATION;
|
|
116
|
+
const o = idx * 4;
|
|
117
|
+
const p = (idx - 1) * 4;
|
|
118
|
+
buf.fg[o] = buf.fg[p]!;
|
|
119
|
+
buf.fg[o + 1] = buf.fg[p + 1]!;
|
|
120
|
+
buf.fg[o + 2] = buf.fg[p + 2]!;
|
|
121
|
+
buf.fg[o + 3] = buf.fg[p + 3]!;
|
|
122
|
+
buf.bg[o] = buf.bg[p]!;
|
|
123
|
+
buf.bg[o + 1] = buf.bg[p + 1]!;
|
|
124
|
+
buf.bg[o + 2] = buf.bg[p + 2]!;
|
|
125
|
+
buf.bg[o + 3] = buf.bg[p + 3]!;
|
|
126
|
+
buf.attributes[idx] = buf.attributes[idx - 1]!;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Swap fg/bg across cells `[from,to]` (inclusive, clamped to the row) of row `y`
|
|
131
|
+
* — the drag-selection reverse-video post-pass. A swap over the cursor cell (also
|
|
132
|
+
* swapped in the blit) returns it to normal, matching the run path's XOR.
|
|
133
|
+
*/
|
|
134
|
+
export function swapCells(
|
|
135
|
+
buf: CellArrays,
|
|
136
|
+
width: number,
|
|
137
|
+
y: number,
|
|
138
|
+
from: number,
|
|
139
|
+
to: number,
|
|
140
|
+
): void {
|
|
141
|
+
const lo = Math.max(0, from);
|
|
142
|
+
const hi = Math.min(width - 1, to);
|
|
143
|
+
const base = y * width;
|
|
144
|
+
for (let x = lo; x <= hi; x++) {
|
|
145
|
+
const o = (base + x) * 4;
|
|
146
|
+
const r = buf.fg[o]!;
|
|
147
|
+
const g = buf.fg[o + 1]!;
|
|
148
|
+
const b = buf.fg[o + 2]!;
|
|
149
|
+
const a = buf.fg[o + 3]!;
|
|
150
|
+
buf.fg[o] = buf.bg[o]!;
|
|
151
|
+
buf.fg[o + 1] = buf.bg[o + 1]!;
|
|
152
|
+
buf.fg[o + 2] = buf.bg[o + 2]!;
|
|
153
|
+
buf.fg[o + 3] = buf.bg[o + 3]!;
|
|
154
|
+
buf.bg[o] = r;
|
|
155
|
+
buf.bg[o + 1] = g;
|
|
156
|
+
buf.bg[o + 2] = b;
|
|
157
|
+
buf.bg[o + 3] = a;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Paint the background of cells `[from,to]` (inclusive, clamped) of row `y` to
|
|
163
|
+
* packed `bg` — the scrollback-search highlight post-pass (the twin of
|
|
164
|
+
* {@link invertCells}; distinct treatment so a match and the selection coexist).
|
|
165
|
+
*/
|
|
166
|
+
export function paintBg(
|
|
167
|
+
buf: CellArrays,
|
|
168
|
+
width: number,
|
|
169
|
+
y: number,
|
|
170
|
+
from: number,
|
|
171
|
+
to: number,
|
|
172
|
+
bg: number,
|
|
173
|
+
): void {
|
|
174
|
+
const lo = Math.max(0, from);
|
|
175
|
+
const hi = Math.min(width - 1, to);
|
|
176
|
+
const r = (bg >> 16) & 0xff;
|
|
177
|
+
const g = (bg >> 8) & 0xff;
|
|
178
|
+
const b = bg & 0xff;
|
|
179
|
+
for (let x = lo; x <= hi; x++) {
|
|
180
|
+
const o = (y * width + x) * 4;
|
|
181
|
+
buf.bg[o] = r;
|
|
182
|
+
buf.bg[o + 1] = g;
|
|
183
|
+
buf.bg[o + 2] = b;
|
|
184
|
+
buf.bg[o + 3] = 255;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
@@ -12,14 +12,29 @@
|
|
|
12
12
|
* intact; `parseControlLine`/`decodeControlBytes` do the pure protocol work.
|
|
13
13
|
*/
|
|
14
14
|
import { spawn, type ChildProcess } from "node:child_process";
|
|
15
|
+
import { appendFileSync } from "node:fs";
|
|
15
16
|
import { parseControlLine, textToHexKeys } from "./control.ts";
|
|
16
17
|
|
|
17
18
|
interface PendingReply {
|
|
19
|
+
discard?: false;
|
|
18
20
|
resolve: (lines: string[]) => void;
|
|
19
21
|
reject: (err: Error) => void;
|
|
20
22
|
lines: string[];
|
|
21
23
|
}
|
|
22
24
|
|
|
25
|
+
/** A fire-and-forget command's FIFO placeholder: its reply block is consumed
|
|
26
|
+
* and dropped (errors counted). `lines` collects the error body only when
|
|
27
|
+
* TMUX_IDE_MIRROR_DEBUG asks for it. */
|
|
28
|
+
interface DiscardedReply {
|
|
29
|
+
discard: true;
|
|
30
|
+
lines?: string[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type Pending = PendingReply | DiscardedReply;
|
|
34
|
+
|
|
35
|
+
/** The shared no-debug placeholder — fire-and-forget input allocates NOTHING. */
|
|
36
|
+
const DISCARDED: DiscardedReply = { discard: true };
|
|
37
|
+
|
|
23
38
|
export interface ControlClientOptions {
|
|
24
39
|
/** Session (or other tmux target) to attach the control client to. */
|
|
25
40
|
attachTarget: string;
|
|
@@ -33,9 +48,10 @@ export interface ControlClientOptions {
|
|
|
33
48
|
|
|
34
49
|
export class ControlModeClient {
|
|
35
50
|
private proc: ChildProcess | null = null;
|
|
36
|
-
private readonly pending:
|
|
51
|
+
private readonly pending: Pending[] = [];
|
|
37
52
|
private inReply = false;
|
|
38
53
|
private buffer = "";
|
|
54
|
+
private discardedErrors = 0;
|
|
39
55
|
private readonly opts: ControlClientOptions;
|
|
40
56
|
|
|
41
57
|
constructor(opts: ControlClientOptions) {
|
|
@@ -72,14 +88,38 @@ export class ControlModeClient {
|
|
|
72
88
|
});
|
|
73
89
|
}
|
|
74
90
|
|
|
75
|
-
/**
|
|
76
|
-
|
|
77
|
-
|
|
91
|
+
/**
|
|
92
|
+
* The INPUT FAST PATH (M21.5): write a command fire-and-forget. The bytes
|
|
93
|
+
* hit tmux's stdin exactly as immediately as `command()`'s do, but no
|
|
94
|
+
* Promise/resolver is allocated and nothing ever waits on the reply. Every
|
|
95
|
+
* control-mode command still produces exactly one `%begin/%end` block, so a
|
|
96
|
+
* placeholder is pushed onto the SAME pending FIFO — reply matching for
|
|
97
|
+
* reply-carrying commands (list-panes, capture-pane, …) stays aligned by
|
|
98
|
+
* construction. Errors are swallowed but counted ({@link inputErrorCount});
|
|
99
|
+
* with TMUX_IDE_MIRROR_DEBUG set the error body is appended to
|
|
100
|
+
* /tmp/zz-input-errors.log.
|
|
101
|
+
*/
|
|
102
|
+
send(cmd: string): void {
|
|
103
|
+
const proc = this.proc;
|
|
104
|
+
if (!proc?.stdin?.writable) return;
|
|
105
|
+
this.pending.push(process.env.TMUX_IDE_MIRROR_DEBUG ? { discard: true, lines: [] } : DISCARDED);
|
|
106
|
+
proc.stdin.write(`${cmd}\n`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Type literal text into a pane (UTF-8, sent as hex bytes — quote-proof).
|
|
110
|
+
* Fire-and-forget: input never queues behind a slow structural reply. */
|
|
111
|
+
sendText(pane: string, text: string): void {
|
|
112
|
+
this.send(`send-keys -t ${pane} -H ${textToHexKeys(text).join(" ")}`);
|
|
78
113
|
}
|
|
79
114
|
|
|
80
|
-
/** Send a named tmux key (Enter, Escape, Up, C-c, …) to a pane. */
|
|
81
|
-
sendKey(pane: string, key: string):
|
|
82
|
-
|
|
115
|
+
/** Send a named tmux key (Enter, Escape, Up, C-c, …) to a pane. Fire-and-forget. */
|
|
116
|
+
sendKey(pane: string, key: string): void {
|
|
117
|
+
this.send(`send-keys -t ${pane} ${key}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** How many fire-and-forget commands came back `%error` (debug/tests). */
|
|
121
|
+
get inputErrorCount(): number {
|
|
122
|
+
return this.discardedErrors;
|
|
83
123
|
}
|
|
84
124
|
|
|
85
125
|
dispose(): void {
|
|
@@ -97,6 +137,7 @@ export class ControlModeClient {
|
|
|
97
137
|
}
|
|
98
138
|
|
|
99
139
|
private feed(chunk: string): void {
|
|
140
|
+
const t0 = process.env.TMUX_IDE_ZZ_PERF ? performance.now() : 0;
|
|
100
141
|
this.buffer += chunk;
|
|
101
142
|
let nl: number;
|
|
102
143
|
while ((nl = this.buffer.indexOf("\n")) !== -1) {
|
|
@@ -106,6 +147,16 @@ export class ControlModeClient {
|
|
|
106
147
|
this.buffer = this.buffer.slice(nl + 1);
|
|
107
148
|
this.handleLine(line);
|
|
108
149
|
}
|
|
150
|
+
if (t0) {
|
|
151
|
+
try {
|
|
152
|
+
appendFileSync(
|
|
153
|
+
"/tmp/zz-feed.log",
|
|
154
|
+
`${chunk.length} ${(performance.now() - t0).toFixed(2)}\n`,
|
|
155
|
+
);
|
|
156
|
+
} catch {
|
|
157
|
+
/* perf tap */
|
|
158
|
+
}
|
|
159
|
+
}
|
|
109
160
|
}
|
|
110
161
|
|
|
111
162
|
private handleLine(line: string): void {
|
|
@@ -114,14 +165,34 @@ export class ControlModeClient {
|
|
|
114
165
|
case "begin":
|
|
115
166
|
this.inReply = true;
|
|
116
167
|
break;
|
|
117
|
-
case "reply-line":
|
|
118
|
-
this.pending[0]
|
|
168
|
+
case "reply-line": {
|
|
169
|
+
const head = this.pending[0];
|
|
170
|
+
// Discarded (fire-and-forget) replies skip body collection entirely —
|
|
171
|
+
// unless the debug placeholder brought its own lines buffer.
|
|
172
|
+
if (head) head.lines?.push(event.line);
|
|
119
173
|
break;
|
|
174
|
+
}
|
|
120
175
|
case "end":
|
|
121
176
|
case "error": {
|
|
122
177
|
this.inReply = false;
|
|
123
178
|
const reply = this.pending.shift();
|
|
124
179
|
if (!reply) break; // unsolicited block (e.g. greeting after a race)
|
|
180
|
+
if (reply.discard) {
|
|
181
|
+
if (event.kind === "error") {
|
|
182
|
+
this.discardedErrors++;
|
|
183
|
+
if (process.env.TMUX_IDE_MIRROR_DEBUG) {
|
|
184
|
+
try {
|
|
185
|
+
appendFileSync(
|
|
186
|
+
"/tmp/zz-input-errors.log",
|
|
187
|
+
`#${this.discardedErrors} ${reply.lines?.join(" | ") ?? ""}\n`,
|
|
188
|
+
);
|
|
189
|
+
} catch {
|
|
190
|
+
// debug tap only
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
125
196
|
if (event.kind === "error") {
|
|
126
197
|
reply.reject(new Error(reply.lines.join("\n") || "tmux command failed"));
|
|
127
198
|
} else {
|