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