project-graph-mcp 2.1.2 → 2.1.4
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/GUIDE.md +237 -0
- package/package.json +2 -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,34 +1,8 @@
|
|
|
1
1
|
// @ctx .context/src/network/web-server.ctx
|
|
2
|
-
import e from"node:http";
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
const i=o.extname(r),l=u[i]||"application/octet-stream",d=t.readFileSync(r);n.writeHead(200,{"Content-Type":l,"Cache-Control":"no-cache, no-store, must-revalidate"}),n.end(d)}
|
|
10
|
-
function computeWSAccept(e){return n.createHash("sha1").update(e+"258EAFA5-E914-47DA-95CA-5AB5ADF35C70").digest("base64")}
|
|
11
|
-
function encodeWSFrame(e){const t=Buffer.from(e,"utf8"),o=t.length;
|
|
12
|
-
let n;return o<126?(n=Buffer.alloc(2),n[0]=129,n[1]=o):o<65536?(n=Buffer.alloc(4),n[0]=129,n[1]=126,n.writeUInt16BE(o,2)):(n=Buffer.alloc(10),n[0]=129,n[1]=127,n.writeBigUInt64BE(BigInt(o),2)),Buffer.concat([n,t])}
|
|
13
|
-
function decodeWSFrame(e){if(e.length<2)return null;
|
|
14
|
-
const t=15&e[0],o=!!(128&e[1]);
|
|
15
|
-
let n=127&e[1],a=2;if(126===n){if(e.length<4)return null;n=e.readUInt16BE(2),a=4}else if(127===n){if(e.length<10)return null;n=Number(e.readBigUInt64BE(2)),a=10}if(o){if(e.length<a+4+n)return null;
|
|
16
|
-
const o=e.slice(a,a+4);a+=4;
|
|
17
|
-
const s=e.slice(a,a+n);for(let e=0;e<s.length;e++)s[e]^=o[e%4];return{opcode:t,data:s.toString("utf8"),totalLen:a+n}}return e.length<a+n?null:{opcode:t,data:e.slice(a,a+n).toString("utf8"),totalLen:a+n}}
|
|
18
|
-
export function startWebServer(t,a){const d=r(()=>{}),p=o.basename(o.resolve(t))||"root";
|
|
19
|
-
let m=1;
|
|
20
|
-
const h=o.resolve(t),u=n.createHash("md5").update(h).digest("hex"),g=parseInt(u.slice(0,4),16)%360,f={project:{name:p,path:h,color:`hsl(${g}, 65%, 55%)`,agents:0,pid:process.pid},skeleton:null,events:[]};function broadcastRPC(e,t){const o=JSON.stringify({jsonrpc:"2.0",method:e,params:t});for(const e of y)try{e.send(o)}catch{y.delete(e)}}
|
|
21
|
-
function patchState(e,t){const o=e.split(".");
|
|
22
|
-
let n=f;for(let e=0;e<o.length-1;e++)n=n[o[e]];n[o[o.length-1]]=t,broadcastRPC("patch",{path:e,value:t})}
|
|
23
|
-
async function ensureSkeleton(){if(!f.skeleton)try{f.skeleton=await d.executeTool("get_skeleton",{path:t})}catch{}return f.skeleton}const w=new Map,y=new Set;
|
|
24
|
-
let S=null;function hasActiveClients(){return w.size>0||y.size>0}
|
|
25
|
-
function resetShutdownTimer(){S&&(clearTimeout(S),S=null)}
|
|
26
|
-
function startShutdownTimer(){hasActiveClients()||(resetShutdownTimer(),S=setTimeout(()=>{hasActiveClients()||(console.log("[project-graph] No clients for 15 min — shutting down."),process.exit(0))},9e5))}
|
|
27
|
-
function touchActivity(){resetShutdownTimer(),startShutdownTimer()}
|
|
28
|
-
async function handleAPI(e,a,s,r){try{let c; const i=a.get("path")||t;switch(e){case"/api/skeleton":c=await d.executeTool("get_skeleton",{path:i});break;case"/api/file":{const e=a.get("path");if(e){const{resolve:n,basename:a,extname:s}=await import("path"),{readFileSync:r,existsSync:i}=await import("fs"),d=n(t,e),p=a(e,s(e))+".ctx",m=o.resolve(t,".context",o.dirname(e),p),h=i(m)?r(m,"utf-8"):null,u=await l(d,h),g=Math.ceil(u.original/4),f=Math.ceil(u.decompiled/4),K=h?Math.ceil(h.length/4):0,T=g+K,w=f>0?Math.round(100*(1-T/f)):0;c={code:u.code,file:e,injected:u.injected,codeTok:g,ctxTok:K,totalTok:T,expanded:f,savings:w+"%"}}else c={code:"// No file specified",file:""};break}case"/api/raw-file":{const e=a.get("path");try{const{readFileSync:o}=await import("fs"),{resolve:n,relative:a}=await import("path");c={content:o(n(t,e),"utf-8"),file:e}}catch(t){c={content:`// Cannot read: ${t.message}`,file:e}}break}case"/api/compression-stats":{const{readdirSync:e,statSync:n,readFileSync:a,existsSync:g}=await import("fs"),{join:s,extname:r,basename:x,dirname:q,relative:z}=await import("path"),$ext=new Set([".js",".mjs"]),d=[],p=["node_modules",".git","vendor",".context",".expanded","web"];(function walk(t){try{for(const o of e(t)){if(o.startsWith("."))continue;const e=s(t,o);n(e).isDirectory()?p.includes(o)||walk(e):$ext.has(r(o))&&d.push(e)}}catch{}})(o.resolve(t,"src"));let m=0,u=0,C=0,E=0;const B=o.resolve(t);for(const e of d){try{m+=n(e).size;const r=z(B,e),s=x(r,".js"),v=o.resolve(B,".context",q(r),s+".ctx");let ct=0;if(g(v)){ct=n(v).size;C+=ct}try{const h=g(v)?a(v,"utf-8"):null,ex=await l(e,h);E+=ex.decompiled}catch{E+=n(e).size*1.3}}catch{continue}u++}c={files:u,codeTok:Math.ceil(m/4),ctxTok:Math.ceil(C/4),totalTok:Math.ceil((m+C)/4),expanded:Math.ceil(E/4)};break}case"/api/docs":c=await d.executeTool("docs",{action:"get",path:i,file:a.get("file")});break;case"/api/analysis":c=await d.executeTool("analyze",{action:"full_analysis",path:i});break;case"/api/analysis-summary":c=await d.executeTool("analyze",{action:"analysis_summary",path:i});break;case"/api/deps":c=await d.executeTool("navigate",{action:"deps",symbol:a.get("symbol")});break;case"/api/usages":c=await d.executeTool("navigate",{action:"usages",symbol:a.get("symbol")});break;case"/api/expand":c=await d.executeTool("navigate",{action:"expand",symbol:a.get("symbol")});break;case"/api/chain":c=await d.executeTool("navigate",{action:"call_chain",from:a.get("from"),to:a.get("to")});break;case"/api/project-info":{const e=o.resolve(t),a=n.createHash("md5").update(e).digest("hex"),s=parseInt(a.slice(0,4),16)%360;c={name:p,path:e,color:`hsl(${s}, 65%, 55%)`,agents:w.size,pid:process.pid};break}case"/api/instances":try{const{listBackends:e}=await import("./backend-lifecycle.js");c=e()}catch{c=[{name:p,path:o.resolve(t),agents:w.size}]}break;default:return"POST"===s&&"/api/restart"===e?(r.writeHead(200,{"Content-Type":"application/json","Cache-Control":"no-cache"}),r.end(JSON.stringify({ok:!0,message:"Server restarting..."})),void setTimeout(()=>process.exit(0),200)):(r.writeHead(200,{"Content-Type":"application/json","Cache-Control":"no-cache, no-store, must-revalidate"}),void r.end(JSON.stringify({error:"Unknown API endpoint"})))}r.writeHead(200,{"Content-Type":"application/json","Access-Control-Allow-Origin":"*","Cache-Control":"no-cache"}),r.end(JSON.stringify(c))}catch(e){r.writeHead(500,{"Content-Type":"application/json","Cache-Control":"no-cache, no-store, must-revalidate"}),r.end(JSON.stringify({error:e.message}))}}startShutdownTimer(),c.on("tool:call",e=>{f.events.push(e),f.events.length>500&&f.events.shift(),broadcastRPC("event",e)}),c.on("tool:result",e=>{f.events.push(e),f.events.length>500&&f.events.shift(),broadcastRPC("event",e)});
|
|
29
|
-
const v=e.createServer((e,t)=>{const o=new URL(e.url,`http://localhost:${a||0}`);return"OPTIONS"===e.method?(t.writeHead(204,{"Access-Control-Allow-Origin":"*","Access-Control-Allow-Methods":"GET, POST","Access-Control-Allow-Headers":"Content-Type"}),void t.end()):o.pathname.startsWith("/api/")?(touchActivity(),void handleAPI(o.pathname,o.searchParams,e.method,t)):("/ws/monitor"===o.pathname&&console.log("UNEXPECTED HTTP /ws/monitor",e.headers),void serveStatic(o.pathname,t))}),j=new s({noServer:!0});j.on("connection",async e=>{y.add(e),touchActivity(),await ensureSkeleton();
|
|
30
|
-
const t={project:f.project,skeleton:f.skeleton};try{e.send(JSON.stringify({jsonrpc:"2.0",method:"snapshot",params:{state:t}}))}catch{}e.on("message",async t=>{let n;touchActivity();try{n=JSON.parse(t.toString())}catch{return}if(n.jsonrpc&&n.id&&n.method)if("tool"===n.method){const{name:a,args:s}=n.params||{};try{let t;if("compact"===a&&"compact_file"===s?.action&&s?.path){const e=o.resolve(h,s.path),n=o.basename(s.path,o.extname(s.path))+".ctx",a=o.resolve(h,".context",o.dirname(s.path),n);
|
|
31
|
-
let r=null;try{const{readFileSync:e,existsSync:t}=await import("fs");t(a)&&(r=e(a,"utf-8"))}catch{}const c=await l(e,r),i=Math.ceil(c.original/4),d=Math.ceil(c.decompiled/4),K=r?Math.ceil(r.length/4):0,T=i+K,p=d>0?Math.round(100*(1-T/d)):0;t={code:c.code,file:s.path,injected:c.injected,codeTok:i,ctxTok:K,totalTok:T,expanded:d,savings:p+"%"}}else t=await d.executeTool(a,s||{});e.send(JSON.stringify({jsonrpc:"2.0",id:n.id,result:t}))}catch(t){e.send(JSON.stringify({jsonrpc:"2.0",id:n.id,error:{code:-32e3,message:t.message}}))}}else e.send(JSON.stringify({jsonrpc:"2.0",id:n.id,error:{code:-32601,message:`Unknown method: ${n.method}`}}))}),e.on("close",()=>{y.delete(e),startShutdownTimer()}),e.on("error",()=>{y.delete(e),startShutdownTimer()})}),v.on("upgrade",(e,t,o)=>{const n=e.headers["sec-websocket-key"];if(n)if("/ws/monitor"!==e.url){if("/mcp-ws"===e.url){const e=computeWSAccept(n);t.write(`HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: ${e}\r\n\r\n`);
|
|
32
|
-
const o="agent-"+m++,a=r(e=>{try{t.write(encodeWSFrame(JSON.stringify(e)))}catch{}});w.set(t,{agentId:o,mcpServer:a,connectedAt:Date.now()}),resetShutdownTimer(),patchState("project.agents",w.size),broadcastRPC("event",{type:"agent_connect",agentId:o,agents:w.size,ts:Date.now()});
|
|
33
|
-
let s=Buffer.alloc(0);return t.on("data",e=>{for(s=Buffer.concat([s,e]);s.length>=2;){const n=decodeWSFrame(s);if(!n)break;if(s=s.slice(n.totalLen),8===n.opcode)return w.delete(t),patchState("project.agents",w.size),broadcastRPC("event",{type:"agent_disconnect",agentId:o,agents:w.size,ts:Date.now()}),t.end(),void(0===w.size&&startShutdownTimer());if(9===n.opcode){const o=Buffer.from(e);o[0]=240&o[0]|10,t.write(o);continue}1===n.opcode&&(async()=>{try{const e=JSON.parse(n.data),o=await a.handleMessage(e);null!==o&&t.write(encodeWSFrame(JSON.stringify(o)))}catch(e){t.write(encodeWSFrame(JSON.stringify({jsonrpc:"2.0",error:{code:-32700,message:"Parse error"}})))}})()}}),t.on("close",()=>{w.delete(t),patchState("project.agents",w.size),broadcastRPC("event",{type:"agent_disconnect",agentId:o,agents:w.size,ts:Date.now()}),0===w.size&&startShutdownTimer()}),void t.on("error",()=>{w.delete(t),0===w.size&&startShutdownTimer()})}t.destroy()}else j.handleUpgrade(e,t,o,t=>{j.emit("connection",t,e)});else t.destroy()});
|
|
34
|
-
const b=!a,T=a||0;return v.listen(T,"127.0.0.1",()=>{const e=v.address().port;if(b){const n=i("project-graph",e,{projectPath:o.resolve(t),projectName:p});setTimeout(()=>{const a=n.url;console.log("\n ⬡ project-graph-mcp"),console.log(" ─────────────────────────────"),console.log(` → ${a}`),console.log(` → ${n.directUrl} (direct)`),console.log(` → Project: ${o.resolve(t)}`),console.log(` → MCP WebSocket: ws://127.0.0.1:${e}/mcp-ws\n`)},200)}else console.log("\n ⬡ project-graph-mcp"),console.log(" ─────────────────────────────"),console.log(` → http://localhost:${e}/`),console.log(` → Project: ${o.resolve(t)}`),console.log(` → MCP WebSocket: ws://127.0.0.1:${e}/mcp-ws\n`)}),v}
|
|
2
|
+
import e from"node:http";import t from"node:fs";import o from"node:path";import n from"node:crypto";import{fileURLToPath as a}from"node:url";import{WebSocketServer as s}from"ws";import{createServer as i}from"../mcp/mcp-server.js";import c from"../core/event-bus.js";import{registerService as r}from"./local-gateway.js";import{expandFile as l}from"../compact/expand.js";import{setRoots as _setRoots}from"../core/workspace.js";
|
|
3
|
+
const d=o.dirname(a(import.meta.url)),p=o.join(d,"..",".."),m=o.join(p,"web"),h={"symbiote-node":o.join(p,"node_modules","symbiote-node"),symbiote:o.join(p,"node_modules","@symbiotejs","symbiote")},f={".html":"text/html",".js":"text/javascript",".mjs":"text/javascript",".css":"text/css",".json":"application/json",".svg":"image/svg+xml",".png":"image/png",".ico":"image/x-icon",".woff2":"font/woff2"};
|
|
4
|
+
function g(e,n){const a=o.normalize(e).replace(/^(\.\.[/\\])+/,""),s=a.match(/^[/\\]?vendor[/\\]([^/\\]+)[/\\]?(.*)/);let i,c;if(s&&h[s[1]]?(c=h[s[1]],i=o.join(c,s[2]||"index.js")):(c=m,i=o.join(m,"/"===a?"index.html":a)),!i.startsWith(c))return n.writeHead(403),void n.end("Forbidden");if(t.existsSync(i)&&t.statSync(i).isDirectory()&&(i=o.join(i,"index.html")),!t.existsSync(i))return n.writeHead(404),void n.end("Not Found");const r=o.extname(i),l=f[r]||"application/octet-stream",d=t.readFileSync(i);n.writeHead(200,{"Content-Type":l,"Cache-Control":"no-cache, no-store, must-revalidate"}),n.end(d)}
|
|
5
|
+
function u(e){return n.createHash("sha1").update(e+"258EAFA5-E914-47DA-95CA-5AB5ADF35C70").digest("base64")}
|
|
6
|
+
function y(e){const t=Buffer.from(e,"utf8"),o=t.length;let n;return o<126?(n=Buffer.alloc(2),n[0]=129,n[1]=o):o<65536?(n=Buffer.alloc(4),n[0]=129,n[1]=126,n.writeUInt16BE(o,2)):(n=Buffer.alloc(10),n[0]=129,n[1]=127,n.writeBigUInt64BE(BigInt(o),2)),Buffer.concat([n,t])}
|
|
7
|
+
function w(e){if(e.length<2)return null;const t=15&e[0],o=!!(128&e[1]);let n=127&e[1],a=2;if(126===n){if(e.length<4)return null;n=e.readUInt16BE(2),a=4}else if(127===n){if(e.length<10)return null;n=Number(e.readBigUInt64BE(2)),a=10}if(o){if(e.length<a+4+n)return null;const o=e.slice(a,a+4);a+=4;const s=e.slice(a,a+n);for(let e=0;e<s.length;e++)s[e]^=o[e%4];return{opcode:t,data:s.toString("utf8"),totalLen:a+n}}return e.length<a+n?null:{opcode:t,data:e.slice(a,a+n).toString("utf8"),totalLen:a+n}}
|
|
8
|
+
export function startWebServer(t,a){_setRoots([{uri:"file://"+o.resolve(t)}]);const d=i(()=>{}),p=o.basename(o.resolve(t))||"root";let m=1;const h=o.resolve(t),f=n.createHash("md5").update(h).digest("hex"),v=parseInt(f.slice(0,4),16)%360,j={project:{name:p,path:h,color:`hsl(${v}, 65%, 55%)`,agents:0,pid:process.pid},skeleton:null,events:[]};function x(e,t){const o=JSON.stringify({jsonrpc:"2.0",method:e,params:t});for(const e of T)try{e.send(o)}catch{T.delete(e)}}function S(e,t){const o=e.split(".");let n=j;for(let e=0;e<o.length-1;e++)n=n[o[e]];n[o[o.length-1]]=t,x("patch",{path:e,value:t})}async function k(){if(!j.skeleton)try{j.skeleton=await d.executeTool("get_skeleton",{path:t})}catch(e){console.error("[project-graph] Failed to load skeleton:",e.message)}return j.skeleton}const b=new Map,T=new Set;let C=null;function N(){return b.size>0||T.size>0}function O(){C&&(clearTimeout(C),C=null)}function P(){N()||(O(),C=setTimeout(()=>{N()||(console.log("[project-graph] No clients for 15 min — shutting down."),process.exit(0))},9e5))}function z(){O(),P()}async function A(e,a,s,i){try{let c;const r=a.get("path")||t;switch(e){case"/api/skeleton":c=await d.executeTool("get_skeleton",{path:r});break;case"/api/file":{const e=a.get("path");if(e){const{resolve:n,basename:a,extname:s}=await import("path"),{readFileSync:i,existsSync:r}=await import("fs"),d=n(t,e),p=a(e,s(e))+".ctx",m=o.resolve(t,".context",o.dirname(e),p),h=r(m)?i(m,"utf-8"):null,f=await l(d,h),g=Math.ceil(f.original/4),u=Math.ceil(f.decompiled/4),y=h?Math.ceil(h.length/4):0,w=g+y,v=u>0?Math.round(100*(1-w/u)):0;c={code:f.code,file:e,injected:f.injected,codeTok:g,ctxTok:y,totalTok:w,expanded:u,savings:v+"%"}}else c={code:"// No file specified",file:""};break}case"/api/raw-file":{const e=a.get("path");try{const{readFileSync:o}=await import("fs"),{resolve:n,relative:a}=await import("path");c={content:o(n(t,e),"utf-8"),file:e}}catch(t){c={content:`// Cannot read: ${t.message}`,file:e}}break}case"/api/compression-stats":{const{readdirSync:e,statSync:n,readFileSync:a,existsSync:s}=await import("fs"),{join:i,extname:r,basename:d,dirname:p,relative:m}=await import("path"),h=new Set([".js",".mjs"]),f=[],g=["node_modules",".git","vendor",".context",".expanded","web"];!function t(o){try{for(const a of e(o)){if(a.startsWith("."))continue;const e=i(o,a);n(e).isDirectory()?g.includes(a)||t(e):h.has(r(a))&&f.push(e)}}catch{}}(o.resolve(t,"src"));let u=0,y=0,w=0,v=0;const j=o.resolve(t);for(const e of f){try{u+=n(e).size;const t=m(j,e),i=d(t,".js"),c=o.resolve(j,".context",p(t),i+".ctx");let r=0;s(c)&&(r=n(c).size,w+=r);try{const t=s(c)?a(c,"utf-8"):null;v+=(await l(e,t)).decompiled}catch{v+=1.3*n(e).size}}catch{continue}y++}c={files:y,codeTok:Math.ceil(u/4),ctxTok:Math.ceil(w/4),totalTok:Math.ceil((u+w)/4),expanded:Math.ceil(v/4)};break}case"/api/docs":c=await d.executeTool("docs",{action:"get",path:r,file:a.get("file")});break;case"/api/analysis":c=await d.executeTool("analyze",{action:"full_analysis",path:r});break;case"/api/analysis-summary":c=await d.executeTool("analyze",{action:"analysis_summary",path:r});break;case"/api/deps":c=await d.executeTool("navigate",{action:"deps",symbol:a.get("symbol")});break;case"/api/usages":c=await d.executeTool("navigate",{action:"usages",symbol:a.get("symbol")});break;case"/api/expand":c=await d.executeTool("navigate",{action:"expand",symbol:a.get("symbol")});break;case"/api/chain":c=await d.executeTool("navigate",{action:"call_chain",from:a.get("from"),to:a.get("to")});break;case"/api/project-info":{const e=o.resolve(t),a=n.createHash("md5").update(e).digest("hex"),s=parseInt(a.slice(0,4),16)%360;c={name:p,path:e,color:`hsl(${s}, 65%, 55%)`,agents:b.size,pid:process.pid};break}case"/api/instances":try{const{listBackends:e}=await import("./backend-lifecycle.js");c=e()}catch{c=[{name:p,path:o.resolve(t),agents:b.size}]}break;default:return"POST"===s&&"/api/restart"===e?(i.writeHead(200,{"Content-Type":"application/json","Cache-Control":"no-cache"}),i.end(JSON.stringify({ok:!0,message:"Server restarting..."})),void setTimeout(async()=>{const{spawn:e}=await import("child_process"),{removePortFile:n}=await import("./backend-lifecycle.js"),{fileURLToPath:a}=await import("url"),s=o.join(o.dirname(a(import.meta.url)),"backend.js");n(t),e(process.execPath,[s,o.resolve(t)],{detached:!0,stdio:"ignore",env:{...process.env,PROJECT_GRAPH_BACKEND:"1"}}).unref(),setTimeout(()=>process.exit(0),300)},200)):(i.writeHead(200,{"Content-Type":"application/json","Cache-Control":"no-cache, no-store, must-revalidate"}),void i.end(JSON.stringify({error:"Unknown API endpoint"})))}i.writeHead(200,{"Content-Type":"application/json","Access-Control-Allow-Origin":"*","Cache-Control":"no-cache"}),i.end(JSON.stringify(c))}catch(e){i.writeHead(500,{"Content-Type":"application/json","Cache-Control":"no-cache, no-store, must-revalidate"}),i.end(JSON.stringify({error:e.message}))}}P(),c.on("tool:call",e=>{j.events.push(e),j.events.length>500&&j.events.shift(),x("event",e)}),c.on("tool:result",e=>{j.events.push(e),j.events.length>500&&j.events.shift(),x("event",e)});const B=e.createServer((e,t)=>{const o=new URL(e.url,`http://localhost:${a||0}`);return"OPTIONS"===e.method?(t.writeHead(204,{"Access-Control-Allow-Origin":"*","Access-Control-Allow-Methods":"GET, POST","Access-Control-Allow-Headers":"Content-Type"}),void t.end()):o.pathname.startsWith("/api/")?(z(),void A(o.pathname,o.searchParams,e.method,t)):("/ws/monitor"===o.pathname&&console.log("UNEXPECTED HTTP /ws/monitor",e.headers),void g(o.pathname,t))}),M=new s({noServer:!0});M.on("connection",async e=>{T.add(e),z(),await k();const t={project:j.project,skeleton:j.skeleton};try{e.send(JSON.stringify({jsonrpc:"2.0",method:"snapshot",params:{state:t}}))}catch{}e.on("message",async t=>{let n;z();try{n=JSON.parse(t.toString())}catch{return}if(n.jsonrpc&&n.id&&n.method)if("tool"===n.method){const{name:a,args:s}=n.params||{};try{let t;if("compact"===a&&"compact_file"===s?.action&&s?.path){const e=o.resolve(h,s.path),n=o.basename(s.path,o.extname(s.path))+".ctx",a=o.resolve(h,".context",o.dirname(s.path),n);let i=null;try{const{readFileSync:e,existsSync:t}=await import("fs");t(a)&&(i=e(a,"utf-8"))}catch{}const c=await l(e,i),r=Math.ceil(c.original/4),d=Math.ceil(c.decompiled/4),p=i?Math.ceil(i.length/4):0,m=r+p,f=d>0?Math.round(100*(1-m/d)):0;t={code:c.code,file:s.path,injected:c.injected,codeTok:r,ctxTok:p,totalTok:m,expanded:d,savings:f+"%"}}else t=await d.executeTool(a,s||{});e.send(JSON.stringify({jsonrpc:"2.0",id:n.id,result:t}))}catch(t){e.send(JSON.stringify({jsonrpc:"2.0",id:n.id,error:{code:-32e3,message:t.message}}))}}else e.send(JSON.stringify({jsonrpc:"2.0",id:n.id,error:{code:-32601,message:`Unknown method: ${n.method}`}}))}),e.on("close",()=>{T.delete(e),P()}),e.on("error",()=>{T.delete(e),P()})}),B.on("upgrade",(e,t,o)=>{const n=e.headers["sec-websocket-key"];if(n)if("/ws/monitor"!==e.url){if("/mcp-ws"===e.url){const e=u(n);t.write(`HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: ${e}\r\n\r\n`);const o="agent-"+m++,a=i(e=>{try{t.write(y(JSON.stringify(e)))}catch{}});b.set(t,{agentId:o,mcpServer:a,connectedAt:Date.now()}),O(),S("project.agents",b.size),x("event",{type:"agent_connect",agentId:o,agents:b.size,ts:Date.now()});let s=Buffer.alloc(0);return t.on("data",e=>{for(s=Buffer.concat([s,e]);s.length>=2;){const n=w(s);if(!n)break;if(s=s.slice(n.totalLen),8===n.opcode)return b.delete(t),S("project.agents",b.size),x("event",{type:"agent_disconnect",agentId:o,agents:b.size,ts:Date.now()}),t.end(),void(0===b.size&&P());if(9===n.opcode){const o=Buffer.from(e);o[0]=240&o[0]|10,t.write(o);continue}1===n.opcode&&(async()=>{try{const e=JSON.parse(n.data),o=await a.handleMessage(e);null!==o&&t.write(y(JSON.stringify(o)))}catch(e){t.write(y(JSON.stringify({jsonrpc:"2.0",error:{code:-32700,message:"Parse error"}})))}})()}}),t.on("close",()=>{b.delete(t),S("project.agents",b.size),x("event",{type:"agent_disconnect",agentId:o,agents:b.size,ts:Date.now()}),0===b.size&&P()}),void t.on("error",()=>{b.delete(t),0===b.size&&P()})}t.destroy()}else M.handleUpgrade(e,t,o,t=>{M.emit("connection",t,e)});else t.destroy()});const H=!a,J=a||0;return B.listen(J,"127.0.0.1",()=>{const e=B.address().port;if(H){const n=r("project-graph",e,{projectPath:o.resolve(t),projectName:p});setTimeout(()=>{const a=n.url;console.log("\n ⬡ project-graph-mcp"),console.log(" ─────────────────────────────"),console.log(` → ${a}`),console.log(` → ${n.directUrl} (direct)`),console.log(` → Project: ${o.resolve(t)}`),console.log(` → MCP WebSocket: ws://127.0.0.1:${e}/mcp-ws\n`)},200)}else console.log("\n ⬡ project-graph-mcp"),console.log(" ─────────────────────────────"),console.log(` → http://localhost:${e}/`),console.log(` → Project: ${o.resolve(t)}`),console.log(` → MCP WebSocket: ws://127.0.0.1:${e}/mcp-ws\n`)}),B}
|
package/web/app.js
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
// @ctx .context/web/app.ctx
|
|
2
|
-
import{Layout as
|
|
2
|
+
import{Layout as e,LayoutTree as t,applyTheme as n}from"symbiote-node";
|
|
3
|
+
import{CARBON as o}from"./vendor/symbiote-node/themes/carbon.js";
|
|
4
|
+
import{state as a,subscribe as s,onEvent as i,call as r,connect as c}from"./state.js";
|
|
5
|
+
import"./panels/file-tree.js";
|
|
6
|
+
import"./panels/code-viewer.js";
|
|
7
|
+
import"./panels/ctx-panel.js";
|
|
8
|
+
import"./panels/dep-graph.js";
|
|
9
|
+
import"./panels/health-panel.js";
|
|
10
|
+
import"./panels/live-monitor.js";
|
|
11
|
+
import"./panels/SettingsPanel/SettingsPanel.js";
|
|
12
|
+
import"./components/quick-open.js";
|
|
3
13
|
export const state={skeleton:null,activeFile:null,ws:null,monitorEvents:[]};
|
|
4
|
-
const
|
|
5
|
-
export async function api(t
|
|
6
|
-
async function p(t,n){const o={"/api/skeleton":{name:"get_skeleton",args:t=>({path:t.path})},"/api/file":{name:"compact",args:t=>({action:"compact_file",path:t.path,beautify:!0})},"/api/docs":{name:"docs",args:t=>({action:"get",path:t.path,file:t.file})},"/api/analysis":{name:"analyze",args:t=>({action:"full_analysis",path:t.path})},"/api/analysis-summary":{name:"analyze",args:t=>({action:"analysis_summary",path:t.path})},"/api/deps":{name:"navigate",args:t=>({action:"deps",symbol:t.symbol})},"/api/usages":{name:"navigate",args:t=>({action:"usages",symbol:t.symbol})},"/api/expand":{name:"navigate",args:t=>({action:"expand",symbol:t.symbol})},"/api/chain":{name:"navigate",args:t=>({action:"call_chain",from:t.from,to:t.to})}}[t];return o?c(o.name,o.args(n)):null}
|
|
14
|
+
const l=new URL(".",import.meta.url).href;
|
|
15
|
+
export async function api(e,t={}){if(a.connected&&e.startsWith("/api/")){const n=await async function(e,t){const n={"/api/skeleton":{name:"get_skeleton",args:e=>({path:e.path})},"/api/file":{name:"compact",args:e=>({action:"compact_file",path:e.path,beautify:!0})},"/api/docs":{name:"docs",args:e=>({action:"get",path:e.path,file:e.file})},"/api/analysis":{name:"analyze",args:e=>({action:"full_analysis",path:e.path})},"/api/analysis-summary":{name:"analyze",args:e=>({action:"analysis_summary",path:e.path})},"/api/deps":{name:"navigate",args:e=>({action:"deps",symbol:e.symbol})},"/api/usages":{name:"navigate",args:e=>({action:"usages",symbol:e.symbol})},"/api/expand":{name:"navigate",args:e=>({action:"expand",symbol:e.symbol})},"/api/chain":{name:"navigate",args:e=>({action:"call_chain",from:e.from,to:e.to})}}[e];return n?r(n.name,n.args(t)):null}(e,t);if(null!==n)return n}const n=new URLSearchParams(t).toString(),o=e.replace(/^\//,""),s=n?`${l}${o}?${n}`:`${l}${o}`,i=await fetch(s);if(!i.ok)throw new Error(`API error: ${i.status}`);return i.json()}
|
|
7
16
|
export const events=new EventTarget;
|
|
8
|
-
export function emit(t
|
|
9
|
-
const t
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const o=document.getElementById("project-files");o&&(o.textContent=`${n.size} files`),emit("skeleton-loaded",t),fetch(m+"api/compression-stats").then(t=>t.json()).then(t=>{const n=document.getElementById("compression-stats");if(n&&t.codeTok){const o=t.ctxTok?`${(t.codeTok/1e3).toFixed(1)}K + ${(t.ctxTok/1e3).toFixed(1)}K ctx = ${(t.totalTok/1e3).toFixed(1)}K tok`:`${(t.codeTok/1e3).toFixed(1)}K tok`;n.textContent=o,n.style.display=""}})}),i("connected",t=>{const n=document.getElementById("status-indicator");n&&(n.className=t?"status connected":"status disconnected")}),r(t=>{if("agent_connect"===t.type||"agent_disconnect"===t.type)return h(t.agents),void emit("agent-event",t);state.monitorEvents.push(t),state.monitorEvents.length>500&&state.monitorEvents.shift(),emit("tool-event",t)})}
|
|
15
|
-
function h(t){let n=document.getElementById("agent-badge");if(!n){const t=document.querySelector(".app-topbar");if(!t)return;n=document.createElement("span"),n.id="agent-badge",n.className="agent-badge",t.appendChild(n)}n.textContent=t>0?`● ${t} agent${1!==t?"s":""}`:"",n.style.display=t>0?"":"none"}
|
|
16
|
-
function y(){document.querySelector("pg-quick-open")||document.body.appendChild(document.createElement("pg-quick-open"))}
|
|
17
|
-
"loading"===document.readyState?document.addEventListener("DOMContentLoaded",()=>{f(),y()}):setTimeout(()=>{f(),y()},100);
|
|
17
|
+
export function emit(e,t={}){events.dispatchEvent(new CustomEvent(e,{detail:t}))}
|
|
18
|
+
const p={"file-tree":{title:"Files",icon:"folder",component:"pg-file-tree"},"code-viewer":{title:"Code",icon:"code",component:"pg-code-viewer"},"ctx-panel":{title:"Documentation",icon:"description",component:"pg-ctx-panel"},"dep-graph":{title:"Dependencies",icon:"account_tree",component:"pg-dep-graph"},health:{title:"Health",icon:"analytics",component:"pg-health-panel"},monitor:{title:"Live Monitor",icon:"monitor_heart",component:"pg-live-monitor"},settings:{title:"Settings",icon:"settings",component:"pg-settings-panel"}},m=[{id:"explorer",icon:"folder_open",label:"Explorer"},{id:"analysis",icon:"analytics",label:"Analysis"},{id:"monitor",icon:"monitor_heart",label:"Monitor"},{id:"settings",icon:"settings",label:"Settings"}],d={explorer:()=>t.createSplit("horizontal",t.createPanel("file-tree"),t.createSplit("horizontal",t.createPanel("code-viewer"),t.createPanel("ctx-panel"),.65),.2),analysis:()=>t.createSplit("horizontal",t.createPanel("health"),t.createPanel("dep-graph"),.5),monitor:()=>t.createPanel("monitor"),settings:()=>t.createPanel("settings")};
|
|
19
|
+
async function u(){(function(){n(document.documentElement,o);const e=document.querySelector(".app-workspace"),t=document.createElement("layout-sidebar");e.prepend(t);const a=e.querySelector(".app-content"),s=document.createElement("panel-layout");s.setAttribute("storage-key","pg-explorer-layout"),s.setAttribute("min-panel-size","150"),s.id="main-layout",a.appendChild(s),requestAnimationFrame(()=>{for(const[e,t]of Object.entries(p))s.registerPanelType(e,t);function e(){const e=location.hash.replace("#","")||"explorer",t=e.indexOf("?"),n=t>=0?e.substring(0,t):e,o=n.indexOf("/"),a=o>=0?n.substring(0,o):n,i=o>=0?n.substring(o+1):"";d[a]&&s.setLayout(d[a]()),"explorer"===a&&i&&requestAnimationFrame(()=>{state.activeFile=i,emit("file-selected",{path:i,fromRoute:!0})})}t.setSections(m),window.addEventListener("hashchange",e),events.addEventListener("file-selected",e=>{if(e.detail.fromRoute)return;const t=e.detail.path;t&&history.replaceState(null,"",`#explorer/${t}`)}),localStorage.getItem("pg-explorer-layout")||s.setLayout(d.explorer()),location.hash&&"#"!==location.hash?e():location.hash="explorer"})})(),c(),s("project",e=>{e&&(document.title=`${e.name} — Project Graph`,document.getElementById("project-name").textContent=e.name,document.documentElement.style.setProperty("--project-accent",e.color),g(e.agents))}),s("skeleton",e=>{if(!e)return;state.skeleton=e;const t=new Set;for(const n of Object.values(e.n||{}))n.f&&t.add(n.f);for(const n of Object.keys(e.X||{}))t.add(n);for(const[n,o]of Object.entries(e.f||{}))for(const e of o)t.add("./"===n?e:`${n}${e}`);for(const[n,o]of Object.entries(e.a||{}))for(const e of o)t.add("./"===n?e:`${n}${e}`);const n=document.getElementById("project-files");n&&(n.textContent=`${t.size} files`),emit("skeleton-loaded",e),fetch(l+"api/compression-stats").then(e=>e.json()).then(e=>{const t=document.getElementById("compression-stats");if(t&&e.codeTok){const n=e.ctxTok?`${(e.codeTok/1e3).toFixed(1)}K + ${(e.ctxTok/1e3).toFixed(1)}K ctx = ${(e.totalTok/1e3).toFixed(1)}K tok`:`${(e.codeTok/1e3).toFixed(1)}K tok`;t.textContent=n,t.style.display=""}})}),s("connected",e=>{const t=document.getElementById("status-indicator");t&&(t.className=e?"status connected":"status disconnected")}),i(e=>{if("agent_connect"===e.type||"agent_disconnect"===e.type)return g(e.agents),void emit("agent-event",e);state.monitorEvents.push(e),state.monitorEvents.length>500&&state.monitorEvents.shift(),emit("tool-event",e)})}
|
|
20
|
+
function g(e){let t=document.getElementById("agent-badge");if(!t){const e=document.querySelector(".app-topbar");if(!e)return;t=document.createElement("span"),t.id="agent-badge",t.className="agent-badge",e.appendChild(t)}t.textContent=e>0?`● ${e} agent${1!==e?"s":""}`:"",t.style.display=e>0?"":"none"}
|
|
21
|
+
function f(){document.querySelector("pg-quick-open")||document.body.appendChild(document.createElement("pg-quick-open"))}
|
|
22
|
+
"loading"===document.readyState?document.addEventListener("DOMContentLoaded",()=>{u(),f()}):setTimeout(()=>{u(),f()},100);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// @ctx .context/web/components/code-block.ctx
|
|
1
2
|
import o from"@symbiotejs/symbiote";import{highlight as n}from"../highlight.js";
|
|
2
3
|
export class CodeBlock extends o{init$={code:"",highlighted:"",lineNums:""};renderCallback(){this.sub("code",o=>{if(!o)return this.$.highlighted="",void(this.$.lineNums="");this.$.highlighted=n(o);
|
|
3
4
|
const e=o.split("\n").length,t=[];for(let o=1;o<=e;o++)t.push(o);this.$.lineNums=t.join("\n")})}}CodeBlock.template='\n <div class="cb-scroll">\n <pre class="cb-gutter" bind="textContent: lineNums"></pre>\n <pre class="cb-pre"><code bind="innerHTML: highlighted"></code></pre>\n </div>\n',CodeBlock.rootStyles="\n code-block {\n display: block;\n height: 100%;\n overflow: hidden;\n }\n code-block .cb-scroll {\n display: flex;\n height: 100%;\n overflow: auto;\n align-items: stretch;\n }\n code-block .cb-gutter {\n position: sticky;\n left: 0;\n z-index: 1;\n margin: 0;\n padding: 12px 8px 12px 12px;\n font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;\n font-size: 12px;\n line-height: 1.6;\n text-align: right;\n color: var(--sn-text-dim, hsl(30, 10%, 55%));\n opacity: 0.45;\n background: var(--sn-bg, hsl(37, 30%, 96%));\n border-right: 1px solid var(--sn-node-border, hsl(35, 18%, 88%));\n user-select: none;\n white-space: pre;\n min-width: 32px;\n flex-shrink: 0;\n }\n code-block .cb-pre {\n margin: 0;\n padding: 12px;\n flex: 1;\n min-width: 0;\n font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;\n font-size: 12px;\n line-height: 1.6;\n color: var(--sn-text, hsl(30, 15%, 18%));\n tab-size: 2;\n white-space: pre;\n box-sizing: border-box;\n }\n /* Token colors */\n code-block .t-kw { color: rgb(254, 165, 176); }\n code-block .t-str { color: rgb(251, 182, 79); }\n code-block .t-cm { color: rgb(149, 149, 149); font-style: italic; }\n code-block .t-fn { color: rgb(180, 243, 255); }\n code-block .t-num { color: rgb(251, 182, 79); }\n code-block .t-bi { color: rgb(180, 243, 255); }\n code-block .t-prop { color: rgb(238, 131, 252); }\n code-block .t-lit { color: rgb(254, 165, 176); }\n /* JSDoc */\n code-block .t-jd { color: rgb(130, 155, 130); font-style: italic; }\n code-block .t-jd-tag { color: rgb(180, 220, 140); font-style: normal; font-weight: 500; }\n code-block .t-jd-type { color: rgb(130, 210, 240); font-style: normal; }\n",CodeBlock.reg("code-block");
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// @ctx .context/web/components/quick-open.ctx
|
|
1
2
|
import e from"@symbiotejs/symbiote";import{state as n,events as t,emit as s}from"../app.js";
|
|
2
3
|
export class QuickOpen extends e{init$={visible:!1,query:"",resultsHTML:"",selectedIdx:0};_results=[];_allFiles=[];renderCallback(){t.addEventListener("skeleton-loaded",e=>this._collectFiles(e.detail)),n.skeleton&&this._collectFiles(n.skeleton),this._overlay=this.querySelector(".qo-overlay"),this._overlay.addEventListener("click",e=>{e.target===this._overlay&&this._close()}),document.addEventListener("keydown",e=>{(e.metaKey||e.ctrlKey)&&"k"===e.key&&(e.preventDefault(),this._toggle()),"Escape"===e.key&&this.$.visible&&(e.preventDefault(),this._close())}),this.sub("visible",e=>{this._overlay&&(this._overlay.style.display=e?"flex":"none",e&&requestAnimationFrame(()=>{const e=this.querySelector(".qo-input");e&&(e.value="",e.focus())}))})}_collectFiles(e){const n=new Set;for(const t of Object.keys(e.X||{}))n.add(t);for(const t of Object.values(e.n||{}))t.f&&n.add(t.f);for(const[t,s]of Object.entries(e.f||{}))for(const e of s)n.add("./"===t?e:`${t}${e}`);for(const[t,s]of Object.entries(e.a||{}))for(const e of s)n.add("./"===t?e:`${t}${e}`);this._allFiles=[...n].sort()}_toggle(){this.$.visible=!this.$.visible,this.$.visible&&(this.$.query="",this.$.selectedIdx=0,this._search(""))}_close(){this.$.visible=!1}_onInput(e){this.$.query=e.target.value,this.$.selectedIdx=0,this._search(this.$.query)}_onKeydown(e){if("ArrowDown"===e.key)e.preventDefault(),this.$.selectedIdx=Math.min(this.$.selectedIdx+1,this._results.length-1),this._renderResults();else if("ArrowUp"===e.key)e.preventDefault(),this.$.selectedIdx=Math.max(this.$.selectedIdx-1,0),this._renderResults();else if("Enter"===e.key){e.preventDefault();
|
|
3
4
|
const t=this._results[this.$.selectedIdx];t&&(this._close(),n.activeFile=t.file,s("file-selected",{path:t.file}),location.hash.startsWith("#explorer")?history.replaceState(null,"",`#explorer/${t.file}`):location.hash=`explorer/${t.file}`)}}_search(e){const n=e.toLowerCase().trim();if(n){const e=[];for(const t of this._allFiles){const s=QuickOpen._fuzzyScore(n,t.toLowerCase());s>0&&e.push({file:t,score:s})}e.sort((e,n)=>n.score-e.score),this._results=e.slice(0,15)}else this._results=this._allFiles.slice(0,15).map(e=>({file:e,score:0}));this._renderResults()}static _fuzzyScore(e,n){if(n.includes(e))return 100+e.length/n.length*50;
|
package/web/dashboard-state.js
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
+
// @ctx .context/web/panels/ActionBoard/ActionBoard.css.ctx
|
|
1
2
|
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";
|
|
@@ -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");
|