tmux-ide 2.6.1 → 2.7.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 (83) hide show
  1. package/README.md +14 -9
  2. package/bin/cli.js +1013 -516
  3. package/bin/cli.ts +62 -5
  4. package/package.json +9 -6
  5. package/packages/daemon/dist/agent-explain.d.ts +8 -1
  6. package/packages/daemon/dist/agent-explain.js +19 -3
  7. package/packages/daemon/dist/lib/tui-binary.d.ts +57 -0
  8. package/packages/daemon/dist/lib/tui-binary.js +130 -0
  9. package/packages/daemon/dist/widgets/explorer/breadcrumbs.d.ts +1 -1
  10. package/packages/daemon/dist/widgets/explorer/footer.d.ts +1 -1
  11. package/packages/daemon/dist/widgets/explorer/tree.d.ts +1 -1
  12. package/packages/daemon/dist/widgets/lib/help-overlay.d.ts +1 -1
  13. package/packages/daemon/dist/widgets/setup/agent-naming.d.ts +1 -1
  14. package/packages/daemon/dist/widgets/setup/config-tree.d.ts +1 -1
  15. package/packages/daemon/dist/widgets/setup/detect-panel.d.ts +1 -1
  16. package/packages/daemon/dist/widgets/setup/field-editor.d.ts +1 -1
  17. package/packages/daemon/dist/widgets/setup/footer.d.ts +1 -1
  18. package/packages/daemon/dist/widgets/setup/layout-picker.d.ts +1 -1
  19. package/packages/daemon/src/agent-explain.ts +34 -6
  20. package/packages/daemon/src/lib/app-config.ts +93 -2
  21. package/packages/daemon/src/lib/tui-binary.ts +165 -0
  22. package/packages/daemon/src/tui/chrome/notify.ts +221 -34
  23. package/packages/daemon/src/tui/chrome/updater.ts +89 -7
  24. package/packages/daemon/src/tui/compiled.ts +11 -3
  25. package/packages/daemon/src/tui/detect/classify.ts +15 -0
  26. package/packages/daemon/src/tui/detect/manifest-loader.ts +2 -1
  27. package/packages/daemon/src/tui/detect/manifest.ts +15 -0
  28. package/packages/daemon/src/tui/detect/manifests.ts +161 -33
  29. package/packages/daemon/src/tui/detect/process-tree.ts +20 -0
  30. package/packages/daemon/src/tui/main.ts +13 -1
  31. package/packages/daemon/src/tui/mirror/ack-writer.ts +77 -0
  32. package/packages/daemon/src/tui/mirror/agent-chip.ts +97 -0
  33. package/packages/daemon/src/tui/mirror/agent-rows.ts +133 -0
  34. package/packages/daemon/src/tui/mirror/app-state.ts +179 -0
  35. package/packages/daemon/src/tui/mirror/app.tsx +5265 -0
  36. package/packages/daemon/src/tui/mirror/blit.ts +186 -0
  37. package/packages/daemon/src/tui/mirror/control-client.ts +80 -9
  38. package/packages/daemon/src/tui/mirror/dialog-model.ts +298 -0
  39. package/packages/daemon/src/tui/mirror/dialog-stack.ts +354 -0
  40. package/packages/daemon/src/tui/mirror/diff-model.ts +112 -0
  41. package/packages/daemon/src/tui/mirror/editor-buffer.ts +117 -0
  42. package/packages/daemon/src/tui/mirror/file-tree.ts +97 -0
  43. package/packages/daemon/src/tui/mirror/focus-border.ts +57 -0
  44. package/packages/daemon/src/tui/mirror/folder-picker.ts +124 -0
  45. package/packages/daemon/src/tui/mirror/home-model.ts +174 -0
  46. package/packages/daemon/src/tui/mirror/input-coalescer.ts +105 -0
  47. package/packages/daemon/src/tui/mirror/menu-model.ts +187 -0
  48. package/packages/daemon/src/tui/mirror/palette.ts +274 -0
  49. package/packages/daemon/src/tui/mirror/pane-mirror.ts +499 -19
  50. package/packages/daemon/src/tui/mirror/pane-surface.tsx +415 -0
  51. package/packages/daemon/src/tui/mirror/perf-tap.ts +160 -0
  52. package/packages/daemon/src/tui/mirror/resize-model.ts +85 -0
  53. package/packages/daemon/src/tui/mirror/scrollbar-model.ts +88 -0
  54. package/packages/daemon/src/tui/mirror/search-model.ts +70 -0
  55. package/packages/daemon/src/tui/mirror/selection.ts +262 -0
  56. package/packages/daemon/src/tui/mirror/session-mirror.ts +443 -0
  57. package/packages/daemon/src/tui/mirror/settings-model.ts +345 -0
  58. package/packages/daemon/src/tui/mirror/size-truth.ts +77 -0
  59. package/packages/daemon/src/tui/mirror/spans.ts +46 -0
  60. package/packages/daemon/src/tui/mirror/status-grammar.ts +32 -0
  61. package/packages/daemon/src/tui/team/entry.ts +34 -7
  62. package/packages/daemon/src/tui/team/report.ts +11 -1
  63. package/packages/daemon/src/tui/team/sessions.ts +102 -13
  64. package/scripts/build-tui.mjs +11 -4
  65. package/scripts/perf-mirror.mjs +313 -0
  66. package/skill/SKILL.md +22 -0
  67. package/templates/AGENTS.md +14 -7
  68. package/templates/agent-team-monorepo.yml +8 -0
  69. package/templates/agent-team-nextjs.yml +8 -0
  70. package/templates/agent-team.yml +10 -0
  71. package/templates/convex.yml +2 -0
  72. package/templates/default.yml +11 -5
  73. package/templates/go.yml +4 -0
  74. package/templates/missions.yml +6 -0
  75. package/templates/nextjs.yml +4 -0
  76. package/templates/python.yml +4 -0
  77. package/templates/skills/backend.md +5 -12
  78. package/templates/skills/frontend.md +5 -12
  79. package/templates/skills/general-worker.md +5 -12
  80. package/templates/skills/researcher.md +7 -12
  81. package/templates/skills/reviewer.md +7 -16
  82. package/templates/vite.yml +4 -0
  83. package/packages/daemon/src/tui/mirror/viewer.tsx +0 -166
@@ -0,0 +1,274 @@
1
+ /**
2
+ * The command palette's action model (M18.4) — PURE so it unit-tests without
3
+ * OpenTUI. app.tsx opens a centered overlay (F5), feeds keystrokes to a query,
4
+ * and runs the selected {@link PaletteAction}; the DESCRIPTORS and the fuzzy
5
+ * filtering live here, the closures that actually attach/save/quit stay in the
6
+ * app (a `dispatch` switch over `action.kind`).
7
+ *
8
+ * Reuses {@link ../team/fuzzy.ts} for the same subsequence match the fleet
9
+ * quick-jump uses, so typing "ses" ranks "Attach session: …" the way a human
10
+ * expects. The dynamic "Open file: <query>" action is always offered when the
11
+ * query is non-empty (you can't fuzzy-match a path you're still typing), pinned
12
+ * to the top when the query looks like a path.
13
+ */
14
+ import { fuzzyFilter } from "../team/fuzzy.ts";
15
+ import type { Tab } from "./app-state.ts";
16
+ import type { AgentRowInput } from "./agent-rows.ts";
17
+ import { SETTINGS_PALETTE_COMMANDS, type SettingsCommandId } from "./settings-model.ts";
18
+
19
+ /** One runnable palette entry. `label` is what the list shows and what the
20
+ * fuzzy filter scores; `kind` (+ payload) is what the app dispatches on. */
21
+ export type PaletteAction =
22
+ | { kind: "tab"; tab: Tab; label: string }
23
+ | { kind: "open-folder"; label: string }
24
+ | { kind: "attach"; session: string; label: string }
25
+ | { kind: "jump-agent"; paneId: string; session: string; windowIndex: number; label: string }
26
+ | { kind: "open-file"; path: string; label: string }
27
+ | { kind: "save"; label: string }
28
+ | { kind: "refresh-diff"; label: string }
29
+ | { kind: "paste-buffer"; label: string }
30
+ | { kind: "new-window"; label: string }
31
+ | { kind: "rename-window"; name: string; label: string }
32
+ | { kind: "kill-window"; label: string }
33
+ | { kind: "zoom-pane"; label: string }
34
+ | { kind: "swap-pane"; label: string }
35
+ | { kind: "break-pane"; label: string }
36
+ | { kind: "rotate-window"; label: string }
37
+ | { kind: "select-layout"; layout: string; label: string }
38
+ | { kind: "sync-toggle"; label: string }
39
+ | { kind: "select-text"; label: string }
40
+ | { kind: "resize-window"; label: string }
41
+ | { kind: "settings"; id: SettingsCommandId; label: string }
42
+ | { kind: "quit"; label: string };
43
+
44
+ /** The five tmux `select-layout` presets, offered one palette action each so the
45
+ * fuzzy filter finds "tiled" / "main-vertical" directly. Shared with the pane
46
+ * context menu's Layouts submenu (menu-model). */
47
+ export const LAYOUT_PRESETS = [
48
+ "even-horizontal",
49
+ "even-vertical",
50
+ "main-horizontal",
51
+ "main-vertical",
52
+ "tiled",
53
+ ] as const;
54
+
55
+ /** Context the palette needs beyond the fuzzy query. `terminal` gates the
56
+ * window/pane verbs (New/Rename/Kill window, Zoom pane) to the Terminal surface
57
+ * — they are no-ops on Home/Files/Diff, so they only clutter the list there. */
58
+ export interface PaletteContext {
59
+ terminal?: boolean;
60
+ /** The fleet's agents (M22.2) — the palette offers one "Agent: …" jump action
61
+ * each, the keyboard twin of the sidebar's clickable agent rows. Already
62
+ * sorted attention-first by the caller (fleetAgents). */
63
+ agents?: AgentRowInput[];
64
+ /** A co-attached terminal has sized the tmux window away from our canvas
65
+ * (M22.8). Only then is the "Resize to fit this window" reclaim action worth
66
+ * offering — otherwise it is a no-op that clutters the list. */
67
+ sizeMismatch?: boolean;
68
+ /** The focused pane's app turned mouse reporting on (M22.9) — only then is
69
+ * "Select text in pane" offered (ordinary panes drag-select directly, so the
70
+ * action would be a no-op that clutters the list). */
71
+ appMousePane?: boolean;
72
+ }
73
+
74
+ const TAB_LABELS: { tab: Tab; label: string }[] = [
75
+ { tab: "home", label: "Switch tab: Home" },
76
+ { tab: "terminal", label: "Switch tab: Terminal" },
77
+ { tab: "files", label: "Switch tab: Files" },
78
+ { tab: "diff", label: "Switch tab: Diff" },
79
+ ];
80
+
81
+ /** PURE — the always-available static actions: the four tab switches, one
82
+ * attach-session per fleet session, then Save / Refresh diff, the Terminal-only
83
+ * window/pane verbs, and Quit. */
84
+ export function staticPaletteActions(
85
+ sessions: string[],
86
+ ctx: PaletteContext = {},
87
+ ): PaletteAction[] {
88
+ const actions: PaletteAction[] = TAB_LABELS.map((t) => ({
89
+ kind: "tab" as const,
90
+ tab: t.tab,
91
+ label: t.label,
92
+ }));
93
+ // The non-technicals' front door (M22.5): a filesystem picker → create-or-
94
+ // attach a session in the chosen folder. Offered on every surface.
95
+ actions.push({ kind: "open-folder", label: "Open folder…" });
96
+ for (const s of sessions) {
97
+ actions.push({ kind: "attach", session: s, label: `Attach session: ${s}` });
98
+ }
99
+ // One jump per fleet agent (M22.2) — the sidebar rows' keyboard twin. Labeled
100
+ // "Agent: <kind> · <session> (<state>)" so the fuzzy filter finds it by kind,
101
+ // session, or state (typing "agent", "claude", or "blocked" all narrow to it).
102
+ for (const a of ctx.agents ?? []) {
103
+ actions.push({
104
+ kind: "jump-agent",
105
+ paneId: a.paneId,
106
+ session: a.session,
107
+ windowIndex: a.windowIndex,
108
+ label: `Agent: ${a.kind} · ${a.session} (${a.state})`,
109
+ });
110
+ }
111
+ actions.push({ kind: "save", label: "Save file" });
112
+ actions.push({ kind: "refresh-diff", label: "Refresh diff" });
113
+ // Paste target is the focused surface (a pane, or the editor buffer), so this
114
+ // is offered on every surface — selecting it opens a second-level list of the
115
+ // tmux paste buffers rather than dispatching directly.
116
+ actions.push({ kind: "paste-buffer", label: "Paste buffer…" });
117
+ // The SETTINGS category (M22.4) — every setting is a command; "Settings…" is
118
+ // the categorized umbrella over the same dialogs. Offered on every surface.
119
+ for (const c of SETTINGS_PALETTE_COMMANDS) {
120
+ actions.push({ kind: "settings", id: c.id, label: c.label });
121
+ }
122
+ if (ctx.terminal) {
123
+ actions.push({ kind: "new-window", label: "New window" });
124
+ actions.push({ kind: "kill-window", label: "Kill window" });
125
+ actions.push({ kind: "zoom-pane", label: "Zoom pane" });
126
+ actions.push({ kind: "swap-pane", label: "Swap pane with next" });
127
+ actions.push({ kind: "break-pane", label: "Break pane to window" });
128
+ actions.push({ kind: "rotate-window", label: "Rotate panes" });
129
+ actions.push({ kind: "sync-toggle", label: "Synchronize panes (toggle)" });
130
+ // Selection entry on app-mouse panes (M22.9) — the pane menu verb's
131
+ // keyboard twin, offered only where forwarding blocks a direct drag.
132
+ if (ctx.appMousePane) {
133
+ actions.push({ kind: "select-text", label: "Select text in pane" });
134
+ }
135
+ for (const layout of LAYOUT_PRESETS) {
136
+ actions.push({ kind: "select-layout", layout, label: `Layout: ${layout}` });
137
+ }
138
+ // Only when a co-attached terminal is dictating the window size — the reclaim
139
+ // is a no-op otherwise, and we never fight the other terminal unasked.
140
+ if (ctx.sizeMismatch) {
141
+ actions.push({ kind: "resize-window", label: "Resize to fit this window" });
142
+ }
143
+ }
144
+ actions.push({ kind: "quit", label: "Quit" });
145
+ return actions;
146
+ }
147
+
148
+ /** One tmux paste buffer for the picker: its `name` (e.g. `buffer0`) and a
149
+ * short, control-char-sanitized `preview` of its content. */
150
+ export interface TmuxBuffer {
151
+ name: string;
152
+ preview: string;
153
+ }
154
+
155
+ /**
156
+ * PURE — parse `list-buffers -F "#{buffer_name}\t#{buffer_sample}"` reply lines
157
+ * into pickable buffers. The sample is truncated to `previewLen` chars and any
158
+ * control characters (tabs/newlines that survived a sample, C0 bytes) collapse
159
+ * to a middle-dot so one buffer stays one clean row. Nameless/blank lines drop.
160
+ */
161
+ export function parseBufferList(lines: readonly string[], previewLen = 40): TmuxBuffer[] {
162
+ const out: TmuxBuffer[] = [];
163
+ for (const raw of lines) {
164
+ if (!raw) continue;
165
+ const tab = raw.indexOf("\t");
166
+ const name = (tab === -1 ? raw : raw.slice(0, tab)).trim();
167
+ if (!name) continue;
168
+ const sample = tab === -1 ? "" : raw.slice(tab + 1);
169
+ const preview = sample
170
+ .slice(0, previewLen)
171
+ // eslint-disable-next-line no-control-regex
172
+ .replace(/[\x00-\x1f\x7f]/g, "·");
173
+ out.push({ name, preview });
174
+ }
175
+ return out;
176
+ }
177
+
178
+ // ── Overlay geometry (M21.9 — palette mouse support) ────────────────────────
179
+ // The palette overlay is app-rendered (a centered <box border paddingLeft=1>),
180
+ // so its full geometry is knowable by pure math. These helpers are shared by
181
+ // the RENDER (placement) and the central mouse ROUTER (row hit-test / inside-
182
+ // vs-outside), so a click can never land where a row isn't drawn. Layout, in
183
+ // screen rows from `top`: border (top+0) · input/header line (top+1) · rule
184
+ // (top+2) · result rows (top+3 …) · an optional "no matches" row when empty ·
185
+ // border. Rows span the box interior in x: [left+1, left+width-1).
186
+
187
+ /** Rows of chrome above the first result row: top border + input line + rule. */
188
+ export const PALETTE_HEADER_ROWS = 3;
189
+
190
+ /** The palette box geometry the router hit-tests: the placed box plus how many
191
+ * result rows are currently visible (min(count - top, pageRows)). */
192
+ export interface PaletteGeom {
193
+ left: number;
194
+ top: number;
195
+ width: number;
196
+ /** Result rows on screen right now (0 when the list is empty). */
197
+ visibleRows: number;
198
+ }
199
+
200
+ /** PURE — the overlay's placement for a terminal size: horizontally centered,
201
+ * top at a sixth of the height (min 1 — below the surface tab bar). MUST match
202
+ * the render's `left`/`top` props (the render calls this). */
203
+ export function palettePos(
204
+ termW: number,
205
+ termH: number,
206
+ width: number,
207
+ ): { left: number; top: number } {
208
+ return {
209
+ left: Math.max(0, Math.floor((termW - width) / 2)),
210
+ top: Math.max(1, Math.floor(termH / 6)),
211
+ };
212
+ }
213
+
214
+ /** PURE — total box height for `visibleRows` results: header chrome + the rows
215
+ * (an empty list still shows one "no matches"/"no buffers" row) + the bottom
216
+ * border. Used for inside/outside containment. */
217
+ export function paletteHeight(visibleRows: number): number {
218
+ return PALETTE_HEADER_ROWS + Math.max(1, visibleRows) + 1;
219
+ }
220
+
221
+ /** PURE — the VISIBLE result-row index under (x, y), or -1. Only real rows hit
222
+ * (the "no matches" placeholder row is not clickable); x must be inside the
223
+ * box interior (borders excluded). */
224
+ export function paletteRowAt(g: PaletteGeom, x: number, y: number): number {
225
+ if (x < g.left + 1 || x >= g.left + g.width - 1) return -1;
226
+ const row = y - (g.top + PALETTE_HEADER_ROWS);
227
+ return row >= 0 && row < g.visibleRows ? row : -1;
228
+ }
229
+
230
+ /** PURE — whether (x, y) falls anywhere on the palette box (border included).
231
+ * A press outside dismisses the overlay; inside-but-not-a-row is a no-op. */
232
+ export function paletteContains(g: PaletteGeom, x: number, y: number): boolean {
233
+ return (
234
+ x >= g.left && x < g.left + g.width && y >= g.top && y < g.top + paletteHeight(g.visibleRows)
235
+ );
236
+ }
237
+
238
+ /** PURE — clamp a wheel-scrolled list top into [0, count - pageRows]. */
239
+ export function clampPaletteTop(top: number, count: number, pageRows: number): number {
240
+ return Math.max(0, Math.min(top, count - pageRows));
241
+ }
242
+
243
+ /** A typed query "looks like a path" when it carries a separator or dot — then
244
+ * the Open-file action is worth pinning to the top of the results. */
245
+ function looksLikePath(q: string): boolean {
246
+ return /[/.~]/.test(q);
247
+ }
248
+
249
+ /**
250
+ * PURE — the palette result list for `query`: the static actions fuzzy-filtered
251
+ * and score-sorted, plus (when the query is non-empty) a dynamic
252
+ * "Open file: <query>" action. That open-file entry is pinned FIRST when the
253
+ * query looks like a path, otherwise appended LAST so it never buries a real
254
+ * match. An empty query returns every static action in natural order.
255
+ */
256
+ export function filterPaletteActions(
257
+ query: string,
258
+ sessions: string[],
259
+ ctx: PaletteContext = {},
260
+ ): PaletteAction[] {
261
+ const statics = staticPaletteActions(sessions, ctx);
262
+ const q = query.trim();
263
+ const matched =
264
+ q.length === 0 ? statics : fuzzyFilter(query, statics, (a) => a.label).map((m) => m.item);
265
+ if (q.length === 0) return matched;
266
+ const dynamic: PaletteAction[] = [{ kind: "open-file", path: q, label: `Open file: ${q}` }];
267
+ // On the Terminal surface a non-empty query also offers a rename-to-<query>
268
+ // window verb (there is no way to fuzzy-match a name you are still typing —
269
+ // same reasoning as the open-file entry).
270
+ if (ctx.terminal) {
271
+ dynamic.push({ kind: "rename-window", name: q, label: `Rename window: ${q}` });
272
+ }
273
+ return looksLikePath(q) ? [...dynamic, ...matched] : [...matched, ...dynamic];
274
+ }