mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-23 10:45:33 -04:00
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:
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user