project-graph-mcp 2.1.2 → 2.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/rules/test-rules.json +15 -0
- package/src/.project-graph-cache.json +1 -1
- package/src/analysis/analysis-cache.js +3 -1
- package/src/analysis/complexity.js +9 -13
- package/src/analysis/custom-rules.js +16 -35
- package/src/analysis/db-analysis.js +2 -6
- package/src/analysis/dead-code.js +8 -18
- package/src/analysis/full-analysis.js +9 -17
- package/src/analysis/jsdoc-checker.js +11 -23
- package/src/analysis/jsdoc-generator.js +8 -9
- package/src/analysis/similar-functions.js +8 -15
- package/src/analysis/test-annotations.js +12 -20
- package/src/analysis/type-checker.js +5 -7
- package/src/analysis/undocumented.js +10 -13
- package/src/cli/cli-handlers.js +4 -3
- package/src/compact/ai-context.js +2 -2
- package/src/compact/compact-migrate.js +8 -16
- package/src/compact/compact.js +3 -5
- package/src/compact/compress.js +7 -13
- package/src/compact/ctx-resolver.js +5 -0
- package/src/compact/ctx-to-jsdoc.js +13 -28
- package/src/compact/doc-dialect.js +18 -29
- package/src/compact/expand.js +10 -36
- package/src/compact/jsdoc-builder.js +5 -0
- package/src/compact/mode-config.js +6 -6
- package/src/compact/split-declarations.js +2 -0
- package/src/compact/validate-pipeline.js +7 -8
- package/src/core/event-bus.js +2 -1
- package/src/core/file-walker.js +4 -0
- package/src/core/filters.js +6 -5
- package/src/core/graph-builder.js +4 -11
- package/src/core/parser.js +19 -29
- package/src/core/utils.js +2 -0
- package/src/lang/lang-sql.js +7 -20
- package/src/mcp/mcp-server.js +2 -3
- package/src/mcp/tool-defs.js +1 -1
- package/src/mcp/tools.js +13 -21
- package/src/network/backend-lifecycle.js +15 -18
- package/src/network/local-gateway.js +10 -22
- package/src/network/mdns.js +5 -11
- package/src/network/server.js +1 -2
- package/src/network/web-server.js +7 -33
- package/web/app.js +19 -14
- package/web/components/code-block.js +1 -0
- package/web/components/quick-open.js +1 -0
- package/web/dashboard-state.js +1 -0
- package/web/panels/ActionBoard/ActionBoard.css.js +1 -0
- package/web/panels/ActionBoard/ActionBoard.js +5 -4
- package/web/panels/ActionBoard/ActionBoard.tpl.js +1 -0
- package/web/panels/EventItem/EventItem.css.js +1 -0
- package/web/panels/EventItem/EventItem.js +4 -4
- package/web/panels/EventItem/EventItem.tpl.js +1 -0
- package/web/panels/ProjectItem/ProjectItem.css.js +2 -1
- package/web/panels/ProjectItem/ProjectItem.js +3 -4
- package/web/panels/ProjectItem/ProjectItem.tpl.js +2 -1
- package/web/panels/ProjectList/ProjectList.css.js +1 -0
- package/web/panels/ProjectList/ProjectList.js +5 -4
- package/web/panels/ProjectList/ProjectList.tpl.js +1 -0
- package/web/panels/SettingsPanel/SettingsPanel.css.js +1 -0
- package/web/panels/SettingsPanel/SettingsPanel.js +2 -3
- package/web/panels/SettingsPanel/SettingsPanel.tpl.js +1 -0
- package/web/panels/code-viewer.js +1 -0
- package/web/panels/ctx-panel.js +1 -0
- package/web/panels/dep-graph.js +1 -0
- package/web/panels/file-tree.js +4 -188
- package/web/panels/health-panel.js +1 -0
- package/web/panels/live-monitor.js +1 -0
- package/web/state.js +7 -10
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
import s from"./ActionBoard.css.js";
|
|
3
|
-
import
|
|
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()}}
|
|
1
|
+
// @ctx .context/web/panels/ActionBoard/ActionBoard.ctx
|
|
2
|
+
import t from"@symbiotejs/symbiote";import{state as e,events as o}from"../../dashboard-state.js";import s from"./ActionBoard.css.js";import n from"./ActionBoard.tpl.js";
|
|
3
|
+
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()}}
|
|
5
|
+
ActionBoard.template=n,ActionBoard.rootStyles=s,ActionBoard.reg("pg-action-board");
|
|
@@ -1 +1,2 @@
|
|
|
1
|
+
// @ctx .context/web/panels/EventItem/EventItem.css.ctx
|
|
1
2
|
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";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
import e from"./EventItem.css.js";
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
// @ctx .context/web/panels/EventItem/EventItem.ctx
|
|
2
|
+
import t from"@symbiotejs/symbiote";import e from"./EventItem.css.js";import s from"./EventItem.tpl.js";
|
|
3
|
+
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"))}}}
|
|
4
|
+
EventItem.template=s,EventItem.rootStyles=e,EventItem.reg("pg-event-item");
|
|
@@ -1 +1,2 @@
|
|
|
1
|
+
// @ctx .context/web/panels/EventItem/EventItem.tpl.ctx
|
|
1
2
|
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';
|
|
@@ -1 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
// @ctx .context/web/panels/ProjectItem/ProjectItem.css.ctx
|
|
2
|
+
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.delete-btn {\n margin-left: auto;\n background: none;\n border: none;\n color: var(--sn-fg-muted);\n font-size: 16px;\n cursor: pointer;\n padding: 0 4px;\n line-height: 1;\n opacity: 0;\n transition: opacity 0.2s, color 0.2s;\n}\n.card:hover .delete-btn {\n opacity: 1;\n}\n.delete-btn:hover {\n color: #ef5350;\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";
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
import t from"./ProjectItem.css.js";
|
|
3
|
-
|
|
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(()=>{})}})}}
|
|
1
|
+
// @ctx .context/web/panels/ProjectItem/ProjectItem.ctx
|
|
2
|
+
import e from"@symbiotejs/symbiote";import t from"./ProjectItem.css.js";import r from"./ProjectItem.tpl.js";
|
|
3
|
+
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(()=>{})}});this.ref.deleteBtn.addEventListener("click",async(ev)=>{ev.preventDefault();ev.stopPropagation();const prefix=this.$.prefix;if(!prefix||!confirm(`Remove ${this.$.projectName}?`))return;await fetch("/api/remove-project",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({route:prefix})});this.remove()})}}
|
|
5
4
|
ProjectItem.template=r,ProjectItem.rootStyles=t,ProjectItem.reg("pg-project-item");
|
|
@@ -1 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
// @ctx .context/web/panels/ProjectItem/ProjectItem.tpl.ctx
|
|
2
|
+
export default'\n<div class="card">\n <div class="title"><a ref="link">{{projectName}}</a><span class="token-badge" ref="tokenBadge"></span><button ref="deleteBtn" class="delete-btn" title="Remove project">×</button></div>\n <div class="path">{{projectPath}}</div>\n</div>\n';
|
|
@@ -1 +1,2 @@
|
|
|
1
|
+
// @ctx .context/web/panels/ProjectList/ProjectList.css.ctx
|
|
1
2
|
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";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
import r from"./ProjectList.css.js";
|
|
3
|
-
import
|
|
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})}}
|
|
1
|
+
// @ctx .context/web/panels/ProjectList/ProjectList.ctx
|
|
2
|
+
import t from"@symbiotejs/symbiote";import{state as s,events as e}from"../../dashboard-state.js";import r from"./ProjectList.css.js";import o from"./ProjectList.tpl.js";
|
|
3
|
+
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})}}
|
|
5
|
+
ProjectList.template=o,ProjectList.rootStyles=r,ProjectList.reg("pg-project-list");
|
|
@@ -1 +1,2 @@
|
|
|
1
|
+
// @ctx .context/web/panels/SettingsPanel/SettingsPanel.css.ctx
|
|
1
2
|
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";
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
|
|
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>`}
|
|
1
|
+
// @ctx .context/web/panels/SettingsPanel/SettingsPanel.ctx
|
|
2
|
+
import t from"@symbiotejs/symbiote";import e from"./SettingsPanel.css.js";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
3
|
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
4
|
let e=0;
|
|
6
5
|
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("");
|
|
@@ -1 +1,2 @@
|
|
|
1
|
+
// @ctx .context/web/panels/SettingsPanel/SettingsPanel.tpl.ctx
|
|
1
2
|
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';
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// @ctx .context/web/panels/code-viewer.ctx
|
|
1
2
|
import e from"@symbiotejs/symbiote";import{api as n,events as t,state as o}from"../app.js";import"../components/code-block.js";
|
|
2
3
|
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
4
|
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);
|
package/web/panels/ctx-panel.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// @ctx .context/web/panels/ctx-panel.ctx
|
|
1
2
|
import n from"@symbiotejs/symbiote";import{api as t,events as e,state as i}from"../app.js";
|
|
2
3
|
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
4
|
const e=t.X||{},s=t.L||{},a=e[n];if(!a||0===a.length)return void(this.$.outlineHTML="");
|
package/web/panels/dep-graph.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// @ctx .context/web/panels/dep-graph.ctx
|
|
1
2
|
import s from"@symbiotejs/symbiote";import{api as e,state as p,events as n,emit as t}from"../app.js";
|
|
2
3
|
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
4
|
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>");
|
package/web/panels/file-tree.js
CHANGED
|
@@ -1,188 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
class FileTree extends e
|
|
4
|
-
.
|
|
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");
|
|
1
|
+
// @ctx .context/web/panels/file-tree.ctx
|
|
2
|
+
import e from"@symbiotejs/symbiote";import{api as t,state as n,events as s,emit as r}from"../app.js";
|
|
3
|
+
export class FileTree extends e{init$={treeHTML:'<div class="pg-placeholder">Loading files...</div>',filterText:"",onFilterInput:e=>{this.$.filterText=e.target.value.toLowerCase(),this._applyFilter()},onCollapseAll:()=>{this._collapseAll()}};initCallback(){this._expandedDirs=new Set;try{const e=localStorage.getItem("pg-tree-expanded");if(e){const t=JSON.parse(e);Array.isArray(t)&&(this._expandedDirs=new Set(t))}}catch(e){}s.addEventListener("skeleton-loaded",e=>{this._renderTree(e.detail),n.activeFile&&requestAnimationFrame(()=>this._highlightFile(n.activeFile))}),n.skeleton&&this._renderTree(n.skeleton),s.addEventListener("file-selected",e=>{e.detail.fromRoute&&requestAnimationFrame(()=>this._highlightFile(e.detail.path))}),this.addEventListener("click",e=>{const t=e.target.closest(".pg-tree-file");if(t)return this.querySelectorAll(".pg-tree-file.active").forEach(e=>e.classList.remove("active")),t.classList.add("active"),n.activeFile=t.dataset.file,void r("file-selected",{path:t.dataset.file});const s=e.target.closest(".pg-tree-dir");if(s){const e=s.dataset.dir;null!=e&&this._toggleDir(e)}})}_toggleDir(e){this._expandedDirs.has(e)?this._expandedDirs.delete(e):this._expandedDirs.add(e),this._saveExpandedState(),this._updateDirDOM(e)}_saveExpandedState(){localStorage.setItem("pg-tree-expanded",JSON.stringify(Array.from(this._expandedDirs)))}_updateDirDOM(e){const t=this.querySelector(`.pg-tree-dir[data-dir="${CSS.escape(e)}"]`),n=this.querySelector(`.pg-tree-children[data-dir="${CSS.escape(e)}"]`);if(t&&n){const s=this._expandedDirs.has(e),r=t.querySelector(".pg-chevron");r&&(r.textContent=s?"expand_more":"chevron_right"),s?n.removeAttribute("hidden"):n.setAttribute("hidden","")}}_collapseAll(){this._expandedDirs.clear(),this._saveExpandedState(),this.querySelectorAll(".pg-tree-dir").forEach(e=>{this._updateDirDOM(e.dataset.dir)})}_highlightFile(e){const t=this.querySelector(`.pg-tree-file[data-file="${CSS.escape(e)}"]`);if(t){this.querySelectorAll(".pg-tree-file.active").forEach(e=>e.classList.remove("active")),t.classList.add("active");const n=e.split("/");n.pop();let s=!1;for(let e=1;e<=n.length;e++){const t=n.slice(0,e).join("/");this._expandedDirs.has(t)||(this._expandedDirs.add(t),this._updateDirDOM(t),s=!0)}s&&this._saveExpandedState(),t.scrollIntoView({block:"center",behavior:"smooth"})}}_renderTree(e){if(!e)return void(this.$.treeHTML='<div class="pg-placeholder">No files found</div>');const t=new Map,n=e.n||{};for(const e of Object.values(n))if(e.f){const n=t.get(e.f)||{exports:0,classes:0};n.classes++,t.set(e.f,n)}const s=e.X||{};for(const[e,n]of Object.entries(s)){const s=t.get(e)||{exports:0,classes:0};s.exports=n.length,t.set(e,s)}const r=e.f||{};for(const[e,n]of Object.entries(r))for(const s of n){const n="./"===e?s:`${e}${s}`;t.has(n)||t.set(n,{exports:0,classes:0})}const i=e.a||{};for(const[e,n]of Object.entries(i))for(const s of n){const n="./"===e?s:`${e}${s}`;t.has(n)||t.set(n,{exports:0,classes:0,nonSource:!0})}if(0===t.size)return void(this.$.treeHTML='<div class="pg-placeholder">No files found</div>');const l={children:{},files:[]};for(const[e,n]of t){const t=e.split("/"),s=t.pop();let r=l;for(const e of t)r.children[e]||(r.children[e]={children:{},files:[]}),r=r.children[e];r.files.push({f:e,name:s,...n})}const o=(e,t,n)=>{const s=[],r=Object.keys(e.children).sort(),i=e.files.sort((e,t)=>e.name.localeCompare(t.name)),l=16*n;for(const i of r){const r=t?`${t}/${i}`:i,a=this._expandedDirs&&this._expandedDirs.has(r),d=a?"expand_more":"chevron_right",p=a?"":" hidden";s.push(`<div class="pg-tree-dir" data-dir="${r}" style="padding-left:${l+6}px"><span class="material-symbols-outlined pg-chevron" style="font-size:16px">${d}</span> <span class="material-symbols-outlined" style="font-size:16px">folder</span> ${i}</div>`),s.push(`<div class="pg-tree-children" data-dir="${r}"${p}>`),s.push(o(e.children[i],r,n+1)),s.push("</div>")}for(const e of i){const t=FileTree._getFileIcon(e.name),n=[];e.exports>0&&n.push(`${e.exports}f`),e.classes>0&&n.push(`${e.classes}c`);const r=n.length>0?`<span class="pg-badge">${n.join(" ")}</span>`:"",i=e.nonSource?" pg-non-source":"";s.push(`<div class="pg-tree-file${i}" data-file="${e.f}" style="padding-left:${l+24}px"><span class="material-symbols-outlined" style="font-size:14px">${t}</span> ${e.name}${r}</div>`)}return s.join("")};this.$.treeHTML=o(l,"",0)}static _getFileIcon(e){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"}_applyFilter(){const e=this.$.filterText;let t=!1;this.querySelectorAll(".pg-tree-file").forEach(n=>{const s=!e||n.dataset.file.toLowerCase().includes(e);if(n.hidden=!s,e&&s){const e=n.dataset.file.split("/");e.pop();for(let n=1;n<=e.length;n++){const s=e.slice(0,n).join("/");this._expandedDirs.has(s)||(this._expandedDirs.add(s),t=!0,this._updateDirDOM(s))}}}),t&&this._saveExpandedState(),e?this.querySelectorAll(".pg-tree-dir").forEach(e=>{const t=e.dataset.dir,n=this.querySelector(`.pg-tree-children[data-dir="${CSS.escape(t)}"]`);if(!n)return;let s=!1;n.querySelectorAll(".pg-tree-file").forEach(e=>{e.hidden||(s=!0)}),n.querySelectorAll(".pg-tree-children").forEach(e=>{e.querySelector(".pg-tree-file:not([hidden])")&&(s=!0)}),e.hidden=!s}):this.querySelectorAll(".pg-tree-dir").forEach(e=>{e.hidden=!1})}}
|
|
4
|
+
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',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",FileTree.reg("pg-file-tree");
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// @ctx .context/web/panels/health-panel.ctx
|
|
1
2
|
import e from"@symbiotejs/symbiote";import{api as t,state as n,events as s}from"../app.js";
|
|
2
3
|
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
4
|
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");
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// @ctx .context/web/panels/live-monitor.ctx
|
|
1
2
|
import n from"@symbiotejs/symbiote";import{events as o}from"../app.js";
|
|
2
3
|
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
4
|
const o=this._events.slice(0,100).map(n=>{if("tool_call"===n.type){const o=JSON.stringify(n.args||{}).slice(0,80);return`<div class="pg-mon-event pg-mon-call">\n <span class="pg-mon-arrow">→</span>\n <span class="pg-mon-tool">${n.tool}</span>\n <span class="pg-mon-args">${this._esc(o)}</span>\n <span class="pg-mon-time">${this._formatTime(n.ts)}</span>\n </div>`}return`<div class="pg-mon-event pg-mon-result ${n.success?"pg-mon-ok":"pg-mon-err"}">\n <span class="pg-mon-arrow">←</span>\n <span class="pg-mon-tool">${n.tool}</span>\n <span class="pg-mon-duration">${n.duration_ms}ms</span>\n <span class="pg-mon-time">${this._formatTime(n.ts)}</span>\n </div>`}).join("");this.$.eventsHTML=o||'<div class="pg-placeholder">Waiting for tool calls...</div>'}_esc(n){return n.replace(/</g,"<").replace(/>/g,">")}_formatTime(n){return n?new Date(n).toLocaleTimeString("en",{hour12:!1,hour:"2-digit",minute:"2-digit",second:"2-digit"}):""}}LiveMonitor.template='\n <div class="pg-mon-header">\n <span>Events: </span><span bind="textContent: eventCount"></span>\n </div>\n <div class="pg-mon-body" bind="innerHTML: eventsHTML"></div>\n',LiveMonitor.rootStyles="\n pg-live-monitor { display:flex; flex-direction:column; height:100%; overflow:hidden; font-size:12px; font-family:var(--sn-font, Georgia, serif); }\n .pg-mon-header { padding:6px 12px; border-bottom:1px solid var(--sn-node-border); background:var(--sn-node-header-bg); font-size:11px; color:var(--sn-text-dim); }\n .pg-mon-body { flex:1; overflow-y:auto; padding:4px; }\n .pg-mon-event {\n display:flex; align-items:center; gap:8px;\n padding:4px 8px; border-radius:4px; font-family:monospace; font-size:11px;\n animation: slideIn 0.15s ease;\n }\n .pg-mon-event:hover { background:var(--sn-node-hover); }\n .pg-mon-arrow { font-weight:bold; width:14px; }\n .pg-mon-call .pg-mon-arrow { color: var(--sn-cat-server, hsl(210, 45%, 45%)); }\n .pg-mon-ok .pg-mon-arrow { color: var(--sn-success-color, hsl(150, 55%, 38%)); }\n .pg-mon-err .pg-mon-arrow { color: var(--sn-danger-color, hsl(4, 55%, 48%)); }\n .pg-mon-tool { color:var(--sn-text); font-weight:600; min-width:100px; }\n .pg-mon-args { color:var(--sn-text-dim); flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }\n .pg-mon-duration { color: hsl(250, 35%, 50%); min-width:50px; text-align:right; }\n .pg-mon-time { color:var(--sn-text-dim); font-size:10px; min-width:60px; text-align:right; }\n .pg-placeholder { color:var(--sn-text-dim); text-align:center; padding:30px; font-style:italic; }\n @keyframes slideIn { from{opacity:0;transform:translateY(-4px)} to{opacity:1;transform:translateY(0)} }\n",LiveMonitor.reg("pg-live-monitor");
|
package/web/state.js
CHANGED
|
@@ -2,16 +2,13 @@
|
|
|
2
2
|
const e=new URL(".",import.meta.url).href;
|
|
3
3
|
export const state={project:null,skeleton:null,events:[],connected:!1};
|
|
4
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)}
|
|
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)}
|
|
6
|
+
const n=new Set;
|
|
6
7
|
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
|
-
|
|
8
|
+
function o(e,n){t.get(e)?.forEach(t=>t(n,e));const o=e.indexOf(".");if(o>0){const n=e.slice(0,o);t.get(n)?.forEach(e=>e(state[n],n))}t.get("*")?.forEach(t=>t(n,e))}
|
|
9
|
+
let r=1;
|
|
9
10
|
const c=new Map;
|
|
10
|
-
export function call(e,t={}){return new Promise((n,
|
|
11
|
-
|
|
12
|
-
function
|
|
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=()=>{}}
|
|
11
|
+
export function call(e,t={}){return new Promise((n,o)=>{if(!s||s.readyState!==WebSocket.OPEN)return void o(new Error("WebSocket not connected"));const a=r++;c.set(a,{resolve:n,reject:o}),s.send(JSON.stringify({jsonrpc:"2.0",id:a,method:"tool",params:{name:e,args:t}})),setTimeout(()=>{c.has(a)&&(c.delete(a),o(new Error(`Tool call timeout: ${e}`)))},3e4)})}
|
|
12
|
+
let s=null,a=null;
|
|
13
|
+
export function connect(){if(s)return;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=>function(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)):function(e,t){const n=e.split(".");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)}(t.params.path,t.params.value):function(e){Object.assign(state,e),state.connected=!0,o("*",state);for(const t of Object.keys(e))o(t,e[t])}(t.params.state)}(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
14
|
export function disconnect(){a&&(clearTimeout(a),a=null),s&&(s.close(),s=null)}
|