pi-orchestrator-config 4.2.0 → 4.3.2

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 (100) hide show
  1. package/README.md +76 -67
  2. package/agents/code-reviewer-docs.md +11 -1
  3. package/agents/code-reviewer-guidelines.md +11 -1
  4. package/agents/code-reviewer-quality.md +11 -1
  5. package/agents/code-reviewer-security.md +11 -1
  6. package/agents/code-reviewer-spec.md +11 -1
  7. package/agents/git-expert.md +42 -3
  8. package/agents/github-expert.md +11 -0
  9. package/extensions/acpx-provider/runtime-models.ts +18 -6
  10. package/extensions/cli-provider/agents/claude.ts +3 -2
  11. package/extensions/cli-provider/agents/cursor.ts +3 -5
  12. package/extensions/cli-provider/agents/gemini.ts +3 -2
  13. package/extensions/cli-provider/discover.ts +7 -7
  14. package/extensions/cli-provider/index.ts +5 -0
  15. package/extensions/cli-provider/runtime-models.ts +18 -6
  16. package/extensions/cli-provider/session-reaper.ts +5 -7
  17. package/extensions/cli-provider/sessions.ts +4 -0
  18. package/extensions/cli-provider/shared/discover-cache.ts +17 -176
  19. package/extensions/coms/coms-p2p.ts +1719 -423
  20. package/extensions/coms/coms-shared.ts +261 -39
  21. package/extensions/coms/coms-wrapper.ts +100 -62
  22. package/extensions/coms/index.ts +8 -4
  23. package/extensions/coms/mixed-turn.ts +26 -0
  24. package/extensions/coms/probe-socket.ts +42 -0
  25. package/extensions/coms/themeMap.ts +0 -1
  26. package/extensions/image-gen/image-gen.ts +19 -1
  27. package/extensions/orchestrator/async-agents.ts +563 -102
  28. package/extensions/orchestrator/async-capability.ts +8 -8
  29. package/extensions/orchestrator/cron.ts +15 -9
  30. package/extensions/orchestrator/dreaming.ts +32 -25
  31. package/extensions/orchestrator/enforcement-helpers.ts +459 -17
  32. package/extensions/orchestrator/enforcement-rules.ts +5 -4
  33. package/extensions/orchestrator/enforcement.ts +80 -27
  34. package/extensions/orchestrator/extended-autocomplete.ts +7 -0
  35. package/extensions/orchestrator/git-helpers.ts +2 -2
  36. package/extensions/orchestrator/index.ts +78 -10
  37. package/extensions/orchestrator/markdown-transformer.ts +313 -0
  38. package/extensions/orchestrator/memory-embeddings.ts +36 -34
  39. package/extensions/orchestrator/memory-provenance.ts +29 -25
  40. package/extensions/orchestrator/memory-scoring.ts +23 -18
  41. package/extensions/orchestrator/memory-tools.ts +10 -3
  42. package/extensions/orchestrator/overlay-dashboard.ts +17 -5
  43. package/extensions/orchestrator/parse-model-override.ts +29 -0
  44. package/extensions/orchestrator/pi-config-review-state.ts +57 -33
  45. package/extensions/orchestrator/project-settings.ts +333 -127
  46. package/extensions/orchestrator/resolve-agent-model.ts +24 -13
  47. package/extensions/orchestrator/review-porcelain.ts +4 -0
  48. package/extensions/orchestrator/review-ui.ts +63 -15
  49. package/extensions/orchestrator/rule-placeholders.ts +411 -2
  50. package/extensions/orchestrator/rules.ts +80 -132
  51. package/extensions/orchestrator/settings-tui-helpers.ts +380 -0
  52. package/extensions/orchestrator/settings-tui-submenus.ts +647 -0
  53. package/extensions/orchestrator/settings-tui.ts +636 -0
  54. package/extensions/orchestrator/state-jsonl.ts +431 -0
  55. package/extensions/orchestrator/subagent-tool.ts +157 -59
  56. package/extensions/orchestrator/sync-limit.ts +23 -0
  57. package/extensions/orchestrator/task-lifecycle.ts +77 -67
  58. package/extensions/orchestrator/utils.ts +8 -5
  59. package/extensions/pidash/pidash.ts +193 -156
  60. package/extensions/pidiff/pidiff.ts +96 -38
  61. package/extensions/pidiff/spawn-lock.ts +62 -0
  62. package/extensions/pitasks/index.ts +430 -0
  63. package/extensions/pitasks/reminders.ts +37 -0
  64. package/extensions/pitasks/task-store.ts +467 -0
  65. package/extensions/pitasks/task-tools.ts +375 -0
  66. package/extensions/pitasks/task-widget.ts +181 -0
  67. package/extensions/pitasks/types.ts +38 -0
  68. package/extensions/providers/acpx-driver.ts +12 -2
  69. package/extensions/providers/built-in-drivers.ts +6 -6
  70. package/extensions/providers/claude-driver.ts +1 -1
  71. package/extensions/providers/cursor-cli-driver.ts +1 -1
  72. package/extensions/providers/gemini-driver.ts +1 -1
  73. package/extensions/providers/index.ts +144 -26
  74. package/extensions/providers/initialized-guard.ts +27 -0
  75. package/extensions/providers/restore-default-model.ts +596 -0
  76. package/extensions/providers/session-shutdown.ts +39 -0
  77. package/extensions/shared/coms-active.ts +23 -0
  78. package/extensions/shared/create-runtime-provider.ts +2 -0
  79. package/extensions/shared/daemon-manager.ts +0 -8
  80. package/extensions/shared/file-logger.ts +132 -25
  81. package/extensions/shared/logger.ts +50 -0
  82. package/extensions/shared/models-dev.ts +421 -0
  83. package/extensions/shared/oneshot.ts +116 -0
  84. package/extensions/shared/resolve-binary.ts +191 -0
  85. package/extensions/shared/types.ts +3 -0
  86. package/package.json +9 -3
  87. package/prompts/coderabbit-rate-limit.md +2 -0
  88. package/rules/00-orchestrator-core.md +3 -0
  89. package/rules/10-agent-routing.md +12 -1
  90. package/rules/20-code-review-loop.md +52 -25
  91. package/rules/30-prompt-templates.md +4 -0
  92. package/rules/35-memory.md +3 -3
  93. package/rules/40-critical-rules.md +3 -0
  94. package/rules/55-coms-protocol.md +84 -18
  95. package/rules/60-task-tracking.md +2 -0
  96. package/settings-keys.json +563 -0
  97. package/templates/coms-feature-manager-prompt.md +4 -6
  98. package/extensions/coms/coms-net-server.ts +0 -1642
  99. package/extensions/coms/coms-net-wrapper.ts +0 -521
  100. package/extensions/coms/coms-net.ts +0 -1589
@@ -17,17 +17,21 @@ import { setupHeartbeat, setupReconnectPoller } from "../shared/ws-client.js";
17
17
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
18
18
  import { SessionManager } from "@earendil-works/pi-coding-agent";
19
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";
20
+ import { checkHealth, ensureUiBuilt, spawnDaemon as spawnDaemonGeneric, killDaemon } from "../shared/daemon-manager.js";
23
21
  import { getSetting } from "../orchestrator/project-settings.js";
22
+ import { shouldSkipOneshotRegister } from "../shared/oneshot.js";
23
+ import { createLogger } from "../shared/logger.js";
24
24
 
25
- const RECONNECT_INTERVAL_MS = 5000;
25
+ const log = createLogger("pidash");
26
+
27
+ // Module-level ref — survives closure replacement on /reload.
28
+ // raceWithBrowser captures `pi` from the closure, but after reload
29
+ // the old `pi` is stale (assertActive throws). This ref is updated
30
+ // on each extension factory call so event listeners always use the
31
+ // current runtime.
32
+ let currentPiEvents: any = null;
26
33
 
27
- const debugLog = createLogger(
28
- path.join(process.env.HOME || "/tmp", ".pi", "pidash-debug.log"),
29
- "ext",
30
- );
34
+ const RECONNECT_INTERVAL_MS = 5000;
31
35
 
32
36
  // ── Helpers ──────────────────────────────────────────────────────────
33
37
 
@@ -36,12 +40,12 @@ function isDaemonRunning(port: number): Promise<boolean> {
36
40
  }
37
41
 
38
42
  function spawnDaemon(port: number): void {
39
- ensureUiBuilt(import.meta.url, "pidash-ui", debugLog);
43
+ ensureUiBuilt(import.meta.url, "pidash-ui", (msg) => log.debug(msg));
40
44
  spawnDaemonGeneric({
41
45
  serverScript: "pidash-server.ts",
42
46
  logFile: path.join(process.env.HOME || "/tmp", ".pi", "pidash-server.log"),
43
47
  env: { PI_PIDASH_PORT: String(port) },
44
- log: debugLog,
48
+ log: (msg) => log.debug(msg),
45
49
  });
46
50
  }
47
51
 
@@ -82,6 +86,8 @@ export function registerPidash(
82
86
  pi: ExtensionAPI,
83
87
  ): void {
84
88
  if (process.env.PI_SUBAGENT_CHILD === "1") return;
89
+ if (shouldSkipOneshotRegister(log)) return;
90
+ currentPiEvents = pi.events;
85
91
 
86
92
  const projectCwd = process.cwd();
87
93
  const pidashPort = getSetting(projectCwd, "pidash_port");
@@ -113,11 +119,50 @@ export function registerPidash(
113
119
  let shuttingDown = false;
114
120
  let spawning = false;
115
121
  let lastCtx: any = null;
122
+ let lastCmdCtx: any = null; // Command context with switchSession — updated by /pidash handler
116
123
  let cleanupHeartbeat: (() => void) | null = null;
124
+ let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
117
125
  const sessionId = `${process.pid}:${process.cwd()}`;
118
126
  const eventBuffer: string[] = []; // Buffer events for replay on daemon reconnect
119
127
  let execCtx: any = null;
120
- let pidashCommandCtx: any = null; // Real ExtensionCommandContext from /pidash handler
128
+ let comsIdentityName: string | undefined;
129
+ let comsIdentityPurpose: string | undefined;
130
+ let comsIdentityProject: string | undefined;
131
+ const commandHandlerRegistry = new Map<string, (args: string, ctx: any) => Promise<void> | void>();
132
+
133
+ // Listen for coms identity — coms ext emits this after boot
134
+ pi.events.on("pidash:coms-identity", (data: any) => {
135
+ comsIdentityName = data?.name;
136
+ comsIdentityPurpose = data?.purpose;
137
+ comsIdentityProject = data?.project;
138
+ log.debug("coms_identity_received", comsIdentityName);
139
+ if (ws && connected) {
140
+ try { ws.send(JSON.stringify({ type: "update_info", comsName: comsIdentityName, comsPurpose: comsIdentityPurpose, comsProject: comsIdentityProject })); } catch {}
141
+ }
142
+ });
143
+
144
+ // ── Command handler bridge via pi.events ──────────────────────────
145
+ // TODO: Remove once pidash uses PiClient/RemoteSession (#732) — native
146
+ // command dispatch via RemoteSession.submit() eliminates this.
147
+ pi.events.on("pidash:register-command", (data: unknown) => {
148
+ const d = data as { name: string; handler: (args: string, ctx: any) => Promise<void> };
149
+ if (typeof d?.name === "string" && typeof d?.handler === "function") {
150
+ commandHandlerRegistry.set(d.name, d.handler);
151
+ log.debug("registered command handler", d.name);
152
+ }
153
+ });
154
+
155
+ // Request existing commands (handles load order — orchestrator loaded first)
156
+ // TODO: Remove once pidash uses PiClient/RemoteSession (#732)
157
+ pi.events.emit("pidash:request-commands");
158
+
159
+ // Capture command context from orchestrator
160
+ // TODO: Remove once pidash uses PiClient/RemoteSession (#732)
161
+ pi.events.on("pidash:command-ctx", (ctx: unknown) => {
162
+ if (ctx && typeof (ctx as any)?.switchSession === "function") {
163
+ lastCmdCtx = ctx as any;
164
+ }
165
+ });
121
166
 
122
167
  // ── Extracted inner functions ──────────────────────────────────────
123
168
 
@@ -207,9 +252,9 @@ export function registerPidash(
207
252
 
208
253
  historyCount++;
209
254
  }
210
- if (historyCount > 0) debugLog(`loaded ${historyCount} entries from session history`);
255
+ if (historyCount > 0) log.debug(`loaded ${historyCount} entries from session history`);
211
256
  } catch (e: any) {
212
- debugLog(`session history load error: ${e.message}`);
257
+ log.debug(`session history load error: ${e.message}`);
213
258
  }
214
259
 
215
260
  // Signal replay is complete so the server can stop suppressing notifications
@@ -221,10 +266,10 @@ export function registerPidash(
221
266
  try {
222
267
  const parsed = JSON.parse(data.toString());
223
268
  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]` : ""}`);
269
+ log.debug(`received prompt from browser: ${(parsed.text || "").slice(0, 100)}${parsed.images ? ` [+${parsed.images.length} images]` : ""}`);
225
270
  // Notify browser if prompt is queued during streaming
226
271
  if (isStreaming && ws && connected) {
227
- try { ws.send(JSON.stringify({ type: "prompt-queued" })); } catch (e: any) { debugLog(`prompt-queued send error: ${e.message}`); }
272
+ try { ws.send(JSON.stringify({ type: "prompt-queued" })); } catch (e: any) { log.debug(`prompt-queued send error: ${e.message}`); }
228
273
  }
229
274
  if (parsed.images && parsed.images.length > 0) {
230
275
  // Build content array with text + images
@@ -241,18 +286,18 @@ export function registerPidash(
241
286
  }
242
287
  }
243
288
  if (parsed.type === "extension_ui_response" && parsed.id) {
244
- debugLog(`received UI response from browser: ${JSON.stringify(parsed).slice(0, 100)}`);
289
+ log.debug(`received UI response from browser: ${JSON.stringify(parsed).slice(0, 100)}`);
245
290
  pi.events.emit("pidash:ui-response", parsed);
246
291
  }
247
292
  if (parsed.type === "pidash-command") {
248
- debugLog(`received command from browser: ${parsed.command}`);
293
+ log.debug(`received command from browser: ${parsed.command}`);
249
294
 
250
295
  if (parsed.command === "list-sessions") {
251
296
  try {
252
297
  const sessions = await SessionManager.list(lastCtx?.cwd || process.cwd());
253
- debugLog(`list-sessions: found ${sessions.length}`);
298
+ log.debug(`list-sessions: found ${sessions.length}`);
254
299
  if (ws && connected) ws.send(JSON.stringify({ type: "sessions-list", sessions }));
255
- } catch (e: any) { debugLog(`list-sessions error: ${e.message}`); }
300
+ } catch (e: any) { log.debug(`list-sessions error: ${e.message}`); }
256
301
  }
257
302
 
258
303
  if (parsed.command === "list-models") {
@@ -264,12 +309,12 @@ export function registerPidash(
264
309
  name: m.name,
265
310
  provider: typeof m.provider === "string" ? m.provider : m.provider?.name || "",
266
311
  }));
267
- debugLog(`models found: ${list.length}`);
312
+ log.debug(`models found: ${list.length}`);
268
313
  if (ws && connected) ws.send(JSON.stringify({ type: "models-list", models: list }));
269
314
  } else {
270
- debugLog("list-models: no modelRegistry on ctx");
315
+ log.debug("list-models: no modelRegistry on ctx");
271
316
  }
272
- } catch (e: any) { debugLog(`list-models error: ${e.message}`); }
317
+ } catch (e: any) { log.debug(`list-models error: ${e.message}`); }
273
318
  }
274
319
 
275
320
  if (parsed.command === "set-model" && parsed.modelId) {
@@ -278,68 +323,85 @@ export function registerPidash(
278
323
  m.id === parsed.modelId || m.name === parsed.modelId || m.id.includes(parsed.modelId) || m.name.includes(parsed.modelId));
279
324
  if (model) {
280
325
  await (pi as any).setModel(model);
281
- debugLog(`model set to: ${model.name}`);
326
+ log.debug(`model set to: ${model.name}`);
282
327
  ws.send(JSON.stringify({ type: "update_info", model: model.name, contextWindow: model.contextWindow || 0 }));
283
328
  }
284
- } catch (e: any) { debugLog(`set-model error: ${e.message}`); }
329
+ } catch (e: any) { log.debug(`set-model error: ${e.message}`); }
285
330
  }
286
331
 
287
332
  if (parsed.command === "set-thinking" && parsed.level) {
288
333
  try {
289
334
  (pi as any).setThinkingLevel(parsed.level);
290
- debugLog(`thinking set to: ${parsed.level}`);
335
+ log.debug(`thinking set to: ${parsed.level}`);
291
336
  ws.send(JSON.stringify({ type: "update_info", thinkingLevel: parsed.level }));
292
- } catch (e: any) { debugLog(`set-thinking error: ${e.message}`); }
337
+ } catch (e: any) { log.debug(`set-thinking error: ${e.message}`); }
293
338
  }
294
339
 
295
340
  if (parsed.command === "switch-session" && parsed.sessionFile) {
296
- debugLog(`switch-session: ${parsed.sessionFile}`);
297
- const ctx = pidashCommandCtx;
341
+ log.debug(`switch-session: ${parsed.sessionFile}`);
342
+ const ctx = lastCmdCtx ?? lastCtx;
298
343
  if (ctx?.switchSession) {
299
344
  try {
300
345
  await ctx.switchSession(parsed.sessionFile, {
301
346
  withSession: async () => {
302
- debugLog("switch-session: completed");
347
+ log.debug("switch-session: completed");
303
348
  },
304
349
  });
305
350
  } catch (e: any) {
306
- debugLog(`switch-session error: ${e.message}`);
351
+ log.debug(`switch-session error: ${e.message}`);
307
352
  }
308
353
  } else {
309
- debugLog("switch-session: no command context available");
354
+ log.debug("switch-session: no command context available");
310
355
  }
311
356
  }
312
357
 
313
358
  if (parsed.command === "abort") {
314
359
  if (lastCtx) {
315
- try { lastCtx.abort(); debugLog("abort sent"); } catch {}
360
+ try { lastCtx.abort(); log.debug("abort sent"); } catch {}
316
361
  }
317
362
  }
318
363
 
319
364
  if (parsed.command === "async-kill" && parsed.target) {
320
- debugLog(`async-kill from browser: ${parsed.target}`);
365
+ log.debug(`async-kill from browser: ${parsed.target}`);
321
366
  pi.events.emit("pidash:async-kill", parsed.target);
322
367
  }
323
368
 
324
369
  if (parsed.command === "cron-kill" && parsed.target) {
325
- debugLog(`cron-kill from browser: ${parsed.target}`);
370
+ log.debug(`cron-kill from browser: ${parsed.target}`);
326
371
  pi.events.emit("pidash:cron-kill", parsed.target);
327
372
  }
328
373
 
374
+ if (parsed.command === "rename-session") {
375
+ log.debug("rename-session full parsed", JSON.stringify(parsed));
376
+ const newName = parsed.name ?? parsed.newName ?? "";
377
+ if (newName !== undefined) {
378
+ try {
379
+ (pi as any).setSessionName?.(newName);
380
+ sessionNamed = true;
381
+ log.debug(`session renamed: ${newName}`);
382
+ // Also send update_info to ensure sidebar updates immediately
383
+ if (ws && connected) {
384
+ try { ws.send(JSON.stringify({ type: "update_info", name: newName })); } catch {}
385
+ }
386
+ } catch (e: any) { log.debug(`rename-session error: ${e.message}`); }
387
+ }
388
+ }
389
+
329
390
  if (parsed.command === "list-commands") {
330
391
  try {
331
392
  const cmds = (pi as any).getCommands?.() || [];
332
393
  const list = cmds.map((c: any) => ({ name: c.name, description: c.description || "" }));
333
394
  if (ws && connected) ws.send(JSON.stringify({ type: "commands-list", commands: list }));
334
- } catch (e: any) { debugLog(`list-commands error: ${e.message}`); }
395
+ } catch (e: any) { log.debug(`list-commands error: ${e.message}`); }
335
396
  }
336
397
 
337
398
  }
338
- } catch (e: any) { debugLog(`message handler error: ${e.message}`); }
399
+ } catch (e: any) { log.debug(`message handler error: ${e.message}`); }
339
400
  }
340
401
 
341
402
  /** Handle /pidash command (start|stop|restart|status). */
342
403
  async function handlePidashCommand(args: string, ctx: any): Promise<void> {
404
+ lastCmdCtx = ctx;
343
405
  // Guard: pidash daemon connections only in TUI mode
344
406
  if (ctx.mode !== "tui") {
345
407
  if (ctx.hasUI) ctx.ui.notify("pidash is only available in TUI mode.", "info");
@@ -347,15 +409,13 @@ export function registerPidash(
347
409
  }
348
410
 
349
411
  execCtx = ctx;
350
- pidashCommandCtx = ctx; // Real ExtensionCommandContext
351
- debugLog("pidashCommandCtx captured from /pidash handler");
352
412
 
353
413
  const cmd = (args || "").trim().toLowerCase();
354
414
 
355
415
  if (cmd === "stop") {
356
416
  if (ws) { try { ws.close(); } catch {} ws = null; }
357
417
  connected = false;
358
- killDaemon("pidash-server", debugLog);
418
+ killDaemon("pidash-server", (msg) => log.debug(msg));
359
419
  if (ctx.hasUI) {
360
420
  ctx.ui.setStatus("9-pidash", undefined);
361
421
  ctx.ui.notify("pidash server stopped", "info");
@@ -387,7 +447,7 @@ export function registerPidash(
387
447
  if (cmd === "restart") {
388
448
  if (ws) { try { ws.close(); } catch {} ws = null; }
389
449
  connected = false;
390
- killDaemon("pidash-server", debugLog);
450
+ killDaemon("pidash-server", (msg) => log.debug(msg));
391
451
  await new Promise(r => setTimeout(r, 1000));
392
452
  spawnDaemon(pidashPort);
393
453
  if (ctx.hasUI) ctx.ui.notify("Restarting pidash server...", "info");
@@ -420,31 +480,33 @@ export function registerPidash(
420
480
  // ── connect() ──────────────────────────────────────────────────────
421
481
 
422
482
  async function connect(ctx: any) {
423
- debugLog(`connect() called, connected=${connected}, connecting=${connecting}, shuttingDown=${shuttingDown}, cwd=${ctx?.cwd}`);
483
+ log.debug(`connect() called, connected=${connected}, connecting=${connecting}, shuttingDown=${shuttingDown}, cwd=${ctx?.cwd}`);
424
484
  if (connected || connecting || shuttingDown) return;
485
+ // Guard against stale ctx from surviving setTimeout after reload
486
+ try { void ctx?.ui?.theme; } catch { log.debug("connect() skipped — stale ctx"); return; }
425
487
  connecting = true;
426
488
  lastCtx = ctx;
427
489
 
428
490
  const running = await isDaemonRunning(pidashPort);
429
- debugLog(`daemon running: ${running}`);
491
+ log.debug(`daemon running: ${running}`);
430
492
  if (!running) {
431
493
  if (spawning) {
432
- debugLog("daemon already spawning, waiting...");
494
+ log.debug("daemon already spawning, waiting...");
433
495
  } else {
434
496
  spawning = true;
435
- debugLog("spawning daemon...");
497
+ log.debug("spawning daemon...");
436
498
  spawnDaemon(pidashPort);
437
499
  }
438
500
  // jiti cold compilation can take 30+ seconds on first run
439
501
  for (let i = 0; i < 60; i++) {
440
502
  await new Promise(r => setTimeout(r, 1000));
441
503
  if (await isDaemonRunning(pidashPort)) {
442
- debugLog(`daemon ready after ${i + 1}s`);
504
+ log.debug(`daemon ready after ${i + 1}s`);
443
505
  break;
444
506
  }
445
507
  }
446
508
  if (!(await isDaemonRunning(pidashPort))) {
447
- debugLog("daemon failed to start after 60s");
509
+ log.debug("daemon failed to start after 60s");
448
510
  spawning = false;
449
511
  connecting = false;
450
512
  return;
@@ -455,11 +517,11 @@ export function registerPidash(
455
517
  try {
456
518
  const _require = createRequire(import.meta.url);
457
519
  const WebSocket = _require("ws");
458
- debugLog("creating WebSocket client...");
520
+ log.debug("creating WebSocket client...");
459
521
  const wsClient = new WebSocket(`ws://127.0.0.1:${pidashPort}/ws/pi`);
460
522
 
461
523
  wsClient.on("open", () => {
462
- debugLog("WebSocket connected!");
524
+ log.debug("WebSocket connected!");
463
525
  ws = wsClient;
464
526
  connected = true;
465
527
  connecting = false;
@@ -488,8 +550,12 @@ export function registerPidash(
488
550
  sessionFile: ctx.sessionManager?.getSessionFile?.() || ctx.sessionFile || "",
489
551
  thinkingLevel: thinking,
490
552
  diffPort,
553
+ name: (pi as any).getSessionName?.() || undefined,
554
+ comsName: comsIdentityName || (pi as any).getFlag?.("cname") || undefined,
555
+ comsPurpose: comsIdentityPurpose || (pi as any).getFlag?.("purpose") || undefined,
556
+ comsProject: comsIdentityProject || undefined,
491
557
  });
492
- debugLog(`sending register: ${reg}`);
558
+ log.debug(`sending register: ${reg}`);
493
559
  wsClient.send(reg);
494
560
 
495
561
  // Always load history from session file (source of truth)
@@ -503,18 +569,21 @@ export function registerPidash(
503
569
 
504
570
  } catch (err) {
505
571
  // ctx may be stale if session was replaced during WebSocket connect
506
- debugLog(`open handler error (likely stale ctx): ${err}`);
572
+ log.debug(`open handler error (likely stale ctx): ${err}`);
507
573
  }
508
574
 
509
575
  // Keepalive + dead connection detection
510
576
  if (cleanupHeartbeat) cleanupHeartbeat();
511
577
  cleanupHeartbeat = setupHeartbeat({
512
578
  ws: wsClient,
513
- log: debugLog,
579
+ log: (msg) => log.debug(msg),
514
580
  onDead: () => {
515
581
  connected = false;
516
582
  ws = null;
517
- if (!shuttingDown && lastCtx) setTimeout(() => connect(lastCtx), 1000);
583
+ if (!shuttingDown && lastCtx) {
584
+ if (reconnectTimer) clearTimeout(reconnectTimer);
585
+ reconnectTimer = setTimeout(() => { reconnectTimer = null; if (lastCtx && !shuttingDown) connect(lastCtx); }, 1000);
586
+ }
518
587
  },
519
588
  });
520
589
 
@@ -532,21 +601,22 @@ export function registerPidash(
532
601
  wsClient.on("message", (data: Buffer) => { handleWsMessage(data); });
533
602
 
534
603
  wsClient.on("close", (code: number, reason: Buffer) => {
535
- debugLog(`WebSocket closed: code=${code} reason=${reason?.toString() || 'none'}`);
604
+ log.debug(`WebSocket closed: code=${code} reason=${reason?.toString() || 'none'}`);
536
605
  connecting = false;
537
- debugLog("WebSocket closed");
606
+ log.debug("WebSocket closed");
538
607
  connected = false;
539
608
  ws = null;
540
609
  if (!shuttingDown) {
541
- setTimeout(() => { if (lastCtx && !shuttingDown) connect(lastCtx); }, RECONNECT_INTERVAL_MS);
610
+ if (reconnectTimer) clearTimeout(reconnectTimer);
611
+ reconnectTimer = setTimeout(() => { reconnectTimer = null; if (lastCtx && !shuttingDown) connect(lastCtx); }, RECONNECT_INTERVAL_MS);
542
612
  }
543
613
  });
544
614
 
545
615
  wsClient.on("error", (e: Error) => {
546
- debugLog(`WebSocket error: ${e.message}`);
616
+ log.debug(`WebSocket error: ${e.message}`);
547
617
  });
548
618
  } catch (e: any) {
549
- debugLog(`connect error: ${e.message}`);
619
+ log.debug(`connect error: ${e.message}`);
550
620
  connecting = false;
551
621
  }
552
622
  }
@@ -556,6 +626,7 @@ export function registerPidash(
556
626
  // Forward events to daemon
557
627
  function forward(type: string) {
558
628
  pi.on(type as any, (event: any, ctx: any) => {
629
+ if (shuttingDown) return;
559
630
  lastCtx = ctx;
560
631
  let payload: any = { type, ...event, timestamp: Date.now() };
561
632
 
@@ -585,6 +656,7 @@ export function registerPidash(
585
656
  // Keep only role and essential content
586
657
  if (type === "message_end" && payload.message) {
587
658
  const m = payload.message;
659
+ const isStreamed = ["user", "assistant", "toolResult"].includes(m.role);
588
660
  payload = {
589
661
  type,
590
662
  message: {
@@ -594,6 +666,7 @@ export function registerPidash(
594
666
  provider: m.provider,
595
667
  customType: m.customType,
596
668
  display: m.display,
669
+ ...(isStreamed ? {} : { content: m.content }),
597
670
  },
598
671
  timestamp: payload.timestamp,
599
672
  };
@@ -606,78 +679,15 @@ export function registerPidash(
606
679
  while (eventBuffer.length > 10000) eventBuffer.shift();
607
680
  }
608
681
  if (ws && connected) {
609
- try { ws.send(msg); } catch (e: any) { debugLog(`forward ${type} error: ${e.message}`); }
682
+ try { ws.send(msg); } catch (e: any) { log.debug(`forward ${type} error: ${e.message}`); }
610
683
  } else if (!connected) {
611
- debugLog(`forward ${type}: ws not connected`);
684
+ log.debug(`forward ${type}: ws not connected`);
612
685
  }
613
686
  });
614
687
  }
615
688
 
616
689
  // ── Setup functions ────────────────────────────────────────────────
617
690
 
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
691
  /** Register all forward() calls and individual pi.on() handlers that forward events to the daemon. */
682
692
  function setupEventForwarding(): void {
683
693
  forward("agent_start");
@@ -685,8 +695,8 @@ export function registerPidash(
685
695
  forward("agent_settled");
686
696
 
687
697
  // Track streaming state for prompt-queued feedback
688
- pi.on("agent_start", () => { isStreaming = true; });
689
- pi.on("agent_settled", () => { isStreaming = false; });
698
+ pi.on("agent_start", () => { if (shuttingDown) return; isStreaming = true; });
699
+ pi.on("agent_settled", () => { if (shuttingDown) return; isStreaming = false; });
690
700
  forward("turn_start");
691
701
  forward("turn_end");
692
702
  forward("message_start");
@@ -700,6 +710,7 @@ export function registerPidash(
700
710
  forward("session_info_changed");
701
711
 
702
712
  pi.on("model_select", (event: any) => {
713
+ if (shuttingDown) return;
703
714
  if (ws && connected) {
704
715
  ws.send(JSON.stringify({
705
716
  type: "update_info",
@@ -711,6 +722,7 @@ export function registerPidash(
711
722
 
712
723
  // Sync thinking level on every turn
713
724
  pi.on("turn_end", () => {
725
+ if (shuttingDown) return;
714
726
  if (!ws || !connected) return;
715
727
  try {
716
728
  const level = (pi as any).getThinkingLevel?.();
@@ -730,6 +742,7 @@ export function registerPidash(
730
742
 
731
743
  // Forward provider response info to pidash
732
744
  pi.on("after_provider_response" as any, (event: any) => {
745
+ if (shuttingDown) return;
733
746
  if (!ws || !connected) return;
734
747
  try {
735
748
  const info: any = { type: "provider_response" };
@@ -741,7 +754,7 @@ export function registerPidash(
741
754
  if (event.headers["x-request-id"]) info.requestId = event.headers["x-request-id"];
742
755
  }
743
756
  ws.send(JSON.stringify(info));
744
- } catch (e: any) { debugLog(`provider response forward error: ${e.message}`); }
757
+ } catch (e: any) { log.debug(`provider response forward error: ${e.message}`); }
745
758
  });
746
759
  }
747
760
 
@@ -775,28 +788,15 @@ export function registerPidash(
775
788
  }
776
789
  });
777
790
 
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}`);
791
+ // Forward coms peer join/leave events to browser
792
+ pi.events.on("pidash:coms-peer-event", (data: unknown) => {
793
+ const d = data as { customType?: string; content?: string };
794
+ if (!d?.customType) return;
795
+ if (ws && connected) {
796
+ try { ws.send(JSON.stringify({ type: "coms_peer_event", customType: d.customType, content: d.content || "" })); } catch {}
787
797
  }
788
798
  });
789
799
 
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
800
  }
801
801
 
802
802
  /** Periodically send git status updates to the daemon. */
@@ -812,16 +812,18 @@ export function registerPidash(
812
812
  gitDirty: git.dirty,
813
813
  gitChanges: git.changes,
814
814
  }));
815
- } catch (e: any) { debugLog(`periodic status error: ${e?.message || e}`); }
815
+ } catch (e: any) { log.debug(`periodic status error: ${e?.message || e}`); }
816
816
  }, 10000);
817
817
  if (statusInterval.unref) statusInterval.unref();
818
818
  }
819
819
 
820
820
  /** Handle session_start event — connect or notify session switch. */
821
- function handleSessionStart(_event: any, ctx: any): void {
821
+ function handleSessionStart(event: any, ctx: any): void {
822
+ sessionNamed = false;
822
823
  execCtx = ctx;
823
- pidashCommandCtx = null;
824
- debugLog("execCtx created from session_start");
824
+ log.debug("execCtx created from session_start");
825
+
826
+ log.debug("session_start", (event as any)?.reason);
825
827
 
826
828
  // Auto-capture command context: silently run /pidash status.
827
829
  // pi.sendUserMessage won't trigger command dispatch (expandPromptTemplates: false),
@@ -842,7 +844,7 @@ export function registerPidash(
842
844
  branch: getCurrentBranch(ctx.cwd),
843
845
  sessionFile: ctx.sessionManager?.getSessionFile?.() || ctx.sessionFile || "",
844
846
  }));
845
- debugLog(`session_switch sent: cwd=${ctx.cwd}`);
847
+ log.debug(`session_switch sent: cwd=${ctx.cwd}`);
846
848
  }
847
849
  }
848
850
 
@@ -850,7 +852,7 @@ export function registerPidash(
850
852
  async function handleBrowserInput(event: any, _ctx: any): Promise<{ action: "handled" } | void> {
851
853
  if ((event as any).streamingBehavior && ws && connected) {
852
854
  try { ws.send(JSON.stringify({ type: "streaming-behavior", behavior: (event as any).streamingBehavior })); }
853
- catch (e: any) { debugLog(`streaming-behavior send error: ${e.message}`); }
855
+ catch (e: any) { log.debug(`streaming-behavior send error: ${e.message}`); }
854
856
  }
855
857
 
856
858
  // Intercept extension commands from browser
@@ -862,30 +864,40 @@ export function registerPidash(
862
864
  const cmdName = (spaceIdx > 0 ? text.slice(1, spaceIdx) : text.slice(1)).toLowerCase();
863
865
  const arg = spaceIdx > 0 ? text.slice(spaceIdx + 1).trim() : "";
864
866
 
865
- debugLog(`browser command: /${cmdName} ${arg.slice(0, 80)}`);
867
+ log.debug(`browser command: /${cmdName} ${arg.slice(0, 80)}`);
866
868
 
867
869
  const handler = commandHandlerRegistry.get(cmdName);
868
870
  if (handler) {
869
871
  try {
870
- await handler(arg, execCtx);
872
+ const rawCtx = lastCmdCtx ?? execCtx ?? lastCtx;
873
+ const ctx = rawCtx?.ui ? { ...rawCtx, ui: { ...rawCtx.ui, notify: (msg: string, level?: string) => {
874
+ if (ws && connected) { try { ws.send(JSON.stringify({ type: "notification", level: level || "info", message: String(msg) })); } catch {} }
875
+ return rawCtx.ui.notify(msg, level);
876
+ } } } : rawCtx;
877
+ await handler(arg, ctx);
871
878
  } catch (e: any) {
872
- debugLog(`command /${cmdName} error: ${e.message}`);
879
+ log.error(`command /${cmdName} error:`, e);
873
880
  }
874
881
  return { action: "handled" as const };
875
882
  }
883
+ // Unknown command from browser — send error notification
884
+ if (ws && connected) {
885
+ try { ws.send(JSON.stringify({ type: "notification", level: "warning", message: `Unknown command: /${cmdName}` })); } catch {}
886
+ }
887
+ return { action: "handled" as const };
876
888
  }
877
889
 
878
890
  // ── Wire everything up ─────────────────────────────────────────────
879
891
 
880
- setupCtxWrapping();
881
892
  setupEventForwarding();
882
893
  setupPidashEventListeners();
883
894
  setupPeriodicStatus();
884
895
 
885
- pi.on("session_start", handleSessionStart);
896
+ pi.on("session_start", (event: any, ctx: any) => { if (shuttingDown) return; handleSessionStart(event, ctx); });
886
897
 
887
898
  // Fallback for /reload — connect on first tool_result if not connected
888
899
  pi.on("tool_result", (_event, ctx) => {
900
+ if (shuttingDown) return;
889
901
  if (!connected && !shuttingDown && ctx.mode === "tui") connect(ctx);
890
902
  });
891
903
 
@@ -918,8 +930,32 @@ export function registerPidash(
918
930
  },
919
931
  handler: (args, ctx) => handlePidashCommand(args, ctx),
920
932
  });
933
+ commandHandlerRegistry.set("pidash", (args, ctx) => handlePidashCommand(args, ctx));
934
+
935
+ pi.on("input", async (event, _ctx) => { if (shuttingDown) return; return handleBrowserInput(event, _ctx); });
921
936
 
922
- pi.on("input", async (event, _ctx) => handleBrowserInput(event, _ctx));
937
+ // Auto-name session from first user prompt if no name set
938
+ let sessionNamed = false;
939
+ pi.on("agent_end" as any, () => {
940
+ if (sessionNamed || shuttingDown) return;
941
+ const currentName = (pi as any).getSessionName?.();
942
+ if (currentName) { sessionNamed = true; return; }
943
+ try {
944
+ const entries = lastCtx?.sessionManager?.getEntries?.() || [];
945
+ const firstUser = entries.find((e: any) => e.type === "message" && e.message?.role === "user");
946
+ if (firstUser) {
947
+ const content = firstUser.message.content;
948
+ const text = typeof content === "string" ? content :
949
+ Array.isArray(content) ? content.filter((b: any) => b.type === "text").map((b: any) => b.text).join(" ") : "";
950
+ if (text) {
951
+ const name = text.slice(0, 60).replace(/\n/g, " ").trim();
952
+ (pi as any).setSessionName?.(name);
953
+ sessionNamed = true;
954
+ log.debug("auto_session_name", name);
955
+ }
956
+ }
957
+ } catch (e: any) { log.debug("auto_session_name_error", e?.message); }
958
+ });
923
959
 
924
960
  pi.on("session_shutdown", (event) => {
925
961
  // Forward shutdown reason to pidash dashboard
@@ -934,6 +970,7 @@ export function registerPidash(
934
970
  }
935
971
  shuttingDown = true;
936
972
  cleanupReconnect();
973
+ if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
937
974
  if (cleanupHeartbeat) { cleanupHeartbeat(); cleanupHeartbeat = null; }
938
975
  if (ws) { try { ws.close(); } catch {} ws = null; }
939
976
  connected = false;