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.
Files changed (101) hide show
  1. package/README.md +22 -5
  2. package/bin/cli.js +3532 -1090
  3. package/bin/cli.ts +368 -71
  4. package/package.json +2 -1
  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/control/client.d.ts +23 -0
  9. package/packages/daemon/dist/control/client.js +105 -0
  10. package/packages/daemon/dist/control/dispatch.d.ts +34 -0
  11. package/packages/daemon/dist/control/dispatch.js +83 -0
  12. package/packages/daemon/dist/control/fanout.d.ts +19 -0
  13. package/packages/daemon/dist/control/fanout.js +37 -0
  14. package/packages/daemon/dist/control/frames.d.ts +23 -0
  15. package/packages/daemon/dist/control/frames.js +37 -0
  16. package/packages/daemon/dist/control/lifecycle.d.ts +45 -0
  17. package/packages/daemon/dist/control/lifecycle.js +114 -0
  18. package/packages/daemon/dist/control/server.d.ts +16 -0
  19. package/packages/daemon/dist/control/server.js +214 -0
  20. package/packages/daemon/dist/control/verbs.d.ts +11 -0
  21. package/packages/daemon/dist/control/verbs.js +91 -0
  22. package/packages/daemon/dist/doctor.d.ts +18 -0
  23. package/packages/daemon/dist/doctor.js +105 -15
  24. package/packages/daemon/dist/lib/agent-discovery.d.ts +27 -2
  25. package/packages/daemon/dist/lib/agent-discovery.js +29 -14
  26. package/packages/daemon/dist/lib/app-config.d.ts +106 -0
  27. package/packages/daemon/dist/lib/app-config.js +104 -5
  28. package/packages/daemon/dist/lib/manifest-pack.d.ts +79 -0
  29. package/packages/daemon/dist/lib/manifest-pack.js +232 -0
  30. package/packages/daemon/dist/lib/state-home.d.ts +2 -0
  31. package/packages/daemon/dist/lib/state-home.js +12 -0
  32. package/packages/daemon/dist/lib/update-check.js +5 -0
  33. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Info.plist +34 -0
  34. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/MacOS/tmux-ide-notifier +0 -0
  35. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/PkgInfo +1 -0
  36. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Resources/AppIcon.icns +0 -0
  37. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Resources/Assets.car +0 -0
  38. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/_CodeSignature/CodeResources +139 -0
  39. package/packages/daemon/dist/restore.d.ts +35 -8
  40. package/packages/daemon/dist/restore.js +52 -15
  41. package/packages/daemon/dist/send.d.ts +33 -1
  42. package/packages/daemon/dist/send.js +32 -19
  43. package/packages/daemon/src/control/client.ts +128 -0
  44. package/packages/daemon/src/control/dispatch.ts +107 -0
  45. package/packages/daemon/src/control/fanout.ts +44 -0
  46. package/packages/daemon/src/control/frames.ts +40 -0
  47. package/packages/daemon/src/control/lifecycle.ts +151 -0
  48. package/packages/daemon/src/control/server.ts +237 -0
  49. package/packages/daemon/src/control/verbs.ts +118 -0
  50. package/packages/daemon/src/doctor.ts +113 -28
  51. package/packages/daemon/src/lib/agent-discovery.ts +53 -13
  52. package/packages/daemon/src/lib/app-config.ts +103 -5
  53. package/packages/daemon/src/lib/manifest-pack.ts +255 -0
  54. package/packages/daemon/src/lib/state-home.ts +13 -0
  55. package/packages/daemon/src/lib/update-check.ts +5 -0
  56. package/packages/daemon/src/restore.ts +53 -15
  57. package/packages/daemon/src/send.ts +55 -21
  58. package/packages/daemon/src/tui/chrome/events.ts +4 -4
  59. package/packages/daemon/src/tui/chrome/front-door.ts +39 -0
  60. package/packages/daemon/src/tui/chrome/notify-prefs.ts +58 -0
  61. package/packages/daemon/src/tui/chrome/notify-state.ts +76 -0
  62. package/packages/daemon/src/tui/chrome/notify.ts +582 -84
  63. package/packages/daemon/src/tui/chrome/updater.ts +268 -62
  64. package/packages/daemon/src/tui/detect/classify.ts +34 -0
  65. package/packages/daemon/src/tui/detect/manifest-loader.ts +54 -5
  66. package/packages/daemon/src/tui/detect/manifest.ts +24 -3
  67. package/packages/daemon/src/tui/detect/manifests.ts +240 -6
  68. package/packages/daemon/src/tui/detect/process-tree.ts +13 -3
  69. package/packages/daemon/src/tui/detect/session-id.ts +503 -0
  70. package/packages/daemon/src/tui/integrations/opencode.ts +121 -0
  71. package/packages/daemon/src/tui/mirror/agent-chip.ts +40 -11
  72. package/packages/daemon/src/tui/mirror/agent-lifecycle.ts +437 -0
  73. package/packages/daemon/src/tui/mirror/agent-rows.ts +27 -5
  74. package/packages/daemon/src/tui/mirror/app-state.ts +171 -8
  75. package/packages/daemon/src/tui/mirror/app.tsx +2182 -399
  76. package/packages/daemon/src/tui/mirror/attention.ts +110 -0
  77. package/packages/daemon/src/tui/mirror/dialog-stack.ts +17 -4
  78. package/packages/daemon/src/tui/mirror/diff-model.ts +279 -4
  79. package/packages/daemon/src/tui/mirror/file-tree.ts +231 -6
  80. package/packages/daemon/src/tui/mirror/host-terminal.ts +49 -0
  81. package/packages/daemon/src/tui/mirror/hosted.ts +205 -0
  82. package/packages/daemon/src/tui/mirror/layout-parse.ts +154 -0
  83. package/packages/daemon/src/tui/mirror/menu-model.ts +27 -4
  84. package/packages/daemon/src/tui/mirror/palette.ts +299 -9
  85. package/packages/daemon/src/tui/mirror/pane-mirror.ts +82 -4
  86. package/packages/daemon/src/tui/mirror/pane-surface.tsx +18 -11
  87. package/packages/daemon/src/tui/mirror/perf-tap.ts +29 -3
  88. package/packages/daemon/src/tui/mirror/selection.ts +122 -8
  89. package/packages/daemon/src/tui/mirror/session-mirror.ts +349 -68
  90. package/packages/daemon/src/tui/mirror/settings-model.ts +96 -16
  91. package/packages/daemon/src/tui/mirror/sidebar.tsx +218 -0
  92. package/packages/daemon/src/tui/mirror/size-truth.ts +53 -0
  93. package/packages/daemon/src/tui/mirror/theme.ts +45 -0
  94. package/packages/daemon/src/tui/team/fuzzy.ts +20 -0
  95. package/packages/daemon/src/tui/team/sessions.ts +85 -7
  96. package/packages/daemon/src/tui/team/wait.ts +144 -0
  97. package/scripts/build-macos-notifier.mjs +160 -0
  98. package/scripts/postinstall.js +8 -1
  99. package/scripts/prepublish-check.mjs +37 -1
  100. package/scripts/publish-tap.sh +55 -0
  101. package/skill/SKILL.md +88 -2
@@ -0,0 +1,232 @@
1
+ /**
2
+ * Remote agent-detection manifest packs — `tmux-ide update --manifests` (M25.4).
3
+ *
4
+ * Detection breadth shouldn't wait for an npm release: agent TUIs change their
5
+ * chrome between our versions, and a re-tuned manifest is pure data. This
6
+ * module fetches a versioned MANIFEST PACK from a static URL and installs it
7
+ * where the manifest loader already hot-merges from.
8
+ *
9
+ * ## The pack format (publish contract)
10
+ *
11
+ * A single JSON document:
12
+ *
13
+ * ```json
14
+ * {
15
+ * "schema": 1, // this format's version — reject others
16
+ * "pack": "2026.07.12", // pack version (informational, any string)
17
+ * "manifests": [ // one AgentManifest per entry, exactly the
18
+ * { // override-file shape manifest-loader.ts
19
+ * "id": "claude", // documents (id, commands[], states,
20
+ * "commands": ["claude"], // optional confidence)
21
+ * "confidence": "tuned",
22
+ * "states": { "working": { "any": [{ "contains": "esc to interrupt" }] } }
23
+ * }
24
+ * ]
25
+ * }
26
+ * ```
27
+ *
28
+ * Publishing: attach the document as the `agent-manifests.json` asset on a
29
+ * GitHub release — {@link manifestPackUrl} constructs the same
30
+ * `releases/download/v<version>/<asset>` URL the TUI-binary download uses, so
31
+ * a pack rides every release and a NEWER pack can be re-attached to the
32
+ * CURRENT release tag at any time (the fetch always re-downloads; the pane
33
+ * option cache never pins it).
34
+ *
35
+ * ## Precedence (verified by tests)
36
+ *
37
+ * The pack installs into `<agent-detection>/pack/manifest-pack.json` — a
38
+ * SUBDIRECTORY of the user-override dir, so `readOverrideManifests` (which
39
+ * lists only `*.json` FILES in the top level) never sees it. The loader merges
40
+ * `bundled → pack → user`: a pack manifest replaces a bundled id or appends a
41
+ * new one, and a user's own `<agent-detection>/*.json` file ALWAYS beats both.
42
+ *
43
+ * ## Safety
44
+ *
45
+ * - Https only for remote URLs. `file://` and loopback `http://` are accepted
46
+ * ONLY so tests/dev can exercise the flow without the network — anything
47
+ * else is rejected before a byte is fetched.
48
+ * - The pack is schema-validated ({@link validateManifestPack}) BEFORE being
49
+ * written; an invalid pack fails loudly and leaves the previous one intact.
50
+ * - The gated periodic refresh ({@link maybeRefreshManifestPack}) rides the
51
+ * existing daily update check (see {@link ./update-check.ts}) — no timers or
52
+ * daemons of its own — and is double-gated on `updates.check` (the caller)
53
+ * and `updates.manifests` (read here, default false).
54
+ */
55
+ import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
56
+ import { dirname, join } from "node:path";
57
+ import { fileURLToPath } from "node:url";
58
+ import { overrideDir, validateManifestShape } from "../tui/detect/manifest-loader.js";
59
+ import { getAppConfig } from "./app-config.js";
60
+ import { getCurrentVersion } from "./update-check.js";
61
+ import { RELEASE_REPO } from "./tui-binary.js";
62
+ /** The pack-format version this build understands. */
63
+ export const MANIFEST_PACK_SCHEMA = 1;
64
+ /** The release asset name a pack is published under. */
65
+ export const MANIFEST_PACK_ASSET = "agent-manifests.json";
66
+ /** Env override for the pack URL (tests/dev; may be `file://` or loopback http). */
67
+ export const MANIFEST_PACK_URL_ENV = "TMUX_IDE_MANIFEST_PACK_URL";
68
+ // ---------------------------------------------------------------------------
69
+ // Pure
70
+ // ---------------------------------------------------------------------------
71
+ /**
72
+ * PURE — the default pack URL for a version:
73
+ * `https://github.com/<repo>/releases/download/v<version>/agent-manifests.json`
74
+ * (the same asset-URL construction as the TUI binary download).
75
+ */
76
+ export function manifestPackUrl(version = getCurrentVersion()) {
77
+ const v = version.startsWith("v") ? version.slice(1) : version;
78
+ return `https://github.com/${RELEASE_REPO}/releases/download/v${v}/${MANIFEST_PACK_ASSET}`;
79
+ }
80
+ /**
81
+ * PURE — is `url` an acceptable pack source? `https:` always; `file:` and
82
+ * LOOPBACK `http:` (127.0.0.1 / localhost / [::1]) only so tests and local
83
+ * fixtures can drive the flow. Any other scheme/host is rejected.
84
+ */
85
+ export function isAllowedPackUrl(url) {
86
+ let parsed;
87
+ try {
88
+ parsed = new URL(url);
89
+ }
90
+ catch {
91
+ return false;
92
+ }
93
+ if (parsed.protocol === "https:" || parsed.protocol === "file:")
94
+ return true;
95
+ if (parsed.protocol === "http:") {
96
+ return ["127.0.0.1", "localhost", "[::1]", "::1"].includes(parsed.hostname);
97
+ }
98
+ return false;
99
+ }
100
+ /**
101
+ * PURE — validate an untrusted pack document. Returns the typed pack or a
102
+ * human-readable reason. Every manifest entry must pass the SAME structural
103
+ * validation user override files do ({@link validateManifestShape}) — one bad
104
+ * entry rejects the whole pack (a partial install would be harder to reason
105
+ * about than a loud failure). Never throws.
106
+ */
107
+ export function validateManifestPack(value) {
108
+ if (typeof value !== "object" || value === null)
109
+ return { ok: false, reason: "not an object" };
110
+ const v = value;
111
+ if (v.schema !== MANIFEST_PACK_SCHEMA) {
112
+ return {
113
+ ok: false,
114
+ reason: `unsupported schema ${JSON.stringify(v.schema)} (want ${MANIFEST_PACK_SCHEMA})`,
115
+ };
116
+ }
117
+ if (typeof v.pack !== "string" || v.pack.trim().length === 0) {
118
+ return { ok: false, reason: "missing pack version string" };
119
+ }
120
+ if (!Array.isArray(v.manifests) || v.manifests.length === 0) {
121
+ return { ok: false, reason: "manifests must be a non-empty array" };
122
+ }
123
+ for (let i = 0; i < v.manifests.length; i++) {
124
+ if (!validateManifestShape(v.manifests[i])) {
125
+ return {
126
+ ok: false,
127
+ reason: `manifests[${i}] is not a valid AgentManifest (need id, commands[], states)`,
128
+ };
129
+ }
130
+ }
131
+ return {
132
+ ok: true,
133
+ pack: { schema: MANIFEST_PACK_SCHEMA, pack: v.pack, manifests: v.manifests },
134
+ };
135
+ }
136
+ // ---------------------------------------------------------------------------
137
+ // io
138
+ // ---------------------------------------------------------------------------
139
+ /** The installed pack's directory: `<agent-detection>/pack/`. */
140
+ export function packDir() {
141
+ return join(overrideDir(), "pack");
142
+ }
143
+ /** The installed pack's path: `<agent-detection>/pack/manifest-pack.json`. */
144
+ export function packPath() {
145
+ return join(packDir(), "manifest-pack.json");
146
+ }
147
+ /**
148
+ * io — fetch and validate a pack. `file://` URLs are read from disk (Node's
149
+ * fetch does not speak file:); everything else goes through fetch with a
150
+ * timeout. THROWS with an actionable message on a disallowed URL, a failed
151
+ * fetch, malformed JSON, or a schema-invalid pack — the caller decides whether
152
+ * that is loud (CLI) or swallowed (the background refresh).
153
+ */
154
+ export async function fetchManifestPack(url, timeoutMs = 5000) {
155
+ if (!isAllowedPackUrl(url)) {
156
+ throw new Error(`refusing manifest-pack URL ${url} — https only (file:// and loopback http are allowed for local testing)`);
157
+ }
158
+ let body;
159
+ if (url.startsWith("file:")) {
160
+ body = readFileSync(fileURLToPath(url), "utf8");
161
+ }
162
+ else {
163
+ const controller = new AbortController();
164
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
165
+ try {
166
+ const res = await fetch(url, { signal: controller.signal });
167
+ if (!res.ok) {
168
+ throw new Error(`manifest-pack download failed (${url} → HTTP ${res.status} ${res.statusText})`);
169
+ }
170
+ body = await res.text();
171
+ }
172
+ finally {
173
+ clearTimeout(timer);
174
+ }
175
+ }
176
+ let parsed;
177
+ try {
178
+ parsed = JSON.parse(body);
179
+ }
180
+ catch {
181
+ throw new Error(`manifest pack at ${url} is not valid JSON`);
182
+ }
183
+ const verdict = validateManifestPack(parsed);
184
+ if (!verdict.ok) {
185
+ throw new Error(`manifest pack at ${url} rejected: ${verdict.reason}`);
186
+ }
187
+ return verdict.pack;
188
+ }
189
+ /**
190
+ * io — write a validated pack into place atomically (temp-in-same-dir →
191
+ * rename, so a crash never leaves a half-written pack the loader would then
192
+ * warn about every run). Returns the installed path.
193
+ */
194
+ export function installManifestPack(pack, dest = packPath()) {
195
+ mkdirSync(dirname(dest), { recursive: true });
196
+ const tmp = `${dest}.${process.pid}.tmp`;
197
+ writeFileSync(tmp, JSON.stringify(pack, null, 2));
198
+ renameSync(tmp, dest);
199
+ return dest;
200
+ }
201
+ /**
202
+ * io — the `tmux-ide update --manifests` flow: resolve the URL (env override →
203
+ * the release-asset default), fetch, validate, install. Throws loudly on any
204
+ * failure (the CLI surfaces it); returns what was installed for reporting.
205
+ */
206
+ export async function updateManifestPack(opts = {}) {
207
+ const log = opts.log ?? (() => { });
208
+ const url = opts.url ?? process.env[MANIFEST_PACK_URL_ENV] ?? manifestPackUrl();
209
+ log(`fetching manifest pack from ${url}`);
210
+ const pack = await fetchManifestPack(url);
211
+ const path = installManifestPack(pack);
212
+ log(`installed pack ${pack.pack} (${pack.manifests.length} manifests) → ${path}`);
213
+ return { path, packVersion: pack.pack, count: pack.manifests.length };
214
+ }
215
+ /**
216
+ * io — the GATED background refresh the daily update check invokes (see
217
+ * `runUpdateCheck` in {@link ./update-check.ts}). Opt-in via
218
+ * `updates.manifests: true` (default false); NEVER throws — a failed refresh
219
+ * just keeps the previous pack, exactly like a failed version check keeps the
220
+ * previous `latest`.
221
+ */
222
+ export async function maybeRefreshManifestPack() {
223
+ try {
224
+ if (!getAppConfig().updates.manifests)
225
+ return;
226
+ await updateManifestPack();
227
+ }
228
+ catch {
229
+ // Offline, a missing release asset, or an invalid pack — all degrade to
230
+ // "keep what we have"; the explicit CLI path reports errors loudly instead.
231
+ }
232
+ }
@@ -0,0 +1,2 @@
1
+ /** Absolute path to the state home directory (not created here). */
2
+ export declare function stateHome(): string;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * THE tmux-ide state home — `TMUX_IDE_HOME` when set (tests / per-run
3
+ * overrides), else `~/.tmux-ide`. The same resolution the welcome marker,
4
+ * update cache, and app state each grew locally; new state files should
5
+ * resolve through here so none of them can drift from the override again.
6
+ */
7
+ import { homedir } from "node:os";
8
+ import { join } from "node:path";
9
+ /** Absolute path to the state home directory (not created here). */
10
+ export function stateHome() {
11
+ return process.env.TMUX_IDE_HOME ?? join(homedir(), ".tmux-ide");
12
+ }
@@ -223,6 +223,11 @@ export async function runUpdateCheck({ now = Date.now() } = {}) {
223
223
  const cache = readUpdateCache();
224
224
  if (!shouldCheck(cache?.lastCheckedAt ?? null, now))
225
225
  return;
226
+ // The agent-detection manifest-pack refresh RIDES this same daily throttle
227
+ // (M25.4) — no timer of its own, gated again inside on `updates.manifests`
228
+ // (default false) and never throwing. Dynamic import: manifest-pack imports
229
+ // this module for getCurrentVersion, so a static import would be a cycle.
230
+ void import("./manifest-pack.js").then((m) => m.maybeRefreshManifestPack()).catch(() => { });
226
231
  const fetched = await fetchLatestVersion();
227
232
  writeUpdateCache({
228
233
  lastCheckedAt: now,
@@ -0,0 +1,34 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>CFBundleDevelopmentRegion</key>
6
+ <string>en</string>
7
+ <key>CFBundleDisplayName</key>
8
+ <string>tmux-ide</string>
9
+ <key>CFBundleExecutable</key>
10
+ <string>tmux-ide-notifier</string>
11
+ <key>CFBundleIconFile</key>
12
+ <string>AppIcon</string>
13
+ <key>CFBundleIconName</key>
14
+ <string>AppIcon</string>
15
+ <key>CFBundleIdentifier</key>
16
+ <string>com.wavyrai.tmuxide.notifier</string>
17
+ <key>CFBundleInfoDictionaryVersion</key>
18
+ <string>6.0</string>
19
+ <key>CFBundleName</key>
20
+ <string>tmux-ide</string>
21
+ <key>CFBundlePackageType</key>
22
+ <string>APPL</string>
23
+ <key>CFBundleShortVersionString</key>
24
+ <string>2.8.0</string>
25
+ <key>CFBundleVersion</key>
26
+ <string>2.8.0</string>
27
+ <key>LSMinimumSystemVersion</key>
28
+ <string>11.0</string>
29
+ <key>LSUIElement</key>
30
+ <true/>
31
+ <key>NSPrincipalClass</key>
32
+ <string>NSApplication</string>
33
+ </dict>
34
+ </plist>
@@ -0,0 +1,139 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>files</key>
6
+ <dict>
7
+ <key>Resources/AppIcon.icns</key>
8
+ <data>
9
+ X3UpT06Bz44e6JibpJhhiMxRfno=
10
+ </data>
11
+ <key>Resources/Assets.car</key>
12
+ <data>
13
+ I3z+NDxWadVSrUS0xGMi1yZxHfA=
14
+ </data>
15
+ </dict>
16
+ <key>files2</key>
17
+ <dict>
18
+ <key>Resources/AppIcon.icns</key>
19
+ <dict>
20
+ <key>hash2</key>
21
+ <data>
22
+ tkz+Ee3lkawLrys701JjQo2Z+YlNtbudhtH13NTsrQw=
23
+ </data>
24
+ </dict>
25
+ <key>Resources/Assets.car</key>
26
+ <dict>
27
+ <key>hash2</key>
28
+ <data>
29
+ vuVGO00nkAAaL21f21SA22M7NZ311c+KH7/lizezdck=
30
+ </data>
31
+ </dict>
32
+ </dict>
33
+ <key>rules</key>
34
+ <dict>
35
+ <key>^Resources/</key>
36
+ <true/>
37
+ <key>^Resources/.*\.lproj/</key>
38
+ <dict>
39
+ <key>optional</key>
40
+ <true/>
41
+ <key>weight</key>
42
+ <real>1000</real>
43
+ </dict>
44
+ <key>^Resources/.*\.lproj/locversion.plist$</key>
45
+ <dict>
46
+ <key>omit</key>
47
+ <true/>
48
+ <key>weight</key>
49
+ <real>1100</real>
50
+ </dict>
51
+ <key>^Resources/Base\.lproj/</key>
52
+ <dict>
53
+ <key>weight</key>
54
+ <real>1010</real>
55
+ </dict>
56
+ <key>^version.plist$</key>
57
+ <true/>
58
+ </dict>
59
+ <key>rules2</key>
60
+ <dict>
61
+ <key>.*\.dSYM($|/)</key>
62
+ <dict>
63
+ <key>weight</key>
64
+ <real>11</real>
65
+ </dict>
66
+ <key>^(.*/)?\.DS_Store$</key>
67
+ <dict>
68
+ <key>omit</key>
69
+ <true/>
70
+ <key>weight</key>
71
+ <real>2000</real>
72
+ </dict>
73
+ <key>^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/</key>
74
+ <dict>
75
+ <key>nested</key>
76
+ <true/>
77
+ <key>weight</key>
78
+ <real>10</real>
79
+ </dict>
80
+ <key>^.*</key>
81
+ <true/>
82
+ <key>^Info\.plist$</key>
83
+ <dict>
84
+ <key>omit</key>
85
+ <true/>
86
+ <key>weight</key>
87
+ <real>20</real>
88
+ </dict>
89
+ <key>^PkgInfo$</key>
90
+ <dict>
91
+ <key>omit</key>
92
+ <true/>
93
+ <key>weight</key>
94
+ <real>20</real>
95
+ </dict>
96
+ <key>^Resources/</key>
97
+ <dict>
98
+ <key>weight</key>
99
+ <real>20</real>
100
+ </dict>
101
+ <key>^Resources/.*\.lproj/</key>
102
+ <dict>
103
+ <key>optional</key>
104
+ <true/>
105
+ <key>weight</key>
106
+ <real>1000</real>
107
+ </dict>
108
+ <key>^Resources/.*\.lproj/locversion.plist$</key>
109
+ <dict>
110
+ <key>omit</key>
111
+ <true/>
112
+ <key>weight</key>
113
+ <real>1100</real>
114
+ </dict>
115
+ <key>^Resources/Base\.lproj/</key>
116
+ <dict>
117
+ <key>weight</key>
118
+ <real>1010</real>
119
+ </dict>
120
+ <key>^[^/]+$</key>
121
+ <dict>
122
+ <key>nested</key>
123
+ <true/>
124
+ <key>weight</key>
125
+ <real>10</real>
126
+ </dict>
127
+ <key>^embedded\.provisionprofile$</key>
128
+ <dict>
129
+ <key>weight</key>
130
+ <real>20</real>
131
+ </dict>
132
+ <key>^version\.plist$</key>
133
+ <dict>
134
+ <key>weight</key>
135
+ <real>20</real>
136
+ </dict>
137
+ </dict>
138
+ </dict>
139
+ </plist>
@@ -32,19 +32,46 @@ export interface RestorePlan {
32
32
  * config; failing that, we raw-rebuild from the recorded windows/panes.
33
33
  */
34
34
  export declare function buildRestorePlan(snapshot: FleetSnapshot, liveSessionNames: Iterable<string>, ideProjects?: Map<string, string>): RestorePlan;
35
+ /**
36
+ * The native session-resume invocation per agent kind (M24.1 — the table was
37
+ * claude-only until then). ONLY VERIFIED entries ship; each spelling's source:
38
+ * - `claude --resume <id>`: the shipped integration (its hooks record the id).
39
+ * - `codex resume <id>`: `codex resume --help` — "Usage: codex resume
40
+ * [OPTIONS] [SESSION_ID] … Session id (UUID) or session name".
41
+ * - `opencode --session <id>`: `opencode --help` — "-s, --session session id
42
+ * to continue".
43
+ * - `cursor-agent --resume <id>`: `cursor-agent --help` — "--resume [chatId]
44
+ * Select a session to resume".
45
+ * - `copilot --resume=<id>`: GitHub Copilot CLI command reference —
46
+ * "--resume[=VALUE] Resume a previous interactive session … Optionally
47
+ * specify a session ID" (the `=` form, since the value is optional).
48
+ * Unverified (no entry): gemini, aider, goose, amp — no confirmed native
49
+ * per-session resume invocation at the time of writing.
50
+ *
51
+ * How `@agent_session_id` gets recorded per kind (M25.3):
52
+ * - claude — the hooks integration (`integration install claude`).
53
+ * - opencode — the plugin integration (`integration install opencode`).
54
+ * - codex, cursor — automatic: the chrome updater probes the agent's own
55
+ * on-disk session state ({@link ./tui/detect/session-id.ts}).
56
+ * - copilot — no automatic capture (skipped with evidence — see the
57
+ * session-id module header); fires only for panes whose agent self-reported
58
+ * the id per the agent contract
59
+ * (`tmux set-option -p @agent_session_id <id>`), which any kind may do.
60
+ */
61
+ export declare const AGENT_RESUME_COMMANDS: Record<string, (id: string) => string>;
35
62
  /**
36
63
  * PURE — the command to relaunch a pane so its agent conversation is revived,
37
64
  * or `null` for "no resume — leave it a plain shell (or replay `command`)".
38
65
  *
39
66
  * Decision table (only fires when `resumeAgents` is on):
40
- * - claude + a recorded `@agent_session_id` → `claude --resume <id>`. The id is
41
- * a uuid, but we don't trust the snapshot: unless it matches
42
- * {@link SAFE_SESSION_ID} (`[A-Za-z0-9-]`) we bail to `null` rather than risk
43
- * feeding shell metacharacters into send-keys.
44
- * - claude with NO session id → `null`. `claude --continue` would be too
45
- * magical here (it resumes the most-recent conversation in the cwd, which may
46
- * belong to a different pane); a fresh shell is the safe, predictable default.
47
- * - any other agent → `null`. No native resume story yet.
67
+ * - a kind in {@link AGENT_RESUME_COMMANDS} + a recorded `@agent_session_id` →
68
+ * that kind's resume invocation. The id is uuid-ish, but we don't trust the
69
+ * snapshot: unless it matches {@link SAFE_SESSION_ID} we bail to `null`
70
+ * rather than risk feeding shell metacharacters into send-keys.
71
+ * - a known kind with NO session id → `null`. A "continue most recent" flag
72
+ * would be too magical here (it resumes the cwd's most-recent conversation,
73
+ * which may belong to a different pane); a fresh shell is the safe default.
74
+ * - any other agent → `null`. No VERIFIED resume story.
48
75
  * - `resumeAgents` off → always `null`.
49
76
  */
50
77
  export declare function paneResumeCommand(pane: PaneSnapshot, opts: {
@@ -13,9 +13,11 @@
13
13
  *
14
14
  * `--resume-agents` (or `{ "restore": { "resumeAgents": true } }` in
15
15
  * `~/.tmux-ide/config.json`) layers native agent-resume on top: a rebuilt
16
- * claude pane that carries a recorded `@agent_session_id` relaunches as
17
- * `claude --resume <id>`, reviving the actual conversation rather than opening
18
- * a fresh shell. See {@link paneResumeCommand} for the exact decision table.
16
+ * agent pane that carries a recorded `@agent_session_id` relaunches with its
17
+ * kind's VERIFIED resume invocation (`claude --resume <id>`, `codex resume
18
+ * <id>`, … — {@link AGENT_RESUME_COMMANDS}), reviving the actual conversation
19
+ * rather than opening a fresh shell. See {@link paneResumeCommand} for the
20
+ * exact decision table.
19
21
  *
20
22
  * {@link buildRestorePlan} + {@link paneResumeCommand} + {@link restorePrefs}
21
23
  * are PURE (unit-tested); {@link restore} is the thin io wrapper that reads the
@@ -58,32 +60,67 @@ export function buildRestorePlan(snapshot, liveSessionNames, ideProjects = new M
58
60
  // ---------------------------------------------------------------------------
59
61
  // Pure — agent resume decision
60
62
  // ---------------------------------------------------------------------------
61
- /** A session id is trusted only if it's the uuid alphabet — nothing shell-active. */
62
- const SAFE_SESSION_ID = /^[A-Za-z0-9-]+$/;
63
+ /** A session id is trusted only if it's uuid-ish (`[A-Za-z0-9_-]` — underscore
64
+ * covers opencode's `ses_…` ids) — nothing shell-active. */
65
+ const SAFE_SESSION_ID = /^[A-Za-z0-9_-]+$/;
66
+ /**
67
+ * The native session-resume invocation per agent kind (M24.1 — the table was
68
+ * claude-only until then). ONLY VERIFIED entries ship; each spelling's source:
69
+ * - `claude --resume <id>`: the shipped integration (its hooks record the id).
70
+ * - `codex resume <id>`: `codex resume --help` — "Usage: codex resume
71
+ * [OPTIONS] [SESSION_ID] … Session id (UUID) or session name".
72
+ * - `opencode --session <id>`: `opencode --help` — "-s, --session session id
73
+ * to continue".
74
+ * - `cursor-agent --resume <id>`: `cursor-agent --help` — "--resume [chatId]
75
+ * Select a session to resume".
76
+ * - `copilot --resume=<id>`: GitHub Copilot CLI command reference —
77
+ * "--resume[=VALUE] Resume a previous interactive session … Optionally
78
+ * specify a session ID" (the `=` form, since the value is optional).
79
+ * Unverified (no entry): gemini, aider, goose, amp — no confirmed native
80
+ * per-session resume invocation at the time of writing.
81
+ *
82
+ * How `@agent_session_id` gets recorded per kind (M25.3):
83
+ * - claude — the hooks integration (`integration install claude`).
84
+ * - opencode — the plugin integration (`integration install opencode`).
85
+ * - codex, cursor — automatic: the chrome updater probes the agent's own
86
+ * on-disk session state ({@link ./tui/detect/session-id.ts}).
87
+ * - copilot — no automatic capture (skipped with evidence — see the
88
+ * session-id module header); fires only for panes whose agent self-reported
89
+ * the id per the agent contract
90
+ * (`tmux set-option -p @agent_session_id <id>`), which any kind may do.
91
+ */
92
+ export const AGENT_RESUME_COMMANDS = {
93
+ claude: (id) => `claude --resume ${id}`,
94
+ codex: (id) => `codex resume ${id}`,
95
+ opencode: (id) => `opencode --session ${id}`,
96
+ cursor: (id) => `cursor-agent --resume ${id}`,
97
+ copilot: (id) => `copilot --resume=${id}`,
98
+ };
63
99
  /**
64
100
  * PURE — the command to relaunch a pane so its agent conversation is revived,
65
101
  * or `null` for "no resume — leave it a plain shell (or replay `command`)".
66
102
  *
67
103
  * Decision table (only fires when `resumeAgents` is on):
68
- * - claude + a recorded `@agent_session_id` → `claude --resume <id>`. The id is
69
- * a uuid, but we don't trust the snapshot: unless it matches
70
- * {@link SAFE_SESSION_ID} (`[A-Za-z0-9-]`) we bail to `null` rather than risk
71
- * feeding shell metacharacters into send-keys.
72
- * - claude with NO session id → `null`. `claude --continue` would be too
73
- * magical here (it resumes the most-recent conversation in the cwd, which may
74
- * belong to a different pane); a fresh shell is the safe, predictable default.
75
- * - any other agent → `null`. No native resume story yet.
104
+ * - a kind in {@link AGENT_RESUME_COMMANDS} + a recorded `@agent_session_id` →
105
+ * that kind's resume invocation. The id is uuid-ish, but we don't trust the
106
+ * snapshot: unless it matches {@link SAFE_SESSION_ID} we bail to `null`
107
+ * rather than risk feeding shell metacharacters into send-keys.
108
+ * - a known kind with NO session id → `null`. A "continue most recent" flag
109
+ * would be too magical here (it resumes the cwd's most-recent conversation,
110
+ * which may belong to a different pane); a fresh shell is the safe default.
111
+ * - any other agent → `null`. No VERIFIED resume story.
76
112
  * - `resumeAgents` off → always `null`.
77
113
  */
78
114
  export function paneResumeCommand(pane, opts) {
79
115
  if (!opts.resumeAgents)
80
116
  return null;
81
- if (pane.agent !== "claude")
117
+ const resume = pane.agent ? AGENT_RESUME_COMMANDS[pane.agent] : undefined;
118
+ if (!resume)
82
119
  return null;
83
120
  const id = pane.agentSessionId;
84
121
  if (!id || !SAFE_SESSION_ID.test(id))
85
122
  return null;
86
- return `claude --resume ${id}`;
123
+ return resume(id);
87
124
  }
88
125
  /** PURE — how many of a session's panes would resume under `resumeAgents`. */
89
126
  export function countResumableAgents(session, resumeAgents) {
@@ -1,4 +1,4 @@
1
- import { type PaneInfo } from "./widgets/lib/pane-comms.ts";
1
+ import { type PaneInfo, type PaneBusyStatus } from "./widgets/lib/pane-comms.ts";
2
2
  export declare const LONG_MESSAGE_THRESHOLD = 150;
3
3
  /**
4
4
  * Write a long message to a dispatch file and return the short trigger command.
@@ -23,5 +23,37 @@ interface SendOptions {
23
23
  * 5. Case-insensitive partial title match
24
24
  */
25
25
  export declare function resolvePane(panes: PaneInfo[], target: string): PaneInfo | null;
26
+ /** What a delivery reports back (the `send --json` payload, and the control
27
+ * socket's `send` verb data). */
28
+ export interface DeliverResult {
29
+ ok: true;
30
+ session: string;
31
+ target: {
32
+ paneId: string;
33
+ name: string | null;
34
+ title: string;
35
+ role: string | null;
36
+ };
37
+ message: string;
38
+ busyStatus: PaneBusyStatus;
39
+ sentViaFile: boolean;
40
+ warning?: "agent_busy";
41
+ }
42
+ /**
43
+ * Deliver `message` to a pane of `session` — the SHARED core behind the
44
+ * `tmux-ide send` CLI case and the control socket's `send` verb. Resolves
45
+ * `target` (pane id / @ide_name / title / role / partial title), adapts to
46
+ * the pane's busy status, and routes long messages through a dispatch file
47
+ * under `dir` when one is given (without a `dir` the text is sent directly).
48
+ * Throws `IdeError` (SESSION_NOT_FOUND / PANE_NOT_FOUND) — no printing here.
49
+ */
50
+ export declare function deliverMessage(opts: {
51
+ session: string;
52
+ target: string;
53
+ message: string;
54
+ noEnter?: boolean;
55
+ /** The project dir long messages are dispatched through as a file. */
56
+ dir?: string;
57
+ }): DeliverResult;
26
58
  export declare function send(targetDir: string | undefined, opts: SendOptions): Promise<void>;
27
59
  export {};