diff --git a/src/web/plan-usage-latest.ts b/src/web/plan-usage-latest.ts new file mode 100644 index 00000000..eb497aaf --- /dev/null +++ b/src/web/plan-usage-latest.ts @@ -0,0 +1,22 @@ +/** + * @fileoverview Process-wide last-known plan-usage telemetry (account-global). + * + * The status-telemetry route writes the latest broadcast value here; the SSE + * init snapshot (`getLightState`) replays it so the header "Plan Usage Limits" + * chip shows immediately on a fresh page load / SSE reconnect — before any new + * statusline render arrives, and without relying on per-browser localStorage. + * + * Null until the first telemetry of the process; cleared naturally on restart. + * + * @module plan-usage-latest + */ + +let latest: Record | null = null; + +export function setLatestPlanUsage(value: Record): void { + latest = value; +} + +export function getLatestPlanUsage(): Record | null { + return latest; +} diff --git a/src/web/public/app.js b/src/web/public/app.js index af93e7ee..904111a7 100644 --- a/src/web/public/app.js +++ b/src/web/public/app.js @@ -2258,6 +2258,10 @@ class CodemanApp { this._serverCjkOverride = data.inputCjkForm || false; this._updateCjkInputState(); + // Plan-usage chip: server's last-known telemetry, so it shows immediately on + // a fresh load / reconnect (authoritative; wins over the localStorage restore). + if (data.planUsage) this.updatePlanUsageChip(data.planUsage); + // Update version displays (header and toolbar) if (data.version) { const versionEl = this.$('versionDisplay'); diff --git a/src/web/routes/status-telemetry-routes.ts b/src/web/routes/status-telemetry-routes.ts index 46673ebd..f8c90efc 100644 --- a/src/web/routes/status-telemetry-routes.ts +++ b/src/web/routes/status-telemetry-routes.ts @@ -23,6 +23,7 @@ import { type RawStatuslinePayload, } from '../../usage-telemetry.js'; import { SessionStatusTelemetry } from '../sse-events.js'; +import { setLatestPlanUsage } from '../plan-usage-latest.js'; import type { SessionPort, EventPort } from '../ports/index.js'; export function registerStatusTelemetryRoutes(app: FastifyInstance, ctx: SessionPort & EventPort): void { @@ -56,7 +57,9 @@ export function registerStatusTelemetryRoutes(app: FastifyInstance, ctx: Session if (!ctx.sessions.has(id)) lastSig.delete(id); } } - ctx.broadcast(SessionStatusTelemetry, { sessionId, ...telemetry }); + const payload = { sessionId, ...telemetry }; + setLatestPlanUsage(payload); // replayed in the SSE init snapshot for fresh loads + ctx.broadcast(SessionStatusTelemetry, payload); } } diff --git a/src/web/server.ts b/src/web/server.ts index a7ce1959..d20ad147 100644 --- a/src/web/server.ts +++ b/src/web/server.ts @@ -124,6 +124,7 @@ import { import type { EventLoopMonitorHandle } from '../utils/index.js'; import { MAX_CONCURRENT_SESSIONS, MAX_SSE_CLIENTS } from '../config/map-limits.js'; import { SseEvent } from './sse-events.js'; +import { getLatestPlanUsage } from './plan-usage-latest.js'; import type { ScheduledRun } from './ports/index.js'; import { registerAuthMiddleware, registerSecurityHeaders, registerHostGuard } from './middleware/auth.js'; import { installRouteErrorHandler } from './route-error-handler.js'; @@ -1627,6 +1628,7 @@ export class WebServer extends EventEmitter { subagents: subagentWatcher.getRecentSubagents(15), // 15 min to avoid stale agents timestamp: now, inputCjkForm: process.env.INPUT_CJK_FORM?.toUpperCase() === 'ON', + planUsage: getLatestPlanUsage(), // last-known plan-usage telemetry, for the header chip on fresh load }; this.cachedLightState = { data: result, timestamp: now };