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.
Files changed (125) hide show
  1. package/README.md +171 -31
  2. package/docs/img/explorer-compact.jpg +0 -0
  3. package/docs/img/explorer-expanded.jpg +0 -0
  4. package/package.json +12 -8
  5. package/src/.project-graph-cache.json +1 -1
  6. package/src/analysis/analysis-cache.js +7 -0
  7. package/src/analysis/complexity.js +14 -0
  8. package/src/analysis/custom-rules.js +36 -0
  9. package/src/analysis/db-analysis.js +9 -0
  10. package/src/analysis/dead-code.js +19 -0
  11. package/src/analysis/full-analysis.js +18 -0
  12. package/src/analysis/jsdoc-checker.js +24 -0
  13. package/src/analysis/jsdoc-generator.js +10 -0
  14. package/src/analysis/large-files.js +11 -0
  15. package/src/analysis/outdated-patterns.js +12 -0
  16. package/src/analysis/similar-functions.js +16 -0
  17. package/src/analysis/test-annotations.js +21 -0
  18. package/src/analysis/type-checker.js +8 -0
  19. package/src/analysis/undocumented.js +14 -0
  20. package/src/cli/cli-handlers.js +4 -0
  21. package/src/cli/cli.js +5 -0
  22. package/src/compact/.project-graph-cache.json +1 -0
  23. package/src/compact/ai-context.js +7 -0
  24. package/src/compact/compact-migrate.js +17 -0
  25. package/src/compact/compact.js +18 -0
  26. package/src/compact/compress.js +14 -0
  27. package/src/compact/ctx-to-jsdoc.js +29 -0
  28. package/src/compact/doc-dialect.js +30 -0
  29. package/src/compact/expand.js +37 -0
  30. package/src/compact/framework-references.js +5 -0
  31. package/src/compact/instructions.js +3 -0
  32. package/src/compact/mode-config.js +8 -0
  33. package/src/compact/validate-pipeline.js +9 -0
  34. package/src/core/event-bus.js +9 -0
  35. package/src/core/filters.js +14 -0
  36. package/src/core/graph-builder.js +12 -0
  37. package/src/core/parser.js +31 -0
  38. package/src/core/workspace.js +8 -0
  39. package/src/lang/lang-go.js +17 -0
  40. package/src/lang/lang-python.js +12 -0
  41. package/src/lang/lang-sql.js +23 -0
  42. package/src/lang/lang-typescript.js +9 -0
  43. package/src/lang/lang-utils.js +4 -0
  44. package/src/mcp/mcp-server.js +17 -0
  45. package/src/mcp/tool-defs.js +3 -0
  46. package/src/mcp/tools.js +25 -0
  47. package/src/network/backend-lifecycle.js +19 -0
  48. package/src/network/backend.js +5 -0
  49. package/src/network/local-gateway.js +23 -0
  50. package/src/network/mdns.js +13 -0
  51. package/src/network/server.js +10 -0
  52. package/src/network/web-server.js +34 -0
  53. package/web/.project-graph-cache.json +1 -0
  54. package/web/app.js +17 -0
  55. package/web/components/code-block.js +3 -0
  56. package/web/components/quick-open.js +5 -0
  57. package/web/dashboard-state.js +3 -0
  58. package/web/dashboard.html +27 -0
  59. package/web/dashboard.js +8 -0
  60. package/web/highlight.js +13 -0
  61. package/web/index.html +35 -0
  62. package/web/panels/ActionBoard/ActionBoard.css.js +1 -0
  63. package/web/panels/ActionBoard/ActionBoard.js +4 -0
  64. package/web/panels/ActionBoard/ActionBoard.tpl.js +1 -0
  65. package/web/panels/EventItem/EventItem.css.js +1 -0
  66. package/web/panels/EventItem/EventItem.js +4 -0
  67. package/web/panels/EventItem/EventItem.tpl.js +1 -0
  68. package/web/panels/ProjectItem/ProjectItem.css.js +1 -0
  69. package/web/panels/ProjectItem/ProjectItem.js +5 -0
  70. package/web/panels/ProjectItem/ProjectItem.tpl.js +1 -0
  71. package/web/panels/ProjectList/ProjectList.css.js +1 -0
  72. package/web/panels/ProjectList/ProjectList.js +4 -0
  73. package/web/panels/ProjectList/ProjectList.tpl.js +1 -0
  74. package/web/panels/SettingsPanel/.project-graph-cache.json +1 -0
  75. package/web/panels/SettingsPanel/SettingsPanel.css.js +1 -0
  76. package/web/panels/SettingsPanel/SettingsPanel.js +7 -0
  77. package/web/panels/SettingsPanel/SettingsPanel.tpl.js +1 -0
  78. package/web/panels/code-viewer.js +5 -0
  79. package/web/panels/ctx-panel.js +4 -0
  80. package/web/panels/dep-graph.js +6 -0
  81. package/web/panels/file-tree.js +188 -0
  82. package/web/panels/health-panel.js +3 -0
  83. package/web/panels/live-monitor.js +3 -0
  84. package/web/state.js +17 -0
  85. package/web/style.css +157 -0
  86. package/references/symbiote-3x.md +0 -834
  87. package/src/ai-context.js +0 -113
  88. package/src/analysis-cache.js +0 -155
  89. package/src/cli-handlers.js +0 -271
  90. package/src/cli.js +0 -95
  91. package/src/compact.js +0 -207
  92. package/src/complexity.js +0 -237
  93. package/src/compress.js +0 -319
  94. package/src/ctx-to-jsdoc.js +0 -514
  95. package/src/custom-rules.js +0 -584
  96. package/src/db-analysis.js +0 -194
  97. package/src/dead-code.js +0 -468
  98. package/src/doc-dialect.js +0 -716
  99. package/src/filters.js +0 -227
  100. package/src/framework-references.js +0 -177
  101. package/src/full-analysis.js +0 -470
  102. package/src/graph-builder.js +0 -299
  103. package/src/instructions.js +0 -73
  104. package/src/jsdoc-checker.js +0 -351
  105. package/src/jsdoc-generator.js +0 -203
  106. package/src/lang-go.js +0 -285
  107. package/src/lang-python.js +0 -197
  108. package/src/lang-sql.js +0 -309
  109. package/src/lang-typescript.js +0 -190
  110. package/src/lang-utils.js +0 -124
  111. package/src/large-files.js +0 -163
  112. package/src/mcp-server.js +0 -675
  113. package/src/mode-config.js +0 -127
  114. package/src/outdated-patterns.js +0 -296
  115. package/src/parser.js +0 -662
  116. package/src/server.js +0 -28
  117. package/src/similar-functions.js +0 -279
  118. package/src/test-annotations.js +0 -323
  119. package/src/tool-defs.js +0 -793
  120. package/src/tools.js +0 -470
  121. package/src/type-checker.js +0 -188
  122. package/src/undocumented.js +0 -259
  123. package/src/workspace.js +0 -70
  124. /package/{AGENT_ROLE.md → docs/examples/AGENT_ROLE.md} +0 -0
  125. /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,"&lt;").replace(/>/g,"&gt;")}_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
+ }