@towles/tool 0.0.111 → 0.0.113
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/components/SessionCard.tsx +2 -0
- package/packages/agentboard/apps/tui/src/index.tsx +85 -48
- package/packages/agentboard/packages/runtime/src/agents/watchers/claude-code.test.ts +7 -7
- package/packages/agentboard/packages/runtime/src/agents/watchers/claude-code.ts +10 -7
- package/packages/agentboard/packages/runtime/src/server/index.ts +33 -21
- package/packages/agentboard/packages/runtime/src/server/pane-scanner.ts +10 -3
- package/packages/agentboard/packages/runtime/src/server/session-order.ts +6 -41
- package/packages/agentboard/packages/runtime/src/shared.ts +0 -2
- package/src/commands/agentboard.ts +27 -2
package/package.json
CHANGED
|
@@ -33,6 +33,7 @@ export function SessionCard(props: SessionCardProps) {
|
|
|
33
33
|
if (s === "error") return P().red;
|
|
34
34
|
if (s === "interrupted") return P().peach;
|
|
35
35
|
if (s === "running") return P().yellow;
|
|
36
|
+
if (s === "waiting") return P().blue;
|
|
36
37
|
if (props.isFocused) return P().lavender;
|
|
37
38
|
return "transparent";
|
|
38
39
|
};
|
|
@@ -47,6 +48,7 @@ export function SessionCard(props: SessionCardProps) {
|
|
|
47
48
|
const statusIcon = () => {
|
|
48
49
|
const s = status();
|
|
49
50
|
if (s === "running") return SPINNERS[props.spinIdx() % SPINNERS.length]!;
|
|
51
|
+
if (s === "waiting") return "◉";
|
|
50
52
|
if (isUnseenTerminal()) return UNSEEN_ICON;
|
|
51
53
|
return "";
|
|
52
54
|
};
|
|
@@ -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
|
});
|
|
@@ -460,7 +495,7 @@ function App() {
|
|
|
460
495
|
if (currentModal === "confirm-kill") {
|
|
461
496
|
if (key.name === "y") {
|
|
462
497
|
const target = killTarget();
|
|
463
|
-
if (target) send({ type: "kill-session", name: target });
|
|
498
|
+
if (target) send({ type: "kill-session", name: target }, `killed ${target}`);
|
|
464
499
|
setKillTarget(null);
|
|
465
500
|
setModal("none");
|
|
466
501
|
} else {
|
|
@@ -541,20 +576,11 @@ function App() {
|
|
|
541
576
|
break;
|
|
542
577
|
}
|
|
543
578
|
case "r":
|
|
544
|
-
send({ type: "refresh" });
|
|
579
|
+
send({ type: "refresh" }, "refreshing sessions");
|
|
545
580
|
break;
|
|
546
|
-
case "
|
|
547
|
-
|
|
581
|
+
case "d":
|
|
582
|
+
if (panelFocus() === "agents") dismissFocusedAgent();
|
|
548
583
|
break;
|
|
549
|
-
case "d": {
|
|
550
|
-
if (panelFocus() === "agents") {
|
|
551
|
-
dismissFocusedAgent();
|
|
552
|
-
} else {
|
|
553
|
-
const focused = focusedSession();
|
|
554
|
-
if (focused) send({ type: "hide-session", name: focused });
|
|
555
|
-
}
|
|
556
|
-
break;
|
|
557
|
-
}
|
|
558
584
|
case "x": {
|
|
559
585
|
if (panelFocus() === "agents") {
|
|
560
586
|
killFocusedAgentPane();
|
|
@@ -682,6 +708,20 @@ function App() {
|
|
|
682
708
|
<box height={1}>
|
|
683
709
|
<text style={{ fg: P().surface2 }}>{DIVIDER}</text>
|
|
684
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>
|
|
685
725
|
<Show
|
|
686
726
|
when={panelFocus() === "sessions"}
|
|
687
727
|
fallback={
|
|
@@ -700,11 +740,10 @@ function App() {
|
|
|
700
740
|
palette={P}
|
|
701
741
|
hints={[
|
|
702
742
|
["⇥", "cycle"],
|
|
703
|
-
["⏎", "
|
|
704
|
-
["→", "
|
|
743
|
+
["⏎", "switch"],
|
|
744
|
+
["→", "agents"],
|
|
705
745
|
["n", "new"],
|
|
706
746
|
["e", "edit"],
|
|
707
|
-
["d", "hide"],
|
|
708
747
|
["x", "kill"],
|
|
709
748
|
["r", "refresh"],
|
|
710
749
|
["q", "quit"],
|
|
@@ -768,11 +807,9 @@ const HELP_KEYS: [string, string][] = [
|
|
|
768
807
|
["Tab", "Cycle sessions"],
|
|
769
808
|
["n", "New session"],
|
|
770
809
|
["e", "Open in editor"],
|
|
771
|
-
["d", "Hide session"],
|
|
772
810
|
["x", "Kill session"],
|
|
773
811
|
["r", "Refresh"],
|
|
774
|
-
["
|
|
775
|
-
["→/l", "Select panel"],
|
|
812
|
+
["→/l", "Agents panel"],
|
|
776
813
|
["←/h/Esc", "Back to sessions"],
|
|
777
814
|
["Alt+↑↓", "Reorder sessions"],
|
|
778
815
|
["q", "Quit"],
|
|
@@ -2,13 +2,13 @@ import { describe, it, expect } from "bun:test";
|
|
|
2
2
|
import { determineStatus } from "./claude-code";
|
|
3
3
|
|
|
4
4
|
describe("determineStatus", () => {
|
|
5
|
-
it(
|
|
6
|
-
expect(determineStatus({})).
|
|
7
|
-
expect(determineStatus({ message: undefined })).
|
|
5
|
+
it("returns null when no message", () => {
|
|
6
|
+
expect(determineStatus({})).toBeNull();
|
|
7
|
+
expect(determineStatus({ message: undefined })).toBeNull();
|
|
8
8
|
});
|
|
9
9
|
|
|
10
|
-
it(
|
|
11
|
-
expect(determineStatus({ message: { content: "hi" } })).
|
|
10
|
+
it("returns null when message has no role", () => {
|
|
11
|
+
expect(determineStatus({ message: { content: "hi" } })).toBeNull();
|
|
12
12
|
});
|
|
13
13
|
|
|
14
14
|
it('returns "running" for user messages', () => {
|
|
@@ -53,11 +53,11 @@ describe("determineStatus", () => {
|
|
|
53
53
|
).toBe("done");
|
|
54
54
|
});
|
|
55
55
|
|
|
56
|
-
it(
|
|
56
|
+
it("returns null for unknown roles", () => {
|
|
57
57
|
expect(
|
|
58
58
|
determineStatus({
|
|
59
59
|
message: { role: "system", content: "system message" },
|
|
60
60
|
}),
|
|
61
|
-
).
|
|
61
|
+
).toBeNull();
|
|
62
62
|
});
|
|
63
63
|
});
|
|
@@ -47,9 +47,9 @@ const STALE_MS = 5 * 60 * 1000;
|
|
|
47
47
|
|
|
48
48
|
// --- Status detection ---
|
|
49
49
|
|
|
50
|
-
export function determineStatus(entry: JournalEntry): AgentStatus {
|
|
50
|
+
export function determineStatus(entry: JournalEntry): AgentStatus | null {
|
|
51
51
|
const msg = entry.message;
|
|
52
|
-
if (!msg?.role) return
|
|
52
|
+
if (!msg?.role) return null;
|
|
53
53
|
|
|
54
54
|
const content = msg.content;
|
|
55
55
|
const items: ContentItem[] = Array.isArray(content)
|
|
@@ -59,13 +59,16 @@ export function determineStatus(entry: JournalEntry): AgentStatus {
|
|
|
59
59
|
: [];
|
|
60
60
|
|
|
61
61
|
if (msg.role === "assistant") {
|
|
62
|
-
const
|
|
63
|
-
|
|
62
|
+
const toolUses = items.filter((c) => c.type === "tool_use");
|
|
63
|
+
if (toolUses.length === 0) return "done";
|
|
64
|
+
// AskUserQuestion means the agent is waiting for user input, not running
|
|
65
|
+
const allWaiting = toolUses.every((c) => (c as any).name === "AskUserQuestion");
|
|
66
|
+
return allWaiting ? "waiting" : "running";
|
|
64
67
|
}
|
|
65
68
|
|
|
66
69
|
if (msg.role === "user") return "running";
|
|
67
70
|
|
|
68
|
-
return
|
|
71
|
+
return null;
|
|
69
72
|
}
|
|
70
73
|
|
|
71
74
|
function extractThreadName(entry: JournalEntry): string | undefined {
|
|
@@ -195,7 +198,7 @@ export class ClaudeCodeAgentWatcher implements AgentWatcher {
|
|
|
195
198
|
const name = extractThreadName(entry);
|
|
196
199
|
if (name) threadName = name;
|
|
197
200
|
}
|
|
198
|
-
latestStatus = determineStatus(entry);
|
|
201
|
+
latestStatus = determineStatus(entry) ?? latestStatus;
|
|
199
202
|
}
|
|
200
203
|
|
|
201
204
|
// If "running" but journal file is stale, the process likely exited
|
|
@@ -238,7 +241,7 @@ export class ClaudeCodeAgentWatcher implements AgentWatcher {
|
|
|
238
241
|
if (name) threadName = name;
|
|
239
242
|
}
|
|
240
243
|
|
|
241
|
-
latestStatus = determineStatus(entry);
|
|
244
|
+
latestStatus = determineStatus(entry) ?? latestStatus;
|
|
242
245
|
}
|
|
243
246
|
|
|
244
247
|
const prevStatus = prev?.status;
|
|
@@ -187,6 +187,22 @@ export function startServer(
|
|
|
187
187
|
return null;
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
+
/** If any pane agent is alive for this session, override terminal agentState to waiting. */
|
|
191
|
+
function overrideTerminalIfPaneAlive(
|
|
192
|
+
sessionName: string,
|
|
193
|
+
state: AgentEvent | null,
|
|
194
|
+
): AgentEvent | null {
|
|
195
|
+
if (!state || !TERMINAL_STATUSES.has(state.status)) return state;
|
|
196
|
+
const paneAgents = paneAgentsBySession.get(sessionName);
|
|
197
|
+
if (!paneAgents || paneAgents.size === 0) return state;
|
|
198
|
+
for (const presence of paneAgents.values()) {
|
|
199
|
+
if (presence.agent === state.agent && presence.threadId === state.threadId) {
|
|
200
|
+
return { ...state, status: "waiting" };
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return state;
|
|
204
|
+
}
|
|
205
|
+
|
|
190
206
|
/** Merge pane-detected agents into watcher-provided agents for a session.
|
|
191
207
|
* Watcher events take precedence — pane presence only adds synthetic entries
|
|
192
208
|
* for agents that aren't already tracked by watchers. */
|
|
@@ -205,12 +221,11 @@ export function startServer(
|
|
|
205
221
|
const trackedIdx = trackedByKey.get(key);
|
|
206
222
|
|
|
207
223
|
if (trackedIdx != null) {
|
|
208
|
-
// Watcher already tracks this agent —
|
|
209
|
-
//
|
|
210
|
-
// journal status is a between-turn artifact).
|
|
224
|
+
// Watcher already tracks this agent — process is confirmed alive,
|
|
225
|
+
// so terminal journal status means it's waiting for user input.
|
|
211
226
|
const tracked = result[trackedIdx]!;
|
|
212
227
|
if (TERMINAL_STATUSES.has(tracked.status)) {
|
|
213
|
-
tracked.status = "
|
|
228
|
+
tracked.status = "waiting";
|
|
214
229
|
tracked.paneId = presence.paneId;
|
|
215
230
|
}
|
|
216
231
|
continue;
|
|
@@ -248,13 +263,8 @@ export function startServer(
|
|
|
248
263
|
return a.name.localeCompare(b.name);
|
|
249
264
|
});
|
|
250
265
|
|
|
251
|
-
const currentSession = getCurrentSession();
|
|
252
|
-
|
|
253
266
|
// Sync custom ordering with current session list
|
|
254
267
|
sessionOrder.sync(allMuxSessions.map((s) => s.name));
|
|
255
|
-
if (currentSession) {
|
|
256
|
-
sessionOrder.show(currentSession);
|
|
257
|
-
}
|
|
258
268
|
|
|
259
269
|
// Apply custom ordering
|
|
260
270
|
const orderedNames = sessionOrder.apply(allMuxSessions.map((s) => s.name));
|
|
@@ -303,7 +313,7 @@ export function startServer(
|
|
|
303
313
|
ports: getSessionPorts(name),
|
|
304
314
|
windows,
|
|
305
315
|
uptime,
|
|
306
|
-
agentState: tracker.getState(name),
|
|
316
|
+
agentState: overrideTerminalIfPaneAlive(name, tracker.getState(name)),
|
|
307
317
|
agents: mergeAgentsWithPanePresence(name, tracker.getAgents(name)),
|
|
308
318
|
eventTimestamps: tracker.getEventTimestamps(name),
|
|
309
319
|
metadata: metadataStore.get(name),
|
|
@@ -1084,8 +1094,12 @@ export function startServer(
|
|
|
1084
1094
|
const data = JSON.parse(readFileSync(join(sessionsDir, `${agentPid}.json`), "utf-8"));
|
|
1085
1095
|
const threadId: string | undefined = data.sessionId;
|
|
1086
1096
|
if (!threadId) return {};
|
|
1087
|
-
// Try to get thread name and status from the journal
|
|
1088
1097
|
const journalInfo = resolveClaudeCodeJournalInfo(threadId);
|
|
1098
|
+
// Process is alive (found via process tree), so terminal journal status
|
|
1099
|
+
// means it's waiting for user input.
|
|
1100
|
+
if (journalInfo.status && TERMINAL_STATUSES.has(journalInfo.status)) {
|
|
1101
|
+
journalInfo.status = "waiting";
|
|
1102
|
+
}
|
|
1089
1103
|
return { threadId, ...journalInfo };
|
|
1090
1104
|
} catch {
|
|
1091
1105
|
return {};
|
|
@@ -1124,10 +1138,16 @@ export function startServer(
|
|
|
1124
1138
|
if (t && !t.startsWith("<") && !t.startsWith("{")) threadName = t.slice(0, 80);
|
|
1125
1139
|
}
|
|
1126
1140
|
|
|
1127
|
-
// Determine status from last entry (same logic as ClaudeCodeAgentWatcher)
|
|
1128
1141
|
if (msg.role === "assistant") {
|
|
1129
1142
|
const items = Array.isArray(msg.content) ? msg.content : [];
|
|
1130
|
-
|
|
1143
|
+
const toolUses = items.filter((c: any) => c.type === "tool_use");
|
|
1144
|
+
if (toolUses.length === 0) {
|
|
1145
|
+
lastStatus = "done";
|
|
1146
|
+
} else {
|
|
1147
|
+
lastStatus = toolUses.every((c: any) => c.name === "AskUserQuestion")
|
|
1148
|
+
? "waiting"
|
|
1149
|
+
: "running";
|
|
1150
|
+
}
|
|
1131
1151
|
} else if (msg.role === "user") {
|
|
1132
1152
|
lastStatus = "running";
|
|
1133
1153
|
}
|
|
@@ -1366,14 +1386,6 @@ export function startServer(
|
|
|
1366
1386
|
mux.createSession();
|
|
1367
1387
|
broadcastState();
|
|
1368
1388
|
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
1389
|
case "kill-session": {
|
|
1378
1390
|
const p = sessionProviders.get(cmd.name) ?? mux;
|
|
1379
1391
|
p.killSession(cmd.name);
|
|
@@ -100,9 +100,9 @@ function resolveClaudeCodePaneInfo(
|
|
|
100
100
|
if (!threadId) return {};
|
|
101
101
|
const journalInfo = resolveClaudeCodeJournalInfo(threadId);
|
|
102
102
|
// Process is alive (found via process tree), so terminal journal status
|
|
103
|
-
//
|
|
103
|
+
// means it's waiting for user input.
|
|
104
104
|
if (journalInfo.status && TERMINAL_STATUSES.has(journalInfo.status)) {
|
|
105
|
-
journalInfo.status = "
|
|
105
|
+
journalInfo.status = "waiting";
|
|
106
106
|
}
|
|
107
107
|
return { threadId, ...journalInfo };
|
|
108
108
|
} catch {
|
|
@@ -143,7 +143,14 @@ function resolveClaudeCodeJournalInfo(threadId: string): {
|
|
|
143
143
|
|
|
144
144
|
if (msg.role === "assistant") {
|
|
145
145
|
const items = Array.isArray(msg.content) ? msg.content : [];
|
|
146
|
-
|
|
146
|
+
const toolUses = items.filter((c: any) => c.type === "tool_use");
|
|
147
|
+
if (toolUses.length === 0) {
|
|
148
|
+
lastStatus = "done";
|
|
149
|
+
} else {
|
|
150
|
+
lastStatus = toolUses.every((c: any) => c.name === "AskUserQuestion")
|
|
151
|
+
? "waiting"
|
|
152
|
+
: "running";
|
|
153
|
+
}
|
|
147
154
|
} else if (msg.role === "user") {
|
|
148
155
|
lastStatus = "running";
|
|
149
156
|
}
|
|
@@ -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
|
}
|
|
@@ -111,8 +111,6 @@ export type ClientCommand =
|
|
|
111
111
|
| { type: "switch-session"; name: string; clientTty?: string }
|
|
112
112
|
| { type: "switch-index"; index: number }
|
|
113
113
|
| { type: "new-session" }
|
|
114
|
-
| { type: "hide-session"; name: string }
|
|
115
|
-
| { type: "show-all-sessions" }
|
|
116
114
|
| { type: "kill-session"; name: string }
|
|
117
115
|
| { type: "reorder-session"; name: string; delta: -1 | 1 }
|
|
118
116
|
| { type: "refresh" }
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { defineCommand } from "citty";
|
|
2
2
|
import { execSync, spawn, spawnSync } from "node:child_process";
|
|
3
|
-
import { readFileSync, writeFileSync, existsSync, realpathSync } from "node:fs";
|
|
3
|
+
import { readFileSync, writeFileSync, existsSync, realpathSync, unlinkSync } from "node:fs";
|
|
4
4
|
import { resolve } from "node:path";
|
|
5
5
|
import consola from "consola";
|
|
6
6
|
import { colors } from "consola/utils";
|
|
@@ -186,6 +186,23 @@ async function serverAlive(): Promise<boolean> {
|
|
|
186
186
|
}
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
+
const PID_FILE = "/tmp/agentboard.pid";
|
|
190
|
+
|
|
191
|
+
function stopServer(): boolean {
|
|
192
|
+
if (!existsSync(PID_FILE)) return false;
|
|
193
|
+
const pid = Number.parseInt(readFileSync(PID_FILE, "utf8").trim(), 10);
|
|
194
|
+
if (Number.isNaN(pid)) return false;
|
|
195
|
+
try {
|
|
196
|
+
process.kill(pid, "SIGTERM");
|
|
197
|
+
} catch {
|
|
198
|
+
// process already dead
|
|
199
|
+
}
|
|
200
|
+
try {
|
|
201
|
+
unlinkSync(PID_FILE);
|
|
202
|
+
} catch {}
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
|
|
189
206
|
async function ensureServerUp(): Promise<boolean> {
|
|
190
207
|
if (await serverAlive()) return true;
|
|
191
208
|
|
|
@@ -374,7 +391,15 @@ async function restart(): Promise<void> {
|
|
|
374
391
|
// no tmux or no sessions
|
|
375
392
|
}
|
|
376
393
|
|
|
377
|
-
// 2.
|
|
394
|
+
// 2. Stop existing server, then start fresh
|
|
395
|
+
const wasStopped = stopServer();
|
|
396
|
+
if (wasStopped) {
|
|
397
|
+
// Wait for port to free up
|
|
398
|
+
for (let i = 0; i < 20; i++) {
|
|
399
|
+
if (!(await serverAlive())) break;
|
|
400
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
401
|
+
}
|
|
402
|
+
}
|
|
378
403
|
if (!(await ensureServerUp())) {
|
|
379
404
|
consola.error("Failed to start agentboard server");
|
|
380
405
|
process.exit(1);
|