Skip to main content

Sandbox File Explorer

When the agent touches files inside a sandbox, the user needs to see that tree. The SDK ships a File Explorer aside: a sandbox dropdown, a lazily-expanded tree rooted at workingDirectory, a single-file CodeMirror 6 view with watch-and-reload, plus a right-click context menu and clipboard-style move/copy.

Sandbox File Explorer

When the agent touches files inside a sandbox, the user needs to see that tree. The SDK ships a File Explorer aside: a sandbox dropdown, a lazily-expanded tree rooted at workingDirectory, a single-file CodeMirror 6 view with watch-and-reload, plus a right-click context menu and clipboard-style move/copy.

Try it

Expand src/ or docs/ → double-click a file to open → edit and save → right-click for new folder, delete, copy/paste.

Built-in vs. placed yourself

The default fileExplorer="builtin" puts a folder toggle on the header that opens a right-side aside, all wired up. Set it to "off" and neither appears — you place the exported <FileExplorerPanel> wherever you like, as this demo does.

import {
  FileExplorerPanel, useFileExplorerController, AsgardThemeScope,
} from '@asgard-js/react';
import { createSandboxFsProviders } from '@asgard-js/react';

const controller = useFileExplorerController({ open: true });
const fs = createSandboxFsProviders(client);

<Chatbot fileExplorer="off" {...rest} />

<AsgardThemeScope theme={theme}>
  <FileExplorerPanel
    sandboxes={launchedSandboxes}   // from channel.launchedSandboxes$
    controller={controller}
    {...fs}                          // listDir / readFile / saveFile / watchFile…
    onNudge={() => channel.nudge()}   // wake an idle sandbox
    nudgeDisabled={isConnecting}
  />
</AsgardThemeScope>

This demo swaps listDir / readFile / saveFile / watchFile for an in-memory tree — the panel does not care where the data comes from. In production you wire it to AsgardServiceClient's sandbox fs methods (see createSandboxFsProviders below).

Loading File Explorer...
This demo runs on a fake tree

The panel takes file access as a set of async callbacks, so the demo above swaps listDir / readFile / saveFile / watchFile for an in-memory tree — lazy expansion, editing, saving and the context menu are all genuinely running, there is just no sandbox behind them. In production you wire createSandboxFsProviders.

Built-in vs. Placed Yourself

// Default: a folder toggle on the header opens a right-side aside, all wired up
<Chatbot fileExplorer="builtin" {...rest} />

// Opt out: neither appears — you place <FileExplorerPanel> wherever you like
<Chatbot fileExplorer="off" {...rest} />

Related Chatbot props:

PropPurpose
fileExplorer'builtin' (default) or 'off'
autoRevealOnOpenFileCardWhether an arriving open-file card auto-reveals the aside (default true); suppressed while a file has unsaved edits
fileExplorerBasePathOverride the tree root (absolute path) instead of the sandbox's workingDirectory

Placing It Yourself

import {
FileExplorerPanel,
useFileExplorerController,
createSandboxFsProviders,
AsgardThemeScope,
} from "@asgard-js/react";

const controller = useFileExplorerController({ open: true });
const fs = createSandboxFsProviders(client);

<Chatbot fileExplorer="off" {...rest} />

<AsgardThemeScope theme={theme}>
<FileExplorerPanel
sandboxes={launchedSandboxes} // from channel.launchedSandboxes$
controller={controller}
{...fs} // listDir / readFile / saveFile / watchFile / mkdir…
onNudge={() => channel.nudge()} // wake an idle sandbox
nudgeDisabled={isConnecting}
chrome="card" // 'card' standalone / 'flush' built-in aside
/>
</AsgardThemeScope>
Always wrap in AsgardThemeScope

Outside <Chatbot> the panel misses the design tokens the chat shell emits and falls back to the light-themed defaults. See Re-establish the Theme.

The Shared Controller

The header toggle, the open-file handoff card, and the panel itself all bind one controller:

interface FileExplorerController {
open: boolean;
activeSandboxName: string | null;
requestedFile: RequestedFile | null;
isEditingDirty: boolean; // a file has unsaved changes
openExplorer(): void;
closeExplorer(): void;
toggle(): void;
selectSandbox(sandboxName: string): void;
requestFile(
sandboxName: string,
absolutePath: string,
options?: { reveal?: boolean }, // reveal: false = fire the intent without yanking the panel
): void;
setEditingDirty(dirty: boolean): void;
}

requestFile is exactly what a sandbox://…/open-file card calls under the hood.

File-access Providers

The panel does not care where the data comes from — it only needs these functions:

type FsListDir = (sandboxName: string, path: string) => Promise<SandboxFsListResult>;
type FsReadFile = (sandboxName: string, path: string) => Promise<string>;
type FsSaveFile = (sandboxName: string, path: string, content: string) => Promise<void> | void;
// Subscribe to one path's changes, return an unsubscribe. The payload is deliberately
// not surfaced — the view reloads from disk either way.
type FsWatchFile = (sandboxName: string, path: string, onChange: () => void) => () => void;

There is also an optional set of mutation callbacks; omit one and the matching action simply is not offered: mkdir, remove, copy, move, upload, download.

createSandboxFsProviders(client, options) wires core's sandbox fs methods into that whole set — image files resolve to a data URL for <img src>, text files to a decoded string. It also tracks failures: once a sandbox fails enough consecutive calls it invokes onSandboxUnreachable so you can drop it from the dropdown.

Nudge: Waking an Idle Sandbox

Once a sandbox has gone idle and shut down, the panel's empty state offers a Nudge button. It sends an invisible NUDGE turn — nothing appears in the thread:

await channel.nudge();

A nudge is a turn, so it is refused outright while a run holds the channel. Pass the host's "a run is in flight" state to nudgeDisabled so the button is not a dead click.

See Also