From b1e769b4d9118f18de5d079d5936b151a3062fa7 Mon Sep 17 00:00:00 2001 From: jonathancostin <66714927+jonathancostin@users.noreply.github.com> Date: Wed, 11 Mar 2026 19:18:08 -0500 Subject: [PATCH] feat: hide footer during auto-mode, show all stats in progress widget (#75) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit During auto-mode, the built-in footer is hidden entirely via setFooter() and all its info is moved into the progress widget: - pwd + git branch shown inside the widget - Token stats (↑/↓/R/W) from current unit session - Cumulative cost from metrics ledger (survives across unit resets) - Context window usage with color coding (warning >70%, error >90%) - Model name right-aligned - Footer restored to built-in on pause or stop - No model duplication (removed from hints) --- src/resources/extensions/gsd/auto.ts | 95 +++++++++++++++++++++++++++- 1 file changed, 93 insertions(+), 2 deletions(-) diff --git a/src/resources/extensions/gsd/auto.ts b/src/resources/extensions/gsd/auto.ts index 16fdd3431..20d654832 100644 --- a/src/resources/extensions/gsd/auto.ts +++ b/src/resources/extensions/gsd/auto.ts @@ -64,6 +64,7 @@ import { getSliceBranchName, switchToMain, mergeSliceToMain, + getCurrentBranch, } from "./worktree.ts"; import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui"; import { makeUI, GLYPH, INDENT } from "../shared/ui.js"; @@ -102,6 +103,26 @@ let unitTimeoutHandle: ReturnType | null = null; let wrapupWarningHandle: ReturnType | null = null; let idleWatchdogHandle: ReturnType | null = null; +/** Format token counts for compact display */ +function formatWidgetTokens(count: number): string { + if (count < 1000) return count.toString(); + if (count < 10000) return `${(count / 1000).toFixed(1)}k`; + if (count < 1000000) return `${Math.round(count / 1000)}k`; + if (count < 10000000) return `${(count / 1000000).toFixed(1)}M`; + return `${Math.round(count / 1000000)}M`; +} + +/** + * Footer factory that renders zero lines — hides the built-in footer entirely. + * All footer info (pwd, branch, tokens, cost, model) is shown inside the + * progress widget instead, so there's no gap or redundancy. + */ +const hideFooter = () => ({ + render(_width: number): string[] { return []; }, + invalidate() {}, + dispose() {}, +}); + /** Dashboard data for the overlay */ export interface AutoDashboardData { active: boolean; @@ -192,6 +213,7 @@ export async function stopAuto(ctx?: ExtensionContext, pi?: ExtensionAPI): Promi pendingCrashRecovery = null; ctx?.ui.setStatus("gsd-auto", undefined); ctx?.ui.setWidget("gsd-progress", undefined); + ctx?.ui.setFooter(undefined); // Restore the user's original model if (pi && ctx && originalModelId) { @@ -219,6 +241,7 @@ export async function pauseAuto(ctx?: ExtensionContext, _pi?: ExtensionAPI): Pro // — all needed for resume and dashboard display ctx?.ui.setStatus("gsd-auto", "paused"); ctx?.ui.setWidget("gsd-progress", undefined); + ctx?.ui.setFooter(undefined); const resumeCmd = stepMode ? "/gsd next" : "/gsd auto"; ctx?.ui.notify( `${stepMode ? "Step" : "Auto"}-mode paused (Escape). Type to interact, or ${resumeCmd} to resume.`, @@ -248,6 +271,7 @@ export async function startAuto( // Re-initialize metrics in case ledger was lost during pause if (!getLedger()) initMetrics(base); ctx.ui.setStatus("gsd-auto", stepMode ? "next" : "auto"); + ctx.ui.setFooter(hideFooter); ctx.ui.notify(stepMode ? "Step-mode resumed." : "Auto-mode resumed.", "info"); // Rebuild disk state before resuming — user interaction during pause may have changed files try { await rebuildState(base); } catch { /* non-fatal */ } @@ -352,6 +376,7 @@ export async function startAuto( } ctx.ui.setStatus("gsd-auto", stepMode ? "next" : "auto"); + ctx.ui.setFooter(hideFooter); const modeLabel = stepMode ? "Step-mode" : "Auto-mode"; const pendingCount = state.registry.filter(m => m.status !== 'complete').length; const scopeMsg = pendingCount > 1 @@ -594,7 +619,18 @@ function updateProgressWidget( const slice = state.activeSlice; const task = state.activeTask; const next = peekNext(unitType, state); - const preferredModel = resolveModelForUnit(unitType); + + // Cache git branch at widget creation time (not per render) + let cachedBranch: string | null = null; + try { cachedBranch = getCurrentBranch(basePath); } catch { /* not in git repo */ } + + // Cache pwd with ~ substitution + let widgetPwd = process.cwd(); + const widgetHome = process.env.HOME || process.env.USERPROFILE; + if (widgetHome && widgetPwd.startsWith(widgetHome)) { + widgetPwd = `~${widgetPwd.slice(widgetHome.length)}`; + } + if (cachedBranch) widgetPwd = `${widgetPwd} (${cachedBranch})`; ctx.ui.setWidget("gsd-progress", (tui, theme) => { let pulseBright = true; @@ -677,8 +713,63 @@ function updateProgressWidget( )); } + // ── Footer info (pwd, tokens, cost, context, model) ────────────── + lines.push(""); + lines.push(truncateToWidth(theme.fg("dim", `${pad}${widgetPwd}`), width, theme.fg("dim", "…"))); + + // Token stats from current unit session + cumulative cost from metrics + { + let totalInput = 0, totalOutput = 0; + let totalCacheRead = 0, totalCacheWrite = 0; + if (cmdCtx) { + for (const entry of cmdCtx.sessionManager.getEntries()) { + if (entry.type === "message" && (entry as any).message?.role === "assistant") { + const u = (entry as any).message.usage; + if (u) { + totalInput += u.input || 0; + totalOutput += u.output || 0; + totalCacheRead += u.cacheRead || 0; + totalCacheWrite += u.cacheWrite || 0; + } + } + } + } + const mLedger = getLedger(); + const autoTotals = mLedger ? getProjectTotals(mLedger.units) : null; + const cumulativeCost = autoTotals?.cost ?? 0; + + const cxUsage = cmdCtx?.getContextUsage?.(); + const cxWindow = cxUsage?.contextWindow ?? cmdCtx?.model?.contextWindow ?? 0; + const cxPctVal = cxUsage?.percent ?? 0; + const cxPct = cxUsage?.percent !== null ? cxPctVal.toFixed(1) : "?"; + + const sp: string[] = []; + if (totalInput) sp.push(`↑${formatWidgetTokens(totalInput)}`); + if (totalOutput) sp.push(`↓${formatWidgetTokens(totalOutput)}`); + if (totalCacheRead) sp.push(`R${formatWidgetTokens(totalCacheRead)}`); + if (totalCacheWrite) sp.push(`W${formatWidgetTokens(totalCacheWrite)}`); + if (cumulativeCost) sp.push(`$${cumulativeCost.toFixed(3)}`); + + const cxDisplay = cxPct === "?" + ? `?/${formatWidgetTokens(cxWindow)}` + : `${cxPct}%/${formatWidgetTokens(cxWindow)}`; + if (cxPctVal > 90) { + sp.push(theme.fg("error", cxDisplay)); + } else if (cxPctVal > 70) { + sp.push(theme.fg("warning", cxDisplay)); + } else { + sp.push(cxDisplay); + } + + const sLeft = sp.map(p => p.includes("\x1b[") ? p : theme.fg("dim", p)) + .join(theme.fg("dim", " ")); + + const modelId = cmdCtx?.model?.id ?? ""; + const sRight = modelId ? theme.fg("dim", modelId) : ""; + lines.push(rightAlign(`${pad}${sLeft}`, sRight, width)); + } + const hintParts: string[] = []; - if (preferredModel) hintParts.push(preferredModel); hintParts.push("esc pause"); hintParts.push("Ctrl+Alt+G dashboard"); lines.push(...ui.hints(hintParts));