gsd-pi 2.35.0 → 2.36.0-dev.d612764
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/README.md +3 -1
- package/dist/cli.js +7 -2
- package/dist/resource-loader.d.ts +1 -1
- package/dist/resource-loader.js +13 -1
- package/dist/resources/extensions/async-jobs/await-tool.js +0 -2
- package/dist/resources/extensions/async-jobs/job-manager.js +0 -6
- package/dist/resources/extensions/bg-shell/output-formatter.js +1 -19
- package/dist/resources/extensions/bg-shell/process-manager.js +0 -4
- package/dist/resources/extensions/bg-shell/types.js +0 -2
- package/dist/resources/extensions/cmux/index.js +321 -0
- package/dist/resources/extensions/context7/index.js +5 -0
- package/dist/resources/extensions/get-secrets-from-user.js +2 -30
- package/dist/resources/extensions/google-search/index.js +5 -0
- package/dist/resources/extensions/gsd/auto-dashboard.js +334 -104
- package/dist/resources/extensions/gsd/auto-dispatch.js +43 -1
- package/dist/resources/extensions/gsd/auto-loop.js +28 -3
- package/dist/resources/extensions/gsd/auto-model-selection.js +15 -3
- package/dist/resources/extensions/gsd/auto-recovery.js +35 -0
- package/dist/resources/extensions/gsd/auto-start.js +35 -2
- package/dist/resources/extensions/gsd/auto.js +75 -4
- package/dist/resources/extensions/gsd/commands-cmux.js +120 -0
- package/dist/resources/extensions/gsd/commands-handlers.js +2 -2
- package/dist/resources/extensions/gsd/commands-inspect.js +10 -3
- package/dist/resources/extensions/gsd/commands-prefs-wizard.js +1 -1
- package/dist/resources/extensions/gsd/commands-rate.js +31 -0
- package/dist/resources/extensions/gsd/commands.js +94 -2
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +25 -0
- package/dist/resources/extensions/gsd/doctor-environment.js +26 -17
- package/dist/resources/extensions/gsd/files.js +11 -2
- package/dist/resources/extensions/gsd/gitignore.js +54 -7
- package/dist/resources/extensions/gsd/guided-flow.js +8 -2
- package/dist/resources/extensions/gsd/health-widget-core.js +96 -0
- package/dist/resources/extensions/gsd/health-widget.js +97 -46
- package/dist/resources/extensions/gsd/index.js +31 -33
- package/dist/resources/extensions/gsd/migrate-external.js +55 -2
- package/dist/resources/extensions/gsd/milestone-ids.js +3 -2
- package/dist/resources/extensions/gsd/notifications.js +10 -1
- package/dist/resources/extensions/gsd/paths.js +74 -7
- package/dist/resources/extensions/gsd/post-unit-hooks.js +4 -1
- package/dist/resources/extensions/gsd/preferences-types.js +2 -0
- package/dist/resources/extensions/gsd/preferences-validation.js +45 -1
- package/dist/resources/extensions/gsd/preferences.js +15 -0
- package/dist/resources/extensions/gsd/prompts/complete-milestone.md +2 -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/prompts/validate-milestone.md +2 -0
- package/dist/resources/extensions/gsd/roadmap-mutations.js +55 -0
- package/dist/resources/extensions/gsd/session-lock.js +53 -2
- package/dist/resources/extensions/gsd/state.js +2 -1
- package/dist/resources/extensions/gsd/templates/plan.md +8 -0
- package/dist/resources/extensions/gsd/templates/preferences.md +6 -0
- package/dist/resources/extensions/gsd/worktree-resolver.js +12 -0
- package/dist/resources/extensions/remote-questions/remote-command.js +2 -22
- package/dist/resources/extensions/search-the-web/native-search.js +45 -4
- package/dist/resources/extensions/shared/mod.js +1 -1
- package/dist/resources/extensions/shared/sanitize.js +30 -0
- package/dist/resources/extensions/shared/terminal.js +5 -0
- package/dist/resources/extensions/subagent/index.js +186 -74
- package/dist/resources/skills/core-web-vitals/SKILL.md +1 -1
- package/dist/resources/skills/create-gsd-extension/workflows/debug-extension.md +1 -1
- package/dist/resources/skills/github-workflows/SKILL.md +0 -2
- package/dist/resources/skills/web-quality-audit/SKILL.md +0 -2
- package/package.json +2 -1
- package/packages/pi-agent-core/dist/agent.d.ts +10 -2
- package/packages/pi-agent-core/dist/agent.d.ts.map +1 -1
- package/packages/pi-agent-core/dist/agent.js +19 -8
- package/packages/pi-agent-core/dist/agent.js.map +1 -1
- package/packages/pi-agent-core/src/agent.ts +31 -10
- package/packages/pi-ai/dist/providers/openai-responses.js +1 -1
- package/packages/pi-ai/dist/providers/openai-responses.js.map +1 -1
- package/packages/pi-ai/src/providers/openai-responses.ts +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js +20 -4
- package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/resource-loader.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/resource-loader.js +13 -2
- package/packages/pi-coding-agent/dist/core/resource-loader.js.map +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/packages/pi-coding-agent/src/core/agent-session.ts +36 -12
- package/packages/pi-coding-agent/src/core/resource-loader.ts +13 -2
- 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/async-jobs/await-tool.ts +0 -2
- package/src/resources/extensions/async-jobs/job-manager.ts +0 -7
- package/src/resources/extensions/bg-shell/output-formatter.ts +0 -17
- package/src/resources/extensions/bg-shell/process-manager.ts +0 -4
- package/src/resources/extensions/bg-shell/types.ts +0 -12
- package/src/resources/extensions/cmux/index.ts +384 -0
- package/src/resources/extensions/context7/index.ts +7 -0
- package/src/resources/extensions/get-secrets-from-user.ts +2 -35
- package/src/resources/extensions/google-search/index.ts +7 -0
- package/src/resources/extensions/gsd/auto-dashboard.ts +363 -116
- package/src/resources/extensions/gsd/auto-dispatch.ts +49 -1
- package/src/resources/extensions/gsd/auto-loop.ts +64 -2
- package/src/resources/extensions/gsd/auto-model-selection.ts +23 -2
- package/src/resources/extensions/gsd/auto-recovery.ts +39 -0
- package/src/resources/extensions/gsd/auto-start.ts +42 -2
- package/src/resources/extensions/gsd/auto.ts +82 -3
- package/src/resources/extensions/gsd/commands-cmux.ts +143 -0
- package/src/resources/extensions/gsd/commands-handlers.ts +2 -2
- package/src/resources/extensions/gsd/commands-inspect.ts +10 -3
- package/src/resources/extensions/gsd/commands-prefs-wizard.ts +1 -1
- package/src/resources/extensions/gsd/commands-rate.ts +55 -0
- package/src/resources/extensions/gsd/commands.ts +97 -2
- package/src/resources/extensions/gsd/docs/preferences-reference.md +25 -0
- package/src/resources/extensions/gsd/doctor-environment.ts +26 -16
- package/src/resources/extensions/gsd/files.ts +12 -2
- package/src/resources/extensions/gsd/gitignore.ts +54 -7
- package/src/resources/extensions/gsd/guided-flow.ts +8 -2
- package/src/resources/extensions/gsd/health-widget-core.ts +129 -0
- package/src/resources/extensions/gsd/health-widget.ts +103 -59
- package/src/resources/extensions/gsd/index.ts +37 -32
- package/src/resources/extensions/gsd/migrate-external.ts +47 -2
- package/src/resources/extensions/gsd/milestone-ids.ts +3 -2
- package/src/resources/extensions/gsd/notifications.ts +10 -1
- package/src/resources/extensions/gsd/paths.ts +73 -7
- package/src/resources/extensions/gsd/post-unit-hooks.ts +5 -1
- package/src/resources/extensions/gsd/preferences-types.ts +13 -0
- package/src/resources/extensions/gsd/preferences-validation.ts +42 -1
- package/src/resources/extensions/gsd/preferences.ts +18 -1
- package/src/resources/extensions/gsd/prompts/complete-milestone.md +2 -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/prompts/validate-milestone.md +2 -0
- package/src/resources/extensions/gsd/roadmap-mutations.ts +66 -0
- package/src/resources/extensions/gsd/session-lock.ts +59 -2
- package/src/resources/extensions/gsd/state.ts +2 -1
- package/src/resources/extensions/gsd/templates/plan.md +8 -0
- 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/commands-inspect-open-db.test.ts +46 -0
- package/src/resources/extensions/gsd/tests/files-loadfile-eisdir.test.ts +20 -0
- package/src/resources/extensions/gsd/tests/gitignore-tracked-gsd.test.ts +214 -0
- package/src/resources/extensions/gsd/tests/health-widget.test.ts +158 -0
- package/src/resources/extensions/gsd/tests/paths.test.ts +113 -0
- package/src/resources/extensions/gsd/tests/preferences.test.ts +35 -2
- package/src/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +26 -0
- package/src/resources/extensions/gsd/tests/test-utils.ts +165 -0
- package/src/resources/extensions/gsd/tests/validate-directory.test.ts +15 -0
- package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +7 -0
- package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +32 -0
- package/src/resources/extensions/gsd/worktree-resolver.ts +11 -0
- package/src/resources/extensions/remote-questions/remote-command.ts +2 -23
- package/src/resources/extensions/search-the-web/native-search.ts +50 -4
- package/src/resources/extensions/shared/mod.ts +1 -1
- package/src/resources/extensions/shared/sanitize.ts +36 -0
- package/src/resources/extensions/shared/terminal.ts +5 -0
- package/src/resources/extensions/subagent/index.ts +242 -91
- package/src/resources/skills/core-web-vitals/SKILL.md +1 -1
- package/src/resources/skills/create-gsd-extension/workflows/debug-extension.md +1 -1
- package/src/resources/skills/github-workflows/SKILL.md +0 -2
- package/src/resources/skills/web-quality-audit/SKILL.md +0 -2
- package/dist/resources/extensions/shared/wizard-ui.js +0 -478
- package/dist/resources/skills/swiftui/SKILL.md +0 -208
- package/dist/resources/skills/swiftui/references/animations.md +0 -921
- package/dist/resources/skills/swiftui/references/architecture.md +0 -1561
- package/dist/resources/skills/swiftui/references/layout-system.md +0 -1186
- package/dist/resources/skills/swiftui/references/navigation.md +0 -1492
- package/dist/resources/skills/swiftui/references/networking-async.md +0 -214
- package/dist/resources/skills/swiftui/references/performance.md +0 -1706
- package/dist/resources/skills/swiftui/references/platform-integration.md +0 -204
- package/dist/resources/skills/swiftui/references/state-management.md +0 -1443
- package/dist/resources/skills/swiftui/references/swiftdata.md +0 -297
- package/dist/resources/skills/swiftui/references/testing-debugging.md +0 -247
- package/dist/resources/skills/swiftui/references/uikit-appkit-interop.md +0 -218
- package/dist/resources/skills/swiftui/workflows/add-feature.md +0 -191
- package/dist/resources/skills/swiftui/workflows/build-new-app.md +0 -311
- package/dist/resources/skills/swiftui/workflows/debug-swiftui.md +0 -192
- package/dist/resources/skills/swiftui/workflows/optimize-performance.md +0 -197
- package/dist/resources/skills/swiftui/workflows/ship-app.md +0 -203
- package/dist/resources/skills/swiftui/workflows/write-tests.md +0 -235
- package/src/resources/extensions/shared/wizard-ui.ts +0 -551
- package/src/resources/skills/swiftui/SKILL.md +0 -208
- package/src/resources/skills/swiftui/references/animations.md +0 -921
- package/src/resources/skills/swiftui/references/architecture.md +0 -1561
- package/src/resources/skills/swiftui/references/layout-system.md +0 -1186
- package/src/resources/skills/swiftui/references/navigation.md +0 -1492
- package/src/resources/skills/swiftui/references/networking-async.md +0 -214
- package/src/resources/skills/swiftui/references/performance.md +0 -1706
- package/src/resources/skills/swiftui/references/platform-integration.md +0 -204
- package/src/resources/skills/swiftui/references/state-management.md +0 -1443
- package/src/resources/skills/swiftui/references/swiftdata.md +0 -297
- package/src/resources/skills/swiftui/references/testing-debugging.md +0 -247
- package/src/resources/skills/swiftui/references/uikit-appkit-interop.md +0 -218
- package/src/resources/skills/swiftui/workflows/add-feature.md +0 -191
- package/src/resources/skills/swiftui/workflows/build-new-app.md +0 -311
- package/src/resources/skills/swiftui/workflows/debug-swiftui.md +0 -192
- package/src/resources/skills/swiftui/workflows/optimize-performance.md +0 -197
- package/src/resources/skills/swiftui/workflows/ship-app.md +0 -203
- package/src/resources/skills/swiftui/workflows/write-tests.md +0 -235
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
} from "./paths.js";
|
|
24
24
|
import { join } from "node:path";
|
|
25
25
|
import { readFileSync, existsSync, mkdirSync, readdirSync, rmSync, unlinkSync } from "node:fs";
|
|
26
|
+
import { readSessionLockData, isSessionLockProcessAlive } from "./session-lock.js";
|
|
26
27
|
import { nativeIsRepo, nativeInit } from "./native-git-bridge.js";
|
|
27
28
|
import { ensureGitignore, ensurePreferences, untrackRuntimeFiles } from "./gitignore.js";
|
|
28
29
|
import { loadEffectiveGSDPreferences } from "./preferences.js";
|
|
@@ -191,7 +192,7 @@ type UIContext = ExtensionContext;
|
|
|
191
192
|
* This is the only way the wizard triggers work — everything else is the LLM's job.
|
|
192
193
|
*/
|
|
193
194
|
function dispatchWorkflow(pi: ExtensionAPI, note: string, customType = "gsd-run"): void {
|
|
194
|
-
const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(process.env.HOME ?? "~", ".
|
|
195
|
+
const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(process.env.HOME ?? "~", ".gsd", "agent", "GSD-WORKFLOW.md");
|
|
195
196
|
const workflow = readFileSync(workflowPath, "utf-8");
|
|
196
197
|
|
|
197
198
|
pi.sendMessage(
|
|
@@ -516,8 +517,13 @@ export async function showDiscuss(
|
|
|
516
517
|
// If all pending slices are discussed, notify and exit instead of looping
|
|
517
518
|
const allDiscussed = pendingSlices.every(s => discussedMap.get(s.id));
|
|
518
519
|
if (allDiscussed) {
|
|
520
|
+
const lockData = readSessionLockData(basePath);
|
|
521
|
+
const remoteAutoRunning = lockData && lockData.pid !== process.pid && isSessionLockProcessAlive(lockData);
|
|
522
|
+
const nextStep = remoteAutoRunning
|
|
523
|
+
? "Auto-mode is already running — use /gsd status to check progress."
|
|
524
|
+
: "Run /gsd to start planning.";
|
|
519
525
|
ctx.ui.notify(
|
|
520
|
-
`All ${pendingSlices.length} slices discussed.
|
|
526
|
+
`All ${pendingSlices.length} slices discussed. ${nextStep}`,
|
|
521
527
|
"info",
|
|
522
528
|
);
|
|
523
529
|
return;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure GSD health widget logic.
|
|
3
|
+
*
|
|
4
|
+
* Separates project-state detection and line rendering from the widget's
|
|
5
|
+
* runtime integrations so the regressions can be tested directly.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
9
|
+
import { gsdRoot } from "./paths.js";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import type { GSDState, Phase } from "./types.js";
|
|
12
|
+
|
|
13
|
+
export type HealthWidgetProjectState = "none" | "initialized" | "active";
|
|
14
|
+
|
|
15
|
+
export interface HealthWidgetData {
|
|
16
|
+
projectState: HealthWidgetProjectState;
|
|
17
|
+
budgetCeiling: number | undefined;
|
|
18
|
+
budgetSpent: number;
|
|
19
|
+
providerIssue: string | null;
|
|
20
|
+
environmentErrorCount: number;
|
|
21
|
+
environmentWarningCount: number;
|
|
22
|
+
lastRefreshed: number;
|
|
23
|
+
executionPhase?: Phase;
|
|
24
|
+
executionStatus?: string;
|
|
25
|
+
executionTarget?: string;
|
|
26
|
+
nextAction?: string;
|
|
27
|
+
blocker?: string | null;
|
|
28
|
+
activeMilestoneId?: string;
|
|
29
|
+
activeSliceId?: string;
|
|
30
|
+
activeTaskId?: string;
|
|
31
|
+
progress?: GSDState["progress"];
|
|
32
|
+
eta?: string | null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function detectHealthWidgetProjectState(basePath: string): HealthWidgetProjectState {
|
|
36
|
+
const root = gsdRoot(basePath);
|
|
37
|
+
if (!existsSync(root)) return "none";
|
|
38
|
+
|
|
39
|
+
// Lightweight milestone count — avoids the full detectProjectState() scan
|
|
40
|
+
// (CI markers, Makefile targets, etc.) that is unnecessary on the 60s refresh.
|
|
41
|
+
try {
|
|
42
|
+
const milestonesDir = join(root, "milestones");
|
|
43
|
+
if (existsSync(milestonesDir)) {
|
|
44
|
+
const entries = readdirSync(milestonesDir, { withFileTypes: true });
|
|
45
|
+
if (entries.some(e => e.isDirectory())) return "active";
|
|
46
|
+
}
|
|
47
|
+
} catch { /* non-fatal */ }
|
|
48
|
+
|
|
49
|
+
return "initialized";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function formatCost(n: number): string {
|
|
53
|
+
return n >= 1 ? `$${n.toFixed(2)}` : `${(n * 100).toFixed(1)}¢`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function formatProgress(progress?: GSDState["progress"]): string | null {
|
|
57
|
+
if (!progress) return null;
|
|
58
|
+
|
|
59
|
+
const parts: string[] = [];
|
|
60
|
+
parts.push(`M ${progress.milestones.done}/${progress.milestones.total}`);
|
|
61
|
+
if (progress.slices) parts.push(`S ${progress.slices.done}/${progress.slices.total}`);
|
|
62
|
+
if (progress.tasks) parts.push(`T ${progress.tasks.done}/${progress.tasks.total}`);
|
|
63
|
+
return parts.length > 0 ? `Progress: ${parts.join(" · ")}` : null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function formatEnvironmentSummary(errorCount: number, warningCount: number): string | null {
|
|
67
|
+
if (errorCount <= 0 && warningCount <= 0) return null;
|
|
68
|
+
|
|
69
|
+
const parts: string[] = [];
|
|
70
|
+
if (errorCount > 0) parts.push(`${errorCount} error${errorCount > 1 ? "s" : ""}`);
|
|
71
|
+
if (warningCount > 0) parts.push(`${warningCount} warning${warningCount > 1 ? "s" : ""}`);
|
|
72
|
+
return `Env: ${parts.join(", ")}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function formatBudgetSummary(data: HealthWidgetData): string | null {
|
|
76
|
+
if (data.budgetCeiling !== undefined && data.budgetCeiling > 0) {
|
|
77
|
+
const pct = Math.min(100, (data.budgetSpent / data.budgetCeiling) * 100);
|
|
78
|
+
return `Budget: ${formatCost(data.budgetSpent)}/${formatCost(data.budgetCeiling)} (${pct.toFixed(0)}%)`;
|
|
79
|
+
}
|
|
80
|
+
if (data.budgetSpent > 0) {
|
|
81
|
+
return `Spent: ${formatCost(data.budgetSpent)}`;
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function buildExecutionHeadline(data: HealthWidgetData): string {
|
|
87
|
+
const status = data.executionStatus ?? "Active project";
|
|
88
|
+
const target = data.executionTarget ?? data.blocker ?? "loading status…";
|
|
89
|
+
return ` GSD ${status}${target ? ` - ${target}` : ""}`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Build compact health lines for the widget.
|
|
94
|
+
* Returns a string array suitable for setWidget().
|
|
95
|
+
*/
|
|
96
|
+
export function buildHealthLines(data: HealthWidgetData): string[] {
|
|
97
|
+
if (data.projectState === "none") {
|
|
98
|
+
return [" GSD No project loaded — run /gsd to start"];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (data.projectState === "initialized") {
|
|
102
|
+
return [" GSD Project initialized — run /gsd to continue setup"];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const lines = [buildExecutionHeadline(data)];
|
|
106
|
+
const details: string[] = [];
|
|
107
|
+
|
|
108
|
+
const progress = formatProgress(data.progress);
|
|
109
|
+
if (progress) details.push(progress);
|
|
110
|
+
|
|
111
|
+
if (data.providerIssue) details.push(data.providerIssue);
|
|
112
|
+
|
|
113
|
+
const environment = formatEnvironmentSummary(
|
|
114
|
+
data.environmentErrorCount,
|
|
115
|
+
data.environmentWarningCount,
|
|
116
|
+
);
|
|
117
|
+
if (environment) details.push(environment);
|
|
118
|
+
|
|
119
|
+
const budget = formatBudgetSummary(data);
|
|
120
|
+
if (budget) details.push(budget);
|
|
121
|
+
|
|
122
|
+
if (data.eta) details.push(data.eta);
|
|
123
|
+
|
|
124
|
+
if (details.length > 0) {
|
|
125
|
+
lines.push(` ${details.join(" │ ")}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return lines;
|
|
129
|
+
}
|
|
@@ -9,41 +9,37 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import type { ExtensionContext } from "@gsd/pi-coding-agent";
|
|
12
|
+
import type { GSDState } from "./types.js";
|
|
12
13
|
import { runProviderChecks, summariseProviderIssues } from "./doctor-providers.js";
|
|
13
14
|
import { runEnvironmentChecks } from "./doctor-environment.js";
|
|
14
15
|
import { loadEffectiveGSDPreferences } from "./preferences.js";
|
|
15
16
|
import { loadLedgerFromDisk, getProjectTotals } from "./metrics.js";
|
|
17
|
+
import { describeNextUnit, estimateTimeRemaining, updateSliceProgressCache } from "./auto-dashboard.js";
|
|
16
18
|
import { projectRoot } from "./commands.js";
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
budgetSpent: number;
|
|
24
|
-
providerIssue: string | null; // compact summary from summariseProviderIssues()
|
|
25
|
-
environmentErrorCount: number;
|
|
26
|
-
environmentWarningCount: number;
|
|
27
|
-
lastRefreshed: number;
|
|
28
|
-
}
|
|
19
|
+
import { deriveState, invalidateStateCache } from "./state.js";
|
|
20
|
+
import {
|
|
21
|
+
buildHealthLines,
|
|
22
|
+
detectHealthWidgetProjectState,
|
|
23
|
+
type HealthWidgetData,
|
|
24
|
+
} from "./health-widget-core.js";
|
|
29
25
|
|
|
30
26
|
// ── Data loader ────────────────────────────────────────────────────────────────
|
|
31
27
|
|
|
32
|
-
function
|
|
33
|
-
let hasProject = false;
|
|
28
|
+
function loadBaseHealthWidgetData(basePath: string): HealthWidgetData {
|
|
34
29
|
let budgetCeiling: number | undefined;
|
|
35
30
|
let budgetSpent = 0;
|
|
36
31
|
let providerIssue: string | null = null;
|
|
37
32
|
let environmentErrorCount = 0;
|
|
38
33
|
let environmentWarningCount = 0;
|
|
39
34
|
|
|
35
|
+
const projectState = detectHealthWidgetProjectState(basePath);
|
|
36
|
+
|
|
40
37
|
try {
|
|
41
38
|
const prefs = loadEffectiveGSDPreferences();
|
|
42
39
|
budgetCeiling = prefs?.preferences?.budget_ceiling;
|
|
43
40
|
|
|
44
41
|
const ledger = loadLedgerFromDisk(basePath);
|
|
45
42
|
if (ledger) {
|
|
46
|
-
hasProject = true;
|
|
47
43
|
const totals = getProjectTotals(ledger.units ?? []);
|
|
48
44
|
budgetSpent = totals.cost;
|
|
49
45
|
}
|
|
@@ -63,7 +59,7 @@ function loadHealthWidgetData(basePath: string): HealthWidgetData {
|
|
|
63
59
|
} catch { /* non-fatal */ }
|
|
64
60
|
|
|
65
61
|
return {
|
|
66
|
-
|
|
62
|
+
projectState,
|
|
67
63
|
budgetCeiling,
|
|
68
64
|
budgetSpent,
|
|
69
65
|
providerIssue,
|
|
@@ -73,54 +69,88 @@ function loadHealthWidgetData(basePath: string): HealthWidgetData {
|
|
|
73
69
|
};
|
|
74
70
|
}
|
|
75
71
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
return
|
|
72
|
+
function compactText(text: string, max = 64): string {
|
|
73
|
+
const trimmed = text.replace(/\s+/g, " ").trim();
|
|
74
|
+
if (trimmed.length <= max) return trimmed;
|
|
75
|
+
return `${trimmed.slice(0, max - 1).trimEnd()}…`;
|
|
80
76
|
}
|
|
81
77
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
78
|
+
function summarizeExecutionStatus(state: GSDState): string {
|
|
79
|
+
switch (state.phase) {
|
|
80
|
+
case "blocked": return "Blocked";
|
|
81
|
+
case "paused": return "Paused";
|
|
82
|
+
case "complete": return "Complete";
|
|
83
|
+
case "executing": return "Executing";
|
|
84
|
+
case "planning": return "Planning";
|
|
85
|
+
case "pre-planning": return "Pre-planning";
|
|
86
|
+
case "summarizing": return "Summarizing";
|
|
87
|
+
case "validating-milestone": return "Validating";
|
|
88
|
+
case "completing-milestone": return "Completing";
|
|
89
|
+
case "needs-discussion": return "Needs discussion";
|
|
90
|
+
case "replanning-slice": return "Replanning";
|
|
91
|
+
default: return "Active";
|
|
89
92
|
}
|
|
93
|
+
}
|
|
90
94
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
95
|
+
function summarizeExecutionTarget(state: GSDState): string {
|
|
96
|
+
switch (state.phase) {
|
|
97
|
+
case "needs-discussion":
|
|
98
|
+
return state.activeMilestone ? `Discuss ${state.activeMilestone.id}` : "Discuss milestone draft";
|
|
99
|
+
case "pre-planning":
|
|
100
|
+
return state.activeMilestone ? `Plan ${state.activeMilestone.id}` : "Research & plan milestone";
|
|
101
|
+
case "planning":
|
|
102
|
+
return state.activeSlice ? `Plan ${state.activeSlice.id}` : "Plan next slice";
|
|
103
|
+
case "executing":
|
|
104
|
+
return state.activeTask ? `Execute ${state.activeTask.id}` : "Execute next task";
|
|
105
|
+
case "summarizing":
|
|
106
|
+
return state.activeSlice ? `Complete ${state.activeSlice.id}` : "Complete current slice";
|
|
107
|
+
case "validating-milestone":
|
|
108
|
+
return state.activeMilestone ? `Validate ${state.activeMilestone.id}` : "Validate milestone";
|
|
109
|
+
case "completing-milestone":
|
|
110
|
+
return state.activeMilestone ? `Complete ${state.activeMilestone.id}` : "Complete milestone";
|
|
111
|
+
case "replanning-slice":
|
|
112
|
+
return state.activeSlice ? `Replan ${state.activeSlice.id}` : "Replan current slice";
|
|
113
|
+
case "blocked":
|
|
114
|
+
return `waiting on ${compactText(state.blockers[0] ?? state.nextAction, 56)}`;
|
|
115
|
+
case "paused":
|
|
116
|
+
return compactText(state.nextAction || "waiting to resume", 56);
|
|
117
|
+
case "complete":
|
|
118
|
+
return "All milestones complete";
|
|
119
|
+
default:
|
|
120
|
+
return compactText(describeNextUnit(state).label, 56);
|
|
101
121
|
}
|
|
122
|
+
}
|
|
102
123
|
|
|
103
|
-
|
|
104
|
-
if (
|
|
105
|
-
const pct = Math.min(100, (data.budgetSpent / data.budgetCeiling) * 100);
|
|
106
|
-
parts.push(`Budget: ${formatCost(data.budgetSpent)}/${formatCost(data.budgetCeiling)} (${pct.toFixed(0)}%)`);
|
|
107
|
-
} else if (data.budgetSpent > 0) {
|
|
108
|
-
parts.push(`Spent: ${formatCost(data.budgetSpent)}`);
|
|
109
|
-
}
|
|
124
|
+
async function enrichHealthWidgetData(basePath: string, baseData: HealthWidgetData): Promise<HealthWidgetData> {
|
|
125
|
+
if (baseData.projectState !== "active") return baseData;
|
|
110
126
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
}
|
|
127
|
+
try {
|
|
128
|
+
invalidateStateCache();
|
|
129
|
+
const state = await deriveState(basePath);
|
|
115
130
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
parts.push(`Env: ${data.environmentWarningCount} warning${data.environmentWarningCount > 1 ? "s" : ""}`);
|
|
121
|
-
}
|
|
131
|
+
if (state.activeMilestone) {
|
|
132
|
+
// Warm the slice-progress cache so estimateTimeRemaining() has data
|
|
133
|
+
updateSliceProgressCache(basePath, state.activeMilestone.id, state.activeSlice?.id);
|
|
134
|
+
}
|
|
122
135
|
|
|
123
|
-
|
|
136
|
+
return {
|
|
137
|
+
...baseData,
|
|
138
|
+
executionPhase: state.phase,
|
|
139
|
+
executionStatus: summarizeExecutionStatus(state),
|
|
140
|
+
executionTarget: summarizeExecutionTarget(state),
|
|
141
|
+
nextAction: state.nextAction,
|
|
142
|
+
blocker: state.blockers[0] ?? null,
|
|
143
|
+
activeMilestoneId: state.activeMilestone?.id,
|
|
144
|
+
activeSliceId: state.activeSlice?.id,
|
|
145
|
+
activeTaskId: state.activeTask?.id,
|
|
146
|
+
progress: state.progress,
|
|
147
|
+
eta: state.phase === "blocked" || state.phase === "paused" || state.phase === "complete"
|
|
148
|
+
? null
|
|
149
|
+
: estimateTimeRemaining(),
|
|
150
|
+
};
|
|
151
|
+
} catch {
|
|
152
|
+
return baseData;
|
|
153
|
+
}
|
|
124
154
|
}
|
|
125
155
|
|
|
126
156
|
// ── Widget init ────────────────────────────────────────────────────────────────
|
|
@@ -137,20 +167,34 @@ export function initHealthWidget(ctx: ExtensionContext): void {
|
|
|
137
167
|
const basePath = projectRoot();
|
|
138
168
|
|
|
139
169
|
// String-array fallback — used in RPC mode (factory is a no-op there)
|
|
140
|
-
const initialData =
|
|
170
|
+
const initialData = loadBaseHealthWidgetData(basePath);
|
|
141
171
|
ctx.ui.setWidget("gsd-health", buildHealthLines(initialData), { placement: "belowEditor" });
|
|
142
172
|
|
|
143
173
|
// Factory-based widget for TUI mode — replaces the string-array above
|
|
144
174
|
ctx.ui.setWidget("gsd-health", (_tui, _theme) => {
|
|
145
175
|
let data = initialData;
|
|
146
176
|
let cachedLines: string[] | undefined;
|
|
177
|
+
let refreshInFlight = false;
|
|
147
178
|
|
|
148
|
-
const
|
|
179
|
+
const refresh = async () => {
|
|
180
|
+
if (refreshInFlight) return;
|
|
181
|
+
refreshInFlight = true;
|
|
149
182
|
try {
|
|
150
|
-
|
|
183
|
+
const baseData = loadBaseHealthWidgetData(basePath);
|
|
184
|
+
data = await enrichHealthWidgetData(basePath, baseData);
|
|
151
185
|
cachedLines = undefined;
|
|
152
186
|
_tui.requestRender();
|
|
153
|
-
} catch { /* non-fatal */ }
|
|
187
|
+
} catch { /* non-fatal */ } finally {
|
|
188
|
+
refreshInFlight = false;
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
// Fire first enrichment immediately. requestRender() inside is a no-op
|
|
193
|
+
// if the widget has not yet rendered, so this is safe before factory return.
|
|
194
|
+
void refresh();
|
|
195
|
+
|
|
196
|
+
const refreshTimer = setInterval(() => {
|
|
197
|
+
void refresh();
|
|
154
198
|
}, REFRESH_INTERVAL_MS);
|
|
155
199
|
|
|
156
200
|
return {
|
|
@@ -65,33 +65,26 @@ import { pauseAutoForProviderError, classifyProviderError } from "./provider-err
|
|
|
65
65
|
import { toPosixPath } from "../shared/mod.js";
|
|
66
66
|
import { isParallelActive, shutdownParallel } from "./parallel-orchestrator.js";
|
|
67
67
|
import { DEFAULT_BASH_TIMEOUT_SECS } from "./constants.js";
|
|
68
|
+
import { markCmuxPromptShown, shouldPromptToEnableCmux } from "../cmux/index.js";
|
|
68
69
|
|
|
69
|
-
// ── Agent Instructions
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
// Both are loaded and concatenated (global first, project appends).
|
|
70
|
+
// ── Agent Instructions (DEPRECATED) ──────────────────────────────────────
|
|
71
|
+
// agent-instructions.md is deprecated. Use AGENTS.md or CLAUDE.md instead.
|
|
72
|
+
// Pi core natively supports AGENTS.md (with CLAUDE.md fallback) per directory.
|
|
73
73
|
|
|
74
|
-
function
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
try {
|
|
88
|
-
const content = readFileSync(projectPath, "utf-8").trim();
|
|
89
|
-
if (content) parts.push(content);
|
|
90
|
-
} catch { /* non-fatal — skip unreadable file */ }
|
|
74
|
+
function warnDeprecatedAgentInstructions(): void {
|
|
75
|
+
const paths = [
|
|
76
|
+
join(homedir(), ".gsd", "agent-instructions.md"),
|
|
77
|
+
join(process.cwd(), ".gsd", "agent-instructions.md"),
|
|
78
|
+
];
|
|
79
|
+
for (const p of paths) {
|
|
80
|
+
if (existsSync(p)) {
|
|
81
|
+
console.warn(
|
|
82
|
+
`[GSD] DEPRECATED: ${p} is no longer loaded. ` +
|
|
83
|
+
`Migrate your instructions to AGENTS.md (or CLAUDE.md) in the same directory. ` +
|
|
84
|
+
`See https://github.com/gsd-build/GSD-2/issues/1492`,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
91
87
|
}
|
|
92
|
-
|
|
93
|
-
if (parts.length === 0) return null;
|
|
94
|
-
return parts.join("\n\n");
|
|
95
88
|
}
|
|
96
89
|
|
|
97
90
|
// ── Depth verification state ──────────────────────────────────────────────
|
|
@@ -175,7 +168,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
175
168
|
// Pipe closed — nothing we can write; just exit cleanly
|
|
176
169
|
process.exit(0);
|
|
177
170
|
}
|
|
178
|
-
|
|
171
|
+
if ((err as NodeJS.ErrnoException).code === "ENOENT" &&
|
|
172
|
+
(err as any).syscall?.startsWith("spawn")) {
|
|
173
|
+
// spawn ENOENT — command not found (e.g., npx on Windows).
|
|
174
|
+
// This surfaces as an uncaught exception from child_process but
|
|
175
|
+
// is not a fatal process error. Log and continue instead of
|
|
176
|
+
// crashing auto-mode (#1384).
|
|
177
|
+
process.stderr.write(`[gsd] spawn ENOENT: ${(err as any).path ?? "unknown"} — command not found\n`);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
// Re-throw anything that isn't EPIPE/ENOENT so real crashes still surface
|
|
179
181
|
throw err;
|
|
180
182
|
};
|
|
181
183
|
process.on("uncaughtException", _gsdEpipeGuard);
|
|
@@ -622,6 +624,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
622
624
|
const stopContextTimer = debugTime("context-inject");
|
|
623
625
|
const systemContent = loadPrompt("system");
|
|
624
626
|
const loadedPreferences = loadEffectiveGSDPreferences();
|
|
627
|
+
if (shouldPromptToEnableCmux(loadedPreferences?.preferences)) {
|
|
628
|
+
markCmuxPromptShown();
|
|
629
|
+
ctx.ui.notify(
|
|
630
|
+
"cmux detected. Run /gsd cmux on to enable sidebar metadata, notifications, and visual subagent splits for this project.",
|
|
631
|
+
"info",
|
|
632
|
+
);
|
|
633
|
+
}
|
|
625
634
|
let preferenceBlock = "";
|
|
626
635
|
if (loadedPreferences) {
|
|
627
636
|
const cwd = process.cwd();
|
|
@@ -673,12 +682,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
673
682
|
}
|
|
674
683
|
}
|
|
675
684
|
|
|
676
|
-
//
|
|
677
|
-
|
|
678
|
-
const agentInstructions = loadAgentInstructions();
|
|
679
|
-
if (agentInstructions) {
|
|
680
|
-
agentInstructionsBlock = `\n\n## Agent Instructions\n\nThe following instructions were provided by the user and must be followed in every session:\n\n${agentInstructions}`;
|
|
681
|
-
}
|
|
685
|
+
// Warn if deprecated agent-instructions.md files are still present
|
|
686
|
+
warnDeprecatedAgentInstructions();
|
|
682
687
|
|
|
683
688
|
const injection = await buildGuidedExecuteContextInjection(event.prompt, process.cwd());
|
|
684
689
|
|
|
@@ -723,7 +728,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
723
728
|
].join("\n");
|
|
724
729
|
}
|
|
725
730
|
|
|
726
|
-
const fullSystem = `${event.systemPrompt}\n\n[SYSTEM CONTEXT — GSD]\n\n${systemContent}${preferenceBlock}${
|
|
731
|
+
const fullSystem = `${event.systemPrompt}\n\n[SYSTEM CONTEXT — GSD]\n\n${systemContent}${preferenceBlock}${knowledgeBlock}${memoryBlock}${newSkillsBlock}${worktreeBlock}`;
|
|
727
732
|
stopContextTimer({
|
|
728
733
|
systemPromptSize: fullSystem.length,
|
|
729
734
|
injectionSize: injection?.length ?? 0,
|
|
@@ -6,10 +6,11 @@
|
|
|
6
6
|
* symlink replaces the original directory so all paths remain valid.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { existsSync, lstatSync, mkdirSync, readdirSync, renameSync, cpSync, rmSync, symlinkSync } from "node:fs";
|
|
9
|
+
import { existsSync, lstatSync, mkdirSync, readdirSync, realpathSync, renameSync, cpSync, rmSync, symlinkSync } from "node:fs";
|
|
10
10
|
import { join } from "node:path";
|
|
11
11
|
import { externalGsdRoot } from "./repo-identity.js";
|
|
12
12
|
import { getErrorMessage } from "./error-utils.js";
|
|
13
|
+
import { hasGitTrackedGsdFiles } from "./gitignore.js";
|
|
13
14
|
|
|
14
15
|
export interface MigrationResult {
|
|
15
16
|
migrated: boolean;
|
|
@@ -51,6 +52,28 @@ export function migrateToExternalState(basePath: string): MigrationResult {
|
|
|
51
52
|
return { migrated: false, error: `Cannot stat .gsd: ${getErrorMessage(err)}` };
|
|
52
53
|
}
|
|
53
54
|
|
|
55
|
+
// Skip if .gsd/ contains git-tracked files — the project intentionally
|
|
56
|
+
// keeps .gsd/ in version control and migration would destroy that.
|
|
57
|
+
if (hasGitTrackedGsdFiles(basePath)) {
|
|
58
|
+
return { migrated: false };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Skip if .gsd/worktrees/ has active worktree directories (#1337).
|
|
62
|
+
// On Windows, active git worktrees hold OS-level directory handles that
|
|
63
|
+
// prevent rename/delete. Attempting migration causes EBUSY and data loss.
|
|
64
|
+
const worktreesDir = join(localGsd, "worktrees");
|
|
65
|
+
if (existsSync(worktreesDir)) {
|
|
66
|
+
try {
|
|
67
|
+
const entries = readdirSync(worktreesDir, { withFileTypes: true });
|
|
68
|
+
if (entries.some(e => e.isDirectory())) {
|
|
69
|
+
return { migrated: false };
|
|
70
|
+
}
|
|
71
|
+
} catch {
|
|
72
|
+
// Can't read worktrees dir — skip migration to be safe
|
|
73
|
+
return { migrated: false };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
54
77
|
const externalPath = externalGsdRoot(basePath);
|
|
55
78
|
const migratingPath = join(basePath, ".gsd.migrating");
|
|
56
79
|
|
|
@@ -99,7 +122,29 @@ export function migrateToExternalState(basePath: string): MigrationResult {
|
|
|
99
122
|
// Create symlink .gsd -> external path
|
|
100
123
|
symlinkSync(externalPath, localGsd, "junction");
|
|
101
124
|
|
|
102
|
-
//
|
|
125
|
+
// Verify the symlink resolves correctly before removing the backup (#1377).
|
|
126
|
+
// On Windows, junction creation can silently succeed but resolve to the wrong
|
|
127
|
+
// target, or the external dir may not be accessible. If verification fails,
|
|
128
|
+
// restore from the backup.
|
|
129
|
+
try {
|
|
130
|
+
const resolved = realpathSync(localGsd);
|
|
131
|
+
const resolvedExternal = realpathSync(externalPath);
|
|
132
|
+
if (resolved !== resolvedExternal) {
|
|
133
|
+
// Symlink points to wrong target — restore backup
|
|
134
|
+
try { rmSync(localGsd, { force: true }); } catch { /* may not exist */ }
|
|
135
|
+
renameSync(migratingPath, localGsd);
|
|
136
|
+
return { migrated: false, error: `Migration verification failed: symlink resolves to ${resolved}, expected ${resolvedExternal}` };
|
|
137
|
+
}
|
|
138
|
+
// Verify we can read through the symlink
|
|
139
|
+
readdirSync(localGsd);
|
|
140
|
+
} catch (verifyErr) {
|
|
141
|
+
// Symlink broken or unreadable — restore backup
|
|
142
|
+
try { rmSync(localGsd, { force: true }); } catch { /* may not exist */ }
|
|
143
|
+
try { renameSync(migratingPath, localGsd); } catch { /* best-effort restore */ }
|
|
144
|
+
return { migrated: false, error: `Migration verification failed: ${getErrorMessage(verifyErr)}` };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Remove .gsd.migrating only after symlink is verified
|
|
103
148
|
rmSync(migratingPath, { recursive: true, force: true });
|
|
104
149
|
|
|
105
150
|
return { migrated: true };
|
|
@@ -80,8 +80,9 @@ export function findMilestoneIds(basePath: string): string[] {
|
|
|
80
80
|
.filter((d) => d.isDirectory())
|
|
81
81
|
.map((d) => {
|
|
82
82
|
const match = d.name.match(/^(M\d+(?:-[a-z0-9]{6})?)/);
|
|
83
|
-
return match ? match[1] :
|
|
84
|
-
})
|
|
83
|
+
return match ? match[1] : null;
|
|
84
|
+
})
|
|
85
|
+
.filter((id): id is string => id !== null);
|
|
85
86
|
|
|
86
87
|
// Apply custom queue order if available, else fall back to numeric sort
|
|
87
88
|
const customOrder = loadQueueOrder(basePath);
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { execFileSync } from "node:child_process";
|
|
5
5
|
import type { NotificationPreferences } from "./types.js";
|
|
6
6
|
import { loadEffectiveGSDPreferences } from "./preferences.js";
|
|
7
|
+
import { CmuxClient, emitOsc777Notification, resolveCmuxConfig } from "../cmux/index.js";
|
|
7
8
|
|
|
8
9
|
export type NotifyLevel = "info" | "success" | "warning" | "error";
|
|
9
10
|
export type NotificationKind = "complete" | "error" | "budget" | "milestone" | "attention";
|
|
@@ -23,7 +24,15 @@ export function sendDesktopNotification(
|
|
|
23
24
|
level: NotifyLevel = "info",
|
|
24
25
|
kind: NotificationKind = "complete",
|
|
25
26
|
): void {
|
|
26
|
-
|
|
27
|
+
const loaded = loadEffectiveGSDPreferences()?.preferences;
|
|
28
|
+
if (!shouldSendDesktopNotification(kind, loaded?.notifications)) return;
|
|
29
|
+
|
|
30
|
+
const cmux = resolveCmuxConfig(loaded);
|
|
31
|
+
if (cmux.notifications) {
|
|
32
|
+
const delivered = CmuxClient.fromPreferences(loaded).notify(title, message);
|
|
33
|
+
if (delivered) return;
|
|
34
|
+
emitOsc777Notification(title, message);
|
|
35
|
+
}
|
|
27
36
|
|
|
28
37
|
try {
|
|
29
38
|
const command = buildDesktopNotificationCommand(process.platform, title, message, level);
|