@hugobatist/smartcode 0.1.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 (60) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +292 -0
  3. package/dist/cli.js +4324 -0
  4. package/dist/cli.js.map +1 -0
  5. package/dist/index.d.ts +374 -0
  6. package/dist/index.js +1167 -0
  7. package/dist/index.js.map +1 -0
  8. package/dist/static/annotations-panel.js +133 -0
  9. package/dist/static/annotations-svg.js +108 -0
  10. package/dist/static/annotations.css +367 -0
  11. package/dist/static/annotations.js +367 -0
  12. package/dist/static/app-init.js +497 -0
  13. package/dist/static/breakpoints.css +69 -0
  14. package/dist/static/breakpoints.js +197 -0
  15. package/dist/static/clipboard.js +94 -0
  16. package/dist/static/collapse-ui.js +325 -0
  17. package/dist/static/command-history.js +89 -0
  18. package/dist/static/context-menu.js +334 -0
  19. package/dist/static/custom-renderer.js +201 -0
  20. package/dist/static/dagre-layout.js +291 -0
  21. package/dist/static/diagram-dom.js +241 -0
  22. package/dist/static/diagram-editor.js +368 -0
  23. package/dist/static/editor-panel.js +107 -0
  24. package/dist/static/editor-popovers.js +187 -0
  25. package/dist/static/event-bus.js +57 -0
  26. package/dist/static/export.js +181 -0
  27. package/dist/static/file-tree.js +470 -0
  28. package/dist/static/ghost-paths.js +397 -0
  29. package/dist/static/heatmap.css +116 -0
  30. package/dist/static/heatmap.js +308 -0
  31. package/dist/static/icons.js +66 -0
  32. package/dist/static/inline-edit.js +294 -0
  33. package/dist/static/interaction-state.js +155 -0
  34. package/dist/static/interaction-tracker.js +93 -0
  35. package/dist/static/live.html +239 -0
  36. package/dist/static/main-layout.css +220 -0
  37. package/dist/static/main.css +334 -0
  38. package/dist/static/mcp-sessions.js +202 -0
  39. package/dist/static/modal.css +109 -0
  40. package/dist/static/modal.js +171 -0
  41. package/dist/static/node-drag.js +293 -0
  42. package/dist/static/pan-zoom.js +199 -0
  43. package/dist/static/renderer.js +280 -0
  44. package/dist/static/search.css +103 -0
  45. package/dist/static/search.js +304 -0
  46. package/dist/static/selection.js +353 -0
  47. package/dist/static/session-player.css +137 -0
  48. package/dist/static/session-player.js +411 -0
  49. package/dist/static/sidebar.css +248 -0
  50. package/dist/static/svg-renderer.js +313 -0
  51. package/dist/static/svg-shapes.js +218 -0
  52. package/dist/static/tokens.css +76 -0
  53. package/dist/static/vendor/dagre-bundle.js +43 -0
  54. package/dist/static/vendor/dagre.min.js +3 -0
  55. package/dist/static/vendor/graphlib.min.js +2 -0
  56. package/dist/static/viewport-transform.js +107 -0
  57. package/dist/static/workspace-switcher.js +202 -0
  58. package/dist/static/ws-client.js +71 -0
  59. package/dist/static/ws-handler.js +125 -0
  60. package/package.json +74 -0
@@ -0,0 +1,397 @@
1
+ /**
2
+ * SmartCode Ghost Paths -- renders discarded reasoning branches as
3
+ * curved dashed lines on the diagram SVG. Toggle visibility via button.
4
+ *
5
+ * Dependencies:
6
+ * - diagram-dom.js (DiagramDOM)
7
+ * - event-bus.js (SmartCodeEventBus)
8
+ */
9
+ (function() {
10
+ 'use strict';
11
+
12
+ var SVG_NS = 'http://www.w3.org/2000/svg';
13
+ var LS_KEY = 'smartcode-ghost-paths-visible';
14
+
15
+ // Visual config
16
+ var GHOST_COLOR = '#8b5cf6'; // matches --ghost-path token
17
+ var GHOST_OPACITY = 0.55;
18
+ var GHOST_STROKE_W = 2;
19
+ var GHOST_DASH = '6,4';
20
+ var LABEL_FONT_SIZE = 10;
21
+ var LABEL_BG_PAD_X = 6;
22
+ var LABEL_BG_PAD_Y = 3;
23
+ var LABEL_BG_COLOR = '#18181b'; // matches --surface-1 token
24
+ var LABEL_BG_OPACITY = 0.85;
25
+ var LABEL_MAX_CHARS = 40;
26
+ var CURVE_OFFSET = 60; // Bezier curve offset to avoid straight-line crossings
27
+
28
+ // ── Module State ──
29
+ var ghostPathsByFile = {}; // { filePath: [ghostPath, ...] }
30
+ var visible = false;
31
+ var userExplicitlyHid = false; // Track if user manually toggled off (GHOST-06)
32
+
33
+ // ── Helpers ──
34
+
35
+ function loadVisibility() {
36
+ try {
37
+ var stored = localStorage.getItem(LS_KEY);
38
+ if (stored !== null) visible = stored === 'true';
39
+ } catch (e) { /* localStorage unavailable */ }
40
+ }
41
+
42
+ function saveVisibility() {
43
+ try { localStorage.setItem(LS_KEY, String(visible)); } catch (e) {}
44
+ }
45
+
46
+ /** Get ghost paths for the currently viewed file */
47
+ function getCurrentPaths() {
48
+ var file = window.SmartCodeFileTree ? SmartCodeFileTree.getCurrentFile() : null;
49
+ if (!file) return [];
50
+ return ghostPathsByFile[file] || [];
51
+ }
52
+
53
+ function updateBadge() {
54
+ var badge = document.getElementById('ghostCountBadge');
55
+ if (badge) {
56
+ var count = getCurrentPaths().length;
57
+ badge.textContent = count || '';
58
+ badge.dataset.count = count;
59
+ }
60
+ }
61
+
62
+ function updateButtonState() {
63
+ var btn = document.getElementById('btnGhostPaths');
64
+ if (btn) btn.classList.toggle('active', visible);
65
+ }
66
+
67
+ function truncateLabel(label) {
68
+ if (!label) return '';
69
+ return label.length > LABEL_MAX_CHARS
70
+ ? label.substring(0, LABEL_MAX_CHARS - 1) + '\u2026'
71
+ : label;
72
+ }
73
+
74
+ // ── Dedup: keep only the latest ghost path per from->to pair ──
75
+ function dedupPaths(paths) {
76
+ var seen = {};
77
+ var result = [];
78
+ // Walk backwards so the last recorded wins
79
+ for (var i = paths.length - 1; i >= 0; i--) {
80
+ var key = paths[i].fromNodeId + '->' + paths[i].toNodeId;
81
+ if (!seen[key]) {
82
+ seen[key] = true;
83
+ result.unshift(paths[i]);
84
+ }
85
+ }
86
+ return result;
87
+ }
88
+
89
+ // ── Ghost Path Rendering ──
90
+
91
+ /**
92
+ * Parse translate(x,y) from an element's transform attribute.
93
+ * Returns { tx, ty } with offsets (0,0 if no transform).
94
+ */
95
+ function parseTranslate(el) {
96
+ var tx = 0, ty = 0;
97
+ var transform = el.getAttribute('transform');
98
+ if (transform) {
99
+ var match = transform.match(/translate\(\s*([-\d.]+)\s*,\s*([-\d.]+)\s*\)/);
100
+ if (match) {
101
+ tx = parseFloat(match[1]);
102
+ ty = parseFloat(match[2]);
103
+ }
104
+ }
105
+ return { tx: tx, ty: ty };
106
+ }
107
+
108
+ function getNodeCenter(nodeId) {
109
+ var el = DiagramDOM.findNodeElement(nodeId);
110
+ if (!el || !el.getBBox) return null;
111
+ var bbox = el.getBBox();
112
+ var t = parseTranslate(el);
113
+ return {
114
+ x: t.tx + bbox.x + bbox.width / 2,
115
+ y: t.ty + bbox.y + bbox.height / 2
116
+ };
117
+ }
118
+
119
+ function getNodeBottom(nodeId) {
120
+ var el = DiagramDOM.findNodeElement(nodeId);
121
+ if (!el || !el.getBBox) return null;
122
+ var bbox = el.getBBox();
123
+ var t = parseTranslate(el);
124
+ return {
125
+ x: t.tx + bbox.x + bbox.width / 2,
126
+ y: t.ty + bbox.y + bbox.height
127
+ };
128
+ }
129
+
130
+ function getNodeTop(nodeId) {
131
+ var el = DiagramDOM.findNodeElement(nodeId);
132
+ if (!el || !el.getBBox) return null;
133
+ var bbox = el.getBBox();
134
+ var t = parseTranslate(el);
135
+ return {
136
+ x: t.tx + bbox.x + bbox.width / 2,
137
+ y: t.ty + bbox.y
138
+ };
139
+ }
140
+
141
+ /**
142
+ * Build a cubic bezier that curves to the right of the straight line,
143
+ * so it doesn't overlap existing edges.
144
+ */
145
+ function buildCurvedPath(from, to, index) {
146
+ var dx = to.x - from.x;
147
+ var dy = to.y - from.y;
148
+ // Perpendicular offset direction (alternate sides for multiple ghosts)
149
+ var side = (index % 2 === 0) ? 1 : -1;
150
+ var offset = CURVE_OFFSET * side;
151
+
152
+ // Midpoint
153
+ var mx = (from.x + to.x) / 2;
154
+ var my = (from.y + to.y) / 2;
155
+
156
+ // Perpendicular vector (normalized)
157
+ var len = Math.sqrt(dx * dx + dy * dy) || 1;
158
+ var px = -dy / len * offset;
159
+ var py = dx / len * offset;
160
+
161
+ // Control point
162
+ var cx = mx + px;
163
+ var cy = my + py;
164
+
165
+ return {
166
+ d: 'M ' + from.x + ' ' + from.y +
167
+ ' Q ' + cx + ' ' + cy +
168
+ ' ' + to.x + ' ' + to.y,
169
+ labelX: (from.x + 2 * cx + to.x) / 4,
170
+ labelY: (from.y + 2 * cy + to.y) / 4
171
+ };
172
+ }
173
+
174
+ function createArrowMarker(svg) {
175
+ // Check if our ghost arrow marker already exists
176
+ if (svg.querySelector('#ghost-arrow')) return;
177
+
178
+ var defs = svg.querySelector('defs');
179
+ if (!defs) {
180
+ defs = document.createElementNS(SVG_NS, 'defs');
181
+ svg.insertBefore(defs, svg.firstChild);
182
+ }
183
+
184
+ var marker = document.createElementNS(SVG_NS, 'marker');
185
+ marker.setAttribute('id', 'ghost-arrow');
186
+ marker.setAttribute('viewBox', '0 0 10 10');
187
+ marker.setAttribute('refX', '9');
188
+ marker.setAttribute('refY', '5');
189
+ marker.setAttribute('markerWidth', '6');
190
+ marker.setAttribute('markerHeight', '6');
191
+ marker.setAttribute('orient', 'auto-start-reverse');
192
+
193
+ var arrow = document.createElementNS(SVG_NS, 'path');
194
+ arrow.setAttribute('d', 'M 0 0 L 10 5 L 0 10 z');
195
+ arrow.setAttribute('fill', GHOST_COLOR);
196
+ marker.appendChild(arrow);
197
+ defs.appendChild(marker);
198
+ }
199
+
200
+ function renderGhostPaths() {
201
+ var svg = DiagramDOM.getSVG();
202
+ if (!svg) return;
203
+
204
+ // Remove existing ghost path elements
205
+ svg.querySelectorAll('.ghost-path').forEach(function(el) { el.remove(); });
206
+
207
+ var paths = getCurrentPaths();
208
+ if (!visible || paths.length === 0) return;
209
+
210
+ createArrowMarker(svg);
211
+
212
+ var unique = dedupPaths(paths);
213
+ var diagramGroup = svg.querySelector('.smartcode-diagram');
214
+ var container = diagramGroup || svg;
215
+
216
+ for (var i = 0; i < unique.length; i++) {
217
+ var gp = unique[i];
218
+ var fromPt = getNodeBottom(gp.fromNodeId) || getNodeCenter(gp.fromNodeId);
219
+ var toPt = getNodeTop(gp.toNodeId) || getNodeCenter(gp.toNodeId);
220
+ if (!fromPt || !toPt) {
221
+ console.warn('[SmartCode GhostPaths] Node not found in SVG:',
222
+ !fromPt ? gp.fromNodeId : gp.toNodeId,
223
+ '— ghost path skipped:', gp.fromNodeId, '->', gp.toNodeId);
224
+ continue;
225
+ }
226
+
227
+ var g = document.createElementNS(SVG_NS, 'g');
228
+ g.setAttribute('class', 'ghost-path');
229
+ g.setAttribute('opacity', String(GHOST_OPACITY));
230
+
231
+ // Curved path
232
+ var curve = buildCurvedPath(fromPt, toPt, i);
233
+ var pathEl = document.createElementNS(SVG_NS, 'path');
234
+ pathEl.setAttribute('d', curve.d);
235
+ pathEl.setAttribute('stroke-dasharray', GHOST_DASH);
236
+ pathEl.setAttribute('stroke', GHOST_COLOR);
237
+ pathEl.setAttribute('stroke-width', String(GHOST_STROKE_W));
238
+ pathEl.setAttribute('fill', 'none');
239
+ pathEl.setAttribute('marker-end', 'url(#ghost-arrow)');
240
+ g.appendChild(pathEl);
241
+
242
+ // Label with background pill
243
+ if (gp.label) {
244
+ var labelText = truncateLabel(gp.label);
245
+
246
+ // Create text first to measure it
247
+ var text = document.createElementNS(SVG_NS, 'text');
248
+ text.setAttribute('x', String(curve.labelX));
249
+ text.setAttribute('y', String(curve.labelY));
250
+ text.setAttribute('text-anchor', 'middle');
251
+ text.setAttribute('dominant-baseline', 'central');
252
+ text.setAttribute('fill', GHOST_COLOR);
253
+ text.setAttribute('font-size', String(LABEL_FONT_SIZE));
254
+ text.setAttribute('font-family', "'JetBrains Mono', 'Inter', monospace");
255
+ text.setAttribute('font-weight', '500');
256
+ text.textContent = labelText;
257
+
258
+ // Append text temporarily to measure
259
+ g.appendChild(text);
260
+ container.appendChild(g);
261
+ var tBox = text.getBBox();
262
+ container.removeChild(g);
263
+ g.removeChild(text);
264
+
265
+ // Background pill
266
+ var bg = document.createElementNS(SVG_NS, 'rect');
267
+ bg.setAttribute('x', String(tBox.x - LABEL_BG_PAD_X));
268
+ bg.setAttribute('y', String(tBox.y - LABEL_BG_PAD_Y));
269
+ bg.setAttribute('width', String(tBox.width + LABEL_BG_PAD_X * 2));
270
+ bg.setAttribute('height', String(tBox.height + LABEL_BG_PAD_Y * 2));
271
+ bg.setAttribute('rx', '4');
272
+ bg.setAttribute('fill', LABEL_BG_COLOR);
273
+ bg.setAttribute('opacity', String(LABEL_BG_OPACITY));
274
+ g.appendChild(bg);
275
+ g.appendChild(text);
276
+ }
277
+
278
+ container.appendChild(g);
279
+ }
280
+ }
281
+
282
+ // ── Public API Methods ──
283
+
284
+ function toggle() {
285
+ visible = !visible;
286
+ // Track explicit user choice: if they hide, don't auto-show again
287
+ userExplicitlyHid = !visible;
288
+ saveVisibility();
289
+ updateButtonState();
290
+ renderGhostPaths();
291
+ }
292
+
293
+ function isVisibleFn() {
294
+ return visible;
295
+ }
296
+
297
+ function updateGhostPathsFn(file, paths) {
298
+ // Support legacy call without file: updateGhostPaths(paths)
299
+ if (Array.isArray(file)) {
300
+ paths = file;
301
+ file = window.SmartCodeFileTree ? SmartCodeFileTree.getCurrentFile() : 'unknown';
302
+ }
303
+ var list = Array.isArray(paths) ? paths : [];
304
+ if (file) {
305
+ ghostPathsByFile[file] = list;
306
+ }
307
+ updateBadge();
308
+
309
+ // Auto-show ghost paths when new ones arrive -- but respect user choice
310
+ var currentFile = window.SmartCodeFileTree ? SmartCodeFileTree.getCurrentFile() : null;
311
+ if (list.length > 0 && file === currentFile && !visible && !userExplicitlyHid) {
312
+ visible = true;
313
+ saveVisibility();
314
+ updateButtonState();
315
+ }
316
+
317
+ renderGhostPaths();
318
+ }
319
+
320
+ function getCount() {
321
+ return getCurrentPaths().length;
322
+ }
323
+
324
+ // ── Init ──
325
+
326
+ function init() {
327
+ loadVisibility();
328
+ updateButtonState();
329
+ updateBadge();
330
+
331
+ // Re-render ghost paths after each diagram render
332
+ if (window.SmartCodeEventBus) {
333
+ SmartCodeEventBus.on('diagram:rendered', renderGhostPaths);
334
+ }
335
+
336
+ renderGhostPaths();
337
+ }
338
+
339
+ // ── URL helper ──
340
+ function bUrl(path) { return (window.SmartCodeBaseUrl || '') + path; }
341
+
342
+ /** Create a ghost path via REST API and update UI */
343
+ function createGhostPath(fromNodeId, toNodeId, label) {
344
+ var file = window.SmartCodeFileTree ? SmartCodeFileTree.getCurrentFile() : null;
345
+ if (!file) return;
346
+ var encoded = encodeURIComponent(file);
347
+ fetch(bUrl('/api/ghost-paths/' + encoded), {
348
+ method: 'POST',
349
+ headers: { 'Content-Type': 'application/json' },
350
+ body: JSON.stringify({ fromNodeId: fromNodeId, toNodeId: toNodeId, label: label || undefined }),
351
+ }).then(function(r) {
352
+ if (!r.ok) throw new Error('Failed');
353
+ // Refetch to get updated list
354
+ return fetch(bUrl('/api/ghost-paths/' + encoded));
355
+ }).then(function(r) { return r.ok ? r.json() : null; })
356
+ .then(function(d) {
357
+ if (d) updateGhostPathsFn(file, d.ghostPaths || []);
358
+ if (window.toast) toast('Ghost path created');
359
+ }).catch(function() {
360
+ if (window.toast) toast('Error creating ghost path');
361
+ });
362
+ }
363
+
364
+ /** Clear all ghost paths for the current file via REST API */
365
+ function clearAllGhostPaths() {
366
+ var file = window.SmartCodeFileTree ? SmartCodeFileTree.getCurrentFile() : null;
367
+ if (!file) return;
368
+ var encoded = encodeURIComponent(file);
369
+ fetch(bUrl('/api/ghost-paths/' + encoded), {
370
+ method: 'DELETE',
371
+ }).then(function(r) {
372
+ if (!r.ok) throw new Error('Failed');
373
+ updateGhostPathsFn(file, []);
374
+ if (window.toast) toast('Ghost paths cleared');
375
+ }).catch(function() {
376
+ if (window.toast) toast('Error clearing ghost paths');
377
+ });
378
+ }
379
+
380
+ /** Reset the explicit-hide flag (called on file switch) */
381
+ function resetUserHide() {
382
+ userExplicitlyHid = false;
383
+ }
384
+
385
+ // ── Public API ──
386
+ window.SmartCodeGhostPaths = {
387
+ init: init,
388
+ toggle: toggle,
389
+ isVisible: isVisibleFn,
390
+ updateGhostPaths: updateGhostPathsFn,
391
+ renderGhostPaths: renderGhostPaths,
392
+ getCount: getCount,
393
+ createGhostPath: createGhostPath,
394
+ clearAll: clearAllGhostPaths,
395
+ resetUserHide: resetUserHide,
396
+ };
397
+ })();
@@ -0,0 +1,116 @@
1
+ /* ===================================================
2
+ SmartCode -- Heatmap Toggle & Legend Styles
3
+ =================================================== */
4
+
5
+ /* Heatmap toggle button active state */
6
+ #btnHeatmap {
7
+ position: relative;
8
+ }
9
+ #btnHeatmap.active {
10
+ background: rgba(239, 68, 68, 0.15) !important;
11
+ color: var(--status-problem) !important;
12
+ border-color: rgba(239, 68, 68, 0.3) !important;
13
+ }
14
+
15
+ /* Legend container -- bottom-right of preview area */
16
+ .heatmap-legend {
17
+ position: absolute;
18
+ bottom: 60px;
19
+ right: 16px;
20
+ z-index: 50;
21
+ background: var(--surface-1);
22
+ border: 1px solid var(--border-default);
23
+ border-radius: 6px;
24
+ padding: 8px 12px;
25
+ font-family: 'Inter', sans-serif;
26
+ font-size: 11px;
27
+ color: var(--text-primary);
28
+ min-width: 80px;
29
+ box-shadow: 0 2px 8px rgba(0,0,0,0.3);
30
+ }
31
+
32
+ .heatmap-legend-title {
33
+ font-weight: 600;
34
+ font-size: 10px;
35
+ text-transform: uppercase;
36
+ letter-spacing: 1px;
37
+ color: var(--text-secondary);
38
+ margin-bottom: 6px;
39
+ }
40
+
41
+ /* Risk mode legend items */
42
+ .heatmap-legend-item {
43
+ display: flex;
44
+ align-items: center;
45
+ gap: 6px;
46
+ padding: 2px 0;
47
+ }
48
+
49
+ .heatmap-legend-dot {
50
+ width: 10px;
51
+ height: 10px;
52
+ border-radius: 50%;
53
+ flex-shrink: 0;
54
+ }
55
+
56
+ /* Frequency mode gradient bar */
57
+ .heatmap-legend-gradient {
58
+ width: 100%;
59
+ height: 10px;
60
+ border-radius: 4px;
61
+ background: linear-gradient(to right,
62
+ rgb(66, 133, 244),
63
+ rgb(160, 106, 122),
64
+ rgb(255, 80, 0)
65
+ );
66
+ margin: 4px 0;
67
+ }
68
+
69
+ .heatmap-legend-labels {
70
+ display: flex;
71
+ justify-content: space-between;
72
+ font-size: 9px;
73
+ color: var(--text-secondary);
74
+ }
75
+
76
+ /* Legend header with title + mode toggle */
77
+ .heatmap-legend-header {
78
+ display: flex;
79
+ align-items: center;
80
+ justify-content: space-between;
81
+ gap: 8px;
82
+ margin-bottom: 6px;
83
+ }
84
+
85
+ .heatmap-legend-header .heatmap-legend-title {
86
+ margin-bottom: 0;
87
+ }
88
+
89
+ /* Mode toggle button */
90
+ .heatmap-mode-toggle {
91
+ font-size: 9px;
92
+ font-family: 'Inter', sans-serif;
93
+ font-weight: 500;
94
+ padding: 2px 6px;
95
+ border-radius: 3px;
96
+ border: 1px solid var(--border-default);
97
+ background: var(--surface-2, #2a2a2a);
98
+ color: var(--text-secondary);
99
+ cursor: pointer;
100
+ white-space: nowrap;
101
+ transition: background 0.15s, color 0.15s;
102
+ }
103
+
104
+ .heatmap-mode-toggle:hover {
105
+ background: var(--surface-3, #333);
106
+ color: var(--text-primary);
107
+ }
108
+
109
+ /* Empty state guidance */
110
+ .heatmap-empty-state {
111
+ font-size: 10px;
112
+ color: var(--text-secondary);
113
+ line-height: 1.4;
114
+ padding: 4px 0;
115
+ max-width: 180px;
116
+ }