pi-orchestrator-config 4.1.5

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 (175) hide show
  1. package/README.md +808 -0
  2. package/agents/api-documenter.md +32 -0
  3. package/agents/bash-expert.md +53 -0
  4. package/agents/code-reviewer-docs.md +145 -0
  5. package/agents/code-reviewer-guidelines.md +97 -0
  6. package/agents/code-reviewer-quality.md +142 -0
  7. package/agents/code-reviewer-security.md +107 -0
  8. package/agents/code-reviewer-spec.md +164 -0
  9. package/agents/debugger.md +63 -0
  10. package/agents/docker-expert.md +40 -0
  11. package/agents/docs-fetcher.md +48 -0
  12. package/agents/git-expert.md +63 -0
  13. package/agents/github-expert.md +83 -0
  14. package/agents/go-expert.md +62 -0
  15. package/agents/issue-reviewer-feasibility.md +60 -0
  16. package/agents/issue-reviewer-scope.md +63 -0
  17. package/agents/issue-reviewer-spec.md +77 -0
  18. package/agents/java-expert.md +31 -0
  19. package/agents/jenkins-expert.md +38 -0
  20. package/agents/kubernetes-expert.md +40 -0
  21. package/agents/planner.md +42 -0
  22. package/agents/python-expert.md +61 -0
  23. package/agents/reviewer.md +26 -0
  24. package/agents/scout.md +40 -0
  25. package/agents/security-auditor.md +254 -0
  26. package/agents/technical-documentation-writer.md +37 -0
  27. package/agents/test-automator.md +55 -0
  28. package/agents/test-runner.md +47 -0
  29. package/agents/ts-expert.md +23 -0
  30. package/agents/worker.md +30 -0
  31. package/extensions/acpx-provider/index.ts +104 -0
  32. package/extensions/acpx-provider/load-runtime.ts +99 -0
  33. package/extensions/acpx-provider/runtime-models.ts +40 -0
  34. package/extensions/cli-provider/agents/claude.ts +128 -0
  35. package/extensions/cli-provider/agents/cursor.ts +84 -0
  36. package/extensions/cli-provider/agents/gemini.ts +100 -0
  37. package/extensions/cli-provider/discover.ts +77 -0
  38. package/extensions/cli-provider/index.ts +57 -0
  39. package/extensions/cli-provider/parsers.ts +304 -0
  40. package/extensions/cli-provider/providers.ts +58 -0
  41. package/extensions/cli-provider/runner.ts +225 -0
  42. package/extensions/cli-provider/runtime-models.ts +32 -0
  43. package/extensions/cli-provider/session-reaper.ts +122 -0
  44. package/extensions/cli-provider/sessions.ts +533 -0
  45. package/extensions/cli-provider/shared/discover-cache.ts +235 -0
  46. package/extensions/cli-provider/types.ts +48 -0
  47. package/extensions/coms/coms-net-server.ts +1642 -0
  48. package/extensions/coms/coms-net-wrapper.ts +521 -0
  49. package/extensions/coms/coms-net.ts +1589 -0
  50. package/extensions/coms/coms-p2p.ts +1879 -0
  51. package/extensions/coms/coms-shared.ts +510 -0
  52. package/extensions/coms/coms-wrapper.ts +138 -0
  53. package/extensions/coms/index.ts +20 -0
  54. package/extensions/coms/themeMap.ts +147 -0
  55. package/extensions/image-gen/image-gen.ts +332 -0
  56. package/extensions/image-gen/index.ts +14 -0
  57. package/extensions/orchestrator/agents.ts +134 -0
  58. package/extensions/orchestrator/ask-user.ts +293 -0
  59. package/extensions/orchestrator/async-agents.ts +1037 -0
  60. package/extensions/orchestrator/async-capability.ts +129 -0
  61. package/extensions/orchestrator/async-runner.ts +324 -0
  62. package/extensions/orchestrator/async-status-parse.ts +31 -0
  63. package/extensions/orchestrator/async-status-ui.ts +201 -0
  64. package/extensions/orchestrator/async-wait.ts +36 -0
  65. package/extensions/orchestrator/btw.ts +210 -0
  66. package/extensions/orchestrator/cron-status-format.ts +126 -0
  67. package/extensions/orchestrator/cron-status-ui.ts +132 -0
  68. package/extensions/orchestrator/cron.ts +730 -0
  69. package/extensions/orchestrator/dreaming.ts +355 -0
  70. package/extensions/orchestrator/enforcement-helpers.ts +530 -0
  71. package/extensions/orchestrator/enforcement-rules.ts +329 -0
  72. package/extensions/orchestrator/enforcement.ts +828 -0
  73. package/extensions/orchestrator/extended-autocomplete.ts +593 -0
  74. package/extensions/orchestrator/git-helpers.ts +420 -0
  75. package/extensions/orchestrator/github-autocomplete.ts +250 -0
  76. package/extensions/orchestrator/icons.ts +17 -0
  77. package/extensions/orchestrator/index.ts +100 -0
  78. package/extensions/orchestrator/memory-embeddings.ts +281 -0
  79. package/extensions/orchestrator/memory-promotion.ts +449 -0
  80. package/extensions/orchestrator/memory-provenance.ts +80 -0
  81. package/extensions/orchestrator/memory-query-class.ts +90 -0
  82. package/extensions/orchestrator/memory-scoring.ts +434 -0
  83. package/extensions/orchestrator/memory-tools.ts +883 -0
  84. package/extensions/orchestrator/memory-tree.ts +212 -0
  85. package/extensions/orchestrator/nvim.ts +169 -0
  86. package/extensions/orchestrator/overlay-dashboard-utils.ts +25 -0
  87. package/extensions/orchestrator/overlay-dashboard.ts +512 -0
  88. package/extensions/orchestrator/pi-config-review-state.ts +303 -0
  89. package/extensions/orchestrator/preference-extractor.ts +106 -0
  90. package/extensions/orchestrator/project-settings.ts +468 -0
  91. package/extensions/orchestrator/promotion-queue.ts +311 -0
  92. package/extensions/orchestrator/resolve-agent-model.ts +32 -0
  93. package/extensions/orchestrator/review-ui.ts +204 -0
  94. package/extensions/orchestrator/rule-placeholders.ts +12 -0
  95. package/extensions/orchestrator/rules.ts +481 -0
  96. package/extensions/orchestrator/session-search.ts +243 -0
  97. package/extensions/orchestrator/session-validation.ts +374 -0
  98. package/extensions/orchestrator/situation-report.ts +370 -0
  99. package/extensions/orchestrator/status-bar.ts +31 -0
  100. package/extensions/orchestrator/status-line.ts +192 -0
  101. package/extensions/orchestrator/status.ts +110 -0
  102. package/extensions/orchestrator/subagent-tool.ts +1748 -0
  103. package/extensions/orchestrator/task-lifecycle.ts +88 -0
  104. package/extensions/orchestrator/utils.ts +227 -0
  105. package/extensions/pidash/index.ts +15 -0
  106. package/extensions/pidash/pidash.ts +941 -0
  107. package/extensions/pidiff/index.ts +13 -0
  108. package/extensions/pidiff/pidiff.ts +369 -0
  109. package/extensions/providers/acpx-driver.ts +479 -0
  110. package/extensions/providers/built-in-drivers.ts +52 -0
  111. package/extensions/providers/claude-driver.ts +319 -0
  112. package/extensions/providers/cursor-acpx-driver.ts +398 -0
  113. package/extensions/providers/cursor-cli-driver.ts +484 -0
  114. package/extensions/providers/gemini-driver.ts +302 -0
  115. package/extensions/providers/index.ts +513 -0
  116. package/extensions/shared/build-system-prompt.ts +57 -0
  117. package/extensions/shared/create-runtime-provider.ts +190 -0
  118. package/extensions/shared/daemon-manager.ts +248 -0
  119. package/extensions/shared/file-logger.ts +115 -0
  120. package/extensions/shared/managed-refresh.ts +211 -0
  121. package/extensions/shared/provider-driver.ts +295 -0
  122. package/extensions/shared/provider-errors.ts +305 -0
  123. package/extensions/shared/provider-registry.ts +388 -0
  124. package/extensions/shared/stream-builder.ts +275 -0
  125. package/extensions/shared/types.ts +20 -0
  126. package/extensions/shared/ui/README.md +10 -0
  127. package/extensions/shared/ui/badge.tsx +52 -0
  128. package/extensions/shared/ui/button.tsx +58 -0
  129. package/extensions/shared/ui/collapsible.tsx +21 -0
  130. package/extensions/shared/ui/dropdown-menu.tsx +266 -0
  131. package/extensions/shared/ui/input.tsx +20 -0
  132. package/extensions/shared/ui/popover.tsx +114 -0
  133. package/extensions/shared/ui/resizable.tsx +48 -0
  134. package/extensions/shared/ui/scroll-area.tsx +53 -0
  135. package/extensions/shared/ui/select.tsx +201 -0
  136. package/extensions/shared/ui/separator.tsx +25 -0
  137. package/extensions/shared/ui/tooltip.tsx +64 -0
  138. package/extensions/shared/ws-client.ts +74 -0
  139. package/package.json +59 -0
  140. package/prompts/coderabbit-rate-limit.md +95 -0
  141. package/prompts/create-coms-feature-manager.md +186 -0
  142. package/prompts/create-skill.md +128 -0
  143. package/prompts/domain-model.md +109 -0
  144. package/prompts/external-ai.md +665 -0
  145. package/prompts/implement-and-review.md +25 -0
  146. package/prompts/implement.md +26 -0
  147. package/prompts/issue-review.md +256 -0
  148. package/prompts/pr-review.md +753 -0
  149. package/prompts/query-db.md +154 -0
  150. package/prompts/refine-review.md +208 -0
  151. package/prompts/release.md +235 -0
  152. package/prompts/remember.md +33 -0
  153. package/prompts/review-handler-status.md +27 -0
  154. package/prompts/review-handler.md +677 -0
  155. package/prompts/review-local.md +159 -0
  156. package/prompts/scout-and-plan.md +21 -0
  157. package/rules/00-orchestrator-core.md +42 -0
  158. package/rules/05-issue-first-workflow.md +119 -0
  159. package/rules/10-agent-routing.md +58 -0
  160. package/rules/15-mcp-launchpad.md +46 -0
  161. package/rules/20-code-review-loop.md +234 -0
  162. package/rules/25-documentation-updates.md +15 -0
  163. package/rules/30-prompt-templates.md +19 -0
  164. package/rules/35-memory.md +240 -0
  165. package/rules/40-critical-rules.md +124 -0
  166. package/rules/45-file-preview.md +29 -0
  167. package/rules/50-agent-bug-reporting.md +90 -0
  168. package/rules/55-coms-protocol.md +63 -0
  169. package/rules/60-task-tracking.md +66 -0
  170. package/skills/conflict-resolver/SKILL.md +213 -0
  171. package/skills/skill-creator/SKILL.md +85 -0
  172. package/skills/systematic-debugging/SKILL.md +353 -0
  173. package/skills/test-driven-development/SKILL.md +335 -0
  174. package/skills/update-agents-md/SKILL.md +282 -0
  175. package/templates/coms-feature-manager-prompt.md +313 -0
@@ -0,0 +1,941 @@
1
+ /**
2
+ * Pidash extension — connects to the pidash daemon to expose this session.
3
+ *
4
+ * On session_start:
5
+ * 1. Check if pidash daemon is running (HTTP health check)
6
+ * 2. If not, spawn it as a detached process
7
+ * 3. Connect via WebSocket, register this session
8
+ * 4. Forward all pi events to the daemon
9
+ * 5. Receive prompts from the daemon (browser → daemon → here → pi)
10
+ */
11
+
12
+ import { execFileSync } from "node:child_process";
13
+ import * as fs from "node:fs";
14
+ import { createRequire } from "node:module";
15
+ import * as path from "node:path";
16
+ import { setupHeartbeat, setupReconnectPoller } from "../shared/ws-client.js";
17
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
18
+ import { SessionManager } from "@earendil-works/pi-coding-agent";
19
+ import { hyperlink } from "@earendil-works/pi-tui";
20
+ // Command handler registry — standalone, populated via pi.events from other extensions
21
+ const commandHandlerRegistry = new Map<string, (args: string, ctx: any) => Promise<void>>();
22
+ import { checkHealth, ensureUiBuilt, spawnDaemon as spawnDaemonGeneric, killDaemon, createLogger } from "../shared/daemon-manager.js";
23
+ import { getSetting } from "../orchestrator/project-settings.js";
24
+
25
+ const RECONNECT_INTERVAL_MS = 5000;
26
+
27
+ const debugLog = createLogger(
28
+ path.join(process.env.HOME || "/tmp", ".pi", "pidash-debug.log"),
29
+ "ext",
30
+ );
31
+
32
+ // ── Helpers ──────────────────────────────────────────────────────────
33
+
34
+ function isDaemonRunning(port: number): Promise<boolean> {
35
+ return checkHealth(port);
36
+ }
37
+
38
+ function spawnDaemon(port: number): void {
39
+ ensureUiBuilt(import.meta.url, "pidash-ui", debugLog);
40
+ spawnDaemonGeneric({
41
+ serverScript: "pidash-server.ts",
42
+ logFile: path.join(process.env.HOME || "/tmp", ".pi", "pidash-server.log"),
43
+ env: { PI_PIDASH_PORT: String(port) },
44
+ log: debugLog,
45
+ });
46
+ }
47
+
48
+ function getGitStatus(cwd: string): { branch: string; dirty: boolean; changes: number } {
49
+ try {
50
+ const branch = execFileSync("git", ["branch", "--show-current"], {
51
+ cwd, encoding: "utf-8", timeout: 3000, stdio: ["ignore", "pipe", "ignore"],
52
+ }).trim();
53
+ const status = execFileSync("git", ["status", "--porcelain"], {
54
+ cwd, encoding: "utf-8", timeout: 3000, stdio: ["ignore", "pipe", "ignore"],
55
+ }).trim();
56
+ const changes = status ? status.split("\n").length : 0;
57
+ return { branch, dirty: changes > 0, changes };
58
+ } catch { return { branch: "", dirty: false, changes: 0 }; }
59
+ }
60
+
61
+ function isContainer(): boolean {
62
+ try {
63
+ return fs.existsSync("/.dockerenv") || fs.existsSync("/run/.containerenv");
64
+ } catch { return false; }
65
+ }
66
+
67
+ function getCurrentBranch(cwd: string): string {
68
+ try {
69
+ return execFileSync("git", ["branch", "--show-current"], {
70
+ cwd, encoding: "utf-8", timeout: 3000,
71
+ stdio: ["ignore", "pipe", "ignore"],
72
+ }).trim();
73
+ } catch { return ""; }
74
+ }
75
+
76
+ let diffPort: number | null = null;
77
+ let isStreaming = false;
78
+
79
+ // ── Registration ─────────────────────────────────────────────────────
80
+
81
+ export function registerPidash(
82
+ pi: ExtensionAPI,
83
+ ): void {
84
+ if (process.env.PI_SUBAGENT_CHILD === "1") return;
85
+
86
+ const projectCwd = process.cwd();
87
+ const pidashPort = getSetting(projectCwd, "pidash_port");
88
+ const pidashDisabled = !getSetting(projectCwd, "pidash_enable");
89
+ if (pidashDisabled) {
90
+ pi.registerCommand("pidash", {
91
+ description: "Manage pidash server — /pidash start|stop|restart|status",
92
+ getArgumentCompletions: (prefix: string) => {
93
+ const items = [
94
+ { value: "start", label: "start", description: "Start pidash server" },
95
+ { value: "stop", label: "stop", description: "Stop pidash server" },
96
+ { value: "restart", label: "restart", description: "Restart pidash server" },
97
+ { value: "status", label: "status", description: "Show pidash status" },
98
+ ];
99
+ return items.filter(i => i.value.startsWith(prefix.toLowerCase()));
100
+ },
101
+ handler: async (_args, ctx) => {
102
+ if (ctx.hasUI) ctx.ui.notify("pidash is disabled (pidash_enable=false in pi-config-settings.json or PI_PIDASH_ENABLE=false).", "info");
103
+ },
104
+ });
105
+ return;
106
+ }
107
+
108
+ // ── Closure state ──────────────────────────────────────────────────
109
+
110
+ let ws: any = null;
111
+ let connected = false;
112
+ let connecting = false;
113
+ let shuttingDown = false;
114
+ let spawning = false;
115
+ let lastCtx: any = null;
116
+ let cleanupHeartbeat: (() => void) | null = null;
117
+ const sessionId = `${process.pid}:${process.cwd()}`;
118
+ const eventBuffer: string[] = []; // Buffer events for replay on daemon reconnect
119
+ let execCtx: any = null;
120
+ let pidashCommandCtx: any = null; // Real ExtensionCommandContext from /pidash handler
121
+
122
+ // ── Extracted inner functions ──────────────────────────────────────
123
+
124
+ /** Replay session history to the WebSocket client for browser display. */
125
+ function replaySessionHistory(wsClient: any, ctx: any, pushBuffered: (ev: string) => void): void {
126
+ try {
127
+ const entries = ctx.sessionManager?.getEntries?.() || [];
128
+ let historyCount = 0;
129
+ for (const entry of entries) {
130
+ const e = entry as any;
131
+ if (e.type !== "message" || !e.message) continue;
132
+ const msg = e.message;
133
+ const ts = e.timestamp ? new Date(e.timestamp).getTime() : Date.now();
134
+
135
+ if (msg.role === "user") {
136
+ pushBuffered(JSON.stringify({ type: "message_start", message: msg, timestamp: ts }));
137
+ }
138
+
139
+ if (msg.role === "assistant" && msg.content) {
140
+ // Send thinking blocks
141
+ for (const part of msg.content) {
142
+ if (part.type === "thinking" && part.thinking) {
143
+ const thinkEv = JSON.stringify({
144
+ type: "message_update",
145
+ assistantMessageEvent: { type: "thinking_delta", delta: part.thinking, partial: { model: msg.model, usage: msg.usage } },
146
+ timestamp: ts,
147
+ });
148
+ pushBuffered(thinkEv);
149
+ }
150
+ }
151
+
152
+ // Send text blocks
153
+ for (const part of msg.content) {
154
+ if (part.type === "text" && part.text) {
155
+ const textEv = JSON.stringify({
156
+ type: "message_update",
157
+ assistantMessageEvent: { type: "text_delta", delta: part.text, partial: { model: msg.model, usage: msg.usage } },
158
+ timestamp: ts,
159
+ });
160
+ pushBuffered(textEv);
161
+ }
162
+ }
163
+
164
+ // Send tool calls
165
+ for (const part of msg.content) {
166
+ if (part.type === "toolCall") {
167
+ const toolEv = JSON.stringify({
168
+ type: "tool_execution_start",
169
+ toolName: part.name,
170
+ args: part.arguments,
171
+ timestamp: ts,
172
+ });
173
+ pushBuffered(toolEv);
174
+ }
175
+ }
176
+
177
+ // Send message_end
178
+ pushBuffered(JSON.stringify({ type: "message_end", message: msg, timestamp: ts }));
179
+ }
180
+
181
+ // Tool results
182
+ if (msg.role === "toolResult") {
183
+ const resultEv = JSON.stringify({
184
+ type: "tool_execution_end",
185
+ toolName: msg.toolName,
186
+ result: { content: msg.content },
187
+ isError: msg.isError || false,
188
+ timestamp: ts,
189
+ });
190
+ pushBuffered(resultEv);
191
+ }
192
+
193
+ // Custom messages (async agent results, etc.)
194
+ if (msg.role === "custom" && msg.display) {
195
+ const content = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content || "");
196
+ pushBuffered(JSON.stringify({
197
+ type: "message_start",
198
+ message: { role: "custom", display: true, content, customType: msg.customType },
199
+ timestamp: ts,
200
+ }));
201
+ }
202
+
203
+ // Catch-all: any other message role — forward as-is
204
+ if (!["user", "assistant", "toolResult", "custom"].includes(msg.role)) {
205
+ pushBuffered(JSON.stringify({ type: "message_start", message: msg, timestamp: ts }));
206
+ }
207
+
208
+ historyCount++;
209
+ }
210
+ if (historyCount > 0) debugLog(`loaded ${historyCount} entries from session history`);
211
+ } catch (e: any) {
212
+ debugLog(`session history load error: ${e.message}`);
213
+ }
214
+
215
+ // Signal replay is complete so the server can stop suppressing notifications
216
+ try { wsClient.send(JSON.stringify({ type: "replay_complete" })); } catch {}
217
+ }
218
+
219
+ /** Handle incoming WebSocket messages from the pidash daemon (prompts, commands). */
220
+ async function handleWsMessage(data: Buffer): Promise<void> {
221
+ try {
222
+ const parsed = JSON.parse(data.toString());
223
+ if (parsed.type === "prompt" && (parsed.text || parsed.images)) {
224
+ debugLog(`received prompt from browser: ${(parsed.text || "").slice(0, 100)}${parsed.images ? ` [+${parsed.images.length} images]` : ""}`);
225
+ // Notify browser if prompt is queued during streaming
226
+ if (isStreaming && ws && connected) {
227
+ try { ws.send(JSON.stringify({ type: "prompt-queued" })); } catch (e: any) { debugLog(`prompt-queued send error: ${e.message}`); }
228
+ }
229
+ if (parsed.images && parsed.images.length > 0) {
230
+ // Build content array with text + images
231
+ const content: Array<{ type: "text"; text: string } | { type: "image"; data: string; mimeType: string }> = [];
232
+ if (parsed.text) {
233
+ content.push({ type: "text", text: parsed.text });
234
+ }
235
+ for (const img of parsed.images) {
236
+ content.push({ type: "image", data: img.data, mimeType: img.mimeType });
237
+ }
238
+ pi.sendUserMessage(content, { deliverAs: "followUp" });
239
+ } else {
240
+ pi.sendUserMessage(parsed.text, { deliverAs: "followUp" });
241
+ }
242
+ }
243
+ if (parsed.type === "extension_ui_response" && parsed.id) {
244
+ debugLog(`received UI response from browser: ${JSON.stringify(parsed).slice(0, 100)}`);
245
+ pi.events.emit("pidash:ui-response", parsed);
246
+ }
247
+ if (parsed.type === "pidash-command") {
248
+ debugLog(`received command from browser: ${parsed.command}`);
249
+
250
+ if (parsed.command === "list-sessions") {
251
+ try {
252
+ const sessions = await SessionManager.list(lastCtx?.cwd || process.cwd());
253
+ debugLog(`list-sessions: found ${sessions.length}`);
254
+ if (ws && connected) ws.send(JSON.stringify({ type: "sessions-list", sessions }));
255
+ } catch (e: any) { debugLog(`list-sessions error: ${e.message}`); }
256
+ }
257
+
258
+ if (parsed.command === "list-models") {
259
+ try {
260
+ if (lastCtx?.modelRegistry) {
261
+ const available = lastCtx.modelRegistry.getAvailable();
262
+ const list = available.map((m: any) => ({
263
+ id: m.id,
264
+ name: m.name,
265
+ provider: typeof m.provider === "string" ? m.provider : m.provider?.name || "",
266
+ }));
267
+ debugLog(`models found: ${list.length}`);
268
+ if (ws && connected) ws.send(JSON.stringify({ type: "models-list", models: list }));
269
+ } else {
270
+ debugLog("list-models: no modelRegistry on ctx");
271
+ }
272
+ } catch (e: any) { debugLog(`list-models error: ${e.message}`); }
273
+ }
274
+
275
+ if (parsed.command === "set-model" && parsed.modelId) {
276
+ try {
277
+ const model = lastCtx?.modelRegistry?.getAvailable()?.find((m: any) =>
278
+ m.id === parsed.modelId || m.name === parsed.modelId || m.id.includes(parsed.modelId) || m.name.includes(parsed.modelId));
279
+ if (model) {
280
+ await (pi as any).setModel(model);
281
+ debugLog(`model set to: ${model.name}`);
282
+ ws.send(JSON.stringify({ type: "update_info", model: model.name, contextWindow: model.contextWindow || 0 }));
283
+ }
284
+ } catch (e: any) { debugLog(`set-model error: ${e.message}`); }
285
+ }
286
+
287
+ if (parsed.command === "set-thinking" && parsed.level) {
288
+ try {
289
+ (pi as any).setThinkingLevel(parsed.level);
290
+ debugLog(`thinking set to: ${parsed.level}`);
291
+ ws.send(JSON.stringify({ type: "update_info", thinkingLevel: parsed.level }));
292
+ } catch (e: any) { debugLog(`set-thinking error: ${e.message}`); }
293
+ }
294
+
295
+ if (parsed.command === "switch-session" && parsed.sessionFile) {
296
+ debugLog(`switch-session: ${parsed.sessionFile}`);
297
+ const ctx = pidashCommandCtx;
298
+ if (ctx?.switchSession) {
299
+ try {
300
+ await ctx.switchSession(parsed.sessionFile, {
301
+ withSession: async () => {
302
+ debugLog("switch-session: completed");
303
+ },
304
+ });
305
+ } catch (e: any) {
306
+ debugLog(`switch-session error: ${e.message}`);
307
+ }
308
+ } else {
309
+ debugLog("switch-session: no command context available");
310
+ }
311
+ }
312
+
313
+ if (parsed.command === "abort") {
314
+ if (lastCtx) {
315
+ try { lastCtx.abort(); debugLog("abort sent"); } catch {}
316
+ }
317
+ }
318
+
319
+ if (parsed.command === "async-kill" && parsed.target) {
320
+ debugLog(`async-kill from browser: ${parsed.target}`);
321
+ pi.events.emit("pidash:async-kill", parsed.target);
322
+ }
323
+
324
+ if (parsed.command === "cron-kill" && parsed.target) {
325
+ debugLog(`cron-kill from browser: ${parsed.target}`);
326
+ pi.events.emit("pidash:cron-kill", parsed.target);
327
+ }
328
+
329
+ if (parsed.command === "list-commands") {
330
+ try {
331
+ const cmds = (pi as any).getCommands?.() || [];
332
+ const list = cmds.map((c: any) => ({ name: c.name, description: c.description || "" }));
333
+ if (ws && connected) ws.send(JSON.stringify({ type: "commands-list", commands: list }));
334
+ } catch (e: any) { debugLog(`list-commands error: ${e.message}`); }
335
+ }
336
+
337
+ }
338
+ } catch (e: any) { debugLog(`message handler error: ${e.message}`); }
339
+ }
340
+
341
+ /** Handle /pidash command (start|stop|restart|status). */
342
+ async function handlePidashCommand(args: string, ctx: any): Promise<void> {
343
+ // Guard: pidash daemon connections only in TUI mode
344
+ if (ctx.mode !== "tui") {
345
+ if (ctx.hasUI) ctx.ui.notify("pidash is only available in TUI mode.", "info");
346
+ return;
347
+ }
348
+
349
+ execCtx = ctx;
350
+ pidashCommandCtx = ctx; // Real ExtensionCommandContext
351
+ debugLog("pidashCommandCtx captured from /pidash handler");
352
+
353
+ const cmd = (args || "").trim().toLowerCase();
354
+
355
+ if (cmd === "stop") {
356
+ if (ws) { try { ws.close(); } catch {} ws = null; }
357
+ connected = false;
358
+ killDaemon("pidash-server", debugLog);
359
+ if (ctx.hasUI) {
360
+ ctx.ui.setStatus("9-pidash", undefined);
361
+ ctx.ui.notify("pidash server stopped", "info");
362
+ }
363
+ return;
364
+ }
365
+
366
+ if (cmd === "start") {
367
+ if (await isDaemonRunning(pidashPort)) {
368
+ if (ctx.hasUI) ctx.ui.notify(`pidash already running at http://localhost:${pidashPort}`, "info");
369
+ if (!connected) connect(ctx);
370
+ return;
371
+ }
372
+ spawnDaemon(pidashPort);
373
+ if (ctx.hasUI) ctx.ui.notify("Starting pidash server...", "info");
374
+ for (let i = 0; i < 60; i++) {
375
+ await new Promise(r => setTimeout(r, 1000));
376
+ if (await isDaemonRunning(pidashPort)) break;
377
+ }
378
+ if (await isDaemonRunning(pidashPort)) {
379
+ connect(ctx);
380
+ if (ctx.hasUI) ctx.ui.notify(`pidash server started at http://localhost:${pidashPort}`, "info");
381
+ } else {
382
+ if (ctx.hasUI) ctx.ui.notify("pidash server failed to start — check ~/.pi/pidash-server.log", "warning");
383
+ }
384
+ return;
385
+ }
386
+
387
+ if (cmd === "restart") {
388
+ if (ws) { try { ws.close(); } catch {} ws = null; }
389
+ connected = false;
390
+ killDaemon("pidash-server", debugLog);
391
+ await new Promise(r => setTimeout(r, 1000));
392
+ spawnDaemon(pidashPort);
393
+ if (ctx.hasUI) ctx.ui.notify("Restarting pidash server...", "info");
394
+ for (let i = 0; i < 60; i++) {
395
+ await new Promise(r => setTimeout(r, 1000));
396
+ if (await isDaemonRunning(pidashPort)) break;
397
+ }
398
+ if (await isDaemonRunning(pidashPort)) {
399
+ connect(ctx);
400
+ if (ctx.hasUI) ctx.ui.notify(`pidash server restarted at http://localhost:${pidashPort}`, "info");
401
+ } else {
402
+ if (ctx.hasUI) ctx.ui.notify("pidash server failed to restart — check ~/.pi/pidash-server.log", "warning");
403
+ }
404
+ return;
405
+ }
406
+
407
+ if (cmd === "status" || cmd === "") {
408
+ const running = await isDaemonRunning(pidashPort);
409
+ let msg = `Server: ${running ? "running" : "stopped"}\n`;
410
+ msg += `Port: ${pidashPort}\n`;
411
+ msg += `Extension: ${connected ? "connected" : "disconnected"}\n`;
412
+ msg += `URL: http://localhost:${pidashPort}`;
413
+ if (ctx.hasUI) ctx.ui.notify(msg, "info");
414
+ return;
415
+ }
416
+
417
+ if (ctx.hasUI) ctx.ui.notify("Usage: /pidash start|stop|restart|status", "info");
418
+ }
419
+
420
+ // ── connect() ──────────────────────────────────────────────────────
421
+
422
+ async function connect(ctx: any) {
423
+ debugLog(`connect() called, connected=${connected}, connecting=${connecting}, shuttingDown=${shuttingDown}, cwd=${ctx?.cwd}`);
424
+ if (connected || connecting || shuttingDown) return;
425
+ connecting = true;
426
+ lastCtx = ctx;
427
+
428
+ const running = await isDaemonRunning(pidashPort);
429
+ debugLog(`daemon running: ${running}`);
430
+ if (!running) {
431
+ if (spawning) {
432
+ debugLog("daemon already spawning, waiting...");
433
+ } else {
434
+ spawning = true;
435
+ debugLog("spawning daemon...");
436
+ spawnDaemon(pidashPort);
437
+ }
438
+ // jiti cold compilation can take 30+ seconds on first run
439
+ for (let i = 0; i < 60; i++) {
440
+ await new Promise(r => setTimeout(r, 1000));
441
+ if (await isDaemonRunning(pidashPort)) {
442
+ debugLog(`daemon ready after ${i + 1}s`);
443
+ break;
444
+ }
445
+ }
446
+ if (!(await isDaemonRunning(pidashPort))) {
447
+ debugLog("daemon failed to start after 60s");
448
+ spawning = false;
449
+ connecting = false;
450
+ return;
451
+ }
452
+ spawning = false;
453
+ }
454
+
455
+ try {
456
+ const _require = createRequire(import.meta.url);
457
+ const WebSocket = _require("ws");
458
+ debugLog("creating WebSocket client...");
459
+ const wsClient = new WebSocket(`ws://127.0.0.1:${pidashPort}/ws/pi`);
460
+
461
+ wsClient.on("open", () => {
462
+ debugLog("WebSocket connected!");
463
+ ws = wsClient;
464
+ connected = true;
465
+ connecting = false;
466
+
467
+ try {
468
+ // Register this session
469
+ const git = getGitStatus(ctx.cwd);
470
+ const m = ctx.model;
471
+ // Read thinking level from session entries
472
+ let thinking = "medium";
473
+ try {
474
+ thinking = (pi as any).getThinkingLevel?.() || "medium";
475
+ } catch (e: any) { console.debug("[pidash] getThinkingLevel failed:", e?.message || e); }
476
+ const reg = JSON.stringify({
477
+ type: "register",
478
+ pid: process.pid,
479
+ sessionId,
480
+ cwd: ctx.cwd,
481
+ branch: git.branch,
482
+ gitDirty: git.dirty,
483
+ gitChanges: git.changes,
484
+ container: isContainer(),
485
+ model: m?.name || m?.id || "",
486
+ contextWindow: m?.contextWindow || 0,
487
+ startedAt: new Date().toISOString(),
488
+ sessionFile: ctx.sessionManager?.getSessionFile?.() || ctx.sessionFile || "",
489
+ thinkingLevel: thinking,
490
+ diffPort,
491
+ });
492
+ debugLog(`sending register: ${reg}`);
493
+ wsClient.send(reg);
494
+
495
+ // Always load history from session file (source of truth)
496
+ eventBuffer.length = 0;
497
+ const pushBuffered = (ev: string) => {
498
+ wsClient.send(ev);
499
+ eventBuffer.push(ev);
500
+ while (eventBuffer.length > 10000) eventBuffer.shift();
501
+ };
502
+ replaySessionHistory(wsClient, ctx, pushBuffered);
503
+
504
+ } catch (err) {
505
+ // ctx may be stale if session was replaced during WebSocket connect
506
+ debugLog(`open handler error (likely stale ctx): ${err}`);
507
+ }
508
+
509
+ // Keepalive + dead connection detection
510
+ if (cleanupHeartbeat) cleanupHeartbeat();
511
+ cleanupHeartbeat = setupHeartbeat({
512
+ ws: wsClient,
513
+ log: debugLog,
514
+ onDead: () => {
515
+ connected = false;
516
+ ws = null;
517
+ if (!shuttingDown && lastCtx) setTimeout(() => connect(lastCtx), 1000);
518
+ },
519
+ });
520
+
521
+ // Show status
522
+ try {
523
+ if (ctx.hasUI) {
524
+ const link = hyperlink("pi-dash", `http://localhost:${pidashPort}`);
525
+ ctx.ui.setStatus("9-pidash", ctx.ui.theme.fg("accent", link));
526
+ }
527
+ } catch {
528
+ // ctx may be stale if session was replaced during WebSocket connect
529
+ }
530
+ });
531
+
532
+ wsClient.on("message", (data: Buffer) => { handleWsMessage(data); });
533
+
534
+ wsClient.on("close", (code: number, reason: Buffer) => {
535
+ debugLog(`WebSocket closed: code=${code} reason=${reason?.toString() || 'none'}`);
536
+ connecting = false;
537
+ debugLog("WebSocket closed");
538
+ connected = false;
539
+ ws = null;
540
+ if (!shuttingDown) {
541
+ setTimeout(() => { if (lastCtx && !shuttingDown) connect(lastCtx); }, RECONNECT_INTERVAL_MS);
542
+ }
543
+ });
544
+
545
+ wsClient.on("error", (e: Error) => {
546
+ debugLog(`WebSocket error: ${e.message}`);
547
+ });
548
+ } catch (e: any) {
549
+ debugLog(`connect error: ${e.message}`);
550
+ connecting = false;
551
+ }
552
+ }
553
+
554
+ // ── forward() helper ──────────────────────────────────────────────
555
+
556
+ // Forward events to daemon
557
+ function forward(type: string) {
558
+ pi.on(type as any, (event: any, ctx: any) => {
559
+ lastCtx = ctx;
560
+ let payload: any = { type, ...event, timestamp: Date.now() };
561
+
562
+ // Optimize message_update: strip the full accumulated partial message
563
+ // to prevent events growing larger as streaming progresses.
564
+ // Keep only delta + essential metadata (model, usage).
565
+ if (type === "message_update" && payload.assistantMessageEvent?.partial) {
566
+ const ae = payload.assistantMessageEvent;
567
+ payload = {
568
+ type,
569
+ assistantMessageEvent: {
570
+ type: ae.type,
571
+ delta: ae.delta,
572
+ contentIndex: ae.contentIndex,
573
+ content: ae.content,
574
+ partial: ae.partial ? {
575
+ model: ae.partial.model,
576
+ usage: ae.partial.usage,
577
+ provider: ae.partial.provider,
578
+ } : undefined,
579
+ },
580
+ timestamp: payload.timestamp,
581
+ };
582
+ }
583
+
584
+ // Strip full message from message_end (can be large for resumed sessions)
585
+ // Keep only role and essential content
586
+ if (type === "message_end" && payload.message) {
587
+ const m = payload.message;
588
+ payload = {
589
+ type,
590
+ message: {
591
+ role: m.role,
592
+ model: m.model,
593
+ usage: m.usage,
594
+ provider: m.provider,
595
+ customType: m.customType,
596
+ display: m.display,
597
+ },
598
+ timestamp: payload.timestamp,
599
+ };
600
+ }
601
+
602
+ const msg = JSON.stringify(payload);
603
+ if (type !== "extension_ui_request") {
604
+ eventBuffer.push(msg);
605
+ // Cap incremental buffer — session file is the source of truth for full history
606
+ while (eventBuffer.length > 10000) eventBuffer.shift();
607
+ }
608
+ if (ws && connected) {
609
+ try { ws.send(msg); } catch (e: any) { debugLog(`forward ${type} error: ${e.message}`); }
610
+ } else if (!connected) {
611
+ debugLog(`forward ${type}: ws not connected`);
612
+ }
613
+ });
614
+ }
615
+
616
+ // ── Setup functions ────────────────────────────────────────────────
617
+
618
+ /** Register wrapCtx on events that provide ctx, bridging TUI dialogs to browser. */
619
+ function setupCtxWrapping(): void {
620
+ // Wrap ALL ctx.ui dialog methods for pidash bridging
621
+ const wrapCtx = (_event: any, ctx: any) => {
622
+ if (!ctx?.ui || ctx.ui.__pidashWrapped) return;
623
+ ctx.ui.__pidashWrapped = true;
624
+ const origSelect = ctx.ui.select.bind(ctx.ui);
625
+ const origConfirm = ctx.ui.confirm.bind(ctx.ui);
626
+
627
+ function raceWithBrowser<T>(
628
+ askId: string,
629
+ payload: object,
630
+ origFn: (...args: any[]) => Promise<T>,
631
+ origArgs: any[],
632
+ extractBrowserValue: (r: any) => T,
633
+ opts?: any,
634
+ ): Promise<T> {
635
+ if (ws && connected) ws.send(JSON.stringify({ id: askId, ...payload }));
636
+ const ac = new AbortController();
637
+ let browserResolve: ((v: T) => void) | null = null;
638
+ const unsub = pi.events.on("pidash:ui-response", (data: unknown) => {
639
+ const r = data as any;
640
+ if (r.id === askId && browserResolve) { browserResolve(extractBrowserValue(r)); ac.abort(); }
641
+ });
642
+ return Promise.race([
643
+ origFn(...origArgs, { ...opts, signal: opts?.signal || ac.signal }).then((v: T) => {
644
+ browserResolve = null;
645
+ pi.events.emit("pidash:ui-dismiss", { type: "ui-dismiss", id: askId });
646
+ return v;
647
+ }),
648
+ new Promise<T>((resolve) => { browserResolve = resolve; }),
649
+ ]).finally(() => unsub());
650
+ }
651
+
652
+ ctx.ui.select = async (title: string, options: string[], opts?: any) => {
653
+ const askId = `dialog-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
654
+ return raceWithBrowser<string | undefined>(
655
+ askId,
656
+ { type: "extension_ui_request", method: "select", title, options },
657
+ origSelect, [title, options],
658
+ (r) => r.cancelled ? undefined : r.value,
659
+ opts,
660
+ );
661
+ };
662
+
663
+ ctx.ui.confirm = async (title: string, message: string, opts?: any) => {
664
+ const askId = `dialog-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
665
+ return raceWithBrowser<boolean>(
666
+ askId,
667
+ { type: "extension_ui_request", method: "confirm", title, message },
668
+ origConfirm, [title, message],
669
+ (r) => r.confirmed ?? false,
670
+ opts,
671
+ );
672
+ };
673
+ };
674
+
675
+ // Register wrapper on all events that provide ctx
676
+ for (const evt of ["tool_call", "tool_result", "agent_start", "turn_start"] as const) {
677
+ pi.on(evt as any, wrapCtx);
678
+ }
679
+ }
680
+
681
+ /** Register all forward() calls and individual pi.on() handlers that forward events to the daemon. */
682
+ function setupEventForwarding(): void {
683
+ forward("agent_start");
684
+ forward("agent_end");
685
+ forward("agent_settled");
686
+
687
+ // Track streaming state for prompt-queued feedback
688
+ pi.on("agent_start", () => { isStreaming = true; });
689
+ pi.on("agent_settled", () => { isStreaming = false; });
690
+ forward("turn_start");
691
+ forward("turn_end");
692
+ forward("message_start");
693
+ forward("message_update");
694
+ forward("message_end");
695
+ forward("tool_execution_start");
696
+ forward("tool_execution_update");
697
+ forward("tool_execution_end");
698
+ forward("tool_call");
699
+ forward("tool_result");
700
+ forward("session_info_changed");
701
+
702
+ pi.on("model_select", (event: any) => {
703
+ if (ws && connected) {
704
+ ws.send(JSON.stringify({
705
+ type: "update_info",
706
+ model: event.model?.name || event.model?.id || "",
707
+ contextWindow: event.model?.contextWindow || 0,
708
+ }));
709
+ }
710
+ });
711
+
712
+ // Sync thinking level on every turn
713
+ pi.on("turn_end", () => {
714
+ if (!ws || !connected) return;
715
+ try {
716
+ const level = (pi as any).getThinkingLevel?.();
717
+ if (level) ws.send(JSON.stringify({ type: "update_info", thinkingLevel: level }));
718
+ } catch {}
719
+ });
720
+
721
+ // Track diff viewer port
722
+ pi.events.on("diff-viewer:port", (port: unknown) => {
723
+ if (typeof port === "number") {
724
+ diffPort = port;
725
+ if (ws && connected) {
726
+ ws.send(JSON.stringify({ type: "update_info", diffPort: port }));
727
+ }
728
+ }
729
+ });
730
+
731
+ // Forward provider response info to pidash
732
+ pi.on("after_provider_response" as any, (event: any) => {
733
+ if (!ws || !connected) return;
734
+ try {
735
+ const info: any = { type: "provider_response" };
736
+ if (event.status) info.status = event.status;
737
+ if (event.headers) {
738
+ if (event.headers["x-ratelimit-remaining"]) info.rateLimitRemaining = event.headers["x-ratelimit-remaining"];
739
+ if (event.headers["x-ratelimit-reset"]) info.rateLimitReset = event.headers["x-ratelimit-reset"];
740
+ if (event.headers["retry-after"]) info.retryAfter = event.headers["retry-after"];
741
+ if (event.headers["x-request-id"]) info.requestId = event.headers["x-request-id"];
742
+ }
743
+ ws.send(JSON.stringify(info));
744
+ } catch (e: any) { debugLog(`provider response forward error: ${e.message}`); }
745
+ });
746
+ }
747
+
748
+ /** Register pi.events.on() listeners for pidash-specific events. */
749
+ function setupPidashEventListeners(): void {
750
+ // Forward ask_user requests to the daemon for browser display
751
+ pi.events.on("pidash:ui-request", (data: unknown) => {
752
+ if (ws && connected) {
753
+ try { ws.send(JSON.stringify(data)); } catch {}
754
+ }
755
+ });
756
+
757
+ // Forward dialog dismissals to the browser
758
+ pi.events.on("pidash:ui-dismiss", (data: unknown) => {
759
+ if (ws && connected) {
760
+ try { ws.send(JSON.stringify(data)); } catch {}
761
+ }
762
+ });
763
+
764
+ // Forward async agent status to browser
765
+ pi.events.on("pidash:async-status", (data: unknown) => {
766
+ if (ws && connected) {
767
+ try { ws.send(JSON.stringify({ type: "async-status", ...(data as any) })); } catch {}
768
+ }
769
+ });
770
+
771
+ // Forward cron status to browser
772
+ pi.events.on("pidash:cron-status", (data: unknown) => {
773
+ if (ws && connected) {
774
+ try { ws.send(JSON.stringify({ type: "cron-status", ...(data as any) })); } catch {}
775
+ }
776
+ });
777
+
778
+ // Listen for command handler registrations from other extensions
779
+ pi.events.on("pidash:register-command", (data: unknown) => {
780
+ const d = data as { name: string; handler: (args: string, ctx: any) => Promise<void> };
781
+ if (typeof d?.name === "string" && typeof d?.handler === "function") {
782
+ if (commandHandlerRegistry.has(d.name)) {
783
+ debugLog(`command handler overwritten: ${d.name}`);
784
+ }
785
+ commandHandlerRegistry.set(d.name, d.handler);
786
+ debugLog(`registered command handler: ${d.name}`);
787
+ }
788
+ });
789
+
790
+ // Request existing commands (handles load order — orchestrator may have loaded first)
791
+ pi.events.emit("pidash:request-commands");
792
+
793
+ // Capture command context from orchestrator for switch-session fallback
794
+ // Only accept real ExtensionCommandContext (has switchSession method)
795
+ pi.events.on("pidash:command-ctx", (ctx: unknown) => {
796
+ if (ctx && typeof (ctx as any)?.switchSession === "function") {
797
+ pidashCommandCtx = ctx as any;
798
+ }
799
+ });
800
+ }
801
+
802
+ /** Periodically send git status updates to the daemon. */
803
+ function setupPeriodicStatus(): void {
804
+ const statusInterval = setInterval(() => {
805
+ if (!ws || !connected || !lastCtx) return;
806
+ if (lastCtx.mode !== "tui") return;
807
+ try {
808
+ const git = getGitStatus(lastCtx.cwd);
809
+ ws.send(JSON.stringify({
810
+ type: "update_info",
811
+ branch: git.branch,
812
+ gitDirty: git.dirty,
813
+ gitChanges: git.changes,
814
+ }));
815
+ } catch (e: any) { debugLog(`periodic status error: ${e?.message || e}`); }
816
+ }, 10000);
817
+ if (statusInterval.unref) statusInterval.unref();
818
+ }
819
+
820
+ /** Handle session_start event — connect or notify session switch. */
821
+ function handleSessionStart(_event: any, ctx: any): void {
822
+ execCtx = ctx;
823
+ pidashCommandCtx = null;
824
+ debugLog("execCtx created from session_start");
825
+
826
+ // Auto-capture command context: silently run /pidash status.
827
+ // pi.sendUserMessage won't trigger command dispatch (expandPromptTemplates: false),
828
+ // so we call the handler directly. The context won't have switchSession yet,
829
+ // but the /pidash handler will be called with a real ExtensionCommandContext
830
+ // when the user types their first prompt (via before_agent_start triggering
831
+ // the input pipeline). For now, this at least initializes the connection.
832
+ // Session switching requires the user to have typed at least one slash command.
833
+ if (!connected && ctx.mode === "tui") {
834
+ connect(ctx);
835
+ } else if (ws) {
836
+ // Already connected — session switched (e.g., /resume, /new)
837
+ eventBuffer.length = 0; // Clear stale events to prevent cross-session replay on reconnect
838
+ ws.send(JSON.stringify({
839
+ type: "session_switch",
840
+ sessionId,
841
+ cwd: ctx.cwd,
842
+ branch: getCurrentBranch(ctx.cwd),
843
+ sessionFile: ctx.sessionManager?.getSessionFile?.() || ctx.sessionFile || "",
844
+ }));
845
+ debugLog(`session_switch sent: cwd=${ctx.cwd}`);
846
+ }
847
+ }
848
+
849
+ /** Handle input event — intercept extension commands from browser. */
850
+ async function handleBrowserInput(event: any, _ctx: any): Promise<{ action: "handled" } | void> {
851
+ if ((event as any).streamingBehavior && ws && connected) {
852
+ try { ws.send(JSON.stringify({ type: "streaming-behavior", behavior: (event as any).streamingBehavior })); }
853
+ catch (e: any) { debugLog(`streaming-behavior send error: ${e.message}`); }
854
+ }
855
+
856
+ // Intercept extension commands from browser
857
+ if (event.source !== "extension") return;
858
+ if (!event.text.startsWith("/")) return;
859
+
860
+ const text = event.text.trim();
861
+ const spaceIdx = text.indexOf(" ");
862
+ const cmdName = (spaceIdx > 0 ? text.slice(1, spaceIdx) : text.slice(1)).toLowerCase();
863
+ const arg = spaceIdx > 0 ? text.slice(spaceIdx + 1).trim() : "";
864
+
865
+ debugLog(`browser command: /${cmdName} ${arg.slice(0, 80)}`);
866
+
867
+ const handler = commandHandlerRegistry.get(cmdName);
868
+ if (handler) {
869
+ try {
870
+ await handler(arg, execCtx);
871
+ } catch (e: any) {
872
+ debugLog(`command /${cmdName} error: ${e.message}`);
873
+ }
874
+ return { action: "handled" as const };
875
+ }
876
+ }
877
+
878
+ // ── Wire everything up ─────────────────────────────────────────────
879
+
880
+ setupCtxWrapping();
881
+ setupEventForwarding();
882
+ setupPidashEventListeners();
883
+ setupPeriodicStatus();
884
+
885
+ pi.on("session_start", handleSessionStart);
886
+
887
+ // Fallback for /reload — connect on first tool_result if not connected
888
+ pi.on("tool_result", (_event, ctx) => {
889
+ if (!connected && !shuttingDown && ctx.mode === "tui") connect(ctx);
890
+ });
891
+
892
+ // Periodic reconnect — ensures sessions that started before the daemon still connect
893
+ const cleanupReconnect = setupReconnectPoller({
894
+ isConnected: () => connected,
895
+ isConnecting: () => connecting,
896
+ isShuttingDown: () => shuttingDown,
897
+ connect: () => { if (lastCtx?.mode === "tui") connect(lastCtx); },
898
+ });
899
+
900
+ // ── Command execution from browser ────────────────────────────────
901
+ //
902
+ // Problem: pi.sendUserMessage() disables command handling (expandPromptTemplates: false).
903
+ // Solution: Register a command that can execute other commands, and intercept
904
+ // browser-sourced / messages in the input event to route through it.
905
+
906
+ // Hidden command that captures ExtensionCommandContext
907
+ // Called once at startup via /pidash, then reused for all browser commands
908
+ pi.registerCommand("pidash", {
909
+ description: "Manage pidash server — /pidash start|stop|restart|status",
910
+ getArgumentCompletions: (prefix: string) => {
911
+ const items = [
912
+ { value: "start", label: "start", description: "Start pidash server" },
913
+ { value: "stop", label: "stop", description: "Stop pidash server" },
914
+ { value: "restart", label: "restart", description: "Restart pidash server" },
915
+ { value: "status", label: "status", description: "Show pidash status" },
916
+ ];
917
+ return items.filter(i => i.value.startsWith(prefix.toLowerCase()));
918
+ },
919
+ handler: (args, ctx) => handlePidashCommand(args, ctx),
920
+ });
921
+
922
+ pi.on("input", async (event, _ctx) => handleBrowserInput(event, _ctx));
923
+
924
+ pi.on("session_shutdown", (event) => {
925
+ // Forward shutdown reason to pidash dashboard
926
+ if (ws && connected) {
927
+ try {
928
+ ws.send(JSON.stringify({
929
+ type: "session_shutdown",
930
+ reason: (event as any).reason,
931
+ targetSessionFile: (event as any).targetSessionFile,
932
+ }));
933
+ } catch {}
934
+ }
935
+ shuttingDown = true;
936
+ cleanupReconnect();
937
+ if (cleanupHeartbeat) { cleanupHeartbeat(); cleanupHeartbeat = null; }
938
+ if (ws) { try { ws.close(); } catch {} ws = null; }
939
+ connected = false;
940
+ });
941
+ }