project-graph-mcp 2.2.6 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/ARCHITECTURE.md +81 -0
  2. package/CHANGELOG.md +57 -0
  3. package/README.md +9 -4
  4. package/package.json +6 -13
  5. package/src/compact/expand.js +1 -1
  6. package/src/core/graph-builder.js +2 -2
  7. package/src/core/parser.js +2 -2
  8. package/src/network/server.js +1 -2
  9. package/vendor/symbiote-node/CHANGELOG.md +31 -0
  10. package/vendor/symbiote-node/LICENSE +21 -0
  11. package/vendor/symbiote-node/README.md +206 -0
  12. package/vendor/symbiote-node/canvas/AutoLayout.js +725 -0
  13. package/vendor/symbiote-node/canvas/Breadcrumb/Breadcrumb.css.js +73 -0
  14. package/vendor/symbiote-node/canvas/Breadcrumb/Breadcrumb.js +93 -0
  15. package/vendor/symbiote-node/canvas/Breadcrumb/Breadcrumb.tpl.js +9 -0
  16. package/vendor/symbiote-node/canvas/CanvasConnectionRenderer.js +962 -0
  17. package/vendor/symbiote-node/canvas/ConnectionRenderer.js +1468 -0
  18. package/vendor/symbiote-node/canvas/FlowSimulator.js +323 -0
  19. package/vendor/symbiote-node/canvas/ForceLayout.js +189 -0
  20. package/vendor/symbiote-node/canvas/ForceWorker.js +1325 -0
  21. package/vendor/symbiote-node/canvas/GraphTabs/GraphTabs.css.js +97 -0
  22. package/vendor/symbiote-node/canvas/GraphTabs/GraphTabs.js +176 -0
  23. package/vendor/symbiote-node/canvas/GraphTabs/GraphTabs.tpl.js +12 -0
  24. package/vendor/symbiote-node/canvas/LODManager.js +88 -0
  25. package/vendor/symbiote-node/canvas/Minimap/Minimap.css.js +71 -0
  26. package/vendor/symbiote-node/canvas/Minimap/Minimap.js +207 -0
  27. package/vendor/symbiote-node/canvas/Minimap/Minimap.tpl.js +9 -0
  28. package/vendor/symbiote-node/canvas/NodeCanvas/NodeCanvas.css.js +261 -0
  29. package/vendor/symbiote-node/canvas/NodeCanvas/NodeCanvas.js +1840 -0
  30. package/vendor/symbiote-node/canvas/NodeCanvas/NodeCanvas.tpl.js +22 -0
  31. package/vendor/symbiote-node/canvas/NodeSearch/NodeSearch.css.js +97 -0
  32. package/vendor/symbiote-node/canvas/NodeSearch/NodeSearch.js +132 -0
  33. package/vendor/symbiote-node/canvas/NodeSearch/NodeSearch.tpl.js +21 -0
  34. package/vendor/symbiote-node/canvas/NodeViewManager.js +584 -0
  35. package/vendor/symbiote-node/canvas/PinExpansion.js +131 -0
  36. package/vendor/symbiote-node/canvas/PseudoConnection.js +80 -0
  37. package/vendor/symbiote-node/canvas/SubgraphManager.js +201 -0
  38. package/vendor/symbiote-node/canvas/SubgraphRouter.js +443 -0
  39. package/vendor/symbiote-node/canvas/ViewportActions.js +446 -0
  40. package/vendor/symbiote-node/core/Connection.js +45 -0
  41. package/vendor/symbiote-node/core/Editor.js +451 -0
  42. package/vendor/symbiote-node/core/Frame.js +31 -0
  43. package/vendor/symbiote-node/core/GraphMermaid.js +348 -0
  44. package/vendor/symbiote-node/core/GraphText.js +210 -0
  45. package/vendor/symbiote-node/core/Node.js +143 -0
  46. package/vendor/symbiote-node/core/Portal.js +104 -0
  47. package/vendor/symbiote-node/core/Socket.js +185 -0
  48. package/vendor/symbiote-node/core/SubgraphNode.js +125 -0
  49. package/vendor/symbiote-node/index.js +103 -0
  50. package/vendor/symbiote-node/inspector/InspectorPanel/InspectorPanel.css.js +361 -0
  51. package/vendor/symbiote-node/inspector/InspectorPanel/InspectorPanel.js +332 -0
  52. package/vendor/symbiote-node/inspector/InspectorPanel/InspectorPanel.tpl.js +96 -0
  53. package/vendor/symbiote-node/inspector/TemplatePreview/TemplatePreview.css.js +104 -0
  54. package/vendor/symbiote-node/inspector/TemplatePreview/TemplatePreview.js +133 -0
  55. package/vendor/symbiote-node/inspector/TemplatePreview/TemplatePreview.tpl.js +33 -0
  56. package/vendor/symbiote-node/interactions/ConnectFlow.js +307 -0
  57. package/vendor/symbiote-node/interactions/Drag.js +102 -0
  58. package/vendor/symbiote-node/interactions/Selector.js +132 -0
  59. package/vendor/symbiote-node/interactions/SnapGrid.js +65 -0
  60. package/vendor/symbiote-node/interactions/Zoom.js +140 -0
  61. package/vendor/symbiote-node/layout/ActionZone/ActionZone.css.js +88 -0
  62. package/vendor/symbiote-node/layout/ActionZone/ActionZone.js +254 -0
  63. package/vendor/symbiote-node/layout/ActionZone/ActionZone.tpl.js +11 -0
  64. package/vendor/symbiote-node/layout/Layout/Layout.css.js +88 -0
  65. package/vendor/symbiote-node/layout/Layout/Layout.js +622 -0
  66. package/vendor/symbiote-node/layout/Layout/Layout.tpl.js +25 -0
  67. package/vendor/symbiote-node/layout/LayoutNode/LayoutNode.css.js +293 -0
  68. package/vendor/symbiote-node/layout/LayoutNode/LayoutNode.js +467 -0
  69. package/vendor/symbiote-node/layout/LayoutNode/LayoutNode.tpl.js +33 -0
  70. package/vendor/symbiote-node/layout/LayoutPreview/LayoutPreview.css.js +46 -0
  71. package/vendor/symbiote-node/layout/LayoutPreview/LayoutPreview.js +102 -0
  72. package/vendor/symbiote-node/layout/LayoutPreview/LayoutPreview.tpl.js +6 -0
  73. package/vendor/symbiote-node/layout/LayoutRouter/LayoutRouter.js +156 -0
  74. package/vendor/symbiote-node/layout/LayoutRouter/routerSync.js +250 -0
  75. package/vendor/symbiote-node/layout/LayoutSidebar/LayoutSidebar.css.js +379 -0
  76. package/vendor/symbiote-node/layout/LayoutSidebar/LayoutSidebar.js +263 -0
  77. package/vendor/symbiote-node/layout/LayoutSidebar/LayoutSidebar.tpl.js +20 -0
  78. package/vendor/symbiote-node/layout/LayoutSidebar/SidebarSection.js +183 -0
  79. package/vendor/symbiote-node/layout/LayoutTree.js +246 -0
  80. package/vendor/symbiote-node/layout/PanelMenu/PanelMenu.css.js +43 -0
  81. package/vendor/symbiote-node/layout/PanelMenu/PanelMenu.js +89 -0
  82. package/vendor/symbiote-node/layout/PanelMenu/PanelMenu.tpl.js +14 -0
  83. package/vendor/symbiote-node/layout/index.js +16 -0
  84. package/vendor/symbiote-node/menu/ContextMenu/ContextMenu.css.js +61 -0
  85. package/vendor/symbiote-node/menu/ContextMenu/ContextMenu.js +79 -0
  86. package/vendor/symbiote-node/menu/ContextMenu/ContextMenu.tpl.js +19 -0
  87. package/vendor/symbiote-node/node/CtrlItem/CtrlItem.css.js +41 -0
  88. package/vendor/symbiote-node/node/CtrlItem/CtrlItem.js +24 -0
  89. package/vendor/symbiote-node/node/CtrlItem/CtrlItem.tpl.js +16 -0
  90. package/vendor/symbiote-node/node/GraphFrame/GraphFrame.css.js +65 -0
  91. package/vendor/symbiote-node/node/GraphFrame/GraphFrame.js +29 -0
  92. package/vendor/symbiote-node/node/GraphFrame/GraphFrame.tpl.js +13 -0
  93. package/vendor/symbiote-node/node/GraphNode/GraphNode.css.js +683 -0
  94. package/vendor/symbiote-node/node/GraphNode/GraphNode.js +92 -0
  95. package/vendor/symbiote-node/node/GraphNode/GraphNode.tpl.js +17 -0
  96. package/vendor/symbiote-node/node/NodeSocket/NodeSocket.js +25 -0
  97. package/vendor/symbiote-node/node/NodeSocket/NodeSocket.tpl.js +7 -0
  98. package/vendor/symbiote-node/node/PortItem/PortItem.css.js +90 -0
  99. package/vendor/symbiote-node/node/PortItem/PortItem.js +87 -0
  100. package/vendor/symbiote-node/node/PortItem/PortItem.tpl.js +10 -0
  101. package/vendor/symbiote-node/package.json +59 -0
  102. package/vendor/symbiote-node/palette/PaletteBrowser/PaletteBrowser.css.js +143 -0
  103. package/vendor/symbiote-node/palette/PaletteBrowser/PaletteBrowser.js +131 -0
  104. package/vendor/symbiote-node/palette/PaletteBrowser/PaletteBrowser.tpl.js +16 -0
  105. package/vendor/symbiote-node/plugins/History.js +384 -0
  106. package/vendor/symbiote-node/plugins/Readonly.js +59 -0
  107. package/vendor/symbiote-node/shapes/CircleShape.js +80 -0
  108. package/vendor/symbiote-node/shapes/CommentShape.js +35 -0
  109. package/vendor/symbiote-node/shapes/DiamondShape.js +115 -0
  110. package/vendor/symbiote-node/shapes/NodeShape.js +80 -0
  111. package/vendor/symbiote-node/shapes/PillShape.js +91 -0
  112. package/vendor/symbiote-node/shapes/RectShape.js +72 -0
  113. package/vendor/symbiote-node/shapes/SVGShape.js +494 -0
  114. package/vendor/symbiote-node/shapes/index.js +53 -0
  115. package/vendor/symbiote-node/themes/Palette.js +32 -0
  116. package/vendor/symbiote-node/themes/Skin.js +113 -0
  117. package/vendor/symbiote-node/themes/Theme.js +84 -0
  118. package/vendor/symbiote-node/themes/carbon.js +137 -0
  119. package/vendor/symbiote-node/themes/dark.js +137 -0
  120. package/vendor/symbiote-node/themes/ebook.js +138 -0
  121. package/vendor/symbiote-node/themes/grey.js +137 -0
  122. package/vendor/symbiote-node/themes/light.js +137 -0
  123. package/vendor/symbiote-node/themes/neon.js +138 -0
  124. package/vendor/symbiote-node/themes/pcb.js +273 -0
  125. package/vendor/symbiote-node/themes/synthwave.js +137 -0
  126. package/vendor/symbiote-node/toolbar/QuickToolbar/QuickToolbar.css.js +86 -0
  127. package/vendor/symbiote-node/toolbar/QuickToolbar/QuickToolbar.js +128 -0
  128. package/vendor/symbiote-node/toolbar/QuickToolbar/QuickToolbar.tpl.js +29 -0
  129. package/web/app.js +6 -5
  130. package/web/components/canvas-graph.js +1666 -0
  131. package/web/components/event-feed/CodeWidget.js +32 -0
  132. package/web/components/event-feed/EventWidget.js +97 -0
  133. package/web/components/event-feed/ListWidget.js +57 -0
  134. package/web/components/event-feed/MiniGraphWidget.js +69 -0
  135. package/web/dashboard.js +1 -1
  136. package/web/index.html +4 -0
  137. package/web/panels/ActionBoard/ActionBoard.js +1 -1
  138. package/web/panels/SettingsPanel/SettingsPanel.tpl.js +1 -1
  139. package/web/panels/code-viewer.js +50 -15
  140. package/web/panels/dep-graph.js +2712 -7
  141. package/web/panels/file-tree.js +5 -2
  142. package/web/panels/live-monitor.js +75 -3
  143. package/web/style.css +33 -0
  144. package/docs/img/explorer-compact.jpg +0 -0
  145. package/docs/img/explorer-expanded.jpg +0 -0
  146. package/src/.contextignore +0 -22
  147. package/src/.project-graph-cache.json +0 -1
  148. package/src/compact/.project-graph-cache.json +0 -1
  149. package/web/.project-graph-cache.json +0 -1
  150. package/web/panels/SettingsPanel/.project-graph-cache.json +0 -1
@@ -0,0 +1,32 @@
1
+ import Symbiote from "@symbiotejs/symbiote";
2
+
3
+ export class CodeWidget extends Symbiote {
4
+ init$ = {
5
+ '@source': '',
6
+ truncatedSource: '',
7
+ expanded: false,
8
+ hasMore: false
9
+ };
10
+
11
+ renderCallback() {
12
+ this.sub('@source', (src) => {
13
+ if (!src) return;
14
+ const lines = src.split('\n');
15
+ if (lines.length > 10) {
16
+ this.$.hasMore = true;
17
+ this.$.truncatedSource = lines.slice(0, 10).join('\n') + '\n...';
18
+ } else {
19
+ this.$.hasMore = false;
20
+ this.$.truncatedSource = src;
21
+ }
22
+ });
23
+ }
24
+ }
25
+
26
+ CodeWidget.template = `
27
+ <div class="code-widget">
28
+ <pre class="code-block" ${{ textContent: 'truncatedSource' }}></pre>
29
+ </div>
30
+ `;
31
+
32
+ CodeWidget.reg('pg-code-widget');
@@ -0,0 +1,97 @@
1
+ import Symbiote from "@symbiotejs/symbiote";
2
+
3
+ export class EventWidget extends Symbiote {
4
+ init$ = {
5
+ '@eventData': null,
6
+ isCall: true,
7
+ tool: '',
8
+ argsJSON: '',
9
+ timeStr: '',
10
+ duration: '',
11
+ success: true,
12
+ widgetHTML: '',
13
+ };
14
+
15
+ renderCallback() {
16
+ this.sub('@eventData', (evStr) => {
17
+ if (!evStr) return;
18
+ let ev;
19
+ try {
20
+ ev = JSON.parse(evStr);
21
+ } catch {
22
+ return;
23
+ }
24
+
25
+ this.$.isCall = ev.type === 'tool_call';
26
+ this.$.tool = ev.tool;
27
+ this.$.timeStr = this._formatTime(ev.ts);
28
+
29
+ if (this.$.isCall) {
30
+ this.$.argsJSON = JSON.stringify(ev.args || {});
31
+ } else {
32
+ this.$.duration = `${ev.duration_ms}ms`;
33
+ this.$.success = ev.success !== false;
34
+ }
35
+
36
+ this._renderWidget(ev);
37
+ });
38
+ }
39
+
40
+ _renderWidget(ev) {
41
+ if (ev.type === 'tool_call') {
42
+ this.$.widgetHTML = '';
43
+ return;
44
+ }
45
+
46
+ const { tool, output, success } = ev;
47
+ if (!success || !output) {
48
+ this.$.widgetHTML = `<div class="error-msg">${this._esc(output || 'Error')}</div>`;
49
+ return;
50
+ }
51
+
52
+ let data;
53
+ try {
54
+ data = JSON.parse(output);
55
+ } catch {
56
+ data = output;
57
+ }
58
+
59
+ if (tool === 'default_api:view_file' || tool === 'default_api:replace_file_content' || tool === 'default_api:multi_replace_file_content' || tool === 'default_api:write_to_file') {
60
+ this.$.widgetHTML = `<pg-code-widget source='${this._esc(output)}'></pg-code-widget>`;
61
+ } else if (tool === 'default_api:mcp_project-graph_navigate' || tool === 'default_api:mcp_project-graph_get_skeleton') {
62
+ this.$.widgetHTML = `<pg-mini-graph data='${this._esc(JSON.stringify(data))}'></pg-mini-graph>`;
63
+ } else if (tool === 'default_api:list_dir' || tool === 'default_api:grep_search') {
64
+ this.$.widgetHTML = `<pg-list-widget data='${this._esc(output)}'></pg-list-widget>`;
65
+ } else {
66
+ this.$.widgetHTML = `<pre class="raw-output">${this._esc(output).substring(0, 500)}${output.length > 500 ? '...' : ''}</pre>`;
67
+ }
68
+ }
69
+
70
+ _esc(s) {
71
+ if (typeof s !== 'string') return '';
72
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/'/g, "&#39;").replace(/"/g, "&quot;");
73
+ }
74
+
75
+ _formatTime(ts) {
76
+ return ts ? new Date(ts).toLocaleTimeString('en', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' }) : '';
77
+ }
78
+ }
79
+
80
+ EventWidget.template = `
81
+ <div class="pg-mon-event" \${{ 'data-is-call': 'isCall' }}>
82
+ <div class="event-header">
83
+ <span class="pg-mon-arrow" \${{ textContent: 'isCall ? "→" : "←"' }}></span>
84
+ <span class="pg-mon-tool" \${{ textContent: 'tool' }}></span>
85
+ <span class="pg-mon-time" \${{ textContent: 'timeStr' }}></span>
86
+ <span class="pg-mon-duration" \${{ textContent: 'duration' }}></span>
87
+ </div>
88
+ <div class="event-body" \${{ hidden: '!isCall' }}>
89
+ <span class="pg-mon-args" \${{ textContent: 'argsJSON' }}></span>
90
+ </div>
91
+ <div class="event-body result-body" \${{ hidden: 'isCall' }}>
92
+ <div bind="innerHTML: widgetHTML"></div>
93
+ </div>
94
+ </div>
95
+ `;
96
+
97
+ EventWidget.reg('pg-event-widget');
@@ -0,0 +1,57 @@
1
+ import Symbiote from "@symbiotejs/symbiote";
2
+
3
+ export class ListWidget extends Symbiote {
4
+ init$ = {
5
+ '@data': '',
6
+ listHTML: ''
7
+ };
8
+
9
+ renderCallback() {
10
+ this.sub('@data', (dataStr) => {
11
+ if (!dataStr) return;
12
+
13
+ let items = [];
14
+ try {
15
+ const parsed = JSON.parse(dataStr);
16
+ if (Array.isArray(parsed)) items = parsed;
17
+ else if (typeof parsed === 'object') items = Object.entries(parsed).map(([k, v]) => `${k}: ${v}`);
18
+ } catch {
19
+ // If not JSON, split by lines
20
+ items = dataStr.split('\n').filter(Boolean);
21
+ }
22
+
23
+ if (items.length === 0) {
24
+ this.$.listHTML = '<div class="pg-placeholder">Empty list</div>';
25
+ return;
26
+ }
27
+
28
+ // Truncate to 50 items
29
+ const hasMore = items.length > 50;
30
+ const displayItems = items.slice(0, 50);
31
+
32
+ let html = '<ul class="list-widget-ul">';
33
+ displayItems.forEach(item => {
34
+ let text = typeof item === 'string' ? item : JSON.stringify(item);
35
+ html += `<li>${this._esc(text)}</li>`;
36
+ });
37
+ html += '</ul>';
38
+
39
+ if (hasMore) {
40
+ html += `<div class="list-widget-more">...and ${items.length - 50} more items</div>`;
41
+ }
42
+
43
+ this.$.listHTML = html;
44
+ });
45
+ }
46
+
47
+ _esc(s) {
48
+ if (typeof s !== 'string') return '';
49
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/'/g, "&#39;").replace(/"/g, "&quot;");
50
+ }
51
+ }
52
+
53
+ ListWidget.template = `
54
+ <div class="list-widget" bind="innerHTML: listHTML"></div>
55
+ `;
56
+
57
+ ListWidget.reg('pg-list-widget');
@@ -0,0 +1,69 @@
1
+ import Symbiote from "@symbiotejs/symbiote";
2
+
3
+ export class MiniGraphWidget extends Symbiote {
4
+ init$ = {
5
+ '@data': '',
6
+ svgContent: ''
7
+ };
8
+
9
+ renderCallback() {
10
+ this.sub('@data', (dataStr) => {
11
+ if (!dataStr) return;
12
+ let data;
13
+ try {
14
+ data = JSON.parse(dataStr);
15
+ } catch {
16
+ return;
17
+ }
18
+ this._renderSVG(data);
19
+ });
20
+ }
21
+
22
+ _renderSVG(data) {
23
+ // Basic SVG renderer for mini graphs to avoid WebGL context explosion
24
+ // Extracts nodes and links if present in the data
25
+ const nodes = data.nodes || (data.n ? Object.keys(data.n).map(id => ({ id, ...data.n[id] })) : []);
26
+ const links = data.links || [];
27
+
28
+ if (!nodes.length) {
29
+ this.$.svgContent = '<text x="10" y="20" fill="var(--sn-text-dim)">No graph data</text>';
30
+ return;
31
+ }
32
+
33
+ // Simple circle layout for demonstration
34
+ const width = 300;
35
+ const height = 150;
36
+ const cx = width / 2;
37
+ const cy = height / 2;
38
+ const radius = 50;
39
+
40
+ let svg = '';
41
+
42
+ // Draw links
43
+ // (Needs actual layout logic, this is a placeholder circle layout)
44
+
45
+ // Draw nodes
46
+ nodes.forEach((n, i) => {
47
+ const angle = (i / nodes.length) * Math.PI * 2;
48
+ const x = cx + Math.cos(angle) * radius;
49
+ const y = cy + Math.sin(angle) * radius;
50
+ svg += `<circle cx="${x}" cy="${y}" r="4" fill="var(--sn-node-selected, #4c8bf5)"></circle>`;
51
+ svg += `<text x="${x + 6}" y="${y + 3}" fill="var(--sn-text)" font-size="10">${this._esc(n.id || n.name || 'node')}</text>`;
52
+ });
53
+
54
+ this.$.svgContent = svg;
55
+ }
56
+
57
+ _esc(s) {
58
+ if (typeof s !== 'string') return '';
59
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/'/g, "&#39;").replace(/"/g, "&quot;");
60
+ }
61
+ }
62
+
63
+ MiniGraphWidget.template = `
64
+ <div class="mini-graph-widget">
65
+ <svg width="100%" height="150" viewBox="0 0 300 150" bind="innerHTML: svgContent"></svg>
66
+ </div>
67
+ `;
68
+
69
+ MiniGraphWidget.reg('pg-mini-graph');
package/web/dashboard.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // @ctx .context/web/dashboard.ctx
2
- import{Layout as e,LayoutTree as t,applyTheme as o}from"symbiote-node";import{CARBON as r}from"./vendor/symbiote-node/themes/carbon.js";import"./panels/ProjectList/ProjectList.js";import"./panels/ActionBoard/ActionBoard.js";import"./panels/SettingsPanel/SettingsPanel.js";import{state as a,events as n,emit as s}from"./dashboard-state.js";async function c(){const e=await fetch("/api/gateway-info");if(!e.ok){const t=await e.text();throw console.error("[dashboard] fetchGatewayInfo failed:",e.status,t),new Error(`Gateway info failed: ${e.status}`)}return e.json()}
2
+ import{Layout as e,LayoutTree as t,applyTheme as o,CARBON as r}from"symbiote-node";import"./panels/ProjectList/ProjectList.js";import"./panels/ActionBoard/ActionBoard.js";import"./panels/SettingsPanel/SettingsPanel.js";import{state as a,events as n,emit as s}from"./dashboard-state.js";async function c(){const e=await fetch("/api/gateway-info");if(!e.ok){const t=await e.text();throw console.error("[dashboard] fetchGatewayInfo failed:",e.status,t),new Error(`Gateway info failed: ${e.status}`)}return e.json()}
3
3
  function p(e){if(!e.length)return void console.warn("[dashboard] No projects to connect WebSockets for");
4
4
  const t="https:"===location.protocol?"wss://":"ws://",o=location.host;for(const r of e)i(r,t,o)}
5
5
  function i(e,t,o,_att=0){const r=`${t}${o}${e.prefix}/ws/monitor`,n=new WebSocket(r);n.onopen=()=>{_att=0;console.log("[dashboard] WS connected:",e.projectName)},n.onmessage=t=>{let o;try{o=JSON.parse(t.data)}catch{return}if("snapshot"===o.method&&o.params?.state){const t=o.params.state,r=a.projects.find(t=>t.prefix===e.prefix);return void(r&&t.project&&(Object.assign(r,{projectName:t.project.name,projectPath:t.project.path,color:t.project.color,agents:t.project.agents,pid:t.project.pid,connected:!0}),s("projects-updated",a.projects)))}if("patch"===o.method&&o.params){const t=a.projects.find(t=>t.prefix===e.prefix);return void(t&&"project.agents"===o.params.path&&(t.agents=o.params.value,s("projects-updated",a.projects)))}if("event"===o.method&&o.params){const t=o.params;return t._projectPrefix=e.prefix,t._projectName=e.projectName,a.events.push(t),a.events.length>1e3&&a.events.shift(),void s("global-tool-event",t)}o.type&&(o._projectPrefix=e.prefix,o._projectName=e.projectName,a.events.push(o),a.events.length>1e3&&a.events.shift(),s("global-tool-event",o))},n.onerror=()=>{console.error("[dashboard] WS error:",e.projectName)},n.onclose=r=>{console.warn("[dashboard] WS closed:",e.projectName,r.code);
package/web/index.html CHANGED
@@ -22,6 +22,10 @@
22
22
  <span id="compression-stats" class="compression-stats" style="display:none"></span>
23
23
  </div>
24
24
  <div class="topbar-right">
25
+ <button id="follow-btn" class="follow-btn" title="Follow mode: UI follows agent actions">
26
+ <span class="material-symbols-outlined">smart_toy</span>
27
+ FOLLOW
28
+ </button>
25
29
  <span id="agent-badge" class="agent-badge" style="display:none"></span>
26
30
  <span id="status-indicator" class="status connected"></span>
27
31
  </div>
@@ -1,5 +1,5 @@
1
1
  // @ctx .context/web/panels/ActionBoard/ActionBoard.ctx
2
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
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()}}
4
+ export class ActionBoard extends t{init$={eventsItems:[]};initCallback(){o.addEventListener("global-tool-event",t=>{const o=[...e.events].reverse();this.$.eventsItems=o}),this.$.eventsItems=[...e.events].reverse()}}
5
5
  ActionBoard.template=n,ActionBoard.rootStyles=s,ActionBoard.reg("pg-action-board");
@@ -1,2 +1,2 @@
1
1
  // @ctx .context/web/panels/SettingsPanel/SettingsPanel.tpl.ctx
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">Server Lifecycle</div>\n<div class="pg-stg-card" ref="lifecycleCard">\n <div class="pg-stg-metric"><span>Auto-shutdown</span><span class="pg-stg-val" ref="shutdownTimer">—</span></div>\n <div class="pg-stg-metric"><span>Uptime</span><span class="pg-stg-val" ref="uptimeVal">—</span></div>\n</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</button>\n<button class="pg-stg-btn pg-stg-btn-danger" ref="stopBtn">⏹ Stop</button>\n</div>\n<div ref="restartStatus" style="margin-top:8px;font-size:11px;color:var(--sn-text-dim)"></div>\n';
2
+ export default'\n<div class="pg-stg-title">Actions</div>\n<div style="display:flex;gap:8px;margin-bottom:16px">\n<button class="pg-stg-btn" ref="refreshBtn">↻ Refresh</button>\n<button class="pg-stg-btn pg-stg-btn-danger" ref="restartBtn">⟳ Restart</button>\n<button class="pg-stg-btn pg-stg-btn-danger" ref="stopBtn">⏹ Stop</button>\n</div>\n<div ref="restartStatus" style="margin-bottom:16px;font-size:11px;color:var(--sn-text-dim)"></div>\n\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">Server Lifecycle</div>\n<div class="pg-stg-card" ref="lifecycleCard">\n <div class="pg-stg-metric"><span>Auto-shutdown</span><span class="pg-stg-val" ref="shutdownTimer">—</span></div>\n <div class="pg-stg-metric"><span>Uptime</span><span class="pg-stg-val" ref="uptimeVal">—</span></div>\n</div>\n';
@@ -16,7 +16,10 @@ function _getLang(path){if(!path)return'js';const i=path.lastIndexOf('.');if(i<0
16
16
  // Toggle button label: "EXPAND" — beautifies via Terser + injects JSDoc from .ctx.
17
17
  // _isReadable = false (compression saves <15% — already compact)
18
18
 
19
- export class CodeViewer extends e{init$={filename:"Select a file",hasFile:!1,viewMode:"source",modeLabel:"source",statsText:"",showToggle:!1,toggleLabel:"",onToggleMode:()=>{
19
+ export class CodeViewer extends e{init$={filename:"Select a file",hasFile:!1,viewMode:"source",modeLabel:"source",statsText:"",showToggle:!1,toggleLabel:"",onShowInGraph:()=>{
20
+ if(!this._currentPath)return;
21
+ window.location.hash = `#graph?focus=${encodeURIComponent(this._currentPath)}`;
22
+ },onToggleMode:()=>{
20
23
  const lang=_getLang(this._currentPath);
21
24
  if(lang==='md'){
22
25
  this.$.viewMode=this.$.viewMode==="rendered"?"raw":"rendered";
@@ -26,7 +29,7 @@ export class CodeViewer extends e{init$={filename:"Select a file",hasFile:!1,vie
26
29
  // Toggle between source and the transformation
27
30
  this.$.viewMode=this.$.viewMode==="source"?"transformed":"source";
28
31
  this._showCurrentMode();
29
- }};_fileData=null;_isReadable=!1;_transformCache=null;_loadingTransform=!1;_currentPath=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=>{
32
+ }};_fileData=null;_isReadable=!1;_transformCache=null;_loadingTransform=!1;_currentPath=null;initCallback(){t.addEventListener("file-selected",e=>this._loadFile(e.detail.path));if(o.activeFile)requestAnimationFrame(()=>this._loadFile(o.activeFile))}renderCallback(){this.sub("hasFile",e=>{this.toggleAttribute("has-file",e)}),this.sub("viewMode",e=>{
30
33
  const lang=_getLang(this._currentPath);
31
34
  this.toggleAttribute("mode-raw","source"!==e);
32
35
  if(lang==='md'){
@@ -50,28 +53,36 @@ if(lang==='md'){
50
53
  e.$.lang=lang;
51
54
  if("transformed"===this.$.viewMode){
52
55
  // Show cached transform if available
53
- if(this._transformCache){e.$.code=this._transformCache;return}
56
+ if(this._transformCache){
57
+ e.$.code=this._transformCache;
58
+ if(this._transformStatsText) this.$.statsText=this._transformStatsText;
59
+ return;
60
+ }
54
61
  if(this._loadingTransform)return;
55
62
  this._loadingTransform=!0;
56
63
  e.$.code=this._isReadable?"// Compressing...":"// Expanding...";
57
64
  try{
58
- if(this._isReadable){
59
- // MODE A: readable source → compress
60
- const t=await n("/api/compact-file",{path:this._currentPath});
61
- this._transformCache=t?.code||"// Compression unavailable";
62
- }else{
63
- // MODE B: compact source → expand (beautify + inject JSDoc from .ctx)
64
- const t=await n("/api/expand-file",{path:this._currentPath});
65
- this._transformCache=t?.code||"// Expand unavailable";
66
- }
65
+ if(this._isReadable){
66
+ // MODE A: readable source → compress
67
+ const t=await n("/api/compact-file",{path:this._currentPath});
68
+ this._transformCache=t?.code||"// Compression unavailable";
69
+ this._transformStatsText=t?`Compressed: ${(t.compressed/1000).toFixed(1)}K chars (${t.savings})`:"";
70
+ }else{
71
+ // MODE B: compact source → expand (beautify + inject JSDoc from .ctx)
72
+ const t=await n("/api/expand-file",{path:this._currentPath});
73
+ this._transformCache=t?.code||"// Expand unavailable";
74
+ this._transformStatsText=t?`Expanded: ${(t.decompiled/1000).toFixed(1)}K chars | JSDocs injected: ${t.injected||0}`:"";
75
+ }
76
+ if(this._transformStatsText)this.$.statsText=this._transformStatsText;
67
77
  e.$.code=this._transformCache;
68
78
  }catch{e.$.code=this._isReadable?"// Compression failed":"// Expand failed"}
69
79
  finally{this._loadingTransform=!1}
70
80
  return;
71
81
  }
72
82
  // Source mode — raw file as-is
83
+ this.$.statsText=this._baseStatsText;
73
84
  e.$.code=this._fileData.raw;
74
- }async _loadFile(e){this.$.filename=e,this.$.hasFile=!1,this._fileData=null,this.$.statsText="",this._transformCache=null,this._currentPath=e;
85
+ }async _loadFile(e){this.$.filename=e,this.$.hasFile=!1,this._fileData=null,this.$.statsText="",this._baseStatsText="",this._transformStatsText="",this._transformCache=null,this._currentPath=e;
75
86
  const lang=_getLang(e);
76
87
  if(lang==='image'){
77
88
  const i=this._getCodeBlock();
@@ -97,7 +108,10 @@ let s=_raw?.content||o;
97
108
  // If no .ctx, source is readable → COMPACT available
98
109
  const hasCtx=!!(t.ctxTok&&t.ctxTok>0);
99
110
  this._isReadable=!hasCtx;
100
- this._fileData={compact:o,raw:s,codeTok:t.codeTok||0,ctxTok:t.ctxTok||0,totalTok:t.totalTok||0,expanded:t.expanded||0,savings:t.savings||"0%"},t.codeTok&&t.expanded&&(this.$.statsText=formatStats(t));const i=this._getCodeBlock();
111
+ this._fileData={compact:o,raw:s,codeTok:t.codeTok||0,ctxTok:t.ctxTok||0,totalTok:t.totalTok||0,expanded:t.expanded||0,savings:t.savings||"0%"};
112
+ this._baseStatsText=t.codeTok&&t.expanded?formatStats(t):"";
113
+ this.$.statsText=this._baseStatsText;
114
+ const i=this._getCodeBlock();
101
115
  if(lang==='md'){
102
116
  this.$.viewMode="rendered";
103
117
  this.$.modeLabel="rendered";
@@ -114,4 +128,25 @@ if(lang==='md'){
114
128
  this.$.toggleLabel=this._isReadable?"compact":"expand";
115
129
  i&&(i.$.code=s);
116
130
  }
117
- this.$.hasFile=!0}catch(e){const n=this._getCodeBlock();n&&(n.$.lang='plain',n.$.code=`// Error: ${e.message}`),this.$.showToggle=!1,this.$.hasFile=!0}}}CodeViewer.template='\n <div class="pg-code-header">\n <span class="pg-code-filename" bind="textContent: filename"></span>\n <div class="pg-code-controls">\n <span class="pg-code-stats" bind="textContent: statsText"></span>\n <button class="pg-mode-toggle" bind="onclick: onToggleMode; hidden: !showToggle" title="Toggle view mode">\n <span class="material-symbols-outlined" style="font-size:14px">compress</span>\n <span class="pg-mode-label" bind="textContent: modeLabel"></span>\n </button>\n </div>\n </div>\n <code-block></code-block>\n',CodeViewer.rootStyles="\n pg-code-viewer {\n display: flex;\n flex-direction: column;\n height: 100%;\n overflow: hidden;\n }\n pg-code-viewer:not([has-file]) code-block {\n display: none;\n }\n .pg-code-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 6px 12px;\n font-family: 'SF Mono', 'Fira Code', monospace;\n font-size: 11px;\n color: var(--sn-text-dim, hsl(30, 10%, 45%));\n border-bottom: 1px solid var(--sn-node-border, hsl(35, 18%, 80%));\n background: var(--sn-node-header-bg, hsl(37, 25%, 93%));\n gap: 8px;\n }\n .pg-code-filename {\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n min-width: 0;\n }\n .pg-code-controls {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-shrink: 0;\n }\n .pg-code-stats {\n font-size: 10px;\n color: var(--sn-cat-server, hsl(210, 45%, 45%));\n white-space: nowrap;\n }\n .pg-mode-toggle {\n display: flex;\n align-items: center;\n gap: 3px;\n padding: 2px 8px;\n border: 1px solid var(--sn-node-border, hsl(35, 18%, 80%));\n border-radius: 4px;\n background: var(--sn-bg, hsl(37, 30%, 91%));\n color: var(--sn-text-dim, hsl(30, 10%, 45%));\n font-family: inherit;\n font-size: 10px;\n cursor: pointer;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n transition: all 120ms ease;\n }\n .pg-mode-toggle:hover {\n background: var(--sn-node-hover, hsl(36, 22%, 88%));\n color: var(--sn-text, hsl(30, 15%, 18%));\n }\n pg-code-viewer[mode-raw] .pg-mode-toggle {\n background: hsla(210, 45%, 45%, 0.12);\n border-color: var(--sn-cat-server, hsl(210, 45%, 45%));\n color: var(--sn-cat-server, hsl(210, 45%, 45%));\n }\n .pg-mode-toggle[hidden] {\n display: none;\n }\n code-block {\n flex: 1;\n min-height: 0;\n }\n",CodeViewer.reg("pg-code-viewer");
131
+ this.$.hasFile=!0}catch(e){const n=this._getCodeBlock();n&&(n.$.lang='plain',n.$.code=`// Error: ${e.message}`),this.$.showToggle=!1,this.$.hasFile=!0}}}
132
+
133
+ CodeViewer.template=`
134
+ <div class="pg-code-header">
135
+ <span class="pg-code-filename" bind="textContent: filename"></span>
136
+ <div class="pg-code-controls">
137
+ <span class="pg-code-stats" bind="textContent: statsText"></span>
138
+ <button class="pg-mode-toggle" bind="onclick: onShowInGraph" title="Show in Graph">
139
+ <span class="material-symbols-outlined" style="font-size:14px">account_tree</span>
140
+ <span class="pg-mode-label">graph</span>
141
+ </button>
142
+ <button class="pg-mode-toggle" bind="onclick: onToggleMode; hidden: !showToggle" title="Toggle view mode">
143
+ <span class="material-symbols-outlined" style="font-size:14px">compress</span>
144
+ <span class="pg-mode-label" bind="textContent: modeLabel"></span>
145
+ </button>
146
+ </div>
147
+ </div>
148
+ <code-block></code-block>
149
+ `;
150
+
151
+ CodeViewer.rootStyles="\n pg-code-viewer {\n display: flex;\n flex-direction: column;\n height: 100%;\n overflow: hidden;\n }\n pg-code-viewer:not([has-file]) code-block {\n display: none;\n }\n .pg-code-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 6px 12px;\n font-family: 'SF Mono', 'Fira Code', monospace;\n font-size: 11px;\n color: var(--sn-text-dim, hsl(30, 10%, 45%));\n border-bottom: 1px solid var(--sn-node-border, hsl(35, 18%, 80%));\n background: var(--sn-node-header-bg, hsl(37, 25%, 93%));\n gap: 8px;\n }\n .pg-code-filename {\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n min-width: 0;\n }\n .pg-code-controls {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-shrink: 0;\n }\n .pg-code-stats {\n font-size: 10px;\n color: var(--sn-cat-server, hsl(210, 45%, 45%));\n white-space: nowrap;\n }\n .pg-mode-toggle {\n display: flex;\n align-items: center;\n gap: 3px;\n padding: 2px 8px;\n border: 1px solid var(--sn-node-border, hsl(35, 18%, 80%));\n border-radius: 4px;\n background: var(--sn-bg, hsl(37, 30%, 91%));\n color: var(--sn-text-dim, hsl(30, 10%, 45%));\n font-family: inherit;\n font-size: 10px;\n cursor: pointer;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n transition: all 120ms ease;\n }\n .pg-mode-toggle:hover {\n background: var(--sn-node-hover, hsl(36, 22%, 88%));\n color: var(--sn-text, hsl(30, 15%, 18%));\n }\n pg-code-viewer[mode-raw] .pg-mode-toggle {\n background: hsla(210, 45%, 45%, 0.12);\n border-color: var(--sn-cat-server, hsl(210, 45%, 45%));\n color: var(--sn-cat-server, hsl(210, 45%, 45%));\n }\n .pg-mode-toggle[hidden] {\n display: none;\n }\n code-block {\n flex: 1;\n min-height: 0;\n }\n";
152
+ CodeViewer.reg("pg-code-viewer");