Admin view in the web UI

This commit is contained in:
Ian Gulliver
2026-10-14 21:06:30 -07:00
parent b2bd894677
commit 4a2bbe0859
34 changed files with 838 additions and 214 deletions
+28 -3
View File
@@ -726,11 +726,18 @@ components:
Status: Status:
type: object type: object
required: [started_at, db_bytes, last_event_seq, projects, active_tasks, sse_clients, live_agents, running_turns, queued_turns, rate_limits] required: [started_at, db_bytes, last_event_seq, projects, active_tasks, sse_clients, live_agents, running_turns, queued_turns, caps, rate_limits]
properties: properties:
live_agents: { type: integer, description: Agent processes currently running. } live_agents: { type: integer, description: Agent processes currently running. }
running_turns: { type: integer } running_turns:
queued_turns: { type: integer } type: array
description: Turns running now, oldest session first.
items: { $ref: "#/components/schemas/TurnRef" }
queued_turns:
type: array
description: Turns waiting for room under the caps, in the order they will start.
items: { $ref: "#/components/schemas/TurnRef" }
caps: { $ref: "#/components/schemas/Caps" }
rate_limits: rate_limits:
type: object type: object
description: The latest rate-limit status each vendor reported, keyed by vendor. description: The latest rate-limit status each vendor reported, keyed by vendor.
@@ -741,3 +748,21 @@ components:
projects: { type: integer } projects: { type: integer }
active_tasks: { type: integer } active_tasks: { type: integer }
sse_clients: { type: integer } sse_clients: { type: integer }
Caps:
type: object
required: [global, per_vendor]
properties:
global: { type: integer, description: Most turns running at once across all vendors. }
per_vendor:
type: object
description: Most turns running at once per vendor, keyed by vendor.
additionalProperties: { type: integer }
TurnRef:
type: object
required: [session_id, task_id, vendor]
properties:
session_id: { type: string }
task_id: { type: string }
vendor: { type: string }
+7 -1
View File
@@ -74,7 +74,13 @@ A turn's final message is its summary. When a turn ends normally (not an error,
Running turns are capped globally and per vendor (`DefaultLimits` in `internal/agents/manager.go`). A turn that would exceed a cap leaves its session `queued`, with the message held in the `turn_queue` table; whenever a turn ends, a process exits, or a task is merged or discarded, the oldest queued turns whose vendor has room start. Interrupting a `queued` session cancels its queued turn. Idle sessions with a live process don't count against the caps. Whether a turn resumes the Claude conversation is decided by whether the session has ever sent one. Running turns are capped globally and per vendor (`DefaultLimits` in `internal/agents/manager.go`). A turn that would exceed a cap leaves its session `queued`, with the message held in the `turn_queue` table; whenever a turn ends, a process exits, or a task is merged or discarded, the oldest queued turns whose vendor has room start. Interrupting a `queued` session cancels its queued turn. Idle sessions with a live process don't count against the caps. Whether a turn resumes the Claude conversation is decided by whether the session has ever sent one.
When Claude Code reports a rate-limit status of `rejected` and the turn then fails, the inbox gets a `rate_limited` item carrying the vendor's status (including when the limit resets) instead of a generic error. `GET /api/admin/status` reports running and queued turns and the latest rate-limit status each vendor sent. When Claude Code reports a rate-limit status of `rejected` and the turn then fails, the inbox gets a `rate_limited` item carrying the vendor's status (including when the limit resets) instead of a generic error.
## Admin view
`GET /api/admin/status` reports `running_turns` (sessions now `running`) and `queued_turns` (rows of `turn_queue`, in the order they will start), each as session, task and vendor; `caps`, the global and per-vendor limits from `DefaultLimits`; `rate_limits`, the latest `rate_limit_info` each vendor sent since byre started, unchanged; and server counts (start time, agent processes, projects, active tasks, SSE clients, database size including WAL, last event seq). A turn's queue row is written before its session's `queued` state event, so a refetch triggered by that event sees it.
The Admin view (a pane tab, also opened in the right pane by the navbar's **Admin** button) shows all of it: running turns against each cap, queued turns in order, both linking to their tasks, each vendor's rate-limit status with its reset time and per-window utilization, and the server counts. It refetches the status when a `session.created`, `session.state`, `task.*` or `project.created` event arrives, when an `agent.vendor` event carries a `rate_limit_event`, and when the SSE stream reconnects; other events only advance the last-event seq it shows. There is no timer, so the agent-process count can lag until one of those events, and the "in 2h 10m" beside a reset time is as of the last render.
Agent activity becomes events with `agent_session_id` set: `user.message`, `agent.message`, `agent.thinking`, `agent.tool_call`, `agent.tool_result`, `turn.ended`, `session.state`, `session.model`, and `session.context`. Anything else Claude Code emits (rate-limit status, task progress, state changes) is recorded unchanged as `agent.vendor`. The one exception is partial output: each top-level `text_delta` is broadcast to SSE clients as an `agent.delta` event (`payload.text`, `seq` 0) that is never stored, and the transcript grows a live message from them until the finished `agent.message` replaces it. A `turn.ended` event carries `inbox_item_id` when its final message was filed as a `report` item. `session.context` records the context size whenever it changes: `used` is the last assistant message's input, cache-read, cache-creation and output tokens, and `window` is the model's context window from the turn's `result` (`modelUsage[model].contextWindow`), so it is 0 until the session's first turn after a server start ends. The task header shows the latest one as a percentage of the window, coloured green, amber from 70%, red from 90%. Agent activity becomes events with `agent_session_id` set: `user.message`, `agent.message`, `agent.thinking`, `agent.tool_call`, `agent.tool_result`, `turn.ended`, `session.state`, `session.model`, and `session.context`. Anything else Claude Code emits (rate-limit status, task progress, state changes) is recorded unchanged as `agent.vendor`. The one exception is partial output: each top-level `text_delta` is broadcast to SSE clients as an `agent.delta` event (`payload.text`, `seq` 0) that is never stored, and the transcript grows a live message from them until the finished `agent.message` replaces it. A `turn.ended` event carries `inbox_item_id` when its final message was filed as a `report` item. `session.context` records the context size whenever it changes: `used` is the last assistant message's input, cache-read, cache-creation and output tokens, and `window` is the model's context window from the turn's `result` (`modelUsage[model].contextWindow`), so it is 0 until the session's first turn after a server start ends. The task header shows the latest one as a percentage of the window, coloured green, amber from 70%, red from 90%.
+1 -1
View File
@@ -474,7 +474,7 @@ Record transcripts from the spike as the seed of the contract test suite.
### Milestones ### Milestones
- **M0 — Skeleton.** launchd service, tailscale serve auth, SQLite schema, OpenAPI spec, projects, tasks, worktree create/delete, event log, SSE stream with resume, embedded SPA shell, basic server observability. - **M0 — Skeleton.** launchd service, tailscale serve auth, SQLite schema, OpenAPI spec, projects, tasks, worktree create/delete, event log, SSE stream with resume, embedded SPA shell, basic server observability.
- **Status: done**, and running as a service on the Mac Studio. Observability is exposed through `GET /api/admin/status` only; the SPA has no admin view yet. - **Status: done**, and running as a service on the Mac Studio. Observability covers queue state and vendor rate-limit status, through `GET /api/admin/status` and the SPA's Admin view; process liveness and stuck-process detection, adapter parse errors, and per-task snapshot counts and disk usage are not done.
- **M1 — One agent end to end (Claude).** Sessions, streaming, messaging, the interface coverage for Claude, the `ask_human` MCP tool, turn summaries, inbox with all views and response types, web push notifications, restart-and-resume, concurrency caps. - **M1 — One agent end to end (Claude).** Sessions, streaming, messaging, the interface coverage for Claude, the `ask_human` MCP tool, turn summaries, inbox with all views and response types, web push notifications, restart-and-resume, concurrency caps.
- **Done:** sessions, streaming, messaging, interrupt; `ask_human` with validation; each turn's final message filed to the inbox as its summary; inbox with global, project, and task views and every response type, answered as one batch and delivered to the agent as one message; restart-and-resume; global and per-vendor caps with queueing; rate limits as their own inbox state; the web UI (two panes, task switcher, transcript, inbox) with Markdown rendering; the session instructions from "Session configuration", given as an appended system prompt and a per-turn reminder; per-turn commits by byre; bouncing questions asked in plain prose. - **Done:** sessions, streaming, messaging, interrupt; `ask_human` with validation; each turn's final message filed to the inbox as its summary; inbox with global, project, and task views and every response type, answered as one batch and delivered to the agent as one message; restart-and-resume; global and per-vendor caps with queueing; rate limits as their own inbox state; the web UI (two panes, task switcher, transcript, inbox) with Markdown rendering; the session instructions from "Session configuration", given as an appended system prompt and a per-turn reminder; per-turn commits by byre; bouncing questions asked in plain prose.
- **Not done:** web push notifications. Compaction is done, as a handoff into a fresh session; model switching is done, per message; image input is done, pasted or dropped into the composer and stored as blobs. - **Not done:** web push notifications. Compaction is done, as a handoff into a fresh session; model switching is done, per message; image input is done, pasted or dropped into the composer and stored as blobs.
+27 -12
View File
@@ -282,27 +282,38 @@ func (m *Manager) LiveCount() int {
} }
type Usage struct { type Usage struct {
Running int Running []api.TurnRef
Queued int Queued []api.TurnRef
Caps api.Caps
RateLimits map[string]map[string]any RateLimits map[string]map[string]any
} }
func (m *Manager) Usage(ctx context.Context) (Usage, error) { func (m *Manager) Usage(ctx context.Context) (Usage, error) {
u := Usage{RateLimits: map[string]map[string]any{}} u := Usage{
Running: []api.TurnRef{},
Queued: []api.TurnRef{},
Caps: api.Caps{Global: m.limits.Global, PerVendor: map[string]int{}},
RateLimits: map[string]map[string]any{},
}
for vendor, n := range m.limits.PerVendor {
u.Caps.PerVendor[string(vendor)] = n
}
counts, err := m.store.CountRunning(ctx) running, err := m.store.SessionsInState(ctx, api.AgentSessionStateRunning)
if err != nil { if err != nil {
return u, err return u, err
} }
for _, n := range counts { for _, sess := range running {
u.Running += n u.Running = append(u.Running, api.TurnRef{SessionId: sess.Id, TaskId: sess.TaskId, Vendor: string(sess.Vendor)})
} }
queued, err := m.store.QueuedTurns(ctx) queued, err := m.store.QueuedTurns(ctx)
if err != nil { if err != nil {
return u, err return u, err
} }
u.Queued = len(queued) for _, q := range queued {
u.Queued = append(u.Queued, api.TurnRef{SessionId: q.SessionID, TaskId: q.TaskID, Vendor: string(q.Vendor)})
}
m.mu.Lock() m.mu.Lock()
defer m.mu.Unlock() defer m.mu.Unlock()
@@ -325,11 +336,6 @@ func (m *Manager) turn(ctx context.Context, sess api.AgentSession, task api.Task
return m.start(ctx, sess, task, actor, text, images, api.AgentSessionStateIdle) return m.start(ctx, sess, task, actor, text, images, api.AgentSessionStateIdle)
} }
err = transition(ctx, m.store, sess.Id, api.AgentSessionStateIdle, api.AgentSessionStateQueued, actor)
if err != nil {
return api.AgentSession{}, err
}
err = m.store.EnqueueTurn(ctx, store.QueuedTurn{ err = m.store.EnqueueTurn(ctx, store.QueuedTurn{
SessionID: sess.Id, SessionID: sess.Id,
Vendor: sess.Vendor, Vendor: sess.Vendor,
@@ -342,6 +348,15 @@ func (m *Manager) turn(ctx context.Context, sess api.AgentSession, task api.Task
return api.AgentSession{}, err return api.AgentSession{}, err
} }
err = transition(ctx, m.store, sess.Id, api.AgentSessionStateIdle, api.AgentSessionStateQueued, actor)
if err != nil {
derr := m.store.DeleteQueuedTurn(ctx, sess.Id)
if derr != nil {
slog.Error("remove queued turn", "session", sess.Id, "err", derr)
}
return api.AgentSession{}, err
}
return m.store.GetSession(ctx, sess.Id) return m.store.GetSession(ctx, sess.Id)
} }
+1 -1
View File
@@ -753,7 +753,7 @@ func TestConcurrencyQueue(t *testing.T) {
} }
usage, err := m.Usage(e.ctx) usage, err := m.Usage(e.ctx)
if err != nil || usage.Running != 1 || usage.Queued != 1 { if err != nil || len(usage.Running) != 1 || len(usage.Queued) != 1 || usage.Queued[0].TaskId != second.TaskId {
t.Fatalf("usage %+v %v", usage, err) t.Fatalf("usage %+v %v", usage, err)
} }
+103 -80
View File
@@ -312,6 +312,15 @@ type Blob struct {
Id string `json:"id"` Id string `json:"id"`
} }
// Caps defines model for Caps.
type Caps struct {
// Global Most turns running at once across all vendors.
Global int `json:"global"`
// PerVendor Most turns running at once per vendor, keyed by vendor.
PerVendor map[string]int `json:"per_vendor"`
}
// Error defines model for Error. // Error defines model for Error.
type Error struct { type Error struct {
Message string `json:"message"` Message string `json:"message"`
@@ -458,19 +467,24 @@ type SessionCreate struct {
// Status defines model for Status. // Status defines model for Status.
type Status struct { type Status struct {
ActiveTasks int `json:"active_tasks"` ActiveTasks int `json:"active_tasks"`
Caps Caps `json:"caps"`
DbBytes int64 `json:"db_bytes"` DbBytes int64 `json:"db_bytes"`
LastEventSeq int64 `json:"last_event_seq"` LastEventSeq int64 `json:"last_event_seq"`
// LiveAgents Agent processes currently running. // LiveAgents Agent processes currently running.
LiveAgents int `json:"live_agents"` LiveAgents int `json:"live_agents"`
Projects int `json:"projects"` Projects int `json:"projects"`
QueuedTurns int `json:"queued_turns"`
// QueuedTurns Turns waiting for room under the caps, in the order they will start.
QueuedTurns []TurnRef `json:"queued_turns"`
// RateLimits The latest rate-limit status each vendor reported, keyed by vendor. // RateLimits The latest rate-limit status each vendor reported, keyed by vendor.
RateLimits map[string]map[string]interface{} `json:"rate_limits"` RateLimits map[string]map[string]interface{} `json:"rate_limits"`
RunningTurns int `json:"running_turns"`
SseClients int `json:"sse_clients"` // RunningTurns Turns running now, oldest session first.
StartedAt time.Time `json:"started_at"` RunningTurns []TurnRef `json:"running_turns"`
SseClients int `json:"sse_clients"`
StartedAt time.Time `json:"started_at"`
} }
// Task defines model for Task. // Task defines model for Task.
@@ -510,6 +524,13 @@ type TaskCreate struct {
Title string `json:"title"` Title string `json:"title"`
} }
// TurnRef defines model for TurnRef.
type TurnRef struct {
SessionId string `json:"session_id"`
TaskId string `json:"task_id"`
Vendor string `json:"vendor"`
}
// Unmerged defines model for Unmerged. // Unmerged defines model for Unmerged.
type Unmerged struct { type Unmerged struct {
// Commits Commits on the task branch that aren't on the default branch. // Commits Commits on the task branch that aren't on the default branch.
@@ -1662,79 +1683,81 @@ func HandlerWithOptions(si ServerInterface, options StdHTTPServerOptions) http.H
// const string: with thousands of chunks the chained `+` fold is several // const string: with thousands of chunks the chained `+` fold is several
// times slower for the Go compiler than parsing a slice literal. // times slower for the Go compiler than parsing a slice literal.
var swaggerSpec = []string{ var swaggerSpec = []string{
"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",
"//8=", "nH4ynIJQE9JVIR218R9YKOj8n4SnAPaV/H9YPwWsQ7Zzb+H3Ojz0HOZ2yPvjasADyQhL2vGSvh3jeyjP",
"K4cLmcpHobQasoHC9r7AMKzlxHP+LjPbT/g/fRJ4+CmKZy4OHyPAvZLZP4esNr3jk9Gg4TdyjSJQKVSZ",
"+9OLCI1Jc5ErMPLMRjMp7eHNU7adhTmmWs/8/XC2cfZPmwO4v/+/AAAA//8=",
} }
// decodeSpec returns the embedded OpenAPI spec as raw JSON bytes, // decodeSpec returns the embedded OpenAPI spec as raw JSON bytes,
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
View File
@@ -391,6 +391,7 @@ func (s *Server) GetStatus(w http.ResponseWriter, r *http.Request) {
LiveAgents: s.agents.LiveCount(), LiveAgents: s.agents.LiveCount(),
RunningTurns: usage.Running, RunningTurns: usage.Running,
QueuedTurns: usage.Queued, QueuedTurns: usage.Queued,
Caps: usage.Caps,
RateLimits: usage.RateLimits, RateLimits: usage.RateLimits,
}) })
} }
+3 -2
View File
@@ -12,6 +12,7 @@ import (
type QueuedTurn struct { type QueuedTurn struct {
SessionID string SessionID string
TaskID string
Vendor api.AgentSessionVendor Vendor api.AgentSessionVendor
Actor string Actor string
Text string Text string
@@ -59,7 +60,7 @@ func (s *Store) EnqueueTurn(ctx context.Context, q QueuedTurn) error {
func (s *Store) QueuedTurns(ctx context.Context) ([]QueuedTurn, error) { func (s *Store) QueuedTurns(ctx context.Context) ([]QueuedTurn, error) {
rows, err := s.db.QueryContext(ctx, ` rows, err := s.db.QueryContext(ctx, `
SELECT q.session_id, s.vendor, q.actor, q.text, q.images, q.created_at SELECT q.session_id, s.task_id, s.vendor, q.actor, q.text, q.images, q.created_at
FROM turn_queue q JOIN agent_sessions s ON s.id = q.session_id FROM turn_queue q JOIN agent_sessions s ON s.id = q.session_id
ORDER BY q.created_at ORDER BY q.created_at
`) `)
@@ -74,7 +75,7 @@ func (s *Store) QueuedTurns(ctx context.Context) ([]QueuedTurn, error) {
vendor := "" vendor := ""
images := "" images := ""
createdAt := "" createdAt := ""
err = rows.Scan(&q.SessionID, &vendor, &q.Actor, &q.Text, &images, &createdAt) err = rows.Scan(&q.SessionID, &q.TaskID, &vendor, &q.Actor, &q.Text, &images, &createdAt)
if err != nil { if err != nil {
return nil, err return nil, err
} }
+144
View File
@@ -0,0 +1,144 @@
import { get } from "./api.js";
import { ago, errorText, h, replace } from "./dom.js";
import { findTask, on, onEach, state } from "./state.js";
const refreshKinds = new Set(["session.created", "session.state", "task.created", "task.merged", "task.discarded", "project.created"]);
export function mountAdmin(root) {
let status;
let error = "";
let inFlight = false;
let again = false;
let disposed = false;
const render = () => {
if (!status) {
replace(root, h("div", { class: error ? "empty error-text" : "empty" }, error || "Loading…"));
return;
}
replace(root, h("div", { class: "admin-view" }, error && h("p", { class: "error-text" }, error), turnsSection(status), rateLimitsSection(status.rate_limits), serverSection(status)));
};
const refresh = async () => {
if (inFlight) {
again = true;
return;
}
inFlight = true;
try {
const next = await get("/api/admin/status");
if (disposed) {
return;
}
status = next;
error = "";
}
catch (err) {
error = `Couldn't load status: ${errorText(err)}`;
}
inFlight = false;
render();
if (again && !disposed) {
again = false;
void refresh();
}
};
const onEvent = (detail) => {
const ev = detail;
if (refreshKinds.has(ev.kind) || (ev.kind === "agent.vendor" && ev.payload["type"] === "rate_limit_event")) {
void refresh();
return;
}
if (status && ev.seq > status.last_event_seq) {
status.last_event_seq = ev.seq;
render();
}
};
const offs = [
on("event", onEvent),
on("connection", () => {
if (state.connected) {
void refresh();
}
}),
onEach(["projects", "tasks"], render),
];
render();
void refresh();
return () => {
disposed = true;
for (const off of offs) {
off();
}
};
}
function turnsSection(s) {
const vendors = [...new Set([...Object.keys(s.caps.per_vendor), ...s.running_turns.map((t) => t.vendor), ...s.queued_turns.map((t) => t.vendor)])].sort();
const runningFor = (vendor) => s.running_turns.filter((t) => t.vendor === vendor).length;
return h("section", { class: "admin-section" }, h("h2", {}, "Turns"), h("div", { class: "caps" }, capTile("All vendors", s.running_turns.length, s.caps.global), ...vendors.map((v) => capTile(v, runningFor(v), s.caps.per_vendor[v])), h("div", { class: "cap-tile" }, h("div", { class: "cap-label" }, "Queued"), h("div", { class: s.queued_turns.length > 0 ? "cap-value warn" : "cap-value" }, s.queued_turns.length))), h("h3", {}, "Running"), turnList(s.running_turns, "running", "No turns running."), h("h3", {}, "Queued"), turnList(s.queued_turns, "queued", "Nothing queued."));
}
function capTile(label, running, cap) {
const full = cap !== undefined && running >= cap;
return h("div", { class: "cap-tile" }, h("div", { class: "cap-label" }, label), h("div", { class: full ? "cap-value warn" : "cap-value" }, running, h("span", { class: "cap-max" }, cap === undefined ? " / no cap" : ` / ${cap}`)));
}
function turnList(turns, kind, empty) {
return h("ul", { class: "task-list" }, ...turns.map((t, i) => {
const task = findTask(t.task_id);
const project = task ? state.projects.find((p) => p.id === task.project_id) : undefined;
return h("li", {}, h("a", { href: `#/task/${t.task_id}` }, kind === "queued" ? h("span", { class: "queue-pos" }, `${i + 1}.`) : h("span", { class: "dot running", title: "running" }), h("span", { class: "task-title" }, project && h("span", { class: "project-name" }, `${project.name} ▸ `), task?.title ?? t.task_id), h("span", { class: "task-state" }, t.vendor)));
}), turns.length === 0 && h("li", { class: "empty-row" }, empty));
}
function rateLimitsSection(limits) {
const vendors = Object.keys(limits).sort();
return h("section", { class: "admin-section" }, h("h2", {}, "Rate limits"), ...vendors.map((v) => rateLimitCard(v, limits[v] ?? {})), vendors.length === 0 && h("p", { class: "hint" }, "No vendor has reported a rate-limit status since byre started."));
}
function rateLimitCard(vendor, info) {
const { status, rateLimitType, resetsAt, unifiedWindows, ...rest } = info;
const statusText = String(status ?? "unknown");
const tone = statusText === "allowed" ? "ok" : statusText === "rejected" ? "danger" : "warn";
const windows = (unifiedWindows ?? {});
return h("div", { class: "card rate-limit" }, h("header", {}, h("span", { class: "vendor" }, vendor), h("span", { class: `rl-status ${tone}` }, statusText), rateLimitType !== undefined && h("span", {}, String(rateLimitType)), typeof resetsAt === "number" && h("span", { class: "resets" }, "resets ", resetText(resetsAt))), Object.keys(windows).length > 0 &&
h("table", { class: "rl-windows" }, h("tr", {}, h("th", {}, "Window"), h("th", {}, "Used"), h("th", {}, "Resets")), ...Object.entries(windows).map(([name, w]) => h("tr", {}, h("td", {}, name), h("td", { class: utilizationClass(w.utilization) }, w.utilization === undefined ? "—" : `${Math.round(w.utilization * 100)}%`), h("td", {}, w.resetsAt === undefined ? "—" : resetText(w.resetsAt))))), Object.keys(rest).length > 0 &&
h("dl", { class: "kv" }, ...Object.entries(rest).flatMap(([k, v]) => [h("dt", {}, k), h("dd", {}, typeof v === "object" ? JSON.stringify(v) : String(v))])));
}
function utilizationClass(u) {
if (u === undefined) {
return "";
}
if (u >= 0.9) {
return "danger";
}
if (u >= 0.7) {
return "warn";
}
return "";
}
function resetText(unixSeconds) {
const at = new Date(unixSeconds * 1000);
const time = at.toLocaleString(undefined, { weekday: "short", hour: "numeric", minute: "2-digit" });
const minutes = Math.round((at.getTime() - Date.now()) / 60000);
if (minutes <= 0) {
return `${time} (passed)`;
}
const rel = minutes < 60 ? `${minutes}m` : minutes < 1440 ? `${Math.floor(minutes / 60)}h ${minutes % 60}m` : `${Math.floor(minutes / 1440)}d ${Math.floor((minutes % 1440) / 60)}h`;
return `${time} (in ${rel})`;
}
function serverSection(s) {
const rows = [
["Started", `${new Date(s.started_at).toLocaleString()} (${ago(s.started_at)})`],
["Agent processes", String(s.live_agents)],
["Projects", String(s.projects)],
["Active tasks", String(s.active_tasks)],
["Live clients", String(s.sse_clients)],
["Database", bytes(s.db_bytes)],
["Last event", `#${s.last_event_seq}`],
];
return h("section", { class: "admin-section" }, h("h2", {}, "Server"), h("dl", { class: "kv" }, ...rows.flatMap(([k, v]) => [h("dt", {}, k), h("dd", {}, v)])));
}
function bytes(n) {
const units = ["B", "KB", "MB", "GB", "TB"];
let i = 0;
let v = n;
while (v >= 1024 && i < units.length - 1) {
v /= 1024;
i++;
}
return `${v.toFixed(i === 0 ? 0 : 1)} ${units[i]}`;
}
//# sourceMappingURL=admin.js.map
+1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"} {"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}
+1 -1
View File
@@ -43,7 +43,7 @@ async function main() {
const left = new Pane("left", "transcript"); const left = new Pane("left", "transcript");
const right = new Pane("right", "inbox"); const right = new Pane("right", "inbox");
replace(document.body, navbar, banner, h("div", { class: "panes" }, left.el, right.el)); replace(document.body, navbar, banner, h("div", { class: "panes" }, left.el, right.el));
mountNavbar(navbar, () => right.show("inbox")); mountNavbar(navbar, (view) => right.show(view));
identityBanner(banner); identityBanner(banner);
const status = await get("/api/admin/status"); const status = await get("/api/admin/status");
await Promise.all([loadMe(), loadProjects(), loadInbox()]); await Promise.all([loadMe(), loadProjects(), loadInbox()]);
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"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"} {"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"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"}
+5 -3
View File
@@ -1,10 +1,12 @@
import { h, replace } from "./dom.js"; import { h, replace } from "./dom.js";
import { activeTasks, findTask, onEach, state } from "./state.js"; import { activeTasks, findTask, onEach, state } from "./state.js";
export function mountNavbar(root, showInbox) { export function mountNavbar(root, show) {
const crumb = h("button", { class: "crumb", title: "Switch task (⌘K)" }); const crumb = h("button", { class: "crumb", title: "Switch task (⌘K)" });
crumb.onclick = openSwitcher; crumb.onclick = openSwitcher;
const inbox = h("button", { class: "inbox-count" }); const inbox = h("button", { class: "inbox-count" });
inbox.onclick = showInbox; inbox.onclick = () => show("inbox");
const admin = h("button", { class: "nav-link" }, "Admin");
admin.onclick = () => show("admin");
const conn = h("span", { class: "connection down", title: "connecting" }); const conn = h("span", { class: "connection down", title: "connecting" });
const me = h("span", { class: "me" }); const me = h("span", { class: "me" });
const render = () => { const render = () => {
@@ -19,7 +21,7 @@ export function mountNavbar(root, showInbox) {
replace(me, state.me?.display_name ?? ""); replace(me, state.me?.display_name ?? "");
}; };
onEach(["me", "projects", "tasks", "inbox", "selection", "connection"], render); onEach(["me", "projects", "tasks", "inbox", "selection", "connection"], render);
replace(root, h("a", { href: "#/", class: "brand" }, "byre"), crumb, h("span", { class: "spacer" }), inbox, conn, me); replace(root, h("a", { href: "#/", class: "brand" }, "byre"), crumb, h("span", { class: "spacer" }), inbox, admin, conn, me);
render(); render();
addEventListener("keydown", (e) => { addEventListener("keydown", (e) => {
if (e.key === "k" && (e.metaKey || e.ctrlKey)) { if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
+1 -1
View File
File diff suppressed because one or more lines are too long
+3 -1
View File
@@ -1,3 +1,4 @@
import { mountAdmin } from "./admin.js";
import { mountChanges } from "./changes.js"; import { mountChanges } from "./changes.js";
import { h, replace } from "./dom.js"; import { h, replace } from "./dom.js";
import { mountInbox } from "./inbox.js"; import { mountInbox } from "./inbox.js";
@@ -8,8 +9,9 @@ const views = {
changes: { label: "Changes", mount: (root) => mountChanges(root) }, changes: { label: "Changes", mount: (root) => mountChanges(root) },
tasks: { label: "Tasks", mount: mountTasks }, tasks: { label: "Tasks", mount: mountTasks },
inbox: { label: "Inbox", mount: mountInbox }, inbox: { label: "Inbox", mount: mountInbox },
admin: { label: "Admin", mount: mountAdmin },
}; };
const order = ["transcript", "changes", "tasks", "inbox"]; const order = ["transcript", "changes", "tasks", "inbox", "admin"];
export class Pane { export class Pane {
key; key;
el; el;
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"panes.js","sourceRoot":"","sources":["../src/panes.ts"],"names":[],"mappings":"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"} {"version":3,"file":"panes.js","sourceRoot":"","sources":["../src/panes.ts"],"names":[],"mappings":"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"}
+1
View File
@@ -148,5 +148,6 @@ export function handleEvent(ev) {
void loadInbox(); void loadInbox();
break; break;
} }
notify("event", ev);
} }
//# sourceMappingURL=state.js.map //# sourceMappingURL=state.js.map
+1 -1
View File
File diff suppressed because one or more lines are too long
+242
View File
@@ -0,0 +1,242 @@
import { get, type ByreEvent, type Status, type TurnRef } from "./api.js";
import { ago, errorText, h, replace } from "./dom.js";
import { findTask, on, onEach, state } from "./state.js";
const refreshKinds = new Set(["session.created", "session.state", "task.created", "task.merged", "task.discarded", "project.created"]);
export function mountAdmin(root: HTMLElement): () => void {
let status: Status | undefined;
let error = "";
let inFlight = false;
let again = false;
let disposed = false;
const render = () => {
if (!status) {
replace(root, h("div", { class: error ? "empty error-text" : "empty" }, error || "Loading…"));
return;
}
replace(
root,
h(
"div",
{ class: "admin-view" },
error && h("p", { class: "error-text" }, error),
turnsSection(status),
rateLimitsSection(status.rate_limits),
serverSection(status),
),
);
};
const refresh = async () => {
if (inFlight) {
again = true;
return;
}
inFlight = true;
try {
const next = await get("/api/admin/status");
if (disposed) {
return;
}
status = next;
error = "";
} catch (err) {
error = `Couldn't load status: ${errorText(err)}`;
}
inFlight = false;
render();
if (again && !disposed) {
again = false;
void refresh();
}
};
const onEvent = (detail?: unknown) => {
const ev = detail as ByreEvent;
if (refreshKinds.has(ev.kind) || (ev.kind === "agent.vendor" && ev.payload["type"] === "rate_limit_event")) {
void refresh();
return;
}
if (status && ev.seq > status.last_event_seq) {
status.last_event_seq = ev.seq;
render();
}
};
const offs = [
on("event", onEvent),
on("connection", () => {
if (state.connected) {
void refresh();
}
}),
onEach(["projects", "tasks"], render),
];
render();
void refresh();
return () => {
disposed = true;
for (const off of offs) {
off();
}
};
}
function turnsSection(s: Status): HTMLElement {
const vendors = [...new Set([...Object.keys(s.caps.per_vendor), ...s.running_turns.map((t) => t.vendor), ...s.queued_turns.map((t) => t.vendor)])].sort();
const runningFor = (vendor: string) => s.running_turns.filter((t) => t.vendor === vendor).length;
return h(
"section",
{ class: "admin-section" },
h("h2", {}, "Turns"),
h(
"div",
{ class: "caps" },
capTile("All vendors", s.running_turns.length, s.caps.global),
...vendors.map((v) => capTile(v, runningFor(v), s.caps.per_vendor[v])),
h("div", { class: "cap-tile" }, h("div", { class: "cap-label" }, "Queued"), h("div", { class: s.queued_turns.length > 0 ? "cap-value warn" : "cap-value" }, s.queued_turns.length)),
),
h("h3", {}, "Running"),
turnList(s.running_turns, "running", "No turns running."),
h("h3", {}, "Queued"),
turnList(s.queued_turns, "queued", "Nothing queued."),
);
}
function capTile(label: string, running: number, cap: number | undefined): HTMLElement {
const full = cap !== undefined && running >= cap;
return h(
"div",
{ class: "cap-tile" },
h("div", { class: "cap-label" }, label),
h("div", { class: full ? "cap-value warn" : "cap-value" }, running, h("span", { class: "cap-max" }, cap === undefined ? " / no cap" : ` / ${cap}`)),
);
}
function turnList(turns: TurnRef[], kind: "running" | "queued", empty: string): HTMLElement {
return h(
"ul",
{ class: "task-list" },
...turns.map((t, i) => {
const task = findTask(t.task_id);
const project = task ? state.projects.find((p) => p.id === task.project_id) : undefined;
return h(
"li",
{},
h(
"a",
{ href: `#/task/${t.task_id}` },
kind === "queued" ? h("span", { class: "queue-pos" }, `${i + 1}.`) : h("span", { class: "dot running", title: "running" }),
h("span", { class: "task-title" }, project && h("span", { class: "project-name" }, `${project.name} ▸ `), task?.title ?? t.task_id),
h("span", { class: "task-state" }, t.vendor),
),
);
}),
turns.length === 0 && h("li", { class: "empty-row" }, empty),
);
}
function rateLimitsSection(limits: Status["rate_limits"]): HTMLElement {
const vendors = Object.keys(limits).sort();
return h(
"section",
{ class: "admin-section" },
h("h2", {}, "Rate limits"),
...vendors.map((v) => rateLimitCard(v, limits[v] ?? {})),
vendors.length === 0 && h("p", { class: "hint" }, "No vendor has reported a rate-limit status since byre started."),
);
}
function rateLimitCard(vendor: string, info: Record<string, unknown>): HTMLElement {
const { status, rateLimitType, resetsAt, unifiedWindows, ...rest } = info;
const statusText = String(status ?? "unknown");
const tone = statusText === "allowed" ? "ok" : statusText === "rejected" ? "danger" : "warn";
const windows = (unifiedWindows ?? {}) as Record<string, { utilization?: number; resetsAt?: number }>;
return h(
"div",
{ class: "card rate-limit" },
h(
"header",
{},
h("span", { class: "vendor" }, vendor),
h("span", { class: `rl-status ${tone}` }, statusText),
rateLimitType !== undefined && h("span", {}, String(rateLimitType)),
typeof resetsAt === "number" && h("span", { class: "resets" }, "resets ", resetText(resetsAt)),
),
Object.keys(windows).length > 0 &&
h(
"table",
{ class: "rl-windows" },
h("tr", {}, h("th", {}, "Window"), h("th", {}, "Used"), h("th", {}, "Resets")),
...Object.entries(windows).map(([name, w]) =>
h(
"tr",
{},
h("td", {}, name),
h("td", { class: utilizationClass(w.utilization) }, w.utilization === undefined ? "—" : `${Math.round(w.utilization * 100)}%`),
h("td", {}, w.resetsAt === undefined ? "—" : resetText(w.resetsAt)),
),
),
),
Object.keys(rest).length > 0 &&
h(
"dl",
{ class: "kv" },
...Object.entries(rest).flatMap(([k, v]) => [h("dt", {}, k), h("dd", {}, typeof v === "object" ? JSON.stringify(v) : String(v))]),
),
);
}
function utilizationClass(u: number | undefined): string {
if (u === undefined) {
return "";
}
if (u >= 0.9) {
return "danger";
}
if (u >= 0.7) {
return "warn";
}
return "";
}
function resetText(unixSeconds: number): string {
const at = new Date(unixSeconds * 1000);
const time = at.toLocaleString(undefined, { weekday: "short", hour: "numeric", minute: "2-digit" });
const minutes = Math.round((at.getTime() - Date.now()) / 60000);
if (minutes <= 0) {
return `${time} (passed)`;
}
const rel = minutes < 60 ? `${minutes}m` : minutes < 1440 ? `${Math.floor(minutes / 60)}h ${minutes % 60}m` : `${Math.floor(minutes / 1440)}d ${Math.floor((minutes % 1440) / 60)}h`;
return `${time} (in ${rel})`;
}
function serverSection(s: Status): HTMLElement {
const rows: [string, string][] = [
["Started", `${new Date(s.started_at).toLocaleString()} (${ago(s.started_at)})`],
["Agent processes", String(s.live_agents)],
["Projects", String(s.projects)],
["Active tasks", String(s.active_tasks)],
["Live clients", String(s.sse_clients)],
["Database", bytes(s.db_bytes)],
["Last event", `#${s.last_event_seq}`],
];
return h("section", { class: "admin-section" }, h("h2", {}, "Server"), h("dl", { class: "kv" }, ...rows.flatMap(([k, v]) => [h("dt", {}, k), h("dd", {}, v)])));
}
function bytes(n: number): string {
const units = ["B", "KB", "MB", "GB", "TB"];
let i = 0;
let v = n;
while (v >= 1024 && i < units.length - 1) {
v /= 1024;
i++;
}
return `${v.toFixed(i === 0 ? 0 : 1)} ${units[i]}`;
}
+18 -2
View File
@@ -655,8 +655,11 @@ export interface components {
Status: { Status: {
/** @description Agent processes currently running. */ /** @description Agent processes currently running. */
live_agents: number; live_agents: number;
running_turns: number; /** @description Turns running now, oldest session first. */
queued_turns: number; running_turns: components["schemas"]["TurnRef"][];
/** @description Turns waiting for room under the caps, in the order they will start. */
queued_turns: components["schemas"]["TurnRef"][];
caps: components["schemas"]["Caps"];
/** @description The latest rate-limit status each vendor reported, keyed by vendor. */ /** @description The latest rate-limit status each vendor reported, keyed by vendor. */
rate_limits: { rate_limits: {
[key: string]: Record<string, never>; [key: string]: Record<string, never>;
@@ -671,6 +674,19 @@ export interface components {
active_tasks: number; active_tasks: number;
sse_clients: number; sse_clients: number;
}; };
Caps: {
/** @description Most turns running at once across all vendors. */
global: number;
/** @description Most turns running at once per vendor, keyed by vendor. */
per_vendor: {
[key: string]: number;
};
};
TurnRef: {
session_id: string;
task_id: string;
vendor: string;
};
}; };
responses: { responses: {
/** @description Error. */ /** @description Error. */
+1
View File
@@ -12,6 +12,7 @@ export type InboxResponseCreate = Schemas["InboxResponseCreate"];
export type Attachment = Schemas["Attachment"]; export type Attachment = Schemas["Attachment"];
export type ByreEvent = Schemas["Event"]; export type ByreEvent = Schemas["Event"];
export type Status = Schemas["Status"]; export type Status = Schemas["Status"];
export type TurnRef = Schemas["TurnRef"];
type Method = "get" | "post" | "put"; type Method = "get" | "post" | "put";
+1 -1
View File
@@ -46,7 +46,7 @@ async function main(): Promise<void> {
const right = new Pane("right", "inbox"); const right = new Pane("right", "inbox");
replace(document.body, navbar, banner, h("div", { class: "panes" }, left.el, right.el)); replace(document.body, navbar, banner, h("div", { class: "panes" }, left.el, right.el));
mountNavbar(navbar, () => right.show("inbox")); mountNavbar(navbar, (view) => right.show(view));
identityBanner(banner); identityBanner(banner);
const status = await get("/api/admin/status"); const status = await get("/api/admin/status");
+7 -3
View File
@@ -1,12 +1,16 @@
import { h, replace } from "./dom.js"; import { h, replace } from "./dom.js";
import type { View } from "./panes.js";
import { activeTasks, findTask, onEach, state } from "./state.js"; import { activeTasks, findTask, onEach, state } from "./state.js";
export function mountNavbar(root: HTMLElement, showInbox: () => void): void { export function mountNavbar(root: HTMLElement, show: (view: View) => void): void {
const crumb = h("button", { class: "crumb", title: "Switch task (⌘K)" }); const crumb = h("button", { class: "crumb", title: "Switch task (⌘K)" });
crumb.onclick = openSwitcher; crumb.onclick = openSwitcher;
const inbox = h("button", { class: "inbox-count" }); const inbox = h("button", { class: "inbox-count" });
inbox.onclick = showInbox; inbox.onclick = () => show("inbox");
const admin = h("button", { class: "nav-link" }, "Admin");
admin.onclick = () => show("admin");
const conn = h("span", { class: "connection down", title: "connecting" }); const conn = h("span", { class: "connection down", title: "connecting" });
const me = h("span", { class: "me" }); const me = h("span", { class: "me" });
@@ -26,7 +30,7 @@ export function mountNavbar(root: HTMLElement, showInbox: () => void): void {
}; };
onEach(["me", "projects", "tasks", "inbox", "selection", "connection"], render); onEach(["me", "projects", "tasks", "inbox", "selection", "connection"], render);
replace(root, h("a", { href: "#/", class: "brand" }, "byre"), crumb, h("span", { class: "spacer" }), inbox, conn, me); replace(root, h("a", { href: "#/", class: "brand" }, "byre"), crumb, h("span", { class: "spacer" }), inbox, admin, conn, me);
render(); render();
addEventListener("keydown", (e) => { addEventListener("keydown", (e) => {
+4 -2
View File
@@ -1,19 +1,21 @@
import { mountAdmin } from "./admin.js";
import { mountChanges } from "./changes.js"; import { mountChanges } from "./changes.js";
import { h, replace } from "./dom.js"; import { h, replace } from "./dom.js";
import { mountInbox } from "./inbox.js"; import { mountInbox } from "./inbox.js";
import { mountTranscript } from "./task.js"; import { mountTranscript } from "./task.js";
import { mountTasks } from "./tasks.js"; import { mountTasks } from "./tasks.js";
export type View = "transcript" | "changes" | "tasks" | "inbox"; export type View = "transcript" | "changes" | "tasks" | "inbox" | "admin";
const views: Record<View, { label: string; mount: (root: HTMLElement) => () => void }> = { const views: Record<View, { label: string; mount: (root: HTMLElement) => () => void }> = {
transcript: { label: "Transcript", mount: mountTranscript }, transcript: { label: "Transcript", mount: mountTranscript },
changes: { label: "Changes", mount: (root) => mountChanges(root) }, changes: { label: "Changes", mount: (root) => mountChanges(root) },
tasks: { label: "Tasks", mount: mountTasks }, tasks: { label: "Tasks", mount: mountTasks },
inbox: { label: "Inbox", mount: mountInbox }, inbox: { label: "Inbox", mount: mountInbox },
admin: { label: "Admin", mount: mountAdmin },
}; };
const order: View[] = ["transcript", "changes", "tasks", "inbox"]; const order: View[] = ["transcript", "changes", "tasks", "inbox", "admin"];
export class Pane { export class Pane {
readonly el: HTMLElement; readonly el: HTMLElement;
+3 -1
View File
@@ -1,6 +1,6 @@
import { get, type AgentSession, type ByreEvent, type InboxItem, type Project, type Task, type User } from "./api.js"; import { get, type AgentSession, type ByreEvent, type InboxItem, type Project, type Task, type User } from "./api.js";
export type Topic = "me" | "projects" | "tasks" | "sessions" | "inbox" | "selection" | "transcript" | "stream" | "connection"; export type Topic = "me" | "projects" | "tasks" | "sessions" | "inbox" | "selection" | "transcript" | "stream" | "connection" | "event";
export const state = { export const state = {
me: undefined as User | undefined, me: undefined as User | undefined,
@@ -170,4 +170,6 @@ export function handleEvent(ev: ByreEvent): void {
void loadInbox(); void loadInbox();
break; break;
} }
notify("event", ev);
} }
+130
View File
@@ -1247,3 +1247,133 @@ pre.diff .meta {
padding: 24px; padding: 24px;
color: var(--danger); color: var(--danger);
} }
.nav-link {
background: transparent;
}
.admin-view {
flex: 1;
min-height: 0;
overflow-y: auto;
padding: 8px 16px 16px;
}
.admin-section h2 {
font-size: 13px;
margin: 16px 0 8px;
}
.admin-section h3 {
font-size: 12px;
color: var(--muted);
margin: 12px 0 4px;
}
.caps {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.cap-tile {
border: 1px solid var(--border);
border-radius: 8px;
background: var(--surface);
padding: 6px 12px;
min-width: 110px;
}
.cap-label {
font-size: 11px;
color: var(--muted);
}
.cap-value {
font-size: 18px;
font-weight: 700;
}
.cap-value.warn {
color: var(--warn);
}
.cap-max {
font-size: 12px;
font-weight: 400;
color: var(--muted);
}
.queue-pos {
width: 3ch;
color: var(--muted);
flex-shrink: 0;
}
.rate-limit + .rate-limit {
margin-top: 8px;
}
.rate-limit .vendor {
color: var(--text);
font-weight: 700;
font-size: 13px;
}
.rate-limit .resets {
margin-left: auto;
}
.rl-status {
font-weight: 700;
}
.rl-status.ok,
.rl-windows .ok {
color: var(--ok);
}
.rl-status.warn,
.rl-windows .warn {
color: var(--warn);
}
.rl-status.danger,
.rl-windows .danger {
color: var(--danger);
}
.rl-windows {
border-collapse: collapse;
margin-top: 8px;
width: 100%;
}
.rl-windows th,
.rl-windows td {
text-align: left;
padding: 2px 12px 2px 0;
border-bottom: 1px solid var(--border);
}
.rl-windows th {
font-weight: 400;
font-size: 11px;
color: var(--muted);
}
.kv {
display: grid;
grid-template-columns: max-content 1fr;
gap: 2px 16px;
margin: 8px 0 0;
}
.kv dt {
color: var(--muted);
}
.kv dd {
margin: 0;
word-break: break-word;
}