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.
Files changed (154) hide show
  1. package/README.md +36 -14
  2. package/bin/cli.js +4174 -1235
  3. package/bin/cli.ts +426 -72
  4. package/package.json +10 -6
  5. package/packages/contracts/src/__tests__/control.test.ts +154 -0
  6. package/packages/contracts/src/control.ts +217 -0
  7. package/packages/contracts/src/index.ts +1 -0
  8. package/packages/daemon/dist/agent-explain.d.ts +8 -1
  9. package/packages/daemon/dist/agent-explain.js +19 -3
  10. package/packages/daemon/dist/control/client.d.ts +23 -0
  11. package/packages/daemon/dist/control/client.js +105 -0
  12. package/packages/daemon/dist/control/dispatch.d.ts +34 -0
  13. package/packages/daemon/dist/control/dispatch.js +83 -0
  14. package/packages/daemon/dist/control/fanout.d.ts +19 -0
  15. package/packages/daemon/dist/control/fanout.js +37 -0
  16. package/packages/daemon/dist/control/frames.d.ts +23 -0
  17. package/packages/daemon/dist/control/frames.js +37 -0
  18. package/packages/daemon/dist/control/lifecycle.d.ts +45 -0
  19. package/packages/daemon/dist/control/lifecycle.js +114 -0
  20. package/packages/daemon/dist/control/server.d.ts +16 -0
  21. package/packages/daemon/dist/control/server.js +214 -0
  22. package/packages/daemon/dist/control/verbs.d.ts +11 -0
  23. package/packages/daemon/dist/control/verbs.js +91 -0
  24. package/packages/daemon/dist/doctor.d.ts +18 -0
  25. package/packages/daemon/dist/doctor.js +105 -15
  26. package/packages/daemon/dist/lib/agent-discovery.d.ts +27 -2
  27. package/packages/daemon/dist/lib/agent-discovery.js +29 -14
  28. package/packages/daemon/dist/lib/app-config.d.ts +106 -0
  29. package/packages/daemon/dist/lib/app-config.js +104 -5
  30. package/packages/daemon/dist/lib/manifest-pack.d.ts +79 -0
  31. package/packages/daemon/dist/lib/manifest-pack.js +232 -0
  32. package/packages/daemon/dist/lib/state-home.d.ts +2 -0
  33. package/packages/daemon/dist/lib/state-home.js +12 -0
  34. package/packages/daemon/dist/lib/tui-binary.d.ts +57 -0
  35. package/packages/daemon/dist/lib/tui-binary.js +130 -0
  36. package/packages/daemon/dist/lib/update-check.js +5 -0
  37. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Info.plist +34 -0
  38. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/MacOS/tmux-ide-notifier +0 -0
  39. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/PkgInfo +1 -0
  40. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Resources/AppIcon.icns +0 -0
  41. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Resources/Assets.car +0 -0
  42. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/_CodeSignature/CodeResources +139 -0
  43. package/packages/daemon/dist/restore.d.ts +35 -8
  44. package/packages/daemon/dist/restore.js +52 -15
  45. package/packages/daemon/dist/send.d.ts +33 -1
  46. package/packages/daemon/dist/send.js +32 -19
  47. package/packages/daemon/dist/widgets/explorer/breadcrumbs.d.ts +1 -1
  48. package/packages/daemon/dist/widgets/explorer/footer.d.ts +1 -1
  49. package/packages/daemon/dist/widgets/explorer/tree.d.ts +1 -1
  50. package/packages/daemon/dist/widgets/lib/help-overlay.d.ts +1 -1
  51. package/packages/daemon/dist/widgets/setup/agent-naming.d.ts +1 -1
  52. package/packages/daemon/dist/widgets/setup/config-tree.d.ts +1 -1
  53. package/packages/daemon/dist/widgets/setup/detect-panel.d.ts +1 -1
  54. package/packages/daemon/dist/widgets/setup/field-editor.d.ts +1 -1
  55. package/packages/daemon/dist/widgets/setup/footer.d.ts +1 -1
  56. package/packages/daemon/dist/widgets/setup/layout-picker.d.ts +1 -1
  57. package/packages/daemon/src/agent-explain.ts +34 -6
  58. package/packages/daemon/src/control/client.ts +128 -0
  59. package/packages/daemon/src/control/dispatch.ts +107 -0
  60. package/packages/daemon/src/control/fanout.ts +44 -0
  61. package/packages/daemon/src/control/frames.ts +40 -0
  62. package/packages/daemon/src/control/lifecycle.ts +151 -0
  63. package/packages/daemon/src/control/server.ts +237 -0
  64. package/packages/daemon/src/control/verbs.ts +118 -0
  65. package/packages/daemon/src/doctor.ts +113 -28
  66. package/packages/daemon/src/lib/agent-discovery.ts +53 -13
  67. package/packages/daemon/src/lib/app-config.ts +194 -5
  68. package/packages/daemon/src/lib/manifest-pack.ts +255 -0
  69. package/packages/daemon/src/lib/state-home.ts +13 -0
  70. package/packages/daemon/src/lib/tui-binary.ts +165 -0
  71. package/packages/daemon/src/lib/update-check.ts +5 -0
  72. package/packages/daemon/src/restore.ts +53 -15
  73. package/packages/daemon/src/send.ts +55 -21
  74. package/packages/daemon/src/tui/chrome/events.ts +4 -4
  75. package/packages/daemon/src/tui/chrome/front-door.ts +39 -0
  76. package/packages/daemon/src/tui/chrome/notify-prefs.ts +58 -0
  77. package/packages/daemon/src/tui/chrome/notify-state.ts +76 -0
  78. package/packages/daemon/src/tui/chrome/notify.ts +737 -52
  79. package/packages/daemon/src/tui/chrome/updater.ts +318 -30
  80. package/packages/daemon/src/tui/compiled.ts +11 -3
  81. package/packages/daemon/src/tui/detect/classify.ts +49 -0
  82. package/packages/daemon/src/tui/detect/manifest-loader.ts +56 -6
  83. package/packages/daemon/src/tui/detect/manifest.ts +39 -3
  84. package/packages/daemon/src/tui/detect/manifests.ts +395 -33
  85. package/packages/daemon/src/tui/detect/process-tree.ts +33 -3
  86. package/packages/daemon/src/tui/detect/session-id.ts +503 -0
  87. package/packages/daemon/src/tui/integrations/opencode.ts +121 -0
  88. package/packages/daemon/src/tui/main.ts +13 -1
  89. package/packages/daemon/src/tui/mirror/ack-writer.ts +77 -0
  90. package/packages/daemon/src/tui/mirror/agent-chip.ts +126 -0
  91. package/packages/daemon/src/tui/mirror/agent-lifecycle.ts +437 -0
  92. package/packages/daemon/src/tui/mirror/agent-rows.ts +155 -0
  93. package/packages/daemon/src/tui/mirror/app-state.ts +342 -0
  94. package/packages/daemon/src/tui/mirror/app.tsx +7048 -0
  95. package/packages/daemon/src/tui/mirror/attention.ts +110 -0
  96. package/packages/daemon/src/tui/mirror/blit.ts +186 -0
  97. package/packages/daemon/src/tui/mirror/control-client.ts +80 -9
  98. package/packages/daemon/src/tui/mirror/dialog-model.ts +298 -0
  99. package/packages/daemon/src/tui/mirror/dialog-stack.ts +367 -0
  100. package/packages/daemon/src/tui/mirror/diff-model.ts +387 -0
  101. package/packages/daemon/src/tui/mirror/editor-buffer.ts +117 -0
  102. package/packages/daemon/src/tui/mirror/file-tree.ts +322 -0
  103. package/packages/daemon/src/tui/mirror/focus-border.ts +57 -0
  104. package/packages/daemon/src/tui/mirror/folder-picker.ts +124 -0
  105. package/packages/daemon/src/tui/mirror/home-model.ts +174 -0
  106. package/packages/daemon/src/tui/mirror/host-terminal.ts +49 -0
  107. package/packages/daemon/src/tui/mirror/hosted.ts +205 -0
  108. package/packages/daemon/src/tui/mirror/input-coalescer.ts +105 -0
  109. package/packages/daemon/src/tui/mirror/layout-parse.ts +154 -0
  110. package/packages/daemon/src/tui/mirror/menu-model.ts +210 -0
  111. package/packages/daemon/src/tui/mirror/palette.ts +564 -0
  112. package/packages/daemon/src/tui/mirror/pane-mirror.ts +578 -20
  113. package/packages/daemon/src/tui/mirror/pane-surface.tsx +422 -0
  114. package/packages/daemon/src/tui/mirror/perf-tap.ts +186 -0
  115. package/packages/daemon/src/tui/mirror/resize-model.ts +85 -0
  116. package/packages/daemon/src/tui/mirror/scrollbar-model.ts +88 -0
  117. package/packages/daemon/src/tui/mirror/search-model.ts +70 -0
  118. package/packages/daemon/src/tui/mirror/selection.ts +376 -0
  119. package/packages/daemon/src/tui/mirror/session-mirror.ts +724 -0
  120. package/packages/daemon/src/tui/mirror/settings-model.ts +425 -0
  121. package/packages/daemon/src/tui/mirror/sidebar.tsx +218 -0
  122. package/packages/daemon/src/tui/mirror/size-truth.ts +130 -0
  123. package/packages/daemon/src/tui/mirror/spans.ts +46 -0
  124. package/packages/daemon/src/tui/mirror/status-grammar.ts +32 -0
  125. package/packages/daemon/src/tui/mirror/theme.ts +45 -0
  126. package/packages/daemon/src/tui/team/entry.ts +34 -7
  127. package/packages/daemon/src/tui/team/fuzzy.ts +20 -0
  128. package/packages/daemon/src/tui/team/report.ts +11 -1
  129. package/packages/daemon/src/tui/team/sessions.ts +184 -17
  130. package/packages/daemon/src/tui/team/wait.ts +144 -0
  131. package/scripts/build-macos-notifier.mjs +160 -0
  132. package/scripts/build-tui.mjs +11 -4
  133. package/scripts/perf-mirror.mjs +313 -0
  134. package/scripts/postinstall.js +8 -1
  135. package/scripts/prepublish-check.mjs +37 -1
  136. package/scripts/publish-tap.sh +55 -0
  137. package/skill/SKILL.md +110 -2
  138. package/templates/AGENTS.md +14 -7
  139. package/templates/agent-team-monorepo.yml +8 -0
  140. package/templates/agent-team-nextjs.yml +8 -0
  141. package/templates/agent-team.yml +10 -0
  142. package/templates/convex.yml +2 -0
  143. package/templates/default.yml +11 -5
  144. package/templates/go.yml +4 -0
  145. package/templates/missions.yml +6 -0
  146. package/templates/nextjs.yml +4 -0
  147. package/templates/python.yml +4 -0
  148. package/templates/skills/backend.md +5 -12
  149. package/templates/skills/frontend.md +5 -12
  150. package/templates/skills/general-worker.md +5 -12
  151. package/templates/skills/researcher.md +7 -12
  152. package/templates/skills/reviewer.md +7 -16
  153. package/templates/vite.yml +4 -0
  154. package/packages/daemon/src/tui/mirror/viewer.tsx +0 -166
@@ -0,0 +1,313 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * perf-mirror — the M21 baseline harness.
4
+ *
5
+ * Drives the real unified app (`tmux-ide app`) against a scripted tmux session
6
+ * on the default socket, exercising the three mirror-pipeline taps that gate
7
+ * behind TMUX_IDE_ZZ_PERF, then prints a p50/p95 table for:
8
+ * - feed-parse ms per stdout chunk (/tmp/zz-feed.log, control-client)
9
+ * - snapshot ms per 8ms paint tick (/tmp/zz-perf.log, app.tsx)
10
+ * - input-echo ms keystroke→echo→paint (/tmp/zz-input.log, perf-tap)
11
+ * across three scenarios: IDLE, FLOOD (scrolling), ALT (alt-screen redraw).
12
+ *
13
+ * Everything it creates is `zz-perf-*` on the default socket and is killed on
14
+ * exit (success OR failure). It NEVER touches any other session. Run it from
15
+ * the repo root: bun scripts/perf-mirror.mjs
16
+ *
17
+ * Env knobs (all optional): PERF_FLOOD_MS, PERF_ALT_MS, PERF_IDLE_MS,
18
+ * PERF_KEYS (input samples), PERF_KEY_GAP_MS, PERF_WARMUP_MS.
19
+ */
20
+ import { execFileSync } from "node:child_process";
21
+ import { readFileSync, writeFileSync, existsSync } from "node:fs";
22
+ import { fileURLToPath } from "node:url";
23
+ import { dirname, resolve } from "node:path";
24
+ import os from "node:os";
25
+ import { summarize } from "../packages/daemon/src/tui/mirror/perf-tap.ts";
26
+
27
+ const REPO = resolve(dirname(fileURLToPath(import.meta.url)), "..");
28
+ const APP = "packages/daemon/src/tui/mirror/app.tsx";
29
+ const TARGET = "zz-perf-target";
30
+ const HOST = "zz-perf-host";
31
+ const FEED_LOG = "/tmp/zz-feed.log";
32
+ const SNAP_LOG = "/tmp/zz-perf.log";
33
+ const INPUT_LOG = "/tmp/zz-input.log";
34
+
35
+ const cfg = {
36
+ floodMs: num(process.env.PERF_FLOOD_MS, 6000),
37
+ altMs: num(process.env.PERF_ALT_MS, 6000),
38
+ idleMs: num(process.env.PERF_IDLE_MS, 4000),
39
+ keys: num(process.env.PERF_KEYS, 50),
40
+ keyGapMs: num(process.env.PERF_KEY_GAP_MS, 90),
41
+ warmupMs: num(process.env.PERF_WARMUP_MS, 5000),
42
+ };
43
+
44
+ function num(v, d) {
45
+ const n = Number(v);
46
+ return Number.isFinite(n) && n > 0 ? n : d;
47
+ }
48
+
49
+ /** tmux on the DEFAULT socket, un-nested (TMUX/TMUX_TMPDIR cleared) — the socket
50
+ * the app's control client attaches to and where the user's real sessions live.
51
+ * We only ever name zz-perf-* targets. */
52
+ function tmux(args, opts = {}) {
53
+ return execFileSync("tmux", args, {
54
+ env: { ...process.env, TMUX: "", TMUX_TMPDIR: "" },
55
+ encoding: "utf8",
56
+ stdio: opts.inherit ? "inherit" : ["ignore", "pipe", "pipe"],
57
+ ...opts,
58
+ });
59
+ }
60
+ function tmuxQuiet(args) {
61
+ try {
62
+ return tmux(args);
63
+ } catch {
64
+ return "";
65
+ }
66
+ }
67
+ function sessionExists(name) {
68
+ try {
69
+ tmux(["has-session", "-t", name]);
70
+ return true;
71
+ } catch {
72
+ return false;
73
+ }
74
+ }
75
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
76
+ const truncate = (p) => writeFileSync(p, "");
77
+ function readCol(path, col) {
78
+ if (!existsSync(path)) return [];
79
+ return readFileSync(path, "utf8")
80
+ .split("\n")
81
+ .filter(Boolean)
82
+ .map((l) => Number(l.trim().split(/\s+/)[col]))
83
+ .filter((n) => Number.isFinite(n));
84
+ }
85
+
86
+ // ── keystroke / flood injection ─────────────────────────────────────────────
87
+
88
+ /** Type into the HOST (the app), which forwards to the focused target pane. */
89
+ const sendHostKey = (key) => tmuxQuiet(["send-keys", "-t", HOST, key]);
90
+ const sendHostChar = (ch) => tmuxQuiet(["send-keys", "-t", HOST, "-l", ch]);
91
+ /** Run a command directly in the TARGET pane (bypasses the app — pure output). */
92
+ function runInTarget(line) {
93
+ tmuxQuiet(["send-keys", "-t", TARGET, "-l", line]);
94
+ tmuxQuiet(["send-keys", "-t", TARGET, "Enter"]);
95
+ }
96
+ /** Interrupt whatever the target pane is running, back to a clean prompt. */
97
+ function calmTarget() {
98
+ tmuxQuiet(["send-keys", "-t", TARGET, "C-c"]);
99
+ tmuxQuiet(["send-keys", "-t", TARGET, "-l", "printf '\\033[?1049l'"]);
100
+ tmuxQuiet(["send-keys", "-t", TARGET, "Enter"]);
101
+ tmuxQuiet(["send-keys", "-t", TARGET, "-l", "clear"]);
102
+ tmuxQuiet(["send-keys", "-t", TARGET, "Enter"]);
103
+ }
104
+
105
+ // ── lifecycle ───────────────────────────────────────────────────────────────
106
+
107
+ function cleanup() {
108
+ for (const s of [HOST, TARGET]) {
109
+ if (sessionExists(s)) tmuxQuiet(["kill-session", "-t", s]);
110
+ }
111
+ }
112
+
113
+ async function setup() {
114
+ cleanup(); // clear any leftovers from a crashed prior run
115
+ truncate(FEED_LOG);
116
+ truncate(SNAP_LOG);
117
+ truncate(INPUT_LOG);
118
+
119
+ // Target: a plain shell on the default socket. The app mirrors this session;
120
+ // its control client (`tmux -C attach`, TMUX="") reaches the default socket.
121
+ tmux(["new-session", "-d", "-s", TARGET, "-x", "200", "-y", "50"]);
122
+ // A generous host so the app has room and never wraps the flood oddly.
123
+ tmux([
124
+ "new-session",
125
+ "-d",
126
+ "-s",
127
+ HOST,
128
+ "-x",
129
+ "220",
130
+ "-y",
131
+ "60",
132
+ // Forward TMUX_IDE_FB_PANES verbatim so the default incremental blit path
133
+ // (M21.4) and the `=0` StyledRun kill switch can be measured on the same
134
+ // harness/taps. Unset → the app's default (blit).
135
+ `cd ${REPO} && TMUX_IDE_ZZ_PERF=1 ${process.env.TMUX_IDE_FB_PANES !== undefined ? `TMUX_IDE_FB_PANES=${process.env.TMUX_IDE_FB_PANES} ` : ""}exec bun ${APP} --target=${TARGET}`,
136
+ ]);
137
+
138
+ process.stdout.write(` warming up (${cfg.warmupMs}ms) — app attaching + seeding…\n`);
139
+ await sleep(cfg.warmupMs);
140
+ sendHostKey("F2"); // force the Terminal surface (pane-forward keyboard path)
141
+ await sleep(400);
142
+ }
143
+
144
+ /** Assert the mirror is actually live: a brief flood must grow the feed log. */
145
+ async function assertLive() {
146
+ truncate(FEED_LOG);
147
+ runInTarget("for i in 1 2 3 4 5; do echo probe-$i; done");
148
+ await sleep(800);
149
+ const n = readCol(FEED_LOG, 1).length;
150
+ if (n === 0) {
151
+ const pane = tmuxQuiet(["capture-pane", "-t", HOST, "-p"]).slice(0, 800);
152
+ throw new Error(
153
+ `mirror never went live — no %output feed after a probe.\n--- host pane ---\n${pane}`,
154
+ );
155
+ }
156
+ calmTarget();
157
+ await sleep(500);
158
+ }
159
+
160
+ // ── scenarios ────────────────────────────────────────────────────────────────
161
+
162
+ /** Idle: quiet pane; capture the low-water feed/snapshot floor. */
163
+ async function scenarioIdle() {
164
+ calmTarget();
165
+ await sleep(600);
166
+ truncate(FEED_LOG);
167
+ truncate(SNAP_LOG);
168
+ await sleep(cfg.idleMs);
169
+ return { feed: readCol(FEED_LOG, 1), snap: readCol(SNAP_LOG, 0) };
170
+ }
171
+
172
+ /** Flood: a steady stream of new lines (scrollback growth + re-snapshot). */
173
+ async function scenarioFlood() {
174
+ calmTarget();
175
+ await sleep(500);
176
+ truncate(FEED_LOG);
177
+ truncate(SNAP_LOG);
178
+ // ~200 lines/sec — a busy build/test log, not a fork bomb.
179
+ runInTarget(
180
+ 'i=0; while true; do echo "flood line $i $(date +%s%N)"; i=$((i+1)); sleep 0.005; done',
181
+ );
182
+ await sleep(cfg.floodMs);
183
+ const out = { feed: readCol(FEED_LOG, 1), snap: readCol(SNAP_LOG, 0) };
184
+ calmTarget();
185
+ await sleep(500);
186
+ return out;
187
+ }
188
+
189
+ /** Alt-screen: an app that repaints in place (cursor addressing, no scrollback). */
190
+ async function scenarioAlt() {
191
+ calmTarget();
192
+ await sleep(500);
193
+ truncate(FEED_LOG);
194
+ truncate(SNAP_LOG);
195
+ runInTarget(
196
+ "printf '\\033[?1049h'; while true; do printf '\\033[H\\033[2J'; date +%s.%N; seq 1 30; sleep 0.05; done",
197
+ );
198
+ await sleep(cfg.altMs);
199
+ const out = { feed: readCol(FEED_LOG, 1), snap: readCol(SNAP_LOG, 0) };
200
+ calmTarget();
201
+ await sleep(500);
202
+ return out;
203
+ }
204
+
205
+ /** Input latency: type single chars into the app at a shell prompt; each echoes. */
206
+ async function scenarioInput() {
207
+ calmTarget();
208
+ await sleep(600);
209
+ truncate(INPUT_LOG);
210
+ const glyphs = "abcdefghijklmnopqrstuvwxyz0123456789";
211
+ for (let k = 0; k < cfg.keys; k++) {
212
+ sendHostChar(glyphs[k % glyphs.length]);
213
+ await sleep(cfg.keyGapMs);
214
+ }
215
+ await sleep(400);
216
+ const out = { echo: readCol(INPUT_LOG, 1), paint: readCol(INPUT_LOG, 2) };
217
+ // Drop the half-typed line by interrupting the TARGET pane directly. NEVER
218
+ // send C-c to the host: the app exits on ctrl-c, which would kill the mirror
219
+ // and silence every later scenario.
220
+ tmuxQuiet(["send-keys", "-t", TARGET, "C-c"]);
221
+ return out;
222
+ }
223
+
224
+ // ── reporting ────────────────────────────────────────────────────────────────
225
+
226
+ function fmt(n) {
227
+ return n.toFixed(2).padStart(8);
228
+ }
229
+ function row(label, s) {
230
+ return ` ${label.padEnd(26)} ${String(s.count).padStart(6)} ${fmt(s.p50)} ${fmt(s.p95)} ${fmt(s.max)} ${fmt(s.mean)}`;
231
+ }
232
+ function table(rows) {
233
+ const head = ` ${"metric".padEnd(26)} ${"n".padStart(6)} ${"p50".padStart(8)} ${"p95".padStart(8)} ${"max".padStart(8)} ${"mean".padStart(8)}`;
234
+ return [head, " " + "-".repeat(head.length - 2), ...rows].join("\n");
235
+ }
236
+
237
+ function machineInfo() {
238
+ const cpu = os.cpus()[0]?.model ?? "unknown";
239
+ let tmuxV = "unknown";
240
+ try {
241
+ tmuxV = tmux(["-V"]).trim();
242
+ } catch {
243
+ /* ignore */
244
+ }
245
+ let opentui = "unknown";
246
+ try {
247
+ const pkg = JSON.parse(readFileSync(resolve(REPO, "packages/daemon/package.json"), "utf8"));
248
+ opentui =
249
+ pkg.dependencies?.["@opentui/core"] ?? pkg.dependencies?.["@opentui/solid"] ?? "unknown";
250
+ } catch {
251
+ /* ignore */
252
+ }
253
+ return {
254
+ date: new Date().toISOString(),
255
+ platform: `${os.platform()} ${os.release()} (${os.arch()})`,
256
+ cpu: `${cpu} × ${os.cpus().length}`,
257
+ mem: `${(os.totalmem() / 1024 ** 3).toFixed(0)} GB`,
258
+ bun: process.versions.bun ?? "n/a",
259
+ node: process.versions.node,
260
+ tmux: tmuxV,
261
+ opentui,
262
+ };
263
+ }
264
+
265
+ async function main() {
266
+ const info = machineInfo();
267
+ process.stdout.write("perf-mirror — M21 baseline harness\n");
268
+ process.stdout.write(
269
+ ` ${info.platform} · ${info.cpu} · bun ${info.bun} · ${info.tmux} · @opentui ${info.opentui}\n\n`,
270
+ );
271
+
272
+ await setup();
273
+ await assertLive();
274
+
275
+ process.stdout.write(" running IDLE…\n");
276
+ const idle = await scenarioIdle();
277
+ process.stdout.write(" running INPUT…\n");
278
+ const input = await scenarioInput();
279
+ process.stdout.write(" running FLOOD…\n");
280
+ const flood = await scenarioFlood();
281
+ process.stdout.write(" running ALT-SCREEN…\n\n");
282
+ const alt = await scenarioAlt();
283
+
284
+ const rows = [
285
+ row("feed-parse ms/chunk [idle]", summarize(idle.feed)),
286
+ row("feed-parse ms/chunk [flood]", summarize(flood.feed)),
287
+ row("feed-parse ms/chunk [alt]", summarize(alt.feed)),
288
+ row("snapshot ms/tick [idle]", summarize(idle.snap)),
289
+ row("snapshot ms/tick [flood]", summarize(flood.snap)),
290
+ row("snapshot ms/tick [alt]", summarize(alt.snap)),
291
+ row("input echo ms (t1-t0)", summarize(input.echo)),
292
+ row("input paint ms (t2-t0)", summarize(input.paint)),
293
+ ];
294
+ process.stdout.write(table(rows) + "\n");
295
+
296
+ return { info, rows };
297
+ }
298
+
299
+ let exitCode = 0;
300
+ try {
301
+ await main();
302
+ } catch (err) {
303
+ exitCode = 1;
304
+ process.stderr.write(`\nperf-mirror FAILED: ${err?.stack ?? err}\n`);
305
+ } finally {
306
+ cleanup();
307
+ // Best-effort confirmation the zz-perf-* sessions are gone.
308
+ const leftover = [HOST, TARGET].filter(sessionExists);
309
+ if (leftover.length)
310
+ process.stderr.write(` WARNING leftover sessions: ${leftover.join(", ")}\n`);
311
+ else process.stdout.write("\n cleanup: zz-perf-* sessions removed.\n");
312
+ }
313
+ process.exit(exitCode);
@@ -91,7 +91,14 @@ const nextSettings = {
91
91
  },
92
92
  };
93
93
 
94
- writeFileSync(settingsPath, `${JSON.stringify(nextSettings, null, 2)}\n`);
94
+ try {
95
+ writeFileSync(settingsPath, `${JSON.stringify(nextSettings, null, 2)}\n`);
96
+ } catch (error) {
97
+ // Best-effort like everything above: sandboxed installs (e.g. a package
98
+ // manager building in a $HOME-restricted sandbox) may deny this write —
99
+ // that must never fail the install itself.
100
+ console.warn(`[tmux-ide] Skipping Claude settings update: ${error.message}`);
101
+ }
95
102
 
96
103
  function shouldInstallClaudeIntegration() {
97
104
  return process.env.npm_config_global === "true";
@@ -10,7 +10,7 @@
10
10
  */
11
11
 
12
12
  import { execFileSync } from "node:child_process";
13
- import { existsSync, statSync } from "node:fs";
13
+ import { accessSync, constants, existsSync, statSync } from "node:fs";
14
14
  import { join } from "node:path";
15
15
 
16
16
  function run(command, args) {
@@ -42,3 +42,39 @@ if (cliJsMtime < cliTsMtime) {
42
42
  "bin/cli.js is older than bin/cli.ts — run: pnpm build:cli && git add bin/cli.js",
43
43
  );
44
44
  }
45
+
46
+ // ---------------------------------------------------------------
47
+ // Native macOS notification sender — release.yml builds this on a macOS 26
48
+ // runner and injects it into packages/daemon/dist before npm publish. npm and
49
+ // Homebrew consume the same tarball, so a missing bundle would silently put
50
+ // users back on an unbranded AppleScript fallback.
51
+ // ---------------------------------------------------------------
52
+ const notifierRoot = join(
53
+ process.cwd(),
54
+ "packages",
55
+ "daemon",
56
+ "dist",
57
+ "native",
58
+ "TmuxIdeNotifier.app",
59
+ "Contents",
60
+ );
61
+ for (const relative of [
62
+ "Info.plist",
63
+ join("Resources", "Assets.car"),
64
+ join("Resources", "AppIcon.icns"),
65
+ ]) {
66
+ const path = join(notifierRoot, relative);
67
+ if (!existsSync(path)) {
68
+ throw new Error(
69
+ `native macOS notifier is incomplete (${relative} missing) — run pnpm build:macos-notifier on macOS`,
70
+ );
71
+ }
72
+ }
73
+ const notifierExecutable = join(notifierRoot, "MacOS", "tmux-ide-notifier");
74
+ try {
75
+ accessSync(notifierExecutable, constants.X_OK);
76
+ } catch {
77
+ throw new Error(
78
+ "native macOS notifier executable is missing or not executable — run pnpm build:macos-notifier on macOS",
79
+ );
80
+ }
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env bash
2
+ # Publish packaging/homebrew/Formula/tmux-ide.rb to the Homebrew tap repo
3
+ # (wavyrai/homebrew-tap), i.e. the one-time seeding the CI `bump_tap` job
4
+ # assumes. After this, every release keeps the tap current automatically
5
+ # (given the TAP_PUSH_TOKEN secret — see .github/workflows/release.yml).
6
+ #
7
+ # Usage:
8
+ # scripts/publish-tap.sh [path-to-tap-checkout]
9
+ #
10
+ # With no argument, clones git@github.com:wavyrai/homebrew-tap.git into a
11
+ # temp dir. The tap repo must already exist on GitHub (create it empty —
12
+ # the name MUST be exactly `homebrew-tap` for `brew install
13
+ # wavyrai/tap/tmux-ide` to resolve).
14
+ set -euo pipefail
15
+
16
+ repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
17
+ formula="$repo_root/packaging/homebrew/Formula/tmux-ide.rb"
18
+ [[ -f "$formula" ]] || {
19
+ echo "formula not found: $formula" >&2
20
+ exit 1
21
+ }
22
+
23
+ tap_dir="${1:-}"
24
+ cleanup=""
25
+ if [[ -z "$tap_dir" ]]; then
26
+ tap_dir="$(mktemp -d)/homebrew-tap"
27
+ cleanup="$(dirname "$tap_dir")"
28
+ git clone git@github.com:wavyrai/homebrew-tap.git "$tap_dir"
29
+ fi
30
+ [[ -d "$tap_dir/.git" ]] || {
31
+ echo "not a git checkout: $tap_dir" >&2
32
+ exit 1
33
+ }
34
+
35
+ version="$(sed -nE 's#^ url ".*/tmux-ide-([0-9][^"]*)\.tgz"$#\1#p' "$formula")"
36
+ [[ -n "$version" ]] || {
37
+ echo "could not read the version from the formula url" >&2
38
+ exit 1
39
+ }
40
+
41
+ mkdir -p "$tap_dir/Formula"
42
+ cp "$formula" "$tap_dir/Formula/tmux-ide.rb"
43
+
44
+ cd "$tap_dir"
45
+ git add Formula/tmux-ide.rb
46
+ if git diff --cached --quiet; then
47
+ echo "tap already has this formula version (v$version) — nothing to push."
48
+ else
49
+ git commit -m "tmux-ide $version"
50
+ git push origin HEAD
51
+ echo "pushed tmux-ide v$version to $(git remote get-url origin)"
52
+ fi
53
+
54
+ [[ -n "$cleanup" ]] && rm -rf "$cleanup"
55
+ echo "install with: brew install wavyrai/tap/tmux-ide"
package/skill/SKILL.md CHANGED
@@ -34,10 +34,26 @@ minutes is treated as stale (the detector falls back to Layer 2), so long-runnin
34
34
  agents should re-stamp periodically. Two optional companions:
35
35
 
36
36
  ```bash
37
- tmux set-option -p @agent_session_id "<id>" # your Claude session id — powers restore --resume-agents
37
+ tmux set-option -p @agent_session_id "<id>" # your own session id — powers restore --resume-agents
38
38
  tmux set-option -p @agent_hint claude # force which agent manifest Layer 2 uses for this pane
39
39
  ```
40
40
 
41
+ **Display metadata** — say WHAT you're doing and WHO you are, right in the
42
+ fleet UI. Two more optional pane-local options:
43
+
44
+ ```bash
45
+ tmux set-option -p @agent_status_text "refactoring auth" # one-liner, ≤32 chars — shows in the pane chip ("● claude · refactoring auth")
46
+ tmux set-option -p @agent_display_name "reviewer" # your name — replaces the detected kind in sidebar rows & chips
47
+ ```
48
+
49
+ Plain text only (control characters are stripped, tabs break the line for your
50
+ pane — don't stamp them; anything past 32 chars is ellipsized). Both surface in
51
+ `tmux-ide team --json` (per-pane `statusText` / `displayName`), the unified
52
+ app's pane chips, and the sidebar agent rows. They follow the SAME staleness
53
+ rules as `@agent_state`: they only show while your state stamp is fresh, so
54
+ re-stamp `@agent_state` alongside — update the text whenever your focus
55
+ changes, and it disappears with a stale/cleared state instead of lying.
56
+
41
57
  **Claude Code users get this for free** — `tmux-ide integration install claude`
42
58
  writes a POSIX hook into `~/.claude/settings.json` that stamps `@agent_state` on
43
59
  every lifecycle event (UserPromptSubmit/PreToolUse → working, Notification →
@@ -45,6 +61,13 @@ blocked, Stop → done, SessionEnd → idle) and records `@agent_session_id`. It
45
61
  takes effect for **new** Claude Code sessions; the merge is reversible
46
62
  (`integration uninstall claude`).
47
63
 
64
+ **Session-id capture for other kinds** (what `restore --resume-agents` resumes
65
+ from): codex and cursor-agent panes are stamped **automatically** — the chrome
66
+ updater reads each CLI's own on-disk session state; opencode gets a plugin via
67
+ `tmux-ide integration install opencode`. `tmux-ide integration status` shows
68
+ what's active. Kinds without a verified resume story (gemini, aider, copilot, …)
69
+ can self-report the id as above.
70
+
48
71
  **How detection layers work:** Layer 1 is the authority above — a fresh
49
72
  `@agent_state` option is ground truth. When none is present, Layer 2 resolves the
50
73
  agent from the pane's process tree and reads the visible screen against
@@ -91,7 +114,7 @@ tmux-ide adopt --all # adopt every live session
91
114
  tmux-ide unadopt <session> # remove the dock — sessions keep running as plain tmux
92
115
 
93
116
  tmux-ide restore --dry-run --json # preview rebuilding the fleet from the last snapshot
94
- tmux-ide restore --resume-agents # rebuild after a tmux crash; revive Claude convos via claude --resume
117
+ tmux-ide restore --resume-agents # rebuild after a tmux crash; revive agent convos (claude/codex/cursor/opencode)
95
118
 
96
119
  tmux-ide worktree create <branch> --from <ref> # git worktree on a new branch + a session in it
97
120
  tmux-ide worktree open <branch> # open/switch to an existing worktree's session
@@ -102,6 +125,69 @@ tmux-ide update --dry-run # detect install method (dev checkout
102
125
  tmux-ide doctor # system + integration health (tmux version, TUI surfaces, skill freshness)
103
126
  ```
104
127
 
128
+ ## Drive tmux-ide over the socket (agent loops)
129
+
130
+ The CLI above spawns a process per call. If you are driving the fleet in a
131
+ loop — polling status, waiting on agents, reacting to transitions — start the
132
+ control server once and keep ONE connection open instead:
133
+
134
+ ```bash
135
+ tmux-ide serve & # local Unix socket at ~/.tmux-ide/control.sock (0600, this user only)
136
+ ```
137
+
138
+ **Frame format:** newline-delimited JSON, one object per line. Send
139
+ `{"v":1,"id":<any>,"verb":"<verb>","params":{…}}`; you get back
140
+ `{"v":1,"id":<same>,"ok":true,"data":…}` or
141
+ `{"v":1,"id":<same>,"ok":false,"error":{"code","message"}}`. Responses
142
+ correlate by `id` (they may arrive out of order — a long `wait` doesn't block
143
+ other verbs on the same connection). After the `subscribe` verb the server
144
+ also PUSHES unsolicited `{"v":1,"event":"agent-status","data":{ts,session,from,to}}`
145
+ frames the moment its detection tick sees a session change state — no polling.
146
+
147
+ **Verbs:** `fleet` (the `team --json` payload) · `agents` (per-pane entries,
148
+ optional `{session}`) · `send` (`{session,target,message,noEnter?,dir?}`) ·
149
+ `wait` (`{kind:"agent-status",session,status,timeoutMs?}` or
150
+ `{kind:"output",target,match,timeoutMs?}`; a timeout is an error response with
151
+ code `timeout`) · `spawn` (`{kind|command, session?|sessionName, dir?,
152
+ placement?, paneId?}` → the new `paneId`) · `restart-agent` / `stop-agent`
153
+ (`{paneId, kind|command}`) · `explain` (`{target}`) · `subscribe`.
154
+
155
+ One-shot from a shell (nc keeps the pipe open for the response):
156
+
157
+ ```bash
158
+ printf '{"v":1,"id":1,"verb":"fleet"}\n' | nc -U ~/.tmux-ide/control.sock | head -1
159
+ ```
160
+
161
+ A subscribe loop from node:
162
+
163
+ ```js
164
+ const net = require("node:net");
165
+ const os = require("node:os");
166
+ const sock = net.connect(`${os.homedir()}/.tmux-ide/control.sock`);
167
+ let buf = "";
168
+ sock.on("data", (chunk) => {
169
+ buf += chunk;
170
+ const lines = buf.split("\n");
171
+ buf = lines.pop();
172
+ for (const line of lines.filter(Boolean)) {
173
+ const frame = JSON.parse(line);
174
+ if (frame.event === "agent-status") console.log(frame.data); // react here
175
+ }
176
+ });
177
+ sock.write('{"v":1,"id":1,"verb":"subscribe"}\n');
178
+ ```
179
+
180
+ Right after `subscribe`, the first tick reports every session once with
181
+ `from:null` (a snapshot of where the fleet stands); real transitions follow.
182
+
183
+ **Socket vs CLI:** prefer the socket for anything event-driven or repeated
184
+ (subscribe replaces an `events --follow` poll; a server-held `wait` costs no
185
+ spawn-per-poll). Prefer the CLI for one-shot reads and anything a human might
186
+ re-run — it needs no server. With a server running, `tmux-ide events --follow
187
+ --socket` and `tmux-ide wait … --socket` use it automatically and fall back to
188
+ polling silently when it's gone. The server is local-only by design: no
189
+ network listener, no tokens — filesystem permissions are the auth.
190
+
105
191
  ## Keys & surfaces to tell USERS about
106
192
 
107
193
  Once a session is adopted, the whole UI is a keystroke away. **Lead with the
@@ -123,6 +209,28 @@ One interaction grammar everywhere: `j`/`k` move, `enter` opens, `/` filters,
123
209
  `esc` backs out, `?` asks. Bare `tmux-ide` with no `ide.yml` opens the **home
124
210
  cockpit** (the fleet home screen). `tmux-ide cheatsheet` prints the full sheet.
125
211
 
212
+ ## The app — `tmux-ide app` (the terminal IDE)
213
+
214
+ v2.7 adds a full-screen unified app: tmux stays the engine (PTYs, agents,
215
+ persistence); the app is the IDE around it. Launch `tmux-ide app` (bare = home
216
+ screen) or `tmux-ide app <session>`. Needs `bun`, or a downloaded binary:
217
+ `tmux-ide update --tui-binary`.
218
+
219
+ - **Tabs** `F1`–`F4`: Home (fleet, pick a session = set the workspace) ·
220
+ Terminal (the session mirrored live — it keeps streaming while you're on
221
+ other tabs) · Files (tree + built-in editor: `^s` save, `^z` undo, click to
222
+ place the cursor) · Diff (colored working-tree changes, `^e` opens the file
223
+ in the editor). `F5` = command palette (fuzzy everything).
224
+ - **Mouse-native**: hover highlights; right-click = context menus (split/zoom/
225
+ kill panes, layouts, synchronize-panes, kill/rename sessions & windows —
226
+ destructive actions confirm); drag pane borders to resize; drag-select text →
227
+ clipboard via OSC52 (works through ssh); scrollbars; clickable buttons.
228
+ - **tmux parity**: `[⛶]` zoom, window verbs, layout presets, `/` scrollback
229
+ search with `n`/`N`, paste-buffer picker. `^q` quits — the session is
230
+ untouched, like you were never there.
231
+ - State persists across launches (~/.tmux-ide/app-state.json): last tab,
232
+ session, open file.
233
+
126
234
  ## ide.yml (optional)
127
235
 
128
236
  Adopt works on any session. If you'd rather have tmux-ide build the layout, describe
@@ -5,16 +5,23 @@
5
5
  ## Boundaries
6
6
 
7
7
  - Stay within the scope of your assigned task
8
- - Report out-of-scope issues via --discovered-issues
8
+ - Surface out-of-scope issues to the lead instead of silently expanding scope
9
9
  - Do not modify files outside your task scope without coordination
10
10
 
11
11
  ## Conventions
12
12
 
13
- - Run tests before marking tasks done
14
- - Use the project existing code style
15
- - Commit messages should reference the task ID
13
+ - Run tests before reporting your work done
14
+ - Use the project's existing code style
15
+ - Reference the task or branch in commit messages
16
16
 
17
- ## Completion Protocol
17
+ ## Status & Reporting
18
18
 
19
- - tmux-ide task done <ID> --proof "summary" --summary "key learnings"
20
- - Report issues: include --discovered-issues for anything out of scope
19
+ Report your state so the fleet tabs show ground truth. Claude Code does this
20
+ automatically once `tmux-ide integration install claude` is run; any agent can
21
+ self-report the same way:
22
+
23
+ tmux set-option -p @agent_state "working:$(date +%s)" # working|blocked|done|idle
24
+
25
+ Coordinate with the lead or a teammate pane:
26
+
27
+ tmux-ide send <session-or-pane> "what changed, how you verified it, follow-ups"
@@ -1,8 +1,16 @@
1
1
  name: my-monorepo
2
2
 
3
+ # Ground-truth agent status (working/blocked/done glyphs on the fleet tabs)
4
+ # comes from Claude Code's lifecycle hooks. Wire them once with:
5
+ # tmux-ide integration install claude
6
+
3
7
  team:
4
8
  name: my-monorepo
5
9
 
10
+ sidebar: true # fleet nav column — jump between agents (toggle: prefix b)
11
+
12
+ before: pnpm install
13
+
6
14
  rows:
7
15
  - size: 70%
8
16
  panes:
@@ -1,8 +1,14 @@
1
1
  name: my-app
2
2
 
3
+ # Ground-truth agent status (working/blocked/done glyphs on the fleet tabs)
4
+ # comes from Claude Code's lifecycle hooks. Wire them once with:
5
+ # tmux-ide integration install claude
6
+
3
7
  team:
4
8
  name: my-app
5
9
 
10
+ sidebar: true # fleet nav column — jump between agents (toggle: prefix b)
11
+
6
12
  before: pnpm install
7
13
 
8
14
  rows:
@@ -24,4 +30,6 @@ rows:
24
30
  - panes:
25
31
  - title: Next.js
26
32
  command: pnpm dev
33
+ - title: Changes
34
+ type: changes # live git changes widget
27
35
  - title: Shell
@@ -1,8 +1,16 @@
1
1
  name: my-project
2
2
 
3
+ # Ground-truth agent status (working/blocked/done glyphs on the fleet tabs)
4
+ # comes from Claude Code's lifecycle hooks. Wire them once with:
5
+ # tmux-ide integration install claude
6
+
3
7
  team:
4
8
  name: my-project
5
9
 
10
+ sidebar: true # fleet nav column — jump between agents (toggle: prefix b)
11
+
12
+ # before: npm install
13
+
6
14
  rows:
7
15
  - size: 70%
8
16
  panes:
@@ -18,6 +26,8 @@ rows:
18
26
  role: teammate
19
27
 
20
28
  - panes:
29
+ - title: Changes
30
+ type: changes # live git changes widget
21
31
  - title: Dev Server
22
32
  # command: npm run dev
23
33
  - title: Shell