@hienlh/ppm 0.13.54 → 0.13.56
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 +8 -1
- package/dist/web/assets/ai-settings-section-BH2UOQH-.js +1 -0
- package/dist/web/assets/{api-settings-C3T95dWg.js → api-settings-uQKmeGkl.js} +1 -1
- package/dist/web/assets/architecture-PBZL5I3N-DLKD1Xjj.js +1 -0
- package/dist/web/assets/arrow-down-D825m4vm.js +1 -0
- package/dist/web/assets/{audio-preview-BF1LU0eY.js → audio-preview-LCxWo-25.js} +1 -1
- package/dist/web/assets/chat-tab-3GUmdAJN.js +16 -0
- package/dist/web/assets/chevron-down-BMo4cBth.js +1 -0
- package/dist/web/assets/code-editor-Cd7NZ0VX.js +8 -0
- package/dist/web/assets/{conflict-editor-DcVj0Z-q.js → conflict-editor-No7IVPRN.js} +3 -3
- package/dist/web/assets/csv-parser-D8VHWVA6.js +6 -0
- package/dist/web/assets/{csv-preview-B3Dyhgho.js → csv-preview-DgArUJhd.js} +2 -2
- package/dist/web/assets/{data-grid-overlay-editor-C1UUm7Ob.js → data-grid-overlay-editor-CmduzuPM.js} +1 -1
- package/dist/web/assets/database-viewer-zeQXxkg-.js +1 -0
- package/dist/web/assets/diff-viewer-DmSkyBaT.js +4 -0
- package/dist/web/assets/{esm-DCbn6xno.js → esm-JPvheKDJ.js} +1 -1
- package/dist/web/assets/{extension-webview-DCmfZH6p.js → extension-webview-DCwz2Wso.js} +1 -1
- package/dist/web/assets/eye-off-BacF7RVS.js +1 -0
- package/dist/web/assets/git-log-panel-Bk7Eh-Bn.js +1 -0
- package/dist/web/assets/gitGraph-HDMCJU4V-2a0r4GHr.js +1 -0
- package/dist/web/assets/glide-data-grid-ha8hjunf.js +136 -0
- package/dist/web/assets/{image-preview-BIJGvZ5-.js → image-preview-C1VLHLdJ.js} +1 -1
- package/dist/web/assets/index-CP6EIXkh.js +27 -0
- package/dist/web/assets/index-DwvSM9vu.css +2 -0
- package/dist/web/assets/info-3K5VOQVL-CWKw4e0V.js +1 -0
- package/dist/web/assets/{input-_LFQwhzd.js → input-B78ol0hV.js} +1 -1
- package/dist/web/assets/keybindings-store-Rf9YKWAp.js +1 -0
- package/dist/web/assets/{markdown-renderer-CwKRCQuc.js → markdown-renderer-C7ZoGWso.js} +3 -3
- package/dist/web/assets/notification-store-C3tg-ZXm.js +1 -0
- package/dist/web/assets/{number-overlay-editor-CyEqxXcg.js → number-overlay-editor-DS-qf63L.js} +1 -1
- package/dist/web/assets/packet-RMMSAZCW-Ar00Wbhd.js +1 -0
- package/dist/web/assets/panel-store-B1pOXkyS.js +1 -0
- package/dist/web/assets/{pdf-preview-CbUTv4dX.js → pdf-preview-BdmR2RsT.js} +1 -1
- package/dist/web/assets/pie-UPGHQEXC-Q4ssDdib.js +1 -0
- package/dist/web/assets/port-forwarding-tab-BMb8neu0.js +1 -0
- package/dist/web/assets/{postgres-viewer-C-A4MMtt.js → postgres-viewer-DkM6ndux.js} +3 -3
- package/dist/web/assets/project-store-BnvrVKBw.js +1 -0
- package/dist/web/assets/radar-KQ55EAFF-kq5v4OKX.js +1 -0
- package/dist/web/assets/{settings-store-8FpQDjEA.js → settings-store-CSDOihqv.js} +2 -2
- package/dist/web/assets/settings-tab-WdL5pYxG.js +1 -0
- package/dist/web/assets/{sql-query-editor-Cu9mYyfb.js → sql-query-editor-CNwOnFgF.js} +1 -1
- package/dist/web/assets/{sqlite-viewer-D6ngJJgP.js → sqlite-viewer-FFlsRPWf.js} +1 -1
- package/dist/web/assets/system-monitor-tab-CefzxLVi.js +1 -0
- package/dist/web/assets/{tab-store-CNas5Ny8.js → tab-store-DzftzxTL.js} +1 -1
- package/dist/web/assets/{terminal-tab-CyuBxW2x.js → terminal-tab-C4MNU5S3.js} +2 -2
- package/dist/web/assets/{x-BPReZWnP.js → trash-2-DkIfBY8d.js} +1 -1
- package/dist/web/assets/treemap-KZPCXAKY-DChODgHt.js +1 -0
- package/dist/web/assets/{use-blob-url-DB4nNruT.js → use-blob-url-CI2h4qu0.js} +1 -1
- package/dist/web/assets/{use-monaco-theme-DEI-tJAh.js → use-monaco-theme-qx6SfVRk.js} +1 -1
- package/dist/web/assets/{vendor-mermaid-D2KKkqNs.js → vendor-mermaid-DCie7hiR.js} +2 -2
- package/dist/web/assets/{video-preview-ChP5ypMo.js → video-preview-izCy3xj7.js} +1 -1
- package/dist/web/assets/wifi-LJEyIdXf.js +1 -0
- package/dist/web/assets/x-DfF6D5Js.js +1 -0
- package/dist/web/index.html +22 -20
- package/dist/web/sw.js +1 -1
- package/docs/project-changelog.md +38 -1
- package/docs/system-architecture.md +2 -1
- package/package.json +1 -1
- package/packages/ext-git-graph/src/webview-html.ts +29 -8
- package/src/server/index.ts +4 -0
- package/src/server/middleware/auth.ts +9 -0
- package/src/server/routes/resources.ts +88 -0
- package/src/services/resource-monitor-utils.ts +129 -0
- package/src/services/resource-monitor.service.ts +122 -0
- package/src/web/components/git/git-log-panel.tsx +206 -0
- package/src/web/components/git/git-ref-badge.tsx +150 -0
- package/src/web/components/git/git-status-panel.tsx +18 -0
- package/src/web/components/layout/mobile-nav.tsx +2 -0
- package/src/web/components/layout/sidebar.tsx +6 -0
- package/src/web/components/layout/tab-bar.tsx +3 -0
- package/src/web/components/layout/tab-content.tsx +10 -0
- package/src/web/components/layout/tab-pool.tsx +2 -0
- package/src/web/components/system/resource-status-bar.tsx +66 -0
- package/src/web/components/system/sparkline-canvas.tsx +64 -0
- package/src/web/components/system/system-monitor-group-row.tsx +133 -0
- package/src/web/components/system/system-monitor-tab.tsx +186 -0
- package/src/web/hooks/use-resource-monitor.ts +114 -0
- package/src/web/stores/panel-store.ts +1 -1
- package/src/web/stores/tab-store.ts +3 -1
- package/dist/web/assets/ai-settings-section-AuV6Lzz2.js +0 -1
- package/dist/web/assets/architecture-PBZL5I3N-fAJMexNi.js +0 -1
- package/dist/web/assets/chat-tab-CCOkAmh8.js +0 -16
- package/dist/web/assets/code-editor-BptkAFVa.js +0 -8
- package/dist/web/assets/csv-parser-Dly5nqE1.js +0 -6
- package/dist/web/assets/database-viewer-CYrsNjRy.js +0 -1
- package/dist/web/assets/diff-viewer-DMBviO6l.js +0 -4
- package/dist/web/assets/file-store-DOxcU_7s.js +0 -1
- package/dist/web/assets/gitGraph-HDMCJU4V-ClRAoBAn.js +0 -1
- package/dist/web/assets/glide-data-grid-DhZjCUqu.js +0 -136
- package/dist/web/assets/index-BA8zQtSN.js +0 -27
- package/dist/web/assets/index-CKKoR3gY.css +0 -2
- package/dist/web/assets/info-3K5VOQVL-D8uyBfC_.js +0 -1
- package/dist/web/assets/keybindings-store-BXumit4n.js +0 -1
- package/dist/web/assets/notification-store-B3Fgo6Qw.js +0 -1
- package/dist/web/assets/packet-RMMSAZCW-BJj3FH_w.js +0 -1
- package/dist/web/assets/panel-store-C8wwxBpn.js +0 -1
- package/dist/web/assets/pie-UPGHQEXC-BKfHRBz7.js +0 -1
- package/dist/web/assets/port-forwarding-tab-Nn3-C-Vu.js +0 -1
- package/dist/web/assets/radar-KQ55EAFF-DkybKTt9.js +0 -1
- package/dist/web/assets/scroll-area-BDi_FNzr.js +0 -1
- package/dist/web/assets/settings-tab-Bzlcvim9.js +0 -1
- package/dist/web/assets/treemap-KZPCXAKY-BvDgIWW9.js +0 -1
- /package/dist/web/assets/{api-client-DIhJ5qVW.js → api-client-DiZgVOok.js} +0 -0
- /package/dist/web/assets/{chevron-right-DnHIvvcy.js → chevron-right-CD8e6Aj4.js} +0 -0
- /package/dist/web/assets/{code-DGBecc50.js → code-DiNmA3eR.js} +0 -0
- /package/dist/web/assets/{data-grid-types-D2cHE8hx.js → data-grid-types-C29KDkZJ.js} +0 -0
- /package/dist/web/assets/{database-DOWH9-Vv.js → database-Dc8mr-dP.js} +0 -0
- /package/dist/web/assets/{dist-DVk8T0R5.js → dist-DeY41KFi.js} +0 -0
- /package/dist/web/assets/{dist-BoIkGNC8.js → dist-PPUhQONj.js} +0 -0
- /package/dist/web/assets/{file-exclamation-point-BwzaQ50n.js → file-exclamation-point-B__2Hrd6.js} +0 -0
- /package/dist/web/assets/{globe-B4Ilypbs.js → globe-CQ8NAYvi.js} +0 -0
- /package/dist/web/assets/{katex-C10ndCVt.js → katex-DUj5OG1J.js} +0 -0
- /package/dist/web/assets/{lib-C2D8j3K3.js → lib-DrypSCq8.js} +0 -0
- /package/dist/web/assets/{react-DMIOAtcX.js → react-CfveccaI.js} +0 -0
- /package/dist/web/assets/{refresh-cw-BjrAbUJe.js → refresh-cw-CRD2qr4U.js} +0 -0
- /package/dist/web/assets/{search-tM8K5zWU.js → search-D90WJ5fo.js} +0 -0
- /package/dist/web/assets/{sparkles-CulWHe4c.js → sparkles-KCOEy7QI.js} +0 -0
- /package/dist/web/assets/{table-BzjWcs87.js → table-2wDtM4_B.js} +0 -0
- /package/dist/web/assets/{text-wrap-DJz9Bgpa.js → text-wrap-AZErifCu.js} +0 -0
- /package/dist/web/assets/{utils-CQux7CsO.js → utils-E0yyGxXt.js} +0 -0
- /package/dist/web/assets/{vendor-xterm-D1P36hcr.js → vendor-xterm-t3d5xZdz.js} +0 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resource Monitor Service — polls `ps` to track PPM process tree,
|
|
3
|
+
* categorizes processes, and maintains a ring buffer of snapshots.
|
|
4
|
+
* Lazy polling: only active when SSE subscribers exist.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { parseProcessList, buildTree, groupProcesses } from "./resource-monitor-utils.ts";
|
|
8
|
+
|
|
9
|
+
// ── Types ──────────────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
export interface ProcessEntry {
|
|
12
|
+
pid: number;
|
|
13
|
+
ppid: number;
|
|
14
|
+
cpu: number;
|
|
15
|
+
ramMB: number;
|
|
16
|
+
command: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ResourceGroup {
|
|
20
|
+
type: "server" | "terminal" | "ai-tool" | "build" | "unknown";
|
|
21
|
+
label: string;
|
|
22
|
+
cpu: number;
|
|
23
|
+
ramMB: number;
|
|
24
|
+
processes: Omit<ProcessEntry, "ppid">[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ResourceSnapshot {
|
|
28
|
+
timestamp: number;
|
|
29
|
+
server: { pid: number; cpu: number; ramMB: number };
|
|
30
|
+
total: { cpu: number; ramMB: number; processCount: number };
|
|
31
|
+
groups: ResourceGroup[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type SnapshotCallback = (snapshot: ResourceSnapshot) => void;
|
|
35
|
+
|
|
36
|
+
// ── Service ────────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
const RING_BUFFER_MAX = 600; // 30 min at 3s interval
|
|
39
|
+
const POLL_INTERVAL = 3000;
|
|
40
|
+
|
|
41
|
+
class ResourceMonitorService {
|
|
42
|
+
private ringBuffer: ResourceSnapshot[] = [];
|
|
43
|
+
private subscribers = new Set<SnapshotCallback>();
|
|
44
|
+
private timer: ReturnType<typeof setInterval> | null = null;
|
|
45
|
+
|
|
46
|
+
subscribe(cb: SnapshotCallback) {
|
|
47
|
+
this.subscribers.add(cb);
|
|
48
|
+
if (this.subscribers.size === 1) this.startPolling();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
unsubscribe(cb: SnapshotCallback) {
|
|
52
|
+
this.subscribers.delete(cb);
|
|
53
|
+
if (this.subscribers.size === 0) this.stopPolling();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
getLatest(): ResourceSnapshot | null {
|
|
57
|
+
return this.ringBuffer.at(-1) ?? null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
getHistory(): ResourceSnapshot[] {
|
|
61
|
+
return this.ringBuffer;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private startPolling() {
|
|
65
|
+
if (this.timer) return;
|
|
66
|
+
if (process.platform === "win32") return; // ps not available on Windows
|
|
67
|
+
this.poll(); // immediate first poll
|
|
68
|
+
this.timer = setInterval(() => this.poll(), POLL_INTERVAL);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private stopPolling() {
|
|
72
|
+
if (this.timer) {
|
|
73
|
+
clearInterval(this.timer);
|
|
74
|
+
this.timer = null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private async poll() {
|
|
79
|
+
try {
|
|
80
|
+
const proc = Bun.spawn({
|
|
81
|
+
cmd: ["ps", "-e", "-o", "pid,ppid,%cpu,rss,args"],
|
|
82
|
+
stdout: "pipe",
|
|
83
|
+
stderr: "ignore",
|
|
84
|
+
});
|
|
85
|
+
const stdout = await new Response(proc.stdout).text();
|
|
86
|
+
await proc.exited;
|
|
87
|
+
|
|
88
|
+
const entries = parseProcessList(stdout);
|
|
89
|
+
const rootPid = process.pid;
|
|
90
|
+
const serverEntry = entries.find((e) => e.pid === rootPid);
|
|
91
|
+
const children = buildTree(entries, rootPid);
|
|
92
|
+
const groups = groupProcesses(serverEntry, children);
|
|
93
|
+
|
|
94
|
+
const allProcs = serverEntry ? [serverEntry, ...children] : children;
|
|
95
|
+
const snapshot: ResourceSnapshot = {
|
|
96
|
+
timestamp: Date.now(),
|
|
97
|
+
server: serverEntry
|
|
98
|
+
? { pid: serverEntry.pid, cpu: serverEntry.cpu, ramMB: serverEntry.ramMB }
|
|
99
|
+
: { pid: rootPid, cpu: 0, ramMB: 0 },
|
|
100
|
+
total: {
|
|
101
|
+
cpu: Math.round(allProcs.reduce((s, p) => s + p.cpu, 0) * 10) / 10,
|
|
102
|
+
ramMB: Math.round(allProcs.reduce((s, p) => s + p.ramMB, 0) * 10) / 10,
|
|
103
|
+
processCount: allProcs.length,
|
|
104
|
+
},
|
|
105
|
+
groups,
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
this.ringBuffer.push(snapshot);
|
|
109
|
+
if (this.ringBuffer.length > RING_BUFFER_MAX) {
|
|
110
|
+
this.ringBuffer.shift();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
for (const cb of this.subscribers) {
|
|
114
|
+
try { cb(snapshot); } catch {}
|
|
115
|
+
}
|
|
116
|
+
} catch (err) {
|
|
117
|
+
console.error("[ResourceMonitor] poll error:", err);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export const resourceMonitor = new ResourceMonitorService();
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { useEffect, useState, useCallback, useRef } from "react";
|
|
2
|
+
import { Loader2, RefreshCw, GitCommitHorizontal } from "lucide-react";
|
|
3
|
+
import { api, projectUrl } from "@/lib/api-client";
|
|
4
|
+
import { useProjectStore } from "@/stores/project-store";
|
|
5
|
+
import { useShallow } from "zustand/react/shallow";
|
|
6
|
+
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
7
|
+
import { GitRefBadge, buildRefBadges } from "./git-ref-badge";
|
|
8
|
+
import type { GitGraphData, GitCommit, GitBranch } from "../../../types/git";
|
|
9
|
+
|
|
10
|
+
const PAGE_SIZE = 100;
|
|
11
|
+
|
|
12
|
+
interface GitLogPanelProps {
|
|
13
|
+
metadata?: Record<string, unknown>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function GitLogPanel({ metadata }: GitLogPanelProps) {
|
|
17
|
+
const projectName = (metadata?.projectName as string) ??
|
|
18
|
+
useProjectStore(useShallow((s) => s.activeProject))?.name;
|
|
19
|
+
const [data, setData] = useState<GitGraphData | null>(null);
|
|
20
|
+
const [loading, setLoading] = useState(false);
|
|
21
|
+
const [hasMore, setHasMore] = useState(true);
|
|
22
|
+
const scrollRef = useRef<HTMLDivElement>(null);
|
|
23
|
+
|
|
24
|
+
const fetchData = useCallback(async (skip = 0, append = false) => {
|
|
25
|
+
if (!projectName) return;
|
|
26
|
+
setLoading(true);
|
|
27
|
+
try {
|
|
28
|
+
const res = await api.get<GitGraphData>(
|
|
29
|
+
`${projectUrl(projectName)}/git/graph?max=${PAGE_SIZE}&skip=${skip}`,
|
|
30
|
+
);
|
|
31
|
+
setData((prev) => {
|
|
32
|
+
if (append && prev) {
|
|
33
|
+
return {
|
|
34
|
+
...res,
|
|
35
|
+
commits: [...prev.commits, ...res.commits],
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
return res;
|
|
39
|
+
});
|
|
40
|
+
setHasMore(res.commits.length === PAGE_SIZE);
|
|
41
|
+
} catch {
|
|
42
|
+
/* silent */
|
|
43
|
+
}
|
|
44
|
+
setLoading(false);
|
|
45
|
+
}, [projectName]);
|
|
46
|
+
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
fetchData();
|
|
49
|
+
}, [fetchData]);
|
|
50
|
+
|
|
51
|
+
const loadMore = useCallback(() => {
|
|
52
|
+
if (loading || !hasMore || !data) return;
|
|
53
|
+
fetchData(data.commits.length, true);
|
|
54
|
+
}, [loading, hasMore, data, fetchData]);
|
|
55
|
+
|
|
56
|
+
// Infinite scroll: load more when near bottom
|
|
57
|
+
const handleScroll = useCallback((e: React.UIEvent<HTMLDivElement>) => {
|
|
58
|
+
const target = e.currentTarget;
|
|
59
|
+
if (target.scrollHeight - target.scrollTop - target.clientHeight < 200) {
|
|
60
|
+
loadMore();
|
|
61
|
+
}
|
|
62
|
+
}, [loadMore]);
|
|
63
|
+
|
|
64
|
+
if (!projectName) {
|
|
65
|
+
return (
|
|
66
|
+
<div className="flex items-center justify-center h-full text-text-secondary text-sm">
|
|
67
|
+
Select a project to view git log
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<div className="flex flex-col h-full">
|
|
74
|
+
{/* Header */}
|
|
75
|
+
<div className="flex items-center gap-2 px-3 py-2 border-b border-border shrink-0">
|
|
76
|
+
<GitCommitHorizontal className="size-4 text-text-secondary" />
|
|
77
|
+
<span className="text-xs font-medium text-text-primary">Git Log</span>
|
|
78
|
+
<span className="text-[10px] text-text-subtle">
|
|
79
|
+
{data ? `${data.commits.length} commits` : ""}
|
|
80
|
+
</span>
|
|
81
|
+
<div className="flex-1" />
|
|
82
|
+
<button
|
|
83
|
+
onClick={() => fetchData()}
|
|
84
|
+
disabled={loading}
|
|
85
|
+
className="p-1 rounded text-text-subtle hover:text-text-secondary transition-colors"
|
|
86
|
+
title="Refresh"
|
|
87
|
+
>
|
|
88
|
+
<RefreshCw className={`size-3.5 ${loading ? "animate-spin" : ""}`} />
|
|
89
|
+
</button>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
{/* Commit list */}
|
|
93
|
+
{!data && loading ? (
|
|
94
|
+
<div className="flex items-center justify-center h-full">
|
|
95
|
+
<Loader2 className="size-5 animate-spin text-primary" />
|
|
96
|
+
</div>
|
|
97
|
+
) : data && data.commits.length === 0 ? (
|
|
98
|
+
<div className="flex items-center justify-center h-full text-text-secondary text-sm">
|
|
99
|
+
No commits yet
|
|
100
|
+
</div>
|
|
101
|
+
) : data ? (
|
|
102
|
+
<div
|
|
103
|
+
ref={scrollRef}
|
|
104
|
+
className="flex-1 overflow-y-auto min-h-0"
|
|
105
|
+
onScroll={handleScroll}
|
|
106
|
+
>
|
|
107
|
+
{data.commits.map((commit) => (
|
|
108
|
+
<CommitRow
|
|
109
|
+
key={commit.hash}
|
|
110
|
+
commit={commit}
|
|
111
|
+
branches={data.branches}
|
|
112
|
+
head={data.head}
|
|
113
|
+
/>
|
|
114
|
+
))}
|
|
115
|
+
{loading && (
|
|
116
|
+
<div className="flex items-center justify-center py-3">
|
|
117
|
+
<Loader2 className="size-4 animate-spin text-text-subtle" />
|
|
118
|
+
</div>
|
|
119
|
+
)}
|
|
120
|
+
{!hasMore && data.commits.length > 0 && (
|
|
121
|
+
<div className="text-center text-[10px] text-text-subtle py-3">
|
|
122
|
+
End of history
|
|
123
|
+
</div>
|
|
124
|
+
)}
|
|
125
|
+
</div>
|
|
126
|
+
) : null}
|
|
127
|
+
</div>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function CommitRow({
|
|
132
|
+
commit,
|
|
133
|
+
branches,
|
|
134
|
+
head,
|
|
135
|
+
}: {
|
|
136
|
+
commit: GitCommit;
|
|
137
|
+
branches: GitBranch[];
|
|
138
|
+
head: string;
|
|
139
|
+
}) {
|
|
140
|
+
const isHead = commit.hash === head;
|
|
141
|
+
const badges = buildRefBadges(commit.hash, commit.refs, branches, head);
|
|
142
|
+
|
|
143
|
+
return (
|
|
144
|
+
<div
|
|
145
|
+
className={`flex items-start gap-2 px-3 py-1.5 border-b border-border/50 hover:bg-surface-elevated transition-colors ${
|
|
146
|
+
isHead ? "bg-primary/5" : ""
|
|
147
|
+
}`}
|
|
148
|
+
>
|
|
149
|
+
{/* Commit dot */}
|
|
150
|
+
<div className="flex items-center pt-1 shrink-0">
|
|
151
|
+
<span
|
|
152
|
+
className={`size-2.5 rounded-full border-2 ${
|
|
153
|
+
isHead
|
|
154
|
+
? "border-primary bg-primary"
|
|
155
|
+
: "border-text-subtle bg-transparent"
|
|
156
|
+
}`}
|
|
157
|
+
/>
|
|
158
|
+
</div>
|
|
159
|
+
|
|
160
|
+
{/* Content */}
|
|
161
|
+
<div className="flex-1 min-w-0">
|
|
162
|
+
{/* Ref badges row */}
|
|
163
|
+
{badges.length > 0 && (
|
|
164
|
+
<div className="flex flex-wrap items-center gap-1 mb-0.5">
|
|
165
|
+
{badges.map((badge) => (
|
|
166
|
+
<GitRefBadge key={`${badge.name}-${badge.syncState}`} {...badge} />
|
|
167
|
+
))}
|
|
168
|
+
</div>
|
|
169
|
+
)}
|
|
170
|
+
|
|
171
|
+
{/* Subject line */}
|
|
172
|
+
<p className="text-xs text-text-primary truncate leading-snug">
|
|
173
|
+
{commit.subject}
|
|
174
|
+
</p>
|
|
175
|
+
|
|
176
|
+
{/* Meta */}
|
|
177
|
+
<div className="flex items-center gap-2 mt-0.5">
|
|
178
|
+
<span className="text-[10px] font-mono text-text-subtle">
|
|
179
|
+
{commit.abbreviatedHash}
|
|
180
|
+
</span>
|
|
181
|
+
<span className="text-[10px] text-text-subtle truncate">
|
|
182
|
+
{commit.authorName}
|
|
183
|
+
</span>
|
|
184
|
+
<span className="text-[10px] text-text-subtle shrink-0">
|
|
185
|
+
{formatRelativeDate(commit.authorDate)}
|
|
186
|
+
</span>
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Compact relative date: "2h", "3d", "Jan 5" */
|
|
194
|
+
function formatRelativeDate(dateStr: string): string {
|
|
195
|
+
const date = new Date(dateStr);
|
|
196
|
+
const now = Date.now();
|
|
197
|
+
const diff = now - date.getTime();
|
|
198
|
+
const mins = Math.floor(diff / 60000);
|
|
199
|
+
if (mins < 1) return "now";
|
|
200
|
+
if (mins < 60) return `${mins}m`;
|
|
201
|
+
const hours = Math.floor(mins / 60);
|
|
202
|
+
if (hours < 24) return `${hours}h`;
|
|
203
|
+
const days = Math.floor(hours / 24);
|
|
204
|
+
if (days < 30) return `${days}d`;
|
|
205
|
+
return date.toLocaleDateString("en", { month: "short", day: "numeric" });
|
|
206
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { Cloud, CloudOff, Tag, ArrowUp, ArrowDown } from "lucide-react";
|
|
2
|
+
import type { GitBranch } from "../../../types/git";
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
|
|
5
|
+
/** Deterministic color from branch name — 8 distinct hues */
|
|
6
|
+
const BRANCH_COLORS = [
|
|
7
|
+
"#2563eb", // blue
|
|
8
|
+
"#16a34a", // green
|
|
9
|
+
"#d97706", // amber
|
|
10
|
+
"#9333ea", // purple
|
|
11
|
+
"#dc2626", // red
|
|
12
|
+
"#0891b2", // cyan
|
|
13
|
+
"#c026d3", // fuchsia
|
|
14
|
+
"#ea580c", // orange
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
function hashColor(name: string): string {
|
|
18
|
+
let hash = 0;
|
|
19
|
+
for (let i = 0; i < name.length; i++) {
|
|
20
|
+
hash = (hash * 31 + name.charCodeAt(i)) | 0;
|
|
21
|
+
}
|
|
22
|
+
return BRANCH_COLORS[Math.abs(hash) % BRANCH_COLORS.length]!;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type RefSyncState = "synced" | "local-only" | "remote-only" | "ahead" | "behind";
|
|
26
|
+
|
|
27
|
+
interface GitRefBadgeProps {
|
|
28
|
+
/** Display name (e.g. "main", "feature/login") */
|
|
29
|
+
name: string;
|
|
30
|
+
syncState: RefSyncState;
|
|
31
|
+
/** True if this is HEAD */
|
|
32
|
+
isHead?: boolean;
|
|
33
|
+
/** True if this is a tag ref */
|
|
34
|
+
isTag?: boolean;
|
|
35
|
+
className?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Merged branch/tag badge — colored border + light bg + black text */
|
|
39
|
+
export function GitRefBadge({ name, syncState, isHead, isTag, className }: GitRefBadgeProps) {
|
|
40
|
+
const color = isTag ? "#d97706" : hashColor(name);
|
|
41
|
+
|
|
42
|
+
const SyncIcon = isTag
|
|
43
|
+
? Tag
|
|
44
|
+
: syncState === "synced"
|
|
45
|
+
? Cloud
|
|
46
|
+
: syncState === "ahead"
|
|
47
|
+
? ArrowUp
|
|
48
|
+
: syncState === "behind"
|
|
49
|
+
? ArrowDown
|
|
50
|
+
: syncState === "remote-only"
|
|
51
|
+
? Cloud
|
|
52
|
+
: CloudOff;
|
|
53
|
+
|
|
54
|
+
const title = isTag
|
|
55
|
+
? `Tag: ${name}`
|
|
56
|
+
: syncState === "synced"
|
|
57
|
+
? `${name} — synced with remote`
|
|
58
|
+
: syncState === "ahead"
|
|
59
|
+
? `${name} — ahead of remote`
|
|
60
|
+
: syncState === "behind"
|
|
61
|
+
? `${name} — behind remote`
|
|
62
|
+
: syncState === "remote-only"
|
|
63
|
+
? `${name} — remote only`
|
|
64
|
+
: `${name} — local only`;
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<span
|
|
68
|
+
className={cn(
|
|
69
|
+
"inline-flex items-center gap-1 rounded-md border px-1.5 py-0.5 text-[11px] font-medium leading-tight max-w-[220px]",
|
|
70
|
+
isHead && "ring-1 ring-offset-1 ring-offset-background",
|
|
71
|
+
syncState === "remote-only" && "opacity-70",
|
|
72
|
+
className,
|
|
73
|
+
)}
|
|
74
|
+
style={{
|
|
75
|
+
borderColor: color,
|
|
76
|
+
backgroundColor: color + "1A", // ~10% opacity
|
|
77
|
+
color: "#1a1a1a",
|
|
78
|
+
// HEAD ring color
|
|
79
|
+
...(isHead ? { ["--tw-ring-color" as string]: color } : {}),
|
|
80
|
+
}}
|
|
81
|
+
title={title}
|
|
82
|
+
>
|
|
83
|
+
<SyncIcon className="size-3 shrink-0" style={{ color }} />
|
|
84
|
+
<span className="truncate">{name}</span>
|
|
85
|
+
</span>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Merge raw commit refs + branch data into deduplicated badge props */
|
|
90
|
+
export function buildRefBadges(
|
|
91
|
+
commitHash: string,
|
|
92
|
+
refs: string[],
|
|
93
|
+
branches: GitBranch[],
|
|
94
|
+
head: string,
|
|
95
|
+
): GitRefBadgeProps[] {
|
|
96
|
+
const badges: GitRefBadgeProps[] = [];
|
|
97
|
+
const isHead = commitHash === head;
|
|
98
|
+
|
|
99
|
+
// Collect tags from refs
|
|
100
|
+
for (const ref of refs) {
|
|
101
|
+
if (ref.startsWith("tag: ")) {
|
|
102
|
+
badges.push({ name: ref.slice(5), syncState: "synced", isTag: true });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Group branches by logical name (strip remotes/origin/ prefix)
|
|
107
|
+
const branchesOnCommit = branches.filter((b) => b.commitHash === commitHash);
|
|
108
|
+
const localBranches = branchesOnCommit.filter((b) => !b.remote);
|
|
109
|
+
const remoteBranches = branchesOnCommit.filter((b) => b.remote);
|
|
110
|
+
|
|
111
|
+
// Build set of remote branch names that have a local counterpart
|
|
112
|
+
const localNames = new Set(localBranches.map((b) => b.name));
|
|
113
|
+
const mergedRemoteNames = new Set<string>();
|
|
114
|
+
|
|
115
|
+
// Add local branches with sync state
|
|
116
|
+
for (const local of localBranches) {
|
|
117
|
+
let syncState: RefSyncState = "local-only";
|
|
118
|
+
if (local.remotes.length > 0) {
|
|
119
|
+
// Has remote tracking
|
|
120
|
+
if (local.ahead > 0 && local.behind === 0) syncState = "ahead";
|
|
121
|
+
else if (local.behind > 0 && local.ahead === 0) syncState = "behind";
|
|
122
|
+
else syncState = "synced";
|
|
123
|
+
}
|
|
124
|
+
badges.push({
|
|
125
|
+
name: local.name,
|
|
126
|
+
syncState,
|
|
127
|
+
isHead: isHead && local.current,
|
|
128
|
+
});
|
|
129
|
+
// Mark corresponding remote names as merged
|
|
130
|
+
for (const remote of remoteBranches) {
|
|
131
|
+
const stripped = remote.name.replace(/^remotes\//, "");
|
|
132
|
+
const slashIdx = stripped.indexOf("/");
|
|
133
|
+
if (slashIdx < 0) continue;
|
|
134
|
+
const remoteBranchName = stripped.slice(slashIdx + 1);
|
|
135
|
+
if (remoteBranchName === local.name) {
|
|
136
|
+
mergedRemoteNames.add(remote.name);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Add remote-only branches (not merged with any local)
|
|
142
|
+
for (const remote of remoteBranches) {
|
|
143
|
+
if (mergedRemoteNames.has(remote.name)) continue;
|
|
144
|
+
// Strip "remotes/" prefix for display, keep "origin/..." format
|
|
145
|
+
const displayName = remote.name.replace(/^remotes\//, "");
|
|
146
|
+
badges.push({ name: displayName, syncState: "remote-only" });
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return badges;
|
|
150
|
+
}
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
ChevronRight,
|
|
13
13
|
ChevronDown,
|
|
14
14
|
FileText,
|
|
15
|
+
GitCommitHorizontal,
|
|
15
16
|
} from "lucide-react";
|
|
16
17
|
import { api, projectUrl } from "@/lib/api-client";
|
|
17
18
|
import { basename } from "@/lib/utils";
|
|
@@ -330,6 +331,23 @@ export function GitStatusPanel({ metadata, tabId, onNavigate }: GitStatusPanelPr
|
|
|
330
331
|
>
|
|
331
332
|
<FolderTree className="size-3.5" />
|
|
332
333
|
</Button>
|
|
334
|
+
<Button
|
|
335
|
+
variant="ghost"
|
|
336
|
+
size="icon-xs"
|
|
337
|
+
onClick={() => {
|
|
338
|
+
openTab({
|
|
339
|
+
type: "git-log",
|
|
340
|
+
title: "Git Log",
|
|
341
|
+
projectId: projectName ?? null,
|
|
342
|
+
closable: true,
|
|
343
|
+
metadata: { projectName },
|
|
344
|
+
});
|
|
345
|
+
onNavigate?.();
|
|
346
|
+
}}
|
|
347
|
+
title="View Git Log"
|
|
348
|
+
>
|
|
349
|
+
<GitCommitHorizontal className="size-3.5" />
|
|
350
|
+
</Button>
|
|
333
351
|
<Button
|
|
334
352
|
variant="ghost"
|
|
335
353
|
size="icon-xs"
|
|
@@ -35,6 +35,8 @@ const TAB_ICONS: Record<TabType, React.ElementType> = {
|
|
|
35
35
|
extension: Puzzle,
|
|
36
36
|
"extension-webview": Puzzle,
|
|
37
37
|
"conflict-editor": FileDiff,
|
|
38
|
+
"system-monitor": Settings,
|
|
39
|
+
"git-log": FileCode,
|
|
38
40
|
};
|
|
39
41
|
|
|
40
42
|
interface MobileNavProps { onMenuPress: () => void; onProjectsPress: () => void; }
|
|
@@ -13,6 +13,7 @@ import { ExtensionTreeView } from "@/components/extensions/extension-tree-view";
|
|
|
13
13
|
import { JiraPanel } from "@/components/jira/jira-panel";
|
|
14
14
|
import { useGitStatusStore, useGitChangesPoller } from "@/stores/git-status-store";
|
|
15
15
|
import { useJiraStore } from "@/stores/jira-store";
|
|
16
|
+
import { ResourceStatusBar } from "@/components/system/resource-status-bar";
|
|
16
17
|
import { cn } from "@/lib/utils";
|
|
17
18
|
|
|
18
19
|
const BUILTIN_TABS: { id: SidebarActiveTab; label: string; icon: React.ElementType }[] = [
|
|
@@ -183,6 +184,11 @@ export const Sidebar = memo(function Sidebar() {
|
|
|
183
184
|
)}
|
|
184
185
|
</div>
|
|
185
186
|
|
|
187
|
+
{/* Resource monitor status bar */}
|
|
188
|
+
<div className="shrink-0 border-t border-border">
|
|
189
|
+
<ResourceStatusBar />
|
|
190
|
+
</div>
|
|
191
|
+
|
|
186
192
|
{/* Resize handle */}
|
|
187
193
|
<ResizeHandle onResize={setSidebarWidth} />
|
|
188
194
|
</aside>
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
ChevronRight,
|
|
12
12
|
Globe,
|
|
13
13
|
Puzzle,
|
|
14
|
+
GitCommitHorizontal,
|
|
14
15
|
} from "lucide-react";
|
|
15
16
|
import { useTabStore, type TabType } from "@/stores/tab-store";
|
|
16
17
|
import { usePanelStore } from "@/stores/panel-store";
|
|
@@ -52,6 +53,8 @@ const TAB_ICONS: Record<TabType, React.ElementType> = {
|
|
|
52
53
|
extension: Puzzle,
|
|
53
54
|
"extension-webview": Puzzle,
|
|
54
55
|
"conflict-editor": FileDiff,
|
|
56
|
+
"system-monitor": Settings,
|
|
57
|
+
"git-log": GitCommitHorizontal,
|
|
55
58
|
};
|
|
56
59
|
|
|
57
60
|
interface TabBarProps {
|
|
@@ -64,6 +64,16 @@ const TAB_COMPONENTS: Record<TabType, React.LazyExoticComponent<React.ComponentT
|
|
|
64
64
|
default: m.ConflictEditor,
|
|
65
65
|
})),
|
|
66
66
|
),
|
|
67
|
+
"system-monitor": lazy(() =>
|
|
68
|
+
import("@/components/system/system-monitor-tab").then((m) => ({
|
|
69
|
+
default: m.SystemMonitorTab,
|
|
70
|
+
})),
|
|
71
|
+
),
|
|
72
|
+
"git-log": lazy(() =>
|
|
73
|
+
import("@/components/git/git-log-panel").then((m) => ({
|
|
74
|
+
default: m.GitLogPanel,
|
|
75
|
+
})),
|
|
76
|
+
),
|
|
67
77
|
};
|
|
68
78
|
|
|
69
79
|
function LoadingFallback() {
|
|
@@ -31,6 +31,8 @@ const TAB_COMPONENTS: Record<TabType, React.LazyExoticComponent<React.ComponentT
|
|
|
31
31
|
extension: lazy(() => import("@/components/extensions/extension-webview").then((m) => ({ default: m.ExtensionWebview }))),
|
|
32
32
|
"extension-webview": lazy(() => import("@/components/extensions/extension-webview").then((m) => ({ default: m.ExtensionWebview }))),
|
|
33
33
|
"conflict-editor": lazy(() => import("@/components/editor/conflict-editor").then((m) => ({ default: m.ConflictEditor }))),
|
|
34
|
+
"system-monitor": lazy(() => import("@/components/system/system-monitor-tab").then((m) => ({ default: m.SystemMonitorTab }))),
|
|
35
|
+
"git-log": lazy(() => import("@/components/git/git-log-panel").then((m) => ({ default: m.GitLogPanel }))),
|
|
34
36
|
};
|
|
35
37
|
|
|
36
38
|
// ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { memo } from "react";
|
|
2
|
+
import { Cpu } from "lucide-react";
|
|
3
|
+
import { useResourceMonitor } from "@/hooks/use-resource-monitor";
|
|
4
|
+
import { useTabStore } from "@/stores/tab-store";
|
|
5
|
+
import { useIsMobile } from "@/hooks/use-is-mobile";
|
|
6
|
+
import { cn } from "@/lib/utils";
|
|
7
|
+
|
|
8
|
+
function cpuColor(cpu: number) {
|
|
9
|
+
if (cpu > 80) return "text-red-500";
|
|
10
|
+
if (cpu > 50) return "text-yellow-500";
|
|
11
|
+
return "text-green-500";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const ResourceStatusBar = memo(function ResourceStatusBar() {
|
|
15
|
+
const { latest, isConnected } = useResourceMonitor();
|
|
16
|
+
const openTab = useTabStore((s) => s.openTab);
|
|
17
|
+
const isMobile = useIsMobile();
|
|
18
|
+
|
|
19
|
+
const handleClick = () => {
|
|
20
|
+
openTab({
|
|
21
|
+
type: "system-monitor",
|
|
22
|
+
title: "System Monitor",
|
|
23
|
+
projectId: null,
|
|
24
|
+
closable: true,
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
if (!isConnected || !latest) {
|
|
29
|
+
return (
|
|
30
|
+
<button
|
|
31
|
+
onClick={handleClick}
|
|
32
|
+
className="flex items-center gap-1.5 px-2 py-1 text-[10px] text-text-subtle hover:text-text-secondary transition-colors w-full"
|
|
33
|
+
>
|
|
34
|
+
<Cpu className="size-3 opacity-50" />
|
|
35
|
+
<span className="opacity-50">Connecting...</span>
|
|
36
|
+
</button>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const { cpu, ramMB, processCount } = latest.total;
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<button
|
|
44
|
+
onClick={handleClick}
|
|
45
|
+
className="flex items-center gap-1.5 px-2 py-1 text-[10px] hover:bg-surface-hover transition-colors w-full cursor-pointer"
|
|
46
|
+
title="Open System Monitor"
|
|
47
|
+
>
|
|
48
|
+
<Cpu className={cn("size-3", cpuColor(cpu))} />
|
|
49
|
+
<span className={cpuColor(cpu)}>
|
|
50
|
+
{isMobile ? `${cpu.toFixed(0)}%` : `CPU ${cpu.toFixed(1)}%`}
|
|
51
|
+
</span>
|
|
52
|
+
<span className="text-text-subtle">|</span>
|
|
53
|
+
<span className="text-text-secondary">
|
|
54
|
+
{ramMB < 1024
|
|
55
|
+
? `${ramMB.toFixed(0)}MB`
|
|
56
|
+
: `${(ramMB / 1024).toFixed(1)}GB`}
|
|
57
|
+
</span>
|
|
58
|
+
{!isMobile && (
|
|
59
|
+
<>
|
|
60
|
+
<span className="text-text-subtle">|</span>
|
|
61
|
+
<span className="text-text-subtle">{processCount} proc</span>
|
|
62
|
+
</>
|
|
63
|
+
)}
|
|
64
|
+
</button>
|
|
65
|
+
);
|
|
66
|
+
});
|