gopeak 2.1.0 → 2.2.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.
- package/README.md +224 -75
- package/build/addon/godot_mcp_editor/mcp_client.gd +178 -0
- package/build/addon/godot_mcp_editor/plugin.cfg +6 -0
- package/build/addon/godot_mcp_editor/plugin.gd +84 -0
- package/build/addon/godot_mcp_editor/tool_executor.gd +114 -0
- package/build/addon/godot_mcp_editor/tools/animation_tools.gd +502 -0
- package/build/addon/godot_mcp_editor/tools/resource_tools.gd +425 -0
- package/build/addon/godot_mcp_editor/tools/scene_tools.gd +710 -0
- package/build/cli/check.js +77 -0
- package/build/cli/notify.js +88 -0
- package/build/cli/setup.js +115 -0
- package/build/cli/star.js +51 -0
- package/build/cli/uninstall.js +26 -0
- package/build/cli/utils.js +149 -0
- package/build/cli.js +91 -0
- package/build/gdscript_parser.js +828 -0
- package/build/godot-bridge.js +556 -0
- package/build/index.js +2761 -2064
- package/build/prompts.js +163 -0
- package/build/visualizer/canvas.js +832 -0
- package/build/visualizer/events.js +814 -0
- package/build/visualizer/layout.js +304 -0
- package/build/visualizer/main.js +245 -0
- package/build/visualizer/modals.js +239 -0
- package/build/visualizer/panel.js +1091 -0
- package/build/visualizer/state.js +210 -0
- package/build/visualizer/syntax.js +106 -0
- package/build/visualizer/usages.js +352 -0
- package/build/visualizer/websocket.js +85 -0
- package/build/visualizer-server.js +375 -0
- package/build/visualizer.html +6395 -0
- package/package.json +15 -6
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Force-directed layout algorithm for node positioning
|
|
3
|
+
*/
|
|
4
|
+
import { nodes, edges, NODE_W, NODE_H } from './state.js';
|
|
5
|
+
// Minimum spacing between nodes
|
|
6
|
+
const MIN_SPACING_X = NODE_W + 40;
|
|
7
|
+
const MIN_SPACING_Y = NODE_H + 30;
|
|
8
|
+
export function initLayout() {
|
|
9
|
+
if (nodes.length === 0)
|
|
10
|
+
return;
|
|
11
|
+
// Build adjacency map for connected nodes
|
|
12
|
+
const adjacency = new Map();
|
|
13
|
+
nodes.forEach(n => {
|
|
14
|
+
adjacency.set(n.path, []);
|
|
15
|
+
});
|
|
16
|
+
edges.forEach(e => {
|
|
17
|
+
if (adjacency.has(e.from) && adjacency.has(e.to)) {
|
|
18
|
+
adjacency.get(e.from).push(e.to);
|
|
19
|
+
adjacency.get(e.to).push(e.from);
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
// Find root nodes (most connections or extends nothing)
|
|
23
|
+
const connectionCount = new Map();
|
|
24
|
+
nodes.forEach(n => {
|
|
25
|
+
const count = (adjacency.get(n.path) || []).length;
|
|
26
|
+
connectionCount.set(n.path, count);
|
|
27
|
+
});
|
|
28
|
+
// Sort nodes by connection count (most connected first)
|
|
29
|
+
const sortedNodes = [...nodes].sort((a, b) => connectionCount.get(b.path) - connectionCount.get(a.path));
|
|
30
|
+
// Initial placement: spread nodes in a grid with good spacing
|
|
31
|
+
const cols = Math.ceil(Math.sqrt(nodes.length));
|
|
32
|
+
const startX = -(cols * MIN_SPACING_X) / 2;
|
|
33
|
+
const startY = -(Math.ceil(nodes.length / cols) * MIN_SPACING_Y) / 2;
|
|
34
|
+
sortedNodes.forEach((n, i) => {
|
|
35
|
+
const col = i % cols;
|
|
36
|
+
const row = Math.floor(i / cols);
|
|
37
|
+
n.x = startX + col * MIN_SPACING_X;
|
|
38
|
+
n.y = startY + row * MIN_SPACING_Y;
|
|
39
|
+
});
|
|
40
|
+
// Run force-directed simulation with collision detection
|
|
41
|
+
const iterations = 150;
|
|
42
|
+
for (let iter = 0; iter < iterations; iter++) {
|
|
43
|
+
const alpha = Math.pow(1 - iter / iterations, 2); // Quadratic cooling
|
|
44
|
+
applyForces(alpha, adjacency);
|
|
45
|
+
resolveCollisions();
|
|
46
|
+
}
|
|
47
|
+
// Final collision resolution pass
|
|
48
|
+
for (let i = 0; i < 10; i++) {
|
|
49
|
+
resolveCollisions();
|
|
50
|
+
}
|
|
51
|
+
// Center the layout
|
|
52
|
+
centerLayout();
|
|
53
|
+
}
|
|
54
|
+
export function initGroupedLayout() {
|
|
55
|
+
if (nodes.length === 0)
|
|
56
|
+
return;
|
|
57
|
+
const groups = {};
|
|
58
|
+
nodes.forEach(n => {
|
|
59
|
+
const cat = n.category || 'other';
|
|
60
|
+
if (!groups[cat])
|
|
61
|
+
groups[cat] = [];
|
|
62
|
+
groups[cat].push(n);
|
|
63
|
+
});
|
|
64
|
+
const groupKeys = Object.keys(groups).sort((a, b) => groups[b].length - groups[a].length);
|
|
65
|
+
const groupCols = Math.ceil(Math.sqrt(groupKeys.length));
|
|
66
|
+
const GROUP_PADDING = 60;
|
|
67
|
+
const GROUP_GAP = 120;
|
|
68
|
+
const groupLayouts = {};
|
|
69
|
+
groupKeys.forEach((cat, gi) => {
|
|
70
|
+
const groupNodes = groups[cat];
|
|
71
|
+
const cols = Math.max(1, Math.ceil(Math.sqrt(groupNodes.length)));
|
|
72
|
+
const rows = Math.ceil(groupNodes.length / cols);
|
|
73
|
+
const innerW = cols * MIN_SPACING_X;
|
|
74
|
+
const innerH = rows * MIN_SPACING_Y;
|
|
75
|
+
const gCol = gi % groupCols;
|
|
76
|
+
const gRow = Math.floor(gi / groupCols);
|
|
77
|
+
groupLayouts[cat] = {
|
|
78
|
+
nodes: groupNodes,
|
|
79
|
+
cols,
|
|
80
|
+
rows,
|
|
81
|
+
innerW,
|
|
82
|
+
innerH,
|
|
83
|
+
totalW: innerW + GROUP_PADDING * 2,
|
|
84
|
+
totalH: innerH + GROUP_PADDING * 2 + 30,
|
|
85
|
+
gCol,
|
|
86
|
+
gRow
|
|
87
|
+
};
|
|
88
|
+
});
|
|
89
|
+
const maxColWidths = [];
|
|
90
|
+
const maxRowHeights = [];
|
|
91
|
+
Object.values(groupLayouts).forEach(g => {
|
|
92
|
+
while (maxColWidths.length <= g.gCol)
|
|
93
|
+
maxColWidths.push(0);
|
|
94
|
+
while (maxRowHeights.length <= g.gRow)
|
|
95
|
+
maxRowHeights.push(0);
|
|
96
|
+
maxColWidths[g.gCol] = Math.max(maxColWidths[g.gCol], g.totalW);
|
|
97
|
+
maxRowHeights[g.gRow] = Math.max(maxRowHeights[g.gRow], g.totalH);
|
|
98
|
+
});
|
|
99
|
+
const colOffsets = [0];
|
|
100
|
+
for (let i = 1; i < maxColWidths.length; i++) {
|
|
101
|
+
colOffsets[i] = colOffsets[i - 1] + maxColWidths[i - 1] + GROUP_GAP;
|
|
102
|
+
}
|
|
103
|
+
const rowOffsets = [0];
|
|
104
|
+
for (let i = 1; i < maxRowHeights.length; i++) {
|
|
105
|
+
rowOffsets[i] = rowOffsets[i - 1] + maxRowHeights[i - 1] + GROUP_GAP;
|
|
106
|
+
}
|
|
107
|
+
Object.entries(groupLayouts).forEach(([cat, g]) => {
|
|
108
|
+
const groupX = colOffsets[g.gCol];
|
|
109
|
+
const groupY = rowOffsets[g.gRow] + 30;
|
|
110
|
+
g.groupBox = {
|
|
111
|
+
x: groupX - GROUP_PADDING,
|
|
112
|
+
y: groupY - GROUP_PADDING - 30,
|
|
113
|
+
w: g.totalW,
|
|
114
|
+
h: g.totalH,
|
|
115
|
+
category: cat
|
|
116
|
+
};
|
|
117
|
+
g.nodes.forEach((n, ni) => {
|
|
118
|
+
const col = ni % g.cols;
|
|
119
|
+
const row = Math.floor(ni / g.cols);
|
|
120
|
+
n.x = groupX + col * MIN_SPACING_X + MIN_SPACING_X / 2;
|
|
121
|
+
n.y = groupY + row * MIN_SPACING_Y + MIN_SPACING_Y / 2;
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
const iterations = 60;
|
|
125
|
+
for (let iter = 0; iter < iterations; iter++) {
|
|
126
|
+
const alpha = Math.pow(1 - iter / iterations, 2) * 0.5;
|
|
127
|
+
Object.values(groupLayouts).forEach(g => {
|
|
128
|
+
for (let i = 0; i < g.nodes.length; i++) {
|
|
129
|
+
for (let j = i + 1; j < g.nodes.length; j++) {
|
|
130
|
+
const a = g.nodes[i];
|
|
131
|
+
const b = g.nodes[j];
|
|
132
|
+
let dx = b.x - a.x;
|
|
133
|
+
let dy = b.y - a.y;
|
|
134
|
+
if (dx === 0 && dy === 0) {
|
|
135
|
+
dx = Math.random() - 0.5;
|
|
136
|
+
dy = Math.random() - 0.5;
|
|
137
|
+
}
|
|
138
|
+
const dist = Math.sqrt(dx * dx + dy * dy) || 1;
|
|
139
|
+
const force = (20000 / (dist * dist)) * alpha;
|
|
140
|
+
const fx = (dx / dist) * force;
|
|
141
|
+
const fy = (dy / dist) * force;
|
|
142
|
+
a.x -= fx;
|
|
143
|
+
a.y -= fy;
|
|
144
|
+
b.x += fx;
|
|
145
|
+
b.y += fy;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
edges.forEach(e => {
|
|
150
|
+
const from = nodes.find(n => n.path === e.from);
|
|
151
|
+
const to = nodes.find(n => n.path === e.to);
|
|
152
|
+
if (!from || !to || from.category !== to.category)
|
|
153
|
+
return;
|
|
154
|
+
const dx = to.x - from.x;
|
|
155
|
+
const dy = to.y - from.y;
|
|
156
|
+
const dist = Math.sqrt(dx * dx + dy * dy) || 1;
|
|
157
|
+
if (dist > MIN_SPACING_X) {
|
|
158
|
+
const force = (dist - MIN_SPACING_X) * 0.05 * alpha;
|
|
159
|
+
const fx = (dx / dist) * force;
|
|
160
|
+
const fy = (dy / dist) * force;
|
|
161
|
+
from.x += fx;
|
|
162
|
+
from.y += fy;
|
|
163
|
+
to.x -= fx;
|
|
164
|
+
to.y -= fy;
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
window.__categoryGroupBoxes = Object.values(groupLayouts).map(g => g.groupBox);
|
|
169
|
+
centerGroupedLayout();
|
|
170
|
+
}
|
|
171
|
+
function applyForces(alpha, adjacency) {
|
|
172
|
+
const repulsion = 50000; // Strong repulsion
|
|
173
|
+
const attraction = 0.08; // Moderate attraction
|
|
174
|
+
const idealEdgeLength = MIN_SPACING_X * 1.2;
|
|
175
|
+
// Repulsion between all nodes
|
|
176
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
177
|
+
for (let j = i + 1; j < nodes.length; j++) {
|
|
178
|
+
const a = nodes[i];
|
|
179
|
+
const b = nodes[j];
|
|
180
|
+
let dx = b.x - a.x;
|
|
181
|
+
let dy = b.y - a.y;
|
|
182
|
+
// Add small random offset if nodes are at same position
|
|
183
|
+
if (dx === 0 && dy === 0) {
|
|
184
|
+
dx = (Math.random() - 0.5) * 10;
|
|
185
|
+
dy = (Math.random() - 0.5) * 10;
|
|
186
|
+
}
|
|
187
|
+
const dist = Math.sqrt(dx * dx + dy * dy) || 1;
|
|
188
|
+
const minDist = MIN_SPACING_X; // Minimum desired distance
|
|
189
|
+
// Stronger repulsion when nodes are close
|
|
190
|
+
let force = repulsion / (dist * dist);
|
|
191
|
+
if (dist < minDist) {
|
|
192
|
+
force *= 3; // Extra push when too close
|
|
193
|
+
}
|
|
194
|
+
const fx = (dx / dist) * force * alpha;
|
|
195
|
+
const fy = (dy / dist) * force * alpha;
|
|
196
|
+
a.x -= fx;
|
|
197
|
+
a.y -= fy;
|
|
198
|
+
b.x += fx;
|
|
199
|
+
b.y += fy;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
// Attraction along edges - pull connected nodes together
|
|
203
|
+
edges.forEach(e => {
|
|
204
|
+
const from = nodes.find(n => n.path === e.from);
|
|
205
|
+
const to = nodes.find(n => n.path === e.to);
|
|
206
|
+
if (!from || !to)
|
|
207
|
+
return;
|
|
208
|
+
const dx = to.x - from.x;
|
|
209
|
+
const dy = to.y - from.y;
|
|
210
|
+
const dist = Math.sqrt(dx * dx + dy * dy) || 1;
|
|
211
|
+
// Only attract if nodes are far apart
|
|
212
|
+
if (dist > idealEdgeLength) {
|
|
213
|
+
const force = (dist - idealEdgeLength) * attraction * alpha;
|
|
214
|
+
const fx = (dx / dist) * force;
|
|
215
|
+
const fy = (dy / dist) * force;
|
|
216
|
+
from.x += fx;
|
|
217
|
+
from.y += fy;
|
|
218
|
+
to.x -= fx;
|
|
219
|
+
to.y -= fy;
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
function resolveCollisions() {
|
|
224
|
+
// Separate overlapping nodes
|
|
225
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
226
|
+
for (let j = i + 1; j < nodes.length; j++) {
|
|
227
|
+
const a = nodes[i];
|
|
228
|
+
const b = nodes[j];
|
|
229
|
+
// Check for overlap using bounding boxes
|
|
230
|
+
const overlapX = MIN_SPACING_X - Math.abs(b.x - a.x);
|
|
231
|
+
const overlapY = MIN_SPACING_Y - Math.abs(b.y - a.y);
|
|
232
|
+
if (overlapX > 0 && overlapY > 0) {
|
|
233
|
+
// Nodes are overlapping - push them apart
|
|
234
|
+
let dx = b.x - a.x;
|
|
235
|
+
let dy = b.y - a.y;
|
|
236
|
+
// Add random offset if exactly overlapping
|
|
237
|
+
if (dx === 0)
|
|
238
|
+
dx = (Math.random() - 0.5) * 2;
|
|
239
|
+
if (dy === 0)
|
|
240
|
+
dy = (Math.random() - 0.5) * 2;
|
|
241
|
+
const dist = Math.sqrt(dx * dx + dy * dy) || 1;
|
|
242
|
+
// Push apart in the direction of least overlap
|
|
243
|
+
if (overlapX < overlapY) {
|
|
244
|
+
// Push horizontally
|
|
245
|
+
const push = (overlapX / 2 + 5) * Math.sign(dx);
|
|
246
|
+
a.x -= push;
|
|
247
|
+
b.x += push;
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
// Push vertically
|
|
251
|
+
const push = (overlapY / 2 + 5) * Math.sign(dy);
|
|
252
|
+
a.y -= push;
|
|
253
|
+
b.y += push;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
function centerLayout() {
|
|
260
|
+
if (nodes.length === 0)
|
|
261
|
+
return;
|
|
262
|
+
// Find bounding box
|
|
263
|
+
let minX = Infinity, maxX = -Infinity;
|
|
264
|
+
let minY = Infinity, maxY = -Infinity;
|
|
265
|
+
nodes.forEach(n => {
|
|
266
|
+
minX = Math.min(minX, n.x);
|
|
267
|
+
maxX = Math.max(maxX, n.x);
|
|
268
|
+
minY = Math.min(minY, n.y);
|
|
269
|
+
maxY = Math.max(maxY, n.y);
|
|
270
|
+
});
|
|
271
|
+
// Center around origin
|
|
272
|
+
const centerX = (minX + maxX) / 2;
|
|
273
|
+
const centerY = (minY + maxY) / 2;
|
|
274
|
+
nodes.forEach(n => {
|
|
275
|
+
n.x -= centerX;
|
|
276
|
+
n.y -= centerY;
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
function centerGroupedLayout() {
|
|
280
|
+
if (nodes.length === 0)
|
|
281
|
+
return;
|
|
282
|
+
let minX = Infinity;
|
|
283
|
+
let maxX = -Infinity;
|
|
284
|
+
let minY = Infinity;
|
|
285
|
+
let maxY = -Infinity;
|
|
286
|
+
nodes.forEach(n => {
|
|
287
|
+
minX = Math.min(minX, n.x);
|
|
288
|
+
maxX = Math.max(maxX, n.x);
|
|
289
|
+
minY = Math.min(minY, n.y);
|
|
290
|
+
maxY = Math.max(maxY, n.y);
|
|
291
|
+
});
|
|
292
|
+
const cx = (minX + maxX) / 2;
|
|
293
|
+
const cy = (minY + maxY) / 2;
|
|
294
|
+
nodes.forEach(n => {
|
|
295
|
+
n.x -= cx;
|
|
296
|
+
n.y -= cy;
|
|
297
|
+
});
|
|
298
|
+
if (window.__categoryGroupBoxes) {
|
|
299
|
+
window.__categoryGroupBoxes.forEach(b => {
|
|
300
|
+
b.x -= cx;
|
|
301
|
+
b.y -= cy;
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Main entry point for the Godot Project Map Visualizer
|
|
3
|
+
*/
|
|
4
|
+
import './usages.js'; // Load usages module for side effects (global functions)
|
|
5
|
+
import { centerOnNodes, draw, fitToView, initCanvas, updateZoomIndicator } from './canvas.js';
|
|
6
|
+
import { buildCategoryList, buildChangesPanel, initEvents, updateStats } from './events.js';
|
|
7
|
+
import { initLayout } from './layout.js';
|
|
8
|
+
import { initModals } from './modals.js';
|
|
9
|
+
import { initPanel } from './panel.js';
|
|
10
|
+
import { addActionEntry, actionLog, gitChangeSummary, nodes, PROJECT_DATA } from './state.js';
|
|
11
|
+
import { connectWebSocket, onActionEvent, sendCommand } from './websocket.js';
|
|
12
|
+
const COMMAND_DISPLAY = {
|
|
13
|
+
create_script_file: (args) => `🆕 Script created: ${shortPath(args.path)}`,
|
|
14
|
+
modify_variable: (args) => {
|
|
15
|
+
const action = args.action || 'update';
|
|
16
|
+
const icon = action === 'add' ? '+' : action === 'delete' ? '−' : '✏️';
|
|
17
|
+
return `${icon} var ${args.name || args.old_name || '?'} ${action === 'add' ? '추가' : action === 'delete' ? '삭제' : '수정'}`;
|
|
18
|
+
},
|
|
19
|
+
modify_function: (args) => `✏️ func ${args.name || '?'}() 수정`,
|
|
20
|
+
modify_function_delete: (args) => `− func ${args.name || '?'}() 삭제`,
|
|
21
|
+
modify_signal: (args) => {
|
|
22
|
+
const action = args.action || 'update';
|
|
23
|
+
const icon = action === 'add' ? '🔗' : action === 'delete' ? '−' : '✏️';
|
|
24
|
+
return `${icon} signal ${args.name || args.old_name || '?'} ${action === 'add' ? '추가' : action === 'delete' ? '삭제' : '수정'}`;
|
|
25
|
+
},
|
|
26
|
+
external_change_detected: (args) => {
|
|
27
|
+
const status = args.status || 'modified';
|
|
28
|
+
const icon = status === 'untracked' ? '🆕' : status === 'added' ? '+' : '✏️';
|
|
29
|
+
return `${icon} external ${status}`;
|
|
30
|
+
},
|
|
31
|
+
modify_script: () => '✏️ Script modified',
|
|
32
|
+
create_script: () => '🆕 Script created'
|
|
33
|
+
};
|
|
34
|
+
function shortPath(p) {
|
|
35
|
+
if (!p)
|
|
36
|
+
return '?';
|
|
37
|
+
const parts = p.replace('res://', '').split('/');
|
|
38
|
+
return parts[parts.length - 1];
|
|
39
|
+
}
|
|
40
|
+
function formatTime(ts) {
|
|
41
|
+
const d = new Date(ts);
|
|
42
|
+
const h = d.getHours().toString().padStart(2, '0');
|
|
43
|
+
const m = d.getMinutes().toString().padStart(2, '0');
|
|
44
|
+
const s = d.getSeconds().toString().padStart(2, '0');
|
|
45
|
+
return `${h}:${m}:${s}`;
|
|
46
|
+
}
|
|
47
|
+
function commandToText(command, args) {
|
|
48
|
+
if (!command)
|
|
49
|
+
return 'Unknown action';
|
|
50
|
+
const formatter = COMMAND_DISPLAY[command];
|
|
51
|
+
if (formatter)
|
|
52
|
+
return formatter(args || {});
|
|
53
|
+
return command.replace(/_/g, ' ');
|
|
54
|
+
}
|
|
55
|
+
/** Normalize server entry format (ts/details) to frontend format (timestamp/args) */
|
|
56
|
+
function normalizeEntry(raw) {
|
|
57
|
+
return {
|
|
58
|
+
command: raw.command || 'unknown',
|
|
59
|
+
args: raw.details || raw.args || {},
|
|
60
|
+
timestamp: raw.ts || raw.timestamp || Date.now(),
|
|
61
|
+
reason: raw.reason || null,
|
|
62
|
+
filePath: raw.filePath || raw.details?.path || raw.args?.path || null
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function initTimeline() {
|
|
66
|
+
renderTimeline();
|
|
67
|
+
const fetchActionLog = (attempt = 0) => {
|
|
68
|
+
sendCommand('get_action_log').then((result) => {
|
|
69
|
+
if (result?.entries) {
|
|
70
|
+
for (const entry of result.entries) {
|
|
71
|
+
addActionEntry(normalizeEntry(entry));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
renderTimeline();
|
|
75
|
+
}).catch((err) => {
|
|
76
|
+
if (err.message === 'WebSocket not connected' && attempt < 10) {
|
|
77
|
+
setTimeout(() => fetchActionLog(attempt + 1), 300);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
console.log('[timeline] Could not fetch action log:', err.message);
|
|
81
|
+
});
|
|
82
|
+
};
|
|
83
|
+
fetchActionLog();
|
|
84
|
+
onActionEvent((msg) => {
|
|
85
|
+
// Server sends { type: 'action_event', entry: { ts, command, filePath, details, reason } }
|
|
86
|
+
const raw = msg.entry || msg;
|
|
87
|
+
addActionEntry(normalizeEntry(raw));
|
|
88
|
+
renderTimeline();
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
function renderTimeline() {
|
|
92
|
+
const list = document.getElementById('tl-list');
|
|
93
|
+
const empty = document.getElementById('tl-empty');
|
|
94
|
+
const count = document.getElementById('tl-count');
|
|
95
|
+
if (!list || !empty || !count)
|
|
96
|
+
return;
|
|
97
|
+
count.textContent = actionLog.length;
|
|
98
|
+
if (actionLog.length === 0) {
|
|
99
|
+
empty.style.display = 'block';
|
|
100
|
+
list.innerHTML = '';
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
empty.style.display = 'none';
|
|
104
|
+
const sorted = [...actionLog].reverse();
|
|
105
|
+
const groups = {};
|
|
106
|
+
for (const entry of sorted) {
|
|
107
|
+
const fp = entry.filePath || entry.args?.path || 'Unknown';
|
|
108
|
+
if (!groups[fp])
|
|
109
|
+
groups[fp] = [];
|
|
110
|
+
groups[fp].push(entry);
|
|
111
|
+
}
|
|
112
|
+
let html = '';
|
|
113
|
+
for (const [filePath, entries] of Object.entries(groups)) {
|
|
114
|
+
const fname = shortPath(filePath);
|
|
115
|
+
const escapedPath = filePath.replace(/'/g, "\\'");
|
|
116
|
+
html += `<div class="tl-card" data-path="${filePath}" onclick="timelineCardClick('${escapedPath}')">`;
|
|
117
|
+
html += '<div class="tl-card-header">';
|
|
118
|
+
html += `<span class="tl-card-file">${fname}</span>`;
|
|
119
|
+
html += `<span class="tl-card-count">${entries.length}</span>`;
|
|
120
|
+
html += '</div>';
|
|
121
|
+
for (const entry of entries) {
|
|
122
|
+
const text = commandToText(entry.command, entry.args);
|
|
123
|
+
const time = formatTime(entry.timestamp);
|
|
124
|
+
html += '<div class="tl-action">';
|
|
125
|
+
html += `<span class="tl-action-text">${text}</span>`;
|
|
126
|
+
html += `<span class="tl-action-time">${time}</span>`;
|
|
127
|
+
if (entry.reason) {
|
|
128
|
+
html += `<div class="tl-action-reason">${entry.reason}</div>`;
|
|
129
|
+
}
|
|
130
|
+
html += '</div>';
|
|
131
|
+
}
|
|
132
|
+
html += '</div>';
|
|
133
|
+
}
|
|
134
|
+
list.innerHTML = html;
|
|
135
|
+
}
|
|
136
|
+
// Initialize everything when DOM is ready
|
|
137
|
+
function init() {
|
|
138
|
+
// Connect WebSocket for real-time communication
|
|
139
|
+
connectWebSocket();
|
|
140
|
+
// Initialize canvas and rendering (also restores saved positions)
|
|
141
|
+
const { positionsRestored } = initCanvas();
|
|
142
|
+
// Initialize panel and modals
|
|
143
|
+
initPanel();
|
|
144
|
+
initModals();
|
|
145
|
+
// Initialize event handlers
|
|
146
|
+
initEvents();
|
|
147
|
+
// Update stats
|
|
148
|
+
updateStats();
|
|
149
|
+
// Build category list UI
|
|
150
|
+
buildCategoryList();
|
|
151
|
+
buildChangesPanel();
|
|
152
|
+
const totalChanges = gitChangeSummary.modified + gitChangeSummary.added + gitChangeSummary.untracked;
|
|
153
|
+
if (totalChanges === 0) {
|
|
154
|
+
const changesPanel = document.getElementById('changes-panel');
|
|
155
|
+
if (changesPanel)
|
|
156
|
+
changesPanel.style.display = 'none';
|
|
157
|
+
}
|
|
158
|
+
initTimeline();
|
|
159
|
+
initTimelineDrag();
|
|
160
|
+
// Hide category panel if no categories
|
|
161
|
+
if (!PROJECT_DATA.categories || PROJECT_DATA.categories.length === 0) {
|
|
162
|
+
const catPanel = document.getElementById('category-panel');
|
|
163
|
+
if (catPanel)
|
|
164
|
+
catPanel.style.display = 'none';
|
|
165
|
+
}
|
|
166
|
+
// Get zoom indicator element
|
|
167
|
+
const zoomIndicator = document.getElementById('zoom-indicator');
|
|
168
|
+
if (nodes.length === 0) {
|
|
169
|
+
// No scripts found - show placeholder
|
|
170
|
+
const ctx = document.getElementById('canvas').getContext('2d');
|
|
171
|
+
const W = window.innerWidth;
|
|
172
|
+
const H = window.innerHeight;
|
|
173
|
+
ctx.font = '18px -apple-system, system-ui, sans-serif';
|
|
174
|
+
ctx.fillStyle = '#484f58';
|
|
175
|
+
ctx.textAlign = 'center';
|
|
176
|
+
ctx.fillText('No scripts found in project', W / 2, H / 2);
|
|
177
|
+
zoomIndicator.style.display = 'none';
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
if (positionsRestored) {
|
|
181
|
+
// Positions were restored from localStorage - just update zoom indicator
|
|
182
|
+
updateZoomIndicator();
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
// No saved positions - run force-directed layout
|
|
186
|
+
initLayout();
|
|
187
|
+
// Fit view to show all nodes
|
|
188
|
+
fitToView(nodes);
|
|
189
|
+
}
|
|
190
|
+
// Initial draw
|
|
191
|
+
draw();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
window.timelineCardClick = function timelineCardClick(filePath) {
|
|
195
|
+
const node = nodes.find(n => n.path === filePath);
|
|
196
|
+
if (node) {
|
|
197
|
+
centerOnNodes([node]);
|
|
198
|
+
draw();
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
window.toggleTimelinePanel = function toggleTimelinePanel() {
|
|
202
|
+
const panel = document.getElementById('timeline-panel');
|
|
203
|
+
if (panel) {
|
|
204
|
+
panel.classList.toggle('collapsed');
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
// ── Timeline panel drag ──
|
|
208
|
+
function initTimelineDrag() {
|
|
209
|
+
const panel = document.getElementById('timeline-panel');
|
|
210
|
+
const header = panel?.querySelector('.tl-header');
|
|
211
|
+
if (!panel || !header)
|
|
212
|
+
return;
|
|
213
|
+
let dragging = false, offsetX = 0, offsetY = 0;
|
|
214
|
+
header.addEventListener('mousedown', (e) => {
|
|
215
|
+
dragging = true;
|
|
216
|
+
const rect = panel.getBoundingClientRect();
|
|
217
|
+
offsetX = e.clientX - rect.left;
|
|
218
|
+
offsetY = e.clientY - rect.top;
|
|
219
|
+
header.style.cursor = 'grabbing';
|
|
220
|
+
e.preventDefault();
|
|
221
|
+
});
|
|
222
|
+
document.addEventListener('mousemove', (e) => {
|
|
223
|
+
if (!dragging)
|
|
224
|
+
return;
|
|
225
|
+
const x = e.clientX - offsetX;
|
|
226
|
+
const y = e.clientY - offsetY;
|
|
227
|
+
panel.style.bottom = 'auto';
|
|
228
|
+
panel.style.right = 'auto';
|
|
229
|
+
panel.style.left = Math.max(0, Math.min(x, window.innerWidth - panel.offsetWidth)) + 'px';
|
|
230
|
+
panel.style.top = Math.max(0, Math.min(y, window.innerHeight - panel.offsetHeight)) + 'px';
|
|
231
|
+
});
|
|
232
|
+
document.addEventListener('mouseup', () => {
|
|
233
|
+
if (dragging) {
|
|
234
|
+
dragging = false;
|
|
235
|
+
header.style.cursor = '';
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
// Start when DOM is loaded
|
|
240
|
+
if (document.readyState === 'loading') {
|
|
241
|
+
document.addEventListener('DOMContentLoaded', init);
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
init();
|
|
245
|
+
}
|