@towles/tool 0.0.107 → 0.0.108
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 +7 -1
- package/package.json +2 -1
- package/plugins/tt-agentboard/README.md +160 -0
- package/plugins/tt-agentboard/apps/server/package.json +20 -0
- package/plugins/tt-agentboard/apps/server/src/main.ts +60 -0
- package/plugins/tt-agentboard/apps/tui/build.ts +11 -0
- package/plugins/tt-agentboard/apps/tui/bunfig.toml +1 -0
- package/plugins/tt-agentboard/apps/tui/package.json +23 -0
- package/plugins/tt-agentboard/apps/tui/scripts/sessionizer.sh +36 -0
- package/plugins/tt-agentboard/apps/tui/src/components/DetailPanel.tsx +350 -0
- package/plugins/tt-agentboard/apps/tui/src/components/DiffStats.tsx +33 -0
- package/plugins/tt-agentboard/apps/tui/src/components/SessionCard.tsx +177 -0
- package/plugins/tt-agentboard/apps/tui/src/components/StatusBar.tsx +49 -0
- package/plugins/tt-agentboard/apps/tui/src/constants.ts +46 -0
- package/plugins/tt-agentboard/apps/tui/src/detail-panel-height.ts +21 -0
- package/plugins/tt-agentboard/apps/tui/src/index.tsx +880 -0
- package/plugins/tt-agentboard/apps/tui/src/mux-context.ts +61 -0
- package/plugins/tt-agentboard/apps/tui/tsconfig.json +15 -0
- package/plugins/tt-agentboard/bun.lock +444 -0
- package/plugins/tt-agentboard/package.json +26 -0
- package/plugins/tt-agentboard/packages/mux-tmux/package.json +14 -0
- package/plugins/tt-agentboard/packages/mux-tmux/src/client.ts +550 -0
- package/plugins/tt-agentboard/packages/mux-tmux/src/index.ts +18 -0
- package/plugins/tt-agentboard/packages/mux-tmux/src/provider.ts +259 -0
- package/plugins/tt-agentboard/packages/mux-tmux/tsconfig.json +13 -0
- package/plugins/tt-agentboard/packages/runtime/package.json +14 -0
- package/plugins/tt-agentboard/packages/runtime/src/agents/tracker.ts +233 -0
- package/plugins/tt-agentboard/packages/runtime/src/agents/watchers/amp.ts +316 -0
- package/plugins/tt-agentboard/packages/runtime/src/agents/watchers/claude-code.ts +374 -0
- package/plugins/tt-agentboard/packages/runtime/src/agents/watchers/codex.ts +364 -0
- package/plugins/tt-agentboard/packages/runtime/src/agents/watchers/opencode.ts +249 -0
- package/plugins/tt-agentboard/packages/runtime/src/config.ts +70 -0
- package/plugins/tt-agentboard/packages/runtime/src/contracts/agent-watcher.ts +38 -0
- package/plugins/tt-agentboard/packages/runtime/src/contracts/agent.ts +16 -0
- package/plugins/tt-agentboard/packages/runtime/src/contracts/index.ts +3 -0
- package/plugins/tt-agentboard/packages/runtime/src/contracts/mux.ts +148 -0
- package/plugins/tt-agentboard/packages/runtime/src/debug.ts +19 -0
- package/plugins/tt-agentboard/packages/runtime/src/index.ts +69 -0
- package/plugins/tt-agentboard/packages/runtime/src/mux/detect.ts +20 -0
- package/plugins/tt-agentboard/packages/runtime/src/mux/registry.ts +45 -0
- package/plugins/tt-agentboard/packages/runtime/src/plugins/loader.ts +152 -0
- package/plugins/tt-agentboard/packages/runtime/src/server/context.ts +112 -0
- package/plugins/tt-agentboard/packages/runtime/src/server/git-info.ts +164 -0
- package/plugins/tt-agentboard/packages/runtime/src/server/index.ts +1753 -0
- package/plugins/tt-agentboard/packages/runtime/src/server/launcher.ts +71 -0
- package/plugins/tt-agentboard/packages/runtime/src/server/metadata-store.ts +86 -0
- package/plugins/tt-agentboard/packages/runtime/src/server/pane-scanner.ts +327 -0
- package/plugins/tt-agentboard/packages/runtime/src/server/port-scanner.ts +155 -0
- package/plugins/tt-agentboard/packages/runtime/src/server/session-order.ts +127 -0
- package/plugins/tt-agentboard/packages/runtime/src/server/sidebar-manager.ts +232 -0
- package/plugins/tt-agentboard/packages/runtime/src/server/sidebar-width-sync.ts +66 -0
- package/plugins/tt-agentboard/packages/runtime/src/shared.ts +179 -0
- package/plugins/tt-agentboard/packages/runtime/src/themes.ts +750 -0
- package/plugins/tt-agentboard/packages/runtime/test/config.test.ts +83 -0
- package/plugins/tt-agentboard/packages/runtime/test/tracker.test.ts +172 -0
- package/plugins/tt-agentboard/packages/runtime/tsconfig.json +13 -0
- package/plugins/tt-agentboard/tsconfig.json +19 -0
- package/plugins/tt-auto-claude/.claude-plugin/plugin.json +8 -0
- package/plugins/tt-auto-claude/commands/create-issue.md +20 -0
- package/plugins/tt-auto-claude/commands/list.md +21 -0
- package/plugins/tt-auto-claude/skills/auto-claude/SKILL.md +71 -0
- package/plugins/tt-core/.claude-plugin/plugin.json +8 -0
- package/plugins/tt-core/README.md +18 -0
- package/plugins/tt-core/commands/improve-architecture.md +66 -0
- package/plugins/tt-core/commands/interview-me.md +38 -0
- package/plugins/tt-core/commands/prd-to-issues.md +49 -0
- package/plugins/tt-core/commands/refine-text.md +30 -0
- package/plugins/tt-core/commands/task.md +37 -0
- package/plugins/tt-core/commands/tdd.md +69 -0
- package/plugins/tt-core/commands/write-prd.md +69 -0
- package/plugins/tt-core/promptfooconfig.interview-me.yaml +155 -0
- package/plugins/tt-core/promptfooconfig.refine-text.yaml +242 -0
- package/plugins/tt-core/promptfooconfig.tdd.yaml +144 -0
- package/plugins/tt-core/promptfooconfig.write-prd.yaml +145 -0
- package/plugins/tt-core/skills/towles-tool/SKILL.md +35 -0
- package/src/commands/agentboard.ts +19 -2
|
@@ -0,0 +1,880 @@
|
|
|
1
|
+
import { render, useKeyboard, useRenderer } from "@opentui/solid";
|
|
2
|
+
import { appendFileSync } from "node:fs";
|
|
3
|
+
import {
|
|
4
|
+
createSignal,
|
|
5
|
+
createEffect,
|
|
6
|
+
onCleanup,
|
|
7
|
+
onMount,
|
|
8
|
+
batch,
|
|
9
|
+
For,
|
|
10
|
+
Show,
|
|
11
|
+
createMemo,
|
|
12
|
+
createSelector,
|
|
13
|
+
} from "solid-js";
|
|
14
|
+
import type { Accessor } from "solid-js";
|
|
15
|
+
import { createStore, reconcile } from "solid-js/store";
|
|
16
|
+
import { TextAttributes } from "@opentui/core";
|
|
17
|
+
import type { MouseEvent } from "@opentui/core";
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
ensureServer,
|
|
21
|
+
SERVER_PORT,
|
|
22
|
+
SERVER_HOST,
|
|
23
|
+
loadConfig,
|
|
24
|
+
resolveTheme,
|
|
25
|
+
saveConfig,
|
|
26
|
+
} from "@tt-agentboard/runtime";
|
|
27
|
+
import type { ServerMessage, SessionData, ClientCommand, Theme } from "@tt-agentboard/runtime";
|
|
28
|
+
import { TmuxClient } from "@tt-agentboard/mux-tmux";
|
|
29
|
+
import { SessionCard } from "./components/SessionCard";
|
|
30
|
+
import { DetailPanel } from "./components/DetailPanel";
|
|
31
|
+
import { StatusBar } from "./components/StatusBar";
|
|
32
|
+
|
|
33
|
+
// Detect tmux context (tmux only)
|
|
34
|
+
type MuxContext = { type: "tmux"; sdk: TmuxClient; paneId: string } | { type: "none" };
|
|
35
|
+
|
|
36
|
+
function detectMuxContext(): MuxContext {
|
|
37
|
+
if (process.env.TMUX_PANE && process.env.TMUX) {
|
|
38
|
+
return { type: "tmux", sdk: new TmuxClient(), paneId: process.env.TMUX_PANE };
|
|
39
|
+
}
|
|
40
|
+
return { type: "none" };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const muxCtx = detectMuxContext();
|
|
44
|
+
|
|
45
|
+
const SPINNERS = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
46
|
+
const BOLD = TextAttributes.BOLD;
|
|
47
|
+
const DIM = TextAttributes.DIM;
|
|
48
|
+
const DEFAULT_DETAIL_PANEL_HEIGHT = 10;
|
|
49
|
+
const MIN_DETAIL_PANEL_HEIGHT = 4;
|
|
50
|
+
const DIVIDER = "─".repeat(200);
|
|
51
|
+
const RESIZE_DEBUG_LOG = "/tmp/agentboard-tui-resize.log";
|
|
52
|
+
|
|
53
|
+
const TUI_DEBUG = !!process.env.TT_AGENTBOARD_DEBUG;
|
|
54
|
+
|
|
55
|
+
function logResizeDebug(message: string, data?: Record<string, unknown>): void {
|
|
56
|
+
if (!TUI_DEBUG) return;
|
|
57
|
+
const ts = new Date().toISOString();
|
|
58
|
+
const extra = data ? ` ${JSON.stringify(data)}` : "";
|
|
59
|
+
try {
|
|
60
|
+
appendFileSync(RESIZE_DEBUG_LOG, `[${ts}] [pid:${process.pid}] ${message}${extra}\n`);
|
|
61
|
+
} catch {}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function clampDetailPanelHeight(height: number): number {
|
|
65
|
+
return Math.max(MIN_DETAIL_PANEL_HEIGHT, Math.round(height));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function getStoredDetailPanelHeight(sessionName: string): number {
|
|
69
|
+
const stored = loadConfig().detailPanelHeights?.[sessionName];
|
|
70
|
+
return typeof stored === "number" ? clampDetailPanelHeight(stored) : DEFAULT_DETAIL_PANEL_HEIGHT;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function persistDetailPanelHeight(sessionName: string, height: number): void {
|
|
74
|
+
const config = loadConfig();
|
|
75
|
+
saveConfig({
|
|
76
|
+
detailPanelHeights: {
|
|
77
|
+
...(config.detailPanelHeights ?? {}),
|
|
78
|
+
[sessionName]: clampDetailPanelHeight(height),
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Refocus the main (non-sidebar) pane after TUI capability detection finishes.
|
|
84
|
+
* This must happen from the TUI process — doing it from the server races with
|
|
85
|
+
* capability query responses and leaks escape sequences to the main pane. */
|
|
86
|
+
function refocusMainPane() {
|
|
87
|
+
if (muxCtx.type === "tmux") {
|
|
88
|
+
try {
|
|
89
|
+
// Use the TUI's own pane ID to find its current window (handles stash restore
|
|
90
|
+
// where the pane may have moved to a different window than the original).
|
|
91
|
+
const windowId =
|
|
92
|
+
process.env.REFOCUS_WINDOW ||
|
|
93
|
+
Bun.spawnSync(["tmux", "display-message", "-t", muxCtx.paneId, "-p", "#{window_id}"], {
|
|
94
|
+
stdout: "pipe",
|
|
95
|
+
stderr: "pipe",
|
|
96
|
+
})
|
|
97
|
+
.stdout.toString()
|
|
98
|
+
.trim();
|
|
99
|
+
if (!windowId) return;
|
|
100
|
+
const r = Bun.spawnSync(
|
|
101
|
+
["tmux", "list-panes", "-t", windowId, "-F", "#{pane_id} #{pane_title}"],
|
|
102
|
+
{ stdout: "pipe", stderr: "pipe" },
|
|
103
|
+
);
|
|
104
|
+
const lines = r.stdout.toString().trim().split("\n");
|
|
105
|
+
const main = lines.find((l) => !l.includes("agentboard-sidebar"));
|
|
106
|
+
if (main) {
|
|
107
|
+
const paneId = main.split(" ")[0];
|
|
108
|
+
Bun.spawnSync(["tmux", "select-pane", "-t", paneId], { stdout: "pipe", stderr: "pipe" });
|
|
109
|
+
}
|
|
110
|
+
} catch {}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function getClientTty(): string {
|
|
115
|
+
if (muxCtx.type === "tmux") {
|
|
116
|
+
const { sdk, paneId } = muxCtx;
|
|
117
|
+
const sessName = sdk.display("#{session_name}", { target: paneId });
|
|
118
|
+
if (sessName) {
|
|
119
|
+
const clients = sdk.listClients();
|
|
120
|
+
const client = clients.find((c) => c.sessionName === sessName);
|
|
121
|
+
if (client) return client.tty;
|
|
122
|
+
}
|
|
123
|
+
return sdk.getClientTty();
|
|
124
|
+
}
|
|
125
|
+
return "";
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function getLocalSessionName(): string | null {
|
|
129
|
+
if (muxCtx.type === "tmux") {
|
|
130
|
+
const sessionName = muxCtx.sdk.display("#{session_name}", { target: muxCtx.paneId });
|
|
131
|
+
return sessionName || null;
|
|
132
|
+
}
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function App() {
|
|
137
|
+
const renderer = useRenderer();
|
|
138
|
+
|
|
139
|
+
// --- Theme state (driven by server) ---
|
|
140
|
+
const [theme, setTheme] = createSignal<Theme>(resolveTheme(undefined));
|
|
141
|
+
const P = () => theme().palette;
|
|
142
|
+
const S = () => theme().status;
|
|
143
|
+
|
|
144
|
+
const [sessions, setSessions] = createStore<SessionData[]>([]);
|
|
145
|
+
const [focusedSession, setFocusedSession] = createSignal<string | null>(null);
|
|
146
|
+
const [currentSession, setCurrentSession] = createSignal<string | null>(null);
|
|
147
|
+
const [mySession, setMySession] = createSignal<string | null>(null);
|
|
148
|
+
const [connected, setConnected] = createSignal(false);
|
|
149
|
+
const [spinIdx, setSpinIdx] = createSignal(0);
|
|
150
|
+
const [detailPanelHeight, setDetailPanelHeight] = createSignal(DEFAULT_DETAIL_PANEL_HEIGHT);
|
|
151
|
+
const [isDetailResizeHover, setIsDetailResizeHover] = createSignal(false);
|
|
152
|
+
const [isDetailResizing, setIsDetailResizing] = createSignal(false);
|
|
153
|
+
const detailPanelSessionName = createMemo(() => focusedSession() ?? mySession());
|
|
154
|
+
|
|
155
|
+
// --- Panel focus: sessions list vs agent detail ---
|
|
156
|
+
type PanelFocus = "sessions" | "agents";
|
|
157
|
+
const [panelFocus, setPanelFocus] = createSignal<PanelFocus>("sessions");
|
|
158
|
+
const [focusedAgentIdx, setFocusedAgentIdx] = createSignal(0);
|
|
159
|
+
|
|
160
|
+
// --- Modal state ---
|
|
161
|
+
const [modal, setModal] = createSignal<"none" | "confirm-kill" | "help">("none");
|
|
162
|
+
const [killTarget, setKillTarget] = createSignal<string | null>(null);
|
|
163
|
+
|
|
164
|
+
const [clientTty, setClientTty] = createSignal(getClientTty());
|
|
165
|
+
let ws: WebSocket | null = null;
|
|
166
|
+
let startupFocusSynced = false;
|
|
167
|
+
let detailResizeStartY = 0;
|
|
168
|
+
let detailResizeStartHeight = DEFAULT_DETAIL_PANEL_HEIGHT;
|
|
169
|
+
const startupSessionName = getLocalSessionName();
|
|
170
|
+
|
|
171
|
+
const focusedData = createMemo(() => sessions.find((s) => s.name === focusedSession()) ?? null);
|
|
172
|
+
|
|
173
|
+
function send(cmd: ClientCommand) {
|
|
174
|
+
if (connected() && ws) ws.send(JSON.stringify(cmd));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function switchToSession(name: string) {
|
|
178
|
+
// Optimistic local update — makes rapid Tab repeat instant by removing
|
|
179
|
+
// the server/hook round-trip from the next-Tab decision.
|
|
180
|
+
// The server's focus/state broadcast will reconcile if needed.
|
|
181
|
+
setCurrentSession(name);
|
|
182
|
+
setFocusedSession(name);
|
|
183
|
+
setPanelFocus("sessions");
|
|
184
|
+
setFocusedAgentIdx(0);
|
|
185
|
+
send({ type: "switch-session", name });
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function reIdentify() {
|
|
189
|
+
const sessionName = getLocalSessionName();
|
|
190
|
+
if (!sessionName) return;
|
|
191
|
+
|
|
192
|
+
if (muxCtx.type === "tmux") {
|
|
193
|
+
send({ type: "identify-pane", paneId: muxCtx.paneId, sessionName });
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function moveLocalFocus(delta: -1 | 1) {
|
|
198
|
+
const list = sessions;
|
|
199
|
+
if (list.length === 0) return;
|
|
200
|
+
|
|
201
|
+
const current = focusedSession();
|
|
202
|
+
const currentIdx = Math.max(
|
|
203
|
+
0,
|
|
204
|
+
list.findIndex((s) => s.name === current),
|
|
205
|
+
);
|
|
206
|
+
const nextIdx = Math.max(0, Math.min(list.length - 1, currentIdx + delta));
|
|
207
|
+
const next = list[nextIdx]?.name ?? null;
|
|
208
|
+
|
|
209
|
+
if (!next || next === current) return;
|
|
210
|
+
|
|
211
|
+
setFocusedSession(next);
|
|
212
|
+
send({ type: "focus-session", name: next });
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function moveAgentFocus(delta: -1 | 1) {
|
|
216
|
+
const data = focusedData();
|
|
217
|
+
const agents = data?.agents ?? [];
|
|
218
|
+
if (agents.length === 0) return;
|
|
219
|
+
const idx = focusedAgentIdx();
|
|
220
|
+
const next = Math.max(0, Math.min(agents.length - 1, idx + delta));
|
|
221
|
+
setFocusedAgentIdx(next);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function activateFocusedAgent() {
|
|
225
|
+
const data = focusedData();
|
|
226
|
+
const agents = data?.agents ?? [];
|
|
227
|
+
const agent = agents[focusedAgentIdx()];
|
|
228
|
+
if (!agent || !data) return;
|
|
229
|
+
if (TUI_DEBUG)
|
|
230
|
+
appendFileSync(
|
|
231
|
+
"/tmp/agentboard-tui-agent-click.log",
|
|
232
|
+
`[${new Date().toISOString()}] keyboard focus-agent-pane session=${data.name} agent=${agent.agent} threadId=${agent.threadId} threadName=${agent.threadName}\n`,
|
|
233
|
+
);
|
|
234
|
+
send({
|
|
235
|
+
type: "focus-agent-pane",
|
|
236
|
+
session: data.name,
|
|
237
|
+
agent: agent.agent,
|
|
238
|
+
threadId: agent.threadId,
|
|
239
|
+
threadName: agent.threadName,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function dismissFocusedAgent() {
|
|
244
|
+
const data = focusedData();
|
|
245
|
+
const agents = data?.agents ?? [];
|
|
246
|
+
const agent = agents[focusedAgentIdx()];
|
|
247
|
+
if (!agent || !data) return;
|
|
248
|
+
send({
|
|
249
|
+
type: "dismiss-agent",
|
|
250
|
+
session: data.name,
|
|
251
|
+
agent: agent.agent,
|
|
252
|
+
threadId: agent.threadId,
|
|
253
|
+
});
|
|
254
|
+
// Adjust index if we dismissed the last item
|
|
255
|
+
if (focusedAgentIdx() >= agents.length - 1 && agents.length > 1) {
|
|
256
|
+
setFocusedAgentIdx(agents.length - 2);
|
|
257
|
+
}
|
|
258
|
+
// If no agents left, go back to sessions
|
|
259
|
+
if (agents.length <= 1) setPanelFocus("sessions");
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function killFocusedAgentPane() {
|
|
263
|
+
const data = focusedData();
|
|
264
|
+
const agents = data?.agents ?? [];
|
|
265
|
+
const agent = agents[focusedAgentIdx()];
|
|
266
|
+
if (!agent || !data) return;
|
|
267
|
+
send({
|
|
268
|
+
type: "kill-agent-pane",
|
|
269
|
+
session: data.name,
|
|
270
|
+
agent: agent.agent,
|
|
271
|
+
threadId: agent.threadId,
|
|
272
|
+
threadName: agent.threadName,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function beginDetailResize(event: MouseEvent) {
|
|
277
|
+
logResizeDebug("beginDetailResize", {
|
|
278
|
+
button: event.button,
|
|
279
|
+
x: event.x,
|
|
280
|
+
y: event.y,
|
|
281
|
+
currentHeight: detailPanelHeight(),
|
|
282
|
+
session: detailPanelSessionName(),
|
|
283
|
+
target: event.target?.id ?? null,
|
|
284
|
+
});
|
|
285
|
+
if (event.button !== 0) return;
|
|
286
|
+
(renderer as any).setCapturedRenderable?.(event.target ?? undefined);
|
|
287
|
+
detailResizeStartY = event.y;
|
|
288
|
+
detailResizeStartHeight = detailPanelHeight();
|
|
289
|
+
setIsDetailResizing(true);
|
|
290
|
+
event.stopPropagation();
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function handleDetailResizeDrag(event: MouseEvent) {
|
|
294
|
+
logResizeDebug("handleDetailResizeDrag", {
|
|
295
|
+
x: event.x,
|
|
296
|
+
y: event.y,
|
|
297
|
+
isResizing: isDetailResizing(),
|
|
298
|
+
startY: detailResizeStartY,
|
|
299
|
+
startHeight: detailResizeStartHeight,
|
|
300
|
+
currentHeight: detailPanelHeight(),
|
|
301
|
+
session: detailPanelSessionName(),
|
|
302
|
+
});
|
|
303
|
+
if (!isDetailResizing()) return;
|
|
304
|
+
const delta = detailResizeStartY - event.y;
|
|
305
|
+
const nextHeight = clampDetailPanelHeight(detailResizeStartHeight + delta);
|
|
306
|
+
setDetailPanelHeight(nextHeight);
|
|
307
|
+
logResizeDebug("handleDetailResizeDrag:applied", {
|
|
308
|
+
delta,
|
|
309
|
+
nextHeight,
|
|
310
|
+
session: detailPanelSessionName(),
|
|
311
|
+
});
|
|
312
|
+
event.stopPropagation();
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function endDetailResize(event?: MouseEvent) {
|
|
316
|
+
logResizeDebug("endDetailResize", {
|
|
317
|
+
x: event?.x,
|
|
318
|
+
y: event?.y,
|
|
319
|
+
isResizing: isDetailResizing(),
|
|
320
|
+
currentHeight: detailPanelHeight(),
|
|
321
|
+
session: detailPanelSessionName(),
|
|
322
|
+
target: event?.target?.id ?? null,
|
|
323
|
+
});
|
|
324
|
+
if (!isDetailResizing()) return;
|
|
325
|
+
(renderer as any).setCapturedRenderable?.(undefined);
|
|
326
|
+
setIsDetailResizing(false);
|
|
327
|
+
setIsDetailResizeHover(false);
|
|
328
|
+
|
|
329
|
+
const sessionName = detailPanelSessionName();
|
|
330
|
+
if (sessionName) {
|
|
331
|
+
persistDetailPanelHeight(sessionName, detailPanelHeight());
|
|
332
|
+
logResizeDebug("endDetailResize:persisted", {
|
|
333
|
+
session: sessionName,
|
|
334
|
+
height: detailPanelHeight(),
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
event?.stopPropagation();
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function createNewSession() {
|
|
342
|
+
if (muxCtx.type !== "tmux") {
|
|
343
|
+
send({ type: "new-session" });
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
const scriptPath = new URL("../scripts/sessionizer.sh", import.meta.url).pathname;
|
|
347
|
+
muxCtx.sdk.displayPopup({
|
|
348
|
+
command: `bash "${scriptPath}"`,
|
|
349
|
+
title: " new session ",
|
|
350
|
+
width: "60%",
|
|
351
|
+
height: "60%",
|
|
352
|
+
closeOnExit: true,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
onMount(() => {
|
|
357
|
+
logResizeDebug("mount", {
|
|
358
|
+
startupSessionName,
|
|
359
|
+
localSessionName: getLocalSessionName(),
|
|
360
|
+
muxType: muxCtx.type,
|
|
361
|
+
tmuxPane: process.env.TMUX_PANE ?? null,
|
|
362
|
+
});
|
|
363
|
+
// Refocus the main pane once terminal capability detection finishes.
|
|
364
|
+
// This avoids the race where the server refocuses too early and capability
|
|
365
|
+
// responses leak as garbage text into the main pane.
|
|
366
|
+
let startupRefocused = false;
|
|
367
|
+
const doStartupRefocus = () => {
|
|
368
|
+
if (startupRefocused) return;
|
|
369
|
+
startupRefocused = true;
|
|
370
|
+
refocusMainPane();
|
|
371
|
+
};
|
|
372
|
+
renderer.on("capabilities", doStartupRefocus);
|
|
373
|
+
// Fallback: if no capability response arrives within 2s, refocus anyway
|
|
374
|
+
const refocusTimeout = setTimeout(doStartupRefocus, 2000);
|
|
375
|
+
|
|
376
|
+
onCleanup(() => {
|
|
377
|
+
clearTimeout(refocusTimeout);
|
|
378
|
+
renderer.removeListener("capabilities", doStartupRefocus);
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
const socket = new WebSocket(`ws://${SERVER_HOST}:${SERVER_PORT}`);
|
|
382
|
+
ws = socket;
|
|
383
|
+
|
|
384
|
+
socket.onopen = () => {
|
|
385
|
+
setConnected(true);
|
|
386
|
+
const tty = clientTty();
|
|
387
|
+
if (tty) send({ type: "identify", clientTty: tty });
|
|
388
|
+
reIdentify();
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
socket.onmessage = (event) => {
|
|
392
|
+
try {
|
|
393
|
+
const msg = JSON.parse(event.data as string) as ServerMessage;
|
|
394
|
+
let startupFocusToPublish: string | null = null;
|
|
395
|
+
batch(() => {
|
|
396
|
+
if (msg.type === "state") {
|
|
397
|
+
const startupFocus =
|
|
398
|
+
!startupFocusSynced &&
|
|
399
|
+
startupSessionName &&
|
|
400
|
+
msg.sessions.some((session) => session.name === startupSessionName)
|
|
401
|
+
? startupSessionName
|
|
402
|
+
: msg.focusedSession;
|
|
403
|
+
|
|
404
|
+
if (startupFocus === startupSessionName) {
|
|
405
|
+
startupFocusSynced = true;
|
|
406
|
+
if (msg.focusedSession !== startupSessionName) {
|
|
407
|
+
startupFocusToPublish = startupSessionName;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
setSessions(reconcile(msg.sessions, { key: "name" }));
|
|
412
|
+
setFocusedSession(startupFocus);
|
|
413
|
+
setCurrentSession(msg.currentSession);
|
|
414
|
+
setTheme(resolveTheme(msg.theme));
|
|
415
|
+
} else if (msg.type === "focus") {
|
|
416
|
+
setFocusedSession(msg.focusedSession);
|
|
417
|
+
setCurrentSession(msg.currentSession);
|
|
418
|
+
} else if (msg.type === "your-session") {
|
|
419
|
+
setMySession(msg.name);
|
|
420
|
+
if (msg.clientTty) setClientTty(msg.clientTty);
|
|
421
|
+
|
|
422
|
+
if (!startupFocusSynced && sessions.some((session) => session.name === msg.name)) {
|
|
423
|
+
startupFocusSynced = true;
|
|
424
|
+
setFocusedSession(msg.name);
|
|
425
|
+
if (focusedSession() !== msg.name) {
|
|
426
|
+
startupFocusToPublish = msg.name;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
} else if (msg.type === "re-identify") {
|
|
430
|
+
reIdentify();
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
if (startupFocusToPublish) {
|
|
435
|
+
send({ type: "focus-session", name: startupFocusToPublish });
|
|
436
|
+
}
|
|
437
|
+
} catch {}
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
socket.onclose = () => {
|
|
441
|
+
setConnected(false);
|
|
442
|
+
renderer.destroy();
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
onCleanup(() => socket.close());
|
|
446
|
+
|
|
447
|
+
// Listen for quit messages from server
|
|
448
|
+
socket.addEventListener("message", (event) => {
|
|
449
|
+
try {
|
|
450
|
+
const msg = JSON.parse(event.data as string);
|
|
451
|
+
if (msg.type === "quit") {
|
|
452
|
+
if (ws) ws.close();
|
|
453
|
+
renderer.destroy();
|
|
454
|
+
}
|
|
455
|
+
} catch {}
|
|
456
|
+
});
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
const hasRunning = createMemo(() => sessions.some((s) => s.agentState?.status === "running"));
|
|
460
|
+
|
|
461
|
+
createEffect(() => {
|
|
462
|
+
if (!hasRunning()) return;
|
|
463
|
+
const interval = setInterval(() => {
|
|
464
|
+
setSpinIdx((i) => (i + 1) % SPINNERS.length);
|
|
465
|
+
}, 120);
|
|
466
|
+
onCleanup(() => clearInterval(interval));
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
createEffect(() => {
|
|
470
|
+
const sessionName = detailPanelSessionName();
|
|
471
|
+
if (!sessionName) return;
|
|
472
|
+
const storedHeight = getStoredDetailPanelHeight(sessionName);
|
|
473
|
+
logResizeDebug("loadStoredDetailPanelHeight", {
|
|
474
|
+
session: sessionName,
|
|
475
|
+
storedHeight,
|
|
476
|
+
});
|
|
477
|
+
setDetailPanelHeight(storedHeight);
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
createEffect(() => {
|
|
481
|
+
logResizeDebug("detailPanelHeight:changed", {
|
|
482
|
+
height: detailPanelHeight(),
|
|
483
|
+
session: detailPanelSessionName(),
|
|
484
|
+
isResizing: isDetailResizing(),
|
|
485
|
+
});
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
useKeyboard((key) => {
|
|
489
|
+
const currentModal = modal();
|
|
490
|
+
|
|
491
|
+
// --- Help modal ---
|
|
492
|
+
if (currentModal === "help") {
|
|
493
|
+
setModal("none");
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// --- Confirm kill modal ---
|
|
498
|
+
if (currentModal === "confirm-kill") {
|
|
499
|
+
if (key.name === "y") {
|
|
500
|
+
const target = killTarget();
|
|
501
|
+
if (target) send({ type: "kill-session", name: target });
|
|
502
|
+
setKillTarget(null);
|
|
503
|
+
setModal("none");
|
|
504
|
+
} else {
|
|
505
|
+
setKillTarget(null);
|
|
506
|
+
setModal("none");
|
|
507
|
+
}
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// --- Normal mode keybindings ---
|
|
512
|
+
// Alt+Up / Alt+Down → reorder session
|
|
513
|
+
if ((key.meta || key.option) && (key.name === "up" || key.name === "down")) {
|
|
514
|
+
const focused = focusedSession();
|
|
515
|
+
if (focused) {
|
|
516
|
+
const delta: -1 | 1 = key.name === "up" ? -1 : 1;
|
|
517
|
+
send({ type: "reorder-session", name: focused, delta });
|
|
518
|
+
}
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
switch (key.name) {
|
|
523
|
+
case "q":
|
|
524
|
+
send({ type: "quit" });
|
|
525
|
+
break;
|
|
526
|
+
case "escape":
|
|
527
|
+
if (panelFocus() === "agents") {
|
|
528
|
+
setPanelFocus("sessions");
|
|
529
|
+
}
|
|
530
|
+
break;
|
|
531
|
+
case "up":
|
|
532
|
+
case "k":
|
|
533
|
+
if (panelFocus() === "agents") {
|
|
534
|
+
moveAgentFocus(-1);
|
|
535
|
+
} else {
|
|
536
|
+
moveLocalFocus(-1);
|
|
537
|
+
}
|
|
538
|
+
break;
|
|
539
|
+
case "down":
|
|
540
|
+
case "j":
|
|
541
|
+
if (panelFocus() === "agents") {
|
|
542
|
+
moveAgentFocus(1);
|
|
543
|
+
} else {
|
|
544
|
+
moveLocalFocus(1);
|
|
545
|
+
}
|
|
546
|
+
break;
|
|
547
|
+
case "left":
|
|
548
|
+
case "h":
|
|
549
|
+
if (panelFocus() === "agents") {
|
|
550
|
+
setPanelFocus("sessions");
|
|
551
|
+
}
|
|
552
|
+
break;
|
|
553
|
+
case "right":
|
|
554
|
+
case "l": {
|
|
555
|
+
const data = focusedData();
|
|
556
|
+
const agents = data?.agents ?? [];
|
|
557
|
+
if (panelFocus() === "sessions" && agents.length > 0) {
|
|
558
|
+
setPanelFocus("agents");
|
|
559
|
+
setFocusedAgentIdx((idx) => Math.min(idx, agents.length - 1));
|
|
560
|
+
}
|
|
561
|
+
break;
|
|
562
|
+
}
|
|
563
|
+
case "return": {
|
|
564
|
+
if (panelFocus() === "agents") {
|
|
565
|
+
activateFocusedAgent();
|
|
566
|
+
} else {
|
|
567
|
+
const focused = focusedSession();
|
|
568
|
+
if (focused) switchToSession(focused);
|
|
569
|
+
}
|
|
570
|
+
break;
|
|
571
|
+
}
|
|
572
|
+
case "tab": {
|
|
573
|
+
const list = sessions;
|
|
574
|
+
if (list.length === 0) break;
|
|
575
|
+
const cur = currentSession();
|
|
576
|
+
const idx = list.findIndex((s) => s.name === cur);
|
|
577
|
+
const next = list[(idx + (key.shift ? list.length - 1 : 1)) % list.length];
|
|
578
|
+
if (next) switchToSession(next.name);
|
|
579
|
+
break;
|
|
580
|
+
}
|
|
581
|
+
case "r":
|
|
582
|
+
send({ type: "refresh" });
|
|
583
|
+
break;
|
|
584
|
+
case "t":
|
|
585
|
+
// reserved — was theme picker
|
|
586
|
+
break;
|
|
587
|
+
case "u":
|
|
588
|
+
send({ type: "show-all-sessions" });
|
|
589
|
+
break;
|
|
590
|
+
case "d": {
|
|
591
|
+
if (panelFocus() === "agents") {
|
|
592
|
+
dismissFocusedAgent();
|
|
593
|
+
} else {
|
|
594
|
+
const focused = focusedSession();
|
|
595
|
+
if (focused) send({ type: "hide-session", name: focused });
|
|
596
|
+
}
|
|
597
|
+
break;
|
|
598
|
+
}
|
|
599
|
+
case "x": {
|
|
600
|
+
if (panelFocus() === "agents") {
|
|
601
|
+
killFocusedAgentPane();
|
|
602
|
+
} else {
|
|
603
|
+
const focused = focusedSession();
|
|
604
|
+
if (focused) {
|
|
605
|
+
setKillTarget(focused);
|
|
606
|
+
setModal("confirm-kill");
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
break;
|
|
610
|
+
}
|
|
611
|
+
case "n":
|
|
612
|
+
case "c":
|
|
613
|
+
createNewSession();
|
|
614
|
+
break;
|
|
615
|
+
case "?":
|
|
616
|
+
setModal("help");
|
|
617
|
+
break;
|
|
618
|
+
default: {
|
|
619
|
+
if (key.number) {
|
|
620
|
+
const idx = Number.parseInt(key.name, 10) - 1;
|
|
621
|
+
const target = sessions[idx];
|
|
622
|
+
if (target) switchToSession(target.name);
|
|
623
|
+
}
|
|
624
|
+
break;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
const runningAgentCount = createMemo(() =>
|
|
630
|
+
sessions.reduce((n, s) => n + (s.agents?.filter((a) => a.status === "running").length ?? 0), 0),
|
|
631
|
+
);
|
|
632
|
+
|
|
633
|
+
const errorAgentCount = createMemo(() =>
|
|
634
|
+
sessions.reduce((n, s) => n + (s.agents?.filter((a) => a.status === "error").length ?? 0), 0),
|
|
635
|
+
);
|
|
636
|
+
|
|
637
|
+
const unseenCount = createMemo(() => sessions.filter((s) => s.unseen).length);
|
|
638
|
+
|
|
639
|
+
const isFocused = createSelector(focusedSession);
|
|
640
|
+
|
|
641
|
+
return (
|
|
642
|
+
<box flexDirection="column" flexGrow={1} backgroundColor={P().crust}>
|
|
643
|
+
{/* Header */}
|
|
644
|
+
<StatusBar
|
|
645
|
+
sessionCount={sessions.length}
|
|
646
|
+
runningCount={runningAgentCount()}
|
|
647
|
+
errorCount={errorAgentCount()}
|
|
648
|
+
unseenCount={unseenCount()}
|
|
649
|
+
theme={theme}
|
|
650
|
+
/>
|
|
651
|
+
|
|
652
|
+
{/* Session list */}
|
|
653
|
+
<scrollbox flexGrow={1} flexShrink={1} paddingTop={1}>
|
|
654
|
+
<For each={sessions}>
|
|
655
|
+
{(session, i) => (
|
|
656
|
+
<SessionCard
|
|
657
|
+
session={session}
|
|
658
|
+
index={i() + 1}
|
|
659
|
+
isFocused={isFocused(session.name)}
|
|
660
|
+
isCurrent={session.name === currentSession()}
|
|
661
|
+
spinIdx={spinIdx}
|
|
662
|
+
theme={theme}
|
|
663
|
+
statusColors={S}
|
|
664
|
+
onSelect={() => {
|
|
665
|
+
setFocusedSession(session.name);
|
|
666
|
+
send({ type: "focus-session", name: session.name });
|
|
667
|
+
switchToSession(session.name);
|
|
668
|
+
}}
|
|
669
|
+
/>
|
|
670
|
+
)}
|
|
671
|
+
</For>
|
|
672
|
+
</scrollbox>
|
|
673
|
+
|
|
674
|
+
{/* Detail panel — focused session info, draggable height */}
|
|
675
|
+
<Show when={focusedData()}>
|
|
676
|
+
{(data) => (
|
|
677
|
+
<scrollbox height={detailPanelHeight()} maxHeight={detailPanelHeight()} flexShrink={0}>
|
|
678
|
+
<DetailPanel
|
|
679
|
+
session={data()}
|
|
680
|
+
theme={theme}
|
|
681
|
+
statusColors={S}
|
|
682
|
+
spinIdx={spinIdx}
|
|
683
|
+
focusedAgentIdx={panelFocus() === "agents" ? focusedAgentIdx() : -1}
|
|
684
|
+
onDismissAgent={(agent) => {
|
|
685
|
+
send({
|
|
686
|
+
type: "dismiss-agent",
|
|
687
|
+
session: data().name,
|
|
688
|
+
agent: agent.agent,
|
|
689
|
+
threadId: agent.threadId,
|
|
690
|
+
});
|
|
691
|
+
}}
|
|
692
|
+
onFocusAgentPane={(agent) => {
|
|
693
|
+
if (TUI_DEBUG)
|
|
694
|
+
appendFileSync(
|
|
695
|
+
"/tmp/agentboard-tui-agent-click.log",
|
|
696
|
+
`[${new Date().toISOString()}] sending focus-agent-pane session=${data().name} agent=${agent.agent} threadId=${agent.threadId} threadName=${agent.threadName}\n`,
|
|
697
|
+
);
|
|
698
|
+
send({
|
|
699
|
+
type: "focus-agent-pane",
|
|
700
|
+
session: data().name,
|
|
701
|
+
agent: agent.agent,
|
|
702
|
+
threadId: agent.threadId,
|
|
703
|
+
threadName: agent.threadName,
|
|
704
|
+
});
|
|
705
|
+
}}
|
|
706
|
+
isResizeHover={isDetailResizeHover()}
|
|
707
|
+
isResizing={isDetailResizing()}
|
|
708
|
+
onResizeStart={beginDetailResize}
|
|
709
|
+
onResizeDrag={handleDetailResizeDrag}
|
|
710
|
+
onResizeEnd={endDetailResize}
|
|
711
|
+
onResizeHoverChange={setIsDetailResizeHover}
|
|
712
|
+
/>
|
|
713
|
+
</scrollbox>
|
|
714
|
+
)}
|
|
715
|
+
</Show>
|
|
716
|
+
|
|
717
|
+
{/* Footer */}
|
|
718
|
+
<box flexDirection="column" paddingLeft={1} paddingBottom={1} paddingTop={0} flexShrink={0}>
|
|
719
|
+
<box height={1}>
|
|
720
|
+
<text style={{ fg: P().surface2 }}>{DIVIDER}</text>
|
|
721
|
+
</box>
|
|
722
|
+
<Show
|
|
723
|
+
when={panelFocus() === "sessions"}
|
|
724
|
+
fallback={
|
|
725
|
+
<text>
|
|
726
|
+
<span style={{ fg: P().overlay0 }}>{"←"}</span>
|
|
727
|
+
<span style={{ fg: P().overlay1 }}>{" back "}</span>
|
|
728
|
+
<span style={{ fg: P().overlay0 }}>{"⏎"}</span>
|
|
729
|
+
<span style={{ fg: P().overlay1 }}>{" focus "}</span>
|
|
730
|
+
<span style={{ fg: P().overlay0 }}>{"d"}</span>
|
|
731
|
+
<span style={{ fg: P().overlay1 }}>{" dismiss "}</span>
|
|
732
|
+
<span style={{ fg: P().overlay0 }}>{"x"}</span>
|
|
733
|
+
<span style={{ fg: P().overlay1 }}>{" kill"}</span>
|
|
734
|
+
</text>
|
|
735
|
+
}
|
|
736
|
+
>
|
|
737
|
+
<text>
|
|
738
|
+
<span style={{ fg: P().overlay0 }}>{"⇥"}</span>
|
|
739
|
+
<span style={{ fg: P().overlay1 }}>{" cycle "}</span>
|
|
740
|
+
<span style={{ fg: P().overlay0 }}>{"⏎"}</span>
|
|
741
|
+
<span style={{ fg: P().overlay1 }}>{" go "}</span>
|
|
742
|
+
<span style={{ fg: P().overlay0 }}>{"→"}</span>
|
|
743
|
+
<span style={{ fg: P().overlay1 }}>{" detail "}</span>
|
|
744
|
+
<span style={{ fg: P().overlay0 }}>{"d"}</span>
|
|
745
|
+
<span style={{ fg: P().overlay1 }}>{" hide "}</span>
|
|
746
|
+
<span style={{ fg: P().overlay0 }}>{"x"}</span>
|
|
747
|
+
<span style={{ fg: P().overlay1 }}>{" kill"}</span>
|
|
748
|
+
</text>
|
|
749
|
+
</Show>
|
|
750
|
+
</box>
|
|
751
|
+
|
|
752
|
+
{/* Kill confirmation overlay */}
|
|
753
|
+
<Show when={modal() === "confirm-kill"}>
|
|
754
|
+
<box
|
|
755
|
+
position="absolute"
|
|
756
|
+
top={0}
|
|
757
|
+
left={0}
|
|
758
|
+
right={0}
|
|
759
|
+
bottom={0}
|
|
760
|
+
justifyContent="center"
|
|
761
|
+
alignItems="center"
|
|
762
|
+
backgroundColor="transparent"
|
|
763
|
+
>
|
|
764
|
+
<box
|
|
765
|
+
border
|
|
766
|
+
borderStyle="rounded"
|
|
767
|
+
borderColor={P().red}
|
|
768
|
+
backgroundColor={P().mantle}
|
|
769
|
+
padding={1}
|
|
770
|
+
paddingX={2}
|
|
771
|
+
flexDirection="column"
|
|
772
|
+
alignItems="center"
|
|
773
|
+
>
|
|
774
|
+
<text>
|
|
775
|
+
<span style={{ fg: P().red, attributes: BOLD }}>Kill session?</span>
|
|
776
|
+
</text>
|
|
777
|
+
<text>
|
|
778
|
+
<span style={{ fg: P().text }}>{killTarget() ?? ""}</span>
|
|
779
|
+
</text>
|
|
780
|
+
<text>
|
|
781
|
+
<span style={{ fg: P().overlay0 }}>y</span>
|
|
782
|
+
<span style={{ fg: P().overlay1 }}>/</span>
|
|
783
|
+
<span style={{ fg: P().overlay0 }}>n</span>
|
|
784
|
+
</text>
|
|
785
|
+
</box>
|
|
786
|
+
</box>
|
|
787
|
+
</Show>
|
|
788
|
+
|
|
789
|
+
{/* Help overlay */}
|
|
790
|
+
<Show when={modal() === "help"}>
|
|
791
|
+
<HelpOverlay palette={P} onClose={() => setModal("none")} />
|
|
792
|
+
</Show>
|
|
793
|
+
</box>
|
|
794
|
+
);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// --- Help Overlay ---
|
|
798
|
+
|
|
799
|
+
function HelpOverlay(props: { palette: Accessor<Theme["palette"]>; onClose: () => void }) {
|
|
800
|
+
const P = () => props.palette();
|
|
801
|
+
const keys: [string, string][] = [
|
|
802
|
+
["j/k ↑↓", "Move focus"],
|
|
803
|
+
["Enter", "Switch to session"],
|
|
804
|
+
["1-9", "Jump to session"],
|
|
805
|
+
["Tab", "Cycle sessions"],
|
|
806
|
+
["n/c", "New session"],
|
|
807
|
+
["d", "Hide session"],
|
|
808
|
+
["x", "Kill session"],
|
|
809
|
+
["r", "Refresh"],
|
|
810
|
+
["u", "Show all sessions"],
|
|
811
|
+
["→/l", "Detail panel"],
|
|
812
|
+
["←/h/Esc", "Back to sessions"],
|
|
813
|
+
["Alt+↑↓", "Reorder sessions"],
|
|
814
|
+
["q", "Quit"],
|
|
815
|
+
];
|
|
816
|
+
|
|
817
|
+
return (
|
|
818
|
+
<box
|
|
819
|
+
position="absolute"
|
|
820
|
+
top={0}
|
|
821
|
+
left={0}
|
|
822
|
+
right={0}
|
|
823
|
+
bottom={0}
|
|
824
|
+
justifyContent="center"
|
|
825
|
+
alignItems="center"
|
|
826
|
+
backgroundColor="transparent"
|
|
827
|
+
>
|
|
828
|
+
<box
|
|
829
|
+
border
|
|
830
|
+
borderStyle="rounded"
|
|
831
|
+
borderColor={P().blue}
|
|
832
|
+
backgroundColor={P().mantle}
|
|
833
|
+
padding={1}
|
|
834
|
+
flexDirection="column"
|
|
835
|
+
width={30}
|
|
836
|
+
>
|
|
837
|
+
<text>
|
|
838
|
+
<span style={{ fg: P().blue, attributes: BOLD }}>Keybindings</span>
|
|
839
|
+
</text>
|
|
840
|
+
<box height={1}>
|
|
841
|
+
<text style={{ fg: P().surface2 }}>{DIVIDER}</text>
|
|
842
|
+
</box>
|
|
843
|
+
<For each={keys}>
|
|
844
|
+
{([key, desc]) => (
|
|
845
|
+
<box flexDirection="row" paddingLeft={1}>
|
|
846
|
+
<box width={12} flexShrink={0}>
|
|
847
|
+
<text>
|
|
848
|
+
<span style={{ fg: P().sky }}>{key}</span>
|
|
849
|
+
</text>
|
|
850
|
+
</box>
|
|
851
|
+
<text truncate>
|
|
852
|
+
<span style={{ fg: P().subtext0 }}>{desc}</span>
|
|
853
|
+
</text>
|
|
854
|
+
</box>
|
|
855
|
+
)}
|
|
856
|
+
</For>
|
|
857
|
+
<box height={1}>
|
|
858
|
+
<text style={{ fg: P().surface2 }}>{DIVIDER}</text>
|
|
859
|
+
</box>
|
|
860
|
+
<text style={{ fg: P().overlay0 }}>
|
|
861
|
+
<span style={{ attributes: DIM }}>Press any key to close</span>
|
|
862
|
+
</text>
|
|
863
|
+
</box>
|
|
864
|
+
</box>
|
|
865
|
+
);
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
async function main() {
|
|
869
|
+
await ensureServer();
|
|
870
|
+
render(() => <App />, {
|
|
871
|
+
exitOnCtrlC: true,
|
|
872
|
+
targetFPS: 30,
|
|
873
|
+
useMouse: true,
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
main().catch((err) => {
|
|
878
|
+
console.error(err);
|
|
879
|
+
process.exit(1);
|
|
880
|
+
});
|