@hienlh/ppm 0.13.97 → 0.13.99

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 (38) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/assets/skills/ppm/SKILL.md +1 -1
  3. package/assets/skills/ppm/references/http-api.md +1 -1
  4. package/dist/web/assets/{audio-preview-C5XtLYr0.js → audio-preview-nh91uUVB.js} +1 -1
  5. package/dist/web/assets/{chat-tab-Crh2a5WT.js → chat-tab-DLDMRI1M.js} +3 -3
  6. package/dist/web/assets/{code-editor-D0n5yRzn.js → code-editor-xUzwLxcl.js} +2 -2
  7. package/dist/web/assets/{conflict-editor-C7tPFwQu.js → conflict-editor-DKOVCVyg.js} +1 -1
  8. package/dist/web/assets/{database-viewer-CENJQA63.js → database-viewer-QcgNoqBG.js} +1 -1
  9. package/dist/web/assets/{diff-viewer-DYskJYPt.js → diff-viewer-BZAwHro2.js} +1 -1
  10. package/dist/web/assets/{docx-preview-Cs1Vck_b.js → docx-preview-DdSlF2Px.js} +1 -1
  11. package/dist/web/assets/{extension-webview-DDNsAryv.js → extension-webview-CwyNu9aJ.js} +1 -1
  12. package/dist/web/assets/{git-log-panel-Bw50iGkP.js → git-log-panel-K47HJ4xO.js} +1 -1
  13. package/dist/web/assets/{glide-data-grid-D-kV0skS.js → glide-data-grid-DKe_vpg6.js} +1 -1
  14. package/dist/web/assets/{image-preview-ICmsfJXP.js → image-preview-DJH0O6Ke.js} +1 -1
  15. package/dist/web/assets/{index-lVDR594A.js → index-DujVI6Rg.js} +4 -4
  16. package/dist/web/assets/keybindings-store-Cu14oYyO.js +1 -0
  17. package/dist/web/assets/{markdown-renderer-VOyp6B1p.js → markdown-renderer-C1UYL3yn.js} +1 -1
  18. package/dist/web/assets/notification-store-Conv2TgB.js +1 -0
  19. package/dist/web/assets/{panel-store-BETELS-N.js → panel-store-DrDP7lP4.js} +1 -1
  20. package/dist/web/assets/{pdf-preview-DTlEFagS.js → pdf-preview-Bd4wJVlK.js} +1 -1
  21. package/dist/web/assets/{port-forwarding-tab-Coe4rUGI.js → port-forwarding-tab-2wR1sQE6.js} +1 -1
  22. package/dist/web/assets/{postgres-viewer-C9G-BZE8.js → postgres-viewer-Fb-C4lu5.js} +1 -1
  23. package/dist/web/assets/{settings-tab-BTEIHM07.js → settings-tab-DcFSxJEG.js} +1 -1
  24. package/dist/web/assets/{sql-query-editor-COQcgsYM.js → sql-query-editor-D39hN-d-.js} +1 -1
  25. package/dist/web/assets/{sqlite-viewer-D0pkAQQa.js → sqlite-viewer-u9iCbfHJ.js} +1 -1
  26. package/dist/web/assets/{system-monitor-tab-VgYnDn6v.js → system-monitor-tab-DpndNYnK.js} +1 -1
  27. package/dist/web/assets/{tab-store-B_Kf29se.js → tab-store-Cr5X8BFJ.js} +1 -1
  28. package/dist/web/assets/{terminal-tab-D08UOpkI.js → terminal-tab-BIqqQjrG.js} +1 -1
  29. package/dist/web/assets/{video-preview-D5ufy0_E.js → video-preview-DdivKT9h.js} +1 -1
  30. package/dist/web/index.html +3 -3
  31. package/dist/web/sw.js +1 -1
  32. package/package.json +1 -1
  33. package/src/server/index.ts +19 -50
  34. package/src/server/ws/chat.ts +11 -0
  35. package/src/services/supervisor.ts +45 -28
  36. package/src/web/stores/panel-store.ts +5 -1
  37. package/dist/web/assets/keybindings-store-zLledTJ_.js +0 -1
  38. package/dist/web/assets/notification-store-XwGVhPdW.js +0 -1
@@ -633,50 +633,35 @@ if (process.argv.includes("__serve__")) {
633
633
  setInterval(() => cleanupOldProxyRequests(30), 24 * 60 * 60 * 1000);
634
634
  }
635
635
 
636
- // On Windows, check for zombie sockets before binding.
637
- // After an upgrade, the old server's socket can stay in LISTENING state
638
- // because SIGTERM maps to TerminateProcess (graceful handler never fires).
639
- let actualPort = port;
636
+ // On Windows the supervisor reaps the previous server's whole process tree
637
+ // before respawning, so the port is released cleanly. A lingering bind can
638
+ // still appear for a moment during an upgrade handoff, so wait for it to
639
+ // free instead of moving to a different port — shifting would split-brain
640
+ // the tunnel and supervisor, which still target the original port (this was
641
+ // the recurring "dies after upgrade" failure on Windows).
640
642
  if (process.platform === "win32") {
641
- const portInUse = await new Promise<boolean>((resolve) => {
643
+ const isPortInUse = () => new Promise<boolean>((resolve) => {
642
644
  const net = require("node:net") as typeof import("node:net");
643
645
  const tester = net.createServer()
644
646
  .once("error", (e: NodeJS.ErrnoException) => resolve(e.code === "EADDRINUSE"))
645
647
  .once("listening", () => tester.close(() => resolve(false)))
646
648
  .listen(port, host);
647
649
  });
648
- if (portInUse) {
649
- try {
650
- const { execSync } = require("node:child_process") as typeof import("node:child_process");
651
- const out = execSync(`netstat -ano | findstr "0.0.0.0:${port}.*LISTENING"`, { encoding: "utf-8", timeout: 5000 });
652
- const match = out.trim().match(/LISTENING\s+(\d+)/);
653
- if (match?.[1]) {
654
- const ownerPid = parseInt(match[1], 10);
655
- let isZombie = false;
656
- try { process.kill(ownerPid, 0); } catch { isZombie = true; }
657
- if (isZombie) {
658
- console.warn(`[serve] Port ${port} held by dead process (PID: ${ownerPid}) — zombie socket`);
659
- for (let candidate = port + 1; candidate <= port + 20; candidate++) {
660
- const busy = await new Promise<boolean>((resolve) => {
661
- const net = require("node:net") as typeof import("node:net");
662
- const tester = net.createServer()
663
- .once("error", (e: NodeJS.ErrnoException) => resolve(e.code === "EADDRINUSE"))
664
- .once("listening", () => tester.close(() => resolve(false)))
665
- .listen(candidate, host);
666
- });
667
- if (!busy) { actualPort = candidate; break; }
668
- }
669
- if (actualPort !== port) {
670
- console.warn(`[serve] Auto-selected port ${actualPort} instead`);
671
- }
672
- }
673
- }
674
- } catch {}
650
+ const deadline = Date.now() + 10_000;
651
+ while (await isPortInUse()) {
652
+ if (Date.now() > deadline) {
653
+ console.error(`\n ✗ Port ${port} is still in use after waiting 10s.`);
654
+ console.error(` A stale process may be holding it. Run 'ppm stop', then start again.`);
655
+ console.error(` If it persists, run PowerShell as Admin: netsh int tcp reset (then restart).\n`);
656
+ process.exit(1); // supervisor will back off and respawn on the same port
657
+ }
658
+ console.warn(`[serve] Port ${port} still releasing, waiting...`);
659
+ await Bun.sleep(250);
675
660
  }
676
661
  }
677
662
 
678
663
  const server = Bun.serve({
679
- port: actualPort,
664
+ port,
680
665
  hostname: host,
681
666
  fetch(req, server) {
682
667
  const url = new URL(req.url);
@@ -780,22 +765,6 @@ if (process.argv.includes("__serve__")) {
780
765
  })
781
766
  .catch(() => {});
782
767
 
783
- // If we auto-selected a different port, update status.json so supervisor
784
- // health checks and tunnel proxy point at the correct port.
785
- if (actualPort !== port) {
786
- try {
787
- const { resolve: r } = await import("node:path");
788
- const { readFileSync: rf, writeFileSync: wf, renameSync: rn } = await import("node:fs");
789
- const { getPpmDir: gd } = await import("../services/ppm-dir.ts");
790
- const sf = r(gd(), "status.json");
791
- const st = JSON.parse(rf(sf, "utf-8"));
792
- st.port = actualPort;
793
- const tmp = sf + ".tmp." + process.pid;
794
- wf(tmp, JSON.stringify(st));
795
- rn(tmp, sf);
796
- } catch {}
797
- }
798
-
799
768
  // Graceful shutdown: close the listening socket so the port is released
800
769
  const gracefulShutdown = () => {
801
770
  try { server.stop(true); } catch {}
@@ -819,5 +788,5 @@ if (process.argv.includes("__serve__")) {
819
788
  }, 200);
820
789
  }
821
790
 
822
- console.log(`Server child ready on port ${actualPort}`);
791
+ console.log(`Server child ready on port ${port}`);
823
792
  }
@@ -832,6 +832,17 @@ export const chatWebSocket = {
832
832
  console.log(`[chat] session=${sessionId} FE disconnected (phase=${entry.phase}, clients=${entry.clients.size})`);
833
833
 
834
834
  if (entry.clients.size === 0) {
835
+ // No clients listening anymore. If Claude is idle (turn finished, no pending
836
+ // approval), tear down the persistent streaming query so the next message
837
+ // recreates it via the resume path — picking up fresh MCP/config while
838
+ // preserving context from JSONL, and freeing the idle subprocess (RAM).
839
+ const provider = providerRegistry.get(entry.providerId);
840
+ const idle = entry.phase === "idle" && !entry.isStreamingActive && !entry.pendingApprovalEvent;
841
+ const hasLiveStream = provider?.hasStreamingSession?.(sessionId) ?? false;
842
+ if (idle && hasLiveStream) {
843
+ provider?.abortQuery?.(sessionId, "tab_closed");
844
+ logSessionEvent(sessionId, "INFO", "Streaming query torn down (all clients gone, idle) — next message resumes with fresh config");
845
+ }
835
846
  startCleanupTimer(sessionId);
836
847
  }
837
848
  },
@@ -86,31 +86,45 @@ function backoffDelay(restartCount: number): number {
86
86
  return Math.min(BACKOFF_BASE_MS * 2 ** (restartCount - 1), BACKOFF_MAX_MS);
87
87
  }
88
88
 
89
- // ─── Graceful server shutdown (Windows-safe) ──────────────────────────
90
- // On Windows, SIGTERM maps to TerminateProcess graceful handlers never fire.
91
- // Instead, write a shutdown file that the server child polls for.
89
+ // ─── Process-tree kill (orphan-safe) ───────────────────────────────────
90
+ // The server child spawns grandchildren (Claude SDK subprocesses). On
91
+ // Windows a forced single-PID kill leaves those grandchildren alive; they
92
+ // keep the inherited TCP listening socket handle open, leaving the port in
93
+ // a zombie LISTENING state owned by a dead PID. Killing the whole tree
94
+ // releases the socket — the Windows analog of POSIX process-group kill.
95
+ function killProcessTree(pid: number): void {
96
+ if (process.platform === "win32") {
97
+ try {
98
+ const { execFileSync } = require("node:child_process") as typeof import("node:child_process");
99
+ execFileSync("taskkill", ["/PID", String(pid), "/T", "/F"], { stdio: "ignore", timeout: 5000 });
100
+ } catch {
101
+ // Already dead, or taskkill unavailable — fall back to single-PID kill.
102
+ try { process.kill(pid, "SIGKILL"); } catch {}
103
+ }
104
+ } else {
105
+ try { process.kill(-pid, "SIGKILL"); } catch { try { process.kill(pid, "SIGKILL"); } catch {} }
106
+ }
107
+ }
108
+
109
+ // ─── Server shutdown ───────────────────────────────────────────────────
110
+ // On Windows the server's Claude SDK grandchildren are node-spawned (the
111
+ // provider forces `executable: "node"`), so they live OUTSIDE Bun's job
112
+ // object. If the server child exits gracefully on its own, those
113
+ // grandchildren orphan and keep the inherited listening socket open →
114
+ // zombie port. They can only be reaped by tree-killing WHILE the parent is
115
+ // still alive, so on Windows we tree-kill immediately rather than waiting
116
+ // for a graceful self-exit (which would let the orphans escape). This
117
+ // mirrors the POSIX process-group kill used on macOS/Linux.
92
118
  function requestServerShutdown(child: Subprocess, timeoutMs: number = 2000): Promise<void> {
93
119
  return new Promise<void>((resolve) => {
94
120
  const pid = child.pid;
95
121
  if (process.platform === "win32") {
96
- try { writeFileSync(serverShutdownFile(), String(Date.now())); } catch {}
97
- const deadline = Date.now() + timeoutMs;
98
- const poll = setInterval(() => {
99
- try { process.kill(pid, 0); } catch {
100
- clearInterval(poll);
101
- resolve();
102
- return;
103
- }
104
- if (Date.now() > deadline) {
105
- clearInterval(poll);
106
- try { process.kill(pid, "SIGKILL"); } catch {}
107
- resolve();
108
- }
109
- }, 100);
122
+ killProcessTree(pid);
123
+ resolve();
110
124
  } else {
111
125
  try { child.kill("SIGTERM"); } catch {}
112
126
  setTimeout(() => {
113
- try { process.kill(pid, "SIGKILL"); } catch {}
127
+ killProcessTree(pid);
114
128
  resolve();
115
129
  }, timeoutMs).unref();
116
130
  resolve();
@@ -351,10 +365,11 @@ function startServerHealthCheck(port: number) {
351
365
  log("WARN", `Server unresponsive (${healthFailCount} failures), killing`);
352
366
  const pid = serverChild.pid;
353
367
  if (process.platform === "win32") {
354
- try { writeFileSync(serverShutdownFile(), String(Date.now())); } catch {}
368
+ killProcessTree(pid);
369
+ } else {
370
+ try { serverChild.kill("SIGTERM"); } catch {}
371
+ setTimeout(() => { killProcessTree(pid); }, 1000).unref();
355
372
  }
356
- try { serverChild.kill("SIGTERM"); } catch {}
357
- setTimeout(() => { try { process.kill(pid, "SIGKILL"); } catch {} }, 1000).unref();
358
373
  healthFailCount = 0;
359
374
  // spawnServer loop handles respawn via exited promise
360
375
  }
@@ -519,9 +534,10 @@ async function selfReplace(): Promise<{ success: boolean; error?: string }> {
519
534
  }
520
535
 
521
536
  // ── Non-systemd path: spawn new supervisor directly (macOS/Windows) ─
522
- // Poll until port is actually free (max 10s) — never guess with fixed sleep
523
- // On Windows, reusePort lets the new server bind over zombie sockets,
524
- // so we only need a brief wait for the killed process to release.
537
+ // Poll until port is actually free (max 10s) — never guess with fixed sleep.
538
+ // The tree-kill above already reaped the server's grandchildren, so the
539
+ // listening socket is released; this loop just waits for the OS to finish
540
+ // tearing it down before the new supervisor binds.
525
541
  const portFreeStart = Date.now();
526
542
  const portTimeout = process.platform === "win32" ? 3_000 : 10_000;
527
543
  while (Date.now() - portFreeStart < portTimeout) {
@@ -860,12 +876,13 @@ export function shutdown() {
860
876
 
861
877
  if (serverChild) {
862
878
  log("INFO", `Killing server child (PID: ${serverChild.pid})`);
879
+ const pid = serverChild.pid;
863
880
  if (process.platform === "win32") {
864
- try { writeFileSync(serverShutdownFile(), String(Date.now())); } catch {}
881
+ killProcessTree(pid);
882
+ } else {
883
+ try { serverChild.kill("SIGTERM"); } catch {}
884
+ setTimeout(() => { killProcessTree(pid); }, 2000).unref();
865
885
  }
866
- const pid = serverChild.pid;
867
- try { serverChild.kill("SIGTERM"); } catch {}
868
- setTimeout(() => { try { process.kill(pid, "SIGKILL"); } catch {} }, 2000).unref();
869
886
  }
870
887
  if (tunnelChild) {
871
888
  log("INFO", `Killing tunnel child (PID: ${tunnelChild.pid})`);
@@ -345,7 +345,11 @@ export const usePanelStore = create<PanelStore>()((set, get) => {
345
345
  if (newTabs.length === 0 && gridPanelCount > 1) {
346
346
  const { [pid]: _, ...rest } = s.panels;
347
347
  const newGrid = gridRemovePanel(s.grid, pid);
348
- const newFocused = s.focusedPanelId === pid ? Object.keys(rest)[0]! : s.focusedPanelId;
348
+ // Focus must land on a panel still in the grid — Object.keys(rest)
349
+ // can return keep-alive panels from other projects (off-grid).
350
+ const newFocused = s.focusedPanelId === pid
351
+ ? (newGrid.flat()[0] ?? Object.keys(rest)[0]!)
352
+ : s.focusedPanelId;
349
353
  return { panels: rest, grid: newGrid, focusedPanelId: newFocused };
350
354
  }
351
355
 
@@ -1 +0,0 @@
1
- import"./vendor-markdown-0Mxgxy0L.js";import"./api-client-bbJLzRVE.js";import{L as e}from"./index-lVDR594A.js";export{e as useKeybindingsStore};
@@ -1 +0,0 @@
1
- import"./vendor-markdown-0Mxgxy0L.js";import"./api-client-bbJLzRVE.js";import{W as e}from"./index-lVDR594A.js";export{e as useNotificationStore};