gitmaps 1.1.0 → 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/README.md +267 -118
  2. package/app/[...slug]/page.client.tsx +1 -0
  3. package/app/[...slug]/page.tsx +6 -0
  4. package/app/analytics.db +0 -0
  5. package/app/api/analytics/route.ts +64 -0
  6. package/app/api/auth/positions/route.ts +95 -33
  7. package/app/api/build-info/route.ts +19 -0
  8. package/app/api/chat/route.ts +13 -2
  9. package/app/api/og-image/route.ts +14 -0
  10. package/app/api/repo/file-content/route.ts +73 -20
  11. package/app/api/repo/load/route.test.ts +62 -0
  12. package/app/api/repo/load/route.ts +41 -1
  13. package/app/api/repo/pdf-thumb/route.ts +127 -0
  14. package/app/api/repo/resolve-slug/route.ts +51 -0
  15. package/app/api/repo/tree/route.ts +188 -104
  16. package/app/api/version/route.ts +26 -0
  17. package/app/globals.css +5706 -4938
  18. package/app/layout.tsx +1279 -490
  19. package/app/lib/auto-arrange.test.ts +158 -0
  20. package/app/lib/auto-arrange.ts +147 -0
  21. package/app/lib/canvas-export.ts +358 -358
  22. package/app/lib/canvas.ts +625 -564
  23. package/app/lib/cards.tsx +1361 -916
  24. package/app/lib/chat.tsx +65 -9
  25. package/app/lib/code-editor.ts +86 -2
  26. package/app/lib/context.test.ts +32 -0
  27. package/app/lib/context.ts +19 -3
  28. package/app/lib/cursor-sharing.ts +34 -0
  29. package/app/lib/events.tsx +71 -93
  30. package/app/lib/export-canvas.ts +287 -0
  31. package/app/lib/file-card-plugin.ts +148 -148
  32. package/app/lib/file-modal.tsx +49 -0
  33. package/app/lib/file-preview.ts +486 -427
  34. package/app/lib/github-import.test.ts +424 -0
  35. package/app/lib/initial-route-hydration.test.ts +283 -0
  36. package/app/lib/initial-route-hydration.ts +202 -0
  37. package/app/lib/landing-reset.test.ts +99 -0
  38. package/app/lib/landing-reset.ts +106 -0
  39. package/app/lib/landing-shell.test.ts +75 -0
  40. package/app/lib/large-repo-optimization.ts +37 -0
  41. package/app/lib/layout-snapshots.ts +320 -0
  42. package/app/lib/loading.test.ts +69 -0
  43. package/app/lib/loading.tsx +160 -45
  44. package/app/lib/mount-cleanup.test.ts +52 -0
  45. package/app/lib/mount-cleanup.ts +34 -0
  46. package/app/lib/mount-init.test.ts +123 -0
  47. package/app/lib/mount-init.ts +107 -0
  48. package/app/lib/mount-lifecycle.test.ts +39 -0
  49. package/app/lib/mount-lifecycle.ts +12 -0
  50. package/app/lib/mount-route-wiring.test.ts +87 -0
  51. package/app/lib/mount-route-wiring.ts +84 -0
  52. package/app/lib/multi-repo.ts +14 -0
  53. package/app/lib/onboarding-tutorial.ts +278 -0
  54. package/app/lib/positions.ts +190 -121
  55. package/app/lib/recent-commits.test.ts +947 -0
  56. package/app/lib/recent-commits.ts +227 -0
  57. package/app/lib/repo-handoff.test.ts +23 -0
  58. package/app/lib/repo-handoff.ts +16 -0
  59. package/app/lib/repo-progressive.ts +119 -0
  60. package/app/lib/repo-select.test.ts +61 -0
  61. package/app/lib/repo-select.ts +74 -0
  62. package/app/lib/repo.tsx +1383 -987
  63. package/app/lib/role.ts +228 -0
  64. package/app/lib/route-catchall.test.ts +27 -0
  65. package/app/lib/route-repo-entry.test.ts +95 -0
  66. package/app/lib/route-repo-entry.ts +36 -0
  67. package/app/lib/router-contract.test.ts +22 -0
  68. package/app/lib/router-contract.ts +19 -0
  69. package/app/lib/shared-layout.test.ts +86 -0
  70. package/app/lib/shared-layout.ts +82 -0
  71. package/app/lib/status-bar.test.ts +118 -0
  72. package/app/lib/status-bar.ts +365 -128
  73. package/app/lib/sync-controls.test.ts +43 -0
  74. package/app/lib/sync-controls.tsx +303 -0
  75. package/app/lib/test-dom.ts +145 -0
  76. package/app/lib/test-fixtures/router-contract/[...slug]/page.tsx +3 -0
  77. package/app/lib/test-fixtures/router-contract/api/health/route.ts +3 -0
  78. package/app/lib/test-fixtures/router-contract/api/version/route.ts +3 -0
  79. package/app/lib/test-fixtures/router-contract/galaxy-canvas/page.tsx +3 -0
  80. package/app/lib/test-fixtures/router-contract/page.tsx +3 -0
  81. package/app/lib/transclusion-smoke.test.ts +163 -0
  82. package/app/lib/tutorial.ts +301 -0
  83. package/app/lib/version.ts +93 -0
  84. package/app/lib/viewport-culling.ts +740 -735
  85. package/app/lib/virtual-files.ts +456 -0
  86. package/app/lib/webgl-text.ts +189 -0
  87. package/app/lib/{galaxydraw-bridge.ts → xydraw-bridge.ts} +485 -482
  88. package/app/lib/{galaxydraw.test.ts → xydraw.test.ts} +228 -229
  89. package/app/og-image.png +0 -0
  90. package/app/page.client.tsx +70 -269
  91. package/app/page.tsx +15 -16
  92. package/app/state/machine.js +13 -0
  93. package/package.json +84 -75
  94. package/server.ts +10 -0
  95. package/app/[owner]/[repo]/page.tsx +0 -6
  96. package/app/[slug]/page.tsx +0 -6
  97. package/packages/galaxydraw/README.md +0 -296
  98. package/packages/galaxydraw/banner.png +0 -0
  99. package/packages/galaxydraw/demo/build-static.ts +0 -100
  100. package/packages/galaxydraw/demo/client.ts +0 -154
  101. package/packages/galaxydraw/demo/dist/client.js +0 -8
  102. package/packages/galaxydraw/demo/index.html +0 -256
  103. package/packages/galaxydraw/demo/server.ts +0 -96
  104. package/packages/galaxydraw/dist/index.js +0 -984
  105. package/packages/galaxydraw/dist/index.js.map +0 -16
  106. package/packages/galaxydraw/node_modules/.bin/tsc.bunx +0 -0
  107. package/packages/galaxydraw/node_modules/.bin/tsc.exe +0 -0
  108. package/packages/galaxydraw/node_modules/.bin/tsserver.bunx +0 -0
  109. package/packages/galaxydraw/node_modules/.bin/tsserver.exe +0 -0
  110. package/packages/galaxydraw/package.json +0 -49
  111. package/packages/galaxydraw/perf.test.ts +0 -284
  112. package/packages/galaxydraw/src/core/cards.ts +0 -435
  113. package/packages/galaxydraw/src/core/engine.ts +0 -339
  114. package/packages/galaxydraw/src/core/events.ts +0 -81
  115. package/packages/galaxydraw/src/core/layout.ts +0 -136
  116. package/packages/galaxydraw/src/core/minimap.ts +0 -216
  117. package/packages/galaxydraw/src/core/state.ts +0 -177
  118. package/packages/galaxydraw/src/core/viewport.ts +0 -106
  119. package/packages/galaxydraw/src/galaxydraw.css +0 -166
  120. package/packages/galaxydraw/src/index.ts +0 -40
  121. 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
+