project-graph-mcp 1.3.0 → 2.0.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 (113) hide show
  1. package/README.md +223 -17
  2. package/{AGENT_ROLE.md → docs/examples/AGENT_ROLE.md} +87 -30
  3. package/{AGENT_ROLE_MINIMAL.md → docs/examples/AGENT_ROLE_MINIMAL.md} +23 -8
  4. package/package.json +12 -8
  5. package/src/.project-graph-cache.json +1 -0
  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/ai-context.js +7 -0
  23. package/src/compact/compact.js +18 -0
  24. package/src/compact/compress.js +13 -0
  25. package/src/compact/ctx-to-jsdoc.js +29 -0
  26. package/src/compact/doc-dialect.js +30 -0
  27. package/src/compact/expand.js +37 -0
  28. package/src/compact/framework-references.js +5 -0
  29. package/src/compact/instructions.js +3 -0
  30. package/src/compact/mode-config.js +8 -0
  31. package/src/compact/validate-pipeline.js +9 -0
  32. package/src/core/event-bus.js +9 -0
  33. package/src/core/filters.js +14 -0
  34. package/src/core/graph-builder.js +12 -0
  35. package/src/core/parser.js +31 -0
  36. package/src/core/workspace.js +8 -0
  37. package/src/lang/lang-go.js +17 -0
  38. package/src/lang/lang-python.js +12 -0
  39. package/src/lang/lang-sql.js +23 -0
  40. package/src/lang/lang-typescript.js +9 -0
  41. package/src/lang/lang-utils.js +4 -0
  42. package/src/mcp/mcp-server.js +17 -0
  43. package/src/mcp/tool-defs.js +3 -0
  44. package/src/mcp/tools.js +25 -0
  45. package/src/network/backend-lifecycle.js +19 -0
  46. package/src/network/backend.js +5 -0
  47. package/src/network/local-gateway.js +23 -0
  48. package/src/network/mdns.js +13 -0
  49. package/src/network/server.js +10 -0
  50. package/src/network/web-server.js +34 -0
  51. package/vendor/terser.mjs +49 -0
  52. package/web/.project-graph-cache.json +1 -0
  53. package/web/app.js +16 -0
  54. package/web/components/code-block.js +3 -0
  55. package/web/components/quick-open.js +5 -0
  56. package/web/dashboard-state.js +3 -0
  57. package/web/dashboard.html +27 -0
  58. package/web/dashboard.js +8 -0
  59. package/web/highlight.js +13 -0
  60. package/web/index.html +35 -0
  61. package/web/panels/ActionBoard/ActionBoard.css.js +1 -0
  62. package/web/panels/ActionBoard/ActionBoard.js +4 -0
  63. package/web/panels/ActionBoard/ActionBoard.tpl.js +1 -0
  64. package/web/panels/EventItem/EventItem.css.js +1 -0
  65. package/web/panels/EventItem/EventItem.js +4 -0
  66. package/web/panels/EventItem/EventItem.tpl.js +1 -0
  67. package/web/panels/ProjectItem/ProjectItem.css.js +1 -0
  68. package/web/panels/ProjectItem/ProjectItem.js +5 -0
  69. package/web/panels/ProjectItem/ProjectItem.tpl.js +1 -0
  70. package/web/panels/ProjectList/ProjectList.css.js +1 -0
  71. package/web/panels/ProjectList/ProjectList.js +4 -0
  72. package/web/panels/ProjectList/ProjectList.tpl.js +1 -0
  73. package/web/panels/SettingsPanel/.project-graph-cache.json +1 -0
  74. package/web/panels/SettingsPanel/SettingsPanel.css.js +1 -0
  75. package/web/panels/SettingsPanel/SettingsPanel.js +7 -0
  76. package/web/panels/SettingsPanel/SettingsPanel.tpl.js +1 -0
  77. package/web/panels/code-viewer.js +5 -0
  78. package/web/panels/ctx-panel.js +4 -0
  79. package/web/panels/dep-graph.js +6 -0
  80. package/web/panels/file-tree.js +188 -0
  81. package/web/panels/health-panel.js +3 -0
  82. package/web/panels/live-monitor.js +3 -0
  83. package/web/state.js +17 -0
  84. package/web/style.css +157 -0
  85. package/references/symbiote-3x.md +0 -834
  86. package/src/cli-handlers.js +0 -140
  87. package/src/cli.js +0 -83
  88. package/src/complexity.js +0 -223
  89. package/src/custom-rules.js +0 -583
  90. package/src/db-analysis.js +0 -194
  91. package/src/dead-code.js +0 -468
  92. package/src/filters.js +0 -227
  93. package/src/framework-references.js +0 -177
  94. package/src/full-analysis.js +0 -174
  95. package/src/graph-builder.js +0 -299
  96. package/src/instructions.js +0 -175
  97. package/src/jsdoc-generator.js +0 -214
  98. package/src/lang-go.js +0 -285
  99. package/src/lang-python.js +0 -197
  100. package/src/lang-sql.js +0 -309
  101. package/src/lang-typescript.js +0 -190
  102. package/src/lang-utils.js +0 -124
  103. package/src/large-files.js +0 -162
  104. package/src/mcp-server.js +0 -468
  105. package/src/outdated-patterns.js +0 -295
  106. package/src/parser.js +0 -452
  107. package/src/server.js +0 -28
  108. package/src/similar-functions.js +0 -278
  109. package/src/test-annotations.js +0 -301
  110. package/src/tool-defs.js +0 -525
  111. package/src/tools.js +0 -470
  112. package/src/undocumented.js +0 -260
  113. package/src/workspace.js +0 -70
@@ -0,0 +1 @@
1
+ {"version":1,"path":"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/web","mtimes":{"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/web/app.js":1775830354553.7673,"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/web/components/code-block.js":1775768594540.4138,"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/web/dashboard-state.js":1775841800058.7944,"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/web/dashboard.js":1775841811371.568,"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/web/highlight.js":1775763193218.1758,"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/web/panels/ActionBoard/ActionBoard.js":1775842060331.7385,"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/web/panels/EventItem/EventItem.js":1775842071481.2783,"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/web/panels/ProjectItem/ProjectItem.js":1775842069353.9573,"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/web/panels/ProjectList/ProjectList.js":1775842139530.3425,"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/web/panels/code-viewer.js":1775769030911.7693,"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/web/panels/ctx-panel.js":1775755674646.3315,"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/web/panels/dep-graph.js":1775755687233.0095,"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/web/panels/file-tree.js":1775784918639.9016,"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/web/panels/health-panel.js":1775755699133.5889,"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/web/panels/live-monitor.js":1775755711324.3772},"graph":{"v":1,"legend":{"CodeBlock":"CB","ActionBoard":"AB","EventItem":"EI","ProjectItem":"PI","ProjectList":"PL","CodeViewer":"CV","CtxPanel":"CP","DepGraph":"DG","FileTree":"FT","HealthPanel":"HP","LiveMonitor":"LM","api":"ap","emit":"em","initWebSocket":"WS","applyRouteLayout":"RL","initLayout":"iL","init":"in","updateAgentBadge":"AB1","fetchGatewayInfo":"GI","initWebSockets":"WS1","esc":"es","highlight":"hi","escRange":"eR","isDigit":"iD","isHexDigit":"HD","isIdentStart":"IS","isIdentPart":"IP","renderCallback":"rC","initCallback":"iC","_getCodeBlock":"_CB","_loadFile":"_F","_loadCtx":"_C","_formatCtx":"_C1","_renderOverview":"_O","_loadDeps":"_D","_highlightFile":"_F1","_renderTree":"_T","_applyFilter":"_F2","_loadHealth":"_H","_metric":"_m","_addEvent":"_E","_esc":"_e","_formatTime":"_T1"},"reverseLegend":{"CB":"CodeBlock","AB":"ActionBoard","EI":"EventItem","PI":"ProjectItem","PL":"ProjectList","CV":"CodeViewer","CP":"CtxPanel","DG":"DepGraph","FT":"FileTree","HP":"HealthPanel","LM":"LiveMonitor","ap":"api","em":"emit","WS":"initWebSocket","RL":"applyRouteLayout","iL":"initLayout","in":"init","AB1":"updateAgentBadge","GI":"fetchGatewayInfo","WS1":"initWebSockets","es":"esc","hi":"highlight","eR":"escRange","iD":"isDigit","HD":"isHexDigit","IS":"isIdentStart","IP":"isIdentPart","rC":"renderCallback","iC":"initCallback","_CB":"_getCodeBlock","_F":"_loadFile","_C":"_loadCtx","_C1":"_formatCtx","_O":"_renderOverview","_D":"_loadDeps","_F1":"_highlightFile","_T":"_renderTree","_F2":"_applyFilter","_H":"_loadHealth","_m":"_metric","_E":"_addEvent","_e":"_esc","_T1":"_formatTime"},"stats":{"files":15,"classes":11,"functions":18,"tables":0},"nodes":{"CB":{"t":"C","x":"Symbiote","m":["rC"],"$":["code","highlighted"],"f":"components/code-block.js"},"AB":{"t":"C","x":"Symbiote","m":["iC"],"$":["eventsItems"],"f":"panels/ActionBoard/ActionBoard.js"},"EI":{"t":"C","x":"Symbiote","m":[],"$":["ts","type","tool"],"f":"panels/EventItem/EventItem.js"},"PI":{"t":"C","x":"Symbiote","m":[],"$":["prefix","projectName","projectPath"],"f":"panels/ProjectItem/ProjectItem.js"},"PL":{"t":"C","x":"Symbiote","m":["iC"],"$":["projects"],"f":"panels/ProjectList/ProjectList.js"},"CV":{"t":"C","x":"Symbiote","m":["iC","rC","_CB","_F"],"$":["filename","hasFile"],"f":"panels/code-viewer.js"},"CP":{"t":"C","x":"Symbiote","m":["iC","_C","_C1"],"$":["contentHTML"],"f":"panels/ctx-panel.js"},"DG":{"t":"C","x":"Symbiote","m":["iC","_O","_D"],"$":["contentHTML"],"f":"panels/dep-graph.js"},"FT":{"t":"C","x":"Symbiote","m":["iC","_F1","_T","_F2"],"$":["treeHTML","filterText","onFilterInput"],"f":"panels/file-tree.js"},"HP":{"t":"C","x":"Symbiote","m":["iC","_H","_m"],"$":["contentHTML","loaded"],"f":"panels/health-panel.js"},"LM":{"t":"C","x":"Symbiote","m":["iC","_E","_e","_T1"],"$":["eventsHTML","eventCount"],"f":"panels/live-monitor.js"},"ap":{"t":"F","e":true,"f":"app.js"},"em":{"t":"F","e":true,"f":"dashboard-state.js"},"WS":{"t":"F","e":false,"f":"app.js"},"RL":{"t":"F","e":false,"f":"app.js"},"iL":{"t":"F","e":false,"f":"app.js"},"in":{"t":"F","e":false,"f":"dashboard.js"},"AB1":{"t":"F","e":false,"f":"app.js"},"GI":{"t":"F","e":false,"f":"dashboard.js"},"WS1":{"t":"F","e":false,"f":"dashboard.js"},"es":{"t":"F","e":false,"f":"highlight.js"},"hi":{"t":"F","e":true,"f":"highlight.js"},"eR":{"t":"F","e":false,"f":"highlight.js"},"iD":{"t":"F","e":false,"f":"highlight.js"},"HD":{"t":"F","e":false,"f":"highlight.js"},"IS":{"t":"F","e":false,"f":"highlight.js"},"IP":{"t":"F","e":false,"f":"highlight.js"}},"edges":[["CB","→","hi"],["CV","→","_F"],["CV","→","ap"],["CV","→","_CB"],["CP","→","_C"],["CP","→","ap"],["CP","→","_C1"],["CP","→","es.slice"],["DG","→","_D"],["DG","→","_O"],["FT","→","_T"],["FT","→","_F1"],["FT","→","em"],["HP","→","_H"],["HP","→","ap"],["HP","→","_m"],["LM","→","_E"],["LM","→","_e"],["LM","→","_T1"]],"orphans":["initWebSocket","applyRouteLayout","initLayout","init","updateAgentBadge","fetchGatewayInfo","initWebSockets","escRange","isDigit","isHexDigit","isIdentStart","isIdentPart"],"duplicates":{"renderCallback":["CodeBlock:15","CodeViewer:9"],"initCallback":["ActionBoard:7","ProjectList:7","CodeViewer:9","CtxPanel:4","DepGraph:4","FileTree:4","HealthPanel:4","LiveMonitor:4"]},"files":["app.js","components/code-block.js","dashboard-state.js","dashboard.js","highlight.js","panels/ActionBoard/ActionBoard.js","panels/EventItem/EventItem.js","panels/ProjectItem/ProjectItem.js","panels/ProjectList/ProjectList.js","panels/code-viewer.js","panels/ctx-panel.js","panels/dep-graph.js","panels/file-tree.js","panels/health-panel.js","panels/live-monitor.js"]}}
package/web/app.js ADDED
@@ -0,0 +1,16 @@
1
+ // @ctx .context/web/app.ctx
2
+ import{Layout as t,LayoutTree as n,applyTheme as o}from"symbiote-node";import{CARBON as a}from"./vendor/symbiote-node/themes/carbon.js";import{state as s,subscribe as i,onEvent as r,call as c,connect as l}from"./state.js";import"./panels/file-tree.js";import"./panels/code-viewer.js";import"./panels/ctx-panel.js";import"./panels/dep-graph.js";import"./panels/health-panel.js";import"./panels/live-monitor.js";import"./panels/SettingsPanel/SettingsPanel.js";import"./components/quick-open.js";
3
+ export const state={skeleton:null,activeFile:null,ws:null,monitorEvents:[]};
4
+ const m=new URL(".",import.meta.url).href;
5
+ export async function api(t,n={}){if(s.connected&&t.startsWith("/api/")){const o=await p(t,n);if(null!==o)return o}const o=new URLSearchParams(n).toString(),a=t.replace(/^\//,""),i=o?`${m}${a}?${o}`:`${m}${a}`,r=await fetch(i);if(!r.ok)throw new Error(`API error: ${r.status}`);return r.json()}
6
+ async function p(t,n){const o={"/api/skeleton":{name:"get_skeleton",args:t=>({path:t.path})},"/api/file":{name:"compact",args:t=>({action:"compact_file",path:t.path,beautify:!0})},"/api/docs":{name:"docs",args:t=>({action:"get",path:t.path,file:t.file})},"/api/analysis":{name:"analyze",args:t=>({action:"full_analysis",path:t.path})},"/api/analysis-summary":{name:"analyze",args:t=>({action:"analysis_summary",path:t.path})},"/api/deps":{name:"navigate",args:t=>({action:"deps",symbol:t.symbol})},"/api/usages":{name:"navigate",args:t=>({action:"usages",symbol:t.symbol})},"/api/expand":{name:"navigate",args:t=>({action:"expand",symbol:t.symbol})},"/api/chain":{name:"navigate",args:t=>({action:"call_chain",from:t.from,to:t.to})}}[t];return o?c(o.name,o.args(n)):null}
7
+ export const events=new EventTarget;
8
+ export function emit(t,n={}){events.dispatchEvent(new CustomEvent(t,{detail:n}))}const d={"file-tree":{title:"Files",icon:"folder",component:"pg-file-tree"},"code-viewer":{title:"Code",icon:"code",component:"pg-code-viewer"},"ctx-panel":{title:"Documentation",icon:"description",component:"pg-ctx-panel"},"dep-graph":{title:"Dependencies",icon:"account_tree",component:"pg-dep-graph"},health:{title:"Health",icon:"analytics",component:"pg-health-panel"},monitor:{title:"Live Monitor",icon:"monitor_heart",component:"pg-live-monitor"},settings:{title:"Settings",icon:"settings",component:"pg-settings-panel"}},u=[{id:"explorer",icon:"folder_open",label:"Explorer"},{id:"analysis",icon:"analytics",label:"Analysis"},{id:"monitor",icon:"monitor_heart",label:"Monitor"},{id:"settings",icon:"settings",label:"Settings"}],b={explorer:()=>n.createSplit("horizontal",n.createPanel("file-tree"),n.createSplit("horizontal",n.createPanel("code-viewer"),n.createPanel("ctx-panel"),.65),.2),analysis:()=>n.createSplit("horizontal",n.createPanel("health"),n.createPanel("dep-graph"),.5),monitor:()=>n.createPanel("monitor"),settings:()=>n.createPanel("settings")};function g(){o(document.documentElement,a);
9
+ const t=document.querySelector(".app-workspace"),n=document.createElement("layout-sidebar");t.prepend(n);
10
+ const s=t.querySelector(".app-content"),i=document.createElement("panel-layout");i.setAttribute("storage-key","pg-explorer-layout"),i.setAttribute("min-panel-size","150"),i.id="main-layout",s.appendChild(i),requestAnimationFrame(()=>{for(const[t,n]of Object.entries(d))i.registerPanelType(t,n);function e(){const t=location.hash.replace("#","")||"explorer",n=t.indexOf("?"),o=n>=0?t.substring(0,n):t,a=o.indexOf("/"),s=a>=0?o.substring(0,a):o,r=a>=0?o.substring(a+1):"";b[s]&&i.setLayout(b[s]()),"explorer"===s&&r&&requestAnimationFrame(()=>{state.activeFile=r,emit("file-selected",{path:r,fromRoute:!0})})}n.setSections(u),window.addEventListener("hashchange",e),events.addEventListener("file-selected",t=>{if(t.detail.fromRoute)return;
11
+ const n=t.detail.path;n&&history.replaceState(null,"",`#explorer/${n}`)}),localStorage.getItem("pg-explorer-layout")||i.setLayout(b.explorer()),location.hash&&"#"!==location.hash?e():location.hash="explorer"})}
12
+ async function f(){g(),l(),i("project",t=>{t&&(document.title=`${t.name} — Project Graph`,document.getElementById("project-name").textContent=t.name,document.documentElement.style.setProperty("--project-accent",t.color),h(t.agents))}),i("skeleton",t=>{if(!t)return;state.skeleton=t;
13
+ const n=new Set;for(const o of Object.values(t.n||{}))o.f&&n.add(o.f);for(const o of Object.keys(t.X||{}))n.add(o);for(const[o,a]of Object.entries(t.f||{}))for(const t of a)n.add("./"===o?t:`${o}${t}`);for(const[o,a]of Object.entries(t.a||{}))for(const t of a)n.add("./"===o?t:`${o}${t}`);
14
+ const o=document.getElementById("project-files");o&&(o.textContent=`${n.size} files`),emit("skeleton-loaded",t),fetch(m+"api/compression-stats").then(t=>t.json()).then(t=>{const n=document.getElementById("compression-stats");if(n&&t.codeTok){const o=t.ctxTok?`${(t.codeTok/1e3).toFixed(1)}K + ${(t.ctxTok/1e3).toFixed(1)}K ctx = ${(t.totalTok/1e3).toFixed(1)}K tok`:`${(t.codeTok/1e3).toFixed(1)}K tok`;n.textContent=o,n.style.display=""}})}),i("connected",t=>{const n=document.getElementById("status-indicator");n&&(n.className=t?"status connected":"status disconnected")}),r(t=>{if("agent_connect"===t.type||"agent_disconnect"===t.type)return h(t.agents),void emit("agent-event",t);state.monitorEvents.push(t),state.monitorEvents.length>500&&state.monitorEvents.shift(),emit("tool-event",t)})}
15
+ function h(t){let n=document.getElementById("agent-badge");if(!n){const t=document.querySelector(".app-topbar");if(!t)return;n=document.createElement("span"),n.id="agent-badge",n.className="agent-badge",t.appendChild(n)}n.textContent=t>0?`● ${t} agent${1!==t?"s":""}`:"",n.style.display=t>0?"":"none"}
16
+ function y(){document.querySelector("pg-quick-open")||document.body.appendChild(document.createElement("pg-quick-open"))}"loading"===document.readyState?document.addEventListener("DOMContentLoaded",()=>{f(),y()}):setTimeout(()=>{f(),y()},100);
@@ -0,0 +1,3 @@
1
+ import o from"@symbiotejs/symbiote";import{highlight as n}from"../highlight.js";
2
+ export class CodeBlock extends o{init$={code:"",highlighted:"",lineNums:""};renderCallback(){this.sub("code",o=>{if(!o)return this.$.highlighted="",void(this.$.lineNums="");this.$.highlighted=n(o);
3
+ const e=o.split("\n").length,t=[];for(let o=1;o<=e;o++)t.push(o);this.$.lineNums=t.join("\n")})}}CodeBlock.template='\n <div class="cb-scroll">\n <pre class="cb-gutter" bind="textContent: lineNums"></pre>\n <pre class="cb-pre"><code bind="innerHTML: highlighted"></code></pre>\n </div>\n',CodeBlock.rootStyles="\n code-block {\n display: block;\n height: 100%;\n overflow: hidden;\n }\n code-block .cb-scroll {\n display: flex;\n height: 100%;\n overflow: auto;\n align-items: stretch;\n }\n code-block .cb-gutter {\n position: sticky;\n left: 0;\n z-index: 1;\n margin: 0;\n padding: 12px 8px 12px 12px;\n font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;\n font-size: 12px;\n line-height: 1.6;\n text-align: right;\n color: var(--sn-text-dim, hsl(30, 10%, 55%));\n opacity: 0.45;\n background: var(--sn-bg, hsl(37, 30%, 96%));\n border-right: 1px solid var(--sn-node-border, hsl(35, 18%, 88%));\n user-select: none;\n white-space: pre;\n min-width: 32px;\n flex-shrink: 0;\n }\n code-block .cb-pre {\n margin: 0;\n padding: 12px;\n flex: 1;\n min-width: 0;\n font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;\n font-size: 12px;\n line-height: 1.6;\n color: var(--sn-text, hsl(30, 15%, 18%));\n tab-size: 2;\n white-space: pre;\n box-sizing: border-box;\n }\n /* Token colors */\n code-block .t-kw { color: rgb(254, 165, 176); }\n code-block .t-str { color: rgb(251, 182, 79); }\n code-block .t-cm { color: rgb(149, 149, 149); font-style: italic; }\n code-block .t-fn { color: rgb(180, 243, 255); }\n code-block .t-num { color: rgb(251, 182, 79); }\n code-block .t-bi { color: rgb(180, 243, 255); }\n code-block .t-prop { color: rgb(238, 131, 252); }\n code-block .t-lit { color: rgb(254, 165, 176); }\n /* JSDoc */\n code-block .t-jd { color: rgb(130, 155, 130); font-style: italic; }\n code-block .t-jd-tag { color: rgb(180, 220, 140); font-style: normal; font-weight: 500; }\n code-block .t-jd-type { color: rgb(130, 210, 240); font-style: normal; }\n",CodeBlock.reg("code-block");
@@ -0,0 +1,5 @@
1
+ import e from"@symbiotejs/symbiote";import{state as n,events as t,emit as s}from"../app.js";
2
+ export class QuickOpen extends e{init$={visible:!1,query:"",resultsHTML:"",selectedIdx:0};_results=[];_allFiles=[];renderCallback(){t.addEventListener("skeleton-loaded",e=>this._collectFiles(e.detail)),n.skeleton&&this._collectFiles(n.skeleton),this._overlay=this.querySelector(".qo-overlay"),this._overlay.addEventListener("click",e=>{e.target===this._overlay&&this._close()}),document.addEventListener("keydown",e=>{(e.metaKey||e.ctrlKey)&&"k"===e.key&&(e.preventDefault(),this._toggle()),"Escape"===e.key&&this.$.visible&&(e.preventDefault(),this._close())}),this.sub("visible",e=>{this._overlay&&(this._overlay.style.display=e?"flex":"none",e&&requestAnimationFrame(()=>{const e=this.querySelector(".qo-input");e&&(e.value="",e.focus())}))})}_collectFiles(e){const n=new Set;for(const t of Object.keys(e.X||{}))n.add(t);for(const t of Object.values(e.n||{}))t.f&&n.add(t.f);for(const[t,s]of Object.entries(e.f||{}))for(const e of s)n.add("./"===t?e:`${t}${e}`);for(const[t,s]of Object.entries(e.a||{}))for(const e of s)n.add("./"===t?e:`${t}${e}`);this._allFiles=[...n].sort()}_toggle(){this.$.visible=!this.$.visible,this.$.visible&&(this.$.query="",this.$.selectedIdx=0,this._search(""))}_close(){this.$.visible=!1}_onInput(e){this.$.query=e.target.value,this.$.selectedIdx=0,this._search(this.$.query)}_onKeydown(e){if("ArrowDown"===e.key)e.preventDefault(),this.$.selectedIdx=Math.min(this.$.selectedIdx+1,this._results.length-1),this._renderResults();else if("ArrowUp"===e.key)e.preventDefault(),this.$.selectedIdx=Math.max(this.$.selectedIdx-1,0),this._renderResults();else if("Enter"===e.key){e.preventDefault();
3
+ const t=this._results[this.$.selectedIdx];t&&(this._close(),n.activeFile=t.file,s("file-selected",{path:t.file}),location.hash.startsWith("#explorer")?history.replaceState(null,"",`#explorer/${t.file}`):location.hash=`explorer/${t.file}`)}}_search(e){const n=e.toLowerCase().trim();if(n){const e=[];for(const t of this._allFiles){const s=QuickOpen._fuzzyScore(n,t.toLowerCase());s>0&&e.push({file:t,score:s})}e.sort((e,n)=>n.score-e.score),this._results=e.slice(0,15)}else this._results=this._allFiles.slice(0,15).map(e=>({file:e,score:0}));this._renderResults()}static _fuzzyScore(e,n){if(n.includes(e))return 100+e.length/n.length*50;
4
+ let t=0,s=0,o=0;for(let i=0;i<n.length&&t<e.length;i++)n[i]===e[t]?(t++,o+=10+s,s+=5,0!==i&&"/"!==n[i-1]&&"-"!==n[i-1]&&"."!==n[i-1]||(o+=15)):s=0;return t===e.length?o:0}_renderResults(){if(0===this._results.length)return void(this.$.resultsHTML='<div class="qo-empty">No files found</div>');
5
+ const e=[];for(let n=0;n<this._results.length;n++){const{file:t}=this._results[n],s=t.split("/").pop(),o=t.includes("/")?t.substring(0,t.lastIndexOf("/")):"",i=n===this.$.selectedIdx?" qo-selected":"";e.push(`<div class="qo-item${i}" data-idx="${n}" data-file="${t}">\n <span class="qo-name">${s}</span>\n <span class="qo-path">${o}</span>\n </div>`)}this.$.resultsHTML=e.join("")}}QuickOpen.template='\n <div class="qo-overlay">\n <div class="qo-dialog" onclick="event.stopPropagation()">\n <div class="qo-input-wrap">\n <span class="material-symbols-outlined qo-icon">search</span>\n <input class="qo-input" type="text" placeholder="Search files… (↑↓ navigate, Enter open)"\n oninput="this.closest(\'pg-quick-open\')._onInput(event)"\n onkeydown="this.closest(\'pg-quick-open\')._onKeydown(event)">\n <kbd class="qo-kbd">ESC</kbd>\n </div>\n <div class="qo-results" bind="innerHTML: resultsHTML"\n onclick="const item=event.target.closest(\'.qo-item\');if(item){this.closest(\'pg-quick-open\').$.selectedIdx=+item.dataset.idx;this.closest(\'pg-quick-open\')._onKeydown({key:\'Enter\',preventDefault(){}});}"></div>\n </div>\n </div>\n',QuickOpen.rootStyles="\n pg-quick-open { position: fixed; inset: 0; z-index: 9999; pointer-events: none; }\n .qo-overlay {\n position: fixed; inset: 0; z-index: 9999;\n background: rgba(0,0,0,0.5);\n display: none; justify-content: center; padding-top: 15vh;\n pointer-events: all;\n animation: qo-fadein 100ms ease;\n }\n .qo-hidden { display: none !important; pointer-events: none; }\n .qo-dialog {\n width: 520px;\n max-height: 420px;\n background: var(--sn-panel-bg, hsl(228, 14%, 18%));\n border: 1px solid var(--sn-node-border, hsl(228, 10%, 28%));\n border-radius: 10px;\n box-shadow: 0 20px 60px rgba(0,0,0,0.5);\n overflow: hidden;\n display: flex;\n flex-direction: column;\n }\n .qo-input-wrap {\n display: flex;\n align-items: center;\n padding: 8px 12px;\n gap: 8px;\n border-bottom: 1px solid var(--sn-node-border, hsl(228, 10%, 28%));\n }\n .qo-icon { color: var(--sn-text-dim); font-size: 20px; }\n .qo-input {\n flex: 1;\n background: transparent;\n border: none;\n color: var(--sn-text, #e0e0e0);\n font-size: 15px;\n font-family: inherit;\n outline: none;\n padding: 6px 0;\n }\n .qo-input::placeholder { color: var(--sn-text-dim); }\n .qo-kbd {\n font-size: 10px;\n padding: 2px 6px;\n border-radius: 4px;\n background: var(--sn-node-bg, hsl(228, 14%, 22%));\n border: 1px solid var(--sn-node-border);\n color: var(--sn-text-dim);\n font-family: monospace;\n }\n .qo-results {\n overflow-y: auto;\n padding: 4px 0;\n max-height: 350px;\n }\n .qo-item {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 8px 16px;\n cursor: pointer;\n transition: background 80ms ease;\n }\n .qo-item:hover { background: var(--sn-node-hover, hsl(228, 14%, 22%)); }\n .qo-item.qo-selected {\n background: hsla(210, 55%, 45%, 0.2);\n }\n .qo-name {\n font-size: 13px;\n color: var(--sn-text, #e0e0e0);\n font-weight: 500;\n }\n .qo-path {\n font-size: 11px;\n color: var(--sn-text-dim);\n margin-left: auto;\n font-family: 'SF Mono', monospace;\n }\n .qo-empty {\n padding: 20px;\n text-align: center;\n color: var(--sn-text-dim);\n font-style: italic;\n }\n @keyframes qo-fadein { from { opacity: 0; } to { opacity: 1; } }\n",QuickOpen.reg("pg-quick-open");
@@ -0,0 +1,3 @@
1
+ export const state={projects:[],events:[]};
2
+ export const events=new EventTarget;
3
+ export function emit(t,e={}){events.dispatchEvent(new CustomEvent(t,{detail:e}))}
@@ -0,0 +1,27 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Project Graph — Dashboard</title>
7
+ <meta name="description" content="Dashboard and Action Board for project-graph-mcp">
8
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap">
9
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=swap">
10
+ <link rel="stylesheet" href="style.css">
11
+ <script type="importmap">{"imports":{"@symbiotejs/symbiote":"./vendor/symbiote/core/index.js","symbiote-node":"./vendor/symbiote-node/index.js"}}</script>
12
+ </head>
13
+ <body>
14
+ <div class="app-shell">
15
+ <header class="app-topbar">
16
+ <div class="topbar-left">
17
+ <span class="material-symbols-outlined" style="font-size:20px;color:var(--project-accent,var(--sn-node-selected,#4c8bf5))">hub</span>
18
+ <span class="app-title">Project Graph Workspace</span>
19
+ </div>
20
+ </header>
21
+ <div class="app-workspace">
22
+ <div class="app-content"></div>
23
+ </div>
24
+ </div>
25
+ <script type="module" src="dashboard.js"></script>
26
+ </body>
27
+ </html>
@@ -0,0 +1,8 @@
1
+ // @ctx .context/web/dashboard.ctx
2
+ import{Layout as e,LayoutTree as t,applyTheme as o}from"symbiote-node";import{CARBON as r}from"./vendor/symbiote-node/themes/carbon.js";import"./panels/ProjectList/ProjectList.js";import"./panels/ActionBoard/ActionBoard.js";import{state as a,events as n,emit as s}from"./dashboard-state.js";async function c(){const e=await fetch("/api/gateway-info");if(!e.ok){const t=await e.text();throw console.error("[dashboard] fetchGatewayInfo failed:",e.status,t),new Error(`Gateway info failed: ${e.status}`)}return e.json()}
3
+ function p(e){if(!e.length)return void console.warn("[dashboard] No projects to connect WebSockets for");
4
+ const t="https:"===location.protocol?"wss://":"ws://",o=location.host;for(const r of e)i(r,t,o)}
5
+ function i(e,t,o){const r=`${t}${o}${e.prefix}/ws/monitor`,n=new WebSocket(r);n.onopen=()=>{console.log("[dashboard] WS connected:",e.projectName)},n.onmessage=t=>{let o;try{o=JSON.parse(t.data)}catch{return}if("snapshot"===o.method&&o.params?.state){const t=o.params.state,r=a.projects.find(t=>t.prefix===e.prefix);return void(r&&t.project&&(Object.assign(r,{projectName:t.project.name,projectPath:t.project.path,color:t.project.color,agents:t.project.agents,pid:t.project.pid,connected:!0}),s("projects-updated",a.projects)))}if("patch"===o.method&&o.params){const t=a.projects.find(t=>t.prefix===e.prefix);return void(t&&"project.agents"===o.params.path&&(t.agents=o.params.value,s("projects-updated",a.projects)))}if("event"===o.method&&o.params){const t=o.params;return t._projectPrefix=e.prefix,t._projectName=e.projectName,a.events.push(t),a.events.length>1e3&&a.events.shift(),void s("global-tool-event",t)}o.type&&(o._projectPrefix=e.prefix,o._projectName=e.projectName,a.events.push(o),a.events.length>1e3&&a.events.shift(),s("global-tool-event",o))},n.onerror=()=>{console.error("[dashboard] WS error:",e.projectName)},n.onclose=r=>{console.warn("[dashboard] WS closed:",e.projectName,r.code);
6
+ const n=a.projects.find(t=>t.prefix===e.prefix);n&&(n.connected=!1,s("projects-updated",a.projects)),setTimeout(()=>i(e,t,o),5e3)}}const d={"project-list":{title:"Projects",icon:"dashboard",component:"pg-project-list"},"action-board":{title:"Action Board",icon:"monitor_heart",component:"pg-action-board"}};async function l(){o(document.documentElement,r);
7
+ const e=document.querySelector(".app-workspace").querySelector(".app-content"),n=document.createElement("panel-layout");n.setAttribute("storage-key","pg-dashboard-layout"),n.setAttribute("min-panel-size","200"),n.id="dashboard-layout",e.appendChild(n),requestAnimationFrame(async()=>{for(const[e,t]of Object.entries(d))n.registerPanelType(e,t);localStorage.getItem("pg-dashboard-layout")||n.setLayout(t.createSplit("vertical",t.createPanel("project-list"),t.createPanel("action-board"),.3));
8
+ const e=await c();a.projects=Object.entries(e.routes||{}).map(([e,t])=>({prefix:e,...t,connected:!1,agents:0})),s("projects-updated",a.projects),p(a.projects)})}"loading"===document.readyState?document.addEventListener("DOMContentLoaded",l):setTimeout(l,100);
@@ -0,0 +1,13 @@
1
+ // @ctx .context/web/highlight.ctx
2
+ const t=new Set(["async","await","break","case","catch","class","const","continue","debugger","default","delete","do","else","export","extends","finally","for","from","function","if","import","in","instanceof","let","new","of","return","super","switch","this","throw","try","typeof","var","void","while","with","yield","static","get","set"]),n=new Set(["true","false","null","undefined","NaN","Infinity"]),s=new Set(["console","document","window","global","process","module","require","Promise","Array","Object","String","Number","Boolean","Map","Set","WeakMap","WeakSet","Symbol","RegExp","Error","JSON","Math","Date","parseInt","parseFloat","setTimeout","setInterval","clearTimeout","clearInterval","fetch","URL","Buffer","EventTarget","CustomEvent","HTMLElement","requestAnimationFrame","queueMicrotask"]);function e(t){return"&"===t?"&amp;":"<"===t?"&lt;":">"===t?"&gt;":t}
3
+ export function highlight(i){const l=[],f=i.length;
4
+ let h=0;for(;h<f;){const g=i[h];if("/"===g&&"/"===i[h+1]){const t=h;for(;h<f&&"\n"!==i[h];)h++;l.push(`<span class="t-cm">${a(i,t,h)}</span>`);continue}if("/"===g&&"*"===i[h+1]){const t=h;for(h+=2;h<f&&("*"!==i[h]||"/"!==i[h+1]);)h++;h+=2;
5
+ const n=i.substring(t,h);n.startsWith("/**")?l.push(u(n)):l.push(`<span class="t-cm">${a(i,t,h)}</span>`);continue}if("'"===g||'"'===g){const t=h;for(h++;h<f&&i[h]!==g;)"\\"===i[h]&&h++,h++;h++,l.push(`<span class="t-str">${a(i,t,h)}</span>`);continue}if("`"===g){const t=h;for(h++;h<f&&"`"!==i[h];)"\\"===i[h]&&h++,h++;h++,l.push(`<span class="t-str">${a(i,t,h)}</span>`);continue}if(r(g)||"."===g&&h+1<f&&r(i[h+1])){const t=h;if("0"!==g||"x"!==i[h+1]&&"X"!==i[h+1])for(;h<f&&(r(i[h])||"."===i[h]||"e"===i[h]||"E"===i[h]||"_"===i[h]);)h++;else for(h+=2;h<f&&o(i[h]);)h++;h<f&&"n"===i[h]&&h++,l.push(`<span class="t-num">${a(i,t,h)}</span>`);continue}if(c(g)){const g=h;for(;h<f&&p(i[h]);)h++;
6
+ const m=i.substring(g,h);
7
+ let d=h;for(;d<f&&" "===i[d];)d++;t.has(m)?l.push(`<span class="t-kw">${m}</span>`):n.has(m)?l.push(`<span class="t-lit">${m}</span>`):s.has(m)?l.push(`<span class="t-bi">${m}</span>`):"("===i[d]?l.push(`<span class="t-fn">${m}</span>`):g>0&&"."===i[g-1]?l.push(`<span class="t-prop">${m}</span>`):l.push(m);continue}l.push(e(g)),h++}return l.join("")}
8
+ function a(t,n,s){let i="";for(let l=n;l<s;l++)i+=e(t[l]);return i}
9
+ function r(t){return t>="0"&&t<="9"}
10
+ function o(t){return r(t)||t>="a"&&t<="f"||t>="A"&&t<="F"}
11
+ function c(t){return t>="a"&&t<="z"||t>="A"&&t<="Z"||"_"===t||"$"===t}
12
+ function p(t){return c(t)||r(t)}
13
+ function u(t){return'<span class="t-jd">'+t.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/(@\w+)/g,'<span class="t-jd-tag">$1</span>').replace(/\{([^}]+)\}/g,'<span class="t-jd-type">{$1}</span>')+"</span>"}
package/web/index.html ADDED
@@ -0,0 +1,35 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Project Graph — Web Explorer</title>
7
+ <meta name="description" content="Interactive codebase explorer powered by project-graph-mcp">
8
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap">
9
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=swap">
10
+ <link rel="stylesheet" href="style.css">
11
+ <script type="importmap">{"imports":{"@symbiotejs/symbiote":"./vendor/symbiote/core/index.js","symbiote-node":"./vendor/symbiote-node/index.js"}}</script>
12
+ </head>
13
+ <body>
14
+ <div class="app-shell">
15
+ <header class="app-topbar">
16
+ <div class="topbar-left">
17
+ <span class="material-symbols-outlined" style="font-size:20px;color:var(--project-accent,var(--sn-node-selected,#4c8bf5))">hub</span>
18
+ <span class="app-title">Project Graph</span>
19
+ <span class="topbar-sep">·</span>
20
+ <span id="project-name" class="project-name">Loading...</span>
21
+ <span id="project-files" class="project-files"></span>
22
+ <span id="compression-stats" class="compression-stats" style="display:none"></span>
23
+ </div>
24
+ <div class="topbar-right">
25
+ <span id="agent-badge" class="agent-badge" style="display:none"></span>
26
+ <span id="status-indicator" class="status connected"></span>
27
+ </div>
28
+ </header>
29
+ <div class="app-workspace">
30
+ <div class="app-content"></div>
31
+ </div>
32
+ </div>
33
+ <script type="module" src="app.js"></script>
34
+ </body>
35
+ </html>
@@ -0,0 +1 @@
1
+ export default"\n:host {\n display: flex;\n flex-direction: column;\n height: 100%;\n background: var(--sn-bg-primary);\n color: var(--sn-fg-primary);\n overflow: hidden;\n}\n.list {\n flex: 1;\n overflow-y: auto;\n padding: 16px;\n}\n";
@@ -0,0 +1,4 @@
1
+ import t from"@symbiotejs/symbiote";import{state as e,events as o}from"../../dashboard-state.js";
2
+ import s from"./ActionBoard.css.js";
3
+ import n from"./ActionBoard.tpl.js";import"../EventItem/EventItem.js";
4
+ export class ActionBoard extends t{init$={eventsItems:[]};initCallback(){console.log("[ActionBoard] initCallback, existing events:",e.events.length),o.addEventListener("global-tool-event",t=>{const o=[...e.events].reverse();console.log("[ActionBoard] global-tool-event received, total:",o.length,"latest:",t.detail?.type,t.detail?.tool),this.$.eventsItems=o}),this.$.eventsItems=[...e.events].reverse()}}ActionBoard.template=n,ActionBoard.rootStyles=s,ActionBoard.reg("pg-action-board");
@@ -0,0 +1 @@
1
+ export default'\n<div class="list" itemize="eventsItems" item-tag="pg-event-item"></div>\n';
@@ -0,0 +1 @@
1
+ export default"\n:host {\n display: block;\n border-bottom: 1px solid var(--sn-border-primary);\n font-family: var(--sn-font-mono, monospace);\n font-size: 13px;\n cursor: pointer;\n transition: background 0.15s;\n}\n:host(:hover) {\n background: var(--sn-bg-secondary, rgba(255,255,255,0.03));\n}\n.event-row {\n display: flex;\n align-items: center;\n padding: 6px 10px;\n gap: 8px;\n}\n.event-icon {\n width: 24px;\n height: 24px;\n display: flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n}\n.event-icon .material-symbols-outlined {\n font-size: 16px;\n color: var(--sn-fg-muted);\n}\n.event-time {\n color: var(--sn-fg-muted);\n width: 70px;\n flex-shrink: 0;\n font-size: 12px;\n}\n.event-type {\n width: 85px;\n flex-shrink: 0;\n font-size: 11px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.3px;\n padding: 2px 6px;\n border-radius: 3px;\n text-align: center;\n}\n.event-type.call {\n color: #60a5fa;\n background: rgba(96, 165, 250, 0.1);\n}\n.event-type.success {\n color: #4ade80;\n background: rgba(74, 222, 128, 0.1);\n}\n.event-type.error {\n color: #f87171;\n background: rgba(248, 113, 113, 0.1);\n}\n.event-tool {\n flex: 1;\n font-weight: 500;\n color: var(--sn-fg-primary);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.event-project {\n color: var(--sn-fg-muted);\n font-size: 11px;\n max-width: 100px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.event-duration {\n color: var(--sn-fg-muted);\n font-size: 11px;\n width: 55px;\n text-align: right;\n flex-shrink: 0;\n}\n.event-chevron {\n width: 20px;\n height: 20px;\n display: flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n transition: transform 0.2s;\n opacity: 0.4;\n}\n.event-chevron .material-symbols-outlined {\n font-size: 18px;\n}\n:host([expanded]) .event-chevron {\n transform: rotate(180deg);\n opacity: 0.8;\n}\n.event-details {\n display: none;\n padding: 0 10px 8px 42px;\n}\n:host([expanded]) .event-details {\n display: block;\n}\n.event-args {\n margin: 0;\n padding: 8px 12px;\n background: var(--sn-bg-primary, rgba(0,0,0,0.2));\n border: 1px solid var(--sn-border-primary);\n border-radius: 4px;\n font-size: 11px;\n line-height: 1.4;\n color: var(--sn-fg-muted);\n overflow-x: auto;\n max-height: 200px;\n overflow-y: auto;\n white-space: pre-wrap;\n word-break: break-all;\n}\n";
@@ -0,0 +1,4 @@
1
+ import t from"@symbiotejs/symbiote";
2
+ import e from"./EventItem.css.js";
3
+ import s from"./EventItem.tpl.js";
4
+ export class EventItem extends t{init$={ts:0,type:"",tool:"",args:null,duration_ms:0,success:!0,result_keys:[],expanded:!1,icon:"arrow_right",detailsText:"",statusClass:"",durationText:"",_projectName:""};renderCallback(){this.sub("ts",t=>{this.ref.time.textContent=t?new Date(t).toLocaleTimeString("ru-RU",{hour:"2-digit",minute:"2-digit",second:"2-digit"}):""}),this.sub("type",t=>{this.$.icon="tool_call"===t?"call_made":"call_received",this.$.statusClass="tool_call"===t?"call":this.$.success?"success":"error"}),this.sub("duration_ms",t=>{this.$.durationText=t?`${t}ms`:""}),this.sub("args",t=>{t&&"object"==typeof t&&Object.keys(t).length>0?this.$.detailsText=JSON.stringify(t,null,2):this.$.detailsText=""}),this.onclick=()=>{(this.$.detailsText||this.$.result_keys?.length)&&(this.$.expanded=!this.$.expanded,this.$.expanded?this.setAttribute("expanded",""):this.removeAttribute("expanded"))}}}EventItem.template=s,EventItem.rootStyles=e,EventItem.reg("pg-event-item");
@@ -0,0 +1 @@
1
+ export default'\n<div class="event-row">\n <span class="event-icon" ref="statusIcon">\n <span class="material-symbols-outlined">{{icon}}</span>\n </span>\n <span class="event-time" ref="time"></span>\n <span class="event-type {{statusClass}}">{{type}}</span>\n <span class="event-tool">{{tool}}</span>\n <span class="event-project">{{_projectName}}</span>\n <span class="event-duration">{{durationText}}</span>\n <span class="event-chevron">\n <span class="material-symbols-outlined" ref="chevron">expand_more</span>\n </span>\n</div>\n<div class="event-details" ref="details">\n <pre class="event-args">{{detailsText}}</pre>\n</div>\n';
@@ -0,0 +1 @@
1
+ export default"\n:host {\n display: block;\n}\n.card {\n background: var(--sn-bg-secondary);\n border: 1px solid var(--sn-border-primary);\n border-radius: 8px;\n padding: 10px 12px;\n margin-bottom: 8px;\n transition: border-color 0.2s;\n}\n.card:hover {\n border-color: var(--project-accent, #7878ff);\n}\n.title {\n font-size: 14px;\n font-weight: 500;\n margin-bottom: 2px;\n display: flex;\n align-items: center;\n gap: 8px;\n}\n.token-badge {\n font-size: 10px;\n font-weight: 500;\n color: #64b5f6;\n padding: 1px 6px;\n border-radius: 8px;\n background: rgba(100, 181, 246, 0.1);\n border: 1px solid rgba(100, 181, 246, 0.15);\n font-family: var(--sn-font-mono, monospace);\n white-space: nowrap;\n}\n.token-badge:empty {\n display: none;\n}\n.path {\n font-size: 11px;\n font-family: var(--sn-font-mono, monospace);\n color: var(--sn-fg-muted);\n word-break: break-all;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\na {\n color: var(--project-accent, #7878ff);\n text-decoration: none;\n}\na:hover {\n text-decoration: underline;\n}\n";
@@ -0,0 +1,5 @@
1
+ import e from"@symbiotejs/symbiote";
2
+ import t from"./ProjectItem.css.js";
3
+ import r from"./ProjectItem.tpl.js";
4
+ export class ProjectItem extends e{init$={prefix:"",projectName:"",projectPath:""};renderCallback(){this.sub("prefix",e=>{this.ref.link.href=e?`${e}/`:"#";if(e){fetch(`${e}/api/compression-stats`).then(r=>r.json()).then(r=>{if(r.codeTok&&this.ref.tokenBadge){const c=(r.codeTok/1e3).toFixed(1),x=(r.ctxTok||0)/1e3,exp=r.expanded||0,expK=(exp/1e3).toFixed(1),codePct=exp>0?Math.round(100*(1-r.codeTok/exp)):0,totalPct=exp>0?Math.round(100*(1-(r.codeTok+(r.ctxTok||0))/exp)):0,fmt=v=>v>=0?`↓${v}%`:`↑${Math.abs(v)}%`;let t=r.ctxTok?`${c}K (${fmt(codePct)}) + ${x.toFixed(1)}K ctx`:`${c}K tok (${fmt(codePct)})`;if(exp>0){t+=r.ctxTok?` = ${((r.codeTok+r.ctxTok)/1e3).toFixed(1)}K (${fmt(totalPct)}) of ${expK}K`:`of ${expK}K`}this.ref.tokenBadge.textContent=t}}).catch(()=>{})}})}}
5
+ ProjectItem.template=r,ProjectItem.rootStyles=t,ProjectItem.reg("pg-project-item");
@@ -0,0 +1 @@
1
+ export default'\n<div class="card">\n <div class="title"><a ref="link">{{projectName}}</a><span class="token-badge" ref="tokenBadge"></span></div>\n <div class="path">{{projectPath}}</div>\n</div>\n';
@@ -0,0 +1 @@
1
+ export default"\n:host {\n display: block;\n height: 100%;\n overflow-y: auto;\n padding: 10px;\n background: var(--sn-bg-primary);\n color: var(--sn-fg-primary);\n}\n.empty {\n color: var(--sn-fg-muted);\n padding: 24px;\n text-align: center;\n}\n";
@@ -0,0 +1,4 @@
1
+ import t from"@symbiotejs/symbiote";import{state as s,events as e}from"../../dashboard-state.js";
2
+ import r from"./ProjectList.css.js";
3
+ import o from"./ProjectList.tpl.js";import"../ProjectItem/ProjectItem.js";
4
+ export class ProjectList extends t{init$={projects:[],hasProjects:!1};initCallback(){e.addEventListener("projects-updated",t=>{this.$.projects=t.detail,this.$.hasProjects=t.detail.length>0}),this.$.projects=s.projects,this.$.hasProjects=s.projects.length>0}renderCallback(){this.sub("hasProjects",t=>{this.ref.emptyMsg.hidden=t})}}ProjectList.template=o,ProjectList.rootStyles=r,ProjectList.reg("pg-project-list");
@@ -0,0 +1 @@
1
+ export default'\n<div itemize="projects" item-tag="pg-project-item"></div>\n<div class="empty" ref="emptyMsg">\n No projects registered.\n</div>\n';
@@ -0,0 +1 @@
1
+ {"version":1,"path":"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/web/panels/SettingsPanel","mtimes":{"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/web/panels/SettingsPanel/SettingsPanel.js":1775871453008.7366},"graph":{"v":1,"legend":{"SettingsPanel":"SP","metric":"me","renderCallback":"rC","fetchInfo":"fI"},"reverseLegend":{"SP":"SettingsPanel","me":"metric","rC":"renderCallback","fI":"fetchInfo"},"stats":{"files":1,"classes":1,"functions":1,"tables":0},"nodes":{"SP":{"t":"C","x":"Symbiote","m":["rC","fI"],"f":"SettingsPanel.js"},"me":{"t":"F","e":false,"f":"SettingsPanel.js"}},"edges":[["SP","→","fI"],["SP","→","me"]],"orphans":[],"duplicates":{},"files":["SettingsPanel.js"]}}
@@ -0,0 +1 @@
1
+ export default"\npg-settings-panel {\n display: block;\n height: 100%;\n overflow-y: auto;\n padding: 16px;\n font-family: var(--sn-font, 'Inter', -apple-system, sans-serif);\n}\n\n.pg-stg-card {\n background: var(--sn-node-bg);\n border: 1px solid var(--sn-node-border);\n border-radius: 8px;\n padding: 14px;\n margin-bottom: 12px;\n}\n\n.pg-stg-title {\n font-size: 11px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n color: var(--sn-text-dim);\n margin-bottom: 8px;\n}\n\n.pg-stg-metric {\n display: flex;\n justify-content: space-between;\n padding: 5px 0;\n border-bottom: 1px solid var(--sn-node-hover);\n font-size: 12px;\n color: var(--sn-text);\n}\n\n.pg-stg-metric:last-child {\n border-bottom: none;\n}\n\n.pg-stg-val {\n font-weight: 600;\n font-family: 'JetBrains Mono', 'Fira Code', monospace;\n}\n\n.pg-stg-ok {\n color: var(--sn-success-color, #4caf50);\n}\n\n.pg-stg-btn {\n background: var(--sn-node-bg);\n border: 1px solid var(--sn-node-border);\n color: var(--sn-text);\n padding: 6px 14px;\n border-radius: 8px;\n cursor: pointer;\n font-size: 12px;\n font-family: inherit;\n transition: border-color 0.15s;\n}\n\n.pg-stg-btn:hover {\n border-color: var(--sn-node-selected, #4c8bf5);\n}\n\n.pg-stg-btn-danger {\n border-color: var(--sn-danger-color, #f44336);\n color: var(--sn-danger-color, #f44336);\n}\n\n.pg-stg-btn-danger:hover {\n background: var(--sn-danger-color, #f44336);\n color: #fff;\n border-color: var(--sn-danger-color, #f44336);\n}\n\n.pg-stg-placeholder {\n color: var(--sn-text-dim);\n text-align: center;\n padding: 20px;\n font-style: italic;\n font-size: 13px;\n}\n\n.pg-stg-pulse {\n animation: pg-stg-pulse 1.5s ease infinite;\n}\n\n@keyframes pg-stg-pulse {\n 0%, 100% { opacity: 1; }\n 50% { opacity: 0.4; }\n}\n";
@@ -0,0 +1,7 @@
1
+ import t from"@symbiotejs/symbiote";
2
+ import e from"./SettingsPanel.css.js";
3
+ import n from"./SettingsPanel.tpl.js";function r(t,e,n=""){return`<div class="pg-stg-metric"><span>${t}</span><span class="pg-stg-val ${n}">${e}</span></div>`}
4
+ export class SettingsPanel extends t{init$={};renderCallback(){this.ref.refreshBtn.onclick=()=>this.fetchInfo(),this.ref.restartBtn.onclick=()=>this.restartServer(),this.fetchInfo()}async restartServer(){const t=this.ref.restartStatus;t.textContent="⏳ Restarting server…",t.style.color="var(--sn-warning-color, #ff9800)";try{await fetch("/api/restart",{method:"POST"}),t.textContent="Server stopped. Reconnecting…";
5
+ let e=0;
6
+ const n=setInterval(async()=>{e++;try{if((await fetch("/api/project-info")).ok)return clearInterval(n),t.textContent="✅ Server restarted successfully",t.style.color="var(--sn-success-color, #4caf50)",this.fetchInfo(),void setTimeout(()=>{t.textContent=""},3e3)}catch{}e>15&&(clearInterval(n),t.textContent="⚠ Server did not come back. Refresh the page manually.",t.style.color="var(--sn-danger-color, #f44336)")},1e3)}catch(e){t.textContent=`Error: ${e.message}`,t.style.color="var(--sn-danger-color, #f44336)"}}async fetchInfo(){this.ref.backendCard.innerHTML='<div class="pg-stg-placeholder pg-stg-pulse">Loading…</div>';try{const[t,e]=await Promise.all([fetch("/api/project-info").then(t=>t.json()),fetch("/api/instances").then(t=>t.json())]);this.ref.backendCard.innerHTML=[r("Status","Running","pg-stg-ok"),r("Project",t.name||"—"),r("Path",t.path||"—"),r("PID",t.pid||"—"),r("Connected Agents",t.agents??"—"),r("Idle Shutdown","15 min")].join("");
7
+ const n=this.ref.instanceList;if(n.innerHTML="",Array.isArray(e)&&e.length>0)for(const t of e){const e=t.startedAt?Math.round((Date.now()-t.startedAt)/6e4):"?",s=document.createElement("div");s.className="pg-stg-card",s.innerHTML=[r("Name",t.name||"unknown"),r("Path",t.project||"—"),r("PID",t.pid),r("Port",t.port),r("Uptime",`${e} min`)].join(""),n.appendChild(s)}else n.innerHTML='<div class="pg-stg-placeholder">No active instances</div>'}catch(t){console.error("[SettingsPanel] fetch error:",t),this.ref.backendCard.innerHTML=`<div class="pg-stg-placeholder" style="color:var(--sn-danger-color)">Error: ${t.message}</div>`}}}SettingsPanel.template=n,SettingsPanel.rootStyles=e,SettingsPanel.reg("pg-settings-panel");
@@ -0,0 +1 @@
1
+ export default'\n<div class="pg-stg-title">Backend</div>\n<div class="pg-stg-card" ref="backendCard"></div>\n\n<div class="pg-stg-title">Active Instances</div>\n<div ref="instanceList"></div>\n\n<div class="pg-stg-title">Actions</div>\n<div style="display:flex;gap:8px">\n<button class="pg-stg-btn" ref="refreshBtn">↻ Refresh</button>\n<button class="pg-stg-btn pg-stg-btn-danger" ref="restartBtn">⟳ Restart Server</button>\n</div>\n<div ref="restartStatus" style="margin-top:8px;font-size:11px;color:var(--sn-text-dim)"></div>\n';
@@ -0,0 +1,5 @@
1
+ import e from"@symbiotejs/symbiote";import{api as n,events as t,state as o}from"../app.js";import"../components/code-block.js";
2
+ export class CodeViewer extends e{init$={filename:"Select a file",hasFile:!1,viewMode:"compact",statsText:"",onToggleMode:()=>{this.$.viewMode="compact"===this.$.viewMode?"raw":"compact",this._showCurrentMode()}};_fileData=null;initCallback(){t.addEventListener("file-selected",e=>this._loadFile(e.detail.path))}renderCallback(){this.sub("hasFile",e=>{this.toggleAttribute("has-file",e)}),this.sub("viewMode",e=>{this.toggleAttribute("mode-raw","raw"===e)})}_getCodeBlock(){return this.querySelector("code-block")}_showCurrentMode(){if(!this._fileData)return;
3
+ const e=this._getCodeBlock();e&&(e.$.code="compact"===this.$.viewMode?this._fileData.compact:this._fileData.raw)}async _loadFile(e){this.$.filename=e,this.$.hasFile=!1,this._fileData=null,this.$.statsText="",this.$.viewMode="compact";try{const t=await n("/api/file",{path:e}),o="string"==typeof t.code?t.code:"string"==typeof t.compressed?t.compressed:t.content||JSON.stringify(t,null,2);
4
+ let s=o;try{const t=await n("/api/raw-file",{path:e});t?.content&&(s=t.content)}catch{}this._fileData={compact:o,raw:s,codeTok:t.codeTok||0,ctxTok:t.ctxTok||0,totalTok:t.totalTok||0,expanded:t.expanded||0,savings:t.savings||"0%"},t.codeTok&&t.expanded&&(()=>{const e=t.expanded,n=e>0?Math.round(100*(1-t.codeTok/e)):0,o=e>0?Math.round(100*(1-t.totalTok/e)):0;const fmt=v=>v>=0?`↓${v}%`:`↑${Math.abs(v)}%`;this.$.statsText=t.ctxTok?`${t.codeTok} (${fmt(n)}) + ${t.ctxTok} ctx = ${t.totalTok} (${fmt(o)}) → ${e} tok`:`${t.codeTok} → ${e} tok (${fmt(n)})`})();
5
+ const i=this._getCodeBlock();i&&(i.$.code=o),this.$.hasFile=!0}catch(e){const n=this._getCodeBlock();n&&(n.$.code=`// Error: ${e.message}`),this.$.hasFile=!0}}}CodeViewer.template='\n <div class="pg-code-header">\n <span class="pg-code-filename" bind="textContent: filename"></span>\n <div class="pg-code-controls">\n <span class="pg-code-stats" bind="textContent: statsText"></span>\n <button class="pg-mode-toggle" bind="onclick: onToggleMode" title="Toggle Compact/Raw view">\n <span class="material-symbols-outlined" style="font-size:14px">compress</span>\n <span class="pg-mode-label" bind="textContent: viewMode"></span>\n </button>\n </div>\n </div>\n <code-block></code-block>\n',CodeViewer.rootStyles="\n pg-code-viewer {\n display: flex;\n flex-direction: column;\n height: 100%;\n overflow: hidden;\n }\n pg-code-viewer:not([has-file]) code-block {\n display: none;\n }\n .pg-code-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 6px 12px;\n font-family: 'SF Mono', 'Fira Code', monospace;\n font-size: 11px;\n color: var(--sn-text-dim, hsl(30, 10%, 45%));\n border-bottom: 1px solid var(--sn-node-border, hsl(35, 18%, 80%));\n background: var(--sn-node-header-bg, hsl(37, 25%, 93%));\n gap: 8px;\n }\n .pg-code-filename {\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n min-width: 0;\n }\n .pg-code-controls {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-shrink: 0;\n }\n .pg-code-stats {\n font-size: 10px;\n color: var(--sn-cat-server, hsl(210, 45%, 45%));\n white-space: nowrap;\n }\n .pg-mode-toggle {\n display: flex;\n align-items: center;\n gap: 3px;\n padding: 2px 8px;\n border: 1px solid var(--sn-node-border, hsl(35, 18%, 80%));\n border-radius: 4px;\n background: var(--sn-bg, hsl(37, 30%, 91%));\n color: var(--sn-text-dim, hsl(30, 10%, 45%));\n font-family: inherit;\n font-size: 10px;\n cursor: pointer;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n transition: all 120ms ease;\n }\n .pg-mode-toggle:hover {\n background: var(--sn-node-hover, hsl(36, 22%, 88%));\n color: var(--sn-text, hsl(30, 15%, 18%));\n }\n pg-code-viewer[mode-raw] .pg-mode-toggle {\n background: hsla(210, 45%, 45%, 0.12);\n border-color: var(--sn-cat-server, hsl(210, 45%, 45%));\n color: var(--sn-cat-server, hsl(210, 45%, 45%));\n }\n code-block {\n flex: 1;\n min-height: 0;\n }\n",CodeViewer.reg("pg-code-viewer");
@@ -0,0 +1,4 @@
1
+ import n from"@symbiotejs/symbiote";import{api as t,events as e,state as i}from"../app.js";
2
+ export class CtxPanel extends n{init$={contentHTML:'<div class="pg-placeholder">Select a file to view documentation</div>',outlineHTML:""};initCallback(){e.addEventListener("file-selected",n=>{this._loadCtx(n.detail.path),this._loadOutline(n.detail.path)})}_loadOutline(n){const t=i.skeleton;if(!t)return void(this.$.outlineHTML="");
3
+ const e=t.X||{},s=t.L||{},a=e[n];if(!a||0===a.length)return void(this.$.outlineHTML="");
4
+ const o=a.map(n=>`<div class="pg-outline-item" title="${n}">\n <span class="material-symbols-outlined" style="font-size:13px">function</span>\n <span>${s[n]||n}</span>\n </div>`).join("");this.$.outlineHTML=`\n <div class="pg-outline-section">\n <div class="pg-outline-title">\n <span class="material-symbols-outlined" style="font-size:14px">account_tree</span>\n Exports · ${a.length}\n </div>\n ${o}\n </div>\n `}async _loadCtx(n){this.$.contentHTML='<div class="pg-placeholder pg-pulse">Loading docs...</div>';try{const e=await t("/api/docs",{file:n}),i=e?.docs||e?.content||"";if(!i)return void(this.$.contentHTML='<div class="pg-placeholder">No .ctx documentation</div>');this.$.contentHTML="string"==typeof i?this._formatCtx(i):`<pre class="pg-ctx-raw">${JSON.stringify(i,null,2)}</pre>`}catch{this.$.contentHTML='<div class="pg-placeholder">No documentation available</div>'}}_formatCtx(n){return n.split("\n").map(n=>{const t=n.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;");return n.startsWith("export ")||n.match(/^\s+\w/)?`<div class="pg-ctx-sig">${t}</div>`:n.startsWith("- [x]")?`<div class="pg-ctx-test passed">✅ ${t.slice(5)}</div>`:n.startsWith("- [ ]")?`<div class="pg-ctx-test pending">⬜ ${t.slice(5)}</div>`:n.trim()?`<div class="pg-ctx-desc">${t}</div>`:""}).join("")}}CtxPanel.template='\n <div class="pg-ctx-outline" bind="innerHTML: outlineHTML"></div>\n <div class="pg-ctx-body" bind="innerHTML: contentHTML"></div>\n',CtxPanel.rootStyles="\n pg-ctx-panel {\n display: flex;\n flex-direction: column;\n height: 100%;\n overflow-y: auto;\n font-size: 12px;\n padding: 0;\n font-family: var(--sn-font, Georgia, serif);\n }\n .pg-ctx-outline { padding: 0; }\n .pg-ctx-body { padding: 8px; }\n .pg-outline-section {\n border-bottom: 1px solid var(--sn-node-border, hsl(228, 10%, 28%));\n padding: 8px;\n }\n .pg-outline-title {\n display: flex;\n align-items: center;\n gap: 6px;\n font-size: 11px;\n font-weight: 600;\n color: var(--sn-text-dim);\n padding: 4px 4px 6px;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n }\n .pg-outline-item {\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 4px 8px;\n cursor: default;\n border-radius: 4px;\n font-family: 'SF Mono', 'Fira Code', monospace;\n font-size: 11px;\n color: var(--sn-cat-server, hsl(210, 45%, 45%));\n transition: background 80ms ease;\n }\n .pg-outline-item:hover {\n background: var(--sn-node-hover, hsl(228, 14%, 22%));\n }\n .pg-ctx-sig {\n font-family: 'SF Mono', monospace;\n font-size: 11px;\n padding: 6px 8px;\n margin: 4px 0;\n background: var(--sn-bg, hsl(37, 30%, 91%));\n border-radius: 4px;\n border-left: 3px solid var(--sn-cat-server, hsl(210, 45%, 45%));\n color: var(--sn-text-dim, hsl(30, 10%, 45%));\n }\n .pg-ctx-desc { padding: 4px 0; color: var(--sn-text); }\n .pg-ctx-test { padding: 3px 0; font-size: 12px; }\n .pg-ctx-raw { font-size: 11px; color: var(--sn-text-dim); }\n .pg-placeholder { color: var(--sn-text-dim); text-align:center; padding:30px; font-style:italic; }\n .pg-pulse { animation: pulse 1.5s ease infinite; }\n @keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.4} }\n",CtxPanel.reg("pg-ctx-panel");
@@ -0,0 +1,6 @@
1
+ import s from"@symbiotejs/symbiote";import{api as e,state as p,events as n,emit as t}from"../app.js";
2
+ export class DepGraph extends s{init$={contentHTML:'<div class="pg-placeholder">Select a file to see dependencies</div>'};initCallback(){n.addEventListener("file-selected",s=>this._loadDeps(s.detail.path)),n.addEventListener("skeleton-loaded",()=>this._renderOverview()),p.skeleton&&this._renderOverview(),this.addEventListener("click",s=>{const e=s.target.closest("[data-file]");if(e){const s=e.dataset.file;p.activeFile=s,t("file-selected",{path:s})}})}_renderOverview(){if(!p.skeleton)return;
3
+ const s=p.skeleton.s||{},e=p.skeleton.X||{},n=(Object.values(p.skeleton.n||{}),['<div class="pg-graph-stats">']),t=s.files||Object.keys(e).length,a=s.functions||0,i=s.classes||0,o=Object.values(e).reduce((s,e)=>s+e.length,0);n.push(`<div class="pg-stat"><span class="pg-stat-val">${t}</span><span class="pg-stat-label">Files</span></div>`),n.push(`<div class="pg-stat"><span class="pg-stat-val">${a}</span><span class="pg-stat-label">Functions</span></div>`),n.push(`<div class="pg-stat"><span class="pg-stat-val">${i}</span><span class="pg-stat-label">Classes</span></div>`),n.push(`<div class="pg-stat"><span class="pg-stat-val">${o}</span><span class="pg-stat-label">Exports</span></div>`),n.push("</div>");
4
+ const l=[];for(const[s,p]of Object.entries(e))l.push({file:s,exports:p.length});l.sort((s,e)=>e.exports-s.exports),n.push('<div class="pg-dep-section"><div class="pg-dep-label">Most Exported</div>');for(const s of l.slice(0,10))n.push(`<div class="pg-dep-item pg-dep-nav" data-file="${s.file}">\n <span class="pg-dep-file">${s.file}</span>\n <span class="pg-dep-badge">${s.exports} exports</span>\n </div>`);n.push("</div>"),this.$.contentHTML=n.join("")}async _loadDeps(s){this.$.contentHTML='<div class="pg-placeholder pg-pulse">Loading dependencies...</div>';try{const e=Object.values(p.skeleton?.n||{}).find(e=>e.f===s);if(!e)return void(this.$.contentHTML='<div class="pg-placeholder">File not in graph</div>');
5
+ const n=[];if(n.push(`<h3 class="pg-dep-title">${s}</h3>`),e.i?.length){n.push('<div class="pg-dep-section"><div class="pg-dep-label">Imports</div>');for(const s of e.i){const e=s.s||s,t=Object.values(p.skeleton.n).find(s=>s.f===e||s.f?.endsWith("/"+e)||s.f?.endsWith(e+".js"));t?n.push(`<div class="pg-dep-item pg-dep-nav pg-dep-import" data-file="${t.f}">\n <span class="material-symbols-outlined" style="font-size:14px">arrow_back</span>\n ${e}\n </div>`):n.push(`<div class="pg-dep-item pg-dep-import">\n <span class="material-symbols-outlined" style="font-size:14px">arrow_back</span>\n ${e}\n <span class="pg-dep-ext">external</span>\n </div>`)}n.push("</div>")}const t=[];for(const e of Object.values(p.skeleton?.n||{})){if(e.f===s)continue;
6
+ const p=e.i||[];for(const n of p){const p=n.s||n;if(p===s||s.endsWith(p)||s.endsWith(p+".js")){t.push(e.f);break}}}if(t.length){n.push('<div class="pg-dep-section"><div class="pg-dep-label">Imported By</div>');for(const s of t)n.push(`<div class="pg-dep-item pg-dep-nav pg-dep-importer" data-file="${s}">\n <span class="material-symbols-outlined" style="font-size:14px">arrow_forward</span>\n ${s}\n </div>`);n.push("</div>")}if(e.e?.length){n.push('<div class="pg-dep-section"><div class="pg-dep-label">Exports</div>');for(const s of e.e)n.push(`<div class="pg-dep-item pg-dep-export">→ ${s}</div>`);n.push("</div>")}if(e.fn?.length){n.push('<div class="pg-dep-section"><div class="pg-dep-label">Functions</div>');for(const s of e.fn){const e=s.n||s,p=s.p?`(${s.p})`:"()";n.push(`<div class="pg-dep-item pg-dep-fn">ƒ ${e}${p}</div>`)}n.push("</div>")}if(e.c?.length){n.push('<div class="pg-dep-section"><div class="pg-dep-label">Classes</div>');for(const s of e.c){const e=s.n||s,p=s.x?` extends ${s.x}`:"",t=s.m?.map(s=>s.n||s).join(", ")||"";n.push(`<div class="pg-dep-item pg-dep-class">◆ ${e}${p}</div>`),t&&n.push(`<div class="pg-dep-item pg-dep-methods">&nbsp;&nbsp;methods: ${t}</div>`)}n.push("</div>")}this.$.contentHTML=n.join("")}catch(s){this.$.contentHTML=`<div class="pg-placeholder" style="color:var(--sn-danger-color)">Error: ${s.message}</div>`}}}DepGraph.template='<div class="pg-graph-body" bind="innerHTML: contentHTML"></div>',DepGraph.rootStyles="\n pg-dep-graph { display:block; height:100%; overflow-y:auto; padding:12px; font-size:12px; font-family:var(--sn-font, Georgia, serif); }\n .pg-graph-stats { display:flex; gap:8px; margin-bottom:16px; flex-wrap:wrap; }\n .pg-stat {\n background: var(--sn-node-bg, hsl(40, 33%, 96%));\n border: 1px solid var(--sn-node-border, hsl(35, 18%, 80%));\n border-radius: 6px;\n padding: 10px 14px;\n text-align: center;\n flex: 1;\n min-width: 60px;\n }\n .pg-stat-val { display:block; font-size:22px; font-weight:700; color:var(--sn-cat-server, hsl(210, 45%, 45%)); font-family:monospace; }\n .pg-stat-label { font-size:9px; text-transform:uppercase; color:var(--sn-text-dim); letter-spacing:0.5px; }\n .pg-dep-title { font-size:13px; color:var(--sn-text); margin:0 0 12px 0; font-family:monospace; }\n .pg-dep-section { margin-bottom:12px; }\n .pg-dep-label { font-size:10px; text-transform:uppercase; letter-spacing:0.5px; color:var(--sn-text-dim); margin-bottom:4px; font-weight:600; }\n .pg-dep-item {\n padding: 4px 8px;\n border-radius: 4px;\n font-family: 'SF Mono', 'Fira Code', monospace;\n font-size: 11px;\n display: flex;\n align-items: center;\n gap: 6px;\n }\n .pg-dep-nav {\n cursor: pointer;\n transition: background 120ms ease;\n }\n .pg-dep-nav:hover {\n background: var(--sn-node-hover, hsl(36, 22%, 88%));\n }\n .pg-dep-import { color: hsl(210, 45%, 45%); }\n .pg-dep-importer { color: hsl(30, 55%, 50%); }\n .pg-dep-export { color: hsl(150, 40%, 38%); }\n .pg-dep-fn { color: hsl(250, 35%, 50%); }\n .pg-dep-class { color: hsl(330, 40%, 50%); }\n .pg-dep-methods { color: var(--sn-text-dim); font-size:10px; }\n .pg-dep-ext {\n font-size: 9px;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n opacity: 0.5;\n margin-left: auto;\n }\n .pg-dep-file { flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }\n .pg-dep-badge {\n font-size: 10px;\n color: var(--sn-text-dim);\n white-space: nowrap;\n }\n .pg-placeholder { color:var(--sn-text-dim); text-align:center; padding:30px; font-style:italic; }\n .pg-pulse { animation:pulse 1.5s ease infinite; }\n @keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.4} }\n",DepGraph.reg("pg-dep-graph");
@@ -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");