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