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.
- package/README.md +14 -9
- package/bin/cli.js +1013 -516
- package/bin/cli.ts +62 -5
- package/package.json +9 -6
- package/packages/daemon/dist/agent-explain.d.ts +8 -1
- package/packages/daemon/dist/agent-explain.js +19 -3
- package/packages/daemon/dist/lib/tui-binary.d.ts +57 -0
- package/packages/daemon/dist/lib/tui-binary.js +130 -0
- package/packages/daemon/dist/widgets/explorer/breadcrumbs.d.ts +1 -1
- package/packages/daemon/dist/widgets/explorer/footer.d.ts +1 -1
- package/packages/daemon/dist/widgets/explorer/tree.d.ts +1 -1
- package/packages/daemon/dist/widgets/lib/help-overlay.d.ts +1 -1
- package/packages/daemon/dist/widgets/setup/agent-naming.d.ts +1 -1
- package/packages/daemon/dist/widgets/setup/config-tree.d.ts +1 -1
- package/packages/daemon/dist/widgets/setup/detect-panel.d.ts +1 -1
- package/packages/daemon/dist/widgets/setup/field-editor.d.ts +1 -1
- package/packages/daemon/dist/widgets/setup/footer.d.ts +1 -1
- package/packages/daemon/dist/widgets/setup/layout-picker.d.ts +1 -1
- package/packages/daemon/src/agent-explain.ts +34 -6
- package/packages/daemon/src/lib/app-config.ts +93 -2
- package/packages/daemon/src/lib/tui-binary.ts +165 -0
- package/packages/daemon/src/tui/chrome/notify.ts +221 -34
- package/packages/daemon/src/tui/chrome/updater.ts +89 -7
- package/packages/daemon/src/tui/compiled.ts +11 -3
- package/packages/daemon/src/tui/detect/classify.ts +15 -0
- package/packages/daemon/src/tui/detect/manifest-loader.ts +2 -1
- package/packages/daemon/src/tui/detect/manifest.ts +15 -0
- package/packages/daemon/src/tui/detect/manifests.ts +161 -33
- package/packages/daemon/src/tui/detect/process-tree.ts +20 -0
- package/packages/daemon/src/tui/main.ts +13 -1
- package/packages/daemon/src/tui/mirror/ack-writer.ts +77 -0
- package/packages/daemon/src/tui/mirror/agent-chip.ts +97 -0
- package/packages/daemon/src/tui/mirror/agent-rows.ts +133 -0
- package/packages/daemon/src/tui/mirror/app-state.ts +179 -0
- package/packages/daemon/src/tui/mirror/app.tsx +5265 -0
- package/packages/daemon/src/tui/mirror/blit.ts +186 -0
- package/packages/daemon/src/tui/mirror/control-client.ts +80 -9
- package/packages/daemon/src/tui/mirror/dialog-model.ts +298 -0
- package/packages/daemon/src/tui/mirror/dialog-stack.ts +354 -0
- package/packages/daemon/src/tui/mirror/diff-model.ts +112 -0
- package/packages/daemon/src/tui/mirror/editor-buffer.ts +117 -0
- package/packages/daemon/src/tui/mirror/file-tree.ts +97 -0
- package/packages/daemon/src/tui/mirror/focus-border.ts +57 -0
- package/packages/daemon/src/tui/mirror/folder-picker.ts +124 -0
- package/packages/daemon/src/tui/mirror/home-model.ts +174 -0
- package/packages/daemon/src/tui/mirror/input-coalescer.ts +105 -0
- package/packages/daemon/src/tui/mirror/menu-model.ts +187 -0
- package/packages/daemon/src/tui/mirror/palette.ts +274 -0
- package/packages/daemon/src/tui/mirror/pane-mirror.ts +499 -19
- package/packages/daemon/src/tui/mirror/pane-surface.tsx +415 -0
- package/packages/daemon/src/tui/mirror/perf-tap.ts +160 -0
- package/packages/daemon/src/tui/mirror/resize-model.ts +85 -0
- package/packages/daemon/src/tui/mirror/scrollbar-model.ts +88 -0
- package/packages/daemon/src/tui/mirror/search-model.ts +70 -0
- package/packages/daemon/src/tui/mirror/selection.ts +262 -0
- package/packages/daemon/src/tui/mirror/session-mirror.ts +443 -0
- package/packages/daemon/src/tui/mirror/settings-model.ts +345 -0
- package/packages/daemon/src/tui/mirror/size-truth.ts +77 -0
- package/packages/daemon/src/tui/mirror/spans.ts +46 -0
- package/packages/daemon/src/tui/mirror/status-grammar.ts +32 -0
- package/packages/daemon/src/tui/team/entry.ts +34 -7
- package/packages/daemon/src/tui/team/report.ts +11 -1
- package/packages/daemon/src/tui/team/sessions.ts +102 -13
- package/scripts/build-tui.mjs +11 -4
- package/scripts/perf-mirror.mjs +313 -0
- package/skill/SKILL.md +22 -0
- package/templates/AGENTS.md +14 -7
- package/templates/agent-team-monorepo.yml +8 -0
- package/templates/agent-team-nextjs.yml +8 -0
- package/templates/agent-team.yml +10 -0
- package/templates/convex.yml +2 -0
- package/templates/default.yml +11 -5
- package/templates/go.yml +4 -0
- package/templates/missions.yml +6 -0
- package/templates/nextjs.yml +4 -0
- package/templates/python.yml +4 -0
- package/templates/skills/backend.md +5 -12
- package/templates/skills/frontend.md +5 -12
- package/templates/skills/general-worker.md +5 -12
- package/templates/skills/researcher.md +7 -12
- package/templates/skills/reviewer.md +7 -16
- package/templates/vite.yml +4 -0
- package/packages/daemon/src/tui/mirror/viewer.tsx +0 -166
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings as COMMANDS (M22.4) — the PURE model behind the app's settings
|
|
3
|
+
* surface. There is NO settings screen: every setting is a palette command
|
|
4
|
+
* executed through the dialog primitives ({@link ./dialog-model.ts} /
|
|
5
|
+
* {@link ./dialog-stack.ts}); this module builds the dialog ITEM LISTS from the
|
|
6
|
+
* real typed config ({@link ../../lib/app-config.ts}) and the real notification
|
|
7
|
+
* prefs ({@link ../chrome/notify.ts}), plus the config PATCHES each choice
|
|
8
|
+
* persists. app.tsx owns the async flows (sequential awaits over the one-shots).
|
|
9
|
+
*
|
|
10
|
+
* Copy is plain language throughout; each dialog's footer says WHERE a change
|
|
11
|
+
* lands ("takes effect immediately" vs "after re-adopt") so non-technicals are
|
|
12
|
+
* never left guessing.
|
|
13
|
+
*/
|
|
14
|
+
import type { AppConfig, AppConfigPatch, AppKeys } from "../../lib/app-config.ts";
|
|
15
|
+
import { parseHHMM, type NotificationPrefs } from "../chrome/notify.ts";
|
|
16
|
+
import type { DialogSelectItem } from "./dialog-model.ts";
|
|
17
|
+
|
|
18
|
+
// ── Command registry (what the palette offers) ───────────────────────────────
|
|
19
|
+
|
|
20
|
+
export type SettingsCommandId =
|
|
21
|
+
| "settings"
|
|
22
|
+
| "settings-theme"
|
|
23
|
+
| "settings-notifications"
|
|
24
|
+
| "settings-quiet-hours"
|
|
25
|
+
| "settings-updates"
|
|
26
|
+
| "settings-restore"
|
|
27
|
+
| "settings-keys"
|
|
28
|
+
| "settings-reset";
|
|
29
|
+
|
|
30
|
+
/** The palette entries, one per settings command (+ the umbrella first). The
|
|
31
|
+
* "Settings" category prefix is how the palette's flat fuzzy list reads as a
|
|
32
|
+
* category — typing "set" narrows to all of them. */
|
|
33
|
+
export const SETTINGS_PALETTE_COMMANDS: ReadonlyArray<{
|
|
34
|
+
id: SettingsCommandId;
|
|
35
|
+
label: string;
|
|
36
|
+
}> = [
|
|
37
|
+
{ id: "settings", label: "Settings…" },
|
|
38
|
+
{ id: "settings-theme", label: "Settings: Accent color" },
|
|
39
|
+
{ id: "settings-notifications", label: "Settings: Notifications" },
|
|
40
|
+
{ id: "settings-quiet-hours", label: "Settings: Quiet hours" },
|
|
41
|
+
{ id: "settings-updates", label: "Settings: Updates & background refresh" },
|
|
42
|
+
{ id: "settings-restore", label: "Settings: Crash restore" },
|
|
43
|
+
{ id: "settings-keys", label: "Settings: Keyboard shortcuts (view)" },
|
|
44
|
+
{ id: "settings-reset", label: "Settings: Reset to defaults" },
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
// ── Where changes land (footer copy) ─────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
/** Notifications are read fresh per event — honest "immediately". */
|
|
50
|
+
export const HINT_LIVE = "takes effect immediately";
|
|
51
|
+
/** The tmux chrome + widget panels read the theme when they (re)build. */
|
|
52
|
+
export const HINT_READOPT = "applies after re-adopt — run: tmux-ide adopt <session>";
|
|
53
|
+
/** The background chrome loads its config at start. */
|
|
54
|
+
export const HINT_CHROME_RESTART = "applies when the background chrome restarts";
|
|
55
|
+
|
|
56
|
+
// ── Theme presets ────────────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
export interface ThemePreset {
|
|
59
|
+
/** The `theme.accent` value persisted (a tmux `colourN` token). */
|
|
60
|
+
accent: string;
|
|
61
|
+
/** Plain name shown in the picker. */
|
|
62
|
+
label: string;
|
|
63
|
+
/** The same color as RGB for the in-dialog swatch / live preview. */
|
|
64
|
+
rgb: [number, number, number];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Curated accents (default first). RGB values are the xterm-256 values of the
|
|
68
|
+
* `colourN` tokens, so the in-app preview shows exactly what tmux will. */
|
|
69
|
+
export const THEME_PRESETS: readonly ThemePreset[] = [
|
|
70
|
+
{ accent: "colour75", label: "Sky blue (default)", rgb: [95, 175, 255] },
|
|
71
|
+
{ accent: "colour114", label: "Soft green", rgb: [135, 215, 135] },
|
|
72
|
+
{ accent: "colour80", label: "Aqua", rgb: [95, 215, 215] },
|
|
73
|
+
{ accent: "colour111", label: "Periwinkle", rgb: [135, 175, 255] },
|
|
74
|
+
{ accent: "colour183", label: "Lavender", rgb: [215, 175, 255] },
|
|
75
|
+
{ accent: "colour216", label: "Peach", rgb: [255, 175, 135] },
|
|
76
|
+
{ accent: "colour221", label: "Amber", rgb: [255, 215, 95] },
|
|
77
|
+
{ accent: "colour203", label: "Coral", rgb: [255, 95, 95] },
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
/** PURE — the theme picker rows: every preset (● on the saved accent). A saved
|
|
81
|
+
* accent that is not a preset (hand-edited config) keeps its place as a
|
|
82
|
+
* "Custom" first row so the picker never lies about the current value. */
|
|
83
|
+
export function themeItems(cfg: AppConfig): DialogSelectItem[] {
|
|
84
|
+
const current = cfg.theme.accent;
|
|
85
|
+
const rows: DialogSelectItem[] = THEME_PRESETS.map((p) => ({
|
|
86
|
+
id: p.accent,
|
|
87
|
+
label: p.label,
|
|
88
|
+
detail: p.accent,
|
|
89
|
+
current: p.accent === current,
|
|
90
|
+
swatch: p.rgb,
|
|
91
|
+
}));
|
|
92
|
+
if (!THEME_PRESETS.some((p) => p.accent === current)) {
|
|
93
|
+
rows.unshift({ id: current, label: "Custom", detail: current, current: true });
|
|
94
|
+
}
|
|
95
|
+
return rows;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** PURE — the RGB swatch for an accent value, when it is a known preset. */
|
|
99
|
+
export function presetRgb(accent: string): [number, number, number] | null {
|
|
100
|
+
return THEME_PRESETS.find((p) => p.accent === accent)?.rgb ?? null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** PURE — the patch persisting a picked accent. */
|
|
104
|
+
export function themePatch(accent: string): AppConfigPatch {
|
|
105
|
+
return { theme: { accent } };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ── Notifications ────────────────────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
export type NotificationToggleId = "enabled" | "toast" | "macos" | "onBlocked" | "onDone";
|
|
111
|
+
|
|
112
|
+
const onOff = (v: boolean) => (v ? "on" : "off");
|
|
113
|
+
|
|
114
|
+
/** PURE — the notification toggle rows (the REAL fields notify.ts reads: the
|
|
115
|
+
* typed toast/macos plus the polish-era raw fields), and a quiet-hours row
|
|
116
|
+
* that descends into its own flow. Enter toggles in place. */
|
|
117
|
+
export function notificationItems(prefs: NotificationPrefs): DialogSelectItem[] {
|
|
118
|
+
return [
|
|
119
|
+
{ id: "enabled", label: "All notifications", detail: onOff(prefs.enabled) },
|
|
120
|
+
{ id: "toast", label: "In-terminal toasts", detail: onOff(prefs.toast) },
|
|
121
|
+
{ id: "macos", label: "macOS banners", detail: onOff(prefs.macos) },
|
|
122
|
+
{ id: "onBlocked", label: "Alert when an agent needs you", detail: onOff(prefs.onBlocked) },
|
|
123
|
+
{ id: "onDone", label: "Alert when an agent finishes", detail: onOff(prefs.onDone) },
|
|
124
|
+
{ id: "quietHours", label: "Quiet hours…", detail: quietHoursSummary(prefs) },
|
|
125
|
+
];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** PURE — flip one notification toggle. */
|
|
129
|
+
export function notificationTogglePatch(
|
|
130
|
+
id: NotificationToggleId,
|
|
131
|
+
prefs: NotificationPrefs,
|
|
132
|
+
): AppConfigPatch {
|
|
133
|
+
return { notifications: { [id]: !prefs[id] } };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** PURE — "22:00–08:00" or "off". */
|
|
137
|
+
export function quietHoursSummary(prefs: NotificationPrefs): string {
|
|
138
|
+
return prefs.quietHours ? `${prefs.quietHours.start}–${prefs.quietHours.end}` : "off";
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** PURE — the quiet-hours chooser rows. */
|
|
142
|
+
export function quietHoursItems(prefs: NotificationPrefs): DialogSelectItem[] {
|
|
143
|
+
return [
|
|
144
|
+
{ id: "off", label: "Off — always notify", current: prefs.quietHours === null },
|
|
145
|
+
{
|
|
146
|
+
id: "window",
|
|
147
|
+
label: "Silence banners during a daily window…",
|
|
148
|
+
detail: prefs.quietHours ? quietHoursSummary(prefs) : undefined,
|
|
149
|
+
current: prefs.quietHours !== null,
|
|
150
|
+
},
|
|
151
|
+
];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** PURE — HH:MM validation with a plain error. */
|
|
155
|
+
export function validateQuietTime(value: string): string | null {
|
|
156
|
+
return parseHHMM(value.trim()) === null ? "Use 24-hour HH:MM — for example 22:00" : null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** PURE — persist a quiet window (times already validated). */
|
|
160
|
+
export function quietHoursPatch(start: string, end: string): AppConfigPatch {
|
|
161
|
+
return { notifications: { quietHours: { start: start.trim(), end: end.trim() } } };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** PURE — remove the quiet window. */
|
|
165
|
+
export function quietHoursOffPatch(): AppConfigPatch {
|
|
166
|
+
return { notifications: { quietHours: undefined } };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ── Updates & updater cadence ────────────────────────────────────────────────
|
|
170
|
+
|
|
171
|
+
/** PURE — the updates dialog rows: the check toggle + the two cadence numbers. */
|
|
172
|
+
export function updatesItems(cfg: AppConfig): DialogSelectItem[] {
|
|
173
|
+
return [
|
|
174
|
+
{ id: "check", label: "Check for tmux-ide updates", detail: onOff(cfg.updates.check) },
|
|
175
|
+
{
|
|
176
|
+
id: "tickMs",
|
|
177
|
+
label: "Background refresh interval…",
|
|
178
|
+
detail: `${cfg.updater.tickMs} ms`,
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
id: "snapshotEvery",
|
|
182
|
+
label: "Save a crash snapshot every…",
|
|
183
|
+
detail: `${cfg.updater.snapshotEvery} refreshes`,
|
|
184
|
+
},
|
|
185
|
+
];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function updatesCheckPatch(cfg: AppConfig): AppConfigPatch {
|
|
189
|
+
return { updates: { check: !cfg.updates.check } };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** PURE — 250–60000 ms, whole numbers (guards a config that would spin or
|
|
193
|
+
* starve the chrome). */
|
|
194
|
+
export function validateTickMs(value: string): string | null {
|
|
195
|
+
const n = Number(value.trim());
|
|
196
|
+
if (!Number.isInteger(n) || n < 250 || n > 60_000) {
|
|
197
|
+
return "Use a whole number of milliseconds between 250 and 60000";
|
|
198
|
+
}
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** PURE — 1–1000 refreshes. */
|
|
203
|
+
export function validateSnapshotEvery(value: string): string | null {
|
|
204
|
+
const n = Number(value.trim());
|
|
205
|
+
if (!Number.isInteger(n) || n < 1 || n > 1000) {
|
|
206
|
+
return "Use a whole number between 1 and 1000";
|
|
207
|
+
}
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function tickMsPatch(value: string): AppConfigPatch {
|
|
212
|
+
return { updater: { tickMs: Number(value.trim()) } };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function snapshotEveryPatch(value: string): AppConfigPatch {
|
|
216
|
+
return { updater: { snapshotEvery: Number(value.trim()) } };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ── Crash restore ────────────────────────────────────────────────────────────
|
|
220
|
+
|
|
221
|
+
/** PURE — the restore On/Off rows. */
|
|
222
|
+
export function restoreItems(cfg: AppConfig): DialogSelectItem[] {
|
|
223
|
+
return [
|
|
224
|
+
{
|
|
225
|
+
id: "on",
|
|
226
|
+
label: "Revive agents automatically",
|
|
227
|
+
detail: "restore resumes claude conversations",
|
|
228
|
+
current: cfg.restore.resumeAgents,
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
id: "off",
|
|
232
|
+
label: "Rebuild sessions only",
|
|
233
|
+
detail: "agents stay stopped",
|
|
234
|
+
current: !cfg.restore.resumeAgents,
|
|
235
|
+
},
|
|
236
|
+
];
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export function restorePatch(id: string): AppConfigPatch {
|
|
240
|
+
return { restore: { resumeAgents: id === "on" } };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ── Keyboard shortcut viewer (read-only) ─────────────────────────────────────
|
|
244
|
+
|
|
245
|
+
/** Mirrors {@link ../chrome/statusline.ts}'s prefix-twin derivation — the
|
|
246
|
+
* letters tmux binds by default (never clobbered) and the documented remaps.
|
|
247
|
+
* A drift test asserts agreement with `prefixKeyBinds` for the defaults. */
|
|
248
|
+
const PREFIX_TAKEN = new Set([..."cdfilmnopqrstwxz"]);
|
|
249
|
+
const PREFIX_REMAP: Record<string, string> = { "M-m": "u", "M-p": "j", "M-,": "v" };
|
|
250
|
+
|
|
251
|
+
/** PURE — the reliable `prefix <letter>` twin for an Alt key, or null when the
|
|
252
|
+
* letter is taken by a stock tmux bind and has no documented remap. */
|
|
253
|
+
export function prefixTwinFor(altKey: string): string | null {
|
|
254
|
+
const remapped = PREFIX_REMAP[altKey];
|
|
255
|
+
if (remapped) return remapped;
|
|
256
|
+
const letter = /^M-([a-z])$/.exec(altKey)?.[1];
|
|
257
|
+
if (!letter || PREFIX_TAKEN.has(letter)) return null;
|
|
258
|
+
return letter;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** PURE — the read-only shortcut rows: the chrome actions from the LIVE config
|
|
262
|
+
* (prefix-first, the Alt fast path second — the prefix form is the one that
|
|
263
|
+
* survives every keyboard protocol) and then the app's fixed keys. */
|
|
264
|
+
export function keybindingItems(keys: AppKeys): DialogSelectItem[] {
|
|
265
|
+
const chrome: Array<{ label: string; altKey: string }> = [
|
|
266
|
+
{ label: "Home cockpit", altKey: keys.home },
|
|
267
|
+
{ label: "Session switcher", altKey: keys.popup },
|
|
268
|
+
{ label: "Cheat sheet", altKey: keys.cheatsheet },
|
|
269
|
+
{ label: "Actions menu", altKey: keys.menu },
|
|
270
|
+
{ label: "Sidebar", altKey: keys.sidebar },
|
|
271
|
+
{ label: "Explorer panel", altKey: keys.panels.explorer },
|
|
272
|
+
{ label: "Git changes panel", altKey: keys.panels.changes },
|
|
273
|
+
{ label: "Config panel", altKey: keys.panels.config },
|
|
274
|
+
];
|
|
275
|
+
const rows: DialogSelectItem[] = chrome.map(({ label, altKey }) => {
|
|
276
|
+
const twin = prefixTwinFor(altKey);
|
|
277
|
+
return {
|
|
278
|
+
id: `chrome:${label}`,
|
|
279
|
+
label,
|
|
280
|
+
detail: twin ? `prefix ${twin} · ${altKey}` : altKey,
|
|
281
|
+
};
|
|
282
|
+
});
|
|
283
|
+
const app: Array<[string, string]> = [
|
|
284
|
+
["Command palette", "F5 · ^p"],
|
|
285
|
+
["Home / Terminal / Files / Diff tabs", "F1–F4"],
|
|
286
|
+
["Back to Home", "^g"],
|
|
287
|
+
["Toggle editor", "^e"],
|
|
288
|
+
["Detach from the app", "^q"],
|
|
289
|
+
];
|
|
290
|
+
for (const [label, detail] of app) rows.push({ id: `app:${label}`, label, detail });
|
|
291
|
+
return rows;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// ── The umbrella + reset ─────────────────────────────────────────────────────
|
|
295
|
+
|
|
296
|
+
/** PURE — the "Settings…" umbrella rows: one per command, categorized by a
|
|
297
|
+
* `Category · name` label (flat list, fuzzy-friendly), each showing its
|
|
298
|
+
* current value as the detail. Reset is the one destructive row. */
|
|
299
|
+
export function settingsRootItems(cfg: AppConfig, prefs: NotificationPrefs): DialogSelectItem[] {
|
|
300
|
+
const accent = cfg.theme.accent;
|
|
301
|
+
const preset = THEME_PRESETS.find((p) => p.accent === accent);
|
|
302
|
+
return [
|
|
303
|
+
{
|
|
304
|
+
id: "settings-theme",
|
|
305
|
+
label: "Appearance · Accent color",
|
|
306
|
+
detail: preset?.label.replace(" (default)", "") ?? accent,
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
id: "settings-notifications",
|
|
310
|
+
label: "Notifications · Alerts & channels",
|
|
311
|
+
detail: prefs.enabled ? "on" : "off",
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
id: "settings-quiet-hours",
|
|
315
|
+
label: "Notifications · Quiet hours",
|
|
316
|
+
detail: quietHoursSummary(prefs),
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
id: "settings-updates",
|
|
320
|
+
label: "Updates · Checks & refresh",
|
|
321
|
+
detail: cfg.updates.check ? "checking" : "off",
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
id: "settings-restore",
|
|
325
|
+
label: "Sessions · Crash restore",
|
|
326
|
+
detail: cfg.restore.resumeAgents ? "revives agents" : "sessions only",
|
|
327
|
+
},
|
|
328
|
+
{ id: "settings-keys", label: "Keyboard · Shortcuts", detail: "view" },
|
|
329
|
+
{ id: "settings-reset", label: "Reset · All settings to defaults", danger: true },
|
|
330
|
+
];
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** PURE — the reset patch: DELETE the blocks the settings surface manages so
|
|
334
|
+
* the parser's defaults take over. Key binds and unknown user fields are
|
|
335
|
+
* deliberately preserved — reset returns the LOOK and BEHAVIOR to stock
|
|
336
|
+
* without silently unbinding the user's keys. */
|
|
337
|
+
export function resetSettingsPatch(): AppConfigPatch {
|
|
338
|
+
return {
|
|
339
|
+
theme: undefined,
|
|
340
|
+
notifications: undefined,
|
|
341
|
+
updater: undefined,
|
|
342
|
+
updates: undefined,
|
|
343
|
+
restore: undefined,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Size truth (M22.8) — is tmux's window the size we pinned, or did a co-attached
|
|
3
|
+
* terminal shrink it out from under us?
|
|
4
|
+
*
|
|
5
|
+
* The mirror pins its virtual control client to the render area
|
|
6
|
+
* (`refresh-client -C`), so when we are the only (or the latest-active) client
|
|
7
|
+
* the window matches our canvas. But tmux's default `window-size latest` lets a
|
|
8
|
+
* second, smaller terminal attached to the same session dictate the window size
|
|
9
|
+
* — and our canvas then renders letterboxed. These PURE helpers detect that
|
|
10
|
+
* mismatch from the pane layout, center the grid inside the canvas, and format
|
|
11
|
+
* the quiet honest hint. No tmux, no render loop — unit-tested in isolation; the
|
|
12
|
+
* app reads the pane geometry and the pinned canvas size and wires the rest.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export interface Size {
|
|
16
|
+
cols: number;
|
|
17
|
+
rows: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** The minimal pane rectangle the bounding box needs (LivePane is assignable). */
|
|
21
|
+
export interface Rect {
|
|
22
|
+
left: number;
|
|
23
|
+
top: number;
|
|
24
|
+
width: number;
|
|
25
|
+
height: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* PURE — the effective window size implied by the pane layout: the bounding box
|
|
30
|
+
* of all pane rects. tmux tiles panes to fill the whole window (edge panes sit
|
|
31
|
+
* flush to the edges, borders live between them), so the maximum right edge
|
|
32
|
+
* (`left + width`) is the window width and the maximum bottom edge
|
|
33
|
+
* (`top + height`) is the window height. Null when there are no panes yet.
|
|
34
|
+
*/
|
|
35
|
+
export function effectiveWindowSize(panes: readonly Rect[]): Size | null {
|
|
36
|
+
if (panes.length === 0) return null;
|
|
37
|
+
let cols = 0;
|
|
38
|
+
let rows = 0;
|
|
39
|
+
for (const p of panes) {
|
|
40
|
+
cols = Math.max(cols, p.left + p.width);
|
|
41
|
+
rows = Math.max(rows, p.top + p.height);
|
|
42
|
+
}
|
|
43
|
+
return { cols, rows };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* PURE — the actual window size when it DIFFERS from what we pinned (another
|
|
48
|
+
* terminal is dictating it), else null. Any difference on either axis counts;
|
|
49
|
+
* equal sizes mean we own the window and there is nothing to surface.
|
|
50
|
+
*/
|
|
51
|
+
export function detectSizeMismatch(pinned: Size, effective: Size): Size | null {
|
|
52
|
+
if (effective.cols === pinned.cols && effective.rows === pinned.rows) return null;
|
|
53
|
+
return { cols: effective.cols, rows: effective.rows };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* PURE — the letterbox offset that centers a `content`-sized grid inside a
|
|
58
|
+
* `canvas`-sized area. Never negative (a window LARGER than the canvas pins to
|
|
59
|
+
* the origin and clips rather than shifting off-screen), floored so cells stay
|
|
60
|
+
* aligned to the grid.
|
|
61
|
+
*/
|
|
62
|
+
export function letterboxOffset(canvas: Size, content: Size): { x: number; y: number } {
|
|
63
|
+
return {
|
|
64
|
+
x: Math.max(0, Math.floor((canvas.cols - content.cols) / 2)),
|
|
65
|
+
y: Math.max(0, Math.floor((canvas.rows - content.rows) / 2)),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* PURE — the quiet, plain-language hint for a size mismatch: the honest answer
|
|
71
|
+
* ("here is the size another terminal chose"), dimensions in the terminal-native
|
|
72
|
+
* `cols×rows` form. Dismiss-free — the app shows it only while the mismatch
|
|
73
|
+
* exists, so it disappears the moment the sizes agree.
|
|
74
|
+
*/
|
|
75
|
+
export function formatSizeHint(effective: Size): string {
|
|
76
|
+
return `window sized by another terminal — ${effective.cols}×${effective.rows}`;
|
|
77
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** Pure horizontal-span geometry shared by the two clickable tab rows — the
|
|
2
|
+
* surface tab bar (F1..F4, startX=0, gap=0) and the per-window strip inside the
|
|
3
|
+
* Terminal surface (startX=SIDEBAR_W+1, gap=1). Both render a row of labels
|
|
4
|
+
* laid out left-to-right and both resolve clicks in the central `route` by
|
|
5
|
+
* x-span math; keeping that math in one tested place is the whole point (the
|
|
6
|
+
* window strip's old inline math drifted from its render and hit the late-mount
|
|
7
|
+
* landmine). Index `i` of the returned array maps to `labels[i]`. */
|
|
8
|
+
export interface Span {
|
|
9
|
+
/** First column (inclusive) this label occupies. */
|
|
10
|
+
start: number;
|
|
11
|
+
/** Cell width — the label's string length (labels are single display-width). */
|
|
12
|
+
width: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Lay `labels` out from `startX`, each `label.length` cells wide, separated by
|
|
16
|
+
* `gap` blank cells. Pure. */
|
|
17
|
+
export function spans(labels: string[], startX: number, gap: number): Span[] {
|
|
18
|
+
const out: Span[] = [];
|
|
19
|
+
let x = startX;
|
|
20
|
+
for (const label of labels) {
|
|
21
|
+
out.push({ start: x, width: label.length });
|
|
22
|
+
x += label.length + gap;
|
|
23
|
+
}
|
|
24
|
+
return out;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Lay `labels` out left-to-right anchored to the RIGHT: the last label ends
|
|
28
|
+
* flush at `rightEdge` (its final cell is `rightEdge - 1`), separated by `gap`.
|
|
29
|
+
* For right-aligned header affordance buttons whose PRECEDING content is
|
|
30
|
+
* variable-width — the layout is pinned to the (fixed) right edge, not a left
|
|
31
|
+
* origin, so the render (a flexGrow spacer then the buttons) and the router
|
|
32
|
+
* agree cell-for-cell. Pure. */
|
|
33
|
+
export function spansFromRight(labels: string[], rightEdge: number, gap: number): Span[] {
|
|
34
|
+
const total = labels.reduce((n, l) => n + l.length, 0) + gap * Math.max(0, labels.length - 1);
|
|
35
|
+
return spans(labels, rightEdge - total, gap);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Index of the span containing column `x`, or -1. Gap cells between spans and
|
|
39
|
+
* columns past the last span resolve to -1 (no hit). */
|
|
40
|
+
export function spanHit(list: Span[], x: number): number {
|
|
41
|
+
for (let i = 0; i < list.length; i++) {
|
|
42
|
+
const s = list[i]!;
|
|
43
|
+
if (x >= s.start && x < s.start + s.width) return i;
|
|
44
|
+
}
|
|
45
|
+
return -1;
|
|
46
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The unified app's status glyph + color grammar — PURE declarative data, hoisted
|
|
3
|
+
* out of app.tsx (M22.2) so every mirror surface shares ONE source of truth: the
|
|
4
|
+
* sidebar fleet tree, the home rollup chips, the AGENTS section, and (post-merge)
|
|
5
|
+
* the pane-chrome agent chips. Keyed by {@link AgentStatus}.
|
|
6
|
+
*
|
|
7
|
+
* This is the MIRROR app's grammar — deliberately distinct from the cockpit's
|
|
8
|
+
* (whose idle glyph is a filled ●) and from chrome's color tokens. Do not swap
|
|
9
|
+
* in either of those: keep every mirror surface in one visual family.
|
|
10
|
+
*/
|
|
11
|
+
import { RGBA } from "@opentui/core";
|
|
12
|
+
import type { AgentStatus } from "../detect/classify.ts";
|
|
13
|
+
|
|
14
|
+
/** Per-state foreground color: blocked red, working amber, done blue, idle
|
|
15
|
+
* green, unknown grey. */
|
|
16
|
+
export const STATUS_COLOR: Record<AgentStatus, RGBA> = {
|
|
17
|
+
blocked: RGBA.fromInts(240, 100, 100, 255),
|
|
18
|
+
working: RGBA.fromInts(235, 200, 100, 255),
|
|
19
|
+
done: RGBA.fromInts(120, 170, 250, 255),
|
|
20
|
+
idle: RGBA.fromInts(120, 200, 140, 255),
|
|
21
|
+
unknown: RGBA.fromInts(110, 110, 130, 255),
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/** Per-state glyph: working/blocked/done share the filled dot (color carries the
|
|
25
|
+
* distinction), idle is hollow, unknown a middot. */
|
|
26
|
+
export const STATUS_GLYPH: Record<AgentStatus, string> = {
|
|
27
|
+
blocked: "●",
|
|
28
|
+
working: "●",
|
|
29
|
+
done: "●",
|
|
30
|
+
idle: "○",
|
|
31
|
+
unknown: "·",
|
|
32
|
+
};
|
|
@@ -1,11 +1,38 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Front-door decision for bare `tmux-ide` / `tmux-ide start`:
|
|
3
|
-
*
|
|
2
|
+
* Front-door decision for bare `tmux-ide` / `tmux-ide start`: what does running
|
|
3
|
+
* the command with no subcommand land on?
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* Three outcomes:
|
|
6
|
+
* - `"project"` — a single-project `ide.yml` is present here (and `--team`
|
|
7
|
+
* wasn't passed): launch that project, exactly as always (backward compatible).
|
|
8
|
+
* - `"app"` — the M22.6 front-door flip: when there's no project to launch and
|
|
9
|
+
* the user opted in (`app.frontDoor`), bare `tmux-ide` opens the unified app
|
|
10
|
+
* (`tmux-ide app`) — the VS-Code-style "starts anywhere" entry.
|
|
11
|
+
* - `"cockpit"` — the classic team cockpit: the default no-project entry while
|
|
12
|
+
* the flip is off, and always the target of an explicit `--team`.
|
|
13
|
+
*
|
|
14
|
+
* `--team` ALWAYS means the classic cockpit (an explicit request for it), so it
|
|
15
|
+
* overrides both a present `ide.yml` and the front-door flip.
|
|
16
|
+
*/
|
|
17
|
+
export type EntryTarget = "project" | "cockpit" | "app";
|
|
18
|
+
|
|
19
|
+
export interface ResolveEntryOptions {
|
|
20
|
+
/** Whether an `ide.yml` is present in the target directory. */
|
|
21
|
+
hasIdeYml: boolean;
|
|
22
|
+
/** Whether `--team` forced the cockpit. */
|
|
23
|
+
teamFlag: boolean;
|
|
24
|
+
/** The `app.frontDoor` config flag — flip the default no-project entry to the app. */
|
|
25
|
+
frontDoor: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* PURE — resolve the bare-invocation target. `--team` wins (always the classic
|
|
30
|
+
* cockpit); otherwise a present `ide.yml` launches the project; otherwise the
|
|
31
|
+
* front-door flag decides between the unified app (opted in) and the classic
|
|
32
|
+
* cockpit (default).
|
|
8
33
|
*/
|
|
9
|
-
export function
|
|
10
|
-
|
|
34
|
+
export function resolveEntry(opts: ResolveEntryOptions): EntryTarget {
|
|
35
|
+
if (opts.teamFlag) return "cockpit";
|
|
36
|
+
if (opts.hasIdeYml) return "project";
|
|
37
|
+
return opts.frontDoor ? "app" : "cockpit";
|
|
11
38
|
}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import type { AgentStatus } from "../detect/classify.ts";
|
|
12
12
|
import type { TeamProject } from "./projects.ts";
|
|
13
|
-
import type { TeamSession } from "./sessions.ts";
|
|
13
|
+
import type { PaneAgentEntry, TeamSession } from "./sessions.ts";
|
|
14
14
|
|
|
15
15
|
/** The stable JSON shape emitted by `tmux-ide team --json`. */
|
|
16
16
|
export interface FleetJson {
|
|
@@ -32,6 +32,13 @@ export interface FleetJson {
|
|
|
32
32
|
panes: number;
|
|
33
33
|
status: AgentStatus;
|
|
34
34
|
}>;
|
|
35
|
+
/**
|
|
36
|
+
* Per-pane agent detail — one entry per pane the detection layer resolves
|
|
37
|
+
* to a real agent, flat across the session's windows (each entry carries
|
|
38
|
+
* its `windowIndex`). ADDITIVE: always present (possibly empty); existing
|
|
39
|
+
* consumers that ignore it are unaffected.
|
|
40
|
+
*/
|
|
41
|
+
agents: PaneAgentEntry[];
|
|
35
42
|
}>;
|
|
36
43
|
}>;
|
|
37
44
|
}
|
|
@@ -57,6 +64,9 @@ export function toFleetJson(projects: TeamProject[]): FleetJson {
|
|
|
57
64
|
panes: w.panes,
|
|
58
65
|
status: w.status,
|
|
59
66
|
})),
|
|
67
|
+
// A pre-agents TeamSession (older constructor/test) yields `[]` — the
|
|
68
|
+
// contract always exposes the array.
|
|
69
|
+
agents: s.agents ?? [],
|
|
60
70
|
})),
|
|
61
71
|
})),
|
|
62
72
|
};
|