@towles/tool 0.0.112 → 0.0.114
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/DetailPanel.tsx +2 -0
- package/packages/agentboard/apps/tui/src/components/SessionCard.tsx +4 -0
- package/packages/agentboard/apps/tui/src/session-status.ts +1 -1
- package/packages/agentboard/packages/runtime/src/agents/tracker.ts +1 -0
- 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 +9 -7
- package/packages/agentboard/packages/runtime/src/contracts/agent.ts +1 -1
- package/packages/agentboard/packages/runtime/src/server/index.ts +33 -8
- package/packages/agentboard/packages/runtime/src/server/pane-scanner.ts +10 -3
- package/packages/agentboard/packages/runtime/src/themes.ts +19 -0
- package/src/commands/agentboard.ts +27 -2
package/package.json
CHANGED
|
@@ -252,6 +252,7 @@ function AgentListItem(props: AgentListItemProps) {
|
|
|
252
252
|
return props.agent.status === "done" ? "✓" : props.agent.status === "error" ? "✗" : "⚠";
|
|
253
253
|
if (props.agent.status === "running") return SPINNERS[props.spinIdx() % SPINNERS.length]!;
|
|
254
254
|
if (props.agent.status === "waiting") return "◉";
|
|
255
|
+
if (props.agent.status === "question") return "?";
|
|
255
256
|
return "○";
|
|
256
257
|
};
|
|
257
258
|
|
|
@@ -270,6 +271,7 @@ function AgentListItem(props: AgentListItemProps) {
|
|
|
270
271
|
if (props.agent.status === "error") return "error";
|
|
271
272
|
if (props.agent.status === "interrupted") return "stopped";
|
|
272
273
|
if (props.agent.status === "waiting") return "waiting";
|
|
274
|
+
if (props.agent.status === "question") return "question";
|
|
273
275
|
return "";
|
|
274
276
|
};
|
|
275
277
|
|
|
@@ -33,6 +33,8 @@ 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;
|
|
37
|
+
if (s === "question") return P().green;
|
|
36
38
|
if (props.isFocused) return P().lavender;
|
|
37
39
|
return "transparent";
|
|
38
40
|
};
|
|
@@ -47,6 +49,8 @@ export function SessionCard(props: SessionCardProps) {
|
|
|
47
49
|
const statusIcon = () => {
|
|
48
50
|
const s = status();
|
|
49
51
|
if (s === "running") return SPINNERS[props.spinIdx() % SPINNERS.length]!;
|
|
52
|
+
if (s === "waiting") return "◉";
|
|
53
|
+
if (s === "question") return "?";
|
|
50
54
|
if (isUnseenTerminal()) return UNSEEN_ICON;
|
|
51
55
|
return "";
|
|
52
56
|
};
|
|
@@ -7,7 +7,7 @@ export function computeSessionStatusCounts(sessions: SessionData[]): SessionStat
|
|
|
7
7
|
let idle = 0;
|
|
8
8
|
for (const s of sessions) {
|
|
9
9
|
const status = s.agentState?.status;
|
|
10
|
-
if (status === "running" || status === "waiting") {
|
|
10
|
+
if (status === "running" || status === "waiting" || status === "question") {
|
|
11
11
|
active++;
|
|
12
12
|
} else if (status === "error") {
|
|
13
13
|
error++;
|
|
@@ -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,15 @@ 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
|
+
const allAsking = toolUses.every((c) => (c as any).name === "AskUserQuestion");
|
|
65
|
+
return allAsking ? "question" : "running";
|
|
64
66
|
}
|
|
65
67
|
|
|
66
68
|
if (msg.role === "user") return "running";
|
|
67
69
|
|
|
68
|
-
return
|
|
70
|
+
return null;
|
|
69
71
|
}
|
|
70
72
|
|
|
71
73
|
function extractThreadName(entry: JournalEntry): string | undefined {
|
|
@@ -195,7 +197,7 @@ export class ClaudeCodeAgentWatcher implements AgentWatcher {
|
|
|
195
197
|
const name = extractThreadName(entry);
|
|
196
198
|
if (name) threadName = name;
|
|
197
199
|
}
|
|
198
|
-
latestStatus = determineStatus(entry);
|
|
200
|
+
latestStatus = determineStatus(entry) ?? latestStatus;
|
|
199
201
|
}
|
|
200
202
|
|
|
201
203
|
// If "running" but journal file is stale, the process likely exited
|
|
@@ -238,7 +240,7 @@ export class ClaudeCodeAgentWatcher implements AgentWatcher {
|
|
|
238
240
|
if (name) threadName = name;
|
|
239
241
|
}
|
|
240
242
|
|
|
241
|
-
latestStatus = determineStatus(entry);
|
|
243
|
+
latestStatus = determineStatus(entry) ?? latestStatus;
|
|
242
244
|
}
|
|
243
245
|
|
|
244
246
|
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;
|
|
@@ -298,7 +313,7 @@ export function startServer(
|
|
|
298
313
|
ports: getSessionPorts(name),
|
|
299
314
|
windows,
|
|
300
315
|
uptime,
|
|
301
|
-
agentState: tracker.getState(name),
|
|
316
|
+
agentState: overrideTerminalIfPaneAlive(name, tracker.getState(name)),
|
|
302
317
|
agents: mergeAgentsWithPanePresence(name, tracker.getAgents(name)),
|
|
303
318
|
eventTimestamps: tracker.getEventTimestamps(name),
|
|
304
319
|
metadata: metadataStore.get(name),
|
|
@@ -1079,8 +1094,12 @@ export function startServer(
|
|
|
1079
1094
|
const data = JSON.parse(readFileSync(join(sessionsDir, `${agentPid}.json`), "utf-8"));
|
|
1080
1095
|
const threadId: string | undefined = data.sessionId;
|
|
1081
1096
|
if (!threadId) return {};
|
|
1082
|
-
// Try to get thread name and status from the journal
|
|
1083
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
|
+
}
|
|
1084
1103
|
return { threadId, ...journalInfo };
|
|
1085
1104
|
} catch {
|
|
1086
1105
|
return {};
|
|
@@ -1119,10 +1138,16 @@ export function startServer(
|
|
|
1119
1138
|
if (t && !t.startsWith("<") && !t.startsWith("{")) threadName = t.slice(0, 80);
|
|
1120
1139
|
}
|
|
1121
1140
|
|
|
1122
|
-
// Determine status from last entry (same logic as ClaudeCodeAgentWatcher)
|
|
1123
1141
|
if (msg.role === "assistant") {
|
|
1124
1142
|
const items = Array.isArray(msg.content) ? msg.content : [];
|
|
1125
|
-
|
|
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
|
+
? "question"
|
|
1149
|
+
: "running";
|
|
1150
|
+
}
|
|
1126
1151
|
} else if (msg.role === "user") {
|
|
1127
1152
|
lastStatus = "running";
|
|
1128
1153
|
}
|
|
@@ -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
|
+
? "question"
|
|
152
|
+
: "running";
|
|
153
|
+
}
|
|
147
154
|
} else if (msg.role === "user") {
|
|
148
155
|
lastStatus = "running";
|
|
149
156
|
}
|
|
@@ -62,6 +62,7 @@ const CATPPUCCIN_MOCHA: Theme = {
|
|
|
62
62
|
done: "#a6e3a1",
|
|
63
63
|
error: "#f38ba8",
|
|
64
64
|
waiting: "#89b4fa",
|
|
65
|
+
question: "#a6e3a1",
|
|
65
66
|
interrupted: "#fab387",
|
|
66
67
|
},
|
|
67
68
|
icons: {
|
|
@@ -70,6 +71,7 @@ const CATPPUCCIN_MOCHA: Theme = {
|
|
|
70
71
|
done: "✓",
|
|
71
72
|
error: "✗",
|
|
72
73
|
waiting: "◉",
|
|
74
|
+
question: "?",
|
|
73
75
|
interrupted: "⚠",
|
|
74
76
|
},
|
|
75
77
|
};
|
|
@@ -104,6 +106,7 @@ const CATPPUCCIN_LATTE: Theme = {
|
|
|
104
106
|
done: "#40a02b",
|
|
105
107
|
error: "#d20f39",
|
|
106
108
|
waiting: "#1e66f5",
|
|
109
|
+
question: "#40a02b",
|
|
107
110
|
interrupted: "#fe640b",
|
|
108
111
|
},
|
|
109
112
|
icons: CATPPUCCIN_MOCHA.icons,
|
|
@@ -139,6 +142,7 @@ const TOKYO_NIGHT: Theme = {
|
|
|
139
142
|
done: "#9ece6a",
|
|
140
143
|
error: "#f7768e",
|
|
141
144
|
waiting: "#7aa2f7",
|
|
145
|
+
question: "#9ece6a",
|
|
142
146
|
interrupted: "#ff9e64",
|
|
143
147
|
},
|
|
144
148
|
icons: CATPPUCCIN_MOCHA.icons,
|
|
@@ -174,6 +178,7 @@ const GRUVBOX_DARK: Theme = {
|
|
|
174
178
|
done: "#b8bb26",
|
|
175
179
|
error: "#fb4934",
|
|
176
180
|
waiting: "#83a598",
|
|
181
|
+
question: "#b8bb26",
|
|
177
182
|
interrupted: "#fe8019",
|
|
178
183
|
},
|
|
179
184
|
icons: CATPPUCCIN_MOCHA.icons,
|
|
@@ -209,6 +214,7 @@ const NORD: Theme = {
|
|
|
209
214
|
done: "#a3be8c",
|
|
210
215
|
error: "#bf616a",
|
|
211
216
|
waiting: "#81a1c1",
|
|
217
|
+
question: "#a3be8c",
|
|
212
218
|
interrupted: "#d08770",
|
|
213
219
|
},
|
|
214
220
|
icons: CATPPUCCIN_MOCHA.icons,
|
|
@@ -244,6 +250,7 @@ const DRACULA: Theme = {
|
|
|
244
250
|
done: "#50fa7b",
|
|
245
251
|
error: "#ff5555",
|
|
246
252
|
waiting: "#8be9fd",
|
|
253
|
+
question: "#50fa7b",
|
|
247
254
|
interrupted: "#ffb86c",
|
|
248
255
|
},
|
|
249
256
|
icons: CATPPUCCIN_MOCHA.icons,
|
|
@@ -279,6 +286,7 @@ const CATPPUCCIN_FRAPPE: Theme = {
|
|
|
279
286
|
done: "#a6d189",
|
|
280
287
|
error: "#e78284",
|
|
281
288
|
waiting: "#8da4e2",
|
|
289
|
+
question: "#a6d189",
|
|
282
290
|
interrupted: "#ef9f76",
|
|
283
291
|
},
|
|
284
292
|
icons: CATPPUCCIN_MOCHA.icons,
|
|
@@ -314,6 +322,7 @@ const CATPPUCCIN_MACCHIATO: Theme = {
|
|
|
314
322
|
done: "#a6da95",
|
|
315
323
|
error: "#ed8796",
|
|
316
324
|
waiting: "#8aadf4",
|
|
325
|
+
question: "#a6da95",
|
|
317
326
|
interrupted: "#f5a97f",
|
|
318
327
|
},
|
|
319
328
|
icons: CATPPUCCIN_MOCHA.icons,
|
|
@@ -349,6 +358,7 @@ const GITHUB_DARK: Theme = {
|
|
|
349
358
|
done: "#3fb950",
|
|
350
359
|
error: "#f85149",
|
|
351
360
|
waiting: "#58a6ff",
|
|
361
|
+
question: "#3fb950",
|
|
352
362
|
interrupted: "#d29922",
|
|
353
363
|
},
|
|
354
364
|
icons: CATPPUCCIN_MOCHA.icons,
|
|
@@ -384,6 +394,7 @@ const ONE_DARK: Theme = {
|
|
|
384
394
|
done: "#98c379",
|
|
385
395
|
error: "#e06c75",
|
|
386
396
|
waiting: "#61afef",
|
|
397
|
+
question: "#98c379",
|
|
387
398
|
interrupted: "#d19a66",
|
|
388
399
|
},
|
|
389
400
|
icons: CATPPUCCIN_MOCHA.icons,
|
|
@@ -419,6 +430,7 @@ const KANAGAWA: Theme = {
|
|
|
419
430
|
done: "#98BB6C",
|
|
420
431
|
error: "#E82424",
|
|
421
432
|
waiting: "#7E9CD8",
|
|
433
|
+
question: "#98BB6C",
|
|
422
434
|
interrupted: "#FFA066",
|
|
423
435
|
},
|
|
424
436
|
icons: CATPPUCCIN_MOCHA.icons,
|
|
@@ -454,6 +466,7 @@ const EVERFOREST: Theme = {
|
|
|
454
466
|
done: "#a7c080",
|
|
455
467
|
error: "#e67e80",
|
|
456
468
|
waiting: "#7fbbb3",
|
|
469
|
+
question: "#a7c080",
|
|
457
470
|
interrupted: "#e69875",
|
|
458
471
|
},
|
|
459
472
|
icons: CATPPUCCIN_MOCHA.icons,
|
|
@@ -489,6 +502,7 @@ const MATERIAL: Theme = {
|
|
|
489
502
|
done: "#c3e88d",
|
|
490
503
|
error: "#f07178",
|
|
491
504
|
waiting: "#82aaff",
|
|
505
|
+
question: "#c3e88d",
|
|
492
506
|
interrupted: "#f78c6c",
|
|
493
507
|
},
|
|
494
508
|
icons: CATPPUCCIN_MOCHA.icons,
|
|
@@ -524,6 +538,7 @@ const COBALT2: Theme = {
|
|
|
524
538
|
done: "#9eff80",
|
|
525
539
|
error: "#ff0088",
|
|
526
540
|
waiting: "#0088ff",
|
|
541
|
+
question: "#9eff80",
|
|
527
542
|
interrupted: "#ff628c",
|
|
528
543
|
},
|
|
529
544
|
icons: CATPPUCCIN_MOCHA.icons,
|
|
@@ -559,6 +574,7 @@ const FLEXOKI: Theme = {
|
|
|
559
574
|
done: "#879A39",
|
|
560
575
|
error: "#D14D41",
|
|
561
576
|
waiting: "#4385BE",
|
|
577
|
+
question: "#879A39",
|
|
562
578
|
interrupted: "#DA702C",
|
|
563
579
|
},
|
|
564
580
|
icons: CATPPUCCIN_MOCHA.icons,
|
|
@@ -594,6 +610,7 @@ const AYU: Theme = {
|
|
|
594
610
|
done: "#7FD962",
|
|
595
611
|
error: "#D95757",
|
|
596
612
|
waiting: "#59C2FF",
|
|
613
|
+
question: "#7FD962",
|
|
597
614
|
interrupted: "#FF8F40",
|
|
598
615
|
},
|
|
599
616
|
icons: CATPPUCCIN_MOCHA.icons,
|
|
@@ -629,6 +646,7 @@ const AURA: Theme = {
|
|
|
629
646
|
done: "#61ffca",
|
|
630
647
|
error: "#ff6767",
|
|
631
648
|
waiting: "#a277ff",
|
|
649
|
+
question: "#61ffca",
|
|
632
650
|
interrupted: "#ffca85",
|
|
633
651
|
},
|
|
634
652
|
icons: CATPPUCCIN_MOCHA.icons,
|
|
@@ -664,6 +682,7 @@ const MATRIX: Theme = {
|
|
|
664
682
|
done: "#62ff94",
|
|
665
683
|
error: "#ff4b4b",
|
|
666
684
|
waiting: "#30b3ff",
|
|
685
|
+
question: "#62ff94",
|
|
667
686
|
interrupted: "#ffa83d",
|
|
668
687
|
},
|
|
669
688
|
icons: CATPPUCCIN_MOCHA.icons,
|
|
@@ -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);
|