@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@towles/tool",
3
- "version": "0.0.110",
3
+ "version": "0.0.112",
4
4
  "description": "One off quality of life scripts that I use on a daily basis.",
5
5
  "homepage": "https://github.com/ChrisTowles/towles-tool#readme",
6
6
  "bugs": {
@@ -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) ws.send(JSON.stringify(cmd));
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
- type: "focus-agent-pane",
185
- session: data.name,
186
- agent: agent.agent,
187
- threadId: agent.threadId,
188
- threadName: agent.threadName,
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
- session: data.name,
200
- agent: agent.agent,
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
- type: "kill-agent-pane",
218
- session: data.name,
219
- agent: agent.agent,
220
- threadId: agent.threadId,
221
- threadName: agent.threadName,
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
- Bun.spawn([editor, data.dir], {
315
- stdout: "ignore",
316
- stderr: "ignore",
317
- stdin: "ignore",
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 (focusedSession() !== msg.name) {
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 "u":
546
- send({ type: "show-all-sessions" });
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
- ["⏎", "go"],
703
- ["→", "select"],
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
- ["u", "Show all sessions"],
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
- .filter((n) => !this.hidden.has(n))
109
- .sort((a, b) => {
110
- const pa = posMap.get(a) ?? Infinity;
111
- const pb = posMap.get(b) ?? Infinity;
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
- const serialized =
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 SERVER_PORT = 4201;
4
- export const SERVER_HOST = "127.0.0.1";
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("agentboard")) {
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("agentboard"))
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 + toggle sidebar on
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}/toggle`, { method: "POST", body: ctx }).catch(
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++) {