feat(tui): add system monitor lights for internet and LLM providers

Add health status indicators to the TUI footer:
- Internet connectivity (◉ NET) with green/red/muted states
- Connected LLM providers count (◈ N LLM)

Also update CLAUDE.md to document both installation methods
(bun global install and manual ~/bin/agent-core).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Artur Do Lago
2026-01-15 11:32:26 +01:00
parent 5564f506c6
commit f5ddded1ee
4 changed files with 109 additions and 2 deletions
+17 -2
View File
@@ -257,13 +257,28 @@ By default, the canonical location is `~/.local/src/agent-core`, but the project
### Binary Location
**Installed binary:** `~/bin/agent-core` (also `$AGENT_CORE_BIN`)
Two installation methods are supported:
1. **Bun global install (recommended):** `~/.bun/bin/agent-core`
- Installed via `bun install -g agent-core`
- Wrapper script resolves the actual binary automatically
2. **Manual install:** `~/bin/agent-core` (also `$AGENT_CORE_BIN`)
- Direct binary copy from build output
- Used by `reload.sh` script
**Run from anywhere:** The binary can be launched from any directory. Just `cd` to your project folder and run `agent-core`.
### Rebuilding
**Recommended:** Use the reload script (handles kill, build, copy, restart):
**For bun global install:**
```bash
# Build and the global install auto-updates
cd packages/agent-core && bun run build
```
**For manual install (reload script):**
```bash
# Full reload - kill all, rebuild, restart daemon
@@ -74,6 +74,10 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
formatter: FormatterStatus[]
vcs: VcsInfo | undefined
path: Path
health: {
internet: "ok" | "fail" | "checking"
providers: { id: string; name: string; status: "ok" | "fail" | "skip" }[]
}
}>({
provider_next: {
all: [],
@@ -101,6 +105,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
formatter: [],
vcs: undefined,
path: { state: "", config: "", worktree: "", directory: "" },
health: { internet: "checking", providers: [] },
})
const sdk = useSDK()
@@ -389,6 +394,13 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
sdk.client.provider.auth().then((x) => setStore("provider_auth", reconcile(x.data ?? {}))),
sdk.client.vcs.get().then((x) => setStore("vcs", reconcile(x.data))),
sdk.client.path.get().then((x) => setStore("path", reconcile(x.data!))),
// Fetch health status (internet + providers)
fetch(`${sdk.url}/global/health/status`)
.then((res) => res.json())
.then((data: { internet: "ok" | "fail" | "checking"; providers: { id: string; name: string; status: "ok" | "fail" | "skip" }[] }) =>
setStore("health", reconcile(data)),
)
.catch(() => setStore("health", "internet", "fail")),
]).then(() => {
setStore("status", "complete")
})
@@ -15,6 +15,8 @@ export function Footer() {
const mcp = createMemo(() => Object.values(sync.data.mcp).filter((x) => x.status === "connected").length)
const mcpError = createMemo(() => Object.values(sync.data.mcp).some((x) => x.status === "failed"))
const lsp = createMemo(() => Object.keys(sync.data.lsp))
const internet = createMemo(() => sync.data.health.internet)
const connectedProviders = createMemo(() => sync.data.health.providers.filter((p) => p.status === "ok").length)
const permissions = createMemo(() => {
if (route.data.type !== "session") return []
return sync.data.permission[route.data.sessionID] ?? []
@@ -71,6 +73,25 @@ export function Footer() {
{permissions().length > 1 ? "s" : ""}
</text>
</Show>
<text fg={theme.text}>
<Switch>
<Match when={internet() === "ok"}>
<span style={{ fg: theme.success }}></span>
</Match>
<Match when={internet() === "fail"}>
<span style={{ fg: theme.error }}></span>
</Match>
<Match when={internet() === "checking"}>
<span style={{ fg: theme.textMuted }}></span>
</Match>
</Switch>
{" "}NET
</text>
<Show when={connectedProviders() > 0}>
<text fg={theme.text}>
<span style={{ fg: theme.success }}></span> {connectedProviders()} LLM
</text>
</Show>
<text fg={theme.text}>
<span style={{ fg: lsp().length > 0 ? theme.success : theme.textMuted }}></span> {lsp().length} LSP
</text>
@@ -4,6 +4,19 @@ import { z } from "zod"
import { streamSSE } from "hono/streaming"
import { GlobalBus } from "@/bus/global"
import { Instance } from "../../project/instance"
import { Provider } from "@/provider/provider"
// Health status schema for system monitoring
const HealthStatus = z.object({
internet: z.enum(["ok", "fail", "checking"]),
providers: z.array(
z.object({
id: z.string(),
name: z.string(),
status: z.enum(["ok", "fail", "skip"]),
}),
),
})
export const GlobalRoute = new Hono()
.get(
@@ -27,6 +40,52 @@ export const GlobalRoute = new Hono()
return c.json(true)
},
)
.get(
"/health/status",
describeRoute({
summary: "System health status",
description: "Get internet connectivity and LLM provider status for system monitoring.",
operationId: "health.status",
responses: {
200: {
description: "Health status",
content: {
"application/json": {
schema: resolver(HealthStatus),
},
},
},
},
}),
async (c) => {
// Check internet connectivity
let internet: "ok" | "fail" | "checking" = "checking"
try {
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), 3000)
const response = await fetch("https://cloudflare.com/cdn-cgi/trace", {
method: "HEAD",
signal: controller.signal,
})
clearTimeout(timeout)
internet = response.ok || response.status < 500 ? "ok" : "fail"
} catch {
internet = "fail"
}
// Get configured providers and their connection status
// Provider.list() returns Record<string, Provider.Info> of loaded providers
const loadedProviders = await Provider.list()
const providers = Object.values(loadedProviders).map((p) => ({
id: p.id,
name: p.name,
status: "ok" as "ok" | "fail" | "skip",
}))
return c.json({ internet, providers })
},
)
.get(
"/event",
describeRoute({