Skip to content

Rool Svelte

v0.12.0

Svelte 5 bindings for Rool Spaces. Adds reactive state to the SDK using $state runes.

Requires Svelte 5. For core concepts (objects, references, AI, WebDAV files, undo/redo), see the SDK documentation.

Installation

Terminal window
npm install @rool-dev/svelte

Quick Start

<script lang="ts">
import { createRool, type ReactiveSpace, type ReactiveConversationHandle } from '@rool-dev/svelte';
const rool = createRool();
void rool.init();
let space = $state<ReactiveSpace | null>(null);
</script>
{#if rool.authenticated === null}
<p>Checking session...</p>
{:else if !rool.authenticated}
<button onclick={() => rool.login('My App')}>Login</button>
{:else}
<h1>My Spaces</h1>
{#each rool.spaces ?? [] as spaceInfo}
<button onclick={async () => {
space = await rool.openSpace(spaceInfo.id);
}}>
{spaceInfo.name}
</button>
{/each}
{#if space}
{@const conversation = space.conversation('main')}
<button onclick={() => void conversation.prompt('Hello')}>Send</button>
{/if}
{/if}

What It Provides

The Svelte wrapper adds reactive state on top of the SDK:

Reactive PropertyDescription
rool.authenticatedAuth state (null = checking, true/false = known)
rool.currentUserCurrent user profile after authentication
rool.spacesList of available spaces
rool.spacesLoadingWhether spaces are loading
rool.spacesErrorError from loading spaces
rool.connectionStateClient SSE connection state
rool.userStorageUser storage (cross-device preferences)
space.fileTreeCanonical reactive WebDAV tree for /, including /space objects and /rool-drive user files
space.objectPathsObject paths derived from space.fileTree
space.collectionsCollection directories derived from space.fileTree
space.conversationsLightweight conversation roster, kept current from openSpace and SSE
thread.interactionsActive branch for one lazily opened conversation
thread.loading / thread.errorLoad state for that conversation only
watch.objectsObjects matching a filter (auto-updates)
watch.loadingWhether watch is loading

Everything else passes through to the SDK directly. See the SDK documentation for full API details.

Reactive File Tree

Every ReactiveSpace owns a canonical reactive WebDAV tree. It is kept current with server filesChanged/filesReset events and WebDAV sync-collection, so it covers both object files and user files without polling.

const space = await rool.openSpace(spaceId);
space.fileTree.nodes; // ReactiveFileNode[]
space.fileTree.byPath['/space']; // lookup by machine/WebDAV path
space.fileTree.childrenOf('/'); // /space and /rool-drive
space.fileTree.childrenOf('/rool-drive');
space.fileTree.objectPaths(); // object paths from /space/**/*.json

Use this tree when UI needs to react to both files and objects. Object helpers like space.object(), space.watch(), space.objectPaths, and space.collections are backed by this tree.

API

Lifecycle

const rool = createRool();
void rool.init(); // Process auth callbacks (call on app startup)
rool.login('My App'); // Redirect to login page
rool.signup('My App'); // Redirect to signup page
rool.verify(token); // Sign in from an email verification link (used by the official Rool app)
rool.logout(); // Clear auth state and close all open spaces
rool.destroy(); // Clean up all resources

Client State

<script>
// All properties are reactive $state
// rool.authenticated → boolean | null
// rool.spaces → RoolSpaceInfo[] | undefined
// rool.spacesLoading → boolean
// rool.spacesError → Error | null
// rool.connectionState → 'connected' | 'disconnected' | 'reconnecting'
// rool.userStorage → Record<string, unknown>
// rool.currentUser → CurrentUser | null
</script>
{#if rool.spacesLoading}
<p>Loading spaces...</p>
{:else if rool.spacesError}
<p>Error: {rool.spacesError.message}</p>
{:else}
{#each rool.spaces ?? [] as space}
<div>{space.name}</div>
{/each}
{/if}

User Storage

Reactive cross-device storage for user preferences. Synced from server on init(), then kept up-to-date via SSE.

<script lang="ts">
import { createRool } from '@rool-dev/svelte';
const rool = createRool();
void rool.init();
</script>
<!-- Reactive binding to storage values -->
{#if rool.userStorage.onboarding_complete}
<Dashboard />
{:else}
<Onboarding onstep={(step) => rool.setUserStorage('onboarding_step', step)} />
{/if}
<!-- Theme toggle -->
<button onclick={() => rool.setUserStorage('theme', rool.userStorage.theme === 'dark' ? 'light' : 'dark')}>
Toggle theme
</button>

Spaces & Conversations

Every space has its own SSE subscription. Conversation metadata is reactive at the space level; full contents are loaded only for conversation handles requested through space.conversation(id). Repeated calls for the same ID return the same handle. Call space.close() when done to stop the subscription and close its conversation handles.

// Open a space — reactive, with SSE
const space = await rool.openSpace('space-id');
// Get explicit conversation handles
const main = space.conversation('main');
const research = space.conversation('research');
// Space admin
await space.rename('New Name');
const invite = await space.createInvite('editor'); // share invite.url
await space.setUserRole(userId, 'admin');
// Create a new space
const fresh = await rool.createSpace('My New Space');
const mainConversation = fresh.conversation('main');
// Import from a zip archive
const imported = await rool.importArchive('Imported', archiveBlob);
// Delete a space permanently
await rool.deleteSpace('space-id');
// Clean up — closes this space and stops its subscription
space.close();

ReactiveSpace

rool.openSpace() returns a ReactiveSpace with reactive conversations, file-tree-backed object helpers, and explicit reactive conversation handles:

<script lang="ts">
import { createRool, type ReactiveSpace, type ReactiveConversationHandle } from '@rool-dev/svelte';
const rool = createRool();
void rool.init();
let space = $state<ReactiveSpace | null>(null);
let conversation = $state<ReactiveConversationHandle | null>(null);
async function open(spaceId: string) {
space = await rool.openSpace(spaceId);
conversation = space.conversation('main');
}
</script>
{#if conversation}
<!-- Reactive: updates when this conversation changes -->
{#each conversation.interactions as interaction}
<div>
<strong>{interaction.operation}</strong>: {interaction.output ?? ''}
</div>
{/each}
<!-- Conversation-scoped SDK methods work directly -->
<button onclick={() => void conversation.prompt('Hello')}>Send</button>
{/if}

Reactive Object

Track a single object by machine path with auto-updates:

<script lang="ts">
import { createRool, type ReactiveSpace, type ReactiveObject } from '@rool-dev/svelte';
const rool = createRool();
void rool.init();
let space = $state<ReactiveSpace | null>(null);
let item = $state<ReactiveObject | null>(null);
async function open(spaceId: string, path: string) {
space = await rool.openSpace(spaceId);
item = space.object(path); // e.g. '/space/article/welcome.json'
}
</script>
{#if item}
{#if item.loading}
<p>Loading...</p>
{:else if item.data}
<div>{String(item.data.body.title ?? '')}</div>
{:else}
<p>Object not found</p>
{/if}
{/if}
// Reactive state
item.data // $state<RoolObject | undefined>
item.loading // $state<boolean>
// Methods
item.refresh() // Manual re-fetch
item.close() // Stop listening for updates

Lifecycle: Reactive objects are tied to their space. Closing the space stops all updates — existing reactive objects will retain their last data but no longer refresh. Calling space.object() after close() throws.

Reactive Watches

Create auto-updating watches of objects filtered by field values:

<script lang="ts">
import { createRool, type ReactiveSpace, type ReactiveWatch } from '@rool-dev/svelte';
const rool = createRool();
void rool.init();
let space = $state<ReactiveSpace | null>(null);
let articles = $state<ReactiveWatch | null>(null);
async function open(spaceId: string) {
space = await rool.openSpace(spaceId);
// Create a reactive watch of all objects in the 'article' collection
articles = space.watch({ collection: 'article' });
}
</script>
{#if articles}
{#if articles.loading}
<p>Loading...</p>
{:else}
{#each articles.objects as article}
<div>{String(article.body.title ?? '')}</div>
{/each}
{/if}
{/if}

Watches automatically re-fetch when matching object files change in space.fileTree.

Lifecycle: Watches are tied to their space. Closing the space stops all updates — existing watches will retain their last data but no longer refresh. Calling space.watch() after close() throws.

// Watch options
const articles = space.watch({
collection: 'article',
where: { status: 'published' },
order: 'desc', // by modifiedAt (default)
limit: 20,
});
// Reactive state
articles.objects // $state<RoolObject[]>
articles.loading // $state<boolean>
// Methods
articles.refresh() // Manual re-fetch
articles.close() // Stop listening for updates

Reactive Space List

rool.spaces is a reactive RoolSpaceInfo[]. Open individual spaces with rool.openSpace(spaceId) and close them when done.

<script lang="ts">
import { createRool, type ReactiveSpace } from '@rool-dev/svelte';
const rool = createRool();
void rool.init();
let current = $state<ReactiveSpace | null>(null);
</script>
{#each rool.spaces ?? [] as space}
<button onclick={async () => current = await rool.openSpace(space.id)}>
{space.name}
</button>
{/each}

Reactive Conversation Handle

For apps with multiple independent interaction threads, use space.conversation() to lazily load and retain reactive state for the threads currently in use:

<script lang="ts">
import { createRool, type ReactiveConversationHandle, type ReactiveSpace } from '@rool-dev/svelte';
const rool = createRool();
void rool.init();
let space = $state<ReactiveSpace | null>(null);
let thread = $state<ReactiveConversationHandle | null>(null);
async function openThread(spaceId: string, threadId: string) {
space = await rool.openSpace(spaceId);
thread = space.conversation(threadId);
}
</script>
{#if thread}
{#each thread.interactions as interaction}
<div>{interaction.output}</div>
{/each}
<button onclick={() => void thread.prompt('Hello')}>Send</button>
{/if}
// State retained for this conversation only
thread.data // Conversation | null | undefined
thread.interactions // active branch; updates from space events
thread.activeLeafId
thread.loading
thread.error
await thread.prompt('Hello')
await thread.stop()
await thread.setSystemInstruction('Respond in haiku')
await thread.rename('Research Thread')
thread.setActiveLeaf(interactionId)
// Evicts this handle from the space; requesting the ID again loads a new one.
thread.close()

Conversations are auto-created when you first write history or settings. Real-time events are owned by the space subscription; conversation handles subscribe to space events locally.

Space Management

// Rename or delete the open space
await space.rename('New Name');
await space.delete();
// Or manage spaces from the client
const created = await rool.createSpace('New Space');
await rool.deleteSpace(created.id);

Using the SDK

Common ReactiveSpace and ReactiveConversationHandle methods:

// Space properties
space.id
space.name
space.role
// Read object data by exact machine path
const path = '/space/note/welcome.json';
await space.getObject(path)
// Object and schema APIs live on the space
await space.putObject(path, { text: 'Hello' })
await space.patchObject(path, { data: { text: 'Updated' } })
await space.moveObject(path, '/space/note/renamed.json')
await space.deleteObjects(['/space/note/renamed.json'])
await space.createCollection('article', [
{ name: 'title', type: { kind: 'string' } },
{ name: 'status', type: { kind: 'enum', values: ['draft', 'published'] } },
])
await space.alterCollection('article', updatedFields)
await space.dropCollection('article')
// Conversation state and operations are scoped to a lazy reactive handle
const conversation = space.conversation('thread-42');
await conversation.prompt('Summarize everything')
await conversation.stop()
await conversation.setSystemInstruction('You are helpful')
conversation.interactions
// Space history/admin
await space.undo()
await space.redo()
await space.deleteConversation('old-thread')
await space.rename('New Name')

See the SDK documentation for complete API details.

Utilities

import { machinePath, machineUri, isObjectPath, generateId } from '@rool-dev/svelte';
machinePath('rool-machine:/rool-drive/docs/read%20me.md');
// '/rool-drive/docs/read me.md'
machineUri('/space/article/welcome.json');
// 'rool-machine:/space/article/welcome.json'
isObjectPath('/space/article/welcome.json'); // true
generateId(); // unique ID suitable for conversation IDs

Exports

// Package types
import type { Rool, ReactiveSpace, ReactiveConversationHandle, ReactiveObject, ReactiveWatch, WatchOptions, ReactiveFileTree, ReactiveFileNode, ReactiveFileRoot, ReactiveFileTreeEvent, ReactiveFileTreeSyncResult } from '@rool-dev/svelte';

The SDK’s entire public API is also re-exported, so apps never need a direct @rool-dev/sdk dependency — classes and helpers as well as every SDK type:

import { NativePkceAuthProvider, InviteError, WebDAVError, machinePath } from '@rool-dev/svelte';
import type { RoolObject, PromptOptions, Interaction, SpaceSchema } from '@rool-dev/svelte';

See the SDK documentation for the full API. The one exception is the Node auth provider: import it from @rool-dev/sdk/node, which stays a separate entry point so Node-only dependencies never reach browser bundles.

License

MIT - see LICENSE for details.