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