vg-coder-cli 2.0.30 → 2.0.32

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 (40) hide show
  1. package/ARCHITECTURE.md +255 -0
  2. package/README.md +0 -11
  3. package/change.sh +0 -0
  4. package/dist/vg-coder-bundle.js +42 -0
  5. package/gulpfile.js +111 -0
  6. package/package.json +19 -11
  7. package/scripts/postinstall.js +13 -3
  8. package/src/index.js +28 -220
  9. package/src/server/api-server.js +120 -428
  10. package/src/server/views/css/bubble.css +81 -0
  11. package/src/server/views/css/code-viewer.css +58 -0
  12. package/src/server/views/css/terminal.css +59 -155
  13. package/src/server/views/dashboard.css +78 -678
  14. package/src/server/views/dashboard.html +39 -278
  15. package/src/server/views/js/api.js +2 -22
  16. package/src/server/views/js/config.js +27 -15
  17. package/src/server/views/js/event-protocol.js +263 -0
  18. package/src/server/views/js/features/bubble-features/index.js +125 -0
  19. package/src/server/views/js/features/bubble-features/paste-run-feature.js +16 -0
  20. package/src/server/views/js/features/bubble-features/terminal-feature.js +16 -0
  21. package/src/server/views/js/features/bubble.js +175 -0
  22. package/src/server/views/js/features/code-viewer.js +90 -0
  23. package/src/server/views/js/features/commands.js +34 -81
  24. package/src/server/views/js/features/editor-tabs.js +19 -46
  25. package/src/server/views/js/features/git-view.js +63 -81
  26. package/src/server/views/js/features/iframe-manager.js +3 -97
  27. package/src/server/views/js/features/monaco-manager.js +19 -39
  28. package/src/server/views/js/features/project-switcher.js +7 -63
  29. package/src/server/views/js/features/resize.js +5 -16
  30. package/src/server/views/js/features/structure.js +38 -106
  31. package/src/server/views/js/features/terminal.js +102 -418
  32. package/src/server/views/js/handlers.js +60 -43
  33. package/src/server/views/js/main.js +75 -179
  34. package/src/server/views/js/shadow-entry.js +21 -0
  35. package/src/server/views/js/utils.js +48 -28
  36. package/src/server/views/vg-coder/_metadata/generated_indexed_rulesets/_ruleset1 +0 -0
  37. package/src/server/views/vg-coder/controller.js +33 -258
  38. package/vetgo-auto/chrome/src/utils/injector-script.ts +33 -258
  39. package/vetgo-auto/vg-coder.zip +0 -0
  40. package/src/server/views/dashboard.js +0 -457
@@ -0,0 +1,263 @@
1
+ /**
2
+ * VG Coder Event Protocol
3
+ *
4
+ * Standardized event communication system for cross-context messaging.
5
+ * Supports: window, shadow-root, iframe contexts.
6
+ */
7
+
8
+ // Event Types Registry
9
+ export const EVENT_TYPES = {
10
+ PASTE_RUN: 'vg:paste-run',
11
+ TERMINAL_NEW: 'vg:terminal-new',
12
+ TERMINAL_EXECUTE: 'vg:terminal-execute',
13
+ FEATURE_TOGGLE: 'vg:feature-toggle',
14
+ };
15
+
16
+ /**
17
+ * EventProtocol - Standardized event format
18
+ */
19
+ export class EventProtocol {
20
+ constructor({ type, source, target, payload = {}, context = 'window' }) {
21
+ this.type = type;
22
+ this.source = source;
23
+ this.target = target;
24
+ this.payload = payload;
25
+ this.timestamp = Date.now();
26
+ this.context = context;
27
+ this.id = `${type}-${this.timestamp}-${Math.random().toString(36).substr(2, 9)}`;
28
+ }
29
+
30
+ /**
31
+ * Validate event structure
32
+ */
33
+ isValid() {
34
+ return !!(this.type && this.source && this.target);
35
+ }
36
+
37
+ /**
38
+ * Convert to plain object
39
+ */
40
+ toObject() {
41
+ return {
42
+ id: this.id,
43
+ type: this.type,
44
+ source: this.source,
45
+ target: this.target,
46
+ payload: this.payload,
47
+ timestamp: this.timestamp,
48
+ context: this.context,
49
+ };
50
+ }
51
+ }
52
+
53
+ /**
54
+ * EventDispatcher - Multi-context event dispatcher
55
+ *
56
+ * Supports communication across:
57
+ * - Window context
58
+ * - Shadow DOM context
59
+ * - Iframe context
60
+ */
61
+ export class EventDispatcher {
62
+ constructor(options = {}) {
63
+ this.listeners = new Map();
64
+ this.contexts = options.contexts || ['window'];
65
+ this.debug = options.debug || false;
66
+ this.eventHistory = [];
67
+ this.maxHistorySize = options.maxHistorySize || 100;
68
+
69
+ // Make available globally for debugging
70
+ if (typeof window !== 'undefined') {
71
+ window.__VG_EVENT_DISPATCHER__ = this;
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Register event listener
77
+ * @param {string} eventType - Event type to listen for
78
+ * @param {Function} handler - Handler function
79
+ * @param {Object} options - Listener options
80
+ */
81
+ on(eventType, handler, options = {}) {
82
+ if (!this.listeners.has(eventType)) {
83
+ this.listeners.set(eventType, []);
84
+ }
85
+
86
+ const listener = {
87
+ handler,
88
+ context: options.context || 'all',
89
+ priority: options.priority || 0,
90
+ once: options.once || false,
91
+ };
92
+
93
+ this.listeners.get(eventType).push(listener);
94
+
95
+ // Sort by priority (higher first)
96
+ this.listeners.get(eventType).sort((a, b) => b.priority - a.priority);
97
+
98
+ if (this.debug) {
99
+ console.log(`[EventDispatcher] Registered listener for: ${eventType}`, listener);
100
+ }
101
+
102
+ // Return unsubscribe function
103
+ return () => this.off(eventType, handler);
104
+ }
105
+
106
+ /**
107
+ * Register one-time event listener
108
+ */
109
+ once(eventType, handler, options = {}) {
110
+ return this.on(eventType, handler, { ...options, once: true });
111
+ }
112
+
113
+ /**
114
+ * Unregister event listener
115
+ */
116
+ off(eventType, handler) {
117
+ if (!this.listeners.has(eventType)) return;
118
+
119
+ const listeners = this.listeners.get(eventType);
120
+ const index = listeners.findIndex(l => l.handler === handler);
121
+
122
+ if (index !== -1) {
123
+ listeners.splice(index, 1);
124
+ if (this.debug) {
125
+ console.log(`[EventDispatcher] Unregistered listener for: ${eventType}`);
126
+ }
127
+ }
128
+
129
+ // Clean up empty listener arrays
130
+ if (listeners.length === 0) {
131
+ this.listeners.delete(eventType);
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Dispatch event
137
+ * @param {Object|EventProtocol} event - Event to dispatch
138
+ */
139
+ async dispatch(event) {
140
+ // Convert to EventProtocol if needed
141
+ const eventObj = event instanceof EventProtocol
142
+ ? event
143
+ : new EventProtocol(event);
144
+
145
+ if (!eventObj.isValid()) {
146
+ console.error('[EventDispatcher] Invalid event:', eventObj);
147
+ return false;
148
+ }
149
+
150
+ // Add to history
151
+ this.eventHistory.push(eventObj.toObject());
152
+ if (this.eventHistory.length > this.maxHistorySize) {
153
+ this.eventHistory.shift();
154
+ }
155
+
156
+ if (this.debug) {
157
+ console.log('[EventDispatcher] Dispatching:', eventObj.toObject());
158
+ }
159
+
160
+ // Get listeners for this event type
161
+ const listeners = this.listeners.get(eventObj.type) || [];
162
+
163
+ // Filter by context
164
+ const contextListeners = listeners.filter(l =>
165
+ l.context === 'all' || l.context === eventObj.context
166
+ );
167
+
168
+ if (contextListeners.length === 0) {
169
+ if (this.debug) {
170
+ console.warn(`[EventDispatcher] No listeners for: ${eventObj.type}`);
171
+ }
172
+ return false;
173
+ }
174
+
175
+ // Execute handlers
176
+ const results = [];
177
+ for (const listener of contextListeners) {
178
+ try {
179
+ const result = await listener.handler(eventObj.toObject());
180
+ results.push({ success: true, result });
181
+
182
+ // Remove if once
183
+ if (listener.once) {
184
+ this.off(eventObj.type, listener.handler);
185
+ }
186
+ } catch (error) {
187
+ console.error(`[EventDispatcher] Handler error for ${eventObj.type}:`, error);
188
+ results.push({ success: false, error });
189
+ }
190
+ }
191
+
192
+ return results;
193
+ }
194
+
195
+ /**
196
+ * Dispatch event across contexts (window + shadow root + iframes)
197
+ */
198
+ async dispatchCrossContext(event) {
199
+ const eventObj = event instanceof EventProtocol
200
+ ? event
201
+ : new EventProtocol(event);
202
+
203
+ const results = [];
204
+
205
+ // Dispatch in current context
206
+ results.push(await this.dispatch(eventObj));
207
+
208
+ // Dispatch to shadow roots if available
209
+ if (typeof window !== 'undefined' && window.__VG_CODER_ROOT__) {
210
+ const shadowDispatcher = window.__VG_CODER_ROOT__.__VG_EVENT_DISPATCHER__;
211
+ if (shadowDispatcher && shadowDispatcher !== this) {
212
+ results.push(await shadowDispatcher.dispatch(eventObj));
213
+ }
214
+ }
215
+
216
+ // Dispatch to iframes (if needed in future)
217
+ // TODO: Add iframe support
218
+
219
+ return results.flat();
220
+ }
221
+
222
+ /**
223
+ * Get event history
224
+ */
225
+ getHistory(limit = 10) {
226
+ return this.eventHistory.slice(-limit);
227
+ }
228
+
229
+ /**
230
+ * Clear event history
231
+ */
232
+ clearHistory() {
233
+ this.eventHistory = [];
234
+ }
235
+
236
+ /**
237
+ * Get all registered event types
238
+ */
239
+ getRegisteredEvents() {
240
+ return Array.from(this.listeners.keys());
241
+ }
242
+
243
+ /**
244
+ * Get listener count for event type
245
+ */
246
+ getListenerCount(eventType) {
247
+ return this.listeners.get(eventType)?.length || 0;
248
+ }
249
+
250
+ /**
251
+ * Enable/disable debug mode
252
+ */
253
+ setDebug(enabled) {
254
+ this.debug = enabled;
255
+ console.log(`[EventDispatcher] Debug mode: ${enabled ? 'ON' : 'OFF'}`);
256
+ }
257
+ }
258
+
259
+ // Create and export singleton instance
260
+ export const globalDispatcher = new EventDispatcher({
261
+ contexts: ['window', 'shadow-root', 'iframe'],
262
+ debug: false,
263
+ });
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Bubble Features Registry
3
+ *
4
+ * Central registry for all bubble menu features.
5
+ * Makes it easy to add/remove features without modifying core code.
6
+ */
7
+
8
+ import { PasteRunFeature } from './paste-run-feature.js';
9
+ import { TerminalFeature } from './terminal-feature.js';
10
+
11
+ /**
12
+ * FeatureRegistry - Manages bubble menu features
13
+ */
14
+ export class FeatureRegistry {
15
+ constructor() {
16
+ this.features = new Map();
17
+
18
+ // Make available globally for debugging
19
+ if (typeof window !== 'undefined') {
20
+ window.__VG_FEATURE_REGISTRY__ = this;
21
+ }
22
+ }
23
+
24
+ /**
25
+ * Register a feature
26
+ * @param {Object} feature - Feature definition
27
+ */
28
+ register(feature) {
29
+ if (!feature.id) {
30
+ console.error('[FeatureRegistry] Feature must have an id', feature);
31
+ return false;
32
+ }
33
+
34
+ if (this.features.has(feature.id)) {
35
+ console.warn(`[FeatureRegistry] Feature ${feature.id} already registered. Overwriting.`);
36
+ }
37
+
38
+ this.features.set(feature.id, feature);
39
+ return true;
40
+ }
41
+
42
+ /**
43
+ * Unregister a feature
44
+ * @param {string} id - Feature id
45
+ */
46
+ unregister(id) {
47
+ return this.features.delete(id);
48
+ }
49
+
50
+ /**
51
+ * Get all features
52
+ * @returns {Array} Array of feature objects
53
+ */
54
+ getFeatures() {
55
+ return Array.from(this.features.values());
56
+ }
57
+
58
+ /**
59
+ * Get enabled features only
60
+ * @returns {Array} Array of enabled feature objects
61
+ */
62
+ getEnabledFeatures() {
63
+ return this.getFeatures()
64
+ .filter(f => f.enabled)
65
+ .sort((a, b) => a.order - b.order);
66
+ }
67
+
68
+ /**
69
+ * Get feature by id
70
+ * @param {string} id - Feature id
71
+ * @returns {Object|null} Feature object or null
72
+ */
73
+ getFeature(id) {
74
+ return this.features.get(id) || null;
75
+ }
76
+
77
+ /**
78
+ * Check if feature is enabled
79
+ * @param {string} id - Feature id
80
+ * @returns {boolean}
81
+ */
82
+ isFeatureEnabled(id) {
83
+ const feature = this.features.get(id);
84
+ return feature ? feature.enabled : false;
85
+ }
86
+
87
+ /**
88
+ * Enable/disable a feature
89
+ * @param {string} id - Feature id
90
+ * @param {boolean} enabled - Enable state
91
+ */
92
+ setFeatureEnabled(id, enabled) {
93
+ const feature = this.features.get(id);
94
+ if (feature) {
95
+ feature.enabled = enabled;
96
+ return true;
97
+ }
98
+ return false;
99
+ }
100
+
101
+ /**
102
+ * Get feature count
103
+ * @returns {number}
104
+ */
105
+ getFeatureCount() {
106
+ return this.features.size;
107
+ }
108
+
109
+ /**
110
+ * Clear all features
111
+ */
112
+ clear() {
113
+ this.features.clear();
114
+ }
115
+ }
116
+
117
+ // Create singleton registry
118
+ export const featureRegistry = new FeatureRegistry();
119
+
120
+ // Register built-in features
121
+ featureRegistry.register(PasteRunFeature);
122
+ featureRegistry.register(TerminalFeature);
123
+
124
+ // Export for external registration
125
+ export { PasteRunFeature, TerminalFeature };
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Bubble Features - Paste & Run Feature
3
+ */
4
+
5
+ import { EVENT_TYPES } from '../../event-protocol.js';
6
+
7
+ export const PasteRunFeature = {
8
+ id: 'paste-run',
9
+ icon: '📋',
10
+ label: 'Paste & Run from Clipboard',
11
+ tooltip: 'Paste & Run from Clipboard',
12
+ eventType: EVENT_TYPES.PASTE_RUN,
13
+ permissions: ['clipboard-read'],
14
+ enabled: true,
15
+ order: 1,
16
+ };
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Bubble Features - Terminal Feature
3
+ */
4
+
5
+ import { EVENT_TYPES } from '../../event-protocol.js';
6
+
7
+ export const TerminalFeature = {
8
+ id: 'terminal-new',
9
+ icon: '🖥️',
10
+ label: 'New Terminal',
11
+ tooltip: 'Open New Terminal',
12
+ eventType: EVENT_TYPES.TERMINAL_NEW,
13
+ permissions: [],
14
+ enabled: true,
15
+ order: 2,
16
+ };
@@ -0,0 +1,175 @@
1
+ import { getById, showToast } from '../utils.js';
2
+ import { globalDispatcher } from '../event-protocol.js';
3
+ import { featureRegistry } from './bubble-features/index.js';
4
+
5
+ export function initBubble() {
6
+ const bubble = getById('vg-bubble');
7
+ const appRoot = getById('vg-app-root');
8
+ const bubbleMenu = bubble?.querySelector('.vg-bubble-menu');
9
+
10
+ if (!bubble || !appRoot) return;
11
+
12
+ // --- RENDER FEATURES DYNAMICALLY ---
13
+ if (bubbleMenu) {
14
+ renderFeatures(bubbleMenu);
15
+ }
16
+
17
+ // --- DRAG LOGIC ---
18
+ let isDragging = false;
19
+ let startX, startY, initialLeft, initialTop;
20
+ let hasMoved = false; // To distinguish click vs drag
21
+ let mouseDownTarget = null; // Track where mousedown started
22
+
23
+ bubble.addEventListener('mousedown', (e) => {
24
+ isDragging = true;
25
+ hasMoved = false;
26
+ mouseDownTarget = e.target; // Remember click target
27
+ startX = e.clientX;
28
+ startY = e.clientY;
29
+
30
+ // Get computed style for accurate position
31
+ const rect = bubble.getBoundingClientRect();
32
+ initialLeft = rect.left;
33
+ initialTop = rect.top;
34
+
35
+ // Prevent text selection
36
+ e.preventDefault();
37
+
38
+ // Set cursor
39
+ bubble.style.cursor = 'grabbing';
40
+ });
41
+
42
+ // Use window events to handle drag outside shadow root if needed,
43
+ // but here we are inside Shadow DOM, so document/root events work
44
+ // We attach to root to capture movement
45
+ const root = window.__VG_CODER_ROOT__ || document;
46
+
47
+ root.addEventListener('mousemove', (e) => {
48
+ if (!isDragging) return;
49
+
50
+ const dx = e.clientX - startX;
51
+ const dy = e.clientY - startY;
52
+
53
+ // Threshold to consider it a move
54
+ if (Math.abs(dx) > 2 || Math.abs(dy) > 2) hasMoved = true;
55
+
56
+ bubble.style.left = `${initialLeft + dx}px`;
57
+ bubble.style.top = `${initialTop + dy}px`;
58
+ bubble.style.bottom = 'auto'; // Clear bottom/right if set by CSS
59
+ bubble.style.right = 'auto';
60
+ });
61
+
62
+ root.addEventListener('mouseup', (e) => {
63
+ if (!isDragging) return;
64
+ isDragging = false;
65
+ bubble.style.cursor = 'grab';
66
+
67
+ if (hasMoved) {
68
+ snapToEdge(bubble);
69
+ } else {
70
+ // Click logic - only toggle if clicked on bubble icon, NOT on menu buttons
71
+ const clickedOnMenu = mouseDownTarget?.closest('.vg-bubble-menu');
72
+ const clickedOnIcon = mouseDownTarget?.closest('.vg-bubble-icon');
73
+
74
+ // Only toggle dashboard if clicked on icon or bubble itself, but NOT on menu
75
+ if (!clickedOnMenu && (clickedOnIcon || mouseDownTarget === bubble)) {
76
+ toggleDashboard(appRoot);
77
+ }
78
+ }
79
+
80
+ // Reset
81
+ mouseDownTarget = null;
82
+ });
83
+ }
84
+
85
+ /**
86
+ * Render features dynamically from feature registry
87
+ */
88
+ function renderFeatures(container) {
89
+ // Clear existing content
90
+ container.innerHTML = '';
91
+
92
+ // Get enabled features
93
+ const features = featureRegistry.getEnabledFeatures();
94
+
95
+ if (features.length === 0) {
96
+ console.warn('[Bubble] No enabled features found');
97
+ return;
98
+ }
99
+
100
+ // Render each feature
101
+ features.forEach(feature => {
102
+ const btn = document.createElement('button');
103
+ btn.id = `bubble-feature-${feature.id}`;
104
+ btn.className = 'bubble-action-btn';
105
+ btn.title = feature.tooltip;
106
+ btn.textContent = feature.label;
107
+
108
+ // Add click handler to dispatch event
109
+ btn.addEventListener('click', (e) => {
110
+ e.stopPropagation(); // Prevent toggling dashboard
111
+
112
+ // Dispatch event via event protocol
113
+ globalDispatcher.dispatch({
114
+ type: feature.eventType,
115
+ source: 'bubble-menu',
116
+ target: 'handlers',
117
+ payload: {
118
+ featureId: feature.id,
119
+ label: feature.label,
120
+ },
121
+ context: 'shadow-root',
122
+ });
123
+ });
124
+
125
+ container.appendChild(btn);
126
+ });
127
+
128
+ console.log(`[Bubble] Rendered ${features.length} features`);
129
+ }
130
+
131
+ function snapToEdge(bubble) {
132
+ const rect = bubble.getBoundingClientRect();
133
+ const winWidth = window.innerWidth;
134
+ const winHeight = window.innerHeight;
135
+
136
+ // Determine nearest horizontal edge
137
+ const distLeft = rect.left;
138
+ const distRight = winWidth - rect.right;
139
+
140
+ let targetLeft;
141
+ if (distLeft < distRight) {
142
+ targetLeft = 20; // 20px padding
143
+ } else {
144
+ targetLeft = winWidth - rect.width - 20;
145
+ }
146
+
147
+ // Keep vertical within bounds
148
+ let targetTop = rect.top;
149
+ if (targetTop < 20) targetTop = 20;
150
+ if (targetTop > winHeight - rect.height - 20) targetTop = winHeight - rect.height - 20;
151
+
152
+ // Animate snap
153
+ bubble.style.transition = 'all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1)';
154
+ bubble.style.left = `${targetLeft}px`;
155
+ bubble.style.top = `${targetTop}px`;
156
+
157
+ // Remove transition after snap to allow immediate drag next time
158
+ setTimeout(() => {
159
+ bubble.style.transition = '';
160
+ }, 300);
161
+ }
162
+
163
+ function toggleDashboard(appRoot) {
164
+ const isVisible = appRoot.classList.contains('visible');
165
+
166
+ if (isVisible) {
167
+ // Hide
168
+ appRoot.classList.remove('visible');
169
+ // Pointer events auto is handled by CSS (when hidden opacity=0, pointer-events usually stay but size is 0x0)
170
+ // Our gulpfile sets width:0, height:0 for base state, so clicks pass through
171
+ } else {
172
+ // Show
173
+ appRoot.classList.add('visible');
174
+ }
175
+ }
@@ -0,0 +1,90 @@
1
+ import { API_BASE } from '../config.js';
2
+ import { showToast, getById } from '../utils.js';
3
+ // Import Highlight.js core
4
+ import hljs from 'highlight.js/lib/core';
5
+
6
+ // Import common languages to reduce bundle size
7
+ import javascript from 'highlight.js/lib/languages/javascript';
8
+ import typescript from 'highlight.js/lib/languages/typescript';
9
+ import json from 'highlight.js/lib/languages/json';
10
+ import xml from 'highlight.js/lib/languages/xml';
11
+ import css from 'highlight.js/lib/languages/css';
12
+ import bash from 'highlight.js/lib/languages/bash';
13
+ import java from 'highlight.js/lib/languages/java';
14
+ import python from 'highlight.js/lib/languages/python';
15
+ import sql from 'highlight.js/lib/languages/sql';
16
+ import markdown from 'highlight.js/lib/languages/markdown';
17
+
18
+ // Register languages
19
+ hljs.registerLanguage('javascript', javascript);
20
+ hljs.registerLanguage('typescript', typescript);
21
+ hljs.registerLanguage('json', json);
22
+ hljs.registerLanguage('xml', xml);
23
+ hljs.registerLanguage('html', xml);
24
+ hljs.registerLanguage('css', css);
25
+ hljs.registerLanguage('bash', bash);
26
+ hljs.registerLanguage('java', java);
27
+ hljs.registerLanguage('python', python);
28
+ hljs.registerLanguage('sql', sql);
29
+ hljs.registerLanguage('markdown', markdown);
30
+
31
+ export async function openFileInViewer(path) {
32
+ const container = getById('code-viewer-container');
33
+ if (!container) return;
34
+
35
+ // Show loading state
36
+ container.innerHTML = '<div class="cv-loading">Loading file content...</div>';
37
+
38
+ try {
39
+ const res = await fetch(`${API_BASE}/api/read-file?path=${encodeURIComponent(path)}`);
40
+ const data = await res.json();
41
+
42
+ if (res.ok) {
43
+ const ext = path.split('.').pop().toLowerCase();
44
+ const language = getLanguageFromExt(ext);
45
+
46
+ // Escape HTML tags to prevent XSS and ensure correct rendering
47
+ const escapedCode = escapeHtml(data.content);
48
+
49
+ // Render Code Block
50
+ container.innerHTML = `
51
+ <div class="cv-wrapper">
52
+ <pre><code class="language-${language}">${escapedCode}</code></pre>
53
+ </div>
54
+ `;
55
+
56
+ // Apply Highlight.js
57
+ const codeBlock = container.querySelector('code');
58
+ if(codeBlock) {
59
+ hljs.highlightElement(codeBlock);
60
+ }
61
+
62
+ } else {
63
+ container.innerHTML = `<div class="cv-error">Error: ${data.error}</div>`;
64
+ showToast(`Error opening file: ${data.error}`, 'error');
65
+ }
66
+ } catch (err) {
67
+ container.innerHTML = `<div class="cv-error">Failed: ${err.message}</div>`;
68
+ showToast(`Failed to load file: ${err.message}`, 'error');
69
+ }
70
+ }
71
+
72
+ function getLanguageFromExt(ext) {
73
+ const map = {
74
+ js: 'javascript', ts: 'typescript', jsx: 'javascript', tsx: 'typescript',
75
+ html: 'xml', xml: 'xml', css: 'css', scss: 'css',
76
+ json: 'json', java: 'java', py: 'python', sh: 'bash', sql: 'sql',
77
+ md: 'markdown'
78
+ };
79
+ return map[ext] || 'plaintext';
80
+ }
81
+
82
+ function escapeHtml(text) {
83
+ if (!text) return '';
84
+ return text
85
+ .replace(/&/g, "&amp;")
86
+ .replace(/</g, "&lt;")
87
+ .replace(/>/g, "&gt;")
88
+ .replace(/"/g, "&quot;")
89
+ .replace(/'/g, "&#039;");
90
+ }