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
package/app/lib/chat.tsx
CHANGED
|
@@ -24,9 +24,29 @@ interface ChatState {
|
|
|
24
24
|
const fileChatStates = new Map<string, ChatState>();
|
|
25
25
|
let canvasChatState: ChatState = { messages: [], isStreaming: false };
|
|
26
26
|
|
|
27
|
-
//
|
|
27
|
+
// Store parsed refactors to avoid passing huge string payloads via DOM attributes
|
|
28
|
+
const pendingRefactors = new Map<string, { path: string, content: string }>();
|
|
29
|
+
|
|
28
30
|
function renderChatContent(text: string): string {
|
|
29
31
|
let html = escapeHtml(text);
|
|
32
|
+
|
|
33
|
+
// Parse escaped <edit_file path="...">...</edit_file>
|
|
34
|
+
const editRegex = /<edit_file path="([^&]+)">([\s\S]*?)<\/edit_file>/g;
|
|
35
|
+
html = html.replace(editRegex, (_, path, content) => {
|
|
36
|
+
const decodedContent = content.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
37
|
+
const id = Math.random().toString(36).substring(7);
|
|
38
|
+
pendingRefactors.set(id, { path, content: decodedContent });
|
|
39
|
+
return `
|
|
40
|
+
<div class="chat-refactor-block" style="margin: 10px 0; border: 1px solid var(--border); border-radius: 6px; overflow: hidden;">
|
|
41
|
+
<div class="chat-refactor-header" style="background: var(--bg-card); padding: 4px 8px; font-size: 11px; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid var(--border);">
|
|
42
|
+
<span style="font-family: monospace;">📄 ${path}</span>
|
|
43
|
+
<button class="chat-refactor-apply btn-primary btn-xs" data-refactor-id="${id}" style="padding: 2px 8px; font-size: 10px; border-radius: 4px;">Apply Edit</button>
|
|
44
|
+
</div>
|
|
45
|
+
<pre class="chat-code-block" style="margin: 0; border-radius: 0; max-height: 200px; overflow-y: auto;"><code class="language-typescript">${content.trim()}</code></pre>
|
|
46
|
+
</div>
|
|
47
|
+
`;
|
|
48
|
+
});
|
|
49
|
+
|
|
30
50
|
html = html.replace(/```(\w+)?\n([\s\S]*?)```/g, (_, lang, code) =>
|
|
31
51
|
`<pre class="chat-code-block"><code class="language-${lang || 'text'}">${code.trim()}</code></pre>`);
|
|
32
52
|
html = html.replace(/`([^`]+)`/g, '<code class="chat-inline-code">$1</code>');
|
|
@@ -60,12 +80,47 @@ function EmptyChat() {
|
|
|
60
80
|
);
|
|
61
81
|
}
|
|
62
82
|
|
|
63
|
-
function MessageList({ messages, isTyping }: { messages: ChatMessage[]; isTyping: boolean }) {
|
|
83
|
+
function MessageList({ messages, isTyping, repoPath }: { messages: ChatMessage[]; isTyping: boolean; repoPath?: string }) {
|
|
64
84
|
if (messages.length === 0 && !isTyping) {
|
|
65
85
|
return <EmptyChat />;
|
|
66
86
|
}
|
|
87
|
+
|
|
88
|
+
const handleClick = async (e: any) => {
|
|
89
|
+
const btn = e.target.closest('.chat-refactor-apply');
|
|
90
|
+
if (!btn || btn.disabled) return;
|
|
91
|
+
|
|
92
|
+
const id = btn.getAttribute('data-refactor-id');
|
|
93
|
+
const refactor = pendingRefactors.get(id);
|
|
94
|
+
if (!refactor) return;
|
|
95
|
+
|
|
96
|
+
const actualRepoPath = repoPath || canvasChatState.canvasContext?.repoPath;
|
|
97
|
+
if (!actualRepoPath) {
|
|
98
|
+
btn.textContent = 'No Repo Context';
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
btn.disabled = true;
|
|
103
|
+
btn.textContent = 'Applying...';
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
const res = await fetch('/api/repo/file-save', {
|
|
107
|
+
method: 'POST',
|
|
108
|
+
body: JSON.stringify({ path: actualRepoPath, filePath: refactor.path, content: refactor.content })
|
|
109
|
+
});
|
|
110
|
+
if (res.ok) {
|
|
111
|
+
btn.textContent = 'Applied ✓';
|
|
112
|
+
btn.style.background = 'var(--accent-success, #10b981)';
|
|
113
|
+
btn.style.borderColor = 'var(--accent-success, #10b981)';
|
|
114
|
+
} else {
|
|
115
|
+
btn.textContent = 'Error';
|
|
116
|
+
}
|
|
117
|
+
} catch (err) {
|
|
118
|
+
btn.textContent = 'Failed';
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
67
122
|
return (
|
|
68
|
-
<div className="chat-message-list-inner">
|
|
123
|
+
<div className="chat-message-list-inner" onClick={handleClick}>
|
|
69
124
|
{messages.map((msg, i) => (
|
|
70
125
|
<div key={i} className={`chat-message chat-${msg.role}`}>
|
|
71
126
|
<div className="chat-message-role">{msg.role === 'user' ? 'You' : 'AI'}</div>
|
|
@@ -78,10 +133,10 @@ function MessageList({ messages, isTyping }: { messages: ChatMessage[]; isTyping
|
|
|
78
133
|
}
|
|
79
134
|
|
|
80
135
|
function ChatPanel({
|
|
81
|
-
containerId, title, messages, isTyping, onSend, onClose
|
|
136
|
+
containerId, title, messages, isTyping, repoPath, onSend, onClose
|
|
82
137
|
}: {
|
|
83
138
|
containerId: string; title: string; messages: ChatMessage[];
|
|
84
|
-
isTyping: boolean; onSend: (text: string) => void; onClose: () => void;
|
|
139
|
+
isTyping: boolean; repoPath?: string; onSend: (text: string) => void; onClose: () => void;
|
|
85
140
|
}) {
|
|
86
141
|
const handleSend = () => {
|
|
87
142
|
const input = document.getElementById(`${containerId}Input`) as HTMLTextAreaElement;
|
|
@@ -108,7 +163,7 @@ function ChatPanel({
|
|
|
108
163
|
<button className="chat-close btn-ghost btn-xs" onClick={onClose}>✕</button>
|
|
109
164
|
</div>
|
|
110
165
|
<div className="chat-messages" id={`${containerId}Messages`}>
|
|
111
|
-
<MessageList messages={messages} isTyping={isTyping} />
|
|
166
|
+
<MessageList messages={messages} isTyping={isTyping} repoPath={repoPath} />
|
|
112
167
|
</div>
|
|
113
168
|
<div className="chat-input-area">
|
|
114
169
|
<textarea
|
|
@@ -146,6 +201,7 @@ function renderChatPanel(container: HTMLElement, containerId: string, title: str
|
|
|
146
201
|
title={title}
|
|
147
202
|
messages={state.messages}
|
|
148
203
|
isTyping={state.isStreaming && state.messages[state.messages.length - 1]?.content === ''}
|
|
204
|
+
repoPath={state.fileContext?.repoPath || state.canvasContext?.repoPath}
|
|
149
205
|
onSend={onSend}
|
|
150
206
|
onClose={onClose}
|
|
151
207
|
/>,
|
|
@@ -240,16 +296,16 @@ async function streamResponse(
|
|
|
240
296
|
}
|
|
241
297
|
|
|
242
298
|
// ─── Open file chat in modal ────────────────────────────
|
|
243
|
-
export function openFileChatInModal(filePath: string, content: string, status: string, diff?: string) {
|
|
299
|
+
export function openFileChatInModal(repoPath: string, filePath: string, content: string, status: string, diff?: string) {
|
|
244
300
|
measure('chat:openFileChat', () => {
|
|
245
301
|
if (!fileChatStates.has(filePath)) {
|
|
246
302
|
fileChatStates.set(filePath, {
|
|
247
303
|
messages: [], isStreaming: false,
|
|
248
|
-
fileContext: { path: filePath, content, status, diff },
|
|
304
|
+
fileContext: { repoPath, path: filePath, content, status, diff },
|
|
249
305
|
});
|
|
250
306
|
}
|
|
251
307
|
const state = fileChatStates.get(filePath)!;
|
|
252
|
-
state.fileContext = { path: filePath, content, status, diff };
|
|
308
|
+
state.fileContext = { repoPath, path: filePath, content, status, diff };
|
|
253
309
|
|
|
254
310
|
let chatContainer = document.getElementById('modalChatContainer');
|
|
255
311
|
if (!chatContainer) return;
|
package/app/lib/code-editor.ts
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* Creates a CodeMirror 6 editor instance with syntax highlighting,
|
|
5
5
|
* dark theme, and integration with the save/commit workflow.
|
|
6
6
|
*/
|
|
7
|
-
import { EditorView, keymap, lineNumbers, highlightActiveLine, highlightActiveLineGutter, drawSelection, rectangularSelection, highlightSpecialChars, ViewPlugin, ViewUpdate } from '@codemirror/view';
|
|
8
|
-
import { EditorState, Compartment } from '@codemirror/state';
|
|
7
|
+
import { EditorView, keymap, lineNumbers, highlightActiveLine, highlightActiveLineGutter, drawSelection, rectangularSelection, highlightSpecialChars, ViewPlugin, ViewUpdate, Decoration, DecorationSet, WidgetType } from '@codemirror/view';
|
|
8
|
+
import { EditorState, Compartment, StateField, StateEffect } from '@codemirror/state';
|
|
9
9
|
import { syntaxHighlighting, defaultHighlightStyle, indentOnInput, bracketMatching, foldGutter, foldKeymap, HighlightStyle } from '@codemirror/language';
|
|
10
10
|
import { defaultKeymap, indentWithTab, history, historyKeymap } from '@codemirror/commands';
|
|
11
11
|
import { searchKeymap, highlightSelectionMatches } from '@codemirror/search';
|
|
@@ -219,6 +219,81 @@ function getLanguageExtension(ext: string) {
|
|
|
219
219
|
}
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
+
// ─── Remote Cursors ─────────────────────────────────────
|
|
223
|
+
export const remoteCursorsEffect = StateEffect.define<any[]>();
|
|
224
|
+
|
|
225
|
+
class RemoteCursorWidget extends WidgetType {
|
|
226
|
+
constructor(readonly color: string, readonly name: string, readonly typing: boolean) { super(); }
|
|
227
|
+
toDOM() {
|
|
228
|
+
const dec = document.createElement("span");
|
|
229
|
+
dec.style.position = "relative";
|
|
230
|
+
dec.style.borderLeft = `2px solid ${this.color}`;
|
|
231
|
+
dec.style.marginLeft = "-1px";
|
|
232
|
+
dec.style.marginRight = "-1px";
|
|
233
|
+
dec.style.zIndex = "10";
|
|
234
|
+
if (this.typing) {
|
|
235
|
+
dec.style.opacity = "0.7";
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const tooltip = document.createElement("div");
|
|
239
|
+
tooltip.textContent = this.name + (this.typing ? '...' : '');
|
|
240
|
+
tooltip.style.position = "absolute";
|
|
241
|
+
tooltip.style.top = "-1.5em";
|
|
242
|
+
tooltip.style.left = "-2px";
|
|
243
|
+
tooltip.style.padding = "1px 4px";
|
|
244
|
+
tooltip.style.borderRadius = "3px";
|
|
245
|
+
tooltip.style.fontSize = "10px";
|
|
246
|
+
tooltip.style.color = "white";
|
|
247
|
+
tooltip.style.whiteSpace = "nowrap";
|
|
248
|
+
tooltip.style.pointerEvents = "none";
|
|
249
|
+
tooltip.style.zIndex = "10";
|
|
250
|
+
tooltip.style.backgroundColor = this.color;
|
|
251
|
+
|
|
252
|
+
dec.appendChild(tooltip);
|
|
253
|
+
return dec;
|
|
254
|
+
}
|
|
255
|
+
ignoreEvent() { return true; }
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const remoteCursorsField = StateField.define<DecorationSet>({
|
|
259
|
+
create() { return Decoration.none; },
|
|
260
|
+
update(decorations, tr) {
|
|
261
|
+
decorations = decorations.map(tr.changes);
|
|
262
|
+
for (let e of tr.effects) {
|
|
263
|
+
if (e.is(remoteCursorsEffect)) {
|
|
264
|
+
let builders: any[] = [];
|
|
265
|
+
for (let cursor of e.value) {
|
|
266
|
+
if (cursor.selections) {
|
|
267
|
+
for (let sel of cursor.selections) {
|
|
268
|
+
let from = Math.min(sel.anchor, sel.head);
|
|
269
|
+
let to = Math.max(sel.anchor, sel.head);
|
|
270
|
+
from = Math.max(0, Math.min(from, tr.state.doc.length));
|
|
271
|
+
to = Math.max(0, Math.min(to, tr.state.doc.length));
|
|
272
|
+
|
|
273
|
+
if (from !== to) {
|
|
274
|
+
builders.push(Decoration.mark({
|
|
275
|
+
attributes: { style: `background-color: ${cursor.color}40` } // 25% opacity
|
|
276
|
+
}).range(from, to));
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
let head = Math.max(0, Math.min(sel.head, tr.state.doc.length));
|
|
280
|
+
builders.push(Decoration.widget({
|
|
281
|
+
widget: new RemoteCursorWidget(cursor.color, cursor.name, cursor.typing || false),
|
|
282
|
+
side: 1
|
|
283
|
+
}).range(head));
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
// Sort builders by start position
|
|
288
|
+
builders.sort((a, b) => a.from - b.from);
|
|
289
|
+
decorations = Decoration.set(builders, true);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return decorations;
|
|
293
|
+
},
|
|
294
|
+
provide: f => EditorView.decorations.from(f)
|
|
295
|
+
});
|
|
296
|
+
|
|
222
297
|
// ─── Create Editor ──────────────────────────────────────
|
|
223
298
|
export interface EditorInstance {
|
|
224
299
|
view: EditorView;
|
|
@@ -226,6 +301,7 @@ export interface EditorInstance {
|
|
|
226
301
|
setContent: (content: string) => void;
|
|
227
302
|
getCursorPosition: () => { line: number; col: number };
|
|
228
303
|
scrollToLine: (line: number) => void;
|
|
304
|
+
setRemoteCursors: (cursors: any[]) => void;
|
|
229
305
|
destroy: () => void;
|
|
230
306
|
focus: () => void;
|
|
231
307
|
}
|
|
@@ -238,6 +314,7 @@ export function createCodeEditor(
|
|
|
238
314
|
onSave?: () => void;
|
|
239
315
|
onChange?: (content: string) => void;
|
|
240
316
|
onCursorMove?: (line: number, col: number) => void;
|
|
317
|
+
onSelectionChange?: (selections: { anchor: number; head: number }[], isTyping: boolean) => void;
|
|
241
318
|
} = {}
|
|
242
319
|
): EditorInstance {
|
|
243
320
|
const langCompartment = new Compartment();
|
|
@@ -280,6 +357,7 @@ export function createCodeEditor(
|
|
|
280
357
|
const pos = update.state.selection.main.head;
|
|
281
358
|
const line = update.state.doc.lineAt(pos);
|
|
282
359
|
options.onCursorMove?.(line.number, pos - line.from + 1);
|
|
360
|
+
options.onSelectionChange?.(update.state.selection.ranges.map(r => ({ anchor: r.anchor, head: r.head })), update.docChanged);
|
|
283
361
|
}
|
|
284
362
|
if (update.docChanged) {
|
|
285
363
|
options.onChange?.(update.state.doc.toString());
|
|
@@ -296,6 +374,7 @@ export function createCodeEditor(
|
|
|
296
374
|
}),
|
|
297
375
|
EditorView.lineWrapping,
|
|
298
376
|
minimapExtension(),
|
|
377
|
+
remoteCursorsField,
|
|
299
378
|
];
|
|
300
379
|
|
|
301
380
|
if (langExt) {
|
|
@@ -336,6 +415,11 @@ export function createCodeEditor(
|
|
|
336
415
|
scrollIntoView: true,
|
|
337
416
|
});
|
|
338
417
|
},
|
|
418
|
+
setRemoteCursors: (cursors: any[]) => {
|
|
419
|
+
view.dispatch({
|
|
420
|
+
effects: remoteCursorsEffect.of(cursors)
|
|
421
|
+
});
|
|
422
|
+
},
|
|
339
423
|
destroy: () => view.destroy(),
|
|
340
424
|
focus: () => view.focus(),
|
|
341
425
|
};
|
package/app/lib/connections.tsx
CHANGED
|
@@ -824,59 +824,50 @@ export function navigateToConnection(ctx: CanvasContext, conn: any, navigateTo:
|
|
|
824
824
|
});
|
|
825
825
|
}
|
|
826
826
|
|
|
827
|
-
// ─── Save connections to
|
|
828
|
-
export
|
|
827
|
+
// ─── Save connections to localStorage ───────────────────
|
|
828
|
+
export function saveConnections(ctx: CanvasContext) {
|
|
829
829
|
const state = ctx.snap().context;
|
|
830
|
+
const repoPath = state.repoPath;
|
|
831
|
+
if (!repoPath) return;
|
|
830
832
|
try {
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
headers: { 'Content-Type': 'application/json' },
|
|
834
|
-
body: JSON.stringify({ connections: state.connections })
|
|
835
|
-
});
|
|
833
|
+
const key = `gitcanvas:connections:${repoPath}`;
|
|
834
|
+
localStorage.setItem(key, JSON.stringify(state.connections));
|
|
836
835
|
} catch (e) {
|
|
837
836
|
measure('connections:saveError', () => e);
|
|
838
837
|
}
|
|
839
838
|
}
|
|
840
839
|
|
|
841
|
-
// ─── Load connections from
|
|
842
|
-
export
|
|
843
|
-
return measure('connections:load',
|
|
840
|
+
// ─── Load connections from localStorage ─────────────────
|
|
841
|
+
export function loadConnections(ctx: CanvasContext) {
|
|
842
|
+
return measure('connections:load', () => {
|
|
844
843
|
try {
|
|
845
|
-
const
|
|
846
|
-
if (!
|
|
847
|
-
const
|
|
848
|
-
|
|
849
|
-
if (
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
});
|
|
868
|
-
ctx.actor.send({
|
|
869
|
-
type: 'COMPLETE_CONNECTION',
|
|
870
|
-
targetFile: conn.targetFile,
|
|
871
|
-
lineStart: conn.targetLineStart,
|
|
872
|
-
lineEnd: conn.targetLineEnd,
|
|
873
|
-
comment: conn.comment,
|
|
874
|
-
});
|
|
844
|
+
const repoPath = ctx.snap().context.repoPath;
|
|
845
|
+
if (!repoPath) return;
|
|
846
|
+
const key = `gitcanvas:connections:${repoPath}`;
|
|
847
|
+
const stored = localStorage.getItem(key);
|
|
848
|
+
if (!stored) return;
|
|
849
|
+
|
|
850
|
+
const connections = JSON.parse(stored);
|
|
851
|
+
if (!Array.isArray(connections) || connections.length === 0) return;
|
|
852
|
+
|
|
853
|
+
connections.forEach(conn => {
|
|
854
|
+
ctx.actor.send({
|
|
855
|
+
type: 'START_CONNECTION',
|
|
856
|
+
sourceFile: conn.sourceFile,
|
|
857
|
+
lineStart: conn.sourceLineStart,
|
|
858
|
+
lineEnd: conn.sourceLineEnd,
|
|
859
|
+
});
|
|
860
|
+
ctx.actor.send({
|
|
861
|
+
type: 'COMPLETE_CONNECTION',
|
|
862
|
+
targetFile: conn.targetFile,
|
|
863
|
+
lineStart: conn.targetLineStart,
|
|
864
|
+
lineEnd: conn.targetLineEnd,
|
|
865
|
+
comment: conn.comment || '',
|
|
875
866
|
});
|
|
867
|
+
});
|
|
876
868
|
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
}
|
|
869
|
+
renderConnections(ctx);
|
|
870
|
+
buildConnectionMarkers(ctx);
|
|
880
871
|
} catch (e) {
|
|
881
872
|
measure('connections:loadError', () => e);
|
|
882
873
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { createCanvasContext, getCanvasContext, setCanvasContext } from './context';
|
|
3
|
+
import { setupDomTest } from './test-dom';
|
|
4
|
+
|
|
5
|
+
describe('shared canvas context lifecycle', () => {
|
|
6
|
+
test('createCanvasContext registers the created context globally', () => {
|
|
7
|
+
const handle = setupDomTest();
|
|
8
|
+
try {
|
|
9
|
+
const actor = { getSnapshot: () => ({ context: {} }) };
|
|
10
|
+
const ctx = createCanvasContext(actor);
|
|
11
|
+
expect(getCanvasContext()).toBe(ctx);
|
|
12
|
+
expect(ctx.snap()).toEqual({ context: {} });
|
|
13
|
+
} finally {
|
|
14
|
+
setCanvasContext(null);
|
|
15
|
+
handle.cleanup();
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('setCanvasContext clears the shared context reference', () => {
|
|
20
|
+
const handle = setupDomTest();
|
|
21
|
+
try {
|
|
22
|
+
const actor = { getSnapshot: () => ({ context: { repoPath: 'x' } }) };
|
|
23
|
+
createCanvasContext(actor);
|
|
24
|
+
expect(getCanvasContext()).not.toBeNull();
|
|
25
|
+
setCanvasContext(null);
|
|
26
|
+
expect(getCanvasContext()).toBeNull();
|
|
27
|
+
} finally {
|
|
28
|
+
setCanvasContext(null);
|
|
29
|
+
handle.cleanup();
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
});
|
package/app/lib/context.ts
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
* Every module gets read/write access to the same mutable state.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
let currentCanvasContext: CanvasContext | null = null;
|
|
10
|
+
|
|
9
11
|
export interface CanvasContext {
|
|
10
12
|
/** XState actor */
|
|
11
13
|
actor: any;
|
|
@@ -41,7 +43,10 @@ export interface CanvasContext {
|
|
|
41
43
|
loadingOverlay: HTMLElement | null;
|
|
42
44
|
|
|
43
45
|
// ─── Text rendering mode ──────────────────
|
|
44
|
-
|
|
46
|
+
// 'dom' = DOM-based rendering (default, best compatibility)
|
|
47
|
+
// 'canvas' = Canvas 2D API (better performance for medium repos)
|
|
48
|
+
// 'webgl' = Pixi.js WebGL (best performance for 1000+ files)
|
|
49
|
+
textRendererMode: 'dom' | 'canvas' | 'webgl';
|
|
45
50
|
|
|
46
51
|
// ─── All-files mode state ─────────────────
|
|
47
52
|
allFilesActive: boolean;
|
|
@@ -62,7 +67,7 @@ export interface CanvasContext {
|
|
|
62
67
|
|
|
63
68
|
/** Creates a fresh context (call once per mount). */
|
|
64
69
|
export function createCanvasContext(actor: any): CanvasContext {
|
|
65
|
-
|
|
70
|
+
const ctx: CanvasContext = {
|
|
66
71
|
actor,
|
|
67
72
|
snap: () => actor.getSnapshot(),
|
|
68
73
|
|
|
@@ -82,7 +87,7 @@ export function createCanvasContext(actor: any): CanvasContext {
|
|
|
82
87
|
scrollTimers: {},
|
|
83
88
|
connectionDragState: null,
|
|
84
89
|
loadingOverlay: null,
|
|
85
|
-
|
|
90
|
+
textRendererMode: (localStorage.getItem('gitcanvas:textRendererMode') as any) || 'dom',
|
|
86
91
|
|
|
87
92
|
allFilesActive: true,
|
|
88
93
|
changedFilePaths: new Set(),
|
|
@@ -91,4 +96,15 @@ export function createCanvasContext(actor: any): CanvasContext {
|
|
|
91
96
|
deferredCards: new Map(),
|
|
92
97
|
controlMode: (localStorage.getItem('gitcanvas:controlMode') as any) || 'advanced',
|
|
93
98
|
};
|
|
99
|
+
|
|
100
|
+
currentCanvasContext = ctx;
|
|
101
|
+
return ctx;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function setCanvasContext(ctx: CanvasContext | null): void {
|
|
105
|
+
currentCanvasContext = ctx;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function getCanvasContext(): CanvasContext | null {
|
|
109
|
+
return currentCanvasContext;
|
|
94
110
|
}
|
|
@@ -34,6 +34,36 @@ const REMOVE_MS = 15000; // remove after 15s
|
|
|
34
34
|
|
|
35
35
|
let _lastBroadcast = 0;
|
|
36
36
|
|
|
37
|
+
// ─── Editor Sync Events ─────────────────────────────────
|
|
38
|
+
export interface EditorSyncData {
|
|
39
|
+
peerId: string;
|
|
40
|
+
color: string;
|
|
41
|
+
name: string;
|
|
42
|
+
file: string;
|
|
43
|
+
selections: { anchor: number; head: number }[];
|
|
44
|
+
typing?: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
type EditorSyncListener = (data: EditorSyncData) => void;
|
|
48
|
+
const _editorSyncListeners = new Set<EditorSyncListener>();
|
|
49
|
+
|
|
50
|
+
export function onEditorSync(listener: EditorSyncListener) {
|
|
51
|
+
_editorSyncListeners.add(listener);
|
|
52
|
+
return () => _editorSyncListeners.delete(listener);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function broadcastEditorSync(file: string, selections: { anchor: number; head: number }[], typing: boolean = false) {
|
|
56
|
+
if (!_ws || _ws.readyState !== WebSocket.OPEN) return;
|
|
57
|
+
const userName = localStorage.getItem('gitcanvas:username') || _peerId || 'anonymous';
|
|
58
|
+
_ws.send(JSON.stringify({
|
|
59
|
+
type: 'editor_sync',
|
|
60
|
+
name: userName,
|
|
61
|
+
file,
|
|
62
|
+
selections,
|
|
63
|
+
typing
|
|
64
|
+
}));
|
|
65
|
+
}
|
|
66
|
+
|
|
37
67
|
// ─── Initialize ─────────────────────────────────────────
|
|
38
68
|
|
|
39
69
|
export function initCursorSharing(ctx: CanvasContext) {
|
|
@@ -111,6 +141,10 @@ function connectWebSocket() {
|
|
|
111
141
|
_color = data.color;
|
|
112
142
|
} else if (data.type === 'cursor') {
|
|
113
143
|
handleRemoteCursor(data);
|
|
144
|
+
} else if (data.type === 'editor_sync') {
|
|
145
|
+
for (const listener of _editorSyncListeners) {
|
|
146
|
+
listener(data);
|
|
147
|
+
}
|
|
114
148
|
} else if (data.type === 'leave') {
|
|
115
149
|
removeRemoteCursor(data.peerId);
|
|
116
150
|
}
|