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
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test';
|
|
2
|
+
import { setupGithubImport } from './events';
|
|
3
|
+
import { installFetchMock, installWindowOpenMock, setupDomTest } from './test-dom';
|
|
4
|
+
|
|
5
|
+
function makeSseResponse(chunks: string[]): Response {
|
|
6
|
+
const stream = new ReadableStream({
|
|
7
|
+
start(controller) {
|
|
8
|
+
const enc = new TextEncoder();
|
|
9
|
+
for (const chunk of chunks) controller.enqueue(enc.encode(chunk));
|
|
10
|
+
controller.close();
|
|
11
|
+
},
|
|
12
|
+
});
|
|
13
|
+
return new Response(stream, {
|
|
14
|
+
headers: { 'Content-Type': 'text/event-stream' },
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe('GitHub import modal smoke', () => {
|
|
19
|
+
let cleanup: (() => void) | undefined;
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
const handle = setupDomTest({
|
|
23
|
+
url: 'http://localhost:3335/',
|
|
24
|
+
html: `
|
|
25
|
+
<button id="githubImportBtn">Import GitHub</button>
|
|
26
|
+
<div class="github-modal" id="githubModal">
|
|
27
|
+
<div class="github-modal-backdrop"></div>
|
|
28
|
+
<div class="github-modal-content">
|
|
29
|
+
<button id="githubModalClose">×</button>
|
|
30
|
+
<input id="githubUserInput" />
|
|
31
|
+
<select id="githubSortSelect">
|
|
32
|
+
<option value="updated">Recently Updated</option>
|
|
33
|
+
<option value="stars">Most Stars</option>
|
|
34
|
+
<option value="name">Name A→Z</option>
|
|
35
|
+
</select>
|
|
36
|
+
<button id="githubSearchBtn">Search</button>
|
|
37
|
+
<div id="githubUrlCloneRow" style="display:none">
|
|
38
|
+
<span id="githubDetectedUrl"></span>
|
|
39
|
+
<button id="githubUrlCloneBtn">Clone & Open</button>
|
|
40
|
+
</div>
|
|
41
|
+
<div id="githubFilterRow" style="display:none">
|
|
42
|
+
<input id="githubRepoFilter" />
|
|
43
|
+
</div>
|
|
44
|
+
<div id="githubProfile" style="display:none"></div>
|
|
45
|
+
<div id="githubReposGrid"></div>
|
|
46
|
+
<div id="githubPagination" style="display:none">
|
|
47
|
+
<button id="githubPrevPage"></button>
|
|
48
|
+
<span id="githubPageInfo"></span>
|
|
49
|
+
<button id="githubNextPage"></button>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
<div id="cloneStatus"></div>
|
|
54
|
+
<select id="repoSelect"></select>
|
|
55
|
+
`,
|
|
56
|
+
});
|
|
57
|
+
cleanup = handle.cleanup;
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
afterEach(() => {
|
|
61
|
+
cleanup?.();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('opens, searches, renders repos, and filters results', async () => {
|
|
65
|
+
const fetchMock = mock(async (input: string) => {
|
|
66
|
+
expect(input).toBe('/api/github/repos?user=7flash&page=1&sort=updated');
|
|
67
|
+
return new Response(JSON.stringify({
|
|
68
|
+
profile: {
|
|
69
|
+
login: '7flash',
|
|
70
|
+
name: '7flash',
|
|
71
|
+
public_repos: 2,
|
|
72
|
+
type: 'User',
|
|
73
|
+
avatar_url: 'https://example.com/avatar.png',
|
|
74
|
+
bio: 'builder',
|
|
75
|
+
},
|
|
76
|
+
repos: [
|
|
77
|
+
{
|
|
78
|
+
name: 'gitmaps',
|
|
79
|
+
clone_url: 'https://github.com/7flash/gitmaps.git',
|
|
80
|
+
description: 'Spatial code explorer',
|
|
81
|
+
language: 'TypeScript',
|
|
82
|
+
size: 2048,
|
|
83
|
+
updated_at: new Date().toISOString(),
|
|
84
|
+
stars: 10,
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: 'jsx-ai',
|
|
88
|
+
clone_url: 'https://github.com/7flash/jsx-ai.git',
|
|
89
|
+
description: 'JSX interface for structured LLM calls',
|
|
90
|
+
language: 'TypeScript',
|
|
91
|
+
size: 512,
|
|
92
|
+
updated_at: new Date().toISOString(),
|
|
93
|
+
stars: 5,
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
page: 1,
|
|
97
|
+
hasNext: true,
|
|
98
|
+
hasPrev: false,
|
|
99
|
+
}), {
|
|
100
|
+
headers: { 'Content-Type': 'application/json' },
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const openMock = mock(() => null as any);
|
|
105
|
+
const fetchHandle = installFetchMock(fetchMock as any);
|
|
106
|
+
const openHandle = installWindowOpenMock(openMock as any);
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
setupGithubImport({} as any);
|
|
110
|
+
|
|
111
|
+
const openBtn = document.getElementById('githubImportBtn') as HTMLButtonElement;
|
|
112
|
+
const modal = document.getElementById('githubModal') as HTMLElement;
|
|
113
|
+
const userInput = document.getElementById('githubUserInput') as HTMLInputElement;
|
|
114
|
+
const searchBtn = document.getElementById('githubSearchBtn') as HTMLButtonElement;
|
|
115
|
+
const profile = document.getElementById('githubProfile') as HTMLElement;
|
|
116
|
+
const grid = document.getElementById('githubReposGrid') as HTMLElement;
|
|
117
|
+
const filterRow = document.getElementById('githubFilterRow') as HTMLElement;
|
|
118
|
+
const filterInput = document.getElementById('githubRepoFilter') as HTMLInputElement;
|
|
119
|
+
const pageInfo = document.getElementById('githubPageInfo') as HTMLElement;
|
|
120
|
+
const nextBtn = document.getElementById('githubNextPage') as HTMLButtonElement;
|
|
121
|
+
|
|
122
|
+
openBtn.click();
|
|
123
|
+
expect(modal.classList.contains('active')).toBe(true);
|
|
124
|
+
|
|
125
|
+
userInput.value = '7flash';
|
|
126
|
+
searchBtn.click();
|
|
127
|
+
await Promise.resolve();
|
|
128
|
+
await Promise.resolve();
|
|
129
|
+
|
|
130
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
131
|
+
expect(localStorage.getItem('gitcanvas:lastGithubUser')).toBe('7flash');
|
|
132
|
+
expect(profile.style.display).toBe('flex');
|
|
133
|
+
expect(profile.textContent).toContain('@7flash');
|
|
134
|
+
expect(grid.textContent).toContain('gitmaps');
|
|
135
|
+
expect(grid.textContent).toContain('jsx-ai');
|
|
136
|
+
expect(filterRow.style.display).toBe('flex');
|
|
137
|
+
expect(pageInfo.textContent).toBe('Page 1');
|
|
138
|
+
expect(nextBtn.disabled).toBe(false);
|
|
139
|
+
|
|
140
|
+
filterInput.value = 'jsx';
|
|
141
|
+
filterInput.dispatchEvent(new window.Event('input', { bubbles: true }));
|
|
142
|
+
|
|
143
|
+
const cards = Array.from(document.querySelectorAll('.github-repo-card')) as HTMLElement[];
|
|
144
|
+
const visibleNames = cards.filter(card => card.style.display !== 'none').map(card => card.dataset.name);
|
|
145
|
+
expect(visibleNames).toEqual(['jsx-ai']);
|
|
146
|
+
|
|
147
|
+
cards[0].click();
|
|
148
|
+
expect(openMock).toHaveBeenCalledWith('https://github.com/7flash/gitmaps', '_blank');
|
|
149
|
+
} finally {
|
|
150
|
+
fetchHandle.restore();
|
|
151
|
+
openHandle.restore();
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('detects direct GitHub URLs and shows clone affordance', () => {
|
|
156
|
+
setupGithubImport({} as any);
|
|
157
|
+
|
|
158
|
+
const userInput = document.getElementById('githubUserInput') as HTMLInputElement;
|
|
159
|
+
const urlRow = document.getElementById('githubUrlCloneRow') as HTMLElement;
|
|
160
|
+
const detected = document.getElementById('githubDetectedUrl') as HTMLElement;
|
|
161
|
+
|
|
162
|
+
userInput.value = 'https://github.com/7flash/jsx-ai';
|
|
163
|
+
userInput.dispatchEvent(new window.Event('input', { bubbles: true }));
|
|
164
|
+
|
|
165
|
+
expect(urlRow.style.display).toBe('flex');
|
|
166
|
+
expect(detected.textContent).toBe('7flash/jsx-ai');
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test('pressing Enter on a direct GitHub URL starts clone-stream and closes the modal', async () => {
|
|
170
|
+
const fetchMock = mock(() => new Promise<Response>(() => {}));
|
|
171
|
+
const fetchHandle = installFetchMock(fetchMock as any);
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
setupGithubImport({} as any);
|
|
175
|
+
|
|
176
|
+
const openBtn = document.getElementById('githubImportBtn') as HTMLButtonElement;
|
|
177
|
+
const modal = document.getElementById('githubModal') as HTMLElement;
|
|
178
|
+
const userInput = document.getElementById('githubUserInput') as HTMLInputElement;
|
|
179
|
+
const cloneStatus = document.getElementById('cloneStatus') as HTMLElement;
|
|
180
|
+
|
|
181
|
+
openBtn.click();
|
|
182
|
+
expect(modal.classList.contains('active')).toBe(true);
|
|
183
|
+
|
|
184
|
+
userInput.value = 'https://github.com/7flash/jsx-ai';
|
|
185
|
+
userInput.dispatchEvent(new window.Event('input', { bubbles: true }));
|
|
186
|
+
userInput.dispatchEvent(new window.KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
|
|
187
|
+
await Promise.resolve();
|
|
188
|
+
|
|
189
|
+
expect(modal.classList.contains('active')).toBe(false);
|
|
190
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
191
|
+
expect(fetchMock.mock.calls[0]?.[0]).toBe('/api/repo/clone-stream');
|
|
192
|
+
expect(String(fetchMock.mock.calls[0]?.[1]?.body)).toContain('https://github.com/7flash/jsx-ai.git');
|
|
193
|
+
expect(cloneStatus.style.display).toBe('block');
|
|
194
|
+
expect(cloneStatus.textContent).toContain('Cloning');
|
|
195
|
+
} finally {
|
|
196
|
+
fetchHandle.restore();
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test('repo card clone button starts clone-stream without opening the GitHub page', async () => {
|
|
201
|
+
const fetchMock = mock(async (input: string) => {
|
|
202
|
+
if (input.startsWith('/api/github/repos')) {
|
|
203
|
+
return new Response(JSON.stringify({
|
|
204
|
+
profile: {
|
|
205
|
+
login: '7flash',
|
|
206
|
+
name: '7flash',
|
|
207
|
+
public_repos: 1,
|
|
208
|
+
type: 'User',
|
|
209
|
+
avatar_url: 'https://example.com/avatar.png',
|
|
210
|
+
},
|
|
211
|
+
repos: [
|
|
212
|
+
{
|
|
213
|
+
name: 'gitmaps',
|
|
214
|
+
clone_url: 'https://github.com/7flash/gitmaps.git',
|
|
215
|
+
description: 'Spatial code explorer',
|
|
216
|
+
language: 'TypeScript',
|
|
217
|
+
size: 2048,
|
|
218
|
+
updated_at: new Date().toISOString(),
|
|
219
|
+
stars: 10,
|
|
220
|
+
},
|
|
221
|
+
],
|
|
222
|
+
page: 1,
|
|
223
|
+
hasNext: false,
|
|
224
|
+
hasPrev: false,
|
|
225
|
+
}), { headers: { 'Content-Type': 'application/json' } });
|
|
226
|
+
}
|
|
227
|
+
return new Promise<Response>(() => {});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
const openMock = mock(() => null as any);
|
|
231
|
+
const fetchHandle = installFetchMock(fetchMock as any);
|
|
232
|
+
const openHandle = installWindowOpenMock(openMock as any);
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
setupGithubImport({} as any);
|
|
236
|
+
|
|
237
|
+
const openBtn = document.getElementById('githubImportBtn') as HTMLButtonElement;
|
|
238
|
+
const userInput = document.getElementById('githubUserInput') as HTMLInputElement;
|
|
239
|
+
const searchBtn = document.getElementById('githubSearchBtn') as HTMLButtonElement;
|
|
240
|
+
const modal = document.getElementById('githubModal') as HTMLElement;
|
|
241
|
+
const cloneStatus = document.getElementById('cloneStatus') as HTMLElement;
|
|
242
|
+
|
|
243
|
+
openBtn.click();
|
|
244
|
+
userInput.value = '7flash';
|
|
245
|
+
searchBtn.click();
|
|
246
|
+
await Promise.resolve();
|
|
247
|
+
await Promise.resolve();
|
|
248
|
+
|
|
249
|
+
const cloneBtn = document.querySelector('.github-clone-btn[data-url="https://github.com/7flash/gitmaps.git"]') as HTMLButtonElement;
|
|
250
|
+
expect(cloneBtn).toBeTruthy();
|
|
251
|
+
|
|
252
|
+
cloneBtn.click();
|
|
253
|
+
await Promise.resolve();
|
|
254
|
+
|
|
255
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
256
|
+
expect(fetchMock.mock.calls[1]?.[0]).toBe('/api/repo/clone-stream');
|
|
257
|
+
expect(String(fetchMock.mock.calls[1]?.[1]?.body)).toContain('https://github.com/7flash/gitmaps.git');
|
|
258
|
+
expect(modal.classList.contains('active')).toBe(false);
|
|
259
|
+
expect(cloneStatus.style.display).toBe('block');
|
|
260
|
+
expect(cloneStatus.textContent).toContain('Cloning');
|
|
261
|
+
expect(openMock).not.toHaveBeenCalled();
|
|
262
|
+
} finally {
|
|
263
|
+
fetchHandle.restore();
|
|
264
|
+
openHandle.restore();
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test('cached clone response shows success and selects the cached repo path', async () => {
|
|
269
|
+
const onRepoReady = mock(() => undefined);
|
|
270
|
+
const ctx = {
|
|
271
|
+
actor: { send: mock(() => undefined) },
|
|
272
|
+
onRepoReady,
|
|
273
|
+
snap: () => ({ context: { repoPath: '', zoom: 1, offsetX: 0, offsetY: 0, commits: [] }, value: { view: 'allfiles' } }),
|
|
274
|
+
fileCards: new Map(),
|
|
275
|
+
deferredCards: new Map(),
|
|
276
|
+
changedFilePaths: new Set(),
|
|
277
|
+
positions: new Map(),
|
|
278
|
+
hiddenFiles: new Set(),
|
|
279
|
+
allFilesData: [],
|
|
280
|
+
commitFilesData: [],
|
|
281
|
+
canvas: document.createElement('div'),
|
|
282
|
+
canvasViewport: document.createElement('div'),
|
|
283
|
+
svgOverlay: null,
|
|
284
|
+
loadingOverlay: null,
|
|
285
|
+
} as any;
|
|
286
|
+
|
|
287
|
+
const fetchMock = mock(async (input: string) => {
|
|
288
|
+
if (input === '/api/repo/clone-stream') {
|
|
289
|
+
return new Response(JSON.stringify({ path: 'C:/Code/gitmaps' }), {
|
|
290
|
+
headers: { 'Content-Type': 'application/json' },
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
if (input === '/api/repo/load') {
|
|
294
|
+
return new Promise<Response>(() => {});
|
|
295
|
+
}
|
|
296
|
+
throw new Error(`Unexpected fetch: ${input}`);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
const fetchHandle = installFetchMock(fetchMock as any);
|
|
300
|
+
|
|
301
|
+
try {
|
|
302
|
+
setupGithubImport(ctx);
|
|
303
|
+
|
|
304
|
+
const openBtn = document.getElementById('githubImportBtn') as HTMLButtonElement;
|
|
305
|
+
const userInput = document.getElementById('githubUserInput') as HTMLInputElement;
|
|
306
|
+
const cloneBtn = document.getElementById('githubUrlCloneBtn') as HTMLButtonElement;
|
|
307
|
+
const cloneStatus = document.getElementById('cloneStatus') as HTMLElement;
|
|
308
|
+
const repoSelect = document.getElementById('repoSelect') as HTMLSelectElement;
|
|
309
|
+
|
|
310
|
+
openBtn.click();
|
|
311
|
+
userInput.value = 'https://github.com/7flash/gitmaps';
|
|
312
|
+
userInput.dispatchEvent(new window.Event('input', { bubbles: true }));
|
|
313
|
+
cloneBtn.click();
|
|
314
|
+
await Promise.resolve();
|
|
315
|
+
await Promise.resolve();
|
|
316
|
+
|
|
317
|
+
expect(cloneStatus.className).toContain('success');
|
|
318
|
+
expect(cloneStatus.textContent).toContain('Updated — loading');
|
|
319
|
+
expect(repoSelect.value).toBe('C:/Code/gitmaps');
|
|
320
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
321
|
+
expect(onRepoReady).toHaveBeenCalledWith('C:/Code/gitmaps');
|
|
322
|
+
} finally {
|
|
323
|
+
fetchHandle.restore();
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
test('SSE clone done updates success state and selects the cloned repo path', async () => {
|
|
328
|
+
const onRepoReady = mock(() => undefined);
|
|
329
|
+
const ctx = {
|
|
330
|
+
actor: { send: mock(() => undefined) },
|
|
331
|
+
onRepoReady,
|
|
332
|
+
snap: () => ({ context: { repoPath: '', zoom: 1, offsetX: 0, offsetY: 0, commits: [] }, value: { view: 'allfiles' } }),
|
|
333
|
+
fileCards: new Map(),
|
|
334
|
+
deferredCards: new Map(),
|
|
335
|
+
changedFilePaths: new Set(),
|
|
336
|
+
positions: new Map(),
|
|
337
|
+
hiddenFiles: new Set(),
|
|
338
|
+
allFilesData: [],
|
|
339
|
+
commitFilesData: [],
|
|
340
|
+
canvas: document.createElement('div'),
|
|
341
|
+
canvasViewport: document.createElement('div'),
|
|
342
|
+
svgOverlay: null,
|
|
343
|
+
loadingOverlay: null,
|
|
344
|
+
} as any;
|
|
345
|
+
|
|
346
|
+
const fetchMock = mock(async (input: string) => {
|
|
347
|
+
if (input === '/api/repo/clone-stream') {
|
|
348
|
+
return makeSseResponse([
|
|
349
|
+
'event: progress\ndata: {"message":"Resolving repository","percent":25}\n\n',
|
|
350
|
+
'event: done\ndata: {"path":"C:/Code/jsx-ai"}\n\n',
|
|
351
|
+
]);
|
|
352
|
+
}
|
|
353
|
+
if (input === '/api/repo/load') {
|
|
354
|
+
return new Promise<Response>(() => {});
|
|
355
|
+
}
|
|
356
|
+
throw new Error(`Unexpected fetch: ${input}`);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
const fetchHandle = installFetchMock(fetchMock as any);
|
|
360
|
+
|
|
361
|
+
try {
|
|
362
|
+
setupGithubImport(ctx);
|
|
363
|
+
|
|
364
|
+
const openBtn = document.getElementById('githubImportBtn') as HTMLButtonElement;
|
|
365
|
+
const userInput = document.getElementById('githubUserInput') as HTMLInputElement;
|
|
366
|
+
const cloneBtn = document.getElementById('githubUrlCloneBtn') as HTMLButtonElement;
|
|
367
|
+
const cloneStatus = document.getElementById('cloneStatus') as HTMLElement;
|
|
368
|
+
const repoSelect = document.getElementById('repoSelect') as HTMLSelectElement;
|
|
369
|
+
|
|
370
|
+
openBtn.click();
|
|
371
|
+
userInput.value = 'https://github.com/7flash/jsx-ai';
|
|
372
|
+
userInput.dispatchEvent(new window.Event('input', { bubbles: true }));
|
|
373
|
+
cloneBtn.click();
|
|
374
|
+
await Promise.resolve();
|
|
375
|
+
await Promise.resolve();
|
|
376
|
+
await Promise.resolve();
|
|
377
|
+
|
|
378
|
+
expect(cloneStatus.className).toContain('success');
|
|
379
|
+
expect(cloneStatus.textContent).toContain('Cloned — loading');
|
|
380
|
+
expect(repoSelect.value).toBe('C:/Code/jsx-ai');
|
|
381
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
382
|
+
expect(onRepoReady).toHaveBeenCalledWith('C:/Code/jsx-ai');
|
|
383
|
+
} finally {
|
|
384
|
+
fetchHandle.restore();
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
test('SSE clone error updates the clone status to an error state', async () => {
|
|
389
|
+
const fetchMock = mock(async (input: string) => {
|
|
390
|
+
if (input === '/api/repo/clone-stream') {
|
|
391
|
+
return makeSseResponse([
|
|
392
|
+
'event: progress\ndata: {"message":"Cloning","percent":40}\n\n',
|
|
393
|
+
'event: error\ndata: {"error":"Repository not found"}\n\n',
|
|
394
|
+
]);
|
|
395
|
+
}
|
|
396
|
+
throw new Error(`Unexpected fetch: ${input}`);
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
const fetchHandle = installFetchMock(fetchMock as any);
|
|
400
|
+
|
|
401
|
+
try {
|
|
402
|
+
setupGithubImport({} as any);
|
|
403
|
+
|
|
404
|
+
const openBtn = document.getElementById('githubImportBtn') as HTMLButtonElement;
|
|
405
|
+
const userInput = document.getElementById('githubUserInput') as HTMLInputElement;
|
|
406
|
+
const cloneBtn = document.getElementById('githubUrlCloneBtn') as HTMLButtonElement;
|
|
407
|
+
const cloneStatus = document.getElementById('cloneStatus') as HTMLElement;
|
|
408
|
+
|
|
409
|
+
openBtn.click();
|
|
410
|
+
userInput.value = 'https://github.com/7flash/missing-repo';
|
|
411
|
+
userInput.dispatchEvent(new window.Event('input', { bubbles: true }));
|
|
412
|
+
cloneBtn.click();
|
|
413
|
+
await Promise.resolve();
|
|
414
|
+
await Promise.resolve();
|
|
415
|
+
await Promise.resolve();
|
|
416
|
+
|
|
417
|
+
expect(cloneStatus.className).toContain('error');
|
|
418
|
+
expect(cloneStatus.textContent).toContain('Repository not found');
|
|
419
|
+
} finally {
|
|
420
|
+
fetchHandle.restore();
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
});
|
|
424
|
+
|
package/app/lib/global-search.ts
CHANGED
|
@@ -15,14 +15,27 @@ let _ctx: CanvasContext | null = null;
|
|
|
15
15
|
/** Toggle the search panel */
|
|
16
16
|
export function toggleGlobalSearch(ctx: CanvasContext) {
|
|
17
17
|
_ctx = ctx;
|
|
18
|
-
|
|
18
|
+
// Panel exists and is visible → close it
|
|
19
|
+
if (_panel && _panel.style.display !== 'none') {
|
|
19
20
|
closeSearch();
|
|
20
21
|
} else {
|
|
22
|
+
// Panel doesn't exist or is hidden → open/restore it
|
|
21
23
|
openSearch();
|
|
22
24
|
}
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
function openSearch() {
|
|
28
|
+
// If panel was hidden (not destroyed), restore it
|
|
29
|
+
if (_panel && _panel.style.display === 'none') {
|
|
30
|
+
_panel.style.display = 'flex';
|
|
31
|
+
document.addEventListener('keydown', _onEsc);
|
|
32
|
+
requestAnimationFrame(() => _panel?.classList.add('visible'));
|
|
33
|
+
// Re-focus search input
|
|
34
|
+
const input = _panel.querySelector('#gsSearchInput') as HTMLInputElement;
|
|
35
|
+
input?.focus();
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
26
39
|
if (_panel) return;
|
|
27
40
|
|
|
28
41
|
_panel = document.createElement('div');
|
|
@@ -82,10 +95,11 @@ export function closeSearch() {
|
|
|
82
95
|
if (!_panel) return;
|
|
83
96
|
document.removeEventListener('keydown', _onEsc);
|
|
84
97
|
_panel.classList.remove('visible');
|
|
85
|
-
//
|
|
98
|
+
// Hide instead of destroy — preserves query + results
|
|
86
99
|
setTimeout(() => {
|
|
87
|
-
_panel
|
|
88
|
-
|
|
100
|
+
if (_panel) {
|
|
101
|
+
_panel.style.display = 'none';
|
|
102
|
+
}
|
|
89
103
|
}, 200);
|
|
90
104
|
if (_abortController) { _abortController.abort(); _abortController = null; }
|
|
91
105
|
if (_searchTimeout) { clearTimeout(_searchTimeout); _searchTimeout = null; }
|
|
@@ -228,37 +242,44 @@ function getFileIcon(name: string): string {
|
|
|
228
242
|
return icons[ext] || '📄';
|
|
229
243
|
}
|
|
230
244
|
|
|
231
|
-
/**
|
|
245
|
+
/** Jump to a file on the canvas from search results */
|
|
232
246
|
function openFileFromSearch(filePath: string, line: number) {
|
|
233
247
|
if (!_ctx) return;
|
|
234
248
|
|
|
235
|
-
//
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
name: filePath.split('/').pop() || filePath,
|
|
239
|
-
content: '',
|
|
240
|
-
lines: 0,
|
|
241
|
-
};
|
|
249
|
+
// Jump to the file on the canvas (handles layer switching and centering)
|
|
250
|
+
import('./canvas').then(({ jumpToFile }) => {
|
|
251
|
+
jumpToFile(_ctx!, filePath);
|
|
242
252
|
|
|
243
|
-
|
|
244
|
-
import('./file-modal').then(({ openFileModal }) => {
|
|
245
|
-
openFileModal(_ctx!, file, 'edit');
|
|
246
|
-
// Scroll to line after editor loads
|
|
253
|
+
// After jump animation settles, scroll to the matching line
|
|
247
254
|
if (line > 1) {
|
|
248
255
|
setTimeout(() => {
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
});
|
|
256
|
+
const card = _ctx?.fileCards.get(filePath);
|
|
257
|
+
if (!card) return;
|
|
258
|
+
const body = card.querySelector('.file-card-body') as HTMLElement;
|
|
259
|
+
if (!body) return;
|
|
260
|
+
// Find the line element
|
|
261
|
+
const lineEl = body.querySelector(`[data-line="${line}"]`) as HTMLElement;
|
|
262
|
+
if (lineEl) {
|
|
263
|
+
lineEl.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
|
264
|
+
// Flash highlight
|
|
265
|
+
lineEl.style.background = 'rgba(124, 58, 237, 0.3)';
|
|
266
|
+
setTimeout(() => { lineEl.style.background = ''; }, 2000);
|
|
257
267
|
}
|
|
258
|
-
},
|
|
268
|
+
}, 600); // Wait for jump animation
|
|
259
269
|
}
|
|
260
270
|
});
|
|
261
271
|
|
|
262
|
-
//
|
|
263
|
-
|
|
272
|
+
// Hide panel but don't destroy — preserve state
|
|
273
|
+
if (_panel) {
|
|
274
|
+
_panel.classList.remove('visible');
|
|
275
|
+
_panel.style.pointerEvents = 'none';
|
|
276
|
+
_panel.style.opacity = '0';
|
|
277
|
+
setTimeout(() => {
|
|
278
|
+
if (_panel) {
|
|
279
|
+
_panel.style.display = 'none';
|
|
280
|
+
_panel.style.pointerEvents = '';
|
|
281
|
+
_panel.style.opacity = '';
|
|
282
|
+
}
|
|
283
|
+
}, 200);
|
|
284
|
+
}
|
|
264
285
|
}
|