tmux-ide 2.6.1 → 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 +36 -14
- package/bin/cli.js +4174 -1235
- package/bin/cli.ts +426 -72
- package/package.json +10 -6
- 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/agent-explain.d.ts +8 -1
- package/packages/daemon/dist/agent-explain.js +19 -3
- 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/tui-binary.d.ts +57 -0
- package/packages/daemon/dist/lib/tui-binary.js +130 -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/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/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 +194 -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/tui-binary.ts +165 -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 +737 -52
- package/packages/daemon/src/tui/chrome/updater.ts +318 -30
- package/packages/daemon/src/tui/compiled.ts +11 -3
- package/packages/daemon/src/tui/detect/classify.ts +49 -0
- package/packages/daemon/src/tui/detect/manifest-loader.ts +56 -6
- package/packages/daemon/src/tui/detect/manifest.ts +39 -3
- package/packages/daemon/src/tui/detect/manifests.ts +395 -33
- package/packages/daemon/src/tui/detect/process-tree.ts +33 -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/main.ts +13 -1
- package/packages/daemon/src/tui/mirror/ack-writer.ts +77 -0
- package/packages/daemon/src/tui/mirror/agent-chip.ts +126 -0
- package/packages/daemon/src/tui/mirror/agent-lifecycle.ts +437 -0
- package/packages/daemon/src/tui/mirror/agent-rows.ts +155 -0
- package/packages/daemon/src/tui/mirror/app-state.ts +342 -0
- package/packages/daemon/src/tui/mirror/app.tsx +7048 -0
- package/packages/daemon/src/tui/mirror/attention.ts +110 -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 +367 -0
- package/packages/daemon/src/tui/mirror/diff-model.ts +387 -0
- package/packages/daemon/src/tui/mirror/editor-buffer.ts +117 -0
- package/packages/daemon/src/tui/mirror/file-tree.ts +322 -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/host-terminal.ts +49 -0
- package/packages/daemon/src/tui/mirror/hosted.ts +205 -0
- package/packages/daemon/src/tui/mirror/input-coalescer.ts +105 -0
- package/packages/daemon/src/tui/mirror/layout-parse.ts +154 -0
- package/packages/daemon/src/tui/mirror/menu-model.ts +210 -0
- package/packages/daemon/src/tui/mirror/palette.ts +564 -0
- package/packages/daemon/src/tui/mirror/pane-mirror.ts +578 -20
- package/packages/daemon/src/tui/mirror/pane-surface.tsx +422 -0
- package/packages/daemon/src/tui/mirror/perf-tap.ts +186 -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 +376 -0
- package/packages/daemon/src/tui/mirror/session-mirror.ts +724 -0
- package/packages/daemon/src/tui/mirror/settings-model.ts +425 -0
- package/packages/daemon/src/tui/mirror/sidebar.tsx +218 -0
- package/packages/daemon/src/tui/mirror/size-truth.ts +130 -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/mirror/theme.ts +45 -0
- package/packages/daemon/src/tui/team/entry.ts +34 -7
- package/packages/daemon/src/tui/team/fuzzy.ts +20 -0
- package/packages/daemon/src/tui/team/report.ts +11 -1
- package/packages/daemon/src/tui/team/sessions.ts +184 -17
- package/packages/daemon/src/tui/team/wait.ts +144 -0
- package/scripts/build-macos-notifier.mjs +160 -0
- package/scripts/build-tui.mjs +11 -4
- package/scripts/perf-mirror.mjs +313 -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 +110 -2
- 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,425 @@
|
|
|
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-prefs.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 =
|
|
111
|
+
| "enabled"
|
|
112
|
+
| "toast"
|
|
113
|
+
| "macos"
|
|
114
|
+
| "terminal"
|
|
115
|
+
| "onBlocked"
|
|
116
|
+
| "onDone";
|
|
117
|
+
|
|
118
|
+
const onOff = (v: boolean) => (v ? "on" : "off");
|
|
119
|
+
|
|
120
|
+
/** PURE — the notification toggle rows (the REAL fields notify.ts reads: the
|
|
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. */
|
|
124
|
+
export function notificationItems(prefs: NotificationPrefs): DialogSelectItem[] {
|
|
125
|
+
return [
|
|
126
|
+
{ id: "enabled", label: "All notifications", detail: onOff(prefs.enabled) },
|
|
127
|
+
{ id: "toast", label: "In-terminal toasts", detail: onOff(prefs.toast) },
|
|
128
|
+
{ id: "macos", label: "System banners", detail: onOff(prefs.macos) },
|
|
129
|
+
{ id: "terminal", label: "Terminal banners (OSC)", detail: onOff(prefs.terminal) },
|
|
130
|
+
{ id: "onBlocked", label: "Alert when an agent needs you", detail: onOff(prefs.onBlocked) },
|
|
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) },
|
|
134
|
+
{ id: "quietHours", label: "Quiet hours…", detail: quietHoursSummary(prefs) },
|
|
135
|
+
];
|
|
136
|
+
}
|
|
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
|
+
|
|
187
|
+
/** PURE — flip one notification toggle. */
|
|
188
|
+
export function notificationTogglePatch(
|
|
189
|
+
id: NotificationToggleId,
|
|
190
|
+
prefs: NotificationPrefs,
|
|
191
|
+
): AppConfigPatch {
|
|
192
|
+
return { notifications: { [id]: !prefs[id] } };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** PURE — "22:00–08:00" or "off". */
|
|
196
|
+
export function quietHoursSummary(prefs: NotificationPrefs): string {
|
|
197
|
+
return prefs.quietHours ? `${prefs.quietHours.start}–${prefs.quietHours.end}` : "off";
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** PURE — the quiet-hours chooser rows. */
|
|
201
|
+
export function quietHoursItems(prefs: NotificationPrefs): DialogSelectItem[] {
|
|
202
|
+
return [
|
|
203
|
+
{ id: "off", label: "Off — always notify", current: prefs.quietHours === null },
|
|
204
|
+
{
|
|
205
|
+
id: "window",
|
|
206
|
+
label: "Silence banners & sounds during a daily window…",
|
|
207
|
+
detail: prefs.quietHours ? quietHoursSummary(prefs) : undefined,
|
|
208
|
+
current: prefs.quietHours !== null,
|
|
209
|
+
},
|
|
210
|
+
];
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** PURE — HH:MM validation with a plain error. */
|
|
214
|
+
export function validateQuietTime(value: string): string | null {
|
|
215
|
+
return parseHHMM(value.trim()) === null ? "Use 24-hour HH:MM — for example 22:00" : null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** PURE — persist a quiet window (times already validated). */
|
|
219
|
+
export function quietHoursPatch(start: string, end: string): AppConfigPatch {
|
|
220
|
+
return { notifications: { quietHours: { start: start.trim(), end: end.trim() } } };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** PURE — remove the quiet window. */
|
|
224
|
+
export function quietHoursOffPatch(): AppConfigPatch {
|
|
225
|
+
return { notifications: { quietHours: undefined } };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ── Updates & updater cadence ────────────────────────────────────────────────
|
|
229
|
+
|
|
230
|
+
/** PURE — the updates dialog rows: the check toggle + the two cadence numbers. */
|
|
231
|
+
export function updatesItems(cfg: AppConfig): DialogSelectItem[] {
|
|
232
|
+
return [
|
|
233
|
+
{ id: "check", label: "Check for tmux-ide updates", detail: onOff(cfg.updates.check) },
|
|
234
|
+
{
|
|
235
|
+
id: "tickMs",
|
|
236
|
+
label: "Background refresh interval…",
|
|
237
|
+
detail: `${cfg.updater.tickMs} ms`,
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
id: "snapshotEvery",
|
|
241
|
+
label: "Save a crash snapshot every…",
|
|
242
|
+
detail: `${cfg.updater.snapshotEvery} refreshes`,
|
|
243
|
+
},
|
|
244
|
+
];
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function updatesCheckPatch(cfg: AppConfig): AppConfigPatch {
|
|
248
|
+
return { updates: { check: !cfg.updates.check } };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** PURE — 250–60000 ms, whole numbers (guards a config that would spin or
|
|
252
|
+
* starve the chrome). */
|
|
253
|
+
export function validateTickMs(value: string): string | null {
|
|
254
|
+
const n = Number(value.trim());
|
|
255
|
+
if (!Number.isInteger(n) || n < 250 || n > 60_000) {
|
|
256
|
+
return "Use a whole number of milliseconds between 250 and 60000";
|
|
257
|
+
}
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** PURE — 1–1000 refreshes. */
|
|
262
|
+
export function validateSnapshotEvery(value: string): string | null {
|
|
263
|
+
const n = Number(value.trim());
|
|
264
|
+
if (!Number.isInteger(n) || n < 1 || n > 1000) {
|
|
265
|
+
return "Use a whole number between 1 and 1000";
|
|
266
|
+
}
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function tickMsPatch(value: string): AppConfigPatch {
|
|
271
|
+
return { updater: { tickMs: Number(value.trim()) } };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function snapshotEveryPatch(value: string): AppConfigPatch {
|
|
275
|
+
return { updater: { snapshotEvery: Number(value.trim()) } };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// ── Crash restore ────────────────────────────────────────────────────────────
|
|
279
|
+
|
|
280
|
+
/** PURE — the restore On/Off rows. */
|
|
281
|
+
export function restoreItems(cfg: AppConfig): DialogSelectItem[] {
|
|
282
|
+
return [
|
|
283
|
+
{
|
|
284
|
+
id: "on",
|
|
285
|
+
label: "Revive agents automatically",
|
|
286
|
+
detail: "restore resumes claude conversations",
|
|
287
|
+
current: cfg.restore.resumeAgents,
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
id: "off",
|
|
291
|
+
label: "Rebuild sessions only",
|
|
292
|
+
detail: "agents stay stopped",
|
|
293
|
+
current: !cfg.restore.resumeAgents,
|
|
294
|
+
},
|
|
295
|
+
];
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export function restorePatch(id: string): AppConfigPatch {
|
|
299
|
+
return { restore: { resumeAgents: id === "on" } };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// ── Keyboard shortcut viewer (read-only) ─────────────────────────────────────
|
|
303
|
+
|
|
304
|
+
/** Mirrors {@link ../chrome/statusline.ts}'s prefix-twin derivation — the
|
|
305
|
+
* letters tmux binds by default (never clobbered) and the documented remaps.
|
|
306
|
+
* A drift test asserts agreement with `prefixKeyBinds` for the defaults. */
|
|
307
|
+
const PREFIX_TAKEN = new Set([..."cdfilmnopqrstwxz"]);
|
|
308
|
+
const PREFIX_REMAP: Record<string, string> = { "M-m": "u", "M-p": "j", "M-,": "v" };
|
|
309
|
+
|
|
310
|
+
/** PURE — the reliable `prefix <letter>` twin for an Alt key, or null when the
|
|
311
|
+
* letter is taken by a stock tmux bind and has no documented remap. */
|
|
312
|
+
export function prefixTwinFor(altKey: string): string | null {
|
|
313
|
+
const remapped = PREFIX_REMAP[altKey];
|
|
314
|
+
if (remapped) return remapped;
|
|
315
|
+
const letter = /^M-([a-z])$/.exec(altKey)?.[1];
|
|
316
|
+
if (!letter || PREFIX_TAKEN.has(letter)) return null;
|
|
317
|
+
return letter;
|
|
318
|
+
}
|
|
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
|
+
|
|
343
|
+
/** PURE — the read-only shortcut rows: the chrome actions from the LIVE config
|
|
344
|
+
* (prefix-first, the Alt fast path second — the prefix form is the one that
|
|
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[] {
|
|
349
|
+
const chrome: Array<{ label: string; altKey: string }> = [
|
|
350
|
+
{ label: "Home cockpit", altKey: keys.home },
|
|
351
|
+
{ label: "Session switcher", altKey: keys.popup },
|
|
352
|
+
{ label: "Cheat sheet", altKey: keys.cheatsheet },
|
|
353
|
+
{ label: "Actions menu", altKey: keys.menu },
|
|
354
|
+
{ label: "Sidebar", altKey: keys.sidebar },
|
|
355
|
+
{ label: "Explorer panel", altKey: keys.panels.explorer },
|
|
356
|
+
{ label: "Git changes panel", altKey: keys.panels.changes },
|
|
357
|
+
{ label: "Config panel", altKey: keys.panels.config },
|
|
358
|
+
];
|
|
359
|
+
const rows: DialogSelectItem[] = chrome.map(({ label, altKey }) => {
|
|
360
|
+
const twin = prefixTwinFor(altKey);
|
|
361
|
+
return {
|
|
362
|
+
id: `chrome:${label}`,
|
|
363
|
+
label,
|
|
364
|
+
detail: twin ? `prefix ${twin} · ${altKey}` : altKey,
|
|
365
|
+
};
|
|
366
|
+
});
|
|
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
|
+
}
|
|
371
|
+
return rows;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// ── The umbrella + reset ─────────────────────────────────────────────────────
|
|
375
|
+
|
|
376
|
+
/** PURE — the "Settings…" umbrella rows: one per command, categorized by a
|
|
377
|
+
* `Category · name` label (flat list, fuzzy-friendly), each showing its
|
|
378
|
+
* current value as the detail. Reset is the one destructive row. */
|
|
379
|
+
export function settingsRootItems(cfg: AppConfig, prefs: NotificationPrefs): DialogSelectItem[] {
|
|
380
|
+
const accent = cfg.theme.accent;
|
|
381
|
+
const preset = THEME_PRESETS.find((p) => p.accent === accent);
|
|
382
|
+
return [
|
|
383
|
+
{
|
|
384
|
+
id: "settings-theme",
|
|
385
|
+
label: "Appearance · Accent color",
|
|
386
|
+
detail: preset?.label.replace(" (default)", "") ?? accent,
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
id: "settings-notifications",
|
|
390
|
+
label: "Notifications · Alerts & channels",
|
|
391
|
+
detail: prefs.enabled ? "on" : "off",
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
id: "settings-quiet-hours",
|
|
395
|
+
label: "Notifications · Quiet hours",
|
|
396
|
+
detail: quietHoursSummary(prefs),
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
id: "settings-updates",
|
|
400
|
+
label: "Updates · Checks & refresh",
|
|
401
|
+
detail: cfg.updates.check ? "checking" : "off",
|
|
402
|
+
},
|
|
403
|
+
{
|
|
404
|
+
id: "settings-restore",
|
|
405
|
+
label: "Sessions · Crash restore",
|
|
406
|
+
detail: cfg.restore.resumeAgents ? "revives agents" : "sessions only",
|
|
407
|
+
},
|
|
408
|
+
{ id: "settings-keys", label: "Keyboard · Shortcuts", detail: "view" },
|
|
409
|
+
{ id: "settings-reset", label: "Reset · All settings to defaults", danger: true },
|
|
410
|
+
];
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/** PURE — the reset patch: DELETE the blocks the settings surface manages so
|
|
414
|
+
* the parser's defaults take over. Key binds and unknown user fields are
|
|
415
|
+
* deliberately preserved — reset returns the LOOK and BEHAVIOR to stock
|
|
416
|
+
* without silently unbinding the user's keys. */
|
|
417
|
+
export function resetSettingsPatch(): AppConfigPatch {
|
|
418
|
+
return {
|
|
419
|
+
theme: undefined,
|
|
420
|
+
notifications: undefined,
|
|
421
|
+
updater: undefined,
|
|
422
|
+
updates: undefined,
|
|
423
|
+
restore: undefined,
|
|
424
|
+
};
|
|
425
|
+
}
|
|
@@ -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
|
+
}
|