History

Add workspace file browser and preview panel (#3087)

Julius Marminge committed 4 months agov0.0.28-nightly.20260615.565

Commit de8bdc11 parent 477795662 files · +2850 −9551Browse files

Showing the changes to apps/server/src/workspace/Layers/WorkspaceFileSystem.ts (1 of 62 files). Show all

Unified · Split

apps/server/src/workspace/Layers/WorkspaceFileSystem.ts+66−1
@@ -1,3 +1,6 @@
1+// @effect-diagnostics nodeBuiltinImport:off
2+import fsPromises from "node:fs/promises";
3+
14 import * as Effect from "effect/Effect";
25 import * as FileSystem from "effect/FileSystem";
36 import * as Layer from "effect/Layer";
@@ -11,12 +14,74 @@ import { · 7 unmodified lines
1114 import { WorkspaceEntries } from "../Services/WorkspaceEntries.ts";
1215 import { WorkspacePaths } from "../Services/WorkspacePaths.ts";
1316
17+const PROJECT_READ_FILE_MAX_BYTES = 1024 * 1024;
18+
1419 export const makeWorkspaceFileSystem = Effect.gen(function* () {
1520 const fileSystem = yield* FileSystem.FileSystem;
1621 const path = yield* Path.Path;
1722 const workspacePaths = yield* WorkspacePaths;
1823 const workspaceEntries = yield* WorkspaceEntries;
1924
25+ const readFile: WorkspaceFileSystemShape["readFile"] = Effect.fn("WorkspaceFileSystem.readFile")(
26+ function* (input) {
27+ const target = yield* workspacePaths.resolveRelativePathWithinRoot({
28+ workspaceRoot: input.cwd,
29+ relativePath: input.relativePath,
30+ });
31+
32+ const result = yield* Effect.tryPromise({
33+ try: async () => {
34+ const [realWorkspaceRoot, realTargetPath] = await Promise.all([
35+ fsPromises.realpath(input.cwd),
36+ fsPromises.realpath(target.absolutePath),
37+ ]);
38+ const relativeRealPath = path.relative(realWorkspaceRoot, realTargetPath);
39+ if (
40+ relativeRealPath.startsWith(`..${path.sep}`) ||
41+ relativeRealPath === ".." ||
42+ path.isAbsolute(relativeRealPath)
43+ ) {
44+ throw new Error("Workspace file path resolves outside the project root.");
45+ }
46+
47+ const handle = await fsPromises.open(realTargetPath, "r");
48+ try {
49+ const stat = await handle.stat();
50+ if (!stat.isFile()) {
51+ throw new Error("Workspace path is not a file.");
52+ }
53+ const bytesToRead = Math.min(stat.size, PROJECT_READ_FILE_MAX_BYTES);
54+ const buffer = Buffer.alloc(bytesToRead);
55+ const { bytesRead } = await handle.read(buffer, 0, bytesToRead, 0);
56+ const fileBytes = buffer.subarray(0, bytesRead);
57+ if (fileBytes.includes(0)) {
58+ throw new Error("Binary files cannot be previewed as text.");
59+ }
60+ const contents = new TextDecoder("utf-8").decode(fileBytes);
61+ return {
62+ relativePath: target.relativePath,
63+ contents,
64+ byteLength: stat.size,
65+ truncated: stat.size > PROJECT_READ_FILE_MAX_BYTES,
66+ };
67+ } finally {
68+ await handle.close();
69+ }
70+ },
71+ catch: (cause) =>
72+ new WorkspaceFileSystemError({
73+ cwd: input.cwd,
74+ relativePath: input.relativePath,
75+ operation: "workspaceFileSystem.readFile",
76+ detail: cause instanceof Error ? cause.message : String(cause),
77+ cause,
78+ }),
79+ });
80+
81+ return result;
82+ },
83+ );
84+
2085 const writeFile: WorkspaceFileSystemShape["writeFile"] = Effect.fn(
2186 "WorkspaceFileSystem.writeFile",
2287 )(function* (input) {
@@ -52,7 +117,7 @@ export const makeWorkspaceFileSystem = Effect.gen(function* () { · 29 unmodified lines
52117 yield* workspaceEntries.invalidate(input.cwd);
53118 return { relativePath: target.relativePath };
54119 });
55− return { writeFile } satisfies WorkspaceFileSystemShape;
120+ return { readFile, writeFile } satisfies WorkspaceFileSystemShape;
56121 });
57122
58123 export const WorkspaceFileSystemLive = Layer.effect(WorkspaceFileSystem, makeWorkspaceFileSystem);