Arcane OS Docs Development only

Reference

Arcane.terminal

Bounded native terminal-session lifecycle and input operations.

  • Reference

This focused page is derived from the mechanically checked full member inventory.

Syntax

Arcane.terminal

Member kind

Namespace

Description

Bounded native terminal-session lifecycle and input operations.

Overview

Arcane.terminal owns a bounded native terminal-session lifecycle. It can start and enumerate app-owned sessions, write input, update dimensions, send a supported control signal, and request closure. Process output and final exit state arrive through Arcane.events; they are not returned by write(), signal(), or close().

Terminal access is deliberately narrow. The bound application must have the exact id terminal, must be granted terminal.execute, and must run through an admitted Core or Android host. Other applications do not receive terminal access by default, and a standalone browser preview is not a native terminal host.

Availability and capability check

Feature detection answers whether the JavaScript surface was projected into the document. Arcane.capabilities.list() confirms the bound application identity, grants, and exact admitted RPC methods. Check both before presenting a terminal workflow.

The complete namespace contains these methods:

Member Purpose
Arcane.terminal.start(options?) Start one native session.
Arcane.terminal.list() Return the wrapper containing current app-owned sessions.
Arcane.terminal.write(sessionId, data) Write one nonempty UTF-8 input chunk.
Arcane.terminal.resize(sessionId, columns, rows) Update the session's bounded dimensions.
Arcane.terminal.signal(sessionId, signal?) Request interrupt or terminate.
Arcane.terminal.close(sessionId) Request session closure.

The host admits at most eight concurrent sessions for the application. Session identifiers are opaque strings returned by start() or list(); do not invent, parse, or persist them as durable identities.

Session lifecycle

Subscribe to terminal events before calling start(). A newly spawned process may produce output immediately, and the output forwarding path is active before the start response reaches application code. Buffer early events by sessionId until the returned session identifies the process of interest.

The normal lifecycle is:

  1. Confirm the feature, application id, grant, and admitted methods.
  2. Subscribe to terminal.output, terminal.exit, and terminal.error.
  3. Call start() and retain the returned session object.
  4. Use write(), resize(), and, when necessary, signal().
  5. Call close() when the application is finished with the session.
  6. Treat terminal.exit as the process-completion observation.
  7. Unsubscribe every listener during teardown.

An accepted write, signal, or close request is not command completion or process exit. In particular, close() resolves when the host accepts the close request; observe terminal.exit for the final process outcome.

Shared values

start() resolves to this exact session shape:

Property Type Meaning
id string Opaque session identifier, at most 128 characters.
shell string Resolved shell name.
cwd string Resolved working directory.
title string Host-provided display title.
columns number Accepted column count, from 20 through 500.
rows number Accepted row count, from 5 through 200.
createdAt string Host timestamp for session creation.

list() resolves to { sessions }, not directly to an array. Each list entry contains id, shell, cwd, columns, rows, createdAt, and state. Supported state values are starting, running, exited, and closed, though a host may remove a completed session from the current inventory promptly.

Terminal events

Terminal events are ordinary, future-only events:

Event Trigger Data payload
terminal.output The host reads a stdout or stderr chunk. { sessionId, stream, data }, where stream is "stdout" or "stderr" and data is a string.
terminal.exit The process exits and the host retires the session. { sessionId, exitCode, signal }; exitCode or signal may be null.
terminal.error A host reports an asynchronous stream/session error. { sessionId, message }; currently used by the Android provider for output-limit and stream-read failures.

Output payloads are chunks, not lines. A chunk may contain part of a line, several lines, or terminal control sequences. Preserve arrival order per session and use a terminal-aware renderer when displaying native output.

Platform behavior

On Microsoft NT Core hosts, auto resolves to PowerShell. PowerShell, Command Prompt, and an installed Bash are selectable; POSIX sh is unavailable. On Linux Core hosts, auto resolves to Bash, sh selects /bin/sh, PowerShell requires an installed pwsh, and Command Prompt is unavailable.

On Android, terminal execution is confined to the Arcane Terminal application's ordinary app identity and private files area. Only auto and sh are accepted, and both resolve to the application-sandbox /system/bin/sh. Android stops a session after one MiB of emitted output and reports the condition through terminal.error.

Errors and recovery

Rejected operations use Arcane.Error. Read code, message, and resolution instead of matching the message text. Common terminal failures include:

Code Recovery
METHOD_NOT_ALLOWED Open Arcane Terminal through an admitted host; another app cannot self-grant terminal.execute.
ARCANE_TRANSPORT_UNAVAILABLE Open the application through the installed Arcane host or its development launcher.
METHOD_CONTRACT_INPUT_INVALID or TERMINAL_REQUEST_INVALID Send only the documented values and bounds.
TERMINAL_SESSION_LIMIT Close an existing session before starting another.
TERMINAL_SHELL_INVALID or TERMINAL_SHELL_UNAVAILABLE Select a supported shell for the active platform.
TERMINAL_CWD_INVALID Choose an existing accessible directory allowed by the host sandbox.
TERMINAL_START_FAILED Verify that the selected shell is installed and available.
TERMINAL_SESSION_INVALID or TERMINAL_SESSION_NOT_FOUND Refresh with list() or start a new session; do not reuse a retired id.
TERMINAL_DATA_INVALID or TERMINAL_INPUT_CLOSED Send a nonempty chunk no larger than 64 KiB to a running session.
TERMINAL_SIGNAL_INVALID Use only interrupt or terminate.

Example

const arcane = globalThis.Arcane;
const terminal = arcane?.terminal;
const events = arcane?.events;

if (!terminal?.start || !events?.on || !arcane?.capabilities?.list) {
    throw new Error('Open Arcane Terminal from an admitted Arcane host.');
}

const access = await arcane.capabilities.list();
const requiredMethods = [
    'terminal.start',
    'terminal.list',
    'terminal.write',
    'terminal.resize',
    'terminal.close'
];

if (
    access.app?.id !== 'terminal'
    || !access.grants.includes('terminal.execute')
    || !requiredMethods.every(function isRequiredTerminalMethodAdmitted(method) {
        return access.methods.includes(method);
    })
) {
    throw new Error('This application is not admitted for terminal execution.');
}

const earlyOutput = new Map();
const observedExits = new Map();
const exitWaiters = new Map();
const maxBufferedChunksPerSession = 128;
let activeSessionId = null;
let session = null;

function displayOutput({stream, data}) {
    const write = stream === 'stderr' ? console.error : console.log;
    write(data);
}

const offOutput = events.on('terminal.output', function handleTerminalOutput(payload) {
    if (payload.sessionId === activeSessionId) {
        displayOutput(payload);
        return;
    }
    const chunks = earlyOutput.get(payload.sessionId) ?? [];
    chunks.push(payload);
    // Bound pre-identification buffering; surface truncation in a real UI.
    if (chunks.length > maxBufferedChunksPerSession) {
        chunks.shift();
    }
    earlyOutput.set(payload.sessionId, chunks);
});

const offExit = events.on('terminal.exit', function handleTerminalExit(payload) {
    observedExits.set(payload.sessionId, payload);
    const waiter = exitWaiters.get(payload.sessionId);
    if (waiter) {
        clearTimeout(waiter.timer);
        exitWaiters.delete(payload.sessionId);
        waiter.resolve(payload);
    }
});

const offError = events.on('terminal.error', function handleTerminalError(payload) {
    console.error(`Terminal ${payload.sessionId}: ${payload.message}`);
});

function waitForExit(sessionId, timeoutMs = 5000) {
    if (observedExits.has(sessionId)) {
        return Promise.resolve(observedExits.get(sessionId));
    }
    return new Promise(function createExitWait(resolve, reject) {
        const timer = setTimeout(function rejectTimedOutExitWait() {
            exitWaiters.delete(sessionId);
            reject(new Error(`Timed out waiting for ${sessionId} to exit.`));
        }, timeoutMs);
        exitWaiters.set(sessionId, {resolve, timer});
    });
}

try {
    session = await terminal.start({
        shell: 'auto',
        cwd: '',
        columns: 120,
        rows: 32
    });
    activeSessionId = session.id;

    for (const payload of earlyOutput.get(session.id) ?? []) {
        displayOutput(payload);
    }
    earlyOutput.delete(session.id);

    await terminal.resize(session.id, 100, 30);
    const {sessions} = await terminal.list();
    console.log('Owned sessions', sessions);

    const lineEnding = ['powershell', 'cmd'].includes(session.shell)
        ? '\r\n'
        : '\n';
    await terminal.write(
        session.id,
        `echo Arcane terminal ready${lineEnding}`
    );

    const exitPromise = waitForExit(session.id);
    const closeResult = await terminal.close(session.id);
    console.log('Close request accepted', closeResult.accepted);

    const exit = await exitPromise;
    console.log('Process exited', exit.exitCode, exit.signal);
} catch (error) {
    if (error instanceof arcane.Error) {
        console.error(error.code, error.message, error.resolution);
    } else {
        throw error;
    }
} finally {
    if (session && !observedExits.has(session.id)) {
        await terminal.close(session.id).catch(function ignoreTerminalCloseFailure() {});
    }
    offOutput();
    offExit();
    offError();
    for (const waiter of exitWaiters.values()) {
        clearTimeout(waiter.timer);
    }
    exitWaiters.clear();
}

Members

  • Arcane.terminal.start() — Starts one of at most eight app-owned native terminal sessions. Requires terminal.execute, app id terminal, and a Core or Android host.
  • Arcane.terminal.list() — Lists the current app-owned sessions; it returns a wrapper object, not a bare array. Requires terminal.execute, app id terminal, and a Core or Android host.
  • Arcane.terminal.write() — Writes one input chunk. Output is delivered separately through terminal.output, so subscribe before starting a session and correlate chunks by sessionId.
  • Arcane.terminal.resize() — Updates the session dimensions. The current hosts record an emulated resize rather than claiming a native pseudo-terminal resize.
  • Arcane.terminal.signal() — Requests a supported control signal. accepted reports process-controller acceptance, not process exit; observe terminal.exit for completion.
  • Arcane.terminal.close() — Closes input and requests session termination. The resolved value acknowledges the request; observe terminal.exit for actual process completion.

Reference group

Namespace, constructor, and values

Repository and reviewed source access