gsd-pi 2.36.0-dev.d612764 → 2.36.0-dev.f887f4e

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/resources/extensions/gsd/auto-dashboard.js +104 -334
  2. package/dist/resources/extensions/gsd/auto-loop.js +0 -11
  3. package/dist/resources/extensions/gsd/auto.js +0 -16
  4. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +1 -1
  5. package/dist/resources/extensions/gsd/commands.js +1 -51
  6. package/dist/resources/extensions/gsd/docs/preferences-reference.md +0 -25
  7. package/dist/resources/extensions/gsd/index.js +0 -5
  8. package/dist/resources/extensions/gsd/notifications.js +1 -10
  9. package/dist/resources/extensions/gsd/preferences-types.js +0 -2
  10. package/dist/resources/extensions/gsd/preferences-validation.js +0 -29
  11. package/dist/resources/extensions/gsd/preferences.js +0 -3
  12. package/dist/resources/extensions/gsd/prompts/research-milestone.md +3 -4
  13. package/dist/resources/extensions/gsd/prompts/research-slice.md +2 -3
  14. package/dist/resources/extensions/gsd/templates/preferences.md +0 -6
  15. package/dist/resources/extensions/search-the-web/native-search.js +4 -45
  16. package/dist/resources/extensions/shared/terminal.js +0 -5
  17. package/dist/resources/extensions/subagent/index.js +60 -180
  18. package/package.json +1 -1
  19. package/packages/pi-tui/dist/terminal-image.d.ts.map +1 -1
  20. package/packages/pi-tui/dist/terminal-image.js +0 -4
  21. package/packages/pi-tui/dist/terminal-image.js.map +1 -1
  22. package/packages/pi-tui/src/terminal-image.ts +0 -5
  23. package/src/resources/extensions/gsd/auto-dashboard.ts +116 -363
  24. package/src/resources/extensions/gsd/auto-loop.ts +0 -42
  25. package/src/resources/extensions/gsd/auto.ts +0 -21
  26. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +1 -1
  27. package/src/resources/extensions/gsd/commands.ts +1 -54
  28. package/src/resources/extensions/gsd/docs/preferences-reference.md +0 -25
  29. package/src/resources/extensions/gsd/index.ts +0 -8
  30. package/src/resources/extensions/gsd/notifications.ts +1 -10
  31. package/src/resources/extensions/gsd/preferences-types.ts +0 -13
  32. package/src/resources/extensions/gsd/preferences-validation.ts +0 -26
  33. package/src/resources/extensions/gsd/preferences.ts +0 -4
  34. package/src/resources/extensions/gsd/prompts/research-milestone.md +3 -4
  35. package/src/resources/extensions/gsd/prompts/research-slice.md +2 -3
  36. package/src/resources/extensions/gsd/templates/preferences.md +0 -6
  37. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +0 -2
  38. package/src/resources/extensions/gsd/tests/preferences.test.ts +0 -23
  39. package/src/resources/extensions/search-the-web/native-search.ts +4 -50
  40. package/src/resources/extensions/shared/terminal.ts +0 -5
  41. package/src/resources/extensions/subagent/index.ts +79 -236
  42. package/dist/resources/extensions/cmux/index.js +0 -321
  43. package/dist/resources/extensions/gsd/commands-cmux.js +0 -120
  44. package/src/resources/extensions/cmux/index.ts +0 -384
  45. package/src/resources/extensions/gsd/commands-cmux.ts +0 -143
  46. package/src/resources/extensions/gsd/tests/cmux.test.ts +0 -98
@@ -10,19 +10,15 @@ import type { ExtensionContext, ExtensionCommandContext, SessionMessageEntry } f
10
10
  import type { GSDState } from "./types.js";
11
11
  import { getCurrentBranch } from "./worktree.js";
12
12
  import { getActiveHook } from "./post-unit-hooks.js";
13
- import { getLedger, getProjectTotals } from "./metrics.js";
13
+ import { getLedger, getProjectTotals, formatCost, formatTokenCount, formatTierSavings } from "./metrics.js";
14
14
  import {
15
15
  resolveMilestoneFile,
16
16
  resolveSliceFile,
17
17
  } from "./paths.js";
18
18
  import { parseRoadmap, parsePlan } from "./files.js";
19
- import { readFileSync, writeFileSync, existsSync } from "node:fs";
20
- import { execFileSync } from "node:child_process";
19
+ import { readFileSync, existsSync } from "node:fs";
21
20
  import { truncateToWidth, visibleWidth } from "@gsd/pi-tui";
22
21
  import { makeUI, GLYPH, INDENT } from "../shared/mod.js";
23
- import { computeProgressScore } from "./progress-score.js";
24
- import { getActiveWorktreeName } from "./worktree-command.js";
25
- import { loadEffectiveGSDPreferences, getGlobalGSDPreferencesPath } from "./preferences.js";
26
22
 
27
23
  // ─── Dashboard Data ───────────────────────────────────────────────────────────
28
24
 
@@ -208,13 +204,6 @@ export function estimateTimeRemaining(): string | null {
208
204
 
209
205
  // ─── Slice Progress Cache ─────────────────────────────────────────────────────
210
206
 
211
- /** Cached task detail for the widget task checklist */
212
- interface CachedTaskDetail {
213
- id: string;
214
- title: string;
215
- done: boolean;
216
- }
217
-
218
207
  /** Cached slice progress for the widget — avoid async in render */
219
208
  let cachedSliceProgress: {
220
209
  done: number;
@@ -222,8 +211,6 @@ let cachedSliceProgress: {
222
211
  milestoneId: string;
223
212
  /** Real task progress for the active slice, if its plan file exists */
224
213
  activeSliceTasks: { done: number; total: number } | null;
225
- /** Full task list for the active slice checklist */
226
- taskDetails: CachedTaskDetail[] | null;
227
214
  } | null = null;
228
215
 
229
216
  export function updateSliceProgressCache(base: string, mid: string, activeSid?: string): void {
@@ -234,7 +221,6 @@ export function updateSliceProgressCache(base: string, mid: string, activeSid?:
234
221
  const roadmap = parseRoadmap(content);
235
222
 
236
223
  let activeSliceTasks: { done: number; total: number } | null = null;
237
- let taskDetails: CachedTaskDetail[] | null = null;
238
224
  if (activeSid) {
239
225
  try {
240
226
  const planFile = resolveSliceFile(base, mid, activeSid, "PLAN");
@@ -245,7 +231,6 @@ export function updateSliceProgressCache(base: string, mid: string, activeSid?:
245
231
  done: plan.tasks.filter(t => t.done).length,
246
232
  total: plan.tasks.length,
247
233
  };
248
- taskDetails = plan.tasks.map(t => ({ id: t.id, title: t.title, done: t.done }));
249
234
  }
250
235
  } catch {
251
236
  // Non-fatal — just omit task count
@@ -257,14 +242,13 @@ export function updateSliceProgressCache(base: string, mid: string, activeSid?:
257
242
  total: roadmap.slices.length,
258
243
  milestoneId: mid,
259
244
  activeSliceTasks,
260
- taskDetails,
261
245
  };
262
246
  } catch {
263
247
  // Non-fatal — widget just won't show progress bar
264
248
  }
265
249
  }
266
250
 
267
- export function getRoadmapSlicesSync(): { done: number; total: number; activeSliceTasks: { done: number; total: number } | null; taskDetails: CachedTaskDetail[] | null } | null {
251
+ export function getRoadmapSlicesSync(): { done: number; total: number; activeSliceTasks: { done: number; total: number } | null } | null {
268
252
  return cachedSliceProgress;
269
253
  }
270
254
 
@@ -272,41 +256,6 @@ export function clearSliceProgressCache(): void {
272
256
  cachedSliceProgress = null;
273
257
  }
274
258
 
275
- // ─── Last Commit Cache ────────────────────────────────────────────────────────
276
-
277
- /** Cached last commit info — refreshed on the 15s timer, not every render */
278
- let cachedLastCommit: { timeAgo: string; message: string } | null = null;
279
- let lastCommitFetchedAt = 0;
280
-
281
- function refreshLastCommit(basePath: string): void {
282
- try {
283
- const raw = execFileSync("git", ["log", "-1", "--format=%cr|%s"], {
284
- cwd: basePath,
285
- encoding: "utf-8",
286
- stdio: ["pipe", "pipe", "pipe"],
287
- timeout: 3000,
288
- }).trim();
289
- const sep = raw.indexOf("|");
290
- if (sep > 0) {
291
- cachedLastCommit = {
292
- timeAgo: raw.slice(0, sep).replace(/ ago$/, "").replace(/ /g, ""),
293
- message: raw.slice(sep + 1),
294
- };
295
- }
296
- lastCommitFetchedAt = Date.now();
297
- } catch {
298
- // Non-fatal — just skip last commit display
299
- }
300
- }
301
-
302
- function getLastCommit(basePath: string): { timeAgo: string; message: string } | null {
303
- // Refresh at most every 15 seconds
304
- if (Date.now() - lastCommitFetchedAt > 15_000) {
305
- refreshLastCommit(basePath);
306
- }
307
- return cachedLastCommit;
308
- }
309
-
310
259
  // ─── Footer Factory ───────────────────────────────────────────────────────────
311
260
 
312
261
  /**
@@ -320,67 +269,6 @@ export const hideFooter = () => ({
320
269
  dispose() {},
321
270
  });
322
271
 
323
- // ─── Widget Display Mode ──────────────────────────────────────────────────────
324
-
325
- /** Widget display modes: full → small → min → off → full */
326
- export type WidgetMode = "full" | "small" | "min" | "off";
327
- const WIDGET_MODES: WidgetMode[] = ["full", "small", "min", "off"];
328
- let widgetMode: WidgetMode = "full";
329
- let widgetModeInitialized = false;
330
-
331
- /** Load widget mode from preferences (once). */
332
- function ensureWidgetModeLoaded(): void {
333
- if (widgetModeInitialized) return;
334
- widgetModeInitialized = true;
335
- try {
336
- const loaded = loadEffectiveGSDPreferences();
337
- const saved = loaded?.preferences?.widget_mode;
338
- if (saved && WIDGET_MODES.includes(saved as WidgetMode)) {
339
- widgetMode = saved as WidgetMode;
340
- }
341
- } catch { /* non-fatal — use default */ }
342
- }
343
-
344
- /** Persist widget mode to global preferences YAML. */
345
- function persistWidgetMode(mode: WidgetMode): void {
346
- try {
347
- const prefsPath = getGlobalGSDPreferencesPath();
348
- let content = "";
349
- if (existsSync(prefsPath)) {
350
- content = readFileSync(prefsPath, "utf-8");
351
- }
352
- const line = `widget_mode: ${mode}`;
353
- const re = /^widget_mode:\s*\S+/m;
354
- if (re.test(content)) {
355
- content = content.replace(re, line);
356
- } else {
357
- content = content.trimEnd() + "\n" + line + "\n";
358
- }
359
- writeFileSync(prefsPath, content, "utf-8");
360
- } catch { /* non-fatal — mode still set in memory */ }
361
- }
362
-
363
- /** Cycle to the next widget mode. Returns the new mode. */
364
- export function cycleWidgetMode(): WidgetMode {
365
- ensureWidgetModeLoaded();
366
- const idx = WIDGET_MODES.indexOf(widgetMode);
367
- widgetMode = WIDGET_MODES[(idx + 1) % WIDGET_MODES.length];
368
- persistWidgetMode(widgetMode);
369
- return widgetMode;
370
- }
371
-
372
- /** Set widget mode directly. */
373
- export function setWidgetMode(mode: WidgetMode): void {
374
- widgetMode = mode;
375
- persistWidgetMode(widgetMode);
376
- }
377
-
378
- /** Get current widget mode. */
379
- export function getWidgetMode(): WidgetMode {
380
- ensureWidgetModeLoaded();
381
- return widgetMode;
382
- }
383
-
384
272
  // ─── Progress Widget ──────────────────────────────────────────────────────────
385
273
 
386
274
  /** State accessors passed to updateProgressWidget to avoid direct global access */
@@ -407,32 +295,19 @@ export function updateProgressWidget(
407
295
  const mid = state.activeMilestone;
408
296
  const slice = state.activeSlice;
409
297
  const task = state.activeTask;
410
- const isHook = unitType.startsWith("hook/");
298
+ const next = peekNext(unitType, state);
411
299
 
412
300
  // Cache git branch at widget creation time (not per render)
413
301
  let cachedBranch: string | null = null;
414
302
  try { cachedBranch = getCurrentBranch(accessors.getBasePath()); } catch { /* not in git repo */ }
415
303
 
416
- // Cache short pwd (last 2 path segments only) + worktree/branch info
417
- let widgetPwd: string;
418
- {
419
- let fullPwd = process.cwd();
420
- const widgetHome = process.env.HOME || process.env.USERPROFILE;
421
- if (widgetHome && fullPwd.startsWith(widgetHome)) {
422
- fullPwd = `~${fullPwd.slice(widgetHome.length)}`;
423
- }
424
- const parts = fullPwd.split("/");
425
- widgetPwd = parts.length > 2 ? parts.slice(-2).join("/") : fullPwd;
426
- }
427
- const worktreeName = getActiveWorktreeName();
428
- if (worktreeName && cachedBranch) {
429
- widgetPwd = `${widgetPwd} (\u2387 ${cachedBranch})`;
430
- } else if (cachedBranch) {
431
- widgetPwd = `${widgetPwd} (${cachedBranch})`;
304
+ // Cache pwd with ~ substitution
305
+ let widgetPwd = process.cwd();
306
+ const widgetHome = process.env.HOME || process.env.USERPROFILE;
307
+ if (widgetHome && widgetPwd.startsWith(widgetHome)) {
308
+ widgetPwd = `~${widgetPwd.slice(widgetHome.length)}`;
432
309
  }
433
-
434
- // Pre-fetch last commit for display
435
- refreshLastCommit(accessors.getBasePath());
310
+ if (cachedBranch) widgetPwd = `${widgetPwd} (${cachedBranch})`;
436
311
 
437
312
  ctx.ui.setWidget("gsd-progress", (tui, theme) => {
438
313
  let pulseBright = true;
@@ -472,267 +347,152 @@ export function updateProgressWidget(
472
347
  : theme.fg("dim", GLYPH.statusPending);
473
348
  const elapsed = formatAutoElapsed(accessors.getAutoStartTime());
474
349
  const modeTag = accessors.isStepMode() ? "NEXT" : "AUTO";
475
-
476
- // Health indicator in header
477
- const score = computeProgressScore();
478
- const healthColor = score.level === "green" ? "success"
479
- : score.level === "yellow" ? "warning"
480
- : "error";
481
- const healthIcon = score.level === "green" ? GLYPH.statusActive
482
- : score.level === "yellow" ? "!"
483
- : "x";
484
- const healthStr = ` ${theme.fg(healthColor, healthIcon)} ${theme.fg(healthColor, score.summary)}`;
485
-
486
- const headerLeft = `${pad}${dot} ${theme.fg("accent", theme.bold("GSD"))} ${theme.fg("success", modeTag)}${healthStr}`;
487
-
488
- // ETA in header right, after elapsed
489
- const eta = estimateTimeRemaining();
490
- const etaShort = eta ? eta.replace(" remaining", " left") : null;
491
- const headerRight = elapsed
492
- ? (etaShort
493
- ? `${theme.fg("dim", elapsed)} ${theme.fg("dim", "·")} ${theme.fg("dim", etaShort)}`
494
- : theme.fg("dim", elapsed))
495
- : "";
350
+ const headerLeft = `${pad}${dot} ${theme.fg("accent", theme.bold("GSD"))} ${theme.fg("success", modeTag)}`;
351
+ const headerRight = elapsed ? theme.fg("dim", elapsed) : "";
496
352
  lines.push(rightAlign(headerLeft, headerRight, width));
497
353
 
498
- // ── Gather stats (needed by multiple modes) ─────────────────────
499
- const cmdCtx = accessors.getCmdCtx();
500
- let totalInput = 0;
501
- let totalCacheRead = 0;
502
- if (cmdCtx) {
503
- for (const entry of cmdCtx.sessionManager.getEntries()) {
504
- if (entry.type === "message") {
505
- const msgEntry = entry as SessionMessageEntry;
506
- if (msgEntry.message?.role === "assistant") {
507
- const u = (msgEntry.message as any).usage;
508
- if (u) {
509
- totalInput += u.input || 0;
510
- totalCacheRead += u.cacheRead || 0;
511
- }
512
- }
513
- }
514
- }
515
- }
516
- const mLedger = getLedger();
517
- const autoTotals = mLedger ? getProjectTotals(mLedger.units) : null;
518
- const cumulativeCost = autoTotals?.cost ?? 0;
519
- const cxUsage = cmdCtx?.getContextUsage?.();
520
- const cxWindow = cxUsage?.contextWindow ?? cmdCtx?.model?.contextWindow ?? 0;
521
- const cxPctVal = cxUsage?.percent ?? 0;
522
- const cxPct = cxUsage?.percent !== null ? cxPctVal.toFixed(1) : "?";
523
-
524
- // Model display — shown in context section, not stats
525
- const modelId = cmdCtx?.model?.id ?? "";
526
- const modelProvider = cmdCtx?.model?.provider ?? "";
527
- const modelDisplay = modelProvider && modelId
528
- ? `${modelProvider}/${modelId}`
529
- : modelId;
530
-
531
- // ── Mode: off — return empty ──────────────────────────────────
532
- if (widgetMode === "off") {
533
- cachedLines = [];
534
- cachedWidth = width;
535
- return [];
354
+ lines.push("");
355
+
356
+ if (mid) {
357
+ lines.push(truncateToWidth(`${pad}${theme.fg("dim", mid.title)}`, width));
536
358
  }
537
359
 
538
- // ── Mode: min header line only ──────────────────────────────
539
- if (widgetMode === "min") {
540
- lines.push(...ui.bar());
541
- cachedLines = lines;
542
- cachedWidth = width;
543
- return lines;
360
+ if (slice && unitType !== "research-milestone" && unitType !== "plan-milestone") {
361
+ lines.push(truncateToWidth(
362
+ `${pad}${theme.fg("text", theme.bold(`${slice.id}: ${slice.title}`))}`,
363
+ width,
364
+ ));
544
365
  }
545
366
 
546
- // ── Mode: small — header + progress bar + compact stats ───────
547
- if (widgetMode === "small") {
548
- lines.push("");
367
+ lines.push("");
549
368
 
550
- // Action line
551
- const target = task ? `${task.id}: ${task.title}` : unitId;
552
- const actionLeft = `${pad}${theme.fg("accent", "▸")} ${theme.fg("accent", verb)} ${theme.fg("text", target)}`;
553
- lines.push(rightAlign(actionLeft, theme.fg("dim", phaseLabel), width));
369
+ const target = task ? `${task.id}: ${task.title}` : unitId;
370
+ const actionLeft = `${pad}${theme.fg("accent", "▸")} ${theme.fg("accent", verb)} ${theme.fg("text", target)}`;
371
+ const tierTag = tierBadge ? theme.fg("dim", `[${tierBadge}] `) : "";
372
+ const phaseBadge = `${tierTag}${theme.fg("dim", phaseLabel)}`;
373
+ lines.push(rightAlign(actionLeft, phaseBadge, width));
374
+ lines.push("");
554
375
 
555
- // Progress bar
556
- const roadmapSlices = mid ? getRoadmapSlicesSync() : null;
376
+ if (mid) {
377
+ const roadmapSlices = getRoadmapSlicesSync();
557
378
  if (roadmapSlices) {
558
379
  const { done, total, activeSliceTasks } = roadmapSlices;
559
- const barWidth = Math.max(6, Math.min(18, Math.floor(width * 0.25)));
380
+ const barWidth = Math.max(8, Math.min(24, Math.floor(width * 0.3)));
560
381
  const pct = total > 0 ? done / total : 0;
561
382
  const filled = Math.round(pct * barWidth);
562
- const bar = theme.fg("success", "".repeat(filled))
563
- + theme.fg("dim", "".repeat(barWidth - filled));
564
- let meta = `${theme.fg("text", `${done}`)}${theme.fg("dim", `/${total} slices`)}`;
383
+ const bar = theme.fg("success", "".repeat(filled))
384
+ + theme.fg("dim", "".repeat(barWidth - filled));
385
+
386
+ let meta = theme.fg("dim", `${done}/${total} slices`);
387
+
565
388
  if (activeSliceTasks && activeSliceTasks.total > 0) {
566
- const tn = Math.min(activeSliceTasks.done + 1, activeSliceTasks.total);
567
- meta += `${theme.fg("dim", " · task ")}${theme.fg("accent", `${tn}`)}${theme.fg("dim", `/${activeSliceTasks.total}`)}`;
389
+ const taskNum = Math.min(activeSliceTasks.done + 1, activeSliceTasks.total);
390
+ meta += theme.fg("dim", ` · task ${taskNum}/${activeSliceTasks.total}`);
568
391
  }
569
- lines.push(`${pad}${bar} ${meta}`);
570
- }
571
392
 
572
- // Compact stats: cost + context only
573
- const smallStats: string[] = [];
574
- if (cumulativeCost) smallStats.push(theme.fg("warning", `$${cumulativeCost.toFixed(2)}`));
575
- const cxDisplay = `${cxPct}%ctx`;
576
- if (cxPctVal > 90) smallStats.push(theme.fg("error", cxDisplay));
577
- else if (cxPctVal > 70) smallStats.push(theme.fg("warning", cxDisplay));
578
- else smallStats.push(theme.fg("dim", cxDisplay));
579
- if (smallStats.length > 0) {
580
- lines.push(rightAlign("", smallStats.join(theme.fg("dim", " ")), width));
581
- }
393
+ // ETA estimate
394
+ const eta = estimateTimeRemaining();
395
+ if (eta) {
396
+ meta += theme.fg("dim", ` · ${eta}`);
397
+ }
582
398
 
583
- lines.push(...ui.bar());
584
- cachedLines = lines;
585
- cachedWidth = width;
586
- return lines;
399
+ lines.push(truncateToWidth(`${pad}${bar} ${meta}`, width));
400
+ }
587
401
  }
588
402
 
589
- // ── Mode: full — complete two-column layout ───────────────────
590
403
  lines.push("");
591
404
 
592
- // Context section: milestone + slice + model
593
- const hasContext = !!(mid || (slice && unitType !== "research-milestone" && unitType !== "plan-milestone"));
594
- if (mid) {
595
- const modelTag = modelDisplay ? theme.fg("muted", ` ${modelDisplay}`) : "";
596
- lines.push(truncateToWidth(`${pad}${theme.fg("dim", mid.title)}${modelTag}`, width));
597
- }
598
- if (slice && unitType !== "research-milestone" && unitType !== "plan-milestone") {
405
+ if (next) {
599
406
  lines.push(truncateToWidth(
600
- `${pad}${theme.fg("text", theme.bold(`${slice.id}: ${slice.title}`))}`,
407
+ `${pad}${theme.fg("dim", "→")} ${theme.fg("dim", `then ${next}`)}`,
601
408
  width,
602
409
  ));
603
410
  }
604
- if (hasContext) lines.push("");
605
-
606
- const target = task ? `${task.id}: ${task.title}` : unitId;
607
- const actionLeft = `${pad}${theme.fg("accent", "▸")} ${theme.fg("accent", verb)} ${theme.fg("text", target)}`;
608
- const tierTag = tierBadge ? theme.fg("dim", `[${tierBadge}] `) : "";
609
- const phaseBadge = `${tierTag}${theme.fg("dim", phaseLabel)}`;
610
- lines.push(rightAlign(actionLeft, phaseBadge, width));
611
411
 
412
+ // ── Footer info (pwd, tokens, cost, context, model) ──────────────
612
413
  lines.push("");
414
+ lines.push(truncateToWidth(theme.fg("dim", `${pad}${widgetPwd}`), width, theme.fg("dim", "…")));
613
415
 
614
- // Two-column body
615
- const minTwoColWidth = 76;
616
- const roadmapSlices = mid ? getRoadmapSlicesSync() : null;
617
- const taskDetailsCol = roadmapSlices?.taskDetails ?? null;
618
- const useTwoCol = width >= minTwoColWidth && taskDetailsCol !== null && taskDetailsCol.length > 0;
619
- const leftColWidth = useTwoCol
620
- ? Math.floor(width * (width >= 100 ? 0.45 : 0.50))
621
- : width;
622
-
623
- const leftLines: string[] = [];
624
-
625
- if (roadmapSlices) {
626
- const { done, total, activeSliceTasks } = roadmapSlices;
627
- const barWidth = Math.max(6, Math.min(18, Math.floor(leftColWidth * 0.4)));
628
- const pct = total > 0 ? done / total : 0;
629
- const filled = Math.round(pct * barWidth);
630
- const bar = theme.fg("success", "━".repeat(filled))
631
- + theme.fg("dim", "─".repeat(barWidth - filled));
632
-
633
- let meta = `${theme.fg("text", `${done}`)}${theme.fg("dim", `/${total} slices`)}`;
634
- if (activeSliceTasks && activeSliceTasks.total > 0) {
635
- const taskNum = isHook
636
- ? Math.max(activeSliceTasks.done, 1)
637
- : Math.min(activeSliceTasks.done + 1, activeSliceTasks.total);
638
- meta += `${theme.fg("dim", " · task ")}${theme.fg("accent", `${taskNum}`)}${theme.fg("dim", `/${activeSliceTasks.total}`)}`;
639
- }
640
- leftLines.push(`${pad}${bar} ${meta}`);
641
- }
642
-
643
- // Build right column: task checklist
644
- const rightLines: string[] = [];
645
- const maxVisibleTasks = 8;
646
-
647
- function formatTaskLine(t: { id: string; title: string; done: boolean }, isCurrent: boolean): string {
648
- const glyph = t.done
649
- ? theme.fg("success", "*")
650
- : isCurrent
651
- ? theme.fg("accent", ">")
652
- : theme.fg("dim", ".");
653
- const id = isCurrent
654
- ? theme.fg("accent", t.id)
655
- : t.done
656
- ? theme.fg("muted", t.id)
657
- : theme.fg("dim", t.id);
658
- const title = isCurrent
659
- ? theme.fg("text", t.title)
660
- : t.done
661
- ? theme.fg("muted", t.title)
662
- : theme.fg("text", t.title);
663
- return `${glyph} ${id}: ${title}`;
664
- }
665
-
666
- if (useTwoCol && taskDetailsCol) {
667
- for (const t of taskDetailsCol.slice(0, maxVisibleTasks)) {
668
- rightLines.push(formatTaskLine(t, !!(task && t.id === task.id)));
669
- }
670
- if (taskDetailsCol.length > maxVisibleTasks) {
671
- rightLines.push(theme.fg("dim", ` +${taskDetailsCol.length - maxVisibleTasks} more`));
672
- }
673
- } else if (!useTwoCol && taskDetailsCol && taskDetailsCol.length > 0) {
674
- for (const t of taskDetailsCol.slice(0, maxVisibleTasks)) {
675
- leftLines.push(`${pad}${formatTaskLine(t, !!(task && t.id === task.id))}`);
676
- }
677
- }
678
-
679
- // Compose columns
680
- if (useTwoCol) {
681
- const maxRows = Math.max(leftLines.length, rightLines.length);
682
- if (maxRows > 0) {
683
- lines.push("");
684
- for (let i = 0; i < maxRows; i++) {
685
- const left = padToWidth(truncateToWidth(leftLines[i] ?? "", leftColWidth), leftColWidth);
686
- const right = rightLines[i] ?? "";
687
- lines.push(`${left}${right}`);
416
+ // Token stats from current unit session + cumulative cost from metrics
417
+ {
418
+ const cmdCtx = accessors.getCmdCtx();
419
+ let totalInput = 0, totalOutput = 0;
420
+ let totalCacheRead = 0, totalCacheWrite = 0;
421
+ if (cmdCtx) {
422
+ for (const entry of cmdCtx.sessionManager.getEntries()) {
423
+ if (entry.type === "message") {
424
+ const msgEntry = entry as SessionMessageEntry;
425
+ if (msgEntry.message?.role === "assistant") {
426
+ const u = (msgEntry.message as any).usage;
427
+ if (u) {
428
+ totalInput += u.input || 0;
429
+ totalOutput += u.output || 0;
430
+ totalCacheRead += u.cacheRead || 0;
431
+ totalCacheWrite += u.cacheWrite || 0;
432
+ }
433
+ }
434
+ }
688
435
  }
689
436
  }
690
- } else {
691
- if (leftLines.length > 0) {
692
- lines.push("");
693
- for (const l of leftLines) lines.push(truncateToWidth(l, width));
694
- }
695
- }
437
+ const mLedger = getLedger();
438
+ const autoTotals = mLedger ? getProjectTotals(mLedger.units) : null;
439
+ const cumulativeCost = autoTotals?.cost ?? 0;
440
+
441
+ const cxUsage = cmdCtx?.getContextUsage?.();
442
+ const cxWindow = cxUsage?.contextWindow ?? cmdCtx?.model?.contextWindow ?? 0;
443
+ const cxPctVal = cxUsage?.percent ?? 0;
444
+ const cxPct = cxUsage?.percent !== null ? cxPctVal.toFixed(1) : "?";
696
445
 
697
- // ── Footer: simplified stats + pwd + last commit + hints ────────
698
- lines.push("");
699
- {
700
446
  const sp: string[] = [];
447
+ if (totalInput) sp.push(`↑${formatWidgetTokens(totalInput)}`);
448
+ if (totalOutput) sp.push(`↓${formatWidgetTokens(totalOutput)}`);
449
+ if (totalCacheRead) sp.push(`R${formatWidgetTokens(totalCacheRead)}`);
450
+ if (totalCacheWrite) sp.push(`W${formatWidgetTokens(totalCacheWrite)}`);
451
+ // Cache hit rate for current unit
701
452
  if (totalCacheRead + totalInput > 0) {
702
453
  const hitRate = Math.round((totalCacheRead / (totalCacheRead + totalInput)) * 100);
703
- const hitColor = hitRate >= 70 ? "success" : hitRate >= 40 ? "warning" : "error";
704
- sp.push(theme.fg(hitColor, `${hitRate}%hit`));
454
+ sp.push(`\u26A1${hitRate}%`);
455
+ }
456
+ if (cumulativeCost) sp.push(`$${cumulativeCost.toFixed(3)}`);
457
+
458
+ const cxDisplay = cxPct === "?"
459
+ ? `?/${formatWidgetTokens(cxWindow)}`
460
+ : `${cxPct}%/${formatWidgetTokens(cxWindow)}`;
461
+ if (cxPctVal > 90) {
462
+ sp.push(theme.fg("error", cxDisplay));
463
+ } else if (cxPctVal > 70) {
464
+ sp.push(theme.fg("warning", cxDisplay));
465
+ } else {
466
+ sp.push(cxDisplay);
705
467
  }
706
- if (cumulativeCost) sp.push(theme.fg("warning", `$${cumulativeCost.toFixed(2)}`));
707
-
708
- const cxDisplay = `${cxPct}%/${formatWidgetTokens(cxWindow)}`;
709
- if (cxPctVal > 90) sp.push(theme.fg("error", cxDisplay));
710
- else if (cxPctVal > 70) sp.push(theme.fg("warning", cxDisplay));
711
- else sp.push(cxDisplay);
712
468
 
713
- const statsLine = sp.map(p => p.includes("\x1b[") ? p : theme.fg("dim", p))
714
- .join(theme.fg("dim", " "));
715
- if (statsLine) {
716
- lines.push(rightAlign("", statsLine, width));
469
+ const sLeft = sp.map(p => p.includes("\x1b[") ? p : theme.fg("dim", p))
470
+ .join(theme.fg("dim", " "));
471
+
472
+ const modelId = cmdCtx?.model?.id ?? "";
473
+ const modelProvider = cmdCtx?.model?.provider ?? "";
474
+ const modelPhase = phaseLabel ? theme.fg("dim", `[${phaseLabel}] `) : "";
475
+ const modelDisplay = modelProvider && modelId
476
+ ? `${modelProvider}/${modelId}`
477
+ : modelId;
478
+ const sRight = modelDisplay
479
+ ? `${modelPhase}${theme.fg("dim", modelDisplay)}`
480
+ : "";
481
+ lines.push(rightAlign(`${pad}${sLeft}`, sRight, width));
482
+
483
+ // Dynamic routing savings summary
484
+ if (mLedger && mLedger.units.some(u => u.tier)) {
485
+ const savings = formatTierSavings(mLedger.units);
486
+ if (savings) {
487
+ lines.push(truncateToWidth(theme.fg("dim", `${pad}${savings}`), width));
488
+ }
717
489
  }
718
490
  }
719
- // PWD line with last commit info right-aligned
720
- const lastCommit = getLastCommit(accessors.getBasePath());
721
- const commitStr = lastCommit
722
- ? theme.fg("dim", `${lastCommit.timeAgo} ago: ${lastCommit.message}`)
723
- : "";
724
- const pwdStr = theme.fg("dim", widgetPwd);
725
- if (commitStr) {
726
- lines.push(rightAlign(`${pad}${pwdStr}`, truncateToWidth(commitStr, Math.floor(width * 0.45)), width));
727
- } else {
728
- lines.push(`${pad}${pwdStr}`);
729
- }
730
- // Hints line
491
+
731
492
  const hintParts: string[] = [];
732
493
  hintParts.push("esc pause");
733
494
  hintParts.push(process.platform === "darwin" ? "⌃⌥G dashboard" : "Ctrl+Alt+G dashboard");
734
- const hintStr = theme.fg("dim", hintParts.join(" | "));
735
- lines.push(rightAlign("", hintStr, width));
495
+ lines.push(...ui.hints(hintParts));
736
496
 
737
497
  lines.push(...ui.bar());
738
498
 
@@ -761,10 +521,3 @@ function rightAlign(left: string, right: string, width: number): string {
761
521
  const gap = Math.max(1, width - leftVis - rightVis);
762
522
  return truncateToWidth(left + " ".repeat(gap) + right, width);
763
523
  }
764
-
765
- /** Pad a string with trailing spaces to fill exactly `colWidth` (ANSI-aware). */
766
- function padToWidth(s: string, colWidth: number): string {
767
- const vis = visibleWidth(s);
768
- if (vis >= colWidth) return truncateToWidth(s, colWidth);
769
- return s + " ".repeat(colWidth - vis);
770
- }