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.
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).
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:
| Prop | Purpose |
|---|---|
fileExplorer | 'builtin' (default) or 'off' |
autoRevealOnOpenFileCard | Whether an arriving open-file card auto-reveals the aside (default true); suppressed while a file has unsaved edits |
fileExplorerBasePath | Override 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>
AsgardThemeScopeOutside <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
- sandbox:// Handoff Cards — where
open-filecomes from - Cold-start HUD — where
launchedSandboxescomes from - Re-establish the Theme — required when placing it yourself