From f5ddded1eee12ff1caac3d2d08f01cecf8fc001e Mon Sep 17 00:00:00 2001 From: Artur Do Lago Date: Thu, 15 Jan 2026 11:32:26 +0100 Subject: [PATCH] feat(tui): add system monitor lights for internet and LLM providers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- CLAUDE.md | 19 +++++- .../src/cli/cmd/tui/context/sync.tsx | 12 ++++ .../src/cli/cmd/tui/routes/session/footer.tsx | 21 +++++++ .../agent-core/src/server/route/global.ts | 59 +++++++++++++++++++ 4 files changed, 109 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 18e1c7d75e..6d680490e5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 diff --git a/packages/agent-core/src/cli/cmd/tui/context/sync.tsx b/packages/agent-core/src/cli/cmd/tui/context/sync.tsx index 006f550179..df132c7392 100644 --- a/packages/agent-core/src/cli/cmd/tui/context/sync.tsx +++ b/packages/agent-core/src/cli/cmd/tui/context/sync.tsx @@ -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") }) diff --git a/packages/agent-core/src/cli/cmd/tui/routes/session/footer.tsx b/packages/agent-core/src/cli/cmd/tui/routes/session/footer.tsx index 4883ea5220..51d8c76526 100644 --- a/packages/agent-core/src/cli/cmd/tui/routes/session/footer.tsx +++ b/packages/agent-core/src/cli/cmd/tui/routes/session/footer.tsx @@ -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" : ""} + + + + + + + + + + + + + {" "}NET + + 0}> + + {connectedProviders()} LLM + + 0 ? theme.success : theme.textMuted }}>• {lsp().length} LSP diff --git a/packages/agent-core/src/server/route/global.ts b/packages/agent-core/src/server/route/global.ts index cc30993864..b4a6de8819 100644 --- a/packages/agent-core/src/server/route/global.ts +++ b/packages/agent-core/src/server/route/global.ts @@ -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 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({