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.
- package/README.md +22 -5
- package/bin/cli.js +3532 -1090
- package/bin/cli.ts +368 -71
- package/package.json +2 -1
- package/packages/contracts/src/__tests__/control.test.ts +154 -0
- package/packages/contracts/src/control.ts +217 -0
- package/packages/contracts/src/index.ts +1 -0
- package/packages/daemon/dist/control/client.d.ts +23 -0
- package/packages/daemon/dist/control/client.js +105 -0
- package/packages/daemon/dist/control/dispatch.d.ts +34 -0
- package/packages/daemon/dist/control/dispatch.js +83 -0
- package/packages/daemon/dist/control/fanout.d.ts +19 -0
- package/packages/daemon/dist/control/fanout.js +37 -0
- package/packages/daemon/dist/control/frames.d.ts +23 -0
- package/packages/daemon/dist/control/frames.js +37 -0
- package/packages/daemon/dist/control/lifecycle.d.ts +45 -0
- package/packages/daemon/dist/control/lifecycle.js +114 -0
- package/packages/daemon/dist/control/server.d.ts +16 -0
- package/packages/daemon/dist/control/server.js +214 -0
- package/packages/daemon/dist/control/verbs.d.ts +11 -0
- package/packages/daemon/dist/control/verbs.js +91 -0
- package/packages/daemon/dist/doctor.d.ts +18 -0
- package/packages/daemon/dist/doctor.js +105 -15
- package/packages/daemon/dist/lib/agent-discovery.d.ts +27 -2
- package/packages/daemon/dist/lib/agent-discovery.js +29 -14
- package/packages/daemon/dist/lib/app-config.d.ts +106 -0
- package/packages/daemon/dist/lib/app-config.js +104 -5
- package/packages/daemon/dist/lib/manifest-pack.d.ts +79 -0
- package/packages/daemon/dist/lib/manifest-pack.js +232 -0
- package/packages/daemon/dist/lib/state-home.d.ts +2 -0
- package/packages/daemon/dist/lib/state-home.js +12 -0
- package/packages/daemon/dist/lib/update-check.js +5 -0
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Info.plist +34 -0
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/MacOS/tmux-ide-notifier +0 -0
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/PkgInfo +1 -0
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Resources/AppIcon.icns +0 -0
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Resources/Assets.car +0 -0
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/_CodeSignature/CodeResources +139 -0
- package/packages/daemon/dist/restore.d.ts +35 -8
- package/packages/daemon/dist/restore.js +52 -15
- package/packages/daemon/dist/send.d.ts +33 -1
- package/packages/daemon/dist/send.js +32 -19
- package/packages/daemon/src/control/client.ts +128 -0
- package/packages/daemon/src/control/dispatch.ts +107 -0
- package/packages/daemon/src/control/fanout.ts +44 -0
- package/packages/daemon/src/control/frames.ts +40 -0
- package/packages/daemon/src/control/lifecycle.ts +151 -0
- package/packages/daemon/src/control/server.ts +237 -0
- package/packages/daemon/src/control/verbs.ts +118 -0
- package/packages/daemon/src/doctor.ts +113 -28
- package/packages/daemon/src/lib/agent-discovery.ts +53 -13
- package/packages/daemon/src/lib/app-config.ts +103 -5
- package/packages/daemon/src/lib/manifest-pack.ts +255 -0
- package/packages/daemon/src/lib/state-home.ts +13 -0
- package/packages/daemon/src/lib/update-check.ts +5 -0
- package/packages/daemon/src/restore.ts +53 -15
- package/packages/daemon/src/send.ts +55 -21
- package/packages/daemon/src/tui/chrome/events.ts +4 -4
- package/packages/daemon/src/tui/chrome/front-door.ts +39 -0
- package/packages/daemon/src/tui/chrome/notify-prefs.ts +58 -0
- package/packages/daemon/src/tui/chrome/notify-state.ts +76 -0
- package/packages/daemon/src/tui/chrome/notify.ts +582 -84
- package/packages/daemon/src/tui/chrome/updater.ts +268 -62
- package/packages/daemon/src/tui/detect/classify.ts +34 -0
- package/packages/daemon/src/tui/detect/manifest-loader.ts +54 -5
- package/packages/daemon/src/tui/detect/manifest.ts +24 -3
- package/packages/daemon/src/tui/detect/manifests.ts +240 -6
- package/packages/daemon/src/tui/detect/process-tree.ts +13 -3
- package/packages/daemon/src/tui/detect/session-id.ts +503 -0
- package/packages/daemon/src/tui/integrations/opencode.ts +121 -0
- package/packages/daemon/src/tui/mirror/agent-chip.ts +40 -11
- package/packages/daemon/src/tui/mirror/agent-lifecycle.ts +437 -0
- package/packages/daemon/src/tui/mirror/agent-rows.ts +27 -5
- package/packages/daemon/src/tui/mirror/app-state.ts +171 -8
- package/packages/daemon/src/tui/mirror/app.tsx +2182 -399
- package/packages/daemon/src/tui/mirror/attention.ts +110 -0
- package/packages/daemon/src/tui/mirror/dialog-stack.ts +17 -4
- package/packages/daemon/src/tui/mirror/diff-model.ts +279 -4
- package/packages/daemon/src/tui/mirror/file-tree.ts +231 -6
- package/packages/daemon/src/tui/mirror/host-terminal.ts +49 -0
- package/packages/daemon/src/tui/mirror/hosted.ts +205 -0
- package/packages/daemon/src/tui/mirror/layout-parse.ts +154 -0
- package/packages/daemon/src/tui/mirror/menu-model.ts +27 -4
- package/packages/daemon/src/tui/mirror/palette.ts +299 -9
- package/packages/daemon/src/tui/mirror/pane-mirror.ts +82 -4
- package/packages/daemon/src/tui/mirror/pane-surface.tsx +18 -11
- package/packages/daemon/src/tui/mirror/perf-tap.ts +29 -3
- package/packages/daemon/src/tui/mirror/selection.ts +122 -8
- package/packages/daemon/src/tui/mirror/session-mirror.ts +349 -68
- package/packages/daemon/src/tui/mirror/settings-model.ts +96 -16
- package/packages/daemon/src/tui/mirror/sidebar.tsx +218 -0
- package/packages/daemon/src/tui/mirror/size-truth.ts +53 -0
- package/packages/daemon/src/tui/mirror/theme.ts +45 -0
- package/packages/daemon/src/tui/team/fuzzy.ts +20 -0
- package/packages/daemon/src/tui/team/sessions.ts +85 -7
- package/packages/daemon/src/tui/team/wait.ts +144 -0
- package/scripts/build-macos-notifier.mjs +160 -0
- package/scripts/postinstall.js +8 -1
- package/scripts/prepublish-check.mjs +37 -1
- package/scripts/publish-tap.sh +55 -0
- package/skill/SKILL.md +88 -2
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* never left guessing.
|
|
13
13
|
*/
|
|
14
14
|
import type { AppConfig, AppConfigPatch, AppKeys } from "../../lib/app-config.ts";
|
|
15
|
-
import { parseHHMM, type NotificationPrefs } from "../chrome/notify.ts";
|
|
15
|
+
import { parseHHMM, type NotificationPrefs } from "../chrome/notify-prefs.ts";
|
|
16
16
|
import type { DialogSelectItem } from "./dialog-model.ts";
|
|
17
17
|
|
|
18
18
|
// ── Command registry (what the palette offers) ───────────────────────────────
|
|
@@ -107,24 +107,83 @@ export function themePatch(accent: string): AppConfigPatch {
|
|
|
107
107
|
|
|
108
108
|
// ── Notifications ────────────────────────────────────────────────────────────
|
|
109
109
|
|
|
110
|
-
export type NotificationToggleId =
|
|
110
|
+
export type NotificationToggleId =
|
|
111
|
+
| "enabled"
|
|
112
|
+
| "toast"
|
|
113
|
+
| "macos"
|
|
114
|
+
| "terminal"
|
|
115
|
+
| "onBlocked"
|
|
116
|
+
| "onDone";
|
|
111
117
|
|
|
112
118
|
const onOff = (v: boolean) => (v ? "on" : "off");
|
|
113
119
|
|
|
114
120
|
/** PURE — the notification toggle rows (the REAL fields notify.ts reads: the
|
|
115
|
-
* typed
|
|
116
|
-
*
|
|
121
|
+
* typed channel fields plus the polish-era raw fields), plus the rows that
|
|
122
|
+
* descend into their own flows (sound tri-choice, delay prompt, quiet hours).
|
|
123
|
+
* Enter toggles the boolean rows in place. */
|
|
117
124
|
export function notificationItems(prefs: NotificationPrefs): DialogSelectItem[] {
|
|
118
125
|
return [
|
|
119
126
|
{ id: "enabled", label: "All notifications", detail: onOff(prefs.enabled) },
|
|
120
127
|
{ id: "toast", label: "In-terminal toasts", detail: onOff(prefs.toast) },
|
|
121
|
-
{ id: "macos", label: "
|
|
128
|
+
{ id: "macos", label: "System banners", detail: onOff(prefs.macos) },
|
|
129
|
+
{ id: "terminal", label: "Terminal banners (OSC)", detail: onOff(prefs.terminal) },
|
|
122
130
|
{ id: "onBlocked", label: "Alert when an agent needs you", detail: onOff(prefs.onBlocked) },
|
|
123
131
|
{ id: "onDone", label: "Alert when an agent finishes", detail: onOff(prefs.onDone) },
|
|
132
|
+
{ id: "sound", label: "Sound…", detail: soundSummary(prefs) },
|
|
133
|
+
{ id: "delaySeconds", label: "Alert delay…", detail: delaySummary(prefs) },
|
|
124
134
|
{ id: "quietHours", label: "Quiet hours…", detail: quietHoursSummary(prefs) },
|
|
125
135
|
];
|
|
126
136
|
}
|
|
127
137
|
|
|
138
|
+
/** PURE — plain words for the sound tri-state. */
|
|
139
|
+
export function soundSummary(prefs: NotificationPrefs): string {
|
|
140
|
+
return prefs.sound === "blocked" ? "when blocked" : prefs.sound === "all" ? "always" : "off";
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** PURE — the sound chooser rows (● on the saved choice). */
|
|
144
|
+
export function soundItems(prefs: NotificationPrefs): DialogSelectItem[] {
|
|
145
|
+
return [
|
|
146
|
+
{
|
|
147
|
+
id: "blocked",
|
|
148
|
+
label: "When an agent needs you",
|
|
149
|
+
detail: "sound + terminal bell on blocked",
|
|
150
|
+
current: prefs.sound === "blocked",
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
id: "all",
|
|
154
|
+
label: "Every alert",
|
|
155
|
+
detail: "blocked and finished",
|
|
156
|
+
current: prefs.sound === "all",
|
|
157
|
+
},
|
|
158
|
+
{ id: "none", label: "Off — no sound", current: prefs.sound === "none" },
|
|
159
|
+
];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** PURE — persist a picked sound mode. */
|
|
163
|
+
export function soundPatch(id: string): AppConfigPatch {
|
|
164
|
+
return { notifications: { sound: id } };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** PURE — "2 s" / "immediately". */
|
|
168
|
+
export function delaySummary(prefs: NotificationPrefs): string {
|
|
169
|
+
return prefs.delaySeconds > 0 ? `${prefs.delaySeconds} s` : "immediately";
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** PURE — 0–60 whole seconds (0 = fire immediately; the delay exists to swallow
|
|
173
|
+
* flappy transitions, and a minute is already generous). */
|
|
174
|
+
export function validateDelaySeconds(value: string): string | null {
|
|
175
|
+
const n = Number(value.trim());
|
|
176
|
+
if (!Number.isInteger(n) || n < 0 || n > 60) {
|
|
177
|
+
return "Use a whole number of seconds between 0 and 60 (0 alerts immediately)";
|
|
178
|
+
}
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** PURE — persist a validated delay. */
|
|
183
|
+
export function delaySecondsPatch(value: string): AppConfigPatch {
|
|
184
|
+
return { notifications: { delaySeconds: Number(value.trim()) } };
|
|
185
|
+
}
|
|
186
|
+
|
|
128
187
|
/** PURE — flip one notification toggle. */
|
|
129
188
|
export function notificationTogglePatch(
|
|
130
189
|
id: NotificationToggleId,
|
|
@@ -144,7 +203,7 @@ export function quietHoursItems(prefs: NotificationPrefs): DialogSelectItem[] {
|
|
|
144
203
|
{ id: "off", label: "Off — always notify", current: prefs.quietHours === null },
|
|
145
204
|
{
|
|
146
205
|
id: "window",
|
|
147
|
-
label: "Silence banners during a daily window…",
|
|
206
|
+
label: "Silence banners & sounds during a daily window…",
|
|
148
207
|
detail: prefs.quietHours ? quietHoursSummary(prefs) : undefined,
|
|
149
208
|
current: prefs.quietHours !== null,
|
|
150
209
|
},
|
|
@@ -258,10 +317,35 @@ export function prefixTwinFor(altKey: string): string | null {
|
|
|
258
317
|
return letter;
|
|
259
318
|
}
|
|
260
319
|
|
|
320
|
+
/** THE app-key enumeration — the ONE place the unified app's fixed keys are
|
|
321
|
+
* listed (M24.4). The keybind viewer's app rows AND the palette's right-aligned
|
|
322
|
+
* row shortcuts both read this; `paletteAction` is the stable palette action
|
|
323
|
+
* key ({@link ./palette.ts}'s paletteActionKey) a keycap attaches to. */
|
|
324
|
+
const APP_KEY_ROWS: ReadonlyArray<{ label: string; keycap: string; paletteAction?: string }> = [
|
|
325
|
+
{ label: "Command palette", keycap: "F5 · ^p" },
|
|
326
|
+
{ label: "Home tab", keycap: "F1", paletteAction: "tab:home" },
|
|
327
|
+
{ label: "Terminal tab", keycap: "F2", paletteAction: "tab:terminal" },
|
|
328
|
+
{ label: "Files tab", keycap: "F3", paletteAction: "tab:files" },
|
|
329
|
+
{ label: "Diff tab", keycap: "F4", paletteAction: "tab:diff" },
|
|
330
|
+
{ label: "Save file", keycap: "^s", paletteAction: "save" },
|
|
331
|
+
{ label: "Back to Home", keycap: "^g" },
|
|
332
|
+
{ label: "Toggle editor", keycap: "^e" },
|
|
333
|
+
{ label: "Quit / detach", keycap: "^q", paletteAction: "quit" },
|
|
334
|
+
];
|
|
335
|
+
|
|
336
|
+
/** PURE — palette action key → keycap, derived from {@link APP_KEY_ROWS}. The
|
|
337
|
+
* palette right-aligns these on rows that have one; app.tsx drops `quit` in
|
|
338
|
+
* HOSTED mode, where ^q detaches instead. */
|
|
339
|
+
export const PALETTE_KEYCAPS: Readonly<Record<string, string>> = Object.fromEntries(
|
|
340
|
+
APP_KEY_ROWS.filter((r) => r.paletteAction).map((r) => [r.paletteAction!, r.keycap]),
|
|
341
|
+
);
|
|
342
|
+
|
|
261
343
|
/** PURE — the read-only shortcut rows: the chrome actions from the LIVE config
|
|
262
344
|
* (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
|
-
|
|
345
|
+
* survives every keyboard protocol) and then the app's fixed keys
|
|
346
|
+
* ({@link APP_KEY_ROWS}). `superK` appends the kitty-protocol ⌘K fast path to
|
|
347
|
+
* the palette row — shown only when the renderer actually enables it. */
|
|
348
|
+
export function keybindingItems(keys: AppKeys, superK = false): DialogSelectItem[] {
|
|
265
349
|
const chrome: Array<{ label: string; altKey: string }> = [
|
|
266
350
|
{ label: "Home cockpit", altKey: keys.home },
|
|
267
351
|
{ label: "Session switcher", altKey: keys.popup },
|
|
@@ -280,14 +364,10 @@ export function keybindingItems(keys: AppKeys): DialogSelectItem[] {
|
|
|
280
364
|
detail: twin ? `prefix ${twin} · ${altKey}` : altKey,
|
|
281
365
|
};
|
|
282
366
|
});
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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 });
|
|
367
|
+
for (const { label, keycap } of APP_KEY_ROWS) {
|
|
368
|
+
const detail = superK && label === "Command palette" ? `${keycap} · ⌘K` : keycap;
|
|
369
|
+
rows.push({ id: `app:${label}`, label, detail });
|
|
370
|
+
}
|
|
291
371
|
return rows;
|
|
292
372
|
}
|
|
293
373
|
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The sidebar — the fleet nav column. Extracted from app.tsx (M26) as a PURE
|
|
3
|
+
* PRESENTATIONAL component: it renders, it does not fetch, resolve, or route.
|
|
4
|
+
*
|
|
5
|
+
* Everything it needs arrives as props, which is what lets TWO hosts render the
|
|
6
|
+
* same component:
|
|
7
|
+
*
|
|
8
|
+
* - the terminal app (app.tsx) — OpenTUI's native renderer, over tmux
|
|
9
|
+
* - the web (docs/tui-web) — a solid-js universal renderer over DOM
|
|
10
|
+
*
|
|
11
|
+
* Consequently: NO node imports, NO tmux, NO app state. The only pointer surface
|
|
12
|
+
* is `onMouse` on the root box, carrying CELL coordinates — the same contract in
|
|
13
|
+
* both hosts (the web host synthesizes cells from DOM events), so the router
|
|
14
|
+
* (`sidebarHit`) is shared too rather than reimplemented per host.
|
|
15
|
+
*
|
|
16
|
+
* Row order is the CALLER's: pass agents already sorted (app's `fleetAgents`
|
|
17
|
+
* memo applies `sortAgentRows`). Sorting here would double-sort and let the two
|
|
18
|
+
* hosts silently disagree with the palette's jump list, which reads the same memo.
|
|
19
|
+
*
|
|
20
|
+
* The y-accounting this render implies — title, rule, session rows, gap, agents
|
|
21
|
+
* header, agent rows — is reversed by `sidebarHit` in agent-rows.ts. The two MUST
|
|
22
|
+
* move together; that's why AGENTS_GAP_ROWS is shared rather than a literal here.
|
|
23
|
+
*/
|
|
24
|
+
import { For, Show } from "solid-js";
|
|
25
|
+
import { STATUS_COLOR, STATUS_GLYPH } from "./status-grammar.ts";
|
|
26
|
+
import {
|
|
27
|
+
AGENTS_ADD_CHIP,
|
|
28
|
+
AGENTS_EMPTY_LINE,
|
|
29
|
+
AGENTS_GAP_ROWS,
|
|
30
|
+
agentAgeLabel,
|
|
31
|
+
agentDisplayKind,
|
|
32
|
+
agentRowLabel,
|
|
33
|
+
agentsHeaderLabel,
|
|
34
|
+
type AgentRowInput,
|
|
35
|
+
} from "./agent-rows.ts";
|
|
36
|
+
import {
|
|
37
|
+
ACCENT,
|
|
38
|
+
BUTTON_HOVER_BG,
|
|
39
|
+
CHIP_ATTN_BG,
|
|
40
|
+
DEFAULT_FG,
|
|
41
|
+
HOVER_BG,
|
|
42
|
+
MUTED,
|
|
43
|
+
SIDEBAR_BG,
|
|
44
|
+
TAB_ACTIVE_BG,
|
|
45
|
+
} from "./theme.ts";
|
|
46
|
+
import type { AgentStatus } from "../detect/classify.ts";
|
|
47
|
+
|
|
48
|
+
/** A session row: the fleet's name + its rolled-up status. */
|
|
49
|
+
export interface SidebarSession {
|
|
50
|
+
name: string;
|
|
51
|
+
status: AgentStatus;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The hover regions the sidebar owns. A subset of app.tsx's HoverRegion — kept
|
|
55
|
+
* structural so the host's wider union assigns without a cast. */
|
|
56
|
+
export type SidebarHoverRegion =
|
|
57
|
+
| "sidebar"
|
|
58
|
+
| "sidebaragent"
|
|
59
|
+
| "agentshdr"
|
|
60
|
+
| "agentschip"
|
|
61
|
+
| "sidebtn";
|
|
62
|
+
|
|
63
|
+
/** The pointer event shape (structurally OpenTUI's MouseEvent; app.tsx's
|
|
64
|
+
* RouteEvent assigns to it). Cell coordinates, not pixels. */
|
|
65
|
+
export interface SidebarMouseEvent {
|
|
66
|
+
type: string;
|
|
67
|
+
x: number;
|
|
68
|
+
y: number;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface SidebarProps {
|
|
72
|
+
/** Column width in cells. */
|
|
73
|
+
width: number;
|
|
74
|
+
sessions: SidebarSession[];
|
|
75
|
+
/** Pre-sorted, attention-first (see the note above). */
|
|
76
|
+
agents: AgentRowInput[];
|
|
77
|
+
/** The session whose workspace is open — renders as the active row. */
|
|
78
|
+
current: string;
|
|
79
|
+
/** Epoch SECONDS, injected so the age readout is testable and the component
|
|
80
|
+
* stays clock-free. */
|
|
81
|
+
nowSec: number;
|
|
82
|
+
isHovered: (region: SidebarHoverRegion, index: number) => boolean;
|
|
83
|
+
/** M25.1 attention flash, by pane id. */
|
|
84
|
+
flashed: (paneId: string) => boolean;
|
|
85
|
+
/** The footer hint's three runs — the middle one is the clickable chip whose
|
|
86
|
+
* cells the router hit-tests (SIDEBAR_HINT_SPAN). Passed in because the quit
|
|
87
|
+
* hint differs by host. */
|
|
88
|
+
hint: { pre: string; btn: string; post: string };
|
|
89
|
+
onMouse?: (e: SidebarMouseEvent) => void;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function Sidebar(props: SidebarProps) {
|
|
93
|
+
return (
|
|
94
|
+
<box
|
|
95
|
+
width={props.width}
|
|
96
|
+
flexDirection="column"
|
|
97
|
+
backgroundColor={SIDEBAR_BG}
|
|
98
|
+
paddingLeft={1}
|
|
99
|
+
overflow="hidden"
|
|
100
|
+
onMouse={(e: SidebarMouseEvent) => props.onMouse?.(e)}
|
|
101
|
+
>
|
|
102
|
+
<text fg={ACCENT} attributes={1}>
|
|
103
|
+
tmux-ide
|
|
104
|
+
</text>
|
|
105
|
+
<text fg={MUTED}>{"─".repeat(props.width - 2)}</text>
|
|
106
|
+
<box flexDirection="column">
|
|
107
|
+
<For each={props.sessions}>
|
|
108
|
+
{(s, i) => (
|
|
109
|
+
<box
|
|
110
|
+
flexDirection="row"
|
|
111
|
+
gap={1}
|
|
112
|
+
backgroundColor={
|
|
113
|
+
s.name === props.current
|
|
114
|
+
? TAB_ACTIVE_BG
|
|
115
|
+
: props.isHovered("sidebar", i())
|
|
116
|
+
? HOVER_BG
|
|
117
|
+
: SIDEBAR_BG
|
|
118
|
+
}
|
|
119
|
+
>
|
|
120
|
+
<text fg={STATUS_COLOR[s.status]}>{STATUS_GLYPH[s.status]}</text>
|
|
121
|
+
<text fg={s.name === props.current ? DEFAULT_FG : MUTED}>
|
|
122
|
+
{s.name.slice(0, props.width - 5)}
|
|
123
|
+
</text>
|
|
124
|
+
</box>
|
|
125
|
+
)}
|
|
126
|
+
</For>
|
|
127
|
+
</box>
|
|
128
|
+
{/* AGENTS section (M22.2): the fleet's agents at a glance, one row per
|
|
129
|
+
agent, sorted attention-first, each a JUMP target (click → its
|
|
130
|
+
session/window/pane). REUSES the session rows' glyph + state-color
|
|
131
|
+
grammar. Hover reveals the state age. */}
|
|
132
|
+
<box flexDirection="column" marginTop={AGENTS_GAP_ROWS}>
|
|
133
|
+
{/* Header row (M24.1): label + a right-aligned [+ agent] chip. The row
|
|
134
|
+
body opens the TEAM dialog, the chip spawns; the router x-tests
|
|
135
|
+
`agentsChipSpans` — the flexGrow spacer lays the chip on exactly those
|
|
136
|
+
cells. The empty state keeps a chip twin so spawning is discoverable
|
|
137
|
+
before any agent runs. */}
|
|
138
|
+
<box
|
|
139
|
+
flexDirection="row"
|
|
140
|
+
backgroundColor={props.isHovered("agentshdr", 0) ? HOVER_BG : SIDEBAR_BG}
|
|
141
|
+
>
|
|
142
|
+
<text fg={MUTED} attributes={1}>
|
|
143
|
+
{agentsHeaderLabel(
|
|
144
|
+
props.agents.length,
|
|
145
|
+
Math.max(1, props.width - AGENTS_ADD_CHIP.length - 2),
|
|
146
|
+
)}
|
|
147
|
+
</text>
|
|
148
|
+
<box flexGrow={1} />
|
|
149
|
+
<text fg={MUTED} bg={props.isHovered("agentschip", 0) ? BUTTON_HOVER_BG : SIDEBAR_BG}>
|
|
150
|
+
{AGENTS_ADD_CHIP}
|
|
151
|
+
</text>
|
|
152
|
+
</box>
|
|
153
|
+
<Show
|
|
154
|
+
when={props.agents.length > 0}
|
|
155
|
+
fallback={
|
|
156
|
+
<box flexDirection="row">
|
|
157
|
+
<text fg={MUTED}>
|
|
158
|
+
{AGENTS_EMPTY_LINE.slice(0, Math.max(1, props.width - AGENTS_ADD_CHIP.length - 2))}
|
|
159
|
+
</text>
|
|
160
|
+
<box flexGrow={1} />
|
|
161
|
+
<text fg={MUTED} bg={props.isHovered("agentschip", 1) ? BUTTON_HOVER_BG : SIDEBAR_BG}>
|
|
162
|
+
{AGENTS_ADD_CHIP}
|
|
163
|
+
</text>
|
|
164
|
+
</box>
|
|
165
|
+
}
|
|
166
|
+
>
|
|
167
|
+
<For each={props.agents}>
|
|
168
|
+
{(a, i) => {
|
|
169
|
+
const hovered = () => props.isHovered("sidebaragent", i());
|
|
170
|
+
const ageShown = () =>
|
|
171
|
+
hovered() ? agentAgeLabel(a.state, a.since, props.nowSec) : null;
|
|
172
|
+
const labelBudget = () => {
|
|
173
|
+
const age = ageShown();
|
|
174
|
+
return props.width - 5 - (age ? age.length + 1 : 0);
|
|
175
|
+
};
|
|
176
|
+
// Blocked is the attention state: bold, matching the statusline's
|
|
177
|
+
// grammar (`blocked` reads bold there too).
|
|
178
|
+
const attn = () => (a.state === "blocked" ? 1 : 0);
|
|
179
|
+
const flashed = () => props.flashed(a.paneId);
|
|
180
|
+
return (
|
|
181
|
+
<box
|
|
182
|
+
flexDirection="row"
|
|
183
|
+
gap={1}
|
|
184
|
+
backgroundColor={flashed() ? CHIP_ATTN_BG : hovered() ? HOVER_BG : SIDEBAR_BG}
|
|
185
|
+
>
|
|
186
|
+
<text fg={STATUS_COLOR[a.state]} attributes={attn()}>
|
|
187
|
+
{STATUS_GLYPH[a.state]}
|
|
188
|
+
</text>
|
|
189
|
+
<text
|
|
190
|
+
fg={a.state === "blocked" ? STATUS_COLOR.blocked : MUTED}
|
|
191
|
+
attributes={attn()}
|
|
192
|
+
>
|
|
193
|
+
{agentRowLabel(agentDisplayKind(a), a.session, labelBudget())}
|
|
194
|
+
</text>
|
|
195
|
+
<Show when={ageShown()}>
|
|
196
|
+
<box flexGrow={1} />
|
|
197
|
+
<text fg={MUTED}>{ageShown()}</text>
|
|
198
|
+
</Show>
|
|
199
|
+
</box>
|
|
200
|
+
);
|
|
201
|
+
}}
|
|
202
|
+
</For>
|
|
203
|
+
</Show>
|
|
204
|
+
</box>
|
|
205
|
+
<box flexGrow={1} />
|
|
206
|
+
{/* Footer hint — its middle segment is a CHIP (M21.9): the router
|
|
207
|
+
hit-tests SIDEBAR_HINT_SPAN on the last screen row, and these three runs
|
|
208
|
+
render the exact same cells. */}
|
|
209
|
+
<box flexDirection="row">
|
|
210
|
+
<text fg={MUTED}>{props.hint.pre}</text>
|
|
211
|
+
<text fg={MUTED} bg={props.isHovered("sidebtn", 0) ? BUTTON_HOVER_BG : SIDEBAR_BG}>
|
|
212
|
+
{props.hint.btn}
|
|
213
|
+
</text>
|
|
214
|
+
<text fg={MUTED}>{props.hint.post}</text>
|
|
215
|
+
</box>
|
|
216
|
+
</box>
|
|
217
|
+
);
|
|
218
|
+
}
|
|
@@ -10,6 +10,18 @@
|
|
|
10
10
|
* mismatch from the pane layout, center the grid inside the canvas, and format
|
|
11
11
|
* the quiet honest hint. No tmux, no render loop — unit-tested in isolation; the
|
|
12
12
|
* app reads the pane geometry and the pinned canvas size and wires the rest.
|
|
13
|
+
*
|
|
14
|
+
* KNOWN GAP, measured (M25.5, tmux 3.7b): when TWO app processes mirror the
|
|
15
|
+
* same session (one per terminal — the non-hosted double-open), the later
|
|
16
|
+
* control client's boot pin wins `window-size latest`, and the other app can
|
|
17
|
+
* NEVER re-win it by interaction — the keys/mouse it forwards travel as
|
|
18
|
+
* control-client `send-keys` commands, which do not update tmux's
|
|
19
|
+
* latest-client
|
|
20
|
+
* bookkeeping (measured: local keystroke and wheel left the window at the
|
|
21
|
+
* other app's pin). The hint + the palette's "Resize to fit" verb are the
|
|
22
|
+
* designed escape; the structural fix is the hosted cockpit (`--detachable`),
|
|
23
|
+
* where one app process serves every terminal and REAL clients attach to the
|
|
24
|
+
* host session, whose native latest handling re-adopts on attach/focus/detach.
|
|
13
25
|
*/
|
|
14
26
|
|
|
15
27
|
export interface Size {
|
|
@@ -66,6 +78,47 @@ export function letterboxOffset(canvas: Size, content: Size): { x: number; y: nu
|
|
|
66
78
|
};
|
|
67
79
|
}
|
|
68
80
|
|
|
81
|
+
/** An in-flight re-pin (M23.5): we changed our pinned size and tmux has not
|
|
82
|
+
* confirmed the new layout yet. `prev` is the pin BEFORE this re-pin. */
|
|
83
|
+
export interface RepinState {
|
|
84
|
+
prev: Size;
|
|
85
|
+
at: number;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** How long any mismatch is suppressed after a re-pin (server round-trip +
|
|
89
|
+
* layout-change latency is sub-ms; the margin covers a loaded machine). */
|
|
90
|
+
export const REPIN_GRACE_MS = 400;
|
|
91
|
+
/** How long a mismatch that still equals the PRE-repin pin is suppressed — the
|
|
92
|
+
* unambiguous "tmux hasn't applied our new pin yet" signature can wait longer
|
|
93
|
+
* before we call it another terminal's doing. */
|
|
94
|
+
export const REPIN_STALE_GRACE_MS = 1500;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* PURE — {@link detectSizeMismatch}, gated for the re-pin transition (M23.5,
|
|
98
|
+
* D4b): between our `refresh-client -C` and tmux's `%layout-change` the
|
|
99
|
+
* effective size is legitimately stale, and surfacing it flashed a false
|
|
100
|
+
* "window sized by another terminal" hint plus a letterbox jump-to-center on
|
|
101
|
+
* EVERY grow (measured). Suppress the mismatch while a re-pin is in flight:
|
|
102
|
+
* any mismatch within {@link REPIN_GRACE_MS}, and — for longer, up to
|
|
103
|
+
* {@link REPIN_STALE_GRACE_MS} — one whose effective size still equals the
|
|
104
|
+
* pre-repin pin exactly (that shape can only be the old pin, not a co-attached
|
|
105
|
+
* terminal's choice). Past the grace the honest hint returns.
|
|
106
|
+
*/
|
|
107
|
+
export function detectSizeMismatchWithRepin(
|
|
108
|
+
pinned: Size,
|
|
109
|
+
effective: Size,
|
|
110
|
+
repin: RepinState | null,
|
|
111
|
+
now: number,
|
|
112
|
+
): Size | null {
|
|
113
|
+
const mm = detectSizeMismatch(pinned, effective);
|
|
114
|
+
if (!mm || !repin) return mm;
|
|
115
|
+
const age = now - repin.at;
|
|
116
|
+
if (age < REPIN_GRACE_MS) return null;
|
|
117
|
+
const isPrevPin = effective.cols === repin.prev.cols && effective.rows === repin.prev.rows;
|
|
118
|
+
if (isPrevPin && age < REPIN_STALE_GRACE_MS) return null;
|
|
119
|
+
return mm;
|
|
120
|
+
}
|
|
121
|
+
|
|
69
122
|
/**
|
|
70
123
|
* PURE — the quiet, plain-language hint for a size mismatch: the honest answer
|
|
71
124
|
* ("here is the size another terminal chose"), dimensions in the terminal-native
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The unified app's color tokens — PURE data, hoisted out of app.tsx (M26) so
|
|
3
|
+
* the surfaces extracted from it (starting with {@link ./sidebar.tsx}) carry
|
|
4
|
+
* their palette with them instead of reaching back into a 21k-line module.
|
|
5
|
+
*
|
|
6
|
+
* Companion to {@link ./status-grammar.ts}: that file owns the per-AGENT-STATE
|
|
7
|
+
* glyph + hue (a status signal), this one owns the CHROME (surface, accent,
|
|
8
|
+
* hover, selection). Keep the two families distinct — focus is an accent signal,
|
|
9
|
+
* agent state is a status signal, and they must never collide on a hue.
|
|
10
|
+
*
|
|
11
|
+
* Node-free on purpose: the web host (docs/tui-web) imports this module
|
|
12
|
+
* verbatim, aliasing @opentui/core to a browser shim for RGBA.
|
|
13
|
+
*/
|
|
14
|
+
import { RGBA } from "@opentui/core";
|
|
15
|
+
|
|
16
|
+
export const DEFAULT_FG = RGBA.fromInts(212, 212, 216, 255);
|
|
17
|
+
export const DEFAULT_BG = RGBA.fromInts(16, 16, 22, 255);
|
|
18
|
+
|
|
19
|
+
/** The sidebar's surface — one lift above DEFAULT_BG so the nav column reads as
|
|
20
|
+
* chrome, not as content. */
|
|
21
|
+
export const SIDEBAR_BG = RGBA.fromInts(22, 22, 30, 255);
|
|
22
|
+
export const ACCENT = RGBA.fromInts(130, 170, 255, 255);
|
|
23
|
+
export const MUTED = RGBA.fromInts(110, 110, 130, 255);
|
|
24
|
+
export const BADGE_BG = RGBA.fromInts(60, 66, 92, 255);
|
|
25
|
+
|
|
26
|
+
/** Focused-pane gutter hairline (M22.7): the ACCENT family, drawn as │/─ glyphs
|
|
27
|
+
* so the gutter stays visually thin (a filled bar read as extra padding — user
|
|
28
|
+
* feedback). Doesn't compete with the blocked chip's red — focus is an accent
|
|
29
|
+
* signal, agent state is a status signal, never the same hue. */
|
|
30
|
+
export const FOCUS_BORDER_FG = RGBA.fromInts(110, 145, 230, 255);
|
|
31
|
+
|
|
32
|
+
/** The selected row/tab. Always wins over HOVER_BG. */
|
|
33
|
+
export const TAB_ACTIVE_BG = RGBA.fromInts(40, 46, 66, 255);
|
|
34
|
+
|
|
35
|
+
/** A single subtle pointer-hover tint, one lift above both DEFAULT_BG (16,16,22)
|
|
36
|
+
* and SIDEBAR_BG (22,22,30) and below TAB_ACTIVE_BG — the active/selected state
|
|
37
|
+
* always wins over hover. Used on every hoverable row/segment. */
|
|
38
|
+
export const HOVER_BG = RGBA.fromInts(30, 34, 48, 255);
|
|
39
|
+
|
|
40
|
+
/** A chip/button under the pointer. */
|
|
41
|
+
export const BUTTON_HOVER_BG = RGBA.fromInts(52, 60, 86, 255);
|
|
42
|
+
|
|
43
|
+
/** The attention flash (M25.1): a just-flipped hidden agent's row pulses this
|
|
44
|
+
* for a beat — the status-strip note's "look here" twin. */
|
|
45
|
+
export const CHIP_ATTN_BG = RGBA.fromInts(92, 44, 48, 255);
|
|
@@ -24,6 +24,15 @@ const START_BONUS = 10; // matched char is the first char of the target
|
|
|
24
24
|
const SEPARATOR_BONUS = 8; // matched char follows a separator (word start)
|
|
25
25
|
const CONTIGUOUS_BONUS = 5; // matched char is adjacent to the previous match
|
|
26
26
|
const BASE = 1; // every matched char is worth at least this
|
|
27
|
+
// Whole-alignment extra when the query is a contiguous PREFIX of the target
|
|
28
|
+
// (M24.4): typing a label's start must rank that label above mid-word runs
|
|
29
|
+
// AND above scattered matches that rack up several word-start bonuses —
|
|
30
|
+
// measured: "save" scored 29 on "Save file" but 30 on "swap pane view east"
|
|
31
|
+
// (three word-start hits). Sized to dominate realistic competitors (a
|
|
32
|
+
// non-prefix alignment outscores a prefix only past ~8 all-word-start chars);
|
|
33
|
+
// word-boundary preference WITHIN non-prefix matches is untouched ("web"
|
|
34
|
+
// still anchors on the "web" of `wavyr-website`, not the leading "w").
|
|
35
|
+
const PREFIX_EXTRA = 24;
|
|
27
36
|
|
|
28
37
|
/**
|
|
29
38
|
* Case-insensitive subsequence match of `query` against `target`.
|
|
@@ -106,6 +115,17 @@ export function fuzzyMatch(
|
|
|
106
115
|
}
|
|
107
116
|
if (start === -1) return null;
|
|
108
117
|
|
|
118
|
+
// The exact/prefix tier (M24.4): when the query IS the target's start, that
|
|
119
|
+
// alignment (positions 0..m-1) + PREFIX_EXTRA competes with the DP's best —
|
|
120
|
+
// and effectively always wins, so prefix matches rank first however many
|
|
121
|
+
// word-start bonuses a scattered alignment collected.
|
|
122
|
+
if (t.startsWith(q)) {
|
|
123
|
+
const prefixScore = m * BASE + START_BONUS + (m - 1) * CONTIGUOUS_BONUS + PREFIX_EXTRA;
|
|
124
|
+
if (prefixScore >= bestScore) {
|
|
125
|
+
return { score: prefixScore, positions: Array.from({ length: m }, (_, i) => i) };
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
109
129
|
const positions: number[] = [];
|
|
110
130
|
let j = start;
|
|
111
131
|
for (let i = 0; i < m && j !== -1; i++) {
|