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.
- package/README.md +808 -0
- package/agents/api-documenter.md +32 -0
- package/agents/bash-expert.md +53 -0
- package/agents/code-reviewer-docs.md +145 -0
- package/agents/code-reviewer-guidelines.md +97 -0
- package/agents/code-reviewer-quality.md +142 -0
- package/agents/code-reviewer-security.md +107 -0
- package/agents/code-reviewer-spec.md +164 -0
- package/agents/debugger.md +63 -0
- package/agents/docker-expert.md +40 -0
- package/agents/docs-fetcher.md +48 -0
- package/agents/git-expert.md +63 -0
- package/agents/github-expert.md +83 -0
- package/agents/go-expert.md +62 -0
- package/agents/issue-reviewer-feasibility.md +60 -0
- package/agents/issue-reviewer-scope.md +63 -0
- package/agents/issue-reviewer-spec.md +77 -0
- package/agents/java-expert.md +31 -0
- package/agents/jenkins-expert.md +38 -0
- package/agents/kubernetes-expert.md +40 -0
- package/agents/planner.md +42 -0
- package/agents/python-expert.md +61 -0
- package/agents/reviewer.md +26 -0
- package/agents/scout.md +40 -0
- package/agents/security-auditor.md +254 -0
- package/agents/technical-documentation-writer.md +37 -0
- package/agents/test-automator.md +55 -0
- package/agents/test-runner.md +47 -0
- package/agents/ts-expert.md +23 -0
- package/agents/worker.md +30 -0
- package/extensions/acpx-provider/index.ts +104 -0
- package/extensions/acpx-provider/load-runtime.ts +99 -0
- package/extensions/acpx-provider/runtime-models.ts +40 -0
- package/extensions/cli-provider/agents/claude.ts +128 -0
- package/extensions/cli-provider/agents/cursor.ts +84 -0
- package/extensions/cli-provider/agents/gemini.ts +100 -0
- package/extensions/cli-provider/discover.ts +77 -0
- package/extensions/cli-provider/index.ts +57 -0
- package/extensions/cli-provider/parsers.ts +304 -0
- package/extensions/cli-provider/providers.ts +58 -0
- package/extensions/cli-provider/runner.ts +225 -0
- package/extensions/cli-provider/runtime-models.ts +32 -0
- package/extensions/cli-provider/session-reaper.ts +122 -0
- package/extensions/cli-provider/sessions.ts +533 -0
- package/extensions/cli-provider/shared/discover-cache.ts +235 -0
- package/extensions/cli-provider/types.ts +48 -0
- package/extensions/coms/coms-net-server.ts +1642 -0
- package/extensions/coms/coms-net-wrapper.ts +521 -0
- package/extensions/coms/coms-net.ts +1589 -0
- package/extensions/coms/coms-p2p.ts +1879 -0
- package/extensions/coms/coms-shared.ts +510 -0
- package/extensions/coms/coms-wrapper.ts +138 -0
- package/extensions/coms/index.ts +20 -0
- package/extensions/coms/themeMap.ts +147 -0
- package/extensions/image-gen/image-gen.ts +332 -0
- package/extensions/image-gen/index.ts +14 -0
- package/extensions/orchestrator/agents.ts +134 -0
- package/extensions/orchestrator/ask-user.ts +293 -0
- package/extensions/orchestrator/async-agents.ts +1037 -0
- package/extensions/orchestrator/async-capability.ts +129 -0
- package/extensions/orchestrator/async-runner.ts +324 -0
- package/extensions/orchestrator/async-status-parse.ts +31 -0
- package/extensions/orchestrator/async-status-ui.ts +201 -0
- package/extensions/orchestrator/async-wait.ts +36 -0
- package/extensions/orchestrator/btw.ts +210 -0
- package/extensions/orchestrator/cron-status-format.ts +126 -0
- package/extensions/orchestrator/cron-status-ui.ts +132 -0
- package/extensions/orchestrator/cron.ts +730 -0
- package/extensions/orchestrator/dreaming.ts +355 -0
- package/extensions/orchestrator/enforcement-helpers.ts +530 -0
- package/extensions/orchestrator/enforcement-rules.ts +329 -0
- package/extensions/orchestrator/enforcement.ts +828 -0
- package/extensions/orchestrator/extended-autocomplete.ts +593 -0
- package/extensions/orchestrator/git-helpers.ts +420 -0
- package/extensions/orchestrator/github-autocomplete.ts +250 -0
- package/extensions/orchestrator/icons.ts +17 -0
- package/extensions/orchestrator/index.ts +100 -0
- package/extensions/orchestrator/memory-embeddings.ts +281 -0
- package/extensions/orchestrator/memory-promotion.ts +449 -0
- package/extensions/orchestrator/memory-provenance.ts +80 -0
- package/extensions/orchestrator/memory-query-class.ts +90 -0
- package/extensions/orchestrator/memory-scoring.ts +434 -0
- package/extensions/orchestrator/memory-tools.ts +883 -0
- package/extensions/orchestrator/memory-tree.ts +212 -0
- package/extensions/orchestrator/nvim.ts +169 -0
- package/extensions/orchestrator/overlay-dashboard-utils.ts +25 -0
- package/extensions/orchestrator/overlay-dashboard.ts +512 -0
- package/extensions/orchestrator/pi-config-review-state.ts +303 -0
- package/extensions/orchestrator/preference-extractor.ts +106 -0
- package/extensions/orchestrator/project-settings.ts +468 -0
- package/extensions/orchestrator/promotion-queue.ts +311 -0
- package/extensions/orchestrator/resolve-agent-model.ts +32 -0
- package/extensions/orchestrator/review-ui.ts +204 -0
- package/extensions/orchestrator/rule-placeholders.ts +12 -0
- package/extensions/orchestrator/rules.ts +481 -0
- package/extensions/orchestrator/session-search.ts +243 -0
- package/extensions/orchestrator/session-validation.ts +374 -0
- package/extensions/orchestrator/situation-report.ts +370 -0
- package/extensions/orchestrator/status-bar.ts +31 -0
- package/extensions/orchestrator/status-line.ts +192 -0
- package/extensions/orchestrator/status.ts +110 -0
- package/extensions/orchestrator/subagent-tool.ts +1748 -0
- package/extensions/orchestrator/task-lifecycle.ts +88 -0
- package/extensions/orchestrator/utils.ts +227 -0
- package/extensions/pidash/index.ts +15 -0
- package/extensions/pidash/pidash.ts +941 -0
- package/extensions/pidiff/index.ts +13 -0
- package/extensions/pidiff/pidiff.ts +369 -0
- package/extensions/providers/acpx-driver.ts +479 -0
- package/extensions/providers/built-in-drivers.ts +52 -0
- package/extensions/providers/claude-driver.ts +319 -0
- package/extensions/providers/cursor-acpx-driver.ts +398 -0
- package/extensions/providers/cursor-cli-driver.ts +484 -0
- package/extensions/providers/gemini-driver.ts +302 -0
- package/extensions/providers/index.ts +513 -0
- package/extensions/shared/build-system-prompt.ts +57 -0
- package/extensions/shared/create-runtime-provider.ts +190 -0
- package/extensions/shared/daemon-manager.ts +248 -0
- package/extensions/shared/file-logger.ts +115 -0
- package/extensions/shared/managed-refresh.ts +211 -0
- package/extensions/shared/provider-driver.ts +295 -0
- package/extensions/shared/provider-errors.ts +305 -0
- package/extensions/shared/provider-registry.ts +388 -0
- package/extensions/shared/stream-builder.ts +275 -0
- package/extensions/shared/types.ts +20 -0
- package/extensions/shared/ui/README.md +10 -0
- package/extensions/shared/ui/badge.tsx +52 -0
- package/extensions/shared/ui/button.tsx +58 -0
- package/extensions/shared/ui/collapsible.tsx +21 -0
- package/extensions/shared/ui/dropdown-menu.tsx +266 -0
- package/extensions/shared/ui/input.tsx +20 -0
- package/extensions/shared/ui/popover.tsx +114 -0
- package/extensions/shared/ui/resizable.tsx +48 -0
- package/extensions/shared/ui/scroll-area.tsx +53 -0
- package/extensions/shared/ui/select.tsx +201 -0
- package/extensions/shared/ui/separator.tsx +25 -0
- package/extensions/shared/ui/tooltip.tsx +64 -0
- package/extensions/shared/ws-client.ts +74 -0
- package/package.json +59 -0
- package/prompts/coderabbit-rate-limit.md +95 -0
- package/prompts/create-coms-feature-manager.md +186 -0
- package/prompts/create-skill.md +128 -0
- package/prompts/domain-model.md +109 -0
- package/prompts/external-ai.md +665 -0
- package/prompts/implement-and-review.md +25 -0
- package/prompts/implement.md +26 -0
- package/prompts/issue-review.md +256 -0
- package/prompts/pr-review.md +753 -0
- package/prompts/query-db.md +154 -0
- package/prompts/refine-review.md +208 -0
- package/prompts/release.md +235 -0
- package/prompts/remember.md +33 -0
- package/prompts/review-handler-status.md +27 -0
- package/prompts/review-handler.md +677 -0
- package/prompts/review-local.md +159 -0
- package/prompts/scout-and-plan.md +21 -0
- package/rules/00-orchestrator-core.md +42 -0
- package/rules/05-issue-first-workflow.md +119 -0
- package/rules/10-agent-routing.md +58 -0
- package/rules/15-mcp-launchpad.md +46 -0
- package/rules/20-code-review-loop.md +234 -0
- package/rules/25-documentation-updates.md +15 -0
- package/rules/30-prompt-templates.md +19 -0
- package/rules/35-memory.md +240 -0
- package/rules/40-critical-rules.md +124 -0
- package/rules/45-file-preview.md +29 -0
- package/rules/50-agent-bug-reporting.md +90 -0
- package/rules/55-coms-protocol.md +63 -0
- package/rules/60-task-tracking.md +66 -0
- package/skills/conflict-resolver/SKILL.md +213 -0
- package/skills/skill-creator/SKILL.md +85 -0
- package/skills/systematic-debugging/SKILL.md +353 -0
- package/skills/test-driven-development/SKILL.md +335 -0
- package/skills/update-agents-md/SKILL.md +282 -0
- 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
|
+
}
|