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
|
@@ -218,10 +218,24 @@ export async function autoLoop(ctx, pi, s, deps) {
|
|
|
218
218
|
}
|
|
219
219
|
try {
|
|
220
220
|
// ── Blanket try/catch: one bad iteration must not kill the session
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
221
|
+
const sessionLockBase = deps.lockBase();
|
|
222
|
+
if (sessionLockBase) {
|
|
223
|
+
const lockStatus = deps.validateSessionLock(sessionLockBase);
|
|
224
|
+
if (!lockStatus.valid) {
|
|
225
|
+
debugLog("autoLoop", {
|
|
226
|
+
phase: "session-lock-invalid",
|
|
227
|
+
reason: lockStatus.failureReason ?? "unknown",
|
|
228
|
+
existingPid: lockStatus.existingPid,
|
|
229
|
+
expectedPid: lockStatus.expectedPid,
|
|
230
|
+
});
|
|
231
|
+
deps.handleLostSessionLock(ctx, lockStatus);
|
|
232
|
+
debugLog("autoLoop", {
|
|
233
|
+
phase: "exit",
|
|
234
|
+
reason: "session-lock-lost",
|
|
235
|
+
detail: lockStatus.failureReason ?? "unknown",
|
|
236
|
+
});
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
225
239
|
}
|
|
226
240
|
// ── Phase 1: Pre-dispatch ───────────────────────────────────────────
|
|
227
241
|
// Resource version guard
|
|
@@ -258,6 +272,7 @@ export async function autoLoop(ctx, pi, s, deps) {
|
|
|
258
272
|
}
|
|
259
273
|
// Derive state
|
|
260
274
|
let state = await deps.deriveState(s.basePath);
|
|
275
|
+
deps.syncCmuxSidebar(deps.loadEffectiveGSDPreferences()?.preferences, state);
|
|
261
276
|
let mid = state.activeMilestone?.id;
|
|
262
277
|
let midTitle = state.activeMilestone?.title;
|
|
263
278
|
debugLog("autoLoop", {
|
|
@@ -270,6 +285,7 @@ export async function autoLoop(ctx, pi, s, deps) {
|
|
|
270
285
|
if (mid && s.currentMilestoneId && mid !== s.currentMilestoneId) {
|
|
271
286
|
ctx.ui.notify(`Milestone ${s.currentMilestoneId} complete. Advancing to ${mid}: ${midTitle}.`, "info");
|
|
272
287
|
deps.sendDesktopNotification("GSD", `Milestone ${s.currentMilestoneId} complete!`, "success", "milestone");
|
|
288
|
+
deps.logCmuxEvent(deps.loadEffectiveGSDPreferences()?.preferences, `Milestone ${s.currentMilestoneId} complete. Advancing to ${mid}.`, "success");
|
|
273
289
|
const vizPrefs = deps.loadEffectiveGSDPreferences()?.preferences;
|
|
274
290
|
if (vizPrefs?.auto_visualize) {
|
|
275
291
|
ctx.ui.notify("Run /gsd visualize to see progress overview.", "info");
|
|
@@ -363,6 +379,7 @@ export async function autoLoop(ctx, pi, s, deps) {
|
|
|
363
379
|
deps.resolver.mergeAndExit(s.currentMilestoneId, ctx.ui);
|
|
364
380
|
}
|
|
365
381
|
deps.sendDesktopNotification("GSD", "All milestones complete!", "success", "milestone");
|
|
382
|
+
deps.logCmuxEvent(deps.loadEffectiveGSDPreferences()?.preferences, "All milestones complete.", "success");
|
|
366
383
|
await deps.stopAuto(ctx, pi, "All milestones complete");
|
|
367
384
|
}
|
|
368
385
|
else if (state.phase === "blocked") {
|
|
@@ -370,6 +387,7 @@ export async function autoLoop(ctx, pi, s, deps) {
|
|
|
370
387
|
await deps.stopAuto(ctx, pi, blockerMsg);
|
|
371
388
|
ctx.ui.notify(`${blockerMsg}. Fix and run /gsd auto.`, "warning");
|
|
372
389
|
deps.sendDesktopNotification("GSD", blockerMsg, "error", "attention");
|
|
390
|
+
deps.logCmuxEvent(deps.loadEffectiveGSDPreferences()?.preferences, blockerMsg, "error");
|
|
373
391
|
}
|
|
374
392
|
else {
|
|
375
393
|
const ids = incomplete.map((m) => m.id).join(", ");
|
|
@@ -415,6 +433,7 @@ export async function autoLoop(ctx, pi, s, deps) {
|
|
|
415
433
|
deps.resolver.mergeAndExit(s.currentMilestoneId, ctx.ui);
|
|
416
434
|
}
|
|
417
435
|
deps.sendDesktopNotification("GSD", `Milestone ${mid} complete!`, "success", "milestone");
|
|
436
|
+
deps.logCmuxEvent(deps.loadEffectiveGSDPreferences()?.preferences, `Milestone ${mid} complete.`, "success");
|
|
418
437
|
await deps.stopAuto(ctx, pi, `Milestone ${mid} complete`);
|
|
419
438
|
debugLog("autoLoop", { phase: "exit", reason: "milestone-complete" });
|
|
420
439
|
break;
|
|
@@ -428,6 +447,7 @@ export async function autoLoop(ctx, pi, s, deps) {
|
|
|
428
447
|
await deps.stopAuto(ctx, pi, blockerMsg);
|
|
429
448
|
ctx.ui.notify(`${blockerMsg}. Fix and run /gsd auto.`, "warning");
|
|
430
449
|
deps.sendDesktopNotification("GSD", blockerMsg, "error", "attention");
|
|
450
|
+
deps.logCmuxEvent(deps.loadEffectiveGSDPreferences()?.preferences, blockerMsg, "error");
|
|
431
451
|
debugLog("autoLoop", { phase: "exit", reason: "blocked" });
|
|
432
452
|
break;
|
|
433
453
|
}
|
|
@@ -458,30 +478,35 @@ export async function autoLoop(ctx, pi, s, deps) {
|
|
|
458
478
|
if (budgetEnforcementAction === "pause") {
|
|
459
479
|
ctx.ui.notify(`${msg} Pausing auto-mode — /gsd auto to override and continue.`, "warning");
|
|
460
480
|
deps.sendDesktopNotification("GSD", msg, "warning", "budget");
|
|
481
|
+
deps.logCmuxEvent(prefs, msg, "warning");
|
|
461
482
|
await deps.pauseAuto(ctx, pi);
|
|
462
483
|
debugLog("autoLoop", { phase: "exit", reason: "budget-pause" });
|
|
463
484
|
break;
|
|
464
485
|
}
|
|
465
486
|
ctx.ui.notify(`${msg} Continuing (enforcement: warn).`, "warning");
|
|
466
487
|
deps.sendDesktopNotification("GSD", msg, "warning", "budget");
|
|
488
|
+
deps.logCmuxEvent(prefs, msg, "warning");
|
|
467
489
|
}
|
|
468
490
|
else if (newBudgetAlertLevel === 90) {
|
|
469
491
|
s.lastBudgetAlertLevel =
|
|
470
492
|
newBudgetAlertLevel;
|
|
471
493
|
ctx.ui.notify(`Budget 90%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`, "warning");
|
|
472
494
|
deps.sendDesktopNotification("GSD", `Budget 90%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`, "warning", "budget");
|
|
495
|
+
deps.logCmuxEvent(prefs, `Budget 90%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`, "warning");
|
|
473
496
|
}
|
|
474
497
|
else if (newBudgetAlertLevel === 80) {
|
|
475
498
|
s.lastBudgetAlertLevel =
|
|
476
499
|
newBudgetAlertLevel;
|
|
477
500
|
ctx.ui.notify(`Approaching budget ceiling — 80%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`, "warning");
|
|
478
501
|
deps.sendDesktopNotification("GSD", `Approaching budget ceiling — 80%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`, "warning", "budget");
|
|
502
|
+
deps.logCmuxEvent(prefs, `Budget 80%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`, "warning");
|
|
479
503
|
}
|
|
480
504
|
else if (newBudgetAlertLevel === 75) {
|
|
481
505
|
s.lastBudgetAlertLevel =
|
|
482
506
|
newBudgetAlertLevel;
|
|
483
507
|
ctx.ui.notify(`Budget 75%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`, "info");
|
|
484
508
|
deps.sendDesktopNotification("GSD", `Budget 75%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`, "info", "budget");
|
|
509
|
+
deps.logCmuxEvent(prefs, `Budget 75%: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`, "progress");
|
|
485
510
|
}
|
|
486
511
|
else if (budgetAlertLevel === 0) {
|
|
487
512
|
s.lastBudgetAlertLevel = 0;
|
|
@@ -18,7 +18,7 @@ import { invalidateAllCaches } from "./cache.js";
|
|
|
18
18
|
import { clearActivityLogState } from "./activity-log.js";
|
|
19
19
|
import { synthesizeCrashRecovery, getDeepDiagnostic, } from "./session-forensics.js";
|
|
20
20
|
import { writeLock, clearLock, readCrashLock, isLockProcessAlive, } from "./crash-recovery.js";
|
|
21
|
-
import { acquireSessionLock,
|
|
21
|
+
import { acquireSessionLock, getSessionLockStatus, releaseSessionLock, updateSessionLock, } from "./session-lock.js";
|
|
22
22
|
import { clearUnitRuntimeRecord, readUnitRuntimeRecord, writeUnitRuntimeRecord, } from "./unit-runtime.js";
|
|
23
23
|
import { resolveAutoSupervisorConfig, loadEffectiveGSDPreferences, getIsolationMode, } from "./preferences.js";
|
|
24
24
|
import { sendDesktopNotification } from "./notifications.js";
|
|
@@ -50,6 +50,7 @@ import { updateProgressWidget as _updateProgressWidget, updateSliceProgressCache
|
|
|
50
50
|
import { registerSigtermHandler as _registerSigtermHandler, deregisterSigtermHandler as _deregisterSigtermHandler, } from "./auto-supervisor.js";
|
|
51
51
|
import { isDbAvailable } from "./gsd-db.js";
|
|
52
52
|
import { countPendingCaptures } from "./captures.js";
|
|
53
|
+
import { clearCmuxSidebar, logCmuxEvent, syncCmuxSidebar } from "../cmux/index.js";
|
|
53
54
|
// ── Extracted modules ──────────────────────────────────────────────────────
|
|
54
55
|
import { startUnitSupervision } from "./auto-timers.js";
|
|
55
56
|
import { runPostUnitVerification } from "./auto-verification.js";
|
|
@@ -242,13 +243,28 @@ function buildSnapshotOpts(unitType, unitId) {
|
|
|
242
243
|
...(runtime?.continueHereFired ? { continueHereFired: true } : {}),
|
|
243
244
|
};
|
|
244
245
|
}
|
|
245
|
-
function handleLostSessionLock(ctx) {
|
|
246
|
-
debugLog("session-lock-lost", {
|
|
246
|
+
function handleLostSessionLock(ctx, lockStatus) {
|
|
247
|
+
debugLog("session-lock-lost", {
|
|
248
|
+
lockBase: lockBase(),
|
|
249
|
+
reason: lockStatus?.failureReason,
|
|
250
|
+
existingPid: lockStatus?.existingPid,
|
|
251
|
+
expectedPid: lockStatus?.expectedPid,
|
|
252
|
+
});
|
|
247
253
|
s.active = false;
|
|
248
254
|
s.paused = false;
|
|
249
255
|
clearUnitTimeout();
|
|
250
256
|
deregisterSigtermHandler();
|
|
251
|
-
|
|
257
|
+
clearCmuxSidebar(loadEffectiveGSDPreferences()?.preferences);
|
|
258
|
+
const message = lockStatus?.failureReason === "pid-mismatch"
|
|
259
|
+
? lockStatus.existingPid
|
|
260
|
+
? `Session lock moved to PID ${lockStatus.existingPid} — another GSD process appears to have taken over. Stopping gracefully.`
|
|
261
|
+
: "Session lock moved to a different process — another GSD process appears to have taken over. Stopping gracefully."
|
|
262
|
+
: lockStatus?.failureReason === "missing-metadata"
|
|
263
|
+
? "Session lock metadata disappeared, so ownership could not be confirmed. Stopping gracefully."
|
|
264
|
+
: lockStatus?.failureReason === "compromised"
|
|
265
|
+
? "Session lock was compromised or invalidated during heartbeat checks; takeover was not confirmed. Stopping gracefully."
|
|
266
|
+
: "Session lock lost. Stopping gracefully.";
|
|
267
|
+
ctx?.ui.notify(message, "error");
|
|
252
268
|
ctx?.ui.setStatus("gsd-auto", undefined);
|
|
253
269
|
ctx?.ui.setWidget("gsd-progress", undefined);
|
|
254
270
|
ctx?.ui.setFooter(undefined);
|
|
@@ -256,6 +272,7 @@ function handleLostSessionLock(ctx) {
|
|
|
256
272
|
export async function stopAuto(ctx, pi, reason) {
|
|
257
273
|
if (!s.active && !s.paused)
|
|
258
274
|
return;
|
|
275
|
+
const loadedPreferences = loadEffectiveGSDPreferences()?.preferences;
|
|
259
276
|
const reasonSuffix = reason ? ` — ${reason}` : "";
|
|
260
277
|
clearUnitTimeout();
|
|
261
278
|
if (lockBase())
|
|
@@ -314,6 +331,8 @@ export async function stopAuto(ctx, pi, reason) {
|
|
|
314
331
|
});
|
|
315
332
|
}
|
|
316
333
|
}
|
|
334
|
+
clearCmuxSidebar(loadedPreferences);
|
|
335
|
+
logCmuxEvent(loadedPreferences, `Auto-mode stopped${reasonSuffix || ""}.`, reason?.startsWith("Blocked:") ? "warning" : "info");
|
|
317
336
|
if (isDebugEnabled()) {
|
|
318
337
|
const logPath = writeDebugSummary();
|
|
319
338
|
if (logPath) {
|
|
@@ -455,6 +474,8 @@ function buildLoopDeps() {
|
|
|
455
474
|
pauseAuto,
|
|
456
475
|
clearUnitTimeout,
|
|
457
476
|
updateProgressWidget,
|
|
477
|
+
syncCmuxSidebar,
|
|
478
|
+
logCmuxEvent,
|
|
458
479
|
// State and cache
|
|
459
480
|
invalidateAllCaches,
|
|
460
481
|
deriveState,
|
|
@@ -466,7 +487,7 @@ function buildLoopDeps() {
|
|
|
466
487
|
// Resource version guard
|
|
467
488
|
checkResourcesStale,
|
|
468
489
|
// Session lock
|
|
469
|
-
validateSessionLock,
|
|
490
|
+
validateSessionLock: getSessionLockStatus,
|
|
470
491
|
updateSessionLock,
|
|
471
492
|
handleLostSessionLock,
|
|
472
493
|
// Milestone transition
|
|
@@ -605,6 +626,7 @@ export async function startAuto(ctx, pi, base, verboseMode, options) {
|
|
|
605
626
|
restoreHookState(s.basePath);
|
|
606
627
|
try {
|
|
607
628
|
await rebuildState(s.basePath);
|
|
629
|
+
syncCmuxSidebar(loadEffectiveGSDPreferences()?.preferences, await deriveState(s.basePath));
|
|
608
630
|
}
|
|
609
631
|
catch (e) {
|
|
610
632
|
debugLog("resume-rebuild-state-failed", {
|
|
@@ -634,6 +656,7 @@ export async function startAuto(ctx, pi, base, verboseMode, options) {
|
|
|
634
656
|
}
|
|
635
657
|
updateSessionLock(lockBase(), "resuming", s.currentMilestoneId ?? "unknown", s.completedUnits.length);
|
|
636
658
|
writeLock(lockBase(), "resuming", s.currentMilestoneId ?? "unknown", s.completedUnits.length);
|
|
659
|
+
logCmuxEvent(loadEffectiveGSDPreferences()?.preferences, s.stepMode ? "Step-mode resumed." : "Auto-mode resumed.", "progress");
|
|
637
660
|
await autoLoop(ctx, pi, s, buildLoopDeps());
|
|
638
661
|
return;
|
|
639
662
|
}
|
|
@@ -647,6 +670,13 @@ export async function startAuto(ctx, pi, base, verboseMode, options) {
|
|
|
647
670
|
const ready = await bootstrapAutoSession(s, ctx, pi, base, verboseMode, requestedStepMode, bootstrapDeps);
|
|
648
671
|
if (!ready)
|
|
649
672
|
return;
|
|
673
|
+
try {
|
|
674
|
+
syncCmuxSidebar(loadEffectiveGSDPreferences()?.preferences, await deriveState(s.basePath));
|
|
675
|
+
}
|
|
676
|
+
catch {
|
|
677
|
+
// Best-effort only — sidebar sync must never block auto-mode startup
|
|
678
|
+
}
|
|
679
|
+
logCmuxEvent(loadEffectiveGSDPreferences()?.preferences, requestedStepMode ? "Step-mode started." : "Auto-mode started.", "progress");
|
|
650
680
|
// Dispatch the first unit
|
|
651
681
|
await autoLoop(ctx, pi, s, buildLoopDeps());
|
|
652
682
|
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { clearCmuxSidebar, CmuxClient, detectCmuxEnvironment, resolveCmuxConfig } from "../cmux/index.js";
|
|
3
|
+
import { saveFile } from "./files.js";
|
|
4
|
+
import { getProjectGSDPreferencesPath, loadEffectiveGSDPreferences, loadProjectGSDPreferences, } from "./preferences.js";
|
|
5
|
+
import { ensurePreferencesFile, serializePreferencesToFrontmatter } from "./commands-prefs-wizard.js";
|
|
6
|
+
function extractBodyAfterFrontmatter(content) {
|
|
7
|
+
const start = content.startsWith("---\n") ? 4 : content.startsWith("---\r\n") ? 5 : -1;
|
|
8
|
+
if (start === -1)
|
|
9
|
+
return null;
|
|
10
|
+
const closingIdx = content.indexOf("\n---", start);
|
|
11
|
+
if (closingIdx === -1)
|
|
12
|
+
return null;
|
|
13
|
+
const after = content.slice(closingIdx + 4);
|
|
14
|
+
return after.trim() ? after : null;
|
|
15
|
+
}
|
|
16
|
+
async function writeProjectCmuxPreferences(ctx, updater) {
|
|
17
|
+
const path = getProjectGSDPreferencesPath();
|
|
18
|
+
await ensurePreferencesFile(path, ctx, "project");
|
|
19
|
+
const existing = loadProjectGSDPreferences();
|
|
20
|
+
const prefs = existing?.preferences ? { ...existing.preferences } : { version: 1 };
|
|
21
|
+
updater(prefs);
|
|
22
|
+
prefs.version = prefs.version || 1;
|
|
23
|
+
const frontmatter = serializePreferencesToFrontmatter(prefs);
|
|
24
|
+
let body = "\n# GSD Skill Preferences\n\nSee `~/.gsd/agent/extensions/gsd/docs/preferences-reference.md` for full field documentation and examples.\n";
|
|
25
|
+
if (existsSync(path)) {
|
|
26
|
+
const preserved = extractBodyAfterFrontmatter(readFileSync(path, "utf-8"));
|
|
27
|
+
if (preserved)
|
|
28
|
+
body = preserved;
|
|
29
|
+
}
|
|
30
|
+
await saveFile(path, `---\n${frontmatter}---${body}`);
|
|
31
|
+
await ctx.waitForIdle();
|
|
32
|
+
await ctx.reload();
|
|
33
|
+
}
|
|
34
|
+
function formatCmuxStatus() {
|
|
35
|
+
const loaded = loadEffectiveGSDPreferences();
|
|
36
|
+
const detected = detectCmuxEnvironment();
|
|
37
|
+
const resolved = resolveCmuxConfig(loaded?.preferences);
|
|
38
|
+
const capabilities = new CmuxClient(resolved).getCapabilities();
|
|
39
|
+
const accessMode = typeof capabilities?.mode === "string"
|
|
40
|
+
? capabilities.mode
|
|
41
|
+
: typeof capabilities?.access_mode === "string"
|
|
42
|
+
? capabilities.access_mode
|
|
43
|
+
: "unknown";
|
|
44
|
+
const methods = Array.isArray(capabilities?.methods) ? capabilities.methods.length : 0;
|
|
45
|
+
return [
|
|
46
|
+
"cmux status",
|
|
47
|
+
"",
|
|
48
|
+
`Detected: ${detected.available ? "yes" : "no"}`,
|
|
49
|
+
`Enabled: ${resolved.enabled ? "yes" : "no"}`,
|
|
50
|
+
`CLI available: ${detected.cliAvailable ? "yes" : "no"}`,
|
|
51
|
+
`Socket: ${detected.socketPath}`,
|
|
52
|
+
`Workspace: ${detected.workspaceId ?? "(none)"}`,
|
|
53
|
+
`Surface: ${detected.surfaceId ?? "(none)"}`,
|
|
54
|
+
`Features: notifications=${resolved.notifications ? "on" : "off"}, sidebar=${resolved.sidebar ? "on" : "off"}, splits=${resolved.splits ? "on" : "off"}, browser=${resolved.browser ? "on" : "off"}`,
|
|
55
|
+
`Capabilities: access=${accessMode}, methods=${methods}`,
|
|
56
|
+
].join("\n");
|
|
57
|
+
}
|
|
58
|
+
function ensureCmuxAvailableForEnable(ctx) {
|
|
59
|
+
const detected = detectCmuxEnvironment();
|
|
60
|
+
if (detected.available)
|
|
61
|
+
return true;
|
|
62
|
+
ctx.ui.notify("cmux not detected. Install it from https://cmux.com and run gsd inside a cmux terminal.", "warning");
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
export async function handleCmux(args, ctx) {
|
|
66
|
+
const trimmed = args.trim();
|
|
67
|
+
if (!trimmed || trimmed === "status") {
|
|
68
|
+
ctx.ui.notify(formatCmuxStatus(), "info");
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (trimmed === "on") {
|
|
72
|
+
if (!ensureCmuxAvailableForEnable(ctx))
|
|
73
|
+
return;
|
|
74
|
+
await writeProjectCmuxPreferences(ctx, (prefs) => {
|
|
75
|
+
prefs.cmux = {
|
|
76
|
+
enabled: true,
|
|
77
|
+
notifications: true,
|
|
78
|
+
sidebar: true,
|
|
79
|
+
splits: false,
|
|
80
|
+
browser: false,
|
|
81
|
+
...(prefs.cmux ?? {}),
|
|
82
|
+
};
|
|
83
|
+
prefs.cmux.enabled = true;
|
|
84
|
+
});
|
|
85
|
+
ctx.ui.notify("cmux integration enabled in project preferences.", "info");
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (trimmed === "off") {
|
|
89
|
+
const effective = loadEffectiveGSDPreferences()?.preferences;
|
|
90
|
+
await writeProjectCmuxPreferences(ctx, (prefs) => {
|
|
91
|
+
prefs.cmux = { ...(prefs.cmux ?? {}), enabled: false };
|
|
92
|
+
});
|
|
93
|
+
clearCmuxSidebar(effective);
|
|
94
|
+
ctx.ui.notify("cmux integration disabled in project preferences.", "info");
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const parts = trimmed.split(/\s+/);
|
|
98
|
+
if (parts.length === 2 && ["notifications", "sidebar", "splits", "browser"].includes(parts[0]) && ["on", "off"].includes(parts[1])) {
|
|
99
|
+
const feature = parts[0];
|
|
100
|
+
const enabled = parts[1] === "on";
|
|
101
|
+
if (enabled && !ensureCmuxAvailableForEnable(ctx))
|
|
102
|
+
return;
|
|
103
|
+
await writeProjectCmuxPreferences(ctx, (prefs) => {
|
|
104
|
+
const next = { ...(prefs.cmux ?? {}) };
|
|
105
|
+
next[feature] = enabled;
|
|
106
|
+
if (enabled)
|
|
107
|
+
next.enabled = true;
|
|
108
|
+
prefs.cmux = next;
|
|
109
|
+
});
|
|
110
|
+
if (!enabled && feature === "sidebar") {
|
|
111
|
+
clearCmuxSidebar(loadEffectiveGSDPreferences()?.preferences);
|
|
112
|
+
}
|
|
113
|
+
const note = feature === "browser" && enabled
|
|
114
|
+
? " Browser surfaces are still a follow-up path."
|
|
115
|
+
: "";
|
|
116
|
+
ctx.ui.notify(`cmux ${feature} ${enabled ? "enabled" : "disabled"}.${note}`, "info");
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
ctx.ui.notify("Usage: /gsd cmux <status|on|off|notifications on|notifications off|sidebar on|sidebar off|splits on|splits off|browser on|browser off>", "info");
|
|
120
|
+
}
|
|
@@ -626,7 +626,7 @@ export function serializePreferencesToFrontmatter(prefs) {
|
|
|
626
626
|
"skill_rules", "custom_instructions", "models", "skill_discovery",
|
|
627
627
|
"skill_staleness_days", "auto_supervisor", "uat_dispatch", "unique_milestone_ids",
|
|
628
628
|
"budget_ceiling", "budget_enforcement", "context_pause_threshold",
|
|
629
|
-
"notifications", "remote_questions", "git",
|
|
629
|
+
"notifications", "cmux", "remote_questions", "git",
|
|
630
630
|
"post_unit_hooks", "pre_dispatch_hooks",
|
|
631
631
|
"dynamic_routing", "token_profile", "phases", "parallel",
|
|
632
632
|
"auto_visualize", "auto_report",
|
|
@@ -37,6 +37,7 @@ import { runEnvironmentChecks } from "./doctor-environment.js";
|
|
|
37
37
|
import { handleLogs } from "./commands-logs.js";
|
|
38
38
|
import { handleStart, handleTemplates, getTemplateCompletions } from "./commands-workflow-templates.js";
|
|
39
39
|
import { readSessionLockData, isSessionLockProcessAlive } from "./session-lock.js";
|
|
40
|
+
import { handleCmux } from "./commands-cmux.js";
|
|
40
41
|
/** Resolve the effective project root, accounting for worktree paths. */
|
|
41
42
|
export function projectRoot() {
|
|
42
43
|
const cwd = process.cwd();
|
|
@@ -89,7 +90,7 @@ function notifyRemoteAutoActive(ctx, basePath) {
|
|
|
89
90
|
}
|
|
90
91
|
export function registerGSDCommand(pi) {
|
|
91
92
|
pi.registerCommand("gsd", {
|
|
92
|
-
description: "GSD — Get Shit Done: /gsd help|start|templates|next|auto|stop|pause|status|visualize|queue|quick|capture|triage|dispatch|history|undo|skip|export|cleanup|mode|prefs|config|keys|hooks|run-hook|skill-health|doctor|forensics|changelog|migrate|remote|steer|knowledge|new-milestone|parallel|update",
|
|
93
|
+
description: "GSD — Get Shit Done: /gsd help|start|templates|next|auto|stop|pause|status|visualize|queue|quick|capture|triage|dispatch|history|undo|skip|export|cleanup|mode|prefs|config|keys|hooks|run-hook|skill-health|doctor|forensics|changelog|migrate|remote|steer|knowledge|new-milestone|parallel|cmux|update",
|
|
93
94
|
getArgumentCompletions: (prefix) => {
|
|
94
95
|
const subcommands = [
|
|
95
96
|
{ cmd: "help", desc: "Categorized command reference with descriptions" },
|
|
@@ -98,6 +99,7 @@ export function registerGSDCommand(pi) {
|
|
|
98
99
|
{ cmd: "stop", desc: "Stop auto mode gracefully" },
|
|
99
100
|
{ cmd: "pause", desc: "Pause auto-mode (preserves state, /gsd auto to resume)" },
|
|
100
101
|
{ cmd: "status", desc: "Progress dashboard" },
|
|
102
|
+
{ cmd: "widget", desc: "Cycle widget: full → small → min → off" },
|
|
101
103
|
{ cmd: "visualize", desc: "Open 10-tab workflow visualizer (progress, timeline, deps, metrics, health, agent, changes, knowledge, captures, export)" },
|
|
102
104
|
{ cmd: "queue", desc: "Queue and reorder future milestones" },
|
|
103
105
|
{ cmd: "quick", desc: "Execute a quick task without full planning overhead" },
|
|
@@ -131,6 +133,7 @@ export function registerGSDCommand(pi) {
|
|
|
131
133
|
{ cmd: "knowledge", desc: "Add persistent project knowledge (rule, pattern, or lesson)" },
|
|
132
134
|
{ cmd: "new-milestone", desc: "Create a milestone from a specification document (headless)" },
|
|
133
135
|
{ cmd: "parallel", desc: "Parallel milestone orchestration (start, status, stop, merge)" },
|
|
136
|
+
{ cmd: "cmux", desc: "Manage cmux integration (status, sidebar, notifications, splits)" },
|
|
134
137
|
{ cmd: "park", desc: "Park a milestone — skip without deleting" },
|
|
135
138
|
{ cmd: "unpark", desc: "Reactivate a parked milestone" },
|
|
136
139
|
{ cmd: "update", desc: "Update GSD to the latest version" },
|
|
@@ -182,6 +185,36 @@ export function registerGSDCommand(pi) {
|
|
|
182
185
|
.filter((s) => s.cmd.startsWith(subPrefix))
|
|
183
186
|
.map((s) => ({ value: `parallel ${s.cmd}`, label: s.cmd, description: s.desc }));
|
|
184
187
|
}
|
|
188
|
+
if (parts[0] === "cmux") {
|
|
189
|
+
if (parts.length <= 2) {
|
|
190
|
+
const subPrefix = parts[1] ?? "";
|
|
191
|
+
const subs = [
|
|
192
|
+
{ cmd: "status", desc: "Show cmux detection, prefs, and capabilities" },
|
|
193
|
+
{ cmd: "on", desc: "Enable cmux integration" },
|
|
194
|
+
{ cmd: "off", desc: "Disable cmux integration" },
|
|
195
|
+
{ cmd: "notifications", desc: "Toggle cmux desktop notifications" },
|
|
196
|
+
{ cmd: "sidebar", desc: "Toggle cmux sidebar metadata" },
|
|
197
|
+
{ cmd: "splits", desc: "Toggle cmux visual subagent splits" },
|
|
198
|
+
{ cmd: "browser", desc: "Toggle future browser integration flag" },
|
|
199
|
+
];
|
|
200
|
+
return subs
|
|
201
|
+
.filter((s) => s.cmd.startsWith(subPrefix))
|
|
202
|
+
.map((s) => ({ value: `cmux ${s.cmd}`, label: s.cmd, description: s.desc }));
|
|
203
|
+
}
|
|
204
|
+
if (parts.length <= 3 && ["notifications", "sidebar", "splits", "browser"].includes(parts[1])) {
|
|
205
|
+
const togglePrefix = parts[2] ?? "";
|
|
206
|
+
return [
|
|
207
|
+
{ cmd: "on", desc: "Enable this cmux area" },
|
|
208
|
+
{ cmd: "off", desc: "Disable this cmux area" },
|
|
209
|
+
]
|
|
210
|
+
.filter((item) => item.cmd.startsWith(togglePrefix))
|
|
211
|
+
.map((item) => ({
|
|
212
|
+
value: `cmux ${parts[1]} ${item.cmd}`,
|
|
213
|
+
label: item.cmd,
|
|
214
|
+
description: item.desc,
|
|
215
|
+
}));
|
|
216
|
+
}
|
|
217
|
+
}
|
|
185
218
|
if (parts[0] === "setup" && parts.length <= 2) {
|
|
186
219
|
const subPrefix = parts[1] ?? "";
|
|
187
220
|
const subs = [
|
|
@@ -430,6 +463,18 @@ export async function handleGSDCommand(args, ctx, pi) {
|
|
|
430
463
|
await handleStatus(ctx);
|
|
431
464
|
return;
|
|
432
465
|
}
|
|
466
|
+
if (trimmed === "widget" || trimmed.startsWith("widget ")) {
|
|
467
|
+
const { cycleWidgetMode, setWidgetMode, getWidgetMode } = await import("./auto-dashboard.js");
|
|
468
|
+
const arg = trimmed.replace(/^widget\s*/, "").trim();
|
|
469
|
+
if (arg === "full" || arg === "small" || arg === "min" || arg === "off") {
|
|
470
|
+
setWidgetMode(arg);
|
|
471
|
+
}
|
|
472
|
+
else {
|
|
473
|
+
cycleWidgetMode();
|
|
474
|
+
}
|
|
475
|
+
ctx.ui.notify(`Widget: ${getWidgetMode()}`, "info");
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
433
478
|
if (trimmed === "visualize") {
|
|
434
479
|
await handleVisualize(ctx);
|
|
435
480
|
return;
|
|
@@ -446,6 +491,10 @@ export async function handleGSDCommand(args, ctx, pi) {
|
|
|
446
491
|
await handlePrefs(trimmed.replace(/^prefs\s*/, "").trim(), ctx);
|
|
447
492
|
return;
|
|
448
493
|
}
|
|
494
|
+
if (trimmed === "cmux" || trimmed.startsWith("cmux ")) {
|
|
495
|
+
await handleCmux(trimmed.replace(/^cmux\s*/, "").trim(), ctx);
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
449
498
|
if (trimmed === "init") {
|
|
450
499
|
const { detectProjectState } = await import("./detection.js");
|
|
451
500
|
const { showProjectInit, handleReinit } = await import("./init-wizard.js");
|
|
@@ -900,6 +949,7 @@ function showHelp(ctx) {
|
|
|
900
949
|
" /gsd setup Global setup status [llm|search|remote|keys|prefs]",
|
|
901
950
|
" /gsd mode Set workflow mode (solo/team) [global|project]",
|
|
902
951
|
" /gsd prefs Manage preferences [global|project|status|wizard|setup|import-claude]",
|
|
952
|
+
" /gsd cmux Manage cmux integration [status|on|off|notifications|sidebar|splits|browser]",
|
|
903
953
|
" /gsd config Set API keys for external tools",
|
|
904
954
|
" /gsd keys API key manager [list|add|remove|test|rotate|doctor]",
|
|
905
955
|
" /gsd hooks Show post-unit hook configuration",
|
|
@@ -173,6 +173,13 @@ Setting `prefer_skills: []` does **not** disable skill discovery — it just mea
|
|
|
173
173
|
- `on_milestone`: boolean — notify when a milestone finishes. Default: `true`.
|
|
174
174
|
- `on_attention`: boolean — notify when manual attention is needed. Default: `true`.
|
|
175
175
|
|
|
176
|
+
- `cmux`: configures cmux terminal integration when GSD is running inside a cmux workspace. Keys:
|
|
177
|
+
- `enabled`: boolean — master toggle for cmux integration. Default: `false`.
|
|
178
|
+
- `notifications`: boolean — route desktop notifications through cmux. Default: `true` when enabled.
|
|
179
|
+
- `sidebar`: boolean — publish status, progress, and log metadata to the cmux sidebar. Default: `true` when enabled.
|
|
180
|
+
- `splits`: boolean — run supported subagent work in visible cmux splits. Default: `false`.
|
|
181
|
+
- `browser`: boolean — reserve the future browser integration flag. Default: `false`.
|
|
182
|
+
|
|
176
183
|
- `dynamic_routing`: configures the dynamic model router that adjusts model selection based on task complexity. Keys:
|
|
177
184
|
- `enabled`: boolean — enable dynamic routing. Default: `false`.
|
|
178
185
|
- `tier_models`: object — model overrides per complexity tier. Keys: `light`, `standard`, `heavy`. Values are model ID strings.
|
|
@@ -477,6 +484,24 @@ Disables per-unit completion notifications (noisy in long runs) while keeping er
|
|
|
477
484
|
|
|
478
485
|
---
|
|
479
486
|
|
|
487
|
+
## cmux Example
|
|
488
|
+
|
|
489
|
+
```yaml
|
|
490
|
+
---
|
|
491
|
+
version: 1
|
|
492
|
+
cmux:
|
|
493
|
+
enabled: true
|
|
494
|
+
notifications: true
|
|
495
|
+
sidebar: true
|
|
496
|
+
splits: true
|
|
497
|
+
browser: false
|
|
498
|
+
---
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
Enables cmux-aware notifications, sidebar metadata, and visible subagent splits when GSD is running inside a cmux terminal.
|
|
502
|
+
|
|
503
|
+
---
|
|
504
|
+
|
|
480
505
|
## Post-Unit Hooks Example
|
|
481
506
|
|
|
482
507
|
```yaml
|
|
@@ -349,10 +349,18 @@ export class GitServiceImpl {
|
|
|
349
349
|
}
|
|
350
350
|
const wtName = detectWorktreeName(this.basePath);
|
|
351
351
|
if (wtName) {
|
|
352
|
+
// Auto-mode worktrees use milestone/<MID> branches (wtName = milestone ID)
|
|
353
|
+
const milestoneBranch = `milestone/${wtName}`;
|
|
354
|
+
const currentBranch = nativeGetCurrentBranch(this.basePath);
|
|
355
|
+
// If we're on a milestone/<MID> branch, use it (auto-mode case)
|
|
356
|
+
if (currentBranch.startsWith("milestone/")) {
|
|
357
|
+
return currentBranch;
|
|
358
|
+
}
|
|
359
|
+
// Otherwise check for manual worktree branch (worktree/<name>)
|
|
352
360
|
const wtBranch = `worktree/${wtName}`;
|
|
353
361
|
if (nativeBranchExists(this.basePath, wtBranch))
|
|
354
362
|
return wtBranch;
|
|
355
|
-
return
|
|
363
|
+
return currentBranch;
|
|
356
364
|
}
|
|
357
365
|
// Repo-level default detection: origin/HEAD → main → master → current branch.
|
|
358
366
|
// Native path uses libgit2 (single call), fallback spawns multiple git processes.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// GSD Extension — Session History View
|
|
2
2
|
// Human-readable display of past auto-mode unit executions.
|
|
3
|
-
import { formatDuration,
|
|
3
|
+
import { formatDuration, truncateWithEllipsis } from "../shared/format-utils.js";
|
|
4
|
+
import { padRight } from "../shared/layout-utils.js";
|
|
4
5
|
import { getLedger, getProjectTotals, formatCost, formatTokenCount, aggregateBySlice, aggregateByPhase, aggregateByModel, loadLedgerFromDisk, } from "./metrics.js";
|
|
5
6
|
/**
|
|
6
7
|
* Show recent unit execution history with cost, tokens, and duration.
|
|
@@ -46,6 +46,7 @@ import { pauseAutoForProviderError, classifyProviderError } from "./provider-err
|
|
|
46
46
|
import { toPosixPath } from "../shared/mod.js";
|
|
47
47
|
import { isParallelActive, shutdownParallel } from "./parallel-orchestrator.js";
|
|
48
48
|
import { DEFAULT_BASH_TIMEOUT_SECS } from "./constants.js";
|
|
49
|
+
import { markCmuxPromptShown, shouldPromptToEnableCmux } from "../cmux/index.js";
|
|
49
50
|
// ── Agent Instructions (DEPRECATED) ──────────────────────────────────────
|
|
50
51
|
// agent-instructions.md is deprecated. Use AGENTS.md or CLAUDE.md instead.
|
|
51
52
|
// Pi core natively supports AGENTS.md (with CLAUDE.md fallback) per directory.
|
|
@@ -532,6 +533,10 @@ export default function (pi) {
|
|
|
532
533
|
const stopContextTimer = debugTime("context-inject");
|
|
533
534
|
const systemContent = loadPrompt("system");
|
|
534
535
|
const loadedPreferences = loadEffectiveGSDPreferences();
|
|
536
|
+
if (shouldPromptToEnableCmux(loadedPreferences?.preferences)) {
|
|
537
|
+
markCmuxPromptShown();
|
|
538
|
+
ctx.ui.notify("cmux detected. Run /gsd cmux on to enable sidebar metadata, notifications, and visual subagent splits for this project.", "info");
|
|
539
|
+
}
|
|
535
540
|
let preferenceBlock = "";
|
|
536
541
|
if (loadedPreferences) {
|
|
537
542
|
const cwd = process.cwd();
|
|
@@ -17,8 +17,10 @@ import { gsdRoot } from "./paths.js";
|
|
|
17
17
|
import { getAndClearSkills } from "./skill-telemetry.js";
|
|
18
18
|
import { loadJsonFile, loadJsonFileOrNull, saveJsonFile } from "./json-persistence.js";
|
|
19
19
|
import { parseUnitId } from "./unit-id.js";
|
|
20
|
-
// Re-export from shared —
|
|
21
|
-
|
|
20
|
+
// Re-export from shared — import directly from format-utils to avoid pulling
|
|
21
|
+
// in the full barrel (mod.js → ui.js → @gsd/pi-tui) which breaks when loaded
|
|
22
|
+
// outside jiti's alias resolution (e.g. dynamic import in auto-loop reports).
|
|
23
|
+
export { formatTokenCount } from "../shared/format-utils.js";
|
|
22
24
|
export function classifyUnitPhase(unitType) {
|
|
23
25
|
switch (unitType) {
|
|
24
26
|
case "research-milestone":
|
|
@@ -2,13 +2,22 @@
|
|
|
2
2
|
// Cross-platform desktop notifications for auto-mode events.
|
|
3
3
|
import { execFileSync } from "node:child_process";
|
|
4
4
|
import { loadEffectiveGSDPreferences } from "./preferences.js";
|
|
5
|
+
import { CmuxClient, emitOsc777Notification, resolveCmuxConfig } from "../cmux/index.js";
|
|
5
6
|
/**
|
|
6
7
|
* Send a native desktop notification. Non-blocking, non-fatal.
|
|
7
8
|
* macOS: osascript, Linux: notify-send, Windows: skipped.
|
|
8
9
|
*/
|
|
9
10
|
export function sendDesktopNotification(title, message, level = "info", kind = "complete") {
|
|
10
|
-
|
|
11
|
+
const loaded = loadEffectiveGSDPreferences()?.preferences;
|
|
12
|
+
if (!shouldSendDesktopNotification(kind, loaded?.notifications))
|
|
11
13
|
return;
|
|
14
|
+
const cmux = resolveCmuxConfig(loaded);
|
|
15
|
+
if (cmux.notifications) {
|
|
16
|
+
const delivered = CmuxClient.fromPreferences(loaded).notify(title, message);
|
|
17
|
+
if (delivered)
|
|
18
|
+
return;
|
|
19
|
+
emitOsc777Notification(title, message);
|
|
20
|
+
}
|
|
12
21
|
try {
|
|
13
22
|
const command = buildDesktopNotificationCommand(process.platform, title, message, level);
|
|
14
23
|
if (!command)
|
|
@@ -47,6 +47,7 @@ export const KNOWN_PREFERENCE_KEYS = new Set([
|
|
|
47
47
|
"budget_enforcement",
|
|
48
48
|
"context_pause_threshold",
|
|
49
49
|
"notifications",
|
|
50
|
+
"cmux",
|
|
50
51
|
"remote_questions",
|
|
51
52
|
"git",
|
|
52
53
|
"post_unit_hooks",
|
|
@@ -63,6 +64,7 @@ export const KNOWN_PREFERENCE_KEYS = new Set([
|
|
|
63
64
|
"search_provider",
|
|
64
65
|
"compression_strategy",
|
|
65
66
|
"context_selection",
|
|
67
|
+
"widget_mode",
|
|
66
68
|
]);
|
|
67
69
|
/** Canonical list of all dispatch unit types. */
|
|
68
70
|
export const KNOWN_UNIT_TYPES = [
|
|
@@ -225,6 +225,35 @@ export function validatePreferences(preferences) {
|
|
|
225
225
|
errors.push("notifications must be an object");
|
|
226
226
|
}
|
|
227
227
|
}
|
|
228
|
+
// ─── Cmux ───────────────────────────────────────────────────────────────
|
|
229
|
+
if (preferences.cmux !== undefined) {
|
|
230
|
+
if (preferences.cmux && typeof preferences.cmux === "object") {
|
|
231
|
+
const cmux = preferences.cmux;
|
|
232
|
+
const validatedCmux = {};
|
|
233
|
+
if (cmux.enabled !== undefined)
|
|
234
|
+
validatedCmux.enabled = !!cmux.enabled;
|
|
235
|
+
if (cmux.notifications !== undefined)
|
|
236
|
+
validatedCmux.notifications = !!cmux.notifications;
|
|
237
|
+
if (cmux.sidebar !== undefined)
|
|
238
|
+
validatedCmux.sidebar = !!cmux.sidebar;
|
|
239
|
+
if (cmux.splits !== undefined)
|
|
240
|
+
validatedCmux.splits = !!cmux.splits;
|
|
241
|
+
if (cmux.browser !== undefined)
|
|
242
|
+
validatedCmux.browser = !!cmux.browser;
|
|
243
|
+
const knownCmuxKeys = new Set(["enabled", "notifications", "sidebar", "splits", "browser"]);
|
|
244
|
+
for (const key of Object.keys(cmux)) {
|
|
245
|
+
if (!knownCmuxKeys.has(key)) {
|
|
246
|
+
warnings.push(`unknown cmux key "${key}" — ignored`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
if (Object.keys(validatedCmux).length > 0) {
|
|
250
|
+
validated.cmux = validatedCmux;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
errors.push("cmux must be an object");
|
|
255
|
+
}
|
|
256
|
+
}
|
|
228
257
|
// ─── Remote Questions ───────────────────────────────────────────────
|
|
229
258
|
if (preferences.remote_questions !== undefined) {
|
|
230
259
|
if (preferences.remote_questions && typeof preferences.remote_questions === "object") {
|