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.
- package/dist/resources/extensions/cmux/index.js +321 -0
- package/dist/resources/extensions/gsd/auto-dashboard.js +334 -104
- package/dist/resources/extensions/gsd/auto-loop.js +11 -0
- package/dist/resources/extensions/gsd/auto.js +16 -0
- 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/index.js +5 -0
- 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/templates/preferences.md +6 -0
- package/dist/resources/extensions/search-the-web/native-search.js +45 -4
- 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/package.json +1 -1
- 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/gsd/auto-dashboard.ts +363 -116
- package/src/resources/extensions/gsd/auto-loop.ts +42 -0
- package/src/resources/extensions/gsd/auto.ts +21 -0
- 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/index.ts +8 -0
- 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/templates/preferences.md +6 -0
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +2 -0
- package/src/resources/extensions/gsd/tests/cmux.test.ts +98 -0
- package/src/resources/extensions/gsd/tests/preferences.test.ts +23 -0
- package/src/resources/extensions/search-the-web/native-search.ts +50 -4
- package/src/resources/extensions/shared/terminal.ts +5 -0
- 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
|
|
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
|
|
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
|
|
238
|
-
let widgetPwd
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
-
|
|
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
|
-
|
|
280
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
369
|
-
const cxDisplay = cxPct
|
|
370
|
-
|
|
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
|
-
|
|
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;
|