RuimteDocs

@ruimte/extension/client

What client/ imports. It runs in the page of every client that loads the extension.

Generated from the type declarations your editor reads, so a name here is a name you can import.

ExtensionErrorCode

Why a call came back without its answer. permission-denied: the manifest does not ask for the permission. no-project: no project with a folder is open. no-canvas: no canvas is in front. not-found: what it names is not there. outside-project: a path leaves the project folder. refused: the machine or the person said no. limit: too many calls in a while. timeout: no answer in time. unavailable: the machine, or server/, cannot answer now. failed: anything else, with its message.

export type ExtensionErrorCode =
    | 'permission-denied'
    | 'no-project'
    | 'no-canvas'
    | 'not-found'
    | 'outside-project'
    | 'refused'
    | 'limit'
    | 'timeout'
    | 'unavailable'
    | 'failed';

Result

What a call that can fail answers: its value, or why not. Only a wrong argument throws. A call with nothing to answer, a Result<void>, answers { ok: true, value: undefined } with the key value there.

export type Result<Value, Code extends string = ExtensionErrorCode> = { ok: true; value: Value } | { ok: false; error: { code: Code; message: string } };

Json

A value as it crosses to the machine and back.

export type Json = string | number | boolean | null | Json[] | { [key: string]: Json | undefined };

Returned

What a method of server/ answers once it crossed as JSON: undefined, and so a method that returns nothing, arrives as null.

export type Returned<Value> = Value extends undefined | void ? null : Value;

LazyComponent

A component is always handed over as a lazy import: () => import('./Panel').

export type LazyComponent<Props = {}> = () => Promise<{ default: ComponentType<Props> }>;

Surface

export type Surface<Props = {}> = { component: LazyComponent<Props> } | { frame: string };

PanelHandle

export interface PanelHandle<Params = void> {
    open(params: Params): void;
    close(): void;
    setBadge(text: string | null): void;
    setTitle(title: string): void;
}

NodeRef

export interface NodeRef {
    id: string;
}

ViewRef

export interface ViewRef {
    id: string;
}

ProjectRef

export interface ProjectRef {
    id: string;
}

definePanel

icon names a Lucide icon. Without one, a surface shows the extension's assets/icon.svg.

export function definePanel<Params = void>(
    panel: { id: string; title: string; icon?: string; minWidth?: number } & Surface<{ params: Params | undefined }>
): PanelHandle<Params>;

defineSidebarCard

export function defineSidebarCard(card: { id: string; title: string; component: LazyComponent }): void;

defineView

params is kept with the view in the project, so it is JSON. A view opened without them, as New <title> opens it, gets undefined.

export function defineView<Params = Json>(
    view: { id: string; title: string; icon?: string } & Surface<{ params: Params | undefined }>
): {
    open(params?: Params): void;
};

defineNode

export function defineNode<Data = Json>(
    node: {
        id: string;
        title: string;
        icon?: string;
        size?: { width: number; height: number };
        initialData?: Data;
        dataVersion?: number;
        /** `stored` is what an older `dataVersion` kept, which only the extension knows the shape of. */
        migrate?: (stored: Json, fromVersion: number) => Data;
        /** Where "Add" shows. By default the Extensions submenu of the canvas menu, at the spot clicked, and the command palette. */
        addFrom?: Partial<Slots>;
    } & Surface
): void;

defineCanvasOverlay

Over the whole canvas, under Ruimte's own controls such as the minimap in the bottom right. It lets clicks through; set pointer-events: auto on what takes them.

export function defineCanvasOverlay(overlay: { id: string; component: LazyComponent }): void;

defineFileViewer

Opens every file with these extensions in your component, with Ruimte's own view one click away.

export function defineFileViewer(viewer: {
    id: string;
    title: string;
    extensions: string[];
    component: LazyComponent<{ file: FileInfo & { text: string | null } }>;
}): void;

defineMarkdownBlock

Draws a fenced block of this language in a chat and in markdown, on this client. Elsewhere it stays code.

export function defineMarkdownBlock(block: { language: string; component: LazyComponent<{ source: string }> }): void;

defineOverlay

Your own UI over the app: Ruimte draws the header with title and a close button, and animates it; you draw the body. open answers what close was given, or null when a person dismissed it.

export function defineOverlay<Params = void, Answer = void>(overlay: {
    id: string;
    title: string;
    size?: 'sm';
    component: LazyComponent<{ params: Params; close(answer?: Answer): void }>;
}): { open(params: Params): Promise<Answer | null>; close(): void };

ToolCall

A call of a tool in a chat, as the thread holds it.

export interface ToolCall {
    name: string;
    input: Json;
    output: string | null;
    state: string;
}

defineToolResult

Draws these tools' calls in a chat, by their exact name, instead of the thread's own view.

export function defineToolResult(result: { id: string; tools: string[]; component: LazyComponent<{ call: ToolCall }> }): void;

definePaletteSource

Rows in the command palette for what a person types. It waits for a pause in the typing; a slow or failing one costs only its own rows.

export function definePaletteSource(source: {
    id: string;
    title: string;
    search(query: string): Promise<{ id: string; title: string; detail?: string; run(): void }[]>;
}): void;

defineSlashCommand

After / in a chat. It expands into the prompt, and the person still sends it.

export function defineSlashCommand(command: { name: string; description: string; run(args: string): string | Promise<string> }): void;

defineReference

Things from outside the project for # in a chat. Picking one puts what read answers in the prompt.

export function defineReference(reference: {
    id: string;
    title: string;
    list(): Promise<{ id: string; name: string }[]>;
    read(item: { id: string; name: string }): Promise<string>;
}): void;

defineDiffViewer

Opens a changed file with these extensions in your component, with Ruimte's own diff one click away.

export function defineDiffViewer(viewer: { id: string; title: string; extensions: string[]; component: LazyComponent<{ file: FileInfo; diff: string }> }): void;

defineFileIcon

An icon from your pack, by its path, for files with these extensions.

export function defineFileIcon(icon: { extensions: string[]; path: string }): void;

defineVoiceAction

What a person can ask for by voice, through extension.voiceList and extension.voiceRun. It answers in words. Type Input after parameters, a JSON Schema.

export function defineVoiceAction<Input extends object = { [key: string]: Json | undefined }>(action: {
    id: string;
    description: string;
    parameters?: object;
    run(input: Input): string | Promise<string>;
}): void;

defineTemplate

New from a template, under Extensions in the canvas menu.

export function defineTemplate(template: {
    id: string;
    title: string;
    icon?: string;
    create(context: BaseContext & { at: { x: number; y: number } }): void | Promise<void>;
}): void;

defineCommand

export function defineCommand(command: { id: string; title: string; run: () => void | Promise<void> }): void;

SettingField

export interface SettingField {
    key: string;
    type: 'boolean' | 'text' | 'select';
    label: string;
    description?: string;
    default: boolean | string;
    options?: { value: string; label: string }[];
}

SettingGroup

A section of an extension's settings page: a title over a card of fields, like Ruimte's own settings.

export interface SettingGroup {
    title?: string;
    description?: string;
    fields: SettingField[];
}

defineExtensionSettings

Your own component, or fields (one card) or groups (a card per group) that Ruimte draws. One call takes one of the three: with fields or groups beside it, component is left out. Read a field with useSetting; keys are unique across groups.

export function defineExtensionSettings(
    settings: { component: LazyComponent; keywords?: string[] } | { fields: SettingField[] } | { groups: SettingGroup[] }
): void;

useSetting

export function useSetting<Value extends boolean | string>(key: string): Value;

useMachine

export function useMachine(): { id: string };

useTheme

export function useTheme(): { side: 'light' | 'dark' };

useChat

The chat of this id on the machine in front, or null when it has none.

export function useChat(id: string): ChatSummary | null;

AgentStatus

Where an agent is, the same word everywhere it shows.

export type AgentStatus = 'idle' | 'running' | 'needsYou' | 'stopped';

AgentInfo

export interface AgentInfo {
    provider: string;
    account: string | null;
    status: AgentStatus;
    cwd: string | null;
    branch: string | null;
    worktree: string | null;
}

ChatSummary

A chat on the machine, as chats.list answers it. Its id is the id of its node.

export interface ChatSummary {
    id: string;
    provider: string;
    status: AgentStatus;
    model: string | null;
    cwd: string;
}

NodeInfo

export interface NodeInfo {
    id: string;
    kind: string;
    title: string;
    color: string | null;
    bounds: { x: number; y: number; width: number; height: number };
    groupId: string | null;
    viewId: string;
    machine: string | null;
    linkedTo: string[];
    agent?: AgentInfo;
    path?: string;
    url?: string;
    extension?: { id: string; type: string };
}

FileInfo

export interface FileInfo {
    path: string;
    name: string;
    extension: string;
}

BaseContext

What every slot, action and widget is told.

export interface BaseContext {
    project: { id: string; name: string; folder: string | null };
    view: { id: string; kind: string; name: string } | null;
    machine: { id: string };
    theme: 'light' | 'dark';
}

SlotAnswer

false hides it, true shows it, an object also changes what it shows there.

export type SlotAnswer = boolean | { title?: string; icon?: string; disabled?: boolean | string; badge?: string; order?: number };

Slot

export type Slot<Context> = true | ((context: BaseContext & Context) => SlotAnswer);

SlotContexts

What each place tells.

export interface SlotContexts {
    selectionBar: { nodes: NodeInfo[] };
    nodeHeader: { node: NodeInfo };
    /** `nodes` is the selection if the node is in it. */
    nodeMenu: { node: NodeInfo; nodes: NodeInfo[] };
    canvasMenu: { at: { x: number; y: number } };
    viewToolbar: {};
    viewMenu: { target: { id: string; kind: string } };
    panelHeader: { panel: string };
    commandPalette: { nodes: NodeInfo[] };
    sidebarRow: { node: NodeInfo };
    tabMenu: { target: { id: string } };
    emptyCanvas: {};
    startScreen: {};
    projectBanner: {};
    threadHeader: { chat: { id: string }; node: NodeInfo };
    emptyThread: { chat: { id: string } };
    toolActions: { chat: { id: string }; tool: ToolCall };
    fileRowBadge: { file: FileInfo };
    fileBreadcrumb: { file: FileInfo };
    terminalMenu: { node: NodeInfo };
    browserMenu: {};
    textMenu: { text: { id: string } };
    noteToolbar: { node: NodeInfo };
    routineMenu: { routine: { id: string } };
    composer: { chat: { id: string }; node: NodeInfo };
    projectMenu: {};
    sidebarFooter: {};
    editorStatusBar: { file: FileInfo };
    messageActions: { chat: { id: string }; message: { id: string; kind: 'user' | 'assistant'; text: string } };
    lineMenu: { from: NodeInfo; to: NodeInfo };
    fileMenu: { file: FileInfo; files: FileInfo[] };
    editorToolbar: { file: FileInfo };
    editorMenu: { file: FileInfo; files: FileInfo[] };
    diffToolbar: { repository: string };
    terminalToolbar: { node: NodeInfo };
    browserToolbar: { node: NodeInfo; url: string };
}

SlotName

export type SlotName = keyof SlotContexts;

Slots

export type Slots<Place extends SlotName = SlotName> = { [Key in Place]: Slot<SlotContexts[Key]> };

ContextOf

export type ContextOf<Place extends SlotName> = { [Key in Place]: BaseContext & SlotContexts[Key] }[Place];

ActionHandle

Runs the action from code, with what its run reads beside the context of the window in front.

export interface ActionHandle<Context extends object = object> {
    run(context?: Partial<BaseContext> & Partial<Context>): Promise<void>;
    useRunning(): boolean;
}

defineAction

While run is pending, its nodes show a spinner that also stops it through signal, and a throw reaches the person. Without slots it only shows where a menu puts it, and gets that menu's context. Type it with defineAction<Context>.

export function defineAction<Place extends SlotName>(action: {
    id: string;
    title: string;
    icon?: string;
    /** Like mod+shift+k. Needs mod or alt. It gets the context of the command palette. */
    shortcut?: string;
    slots: Slots<Place>;
    run(context: ContextOf<Place> & { slot: Place; signal: AbortSignal }): void | Promise<void>;
}): ActionHandle<SlotContexts[Place]>;
export function defineAction<Context extends object = {}>(action: {
    id: string;
    title: string;
    icon?: string;
    shortcut?: string;
    run(context: BaseContext & Context & { slot: string; signal: AbortSignal }): void | Promise<void>;
}): ActionHandle<Context>;
export type MenuItem = ActionHandle | 'separator' | { title: string; run(): void; checked?: boolean } | { title: string; items: MenuItem[] };

defineMenu

A menu of your own: a submenu in a menu, a dropdown in a bar.

export function defineMenu<Place extends SlotName>(menu: {
    id: string;
    title: string;
    icon?: string;
    slots: Slots<Place>;
    items: MenuItem[] | ((context: ContextOf<Place>) => MenuItem[]);
}): void;

defineWidget

Your own React in a slot.

export function defineWidget<Place extends SlotName>(widget: {
    id: string;
    slots: Slots<Place>;
    component: LazyComponent<{ context: ContextOf<Place> }>;
}): void;

ServerClient

Each method calls server/ on the machine in front and answers a Result: failed with the message of what it threw, not-found for a method it lacks, unavailable while it cannot run. .use() suspends until it answers, and keeps that answer for the same arguments until .reload() with them drops it, or the connection to the machine comes back. Every surface that read it then asks again. The value crosses as JSON, see {@link Returned}.

export type ServerClient<Server> = {
    [Method in keyof Server]: Server[Method] extends (...args: infer Args) => Promise<infer Value>
        ? ((...args: Args) => Promise<Result<Returned<Value>>>) & { use(...args: Args): Result<Returned<Value>>; reload(...args: Args): void }
        : never;
};

useServer

export function useServer<Server>(): ServerClient<Server>;

ServerCalls

Each method of server/ as {@link ServerClient} calls it, without .use() and .reload(), which only a component has.

export type ServerCalls<Server> = {
    [Method in keyof Server]: Server[Method] extends (...args: infer Args) => Promise<infer Value> ? (...args: Args) => Promise<Result<Returned<Value>>> : never;
};

getServer

server/ outside a component, such as in the run of a command or an action. Each call goes to the machine in front when it is made.

export function getServer<Server>(): ServerCalls<Server>;

useServerEvent

Hears what emit says in server/, with the payload it was given.

export function useServerEvent<Payload>(name: string, listener: (payload: Payload) => void): void;

useNodeData

export function useNodeData<Data>(): [Data, (next: Data) => void];

useStorage

computer stays on this computer. project and machine live on the machine, where server/ reads machine keys too.

export function useStorage<Value>(key: string, initial: Value, scope?: 'computer' | 'project' | 'machine'): [Value, (next: Value) => void];

useProject

export function useProject(): ProjectRef & { name: string };

useActiveView

export function useActiveView(): ViewRef;

useViews

export function useViews(): { id: string; kind: string; name: string }[];

useSelection

export function useSelection(): NodeInfo[];

useNode

export function useNode(id: string): NodeInfo;

useNodes

export function useNodes(filter?: (node: NodeInfo) => boolean): NodeInfo[];

useLocale

Words from locales/<language>.json, English as the fallback, and every number and date through format.

export function useLocale(): {
    language: string;
    t(key: string, values?: Record<string, string | number>): string;
    format: typeof import('@adecore/ui/format');
};

ClientEvents

export interface ClientEvents {
    'node.added': { node: NodeInfo };
    'node.removed': { id: string };
    'node.changed': { node: NodeInfo };
    'selection.changed': { nodes: NodeInfo[] };
    'view.changed': { viewId: string | null };
    'project.opened': { projectId: string };
    'agent.status': { nodeId: string; status: AgentStatus };
    'chat.turnFinished': { chatId: string };
}

useEvent

export function useEvent<Name extends keyof ClientEvents>(name: Name, listener: (event: ClientEvents[Name]) => void): void;

onEvent

export function onEvent<Name extends keyof ClientEvents>(name: Name, listener: (event: ClientEvents[Name]) => void): () => void;

useAgents

Needs the permission agents.status.

export function useAgents(): { id: string; title: string; status: AgentStatus }[];

useUsageLimits

Needs the permission usage.read. used runs from 0 to 1.

export function useUsageLimits(): { provider: string; plan: string | null; windows: { label: string; used: number; resetsAt: number | null }[] }[];

host

export const host: {
    /** Marks work that is no action as busy: a spinner on `nodes` until it is done, which also stops it through the signal. */
    busy<Value>(work: { title: string; nodes?: NodeRef[] }, run: (signal: AbortSignal) => Value | Promise<Value>): Promise<Value>;
    notify(text: string, options?: { tone?: 'neutral' | 'error'; action?: { title: string; run(): void } }): void;
    /** Null when the person closed the picker. */
    pickFolder(initial?: string): Promise<string | null>;
    openSettings(): void;
    confirm(options: { title: string; body: string; danger?: boolean }): Promise<boolean>;
    /** Null when the person dismissed it. */
    prompt(options: { title: string; placeholder?: string; initial?: string }): Promise<string | null>;
    /** Null when the person dismissed it. */
    pick<Item>(options: { title: string; items: readonly Item[]; label(item: Item): string }): Promise<Item | null>;
    openUrl(url: string): void;
    copy(text: string): void;
};

FileEntry

export interface FileEntry {
    path: string;
    name: string;
    kind: 'file' | 'directory' | 'symlink' | 'other';
}

files

The project folder only, so a path outside it answers outside-project. Needs files.read or files.write, except open.

export const files: {
    readText(path: string): Promise<Result<string>>;
    write(path: string, text: string): Promise<Result<void>>;
    list(folder: string): Promise<Result<FileEntry[]>>;
    /** Calls back when the file or anything under the folder changes. Answers how to stop. */
    watch(path: string, listener: () => void): Result<() => void>;
    open(path: string): void;
};

GitFileStatus

export interface GitFileStatus {
    /** Relative to the root of the repository. */
    path: string;
    state: 'staged' | 'unstaged' | 'untracked' | 'conflicted';
    /** The porcelain letter, such as `M`, `A`, `D` or `?`. */
    status: string;
    added: number;
    deleted: number;
    binary: boolean;
}

GitStatus

export interface GitStatus {
    /** False outside a repository, with everything below empty. */
    repository: boolean;
    root: string | null;
    branch: string | null;
    upstream: string | null;
    ahead: number;
    behind: number;
    files: GitFileStatus[];
    /** More files changed than `files` holds. */
    truncated: boolean;
}

GitFileDiff

export interface GitFileDiff {
    path: string;
    /** The unified diff, empty for a binary file or one too large. */
    diff: string;
    added: number;
    deleted: number;
    binary: boolean;
    kind: 'add' | 'update' | 'delete' | null;
}

GitCommit

export interface GitCommit {
    hash: string;
    shortHash: string;
    subject: string;
    author: string;
    /** Epoch milliseconds of the author date. */
    at: number;
    refs: string[];
}

git

Needs git.read. repository is a folder in the project, the project folder by default.

export const git: {
    status(options?: { repository?: string }): Promise<Result<GitStatus>>;
    /** What changed in one file of the working tree, or in the index with `staged`. */
    diff(options: { path: string; repository?: string; staged?: boolean }): Promise<Result<GitFileDiff>>;
    /** The newest commits first, 30 by default. */
    log(options?: { repository?: string; limit?: number }): Promise<Result<GitCommit[]>>;
};

chats

list and read need chats.read, send needs chats.send.

export const chats: {
    list(): Promise<Result<ChatSummary[]>>;
    /** The thread as text, as an agent reads it. */
    read(chatId: string): Promise<Result<string>>;
    send(chatId: string, text: string): Promise<Result<void>>;
};

LaunchSummary

export interface LaunchSummary {
    id: string;
    name: string;
    kind: 'service' | 'task' | 'group';
    /** Null when it has not run since the machine started, and for a group. */
    state: 'starting' | 'running' | 'stopping' | 'exited' | null;
    exitCode: number | null;
    /** Whether a person approved it on this machine; only then does it start. */
    approved: boolean;
    port: number | null;
    url: string | null;
    /** The launches a group starts. */
    members: string[];
}

RoutineSummary

export interface RoutineSummary {
    id: string;
    title: string;
    kind: 'agent' | 'command' | 'launch';
    approved: boolean;
    paused: boolean;
    /** Epoch milliseconds of its next run, or null when none is due. */
    next: number | null;
    lastRun: { state: 'skipped' | 'starting' | 'running' | 'needsYou' | 'limit' | 'done' | 'failed'; at: number } | null;
}

ArtifactFileInfo

export interface ArtifactFileInfo {
    name: string;
    mime: string;
    size: number;
}

ArtifactInfo

export interface ArtifactInfo {
    id: string;
    title: string;
    kind: string;
    key: string | null;
    /** Epoch milliseconds. */
    createdAt: number;
}

ArtifactSummary

export interface ArtifactSummary extends ArtifactInfo {
    latest: { id: string; number: number; createdAt: number; files: ArtifactFileInfo[] };
    versions: number;
}

BrowserState

export interface BrowserState {
    node: NodeRef;
    /** Whether anyone has the page open; with nobody, `page` is null. */
    open: boolean;
    page: { url: string; title: string; loading: boolean; canGoBack: boolean; canGoForward: boolean } | null;
    error: string | null;
}

DeviceInfo

export interface DeviceInfo {
    name: string;
    platform: 'ios' | 'android';
    kind: 'simulator' | 'physical';
    runtime: string;
}

DeviceState

export interface DeviceState {
    node: NodeRef;
    device: DeviceInfo;
    /** False when this machine has no such device right now. */
    present: boolean;
    state: 'booted' | 'shutdown' | 'transitioning' | null;
    /** The size of the last shot, which `tap` counts its pixels in. */
    screen: { width: number; height: number } | null;
    buttons: string[];
    can: { shot: boolean; input: boolean; type: boolean; launch: boolean };
}

MachineActions

Each action of the machine an extension may call, with what it takes and answers.

export interface MachineActions {
    'launch.list': { input: {}; output: { launches: LaunchSummary[] } };
    /** `launch` is an id, or a name no other launch carries. `tail` keeps the last lines. */
    'launch.read': { input: { launch: string; tail?: number }; output: { id: string; name: string; text: string | null } };
    'launch.start': { input: { launch: string }; output: { id: string; name: string } };
    'launch.restart': { input: { launch: string }; output: { id: string; name: string } };
    'launch.stop': { input: { launch: string }; output: { id: string; name: string; stopping: number } };
    'routine.list': { input: {}; output: { routines: RoutineSummary[] } };
    'routine.run': { input: { routine: string }; output: { id: string; title: string; runId: string } };
    'routine.pause': { input: { routine: string }; output: { id: string; title: string } };
    'routine.resume': { input: { routine: string }; output: { id: string; title: string } };
    'artifact.list': { input: {}; output: { artifacts: ArtifactSummary[] } };
    /** The latest version and its first file unless `version` and `file` say otherwise. `text` is null for a file that is no text. */
    'artifact.read': {
        input: { artifact: string; version?: string; file?: number };
        output: { artifact: ArtifactInfo; version: { id: string; number: number }; file: ArtifactFileInfo; text: string | null; truncated: boolean };
    };
    /**
     * The files themselves, as `text` or as `base64` bytes, up to 2 MB together, in the project in front. The same `key`
     * makes a new version of the same artifact, and the same `requestId` makes a retry land once.
     */
    'artifact.publish': {
        input: {
            files: ({ name: string; text: string } | { name: string; base64: string })[];
            title?: string;
            kind?: 'report' | 'image' | 'build' | 'export' | 'recording' | 'page';
            key?: string;
            requestId?: string;
        };
        output: { artifact: ArtifactInfo; version: { id: string; number: number }; existing: boolean };
    };
    'browser.inspect': { input: { node: NodeRef }; output: BrowserState };
    'browser.navigate': { input: { node: NodeRef; url: string }; output: BrowserState };
    'browser.back': { input: { node: NodeRef }; output: BrowserState };
    'browser.forward': { input: { node: NodeRef }; output: BrowserState };
    'browser.reload': { input: { node: NodeRef; hard?: boolean }; output: BrowserState };
    /** `path` is a png on the machine, or null when nobody has the page open. */
    'browser.screenshot': { input: { node: NodeRef }; output: BrowserState & { path: string | null } };
    'device.list': { input: {}; output: { devices: (DeviceInfo & { id: string; state: 'booted' | 'shutdown' | 'transitioning' })[] } };
    'device.inspect': { input: { node: NodeRef }; output: DeviceState };
    /** A png on the machine, outside the project folder. */
    'device.screenshot': { input: { node: NodeRef }; output: { node: NodeRef; path: string; width: number; height: number } };
    /** A pixel of the last shot, counted from its top-left corner. */
    'device.tap': { input: { node: NodeRef; x: number; y: number }; output: { node: NodeRef } };
    /** `app` is a bundle id on iOS or a package name on Android. */
    'device.launch': { input: { node: NodeRef; app: string }; output: { node: NodeRef } };
}

MachineActionName

export type MachineActionName = keyof MachineActions;

MachineInput

export type MachineInput<Name extends MachineActionName> = MachineActions[Name]['input'];

MachineOutput

export type MachineOutput<Name extends MachineActionName> = MachineActions[Name]['output'];

launches

The machine's own actions, in the project in front: launches.start({ launch }) is launch.start. list and read need launches.read, the rest launches.run.

export const launches: {
    list(): Promise<Result<MachineOutput<'launch.list'>>>;
    read(input: MachineInput<'launch.read'>): Promise<Result<MachineOutput<'launch.read'>>>;
    start(input: MachineInput<'launch.start'>): Promise<Result<MachineOutput<'launch.start'>>>;
    restart(input: MachineInput<'launch.restart'>): Promise<Result<MachineOutput<'launch.restart'>>>;
    stop(input: MachineInput<'launch.stop'>): Promise<Result<MachineOutput<'launch.stop'>>>;
};

routines

list needs routines.read, the rest routines.run.

export const routines: {
    list(): Promise<Result<MachineOutput<'routine.list'>>>;
    run(input: MachineInput<'routine.run'>): Promise<Result<MachineOutput<'routine.run'>>>;
    pause(input: MachineInput<'routine.pause'>): Promise<Result<MachineOutput<'routine.pause'>>>;
    resume(input: MachineInput<'routine.resume'>): Promise<Result<MachineOutput<'routine.resume'>>>;
};

artifacts

list and read need artifacts.read, publish artifacts.write.

export const artifacts: {
    list(): Promise<Result<MachineOutput<'artifact.list'>>>;
    read(input: MachineInput<'artifact.read'>): Promise<Result<MachineOutput<'artifact.read'>>>;
    publish(input: MachineInput<'artifact.publish'>): Promise<Result<MachineOutput<'artifact.publish'>>>;
};

browser

Needs browser.control.

export const browser: {
    inspect(input: MachineInput<'browser.inspect'>): Promise<Result<MachineOutput<'browser.inspect'>>>;
    navigate(input: MachineInput<'browser.navigate'>): Promise<Result<MachineOutput<'browser.navigate'>>>;
    back(input: MachineInput<'browser.back'>): Promise<Result<MachineOutput<'browser.back'>>>;
    forward(input: MachineInput<'browser.forward'>): Promise<Result<MachineOutput<'browser.forward'>>>;
    reload(input: MachineInput<'browser.reload'>): Promise<Result<MachineOutput<'browser.reload'>>>;
    screenshot(input: MachineInput<'browser.screenshot'>): Promise<Result<MachineOutput<'browser.screenshot'>>>;
};

devices

Needs devices.control.

export const devices: {
    list(): Promise<Result<MachineOutput<'device.list'>>>;
    inspect(input: MachineInput<'device.inspect'>): Promise<Result<MachineOutput<'device.inspect'>>>;
    screenshot(input: MachineInput<'device.screenshot'>): Promise<Result<MachineOutput<'device.screenshot'>>>;
    tap(input: MachineInput<'device.tap'>): Promise<Result<MachineOutput<'device.tap'>>>;
    launch(input: MachineInput<'device.launch'>): Promise<Result<MachineOutput<'device.launch'>>>;
};

terminals

export const terminals: {
    /** Needs `terminals.read`. What the terminal shows now. */
    read(terminal: NodeRef): Promise<Result<string>>;
    /** Needs `canvas.write`. Opens a shell and types nothing: only a person types in a terminal. */
    open(options?: { title?: string; cwd?: string; near?: NodeRef[] }): Result<NodeRef>;
};

DatabaseValue

A cell as the database gave it: a large number, a decimal or a date as the text the server writes for it. Bytes come as hex, with their whole length only when the hex is cut off, and a long text cut off as preview with its whole length.

export type DatabaseValue =
    | string
    | number
    | boolean
    | null
    | { kind: 'binary'; hex: string; length?: number }

database

Needs database.read. One SELECT or WITH statement on a read-only session, as for an agent.

export const database: {
    connections(): Promise<Result<{ id: string; name: string }[]>>;
    query(connection: string, sql: string): Promise<Result<{ columns: string[]; rows: DatabaseValue[][]; hasMore: boolean }>>;
};

views

Needs views.write, except open.

export const views: {
    open(view: ViewRef): void;
    createCanvas(name: string): Result<ViewRef>;
    rename(view: ViewRef, name: string): Result<void>;
};

ContextSource

export interface ContextSource {
    id: string;
    /** Such as `text`, `terminal`, `chat`, `browser`, `file` or `extension`; a newer Ruimte may add one. */
    kind: string;
    title: string;
    text: string;
}

context

Needs the permission context.read.

export const context: {
    /** Each node in `nodes` and every node on `view`, each once. Naming neither reads nothing. */
    read(of: { nodes?: NodeRef[]; view?: ViewRef }): Promise<Result<ContextSource[]>>;
};

agents

export const agents: {
    /** Needs `agents.run`. One hidden prompt on a CLI of the person, with the answer whole. */
    run(options: { prompt: string; context?: string[]; provider?: string }): Promise<Result<{ text: string }>>;
    /** Needs `agents.run`. The same, in pieces as the CLI writes them. A failure is the last piece. */
    stream(options: { prompt: string; context?: string[]; provider?: string; signal?: AbortSignal }): AsyncIterable<Result<{ text: string }>>;
    /** Needs `agents.start`. A chat a person sees and can stop, supervised at most: on the canvas in front, or a view of its own in the sidebar with none there. */
    start(options: { prompt?: string; provider?: string; title?: string; near?: NodeRef[] }): Promise<Result<NodeRef>>;
};

NodeColor

An accent of Ruimte, which follows the theme.

export type NodeColor =
    | 'red'
    | 'orange'
    | 'amber'
    | 'yellow'
    | 'lime'
    | 'green'
    | 'emerald'
    | 'teal'
    | 'cyan'
    | 'sky'
    | 'blue'
    | 'indigo'
    | 'violet'
    | 'purple'
    | 'fuchsia'
    | 'pink'
    | 'rose';

canvas

Needs canvas.write, except reveal and select. What makes a node answers no-canvas without a canvas in front.

export const canvas: {
    createNote(note: { title?: string; text: string; near?: NodeRef[]; color?: NodeColor }): Result<NodeRef>;
    arrange(nodes: NodeRef[], layout: 'row' | 'column' | 'grid'): Result<void>;
    /** A person confirms first; `refused` when they did not. */
    remove(nodes: NodeRef[]): Promise<Result<void>>;
    createFile(path: string, options?: { near?: NodeRef[] }): Result<NodeRef>;
    createBrowser(url: string, options?: { near?: NodeRef[] }): Result<NodeRef>;
    /** A node of this extension's own, by the id it gave `defineNode`. */
    createNode<Data>(type: string, options?: { data?: Data; title?: string; near?: NodeRef[] }): Result<NodeRef>;
    update(node: NodeRef, patch: { title?: string; x?: number; y?: number; width?: number; height?: number }): Result<void>;
    writeNote(node: NodeRef, text: string): Result<void>;
    group(nodes: NodeRef[]): Result<NodeRef>;
    link(from: NodeRef, to: NodeRef): Result<void>;
    unlink(from: NodeRef, to: NodeRef): Result<void>;
    select(nodes: NodeRef[]): void;
    reveal(target: NodeRef): void;
};