project-graph-mcp 2.2.6 → 2.3.1

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 (155) hide show
  1. package/ARCHITECTURE.md +81 -0
  2. package/CHANGELOG.md +57 -0
  3. package/README.md +9 -4
  4. package/package.json +4 -13
  5. package/project-graph-mcp-2.3.0.tgz +0 -0
  6. package/src/compact/expand.js +1 -1
  7. package/src/core/graph-builder.js +2 -2
  8. package/src/core/parser.js +2 -2
  9. package/src/network/server.js +1 -2
  10. package/src/network/web-server.js +1 -1
  11. package/vendor/symbiote-node/CHANGELOG.md +31 -0
  12. package/vendor/symbiote-node/LICENSE +21 -0
  13. package/vendor/symbiote-node/README.md +206 -0
  14. package/vendor/symbiote-node/canvas/AutoLayout.js +725 -0
  15. package/vendor/symbiote-node/canvas/Breadcrumb/Breadcrumb.css.js +73 -0
  16. package/vendor/symbiote-node/canvas/Breadcrumb/Breadcrumb.js +93 -0
  17. package/vendor/symbiote-node/canvas/Breadcrumb/Breadcrumb.tpl.js +9 -0
  18. package/vendor/symbiote-node/canvas/CanvasConnectionRenderer.js +962 -0
  19. package/vendor/symbiote-node/canvas/ConnectionRenderer.js +1468 -0
  20. package/vendor/symbiote-node/canvas/FlowSimulator.js +323 -0
  21. package/vendor/symbiote-node/canvas/ForceLayout.js +189 -0
  22. package/vendor/symbiote-node/canvas/ForceWorker.js +1325 -0
  23. package/vendor/symbiote-node/canvas/GraphTabs/GraphTabs.css.js +97 -0
  24. package/vendor/symbiote-node/canvas/GraphTabs/GraphTabs.js +176 -0
  25. package/vendor/symbiote-node/canvas/GraphTabs/GraphTabs.tpl.js +12 -0
  26. package/vendor/symbiote-node/canvas/LODManager.js +88 -0
  27. package/vendor/symbiote-node/canvas/Minimap/Minimap.css.js +71 -0
  28. package/vendor/symbiote-node/canvas/Minimap/Minimap.js +207 -0
  29. package/vendor/symbiote-node/canvas/Minimap/Minimap.tpl.js +9 -0
  30. package/vendor/symbiote-node/canvas/NodeCanvas/NodeCanvas.css.js +261 -0
  31. package/vendor/symbiote-node/canvas/NodeCanvas/NodeCanvas.js +1840 -0
  32. package/vendor/symbiote-node/canvas/NodeCanvas/NodeCanvas.tpl.js +22 -0
  33. package/vendor/symbiote-node/canvas/NodeSearch/NodeSearch.css.js +97 -0
  34. package/vendor/symbiote-node/canvas/NodeSearch/NodeSearch.js +132 -0
  35. package/vendor/symbiote-node/canvas/NodeSearch/NodeSearch.tpl.js +21 -0
  36. package/vendor/symbiote-node/canvas/NodeViewManager.js +584 -0
  37. package/vendor/symbiote-node/canvas/PinExpansion.js +131 -0
  38. package/vendor/symbiote-node/canvas/PseudoConnection.js +80 -0
  39. package/vendor/symbiote-node/canvas/SubgraphManager.js +201 -0
  40. package/vendor/symbiote-node/canvas/SubgraphRouter.js +443 -0
  41. package/vendor/symbiote-node/canvas/ViewportActions.js +446 -0
  42. package/vendor/symbiote-node/core/Connection.js +45 -0
  43. package/vendor/symbiote-node/core/Editor.js +451 -0
  44. package/vendor/symbiote-node/core/Frame.js +31 -0
  45. package/vendor/symbiote-node/core/GraphMermaid.js +348 -0
  46. package/vendor/symbiote-node/core/GraphText.js +210 -0
  47. package/vendor/symbiote-node/core/Node.js +143 -0
  48. package/vendor/symbiote-node/core/Portal.js +104 -0
  49. package/vendor/symbiote-node/core/Socket.js +185 -0
  50. package/vendor/symbiote-node/core/SubgraphNode.js +125 -0
  51. package/vendor/symbiote-node/index.js +103 -0
  52. package/vendor/symbiote-node/inspector/InspectorPanel/InspectorPanel.css.js +361 -0
  53. package/vendor/symbiote-node/inspector/InspectorPanel/InspectorPanel.js +332 -0
  54. package/vendor/symbiote-node/inspector/InspectorPanel/InspectorPanel.tpl.js +96 -0
  55. package/vendor/symbiote-node/inspector/TemplatePreview/TemplatePreview.css.js +104 -0
  56. package/vendor/symbiote-node/inspector/TemplatePreview/TemplatePreview.js +133 -0
  57. package/vendor/symbiote-node/inspector/TemplatePreview/TemplatePreview.tpl.js +33 -0
  58. package/vendor/symbiote-node/interactions/ConnectFlow.js +307 -0
  59. package/vendor/symbiote-node/interactions/Drag.js +102 -0
  60. package/vendor/symbiote-node/interactions/Selector.js +132 -0
  61. package/vendor/symbiote-node/interactions/SnapGrid.js +65 -0
  62. package/vendor/symbiote-node/interactions/Zoom.js +140 -0
  63. package/vendor/symbiote-node/layout/ActionZone/ActionZone.css.js +88 -0
  64. package/vendor/symbiote-node/layout/ActionZone/ActionZone.js +254 -0
  65. package/vendor/symbiote-node/layout/ActionZone/ActionZone.tpl.js +11 -0
  66. package/vendor/symbiote-node/layout/Layout/Layout.css.js +88 -0
  67. package/vendor/symbiote-node/layout/Layout/Layout.js +622 -0
  68. package/vendor/symbiote-node/layout/Layout/Layout.tpl.js +25 -0
  69. package/vendor/symbiote-node/layout/LayoutNode/LayoutNode.css.js +293 -0
  70. package/vendor/symbiote-node/layout/LayoutNode/LayoutNode.js +467 -0
  71. package/vendor/symbiote-node/layout/LayoutNode/LayoutNode.tpl.js +33 -0
  72. package/vendor/symbiote-node/layout/LayoutPreview/LayoutPreview.css.js +46 -0
  73. package/vendor/symbiote-node/layout/LayoutPreview/LayoutPreview.js +102 -0
  74. package/vendor/symbiote-node/layout/LayoutPreview/LayoutPreview.tpl.js +6 -0
  75. package/vendor/symbiote-node/layout/LayoutRouter/LayoutRouter.js +156 -0
  76. package/vendor/symbiote-node/layout/LayoutRouter/routerSync.js +250 -0
  77. package/vendor/symbiote-node/layout/LayoutSidebar/LayoutSidebar.css.js +379 -0
  78. package/vendor/symbiote-node/layout/LayoutSidebar/LayoutSidebar.js +263 -0
  79. package/vendor/symbiote-node/layout/LayoutSidebar/LayoutSidebar.tpl.js +20 -0
  80. package/vendor/symbiote-node/layout/LayoutSidebar/SidebarSection.js +183 -0
  81. package/vendor/symbiote-node/layout/LayoutTree.js +246 -0
  82. package/vendor/symbiote-node/layout/PanelMenu/PanelMenu.css.js +43 -0
  83. package/vendor/symbiote-node/layout/PanelMenu/PanelMenu.js +89 -0
  84. package/vendor/symbiote-node/layout/PanelMenu/PanelMenu.tpl.js +14 -0
  85. package/vendor/symbiote-node/layout/index.js +16 -0
  86. package/vendor/symbiote-node/menu/ContextMenu/ContextMenu.css.js +61 -0
  87. package/vendor/symbiote-node/menu/ContextMenu/ContextMenu.js +79 -0
  88. package/vendor/symbiote-node/menu/ContextMenu/ContextMenu.tpl.js +19 -0
  89. package/vendor/symbiote-node/node/CtrlItem/CtrlItem.css.js +41 -0
  90. package/vendor/symbiote-node/node/CtrlItem/CtrlItem.js +24 -0
  91. package/vendor/symbiote-node/node/CtrlItem/CtrlItem.tpl.js +16 -0
  92. package/vendor/symbiote-node/node/GraphFrame/GraphFrame.css.js +65 -0
  93. package/vendor/symbiote-node/node/GraphFrame/GraphFrame.js +29 -0
  94. package/vendor/symbiote-node/node/GraphFrame/GraphFrame.tpl.js +13 -0
  95. package/vendor/symbiote-node/node/GraphNode/GraphNode.css.js +683 -0
  96. package/vendor/symbiote-node/node/GraphNode/GraphNode.js +92 -0
  97. package/vendor/symbiote-node/node/GraphNode/GraphNode.tpl.js +17 -0
  98. package/vendor/symbiote-node/node/NodeSocket/NodeSocket.js +25 -0
  99. package/vendor/symbiote-node/node/NodeSocket/NodeSocket.tpl.js +7 -0
  100. package/vendor/symbiote-node/node/PortItem/PortItem.css.js +90 -0
  101. package/vendor/symbiote-node/node/PortItem/PortItem.js +87 -0
  102. package/vendor/symbiote-node/node/PortItem/PortItem.tpl.js +10 -0
  103. package/vendor/symbiote-node/package.json +59 -0
  104. package/vendor/symbiote-node/palette/PaletteBrowser/PaletteBrowser.css.js +143 -0
  105. package/vendor/symbiote-node/palette/PaletteBrowser/PaletteBrowser.js +131 -0
  106. package/vendor/symbiote-node/palette/PaletteBrowser/PaletteBrowser.tpl.js +16 -0
  107. package/vendor/symbiote-node/plugins/History.js +384 -0
  108. package/vendor/symbiote-node/plugins/Readonly.js +59 -0
  109. package/vendor/symbiote-node/shapes/CircleShape.js +80 -0
  110. package/vendor/symbiote-node/shapes/CommentShape.js +35 -0
  111. package/vendor/symbiote-node/shapes/DiamondShape.js +115 -0
  112. package/vendor/symbiote-node/shapes/NodeShape.js +80 -0
  113. package/vendor/symbiote-node/shapes/PillShape.js +91 -0
  114. package/vendor/symbiote-node/shapes/RectShape.js +72 -0
  115. package/vendor/symbiote-node/shapes/SVGShape.js +494 -0
  116. package/vendor/symbiote-node/shapes/index.js +53 -0
  117. package/vendor/symbiote-node/themes/Palette.js +32 -0
  118. package/vendor/symbiote-node/themes/Skin.js +113 -0
  119. package/vendor/symbiote-node/themes/Theme.js +84 -0
  120. package/vendor/symbiote-node/themes/carbon.js +137 -0
  121. package/vendor/symbiote-node/themes/dark.js +137 -0
  122. package/vendor/symbiote-node/themes/ebook.js +138 -0
  123. package/vendor/symbiote-node/themes/grey.js +137 -0
  124. package/vendor/symbiote-node/themes/light.js +137 -0
  125. package/vendor/symbiote-node/themes/neon.js +138 -0
  126. package/vendor/symbiote-node/themes/pcb.js +273 -0
  127. package/vendor/symbiote-node/themes/synthwave.js +137 -0
  128. package/vendor/symbiote-node/toolbar/QuickToolbar/QuickToolbar.css.js +86 -0
  129. package/vendor/symbiote-node/toolbar/QuickToolbar/QuickToolbar.js +128 -0
  130. package/vendor/symbiote-node/toolbar/QuickToolbar/QuickToolbar.tpl.js +29 -0
  131. package/web/app.js +9 -5
  132. package/web/components/canvas-graph.js +1705 -0
  133. package/web/components/code-block.js +1 -1
  134. package/web/components/event-feed/CodeWidget.js +32 -0
  135. package/web/components/event-feed/EventWidget.js +97 -0
  136. package/web/components/event-feed/ListWidget.js +57 -0
  137. package/web/components/event-feed/MiniGraphWidget.js +159 -0
  138. package/web/components/follow-ribbon.js +134 -0
  139. package/web/dashboard.js +1 -1
  140. package/web/follow-controller.js +241 -0
  141. package/web/index.html +4 -0
  142. package/web/panels/ActionBoard/ActionBoard.js +1 -1
  143. package/web/panels/SettingsPanel/SettingsPanel.tpl.js +1 -1
  144. package/web/panels/code-viewer.js +50 -15
  145. package/web/panels/dep-graph.js +2691 -7
  146. package/web/panels/file-tree.js +5 -2
  147. package/web/panels/live-monitor.js +75 -3
  148. package/web/style.css +39 -0
  149. package/docs/img/explorer-compact.jpg +0 -0
  150. package/docs/img/explorer-expanded.jpg +0 -0
  151. package/src/.contextignore +0 -22
  152. package/src/.project-graph-cache.json +0 -1
  153. package/src/compact/.project-graph-cache.json +0 -1
  154. package/web/.project-graph-cache.json +0 -1
  155. package/web/panels/SettingsPanel/.project-graph-cache.json +0 -1
@@ -0,0 +1,241 @@
1
+ // @ctx .context/web/follow-controller.ctx
2
+ /**
3
+ * FollowController — Central orchestrator for Follow Mode.
4
+ *
5
+ * Classifies incoming tool-events and dispatches debounced focus-change
6
+ * signals to subscribed panels (graph, code-viewer, monitor).
7
+ * Also manages the status ribbon text shown during active follow.
8
+ *
9
+ * NOTE: Does NOT import from app.js to avoid circular dependency.
10
+ * Call init(events, emit) before enable().
11
+ */
12
+
13
+ /** Debounce delay for heavy visual updates (camera, code loading) */
14
+ const HEAVY_DEBOUNCE = 800;
15
+
16
+ class FollowController {
17
+ /** @type {boolean} */
18
+ enabled = false;
19
+ /** @type {{type: string, target: any, action?: string, meta?: object}|null} */
20
+ currentFocus = null;
21
+ /** @type {string} */
22
+ statusText = '';
23
+ /** @type {number|null} */
24
+ _debounceTimer = null;
25
+ /** @type {string|null} Previous hash before entering follow mode */
26
+ _previousHash = null;
27
+ /** @type {Function|null} */
28
+ _boundHandler = null;
29
+ /** @type {EventTarget|null} */
30
+ _events = null;
31
+ /** @type {Function|null} */
32
+ _emit = null;
33
+
34
+ /**
35
+ * Late-bind events bus and emit function (breaks circular import).
36
+ * Must be called once before enable().
37
+ * @param {EventTarget} events
38
+ * @param {Function} emit
39
+ */
40
+ init(events, emit) {
41
+ this._events = events;
42
+ this._emit = emit;
43
+ }
44
+
45
+ enable() {
46
+ if (this.enabled) return;
47
+ this.enabled = true;
48
+
49
+ // Save current location for restoring later
50
+ this._previousHash = location.hash;
51
+
52
+ // Bind tool-event listener
53
+ this._boundHandler = (e) => this._onToolEvent(e.detail);
54
+ this._events.addEventListener('tool-event', this._boundHandler);
55
+
56
+ this._emit('follow-state-changed', { enabled: true });
57
+ }
58
+
59
+ disable() {
60
+ if (!this.enabled) return;
61
+ this.enabled = false;
62
+
63
+ // Clean up
64
+ if (this._boundHandler) {
65
+ this._events.removeEventListener('tool-event', this._boundHandler);
66
+ this._boundHandler = null;
67
+ }
68
+ if (this._debounceTimer) {
69
+ clearTimeout(this._debounceTimer);
70
+ this._debounceTimer = null;
71
+ }
72
+
73
+ this.currentFocus = null;
74
+ this._emitStatus('');
75
+
76
+ this._emit('follow-state-changed', { enabled: false });
77
+ }
78
+
79
+ /** @returns {string|null} */
80
+ getPreviousHash() {
81
+ return this._previousHash;
82
+ }
83
+
84
+ /**
85
+ * Main tool-event dispatcher. Classifies the event and routes to appropriate action.
86
+ * @param {object} event - Tool event from WebSocket
87
+ */
88
+ _onToolEvent(event) {
89
+ if (!this.enabled) return;
90
+
91
+ const toolName = event.tool || event.name || '';
92
+ const args = event.args || {};
93
+ const isCall = event.type === 'tool_call';
94
+ const isResult = event.type === 'tool_result';
95
+
96
+ // Extract short tool name (strip prefixes like 'default_api:', 'mcp_project-graph_')
97
+ const shortName = this._shortName(toolName);
98
+
99
+ // Status ribbon — update immediately on call
100
+ if (isCall) {
101
+ const statusText = this._buildStatusText(shortName, args);
102
+ if (statusText) this._emitStatus(statusText);
103
+ }
104
+
105
+ // Visual focus — classify and dispatch (debounced for heavy ops)
106
+ const action = this._classify(shortName, args, isCall, isResult, event);
107
+ if (action) {
108
+ if (action.immediate) {
109
+ this._emitFocusNow(action.focus);
110
+ } else {
111
+ this._emitFocusDebounced(action.focus, action.debounce || HEAVY_DEBOUNCE);
112
+ }
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Classify tool event into a visual action.
118
+ * Only handles tools emitted by our MCP server (navigate, get_skeleton, etc.).
119
+ * IDE-local tools (view_file, grep_search) never arrive over WebSocket.
120
+ * @returns {{focus: object, debounce?: number, immediate?: boolean}|null}
121
+ */
122
+ _classify(tool, args, isCall, isResult, raw) {
123
+ if (!isCall) return null;
124
+
125
+ // === Graph navigation ===
126
+ if (tool === 'navigate') {
127
+ if (args.action === 'expand' && args.symbol) {
128
+ return { focus: { type: 'graph', target: args.symbol, action: 'focus' }, debounce: HEAVY_DEBOUNCE };
129
+ }
130
+ if (args.action === 'deps' && args.symbol) {
131
+ return { focus: { type: 'graph', target: args.symbol, action: 'deps' }, debounce: HEAVY_DEBOUNCE };
132
+ }
133
+ if (args.action === 'usages' && args.symbol) {
134
+ return { focus: { type: 'graph', target: args.symbol, action: 'deps' }, debounce: HEAVY_DEBOUNCE };
135
+ }
136
+ if (args.action === 'call_chain' && args.from && args.to) {
137
+ return { focus: { type: 'graph', target: { from: args.from, to: args.to }, action: 'chain' }, immediate: true };
138
+ }
139
+ }
140
+
141
+ // === Skeleton / Overview ===
142
+ if (tool === 'get_skeleton' || tool === 'get_ai_context') {
143
+ return { focus: { type: 'graph', action: 'fit' }, immediate: true };
144
+ }
145
+
146
+ // === Code compaction (compact_file action has a file path) ===
147
+ if (tool === 'compact' && args.path) {
148
+ return { focus: { type: 'file', target: args.path }, debounce: HEAVY_DEBOUNCE };
149
+ }
150
+
151
+ // === Analysis ===
152
+ if (tool === 'analyze') {
153
+ return { focus: { type: 'analysis' }, immediate: true };
154
+ }
155
+
156
+ return null;
157
+ }
158
+
159
+ /**
160
+ * Build human-readable status text for the ribbon.
161
+ * Only MCP-server tools arrive here (navigate, get_skeleton, compact, analyze, docs, etc.).
162
+ * @param {string} tool
163
+ * @param {object} args
164
+ * @returns {string}
165
+ */
166
+ _buildStatusText(tool, args) {
167
+ const file = args.path || '';
168
+ const short = file ? file.split('/').slice(-2).join('/') : '';
169
+
170
+ switch (tool) {
171
+ case 'navigate': {
172
+ if (args.action === 'expand') return `Expanding ${args.symbol}`;
173
+ if (args.action === 'deps') return `Tracing deps of ${args.symbol}`;
174
+ if (args.action === 'usages') return `Finding usages of ${args.symbol}`;
175
+ if (args.action === 'call_chain') return `Tracing ${args.from} → ${args.to}`;
176
+ if (args.action === 'sub_projects') return `Scanning sub-projects`;
177
+ return `Navigating graph`;
178
+ }
179
+ case 'get_skeleton': return `Scanning project structure`;
180
+ case 'get_ai_context': return `Loading AI context`;
181
+ case 'compact': return `Compacting ${short}`;
182
+ case 'analyze': return `Analyzing: ${args.action || ''}`;
183
+ case 'docs': return `Documentation: ${args.action || ''}`;
184
+ case 'jsdoc': return `JSDoc: ${args.action || ''}`;
185
+ case 'db': return `Database: ${args.action || ''}`;
186
+ case 'testing': return `Tests: ${args.action || ''}`;
187
+ case 'filters': return `Filters: ${args.action || ''}`;
188
+ default: return tool ? `Running ${tool}` : '';
189
+ }
190
+ }
191
+
192
+ /**
193
+ * Extract short tool name from full prefixed name.
194
+ * 'default_api:view_file' → 'view_file'
195
+ * 'mcp_project-graph_navigate' → 'navigate'
196
+ */
197
+ _shortName(full) {
198
+ // Strip 'default_api:' prefix
199
+ let name = full.replace(/^default_api:/, '');
200
+ // Strip 'mcp_project-graph_' prefix
201
+ name = name.replace(/^mcp_project-graph_/, '');
202
+ return name;
203
+ }
204
+
205
+ /**
206
+ * Emit focus change immediately (for urgent actions like call_chain).
207
+ */
208
+ _emitFocusNow(focus) {
209
+ if (this._debounceTimer) {
210
+ clearTimeout(this._debounceTimer);
211
+ this._debounceTimer = null;
212
+ }
213
+ this.currentFocus = focus;
214
+ this._emit('follow-focus-changed', focus);
215
+ }
216
+
217
+ /**
218
+ * Emit focus change with debounce (for rapid file reads, etc.).
219
+ */
220
+ _emitFocusDebounced(focus, delay) {
221
+ if (this._debounceTimer) {
222
+ clearTimeout(this._debounceTimer);
223
+ }
224
+ this._debounceTimer = setTimeout(() => {
225
+ this._debounceTimer = null;
226
+ this.currentFocus = focus;
227
+ this._emit('follow-focus-changed', focus);
228
+ }, delay);
229
+ }
230
+
231
+ /**
232
+ * Emit status text for the ribbon.
233
+ */
234
+ _emitStatus(text) {
235
+ this.statusText = text;
236
+ this._emit('follow-status-changed', { text });
237
+ }
238
+ }
239
+
240
+ /** Singleton instance */
241
+ export const followController = new FollowController();
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));t.addEventListener("follow-focus-changed",e=>{const d=e.detail;if(d.type==="file"&&d.target){this._loadFile(d.target);if(d.meta?.startLine){setTimeout(()=>{const c=this._getCodeBlock();if(c&&c.scrollToLine)c.scrollToLine(d.meta.startLine)},200)}}});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");