@hienlh/ppm 0.13.65 → 0.13.67

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 (52) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/assets/skills/ppm/SKILL.md +1 -1
  3. package/assets/skills/ppm/references/http-api.md +2 -1
  4. package/dist/web/assets/{audio-preview-Bog1sIoF.js → audio-preview-Iq-XRBGw.js} +1 -1
  5. package/dist/web/assets/{chat-tab-B-uVAh4d.js → chat-tab-DkVXRD9e.js} +3 -3
  6. package/dist/web/assets/{code-editor-cDv3opsJ.js → code-editor-M6wHw8AZ.js} +2 -2
  7. package/dist/web/assets/{conflict-editor-D5sEfbcX.js → conflict-editor-D_8t44Wi.js} +1 -1
  8. package/dist/web/assets/{database-viewer-BGBVsG5J.js → database-viewer-Cj5yCn4w.js} +1 -1
  9. package/dist/web/assets/diff-viewer-BgPv67fJ.js +4 -0
  10. package/dist/web/assets/{docx-preview-ByzSlSgn.js → docx-preview-BbmDvXdS.js} +1 -1
  11. package/dist/web/assets/extension-webview-CP_AtfYs.js +3 -0
  12. package/dist/web/assets/{git-log-panel-C1T8bav0.js → git-log-panel-DPRoZgWG.js} +1 -1
  13. package/dist/web/assets/{glide-data-grid-DV8ht1BP.js → glide-data-grid-BrtUKC3w.js} +1 -1
  14. package/dist/web/assets/{image-preview-Dbo7SAVb.js → image-preview-BFj-ipom.js} +1 -1
  15. package/dist/web/assets/{index-DU_JZ5MY.js → index-CJZZ6v1o.js} +3 -3
  16. package/dist/web/assets/keybindings-store-BOV4khyp.js +1 -0
  17. package/dist/web/assets/{markdown-renderer-D-QbsfIC.js → markdown-renderer-B63eYfrn.js} +1 -1
  18. package/dist/web/assets/notification-store-BklO85um.js +1 -0
  19. package/dist/web/assets/{pdf-preview-DV96VPTb.js → pdf-preview-JOwOGTIk.js} +1 -1
  20. package/dist/web/assets/{port-forwarding-tab-C4OYC71C.js → port-forwarding-tab-DJRRbLGF.js} +1 -1
  21. package/dist/web/assets/{postgres-viewer-hb-_twEU.js → postgres-viewer-AIOBOfCg.js} +1 -1
  22. package/dist/web/assets/{settings-tab-BUCIqVAl.js → settings-tab-BMHf9pO5.js} +1 -1
  23. package/dist/web/assets/{sql-query-editor-C7YgtDR3.js → sql-query-editor-Dw9UvzWt.js} +1 -1
  24. package/dist/web/assets/{sqlite-viewer-z3pGFSje.js → sqlite-viewer-HusTxs1Z.js} +1 -1
  25. package/dist/web/assets/system-monitor-tab-BNJIkOan.js +1 -0
  26. package/dist/web/assets/{terminal-tab-DbxLHofN.js → terminal-tab-W1VShnP7.js} +1 -1
  27. package/dist/web/assets/{video-preview-DylSBAzo.js → video-preview-BPAYbuvs.js} +1 -1
  28. package/dist/web/index.html +1 -1
  29. package/dist/web/sw.js +1 -1
  30. package/package.json +1 -1
  31. package/src/index.ts +0 -0
  32. package/src/server/middleware/auth.ts +1 -1
  33. package/src/server/routes/fs-browse.ts +18 -4
  34. package/src/server/routes/git.ts +2 -1
  35. package/src/services/download-token.service.ts +1 -2
  36. package/src/services/git.service.ts +17 -7
  37. package/src/services/resource-monitor-utils.ts +18 -6
  38. package/src/services/resource-monitor.service.ts +3 -2
  39. package/src/web/components/editor/diff-viewer.tsx +1 -0
  40. package/src/web/components/extensions/extension-webview.tsx +2 -2
  41. package/src/web/components/system/system-monitor-group-row.tsx +27 -7
  42. package/src/web/components/system/system-monitor-tab.tsx +1 -1
  43. package/src/web/hooks/use-extension-ws.ts +16 -3
  44. package/src/web/hooks/use-resource-monitor.ts +1 -1
  45. package/src/web/lib/file-download.ts +8 -0
  46. package/bun.lock +0 -2170
  47. package/bunfig.toml +0 -2
  48. package/dist/web/assets/diff-viewer-B-O1mvHO.js +0 -4
  49. package/dist/web/assets/extension-webview-0qfU1r7z.js +0 -3
  50. package/dist/web/assets/keybindings-store-0FUOwc9I.js +0 -1
  51. package/dist/web/assets/notification-store-bwd1UKbs.js +0 -1
  52. package/dist/web/assets/system-monitor-tab-Bj6pcRmV.js +0 -1
@@ -16,7 +16,7 @@ export function createDownloadToken(): string {
16
16
  return token;
17
17
  }
18
18
 
19
- /** Validate and consume a download token (one-time use) */
19
+ /** Validate a download token (TTL-based, allows multiple uses within window) */
20
20
  export function consumeDownloadToken(token: string): boolean {
21
21
  const entry = tokens.get(token);
22
22
  if (!entry) return false;
@@ -24,7 +24,6 @@ export function consumeDownloadToken(token: string): boolean {
24
24
  tokens.delete(token);
25
25
  return false;
26
26
  }
27
- tokens.delete(token);
28
27
  return true;
29
28
  }
30
29
 
@@ -132,9 +132,9 @@ class GitService {
132
132
  projectPath: string,
133
133
  filePath: string,
134
134
  ref: string = "HEAD",
135
+ ref2?: string,
135
136
  ): Promise<{ original: string; modified: string }> {
136
137
  const git = this.git(projectPath);
137
- const absPath = path.resolve(projectPath, filePath);
138
138
 
139
139
  let original = "";
140
140
  try {
@@ -145,12 +145,22 @@ class GitService {
145
145
  }
146
146
 
147
147
  let modified = "";
148
- try {
149
- const f = Bun.file(absPath);
150
- if (await f.exists()) modified = await f.text();
151
- } catch {
152
- // File missing on disk (deleted) → empty modified
153
- modified = "";
148
+ if (ref2) {
149
+ // Commit-to-commit diff: read modified from git object store
150
+ try {
151
+ modified = await git.show([`${ref2}:${filePath}`]);
152
+ } catch {
153
+ modified = "";
154
+ }
155
+ } else {
156
+ // Working tree diff: read from disk
157
+ try {
158
+ const absPath = path.resolve(projectPath, filePath);
159
+ const f = Bun.file(absPath);
160
+ if (await f.exists()) modified = await f.text();
161
+ } catch {
162
+ modified = "";
163
+ }
154
164
  }
155
165
 
156
166
  return { original, modified };
@@ -14,6 +14,8 @@ const CATEGORY_PATTERNS: [ResourceGroup["type"], RegExp][] = [
14
14
  ];
15
15
 
16
16
  function categorize(cmd: string): ResourceGroup["type"] {
17
+ // Check full command for well-known AI tool paths before extracting basename
18
+ if (/claude-agent-sdk|@anthropic-ai\/claude/.test(cmd)) return "ai-tool";
17
19
  const basename = cmd.split("/").pop()?.split(" ")[0] ?? cmd;
18
20
  for (const [type, re] of CATEGORY_PATTERNS) {
19
21
  if (re.test(basename)) return type;
@@ -23,23 +25,25 @@ function categorize(cmd: string): ResourceGroup["type"] {
23
25
 
24
26
  // ── Parser ─────────────────────────────────────────────────────────────
25
27
 
26
- /** Parse `ps -e -o pid,ppid,%cpu,rss,args` output into structured entries */
28
+ /** Parse `ps -e -o pid,ppid,%cpu,rss,etimes,args` output into structured entries */
27
29
  export function parseProcessList(stdout: string): ProcessEntry[] {
28
30
  const lines = stdout.trim().split("\n");
29
31
  if (lines.length < 2) return [];
30
32
 
33
+ const now = Date.now();
31
34
  const entries: ProcessEntry[] = [];
32
35
  for (let i = 1; i < lines.length; i++) {
33
36
  const line = lines[i]?.trim();
34
37
  if (!line) continue;
35
38
  const parts = line.split(/\s+/);
36
- if (parts.length < 5) continue;
39
+ if (parts.length < 6) continue;
37
40
 
38
41
  const pid = parseInt(parts[0]!, 10);
39
42
  const ppid = parseInt(parts[1]!, 10);
40
43
  const cpu = parseFloat(parts[2]!);
41
44
  const rssKB = parseInt(parts[3]!, 10);
42
- const command = parts.slice(4).join(" ");
45
+ const etimes = parseInt(parts[4]!, 10);
46
+ const command = parts.slice(5).join(" ");
43
47
 
44
48
  if (isNaN(pid) || pid === 0 || !command) continue;
45
49
 
@@ -48,6 +52,7 @@ export function parseProcessList(stdout: string): ProcessEntry[] {
48
52
  ppid,
49
53
  cpu: Math.round(cpu * 10) / 10,
50
54
  ramMB: Math.round((rssKB / 1024) * 10) / 10,
55
+ startedAt: isNaN(etimes) ? now : now - etimes * 1000,
51
56
  command,
52
57
  });
53
58
  }
@@ -92,6 +97,7 @@ const TYPE_LABELS: Record<ResourceGroup["type"], string> = {
92
97
  export function groupProcesses(
93
98
  serverEntry: ProcessEntry | undefined,
94
99
  children: ProcessEntry[],
100
+ allEntries: ProcessEntry[] = [],
95
101
  ): ResourceGroup[] {
96
102
  const groups: ResourceGroup[] = [];
97
103
 
@@ -101,12 +107,18 @@ export function groupProcesses(
101
107
  label: "PPM Server",
102
108
  cpu: serverEntry.cpu,
103
109
  ramMB: serverEntry.ramMB,
104
- processes: [{ pid: serverEntry.pid, cpu: serverEntry.cpu, ramMB: serverEntry.ramMB, command: serverEntry.command }],
110
+ processes: [{ pid: serverEntry.pid, cpu: serverEntry.cpu, ramMB: serverEntry.ramMB, startedAt: serverEntry.startedAt, command: serverEntry.command }],
105
111
  });
106
112
  }
107
113
 
114
+ // Include external AI tool processes not in PPM's process tree (e.g. Claude Code sessions)
115
+ const ppmPids = new Set([serverEntry?.pid, ...children.map((c) => c.pid)].filter(Boolean));
116
+ const externalAiProcs = allEntries.filter(
117
+ (e) => !ppmPids.has(e.pid) && categorize(e.command) === "ai-tool",
118
+ );
119
+
108
120
  const buckets = new Map<ResourceGroup["type"], ProcessEntry[]>();
109
- for (const child of children) {
121
+ for (const child of [...children, ...externalAiProcs]) {
110
122
  const type = categorize(child.command);
111
123
  const list = buckets.get(type) ?? [];
112
124
  list.push(child);
@@ -121,7 +133,7 @@ export function groupProcesses(
121
133
  label: TYPE_LABELS[type],
122
134
  cpu: Math.round(procs.reduce((s, p) => s + p.cpu, 0) * 10) / 10,
123
135
  ramMB: Math.round(procs.reduce((s, p) => s + p.ramMB, 0) * 10) / 10,
124
- processes: procs.map((p) => ({ pid: p.pid, cpu: p.cpu, ramMB: p.ramMB, command: p.command })),
136
+ processes: procs.map((p) => ({ pid: p.pid, cpu: p.cpu, ramMB: p.ramMB, startedAt: p.startedAt, command: p.command })),
125
137
  });
126
138
  }
127
139
 
@@ -13,6 +13,7 @@ export interface ProcessEntry {
13
13
  ppid: number;
14
14
  cpu: number;
15
15
  ramMB: number;
16
+ startedAt: number;
16
17
  command: string;
17
18
  }
18
19
 
@@ -78,7 +79,7 @@ class ResourceMonitorService {
78
79
  private async poll() {
79
80
  try {
80
81
  const proc = Bun.spawn({
81
- cmd: ["ps", "-e", "-o", "pid,ppid,%cpu,rss,args"],
82
+ cmd: ["ps", "-e", "-o", "pid,ppid,%cpu,rss,etimes,args"],
82
83
  stdout: "pipe",
83
84
  stderr: "ignore",
84
85
  });
@@ -89,7 +90,7 @@ class ResourceMonitorService {
89
90
  const rootPid = process.pid;
90
91
  const serverEntry = entries.find((e) => e.pid === rootPid);
91
92
  const children = buildTree(entries, rootPid);
92
- const groups = groupProcesses(serverEntry, children);
93
+ const groups = groupProcesses(serverEntry, children, entries);
93
94
 
94
95
  const allProcs = serverEntry ? [serverEntry, ...children] : children;
95
96
  const snapshot: ResourceSnapshot = {
@@ -86,6 +86,7 @@ export function DiffViewer({ metadata }: DiffViewerProps) {
86
86
  if (filePath) {
87
87
  const params = new URLSearchParams({ file: filePath });
88
88
  if (ref1) params.set("ref", ref1);
89
+ if (ref2) params.set("ref2", ref2);
89
90
  api
90
91
  .get<{ original: string; modified: string }>(
91
92
  `${projectUrl(projectName)}/git/file-full-diff?${params}`,
@@ -83,7 +83,7 @@ export function ExtensionWebview({ metadata }: ExtensionWebviewProps) {
83
83
  }
84
84
  if (cancelled) return;
85
85
  window.dispatchEvent(new CustomEvent("ext:command:execute", {
86
- detail: { command, args },
86
+ detail: { command, args, recovery: true },
87
87
  }));
88
88
  }
89
89
 
@@ -117,7 +117,7 @@ export function ExtensionWebview({ metadata }: ExtensionWebviewProps) {
117
117
  const match = json.data?.find((p) => p.name === projectName);
118
118
  const args = match ? [match.path] : [];
119
119
  window.dispatchEvent(new CustomEvent("ext:command:execute", {
120
- detail: { command, args },
120
+ detail: { command, args, recovery: true },
121
121
  }));
122
122
  } catch {}
123
123
  })();
@@ -17,6 +17,18 @@ function formatRam(mb: number) {
17
17
  return mb < 1024 ? `${mb.toFixed(0)} MB` : `${(mb / 1024).toFixed(1)} GB`;
18
18
  }
19
19
 
20
+ function formatAge(startedAt?: number) {
21
+ if (!startedAt) return "";
22
+ const secs = Math.round((Date.now() - startedAt) / 1000);
23
+ if (secs < 60) return `${secs}s`;
24
+ const mins = Math.floor(secs / 60);
25
+ if (mins < 60) return `${mins}m`;
26
+ const hrs = Math.floor(mins / 60);
27
+ if (hrs < 24) return `${hrs}h ${mins % 60}m`;
28
+ const days = Math.floor(hrs / 24);
29
+ return `${days}d ${hrs % 24}h`;
30
+ }
31
+
20
32
  export interface GroupRowProps {
21
33
  group: ResourceGroup;
22
34
  Icon: React.ElementType;
@@ -85,13 +97,21 @@ export const GroupRow = memo(function GroupRow({
85
97
  <td className="text-right py-1 px-2 align-top">{formatRam(proc.ramMB)}</td>
86
98
  {!isMobile && (
87
99
  <td className="align-top py-1 px-2">
88
- <button
89
- onClick={(e) => { e.stopPropagation(); onKill(proc.pid); }}
90
- className="opacity-0 group-hover/proc:opacity-100 p-0.5 rounded hover:bg-red-500/20 hover:text-red-500 transition-all"
91
- title={`End process ${proc.pid}`}
92
- >
93
- <X className="size-3" />
94
- </button>
100
+ <div className="flex items-center justify-between gap-1">
101
+ <span
102
+ className="text-text-subtle"
103
+ title={proc.startedAt ? new Date(proc.startedAt).toLocaleString() : ""}
104
+ >
105
+ {formatAge(proc.startedAt)}
106
+ </span>
107
+ <button
108
+ onClick={(e) => { e.stopPropagation(); onKill(proc.pid); }}
109
+ className="opacity-0 group-hover/proc:opacity-100 p-0.5 rounded hover:bg-red-500/20 hover:text-red-500 transition-all"
110
+ title={`End process ${proc.pid}`}
111
+ >
112
+ <X className="size-3" />
113
+ </button>
114
+ </div>
95
115
  </td>
96
116
  )}
97
117
  </tr>
@@ -137,7 +137,7 @@ export const SystemMonitorTab = memo(function SystemMonitorTab() {
137
137
  className="w-20"
138
138
  />
139
139
  {!isMobile && (
140
- <th className="py-1.5 px-2 font-medium w-[130px]">Trend</th>
140
+ <th className="py-1.5 px-2 font-medium w-[130px]">Trend / Age</th>
141
141
  )}
142
142
  </tr>
143
143
  </thead>
@@ -16,6 +16,13 @@ import { toast } from "sonner";
16
16
  */
17
17
  const recentlyClosedViews = new Set<string>();
18
18
 
19
+ /**
20
+ * Track viewTypes whose command dispatch was auto-recovery (not user-initiated).
21
+ * When `webview:create` arrives for a recovery viewType, skip setActiveTab
22
+ * to prevent stealing focus from the user's current tab.
23
+ */
24
+ const recoveryViews = new Set<string>();
25
+
19
26
  /**
20
27
  * Hook that manages the WebSocket connection for extension UI bridge.
21
28
  * Dispatches server messages into the extension Zustand store.
@@ -161,8 +168,11 @@ export function useExtensionWs(enabled = true) {
161
168
  title: msg.title,
162
169
  metadata: { ...existingTab, viewType: viewTypeSlug, panelId: msg.panelId, extensionId: msg.extensionId },
163
170
  });
164
- // Focus the existing tab so Cmd+G / command palette switches to it
165
- useTabStore.getState().setActiveTab(existingTabId);
171
+ // Focus the existing tab only if user explicitly opened it (not auto-recovery)
172
+ if (!recoveryViews.has(viewTypeSlug)) {
173
+ useTabStore.getState().setActiveTab(existingTabId);
174
+ }
175
+ recoveryViews.delete(viewTypeSlug);
166
176
  } else if (!recentlyClosedViews.has(viewTypeSlug)) {
167
177
  // Only create a new tab if this viewType wasn't recently closed by user
168
178
  const currentProject = useTabStore.getState().currentProject;
@@ -223,10 +233,13 @@ export function useExtensionWs(enabled = true) {
223
233
 
224
234
  // Listen for command:execute requests (dispatched by StatusBar / TreeView)
225
235
  const commandHandler = (e: Event) => {
226
- const { command, args } = (e as CustomEvent).detail;
236
+ const { command, args, recovery } = (e as CustomEvent).detail;
227
237
  // User explicitly opened an extension — clear "recently closed" so tab can be created
228
238
  const slug = (command as string).replace(/\.view$/, "");
229
239
  recentlyClosedViews.delete(slug);
240
+ // Track recovery dispatches to avoid stealing focus on webview:create
241
+ if (recovery) recoveryViews.add(slug);
242
+ else recoveryViews.delete(slug);
230
243
  client.send(JSON.stringify({ type: "command:execute", command, args }));
231
244
  };
232
245
  window.addEventListener("ext:command:execute", commandHandler);
@@ -8,7 +8,7 @@ export interface ResourceGroup {
8
8
  label: string;
9
9
  cpu: number;
10
10
  ramMB: number;
11
- processes: { pid: number; cpu: number; ramMB: number; command: string }[];
11
+ processes: { pid: number; cpu: number; ramMB: number; startedAt?: number; command: string }[];
12
12
  }
13
13
 
14
14
  export interface ResourceSnapshot {
@@ -2,6 +2,14 @@ import { api, projectUrl } from "./api-client";
2
2
 
3
3
  /** Trigger browser-native file download via hidden <a> tag */
4
4
  export async function downloadFile(projectName: string, filePath: string): Promise<void> {
5
+ // Absolute paths (external files opened from filesystem browser) use /api/fs routes
6
+ const isAbsolute = /^(\/|[A-Za-z]:[/\\])/.test(filePath);
7
+ if (isAbsolute) {
8
+ const { token } = await api.post<{ token: string }>("/api/fs/download/token");
9
+ const url = `/api/fs/raw?path=${encodeURIComponent(filePath)}&download=true&dl_token=${encodeURIComponent(token)}`;
10
+ triggerDownload(url, filePath.split("/").pop() ?? "download");
11
+ return;
12
+ }
5
13
  const { token } = await api.post<{ token: string }>(`${projectUrl(projectName)}/files/download/token`);
6
14
  const url = `${projectUrl(projectName)}/files/raw?path=${encodeURIComponent(filePath)}&download=true&dl_token=${encodeURIComponent(token)}`;
7
15
  triggerDownload(url, filePath.split("/").pop() ?? "download");