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:
62
src/types.ts
62
src/types.ts
@@ -66,6 +66,8 @@ export interface Config {
|
||||
readonly ghTimeoutMs: number; // GH_TIMEOUT_MS, default 8000 (network — larger than diff)
|
||||
// B2 statusLine telemetry
|
||||
readonly statuslineTtlMs: number; // STATUSLINE_TTL_MS, default 30000
|
||||
// W3 quick-wins (b) cost budget guard
|
||||
readonly costBudgetUsd: number; // COST_BUDGET_USD, default 0 (0/unset = disabled)
|
||||
// B3 git worktree creation
|
||||
readonly worktreeEnabled: boolean; // WORKTREE_ENABLED, default true
|
||||
readonly worktreeRoot: string | undefined; // WORKTREE_ROOT (undefined → computed)
|
||||
@@ -257,6 +259,10 @@ export interface Session {
|
||||
/** A5: true once a stuck alert fired this round; re-armed (→false) by the next
|
||||
* pty.onData so each silent round alerts at most once. */
|
||||
stuckNotified: boolean;
|
||||
/** W3 quick-wins (b): true once the cost-budget alert fired for this session.
|
||||
* One-shot latch — NEVER re-armed (cost is monotonic), so the budget push +
|
||||
* warning broadcast happen at most once per session. */
|
||||
budgetNotified: boolean;
|
||||
/** B2: latest statusLine telemetry for this session; null until first report. */
|
||||
telemetry: StatusTelemetry | null;
|
||||
/** W2: bounded FIFO of verbatim byte strings to inject when Claude next goes
|
||||
@@ -322,6 +328,10 @@ export interface ProjectInfo {
|
||||
branch?: string; // current branch (git repos only)
|
||||
dirty?: boolean; // uncommitted changes (when projectDirtyCheck)
|
||||
lastActiveMs?: number; // newest ~/.claude/projects mtime for this cwd; sort key
|
||||
// W3 quick-wins (a) sync chip — best-effort git ahead/behind vs @{u} + last commit.
|
||||
ahead?: number; // commits on HEAD not on @{u} (git rev-list, right count)
|
||||
behind?: number; // commits on @{u} not on HEAD (git rev-list, left count)
|
||||
lastCommitMs?: number; // git log -1 --format=%ct * 1000 (HEAD commit time)
|
||||
sessions: ProjectSessionRef[]; // running sessions in this project (1:N; may be empty)
|
||||
}
|
||||
|
||||
@@ -431,8 +441,9 @@ export type PermissionMode = 'default' | 'acceptEdits' | 'plan' | 'auto';
|
||||
|
||||
/* ── A1 push notifications (§3.3, §A1) ── */
|
||||
|
||||
/** The three proactive signals pushed to the phone (§3.3 / §A1). */
|
||||
export type NotifyClass = 'needs-input' | 'done' | 'stuck';
|
||||
/** The proactive signals pushed to the phone (§3.3 / §A1). 'budget' (W3
|
||||
* quick-wins b) fires once when a session's cost crosses COST_BUDGET_USD. */
|
||||
export type NotifyClass = 'needs-input' | 'done' | 'stuck' | 'budget';
|
||||
|
||||
/** Outbound push body — ONE shape: push-service sends it, sw-push.js reads `cls`
|
||||
* (§3.3 review #3). Minimal by design: no raw terminal output, no secrets.
|
||||
@@ -601,6 +612,53 @@ export interface UiPrefs {
|
||||
* permission mode when the server forbids it (SEC-M5). */
|
||||
export interface UiConfig {
|
||||
allowAutoMode: boolean;
|
||||
/** W3 quick-wins (b): the cost-budget threshold (USD). Present when > 0 so the
|
||||
* FE can derive cost-overage warn styling client-side; omitted when disabled. */
|
||||
costBudgetUsd?: number;
|
||||
}
|
||||
|
||||
/* ── W3 quick-wins (c) reconnect digest (GET /digest) ── */
|
||||
|
||||
/** One session in the "while you were away" digest — a read-side projection of a
|
||||
* live session plus its latest telemetry/status. All fields derived, no new state. */
|
||||
export interface DigestSession {
|
||||
id: string;
|
||||
title?: string; // last cwd segment
|
||||
status: ClaudeStatus;
|
||||
costUsd?: number; // telemetry.costUsd
|
||||
lastOutputAt?: number;
|
||||
finished: boolean; // status==='idle' && lastOutputAt > since
|
||||
needsInput: boolean; // status==='waiting'
|
||||
stuck: boolean; // status==='stuck'
|
||||
}
|
||||
|
||||
/** GET /digest result — an aggregate over manager.list() since a client's
|
||||
* last-seen timestamp. Pure read (no new state); empty when no sessions. */
|
||||
export interface DigestResult {
|
||||
since: number;
|
||||
generatedAt: number;
|
||||
total: number;
|
||||
finished: number;
|
||||
needsInput: number;
|
||||
stuck: number;
|
||||
working: number;
|
||||
totalCostUsd: number;
|
||||
sessions: DigestSession[];
|
||||
}
|
||||
|
||||
/* ── W3 quick-wins (d) recent-commits log (GET /projects/log) ── */
|
||||
|
||||
/** One commit from `git log` (NUL-record, US-field delimited). `at` = %ct*1000. */
|
||||
export interface CommitLogEntry {
|
||||
hash: string;
|
||||
at: number;
|
||||
subject: string;
|
||||
}
|
||||
|
||||
/** GET /projects/log result. `truncated` = more commits exist beyond the cap. */
|
||||
export interface GitLogResult {
|
||||
commits: CommitLogEntry[];
|
||||
truncated: boolean;
|
||||
}
|
||||
|
||||
/* ─────────────────────── frontend (§5/§6.3) ──────────────────── */
|
||||
|
||||
Reference in New Issue
Block a user