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:
| Handle | What it can do |
|---|---|
ObservedTerminal | read identity and live state |
ControlledTerminal | reveal, write, submit, paste, and execute |
OwnedTerminal | everything 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.command, function defineMod<const TMod extends ModDefinition>(definition: TMod & ValidateModHooks<TMod>): ModThe 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.defineMod, class TerminalErrorRecoverable terminal failure shared by every runtime projection.TerminalError } from '@modular/sdk'; typetype 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: numberGets 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(): stringRemoves the leading and trailing white space and line terminator characters from a string.trim().String.length: numberReturns 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.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: CommandInvocationWhat 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') { returnconst input: { readonly kind: "invalid"; readonly message: string; }input; } ctx: CommandContextctx.ContextBase.logger: LoggerYour 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: TerminalApiCreate and drive terminals.terminal.TerminalApi.agents: TerminalAgentApiProvider-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 TerminalErrorRecoverable terminal failure shared by every runtime projection.TerminalError) { ctx: CommandContextctx.ContextBase.logger: LoggerYour 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: LoggerYour 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 defaultdefineMod<{ 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">]; }): ModThe 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.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.