@katyella/legio 0.1.3 → 0.2.0
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/CHANGELOG.md +40 -3
- package/README.md +15 -8
- package/agents/builder.md +11 -10
- package/agents/coordinator.md +36 -27
- package/agents/cto.md +9 -8
- package/agents/gateway.md +28 -12
- package/agents/lead.md +45 -30
- package/agents/merger.md +4 -4
- package/agents/monitor.md +10 -9
- package/agents/reviewer.md +8 -8
- package/agents/scout.md +10 -10
- package/agents/supervisor.md +60 -45
- package/package.json +2 -2
- package/src/agents/hooks-deployer.test.ts +46 -41
- package/src/agents/hooks-deployer.ts +10 -9
- package/src/agents/manifest.test.ts +6 -2
- package/src/agents/overlay.test.ts +9 -7
- package/src/agents/overlay.ts +29 -7
- package/src/commands/agents.test.ts +1 -5
- package/src/commands/clean.test.ts +2 -5
- package/src/commands/clean.ts +25 -1
- package/src/commands/completions.test.ts +1 -1
- package/src/commands/completions.ts +26 -7
- package/src/commands/coordinator.test.ts +78 -78
- package/src/commands/coordinator.ts +92 -47
- package/src/commands/costs.test.ts +2 -6
- package/src/commands/dashboard.test.ts +2 -5
- package/src/commands/doctor.test.ts +2 -6
- package/src/commands/down.ts +3 -3
- package/src/commands/errors.test.ts +2 -6
- package/src/commands/feed.test.ts +2 -6
- package/src/commands/gateway.test.ts +39 -13
- package/src/commands/gateway.ts +95 -7
- package/src/commands/hooks.test.ts +2 -5
- package/src/commands/init.test.ts +4 -13
- package/src/commands/inspect.test.ts +2 -6
- package/src/commands/log.test.ts +2 -6
- package/src/commands/logs.test.ts +2 -9
- package/src/commands/mail.test.ts +76 -215
- package/src/commands/mail.ts +43 -187
- package/src/commands/metrics.test.ts +3 -10
- package/src/commands/nudge.ts +15 -0
- package/src/commands/prime.test.ts +4 -11
- package/src/commands/replay.test.ts +2 -6
- package/src/commands/server.test.ts +1 -5
- package/src/commands/server.ts +1 -1
- package/src/commands/sling.ts +40 -16
- package/src/commands/spec.test.ts +2 -5
- package/src/commands/status.test.ts +2 -4
- package/src/commands/stop.test.ts +2 -5
- package/src/commands/supervisor.ts +6 -6
- package/src/commands/trace.test.ts +2 -6
- package/src/commands/up.test.ts +43 -9
- package/src/commands/up.ts +15 -11
- package/src/commands/watchman.ts +327 -0
- package/src/commands/worktree.test.ts +2 -6
- package/src/config.test.ts +34 -104
- package/src/config.ts +120 -32
- package/src/doctor/agents.test.ts +7 -2
- package/src/doctor/config-check.test.ts +7 -2
- package/src/doctor/consistency.test.ts +7 -2
- package/src/doctor/databases.test.ts +6 -2
- package/src/doctor/dependencies.test.ts +35 -10
- package/src/doctor/dependencies.ts +16 -92
- package/src/doctor/logs.test.ts +7 -2
- package/src/doctor/merge-queue.test.ts +6 -2
- package/src/doctor/structure.test.ts +7 -2
- package/src/doctor/version.test.ts +7 -2
- package/src/e2e/init-sling-lifecycle.test.ts +2 -5
- package/src/index.ts +7 -7
- package/src/mail/pending.ts +120 -0
- package/src/mail/store.test.ts +89 -0
- package/src/mail/store.ts +11 -0
- package/src/merge/resolver.test.ts +518 -489
- package/src/server/index.ts +33 -2
- package/src/server/public/app.js +3 -3
- package/src/server/public/components/message-bubble.js +11 -1
- package/src/server/public/components/terminal-panel.js +66 -74
- package/src/server/public/views/chat.js +18 -2
- package/src/server/public/views/costs.js +5 -5
- package/src/server/public/views/dashboard.js +80 -51
- package/src/server/public/views/gateway-chat.js +37 -131
- package/src/server/public/views/inspect.js +16 -4
- package/src/server/public/views/issues.js +16 -12
- package/src/server/routes.test.ts +55 -39
- package/src/server/routes.ts +38 -26
- package/src/test-helpers.ts +6 -3
- package/src/tracker/beads.ts +159 -0
- package/src/tracker/exec.ts +44 -0
- package/src/tracker/factory.test.ts +283 -0
- package/src/tracker/factory.ts +59 -0
- package/src/tracker/seeds.ts +156 -0
- package/src/tracker/types.ts +46 -0
- package/src/types.ts +11 -2
- package/src/{watchdog → watchman}/daemon.test.ts +421 -515
- package/src/watchman/daemon.ts +940 -0
- package/src/worktree/tmux.test.ts +2 -1
- package/src/worktree/tmux.ts +4 -4
- package/templates/hooks.json.tmpl +17 -17
- package/src/beads/client.test.ts +0 -210
- package/src/commands/merge.test.ts +0 -676
- package/src/commands/watch.test.ts +0 -152
- package/src/commands/watch.ts +0 -238
- package/src/test-helpers.test.ts +0 -97
- package/src/watchdog/daemon.ts +0 -533
- package/src/watchdog/health.test.ts +0 -371
- package/src/watchdog/triage.test.ts +0 -162
- package/src/worktree/manager.test.ts +0 -444
- /package/src/{watchdog → watchman}/health.ts +0 -0
- /package/src/{watchdog → watchman}/triage.ts +0 -0
package/src/server/index.ts
CHANGED
|
@@ -15,7 +15,7 @@ export interface ServerOptions {
|
|
|
15
15
|
host: string;
|
|
16
16
|
root: string; // Project root directory
|
|
17
17
|
shouldOpen?: boolean; // Auto-open browser
|
|
18
|
-
autoStartCoordinator?: boolean; // Auto-start coordinator with --
|
|
18
|
+
autoStartCoordinator?: boolean; // Auto-start coordinator with --watchman on server start
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
/** Dependency injection for testing. */
|
|
@@ -97,13 +97,44 @@ async function tryStartCoordinator(root: string): Promise<void> {
|
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
// Start coordinator detached so the server doesn't wait on it
|
|
100
|
-
const startProc = spawn("legio", ["coordinator", "start", "--
|
|
100
|
+
const startProc = spawn("legio", ["coordinator", "start", "--watchman", "--no-attach"], {
|
|
101
101
|
cwd: root,
|
|
102
102
|
detached: true,
|
|
103
103
|
stdio: "ignore",
|
|
104
104
|
});
|
|
105
105
|
startProc.unref();
|
|
106
106
|
process.stdout.write("[legio] Coordinator started\n");
|
|
107
|
+
|
|
108
|
+
// Verify coordinator comes up: poll status up to 5 times at 3s intervals
|
|
109
|
+
const MAX_POLLS = 5;
|
|
110
|
+
const POLL_INTERVAL_MS = 3_000;
|
|
111
|
+
for (let i = 0; i < MAX_POLLS; i++) {
|
|
112
|
+
await new Promise<void>((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
113
|
+
const pollProc = spawn("legio", ["coordinator", "status", "--json"], {
|
|
114
|
+
cwd: root,
|
|
115
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
116
|
+
});
|
|
117
|
+
const pollChunks: Buffer[] = [];
|
|
118
|
+
pollProc.stdout?.on("data", (chunk: Buffer) => pollChunks.push(chunk));
|
|
119
|
+
const pollCode = await new Promise<number>((resolve) => {
|
|
120
|
+
pollProc.on("close", (code) => resolve(code ?? 1));
|
|
121
|
+
});
|
|
122
|
+
if (pollCode === 0) {
|
|
123
|
+
try {
|
|
124
|
+
const pollStatus = JSON.parse(Buffer.concat(pollChunks).toString()) as {
|
|
125
|
+
running?: boolean;
|
|
126
|
+
state?: string;
|
|
127
|
+
};
|
|
128
|
+
if (pollStatus.running && pollStatus.state !== "booting") {
|
|
129
|
+
process.stdout.write("[legio] Coordinator verified running\n");
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
} catch {
|
|
133
|
+
// Cannot parse — keep polling
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
process.stderr.write("[legio] Warning: coordinator still booting after verification checks\n");
|
|
107
138
|
}
|
|
108
139
|
|
|
109
140
|
/**
|
package/src/server/public/app.js
CHANGED
|
@@ -84,7 +84,7 @@ function Layout({ view, param }) {
|
|
|
84
84
|
|
|
85
85
|
return html`
|
|
86
86
|
<div class="flex flex-col h-screen bg-[#0f0f0f]">
|
|
87
|
-
<nav class="flex items-center justify-between px-4 border-b border-[#2a2a2a] bg-[#1a1a1a] shrink-0">
|
|
87
|
+
<nav class="flex items-center justify-between px-2 sm:px-4 border-b border-[#2a2a2a] bg-[#1a1a1a] shrink-0">
|
|
88
88
|
<div class="flex items-center">
|
|
89
89
|
${NAV_LINKS.map((link) => {
|
|
90
90
|
const isActive = link.view === view;
|
|
@@ -93,7 +93,7 @@ function Layout({ view, param }) {
|
|
|
93
93
|
key=${link.view}
|
|
94
94
|
href=${link.href}
|
|
95
95
|
class=${
|
|
96
|
-
"px-4 py-3 text-sm font-medium transition-colors border-b-2 " +
|
|
96
|
+
"px-3 sm:px-4 py-3 text-sm font-medium transition-colors border-b-2 " +
|
|
97
97
|
(isActive
|
|
98
98
|
? "text-white border-[#E64415]"
|
|
99
99
|
: "text-[#888] border-transparent hover:text-[#ccc]")
|
|
@@ -104,7 +104,7 @@ function Layout({ view, param }) {
|
|
|
104
104
|
`;
|
|
105
105
|
})}
|
|
106
106
|
</div>
|
|
107
|
-
<div class="flex items-center gap-
|
|
107
|
+
<div class="flex items-center gap-2 pr-1 shrink-0">
|
|
108
108
|
<span
|
|
109
109
|
class=${`w-2 h-2 rounded-full ${connected ? "bg-green-500" : "bg-[#444]"}`}
|
|
110
110
|
title=${connected ? "WebSocket connected" : "WebSocket disconnected"}
|
|
@@ -6,6 +6,16 @@ import { renderMarkdown } from "../lib/markdown.js";
|
|
|
6
6
|
import { html } from "../lib/preact-setup.js";
|
|
7
7
|
import { agentColor, timeAgo } from "../lib/utils.js";
|
|
8
8
|
|
|
9
|
+
function addStatusPrefixes(htmlStr) {
|
|
10
|
+
return htmlStr
|
|
11
|
+
.replace(/\[DONE\]/g, '<span class="text-green-400 font-semibold">[DONE]</span>')
|
|
12
|
+
.replace(/\[ERROR\]/g, '<span class="text-red-400 font-semibold">[ERROR]</span>')
|
|
13
|
+
.replace(/\[INFO\]/g, '<span class="text-blue-400 font-semibold">[INFO]</span>')
|
|
14
|
+
.replace(/\[WARN\]/g, '<span class="text-yellow-400 font-semibold">[WARN]</span>')
|
|
15
|
+
.replace(/\[PENDING\]/g, '<span class="text-[#999] font-semibold">[PENDING]</span>')
|
|
16
|
+
.replace(/\[MERGED\]/g, '<span class="text-purple-400 font-semibold">[MERGED]</span>');
|
|
17
|
+
}
|
|
18
|
+
|
|
9
19
|
/**
|
|
10
20
|
* MessageBubble — renders a single mail message as a conversational bubble.
|
|
11
21
|
*
|
|
@@ -64,7 +74,7 @@ export function MessageBubble({
|
|
|
64
74
|
</div>`
|
|
65
75
|
}
|
|
66
76
|
<div class="text-sm text-[#e5e5e5] break-words chat-markdown"
|
|
67
|
-
dangerouslySetInnerHTML=${{ __html: renderMarkdown(msg.body) }}></div>
|
|
77
|
+
dangerouslySetInnerHTML=${{ __html: addStatusPrefixes(renderMarkdown(msg.body)) }}></div>
|
|
68
78
|
</div>
|
|
69
79
|
`;
|
|
70
80
|
}
|
|
@@ -5,61 +5,45 @@ import { html, useEffect, useRef, useState } from "../lib/preact-setup.js";
|
|
|
5
5
|
|
|
6
6
|
function stripAnsi(str) {
|
|
7
7
|
// biome-ignore lint/suspicious/noControlCharactersInRegex: intentional ANSI escape strip
|
|
8
|
-
return str.replace(/\x1b\[[0-9;]*[
|
|
8
|
+
return str.replace(/\x1b\[[?0-9;]*[a-zA-Z]/g, "");
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const anchor = baselineLines.slice(-anchorLen);
|
|
16
|
-
|
|
17
|
-
// Search for the anchor sequence in current capture (prefer latest match)
|
|
18
|
-
for (let i = currentLines.length - anchorLen; i >= 0; i--) {
|
|
19
|
-
let match = true;
|
|
20
|
-
for (let j = 0; j < anchorLen; j++) {
|
|
21
|
-
if (currentLines[i + j] !== anchor[j]) {
|
|
22
|
-
match = false;
|
|
23
|
-
break;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
if (match) {
|
|
27
|
-
return currentLines.slice(i + anchorLen).join("\n");
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
// Baseline scrolled off — show tail
|
|
31
|
-
return currentLines.slice(-20).join("\n");
|
|
11
|
+
const ACTIVITY_LEVEL = { idle: 0, ready: 1, active: 2 };
|
|
12
|
+
|
|
13
|
+
function higherActivity(a, b) {
|
|
14
|
+
return (ACTIVITY_LEVEL[a] ?? 0) >= (ACTIVITY_LEVEL[b] ?? 0) ? a : b;
|
|
32
15
|
}
|
|
33
16
|
|
|
34
17
|
// TerminalPanel — collapsible terminal capture sub-component
|
|
35
18
|
// Props:
|
|
36
|
-
// chatTarget {string}
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const [
|
|
41
|
-
const [
|
|
42
|
-
const
|
|
19
|
+
// chatTarget {string} — agent name to capture terminal from
|
|
20
|
+
// activity {string} — 'idle'|'active'|'ready' hint from parent (e.g. gateway just sent a message)
|
|
21
|
+
// agentState {string|null} — agent session state ('working'|'booting'|'completed'|'zombie'|null)
|
|
22
|
+
export function TerminalPanel({ chatTarget, activity = "idle", agentState = null }) {
|
|
23
|
+
const [userExpanded, setUserExpanded] = useState(false);
|
|
24
|
+
const [captureText, setCaptureText] = useState("");
|
|
25
|
+
const [captureActivity, setCaptureActivity] = useState("idle");
|
|
26
|
+
const lastChangeTimeRef = useRef(Date.now());
|
|
27
|
+
const lastHashRef = useRef("");
|
|
28
|
+
const agentStateRef = useRef(agentState);
|
|
43
29
|
const terminalRef = useRef(null);
|
|
44
30
|
|
|
31
|
+
// Keep agentStateRef in sync with the agentState prop
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
agentStateRef.current = agentState;
|
|
34
|
+
}, [agentState]);
|
|
35
|
+
|
|
45
36
|
// Reset when chatTarget changes
|
|
46
37
|
useEffect(() => {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
38
|
+
setCaptureText("");
|
|
39
|
+
setCaptureActivity("idle");
|
|
40
|
+
lastChangeTimeRef.current = Date.now();
|
|
41
|
+
lastHashRef.current = "";
|
|
50
42
|
}, [chatTarget]);
|
|
51
43
|
|
|
52
|
-
//
|
|
44
|
+
// Always poll to drive the activity state machine via hash-based change detection
|
|
53
45
|
useEffect(() => {
|
|
54
|
-
if (!thinking && !expanded) {
|
|
55
|
-
setStreamText("");
|
|
56
|
-
setLoading(false);
|
|
57
|
-
baselineCaptureRef.current = null;
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
46
|
let cancelled = false;
|
|
62
|
-
setLoading(true);
|
|
63
47
|
|
|
64
48
|
async function pollCapture() {
|
|
65
49
|
try {
|
|
@@ -67,69 +51,77 @@ export function TerminalPanel({ chatTarget, thinking }) {
|
|
|
67
51
|
if (!res.ok || cancelled) return;
|
|
68
52
|
const data = await res.json();
|
|
69
53
|
const output = stripAnsi(data.output || "");
|
|
70
|
-
if (
|
|
54
|
+
if (cancelled) return;
|
|
71
55
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const delta = diffCapture(baselineCaptureRef.current, output);
|
|
79
|
-
if (!cancelled && delta.trim()) {
|
|
80
|
-
setStreamText(delta);
|
|
81
|
-
}
|
|
56
|
+
const now = Date.now();
|
|
57
|
+
if (output !== lastHashRef.current) {
|
|
58
|
+
// Capture changed — transition to active
|
|
59
|
+
lastHashRef.current = output;
|
|
60
|
+
lastChangeTimeRef.current = now;
|
|
61
|
+
setCaptureActivity("active");
|
|
82
62
|
} else {
|
|
83
|
-
//
|
|
84
|
-
|
|
85
|
-
|
|
63
|
+
// No change — use agent session state to determine activity level
|
|
64
|
+
const state = agentStateRef.current;
|
|
65
|
+
if (state === "working" || state === "booting") {
|
|
66
|
+
setCaptureActivity("ready");
|
|
67
|
+
} else {
|
|
68
|
+
setCaptureActivity("idle");
|
|
86
69
|
}
|
|
87
70
|
}
|
|
71
|
+
|
|
72
|
+
if (output.trim()) {
|
|
73
|
+
setCaptureText(output);
|
|
74
|
+
}
|
|
88
75
|
} catch (_err) {
|
|
89
|
-
// non-fatal — capture may fail if
|
|
76
|
+
// non-fatal — capture may fail if agent tmux session not ready
|
|
90
77
|
}
|
|
91
78
|
}
|
|
92
79
|
|
|
93
80
|
pollCapture();
|
|
94
|
-
const interval = setInterval(pollCapture,
|
|
81
|
+
const interval = setInterval(pollCapture, 2000);
|
|
95
82
|
return () => {
|
|
96
83
|
cancelled = true;
|
|
97
84
|
clearInterval(interval);
|
|
98
85
|
};
|
|
99
|
-
}, [
|
|
86
|
+
}, [chatTarget]);
|
|
100
87
|
|
|
101
88
|
// Auto-scroll terminal to bottom when new output arrives
|
|
102
89
|
useEffect(() => {
|
|
103
90
|
const el = terminalRef.current;
|
|
104
91
|
if (el) el.scrollTop = el.scrollHeight;
|
|
105
|
-
}, [
|
|
92
|
+
}, [captureText]);
|
|
93
|
+
|
|
94
|
+
// Effective activity: max of parent hint and internal capture-driven state
|
|
95
|
+
const effectiveActivity = higherActivity(activity, captureActivity);
|
|
96
|
+
|
|
97
|
+
const dotClass =
|
|
98
|
+
"w-2 h-2 rounded-full flex-shrink-0 " +
|
|
99
|
+
(effectiveActivity === "active"
|
|
100
|
+
? "bg-yellow-500 animate-pulse"
|
|
101
|
+
: effectiveActivity === "ready"
|
|
102
|
+
? "bg-green-500"
|
|
103
|
+
: "bg-[#333]");
|
|
106
104
|
|
|
107
105
|
return html`
|
|
108
106
|
<div class="border-t border-[#2a2a2a] shrink-0">
|
|
109
107
|
<div
|
|
110
108
|
class="flex items-center gap-2 px-3 py-1.5 cursor-pointer hover:bg-white/5"
|
|
111
|
-
onClick=${() =>
|
|
109
|
+
onClick=${() => setUserExpanded((prev) => !prev)}
|
|
112
110
|
>
|
|
113
|
-
<span
|
|
114
|
-
class=${
|
|
115
|
-
"w-2 h-2 rounded-full flex-shrink-0 " +
|
|
116
|
-
(thinking ? "bg-yellow-500 animate-pulse" : "bg-[#333]")
|
|
117
|
-
}
|
|
118
|
-
></span>
|
|
111
|
+
<span class=${dotClass}></span>
|
|
119
112
|
<span class="text-xs text-[#666]">Terminal</span>
|
|
120
|
-
${
|
|
121
|
-
|
|
113
|
+
${effectiveActivity === "active" ? html`<span class="text-xs text-yellow-500 animate-pulse">active</span>` : null}
|
|
114
|
+
${effectiveActivity === "ready" ? html`<span class="text-xs text-green-500">ready</span>` : null}
|
|
115
|
+
<span class="ml-auto text-xs text-[#444]">${userExpanded ? "\u25b2" : "\u25bc"}</span>
|
|
122
116
|
</div>
|
|
123
117
|
${
|
|
124
|
-
|
|
118
|
+
userExpanded
|
|
125
119
|
? html`
|
|
126
120
|
<div ref=${terminalRef} class="max-h-[200px] overflow-y-auto px-3 pb-2">
|
|
127
121
|
${
|
|
128
|
-
|
|
129
|
-
? html`<pre class="text-xs text-[#ccc] font-mono whitespace-pre-wrap break-words">${
|
|
130
|
-
:
|
|
131
|
-
? html`<div class="text-xs text-[#666] py-1 italic animate-pulse">Connecting...</div>`
|
|
132
|
-
: html`<div class="text-xs text-[#444] py-1 italic">No output yet</div>`
|
|
122
|
+
captureText
|
|
123
|
+
? html`<pre class="text-xs text-[#ccc] font-mono whitespace-pre-wrap break-words">${captureText}</pre>`
|
|
124
|
+
: html`<div class="text-xs text-[#444] py-1 italic">No output yet</div>`
|
|
133
125
|
}
|
|
134
126
|
</div>
|
|
135
127
|
`
|
|
@@ -138,6 +138,7 @@ export function ChatView({ state: propState, onSendMessage: propOnSendMessage })
|
|
|
138
138
|
const issues = appState.issues || [];
|
|
139
139
|
|
|
140
140
|
// UI state — local to this component, persisted across re-renders
|
|
141
|
+
const [sidebarOpen, setSidebarOpen] = useState(false);
|
|
141
142
|
const [selectedTask, setSelectedTask] = useState(null);
|
|
142
143
|
const [selectedAgent, setSelectedAgent] = useState(null);
|
|
143
144
|
const [expandedTasks, setExpandedTasks] = useState(() => new Set());
|
|
@@ -344,6 +345,7 @@ export function ChatView({ state: propState, onSendMessage: propOnSendMessage })
|
|
|
344
345
|
setSelectedTask(taskId);
|
|
345
346
|
setSelectedAgent(null);
|
|
346
347
|
setSelectedConversation(null);
|
|
348
|
+
setSidebarOpen(false);
|
|
347
349
|
// Auto-expand task on click (not for general section)
|
|
348
350
|
if (taskId && taskId !== "__general__") {
|
|
349
351
|
setExpandedTasks((prev) => {
|
|
@@ -371,12 +373,14 @@ export function ChatView({ state: propState, onSendMessage: propOnSendMessage })
|
|
|
371
373
|
e.stopPropagation();
|
|
372
374
|
setSelectedAgent(agentName);
|
|
373
375
|
setSelectedConversation(null);
|
|
376
|
+
setSidebarOpen(false);
|
|
374
377
|
}, []);
|
|
375
378
|
|
|
376
379
|
const handleAllMessagesClick = useCallback(() => {
|
|
377
380
|
setSelectedTask(null);
|
|
378
381
|
setSelectedAgent(null);
|
|
379
382
|
setSelectedConversation(null);
|
|
383
|
+
setSidebarOpen(false);
|
|
380
384
|
}, []);
|
|
381
385
|
|
|
382
386
|
const handleThreadToggle = useCallback((threadId) => {
|
|
@@ -395,6 +399,7 @@ export function ChatView({ state: propState, onSendMessage: propOnSendMessage })
|
|
|
395
399
|
setSelectedConversation(conv);
|
|
396
400
|
setSelectedTask(null);
|
|
397
401
|
setSelectedAgent(null);
|
|
402
|
+
setSidebarOpen(false);
|
|
398
403
|
}, []);
|
|
399
404
|
|
|
400
405
|
const handleSend = useCallback(async () => {
|
|
@@ -602,11 +607,11 @@ export function ChatView({ state: propState, onSendMessage: propOnSendMessage })
|
|
|
602
607
|
}
|
|
603
608
|
|
|
604
609
|
return html`
|
|
605
|
-
<div class="flex h-full">
|
|
610
|
+
<div class="flex flex-col md:flex-row h-full">
|
|
606
611
|
|
|
607
612
|
<!-- Sidebar -->
|
|
608
613
|
<div
|
|
609
|
-
class=
|
|
614
|
+
class={`w-full md:w-64 bg-[#0f0f0f] border-r border-[#2a2a2a] border-b md:border-b-0 overflow-y-auto flex-shrink-0${sidebarOpen ? '' : ' hidden md:block'}`}
|
|
610
615
|
>
|
|
611
616
|
<!-- All Messages item -->
|
|
612
617
|
<div
|
|
@@ -793,6 +798,17 @@ export function ChatView({ state: propState, onSendMessage: propOnSendMessage })
|
|
|
793
798
|
|
|
794
799
|
<!-- Header -->
|
|
795
800
|
<div class="px-4 py-3 border-b border-[#2a2a2a] flex items-center gap-2 flex-wrap">
|
|
801
|
+
<button
|
|
802
|
+
class="md:hidden flex-shrink-0 text-[#999] hover:text-[#e5e5e5] p-1"
|
|
803
|
+
onClick=${() => setSidebarOpen((o) => !o)}
|
|
804
|
+
aria-label="Toggle sidebar"
|
|
805
|
+
>
|
|
806
|
+
<svg width="18" height="18" viewBox="0 0 18 18" fill="currentColor">
|
|
807
|
+
<rect x="1" y="3" width="16" height="2"/>
|
|
808
|
+
<rect x="1" y="8" width="16" height="2"/>
|
|
809
|
+
<rect x="1" y="13" width="16" height="2"/>
|
|
810
|
+
</svg>
|
|
811
|
+
</button>
|
|
796
812
|
${
|
|
797
813
|
selectedConversation
|
|
798
814
|
? html`
|
|
@@ -66,7 +66,7 @@ function modelColor(model) {
|
|
|
66
66
|
|
|
67
67
|
function StatCard({ label, value }) {
|
|
68
68
|
return html`
|
|
69
|
-
<div class="bg-surface border border-border rounded-sm p-4 flex-1 min-w-
|
|
69
|
+
<div class="bg-surface border border-border rounded-sm p-4 flex-1 min-w-[140px]">
|
|
70
70
|
<div class="text-xs text-gray-500 uppercase tracking-wider mb-1">${label}</div>
|
|
71
71
|
<div class="text-2xl font-mono text-white">${value}</div>
|
|
72
72
|
</div>
|
|
@@ -95,7 +95,7 @@ function ModelBreakdown({ modelData }) {
|
|
|
95
95
|
const color = modelColor(row.model);
|
|
96
96
|
return html`
|
|
97
97
|
<div key=${row.model || "unknown"} class="flex items-center gap-3">
|
|
98
|
-
<div class="flex items-center gap-2 w-[140px] shrink-0">
|
|
98
|
+
<div class="flex items-center gap-2 w-[100px] md:w-[140px] shrink-0">
|
|
99
99
|
<div class=${`${color} w-2 h-2 rounded-full shrink-0`}></div>
|
|
100
100
|
<span class="text-sm text-gray-300 truncate">${row.model || "unknown"}</span>
|
|
101
101
|
</div>
|
|
@@ -198,7 +198,7 @@ function AgentBarChart({ metrics }) {
|
|
|
198
198
|
<div key=${name} class="flex items-center gap-3">
|
|
199
199
|
<a
|
|
200
200
|
href=${`#inspect/${encodeURIComponent(name)}`}
|
|
201
|
-
class="text-blue-400 hover:text-blue-300 text-sm w-[140px] shrink-0 truncate"
|
|
201
|
+
class="text-blue-400 hover:text-blue-300 text-sm w-[100px] md:w-[140px] shrink-0 truncate"
|
|
202
202
|
>
|
|
203
203
|
${name}
|
|
204
204
|
</a>
|
|
@@ -298,7 +298,7 @@ function CapabilityChart({ metrics }) {
|
|
|
298
298
|
const pct = maxCost > 0 ? (cost / maxCost) * 100 : 0;
|
|
299
299
|
return html`
|
|
300
300
|
<div key=${cap} class="flex items-center gap-3">
|
|
301
|
-
<span class="text-sm text-gray-400 w-[140px] shrink-0 truncate">${cap}</span>
|
|
301
|
+
<span class="text-sm text-gray-400 w-[100px] md:w-[140px] shrink-0 truncate">${cap}</span>
|
|
302
302
|
<div class="flex-1 bg-white/5 rounded-sm h-5 overflow-hidden">
|
|
303
303
|
<div
|
|
304
304
|
class="bg-blue-400 h-full rounded-sm"
|
|
@@ -510,7 +510,7 @@ export function CostsView({ metrics: initialMetrics, snapshots }) {
|
|
|
510
510
|
</div>
|
|
511
511
|
|
|
512
512
|
<!-- Summary Stat Cards -->
|
|
513
|
-
<div class="flex gap-4">
|
|
513
|
+
<div class="flex flex-wrap gap-4">
|
|
514
514
|
<${StatCard}
|
|
515
515
|
label="Total Cost"
|
|
516
516
|
value=${totals.cost != null ? formatCostShort(totals.cost) : "—"}
|
|
@@ -93,7 +93,7 @@ function MetricsStrip({ agents, status }) {
|
|
|
93
93
|
<div class="border-b border-[#2a2a2a] px-3 py-1.5 text-xs font-bold uppercase tracking-wider text-gray-400">
|
|
94
94
|
Metrics
|
|
95
95
|
</div>
|
|
96
|
-
<div class="flex flex-wrap gap-
|
|
96
|
+
<div class="flex flex-wrap gap-x-3 gap-y-1 px-3 py-2">
|
|
97
97
|
${stats.map(
|
|
98
98
|
({ label, value }) => html`
|
|
99
99
|
<span key=${label} class="text-xs text-gray-400">
|
|
@@ -352,7 +352,7 @@ function AgentRoster({ agents, mail, events }) {
|
|
|
352
352
|
<div
|
|
353
353
|
key=${agent.agentName}
|
|
354
354
|
class="mb-1 rounded border border-[#2a2a2a] bg-[#1a1a1a] overflow-hidden"
|
|
355
|
-
style=${{ marginLeft: `${depth *
|
|
355
|
+
style=${{ marginLeft: `${Math.min(depth * 12, 36)}px` }}
|
|
356
356
|
>
|
|
357
357
|
<!-- Row -->
|
|
358
358
|
<div
|
|
@@ -609,54 +609,50 @@ function CoordinatorBar() {
|
|
|
609
609
|
const gwStatusText = gwIsRunning ? "Running" : gwIsStopped ? "Stopped" : "Unknown";
|
|
610
610
|
|
|
611
611
|
return html`
|
|
612
|
-
<div class="
|
|
613
|
-
<div class="flex
|
|
614
|
-
|
|
615
|
-
<div class="flex items-center gap-
|
|
616
|
-
<
|
|
612
|
+
<div class="bg-[#1a1a1a] border-b border-[#2a2a2a] shrink-0 px-3 py-2">
|
|
613
|
+
<div class="flex flex-wrap gap-x-4 gap-y-1.5 items-center">
|
|
614
|
+
<!-- Coordinator group -->
|
|
615
|
+
<div class="flex items-center gap-2">
|
|
616
|
+
<span class="text-xs text-[#666] uppercase tracking-wide whitespace-nowrap">Coordinator</span>
|
|
617
|
+
<div class="w-2 h-2 rounded-full shrink-0 ${dotColor}"></div>
|
|
617
618
|
<span class="text-sm text-[#e5e5e5]">${statusText}</span>
|
|
619
|
+
<button
|
|
620
|
+
onClick=${handleStart}
|
|
621
|
+
disabled=${loading || isRunning}
|
|
622
|
+
class="bg-[#E64415] hover:bg-[#cc3d12] disabled:opacity-50 text-white text-xs px-2 py-0.5 rounded cursor-pointer border-none"
|
|
623
|
+
>
|
|
624
|
+
${loading && !isRunning ? "\u2026" : "Start"}
|
|
625
|
+
</button>
|
|
626
|
+
<button
|
|
627
|
+
onClick=${handleStop}
|
|
628
|
+
disabled=${loading || isStopped || isUnknown}
|
|
629
|
+
class="bg-[#E64415] hover:bg-[#cc3d12] disabled:opacity-50 text-white text-xs px-2 py-0.5 rounded cursor-pointer border-none"
|
|
630
|
+
>
|
|
631
|
+
${loading && isRunning ? "\u2026" : "Stop"}
|
|
632
|
+
</button>
|
|
618
633
|
</div>
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
disabled=${loading || isRunning}
|
|
624
|
-
class="bg-[#E64415] hover:bg-[#cc3d12] disabled:opacity-50 text-white text-sm px-3 py-1 rounded cursor-pointer border-none"
|
|
625
|
-
>
|
|
626
|
-
${loading && !isRunning ? "\u2026" : "Start"}
|
|
627
|
-
</button>
|
|
628
|
-
<button
|
|
629
|
-
onClick=${handleStop}
|
|
630
|
-
disabled=${loading || isStopped || isUnknown}
|
|
631
|
-
class="bg-[#E64415] hover:bg-[#cc3d12] disabled:opacity-50 text-white text-sm px-3 py-1 rounded cursor-pointer border-none"
|
|
632
|
-
>
|
|
633
|
-
${loading && isRunning ? "\u2026" : "Stop"}
|
|
634
|
-
</button>
|
|
635
|
-
</div>
|
|
636
|
-
<div class="border-l border-[#2a2a2a] pl-3 ml-1 flex items-center gap-2">
|
|
637
|
-
<span class="text-xs text-[#666] uppercase tracking-wide">Gateway</span>
|
|
638
|
-
<div class="flex items-center gap-1">
|
|
639
|
-
<div class="w-2 h-2 rounded-full ${gwDotColor}"></div>
|
|
634
|
+
<!-- Gateway group -->
|
|
635
|
+
<div class="flex items-center gap-2">
|
|
636
|
+
<span class="text-xs text-[#666] uppercase tracking-wide whitespace-nowrap">Gateway</span>
|
|
637
|
+
<div class="w-2 h-2 rounded-full shrink-0 ${gwDotColor}"></div>
|
|
640
638
|
<span class="text-sm text-[#e5e5e5]">${gwStatusText}</span>
|
|
639
|
+
<button
|
|
640
|
+
onClick=${handleGwStart}
|
|
641
|
+
disabled=${gwLoading || gwIsRunning}
|
|
642
|
+
class="bg-[#E64415] hover:bg-[#cc3d12] disabled:opacity-50 text-white text-xs px-2 py-0.5 rounded cursor-pointer border-none"
|
|
643
|
+
>
|
|
644
|
+
${gwLoading && !gwIsRunning ? "\u2026" : "Start"}
|
|
645
|
+
</button>
|
|
646
|
+
<button
|
|
647
|
+
onClick=${handleGwStop}
|
|
648
|
+
disabled=${gwLoading || gwIsStopped || gwIsUnknown}
|
|
649
|
+
class="bg-[#E64415] hover:bg-[#cc3d12] disabled:opacity-50 text-white text-xs px-2 py-0.5 rounded cursor-pointer border-none"
|
|
650
|
+
>
|
|
651
|
+
${gwLoading && gwIsRunning ? "\u2026" : "Stop"}
|
|
652
|
+
</button>
|
|
641
653
|
</div>
|
|
642
654
|
</div>
|
|
643
|
-
|
|
644
|
-
<button
|
|
645
|
-
onClick=${handleGwStart}
|
|
646
|
-
disabled=${gwLoading || gwIsRunning}
|
|
647
|
-
class="bg-[#E64415] hover:bg-[#cc3d12] disabled:opacity-50 text-white text-sm px-3 py-1 rounded cursor-pointer border-none"
|
|
648
|
-
>
|
|
649
|
-
${gwLoading && !gwIsRunning ? "\u2026" : "Start"}
|
|
650
|
-
</button>
|
|
651
|
-
<button
|
|
652
|
-
onClick=${handleGwStop}
|
|
653
|
-
disabled=${gwLoading || gwIsStopped || gwIsUnknown}
|
|
654
|
-
class="bg-[#E64415] hover:bg-[#cc3d12] disabled:opacity-50 text-white text-sm px-3 py-1 rounded cursor-pointer border-none"
|
|
655
|
-
>
|
|
656
|
-
${gwLoading && gwIsRunning ? "\u2026" : "Stop"}
|
|
657
|
-
</button>
|
|
658
|
-
</div>
|
|
659
|
-
${error ? html`<span class="text-xs text-red-400">${error}</span>` : null}
|
|
655
|
+
${error ? html`<div class="text-xs text-red-400 mt-1">${error}</div>` : null}
|
|
660
656
|
</div>
|
|
661
657
|
`;
|
|
662
658
|
}
|
|
@@ -754,23 +750,56 @@ export function DashboardView() {
|
|
|
754
750
|
};
|
|
755
751
|
}, []);
|
|
756
752
|
|
|
753
|
+
const [mobileTab, setMobileTab] = useState("chat");
|
|
757
754
|
const agents = appState.agents.value;
|
|
758
755
|
const status = appState.status.value;
|
|
759
756
|
|
|
760
757
|
return html`
|
|
761
758
|
<div class="flex flex-col h-full bg-[#0f0f0f] min-h-0">
|
|
762
759
|
<${CoordinatorBar} />
|
|
763
|
-
|
|
764
|
-
|
|
760
|
+
<!-- Mobile tab bar (hidden on md+) -->
|
|
761
|
+
<div class="flex border-b border-[#2a2a2a] bg-[#1a1a1a] shrink-0 md:hidden">
|
|
762
|
+
<button
|
|
763
|
+
onClick=${() => setMobileTab("chat")}
|
|
764
|
+
class=${
|
|
765
|
+
"flex-1 py-2 text-sm font-medium border-b-2 " +
|
|
766
|
+
(mobileTab === "chat"
|
|
767
|
+
? "text-white border-[#E64415]"
|
|
768
|
+
: "text-[#888] border-transparent")
|
|
769
|
+
}
|
|
770
|
+
>
|
|
771
|
+
Chat
|
|
772
|
+
</button>
|
|
773
|
+
<button
|
|
774
|
+
onClick=${() => setMobileTab("status")}
|
|
775
|
+
class=${
|
|
776
|
+
"flex-1 py-2 text-sm font-medium border-b-2 " +
|
|
777
|
+
(mobileTab === "status"
|
|
778
|
+
? "text-white border-[#E64415]"
|
|
779
|
+
: "text-[#888] border-transparent")
|
|
780
|
+
}
|
|
781
|
+
>
|
|
782
|
+
Status
|
|
783
|
+
</button>
|
|
784
|
+
</div>
|
|
785
|
+
<div class="flex flex-col md:flex-row flex-1 min-h-0">
|
|
786
|
+
<!-- Chat panel: full height on mobile (when chat tab active), left ~58% on md+ -->
|
|
765
787
|
<div
|
|
766
|
-
class
|
|
767
|
-
|
|
788
|
+
class=${
|
|
789
|
+
"flex-col min-h-0 overflow-hidden md:border-r border-[#2a2a2a] md:flex-[58_1_0%] flex-1 " +
|
|
790
|
+
(mobileTab === "chat" ? "flex" : "hidden md:flex")
|
|
791
|
+
}
|
|
768
792
|
>
|
|
769
793
|
<${GatewayChat} gwRunning=${gwRunning} />
|
|
770
794
|
</div>
|
|
771
795
|
|
|
772
|
-
<!-- Sidebar
|
|
773
|
-
<div
|
|
796
|
+
<!-- Sidebar: MetricsStrip + AgentRoster + MailFeed — right ~42% on md+ -->
|
|
797
|
+
<div
|
|
798
|
+
class=${
|
|
799
|
+
"flex-col min-h-0 overflow-hidden md:flex-[42_1_0%] flex-1 " +
|
|
800
|
+
(mobileTab === "status" ? "flex" : "hidden md:flex")
|
|
801
|
+
}
|
|
802
|
+
>
|
|
774
803
|
<${MetricsStrip} agents=${agents} status=${status} />
|
|
775
804
|
<${AgentRoster} agents=${agents} mail=${mail} events=${activityEvents} />
|
|
776
805
|
<${MailFeed} mail=${mail} />
|