Skip to content

Commit b095e62

Browse files
committed
WIP
1 parent e771565 commit b095e62

File tree

8 files changed

+240
-56
lines changed

8 files changed

+240
-56
lines changed

apps/web/client/public/onlook-preload-script.js

Lines changed: 18 additions & 20 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/web/client/src/app/test-fs/page.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
import { useEffect, useState } from 'react';
44

5-
import { type FileChangeEvent, type FileEntry } from '@onlook/file-system';
5+
import { type FileChangeEvent, type FileEntry } from '@onlook/file-system';
6+
import { FileSystem } from '@onlook/file-system';
67
import { useDirectory, useFile, useFS } from '@onlook/file-system/hooks';
78
import { Button } from '@onlook/ui/button';
89

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
'use client';
2+
3+
import { useState, useEffect } from 'react';
4+
import { Button } from '@onlook/ui/button';
5+
import { Badge } from '@onlook/ui/badge';
6+
import { Save } from 'lucide-react';
7+
8+
interface FileEditorProps {
9+
fileName: string | null;
10+
content: string | null;
11+
isLoading?: boolean;
12+
isBinary?: boolean;
13+
onSave?: (content: string) => Promise<void>;
14+
}
15+
16+
export function FileEditor({
17+
fileName,
18+
content,
19+
isLoading,
20+
isBinary,
21+
onSave
22+
}: FileEditorProps) {
23+
const [editContent, setEditContent] = useState(content || '');
24+
const [isSaving, setIsSaving] = useState(false);
25+
const [hasChanges, setHasChanges] = useState(false);
26+
27+
useEffect(() => {
28+
setEditContent(content || '');
29+
setHasChanges(false);
30+
}, [content]);
31+
32+
const handleContentChange = (value: string) => {
33+
setEditContent(value);
34+
setHasChanges(value !== content);
35+
};
36+
37+
const handleSave = async () => {
38+
if (!onSave || !hasChanges) return;
39+
40+
setIsSaving(true);
41+
try {
42+
await onSave(editContent);
43+
setHasChanges(false);
44+
} catch (error) {
45+
console.error('Failed to save file:', error);
46+
} finally {
47+
setIsSaving(false);
48+
}
49+
};
50+
51+
// Handle Cmd+S / Ctrl+S
52+
useEffect(() => {
53+
const handleKeyDown = (e: KeyboardEvent) => {
54+
if ((e.metaKey || e.ctrlKey) && e.key === 's' && hasChanges && !isBinary) {
55+
e.preventDefault();
56+
handleSave();
57+
}
58+
};
59+
60+
window.addEventListener('keydown', handleKeyDown);
61+
return () => window.removeEventListener('keydown', handleKeyDown);
62+
}, [hasChanges, editContent]);
63+
64+
if (!fileName) {
65+
return (
66+
<div className="h-full flex items-center justify-center text-gray-500">
67+
<p className="text-sm">Select a file to view its contents</p>
68+
</div>
69+
);
70+
}
71+
72+
const lines = editContent.split('\n');
73+
74+
return (
75+
<div className="h-full flex flex-col">
76+
<div className="px-4 py-3 border-b border-gray-800 flex items-center justify-between">
77+
<h3 className="text-sm font-mono text-gray-100 truncate">{fileName}</h3>
78+
<div className="flex items-center gap-2">
79+
{!isBinary && content !== null && (
80+
<Badge variant="secondary" className="text-xs">
81+
{lines.length} lines
82+
</Badge>
83+
)}
84+
{isBinary && (
85+
<Badge variant="secondary" className="text-xs">
86+
Binary
87+
</Badge>
88+
)}
89+
{!isBinary && hasChanges && (
90+
<Button
91+
onClick={handleSave}
92+
size="sm"
93+
className="h-7 px-2"
94+
disabled={isSaving}
95+
>
96+
<Save className="h-3 w-3 mr-1" />
97+
{isSaving ? 'Saving...' : 'Save'}
98+
</Button>
99+
)}
100+
</div>
101+
</div>
102+
103+
<div className="flex-1 overflow-hidden">
104+
{isLoading ? (
105+
<div className="p-4 space-y-2 animate-pulse">
106+
<div className="h-4 w-full bg-gray-800 rounded" />
107+
<div className="h-4 w-3/4 bg-gray-800 rounded" />
108+
<div className="h-4 w-5/6 bg-gray-800 rounded" />
109+
<div className="h-4 w-2/3 bg-gray-800 rounded" />
110+
<div className="h-4 w-4/5 bg-gray-800 rounded" />
111+
</div>
112+
) : isBinary ? (
113+
<div className="p-4">
114+
<p className="text-sm text-gray-500 italic">Binary file content not displayed</p>
115+
</div>
116+
) : (
117+
<div className="h-full w-full overflow-auto bg-gray-950 p-4">
118+
<textarea
119+
value={editContent}
120+
onChange={(e) => handleContentChange(e.target.value)}
121+
className="min-h-full min-w-full resize-none border-0 bg-transparent font-mono text-xs text-gray-300 outline-none"
122+
placeholder="Enter file content..."
123+
spellCheck={false}
124+
style={{
125+
lineHeight: '1.25rem',
126+
whiteSpace: 'pre',
127+
overflowWrap: 'normal'
128+
}}
129+
/>
130+
</div>
131+
)}
132+
</div>
133+
</div>
134+
);
135+
}

apps/web/client/src/app/test-sync-engine/_components/file-explorer.tsx

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -29,23 +29,25 @@ export function FileExplorer({
2929
}: FileExplorerProps) {
3030
return (
3131
<div className="h-full flex flex-col">
32-
<div className="px-4 py-3 border-b border-gray-800">
32+
<div className="px-4 py-3 border-b border-gray-800 flex-shrink-0">
3333
<h3 className="text-sm font-semibold text-gray-100">{title}</h3>
3434
</div>
3535

36-
<ScrollArea className="flex-1">
37-
<div className="p-2">
38-
{files.length === 0 ? (
39-
<p className="text-sm text-gray-500 px-2 py-4">{emptyMessage}</p>
40-
) : (
41-
<FileTree
42-
nodes={files}
43-
selectedPath={selectedPath}
44-
onSelectFile={onSelectFile}
45-
/>
46-
)}
47-
</div>
48-
</ScrollArea>
36+
<div className="flex-1 overflow-hidden">
37+
<ScrollArea className="h-full">
38+
<div className="p-2">
39+
{files.length === 0 ? (
40+
<p className="text-sm text-gray-500 px-2 py-4">{emptyMessage}</p>
41+
) : (
42+
<FileTree
43+
nodes={files}
44+
selectedPath={selectedPath}
45+
onSelectFile={onSelectFile}
46+
/>
47+
)}
48+
</div>
49+
</ScrollArea>
50+
</div>
4951
</div>
5052
);
5153
}

apps/web/client/src/app/test-sync-engine/page.tsx

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { useSyncEngine } from '@/services/sync-engine/useSyncEngine';
1616
import { api } from '@/trpc/react';
1717
import { FileExplorer } from './_components/file-explorer';
1818
import { FileViewerWithLineNumbers } from './_components/file-viewer';
19+
import { FileEditor } from './_components/file-editor';
1920
import { SandboxManager } from './_components/sandbox-manager';
2021

2122
// Test project configuration
@@ -287,7 +288,19 @@ export default function TestSyncEnginePage() {
287288
setIsLoadingSandboxContent(true);
288289
try {
289290
const content = await session.fs.readFile(selectedSandboxFile);
290-
if (typeof content === 'string') {
291+
// CodeSandbox SDK returns Buffer/Uint8Array for all files
292+
// We need to convert to string for text files
293+
if (content instanceof Uint8Array || content instanceof ArrayBuffer) {
294+
// Try to decode as UTF-8 text
295+
try {
296+
const decoder = new TextDecoder('utf-8', { fatal: true });
297+
const text = decoder.decode(content);
298+
setSandboxFileContent(text);
299+
} catch (e) {
300+
// If decoding fails, it's a binary file
301+
setSandboxFileContent(null);
302+
}
303+
} else if (typeof content === 'string') {
291304
setSandboxFileContent(content);
292305
} else {
293306
setSandboxFileContent(null);
@@ -339,6 +352,34 @@ export default function TestSyncEnginePage() {
339352

340353
const isBinaryFile = (content: string | null) => content === null;
341354

355+
// Save local file
356+
const handleSaveLocalFile = async (content: string) => {
357+
if (!fs || !selectedLocalFile) return;
358+
359+
try {
360+
await fs.writeFile(selectedLocalFile, content);
361+
// Reload the content to confirm save
362+
await loadLocalFileContent();
363+
} catch (error) {
364+
console.error('Failed to save file:', error);
365+
throw error;
366+
}
367+
};
368+
369+
// Save sandbox file
370+
const handleSaveSandboxFile = async (content: string) => {
371+
if (!session || !selectedSandboxFile) return;
372+
373+
try {
374+
await session.fs.writeTextFile(selectedSandboxFile, content);
375+
// Reload the content to confirm save
376+
await loadSandboxFileContent();
377+
} catch (error) {
378+
console.error('Failed to save sandbox file:', error);
379+
throw error;
380+
}
381+
};
382+
342383
return (
343384
<div className="flex h-screen flex-col bg-gray-950">
344385
{/* Header */}
@@ -411,11 +452,12 @@ export default function TestSyncEnginePage() {
411452
</div>
412453

413454
<div className="flex-1 overflow-hidden">
414-
<FileViewerWithLineNumbers
455+
<FileEditor
415456
fileName={selectedSandboxFile}
416457
content={sandboxFileContent}
417458
isLoading={isLoadingSandboxContent}
418459
isBinary={isBinaryFile(sandboxFileContent)}
460+
onSave={handleSaveSandboxFile}
419461
/>
420462
</div>
421463
</div>
@@ -433,11 +475,12 @@ export default function TestSyncEnginePage() {
433475
</div>
434476

435477
<div className="flex-1 overflow-hidden">
436-
<FileViewerWithLineNumbers
478+
<FileEditor
437479
fileName={selectedLocalFile}
438480
content={localFileContent}
439481
isLoading={isLoadingLocalContent}
440482
isBinary={isBinaryFile(localFileContent)}
483+
onSave={handleSaveLocalFile}
441484
/>
442485
</div>
443486
</div>

apps/web/client/src/services/sync-engine/sync-engine.ts

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ export class CodeSandboxSync {
3232
this.excludePatterns = this.excludes.map((dir) => `${dir}/**`);
3333
}
3434

35-
3635
async start(): Promise<void> {
3736
if (this.isRunning) {
3837
return;
@@ -121,11 +120,7 @@ export class CodeSandboxSync {
121120
// Check if path matches any exclude pattern
122121
const isExcluded = this.excludes.some((exc) => {
123122
// Check if path is within excluded directory or is the excluded item itself
124-
return (
125-
path === exc ||
126-
path.startsWith(`${exc}/`) ||
127-
path.split('/').includes(exc)
128-
);
123+
return path === exc || path.startsWith(`${exc}/`) || path.split('/').includes(exc);
129124
});
130125

131126
if (isExcluded) {
@@ -212,13 +207,20 @@ export class CodeSandboxSync {
212207
switch (type) {
213208
case 'create':
214209
case 'update': {
215-
// Read from local and write to sandbox
216-
const content = await this.fs.readFile(path);
217-
// CodeSandbox SDK has separate methods for text vs binary
218-
if (typeof content === 'string') {
219-
await this.session.fs.writeTextFile(sandboxPath, content);
210+
// Check if it's a directory
211+
const fileInfo = await this.fs.getInfo(path);
212+
if (fileInfo.isDirectory) {
213+
// Create directory in sandbox
214+
await this.session.fs.mkdir(sandboxPath, { recursive: true });
220215
} else {
221-
await this.session.fs.writeFile(sandboxPath, content);
216+
// Read from local and write to sandbox
217+
const content = await this.fs.readFile(path);
218+
// CodeSandbox SDK has separate methods for text vs binary
219+
if (typeof content === 'string') {
220+
await this.session.fs.writeTextFile(sandboxPath, content);
221+
} else {
222+
await this.session.fs.writeFile(sandboxPath, content);
223+
}
222224
}
223225
break;
224226
}
@@ -229,7 +231,9 @@ export class CodeSandboxSync {
229231
case 'rename': {
230232
// Handle rename if oldPath is provided
231233
if (event.oldPath) {
232-
const oldSandboxPath = event.oldPath.startsWith('/') ? event.oldPath.substring(1) : event.oldPath;
234+
const oldSandboxPath = event.oldPath.startsWith('/')
235+
? event.oldPath.substring(1)
236+
: event.oldPath;
233237
await this.session.fs.rename(oldSandboxPath, sandboxPath);
234238
}
235239
break;

apps/web/client/src/services/sync-engine/useSyncEngine.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import { type WebSocketSession } from '@codesandbox/sdk';
33

44
import { type FileSystem } from '@onlook/file-system';
55

6-
import { CodeSandboxSync } from './index';
7-
import { type SyncConfig } from './sync-engine';
6+
import type { SyncConfig } from './sync-engine';
7+
import { CodeSandboxSync } from './sync-engine';
88

99
interface UseSyncEngineOptions {
1010
session: WebSocketSession | null;

packages/file-system/src/config.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { configure, fs } from '@zenfs/core';
1+
import ZenFS, { configure } from '@zenfs/core';
22
import { IndexedDB } from '@zenfs/dom';
33

44
let configPromise: Promise<void> | null = null;
55

6-
export async function getFS(): Promise<typeof fs> {
6+
export async function getFS(): Promise<typeof ZenFS> {
77
// Use a single promise to ensure configuration only happens once
88
configPromise ??= configure({
99
mounts: {
@@ -13,11 +13,12 @@ export async function getFS(): Promise<typeof fs> {
1313
},
1414
},
1515
}).catch((err) => {
16+
console.log(err);
1617
// Reset on error so it can be retried
1718
configPromise = null;
1819
throw err;
1920
});
2021

2122
await configPromise;
22-
return fs;
23+
return ZenFS;
2324
}

0 commit comments

Comments
 (0)