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
@@ -7,16 +7,12 @@
7
7
  */
8
8
  import { getCurrentBranch } from "./worktree.js";
9
9
  import { getActiveHook } from "./post-unit-hooks.js";
10
- import { getLedger, getProjectTotals } from "./metrics.js";
10
+ import { getLedger, getProjectTotals, formatTierSavings } from "./metrics.js";
11
11
  import { resolveMilestoneFile, resolveSliceFile, } from "./paths.js";
12
12
  import { parseRoadmap, parsePlan } from "./files.js";
13
- import { readFileSync, writeFileSync, existsSync } from "node:fs";
14
- import { execFileSync } from "node:child_process";
13
+ import { readFileSync, existsSync } from "node:fs";
15
14
  import { truncateToWidth, visibleWidth } from "@gsd/pi-tui";
16
15
  import { makeUI, GLYPH, INDENT } from "../shared/mod.js";
17
- import { computeProgressScore } from "./progress-score.js";
18
- import { getActiveWorktreeName } from "./worktree-command.js";
19
- import { loadEffectiveGSDPreferences, getGlobalGSDPreferencesPath } from "./preferences.js";
20
16
  // ─── Unit Description Helpers ─────────────────────────────────────────────────
21
17
  export function unitVerb(unitType) {
22
18
  if (unitType.startsWith("hook/"))
@@ -168,6 +164,7 @@ export function estimateTimeRemaining() {
168
164
  const rm = m % 60;
169
165
  return rm > 0 ? `~${h}h ${rm}m remaining` : `~${h}h remaining`;
170
166
  }
167
+ // ─── Slice Progress Cache ─────────────────────────────────────────────────────
171
168
  /** Cached slice progress for the widget — avoid async in render */
172
169
  let cachedSliceProgress = null;
173
170
  export function updateSliceProgressCache(base, mid, activeSid) {
@@ -178,7 +175,6 @@ export function updateSliceProgressCache(base, mid, activeSid) {
178
175
  const content = readFileSync(roadmapFile, "utf-8");
179
176
  const roadmap = parseRoadmap(content);
180
177
  let activeSliceTasks = null;
181
- let taskDetails = null;
182
178
  if (activeSid) {
183
179
  try {
184
180
  const planFile = resolveSliceFile(base, mid, activeSid, "PLAN");
@@ -189,7 +185,6 @@ export function updateSliceProgressCache(base, mid, activeSid) {
189
185
  done: plan.tasks.filter(t => t.done).length,
190
186
  total: plan.tasks.length,
191
187
  };
192
- taskDetails = plan.tasks.map(t => ({ id: t.id, title: t.title, done: t.done }));
193
188
  }
194
189
  }
195
190
  catch {
@@ -201,7 +196,6 @@ export function updateSliceProgressCache(base, mid, activeSid) {
201
196
  total: roadmap.slices.length,
202
197
  milestoneId: mid,
203
198
  activeSliceTasks,
204
- taskDetails,
205
199
  };
206
200
  }
207
201
  catch {
@@ -214,38 +208,6 @@ export function getRoadmapSlicesSync() {
214
208
  export function clearSliceProgressCache() {
215
209
  cachedSliceProgress = null;
216
210
  }
217
- // ─── Last Commit Cache ────────────────────────────────────────────────────────
218
- /** Cached last commit info — refreshed on the 15s timer, not every render */
219
- let cachedLastCommit = null;
220
- let lastCommitFetchedAt = 0;
221
- function refreshLastCommit(basePath) {
222
- try {
223
- const raw = execFileSync("git", ["log", "-1", "--format=%cr|%s"], {
224
- cwd: basePath,
225
- encoding: "utf-8",
226
- stdio: ["pipe", "pipe", "pipe"],
227
- timeout: 3000,
228
- }).trim();
229
- const sep = raw.indexOf("|");
230
- if (sep > 0) {
231
- cachedLastCommit = {
232
- timeAgo: raw.slice(0, sep).replace(/ ago$/, "").replace(/ /g, ""),
233
- message: raw.slice(sep + 1),
234
- };
235
- }
236
- lastCommitFetchedAt = Date.now();
237
- }
238
- catch {
239
- // Non-fatal — just skip last commit display
240
- }
241
- }
242
- function getLastCommit(basePath) {
243
- // Refresh at most every 15 seconds
244
- if (Date.now() - lastCommitFetchedAt > 15_000) {
245
- refreshLastCommit(basePath);
246
- }
247
- return cachedLastCommit;
248
- }
249
211
  // ─── Footer Factory ───────────────────────────────────────────────────────────
250
212
  /**
251
213
  * Footer factory that renders zero lines — hides the built-in footer entirely.
@@ -257,61 +219,6 @@ export const hideFooter = () => ({
257
219
  invalidate() { },
258
220
  dispose() { },
259
221
  });
260
- const WIDGET_MODES = ["full", "small", "min", "off"];
261
- let widgetMode = "full";
262
- let widgetModeInitialized = false;
263
- /** Load widget mode from preferences (once). */
264
- function ensureWidgetModeLoaded() {
265
- if (widgetModeInitialized)
266
- return;
267
- widgetModeInitialized = true;
268
- try {
269
- const loaded = loadEffectiveGSDPreferences();
270
- const saved = loaded?.preferences?.widget_mode;
271
- if (saved && WIDGET_MODES.includes(saved)) {
272
- widgetMode = saved;
273
- }
274
- }
275
- catch { /* non-fatal — use default */ }
276
- }
277
- /** Persist widget mode to global preferences YAML. */
278
- function persistWidgetMode(mode) {
279
- try {
280
- const prefsPath = getGlobalGSDPreferencesPath();
281
- let content = "";
282
- if (existsSync(prefsPath)) {
283
- content = readFileSync(prefsPath, "utf-8");
284
- }
285
- const line = `widget_mode: ${mode}`;
286
- const re = /^widget_mode:\s*\S+/m;
287
- if (re.test(content)) {
288
- content = content.replace(re, line);
289
- }
290
- else {
291
- content = content.trimEnd() + "\n" + line + "\n";
292
- }
293
- writeFileSync(prefsPath, content, "utf-8");
294
- }
295
- catch { /* non-fatal — mode still set in memory */ }
296
- }
297
- /** Cycle to the next widget mode. Returns the new mode. */
298
- export function cycleWidgetMode() {
299
- ensureWidgetModeLoaded();
300
- const idx = WIDGET_MODES.indexOf(widgetMode);
301
- widgetMode = WIDGET_MODES[(idx + 1) % WIDGET_MODES.length];
302
- persistWidgetMode(widgetMode);
303
- return widgetMode;
304
- }
305
- /** Set widget mode directly. */
306
- export function setWidgetMode(mode) {
307
- widgetMode = mode;
308
- persistWidgetMode(widgetMode);
309
- }
310
- /** Get current widget mode. */
311
- export function getWidgetMode() {
312
- ensureWidgetModeLoaded();
313
- return widgetMode;
314
- }
315
222
  export function updateProgressWidget(ctx, unitType, unitId, state, accessors, tierBadge) {
316
223
  if (!ctx.hasUI)
317
224
  return;
@@ -320,33 +227,21 @@ export function updateProgressWidget(ctx, unitType, unitId, state, accessors, ti
320
227
  const mid = state.activeMilestone;
321
228
  const slice = state.activeSlice;
322
229
  const task = state.activeTask;
323
- const isHook = unitType.startsWith("hook/");
230
+ const next = peekNext(unitType, state);
324
231
  // Cache git branch at widget creation time (not per render)
325
232
  let cachedBranch = null;
326
233
  try {
327
234
  cachedBranch = getCurrentBranch(accessors.getBasePath());
328
235
  }
329
236
  catch { /* not in git repo */ }
330
- // Cache short pwd (last 2 path segments only) + worktree/branch info
331
- let widgetPwd;
332
- {
333
- let fullPwd = process.cwd();
334
- const widgetHome = process.env.HOME || process.env.USERPROFILE;
335
- if (widgetHome && fullPwd.startsWith(widgetHome)) {
336
- fullPwd = `~${fullPwd.slice(widgetHome.length)}`;
337
- }
338
- const parts = fullPwd.split("/");
339
- widgetPwd = parts.length > 2 ? parts.slice(-2).join("/") : fullPwd;
237
+ // Cache pwd with ~ substitution
238
+ let widgetPwd = process.cwd();
239
+ const widgetHome = process.env.HOME || process.env.USERPROFILE;
240
+ if (widgetHome && widgetPwd.startsWith(widgetHome)) {
241
+ widgetPwd = `~${widgetPwd.slice(widgetHome.length)}`;
340
242
  }
341
- const worktreeName = getActiveWorktreeName();
342
- if (worktreeName && cachedBranch) {
343
- widgetPwd = `${widgetPwd} (\u2387 ${cachedBranch})`;
344
- }
345
- else if (cachedBranch) {
243
+ if (cachedBranch)
346
244
  widgetPwd = `${widgetPwd} (${cachedBranch})`;
347
- }
348
- // Pre-fetch last commit for display
349
- refreshLastCommit(accessors.getBasePath());
350
245
  ctx.ui.setWidget("gsd-progress", (tui, theme) => {
351
246
  let pulseBright = true;
352
247
  let cachedLines;
@@ -381,250 +276,132 @@ export function updateProgressWidget(ctx, unitType, unitId, state, accessors, ti
381
276
  : theme.fg("dim", GLYPH.statusPending);
382
277
  const elapsed = formatAutoElapsed(accessors.getAutoStartTime());
383
278
  const modeTag = accessors.isStepMode() ? "NEXT" : "AUTO";
384
- // Health indicator in header
385
- const score = computeProgressScore();
386
- const healthColor = score.level === "green" ? "success"
387
- : score.level === "yellow" ? "warning"
388
- : "error";
389
- const healthIcon = score.level === "green" ? GLYPH.statusActive
390
- : score.level === "yellow" ? "!"
391
- : "x";
392
- const healthStr = ` ${theme.fg(healthColor, healthIcon)} ${theme.fg(healthColor, score.summary)}`;
393
- const headerLeft = `${pad}${dot} ${theme.fg("accent", theme.bold("GSD"))} ${theme.fg("success", modeTag)}${healthStr}`;
394
- // ETA in header right, after elapsed
395
- const eta = estimateTimeRemaining();
396
- const etaShort = eta ? eta.replace(" remaining", " left") : null;
397
- const headerRight = elapsed
398
- ? (etaShort
399
- ? `${theme.fg("dim", elapsed)} ${theme.fg("dim", "·")} ${theme.fg("dim", etaShort)}`
400
- : theme.fg("dim", elapsed))
401
- : "";
279
+ const headerLeft = `${pad}${dot} ${theme.fg("accent", theme.bold("GSD"))} ${theme.fg("success", modeTag)}`;
280
+ const headerRight = elapsed ? theme.fg("dim", elapsed) : "";
402
281
  lines.push(rightAlign(headerLeft, headerRight, width));
403
- // ── Gather stats (needed by multiple modes) ─────────────────────
404
- const cmdCtx = accessors.getCmdCtx();
405
- let totalInput = 0;
406
- let totalCacheRead = 0;
407
- if (cmdCtx) {
408
- for (const entry of cmdCtx.sessionManager.getEntries()) {
409
- if (entry.type === "message") {
410
- const msgEntry = entry;
411
- if (msgEntry.message?.role === "assistant") {
412
- const u = msgEntry.message.usage;
413
- if (u) {
414
- totalInput += u.input || 0;
415
- totalCacheRead += u.cacheRead || 0;
416
- }
417
- }
418
- }
419
- }
420
- }
421
- const mLedger = getLedger();
422
- const autoTotals = mLedger ? getProjectTotals(mLedger.units) : null;
423
- const cumulativeCost = autoTotals?.cost ?? 0;
424
- const cxUsage = cmdCtx?.getContextUsage?.();
425
- const cxWindow = cxUsage?.contextWindow ?? cmdCtx?.model?.contextWindow ?? 0;
426
- const cxPctVal = cxUsage?.percent ?? 0;
427
- const cxPct = cxUsage?.percent !== null ? cxPctVal.toFixed(1) : "?";
428
- // Model display — shown in context section, not stats
429
- const modelId = cmdCtx?.model?.id ?? "";
430
- const modelProvider = cmdCtx?.model?.provider ?? "";
431
- const modelDisplay = modelProvider && modelId
432
- ? `${modelProvider}/${modelId}`
433
- : modelId;
434
- // ── Mode: off — return empty ──────────────────────────────────
435
- if (widgetMode === "off") {
436
- cachedLines = [];
437
- cachedWidth = width;
438
- return [];
439
- }
440
- // ── Mode: min — header line only ──────────────────────────────
441
- if (widgetMode === "min") {
442
- lines.push(...ui.bar());
443
- cachedLines = lines;
444
- cachedWidth = width;
445
- return lines;
446
- }
447
- // ── Mode: small — header + progress bar + compact stats ───────
448
- if (widgetMode === "small") {
449
- lines.push("");
450
- // Action line
451
- const target = task ? `${task.id}: ${task.title}` : unitId;
452
- const actionLeft = `${pad}${theme.fg("accent", "▸")} ${theme.fg("accent", verb)} ${theme.fg("text", target)}`;
453
- lines.push(rightAlign(actionLeft, theme.fg("dim", phaseLabel), width));
454
- // Progress bar
455
- const roadmapSlices = mid ? getRoadmapSlicesSync() : null;
456
- if (roadmapSlices) {
457
- const { done, total, activeSliceTasks } = roadmapSlices;
458
- const barWidth = Math.max(6, Math.min(18, Math.floor(width * 0.25)));
459
- const pct = total > 0 ? done / total : 0;
460
- const filled = Math.round(pct * barWidth);
461
- const bar = theme.fg("success", "━".repeat(filled))
462
- + theme.fg("dim", "─".repeat(barWidth - filled));
463
- let meta = `${theme.fg("text", `${done}`)}${theme.fg("dim", `/${total} slices`)}`;
464
- if (activeSliceTasks && activeSliceTasks.total > 0) {
465
- const tn = Math.min(activeSliceTasks.done + 1, activeSliceTasks.total);
466
- meta += `${theme.fg("dim", " · task ")}${theme.fg("accent", `${tn}`)}${theme.fg("dim", `/${activeSliceTasks.total}`)}`;
467
- }
468
- lines.push(`${pad}${bar} ${meta}`);
469
- }
470
- // Compact stats: cost + context only
471
- const smallStats = [];
472
- if (cumulativeCost)
473
- smallStats.push(theme.fg("warning", `$${cumulativeCost.toFixed(2)}`));
474
- const cxDisplay = `${cxPct}%ctx`;
475
- if (cxPctVal > 90)
476
- smallStats.push(theme.fg("error", cxDisplay));
477
- else if (cxPctVal > 70)
478
- smallStats.push(theme.fg("warning", cxDisplay));
479
- else
480
- smallStats.push(theme.fg("dim", cxDisplay));
481
- if (smallStats.length > 0) {
482
- lines.push(rightAlign("", smallStats.join(theme.fg("dim", " ")), width));
483
- }
484
- lines.push(...ui.bar());
485
- cachedLines = lines;
486
- cachedWidth = width;
487
- return lines;
488
- }
489
- // ── Mode: full — complete two-column layout ───────────────────
490
282
  lines.push("");
491
- // Context section: milestone + slice + model
492
- const hasContext = !!(mid || (slice && unitType !== "research-milestone" && unitType !== "plan-milestone"));
493
283
  if (mid) {
494
- const modelTag = modelDisplay ? theme.fg("muted", ` ${modelDisplay}`) : "";
495
- lines.push(truncateToWidth(`${pad}${theme.fg("dim", mid.title)}${modelTag}`, width));
284
+ lines.push(truncateToWidth(`${pad}${theme.fg("dim", mid.title)}`, width));
496
285
  }
497
286
  if (slice && unitType !== "research-milestone" && unitType !== "plan-milestone") {
498
287
  lines.push(truncateToWidth(`${pad}${theme.fg("text", theme.bold(`${slice.id}: ${slice.title}`))}`, width));
499
288
  }
500
- if (hasContext)
501
- lines.push("");
289
+ lines.push("");
502
290
  const target = task ? `${task.id}: ${task.title}` : unitId;
503
291
  const actionLeft = `${pad}${theme.fg("accent", "▸")} ${theme.fg("accent", verb)} ${theme.fg("text", target)}`;
504
292
  const tierTag = tierBadge ? theme.fg("dim", `[${tierBadge}] `) : "";
505
293
  const phaseBadge = `${tierTag}${theme.fg("dim", phaseLabel)}`;
506
294
  lines.push(rightAlign(actionLeft, phaseBadge, width));
507
295
  lines.push("");
508
- // Two-column body
509
- const minTwoColWidth = 76;
510
- const roadmapSlices = mid ? getRoadmapSlicesSync() : null;
511
- const taskDetailsCol = roadmapSlices?.taskDetails ?? null;
512
- const useTwoCol = width >= minTwoColWidth && taskDetailsCol !== null && taskDetailsCol.length > 0;
513
- const leftColWidth = useTwoCol
514
- ? Math.floor(width * (width >= 100 ? 0.45 : 0.50))
515
- : width;
516
- const leftLines = [];
517
- if (roadmapSlices) {
518
- const { done, total, activeSliceTasks } = roadmapSlices;
519
- const barWidth = Math.max(6, Math.min(18, Math.floor(leftColWidth * 0.4)));
520
- const pct = total > 0 ? done / total : 0;
521
- const filled = Math.round(pct * barWidth);
522
- const bar = theme.fg("success", "━".repeat(filled))
523
- + theme.fg("dim", "─".repeat(barWidth - filled));
524
- let meta = `${theme.fg("text", `${done}`)}${theme.fg("dim", `/${total} slices`)}`;
525
- if (activeSliceTasks && activeSliceTasks.total > 0) {
526
- const taskNum = isHook
527
- ? Math.max(activeSliceTasks.done, 1)
528
- : Math.min(activeSliceTasks.done + 1, activeSliceTasks.total);
529
- meta += `${theme.fg("dim", " · task ")}${theme.fg("accent", `${taskNum}`)}${theme.fg("dim", `/${activeSliceTasks.total}`)}`;
530
- }
531
- leftLines.push(`${pad}${bar} ${meta}`);
532
- }
533
- // Build right column: task checklist
534
- const rightLines = [];
535
- const maxVisibleTasks = 8;
536
- function formatTaskLine(t, isCurrent) {
537
- const glyph = t.done
538
- ? theme.fg("success", "*")
539
- : isCurrent
540
- ? theme.fg("accent", ">")
541
- : theme.fg("dim", ".");
542
- const id = isCurrent
543
- ? theme.fg("accent", t.id)
544
- : t.done
545
- ? theme.fg("muted", t.id)
546
- : theme.fg("dim", t.id);
547
- const title = isCurrent
548
- ? theme.fg("text", t.title)
549
- : t.done
550
- ? theme.fg("muted", t.title)
551
- : theme.fg("text", t.title);
552
- return `${glyph} ${id}: ${title}`;
553
- }
554
- if (useTwoCol && taskDetailsCol) {
555
- for (const t of taskDetailsCol.slice(0, maxVisibleTasks)) {
556
- rightLines.push(formatTaskLine(t, !!(task && t.id === task.id)));
557
- }
558
- if (taskDetailsCol.length > maxVisibleTasks) {
559
- rightLines.push(theme.fg("dim", ` +${taskDetailsCol.length - maxVisibleTasks} more`));
560
- }
561
- }
562
- else if (!useTwoCol && taskDetailsCol && taskDetailsCol.length > 0) {
563
- for (const t of taskDetailsCol.slice(0, maxVisibleTasks)) {
564
- leftLines.push(`${pad}${formatTaskLine(t, !!(task && t.id === task.id))}`);
565
- }
566
- }
567
- // Compose columns
568
- if (useTwoCol) {
569
- const maxRows = Math.max(leftLines.length, rightLines.length);
570
- if (maxRows > 0) {
571
- lines.push("");
572
- for (let i = 0; i < maxRows; i++) {
573
- const left = padToWidth(truncateToWidth(leftLines[i] ?? "", leftColWidth), leftColWidth);
574
- const right = rightLines[i] ?? "";
575
- lines.push(`${left}${right}`);
296
+ if (mid) {
297
+ const roadmapSlices = getRoadmapSlicesSync();
298
+ if (roadmapSlices) {
299
+ const { done, total, activeSliceTasks } = roadmapSlices;
300
+ const barWidth = Math.max(8, Math.min(24, Math.floor(width * 0.3)));
301
+ const pct = total > 0 ? done / total : 0;
302
+ const filled = Math.round(pct * barWidth);
303
+ const bar = theme.fg("success", "█".repeat(filled))
304
+ + theme.fg("dim", "░".repeat(barWidth - filled));
305
+ let meta = theme.fg("dim", `${done}/${total} slices`);
306
+ if (activeSliceTasks && activeSliceTasks.total > 0) {
307
+ const taskNum = Math.min(activeSliceTasks.done + 1, activeSliceTasks.total);
308
+ meta += theme.fg("dim", ` · task ${taskNum}/${activeSliceTasks.total}`);
576
309
  }
310
+ // ETA estimate
311
+ const eta = estimateTimeRemaining();
312
+ if (eta) {
313
+ meta += theme.fg("dim", ` · ${eta}`);
314
+ }
315
+ lines.push(truncateToWidth(`${pad}${bar} ${meta}`, width));
577
316
  }
578
317
  }
579
- else {
580
- if (leftLines.length > 0) {
581
- lines.push("");
582
- for (const l of leftLines)
583
- lines.push(truncateToWidth(l, width));
584
- }
318
+ lines.push("");
319
+ if (next) {
320
+ lines.push(truncateToWidth(`${pad}${theme.fg("dim", "→")} ${theme.fg("dim", `then ${next}`)}`, width));
585
321
  }
586
- // ── Footer: simplified stats + pwd + last commit + hints ────────
322
+ // ── Footer info (pwd, tokens, cost, context, model) ──────────────
587
323
  lines.push("");
324
+ lines.push(truncateToWidth(theme.fg("dim", `${pad}${widgetPwd}`), width, theme.fg("dim", "…")));
325
+ // Token stats from current unit session + cumulative cost from metrics
588
326
  {
327
+ const cmdCtx = accessors.getCmdCtx();
328
+ let totalInput = 0, totalOutput = 0;
329
+ let totalCacheRead = 0, totalCacheWrite = 0;
330
+ if (cmdCtx) {
331
+ for (const entry of cmdCtx.sessionManager.getEntries()) {
332
+ if (entry.type === "message") {
333
+ const msgEntry = entry;
334
+ if (msgEntry.message?.role === "assistant") {
335
+ const u = msgEntry.message.usage;
336
+ if (u) {
337
+ totalInput += u.input || 0;
338
+ totalOutput += u.output || 0;
339
+ totalCacheRead += u.cacheRead || 0;
340
+ totalCacheWrite += u.cacheWrite || 0;
341
+ }
342
+ }
343
+ }
344
+ }
345
+ }
346
+ const mLedger = getLedger();
347
+ const autoTotals = mLedger ? getProjectTotals(mLedger.units) : null;
348
+ const cumulativeCost = autoTotals?.cost ?? 0;
349
+ const cxUsage = cmdCtx?.getContextUsage?.();
350
+ const cxWindow = cxUsage?.contextWindow ?? cmdCtx?.model?.contextWindow ?? 0;
351
+ const cxPctVal = cxUsage?.percent ?? 0;
352
+ const cxPct = cxUsage?.percent !== null ? cxPctVal.toFixed(1) : "?";
589
353
  const sp = [];
354
+ if (totalInput)
355
+ sp.push(`↑${formatWidgetTokens(totalInput)}`);
356
+ if (totalOutput)
357
+ sp.push(`↓${formatWidgetTokens(totalOutput)}`);
358
+ if (totalCacheRead)
359
+ sp.push(`R${formatWidgetTokens(totalCacheRead)}`);
360
+ if (totalCacheWrite)
361
+ sp.push(`W${formatWidgetTokens(totalCacheWrite)}`);
362
+ // Cache hit rate for current unit
590
363
  if (totalCacheRead + totalInput > 0) {
591
364
  const hitRate = Math.round((totalCacheRead / (totalCacheRead + totalInput)) * 100);
592
- const hitColor = hitRate >= 70 ? "success" : hitRate >= 40 ? "warning" : "error";
593
- sp.push(theme.fg(hitColor, `${hitRate}%hit`));
365
+ sp.push(`\u26A1${hitRate}%`);
594
366
  }
595
367
  if (cumulativeCost)
596
- sp.push(theme.fg("warning", `$${cumulativeCost.toFixed(2)}`));
597
- const cxDisplay = `${cxPct}%/${formatWidgetTokens(cxWindow)}`;
598
- if (cxPctVal > 90)
368
+ sp.push(`$${cumulativeCost.toFixed(3)}`);
369
+ const cxDisplay = cxPct === "?"
370
+ ? `?/${formatWidgetTokens(cxWindow)}`
371
+ : `${cxPct}%/${formatWidgetTokens(cxWindow)}`;
372
+ if (cxPctVal > 90) {
599
373
  sp.push(theme.fg("error", cxDisplay));
600
- else if (cxPctVal > 70)
374
+ }
375
+ else if (cxPctVal > 70) {
601
376
  sp.push(theme.fg("warning", cxDisplay));
602
- else
377
+ }
378
+ else {
603
379
  sp.push(cxDisplay);
604
- const statsLine = sp.map(p => p.includes("\x1b[") ? p : theme.fg("dim", p))
605
- .join(theme.fg("dim", " "));
606
- if (statsLine) {
607
- lines.push(rightAlign("", statsLine, width));
380
+ }
381
+ const sLeft = sp.map(p => p.includes("\x1b[") ? p : theme.fg("dim", p))
382
+ .join(theme.fg("dim", " "));
383
+ const modelId = cmdCtx?.model?.id ?? "";
384
+ const modelProvider = cmdCtx?.model?.provider ?? "";
385
+ const modelPhase = phaseLabel ? theme.fg("dim", `[${phaseLabel}] `) : "";
386
+ const modelDisplay = modelProvider && modelId
387
+ ? `${modelProvider}/${modelId}`
388
+ : modelId;
389
+ const sRight = modelDisplay
390
+ ? `${modelPhase}${theme.fg("dim", modelDisplay)}`
391
+ : "";
392
+ lines.push(rightAlign(`${pad}${sLeft}`, sRight, width));
393
+ // Dynamic routing savings summary
394
+ if (mLedger && mLedger.units.some(u => u.tier)) {
395
+ const savings = formatTierSavings(mLedger.units);
396
+ if (savings) {
397
+ lines.push(truncateToWidth(theme.fg("dim", `${pad}${savings}`), width));
398
+ }
608
399
  }
609
400
  }
610
- // PWD line with last commit info right-aligned
611
- const lastCommit = getLastCommit(accessors.getBasePath());
612
- const commitStr = lastCommit
613
- ? theme.fg("dim", `${lastCommit.timeAgo} ago: ${lastCommit.message}`)
614
- : "";
615
- const pwdStr = theme.fg("dim", widgetPwd);
616
- if (commitStr) {
617
- lines.push(rightAlign(`${pad}${pwdStr}`, truncateToWidth(commitStr, Math.floor(width * 0.45)), width));
618
- }
619
- else {
620
- lines.push(`${pad}${pwdStr}`);
621
- }
622
- // Hints line
623
401
  const hintParts = [];
624
402
  hintParts.push("esc pause");
625
403
  hintParts.push(process.platform === "darwin" ? "⌃⌥G dashboard" : "Ctrl+Alt+G dashboard");
626
- const hintStr = theme.fg("dim", hintParts.join(" | "));
627
- lines.push(rightAlign("", hintStr, width));
404
+ lines.push(...ui.hints(hintParts));
628
405
  lines.push(...ui.bar());
629
406
  cachedLines = lines;
630
407
  cachedWidth = width;
@@ -650,10 +427,3 @@ function rightAlign(left, right, width) {
650
427
  const gap = Math.max(1, width - leftVis - rightVis);
651
428
  return truncateToWidth(left + " ".repeat(gap) + right, width);
652
429
  }
653
- /** Pad a string with trailing spaces to fill exactly `colWidth` (ANSI-aware). */
654
- function padToWidth(s, colWidth) {
655
- const vis = visibleWidth(s);
656
- if (vis >= colWidth)
657
- return truncateToWidth(s, colWidth);
658
- return s + " ".repeat(colWidth - vis);
659
- }
@@ -258,7 +258,6 @@ export async function autoLoop(ctx, pi, s, deps) {
258
258
  }
259
259
  // Derive state
260
260
  let state = await deps.deriveState(s.basePath);
261
- deps.syncCmuxSidebar(deps.loadEffectiveGSDPreferences()?.preferences, state);
262
261
  let mid = state.activeMilestone?.id;
263
262
  let midTitle = state.activeMilestone?.title;
264
263
  debugLog("autoLoop", {
@@ -271,7 +270,6 @@ export async function autoLoop(ctx, pi, s, deps) {
271
270
  if (mid && s.currentMilestoneId && mid !== s.currentMilestoneId) {
272
271
  ctx.ui.notify(`Milestone ${s.currentMilestoneId} complete. Advancing to ${mid}: ${midTitle}.`, "info");
273
272
  deps.sendDesktopNotification("GSD", `Milestone ${s.currentMilestoneId} complete!`, "success", "milestone");
274
- deps.logCmuxEvent(deps.loadEffectiveGSDPreferences()?.preferences, `Milestone ${s.currentMilestoneId} complete. Advancing to ${mid}.`, "success");
275
273
  const vizPrefs = deps.loadEffectiveGSDPreferences()?.preferences;
276
274
  if (vizPrefs?.auto_visualize) {
277
275
  ctx.ui.notify("Run /gsd visualize to see progress overview.", "info");
@@ -365,7 +363,6 @@ export async function autoLoop(ctx, pi, s, deps) {
365
363
  deps.resolver.mergeAndExit(s.currentMilestoneId, ctx.ui);
366
364
  }
367
365
  deps.sendDesktopNotification("GSD", "All milestones complete!", "success", "milestone");
368
- deps.logCmuxEvent(deps.loadEffectiveGSDPreferences()?.preferences, "All milestones complete.", "success");
369
366
  await deps.stopAuto(ctx, pi, "All milestones complete");
370
367
  }
371
368
  else if (state.phase === "blocked") {
@@ -373,7 +370,6 @@ export async function autoLoop(ctx, pi, s, deps) {
373
370
  await deps.stopAuto(ctx, pi, blockerMsg);
374
371
  ctx.ui.notify(`${blockerMsg}. Fix and run /gsd auto.`, "warning");
375
372
  deps.sendDesktopNotification("GSD", blockerMsg, "error", "attention");
376
- deps.logCmuxEvent(deps.loadEffectiveGSDPreferences()?.preferences, blockerMsg, "error");
377
373
  }
378
374
  else {
379
375
  const ids = incomplete.map((m) => m.id).join(", ");
@@ -419,7 +415,6 @@ export async function autoLoop(ctx, pi, s, deps) {
419
415
  deps.resolver.mergeAndExit(s.currentMilestoneId, ctx.ui);
420
416
  }
421
417
  deps.sendDesktopNotification("GSD", `Milestone ${mid} complete!`, "success", "milestone");
422
- deps.logCmuxEvent(deps.loadEffectiveGSDPreferences()?.preferences, `Milestone ${mid} complete.`, "success");
423
418
  await deps.stopAuto(ctx, pi, `Milestone ${mid} complete`);
424
419
  debugLog("autoLoop", { phase: "exit", reason: "milestone-complete" });
425
420
  break;
@@ -433,7 +428,6 @@ export async function autoLoop(ctx, pi, s, deps) {
433
428
  await deps.stopAuto(ctx, pi, blockerMsg);
434
429
  ctx.ui.notify(`${blockerMsg}. Fix and run /gsd auto.`, "warning");
435
430
  deps.sendDesktopNotification("GSD", blockerMsg, "error", "attention");
436
- deps.logCmuxEvent(deps.loadEffectiveGSDPreferences()?.preferences, blockerMsg, "error");
437
431
  debugLog("autoLoop", { phase: "exit", reason: "blocked" });
438
432
  break;
439
433
  }
@@ -464,35 +458,30 @@ export async function autoLoop(ctx, pi, s, deps) {
464
458
  if (budgetEnforcementAction === "pause") {
465
459
  ctx.ui.notify(`${msg} Pausing auto-mode — /gsd auto to override and continue.`, "warning");
466
460
  deps.sendDesktopNotification("GSD", msg, "warning", "budget");
467
- deps.logCmuxEvent(prefs, msg, "warning");
468
461
  await deps.pauseAuto(ctx, pi);
469
462
  debugLog("autoLoop", { phase: "exit", reason: "budget-pause" });
470
463
  break;
471
464
  }
472
465
  ctx.ui.notify(`${msg} Continuing (enforcement: warn).`, "warning");
473
466
  deps.sendDesktopNotification("GSD", msg, "warning", "budget");
474
- deps.logCmuxEvent(prefs, msg, "warning");
475
467
  }
476
468
  else if (newBudgetAlertLevel === 90) {
477
469
  s.lastBudgetAlertLevel =
478
470
  newBudgetAlertLevel;
479
471
  ctx.ui.notify(`Budget 90%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`, "warning");
480
472
  deps.sendDesktopNotification("GSD", `Budget 90%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`, "warning", "budget");
481
- deps.logCmuxEvent(prefs, `Budget 90%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`, "warning");
482
473
  }
483
474
  else if (newBudgetAlertLevel === 80) {
484
475
  s.lastBudgetAlertLevel =
485
476
  newBudgetAlertLevel;
486
477
  ctx.ui.notify(`Approaching budget ceiling — 80%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`, "warning");
487
478
  deps.sendDesktopNotification("GSD", `Approaching budget ceiling — 80%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`, "warning", "budget");
488
- deps.logCmuxEvent(prefs, `Budget 80%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`, "warning");
489
479
  }
490
480
  else if (newBudgetAlertLevel === 75) {
491
481
  s.lastBudgetAlertLevel =
492
482
  newBudgetAlertLevel;
493
483
  ctx.ui.notify(`Budget 75%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`, "info");
494
484
  deps.sendDesktopNotification("GSD", `Budget 75%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`, "info", "budget");
495
- deps.logCmuxEvent(prefs, `Budget 75%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`, "progress");
496
485
  }
497
486
  else if (budgetAlertLevel === 0) {
498
487
  s.lastBudgetAlertLevel = 0;