project-graph-mcp 2.3.0 → 2.3.2

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 (66) hide show
  1. package/package.json +1 -3
  2. package/project-graph-mcp-2.3.0.tgz +0 -0
  3. package/src/network/web-server.js +1 -1
  4. package/vendor/symbiote-node/engine/AgentUICommands.js +100 -0
  5. package/vendor/symbiote-node/engine/Executor.js +371 -0
  6. package/vendor/symbiote-node/engine/Graph.js +314 -0
  7. package/vendor/symbiote-node/engine/GraphServer.js +353 -0
  8. package/vendor/symbiote-node/engine/HandlerLoader.js +145 -0
  9. package/vendor/symbiote-node/engine/History.js +83 -0
  10. package/vendor/symbiote-node/engine/Lifecycle.js +118 -0
  11. package/vendor/symbiote-node/engine/Persistence.js +84 -0
  12. package/vendor/symbiote-node/engine/Registry.js +264 -0
  13. package/vendor/symbiote-node/engine/SocketTypes.js +79 -0
  14. package/vendor/symbiote-node/engine/cli.js +404 -0
  15. package/vendor/symbiote-node/engine/index.js +56 -0
  16. package/vendor/symbiote-node/engine/nanoid.js +28 -0
  17. package/vendor/symbiote-node/engine/package.json +26 -0
  18. package/vendor/symbiote-node/engine/packs/ai/beat-detect.handler.js +215 -0
  19. package/vendor/symbiote-node/engine/packs/ai/content-adapt.handler.js +238 -0
  20. package/vendor/symbiote-node/engine/packs/ai/face-detect.handler.js +287 -0
  21. package/vendor/symbiote-node/engine/packs/ai/grok-generate.handler.js +565 -0
  22. package/vendor/symbiote-node/engine/packs/ai/kling-lipsync.handler.js +414 -0
  23. package/vendor/symbiote-node/engine/packs/ai/lesson-generate.handler.js +343 -0
  24. package/vendor/symbiote-node/engine/packs/ai/opencode.handler.js +164 -0
  25. package/vendor/symbiote-node/engine/packs/ai/replicate-lipsync.handler.js +341 -0
  26. package/vendor/symbiote-node/engine/packs/ai/tts.handler.js +241 -0
  27. package/vendor/symbiote-node/engine/packs/ai/whisper.handler.js +191 -0
  28. package/vendor/symbiote-node/engine/packs/data/db-query.handler.js +67 -0
  29. package/vendor/symbiote-node/engine/packs/data/news-accumulate.handler.js +281 -0
  30. package/vendor/symbiote-node/engine/packs/data/personas.handler.js +160 -0
  31. package/vendor/symbiote-node/engine/packs/data/prompt-loader.handler.js +193 -0
  32. package/vendor/symbiote-node/engine/packs/data/roles.handler.js +216 -0
  33. package/vendor/symbiote-node/engine/packs/data/rss-feed.handler.js +244 -0
  34. package/vendor/symbiote-node/engine/packs/debug/inject.handler.js +52 -0
  35. package/vendor/symbiote-node/engine/packs/flow/agent.handler.js +73 -0
  36. package/vendor/symbiote-node/engine/packs/flow/if.handler.js +107 -0
  37. package/vendor/symbiote-node/engine/packs/flow/loop.handler.js +58 -0
  38. package/vendor/symbiote-node/engine/packs/flow/merge.handler.js +60 -0
  39. package/vendor/symbiote-node/engine/packs/flow/retry.handler.js +65 -0
  40. package/vendor/symbiote-node/engine/packs/flow/switch.handler.js +64 -0
  41. package/vendor/symbiote-node/engine/packs/flow/wait-all.handler.js +39 -0
  42. package/vendor/symbiote-node/engine/packs/io/http-request.handler.js +82 -0
  43. package/vendor/symbiote-node/engine/packs/io/read-file.handler.js +60 -0
  44. package/vendor/symbiote-node/engine/packs/io/write-file.handler.js +63 -0
  45. package/vendor/symbiote-node/engine/packs/transform/anchor-match.handler.js +494 -0
  46. package/vendor/symbiote-node/engine/packs/transform/effects-skeleton.handler.js +417 -0
  47. package/vendor/symbiote-node/engine/packs/transform/json-parse.handler.js +43 -0
  48. package/vendor/symbiote-node/engine/packs/transform/lipsync-select.handler.js +339 -0
  49. package/vendor/symbiote-node/engine/packs/transform/riopla-adapt.handler.js +432 -0
  50. package/vendor/symbiote-node/engine/packs/transform/set.handler.js +57 -0
  51. package/vendor/symbiote-node/engine/packs/transform/template-builder.handler.js +134 -0
  52. package/vendor/symbiote-node/engine/packs/transform/template.handler.js +79 -0
  53. package/vendor/symbiote-node/engine/packs/transform/timeline-build.handler.js +399 -0
  54. package/vendor/symbiote-node/engine/packs/util/delay.handler.js +39 -0
  55. package/vendor/symbiote-node/engine/packs/util/log.handler.js +44 -0
  56. package/vendor/symbiote-node/engine/packs/video-pack.js +323 -0
  57. package/vendor/symbiote-node/package.json +2 -2
  58. package/web/app.js +6 -3
  59. package/web/components/canvas-graph.js +50 -11
  60. package/web/components/code-block.js +1 -1
  61. package/web/components/event-feed/MiniGraphWidget.js +105 -15
  62. package/web/components/follow-ribbon.js +134 -0
  63. package/web/follow-controller.js +241 -0
  64. package/web/panels/code-viewer.js +1 -1
  65. package/web/panels/dep-graph.js +21 -42
  66. package/web/style.css +6 -0
@@ -0,0 +1,134 @@
1
+ // @ctx .context/web/components/follow-ribbon.ctx
2
+ /**
3
+ * FollowRibbon — Floating status bar that shows current agent action.
4
+ * Appears at the bottom of the screen during Follow Mode.
5
+ * Auto-fades after 4 seconds of inactivity.
6
+ */
7
+ import Symbiote from '@symbiotejs/symbiote';
8
+ import { events } from '../app.js';
9
+
10
+ export class FollowRibbon extends Symbiote {
11
+ init$ = {
12
+ statusText: '',
13
+ visible: false,
14
+ };
15
+
16
+ _fadeTimer = null;
17
+
18
+ initCallback() {
19
+ // Event subscriptions are in renderCallback (after template mount)
20
+ }
21
+
22
+ renderCallback() {
23
+ this.sub('visible', (v) => {
24
+ this.toggleAttribute('visible', v);
25
+ });
26
+
27
+ events.addEventListener('follow-status-changed', (e) => {
28
+ const text = e.detail?.text || '';
29
+ if (!text) {
30
+ this.$.visible = false;
31
+ return;
32
+ }
33
+ this.$.statusText = text;
34
+ this.$.visible = true;
35
+
36
+ // Auto-fade after 4 seconds
37
+ if (this._fadeTimer) clearTimeout(this._fadeTimer);
38
+ this._fadeTimer = setTimeout(() => {
39
+ this.$.visible = false;
40
+ }, 4000);
41
+ });
42
+
43
+ events.addEventListener('follow-state-changed', (e) => {
44
+ if (!e.detail?.enabled) {
45
+ this.$.visible = false;
46
+ this.$.statusText = '';
47
+ if (this._fadeTimer) {
48
+ clearTimeout(this._fadeTimer);
49
+ this._fadeTimer = null;
50
+ }
51
+ }
52
+ });
53
+ }
54
+ }
55
+
56
+ FollowRibbon.template = `
57
+ <div class="fr-inner">
58
+ <span class="fr-icon">smart_toy</span>
59
+ <span class="fr-text" bind="textContent: statusText"></span>
60
+ <span class="fr-dots"></span>
61
+ </div>
62
+ `;
63
+
64
+ FollowRibbon.rootStyles = `
65
+ follow-ribbon {
66
+ position: fixed;
67
+ bottom: 20px;
68
+ left: 50%;
69
+ transform: translateX(-50%) translateY(20px);
70
+ z-index: 9999;
71
+ pointer-events: none;
72
+ opacity: 0;
73
+ transition: opacity 0.4s ease, transform 0.4s ease;
74
+ }
75
+
76
+ follow-ribbon[visible] {
77
+ opacity: 1;
78
+ transform: translateX(-50%) translateY(0);
79
+ }
80
+
81
+ .fr-inner {
82
+ display: flex;
83
+ align-items: center;
84
+ gap: 10px;
85
+ padding: 8px 20px;
86
+ border-radius: 24px;
87
+ background: rgba(20, 20, 25, 0.85);
88
+ backdrop-filter: blur(16px);
89
+ -webkit-backdrop-filter: blur(16px);
90
+ border: 1px solid rgba(76, 139, 245, 0.25);
91
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4), 0 0 16px rgba(76, 139, 245, 0.1);
92
+ font-family: 'Inter', -apple-system, sans-serif;
93
+ font-size: 12px;
94
+ font-weight: 500;
95
+ color: rgba(255, 255, 255, 0.9);
96
+ white-space: nowrap;
97
+ max-width: 500px;
98
+ }
99
+
100
+ .fr-icon {
101
+ font-family: 'Material Symbols Outlined';
102
+ font-size: 16px;
103
+ color: #4c8bf5;
104
+ animation: fr-pulse 2s ease-in-out infinite;
105
+ }
106
+
107
+ .fr-text {
108
+ overflow: hidden;
109
+ text-overflow: ellipsis;
110
+ }
111
+
112
+ .fr-dots::after {
113
+ content: '...';
114
+ animation: fr-dots 1.5s steps(3) infinite;
115
+ display: inline-block;
116
+ width: 16px;
117
+ text-align: left;
118
+ color: rgba(255, 255, 255, 0.4);
119
+ }
120
+
121
+ @keyframes fr-pulse {
122
+ 0%, 100% { opacity: 1; }
123
+ 50% { opacity: 0.5; }
124
+ }
125
+
126
+ @keyframes fr-dots {
127
+ 0% { content: ''; }
128
+ 33% { content: '.'; }
129
+ 66% { content: '..'; }
130
+ 100% { content: '...'; }
131
+ }
132
+ `;
133
+
134
+ FollowRibbon.reg('follow-ribbon');
@@ -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();
@@ -29,7 +29,7 @@ export class CodeViewer extends e{init$={filename:"Select a file",hasFile:!1,vie
29
29
  // Toggle between source and the transformation
30
30
  this.$.viewMode=this.$.viewMode==="source"?"transformed":"source";
31
31
  this._showCurrentMode();
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=>{
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=>{
33
33
  const lang=_getLang(this._currentPath);
34
34
  this.toggleAttribute("mode-raw","source"!==e);
35
35
  if(lang==='md'){
@@ -687,8 +687,6 @@ export class DepGraph extends Symbiote {
687
687
  _wasDragged = false;
688
688
  /** @type {Map<string, string>} */
689
689
  _fileMap = new Map();
690
- /** @type {boolean} */
691
- _autopilot = false;
692
690
  /** @type {HTMLElement|null} */
693
691
  _canvas = null;
694
692
  /** @type {object|null} Skeleton data for resolving pin names */
@@ -851,11 +849,6 @@ export class DepGraph extends Symbiote {
851
849
  this._restoreFlatFocus();
852
850
  }
853
851
  });
854
-
855
- // Follow mode: listen for global state (set from topbar)
856
- events.addEventListener('follow-mode-changed', (e) => {
857
- this._autopilot = e.detail.enabled;
858
- });
859
852
 
860
853
  // Label Mode controls
861
854
  const labelBtns = this.querySelectorAll('.label-mode-btn');
@@ -983,17 +976,14 @@ export class DepGraph extends Symbiote {
983
976
  ro.observe(this);
984
977
  this._resizeObserver = ro;
985
978
 
986
- // Bind and save global listener functions for clean up
987
979
  this._onSkeletonLoaded = (e) => {
988
980
  if (this._graphBuilt || this.style.display === 'none' || this.offsetWidth === 0) return;
989
981
  requestAnimationFrame(() => this._buildGraph(e.detail));
990
982
  };
991
983
 
992
- this._onToolEvent = (e) => {
984
+ this._onFollowFocusChanged = (e) => {
993
985
  if (this.style.display === 'none' || this.offsetWidth === 0) return;
994
- if (this._autopilot) {
995
- this._handleAutopilot(e.detail);
996
- }
986
+ this._handleFollowFocus(e.detail);
997
987
  };
998
988
 
999
989
  this._onFileSelected = (e) => {
@@ -1026,8 +1016,8 @@ export class DepGraph extends Symbiote {
1026
1016
  }).catch(() => {});
1027
1017
  }
1028
1018
 
1029
- // Autopilot: listen for agent tool events
1030
- events.addEventListener('tool-event', this._onToolEvent);
1019
+ // Autopilot: listen for orchestrator events
1020
+ events.addEventListener('follow-focus-changed', this._onFollowFocusChanged);
1031
1021
 
1032
1022
  // Update route within graph section
1033
1023
  // On node click → save file path (just focusing)
@@ -1165,7 +1155,7 @@ export class DepGraph extends Symbiote {
1165
1155
  disconnectedCallback() {
1166
1156
  super.disconnectedCallback?.();
1167
1157
  if (this._onSkeletonLoaded) events.removeEventListener('skeleton-loaded', this._onSkeletonLoaded);
1168
- if (this._onToolEvent) events.removeEventListener('tool-event', this._onToolEvent);
1158
+ if (this._onFollowFocusChanged) events.removeEventListener('follow-focus-changed', this._onFollowFocusChanged);
1169
1159
  if (this._onFileSelected) events.removeEventListener('file-selected', this._onFileSelected);
1170
1160
  if (this._onHashChange) window.removeEventListener('hashchange', this._onHashChange);
1171
1161
  if (this._resizeObserver) {
@@ -2498,37 +2488,26 @@ export class DepGraph extends Symbiote {
2498
2488
  }
2499
2489
 
2500
2490
  /**
2501
- * Handle agent tool events for autopilot mode
2502
- * @param {object} event
2491
+ * Handle orchestrated visual focus from FollowController
2492
+ * @param {object} detail
2503
2493
  */
2504
- _handleAutopilot(event) {
2494
+ _handleFollowFocus({ type, target, action }) {
2505
2495
  if (!this._editor || !this._canvas) return;
2506
-
2507
- const toolName = event.tool || event.name || '';
2508
- const args = event.args || {};
2509
-
2510
- // tool:call events
2511
- if (event.phase === 'call' || event.type === 'tool:call') {
2512
- if (toolName === 'navigate' && args.action === 'expand' && args.symbol) {
2513
- this._focusSymbol(args.symbol);
2514
- } else if (toolName === 'navigate' && args.action === 'deps' && args.symbol) {
2515
- this._highlightDeps(args.symbol);
2516
- } else if (toolName === 'navigate' && args.action === 'call_chain') {
2517
- // Phase 4: animate call chain when agent traces a path
2518
- if (args.from && args.to) {
2519
- this._highlightCallChain(args.from, args.to);
2520
- }
2521
- } else if (toolName === 'navigate' && args.action === 'usages' && args.symbol) {
2522
- this._highlightDeps(args.symbol);
2523
- } else if (toolName === 'get_skeleton') {
2496
+ if (type !== 'graph' && type !== 'file') return; // React to graph and file actions
2497
+
2498
+ if (type === 'graph') {
2499
+ if (action === 'focus' && target) {
2500
+ this._focusSymbol(target);
2501
+ } else if (action === 'deps' && target) {
2502
+ this._highlightDeps(target);
2503
+ } else if (action === 'chain' && target.from && target.to) {
2504
+ this._highlightCallChain(target.from, target.to);
2505
+ } else if (action === 'fit') {
2524
2506
  this._canvas.fitView();
2525
- } else if (toolName === 'compact' && args.path) {
2526
- this._pulseFile(args.path);
2527
- } else if (toolName === 'view_file' && args.path) {
2528
- // Agent opened a file — focus it on the board
2529
- this._focusFile(args.path);
2530
- this._pulseFile(args.path);
2531
2507
  }
2508
+ } else if (type === 'file' && target) {
2509
+ this._focusFile(target);
2510
+ this._pulseFile(target);
2532
2511
  }
2533
2512
  }
2534
2513
 
package/web/style.css CHANGED
@@ -123,6 +123,12 @@ html, body {
123
123
  background: rgba(76, 139, 245, 0.15);
124
124
  color: #4c8bf5;
125
125
  border-color: rgba(76, 139, 245, 0.3);
126
+ animation: follow-btn-glow 2s ease-in-out infinite;
127
+ }
128
+
129
+ @keyframes follow-btn-glow {
130
+ 0%, 100% { box-shadow: 0 0 4px rgba(76, 139, 245, 0.1); }
131
+ 50% { box-shadow: 0 0 12px rgba(76, 139, 245, 0.4); }
126
132
  }
127
133
 
128
134
  /* Agent badge */