Skip to main content

Terminal

ctx.terminal is the live catalog and control surface for every integrated terminal, whether a mod created it or not.

Reference: TerminalApi, TerminalAgentApi, and TerminalAgentStartOptions.

ctx.terminal
├── snapshot                 every panel, editor, and background terminal
├── borrow(id, authority)    acquire an existing terminal
├── create()                 create a terminal owned by this mod
├── getOrCreate(key)         reuse one owned terminal
└── agents
    ├── providers            Claude and Codex capabilities
    ├── promote(terminal)    bind a proven provider session
    ├── start(options)       launch and complete the first turn
    └── getOrStart(key, ...) launch once, then keep the same conversation

Authority is explicit

Acquiring a terminal does not silently make it yours:

HandleWhat it can do
ObservedTerminalread identity and live state
ControlledTerminalreveal, write, submit, paste, and execute
OwnedTerminaleverything above, plus close

Only the mod that created a terminal owns it. Borrow an existing terminal with observe or control authority. The return type exposes only the operations that authority permits.

execute() follows the host shell lifecycle and returns a TerminalCommandResult. It does not infer completion by scraping the visible buffer.

Terminal agents

A TerminalAgent binds an integrated terminal to a provider-native Claude or Codex session. Its turns resolve from the provider's durable session log:

create terminal
  └──▶ wait for provider input
        └──▶ submit first prompt
              └──▶ bind durable session identity
                    └──▶ resolve from the provider transcript

Codex uses inline display by default, so the editor terminal keeps its visible history. agent.run(prompt) sends a follow-up to the same provider session. agent.transcript() reads all normalized user and assistant messages from the durable log.

One command, from shell to Codex and back

This mod publishes a command that accepts one prompt, reuses a keyed Codex terminal, and returns the completed response as structured JSON.

import { function command<TId extends string>(commandValue: CommandInput<TId>): Command<TId>
Declare an action the user runs from the command palette or a keybinding. Reach for this when a person triggers the behavior. The `title` is what they read in the palette, `category` groups the entry, and `when` decides whether it shows up at all. The handler takes no argument, only a context. Two neighbours are easy to confuse. `slashCommand` is also user-triggered, but it is typed inside a chat session and receives free text plus the live AI session. `tool` is not user-triggered at all, the model decides to call it mid-turn. The result goes in the mod's top-level `commands` array, alongside any slash commands. The `id` is the address a keybinding or a `command:` hook targets, so keep it stable.
@categoryAuthoring
command
, function defineMod<const TMod extends ModDefinition>(definition: TMod & ValidateModHooks<TMod>): Mod
The entry point of the SDK. A mod's module default-exports one `defineMod` call and nothing else. The returned value is data. It registers nothing, starts nothing, and touches no host state. The host folds it twice. At build time it runs the module in a throwaway sandbox and reads the value to derive the serializable manifest. At runtime it folds the same value again to wire your handlers into their lanes: ext-host for commands, tools, effects, and hooks; the sandboxed renderer for views and widgets. That double fold is why the module must be pure. It is re-executed to produce the manifest, so anything at the top level, a file read, a network call, a timer, a `console` line, runs during a build with no host attached. Put behavior inside handlers: `run`, `activate`, `handle`. The `ValidateModHooks` constraint makes the call fail to typecheck when a `hook.beforeAction` targets an action that is neither a known host action nor `command:` plus one of this mod's own command ids.
@example```ts export default defineMod({ metadata: { displayName: 'Review', description: 'Review helpers for the current change.', }, commands: [ command({ id: 'review.open', title: 'Review: Open', run: openPanel }), slashCommand({ id: 'review', description: 'Review this change', run: review }), ], ai: { tools: [reviewTool], }, }); ```@categoryAuthoring
defineMod
, class TerminalError
Recoverable terminal failure shared by every runtime projection.
TerminalError
} from '@modular/sdk';
type
type PromptInput = {
    readonly kind: "valid";
    readonly value: string;
} | {
    readonly kind: "invalid";
    readonly message: string;
}
PromptInput
=
| { readonly kind: "valid"kind: 'valid'; readonly value: stringvalue: string } | { readonly kind: "invalid"kind: 'invalid'; readonly message: stringmessage: string }; function function parsePrompt(args: readonly unknown[]): PromptInputparsePrompt(args: readonly unknown[]args: readonly unknown[]):
type PromptInput = {
    readonly kind: "valid";
    readonly value: string;
} | {
    readonly kind: "invalid";
    readonly message: string;
}
PromptInput
{
if (args: readonly unknown[]args.ReadonlyArray<unknown>.length: number
Gets the length of the array. This is a number one higher than the highest element defined in an array.
length
!== 1) {
return { kind: "invalid"kind: 'invalid', message: stringmessage: 'Pass exactly one prompt.' }; } const const value: unknownvalue = args: readonly unknown[]args[0]; if (typeof const value: unknownvalue !== 'string' || const value: stringvalue.String.trim(): string
Removes the leading and trailing white space and line terminator characters from a string.
trim
().String.length: number
Returns the length of a String object.
length
=== 0) {
return { kind: "invalid"kind: 'invalid', message: stringmessage: 'The prompt must be a non-empty string.' }; } return { kind: "valid"kind: 'valid', value: stringvalue }; } const const askCodex: Command<"local.coding-worker.askCodex">askCodex = command<"local.coding-worker.askCodex">(commandValue: CommandInput<"local.coding-worker.askCodex">): Command<"local.coding-worker.askCodex">
Declare an action the user runs from the command palette or a keybinding. Reach for this when a person triggers the behavior. The `title` is what they read in the palette, `category` groups the entry, and `when` decides whether it shows up at all. The handler takes no argument, only a context. Two neighbours are easy to confuse. `slashCommand` is also user-triggered, but it is typed inside a chat session and receives free text plus the live AI session. `tool` is not user-triggered at all, the model decides to call it mid-turn. The result goes in the mod's top-level `commands` array, alongside any slash commands. The `id` is the address a keybinding or a `command:` hook targets, so keep it stable.
@categoryAuthoring
command
({
id: "local.coding-worker.askCodex"id: 'local.coding-worker.askCodex', title: stringtitle: 'Ask Codex in a terminal', category?: string | undefinedcategory: 'Coding worker', run: (ctx: CommandContext) => Awaitable<unknown>run: async ctx: CommandContextctx => { const const input: PromptInputinput = function parsePrompt(args: readonly unknown[]): PromptInputparsePrompt(ctx: CommandContextctx.command: CommandInvocation
What was run, and with what arguments.
command
.CommandInvocation.args: readonly unknown[]
Arguments the caller passed, in the order they were given.
args
);
if (const input: PromptInputinput.kind: "valid" | "invalid"kind === 'invalid') { return
const input: {
    readonly kind: "invalid";
    readonly message: string;
}
input
;
} ctx: CommandContextctx.ContextBase.logger: Logger
Your mod's log channel.
logger
.Logger.info(message: string, ...args: readonly unknown[]): voidinfo('[coding-worker] starting turn');
const const result: TerminalError | TerminalAgentStartResultresult = await ctx: CommandContextctx.ContextBase.terminal: TerminalApi
Create and drive terminals.
terminal
.TerminalApi.agents: TerminalAgentApi
Provider-native Claude and Codex sessions running in integrated terminals.
agents
.TerminalAgentApi.getOrStart(key: string, options: TerminalAgentStartOptions, operationOptions?: TerminalOperationOptions): Promise<TerminalAgentStartResult | TerminalError>getOrStart('coding-worker', {
TerminalAgentCodexStartOptions.provider: "codex"provider: 'codex', TerminalAgentCodexStartOptions.model?: string | undefinedmodel: 'gpt-5.6-luna', TerminalAgentCodexStartOptions.sandbox?: TerminalAgentCodexSandbox | undefinedsandbox: 'workspace-write', TerminalAgentCodexStartOptions.approval?: TerminalAgentCodexApprovalPolicy | undefinedapproval: 'never', TerminalAgentCodexStartOptions.prompt: stringprompt:
const input: {
    readonly kind: "valid";
    readonly value: string;
}
input
.value: stringvalue,
TerminalAgentCodexStartOptions.terminal?: TerminalCreateOptions | undefinedterminal: { TerminalCreateOptions.name?: string | undefinedname: 'Coding worker', TerminalCreateOptions.location?: "editor" | "panel" | undefinedlocation: 'editor', TerminalCreateOptions.reveal?: "none" | "preserve-focus" | "focus" | undefinedreveal: 'focus', }, }); if (const result: TerminalError | TerminalAgentStartResultresult instanceof class TerminalError
Recoverable terminal failure shared by every runtime projection.
TerminalError
) {
ctx: CommandContextctx.ContextBase.logger: Logger
Your mod's log channel.
logger
.Logger.error(message: string, ...args: readonly unknown[]): voiderror('[coding-worker] turn failed', const result: TerminalErrorresult);
return { kind: "failed"kind: 'failed' as type const = "failed"const, code: TerminalErrorCodecode: const result: TerminalErrorresult.TerminalError.code: TerminalErrorCodecode, message: stringmessage: const result: TerminalErrorresult.Error.message: stringmessage, }; } ctx: CommandContextctx.ContextBase.logger: Logger
Your mod's log channel.
logger
.Logger.info(message: string, ...args: readonly unknown[]): voidinfo('[coding-worker] turn completed', {
terminalId: string & $brand<"TerminalId">terminalId: const result: TerminalAgentStartResultresult.TerminalAgentStartResult.agent: TerminalAgent<OwnedTerminal>agent.TerminalAgent<OwnedTerminal>.terminal: OwnedTerminalterminal.TerminalReference.id: string & $brand<"TerminalId">id, completedAt: stringcompletedAt: const result: TerminalAgentStartResultresult.TerminalAgentRunResult.completedAt: stringcompletedAt, }); return { kind: "completed"kind: 'completed' as type const = "completed"const, terminalId: string & $brand<"TerminalId">terminalId: const result: TerminalAgentStartResultresult.TerminalAgentStartResult.agent: TerminalAgent<OwnedTerminal>agent.TerminalAgent<OwnedTerminal>.terminal: OwnedTerminalterminal.TerminalReference.id: string & $brand<"TerminalId">id, completedAt: stringcompletedAt: const result: TerminalAgentStartResultresult.TerminalAgentRunResult.completedAt: stringcompletedAt, response: stringresponse: const result: TerminalAgentStartResultresult.TerminalAgentRunResult.response: stringresponse, }; }, }); export default
defineMod<{
    readonly metadata: {
        readonly displayName: "Coding Worker";
        readonly description: "Runs one durable Codex conversation in an editor terminal.";
    };
    readonly commands: readonly [Command<"local.coding-worker.askCodex">];
}>(definition: {
    readonly metadata: {
        readonly displayName: "Coding Worker";
        readonly description: "Runs one durable Codex conversation in an editor terminal.";
    };
    readonly commands: readonly [Command<"local.coding-worker.askCodex">];
}): Mod
The entry point of the SDK. A mod's module default-exports one `defineMod` call and nothing else. The returned value is data. It registers nothing, starts nothing, and touches no host state. The host folds it twice. At build time it runs the module in a throwaway sandbox and reads the value to derive the serializable manifest. At runtime it folds the same value again to wire your handlers into their lanes: ext-host for commands, tools, effects, and hooks; the sandboxed renderer for views and widgets. That double fold is why the module must be pure. It is re-executed to produce the manifest, so anything at the top level, a file read, a network call, a timer, a `console` line, runs during a build with no host attached. Put behavior inside handlers: `run`, `activate`, `handle`. The `ValidateModHooks` constraint makes the call fail to typecheck when a `hook.beforeAction` targets an action that is neither a known host action nor `command:` plus one of this mod's own command ids.
@example```ts export default defineMod({ metadata: { displayName: 'Review', description: 'Review helpers for the current change.', }, commands: [ command({ id: 'review.open', title: 'Review: Open', run: openPanel }), slashCommand({ id: 'review', description: 'Review this change', run: review }), ], ai: { tools: [reviewTool], }, }); ```@categoryAuthoring
defineMod
({
metadata: {
    readonly displayName: "Coding Worker";
    readonly description: "Runs one durable Codex conversation in an editor terminal.";
}
metadata
: {
displayName: "Coding Worker"displayName: 'Coding Worker', description: "Runs one durable Codex conversation in an editor terminal."description: 'Runs one durable Codex conversation in an editor terminal.', }, commands: readonly [Command<"local.coding-worker.askCodex">]commands: [const askCodex: Command<"local.coding-worker.askCodex">askCodex], });

Run it from the workspace that has the mod loaded:

modular context commands run local.coding-worker.askCodex \
  --args='["Reply with exactly hi and nothing else."]'

The first call creates the editor terminal and completes its first turn. Later calls reuse the coding-worker key, submit follow-ups to the same Codex conversation, and return each durable completion to the CLI.

Failures are values

Terminal operations return TerminalError for recoverable failures such as a closed terminal, denied authority, cancellation, unsupported shell execution, or an agent identity that cannot be proven. Handle that result where the operation crosses into your domain. Do not catch a thrown transport exception or guess success from what happens to be visible on screen.