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
@@ -19,6 +19,7 @@ import { existsSync, readFileSync, mkdirSync } from "node:fs";
19
19
  import { writeFile } from "node:fs/promises";
20
20
  import { homedir } from "node:os";
21
21
  import { dirname, join } from "node:path";
22
+ import { isSpawnWhere, type LastSpawn } from "./agent-lifecycle.ts";
22
23
 
23
24
  /** The four top-level surfaces. `terminal` is the SessionMirror, `files` the
24
25
  * editor + file list, `diff` the git panel, `home` the fleet cockpit. */
@@ -48,6 +49,25 @@ export function clampSidebarWidth(w: number): number {
48
49
  /** How many recently-opened folders home remembers (M22.5). Oldest fall off. */
49
50
  export const RECENTS_CAP = 8;
50
51
 
52
+ /** How many per-context "again" spawn memories persist (M24.1). Oldest fall off. */
53
+ export const SPAWN_MEMORY_CAP = 20;
54
+
55
+ /** How many custom spawn commands the recents list keeps (M24.1, global). */
56
+ export const CUSTOM_COMMANDS_CAP = 5;
57
+
58
+ /** How many palette actions the usage history remembers (M24.4). LRU by
59
+ * last use — the map is insertion-ordered oldest-first, like lastSpawns. */
60
+ export const PALETTE_USAGE_CAP = 50;
61
+
62
+ /** One palette action's usage record (M24.4): how often it ran and when last.
63
+ * Keyed by the STABLE action key ({@link ./palette.ts}'s paletteActionKey) so
64
+ * a relabeled action keeps its history. */
65
+ export interface PaletteUsageEntry {
66
+ count: number;
67
+ /** Epoch seconds of the most recent run. */
68
+ lastUsed: number;
69
+ }
70
+
51
71
  /** The persisted shape. `null` means "nothing remembered" for that slot. */
52
72
  export interface AppState {
53
73
  /** The tab the app was showing when it last saved. */
@@ -63,6 +83,21 @@ export interface AppState {
63
83
  /** Recently-opened folder paths (M22.5), most-recent first, deduped and
64
84
  * capped at {@link RECENTS_CAP}. Home renders these under a "recent" header. */
65
85
  recentFolders: string[];
86
+ /** The "again" memory (M24.1): spawn-context key (a project/session dir, or
87
+ * `session:<name>` when no dir is known) → the last spawn there. Insertion-
88
+ * ordered LRU capped at {@link SPAWN_MEMORY_CAP}. */
89
+ lastSpawns: Record<string, LastSpawn>;
90
+ /** Recent CUSTOM spawn commands (M24.1), most-recent first, deduped, global
91
+ * (not per-project), capped at {@link CUSTOM_COMMANDS_CAP}. */
92
+ customCommands: string[];
93
+ /** Palette usage history (M24.4): stable action key → {count, lastUsed}.
94
+ * Insertion-ordered LRU (oldest first) capped at {@link PALETTE_USAGE_CAP};
95
+ * drives the empty-query "recent" group and the ranking tie-break. */
96
+ paletteUsage: Record<string, PaletteUsageEntry>;
97
+ /** Files surface (M24.6): show dotfiles (H toggle). Default hidden. */
98
+ filesShowHidden: boolean;
99
+ /** Files surface (M24.6): show gitignored entries (I toggle). Default hidden. */
100
+ filesShowIgnored: boolean;
66
101
  }
67
102
 
68
103
  export const DEFAULT_APP_STATE: AppState = {
@@ -72,8 +107,132 @@ export const DEFAULT_APP_STATE: AppState = {
72
107
  diffFile: null,
73
108
  sidebarW: SIDEBAR_W_DEFAULT,
74
109
  recentFolders: [],
110
+ lastSpawns: {},
111
+ customCommands: [],
112
+ paletteUsage: {},
113
+ filesShowHidden: false,
114
+ filesShowIgnored: false,
75
115
  };
76
116
 
117
+ /** PURE — the "again" memory key for a spawn context: the concrete dir when
118
+ * known, else the session name (namespaced so a dirless session can't collide
119
+ * with a path), else null — nothing stable to remember under. */
120
+ export function spawnMemoryKey(dir: string | null, session?: string): string | null {
121
+ if (dir && dir.length > 0) return dir;
122
+ if (session && session.length > 0) return `session:${session}`;
123
+ return null;
124
+ }
125
+
126
+ /**
127
+ * PURE — the spawn-memory map after remembering `spawn` under `key`: the key
128
+ * moves to newest (re-inserted last), and when the map outgrows `cap` the
129
+ * OLDEST entries drop (JS objects preserve string-key insertion order — the
130
+ * same order JSON round-trips, so the LRU survives restarts).
131
+ */
132
+ export function rememberSpawn(
133
+ map: Readonly<Record<string, LastSpawn>>,
134
+ key: string,
135
+ spawn: LastSpawn,
136
+ cap: number = SPAWN_MEMORY_CAP,
137
+ ): Record<string, LastSpawn> {
138
+ const out: Record<string, LastSpawn> = {};
139
+ for (const [k, v] of Object.entries(map)) if (k !== key) out[k] = v;
140
+ out[key] = spawn;
141
+ const keys = Object.keys(out);
142
+ for (let i = 0; i < keys.length - cap; i++) delete out[keys[i]!];
143
+ return out;
144
+ }
145
+
146
+ /**
147
+ * PURE — the palette-usage map after running the action keyed `key` at
148
+ * `nowSec`: count bumps, lastUsed updates, and the key moves to newest
149
+ * (re-inserted last — the rememberSpawn LRU idiom, so JSON round-trips keep the
150
+ * eviction order). Past `cap`, the OLDEST-used entries drop. Blank keys no-op.
151
+ */
152
+ export function recordPaletteUse(
153
+ map: Readonly<Record<string, PaletteUsageEntry>>,
154
+ key: string,
155
+ nowSec: number,
156
+ cap: number = PALETTE_USAGE_CAP,
157
+ ): Record<string, PaletteUsageEntry> {
158
+ if (key.length === 0) return { ...map };
159
+ const out: Record<string, PaletteUsageEntry> = {};
160
+ for (const [k, v] of Object.entries(map)) if (k !== key) out[k] = v;
161
+ out[key] = { count: (map[key]?.count ?? 0) + 1, lastUsed: nowSec };
162
+ const keys = Object.keys(out);
163
+ for (let i = 0; i < keys.length - cap; i++) delete out[keys[i]!];
164
+ return out;
165
+ }
166
+
167
+ /** PURE — a persisted palette-usage value coerced clean: non-object → {},
168
+ * entries with a mistyped/non-finite count or lastUsed drop, order kept,
169
+ * capped from the OLD end (like sanitizeSpawns). */
170
+ function sanitizePaletteUsage(v: unknown): Record<string, PaletteUsageEntry> {
171
+ if (!v || typeof v !== "object" || Array.isArray(v)) return {};
172
+ const out: Record<string, PaletteUsageEntry> = {};
173
+ for (const [key, raw] of Object.entries(v as Record<string, unknown>)) {
174
+ if (key.length === 0) continue;
175
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) continue;
176
+ const o = raw as Record<string, unknown>;
177
+ if (typeof o.count !== "number" || !Number.isFinite(o.count) || o.count < 1) continue;
178
+ if (typeof o.lastUsed !== "number" || !Number.isFinite(o.lastUsed)) continue;
179
+ out[key] = { count: Math.floor(o.count), lastUsed: Math.floor(o.lastUsed) };
180
+ }
181
+ const keys = Object.keys(out);
182
+ for (let i = 0; i < keys.length - PALETTE_USAGE_CAP; i++) delete out[keys[i]!];
183
+ return out;
184
+ }
185
+
186
+ /** PURE — the custom-command recents after running `command`: moved to the
187
+ * front, deduped, capped. Blank commands leave the list unchanged. */
188
+ export function addCustomCommand(
189
+ list: readonly string[],
190
+ command: string,
191
+ cap: number = CUSTOM_COMMANDS_CAP,
192
+ ): string[] {
193
+ const cmd = command.trim();
194
+ if (cmd.length === 0) return [...list];
195
+ return [cmd, ...list.filter((c) => c !== cmd)].slice(0, cap);
196
+ }
197
+
198
+ /** PURE — one persisted spawn coerced to a clean {@link LastSpawn}, or null
199
+ * when any field is missing/mistyped (the whole entry drops). */
200
+ function sanitizeSpawn(v: unknown): LastSpawn | null {
201
+ if (!v || typeof v !== "object" || Array.isArray(v)) return null;
202
+ const o = v as Record<string, unknown>;
203
+ if (typeof o.kind !== "string" || o.kind.length === 0) return null;
204
+ if (typeof o.command !== "string" || o.command.length === 0) return null;
205
+ if (!isSpawnWhere(o.placement)) return null;
206
+ return { kind: o.kind, command: o.command, placement: o.placement };
207
+ }
208
+
209
+ /** PURE — a persisted spawn-memory value coerced clean: non-object → {},
210
+ * malformed entries drop, order kept, capped from the OLD end. */
211
+ function sanitizeSpawns(v: unknown): Record<string, LastSpawn> {
212
+ if (!v || typeof v !== "object" || Array.isArray(v)) return {};
213
+ const out: Record<string, LastSpawn> = {};
214
+ for (const [key, raw] of Object.entries(v as Record<string, unknown>)) {
215
+ if (key.length === 0) continue;
216
+ const spawn = sanitizeSpawn(raw);
217
+ if (spawn) out[key] = spawn;
218
+ }
219
+ const keys = Object.keys(out);
220
+ for (let i = 0; i < keys.length - SPAWN_MEMORY_CAP; i++) delete out[keys[i]!];
221
+ return out;
222
+ }
223
+
224
+ /** PURE — a persisted string list coerced clean: non-empty, deduped (first
225
+ * wins), capped at `cap`. Anything not a string[] yields []. */
226
+ function sanitizeStringList(v: unknown, cap: number): string[] {
227
+ if (!Array.isArray(v)) return [];
228
+ const out: string[] = [];
229
+ for (const item of v) {
230
+ if (typeof item === "string" && item.length > 0 && !out.includes(item)) out.push(item);
231
+ if (out.length >= cap) break;
232
+ }
233
+ return out;
234
+ }
235
+
77
236
  /** PURE — the recents list after opening `dir`: it moves to the front,
78
237
  * any earlier occurrence is removed (dedupe), and the tail past `cap` drops.
79
238
  * Blank paths are ignored (return the list unchanged). */
@@ -89,13 +248,7 @@ export function addRecentFolder(
89
248
  /** PURE — a persisted recents value coerced to clean strings: non-empty,
90
249
  * deduped (first wins), capped. Anything not a string[] yields []. */
91
250
  function sanitizeRecents(v: unknown): string[] {
92
- if (!Array.isArray(v)) return [];
93
- const out: string[] = [];
94
- for (const item of v) {
95
- if (typeof item === "string" && item.length > 0 && !out.includes(item)) out.push(item);
96
- if (out.length >= RECENTS_CAP) break;
97
- }
98
- return out;
251
+ return sanitizeStringList(v, RECENTS_CAP);
99
252
  }
100
253
 
101
254
  /** The tmux-ide home dir: `TMUX_IDE_HOME` when set, else `~/.tmux-ide`. */
@@ -137,10 +290,15 @@ export function parseAppState(raw: string): AppState {
137
290
  ? clampSidebarWidth(obj.sidebarW)
138
291
  : DEFAULT_APP_STATE.sidebarW,
139
292
  recentFolders: sanitizeRecents(obj.recentFolders),
293
+ lastSpawns: sanitizeSpawns(obj.lastSpawns),
294
+ customCommands: sanitizeStringList(obj.customCommands, CUSTOM_COMMANDS_CAP),
295
+ paletteUsage: sanitizePaletteUsage(obj.paletteUsage),
296
+ filesShowHidden: obj.filesShowHidden === true,
297
+ filesShowIgnored: obj.filesShowIgnored === true,
140
298
  };
141
299
  }
142
300
 
143
- /** PURE — serialize an {@link AppState} to the exact four-key JSON we persist
301
+ /** PURE — serialize an {@link AppState} to the exact JSON shape we persist
144
302
  * (extra runtime keys are dropped; stable key order for tidy diffs). */
145
303
  export function serializeAppState(state: AppState): string {
146
304
  const clean: AppState = {
@@ -150,6 +308,11 @@ export function serializeAppState(state: AppState): string {
150
308
  diffFile: optString(state.diffFile),
151
309
  sidebarW: clampSidebarWidth(state.sidebarW),
152
310
  recentFolders: sanitizeRecents(state.recentFolders),
311
+ lastSpawns: sanitizeSpawns(state.lastSpawns),
312
+ customCommands: sanitizeStringList(state.customCommands, CUSTOM_COMMANDS_CAP),
313
+ paletteUsage: sanitizePaletteUsage(state.paletteUsage),
314
+ filesShowHidden: state.filesShowHidden === true,
315
+ filesShowIgnored: state.filesShowIgnored === true,
153
316
  };
154
317
  return JSON.stringify(clean, null, 2);
155
318
  }