gsd-pi 2.36.0 → 2.37.0-dev.b5e7ebc

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 (71) hide show
  1. package/dist/resources/extensions/cmux/index.js +321 -0
  2. package/dist/resources/extensions/cmux/package.json +7 -0
  3. package/dist/resources/extensions/gsd/auto-dashboard.js +334 -104
  4. package/dist/resources/extensions/gsd/auto-loop.js +29 -4
  5. package/dist/resources/extensions/gsd/auto.js +35 -5
  6. package/dist/resources/extensions/gsd/commands-cmux.js +120 -0
  7. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +1 -1
  8. package/dist/resources/extensions/gsd/commands.js +51 -1
  9. package/dist/resources/extensions/gsd/docs/preferences-reference.md +25 -0
  10. package/dist/resources/extensions/gsd/git-service.js +9 -1
  11. package/dist/resources/extensions/gsd/history.js +2 -1
  12. package/dist/resources/extensions/gsd/index.js +5 -0
  13. package/dist/resources/extensions/gsd/metrics.js +4 -2
  14. package/dist/resources/extensions/gsd/notifications.js +10 -1
  15. package/dist/resources/extensions/gsd/preferences-types.js +2 -0
  16. package/dist/resources/extensions/gsd/preferences-validation.js +29 -0
  17. package/dist/resources/extensions/gsd/preferences.js +3 -0
  18. package/dist/resources/extensions/gsd/prompts/research-milestone.md +4 -3
  19. package/dist/resources/extensions/gsd/prompts/research-slice.md +3 -2
  20. package/dist/resources/extensions/gsd/session-lock.js +26 -6
  21. package/dist/resources/extensions/gsd/templates/preferences.md +6 -0
  22. package/dist/resources/extensions/search-the-web/native-search.js +45 -4
  23. package/dist/resources/extensions/shared/format-utils.js +5 -41
  24. package/dist/resources/extensions/shared/layout-utils.js +46 -0
  25. package/dist/resources/extensions/shared/mod.js +2 -1
  26. package/dist/resources/extensions/shared/terminal.js +5 -0
  27. package/dist/resources/extensions/subagent/index.js +180 -60
  28. package/package.json +1 -1
  29. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
  30. package/packages/pi-coding-agent/dist/core/extensions/loader.js +8 -4
  31. package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  32. package/packages/pi-coding-agent/package.json +1 -1
  33. package/packages/pi-coding-agent/src/core/extensions/loader.ts +8 -4
  34. package/packages/pi-tui/dist/terminal-image.d.ts.map +1 -1
  35. package/packages/pi-tui/dist/terminal-image.js +4 -0
  36. package/packages/pi-tui/dist/terminal-image.js.map +1 -1
  37. package/packages/pi-tui/src/terminal-image.ts +5 -0
  38. package/pkg/package.json +1 -1
  39. package/src/resources/extensions/cmux/index.ts +384 -0
  40. package/src/resources/extensions/cmux/package.json +7 -0
  41. package/src/resources/extensions/gsd/auto-dashboard.ts +363 -116
  42. package/src/resources/extensions/gsd/auto-loop.ts +66 -6
  43. package/src/resources/extensions/gsd/auto.ts +45 -5
  44. package/src/resources/extensions/gsd/commands-cmux.ts +143 -0
  45. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +1 -1
  46. package/src/resources/extensions/gsd/commands.ts +54 -1
  47. package/src/resources/extensions/gsd/docs/preferences-reference.md +25 -0
  48. package/src/resources/extensions/gsd/git-service.ts +12 -1
  49. package/src/resources/extensions/gsd/history.ts +2 -1
  50. package/src/resources/extensions/gsd/index.ts +8 -0
  51. package/src/resources/extensions/gsd/metrics.ts +4 -2
  52. package/src/resources/extensions/gsd/notifications.ts +10 -1
  53. package/src/resources/extensions/gsd/preferences-types.ts +13 -0
  54. package/src/resources/extensions/gsd/preferences-validation.ts +26 -0
  55. package/src/resources/extensions/gsd/preferences.ts +4 -0
  56. package/src/resources/extensions/gsd/prompts/research-milestone.md +4 -3
  57. package/src/resources/extensions/gsd/prompts/research-slice.md +3 -2
  58. package/src/resources/extensions/gsd/session-lock.ts +41 -6
  59. package/src/resources/extensions/gsd/templates/preferences.md +6 -0
  60. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +39 -1
  61. package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +19 -0
  62. package/src/resources/extensions/gsd/tests/cmux.test.ts +122 -0
  63. package/src/resources/extensions/gsd/tests/preferences.test.ts +23 -0
  64. package/src/resources/extensions/gsd/tests/session-lock-regression.test.ts +45 -0
  65. package/src/resources/extensions/search-the-web/native-search.ts +50 -4
  66. package/src/resources/extensions/shared/format-utils.ts +5 -44
  67. package/src/resources/extensions/shared/layout-utils.ts +49 -0
  68. package/src/resources/extensions/shared/mod.ts +7 -4
  69. package/src/resources/extensions/shared/terminal.ts +5 -0
  70. package/src/resources/extensions/shared/tests/format-utils.test.ts +5 -3
  71. package/src/resources/extensions/subagent/index.ts +236 -79
@@ -7,12 +7,16 @@
7
7
  */
8
8
  import { getCurrentBranch } from "./worktree.js";
9
9
  import { getActiveHook } from "./post-unit-hooks.js";
10
- import { getLedger, getProjectTotals, formatTierSavings } from "./metrics.js";
10
+ import { getLedger, getProjectTotals } from "./metrics.js";
11
11
  import { resolveMilestoneFile, resolveSliceFile, } from "./paths.js";
12
12
  import { parseRoadmap, parsePlan } from "./files.js";
13
- import { readFileSync, existsSync } from "node:fs";
13
+ import { readFileSync, writeFileSync, existsSync } from "node:fs";
14
+ import { execFileSync } from "node:child_process";
14
15
  import { truncateToWidth, visibleWidth } from "@gsd/pi-tui";
15
16
  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";
16
20
  // ─── Unit Description Helpers ─────────────────────────────────────────────────
17
21
  export function unitVerb(unitType) {
18
22
  if (unitType.startsWith("hook/"))
@@ -164,7 +168,6 @@ export function estimateTimeRemaining() {
164
168
  const rm = m % 60;
165
169
  return rm > 0 ? `~${h}h ${rm}m remaining` : `~${h}h remaining`;
166
170
  }
167
- // ─── Slice Progress Cache ─────────────────────────────────────────────────────
168
171
  /** Cached slice progress for the widget — avoid async in render */
169
172
  let cachedSliceProgress = null;
170
173
  export function updateSliceProgressCache(base, mid, activeSid) {
@@ -175,6 +178,7 @@ export function updateSliceProgressCache(base, mid, activeSid) {
175
178
  const content = readFileSync(roadmapFile, "utf-8");
176
179
  const roadmap = parseRoadmap(content);
177
180
  let activeSliceTasks = null;
181
+ let taskDetails = null;
178
182
  if (activeSid) {
179
183
  try {
180
184
  const planFile = resolveSliceFile(base, mid, activeSid, "PLAN");
@@ -185,6 +189,7 @@ export function updateSliceProgressCache(base, mid, activeSid) {
185
189
  done: plan.tasks.filter(t => t.done).length,
186
190
  total: plan.tasks.length,
187
191
  };
192
+ taskDetails = plan.tasks.map(t => ({ id: t.id, title: t.title, done: t.done }));
188
193
  }
189
194
  }
190
195
  catch {
@@ -196,6 +201,7 @@ export function updateSliceProgressCache(base, mid, activeSid) {
196
201
  total: roadmap.slices.length,
197
202
  milestoneId: mid,
198
203
  activeSliceTasks,
204
+ taskDetails,
199
205
  };
200
206
  }
201
207
  catch {
@@ -208,6 +214,38 @@ export function getRoadmapSlicesSync() {
208
214
  export function clearSliceProgressCache() {
209
215
  cachedSliceProgress = null;
210
216
  }
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
+ }
211
249
  // ─── Footer Factory ───────────────────────────────────────────────────────────
212
250
  /**
213
251
  * Footer factory that renders zero lines — hides the built-in footer entirely.
@@ -219,6 +257,61 @@ export const hideFooter = () => ({
219
257
  invalidate() { },
220
258
  dispose() { },
221
259
  });
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
+ }
222
315
  export function updateProgressWidget(ctx, unitType, unitId, state, accessors, tierBadge) {
223
316
  if (!ctx.hasUI)
224
317
  return;
@@ -227,21 +320,33 @@ export function updateProgressWidget(ctx, unitType, unitId, state, accessors, ti
227
320
  const mid = state.activeMilestone;
228
321
  const slice = state.activeSlice;
229
322
  const task = state.activeTask;
230
- const next = peekNext(unitType, state);
323
+ const isHook = unitType.startsWith("hook/");
231
324
  // Cache git branch at widget creation time (not per render)
232
325
  let cachedBranch = null;
233
326
  try {
234
327
  cachedBranch = getCurrentBranch(accessors.getBasePath());
235
328
  }
236
329
  catch { /* not in git repo */ }
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)}`;
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;
242
340
  }
243
- if (cachedBranch)
341
+ const worktreeName = getActiveWorktreeName();
342
+ if (worktreeName && cachedBranch) {
343
+ widgetPwd = `${widgetPwd} (\u2387 ${cachedBranch})`;
344
+ }
345
+ else if (cachedBranch) {
244
346
  widgetPwd = `${widgetPwd} (${cachedBranch})`;
347
+ }
348
+ // Pre-fetch last commit for display
349
+ refreshLastCommit(accessors.getBasePath());
245
350
  ctx.ui.setWidget("gsd-progress", (tui, theme) => {
246
351
  let pulseBright = true;
247
352
  let cachedLines;
@@ -276,132 +381,250 @@ export function updateProgressWidget(ctx, unitType, unitId, state, accessors, ti
276
381
  : theme.fg("dim", GLYPH.statusPending);
277
382
  const elapsed = formatAutoElapsed(accessors.getAutoStartTime());
278
383
  const modeTag = accessors.isStepMode() ? "NEXT" : "AUTO";
279
- const headerLeft = `${pad}${dot} ${theme.fg("accent", theme.bold("GSD"))} ${theme.fg("success", modeTag)}`;
280
- const headerRight = elapsed ? theme.fg("dim", elapsed) : "";
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
+ : "";
281
402
  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 ───────────────────
282
490
  lines.push("");
491
+ // Context section: milestone + slice + model
492
+ const hasContext = !!(mid || (slice && unitType !== "research-milestone" && unitType !== "plan-milestone"));
283
493
  if (mid) {
284
- lines.push(truncateToWidth(`${pad}${theme.fg("dim", mid.title)}`, width));
494
+ const modelTag = modelDisplay ? theme.fg("muted", ` ${modelDisplay}`) : "";
495
+ lines.push(truncateToWidth(`${pad}${theme.fg("dim", mid.title)}${modelTag}`, width));
285
496
  }
286
497
  if (slice && unitType !== "research-milestone" && unitType !== "plan-milestone") {
287
498
  lines.push(truncateToWidth(`${pad}${theme.fg("text", theme.bold(`${slice.id}: ${slice.title}`))}`, width));
288
499
  }
289
- lines.push("");
500
+ if (hasContext)
501
+ lines.push("");
290
502
  const target = task ? `${task.id}: ${task.title}` : unitId;
291
503
  const actionLeft = `${pad}${theme.fg("accent", "▸")} ${theme.fg("accent", verb)} ${theme.fg("text", target)}`;
292
504
  const tierTag = tierBadge ? theme.fg("dim", `[${tierBadge}] `) : "";
293
505
  const phaseBadge = `${tierTag}${theme.fg("dim", phaseLabel)}`;
294
506
  lines.push(rightAlign(actionLeft, phaseBadge, width));
295
507
  lines.push("");
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}`);
309
- }
310
- // ETA estimate
311
- const eta = estimateTimeRemaining();
312
- if (eta) {
313
- meta += theme.fg("dim", ` · ${eta}`);
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}`);
314
576
  }
315
- lines.push(truncateToWidth(`${pad}${bar} ${meta}`, width));
316
577
  }
317
578
  }
318
- lines.push("");
319
- if (next) {
320
- lines.push(truncateToWidth(`${pad}${theme.fg("dim", "→")} ${theme.fg("dim", `then ${next}`)}`, width));
579
+ else {
580
+ if (leftLines.length > 0) {
581
+ lines.push("");
582
+ for (const l of leftLines)
583
+ lines.push(truncateToWidth(l, width));
584
+ }
321
585
  }
322
- // ── Footer info (pwd, tokens, cost, context, model) ──────────────
586
+ // ── Footer: simplified stats + pwd + last commit + hints ────────
323
587
  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
326
588
  {
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) : "?";
353
589
  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
363
590
  if (totalCacheRead + totalInput > 0) {
364
591
  const hitRate = Math.round((totalCacheRead / (totalCacheRead + totalInput)) * 100);
365
- sp.push(`\u26A1${hitRate}%`);
592
+ const hitColor = hitRate >= 70 ? "success" : hitRate >= 40 ? "warning" : "error";
593
+ sp.push(theme.fg(hitColor, `${hitRate}%hit`));
366
594
  }
367
595
  if (cumulativeCost)
368
- sp.push(`$${cumulativeCost.toFixed(3)}`);
369
- const cxDisplay = cxPct === "?"
370
- ? `?/${formatWidgetTokens(cxWindow)}`
371
- : `${cxPct}%/${formatWidgetTokens(cxWindow)}`;
372
- if (cxPctVal > 90) {
596
+ sp.push(theme.fg("warning", `$${cumulativeCost.toFixed(2)}`));
597
+ const cxDisplay = `${cxPct}%/${formatWidgetTokens(cxWindow)}`;
598
+ if (cxPctVal > 90)
373
599
  sp.push(theme.fg("error", cxDisplay));
374
- }
375
- else if (cxPctVal > 70) {
600
+ else if (cxPctVal > 70)
376
601
  sp.push(theme.fg("warning", cxDisplay));
377
- }
378
- else {
602
+ else
379
603
  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
608
  }
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
- }
399
- }
400
609
  }
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
401
623
  const hintParts = [];
402
624
  hintParts.push("esc pause");
403
625
  hintParts.push(process.platform === "darwin" ? "⌃⌥G dashboard" : "Ctrl+Alt+G dashboard");
404
- lines.push(...ui.hints(hintParts));
626
+ const hintStr = theme.fg("dim", hintParts.join(" | "));
627
+ lines.push(rightAlign("", hintStr, width));
405
628
  lines.push(...ui.bar());
406
629
  cachedLines = lines;
407
630
  cachedWidth = width;
@@ -427,3 +650,10 @@ function rightAlign(left, right, width) {
427
650
  const gap = Math.max(1, width - leftVis - rightVis);
428
651
  return truncateToWidth(left + " ".repeat(gap) + right, width);
429
652
  }
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
+ }