@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.
- package/CHANGELOG.md +14 -0
- package/assets/skills/ppm/SKILL.md +1 -1
- package/assets/skills/ppm/references/http-api.md +2 -1
- package/dist/web/assets/{audio-preview-Bog1sIoF.js → audio-preview-Iq-XRBGw.js} +1 -1
- package/dist/web/assets/{chat-tab-B-uVAh4d.js → chat-tab-DkVXRD9e.js} +3 -3
- package/dist/web/assets/{code-editor-cDv3opsJ.js → code-editor-M6wHw8AZ.js} +2 -2
- package/dist/web/assets/{conflict-editor-D5sEfbcX.js → conflict-editor-D_8t44Wi.js} +1 -1
- package/dist/web/assets/{database-viewer-BGBVsG5J.js → database-viewer-Cj5yCn4w.js} +1 -1
- package/dist/web/assets/diff-viewer-BgPv67fJ.js +4 -0
- package/dist/web/assets/{docx-preview-ByzSlSgn.js → docx-preview-BbmDvXdS.js} +1 -1
- package/dist/web/assets/extension-webview-CP_AtfYs.js +3 -0
- package/dist/web/assets/{git-log-panel-C1T8bav0.js → git-log-panel-DPRoZgWG.js} +1 -1
- package/dist/web/assets/{glide-data-grid-DV8ht1BP.js → glide-data-grid-BrtUKC3w.js} +1 -1
- package/dist/web/assets/{image-preview-Dbo7SAVb.js → image-preview-BFj-ipom.js} +1 -1
- package/dist/web/assets/{index-DU_JZ5MY.js → index-CJZZ6v1o.js} +3 -3
- package/dist/web/assets/keybindings-store-BOV4khyp.js +1 -0
- package/dist/web/assets/{markdown-renderer-D-QbsfIC.js → markdown-renderer-B63eYfrn.js} +1 -1
- package/dist/web/assets/notification-store-BklO85um.js +1 -0
- package/dist/web/assets/{pdf-preview-DV96VPTb.js → pdf-preview-JOwOGTIk.js} +1 -1
- package/dist/web/assets/{port-forwarding-tab-C4OYC71C.js → port-forwarding-tab-DJRRbLGF.js} +1 -1
- package/dist/web/assets/{postgres-viewer-hb-_twEU.js → postgres-viewer-AIOBOfCg.js} +1 -1
- package/dist/web/assets/{settings-tab-BUCIqVAl.js → settings-tab-BMHf9pO5.js} +1 -1
- package/dist/web/assets/{sql-query-editor-C7YgtDR3.js → sql-query-editor-Dw9UvzWt.js} +1 -1
- package/dist/web/assets/{sqlite-viewer-z3pGFSje.js → sqlite-viewer-HusTxs1Z.js} +1 -1
- package/dist/web/assets/system-monitor-tab-BNJIkOan.js +1 -0
- package/dist/web/assets/{terminal-tab-DbxLHofN.js → terminal-tab-W1VShnP7.js} +1 -1
- package/dist/web/assets/{video-preview-DylSBAzo.js → video-preview-BPAYbuvs.js} +1 -1
- package/dist/web/index.html +1 -1
- package/dist/web/sw.js +1 -1
- package/package.json +1 -1
- package/src/index.ts +0 -0
- package/src/server/middleware/auth.ts +1 -1
- package/src/server/routes/fs-browse.ts +18 -4
- package/src/server/routes/git.ts +2 -1
- package/src/services/download-token.service.ts +1 -2
- package/src/services/git.service.ts +17 -7
- package/src/services/resource-monitor-utils.ts +18 -6
- package/src/services/resource-monitor.service.ts +3 -2
- package/src/web/components/editor/diff-viewer.tsx +1 -0
- package/src/web/components/extensions/extension-webview.tsx +2 -2
- package/src/web/components/system/system-monitor-group-row.tsx +27 -7
- package/src/web/components/system/system-monitor-tab.tsx +1 -1
- package/src/web/hooks/use-extension-ws.ts +16 -3
- package/src/web/hooks/use-resource-monitor.ts +1 -1
- package/src/web/lib/file-download.ts +8 -0
- package/bun.lock +0 -2170
- package/bunfig.toml +0 -2
- package/dist/web/assets/diff-viewer-B-O1mvHO.js +0 -4
- package/dist/web/assets/extension-webview-0qfU1r7z.js +0 -3
- package/dist/web/assets/keybindings-store-0FUOwc9I.js +0 -1
- package/dist/web/assets/notification-store-bwd1UKbs.js +0 -1
- 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
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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 <
|
|
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
|
|
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
|
-
<
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
|
165
|
-
|
|
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");
|