@towles/tool 0.0.110 → 0.0.112
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/package.json +1 -1
- package/packages/agentboard/apps/tui/src/index.tsx +87 -49
- package/packages/agentboard/packages/runtime/src/server/index.ts +0 -13
- package/packages/agentboard/packages/runtime/src/server/session-order.ts +6 -41
- package/packages/agentboard/packages/runtime/src/shared.ts +5 -4
- package/src/commands/agentboard.ts +9 -8
package/package.json
CHANGED
|
@@ -110,6 +110,16 @@ function App() {
|
|
|
110
110
|
const [modal, setModal] = createSignal<"none" | "confirm-kill" | "help">("none");
|
|
111
111
|
const [killTarget, setKillTarget] = createSignal<string | null>(null);
|
|
112
112
|
|
|
113
|
+
// --- Transient toast (footer) ---
|
|
114
|
+
type ToastTone = "error" | "info" | "success";
|
|
115
|
+
const [toast, setToast] = createSignal<{ message: string; tone: ToastTone } | null>(null);
|
|
116
|
+
let toastTimer: ReturnType<typeof setTimeout> | null = null;
|
|
117
|
+
function showToast(message: string, tone: ToastTone = "info") {
|
|
118
|
+
setToast({ message, tone });
|
|
119
|
+
if (toastTimer) clearTimeout(toastTimer);
|
|
120
|
+
toastTimer = setTimeout(() => setToast(null), 4000);
|
|
121
|
+
}
|
|
122
|
+
|
|
113
123
|
const [clientTty, setClientTty] = createSignal(getClientTty(muxCtx));
|
|
114
124
|
let ws: WebSocket | null = null;
|
|
115
125
|
let startupFocusSynced = false;
|
|
@@ -119,8 +129,14 @@ function App() {
|
|
|
119
129
|
|
|
120
130
|
const focusedData = createMemo(() => sessions.find((s) => s.name === focusedSession()) ?? null);
|
|
121
131
|
|
|
122
|
-
function send(cmd: ClientCommand) {
|
|
123
|
-
if (connected() && ws)
|
|
132
|
+
function send(cmd: ClientCommand, successMsg?: string): boolean {
|
|
133
|
+
if (connected() && ws) {
|
|
134
|
+
ws.send(JSON.stringify(cmd));
|
|
135
|
+
if (successMsg) showToast(successMsg, "success");
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
showToast("not connected to agentboard server", "error");
|
|
139
|
+
return false;
|
|
124
140
|
}
|
|
125
141
|
|
|
126
142
|
function switchToSession(name: string) {
|
|
@@ -180,13 +196,16 @@ function App() {
|
|
|
180
196
|
"/tmp/agentboard-tui-agent-click.log",
|
|
181
197
|
`[${new Date().toISOString()}] keyboard focus-agent-pane session=${data.name} agent=${agent.agent} threadId=${agent.threadId} threadName=${agent.threadName}\n`,
|
|
182
198
|
);
|
|
183
|
-
send(
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
199
|
+
send(
|
|
200
|
+
{
|
|
201
|
+
type: "focus-agent-pane",
|
|
202
|
+
session: data.name,
|
|
203
|
+
agent: agent.agent,
|
|
204
|
+
threadId: agent.threadId,
|
|
205
|
+
threadName: agent.threadName,
|
|
206
|
+
},
|
|
207
|
+
`focusing ${agent.agent}`,
|
|
208
|
+
);
|
|
190
209
|
}
|
|
191
210
|
|
|
192
211
|
function dismissFocusedAgent() {
|
|
@@ -194,17 +213,13 @@ function App() {
|
|
|
194
213
|
const agents = data?.agents ?? [];
|
|
195
214
|
const agent = agents[focusedAgentIdx()];
|
|
196
215
|
if (!agent || !data) return;
|
|
197
|
-
send(
|
|
198
|
-
type: "dismiss-agent",
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
threadId: agent.threadId,
|
|
202
|
-
});
|
|
203
|
-
// Adjust index if we dismissed the last item
|
|
216
|
+
send(
|
|
217
|
+
{ type: "dismiss-agent", session: data.name, agent: agent.agent, threadId: agent.threadId },
|
|
218
|
+
`dismissed ${agent.agent}`,
|
|
219
|
+
);
|
|
204
220
|
if (focusedAgentIdx() >= agents.length - 1 && agents.length > 1) {
|
|
205
221
|
setFocusedAgentIdx(agents.length - 2);
|
|
206
222
|
}
|
|
207
|
-
// If no agents left, go back to sessions
|
|
208
223
|
if (agents.length <= 1) setPanelFocus("sessions");
|
|
209
224
|
}
|
|
210
225
|
|
|
@@ -213,13 +228,16 @@ function App() {
|
|
|
213
228
|
const agents = data?.agents ?? [];
|
|
214
229
|
const agent = agents[focusedAgentIdx()];
|
|
215
230
|
if (!agent || !data) return;
|
|
216
|
-
send(
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
231
|
+
send(
|
|
232
|
+
{
|
|
233
|
+
type: "kill-agent-pane",
|
|
234
|
+
session: data.name,
|
|
235
|
+
agent: agent.agent,
|
|
236
|
+
threadId: agent.threadId,
|
|
237
|
+
threadName: agent.threadName,
|
|
238
|
+
},
|
|
239
|
+
`killed ${agent.agent} pane`,
|
|
240
|
+
);
|
|
223
241
|
}
|
|
224
242
|
|
|
225
243
|
function beginDetailResize(event: MouseEvent) {
|
|
@@ -311,11 +329,27 @@ function App() {
|
|
|
311
329
|
const data = focusedData();
|
|
312
330
|
if (!data?.dir) return;
|
|
313
331
|
const editor = preferredEditor();
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
332
|
+
try {
|
|
333
|
+
const proc = Bun.spawn([editor, data.dir], {
|
|
334
|
+
stdout: "ignore",
|
|
335
|
+
stderr: "ignore",
|
|
336
|
+
stdin: "ignore",
|
|
337
|
+
});
|
|
338
|
+
showToast(`opening ${data.dir} in ${editor}`, "success");
|
|
339
|
+
void proc.exited.then((code) => {
|
|
340
|
+
if (code !== 0) {
|
|
341
|
+
logResizeDebug("openInEditor failed", { editor, dir: data.dir, code });
|
|
342
|
+
showToast(`failed to open editor "${editor}" (exit ${code})`, "error");
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
} catch (err) {
|
|
346
|
+
logResizeDebug("openInEditor spawn threw", {
|
|
347
|
+
editor,
|
|
348
|
+
dir: data.dir,
|
|
349
|
+
error: String(err),
|
|
350
|
+
});
|
|
351
|
+
showToast(`failed to spawn editor "${editor}": ${String(err)}`, "error");
|
|
352
|
+
}
|
|
319
353
|
}
|
|
320
354
|
|
|
321
355
|
onMount(() => {
|
|
@@ -340,6 +374,7 @@ function App() {
|
|
|
340
374
|
const refocusTimeout = setTimeout(doStartupRefocus, 2000);
|
|
341
375
|
|
|
342
376
|
onCleanup(() => {
|
|
377
|
+
if (toastTimer) clearTimeout(toastTimer);
|
|
343
378
|
clearTimeout(refocusTimeout);
|
|
344
379
|
renderer.removeListener("capabilities", doStartupRefocus);
|
|
345
380
|
});
|
|
@@ -388,8 +423,9 @@ function App() {
|
|
|
388
423
|
|
|
389
424
|
if (!startupFocusSynced && sessions.some((session) => session.name === msg.name)) {
|
|
390
425
|
startupFocusSynced = true;
|
|
426
|
+
const oldFocus = focusedSession();
|
|
391
427
|
setFocusedSession(msg.name);
|
|
392
|
-
if (
|
|
428
|
+
if (oldFocus !== msg.name) {
|
|
393
429
|
startupFocusToPublish = msg.name;
|
|
394
430
|
}
|
|
395
431
|
}
|
|
@@ -459,7 +495,7 @@ function App() {
|
|
|
459
495
|
if (currentModal === "confirm-kill") {
|
|
460
496
|
if (key.name === "y") {
|
|
461
497
|
const target = killTarget();
|
|
462
|
-
if (target) send({ type: "kill-session", name: target });
|
|
498
|
+
if (target) send({ type: "kill-session", name: target }, `killed ${target}`);
|
|
463
499
|
setKillTarget(null);
|
|
464
500
|
setModal("none");
|
|
465
501
|
} else {
|
|
@@ -540,20 +576,11 @@ function App() {
|
|
|
540
576
|
break;
|
|
541
577
|
}
|
|
542
578
|
case "r":
|
|
543
|
-
send({ type: "refresh" });
|
|
579
|
+
send({ type: "refresh" }, "refreshing sessions");
|
|
544
580
|
break;
|
|
545
|
-
case "
|
|
546
|
-
|
|
581
|
+
case "d":
|
|
582
|
+
if (panelFocus() === "agents") dismissFocusedAgent();
|
|
547
583
|
break;
|
|
548
|
-
case "d": {
|
|
549
|
-
if (panelFocus() === "agents") {
|
|
550
|
-
dismissFocusedAgent();
|
|
551
|
-
} else {
|
|
552
|
-
const focused = focusedSession();
|
|
553
|
-
if (focused) send({ type: "hide-session", name: focused });
|
|
554
|
-
}
|
|
555
|
-
break;
|
|
556
|
-
}
|
|
557
584
|
case "x": {
|
|
558
585
|
if (panelFocus() === "agents") {
|
|
559
586
|
killFocusedAgentPane();
|
|
@@ -681,6 +708,20 @@ function App() {
|
|
|
681
708
|
<box height={1}>
|
|
682
709
|
<text style={{ fg: P().surface2 }}>{DIVIDER}</text>
|
|
683
710
|
</box>
|
|
711
|
+
<Show when={toast()}>
|
|
712
|
+
{(t) => (
|
|
713
|
+
<box height={1}>
|
|
714
|
+
<text
|
|
715
|
+
style={{
|
|
716
|
+
fg:
|
|
717
|
+
t().tone === "error" ? P().red : t().tone === "success" ? P().green : P().blue,
|
|
718
|
+
}}
|
|
719
|
+
>
|
|
720
|
+
{t().message}
|
|
721
|
+
</text>
|
|
722
|
+
</box>
|
|
723
|
+
)}
|
|
724
|
+
</Show>
|
|
684
725
|
<Show
|
|
685
726
|
when={panelFocus() === "sessions"}
|
|
686
727
|
fallback={
|
|
@@ -699,11 +740,10 @@ function App() {
|
|
|
699
740
|
palette={P}
|
|
700
741
|
hints={[
|
|
701
742
|
["⇥", "cycle"],
|
|
702
|
-
["⏎", "
|
|
703
|
-
["→", "
|
|
743
|
+
["⏎", "switch"],
|
|
744
|
+
["→", "agents"],
|
|
704
745
|
["n", "new"],
|
|
705
746
|
["e", "edit"],
|
|
706
|
-
["d", "hide"],
|
|
707
747
|
["x", "kill"],
|
|
708
748
|
["r", "refresh"],
|
|
709
749
|
["q", "quit"],
|
|
@@ -767,11 +807,9 @@ const HELP_KEYS: [string, string][] = [
|
|
|
767
807
|
["Tab", "Cycle sessions"],
|
|
768
808
|
["n", "New session"],
|
|
769
809
|
["e", "Open in editor"],
|
|
770
|
-
["d", "Hide session"],
|
|
771
810
|
["x", "Kill session"],
|
|
772
811
|
["r", "Refresh"],
|
|
773
|
-
["
|
|
774
|
-
["→/l", "Select panel"],
|
|
812
|
+
["→/l", "Agents panel"],
|
|
775
813
|
["←/h/Esc", "Back to sessions"],
|
|
776
814
|
["Alt+↑↓", "Reorder sessions"],
|
|
777
815
|
["q", "Quit"],
|
|
@@ -248,13 +248,8 @@ export function startServer(
|
|
|
248
248
|
return a.name.localeCompare(b.name);
|
|
249
249
|
});
|
|
250
250
|
|
|
251
|
-
const currentSession = getCurrentSession();
|
|
252
|
-
|
|
253
251
|
// Sync custom ordering with current session list
|
|
254
252
|
sessionOrder.sync(allMuxSessions.map((s) => s.name));
|
|
255
|
-
if (currentSession) {
|
|
256
|
-
sessionOrder.show(currentSession);
|
|
257
|
-
}
|
|
258
253
|
|
|
259
254
|
// Apply custom ordering
|
|
260
255
|
const orderedNames = sessionOrder.apply(allMuxSessions.map((s) => s.name));
|
|
@@ -1366,14 +1361,6 @@ export function startServer(
|
|
|
1366
1361
|
mux.createSession();
|
|
1367
1362
|
broadcastState();
|
|
1368
1363
|
break;
|
|
1369
|
-
case "hide-session":
|
|
1370
|
-
sessionOrder.hide(cmd.name);
|
|
1371
|
-
broadcastState();
|
|
1372
|
-
break;
|
|
1373
|
-
case "show-all-sessions":
|
|
1374
|
-
sessionOrder.showAll();
|
|
1375
|
-
broadcastState();
|
|
1376
|
-
break;
|
|
1377
1364
|
case "kill-session": {
|
|
1378
1365
|
const p = sessionProviders.get(cmd.name) ?? mux;
|
|
1379
1366
|
p.killSession(cmd.name);
|
|
@@ -3,7 +3,6 @@ import { dirname } from "node:path";
|
|
|
3
3
|
|
|
4
4
|
interface PersistedSessionOrder {
|
|
5
5
|
order?: unknown;
|
|
6
|
-
hidden?: unknown;
|
|
7
6
|
}
|
|
8
7
|
|
|
9
8
|
/**
|
|
@@ -16,7 +15,6 @@ interface PersistedSessionOrder {
|
|
|
16
15
|
*/
|
|
17
16
|
export class SessionOrder {
|
|
18
17
|
private order: string[] = [];
|
|
19
|
-
private hidden = new Set<string>();
|
|
20
18
|
private readonly persistPath: string | null;
|
|
21
19
|
|
|
22
20
|
constructor(persistPath?: string) {
|
|
@@ -33,11 +31,6 @@ export class SessionOrder {
|
|
|
33
31
|
if (Array.isArray(persisted.order)) {
|
|
34
32
|
this.order = persisted.order.filter((n): n is string => typeof n === "string");
|
|
35
33
|
}
|
|
36
|
-
if (Array.isArray(persisted.hidden)) {
|
|
37
|
-
this.hidden = new Set(
|
|
38
|
-
persisted.hidden.filter((n): n is string => typeof n === "string"),
|
|
39
|
-
);
|
|
40
|
-
}
|
|
41
34
|
}
|
|
42
35
|
}
|
|
43
36
|
} catch {
|
|
@@ -51,7 +44,6 @@ export class SessionOrder {
|
|
|
51
44
|
const nameSet = new Set(names);
|
|
52
45
|
// Remove sessions that no longer exist
|
|
53
46
|
this.order = this.order.filter((n) => nameSet.has(n));
|
|
54
|
-
this.hidden = new Set([...this.hidden].filter((n) => nameSet.has(n)));
|
|
55
47
|
// Add new sessions in sorted position
|
|
56
48
|
const newNames = names
|
|
57
49
|
.filter((n) => !this.order.includes(n))
|
|
@@ -78,48 +70,21 @@ export class SessionOrder {
|
|
|
78
70
|
this.save();
|
|
79
71
|
}
|
|
80
72
|
|
|
81
|
-
/** Hide a session from the panel without touching the underlying mux session. */
|
|
82
|
-
hide(name: string): void {
|
|
83
|
-
if (!this.order.includes(name) || this.hidden.has(name)) return;
|
|
84
|
-
this.hidden.add(name);
|
|
85
|
-
this.save();
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/** Make a previously hidden session visible again. */
|
|
89
|
-
show(name: string): void {
|
|
90
|
-
if (!this.hidden.delete(name)) return;
|
|
91
|
-
if (!this.order.includes(name)) {
|
|
92
|
-
this.order.push(name);
|
|
93
|
-
}
|
|
94
|
-
this.save();
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/** Restore all hidden sessions back into the panel. */
|
|
98
|
-
showAll(): void {
|
|
99
|
-
if (this.hidden.size === 0) return;
|
|
100
|
-
this.hidden.clear();
|
|
101
|
-
this.save();
|
|
102
|
-
}
|
|
103
|
-
|
|
104
73
|
/** Apply the custom order to a list of session names. Returns sorted names. */
|
|
105
74
|
apply(names: string[]): string[] {
|
|
106
75
|
const posMap = new Map(this.order.map((n, i) => [n, i]));
|
|
107
|
-
return names
|
|
108
|
-
.
|
|
109
|
-
.
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
return pa - pb;
|
|
113
|
-
});
|
|
76
|
+
return [...names].sort((a, b) => {
|
|
77
|
+
const pa = posMap.get(a) ?? Infinity;
|
|
78
|
+
const pb = posMap.get(b) ?? Infinity;
|
|
79
|
+
return pa - pb;
|
|
80
|
+
});
|
|
114
81
|
}
|
|
115
82
|
|
|
116
83
|
private save(): void {
|
|
117
84
|
if (!this.persistPath) return;
|
|
118
85
|
try {
|
|
119
86
|
mkdirSync(dirname(this.persistPath), { recursive: true });
|
|
120
|
-
|
|
121
|
-
this.hidden.size === 0 ? this.order : { order: this.order, hidden: [...this.hidden] };
|
|
122
|
-
writeFileSync(this.persistPath, JSON.stringify(serialized) + "\n");
|
|
87
|
+
writeFileSync(this.persistPath, JSON.stringify(this.order) + "\n");
|
|
123
88
|
} catch {
|
|
124
89
|
// Best-effort — don't crash if write fails
|
|
125
90
|
}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import type { AgentStatus, AgentEvent } from "./contracts/agent";
|
|
2
2
|
|
|
3
|
-
export const
|
|
4
|
-
export const
|
|
3
|
+
export const DEFAULT_SERVER_PORT = 4201;
|
|
4
|
+
export const DEFAULT_SERVER_HOST = "127.0.0.1";
|
|
5
|
+
|
|
6
|
+
export const SERVER_PORT: number = Number(process.env.TT_AGENTBOARD_PORT) || DEFAULT_SERVER_PORT;
|
|
7
|
+
export const SERVER_HOST: string = process.env.TT_AGENTBOARD_HOST || DEFAULT_SERVER_HOST;
|
|
5
8
|
export const PID_FILE = "/tmp/agentboard.pid";
|
|
6
9
|
export const SERVER_IDLE_TIMEOUT_MS = 30_000;
|
|
7
10
|
export const STUCK_RUNNING_TIMEOUT_MS = 3 * 60 * 1000;
|
|
@@ -108,8 +111,6 @@ export type ClientCommand =
|
|
|
108
111
|
| { type: "switch-session"; name: string; clientTty?: string }
|
|
109
112
|
| { type: "switch-index"; index: number }
|
|
110
113
|
| { type: "new-session" }
|
|
111
|
-
| { type: "hide-session"; name: string }
|
|
112
|
-
| { type: "show-all-sessions" }
|
|
113
114
|
| { type: "kill-session"; name: string }
|
|
114
115
|
| { type: "reorder-session"; name: string; delta: -1 | 1 }
|
|
115
116
|
| { type: "refresh" }
|
|
@@ -6,8 +6,8 @@ import consola from "consola";
|
|
|
6
6
|
import { colors } from "consola/utils";
|
|
7
7
|
import { debugArg } from "./shared.js";
|
|
8
8
|
|
|
9
|
-
const SERVER_HOST = "127.0.0.1";
|
|
10
|
-
const SERVER_PORT = 4201;
|
|
9
|
+
const SERVER_HOST = process.env.TT_AGENTBOARD_HOST || "127.0.0.1";
|
|
10
|
+
const SERVER_PORT = Number(process.env.TT_AGENTBOARD_PORT) || 4201;
|
|
11
11
|
|
|
12
12
|
// Keybinding defaults
|
|
13
13
|
const DEFAULT_KEY = "a";
|
|
@@ -146,14 +146,14 @@ function uninstall(): void {
|
|
|
146
146
|
}
|
|
147
147
|
|
|
148
148
|
const content = readFileSync(editPath, "utf8");
|
|
149
|
-
if (!content.includes(
|
|
149
|
+
if (!content.includes(MARKER) && !content.includes(RUN_SHELL_LINE)) {
|
|
150
150
|
consola.info("agentboard not found in tmux.conf");
|
|
151
151
|
return;
|
|
152
152
|
}
|
|
153
153
|
|
|
154
154
|
const newContent = content
|
|
155
155
|
.split("\n")
|
|
156
|
-
.filter((line) => !line.includes(
|
|
156
|
+
.filter((line) => !line.includes(MARKER) && !line.includes(RUN_SHELL_LINE))
|
|
157
157
|
.join("\n")
|
|
158
158
|
.replace(/\n{3,}/g, "\n\n");
|
|
159
159
|
|
|
@@ -333,12 +333,13 @@ async function runFocus(): Promise<void> {
|
|
|
333
333
|
return;
|
|
334
334
|
}
|
|
335
335
|
|
|
336
|
-
// Otherwise, ensure server +
|
|
336
|
+
// Otherwise, ensure server + open sidebar
|
|
337
337
|
if (!(await ensureServerUp())) process.exit(0);
|
|
338
338
|
const ctx = tmuxContext();
|
|
339
|
-
await fetch(`http://${SERVER_HOST}:${SERVER_PORT}/
|
|
340
|
-
|
|
341
|
-
|
|
339
|
+
await fetch(`http://${SERVER_HOST}:${SERVER_PORT}/ensure-sidebar`, {
|
|
340
|
+
method: "POST",
|
|
341
|
+
body: ctx,
|
|
342
|
+
}).catch(() => {});
|
|
342
343
|
|
|
343
344
|
// Wait for sidebar pane to appear
|
|
344
345
|
for (let i = 0; i < 20; i++) {
|