gsd-pi 2.36.0-dev.f887f4e → 2.37.0

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 (48) hide show
  1. package/dist/resources/extensions/cmux/index.js +321 -0
  2. package/dist/resources/extensions/gsd/auto-dashboard.js +334 -104
  3. package/dist/resources/extensions/gsd/auto-loop.js +11 -0
  4. package/dist/resources/extensions/gsd/auto.js +16 -0
  5. package/dist/resources/extensions/gsd/commands-cmux.js +120 -0
  6. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +1 -1
  7. package/dist/resources/extensions/gsd/commands.js +51 -1
  8. package/dist/resources/extensions/gsd/docs/preferences-reference.md +25 -0
  9. package/dist/resources/extensions/gsd/index.js +5 -0
  10. package/dist/resources/extensions/gsd/notifications.js +10 -1
  11. package/dist/resources/extensions/gsd/preferences-types.js +2 -0
  12. package/dist/resources/extensions/gsd/preferences-validation.js +29 -0
  13. package/dist/resources/extensions/gsd/preferences.js +3 -0
  14. package/dist/resources/extensions/gsd/prompts/research-milestone.md +4 -3
  15. package/dist/resources/extensions/gsd/prompts/research-slice.md +3 -2
  16. package/dist/resources/extensions/gsd/templates/preferences.md +6 -0
  17. package/dist/resources/extensions/search-the-web/native-search.js +45 -4
  18. package/dist/resources/extensions/shared/terminal.js +5 -0
  19. package/dist/resources/extensions/subagent/index.js +180 -60
  20. package/package.json +1 -1
  21. package/packages/pi-coding-agent/package.json +1 -1
  22. package/packages/pi-tui/dist/terminal-image.d.ts.map +1 -1
  23. package/packages/pi-tui/dist/terminal-image.js +4 -0
  24. package/packages/pi-tui/dist/terminal-image.js.map +1 -1
  25. package/packages/pi-tui/src/terminal-image.ts +5 -0
  26. package/pkg/package.json +1 -1
  27. package/src/resources/extensions/cmux/index.ts +384 -0
  28. package/src/resources/extensions/gsd/auto-dashboard.ts +363 -116
  29. package/src/resources/extensions/gsd/auto-loop.ts +42 -0
  30. package/src/resources/extensions/gsd/auto.ts +21 -0
  31. package/src/resources/extensions/gsd/commands-cmux.ts +143 -0
  32. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +1 -1
  33. package/src/resources/extensions/gsd/commands.ts +54 -1
  34. package/src/resources/extensions/gsd/docs/preferences-reference.md +25 -0
  35. package/src/resources/extensions/gsd/index.ts +8 -0
  36. package/src/resources/extensions/gsd/notifications.ts +10 -1
  37. package/src/resources/extensions/gsd/preferences-types.ts +13 -0
  38. package/src/resources/extensions/gsd/preferences-validation.ts +26 -0
  39. package/src/resources/extensions/gsd/preferences.ts +4 -0
  40. package/src/resources/extensions/gsd/prompts/research-milestone.md +4 -3
  41. package/src/resources/extensions/gsd/prompts/research-slice.md +3 -2
  42. package/src/resources/extensions/gsd/templates/preferences.md +6 -0
  43. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +2 -0
  44. package/src/resources/extensions/gsd/tests/cmux.test.ts +98 -0
  45. package/src/resources/extensions/gsd/tests/preferences.test.ts +23 -0
  46. package/src/resources/extensions/search-the-web/native-search.ts +50 -4
  47. package/src/resources/extensions/shared/terminal.ts +5 -0
  48. 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
+ }
@@ -258,6 +258,7 @@ 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);
261
262
  let mid = state.activeMilestone?.id;
262
263
  let midTitle = state.activeMilestone?.title;
263
264
  debugLog("autoLoop", {
@@ -270,6 +271,7 @@ export async function autoLoop(ctx, pi, s, deps) {
270
271
  if (mid && s.currentMilestoneId && mid !== s.currentMilestoneId) {
271
272
  ctx.ui.notify(`Milestone ${s.currentMilestoneId} complete. Advancing to ${mid}: ${midTitle}.`, "info");
272
273
  deps.sendDesktopNotification("GSD", `Milestone ${s.currentMilestoneId} complete!`, "success", "milestone");
274
+ deps.logCmuxEvent(deps.loadEffectiveGSDPreferences()?.preferences, `Milestone ${s.currentMilestoneId} complete. Advancing to ${mid}.`, "success");
273
275
  const vizPrefs = deps.loadEffectiveGSDPreferences()?.preferences;
274
276
  if (vizPrefs?.auto_visualize) {
275
277
  ctx.ui.notify("Run /gsd visualize to see progress overview.", "info");
@@ -363,6 +365,7 @@ export async function autoLoop(ctx, pi, s, deps) {
363
365
  deps.resolver.mergeAndExit(s.currentMilestoneId, ctx.ui);
364
366
  }
365
367
  deps.sendDesktopNotification("GSD", "All milestones complete!", "success", "milestone");
368
+ deps.logCmuxEvent(deps.loadEffectiveGSDPreferences()?.preferences, "All milestones complete.", "success");
366
369
  await deps.stopAuto(ctx, pi, "All milestones complete");
367
370
  }
368
371
  else if (state.phase === "blocked") {
@@ -370,6 +373,7 @@ export async function autoLoop(ctx, pi, s, deps) {
370
373
  await deps.stopAuto(ctx, pi, blockerMsg);
371
374
  ctx.ui.notify(`${blockerMsg}. Fix and run /gsd auto.`, "warning");
372
375
  deps.sendDesktopNotification("GSD", blockerMsg, "error", "attention");
376
+ deps.logCmuxEvent(deps.loadEffectiveGSDPreferences()?.preferences, blockerMsg, "error");
373
377
  }
374
378
  else {
375
379
  const ids = incomplete.map((m) => m.id).join(", ");
@@ -415,6 +419,7 @@ export async function autoLoop(ctx, pi, s, deps) {
415
419
  deps.resolver.mergeAndExit(s.currentMilestoneId, ctx.ui);
416
420
  }
417
421
  deps.sendDesktopNotification("GSD", `Milestone ${mid} complete!`, "success", "milestone");
422
+ deps.logCmuxEvent(deps.loadEffectiveGSDPreferences()?.preferences, `Milestone ${mid} complete.`, "success");
418
423
  await deps.stopAuto(ctx, pi, `Milestone ${mid} complete`);
419
424
  debugLog("autoLoop", { phase: "exit", reason: "milestone-complete" });
420
425
  break;
@@ -428,6 +433,7 @@ export async function autoLoop(ctx, pi, s, deps) {
428
433
  await deps.stopAuto(ctx, pi, blockerMsg);
429
434
  ctx.ui.notify(`${blockerMsg}. Fix and run /gsd auto.`, "warning");
430
435
  deps.sendDesktopNotification("GSD", blockerMsg, "error", "attention");
436
+ deps.logCmuxEvent(deps.loadEffectiveGSDPreferences()?.preferences, blockerMsg, "error");
431
437
  debugLog("autoLoop", { phase: "exit", reason: "blocked" });
432
438
  break;
433
439
  }
@@ -458,30 +464,35 @@ export async function autoLoop(ctx, pi, s, deps) {
458
464
  if (budgetEnforcementAction === "pause") {
459
465
  ctx.ui.notify(`${msg} Pausing auto-mode — /gsd auto to override and continue.`, "warning");
460
466
  deps.sendDesktopNotification("GSD", msg, "warning", "budget");
467
+ deps.logCmuxEvent(prefs, msg, "warning");
461
468
  await deps.pauseAuto(ctx, pi);
462
469
  debugLog("autoLoop", { phase: "exit", reason: "budget-pause" });
463
470
  break;
464
471
  }
465
472
  ctx.ui.notify(`${msg} Continuing (enforcement: warn).`, "warning");
466
473
  deps.sendDesktopNotification("GSD", msg, "warning", "budget");
474
+ deps.logCmuxEvent(prefs, msg, "warning");
467
475
  }
468
476
  else if (newBudgetAlertLevel === 90) {
469
477
  s.lastBudgetAlertLevel =
470
478
  newBudgetAlertLevel;
471
479
  ctx.ui.notify(`Budget 90%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`, "warning");
472
480
  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");
473
482
  }
474
483
  else if (newBudgetAlertLevel === 80) {
475
484
  s.lastBudgetAlertLevel =
476
485
  newBudgetAlertLevel;
477
486
  ctx.ui.notify(`Approaching budget ceiling — 80%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`, "warning");
478
487
  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");
479
489
  }
480
490
  else if (newBudgetAlertLevel === 75) {
481
491
  s.lastBudgetAlertLevel =
482
492
  newBudgetAlertLevel;
483
493
  ctx.ui.notify(`Budget 75%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`, "info");
484
494
  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");
485
496
  }
486
497
  else if (budgetAlertLevel === 0) {
487
498
  s.lastBudgetAlertLevel = 0;