gsd-pi 2.35.0-dev.cd3b7ea → 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.
Files changed (194) hide show
  1. package/README.md +3 -1
  2. package/dist/cli.js +7 -2
  3. package/dist/resource-loader.d.ts +1 -1
  4. package/dist/resource-loader.js +13 -1
  5. package/dist/resources/extensions/async-jobs/await-tool.js +0 -2
  6. package/dist/resources/extensions/async-jobs/job-manager.js +0 -6
  7. package/dist/resources/extensions/bg-shell/output-formatter.js +1 -19
  8. package/dist/resources/extensions/bg-shell/process-manager.js +0 -4
  9. package/dist/resources/extensions/bg-shell/types.js +0 -2
  10. package/dist/resources/extensions/cmux/index.js +321 -0
  11. package/dist/resources/extensions/context7/index.js +5 -0
  12. package/dist/resources/extensions/get-secrets-from-user.js +2 -30
  13. package/dist/resources/extensions/google-search/index.js +5 -0
  14. package/dist/resources/extensions/gsd/auto-dashboard.js +334 -104
  15. package/dist/resources/extensions/gsd/auto-dispatch.js +43 -1
  16. package/dist/resources/extensions/gsd/auto-loop.js +28 -3
  17. package/dist/resources/extensions/gsd/auto-model-selection.js +15 -3
  18. package/dist/resources/extensions/gsd/auto-recovery.js +35 -0
  19. package/dist/resources/extensions/gsd/auto-start.js +35 -2
  20. package/dist/resources/extensions/gsd/auto.js +75 -4
  21. package/dist/resources/extensions/gsd/commands-cmux.js +120 -0
  22. package/dist/resources/extensions/gsd/commands-handlers.js +2 -2
  23. package/dist/resources/extensions/gsd/commands-inspect.js +10 -3
  24. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +1 -1
  25. package/dist/resources/extensions/gsd/commands-rate.js +31 -0
  26. package/dist/resources/extensions/gsd/commands.js +94 -2
  27. package/dist/resources/extensions/gsd/docs/preferences-reference.md +25 -0
  28. package/dist/resources/extensions/gsd/doctor-environment.js +26 -17
  29. package/dist/resources/extensions/gsd/files.js +11 -2
  30. package/dist/resources/extensions/gsd/gitignore.js +54 -7
  31. package/dist/resources/extensions/gsd/guided-flow.js +8 -2
  32. package/dist/resources/extensions/gsd/health-widget-core.js +96 -0
  33. package/dist/resources/extensions/gsd/health-widget.js +97 -46
  34. package/dist/resources/extensions/gsd/index.js +31 -33
  35. package/dist/resources/extensions/gsd/migrate-external.js +55 -2
  36. package/dist/resources/extensions/gsd/milestone-ids.js +3 -2
  37. package/dist/resources/extensions/gsd/notifications.js +10 -1
  38. package/dist/resources/extensions/gsd/paths.js +74 -7
  39. package/dist/resources/extensions/gsd/post-unit-hooks.js +4 -1
  40. package/dist/resources/extensions/gsd/preferences-types.js +2 -0
  41. package/dist/resources/extensions/gsd/preferences-validation.js +45 -1
  42. package/dist/resources/extensions/gsd/preferences.js +15 -0
  43. package/dist/resources/extensions/gsd/prompts/complete-milestone.md +2 -0
  44. package/dist/resources/extensions/gsd/prompts/research-milestone.md +4 -3
  45. package/dist/resources/extensions/gsd/prompts/research-slice.md +3 -2
  46. package/dist/resources/extensions/gsd/prompts/validate-milestone.md +2 -0
  47. package/dist/resources/extensions/gsd/roadmap-mutations.js +55 -0
  48. package/dist/resources/extensions/gsd/session-lock.js +53 -2
  49. package/dist/resources/extensions/gsd/state.js +2 -1
  50. package/dist/resources/extensions/gsd/templates/plan.md +8 -0
  51. package/dist/resources/extensions/gsd/templates/preferences.md +6 -0
  52. package/dist/resources/extensions/gsd/worktree-resolver.js +12 -0
  53. package/dist/resources/extensions/remote-questions/remote-command.js +2 -22
  54. package/dist/resources/extensions/search-the-web/native-search.js +45 -4
  55. package/dist/resources/extensions/shared/mod.js +1 -1
  56. package/dist/resources/extensions/shared/sanitize.js +30 -0
  57. package/dist/resources/extensions/shared/terminal.js +5 -0
  58. package/dist/resources/extensions/subagent/index.js +186 -74
  59. package/dist/resources/skills/core-web-vitals/SKILL.md +1 -1
  60. package/dist/resources/skills/create-gsd-extension/workflows/debug-extension.md +1 -1
  61. package/dist/resources/skills/github-workflows/SKILL.md +0 -2
  62. package/dist/resources/skills/web-quality-audit/SKILL.md +0 -2
  63. package/package.json +2 -1
  64. package/packages/pi-agent-core/dist/agent.d.ts +10 -2
  65. package/packages/pi-agent-core/dist/agent.d.ts.map +1 -1
  66. package/packages/pi-agent-core/dist/agent.js +19 -8
  67. package/packages/pi-agent-core/dist/agent.js.map +1 -1
  68. package/packages/pi-agent-core/src/agent.ts +31 -10
  69. package/packages/pi-ai/dist/providers/openai-responses.js +1 -1
  70. package/packages/pi-ai/dist/providers/openai-responses.js.map +1 -1
  71. package/packages/pi-ai/src/providers/openai-responses.ts +1 -1
  72. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  73. package/packages/pi-coding-agent/dist/core/agent-session.js +20 -4
  74. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  75. package/packages/pi-coding-agent/dist/core/resource-loader.d.ts.map +1 -1
  76. package/packages/pi-coding-agent/dist/core/resource-loader.js +13 -2
  77. package/packages/pi-coding-agent/dist/core/resource-loader.js.map +1 -1
  78. package/packages/pi-coding-agent/package.json +1 -1
  79. package/packages/pi-coding-agent/src/core/agent-session.ts +36 -12
  80. package/packages/pi-coding-agent/src/core/resource-loader.ts +13 -2
  81. package/packages/pi-tui/dist/terminal-image.d.ts.map +1 -1
  82. package/packages/pi-tui/dist/terminal-image.js +4 -0
  83. package/packages/pi-tui/dist/terminal-image.js.map +1 -1
  84. package/packages/pi-tui/src/terminal-image.ts +5 -0
  85. package/pkg/package.json +1 -1
  86. package/src/resources/extensions/async-jobs/await-tool.ts +0 -2
  87. package/src/resources/extensions/async-jobs/job-manager.ts +0 -7
  88. package/src/resources/extensions/bg-shell/output-formatter.ts +0 -17
  89. package/src/resources/extensions/bg-shell/process-manager.ts +0 -4
  90. package/src/resources/extensions/bg-shell/types.ts +0 -12
  91. package/src/resources/extensions/cmux/index.ts +384 -0
  92. package/src/resources/extensions/context7/index.ts +7 -0
  93. package/src/resources/extensions/get-secrets-from-user.ts +2 -35
  94. package/src/resources/extensions/google-search/index.ts +7 -0
  95. package/src/resources/extensions/gsd/auto-dashboard.ts +363 -116
  96. package/src/resources/extensions/gsd/auto-dispatch.ts +49 -1
  97. package/src/resources/extensions/gsd/auto-loop.ts +64 -2
  98. package/src/resources/extensions/gsd/auto-model-selection.ts +23 -2
  99. package/src/resources/extensions/gsd/auto-recovery.ts +39 -0
  100. package/src/resources/extensions/gsd/auto-start.ts +42 -2
  101. package/src/resources/extensions/gsd/auto.ts +82 -3
  102. package/src/resources/extensions/gsd/commands-cmux.ts +143 -0
  103. package/src/resources/extensions/gsd/commands-handlers.ts +2 -2
  104. package/src/resources/extensions/gsd/commands-inspect.ts +10 -3
  105. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +1 -1
  106. package/src/resources/extensions/gsd/commands-rate.ts +55 -0
  107. package/src/resources/extensions/gsd/commands.ts +97 -2
  108. package/src/resources/extensions/gsd/docs/preferences-reference.md +25 -0
  109. package/src/resources/extensions/gsd/doctor-environment.ts +26 -16
  110. package/src/resources/extensions/gsd/files.ts +12 -2
  111. package/src/resources/extensions/gsd/gitignore.ts +54 -7
  112. package/src/resources/extensions/gsd/guided-flow.ts +8 -2
  113. package/src/resources/extensions/gsd/health-widget-core.ts +129 -0
  114. package/src/resources/extensions/gsd/health-widget.ts +103 -59
  115. package/src/resources/extensions/gsd/index.ts +37 -32
  116. package/src/resources/extensions/gsd/migrate-external.ts +47 -2
  117. package/src/resources/extensions/gsd/milestone-ids.ts +3 -2
  118. package/src/resources/extensions/gsd/notifications.ts +10 -1
  119. package/src/resources/extensions/gsd/paths.ts +73 -7
  120. package/src/resources/extensions/gsd/post-unit-hooks.ts +5 -1
  121. package/src/resources/extensions/gsd/preferences-types.ts +13 -0
  122. package/src/resources/extensions/gsd/preferences-validation.ts +42 -1
  123. package/src/resources/extensions/gsd/preferences.ts +18 -1
  124. package/src/resources/extensions/gsd/prompts/complete-milestone.md +2 -0
  125. package/src/resources/extensions/gsd/prompts/research-milestone.md +4 -3
  126. package/src/resources/extensions/gsd/prompts/research-slice.md +3 -2
  127. package/src/resources/extensions/gsd/prompts/validate-milestone.md +2 -0
  128. package/src/resources/extensions/gsd/roadmap-mutations.ts +66 -0
  129. package/src/resources/extensions/gsd/session-lock.ts +59 -2
  130. package/src/resources/extensions/gsd/state.ts +2 -1
  131. package/src/resources/extensions/gsd/templates/plan.md +8 -0
  132. package/src/resources/extensions/gsd/templates/preferences.md +6 -0
  133. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +2 -0
  134. package/src/resources/extensions/gsd/tests/cmux.test.ts +98 -0
  135. package/src/resources/extensions/gsd/tests/commands-inspect-open-db.test.ts +46 -0
  136. package/src/resources/extensions/gsd/tests/files-loadfile-eisdir.test.ts +20 -0
  137. package/src/resources/extensions/gsd/tests/gitignore-tracked-gsd.test.ts +214 -0
  138. package/src/resources/extensions/gsd/tests/health-widget.test.ts +158 -0
  139. package/src/resources/extensions/gsd/tests/paths.test.ts +113 -0
  140. package/src/resources/extensions/gsd/tests/preferences.test.ts +35 -2
  141. package/src/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +26 -0
  142. package/src/resources/extensions/gsd/tests/test-utils.ts +165 -0
  143. package/src/resources/extensions/gsd/tests/validate-directory.test.ts +15 -0
  144. package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +7 -0
  145. package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +32 -0
  146. package/src/resources/extensions/gsd/worktree-resolver.ts +11 -0
  147. package/src/resources/extensions/remote-questions/remote-command.ts +2 -23
  148. package/src/resources/extensions/search-the-web/native-search.ts +50 -4
  149. package/src/resources/extensions/shared/mod.ts +1 -1
  150. package/src/resources/extensions/shared/sanitize.ts +36 -0
  151. package/src/resources/extensions/shared/terminal.ts +5 -0
  152. package/src/resources/extensions/subagent/index.ts +242 -91
  153. package/src/resources/skills/core-web-vitals/SKILL.md +1 -1
  154. package/src/resources/skills/create-gsd-extension/workflows/debug-extension.md +1 -1
  155. package/src/resources/skills/github-workflows/SKILL.md +0 -2
  156. package/src/resources/skills/web-quality-audit/SKILL.md +0 -2
  157. package/dist/resources/extensions/shared/wizard-ui.js +0 -478
  158. package/dist/resources/skills/swiftui/SKILL.md +0 -208
  159. package/dist/resources/skills/swiftui/references/animations.md +0 -921
  160. package/dist/resources/skills/swiftui/references/architecture.md +0 -1561
  161. package/dist/resources/skills/swiftui/references/layout-system.md +0 -1186
  162. package/dist/resources/skills/swiftui/references/navigation.md +0 -1492
  163. package/dist/resources/skills/swiftui/references/networking-async.md +0 -214
  164. package/dist/resources/skills/swiftui/references/performance.md +0 -1706
  165. package/dist/resources/skills/swiftui/references/platform-integration.md +0 -204
  166. package/dist/resources/skills/swiftui/references/state-management.md +0 -1443
  167. package/dist/resources/skills/swiftui/references/swiftdata.md +0 -297
  168. package/dist/resources/skills/swiftui/references/testing-debugging.md +0 -247
  169. package/dist/resources/skills/swiftui/references/uikit-appkit-interop.md +0 -218
  170. package/dist/resources/skills/swiftui/workflows/add-feature.md +0 -191
  171. package/dist/resources/skills/swiftui/workflows/build-new-app.md +0 -311
  172. package/dist/resources/skills/swiftui/workflows/debug-swiftui.md +0 -192
  173. package/dist/resources/skills/swiftui/workflows/optimize-performance.md +0 -197
  174. package/dist/resources/skills/swiftui/workflows/ship-app.md +0 -203
  175. package/dist/resources/skills/swiftui/workflows/write-tests.md +0 -235
  176. package/src/resources/extensions/shared/wizard-ui.ts +0 -551
  177. package/src/resources/skills/swiftui/SKILL.md +0 -208
  178. package/src/resources/skills/swiftui/references/animations.md +0 -921
  179. package/src/resources/skills/swiftui/references/architecture.md +0 -1561
  180. package/src/resources/skills/swiftui/references/layout-system.md +0 -1186
  181. package/src/resources/skills/swiftui/references/navigation.md +0 -1492
  182. package/src/resources/skills/swiftui/references/networking-async.md +0 -214
  183. package/src/resources/skills/swiftui/references/performance.md +0 -1706
  184. package/src/resources/skills/swiftui/references/platform-integration.md +0 -204
  185. package/src/resources/skills/swiftui/references/state-management.md +0 -1443
  186. package/src/resources/skills/swiftui/references/swiftdata.md +0 -297
  187. package/src/resources/skills/swiftui/references/testing-debugging.md +0 -247
  188. package/src/resources/skills/swiftui/references/uikit-appkit-interop.md +0 -218
  189. package/src/resources/skills/swiftui/workflows/add-feature.md +0 -191
  190. package/src/resources/skills/swiftui/workflows/build-new-app.md +0 -311
  191. package/src/resources/skills/swiftui/workflows/debug-swiftui.md +0 -192
  192. package/src/resources/skills/swiftui/workflows/optimize-performance.md +0 -197
  193. package/src/resources/skills/swiftui/workflows/ship-app.md +0 -203
  194. package/src/resources/skills/swiftui/workflows/write-tests.md +0 -235
@@ -22,7 +22,6 @@ import {
22
22
  READINESS_PATTERNS,
23
23
  BUILD_COMPLETE_PATTERNS,
24
24
  TEST_RESULT_PATTERNS,
25
- LINE_DEDUP_MAX,
26
25
  } from "./types.js";
27
26
  import { addEvent, pushAlert } from "./process-manager.js";
28
27
  import { transitionToReady } from "./readiness-detector.js";
@@ -106,22 +105,6 @@ export function analyzeLine(bg: BgProcess, line: string, stream: "stdout" | "std
106
105
  }
107
106
  }
108
107
 
109
- // Dedup tracking — evict oldest entry when map exceeds LINE_DEDUP_MAX (LRU via Map insertion order)
110
- bg.totalRawLines++;
111
- const lineHash = line.trim().slice(0, 100);
112
- const existing = bg.lineDedup.get(lineHash);
113
- if (existing !== undefined) {
114
- // Re-insert to update insertion order (move to tail = most recent)
115
- bg.lineDedup.delete(lineHash);
116
- bg.lineDedup.set(lineHash, existing + 1);
117
- } else {
118
- if (bg.lineDedup.size >= LINE_DEDUP_MAX) {
119
- // Evict oldest entry (Map iteration order = insertion order = LRU at head)
120
- const oldest = bg.lineDedup.keys().next().value;
121
- if (oldest !== undefined) bg.lineDedup.delete(oldest);
122
- }
123
- bg.lineDedup.set(lineHash, 1);
124
- }
125
108
  }
126
109
 
127
110
  // ── Digest Generation ──────────────────────────────────────────────────────
@@ -162,12 +162,8 @@ export function startProcess(opts: StartOptions): BgProcess {
162
162
  group: opts.group || null,
163
163
  lastErrorCount: 0,
164
164
  lastWarningCount: 0,
165
- commandHistory: [],
166
- lineDedup: new Map(),
167
- totalRawLines: 0,
168
165
  stdoutLineCount: 0,
169
166
  stderrLineCount: 0,
170
- envKeys: Object.keys(opts.env || {}),
171
167
  restartCount: 0,
172
168
  startConfig: {
173
169
  command,
@@ -21,9 +21,7 @@ export interface ProcessEvent {
21
21
  | "recovered"
22
22
  | "exited"
23
23
  | "crashed"
24
- | "output"
25
24
  | "port_open"
26
- | "pattern_match"
27
25
  | "port_timeout";
28
26
  timestamp: number;
29
27
  detail: string;
@@ -92,18 +90,10 @@ export interface BgProcess {
92
90
  lastErrorCount: number;
93
91
  /** Last warning count snapshot for diff detection */
94
92
  lastWarningCount: number;
95
- /** Command history for shell-type sessions */
96
- commandHistory: string[];
97
- /** Dedup tracker: hash → count of repeated lines (capped at LINE_DEDUP_MAX entries) */
98
- lineDedup: Map<string, number>;
99
- /** Total raw lines (before dedup) for token savings calc */
100
- totalRawLines: number;
101
93
  /** Tracked stdout line count (incremented in addOutputLine, avoids O(n) filter) */
102
94
  stdoutLineCount: number;
103
95
  /** Tracked stderr line count (incremented in addOutputLine, avoids O(n) filter) */
104
96
  stderrLineCount: number;
105
- /** Env snapshot (keys only, no values for security) */
106
- envKeys: string[];
107
97
  /** Restart count */
108
98
  restartCount: number;
109
99
  /** Original start config for restart */
@@ -187,8 +177,6 @@ export interface ProcessManifest {
187
177
  export const MAX_BUFFER_LINES = 5000;
188
178
  export const MAX_EVENTS = 200;
189
179
  export const DEAD_PROCESS_TTL = 10 * 60 * 1000;
190
- /** Maximum unique entries in the per-process lineDedup Map before LRU eviction. */
191
- export const LINE_DEDUP_MAX = 500;
192
180
  export const PORT_PROBE_TIMEOUT = 500;
193
181
  export const READY_POLL_INTERVAL = 250;
194
182
  export const DEFAULT_READY_TIMEOUT = 30000;
@@ -0,0 +1,384 @@
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
+ }
@@ -414,6 +414,13 @@ export default function (pi: ExtensionAPI) {
414
414
  },
415
415
  });
416
416
 
417
+ // ── Session cleanup ─────────────────────────────────────────────────────
418
+
419
+ pi.on("session_shutdown", async () => {
420
+ searchCache.clear();
421
+ docCache.clear();
422
+ });
423
+
417
424
  // ── Startup notification ─────────────────────────────────────────────────
418
425
 
419
426
  pi.on("session_start", async (_event, ctx) => {
@@ -11,9 +11,9 @@ import { existsSync, statSync } from "node:fs";
11
11
  import { resolve } from "node:path";
12
12
 
13
13
  import type { ExtensionAPI, Theme } from "@gsd/pi-coding-agent";
14
- import { CURSOR_MARKER, Editor, type EditorTheme, Key, matchesKey, Text, truncateToWidth, wrapTextWithAnsi } from "@gsd/pi-tui";
14
+ import { Editor, type EditorTheme, Key, matchesKey, Text, truncateToWidth, wrapTextWithAnsi } from "@gsd/pi-tui";
15
15
  import { Type } from "@sinclair/typebox";
16
- import { makeUI, type ProgressStatus } from "./shared/mod.js";
16
+ import { makeUI, maskEditorLine, type ProgressStatus } from "./shared/mod.js";
17
17
  import { parseSecretsManifest, formatSecretsManifest } from "./gsd/files.js";
18
18
  import { resolveMilestoneFile } from "./gsd/paths.js";
19
19
  import type { SecretsManifestEntry } from "./gsd/types.js";
@@ -42,39 +42,6 @@ function maskPreview(value: string): string {
42
42
  return `${value.slice(0, 4)}${"*".repeat(Math.max(4, value.length - 8))}${value.slice(-4)}`;
43
43
  }
44
44
 
45
- /**
46
- * Replace editor visible text with masked characters while preserving ANSI cursor/sequencer codes.
47
- */
48
- function maskEditorLine(line: string): string {
49
- // Keep border / metadata lines readable.
50
- if (line.startsWith("─")) {
51
- return line;
52
- }
53
-
54
- let output = "";
55
- let i = 0;
56
- while (i < line.length) {
57
- if (line.startsWith(CURSOR_MARKER, i)) {
58
- output += CURSOR_MARKER;
59
- i += CURSOR_MARKER.length;
60
- continue;
61
- }
62
-
63
- const ansiMatch = /^\x1b\[[0-9;]*m/.exec(line.slice(i));
64
- if (ansiMatch) {
65
- output += ansiMatch[0];
66
- i += ansiMatch[0].length;
67
- continue;
68
- }
69
-
70
- const ch = line[i] as string;
71
- output += ch === " " ? " " : "*";
72
- i += 1;
73
- }
74
-
75
- return output;
76
- }
77
-
78
45
  function shellEscapeSingle(value: string): string {
79
46
  return `'${value.replace(/'/g, `'\\''`)}'`;
80
47
  }
@@ -411,6 +411,13 @@ export default function (pi: ExtensionAPI) {
411
411
  },
412
412
  });
413
413
 
414
+ // ── Session cleanup ─────────────────────────────────────────────────────
415
+
416
+ pi.on("session_shutdown", async () => {
417
+ resultCache.clear();
418
+ client = null;
419
+ });
420
+
414
421
  // ── Startup notification ─────────────────────────────────────────────────
415
422
 
416
423
  pi.on("session_start", async (_event, ctx) => {