aidex-mcp 1.4.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/CHANGELOG.md +128 -0
- package/LICENSE +21 -0
- package/MCP-API-REFERENCE.md +690 -0
- package/README.md +314 -0
- package/build/commands/files.d.ts +28 -0
- package/build/commands/files.js +124 -0
- package/build/commands/index.d.ts +14 -0
- package/build/commands/index.js +14 -0
- package/build/commands/init.d.ts +24 -0
- package/build/commands/init.js +396 -0
- package/build/commands/link.d.ts +45 -0
- package/build/commands/link.js +167 -0
- package/build/commands/note.d.ts +29 -0
- package/build/commands/note.js +105 -0
- package/build/commands/query.d.ts +36 -0
- package/build/commands/query.js +176 -0
- package/build/commands/scan.d.ts +25 -0
- package/build/commands/scan.js +104 -0
- package/build/commands/session.d.ts +52 -0
- package/build/commands/session.js +216 -0
- package/build/commands/signature.d.ts +52 -0
- package/build/commands/signature.js +171 -0
- package/build/commands/summary.d.ts +56 -0
- package/build/commands/summary.js +324 -0
- package/build/commands/update.d.ts +36 -0
- package/build/commands/update.js +273 -0
- package/build/constants.d.ts +10 -0
- package/build/constants.js +10 -0
- package/build/db/database.d.ts +69 -0
- package/build/db/database.js +126 -0
- package/build/db/index.d.ts +7 -0
- package/build/db/index.js +6 -0
- package/build/db/queries.d.ts +163 -0
- package/build/db/queries.js +273 -0
- package/build/db/schema.sql +136 -0
- package/build/index.d.ts +13 -0
- package/build/index.js +74 -0
- package/build/parser/extractor.d.ts +41 -0
- package/build/parser/extractor.js +249 -0
- package/build/parser/index.d.ts +7 -0
- package/build/parser/index.js +7 -0
- package/build/parser/languages/c.d.ts +28 -0
- package/build/parser/languages/c.js +70 -0
- package/build/parser/languages/cpp.d.ts +28 -0
- package/build/parser/languages/cpp.js +91 -0
- package/build/parser/languages/csharp.d.ts +32 -0
- package/build/parser/languages/csharp.js +97 -0
- package/build/parser/languages/go.d.ts +28 -0
- package/build/parser/languages/go.js +83 -0
- package/build/parser/languages/index.d.ts +21 -0
- package/build/parser/languages/index.js +107 -0
- package/build/parser/languages/java.d.ts +28 -0
- package/build/parser/languages/java.js +58 -0
- package/build/parser/languages/php.d.ts +28 -0
- package/build/parser/languages/php.js +75 -0
- package/build/parser/languages/python.d.ts +28 -0
- package/build/parser/languages/python.js +67 -0
- package/build/parser/languages/ruby.d.ts +28 -0
- package/build/parser/languages/ruby.js +68 -0
- package/build/parser/languages/rust.d.ts +28 -0
- package/build/parser/languages/rust.js +73 -0
- package/build/parser/languages/typescript.d.ts +28 -0
- package/build/parser/languages/typescript.js +82 -0
- package/build/parser/tree-sitter.d.ts +30 -0
- package/build/parser/tree-sitter.js +132 -0
- package/build/server/mcp-server.d.ts +7 -0
- package/build/server/mcp-server.js +36 -0
- package/build/server/tools.d.ts +18 -0
- package/build/server/tools.js +1245 -0
- package/build/viewer/git-status.d.ts +25 -0
- package/build/viewer/git-status.js +163 -0
- package/build/viewer/index.d.ts +5 -0
- package/build/viewer/index.js +5 -0
- package/build/viewer/server.d.ts +12 -0
- package/build/viewer/server.js +1122 -0
- package/package.json +66 -0
|
@@ -0,0 +1,1122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AiDex Viewer - Local HTTP Server with WebSocket
|
|
3
|
+
* Opens an interactive project tree in the browser
|
|
4
|
+
*
|
|
5
|
+
* Features:
|
|
6
|
+
* - Tab-based navigation (Code/All files, Overview/Code view)
|
|
7
|
+
* - Session change indicators (modified/new files)
|
|
8
|
+
* - Syntax highlighting with highlight.js
|
|
9
|
+
*/
|
|
10
|
+
import express from 'express';
|
|
11
|
+
import { createServer } from 'http';
|
|
12
|
+
import { WebSocketServer, WebSocket } from 'ws';
|
|
13
|
+
import { exec } from 'child_process';
|
|
14
|
+
import path from 'path';
|
|
15
|
+
import { existsSync, readFileSync } from 'fs';
|
|
16
|
+
import chokidar from 'chokidar';
|
|
17
|
+
import { openDatabase, createQueries } from '../db/index.js';
|
|
18
|
+
import { update as updateIndex } from '../commands/update.js';
|
|
19
|
+
import { getGitStatus } from './git-status.js';
|
|
20
|
+
import { PRODUCT_NAME, INDEX_DIR } from '../constants.js';
|
|
21
|
+
const PORT = 3333;
|
|
22
|
+
let server = null;
|
|
23
|
+
let wss = null;
|
|
24
|
+
let fileWatcher = null;
|
|
25
|
+
export async function startViewer(projectPath) {
|
|
26
|
+
// Check if already running
|
|
27
|
+
if (server) {
|
|
28
|
+
return `Viewer already running at http://localhost:${PORT}`;
|
|
29
|
+
}
|
|
30
|
+
const dbPath = path.join(projectPath, INDEX_DIR, 'index.db');
|
|
31
|
+
const db = openDatabase(dbPath, true); // readonly for queries
|
|
32
|
+
const sqlite = db.getDb();
|
|
33
|
+
const queries = createQueries(db);
|
|
34
|
+
const projectRoot = path.resolve(projectPath);
|
|
35
|
+
const absoluteProjectPath = path.resolve(projectPath); // For updateIndex
|
|
36
|
+
// Track files changed - initialize with DB session changes, then add live changes
|
|
37
|
+
const dbSessionChanges = detectSessionChanges(sqlite);
|
|
38
|
+
const viewerSessionChanges = {
|
|
39
|
+
modified: new Set(dbSessionChanges.modified),
|
|
40
|
+
new: new Set(dbSessionChanges.new)
|
|
41
|
+
};
|
|
42
|
+
console.error('[Viewer] Session changes from DB:', viewerSessionChanges.modified.size, 'modified,', viewerSessionChanges.new.size, 'new');
|
|
43
|
+
// Git status - fetch once at startup, refresh on file changes
|
|
44
|
+
let cachedGitInfo;
|
|
45
|
+
const refreshGitStatus = async () => {
|
|
46
|
+
cachedGitInfo = await getGitStatus(projectPath);
|
|
47
|
+
console.error('[Viewer] Git status:', cachedGitInfo.isGitRepo ? 'repo' : 'no-repo', cachedGitInfo.hasRemote ? 'with-remote' : 'no-remote', cachedGitInfo.fileStatuses.size, 'files with status');
|
|
48
|
+
};
|
|
49
|
+
await refreshGitStatus();
|
|
50
|
+
const app = express();
|
|
51
|
+
server = createServer(app);
|
|
52
|
+
wss = new WebSocketServer({ server });
|
|
53
|
+
// File watcher for live reload
|
|
54
|
+
let debounceTimer = null;
|
|
55
|
+
const pendingChanges = new Set(); // Files changed since last broadcast
|
|
56
|
+
const broadcastTreeUpdate = async () => {
|
|
57
|
+
if (!wss)
|
|
58
|
+
return;
|
|
59
|
+
// Re-index changed files before refreshing the tree
|
|
60
|
+
if (pendingChanges.size > 0) {
|
|
61
|
+
console.error('[Viewer] Re-indexing', pendingChanges.size, 'changed file(s)');
|
|
62
|
+
for (const changedFile of pendingChanges) {
|
|
63
|
+
// Convert absolute path to relative path
|
|
64
|
+
const relativePath = path.relative(projectRoot, changedFile).replace(/\\/g, '/');
|
|
65
|
+
try {
|
|
66
|
+
// updateIndex opens its own DB connection with write access
|
|
67
|
+
const result = updateIndex({ path: absoluteProjectPath, file: relativePath });
|
|
68
|
+
console.error('[Viewer] Re-indexed:', relativePath, result.success ? '✓' : '✗');
|
|
69
|
+
// Track as modified in viewer session
|
|
70
|
+
viewerSessionChanges.modified.add(relativePath);
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
console.error('[Viewer] Failed to re-index:', relativePath, err);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
pendingChanges.clear();
|
|
77
|
+
}
|
|
78
|
+
// Refresh git status on file changes
|
|
79
|
+
await refreshGitStatus();
|
|
80
|
+
// Build fresh trees for both modes using viewer session tracking
|
|
81
|
+
const freshDb = openDatabase(dbPath, true);
|
|
82
|
+
const codeTree = await buildTree(freshDb.getDb(), projectPath, 'code', viewerSessionChanges, cachedGitInfo);
|
|
83
|
+
const allTree = await buildTree(freshDb.getDb(), projectPath, 'all', viewerSessionChanges, cachedGitInfo);
|
|
84
|
+
freshDb.close();
|
|
85
|
+
// Broadcast to all connected clients
|
|
86
|
+
wss.clients.forEach((client) => {
|
|
87
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
88
|
+
client.send(JSON.stringify({ type: 'refresh', codeTree, allTree }));
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
console.error('[Viewer] Broadcast tree update to', wss.clients.size, 'clients');
|
|
92
|
+
};
|
|
93
|
+
// Use chokidar for reliable cross-platform file watching
|
|
94
|
+
fileWatcher = chokidar.watch(projectRoot, {
|
|
95
|
+
ignored: [
|
|
96
|
+
'**/node_modules/**',
|
|
97
|
+
'**/.git/**',
|
|
98
|
+
`**/${INDEX_DIR}/**`,
|
|
99
|
+
'**/build/**',
|
|
100
|
+
'**/dist/**'
|
|
101
|
+
],
|
|
102
|
+
ignoreInitial: true,
|
|
103
|
+
persistent: true
|
|
104
|
+
});
|
|
105
|
+
fileWatcher.on('ready', () => {
|
|
106
|
+
console.error('[Viewer] Chokidar ready, watching for changes');
|
|
107
|
+
});
|
|
108
|
+
fileWatcher.on('error', (error) => {
|
|
109
|
+
console.error('[Viewer] Chokidar error:', error);
|
|
110
|
+
});
|
|
111
|
+
fileWatcher.on('all', (event, filePath) => {
|
|
112
|
+
console.error('[Viewer] Chokidar event:', event, filePath);
|
|
113
|
+
// Track changed files for re-indexing (only for change/add events on code files)
|
|
114
|
+
if ((event === 'change' || event === 'add') && /\.(ts|tsx|js|jsx|cs|rs|py|c|cpp|h|hpp|java|go|php|rb)$/i.test(filePath)) {
|
|
115
|
+
pendingChanges.add(filePath);
|
|
116
|
+
}
|
|
117
|
+
// Debounce: wait 500ms after last change before broadcasting
|
|
118
|
+
if (debounceTimer) {
|
|
119
|
+
clearTimeout(debounceTimer);
|
|
120
|
+
}
|
|
121
|
+
debounceTimer = setTimeout(() => {
|
|
122
|
+
console.error('[Viewer] Broadcasting after debounce');
|
|
123
|
+
broadcastTreeUpdate();
|
|
124
|
+
}, 500);
|
|
125
|
+
});
|
|
126
|
+
console.error('[Viewer] Initializing chokidar for', projectRoot);
|
|
127
|
+
// Serve static HTML
|
|
128
|
+
app.get('/', (req, res) => {
|
|
129
|
+
res.send(getViewerHTML(projectPath));
|
|
130
|
+
});
|
|
131
|
+
// Debug endpoint to manually trigger refresh
|
|
132
|
+
app.get('/refresh', async (req, res) => {
|
|
133
|
+
await broadcastTreeUpdate();
|
|
134
|
+
res.send('Refresh triggered');
|
|
135
|
+
});
|
|
136
|
+
// WebSocket handling
|
|
137
|
+
wss.on('connection', (ws) => {
|
|
138
|
+
console.error('[Viewer] Client connected');
|
|
139
|
+
ws.on('message', async (data) => {
|
|
140
|
+
try {
|
|
141
|
+
const msg = JSON.parse(data.toString());
|
|
142
|
+
if (msg.type === 'getTree') {
|
|
143
|
+
const mode = msg.mode || 'code';
|
|
144
|
+
const tree = await buildTree(sqlite, projectPath, mode, viewerSessionChanges, cachedGitInfo);
|
|
145
|
+
ws.send(JSON.stringify({ type: 'tree', mode, data: tree }));
|
|
146
|
+
}
|
|
147
|
+
else if (msg.type === 'getSignature' && msg.file) {
|
|
148
|
+
const signature = await getFileSignature(sqlite, msg.file);
|
|
149
|
+
ws.send(JSON.stringify({ type: 'signature', file: msg.file, data: signature }));
|
|
150
|
+
}
|
|
151
|
+
else if (msg.type === 'getFileContent' && msg.file) {
|
|
152
|
+
const content = getFileContent(projectRoot, msg.file);
|
|
153
|
+
ws.send(JSON.stringify({ type: 'fileContent', file: msg.file, data: content }));
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
catch (err) {
|
|
157
|
+
console.error('[Viewer] Error:', err);
|
|
158
|
+
ws.send(JSON.stringify({ type: 'error', message: String(err) }));
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
ws.on('close', () => {
|
|
162
|
+
console.error('[Viewer] Client disconnected');
|
|
163
|
+
});
|
|
164
|
+
// Send initial tree (code files only)
|
|
165
|
+
buildTree(sqlite, projectPath, 'code', viewerSessionChanges, cachedGitInfo).then(tree => {
|
|
166
|
+
ws.send(JSON.stringify({ type: 'tree', mode: 'code', data: tree }));
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
return new Promise((resolve, reject) => {
|
|
170
|
+
server.listen(PORT, () => {
|
|
171
|
+
const url = `http://localhost:${PORT}`;
|
|
172
|
+
console.error(`[Viewer] Server running at ${url}`);
|
|
173
|
+
// Open browser
|
|
174
|
+
openBrowser(url);
|
|
175
|
+
resolve(`Viewer opened at ${url}`);
|
|
176
|
+
});
|
|
177
|
+
server.on('error', (err) => {
|
|
178
|
+
if (err.code === 'EADDRINUSE') {
|
|
179
|
+
resolve(`Port ${PORT} already in use - viewer may already be running at http://localhost:${PORT}`);
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
reject(err);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
export function stopViewer() {
|
|
188
|
+
if (server) {
|
|
189
|
+
fileWatcher?.close();
|
|
190
|
+
fileWatcher = null;
|
|
191
|
+
wss?.close();
|
|
192
|
+
server.close();
|
|
193
|
+
server = null;
|
|
194
|
+
wss = null;
|
|
195
|
+
return 'Viewer stopped';
|
|
196
|
+
}
|
|
197
|
+
return 'Viewer was not running';
|
|
198
|
+
}
|
|
199
|
+
function openBrowser(url) {
|
|
200
|
+
const platform = process.platform;
|
|
201
|
+
let cmd;
|
|
202
|
+
if (platform === 'win32') {
|
|
203
|
+
cmd = `start "" "${url}"`;
|
|
204
|
+
}
|
|
205
|
+
else if (platform === 'darwin') {
|
|
206
|
+
cmd = `open "${url}"`;
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
cmd = `xdg-open "${url}"`;
|
|
210
|
+
}
|
|
211
|
+
exec(cmd, (err) => {
|
|
212
|
+
if (err)
|
|
213
|
+
console.error('[Viewer] Failed to open browser:', err);
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Detect files changed in the current session
|
|
218
|
+
* Uses last_indexed timestamps vs session start time
|
|
219
|
+
*/
|
|
220
|
+
function detectSessionChanges(db) {
|
|
221
|
+
const changes = {
|
|
222
|
+
modified: new Set(),
|
|
223
|
+
new: new Set()
|
|
224
|
+
};
|
|
225
|
+
try {
|
|
226
|
+
// Get session start time from metadata
|
|
227
|
+
const sessionStartRow = db.prepare(`SELECT value FROM metadata WHERE key = 'current_session_start'`).get();
|
|
228
|
+
if (!sessionStartRow) {
|
|
229
|
+
// No session tracking yet - all files are "unchanged"
|
|
230
|
+
return changes;
|
|
231
|
+
}
|
|
232
|
+
const sessionStart = parseInt(sessionStartRow.value, 10);
|
|
233
|
+
// Find files indexed AFTER session start (not AT session start)
|
|
234
|
+
// This ensures a fresh re-index doesn't mark everything as modified
|
|
235
|
+
const recentlyIndexed = db.prepare(`
|
|
236
|
+
SELECT path, last_indexed,
|
|
237
|
+
(SELECT COUNT(*) FROM lines l WHERE l.file_id = f.id) as line_count
|
|
238
|
+
FROM files f
|
|
239
|
+
WHERE last_indexed > ?
|
|
240
|
+
`).all(sessionStart);
|
|
241
|
+
for (const file of recentlyIndexed) {
|
|
242
|
+
// Heuristic: if file has very few lines, it might be new
|
|
243
|
+
// But we can't really distinguish new vs modified without more metadata
|
|
244
|
+
// For now, mark all recently indexed files as "modified"
|
|
245
|
+
changes.modified.add(file.path);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
// Silently fail
|
|
250
|
+
}
|
|
251
|
+
return changes;
|
|
252
|
+
}
|
|
253
|
+
async function buildTree(db, projectPath, mode, sessionChanges, gitInfo) {
|
|
254
|
+
let files;
|
|
255
|
+
if (mode === 'code') {
|
|
256
|
+
// Only indexed code files (original behavior)
|
|
257
|
+
files = db.prepare(`
|
|
258
|
+
SELECT f.path,
|
|
259
|
+
COUNT(DISTINCT o.item_id) as items,
|
|
260
|
+
(SELECT COUNT(*) FROM methods m WHERE m.file_id = f.id) as methods,
|
|
261
|
+
(SELECT COUNT(*) FROM types t WHERE t.file_id = f.id) as types
|
|
262
|
+
FROM files f
|
|
263
|
+
LEFT JOIN lines l ON l.file_id = f.id
|
|
264
|
+
LEFT JOIN occurrences o ON o.line_id = l.id
|
|
265
|
+
GROUP BY f.id
|
|
266
|
+
ORDER BY f.path
|
|
267
|
+
`).all();
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
// All project files from project_files table
|
|
271
|
+
const projectFiles = db.prepare(`
|
|
272
|
+
SELECT path, type as fileType FROM project_files WHERE type != 'dir' ORDER BY path
|
|
273
|
+
`).all();
|
|
274
|
+
// Get stats for indexed files
|
|
275
|
+
const statsMap = new Map();
|
|
276
|
+
const indexedStats = db.prepare(`
|
|
277
|
+
SELECT f.path,
|
|
278
|
+
COUNT(DISTINCT o.item_id) as items,
|
|
279
|
+
(SELECT COUNT(*) FROM methods m WHERE m.file_id = f.id) as methods,
|
|
280
|
+
(SELECT COUNT(*) FROM types t WHERE t.file_id = f.id) as types
|
|
281
|
+
FROM files f
|
|
282
|
+
LEFT JOIN lines l ON l.file_id = f.id
|
|
283
|
+
LEFT JOIN occurrences o ON o.line_id = l.id
|
|
284
|
+
GROUP BY f.id
|
|
285
|
+
`).all();
|
|
286
|
+
for (const stat of indexedStats) {
|
|
287
|
+
statsMap.set(stat.path, { items: stat.items, methods: stat.methods, types: stat.types });
|
|
288
|
+
}
|
|
289
|
+
files = projectFiles.map(f => ({
|
|
290
|
+
path: f.path,
|
|
291
|
+
fileType: f.fileType,
|
|
292
|
+
items: statsMap.get(f.path)?.items || 0,
|
|
293
|
+
methods: statsMap.get(f.path)?.methods || 0,
|
|
294
|
+
types: statsMap.get(f.path)?.types || 0
|
|
295
|
+
}));
|
|
296
|
+
}
|
|
297
|
+
const root = {
|
|
298
|
+
name: path.basename(projectPath),
|
|
299
|
+
path: '',
|
|
300
|
+
type: 'dir',
|
|
301
|
+
children: []
|
|
302
|
+
};
|
|
303
|
+
for (const file of files) {
|
|
304
|
+
const parts = file.path.split('/');
|
|
305
|
+
let current = root;
|
|
306
|
+
for (let i = 0; i < parts.length; i++) {
|
|
307
|
+
const part = parts[i];
|
|
308
|
+
const isFile = i === parts.length - 1;
|
|
309
|
+
const currentPath = parts.slice(0, i + 1).join('/');
|
|
310
|
+
let child = current.children?.find(c => c.name === part);
|
|
311
|
+
if (!child) {
|
|
312
|
+
child = {
|
|
313
|
+
name: part,
|
|
314
|
+
path: currentPath,
|
|
315
|
+
type: isFile ? 'file' : 'dir',
|
|
316
|
+
fileType: isFile ? file.fileType : undefined,
|
|
317
|
+
children: isFile ? undefined : [],
|
|
318
|
+
stats: isFile ? { items: file.items, methods: file.methods, types: file.types } : undefined,
|
|
319
|
+
status: isFile ? getFileStatus(file.path, sessionChanges) : undefined,
|
|
320
|
+
gitStatus: isFile && gitInfo?.isGitRepo ? getGitFileStatus(file.path, gitInfo) : undefined
|
|
321
|
+
};
|
|
322
|
+
current.children?.push(child);
|
|
323
|
+
}
|
|
324
|
+
current = child;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
// Sort: directories first, then alphabetically
|
|
328
|
+
sortTree(root);
|
|
329
|
+
return root;
|
|
330
|
+
}
|
|
331
|
+
function getFileStatus(filePath, changes) {
|
|
332
|
+
if (changes.modified.has(filePath))
|
|
333
|
+
return 'modified';
|
|
334
|
+
if (changes.new.has(filePath))
|
|
335
|
+
return 'new';
|
|
336
|
+
return 'unchanged';
|
|
337
|
+
}
|
|
338
|
+
function getGitFileStatus(filePath, gitInfo) {
|
|
339
|
+
const status = gitInfo.fileStatuses.get(filePath);
|
|
340
|
+
if (status)
|
|
341
|
+
return status;
|
|
342
|
+
// File is tracked and clean - show as pushed (green) if remote exists, otherwise committed (blue)
|
|
343
|
+
return gitInfo.hasRemote ? 'pushed' : 'committed';
|
|
344
|
+
}
|
|
345
|
+
function sortTree(node) {
|
|
346
|
+
if (node.children) {
|
|
347
|
+
node.children.sort((a, b) => {
|
|
348
|
+
if (a.type !== b.type)
|
|
349
|
+
return a.type === 'dir' ? -1 : 1;
|
|
350
|
+
return a.name.localeCompare(b.name);
|
|
351
|
+
});
|
|
352
|
+
node.children.forEach(sortTree);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
async function getFileSignature(db, filePath) {
|
|
356
|
+
const file = db.prepare(`SELECT id FROM files WHERE path = ?`).get(filePath);
|
|
357
|
+
if (!file) {
|
|
358
|
+
return { error: 'File not found in index' };
|
|
359
|
+
}
|
|
360
|
+
const signature = db.prepare(`SELECT header_comments FROM signatures WHERE file_id = ?`).get(file.id);
|
|
361
|
+
const methods = db.prepare(`
|
|
362
|
+
SELECT prototype, line_number, visibility, is_static, is_async
|
|
363
|
+
FROM methods WHERE file_id = ? ORDER BY line_number
|
|
364
|
+
`).all(file.id);
|
|
365
|
+
const types = db.prepare(`
|
|
366
|
+
SELECT name, kind, line_number
|
|
367
|
+
FROM types WHERE file_id = ? ORDER BY line_number
|
|
368
|
+
`).all(file.id);
|
|
369
|
+
return {
|
|
370
|
+
header: signature?.header_comments || null,
|
|
371
|
+
methods: methods.map(m => ({
|
|
372
|
+
prototype: m.prototype,
|
|
373
|
+
line: m.line_number,
|
|
374
|
+
visibility: m.visibility,
|
|
375
|
+
static: !!m.is_static,
|
|
376
|
+
async: !!m.is_async
|
|
377
|
+
})),
|
|
378
|
+
types: types.map(t => ({
|
|
379
|
+
name: t.name,
|
|
380
|
+
kind: t.kind,
|
|
381
|
+
line: t.line_number
|
|
382
|
+
}))
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Get file content for the Code tab
|
|
387
|
+
*/
|
|
388
|
+
function getFileContent(projectRoot, filePath) {
|
|
389
|
+
const fullPath = path.join(projectRoot, filePath);
|
|
390
|
+
if (!existsSync(fullPath)) {
|
|
391
|
+
return { error: 'File not found' };
|
|
392
|
+
}
|
|
393
|
+
try {
|
|
394
|
+
const content = readFileSync(fullPath, 'utf-8');
|
|
395
|
+
const language = getLanguageFromExtension(filePath);
|
|
396
|
+
return { content, language };
|
|
397
|
+
}
|
|
398
|
+
catch (err) {
|
|
399
|
+
return { error: `Failed to read file: ${err}` };
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Map file extension to highlight.js language identifier
|
|
404
|
+
*/
|
|
405
|
+
function getLanguageFromExtension(filePath) {
|
|
406
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
407
|
+
const langMap = {
|
|
408
|
+
'.ts': 'typescript',
|
|
409
|
+
'.tsx': 'typescript',
|
|
410
|
+
'.js': 'javascript',
|
|
411
|
+
'.jsx': 'javascript',
|
|
412
|
+
'.mjs': 'javascript',
|
|
413
|
+
'.cjs': 'javascript',
|
|
414
|
+
'.cs': 'csharp',
|
|
415
|
+
'.rs': 'rust',
|
|
416
|
+
'.py': 'python',
|
|
417
|
+
'.pyw': 'python',
|
|
418
|
+
'.c': 'c',
|
|
419
|
+
'.h': 'c',
|
|
420
|
+
'.cpp': 'cpp',
|
|
421
|
+
'.cc': 'cpp',
|
|
422
|
+
'.cxx': 'cpp',
|
|
423
|
+
'.hpp': 'cpp',
|
|
424
|
+
'.hxx': 'cpp',
|
|
425
|
+
'.java': 'java',
|
|
426
|
+
'.go': 'go',
|
|
427
|
+
'.php': 'php',
|
|
428
|
+
'.rb': 'ruby',
|
|
429
|
+
'.rake': 'ruby',
|
|
430
|
+
'.json': 'json',
|
|
431
|
+
'.xml': 'xml',
|
|
432
|
+
'.html': 'html',
|
|
433
|
+
'.htm': 'html',
|
|
434
|
+
'.css': 'css',
|
|
435
|
+
'.scss': 'scss',
|
|
436
|
+
'.less': 'less',
|
|
437
|
+
'.yaml': 'yaml',
|
|
438
|
+
'.yml': 'yaml',
|
|
439
|
+
'.md': 'markdown',
|
|
440
|
+
'.sql': 'sql',
|
|
441
|
+
'.sh': 'bash',
|
|
442
|
+
'.bash': 'bash',
|
|
443
|
+
'.bat': 'batch',
|
|
444
|
+
'.ps1': 'powershell',
|
|
445
|
+
'.toml': 'toml',
|
|
446
|
+
'.ini': 'ini',
|
|
447
|
+
'.cfg': 'ini'
|
|
448
|
+
};
|
|
449
|
+
return langMap[ext] || 'plaintext';
|
|
450
|
+
}
|
|
451
|
+
function getViewerHTML(projectPath) {
|
|
452
|
+
const projectName = path.basename(projectPath);
|
|
453
|
+
return `<!DOCTYPE html>
|
|
454
|
+
<html lang="en">
|
|
455
|
+
<head>
|
|
456
|
+
<meta charset="UTF-8">
|
|
457
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
458
|
+
<title>${PRODUCT_NAME} Viewer - ${projectName}</title>
|
|
459
|
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.0/styles/tokyo-night-dark.min.css">
|
|
460
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.0/highlight.min.js"></script>
|
|
461
|
+
<style>
|
|
462
|
+
:root {
|
|
463
|
+
--bg-primary: #1a1b26;
|
|
464
|
+
--bg-secondary: #24283b;
|
|
465
|
+
--bg-tertiary: #414868;
|
|
466
|
+
--text-primary: #c0caf5;
|
|
467
|
+
--text-secondary: #a9b1d6;
|
|
468
|
+
--text-muted: #565f89;
|
|
469
|
+
--accent: #7aa2f7;
|
|
470
|
+
--accent-green: #9ece6a;
|
|
471
|
+
--accent-orange: #ff9e64;
|
|
472
|
+
--accent-purple: #bb9af7;
|
|
473
|
+
--accent-cyan: #7dcfff;
|
|
474
|
+
--accent-yellow: #e0af68;
|
|
475
|
+
--accent-red: #f7768e;
|
|
476
|
+
--border: #3b4261;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
480
|
+
|
|
481
|
+
body {
|
|
482
|
+
font-family: 'Segoe UI', system-ui, sans-serif;
|
|
483
|
+
background: var(--bg-primary);
|
|
484
|
+
color: var(--text-primary);
|
|
485
|
+
height: 100vh;
|
|
486
|
+
display: flex;
|
|
487
|
+
flex-direction: column;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
header {
|
|
491
|
+
background: var(--bg-secondary);
|
|
492
|
+
padding: 12px 20px;
|
|
493
|
+
border-bottom: 1px solid var(--border);
|
|
494
|
+
display: flex;
|
|
495
|
+
align-items: center;
|
|
496
|
+
gap: 15px;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
header h1 {
|
|
500
|
+
font-size: 1.3em;
|
|
501
|
+
color: var(--accent);
|
|
502
|
+
font-weight: 500;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
header .project-name {
|
|
506
|
+
color: var(--accent-purple);
|
|
507
|
+
font-weight: 600;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
.container {
|
|
511
|
+
display: flex;
|
|
512
|
+
flex: 1;
|
|
513
|
+
overflow: hidden;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/* Splitter */
|
|
517
|
+
.splitter {
|
|
518
|
+
width: 6px;
|
|
519
|
+
background: var(--bg-tertiary);
|
|
520
|
+
cursor: col-resize;
|
|
521
|
+
transition: background 0.2s;
|
|
522
|
+
flex-shrink: 0;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
.splitter:hover, .splitter.dragging {
|
|
526
|
+
background: var(--accent);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/* Panel styles */
|
|
530
|
+
.panel {
|
|
531
|
+
display: flex;
|
|
532
|
+
flex-direction: column;
|
|
533
|
+
overflow: hidden;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
.tree-panel {
|
|
537
|
+
width: 350px;
|
|
538
|
+
background: var(--bg-secondary);
|
|
539
|
+
border-right: 1px solid var(--border);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
.detail-panel {
|
|
543
|
+
flex: 1;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/* Tab bar styles */
|
|
547
|
+
.tab-bar {
|
|
548
|
+
display: flex;
|
|
549
|
+
background: var(--bg-tertiary);
|
|
550
|
+
border-bottom: 1px solid var(--border);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
.tab {
|
|
554
|
+
padding: 10px 20px;
|
|
555
|
+
cursor: pointer;
|
|
556
|
+
color: var(--text-muted);
|
|
557
|
+
border-bottom: 2px solid transparent;
|
|
558
|
+
transition: all 0.2s;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
.tab:hover {
|
|
562
|
+
color: var(--text-secondary);
|
|
563
|
+
background: rgba(122, 162, 247, 0.1);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
.tab.active {
|
|
567
|
+
color: var(--accent);
|
|
568
|
+
border-bottom-color: var(--accent);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
.panel-content {
|
|
572
|
+
flex: 1;
|
|
573
|
+
overflow-y: auto;
|
|
574
|
+
padding: 10px 0;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
.detail-panel .panel-content {
|
|
578
|
+
padding: 20px;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/* Tree styles */
|
|
582
|
+
.tree-node {
|
|
583
|
+
padding: 6px 10px 6px 0;
|
|
584
|
+
cursor: pointer;
|
|
585
|
+
display: flex;
|
|
586
|
+
align-items: center;
|
|
587
|
+
gap: 6px;
|
|
588
|
+
white-space: nowrap;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
.tree-node:hover {
|
|
592
|
+
background: rgba(122, 162, 247, 0.1);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
.tree-node.selected {
|
|
596
|
+
background: rgba(122, 162, 247, 0.2);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
.tree-node .status-icon {
|
|
600
|
+
width: 16px;
|
|
601
|
+
font-size: 11px;
|
|
602
|
+
text-align: center;
|
|
603
|
+
flex-shrink: 0;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
.tree-node .status-icon.modified {
|
|
607
|
+
color: var(--accent-orange);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
.tree-node .status-icon.new {
|
|
611
|
+
color: var(--accent-green);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
.tree-node .status-icon.unchanged {
|
|
615
|
+
color: var(--accent-green);
|
|
616
|
+
opacity: 0.7;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/* Git status cat icon */
|
|
620
|
+
.tree-node .git-cat {
|
|
621
|
+
width: 16px;
|
|
622
|
+
height: 16px;
|
|
623
|
+
flex-shrink: 0;
|
|
624
|
+
display: flex;
|
|
625
|
+
align-items: center;
|
|
626
|
+
justify-content: center;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
.tree-node .git-cat svg {
|
|
630
|
+
width: 14px;
|
|
631
|
+
height: 14px;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
.tree-node .git-cat.untracked svg { fill: #6b7280; }
|
|
635
|
+
.tree-node .git-cat.modified svg { fill: #f59e0b; }
|
|
636
|
+
.tree-node .git-cat.committed svg { fill: #3b82f6; }
|
|
637
|
+
.tree-node .git-cat.pushed svg { fill: #22c55e; }
|
|
638
|
+
|
|
639
|
+
.tree-node .icon {
|
|
640
|
+
width: 18px;
|
|
641
|
+
text-align: center;
|
|
642
|
+
flex-shrink: 0;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
.tree-node .name {
|
|
646
|
+
flex: 1;
|
|
647
|
+
overflow: hidden;
|
|
648
|
+
text-overflow: ellipsis;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
.tree-node .stats {
|
|
652
|
+
font-size: 0.75em;
|
|
653
|
+
color: var(--text-muted);
|
|
654
|
+
margin-left: auto;
|
|
655
|
+
padding-right: 10px;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
.tree-node.dir .icon { color: var(--accent-yellow); }
|
|
659
|
+
.tree-node.file .icon { color: var(--accent-cyan); }
|
|
660
|
+
.tree-node.file.config .icon { color: var(--accent-purple); }
|
|
661
|
+
.tree-node.file.doc .icon { color: var(--accent-green); }
|
|
662
|
+
.tree-node.file.test .icon { color: var(--accent-orange); }
|
|
663
|
+
|
|
664
|
+
.tree-children {
|
|
665
|
+
margin-left: 20px;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
.tree-children.collapsed {
|
|
669
|
+
display: none;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/* Detail panel styles */
|
|
673
|
+
.detail-panel h2 {
|
|
674
|
+
color: var(--accent-purple);
|
|
675
|
+
font-size: 1.2em;
|
|
676
|
+
margin-bottom: 15px;
|
|
677
|
+
padding-bottom: 10px;
|
|
678
|
+
border-bottom: 1px solid var(--border);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
.detail-panel .file-path {
|
|
682
|
+
color: var(--text-muted);
|
|
683
|
+
font-size: 0.9em;
|
|
684
|
+
margin-bottom: 20px;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
.section {
|
|
688
|
+
margin-bottom: 25px;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
.section h3 {
|
|
692
|
+
color: var(--accent-cyan);
|
|
693
|
+
font-size: 1em;
|
|
694
|
+
margin-bottom: 10px;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
.header-comment {
|
|
698
|
+
background: var(--bg-secondary);
|
|
699
|
+
padding: 15px;
|
|
700
|
+
border-radius: 6px;
|
|
701
|
+
font-family: 'Consolas', 'Fira Code', monospace;
|
|
702
|
+
font-size: 0.9em;
|
|
703
|
+
white-space: pre-wrap;
|
|
704
|
+
color: var(--accent-green);
|
|
705
|
+
border-left: 3px solid var(--accent-green);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
.method-list, .type-list {
|
|
709
|
+
list-style: none;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
.method-list li, .type-list li {
|
|
713
|
+
padding: 8px 12px;
|
|
714
|
+
background: var(--bg-secondary);
|
|
715
|
+
margin-bottom: 6px;
|
|
716
|
+
border-radius: 4px;
|
|
717
|
+
font-family: 'Consolas', monospace;
|
|
718
|
+
font-size: 0.85em;
|
|
719
|
+
display: flex;
|
|
720
|
+
align-items: center;
|
|
721
|
+
gap: 10px;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
.method-list .line-num, .type-list .line-num {
|
|
725
|
+
color: var(--text-muted);
|
|
726
|
+
font-size: 0.8em;
|
|
727
|
+
min-width: 40px;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
.method-list .visibility {
|
|
731
|
+
color: var(--accent-purple);
|
|
732
|
+
font-size: 0.75em;
|
|
733
|
+
padding: 2px 6px;
|
|
734
|
+
background: rgba(187, 154, 247, 0.15);
|
|
735
|
+
border-radius: 3px;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
.method-list .modifier {
|
|
739
|
+
color: var(--accent-orange);
|
|
740
|
+
font-size: 0.75em;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
.type-list .kind {
|
|
744
|
+
color: var(--accent-yellow);
|
|
745
|
+
font-size: 0.75em;
|
|
746
|
+
padding: 2px 6px;
|
|
747
|
+
background: rgba(224, 175, 104, 0.15);
|
|
748
|
+
border-radius: 3px;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
.empty-state {
|
|
752
|
+
color: var(--text-muted);
|
|
753
|
+
text-align: center;
|
|
754
|
+
padding: 40px;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
.loading {
|
|
758
|
+
color: var(--text-muted);
|
|
759
|
+
padding: 20px;
|
|
760
|
+
text-align: center;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
/* Code view styles */
|
|
764
|
+
.code-view {
|
|
765
|
+
background: var(--bg-secondary);
|
|
766
|
+
border-radius: 6px;
|
|
767
|
+
overflow: hidden;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
.code-view pre {
|
|
771
|
+
margin: 0;
|
|
772
|
+
padding: 15px;
|
|
773
|
+
overflow-x: auto;
|
|
774
|
+
font-size: 0.85em;
|
|
775
|
+
line-height: 1.5;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
.code-view code {
|
|
779
|
+
font-family: 'Consolas', 'Fira Code', monospace;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/* Override highlight.js background to match our theme */
|
|
783
|
+
.hljs {
|
|
784
|
+
background: var(--bg-secondary) !important;
|
|
785
|
+
}
|
|
786
|
+
</style>
|
|
787
|
+
</head>
|
|
788
|
+
<body>
|
|
789
|
+
<header>
|
|
790
|
+
<h1>${PRODUCT_NAME} Viewer</h1>
|
|
791
|
+
<span class="project-name">${projectName}</span>
|
|
792
|
+
</header>
|
|
793
|
+
|
|
794
|
+
<div class="container">
|
|
795
|
+
<div class="panel tree-panel" id="treePanel">
|
|
796
|
+
<div class="tab-bar">
|
|
797
|
+
<div class="tab active" data-tab="code">Code</div>
|
|
798
|
+
<div class="tab" data-tab="all">All</div>
|
|
799
|
+
</div>
|
|
800
|
+
<div class="panel-content" id="tree">
|
|
801
|
+
<div class="loading">Loading project tree...</div>
|
|
802
|
+
</div>
|
|
803
|
+
</div>
|
|
804
|
+
<div class="splitter" id="splitter"></div>
|
|
805
|
+
<div class="panel detail-panel">
|
|
806
|
+
<div class="tab-bar">
|
|
807
|
+
<div class="tab active" data-tab="overview">Overview</div>
|
|
808
|
+
<div class="tab" data-tab="source">Code</div>
|
|
809
|
+
</div>
|
|
810
|
+
<div class="panel-content" id="detail">
|
|
811
|
+
<div class="empty-state">
|
|
812
|
+
<p>Click on a file to view its signature</p>
|
|
813
|
+
</div>
|
|
814
|
+
</div>
|
|
815
|
+
</div>
|
|
816
|
+
</div>
|
|
817
|
+
|
|
818
|
+
<script>
|
|
819
|
+
const ws = new WebSocket('ws://localhost:${PORT}');
|
|
820
|
+
let selectedNode = null;
|
|
821
|
+
let currentFile = null;
|
|
822
|
+
let currentTreeMode = 'code';
|
|
823
|
+
let currentDetailTab = 'overview';
|
|
824
|
+
let cachedSignature = null;
|
|
825
|
+
let cachedContent = null;
|
|
826
|
+
|
|
827
|
+
ws.onopen = () => {
|
|
828
|
+
console.log('Connected to AiDex Viewer');
|
|
829
|
+
};
|
|
830
|
+
|
|
831
|
+
let cachedCodeTree = null;
|
|
832
|
+
let cachedAllTree = null;
|
|
833
|
+
|
|
834
|
+
ws.onmessage = (event) => {
|
|
835
|
+
const msg = JSON.parse(event.data);
|
|
836
|
+
console.log('📨 Received:', msg.type, msg);
|
|
837
|
+
|
|
838
|
+
if (msg.type === 'tree') {
|
|
839
|
+
// Cache the tree for the mode
|
|
840
|
+
if (msg.mode === 'code') cachedCodeTree = msg.data;
|
|
841
|
+
else cachedAllTree = msg.data;
|
|
842
|
+
renderTree(msg.data);
|
|
843
|
+
} else if (msg.type === 'refresh') {
|
|
844
|
+
// Live reload: update cached trees and re-render current mode
|
|
845
|
+
console.log('🔄 Live reload triggered');
|
|
846
|
+
cachedCodeTree = msg.codeTree;
|
|
847
|
+
cachedAllTree = msg.allTree;
|
|
848
|
+
const treeToRender = currentTreeMode === 'code' ? cachedCodeTree : cachedAllTree;
|
|
849
|
+
if (treeToRender) renderTree(treeToRender);
|
|
850
|
+
} else if (msg.type === 'signature') {
|
|
851
|
+
cachedSignature = { file: msg.file, data: msg.data };
|
|
852
|
+
if (currentDetailTab === 'overview') {
|
|
853
|
+
renderSignature(msg.file, msg.data);
|
|
854
|
+
}
|
|
855
|
+
} else if (msg.type === 'fileContent') {
|
|
856
|
+
cachedContent = { file: msg.file, data: msg.data };
|
|
857
|
+
if (currentDetailTab === 'source') {
|
|
858
|
+
renderFileContent(msg.file, msg.data);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
};
|
|
862
|
+
|
|
863
|
+
// Tab switching - Tree panel
|
|
864
|
+
document.querySelectorAll('.tree-panel .tab').forEach(tab => {
|
|
865
|
+
tab.addEventListener('click', () => {
|
|
866
|
+
document.querySelectorAll('.tree-panel .tab').forEach(t => t.classList.remove('active'));
|
|
867
|
+
tab.classList.add('active');
|
|
868
|
+
currentTreeMode = tab.dataset.tab;
|
|
869
|
+
document.getElementById('tree').innerHTML = '<div class="loading">Loading...</div>';
|
|
870
|
+
ws.send(JSON.stringify({ type: 'getTree', mode: currentTreeMode }));
|
|
871
|
+
});
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
// Tab switching - Detail panel
|
|
875
|
+
document.querySelectorAll('.detail-panel .tab').forEach(tab => {
|
|
876
|
+
tab.addEventListener('click', () => {
|
|
877
|
+
if (!currentFile) return;
|
|
878
|
+
|
|
879
|
+
document.querySelectorAll('.detail-panel .tab').forEach(t => t.classList.remove('active'));
|
|
880
|
+
tab.classList.add('active');
|
|
881
|
+
currentDetailTab = tab.dataset.tab;
|
|
882
|
+
|
|
883
|
+
if (currentDetailTab === 'overview') {
|
|
884
|
+
if (cachedSignature && cachedSignature.file === currentFile) {
|
|
885
|
+
renderSignature(cachedSignature.file, cachedSignature.data);
|
|
886
|
+
} else {
|
|
887
|
+
ws.send(JSON.stringify({ type: 'getSignature', file: currentFile }));
|
|
888
|
+
}
|
|
889
|
+
} else if (currentDetailTab === 'source') {
|
|
890
|
+
if (cachedContent && cachedContent.file === currentFile) {
|
|
891
|
+
renderFileContent(cachedContent.file, cachedContent.data);
|
|
892
|
+
} else {
|
|
893
|
+
document.getElementById('detail').innerHTML = '<div class="loading">Loading source...</div>';
|
|
894
|
+
ws.send(JSON.stringify({ type: 'getFileContent', file: currentFile }));
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
});
|
|
898
|
+
});
|
|
899
|
+
|
|
900
|
+
function renderTree(node, container = document.getElementById('tree'), depth = 0) {
|
|
901
|
+
if (depth === 0) {
|
|
902
|
+
container.innerHTML = '';
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
const div = document.createElement('div');
|
|
906
|
+
div.className = 'tree-node ' + node.type + (node.fileType ? ' ' + node.fileType : '');
|
|
907
|
+
div.style.paddingLeft = (depth * 20 + 10) + 'px';
|
|
908
|
+
div.dataset.path = node.path;
|
|
909
|
+
div.dataset.type = node.type;
|
|
910
|
+
|
|
911
|
+
// Status icon (modified/new/unchanged)
|
|
912
|
+
const statusIcon = document.createElement('span');
|
|
913
|
+
statusIcon.className = 'status-icon';
|
|
914
|
+
if (node.status === 'modified') {
|
|
915
|
+
statusIcon.className += ' modified';
|
|
916
|
+
statusIcon.textContent = '✏️';
|
|
917
|
+
statusIcon.title = 'Modified in this session';
|
|
918
|
+
} else if (node.status === 'new') {
|
|
919
|
+
statusIcon.className += ' new';
|
|
920
|
+
statusIcon.textContent = '➕';
|
|
921
|
+
statusIcon.title = 'New in this session';
|
|
922
|
+
} else if (node.status === 'unchanged') {
|
|
923
|
+
statusIcon.className += ' unchanged';
|
|
924
|
+
statusIcon.textContent = '✓';
|
|
925
|
+
statusIcon.title = 'Unchanged';
|
|
926
|
+
}
|
|
927
|
+
div.appendChild(statusIcon);
|
|
928
|
+
|
|
929
|
+
// Git status cat icon (only for files in git repos)
|
|
930
|
+
if (node.gitStatus) {
|
|
931
|
+
const gitCat = document.createElement('span');
|
|
932
|
+
gitCat.className = 'git-cat ' + node.gitStatus;
|
|
933
|
+
// Cat silhouette SVG - simple sitting cat with raised paw
|
|
934
|
+
gitCat.innerHTML = '<svg viewBox="0 0 24 24"><path d="M12,8L10.67,8.09C9.81,7.07 7.4,4.5 5,4.5C5,4.5 3.03,7.46 4.96,11.41C4.41,12.24 4.07,12.67 4,13.66L2.07,13.95L2.28,14.93L4.04,14.67L4.18,15.38L2.61,16.32L3.08,17.21L4.53,16.32C5.68,18.76 8.59,20 12,20C15.41,20 18.32,18.76 19.47,16.32L20.92,17.21L21.39,16.32L19.82,15.38L19.96,14.67L21.72,14.93L21.93,13.95L20,13.66C19.93,12.67 19.59,12.24 19.04,11.41C20.97,7.46 19,4.5 19,4.5C16.6,4.5 14.19,7.07 13.33,8.09L12,8M9,11A1,1 0 0,1 10,12A1,1 0 0,1 9,13A1,1 0 0,1 8,12A1,1 0 0,1 9,11M15,11A1,1 0 0,1 16,12A1,1 0 0,1 15,13A1,1 0 0,1 14,12A1,1 0 0,1 15,11M11,14H13V16H11V14Z"/></svg>';
|
|
935
|
+
const gitTitles = {
|
|
936
|
+
'untracked': 'Untracked - not in git',
|
|
937
|
+
'modified': 'Modified - not committed',
|
|
938
|
+
'committed': 'Committed - not pushed',
|
|
939
|
+
'pushed': 'Pushed - in sync with remote'
|
|
940
|
+
};
|
|
941
|
+
gitCat.title = gitTitles[node.gitStatus] || node.gitStatus;
|
|
942
|
+
div.appendChild(gitCat);
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
// File/folder icon
|
|
946
|
+
const icon = document.createElement('span');
|
|
947
|
+
icon.className = 'icon';
|
|
948
|
+
if (node.type === 'dir') {
|
|
949
|
+
icon.textContent = '📁';
|
|
950
|
+
} else {
|
|
951
|
+
// Different icons for different file types
|
|
952
|
+
const iconMap = {
|
|
953
|
+
'code': '📄',
|
|
954
|
+
'config': '⚙️',
|
|
955
|
+
'doc': '📝',
|
|
956
|
+
'test': '🧪',
|
|
957
|
+
'asset': '🖼️',
|
|
958
|
+
'other': '📄'
|
|
959
|
+
};
|
|
960
|
+
icon.textContent = iconMap[node.fileType] || '📄';
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
const name = document.createElement('span');
|
|
964
|
+
name.className = 'name';
|
|
965
|
+
name.textContent = node.name;
|
|
966
|
+
|
|
967
|
+
div.appendChild(icon);
|
|
968
|
+
div.appendChild(name);
|
|
969
|
+
|
|
970
|
+
if (node.stats && (node.stats.methods > 0 || node.stats.types > 0)) {
|
|
971
|
+
const stats = document.createElement('span');
|
|
972
|
+
stats.className = 'stats';
|
|
973
|
+
stats.textContent = node.stats.methods + 'm ' + node.stats.types + 't';
|
|
974
|
+
div.appendChild(stats);
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
div.onclick = (e) => {
|
|
978
|
+
e.stopPropagation();
|
|
979
|
+
|
|
980
|
+
if (node.type === 'dir') {
|
|
981
|
+
const children = div.nextElementSibling;
|
|
982
|
+
if (children && children.classList.contains('tree-children')) {
|
|
983
|
+
children.classList.toggle('collapsed');
|
|
984
|
+
icon.textContent = children.classList.contains('collapsed') ? '📁' : '📂';
|
|
985
|
+
}
|
|
986
|
+
} else {
|
|
987
|
+
if (selectedNode) selectedNode.classList.remove('selected');
|
|
988
|
+
div.classList.add('selected');
|
|
989
|
+
selectedNode = div;
|
|
990
|
+
currentFile = node.path;
|
|
991
|
+
cachedSignature = null;
|
|
992
|
+
cachedContent = null;
|
|
993
|
+
|
|
994
|
+
// Reset to overview tab
|
|
995
|
+
currentDetailTab = 'overview';
|
|
996
|
+
document.querySelectorAll('.detail-panel .tab').forEach(t => t.classList.remove('active'));
|
|
997
|
+
document.querySelector('.detail-panel .tab[data-tab="overview"]').classList.add('active');
|
|
998
|
+
|
|
999
|
+
ws.send(JSON.stringify({ type: 'getSignature', file: node.path }));
|
|
1000
|
+
}
|
|
1001
|
+
};
|
|
1002
|
+
|
|
1003
|
+
container.appendChild(div);
|
|
1004
|
+
|
|
1005
|
+
if (node.children && node.children.length > 0) {
|
|
1006
|
+
const childContainer = document.createElement('div');
|
|
1007
|
+
childContainer.className = 'tree-children';
|
|
1008
|
+
container.appendChild(childContainer);
|
|
1009
|
+
|
|
1010
|
+
for (const child of node.children) {
|
|
1011
|
+
renderTree(child, childContainer, depth + 1);
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
function renderSignature(filePath, data) {
|
|
1017
|
+
const detail = document.getElementById('detail');
|
|
1018
|
+
|
|
1019
|
+
if (data.error) {
|
|
1020
|
+
detail.innerHTML = '<div class="empty-state">' + data.error + '</div>';
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
let html = '<h2>' + filePath.split('/').pop() + '</h2>';
|
|
1025
|
+
html += '<div class="file-path">' + filePath + '</div>';
|
|
1026
|
+
|
|
1027
|
+
if (data.header) {
|
|
1028
|
+
html += '<div class="section"><h3>Header Comments</h3>';
|
|
1029
|
+
html += '<div class="header-comment">' + escapeHtml(data.header) + '</div></div>';
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
if (data.types && data.types.length > 0) {
|
|
1033
|
+
html += '<div class="section"><h3>Types (' + data.types.length + ')</h3>';
|
|
1034
|
+
html += '<ul class="type-list">';
|
|
1035
|
+
for (const t of data.types) {
|
|
1036
|
+
html += '<li><span class="line-num">:' + t.line + '</span>';
|
|
1037
|
+
html += '<span class="kind">' + t.kind + '</span>';
|
|
1038
|
+
html += '<span>' + escapeHtml(t.name) + '</span></li>';
|
|
1039
|
+
}
|
|
1040
|
+
html += '</ul></div>';
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
if (data.methods && data.methods.length > 0) {
|
|
1044
|
+
html += '<div class="section"><h3>Methods (' + data.methods.length + ')</h3>';
|
|
1045
|
+
html += '<ul class="method-list">';
|
|
1046
|
+
for (const m of data.methods) {
|
|
1047
|
+
html += '<li><span class="line-num">:' + m.line + '</span>';
|
|
1048
|
+
if (m.visibility) html += '<span class="visibility">' + m.visibility + '</span>';
|
|
1049
|
+
if (m.static) html += '<span class="modifier">static</span>';
|
|
1050
|
+
if (m.async) html += '<span class="modifier">async</span>';
|
|
1051
|
+
html += '<span>' + escapeHtml(m.prototype) + '</span></li>';
|
|
1052
|
+
}
|
|
1053
|
+
html += '</ul></div>';
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
if (!data.header && (!data.types || data.types.length === 0) && (!data.methods || data.methods.length === 0)) {
|
|
1057
|
+
html += '<div class="empty-state">No signature data for this file</div>';
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
detail.innerHTML = html;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
function renderFileContent(filePath, data) {
|
|
1064
|
+
const detail = document.getElementById('detail');
|
|
1065
|
+
|
|
1066
|
+
if (data.error) {
|
|
1067
|
+
detail.innerHTML = '<div class="empty-state">' + data.error + '</div>';
|
|
1068
|
+
return;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
let html = '<h2>' + filePath.split('/').pop() + '</h2>';
|
|
1072
|
+
html += '<div class="file-path">' + filePath + '</div>';
|
|
1073
|
+
html += '<div class="code-view"><pre><code class="language-' + data.language + '">' + escapeHtml(data.content) + '</code></pre></div>';
|
|
1074
|
+
|
|
1075
|
+
detail.innerHTML = html;
|
|
1076
|
+
|
|
1077
|
+
// Apply syntax highlighting
|
|
1078
|
+
detail.querySelectorAll('pre code').forEach((block) => {
|
|
1079
|
+
hljs.highlightElement(block);
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
function escapeHtml(text) {
|
|
1084
|
+
const div = document.createElement('div');
|
|
1085
|
+
div.textContent = text;
|
|
1086
|
+
return div.innerHTML;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
// Splitter functionality
|
|
1090
|
+
const splitter = document.getElementById('splitter');
|
|
1091
|
+
const treePanel = document.getElementById('treePanel');
|
|
1092
|
+
let isDragging = false;
|
|
1093
|
+
|
|
1094
|
+
splitter.addEventListener('mousedown', (e) => {
|
|
1095
|
+
isDragging = true;
|
|
1096
|
+
splitter.classList.add('dragging');
|
|
1097
|
+
document.body.style.cursor = 'col-resize';
|
|
1098
|
+
document.body.style.userSelect = 'none';
|
|
1099
|
+
});
|
|
1100
|
+
|
|
1101
|
+
document.addEventListener('mousemove', (e) => {
|
|
1102
|
+
if (!isDragging) return;
|
|
1103
|
+
const containerRect = document.querySelector('.container').getBoundingClientRect();
|
|
1104
|
+
const newWidth = e.clientX - containerRect.left;
|
|
1105
|
+
if (newWidth >= 200 && newWidth <= 800) {
|
|
1106
|
+
treePanel.style.width = newWidth + 'px';
|
|
1107
|
+
}
|
|
1108
|
+
});
|
|
1109
|
+
|
|
1110
|
+
document.addEventListener('mouseup', () => {
|
|
1111
|
+
if (isDragging) {
|
|
1112
|
+
isDragging = false;
|
|
1113
|
+
splitter.classList.remove('dragging');
|
|
1114
|
+
document.body.style.cursor = '';
|
|
1115
|
+
document.body.style.userSelect = '';
|
|
1116
|
+
}
|
|
1117
|
+
});
|
|
1118
|
+
</script>
|
|
1119
|
+
</body>
|
|
1120
|
+
</html>`;
|
|
1121
|
+
}
|
|
1122
|
+
//# sourceMappingURL=server.js.map
|