feat(cockpit): quick wins — sync chip, cost budget guard, digest, recent commits (W3)

Four small, high-delight features that turn passive capture into glanceable signals.

- Sync chip on project cards: ahead/behind vs upstream + last-commit time, folded
  into the existing concurrent per-repo metadata pass (git rev-list --count
  --left-right @{u}...HEAD + git log -1 --format=%ct; no upstream → undefined, no route).
- Cost budget guard: COST_BUDGET_USD env (0 = off); a per-session one-shot latch
  (Session.budgetNotified, cost is monotonic so never re-armed) fires a single push
  on threshold crossing in manager.handleStatusLine; the already-broadcast telemetry
  frame carries the warn (tg-cost-warn styling derived from costUsd>=budget via
  /config/ui — no new ServerMessage). web-push title added to sw-push.js.
- "While you were away" digest: GET /digest?since= → {finished, needsInput, stuck,
  totalCostUsd, sessions[]} aggregate over manager.list(); FE banner on reconnect.
- Recent commits per project: src/http/git-log.ts (NUL-delimited git log → CommitInfo[]),
  GET /projects/log?path= (isValidGitDir), textContent-inert render in project detail.

All git via execFile (no shell) + validated cwd; new routes read-only; commit
messages rendered via textContent. Verified: typecheck + build:web clean, 1904 pass
at --test-timeout=30000 (two default-5s failures are slow-sandbox real-subprocess
timeout flakes — the known ring-buffer test + a new real-git-clone sync test — not
logic regressions).
This commit is contained in:
Yaojia Wang
2026-07-12 21:27:20 +02:00
parent 7551f8a4b2
commit 1dd12b035a
30 changed files with 1911 additions and 8 deletions

View File

@@ -141,6 +141,9 @@ export class TabApp {
// B4: mirrors the server ALLOW_AUTO_MODE gate (from /config/ui); when false the
// high-risk 'auto' permission mode is hidden/refused (SEC-M5).
private allowAutoMode = false
// W3(b): the server COST_BUDGET_USD (from /config/ui); 0 = disabled. The per-tab
// gauge warn-styles the cost chip once costUsd >= this budget.
private costBudgetUsd = 0
private pushHost!: HTMLElement // A1: 🔔 host, mounted once, re-parented per rebuild
private timelinePanel!: HTMLElement // A4: shared timeline panel (one mounted at a time)
private timelineOpen = false
@@ -275,6 +278,11 @@ export class TabApp {
) {
this.allowAutoMode = (data as UiConfig).allowAutoMode
}
// W3(b): read the cost budget (optional, present only when > 0 server-side).
const budget = (data as Record<string, unknown>)?.['costBudgetUsd']
if (typeof budget === 'number' && Number.isFinite(budget) && budget > 0) {
this.costBudgetUsd = budget
}
} catch {
// best-effort — leave allowAutoMode false (auto hidden) on any failure
}
@@ -1441,7 +1449,7 @@ export class TabApp {
queue.textContent = n > 0 ? `${n}` : ''
}
const gauge = el.querySelector<HTMLElement>('.tab-gauge')
if (gauge) renderTelemetryGauge(gauge, entry.session.telemetry, STATUSLINE_TTL_MS) // B2
if (gauge) renderTelemetryGauge(gauge, entry.session.telemetry, STATUSLINE_TTL_MS, this.costBudgetUsd) // B2 + W3(b)
}
/** Full rebuild — ONLY for structural changes (add/close/reorder/rename). */