gsd-pi 2.36.0-dev.d612764 → 2.36.0-dev.f887f4e
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/resources/extensions/gsd/auto-dashboard.js +104 -334
- package/dist/resources/extensions/gsd/auto-loop.js +0 -11
- package/dist/resources/extensions/gsd/auto.js +0 -16
- package/dist/resources/extensions/gsd/commands-prefs-wizard.js +1 -1
- package/dist/resources/extensions/gsd/commands.js +1 -51
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +0 -25
- package/dist/resources/extensions/gsd/index.js +0 -5
- package/dist/resources/extensions/gsd/notifications.js +1 -10
- package/dist/resources/extensions/gsd/preferences-types.js +0 -2
- package/dist/resources/extensions/gsd/preferences-validation.js +0 -29
- package/dist/resources/extensions/gsd/preferences.js +0 -3
- package/dist/resources/extensions/gsd/prompts/research-milestone.md +3 -4
- package/dist/resources/extensions/gsd/prompts/research-slice.md +2 -3
- package/dist/resources/extensions/gsd/templates/preferences.md +0 -6
- package/dist/resources/extensions/search-the-web/native-search.js +4 -45
- package/dist/resources/extensions/shared/terminal.js +0 -5
- package/dist/resources/extensions/subagent/index.js +60 -180
- package/package.json +1 -1
- package/packages/pi-tui/dist/terminal-image.d.ts.map +1 -1
- package/packages/pi-tui/dist/terminal-image.js +0 -4
- package/packages/pi-tui/dist/terminal-image.js.map +1 -1
- package/packages/pi-tui/src/terminal-image.ts +0 -5
- package/src/resources/extensions/gsd/auto-dashboard.ts +116 -363
- package/src/resources/extensions/gsd/auto-loop.ts +0 -42
- package/src/resources/extensions/gsd/auto.ts +0 -21
- package/src/resources/extensions/gsd/commands-prefs-wizard.ts +1 -1
- package/src/resources/extensions/gsd/commands.ts +1 -54
- package/src/resources/extensions/gsd/docs/preferences-reference.md +0 -25
- package/src/resources/extensions/gsd/index.ts +0 -8
- package/src/resources/extensions/gsd/notifications.ts +1 -10
- package/src/resources/extensions/gsd/preferences-types.ts +0 -13
- package/src/resources/extensions/gsd/preferences-validation.ts +0 -26
- package/src/resources/extensions/gsd/preferences.ts +0 -4
- package/src/resources/extensions/gsd/prompts/research-milestone.md +3 -4
- package/src/resources/extensions/gsd/prompts/research-slice.md +2 -3
- package/src/resources/extensions/gsd/templates/preferences.md +0 -6
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +0 -2
- package/src/resources/extensions/gsd/tests/preferences.test.ts +0 -23
- package/src/resources/extensions/search-the-web/native-search.ts +4 -50
- package/src/resources/extensions/shared/terminal.ts +0 -5
- package/src/resources/extensions/subagent/index.ts +79 -236
- package/dist/resources/extensions/cmux/index.js +0 -321
- package/dist/resources/extensions/gsd/commands-cmux.js +0 -120
- package/src/resources/extensions/cmux/index.ts +0 -384
- package/src/resources/extensions/gsd/commands-cmux.ts +0 -143
- package/src/resources/extensions/gsd/tests/cmux.test.ts +0 -98
|
@@ -1,384 +0,0 @@
|
|
|
1
|
-
import { execFile, execFileSync } from "node:child_process";
|
|
2
|
-
import { existsSync } from "node:fs";
|
|
3
|
-
import { promisify } from "node:util";
|
|
4
|
-
import type { GSDPreferences } from "../gsd/preferences.js";
|
|
5
|
-
import type { GSDState, Phase } from "../gsd/types.js";
|
|
6
|
-
|
|
7
|
-
const execFileAsync = promisify(execFile);
|
|
8
|
-
const DEFAULT_SOCKET_PATH = "/tmp/cmux.sock";
|
|
9
|
-
const STATUS_KEY = "gsd";
|
|
10
|
-
const lastSidebarSnapshots = new Map<string, string>();
|
|
11
|
-
let cmuxPromptedThisSession = false;
|
|
12
|
-
let cachedCliAvailability: boolean | null = null;
|
|
13
|
-
|
|
14
|
-
export interface CmuxEnvironment {
|
|
15
|
-
available: boolean;
|
|
16
|
-
cliAvailable: boolean;
|
|
17
|
-
socketPath: string;
|
|
18
|
-
workspaceId?: string;
|
|
19
|
-
surfaceId?: string;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface ResolvedCmuxConfig extends CmuxEnvironment {
|
|
23
|
-
enabled: boolean;
|
|
24
|
-
notifications: boolean;
|
|
25
|
-
sidebar: boolean;
|
|
26
|
-
splits: boolean;
|
|
27
|
-
browser: boolean;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export interface CmuxSidebarProgress {
|
|
31
|
-
value: number;
|
|
32
|
-
label: string;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export type CmuxLogLevel = "info" | "progress" | "success" | "warning" | "error";
|
|
36
|
-
|
|
37
|
-
export function detectCmuxEnvironment(
|
|
38
|
-
env: NodeJS.ProcessEnv = process.env,
|
|
39
|
-
socketExists: (path: string) => boolean = existsSync,
|
|
40
|
-
cliAvailable: () => boolean = isCmuxCliAvailable,
|
|
41
|
-
): CmuxEnvironment {
|
|
42
|
-
const socketPath = env.CMUX_SOCKET_PATH ?? DEFAULT_SOCKET_PATH;
|
|
43
|
-
const workspaceId = env.CMUX_WORKSPACE_ID?.trim() || undefined;
|
|
44
|
-
const surfaceId = env.CMUX_SURFACE_ID?.trim() || undefined;
|
|
45
|
-
const available = Boolean(workspaceId && surfaceId && socketExists(socketPath));
|
|
46
|
-
return {
|
|
47
|
-
available,
|
|
48
|
-
cliAvailable: cliAvailable(),
|
|
49
|
-
socketPath,
|
|
50
|
-
workspaceId,
|
|
51
|
-
surfaceId,
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function resolveCmuxConfig(
|
|
56
|
-
preferences: GSDPreferences | undefined,
|
|
57
|
-
env: NodeJS.ProcessEnv = process.env,
|
|
58
|
-
socketExists: (path: string) => boolean = existsSync,
|
|
59
|
-
cliAvailable: () => boolean = isCmuxCliAvailable,
|
|
60
|
-
): ResolvedCmuxConfig {
|
|
61
|
-
const detected = detectCmuxEnvironment(env, socketExists, cliAvailable);
|
|
62
|
-
const cmux = preferences?.cmux ?? {};
|
|
63
|
-
const enabled = detected.available && cmux.enabled === true;
|
|
64
|
-
return {
|
|
65
|
-
...detected,
|
|
66
|
-
enabled,
|
|
67
|
-
notifications: enabled && cmux.notifications !== false,
|
|
68
|
-
sidebar: enabled && cmux.sidebar !== false,
|
|
69
|
-
splits: enabled && cmux.splits === true,
|
|
70
|
-
browser: enabled && cmux.browser === true,
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export function shouldPromptToEnableCmux(
|
|
75
|
-
preferences: GSDPreferences | undefined,
|
|
76
|
-
env: NodeJS.ProcessEnv = process.env,
|
|
77
|
-
socketExists: (path: string) => boolean = existsSync,
|
|
78
|
-
cliAvailable: () => boolean = isCmuxCliAvailable,
|
|
79
|
-
): boolean {
|
|
80
|
-
if (cmuxPromptedThisSession) return false;
|
|
81
|
-
const detected = detectCmuxEnvironment(env, socketExists, cliAvailable);
|
|
82
|
-
if (!detected.available) return false;
|
|
83
|
-
return preferences?.cmux?.enabled === undefined;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export function markCmuxPromptShown(): void {
|
|
87
|
-
cmuxPromptedThisSession = true;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export function resetCmuxPromptState(): void {
|
|
91
|
-
cmuxPromptedThisSession = false;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export function isCmuxCliAvailable(): boolean {
|
|
95
|
-
if (cachedCliAvailability !== null) return cachedCliAvailability;
|
|
96
|
-
try {
|
|
97
|
-
execFileSync("cmux", ["--help"], { stdio: "ignore", timeout: 1000 });
|
|
98
|
-
cachedCliAvailability = true;
|
|
99
|
-
} catch {
|
|
100
|
-
cachedCliAvailability = false;
|
|
101
|
-
}
|
|
102
|
-
return cachedCliAvailability;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
export function supportsOsc777Notifications(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
106
|
-
const termProgram = env.TERM_PROGRAM?.toLowerCase() ?? "";
|
|
107
|
-
return termProgram === "ghostty" || termProgram === "wezterm" || termProgram === "iterm.app";
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export function emitOsc777Notification(title: string, body: string): void {
|
|
111
|
-
if (!supportsOsc777Notifications()) return;
|
|
112
|
-
const safeTitle = normalizeNotificationText(title).replace(/;/g, ",");
|
|
113
|
-
const safeBody = normalizeNotificationText(body).replace(/;/g, ",");
|
|
114
|
-
process.stdout.write(`\x1b]777;notify;${safeTitle};${safeBody}\x07`);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
export function buildCmuxStatusLabel(state: GSDState): string {
|
|
118
|
-
const parts: string[] = [];
|
|
119
|
-
if (state.activeMilestone) parts.push(state.activeMilestone.id);
|
|
120
|
-
if (state.activeSlice) parts.push(state.activeSlice.id);
|
|
121
|
-
if (state.activeTask) {
|
|
122
|
-
const prev = parts.pop();
|
|
123
|
-
parts.push(prev ? `${prev}/${state.activeTask.id}` : state.activeTask.id);
|
|
124
|
-
}
|
|
125
|
-
if (parts.length === 0) return state.phase;
|
|
126
|
-
return `${parts.join(" ")} · ${state.phase}`;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
export function buildCmuxProgress(state: GSDState): CmuxSidebarProgress | null {
|
|
130
|
-
const progress = state.progress;
|
|
131
|
-
if (!progress) return null;
|
|
132
|
-
|
|
133
|
-
const choose = (done: number, total: number, label: string): CmuxSidebarProgress | null => {
|
|
134
|
-
if (total <= 0) return null;
|
|
135
|
-
return { value: Math.max(0, Math.min(1, done / total)), label: `${done}/${total} ${label}` };
|
|
136
|
-
};
|
|
137
|
-
|
|
138
|
-
return choose(progress.tasks?.done ?? 0, progress.tasks?.total ?? 0, "tasks")
|
|
139
|
-
?? choose(progress.slices?.done ?? 0, progress.slices?.total ?? 0, "slices")
|
|
140
|
-
?? choose(progress.milestones.done, progress.milestones.total, "milestones");
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function phaseVisuals(phase: Phase): { icon: string; color: string } {
|
|
144
|
-
switch (phase) {
|
|
145
|
-
case "blocked":
|
|
146
|
-
return { icon: "triangle-alert", color: "#ef4444" };
|
|
147
|
-
case "paused":
|
|
148
|
-
return { icon: "pause", color: "#f59e0b" };
|
|
149
|
-
case "complete":
|
|
150
|
-
case "completing-milestone":
|
|
151
|
-
return { icon: "check", color: "#22c55e" };
|
|
152
|
-
case "planning":
|
|
153
|
-
case "researching":
|
|
154
|
-
case "replanning-slice":
|
|
155
|
-
return { icon: "compass", color: "#3b82f6" };
|
|
156
|
-
case "validating-milestone":
|
|
157
|
-
case "verifying":
|
|
158
|
-
return { icon: "shield-check", color: "#06b6d4" };
|
|
159
|
-
default:
|
|
160
|
-
return { icon: "rocket", color: "#4ade80" };
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function sidebarSnapshotKey(config: ResolvedCmuxConfig): string {
|
|
165
|
-
return config.workspaceId ?? "default";
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
export class CmuxClient {
|
|
169
|
-
private readonly config: ResolvedCmuxConfig;
|
|
170
|
-
|
|
171
|
-
constructor(config: ResolvedCmuxConfig) {
|
|
172
|
-
this.config = config;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
static fromPreferences(preferences: GSDPreferences | undefined): CmuxClient {
|
|
176
|
-
return new CmuxClient(resolveCmuxConfig(preferences));
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
getConfig(): ResolvedCmuxConfig {
|
|
180
|
-
return this.config;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
private canRun(): boolean {
|
|
184
|
-
return this.config.available && this.config.cliAvailable;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
private appendWorkspace(args: string[]): string[] {
|
|
188
|
-
return this.config.workspaceId ? [...args, "--workspace", this.config.workspaceId] : args;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
private appendSurface(args: string[], surfaceId?: string): string[] {
|
|
192
|
-
return surfaceId ? [...args, "--surface", surfaceId] : args;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
private runSync(args: string[]): string | null {
|
|
196
|
-
if (!this.canRun()) return null;
|
|
197
|
-
try {
|
|
198
|
-
return execFileSync("cmux", args, {
|
|
199
|
-
encoding: "utf-8",
|
|
200
|
-
timeout: 3000,
|
|
201
|
-
env: process.env,
|
|
202
|
-
});
|
|
203
|
-
} catch {
|
|
204
|
-
return null;
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
private async runAsync(args: string[]): Promise<string | null> {
|
|
209
|
-
if (!this.canRun()) return null;
|
|
210
|
-
try {
|
|
211
|
-
const result = await execFileAsync("cmux", args, {
|
|
212
|
-
encoding: "utf-8",
|
|
213
|
-
timeout: 5000,
|
|
214
|
-
env: process.env,
|
|
215
|
-
});
|
|
216
|
-
return result.stdout;
|
|
217
|
-
} catch {
|
|
218
|
-
return null;
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
getCapabilities(): unknown | null {
|
|
223
|
-
const stdout = this.runSync(["capabilities", "--json"]);
|
|
224
|
-
return stdout ? parseJson(stdout) : null;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
identify(): unknown | null {
|
|
228
|
-
const stdout = this.runSync(["identify", "--json"]);
|
|
229
|
-
return stdout ? parseJson(stdout) : null;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
setStatus(label: string, phase: Phase): void {
|
|
233
|
-
if (!this.config.sidebar) return;
|
|
234
|
-
const visuals = phaseVisuals(phase);
|
|
235
|
-
this.runSync(this.appendWorkspace([
|
|
236
|
-
"set-status",
|
|
237
|
-
STATUS_KEY,
|
|
238
|
-
label,
|
|
239
|
-
"--icon",
|
|
240
|
-
visuals.icon,
|
|
241
|
-
"--color",
|
|
242
|
-
visuals.color,
|
|
243
|
-
]));
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
clearStatus(): void {
|
|
247
|
-
if (!this.config.sidebar) return;
|
|
248
|
-
this.runSync(this.appendWorkspace(["clear-status", STATUS_KEY]));
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
setProgress(progress: CmuxSidebarProgress | null): void {
|
|
252
|
-
if (!this.config.sidebar) return;
|
|
253
|
-
if (!progress) {
|
|
254
|
-
this.runSync(this.appendWorkspace(["clear-progress"]));
|
|
255
|
-
return;
|
|
256
|
-
}
|
|
257
|
-
this.runSync(this.appendWorkspace([
|
|
258
|
-
"set-progress",
|
|
259
|
-
progress.value.toFixed(3),
|
|
260
|
-
"--label",
|
|
261
|
-
progress.label,
|
|
262
|
-
]));
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
log(message: string, level: CmuxLogLevel = "info", source = "gsd"): void {
|
|
266
|
-
if (!this.config.sidebar) return;
|
|
267
|
-
this.runSync(this.appendWorkspace([
|
|
268
|
-
"log",
|
|
269
|
-
"--level",
|
|
270
|
-
level,
|
|
271
|
-
"--source",
|
|
272
|
-
source,
|
|
273
|
-
"--",
|
|
274
|
-
message,
|
|
275
|
-
]));
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
notify(title: string, body: string, subtitle?: string): boolean {
|
|
279
|
-
if (!this.config.notifications) return false;
|
|
280
|
-
const args = ["notify", "--title", title, "--body", body];
|
|
281
|
-
if (subtitle) args.push("--subtitle", subtitle);
|
|
282
|
-
return this.runSync(args) !== null;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
async listSurfaceIds(): Promise<string[]> {
|
|
286
|
-
const stdout = await this.runAsync(this.appendWorkspace(["list-surfaces", "--json", "--id-format", "both"]));
|
|
287
|
-
const parsed = stdout ? parseJson(stdout) : null;
|
|
288
|
-
return extractSurfaceIds(parsed);
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
async createSplit(direction: "right" | "down" | "left" | "up"): Promise<string | null> {
|
|
292
|
-
if (!this.config.splits) return null;
|
|
293
|
-
const before = new Set(await this.listSurfaceIds());
|
|
294
|
-
const args = ["new-split", direction];
|
|
295
|
-
const scopedArgs = this.appendSurface(this.appendWorkspace(args), this.config.surfaceId);
|
|
296
|
-
await this.runAsync(scopedArgs);
|
|
297
|
-
const after = await this.listSurfaceIds();
|
|
298
|
-
for (const id of after) {
|
|
299
|
-
if (!before.has(id)) return id;
|
|
300
|
-
}
|
|
301
|
-
return null;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
async sendSurface(surfaceId: string, text: string): Promise<boolean> {
|
|
305
|
-
const payload = text.endsWith("\n") ? text : `${text}\n`;
|
|
306
|
-
const stdout = await this.runAsync(["send-surface", "--surface", surfaceId, payload]);
|
|
307
|
-
return stdout !== null;
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
export function syncCmuxSidebar(preferences: GSDPreferences | undefined, state: GSDState): void {
|
|
312
|
-
const client = CmuxClient.fromPreferences(preferences);
|
|
313
|
-
const config = client.getConfig();
|
|
314
|
-
if (!config.sidebar) return;
|
|
315
|
-
|
|
316
|
-
const label = buildCmuxStatusLabel(state);
|
|
317
|
-
const progress = buildCmuxProgress(state);
|
|
318
|
-
const snapshot = JSON.stringify({ label, progress, phase: state.phase });
|
|
319
|
-
const key = sidebarSnapshotKey(config);
|
|
320
|
-
if (lastSidebarSnapshots.get(key) === snapshot) return;
|
|
321
|
-
|
|
322
|
-
client.setStatus(label, state.phase);
|
|
323
|
-
client.setProgress(progress);
|
|
324
|
-
lastSidebarSnapshots.set(key, snapshot);
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
export function clearCmuxSidebar(preferences: GSDPreferences | undefined): void {
|
|
328
|
-
const config = resolveCmuxConfig(preferences);
|
|
329
|
-
if (!config.available || !config.cliAvailable) return;
|
|
330
|
-
const client = new CmuxClient({ ...config, enabled: true, sidebar: true });
|
|
331
|
-
const key = sidebarSnapshotKey(config);
|
|
332
|
-
client.clearStatus();
|
|
333
|
-
client.setProgress(null);
|
|
334
|
-
lastSidebarSnapshots.delete(key);
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
export function logCmuxEvent(
|
|
338
|
-
preferences: GSDPreferences | undefined,
|
|
339
|
-
message: string,
|
|
340
|
-
level: CmuxLogLevel = "info",
|
|
341
|
-
): void {
|
|
342
|
-
CmuxClient.fromPreferences(preferences).log(message, level);
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
export function shellEscape(value: string): string {
|
|
346
|
-
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
function normalizeNotificationText(value: string): string {
|
|
350
|
-
return value.replace(/\r?\n/g, " ").trim();
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
function parseJson(text: string): unknown {
|
|
354
|
-
try {
|
|
355
|
-
return JSON.parse(text);
|
|
356
|
-
} catch {
|
|
357
|
-
return null;
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
function extractSurfaceIds(value: unknown): string[] {
|
|
362
|
-
const found = new Set<string>();
|
|
363
|
-
|
|
364
|
-
const visit = (node: unknown): void => {
|
|
365
|
-
if (Array.isArray(node)) {
|
|
366
|
-
for (const item of node) visit(item);
|
|
367
|
-
return;
|
|
368
|
-
}
|
|
369
|
-
if (!node || typeof node !== "object") return;
|
|
370
|
-
|
|
371
|
-
for (const [key, child] of Object.entries(node as Record<string, unknown>)) {
|
|
372
|
-
if (
|
|
373
|
-
typeof child === "string"
|
|
374
|
-
&& (key === "surface_id" || key === "surface" || (key === "id" && child.includes("surface")))
|
|
375
|
-
) {
|
|
376
|
-
found.add(child);
|
|
377
|
-
}
|
|
378
|
-
visit(child);
|
|
379
|
-
}
|
|
380
|
-
};
|
|
381
|
-
|
|
382
|
-
visit(value);
|
|
383
|
-
return Array.from(found);
|
|
384
|
-
}
|
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
import type { ExtensionCommandContext } from "@gsd/pi-coding-agent";
|
|
2
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
-
import { clearCmuxSidebar, CmuxClient, detectCmuxEnvironment, resolveCmuxConfig } from "../cmux/index.js";
|
|
4
|
-
import { saveFile } from "./files.js";
|
|
5
|
-
import {
|
|
6
|
-
getProjectGSDPreferencesPath,
|
|
7
|
-
loadEffectiveGSDPreferences,
|
|
8
|
-
loadProjectGSDPreferences,
|
|
9
|
-
} from "./preferences.js";
|
|
10
|
-
import { ensurePreferencesFile, serializePreferencesToFrontmatter } from "./commands-prefs-wizard.js";
|
|
11
|
-
|
|
12
|
-
function extractBodyAfterFrontmatter(content: string): string | null {
|
|
13
|
-
const start = content.startsWith("---\n") ? 4 : content.startsWith("---\r\n") ? 5 : -1;
|
|
14
|
-
if (start === -1) return null;
|
|
15
|
-
const closingIdx = content.indexOf("\n---", start);
|
|
16
|
-
if (closingIdx === -1) return null;
|
|
17
|
-
const after = content.slice(closingIdx + 4);
|
|
18
|
-
return after.trim() ? after : null;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
async function writeProjectCmuxPreferences(
|
|
22
|
-
ctx: ExtensionCommandContext,
|
|
23
|
-
updater: (prefs: Record<string, unknown>) => void,
|
|
24
|
-
): Promise<void> {
|
|
25
|
-
const path = getProjectGSDPreferencesPath();
|
|
26
|
-
await ensurePreferencesFile(path, ctx, "project");
|
|
27
|
-
|
|
28
|
-
const existing = loadProjectGSDPreferences();
|
|
29
|
-
const prefs: Record<string, unknown> = existing?.preferences ? { ...existing.preferences } : { version: 1 };
|
|
30
|
-
updater(prefs);
|
|
31
|
-
prefs.version = prefs.version || 1;
|
|
32
|
-
|
|
33
|
-
const frontmatter = serializePreferencesToFrontmatter(prefs);
|
|
34
|
-
let body = "\n# GSD Skill Preferences\n\nSee `~/.gsd/agent/extensions/gsd/docs/preferences-reference.md` for full field documentation and examples.\n";
|
|
35
|
-
if (existsSync(path)) {
|
|
36
|
-
const preserved = extractBodyAfterFrontmatter(readFileSync(path, "utf-8"));
|
|
37
|
-
if (preserved) body = preserved;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
await saveFile(path, `---\n${frontmatter}---${body}`);
|
|
41
|
-
await ctx.waitForIdle();
|
|
42
|
-
await ctx.reload();
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function formatCmuxStatus(): string {
|
|
46
|
-
const loaded = loadEffectiveGSDPreferences();
|
|
47
|
-
const detected = detectCmuxEnvironment();
|
|
48
|
-
const resolved = resolveCmuxConfig(loaded?.preferences);
|
|
49
|
-
const capabilities = new CmuxClient(resolved).getCapabilities() as Record<string, unknown> | null;
|
|
50
|
-
const accessMode = typeof capabilities?.mode === "string"
|
|
51
|
-
? capabilities.mode
|
|
52
|
-
: typeof capabilities?.access_mode === "string"
|
|
53
|
-
? capabilities.access_mode
|
|
54
|
-
: "unknown";
|
|
55
|
-
const methods = Array.isArray(capabilities?.methods) ? capabilities.methods.length : 0;
|
|
56
|
-
|
|
57
|
-
return [
|
|
58
|
-
"cmux status",
|
|
59
|
-
"",
|
|
60
|
-
`Detected: ${detected.available ? "yes" : "no"}`,
|
|
61
|
-
`Enabled: ${resolved.enabled ? "yes" : "no"}`,
|
|
62
|
-
`CLI available: ${detected.cliAvailable ? "yes" : "no"}`,
|
|
63
|
-
`Socket: ${detected.socketPath}`,
|
|
64
|
-
`Workspace: ${detected.workspaceId ?? "(none)"}`,
|
|
65
|
-
`Surface: ${detected.surfaceId ?? "(none)"}`,
|
|
66
|
-
`Features: notifications=${resolved.notifications ? "on" : "off"}, sidebar=${resolved.sidebar ? "on" : "off"}, splits=${resolved.splits ? "on" : "off"}, browser=${resolved.browser ? "on" : "off"}`,
|
|
67
|
-
`Capabilities: access=${accessMode}, methods=${methods}`,
|
|
68
|
-
].join("\n");
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function ensureCmuxAvailableForEnable(ctx: ExtensionCommandContext): boolean {
|
|
72
|
-
const detected = detectCmuxEnvironment();
|
|
73
|
-
if (detected.available) return true;
|
|
74
|
-
ctx.ui.notify(
|
|
75
|
-
"cmux not detected. Install it from https://cmux.com and run gsd inside a cmux terminal.",
|
|
76
|
-
"warning",
|
|
77
|
-
);
|
|
78
|
-
return false;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export async function handleCmux(args: string, ctx: ExtensionCommandContext): Promise<void> {
|
|
82
|
-
const trimmed = args.trim();
|
|
83
|
-
if (!trimmed || trimmed === "status") {
|
|
84
|
-
ctx.ui.notify(formatCmuxStatus(), "info");
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
if (trimmed === "on") {
|
|
89
|
-
if (!ensureCmuxAvailableForEnable(ctx)) return;
|
|
90
|
-
await writeProjectCmuxPreferences(ctx, (prefs) => {
|
|
91
|
-
prefs.cmux = {
|
|
92
|
-
enabled: true,
|
|
93
|
-
notifications: true,
|
|
94
|
-
sidebar: true,
|
|
95
|
-
splits: false,
|
|
96
|
-
browser: false,
|
|
97
|
-
...((prefs.cmux as Record<string, unknown> | undefined) ?? {}),
|
|
98
|
-
};
|
|
99
|
-
(prefs.cmux as Record<string, unknown>).enabled = true;
|
|
100
|
-
});
|
|
101
|
-
ctx.ui.notify("cmux integration enabled in project preferences.", "info");
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
if (trimmed === "off") {
|
|
106
|
-
const effective = loadEffectiveGSDPreferences()?.preferences;
|
|
107
|
-
await writeProjectCmuxPreferences(ctx, (prefs) => {
|
|
108
|
-
prefs.cmux = { ...((prefs.cmux as Record<string, unknown> | undefined) ?? {}), enabled: false };
|
|
109
|
-
});
|
|
110
|
-
clearCmuxSidebar(effective);
|
|
111
|
-
ctx.ui.notify("cmux integration disabled in project preferences.", "info");
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const parts = trimmed.split(/\s+/);
|
|
116
|
-
if (parts.length === 2 && ["notifications", "sidebar", "splits", "browser"].includes(parts[0]) && ["on", "off"].includes(parts[1])) {
|
|
117
|
-
const feature = parts[0] as "notifications" | "sidebar" | "splits" | "browser";
|
|
118
|
-
const enabled = parts[1] === "on";
|
|
119
|
-
if (enabled && !ensureCmuxAvailableForEnable(ctx)) return;
|
|
120
|
-
|
|
121
|
-
await writeProjectCmuxPreferences(ctx, (prefs) => {
|
|
122
|
-
const next = { ...((prefs.cmux as Record<string, unknown> | undefined) ?? {}) };
|
|
123
|
-
next[feature] = enabled;
|
|
124
|
-
if (enabled) next.enabled = true;
|
|
125
|
-
prefs.cmux = next;
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
if (!enabled && feature === "sidebar") {
|
|
129
|
-
clearCmuxSidebar(loadEffectiveGSDPreferences()?.preferences);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const note = feature === "browser" && enabled
|
|
133
|
-
? " Browser surfaces are still a follow-up path."
|
|
134
|
-
: "";
|
|
135
|
-
ctx.ui.notify(`cmux ${feature} ${enabled ? "enabled" : "disabled"}.${note}`, "info");
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
ctx.ui.notify(
|
|
140
|
-
"Usage: /gsd cmux <status|on|off|notifications on|notifications off|sidebar on|sidebar off|splits on|splits off|browser on|browser off>",
|
|
141
|
-
"info",
|
|
142
|
-
);
|
|
143
|
-
}
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
import test from "node:test";
|
|
2
|
-
import assert from "node:assert/strict";
|
|
3
|
-
import {
|
|
4
|
-
buildCmuxProgress,
|
|
5
|
-
buildCmuxStatusLabel,
|
|
6
|
-
detectCmuxEnvironment,
|
|
7
|
-
markCmuxPromptShown,
|
|
8
|
-
resetCmuxPromptState,
|
|
9
|
-
resolveCmuxConfig,
|
|
10
|
-
shouldPromptToEnableCmux,
|
|
11
|
-
} from "../../cmux/index.ts";
|
|
12
|
-
import type { GSDState } from "../types.ts";
|
|
13
|
-
|
|
14
|
-
test("detectCmuxEnvironment requires workspace, surface, and socket", () => {
|
|
15
|
-
const detected = detectCmuxEnvironment(
|
|
16
|
-
{
|
|
17
|
-
CMUX_WORKSPACE_ID: "workspace:1",
|
|
18
|
-
CMUX_SURFACE_ID: "surface:2",
|
|
19
|
-
CMUX_SOCKET_PATH: "/tmp/cmux.sock",
|
|
20
|
-
},
|
|
21
|
-
(path) => path === "/tmp/cmux.sock",
|
|
22
|
-
() => true,
|
|
23
|
-
);
|
|
24
|
-
assert.equal(detected.available, true);
|
|
25
|
-
assert.equal(detected.cliAvailable, true);
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
test("resolveCmuxConfig enables only when preference and environment are both active", () => {
|
|
29
|
-
const config = resolveCmuxConfig(
|
|
30
|
-
{ cmux: { enabled: true, notifications: true, sidebar: true, splits: true } },
|
|
31
|
-
{
|
|
32
|
-
CMUX_WORKSPACE_ID: "workspace:1",
|
|
33
|
-
CMUX_SURFACE_ID: "surface:2",
|
|
34
|
-
CMUX_SOCKET_PATH: "/tmp/cmux.sock",
|
|
35
|
-
},
|
|
36
|
-
() => true,
|
|
37
|
-
() => true,
|
|
38
|
-
);
|
|
39
|
-
assert.equal(config.enabled, true);
|
|
40
|
-
assert.equal(config.notifications, true);
|
|
41
|
-
assert.equal(config.sidebar, true);
|
|
42
|
-
assert.equal(config.splits, true);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
test("shouldPromptToEnableCmux only prompts once per session", () => {
|
|
46
|
-
resetCmuxPromptState();
|
|
47
|
-
assert.equal(shouldPromptToEnableCmux({}, {}, () => false, () => true), false);
|
|
48
|
-
|
|
49
|
-
assert.equal(
|
|
50
|
-
shouldPromptToEnableCmux(
|
|
51
|
-
{},
|
|
52
|
-
{
|
|
53
|
-
CMUX_WORKSPACE_ID: "workspace:1",
|
|
54
|
-
CMUX_SURFACE_ID: "surface:2",
|
|
55
|
-
CMUX_SOCKET_PATH: "/tmp/cmux.sock",
|
|
56
|
-
},
|
|
57
|
-
() => true,
|
|
58
|
-
() => true,
|
|
59
|
-
),
|
|
60
|
-
true,
|
|
61
|
-
);
|
|
62
|
-
markCmuxPromptShown();
|
|
63
|
-
assert.equal(
|
|
64
|
-
shouldPromptToEnableCmux(
|
|
65
|
-
{},
|
|
66
|
-
{
|
|
67
|
-
CMUX_WORKSPACE_ID: "workspace:1",
|
|
68
|
-
CMUX_SURFACE_ID: "surface:2",
|
|
69
|
-
CMUX_SOCKET_PATH: "/tmp/cmux.sock",
|
|
70
|
-
},
|
|
71
|
-
() => true,
|
|
72
|
-
() => true,
|
|
73
|
-
),
|
|
74
|
-
false,
|
|
75
|
-
);
|
|
76
|
-
resetCmuxPromptState();
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
test("buildCmuxStatusLabel and progress prefer deepest active unit", () => {
|
|
80
|
-
const state: GSDState = {
|
|
81
|
-
activeMilestone: { id: "M001", title: "Milestone" },
|
|
82
|
-
activeSlice: { id: "S02", title: "Slice" },
|
|
83
|
-
activeTask: { id: "T03", title: "Task" },
|
|
84
|
-
phase: "executing",
|
|
85
|
-
recentDecisions: [],
|
|
86
|
-
blockers: [],
|
|
87
|
-
nextAction: "Keep going",
|
|
88
|
-
registry: [],
|
|
89
|
-
progress: {
|
|
90
|
-
milestones: { done: 0, total: 1 },
|
|
91
|
-
slices: { done: 1, total: 3 },
|
|
92
|
-
tasks: { done: 2, total: 5 },
|
|
93
|
-
},
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
assert.equal(buildCmuxStatusLabel(state), "M001 S02/T03 · executing");
|
|
97
|
-
assert.deepEqual(buildCmuxProgress(state), { value: 0.4, label: "2/5 tasks" });
|
|
98
|
-
});
|