Documentation Index
Fetch the complete documentation index at: https://mintlify.com/rbbydotdev/opal/llms.txt
Use this file to discover all available pages before exploring further.
Workspace System
The Workspace class is the central orchestrator in Opal Editor, managing the lifecycle and operations of markdown editing projects. It coordinates between the storage layer (Disk), version control (GitRepo), and user interface.
Architecture Overview
The Workspace system follows a layered architecture:
┌─────────────────────────────────────┐
│ Workspace │
│ (Coordination & Lifecycle) │
├─────────────────────────────────────┤
│ Disk │ GitRepo │ RemoteAuth │
│ (Storage)│ (Version)│ (Deploy) │
└─────────────────────────────────────┘
Core Class: Workspace
Location: ~/workspace/source/src/workspace/Workspace.ts
Class Definition
export class Workspace {
name: string;
guid: string;
private _disk: Disk;
private _thumbs: Disk;
private _repo: GitRepo;
private _playbook: GitPlaybook;
private _remoteAuths?: RemoteAuthDAO[];
imageCache: ImageCache;
local: WorkspaceEventsLocal;
remote: WorkspaceEventsRemote;
// Getters
get disk(): Disk
get repo(): GitRepo
get thumbs(): Disk
get buildStrategy(): BuildStrategy
}
Key Properties
| Property | Type | Description |
|---|
guid | string | Unique identifier for the workspace |
name | string | Slugified workspace name |
disk | Disk | Main storage abstraction for files |
thumbs | Disk | Separate disk for thumbnail storage |
repo | GitRepo | Git repository instance |
imageCache | ImageCache | Browser cache for optimized image loading |
remoteAuths | RemoteAuthDAO[] | Authentication for deployment providers |
Creating Workspaces
Factory Methods
CreateNew - Create from scratch
const workspace = await Workspace.CreateNew({
name: "my-blog",
files: {
"/index.md": "# Welcome",
"/about.md": "# About"
},
diskType: "IndexedDbDisk",
buildStrategy: "freeform"
});
Parameters:
name: Workspace identifier (will be slugified)
files: Initial file tree (object or iterable)
diskType: Storage backend type
diskOptions: Optional configuration for disk
buildStrategy: Build system strategy (“freeform” | “eleventy”)
FromDAO - Restore from database
const workspaceDAO = await WorkspaceDAO.FetchFromGuid(guid);
const workspace = Workspace.FromDAO(workspaceDAO);
FromName - Load by name
const workspace = await Workspace.FromName("my-blog");
Initialization
Workspaces must be initialized before use:
await workspace.init();
// or skip event listeners for background operations
await workspace.initNoListen();
Initialization process:
- Initialize disk and file indexing
- Initialize Git repository
- Set up event listeners (cross-tab sync)
- Recover from non-OK status if needed
File Operations
Creating Files
// Single file
const path = await workspace.newFile(
absPath("/blog"),
relPath("post.md"),
"# New Post"
);
// Multiple files
const paths = await workspace.newFiles([
[absPath("/posts/1.md"), "# Post 1"],
[absPath("/posts/2.md"), "# Post 2"]
]);
// Create directory
await workspace.newDir(absPath("/posts"), relPath("2024"));
Reading Files
const content = await workspace.readFile(absPath("/index.md"));
Renaming Files
const fromNode = workspace.nodeFromPath(absPath("/old.md"))!;
const result = await workspace.renameSingle(
fromNode,
absPath("/new.md")
);
// Multiple renames
await workspace.renameMultiple([
[node1, absPath("/path1.md")],
[node2, absPath("/path2.md")]
]);
Deleting Files
// Single file
await workspace.removeSingle(absPath("/file.md"));
// Multiple files
await workspace.removeMultiple([
absPath("/file1.md"),
absPath("/file2.md")
]);
Trash Operations
// Move to trash
await workspace.trashMultiple([absPath("/file.md")]);
// Restore from trash
await workspace.untrashSingle(absPath("/.trash/file.md"));
// Check if trash has items
if (workspace.hasTrash()) {
// Empty trash logic
}
File Tree Navigation
Accessing Nodes
// Get tree node by path
const node = workspace.nodeFromPath(absPath("/index.md"));
// Get file tree root
const root = workspace.getFileTreeRoot();
// Get first file
const firstFile = await workspace.getFirstFile();
// Get flat tree with filters
const markdownFiles = workspace.getFlatTree({
filterIn: (node) => node.isMarkdownFile(),
filterOut: (node) => node.path.startsWith("/.trash")
});
Image Handling
Upload Images
// Single image
const path = await workspace.uploadSingleImage(
imageFile,
absPath("/images")
);
// Multiple images with concurrency control
const paths = await workspace.uploadMultipleImages(
imageFiles,
absPath("/images"),
8 // concurrency
);
Thumbnails
// Create/read thumbnail
const thumbBlob = await workspace.readOrMakeThumb(
absPath("/images/photo.jpg"),
100 // size in pixels
);
Event System
Workspaces use both local and remote event emitters for cross-tab synchronization.
Listening to File Changes
// Watch for renames
workspace.renameListener((changes) => {
changes.forEach(({ oldPath, newPath }) => {
console.log(`Renamed: ${oldPath} → ${newPath}`);
});
});
// Watch for creates
workspace.createListener(({ filePaths }) => {
console.log("Created:", filePaths);
});
// Watch for deletes
workspace.deleteListener(({ filePaths }) => {
console.log("Deleted:", filePaths);
});
// Watch disk index updates
workspace.watchDiskIndex((fileTree, trigger) => {
if (trigger?.type === "rename") {
// Handle rename
}
});
Workspace Lifecycle Events
// Listen for workspace rename
workspace.renameWorkspaceListener(({ id, oldName, newName }) => {
console.log(`Workspace renamed: ${oldName} → ${newName}`);
});
// Listen for workspace deletion
workspace.deleteWorkspaceListener(() => {
console.log("Workspace deleted");
});
Git Events
// Listen to Git changes
workspace.gitRepoListener((info) => {
console.log("Current branch:", info.currentBranch);
console.log("Has changes:", info.hasChanges);
});
Workspace Management
Rename Workspace
const newName = await workspace.rename("new-name");
Destroy Workspace
await workspace.destroy(); // Deletes all data
Tear Down (Cleanup)
await workspace.tearDown(); // Cleanup without deletion
URL Resolution
// Get workspace home URL
const homeUrl = workspace.home(); // "/workspace/my-blog"
// Resolve file URL
const fileUrl = workspace.resolveFileUrl(absPath("/index.md"));
// "/workspace/my-blog/index.md"
// Get first file URL (or home if no files)
const url = await workspace.tryFirstFileUrl();
Parse Workspace Paths
const { workspaceName, filePath } = Workspace.parseWorkspacePath(
"/workspace/my-blog/posts/hello.md"
);
// workspaceName: "my-blog"
// filePath: "/posts/hello.md"
Copy Operations
// Copy single file
await workspace.copyFile(
absPath("/source.md"),
absPath("/dest.md"),
false // overwrite
);
// Copy from another workspace
await workspace.copyMultipleSourceNodes(
sourceNodes,
sourceDisk
);
Serialization
// To JSON
const json = workspace.toJSON();
// Returns: { name, guid, href, disk, thumbs, remoteAuths, ... }
// From JSON
const workspace = Workspace.FromJSON(json);
Integration with Git
Workspaces integrate tightly with Git:
// Access Git repository
const repo = workspace.repo;
// Git operations trigger disk re-indexing
await repo.commit({ message: "Update" });
// Automatically triggers workspace.disk.triggerIndex()
// Check remote repositories
const remotes = workspace.getRemoteGitRepos();
Best Practices
Always Initialize
// ✅ Good
const workspace = await Workspace.FromName("my-blog");
await workspace.init();
// ❌ Bad - operations may fail
const workspace = await Workspace.FromName("my-blog");
await workspace.readFile(absPath("/index.md")); // May fail!
Clean Up Resources
try {
const workspace = await Workspace.FromName("my-blog");
await workspace.init();
// ... operations
} finally {
await workspace.tearDown();
}
Use Path Helpers
import { absPath, relPath, joinPath } from "@/lib/paths2";
// ✅ Good - type-safe paths
await workspace.newFile(
absPath("/posts"),
relPath("hello.md"),
content
);
// ❌ Bad - plain strings
await workspace.newFile("/posts", "hello.md", content);
Handle Cross-Tab Events
// Listen for changes from other tabs
workspace.watchDiskIndex((fileTree, trigger) => {
if (trigger?.type === "rename") {
// Update UI to reflect rename from another tab
}
});
Common Patterns
Workspace with OPFS Directory Mount
const workspace = await Workspace.CreateNew({
name: "local-folder",
files: [],
diskType: "OpFsDirMountDisk",
diskOptions: {
selectedDirectory: await window.showDirectoryPicker()
}
});
Search Workspace Content
const scannable = workspace.NewScannable();
const results = await scannable.search("search term");
Get All Images
const images = workspace.getImages();
// Returns: AbsPath[] of all image files
Error Handling
import { NotFoundError, BadRequestError } from "@/lib/errors/errors";
try {
await workspace.removeSingle(absPath("/missing.md"));
} catch (error) {
if (error instanceof NotFoundError) {
console.log("File not found");
}
}