perf: rest animations, cheaper chat typing and server polls, async per-tool hooks
UI
- No animation runs forever: Sagi moods, the custom pet sprite, status dots
(pulse-dot, pulse-slow, ping) and the chat shimmer play a few cycles and
rest. An infinite animation kept the browser drawing 60 frames a second.
- useSagiBrain: frames that only move lastActivityAt by <1 s keep the old
state; the 1 s tick re-renders only when the mood changes.
- ConversationView and MessageList are memoized and PaneChat passes stable
callbacks: typing in the chat box no longer re-renders the conversation.
- The chat's 700 ms menu poll reads plain text first and scans the terminal
cell by cell (reusing one cell) only when a row could be a menu cursor row.
Server
- GET /sessions/:id/transcript?after=N skips lines <= N before JSON.parse.
- GET /sessions/:id/outline is cached until the session's events change.
- broadcastLane uses the cached tmux.liveSessions(); tmux.hasSession answers
"yes" from the fresh shared list instead of a synchronous spawn (50-150 ms
per psmux call on Windows, blocking terminal echo).
- pane-mode polls every 150 ms; writeHourly writes only changed hour buckets.
- /api/metrics: agenthub_event_loop_delay_seconds{quantile}.
Hooks
- PreToolUse/PostToolUse run with "async": true (settings installer,
plugin.json, S3 managed settings): Claude Code no longer waits a Node start
before and after every tool call.
Committed with --no-verify at the owner's request: the pre-commit full server
suite fails on 9 tests that fail identically on 0c2f498 (lane-sync merge: no
git identity on this host; read-pool; resolveHost; codex-usage 1 ms flake).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -29,6 +29,22 @@ const { calculateCost, dailyTokenRows } = require("./pricing");
|
||||
|
||||
const router = Router();
|
||||
|
||||
// How long the event loop was held up — what an operator sees as a laggy
|
||||
// terminal: every keystroke's echo waits for it. Sampled every 20 ms and reset
|
||||
// each minute, so the gauges describe the last minute or so.
|
||||
const LOOP_WINDOW_MS = 60_000;
|
||||
const loopDelay = require("perf_hooks").monitorEventLoopDelay({ resolution: 20 });
|
||||
loopDelay.enable();
|
||||
let loopWindow = null; // the last full minute, reported until the next one ends
|
||||
setInterval(() => {
|
||||
loopWindow = {
|
||||
p50: loopDelay.percentile(50),
|
||||
p99: loopDelay.percentile(99),
|
||||
max: loopDelay.max,
|
||||
};
|
||||
loopDelay.reset();
|
||||
}, LOOP_WINDOW_MS).unref();
|
||||
|
||||
// Resolved once at load — the dashboard version, surfaced as a build-info label
|
||||
// (the canonical Prometheus idiom for exposing a version string as a metric).
|
||||
const APP_VERSION = (() => {
|
||||
@@ -100,6 +116,23 @@ router.get("/", (_req, res) => {
|
||||
[{ value: process.memoryUsage().rss }]
|
||||
);
|
||||
|
||||
const loop = loopWindow || {
|
||||
p50: loopDelay.percentile(50),
|
||||
p99: loopDelay.percentile(99),
|
||||
max: loopDelay.max,
|
||||
};
|
||||
appendMetric(
|
||||
out,
|
||||
"agenthub_event_loop_delay_seconds",
|
||||
"Event-loop delay over the last minute (p50, p99, max): how long requests, hooks and terminal echo waited.",
|
||||
"gauge",
|
||||
[
|
||||
{ labels: { quantile: "0.5" }, value: loop.p50 / 1e9 },
|
||||
{ labels: { quantile: "0.99" }, value: loop.p99 / 1e9 },
|
||||
{ labels: { quantile: "1" }, value: loop.max / 1e9 },
|
||||
]
|
||||
);
|
||||
|
||||
// Sessions by status.
|
||||
const sessionCounts = new Map(stmts.sessionStatusCounts.all().map((r) => [r.status, r.count]));
|
||||
appendMetric(
|
||||
|
||||
Reference in New Issue
Block a user