gitmaps 1.0.0 → 1.1.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 +265 -122
- package/app/[...slug]/page.client.tsx +1 -0
- package/app/[...slug]/page.tsx +6 -0
- package/app/[owner]/[repo]/page.client.tsx +5 -0
- package/app/[slug]/page.client.tsx +5 -0
- package/app/analytics.db +0 -0
- package/app/api/analytics/route.ts +64 -0
- package/app/api/auth/positions/route.ts +95 -33
- package/app/api/build-info/route.ts +19 -0
- package/app/api/chat/route.ts +13 -2
- package/app/api/manifest.json/route.ts +20 -0
- package/app/api/og-image/route.ts +14 -0
- package/app/api/pwa-icon/route.ts +14 -0
- package/app/api/repo/clone-stream/route.ts +20 -12
- package/app/api/repo/file-content/route.ts +73 -20
- package/app/api/repo/imports/route.ts +21 -3
- package/app/api/repo/list/route.ts +30 -0
- package/app/api/repo/load/route.test.ts +62 -0
- package/app/api/repo/load/route.ts +41 -1
- package/app/api/repo/pdf-thumb/route.ts +127 -0
- package/app/api/repo/resolve-slug/route.ts +51 -0
- package/app/api/repo/tree/route.ts +188 -104
- package/app/api/repo/upload/route.ts +6 -9
- package/app/api/sw.js/route.ts +70 -0
- package/app/api/version/route.ts +26 -0
- package/app/galaxy-canvas/page.client.tsx +2 -0
- package/app/galaxy-canvas/page.tsx +5 -0
- package/app/globals.css +5844 -4694
- package/app/icon.png +0 -0
- package/app/layout.tsx +1284 -467
- package/app/lib/auto-arrange.test.ts +158 -0
- package/app/lib/auto-arrange.ts +147 -0
- package/app/lib/canvas-export.ts +358 -358
- package/app/lib/canvas-text.ts +4 -72
- package/app/lib/canvas.ts +625 -564
- package/app/lib/card-arrangement.ts +21 -7
- package/app/lib/card-context-menu.tsx +2 -2
- package/app/lib/card-groups.ts +9 -2
- package/app/lib/cards.tsx +1361 -914
- package/app/lib/chat.tsx +65 -9
- package/app/lib/code-editor.ts +86 -2
- package/app/lib/connections.tsx +34 -43
- package/app/lib/context.test.ts +32 -0
- package/app/lib/context.ts +19 -3
- package/app/lib/cursor-sharing.ts +34 -0
- package/app/lib/events.tsx +76 -73
- package/app/lib/export-canvas.ts +287 -0
- package/app/lib/file-card-plugin.ts +148 -134
- package/app/lib/file-modal.tsx +49 -0
- package/app/lib/file-preview.ts +486 -400
- package/app/lib/github-import.test.ts +424 -0
- package/app/lib/global-search.ts +48 -27
- package/app/lib/initial-route-hydration.test.ts +283 -0
- package/app/lib/initial-route-hydration.ts +202 -0
- package/app/lib/landing-reset.test.ts +99 -0
- package/app/lib/landing-reset.ts +106 -0
- package/app/lib/landing-shell.test.ts +75 -0
- package/app/lib/large-repo-optimization.ts +37 -0
- package/app/lib/layers.tsx +17 -18
- package/app/lib/layout-snapshots.ts +320 -0
- package/app/lib/loading.test.ts +69 -0
- package/app/lib/loading.tsx +160 -45
- package/app/lib/mount-cleanup.test.ts +52 -0
- package/app/lib/mount-cleanup.ts +34 -0
- package/app/lib/mount-init.test.ts +123 -0
- package/app/lib/mount-init.ts +107 -0
- package/app/lib/mount-lifecycle.test.ts +39 -0
- package/app/lib/mount-lifecycle.ts +12 -0
- package/app/lib/mount-route-wiring.test.ts +87 -0
- package/app/lib/mount-route-wiring.ts +84 -0
- package/app/lib/multi-repo.ts +14 -0
- package/app/lib/onboarding-tutorial.ts +278 -0
- package/app/lib/perf-overlay.ts +78 -0
- package/app/lib/positions.ts +191 -122
- package/app/lib/recent-commits.test.ts +869 -0
- package/app/lib/recent-commits.ts +227 -0
- package/app/lib/repo-handoff.test.ts +23 -0
- package/app/lib/repo-handoff.ts +16 -0
- package/app/lib/repo-progressive.ts +119 -0
- package/app/lib/repo-select.test.ts +61 -0
- package/app/lib/repo-select.ts +74 -0
- package/app/lib/repo.tsx +1383 -977
- package/app/lib/role.ts +228 -0
- package/app/lib/route-catchall.test.ts +27 -0
- package/app/lib/route-repo-entry.test.ts +95 -0
- package/app/lib/route-repo-entry.ts +36 -0
- package/app/lib/router-contract.test.ts +22 -0
- package/app/lib/router-contract.ts +19 -0
- package/app/lib/shared-layout.test.ts +86 -0
- package/app/lib/shared-layout.ts +82 -0
- package/app/lib/shortcuts-panel.ts +2 -0
- package/app/lib/status-bar.test.ts +118 -0
- package/app/lib/status-bar.ts +365 -128
- package/app/lib/sync-controls.test.ts +43 -0
- package/app/lib/sync-controls.tsx +303 -0
- package/app/lib/test-dom.ts +145 -0
- package/app/lib/test-fixtures/router-contract/[...slug]/page.tsx +3 -0
- package/app/lib/test-fixtures/router-contract/api/health/route.ts +3 -0
- package/app/lib/test-fixtures/router-contract/api/version/route.ts +3 -0
- package/app/lib/test-fixtures/router-contract/galaxy-canvas/page.tsx +3 -0
- package/app/lib/test-fixtures/router-contract/page.tsx +3 -0
- package/app/lib/transclusion-smoke.test.ts +163 -0
- package/app/lib/tutorial.ts +301 -0
- package/app/lib/version.ts +93 -0
- package/app/lib/viewport-culling.ts +740 -728
- package/app/lib/virtual-files.ts +456 -0
- package/app/lib/webgl-text.ts +189 -0
- package/app/lib/{galaxydraw-bridge.ts → xydraw-bridge.ts} +485 -477
- package/app/lib/{galaxydraw.test.ts → xydraw.test.ts} +228 -229
- package/app/og-image.png +0 -0
- package/app/page.client.tsx +70 -215
- package/app/page.tsx +27 -92
- package/app/state/machine.js +13 -0
- package/banner.png +0 -0
- package/package.json +17 -8
- package/server.ts +11 -1
- package/app/api/connections/route.ts +0 -72
- package/app/api/positions/route.ts +0 -80
- package/app/api/repo/browse/route.ts +0 -55
- package/app/lib/pr-review.ts +0 -374
- package/packages/galaxydraw/README.md +0 -296
- package/packages/galaxydraw/banner.png +0 -0
- package/packages/galaxydraw/demo/build-static.ts +0 -100
- package/packages/galaxydraw/demo/client.ts +0 -154
- package/packages/galaxydraw/demo/dist/client.js +0 -8
- package/packages/galaxydraw/demo/index.html +0 -256
- package/packages/galaxydraw/demo/server.ts +0 -96
- package/packages/galaxydraw/dist/index.js +0 -984
- package/packages/galaxydraw/dist/index.js.map +0 -16
- package/packages/galaxydraw/node_modules/.bin/tsc.bunx +0 -0
- package/packages/galaxydraw/node_modules/.bin/tsc.exe +0 -0
- package/packages/galaxydraw/node_modules/.bin/tsserver.bunx +0 -0
- package/packages/galaxydraw/node_modules/.bin/tsserver.exe +0 -0
- package/packages/galaxydraw/package.json +0 -49
- package/packages/galaxydraw/perf.test.ts +0 -284
- package/packages/galaxydraw/src/core/cards.ts +0 -435
- package/packages/galaxydraw/src/core/engine.ts +0 -339
- package/packages/galaxydraw/src/core/events.ts +0 -81
- package/packages/galaxydraw/src/core/layout.ts +0 -136
- package/packages/galaxydraw/src/core/minimap.ts +0 -216
- package/packages/galaxydraw/src/core/state.ts +0 -177
- package/packages/galaxydraw/src/core/viewport.ts +0 -106
- package/packages/galaxydraw/src/galaxydraw.css +0 -166
- package/packages/galaxydraw/src/index.ts +0 -40
- package/packages/galaxydraw/tsconfig.json +0 -30
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import { measure } from 'measure-fn';
|
|
2
|
-
import { Database, z } from 'sqlite-zod-orm';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
|
|
5
|
-
const dbPath = path.join(process.cwd(), 'db', 'connections_v1.sqlite');
|
|
6
|
-
|
|
7
|
-
const db = new Database(dbPath, {
|
|
8
|
-
connections: z.object({
|
|
9
|
-
conn_id: z.string(),
|
|
10
|
-
source_file: z.string(),
|
|
11
|
-
source_line_start: z.number(),
|
|
12
|
-
source_line_end: z.number(),
|
|
13
|
-
target_file: z.string(),
|
|
14
|
-
target_line_start: z.number(),
|
|
15
|
-
target_line_end: z.number(),
|
|
16
|
-
comment: z.string().default(''),
|
|
17
|
-
created_at: z.string().default(() => new Date().toISOString()),
|
|
18
|
-
}),
|
|
19
|
-
}, {
|
|
20
|
-
indexes: { connections: ['source_file', 'target_file'] },
|
|
21
|
-
reactive: false,
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
export async function GET() {
|
|
25
|
-
return measure('api:connections:get', async () => {
|
|
26
|
-
try {
|
|
27
|
-
const connections = db.connections.select().all();
|
|
28
|
-
return Response.json({ connections });
|
|
29
|
-
} catch (error: any) {
|
|
30
|
-
console.error('api:connections:get:error', error);
|
|
31
|
-
return new Response(`Error: ${error.message}`, { status: 500 });
|
|
32
|
-
}
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export async function POST(req: Request) {
|
|
37
|
-
return measure('api:connections:save', async () => {
|
|
38
|
-
try {
|
|
39
|
-
const body = await req.json();
|
|
40
|
-
const { connections } = body;
|
|
41
|
-
|
|
42
|
-
if (!Array.isArray(connections)) {
|
|
43
|
-
return new Response('connections array is required', { status: 400 });
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Delete all existing, then re-insert
|
|
47
|
-
const existing = db.connections.select().all();
|
|
48
|
-
for (const e of existing) {
|
|
49
|
-
e.delete();
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
for (const conn of connections) {
|
|
53
|
-
db.connections.insert({
|
|
54
|
-
conn_id: conn.id,
|
|
55
|
-
source_file: conn.sourceFile,
|
|
56
|
-
source_line_start: conn.sourceLineStart,
|
|
57
|
-
source_line_end: conn.sourceLineEnd,
|
|
58
|
-
target_file: conn.targetFile,
|
|
59
|
-
target_line_start: conn.targetLineStart,
|
|
60
|
-
target_line_end: conn.targetLineEnd,
|
|
61
|
-
comment: conn.comment || '',
|
|
62
|
-
created_at: conn.createdAt || new Date().toISOString(),
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
return Response.json({ success: true, count: connections.length });
|
|
67
|
-
} catch (error: any) {
|
|
68
|
-
console.error('api:connections:save:error', error);
|
|
69
|
-
return new Response(`Error: ${error.message}`, { status: 500 });
|
|
70
|
-
}
|
|
71
|
-
});
|
|
72
|
-
}
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import { measure, measureSync } from 'measure-fn';
|
|
2
|
-
import { Database, z } from 'sqlite-zod-orm';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
|
|
5
|
-
const dbPath = path.join(process.cwd(), 'db', 'positions_v3.sqlite');
|
|
6
|
-
|
|
7
|
-
const db = new Database(dbPath, {
|
|
8
|
-
positions: z.object({
|
|
9
|
-
commit_hash: z.string(),
|
|
10
|
-
file_path: z.string(),
|
|
11
|
-
x: z.number(),
|
|
12
|
-
y: z.number(),
|
|
13
|
-
width: z.number().optional(),
|
|
14
|
-
height: z.number().optional(),
|
|
15
|
-
}),
|
|
16
|
-
}, {
|
|
17
|
-
indexes: { positions: ['commit_hash'] },
|
|
18
|
-
reactive: false,
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
export async function GET(req: Request) {
|
|
22
|
-
return measure('api:positions:get', async () => {
|
|
23
|
-
try {
|
|
24
|
-
const url = new URL(req.url);
|
|
25
|
-
const commitHash = url.searchParams.get('commit');
|
|
26
|
-
|
|
27
|
-
const query = commitHash
|
|
28
|
-
? db.positions.select().where({ commit_hash: commitHash })
|
|
29
|
-
: db.positions.select();
|
|
30
|
-
|
|
31
|
-
const positions = query.all();
|
|
32
|
-
|
|
33
|
-
// Convert to map format
|
|
34
|
-
const positionMap: Record<string, { x: number; y: number; width?: number; height?: number }> = {};
|
|
35
|
-
for (const pos of positions) {
|
|
36
|
-
positionMap[`${pos.commit_hash}:${pos.file_path}`] = {
|
|
37
|
-
x: pos.x,
|
|
38
|
-
y: pos.y,
|
|
39
|
-
width: pos.width ?? undefined,
|
|
40
|
-
height: pos.height ?? undefined,
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
return Response.json(positionMap);
|
|
45
|
-
} catch (error: any) {
|
|
46
|
-
console.error('api:positions:get:error', error);
|
|
47
|
-
return new Response(`Error: ${error.message}`, { status: 500 });
|
|
48
|
-
}
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export async function POST(req: Request) {
|
|
53
|
-
return measure('api:positions:save', async () => {
|
|
54
|
-
try {
|
|
55
|
-
const body = await req.json();
|
|
56
|
-
const { commitHash, filePath, x, y, width, height } = body;
|
|
57
|
-
|
|
58
|
-
if (!commitHash || !filePath || x === undefined || y === undefined) {
|
|
59
|
-
return new Response('commitHash, filePath, x, and y are required', { status: 400 });
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
db.positions.upsert(
|
|
63
|
-
{ commit_hash: commitHash, file_path: filePath },
|
|
64
|
-
{
|
|
65
|
-
commit_hash: commitHash,
|
|
66
|
-
file_path: filePath,
|
|
67
|
-
x,
|
|
68
|
-
y,
|
|
69
|
-
width: width || undefined,
|
|
70
|
-
height: height || undefined,
|
|
71
|
-
},
|
|
72
|
-
);
|
|
73
|
-
|
|
74
|
-
return Response.json({ success: true });
|
|
75
|
-
} catch (error: any) {
|
|
76
|
-
console.error('api:positions:save:error', error);
|
|
77
|
-
return new Response(`Error: ${error.message}`, { status: 500 });
|
|
78
|
-
}
|
|
79
|
-
});
|
|
80
|
-
}
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { measure } from 'measure-fn';
|
|
2
|
-
import { exec } from 'child_process';
|
|
3
|
-
import { promisify } from 'util';
|
|
4
|
-
import { blockInProduction } from '../validate-path';
|
|
5
|
-
|
|
6
|
-
const execAsync = promisify(exec);
|
|
7
|
-
|
|
8
|
-
export async function POST(req: Request) {
|
|
9
|
-
const blocked = blockInProduction('Folder browser');
|
|
10
|
-
if (blocked) return blocked;
|
|
11
|
-
|
|
12
|
-
return measure('api:repo:browse', async () => {
|
|
13
|
-
try {
|
|
14
|
-
// Use PowerShell with -EncodedCommand to avoid quoting issues
|
|
15
|
-
// Use TopMost Form to ensure the dialog pops up ABOVE the browser window
|
|
16
|
-
const psScript = `
|
|
17
|
-
Add-Type -AssemblyName System.Windows.Forms
|
|
18
|
-
$dialog = New-Object System.Windows.Forms.FolderBrowserDialog
|
|
19
|
-
$dialog.Description = "Select Git Repository"
|
|
20
|
-
$dialog.ShowNewFolderButton = $false
|
|
21
|
-
|
|
22
|
-
$form = New-Object System.Windows.Forms.Form
|
|
23
|
-
$form.TopMost = $true
|
|
24
|
-
$result = $dialog.ShowDialog($form)
|
|
25
|
-
|
|
26
|
-
if ($result -eq [System.Windows.Forms.DialogResult]::OK) {
|
|
27
|
-
Write-Output $dialog.SelectedPath
|
|
28
|
-
} else {
|
|
29
|
-
Write-Output ""
|
|
30
|
-
}
|
|
31
|
-
$form.Dispose()
|
|
32
|
-
`.trim();
|
|
33
|
-
|
|
34
|
-
const encoded = Buffer.from(psScript, 'utf16le').toString('base64');
|
|
35
|
-
|
|
36
|
-
// Use async exec so we don't block the entire melina server (which freezes the UI's live updates)
|
|
37
|
-
const { stdout } = await execAsync(
|
|
38
|
-
`powershell -sta -WindowStyle Hidden -NoProfile -EncodedCommand ${encoded}`,
|
|
39
|
-
{ encoding: 'utf-8', timeout: 86400000 }
|
|
40
|
-
);
|
|
41
|
-
|
|
42
|
-
const selected = stdout.trim();
|
|
43
|
-
|
|
44
|
-
if (!selected) {
|
|
45
|
-
return Response.json({ cancelled: true, path: null });
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return Response.json({ cancelled: false, path: selected });
|
|
49
|
-
} catch (error: any) {
|
|
50
|
-
console.error('api:repo:browse:error', error);
|
|
51
|
-
// Even if cancelled or errored, don't crash the server. Let UI reset the dropdown.
|
|
52
|
-
return Response.json({ cancelled: true, path: null, error: error.message });
|
|
53
|
-
}
|
|
54
|
-
});
|
|
55
|
-
}
|
package/app/lib/pr-review.ts
DELETED
|
@@ -1,374 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* PR Review — Inline Comments on Diff Lines
|
|
3
|
-
*
|
|
4
|
-
* Enables code review directly on the canvas:
|
|
5
|
-
* - Click the gutter area of any line to add a comment
|
|
6
|
-
* - Comments stored per file+line in localStorage
|
|
7
|
-
* - Visual markers (purple dot) rendered in the canvas gutter
|
|
8
|
-
* - Comment thread popup with input field and existing comments
|
|
9
|
-
* - Optional WebSocket sync for collaborative review
|
|
10
|
-
*
|
|
11
|
-
* Storage key: `gitcanvas:reviews:{repoSlug}`
|
|
12
|
-
* Data format: { [filePath]: { [lineNum]: ReviewComment[] } }
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
// ─── Types ──────────────────────────────────────────────
|
|
16
|
-
|
|
17
|
-
export interface ReviewComment {
|
|
18
|
-
id: string;
|
|
19
|
-
author: string;
|
|
20
|
-
text: string;
|
|
21
|
-
lineNum: number;
|
|
22
|
-
filePath: string;
|
|
23
|
-
createdAt: number;
|
|
24
|
-
resolved?: boolean;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export interface ReviewStore {
|
|
28
|
-
[filePath: string]: {
|
|
29
|
-
[lineNum: string]: ReviewComment[];
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// ─── Storage ────────────────────────────────────────────
|
|
34
|
-
|
|
35
|
-
const STORAGE_PREFIX = 'gitcanvas:reviews:';
|
|
36
|
-
let currentRepo = '';
|
|
37
|
-
let store: ReviewStore = {};
|
|
38
|
-
let changeListeners: Array<() => void> = [];
|
|
39
|
-
|
|
40
|
-
export function initReviewStore(repoSlug: string) {
|
|
41
|
-
currentRepo = repoSlug;
|
|
42
|
-
try {
|
|
43
|
-
const saved = localStorage.getItem(STORAGE_PREFIX + repoSlug);
|
|
44
|
-
store = saved ? JSON.parse(saved) : {};
|
|
45
|
-
} catch {
|
|
46
|
-
store = {};
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function persist() {
|
|
51
|
-
if (!currentRepo) return;
|
|
52
|
-
try {
|
|
53
|
-
localStorage.setItem(STORAGE_PREFIX + currentRepo, JSON.stringify(store));
|
|
54
|
-
} catch { /* quota exceeded — non-fatal */ }
|
|
55
|
-
changeListeners.forEach(fn => fn());
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export function onReviewChange(fn: () => void): () => void {
|
|
59
|
-
changeListeners.push(fn);
|
|
60
|
-
return () => { changeListeners = changeListeners.filter(f => f !== fn); };
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// ─── CRUD ───────────────────────────────────────────────
|
|
64
|
-
|
|
65
|
-
export function addComment(filePath: string, lineNum: number, text: string, author = 'You'): ReviewComment {
|
|
66
|
-
if (!store[filePath]) store[filePath] = {};
|
|
67
|
-
const key = String(lineNum);
|
|
68
|
-
if (!store[filePath][key]) store[filePath][key] = [];
|
|
69
|
-
|
|
70
|
-
const comment: ReviewComment = {
|
|
71
|
-
id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
72
|
-
author,
|
|
73
|
-
text,
|
|
74
|
-
lineNum,
|
|
75
|
-
filePath,
|
|
76
|
-
createdAt: Date.now(),
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
store[filePath][key].push(comment);
|
|
80
|
-
persist();
|
|
81
|
-
return comment;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export function getComments(filePath: string, lineNum: number): ReviewComment[] {
|
|
85
|
-
return store[filePath]?.[String(lineNum)] || [];
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export function getAllFileComments(filePath: string): Map<number, ReviewComment[]> {
|
|
89
|
-
const map = new Map<number, ReviewComment[]>();
|
|
90
|
-
const fileStore = store[filePath];
|
|
91
|
-
if (!fileStore) return map;
|
|
92
|
-
for (const [key, comments] of Object.entries(fileStore)) {
|
|
93
|
-
const lineNum = parseInt(key, 10);
|
|
94
|
-
if (!isNaN(lineNum) && comments.length > 0) {
|
|
95
|
-
map.set(lineNum, comments);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
return map;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export function resolveComment(filePath: string, lineNum: number, commentId: string) {
|
|
102
|
-
const comments = store[filePath]?.[String(lineNum)];
|
|
103
|
-
if (!comments) return;
|
|
104
|
-
const comment = comments.find(c => c.id === commentId);
|
|
105
|
-
if (comment) {
|
|
106
|
-
comment.resolved = true;
|
|
107
|
-
persist();
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
export function deleteComment(filePath: string, lineNum: number, commentId: string) {
|
|
112
|
-
const key = String(lineNum);
|
|
113
|
-
const comments = store[filePath]?.[key];
|
|
114
|
-
if (!comments) return;
|
|
115
|
-
store[filePath][key] = comments.filter(c => c.id !== commentId);
|
|
116
|
-
if (store[filePath][key].length === 0) delete store[filePath][key];
|
|
117
|
-
if (Object.keys(store[filePath]).length === 0) delete store[filePath];
|
|
118
|
-
persist();
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
export function getReviewStats(): { totalComments: number; totalFiles: number; unresolvedCount: number } {
|
|
122
|
-
let totalComments = 0;
|
|
123
|
-
let unresolvedCount = 0;
|
|
124
|
-
let totalFiles = 0;
|
|
125
|
-
for (const filePath of Object.keys(store)) {
|
|
126
|
-
let hasComments = false;
|
|
127
|
-
for (const comments of Object.values(store[filePath])) {
|
|
128
|
-
totalComments += comments.length;
|
|
129
|
-
unresolvedCount += comments.filter(c => !c.resolved).length;
|
|
130
|
-
if (comments.length > 0) hasComments = true;
|
|
131
|
-
}
|
|
132
|
-
if (hasComments) totalFiles++;
|
|
133
|
-
}
|
|
134
|
-
return { totalComments, totalFiles, unresolvedCount };
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// ─── Comment Thread Popup ───────────────────────────────
|
|
138
|
-
|
|
139
|
-
let activePopup: HTMLElement | null = null;
|
|
140
|
-
|
|
141
|
-
export function showCommentPopup(
|
|
142
|
-
filePath: string,
|
|
143
|
-
lineNum: number,
|
|
144
|
-
anchorX: number,
|
|
145
|
-
anchorY: number,
|
|
146
|
-
onSubmit?: (comment: ReviewComment) => void
|
|
147
|
-
) {
|
|
148
|
-
hideCommentPopup();
|
|
149
|
-
|
|
150
|
-
const comments = getComments(filePath, lineNum);
|
|
151
|
-
const popup = document.createElement('div');
|
|
152
|
-
popup.className = 'review-comment-popup';
|
|
153
|
-
popup.setAttribute('data-line', String(lineNum));
|
|
154
|
-
popup.style.cssText = `
|
|
155
|
-
position: fixed;
|
|
156
|
-
z-index: 10000;
|
|
157
|
-
background: rgba(22, 22, 35, 0.98);
|
|
158
|
-
border: 1px solid rgba(124, 58, 237, 0.5);
|
|
159
|
-
border-radius: 12px;
|
|
160
|
-
padding: 0;
|
|
161
|
-
min-width: 320px;
|
|
162
|
-
max-width: 420px;
|
|
163
|
-
max-height: 360px;
|
|
164
|
-
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6), 0 0 16px rgba(124, 58, 237, 0.2);
|
|
165
|
-
font-family: 'Inter', -apple-system, sans-serif;
|
|
166
|
-
font-size: 13px;
|
|
167
|
-
color: #c9d1d9;
|
|
168
|
-
overflow: hidden;
|
|
169
|
-
backdrop-filter: blur(16px);
|
|
170
|
-
`;
|
|
171
|
-
|
|
172
|
-
// Header
|
|
173
|
-
const header = document.createElement('div');
|
|
174
|
-
header.style.cssText = `
|
|
175
|
-
padding: 10px 14px;
|
|
176
|
-
border-bottom: 1px solid rgba(255,255,255,0.08);
|
|
177
|
-
display: flex;
|
|
178
|
-
justify-content: space-between;
|
|
179
|
-
align-items: center;
|
|
180
|
-
background: rgba(124, 58, 237, 0.1);
|
|
181
|
-
`;
|
|
182
|
-
header.innerHTML = `
|
|
183
|
-
<span style="font-weight: 600; color: #a78bfa;">
|
|
184
|
-
💬 Line ${lineNum}
|
|
185
|
-
</span>
|
|
186
|
-
<span style="color: #6e7681; font-size: 11px;">
|
|
187
|
-
${comments.length} comment${comments.length !== 1 ? 's' : ''}
|
|
188
|
-
</span>
|
|
189
|
-
`;
|
|
190
|
-
popup.appendChild(header);
|
|
191
|
-
|
|
192
|
-
// Existing comments
|
|
193
|
-
if (comments.length > 0) {
|
|
194
|
-
const list = document.createElement('div');
|
|
195
|
-
list.style.cssText = `
|
|
196
|
-
max-height: 180px;
|
|
197
|
-
overflow-y: auto;
|
|
198
|
-
padding: 8px 14px;
|
|
199
|
-
`;
|
|
200
|
-
for (const c of comments) {
|
|
201
|
-
const item = document.createElement('div');
|
|
202
|
-
item.style.cssText = `
|
|
203
|
-
padding: 8px 0;
|
|
204
|
-
border-bottom: 1px solid rgba(255,255,255,0.04);
|
|
205
|
-
${c.resolved ? 'opacity: 0.5;' : ''}
|
|
206
|
-
`;
|
|
207
|
-
const ago = timeAgo(c.createdAt);
|
|
208
|
-
item.innerHTML = `
|
|
209
|
-
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px;">
|
|
210
|
-
<span style="font-weight: 500; color: #a78bfa; font-size: 12px;">${escapeHtml(c.author)}</span>
|
|
211
|
-
<span style="color: #484f58; font-size: 11px;">${ago}</span>
|
|
212
|
-
</div>
|
|
213
|
-
<div style="line-height: 1.5; ${c.resolved ? 'text-decoration: line-through;' : ''}">${escapeHtml(c.text)}</div>
|
|
214
|
-
`;
|
|
215
|
-
|
|
216
|
-
// Resolve/delete buttons
|
|
217
|
-
const actions = document.createElement('div');
|
|
218
|
-
actions.style.cssText = 'display: flex; gap: 8px; margin-top: 4px;';
|
|
219
|
-
|
|
220
|
-
if (!c.resolved) {
|
|
221
|
-
const resolveBtn = document.createElement('button');
|
|
222
|
-
resolveBtn.textContent = '✓ Resolve';
|
|
223
|
-
resolveBtn.style.cssText = btnStyle('#238636');
|
|
224
|
-
resolveBtn.onclick = (e) => {
|
|
225
|
-
e.stopPropagation();
|
|
226
|
-
resolveComment(filePath, lineNum, c.id);
|
|
227
|
-
showCommentPopup(filePath, lineNum, anchorX, anchorY, onSubmit);
|
|
228
|
-
};
|
|
229
|
-
actions.appendChild(resolveBtn);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
const delBtn = document.createElement('button');
|
|
233
|
-
delBtn.textContent = '✕';
|
|
234
|
-
delBtn.style.cssText = btnStyle('#da3633');
|
|
235
|
-
delBtn.onclick = (e) => {
|
|
236
|
-
e.stopPropagation();
|
|
237
|
-
deleteComment(filePath, lineNum, c.id);
|
|
238
|
-
showCommentPopup(filePath, lineNum, anchorX, anchorY, onSubmit);
|
|
239
|
-
};
|
|
240
|
-
actions.appendChild(delBtn);
|
|
241
|
-
|
|
242
|
-
item.appendChild(actions);
|
|
243
|
-
list.appendChild(item);
|
|
244
|
-
}
|
|
245
|
-
popup.appendChild(list);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// Input area
|
|
249
|
-
const inputArea = document.createElement('div');
|
|
250
|
-
inputArea.style.cssText = `
|
|
251
|
-
padding: 10px 14px;
|
|
252
|
-
border-top: 1px solid rgba(255,255,255,0.06);
|
|
253
|
-
display: flex;
|
|
254
|
-
gap: 8px;
|
|
255
|
-
`;
|
|
256
|
-
|
|
257
|
-
const input = document.createElement('input');
|
|
258
|
-
input.type = 'text';
|
|
259
|
-
input.placeholder = 'Add a comment...';
|
|
260
|
-
input.style.cssText = `
|
|
261
|
-
flex: 1;
|
|
262
|
-
background: rgba(255,255,255,0.06);
|
|
263
|
-
border: 1px solid rgba(255,255,255,0.1);
|
|
264
|
-
border-radius: 8px;
|
|
265
|
-
padding: 8px 12px;
|
|
266
|
-
color: #c9d1d9;
|
|
267
|
-
font-size: 13px;
|
|
268
|
-
outline: none;
|
|
269
|
-
font-family: inherit;
|
|
270
|
-
`;
|
|
271
|
-
|
|
272
|
-
const submitBtn = document.createElement('button');
|
|
273
|
-
submitBtn.textContent = '↵';
|
|
274
|
-
submitBtn.style.cssText = `
|
|
275
|
-
background: rgba(124, 58, 237, 0.8);
|
|
276
|
-
border: none;
|
|
277
|
-
border-radius: 8px;
|
|
278
|
-
padding: 8px 14px;
|
|
279
|
-
color: white;
|
|
280
|
-
font-size: 14px;
|
|
281
|
-
cursor: pointer;
|
|
282
|
-
transition: background 0.15s;
|
|
283
|
-
`;
|
|
284
|
-
submitBtn.onmouseenter = () => submitBtn.style.background = 'rgba(124, 58, 237, 1)';
|
|
285
|
-
submitBtn.onmouseleave = () => submitBtn.style.background = 'rgba(124, 58, 237, 0.8)';
|
|
286
|
-
|
|
287
|
-
const submit = () => {
|
|
288
|
-
const text = input.value.trim();
|
|
289
|
-
if (!text) return;
|
|
290
|
-
const comment = addComment(filePath, lineNum, text);
|
|
291
|
-
onSubmit?.(comment);
|
|
292
|
-
// Refresh popup to show new comment
|
|
293
|
-
showCommentPopup(filePath, lineNum, anchorX, anchorY, onSubmit);
|
|
294
|
-
};
|
|
295
|
-
|
|
296
|
-
input.onkeydown = (e) => {
|
|
297
|
-
if (e.key === 'Enter') submit();
|
|
298
|
-
if (e.key === 'Escape') hideCommentPopup();
|
|
299
|
-
e.stopPropagation(); // Don't trigger canvas shortcuts
|
|
300
|
-
};
|
|
301
|
-
submitBtn.onclick = submit;
|
|
302
|
-
|
|
303
|
-
inputArea.appendChild(input);
|
|
304
|
-
inputArea.appendChild(submitBtn);
|
|
305
|
-
popup.appendChild(inputArea);
|
|
306
|
-
|
|
307
|
-
// Position: prefer above the anchor, fall below if near top
|
|
308
|
-
document.body.appendChild(popup);
|
|
309
|
-
const popupRect = popup.getBoundingClientRect();
|
|
310
|
-
let px = anchorX;
|
|
311
|
-
let py = anchorY - popupRect.height - 8;
|
|
312
|
-
if (py < 10) py = anchorY + 24;
|
|
313
|
-
if (px + popupRect.width > window.innerWidth - 10) px = window.innerWidth - popupRect.width - 10;
|
|
314
|
-
popup.style.left = `${px}px`;
|
|
315
|
-
popup.style.top = `${py}px`;
|
|
316
|
-
|
|
317
|
-
activePopup = popup;
|
|
318
|
-
|
|
319
|
-
// Focus input
|
|
320
|
-
requestAnimationFrame(() => input.focus());
|
|
321
|
-
|
|
322
|
-
// Close on outside click (delayed to avoid immediate close)
|
|
323
|
-
setTimeout(() => {
|
|
324
|
-
const closeHandler = (e: MouseEvent) => {
|
|
325
|
-
if (!popup.contains(e.target as Node)) {
|
|
326
|
-
hideCommentPopup();
|
|
327
|
-
document.removeEventListener('mousedown', closeHandler);
|
|
328
|
-
}
|
|
329
|
-
};
|
|
330
|
-
document.addEventListener('mousedown', closeHandler);
|
|
331
|
-
}, 50);
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
export function hideCommentPopup() {
|
|
335
|
-
if (activePopup) {
|
|
336
|
-
activePopup.remove();
|
|
337
|
-
activePopup = null;
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
// ─── Helpers ────────────────────────────────────────────
|
|
342
|
-
|
|
343
|
-
function btnStyle(color: string): string {
|
|
344
|
-
return `
|
|
345
|
-
background: none;
|
|
346
|
-
border: none;
|
|
347
|
-
color: ${color};
|
|
348
|
-
cursor: pointer;
|
|
349
|
-
font-size: 11px;
|
|
350
|
-
padding: 2px 4px;
|
|
351
|
-
border-radius: 4px;
|
|
352
|
-
opacity: 0.7;
|
|
353
|
-
transition: opacity 0.15s;
|
|
354
|
-
`;
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
function escapeHtml(str: string): string {
|
|
358
|
-
return str
|
|
359
|
-
.replace(/&/g, '&')
|
|
360
|
-
.replace(/</g, '<')
|
|
361
|
-
.replace(/>/g, '>')
|
|
362
|
-
.replace(/"/g, '"');
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
function timeAgo(ts: number): string {
|
|
366
|
-
const diff = Date.now() - ts;
|
|
367
|
-
const mins = Math.floor(diff / 60000);
|
|
368
|
-
if (mins < 1) return 'just now';
|
|
369
|
-
if (mins < 60) return `${mins}m ago`;
|
|
370
|
-
const hours = Math.floor(mins / 60);
|
|
371
|
-
if (hours < 24) return `${hours}h ago`;
|
|
372
|
-
const days = Math.floor(hours / 24);
|
|
373
|
-
return `${days}d ago`;
|
|
374
|
-
}
|