project-graph-mcp 1.5.0 → 2.1.0
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/README.md +171 -31
- package/docs/img/explorer-compact.jpg +0 -0
- package/docs/img/explorer-expanded.jpg +0 -0
- package/package.json +12 -8
- package/src/.project-graph-cache.json +1 -1
- package/src/analysis/analysis-cache.js +7 -0
- package/src/analysis/complexity.js +14 -0
- package/src/analysis/custom-rules.js +36 -0
- package/src/analysis/db-analysis.js +9 -0
- package/src/analysis/dead-code.js +19 -0
- package/src/analysis/full-analysis.js +18 -0
- package/src/analysis/jsdoc-checker.js +24 -0
- package/src/analysis/jsdoc-generator.js +10 -0
- package/src/analysis/large-files.js +11 -0
- package/src/analysis/outdated-patterns.js +12 -0
- package/src/analysis/similar-functions.js +16 -0
- package/src/analysis/test-annotations.js +21 -0
- package/src/analysis/type-checker.js +8 -0
- package/src/analysis/undocumented.js +14 -0
- package/src/cli/cli-handlers.js +4 -0
- package/src/cli/cli.js +5 -0
- package/src/compact/.project-graph-cache.json +1 -0
- package/src/compact/ai-context.js +7 -0
- package/src/compact/compact-migrate.js +17 -0
- package/src/compact/compact.js +18 -0
- package/src/compact/compress.js +14 -0
- package/src/compact/ctx-to-jsdoc.js +29 -0
- package/src/compact/doc-dialect.js +30 -0
- package/src/compact/expand.js +37 -0
- package/src/compact/framework-references.js +5 -0
- package/src/compact/instructions.js +3 -0
- package/src/compact/mode-config.js +8 -0
- package/src/compact/validate-pipeline.js +9 -0
- package/src/core/event-bus.js +9 -0
- package/src/core/filters.js +14 -0
- package/src/core/graph-builder.js +12 -0
- package/src/core/parser.js +31 -0
- package/src/core/workspace.js +8 -0
- package/src/lang/lang-go.js +17 -0
- package/src/lang/lang-python.js +12 -0
- package/src/lang/lang-sql.js +23 -0
- package/src/lang/lang-typescript.js +9 -0
- package/src/lang/lang-utils.js +4 -0
- package/src/mcp/mcp-server.js +17 -0
- package/src/mcp/tool-defs.js +3 -0
- package/src/mcp/tools.js +25 -0
- package/src/network/backend-lifecycle.js +19 -0
- package/src/network/backend.js +5 -0
- package/src/network/local-gateway.js +23 -0
- package/src/network/mdns.js +13 -0
- package/src/network/server.js +10 -0
- package/src/network/web-server.js +34 -0
- package/web/.project-graph-cache.json +1 -0
- package/web/app.js +17 -0
- package/web/components/code-block.js +3 -0
- package/web/components/quick-open.js +5 -0
- package/web/dashboard-state.js +3 -0
- package/web/dashboard.html +27 -0
- package/web/dashboard.js +8 -0
- package/web/highlight.js +13 -0
- package/web/index.html +35 -0
- package/web/panels/ActionBoard/ActionBoard.css.js +1 -0
- package/web/panels/ActionBoard/ActionBoard.js +4 -0
- package/web/panels/ActionBoard/ActionBoard.tpl.js +1 -0
- package/web/panels/EventItem/EventItem.css.js +1 -0
- package/web/panels/EventItem/EventItem.js +4 -0
- package/web/panels/EventItem/EventItem.tpl.js +1 -0
- package/web/panels/ProjectItem/ProjectItem.css.js +1 -0
- package/web/panels/ProjectItem/ProjectItem.js +5 -0
- package/web/panels/ProjectItem/ProjectItem.tpl.js +1 -0
- package/web/panels/ProjectList/ProjectList.css.js +1 -0
- package/web/panels/ProjectList/ProjectList.js +4 -0
- package/web/panels/ProjectList/ProjectList.tpl.js +1 -0
- package/web/panels/SettingsPanel/.project-graph-cache.json +1 -0
- package/web/panels/SettingsPanel/SettingsPanel.css.js +1 -0
- package/web/panels/SettingsPanel/SettingsPanel.js +7 -0
- package/web/panels/SettingsPanel/SettingsPanel.tpl.js +1 -0
- package/web/panels/code-viewer.js +5 -0
- package/web/panels/ctx-panel.js +4 -0
- package/web/panels/dep-graph.js +6 -0
- package/web/panels/file-tree.js +188 -0
- package/web/panels/health-panel.js +3 -0
- package/web/panels/live-monitor.js +3 -0
- package/web/state.js +17 -0
- package/web/style.css +157 -0
- package/references/symbiote-3x.md +0 -834
- package/src/ai-context.js +0 -113
- package/src/analysis-cache.js +0 -155
- package/src/cli-handlers.js +0 -271
- package/src/cli.js +0 -95
- package/src/compact.js +0 -207
- package/src/complexity.js +0 -237
- package/src/compress.js +0 -319
- package/src/ctx-to-jsdoc.js +0 -514
- package/src/custom-rules.js +0 -584
- package/src/db-analysis.js +0 -194
- package/src/dead-code.js +0 -468
- package/src/doc-dialect.js +0 -716
- package/src/filters.js +0 -227
- package/src/framework-references.js +0 -177
- package/src/full-analysis.js +0 -470
- package/src/graph-builder.js +0 -299
- package/src/instructions.js +0 -73
- package/src/jsdoc-checker.js +0 -351
- package/src/jsdoc-generator.js +0 -203
- package/src/lang-go.js +0 -285
- package/src/lang-python.js +0 -197
- package/src/lang-sql.js +0 -309
- package/src/lang-typescript.js +0 -190
- package/src/lang-utils.js +0 -124
- package/src/large-files.js +0 -163
- package/src/mcp-server.js +0 -675
- package/src/mode-config.js +0 -127
- package/src/outdated-patterns.js +0 -296
- package/src/parser.js +0 -662
- package/src/server.js +0 -28
- package/src/similar-functions.js +0 -279
- package/src/test-annotations.js +0 -323
- package/src/tool-defs.js +0 -793
- package/src/tools.js +0 -470
- package/src/type-checker.js +0 -188
- package/src/undocumented.js +0 -259
- package/src/workspace.js +0 -70
- /package/{AGENT_ROLE.md → docs/examples/AGENT_ROLE.md} +0 -0
- /package/{AGENT_ROLE_MINIMAL.md → docs/examples/AGENT_ROLE_MINIMAL.md} +0 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/*
|
|
2
|
+
--- file-tree.js ---
|
|
3
|
+
class FileTree extends e
|
|
4
|
+
.initCallback()
|
|
5
|
+
._toggleDir(dir)
|
|
6
|
+
._saveExpandedState()
|
|
7
|
+
._updateDirDOM(dir)
|
|
8
|
+
._collapseAll()
|
|
9
|
+
._highlightFile(e)
|
|
10
|
+
._renderTree(e)
|
|
11
|
+
._getFileIcon(e)
|
|
12
|
+
._applyFilter()
|
|
13
|
+
*/
|
|
14
|
+
import e from "@symbiotejs/symbiote";
|
|
15
|
+
|
|
16
|
+
import { api as t, state as n, events as s, emit as i } from "../app.js";
|
|
17
|
+
|
|
18
|
+
export class FileTree extends e {
|
|
19
|
+
init$={
|
|
20
|
+
treeHTML: '<div class="pg-placeholder">Loading files...</div>',
|
|
21
|
+
filterText: "",
|
|
22
|
+
onFilterInput: e => {
|
|
23
|
+
this.$.filterText = e.target.value.toLowerCase(), this._applyFilter();
|
|
24
|
+
},
|
|
25
|
+
onCollapseAll: () => {
|
|
26
|
+
this._collapseAll();
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
initCallback() {
|
|
30
|
+
this._expandedDirs = new Set;
|
|
31
|
+
try {
|
|
32
|
+
const saved = localStorage.getItem("pg-tree-expanded");
|
|
33
|
+
if (saved) {
|
|
34
|
+
const parsed = JSON.parse(saved);
|
|
35
|
+
Array.isArray(parsed) && (this._expandedDirs = new Set(parsed));
|
|
36
|
+
}
|
|
37
|
+
} catch (e) {}
|
|
38
|
+
s.addEventListener("skeleton-loaded", e => {
|
|
39
|
+
this._renderTree(e.detail), n.activeFile && requestAnimationFrame(() => this._highlightFile(n.activeFile));
|
|
40
|
+
}), n.skeleton && this._renderTree(n.skeleton), s.addEventListener("file-selected", e => {
|
|
41
|
+
e.detail.fromRoute && requestAnimationFrame(() => this._highlightFile(e.detail.path));
|
|
42
|
+
}), this.addEventListener("click", e => {
|
|
43
|
+
const fileEl = e.target.closest(".pg-tree-file");
|
|
44
|
+
if (fileEl) return this.querySelectorAll(".pg-tree-file.active").forEach(el => el.classList.remove("active")),
|
|
45
|
+
fileEl.classList.add("active"), n.activeFile = fileEl.dataset.file, void i("file-selected", {
|
|
46
|
+
path: fileEl.dataset.file
|
|
47
|
+
});
|
|
48
|
+
const dirEl = e.target.closest(".pg-tree-dir");
|
|
49
|
+
if (dirEl) {
|
|
50
|
+
const dir = dirEl.dataset.dir;
|
|
51
|
+
null != dir && this._toggleDir(dir);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
_toggleDir(dir) {
|
|
56
|
+
this._expandedDirs.has(dir) ? this._expandedDirs.delete(dir) : this._expandedDirs.add(dir),
|
|
57
|
+
this._saveExpandedState(), this._updateDirDOM(dir);
|
|
58
|
+
}
|
|
59
|
+
_saveExpandedState() {
|
|
60
|
+
localStorage.setItem("pg-tree-expanded", JSON.stringify(Array.from(this._expandedDirs)));
|
|
61
|
+
}
|
|
62
|
+
_updateDirDOM(dir) {
|
|
63
|
+
const dirEl = this.querySelector(`.pg-tree-dir[data-dir="${CSS.escape(dir)}"]`), childrenEl = this.querySelector(`.pg-tree-children[data-dir="${CSS.escape(dir)}"]`);
|
|
64
|
+
if (dirEl && childrenEl) {
|
|
65
|
+
const isExpanded = this._expandedDirs.has(dir), icon = dirEl.querySelector(".pg-chevron");
|
|
66
|
+
icon && (icon.textContent = isExpanded ? "expand_more" : "chevron_right"), isExpanded ? childrenEl.removeAttribute("hidden") : childrenEl.setAttribute("hidden", "");
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
_collapseAll() {
|
|
70
|
+
this._expandedDirs.clear(), this._saveExpandedState(), this.querySelectorAll(".pg-tree-dir").forEach(dirEl => {
|
|
71
|
+
this._updateDirDOM(dirEl.dataset.dir);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
_highlightFile(e) {
|
|
75
|
+
const fileEl = this.querySelector(`.pg-tree-file[data-file="${CSS.escape(e)}"]`);
|
|
76
|
+
if (fileEl) {
|
|
77
|
+
this.querySelectorAll(".pg-tree-file.active").forEach(el => el.classList.remove("active")),
|
|
78
|
+
fileEl.classList.add("active");
|
|
79
|
+
// Expand all ancestor dirs
|
|
80
|
+
const parts = e.split("/");
|
|
81
|
+
parts.pop(); // remove filename
|
|
82
|
+
let changed = !1;
|
|
83
|
+
for (let i = 1; i <= parts.length; i++) {
|
|
84
|
+
const dir = parts.slice(0, i).join("/");
|
|
85
|
+
this._expandedDirs.has(dir) || (this._expandedDirs.add(dir), this._updateDirDOM(dir), changed = !0);
|
|
86
|
+
}
|
|
87
|
+
changed && this._saveExpandedState();
|
|
88
|
+
fileEl.scrollIntoView({ block: "center", behavior: "smooth" });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
_renderTree(e) {
|
|
92
|
+
if (!e) return void (this.$.treeHTML = '<div class="pg-placeholder">No files found</div>');
|
|
93
|
+
const t = new Map, n = e.n || {};
|
|
94
|
+
for (const val of Object.values(n)) if (val.f) {
|
|
95
|
+
const item = t.get(val.f) || { exports: 0, classes: 0 };
|
|
96
|
+
item.classes++, t.set(val.f, item);
|
|
97
|
+
}
|
|
98
|
+
const s = e.X || {};
|
|
99
|
+
for (const [key, val] of Object.entries(s)) {
|
|
100
|
+
const item = t.get(key) || { exports: 0, classes: 0 };
|
|
101
|
+
item.exports = val.length, t.set(key, item);
|
|
102
|
+
}
|
|
103
|
+
const i = e.f || {};
|
|
104
|
+
for (const [key, val] of Object.entries(i)) for (const s of val) {
|
|
105
|
+
const p = "./" === key ? s : `${key}${s}`;
|
|
106
|
+
t.has(p) || t.set(p, { exports: 0, classes: 0 });
|
|
107
|
+
}
|
|
108
|
+
const o = e.a || {};
|
|
109
|
+
for (const [key, val] of Object.entries(o)) for (const s of val) {
|
|
110
|
+
const p = "./" === key ? s : `${key}${s}`;
|
|
111
|
+
t.has(p) || t.set(p, { exports: 0, classes: 0, nonSource: !0 });
|
|
112
|
+
}
|
|
113
|
+
if (0 === t.size) return void (this.$.treeHTML = '<div class="pg-placeholder">No files found</div>');
|
|
114
|
+
// Build nested tree structure
|
|
115
|
+
const root = { children: {}, files: [] };
|
|
116
|
+
for (const [filePath, meta] of t) {
|
|
117
|
+
const parts = filePath.split("/");
|
|
118
|
+
const fileName = parts.pop();
|
|
119
|
+
let node = root;
|
|
120
|
+
for (const part of parts) {
|
|
121
|
+
node.children[part] || (node.children[part] = { children: {}, files: [] });
|
|
122
|
+
node = node.children[part];
|
|
123
|
+
}
|
|
124
|
+
node.files.push({ f: filePath, name: fileName, ...meta });
|
|
125
|
+
}
|
|
126
|
+
// Render recursively
|
|
127
|
+
const renderNode = (node, dirPath, depth) => {
|
|
128
|
+
const l = [];
|
|
129
|
+
// Sort: dirs first, then files
|
|
130
|
+
const dirs = Object.keys(node.children).sort();
|
|
131
|
+
const files = node.files.sort((a, b) => a.name.localeCompare(b.name));
|
|
132
|
+
const pad = depth * 16;
|
|
133
|
+
for (const dirName of dirs) {
|
|
134
|
+
const childPath = dirPath ? `${dirPath}/${dirName}` : dirName;
|
|
135
|
+
const isExpanded = this._expandedDirs && this._expandedDirs.has(childPath);
|
|
136
|
+
const chevron = isExpanded ? "expand_more" : "chevron_right";
|
|
137
|
+
const hiddenAttr = isExpanded ? "" : " hidden";
|
|
138
|
+
l.push(`<div class="pg-tree-dir" data-dir="${childPath}" style="padding-left:${pad + 6}px"><span class="material-symbols-outlined pg-chevron" style="font-size:16px">${chevron}</span> <span class="material-symbols-outlined" style="font-size:16px">folder</span> ${dirName}</div>`);
|
|
139
|
+
l.push(`<div class="pg-tree-children" data-dir="${childPath}"${hiddenAttr}>`);
|
|
140
|
+
l.push(renderNode(node.children[dirName], childPath, depth + 1));
|
|
141
|
+
l.push("</div>");
|
|
142
|
+
}
|
|
143
|
+
for (const file of files) {
|
|
144
|
+
const icon = FileTree._getFileIcon(file.name), badges = [];
|
|
145
|
+
file.exports > 0 && badges.push(`${file.exports}f`);
|
|
146
|
+
file.classes > 0 && badges.push(`${file.classes}c`);
|
|
147
|
+
const badgeHtml = badges.length > 0 ? `<span class="pg-badge">${badges.join(" ")}</span>` : "";
|
|
148
|
+
const nonSourceClass = file.nonSource ? " pg-non-source" : "";
|
|
149
|
+
l.push(`<div class="pg-tree-file${nonSourceClass}" data-file="${file.f}" style="padding-left:${pad + 24}px"><span class="material-symbols-outlined" style="font-size:14px">${icon}</span> ${file.name}${badgeHtml}</div>`);
|
|
150
|
+
}
|
|
151
|
+
return l.join("");
|
|
152
|
+
};
|
|
153
|
+
this.$.treeHTML = renderNode(root, "", 0);
|
|
154
|
+
}
|
|
155
|
+
static _getFileIcon(e) {
|
|
156
|
+
return e.endsWith(".html") ? "html" : e.endsWith(".css") || e.endsWith(".css.js") ? "css" : e.endsWith(".tpl.js") ? "web" : e.endsWith(".json") ? "data_object" : e.endsWith(".md") ? "description" : e.endsWith(".svg") || e.endsWith(".png") || e.endsWith(".jpg") ? "image" : e.endsWith(".woff2") || e.endsWith(".ttf") ? "font_download" : "insert_drive_file";
|
|
157
|
+
}
|
|
158
|
+
_applyFilter() {
|
|
159
|
+
const e = this.$.filterText;
|
|
160
|
+
let changed = !1;
|
|
161
|
+
this.querySelectorAll(".pg-tree-file").forEach(t => {
|
|
162
|
+
const match = !e || t.dataset.file.toLowerCase().includes(e);
|
|
163
|
+
if (t.hidden = !match, e && match) {
|
|
164
|
+
// Expand all ancestor dirs
|
|
165
|
+
const parts = t.dataset.file.split("/");
|
|
166
|
+
parts.pop();
|
|
167
|
+
for (let i = 1; i <= parts.length; i++) {
|
|
168
|
+
const dir = parts.slice(0, i).join("/");
|
|
169
|
+
this._expandedDirs.has(dir) || (this._expandedDirs.add(dir), changed = !0, this._updateDirDOM(dir));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}), changed && this._saveExpandedState(), e ? this.querySelectorAll(".pg-tree-dir").forEach(dirEl => {
|
|
173
|
+
const dir = dirEl.dataset.dir;
|
|
174
|
+
const childrenEl = this.querySelector(`.pg-tree-children[data-dir="${CSS.escape(dir)}"]`);
|
|
175
|
+
if (!childrenEl) return;
|
|
176
|
+
let hasVisible = !1;
|
|
177
|
+
childrenEl.querySelectorAll(".pg-tree-file").forEach(f => { f.hidden || (hasVisible = !0); });
|
|
178
|
+
childrenEl.querySelectorAll(".pg-tree-children").forEach(c => { c.querySelector(".pg-tree-file:not([hidden])") && (hasVisible = !0); });
|
|
179
|
+
dirEl.hidden = !hasVisible;
|
|
180
|
+
}) : this.querySelectorAll(".pg-tree-dir").forEach(dirEl => {
|
|
181
|
+
dirEl.hidden = !1;
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
FileTree.template = '\n <div class="pg-panel-toolbar">\n <input type="search" placeholder="Filter files..." bind="oninput: onFilterInput">\n <button class="pg-collapse-all" bind="onclick: onCollapseAll" title="Collapse All Folders">\n <span class="material-symbols-outlined" style="font-size:14px">unfold_less</span>\n </button>\n </div>\n <div class="pg-tree-content" bind="innerHTML: treeHTML"></div>\n',
|
|
187
|
+
FileTree.rootStyles = "\n pg-file-tree {\n display: flex;\n flex-direction: column;\n height: 100%;\n overflow: hidden;\n font-size: 12px;\n font-family: var(--sn-font, Georgia, serif);\n }\n pg-file-tree .pg-panel-toolbar {\n padding: 6px 8px;\n border-bottom: 1px solid var(--sn-node-border, hsl(35, 18%, 80%));\n display: flex;\n gap: 6px;\n }\n pg-file-tree .pg-panel-toolbar input {\n flex: 1;\n background: var(--sn-bg, hsl(37, 30%, 91%));\n border: 1px solid var(--sn-node-border, hsl(35, 18%, 80%));\n color: var(--sn-text, hsl(30, 15%, 18%));\n padding: 4px 8px;\n border-radius: 4px;\n font-size: 11px;\n font-family: inherit;\n outline: none;\n min-width: 0;\n }\n pg-file-tree .pg-panel-toolbar input:focus {\n border-color: var(--sn-node-selected, hsl(210, 55%, 42%));\n }\n pg-file-tree .pg-collapse-all {\n background: var(--sn-bg, hsl(37, 30%, 91%));\n border: 1px solid var(--sn-node-border, hsl(35, 18%, 80%));\n color: var(--sn-text, hsl(30, 15%, 18%));\n border-radius: 4px;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 0 6px;\n transition: all 100ms ease;\n }\n pg-file-tree .pg-collapse-all:hover {\n background: var(--sn-node-hover, hsl(36, 22%, 88%));\n }\n pg-file-tree .pg-tree-content {\n flex: 1;\n overflow-y: auto;\n padding: 4px;\n }\n pg-file-tree .pg-tree-dir {\n display: flex;\n align-items: center;\n gap: 4px;\n padding: 3px 6px;\n color: var(--sn-text-dim, hsl(30, 10%, 45%));\n font-weight: 600;\n font-size: 11px;\n cursor: pointer;\n user-select: none;\n }\n pg-file-tree .pg-tree-dir:hover {\n background: var(--sn-node-hover, hsl(36, 22%, 88%));\n border-radius: 4px;\n }\n pg-file-tree .pg-tree-dir .pg-chevron {\n transition: transform 150ms ease;\n }\n pg-file-tree .pg-tree-children[hidden] {\n display: none;\n }\n pg-file-tree .pg-tree-file {\n display: flex;\n align-items: center;\n gap: 4px;\n padding: 3px 6px 3px 24px;\n cursor: pointer;\n border-radius: 4px;\n color: var(--sn-text-dim, hsl(30, 10%, 45%));\n transition: all 100ms ease;\n }\n pg-file-tree .pg-tree-file:hover {\n background: var(--sn-node-hover, hsl(36, 22%, 88%));\n color: var(--sn-text, hsl(30, 15%, 18%));\n }\n pg-file-tree .pg-tree-file.active {\n background: hsla(210, 45%, 45%, 0.12);\n color: var(--sn-cat-server, hsl(210, 45%, 45%));\n }\n pg-file-tree .pg-tree-file[hidden] {\n display: none;\n }\n pg-file-tree .pg-tree-file.pg-non-source {\n opacity: 0.6;\n }\n pg-file-tree .pg-badge {\n margin-left: auto;\n font-size: 10px;\n padding: 0 5px;\n border-radius: 8px;\n background: var(--sn-node-hover, hsl(36, 22%, 88%));\n color: var(--sn-text-dim, hsl(30, 10%, 45%));\n border: 1px solid var(--sn-node-border, hsl(35, 18%, 80%));\n }\n",
|
|
188
|
+
FileTree.reg("pg-file-tree");
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import e from"@symbiotejs/symbiote";import{api as t,state as n,events as s}from"../app.js";
|
|
2
|
+
export class HealthPanel extends e{init$={contentHTML:'<div class="pg-placeholder">Loading health analysis...</div>',loaded:!1};initCallback(){s.addEventListener("skeleton-loaded",()=>this._loadHealth()),setTimeout(()=>this._loadHealth(),500)}async _loadHealth(){if(!this.$.loaded){this.$.contentHTML='<div class="pg-placeholder pg-pulse">Analyzing project health...</div>';try{const e=await t("/api/analysis-summary");this.$.loaded=!0;
|
|
3
|
+
const s=e.healthScore??e.score??"?",a=s>=80?"good":s>=50?"warning":"critical",i=e.grade||(s>=80?"healthy":s>=50?"needs work":"critical"),l=n.skeleton?.s||{},o=l.files||Object.keys(n.skeleton?.X||{}).length||"—",r=l.functions||0,c=l.classes||0,p=Object.values(n.skeleton?.X||{}).reduce((e,t)=>e+t.length,0);this.$.contentHTML=`\n <div class="pg-health-grid">\n <div class="pg-health-card pg-health-score-card">\n <div class="pg-health-score ${a}">${s}</div>\n <div class="pg-health-score-label">Health Score · ${i}</div>\n </div>\n <div class="pg-health-card">\n <div class="pg-health-card-title">\n <span class="material-symbols-outlined" style="font-size:16px">code</span>\n Code\n </div>\n ${this._metric("Source files",o)}\n ${this._metric("Functions",r)}\n ${this._metric("Classes",c)}\n ${this._metric("Exports",p)}\n </div>\n <div class="pg-health-card">\n <div class="pg-health-card-title">\n <span class="material-symbols-outlined" style="font-size:16px">bug_report</span>\n Issues\n </div>\n ${this._metric("Complexity",e.complexity||0,e.complexity>200)}\n ${this._metric("JSDoc issues",e.jsdocIssues||0,e.jsdocIssues>10)}\n ${this._metric("Undocumented",e.undocumented||0,e.undocumented>5)}\n </div>\n <div class="pg-health-card">\n <div class="pg-health-card-title">\n <span class="material-symbols-outlined" style="font-size:16px">speed</span>\n Cache Performance\n </div>\n ${this._metric("Cache hits",e.cache?.hits??"—")}\n ${this._metric("Cache misses",e.cache?.misses??"—")}\n ${this._metric("Hit rate",e.cache?Math.round(e.cache.hits/(e.cache.hits+e.cache.misses)*100)+"%":"—")}\n </div>\n </div>\n ${e.note?`<div class="pg-health-note"><span class="material-symbols-outlined" style="font-size:14px">info</span> ${e.note}</div>`:""}\n `}catch(e){this.$.contentHTML=`<div class="pg-placeholder" style="color:var(--sn-danger-color)">Error: ${e.message}</div>`}}}_metric(e,t,n=!1){return`<div class="pg-metric${n?" pg-metric-warn":""}"><span>${e}</span><span class="pg-metric-val">${t}</span></div>`}}HealthPanel.template='<div bind="innerHTML: contentHTML"></div>',HealthPanel.rootStyles="\n pg-health-panel { display:block; height:100%; overflow-y:auto; padding:16px; font-family:var(--sn-font, Georgia, serif); }\n .pg-health-grid { display:grid; grid-template-columns:repeat(auto-fit, minmax(200px,1fr)); gap:12px; align-content:start; }\n .pg-health-card {\n background: var(--sn-node-bg);\n border: 1px solid var(--sn-node-border);\n border-radius: 8px;\n padding: 14px;\n }\n .pg-health-score-card { text-align:center; grid-column:1/-1; padding:20px; }\n .pg-health-score { font-size:56px; font-weight:800; font-family:monospace; }\n .pg-health-score.good { color: var(--sn-success-color, hsl(150, 55%, 38%)); }\n .pg-health-score.warning { color: var(--sn-warning-color, hsl(38, 55%, 42%)); }\n .pg-health-score.critical { color: var(--sn-danger-color, hsl(4, 55%, 48%)); }\n .pg-health-score-label { font-size:11px; text-transform:uppercase; letter-spacing:1px; color:var(--sn-text-dim); margin-top:4px; }\n .pg-health-card-title {\n display: flex; align-items: center; gap: 6px;\n font-size:11px; font-weight:600; text-transform:uppercase; letter-spacing:0.5px;\n color:var(--sn-text-dim); margin-bottom:8px;\n }\n .pg-metric { display:flex; justify-content:space-between; padding:5px 0; border-bottom:1px solid var(--sn-node-hover); font-size:12px; color:var(--sn-text); }\n .pg-metric:last-child { border:none; }\n .pg-metric-val { font-weight:600; font-family:monospace; }\n .pg-metric-warn .pg-metric-val { color:var(--sn-warning-color); }\n .pg-health-note {\n display: flex; align-items: center; gap: 6px;\n margin-top: 12px; padding: 10px 12px;\n font-size: 11px; color: var(--sn-text-dim);\n background: var(--sn-node-bg);\n border: 1px solid var(--sn-node-border);\n border-radius: 6px;\n }\n .pg-placeholder { color:var(--sn-text-dim); text-align:center; padding:40px; font-style:italic; font-size:13px; }\n .pg-pulse { animation:pulse 1.5s ease infinite; }\n @keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.4} }\n",HealthPanel.reg("pg-health-panel");
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import n from"@symbiotejs/symbiote";import{events as o}from"../app.js";
|
|
2
|
+
export class LiveMonitor extends n{init$={eventsHTML:'<div class="pg-placeholder">Waiting for tool calls...</div>',eventCount:"0"};_events=[];initCallback(){o.addEventListener("tool-event",n=>this._addEvent(n.detail))}_addEvent(n){this._events.unshift(n),this._events.length>200&&this._events.pop(),this.$.eventCount=String(this._events.length);
|
|
3
|
+
const o=this._events.slice(0,100).map(n=>{if("tool_call"===n.type){const o=JSON.stringify(n.args||{}).slice(0,80);return`<div class="pg-mon-event pg-mon-call">\n <span class="pg-mon-arrow">→</span>\n <span class="pg-mon-tool">${n.tool}</span>\n <span class="pg-mon-args">${this._esc(o)}</span>\n <span class="pg-mon-time">${this._formatTime(n.ts)}</span>\n </div>`}return`<div class="pg-mon-event pg-mon-result ${n.success?"pg-mon-ok":"pg-mon-err"}">\n <span class="pg-mon-arrow">←</span>\n <span class="pg-mon-tool">${n.tool}</span>\n <span class="pg-mon-duration">${n.duration_ms}ms</span>\n <span class="pg-mon-time">${this._formatTime(n.ts)}</span>\n </div>`}).join("");this.$.eventsHTML=o||'<div class="pg-placeholder">Waiting for tool calls...</div>'}_esc(n){return n.replace(/</g,"<").replace(/>/g,">")}_formatTime(n){return n?new Date(n).toLocaleTimeString("en",{hour12:!1,hour:"2-digit",minute:"2-digit",second:"2-digit"}):""}}LiveMonitor.template='\n <div class="pg-mon-header">\n <span>Events: </span><span bind="textContent: eventCount"></span>\n </div>\n <div class="pg-mon-body" bind="innerHTML: eventsHTML"></div>\n',LiveMonitor.rootStyles="\n pg-live-monitor { display:flex; flex-direction:column; height:100%; overflow:hidden; font-size:12px; font-family:var(--sn-font, Georgia, serif); }\n .pg-mon-header { padding:6px 12px; border-bottom:1px solid var(--sn-node-border); background:var(--sn-node-header-bg); font-size:11px; color:var(--sn-text-dim); }\n .pg-mon-body { flex:1; overflow-y:auto; padding:4px; }\n .pg-mon-event {\n display:flex; align-items:center; gap:8px;\n padding:4px 8px; border-radius:4px; font-family:monospace; font-size:11px;\n animation: slideIn 0.15s ease;\n }\n .pg-mon-event:hover { background:var(--sn-node-hover); }\n .pg-mon-arrow { font-weight:bold; width:14px; }\n .pg-mon-call .pg-mon-arrow { color: var(--sn-cat-server, hsl(210, 45%, 45%)); }\n .pg-mon-ok .pg-mon-arrow { color: var(--sn-success-color, hsl(150, 55%, 38%)); }\n .pg-mon-err .pg-mon-arrow { color: var(--sn-danger-color, hsl(4, 55%, 48%)); }\n .pg-mon-tool { color:var(--sn-text); font-weight:600; min-width:100px; }\n .pg-mon-args { color:var(--sn-text-dim); flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }\n .pg-mon-duration { color: hsl(250, 35%, 50%); min-width:50px; text-align:right; }\n .pg-mon-time { color:var(--sn-text-dim); font-size:10px; min-width:60px; text-align:right; }\n .pg-placeholder { color:var(--sn-text-dim); text-align:center; padding:30px; font-style:italic; }\n @keyframes slideIn { from{opacity:0;transform:translateY(-4px)} to{opacity:1;transform:translateY(0)} }\n",LiveMonitor.reg("pg-live-monitor");
|
package/web/state.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// @ctx .context/web/state.ctx
|
|
2
|
+
const e=new URL(".",import.meta.url).href;
|
|
3
|
+
export const state={project:null,skeleton:null,events:[],connected:!1};
|
|
4
|
+
const t=new Map;
|
|
5
|
+
export function subscribe(e,n){return t.has(e)||t.set(e,new Set),t.get(e).add(n),()=>t.get(e)?.delete(n)}const n=new Set;
|
|
6
|
+
export function onEvent(e){return n.add(e),()=>n.delete(e)}
|
|
7
|
+
function o(e,n){t.get(e)?.forEach(t=>t(n,e));
|
|
8
|
+
const r=e.indexOf(".");if(r>0){const n=e.slice(0,r);t.get(n)?.forEach(e=>e(state[n],n))}t.get("*")?.forEach(t=>t(n,e))}let r=1;
|
|
9
|
+
const c=new Map;
|
|
10
|
+
export function call(e,t={}){return new Promise((n,a)=>{if(!s||s.readyState!==WebSocket.OPEN)return void a(new Error("WebSocket not connected"));
|
|
11
|
+
const d=r++;c.set(d,{resolve:n,reject:a}),s.send(JSON.stringify({jsonrpc:"2.0",id:d,method:"tool",params:{name:e,args:t}})),setTimeout(()=>{c.has(d)&&(c.delete(d),a(new Error(`Tool call timeout: ${e}`)))},3e4)})}let s=null,a=null;function l(e){Object.assign(state,e),state.connected=!0,o("*",state);for(const t of Object.keys(e))o(t,e[t])}
|
|
12
|
+
function i(e,t){const n=e.split(".");
|
|
13
|
+
let r=state;for(let e=0;e<n.length-1;e++)r[n[e]]||(r[n[e]]={}),r=r[n[e]];r[n[n.length-1]]=t,o(e,t)}
|
|
14
|
+
function u(e){let t;try{t=JSON.parse(e)}catch{return}if(t.id&&(void 0!==t.result||t.error)){const e=c.get(t.id);return void(e&&(c.delete(t.id),t.error?e.reject(new Error(t.error.message||"Tool error")):e.resolve(t.result)))}"snapshot"!==t.method?"patch"!==t.method?"event"!==t.method?t.type&&n.forEach(e=>e(t)):n.forEach(e=>e(t.params)):i(t.params.path,t.params.value):l(t.params.state)}
|
|
15
|
+
export function connect(){if(s)return;
|
|
16
|
+
const t=e.replace(/^http/,"ws");s=new WebSocket(`${t}ws/monitor`),s.onopen=()=>{state.connected=!0,o("connected",!0),a&&(clearTimeout(a),a=null)},s.onmessage=e=>u(e.data),s.onclose=()=>{state.connected=!1,s=null,o("connected",!1);for(const[e,{reject:t}]of c)t(new Error("WebSocket disconnected"));c.clear(),a=setTimeout(connect,3e3)},s.onerror=()=>{}}
|
|
17
|
+
export function disconnect(){a&&(clearTimeout(a),a=null),s&&(s.close(),s=null)}
|
package/web/style.css
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/* Project Graph Web UI — Carbon Theme Styles */
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Design tokens come from symbiote-node CARBON theme via applyTheme().
|
|
5
|
+
* Layout tokens (--bg-*, --text-*) are bridged from --sn-* tokens automatically.
|
|
6
|
+
* Layout structure mirrors admin-panel: topbar + sidebar + content.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
*, *::before, *::after {
|
|
10
|
+
box-sizing: border-box;
|
|
11
|
+
margin: 0;
|
|
12
|
+
padding: 0;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
html, body {
|
|
16
|
+
height: 100%;
|
|
17
|
+
background: var(--sn-bg, #1a1a1a);
|
|
18
|
+
color: var(--sn-text, #f0f0f0);
|
|
19
|
+
font-family: var(--sn-font, 'Inter', -apple-system, sans-serif);
|
|
20
|
+
font-size: 13px;
|
|
21
|
+
line-height: 1.5;
|
|
22
|
+
overflow: hidden;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/* === App shell — full viewport flex column === */
|
|
26
|
+
.app-shell {
|
|
27
|
+
display: flex;
|
|
28
|
+
flex-direction: column;
|
|
29
|
+
width: 100%;
|
|
30
|
+
height: 100%;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/* === Topbar === */
|
|
34
|
+
.app-topbar {
|
|
35
|
+
display: flex;
|
|
36
|
+
align-items: center;
|
|
37
|
+
justify-content: space-between;
|
|
38
|
+
padding: 0 16px;
|
|
39
|
+
height: 40px;
|
|
40
|
+
background: var(--sn-node-bg, #222222);
|
|
41
|
+
border-bottom: 1px solid var(--sn-node-border, rgba(255, 255, 255, 0.1));
|
|
42
|
+
flex-shrink: 0;
|
|
43
|
+
z-index: 100;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.topbar-left {
|
|
47
|
+
display: flex;
|
|
48
|
+
align-items: center;
|
|
49
|
+
gap: 8px;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.app-title {
|
|
53
|
+
font-size: 13px;
|
|
54
|
+
font-weight: 700;
|
|
55
|
+
color: var(--sn-text, #f0f0f0);
|
|
56
|
+
letter-spacing: 0.5px;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.topbar-right {
|
|
60
|
+
display: flex;
|
|
61
|
+
align-items: center;
|
|
62
|
+
gap: 12px;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
.project-name {
|
|
68
|
+
font-size: 13px;
|
|
69
|
+
font-weight: 600;
|
|
70
|
+
color: var(--project-accent, var(--sn-text, #f0f0f0));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.project-files {
|
|
74
|
+
font-size: 11px;
|
|
75
|
+
color: var(--sn-text-dim, #999999);
|
|
76
|
+
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.compression-stats {
|
|
80
|
+
font-size: 10px;
|
|
81
|
+
font-weight: 500;
|
|
82
|
+
color: #64b5f6;
|
|
83
|
+
padding: 2px 8px;
|
|
84
|
+
border-radius: 10px;
|
|
85
|
+
background: rgba(100, 181, 246, 0.1);
|
|
86
|
+
border: 1px solid rgba(100, 181, 246, 0.15);
|
|
87
|
+
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.topbar-sep {
|
|
91
|
+
color: var(--sn-text-dim, #555);
|
|
92
|
+
font-size: 14px;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/* Agent badge */
|
|
96
|
+
.agent-badge {
|
|
97
|
+
font-size: 11px;
|
|
98
|
+
font-weight: 500;
|
|
99
|
+
color: #4caf50;
|
|
100
|
+
padding: 2px 8px;
|
|
101
|
+
border-radius: 10px;
|
|
102
|
+
background: rgba(76, 175, 80, 0.1);
|
|
103
|
+
border: 1px solid rgba(76, 175, 80, 0.2);
|
|
104
|
+
animation: pulse-glow 2s ease-in-out infinite;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
@keyframes pulse-glow {
|
|
108
|
+
0%, 100% { box-shadow: 0 0 4px rgba(76, 175, 80, 0.1); }
|
|
109
|
+
50% { box-shadow: 0 0 8px rgba(76, 175, 80, 0.3); }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/* Status indicator */
|
|
113
|
+
.status {
|
|
114
|
+
width: 8px;
|
|
115
|
+
height: 8px;
|
|
116
|
+
border-radius: 50%;
|
|
117
|
+
background: var(--sn-text-dim, #999);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.status.connected {
|
|
121
|
+
background: #4caf50;
|
|
122
|
+
box-shadow: 0 0 6px rgba(76, 175, 80, 0.5);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.status.disconnected {
|
|
126
|
+
background: #f44336;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/* === Workspace: sidebar + content area === */
|
|
130
|
+
.app-workspace {
|
|
131
|
+
flex: 1;
|
|
132
|
+
overflow: hidden;
|
|
133
|
+
position: relative;
|
|
134
|
+
display: flex;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.app-content {
|
|
138
|
+
flex: 1;
|
|
139
|
+
overflow: hidden;
|
|
140
|
+
position: relative;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.app-content > panel-layout {
|
|
144
|
+
width: 100%;
|
|
145
|
+
height: 100%;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/* === Scrollbar — carbon grey === */
|
|
149
|
+
::-webkit-scrollbar { width: 8px; height: 8px; }
|
|
150
|
+
::-webkit-scrollbar-track { background: transparent; }
|
|
151
|
+
::-webkit-scrollbar-thumb {
|
|
152
|
+
background: rgba(255, 255, 255, 0.1);
|
|
153
|
+
border-radius: 4px;
|
|
154
|
+
}
|
|
155
|
+
::-webkit-scrollbar-thumb:hover {
|
|
156
|
+
background: rgba(255, 255, 255, 0.2);
|
|
157
|
+
}
|