Skip to content

feat(runtime): expose the terminal capability to routes and scripts as request.terminal (#511) - #534

Merged
ScriptedAlchemy merged 7 commits into
mainfrom
feat/511-terminal-capability
Sep 4, 2026
Merged

feat(runtime): expose the terminal capability to routes and scripts as request.terminal (#511)#534
ScriptedAlchemy merged 7 commits into
mainfrom
feat/511-terminal-capability

Conversation

@ScriptedAlchemy

@ScriptedAlchemy ScriptedAlchemy commented Sep 4, 2026

Copy link
Copy Markdown
Owner

Fixes #511.

What

Routes and scripts get a read-only terminal capability from the framework instead of each plugin probing process.stdout.isTTY / columns / FORCE_COLOR itself.

(await agent()).terminal is a new Observed<AgentTerminal> axis beside lineage and workspace:

interface AgentTerminal {
  hostSurface: 'cli' | 'mcp' | 'hook' | 'script' | 'workbench';
  stdout: AgentTerminalStream;
  stderr: AgentTerminalStream;
  sharesTarget: boolean;          // fd 1 and fd 2 name one open file (2>&1, one shared terminal)
}
interface AgentTerminalStream {
  kind: 'tty' | 'pipe' | 'none';  // interactive terminal | any other open descriptor | no stream for the route
  color: 'none' | 'basic' | '256' | 'truecolor';
  columns?: number;               // terminal size, or COLUMNS override
  rows?: number;                  // terminal size, or LINES override
}

Per stream rather than flat because the filing consumer (cargo-hauler src/client/exec.ts) decides stdout and stderr color independently and asks whether the two share a target (2>&1) before merging a child's channels.

Per-surface values (honest, never guessed)

Surface hostSurface stdout / stderr source
Routed CLI executable (npm bin and <target>/bin/<name>.mjs) — plain, rendered, or projected MCP command cli probed from the executable's own process; the rendered worker thread receives the executable's probe, never its own pipes native
Rendered script (src/scripts/<name>.tsx) script probed native
Plain main script / bin cli (bin) / script (artifact) probed; delivered as main(argv, { terminal }) (ExecutableMainContext)
Generated MCP server mcp none / none, color: 'none', sharesTarget: false derived
Event route (shared runtime or standalone hook) hook none / none derived
Workbench lifecycle replay workbench none / none derived
Custom runAgentRequest host that mounts none unavailable (not-provided)

Color precedence (matches the informal standards and the consumer's own colorEnabled): FORCE_COLOR decides outright (0/false off; ''/1/true basic; 2 256; 3 truecolor), CLICOLOR_FORCE forces on for a pipe at the COLORTERM/TERM depth, NO_COLOR (non-empty) and CLICOLOR=0 force off, TERM=dumb none, else tty-at-advertised-depth / pipe-none. COLUMNS/LINES override size. The routed CLI selects its tty-vs-Markdown mode from the same value, so plugin output and framework output can never disagree.

How

  • packages/rsc-runtime/src/agent-request.ts: AgentTerminal* types, terminal on the handle/init, AGENT_REQUEST_STORE_VERSION 3 → 4.
  • packages/agent-bundle/src/terminal-capability.ts (new, plain Node, no deps; rslib entry + agent-bundle/terminal-capability alias): detectProcessTerminal, terminalColor, sharesOutputTarget, noTerminal. No Effect runtime in artifacts; the first-party CLI is untouched (no eager Terminal loads — compatible with perf(cli): load the Effect terminal runtime lazily to restore cold-start time #530).
  • cli-entry.ts: resolveTerminal probes once per invocation (explicit terminal option wins; legacy isTty still honoured as a stdout override), drives the output mode, and hands terminal to execute/render/createSession.
  • entry-shell.ts: CLI bin mounts available(context.terminal, 'native'); rendered-route worker mounts message.terminal (forwarded, never probed); MCP Flight worker forwards message.terminal ?? unavailable; main envelope calls main(argv, { terminal: detectProcessTerminal(<cli|script>) }).
  • mcp-server-runtime.ts host scopes mount noTerminal('mcp') / noTerminal('hook') and forward context.terminal to the worker; standalone hook shell mounts the hook literal; Workbench replay mounts workbench.
  • Harness: invokeCli/runScript tty knob → deterministic synthetic terminal (80×24 basic tty, or two color-free pipes); renderRoute mounts the route kind's artifact value; in-memory MCP forwards the server's none; context.terminal injectable; plain-script child envelope mirrors the generated one.
  • Public types: AgentTerminal* and ExecutableMainContext from agent-bundle / agent-bundle/routes.

Tests

  • unit: terminal-capability.test.ts (color precedence, tty/pipe/closed-fd kinds, COLUMNS/LINES, sharesTarget), entry-shell.test.ts (envelope, bin/worker/Flight-worker templates, digest pin), cli-routes.test.ts (mode ↔ terminal agreement, --json, legacy isTty), rsc-runtime/tests/agent-request.test.ts.
  • route-unit: renderRoute MCP → none, event route → hook/none/none, injection.
  • projection: invokeCli plain (default pipe / tty), projected MCP command harness context --yes --json (→ cli, --json does not change the capability), injection; runScript rendered (script/pipe/pipe, script/tty/tty) and plain checksum --terminal (real child process probe); in-memory MCP → none.
  • packed (packed-stdio-projection.test.ts, through openPackedMcpServer + spawned bin/route-harness.mjs and scripts/checksum.mjs): pipe detection, FORCE_COLOR=3 + COLUMNS=120, CLICOLOR_FORCE vs NO_COLOR, LINES; packed MCP contextnone; shared-runtime event route → hook/none/none.

Ran locally: pnpm typecheck, pnpm lint, pnpm test:unit (3255 passed), pnpm test:route-unit, pnpm test:projection, node scripts/run-packed-tests.mjs packages/agent-bundle/tests/packed-stdio-projection.test.ts, pnpm docs:site:build.

Docs

docs/entry-conventions.md (new "Terminal capability (request.terminal)" section + envelope), docs/effect-conventions.md (Terminal/Stdio cross-reference: the route-facing capability is plain Node, not Terminal), website en+zh: guide/authoring/mcp.mdx (request-context section + per-surface table), package-entries.mdx (envelope), scripts-assets.mdx. One patch changeset for agent-bundle and @agent-bundle/runtime.

Coordination

Review status

Codex reviewed b19e2d8 (two P1 threads); both are addressed in 4df2dd8:

@changeset-bot

changeset-bot Bot commented Sep 4, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c869e09

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
agent-bundle Patch
@agent-bundle/runtime Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 4, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-04T06:29:16.042369Z b19e2d8 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@pkg-pr-new

pkg-pr-new Bot commented Sep 4, 2026

Copy link
Copy Markdown
npm i https://pkg.pr.new/ScriptedAlchemy/agent-bundle@534
npm i https://pkg.pr.new/ScriptedAlchemy/agent-bundle/create-agent-bundle@534
npm i https://pkg.pr.new/ScriptedAlchemy/agent-bundle/@agent-bundle/runtime@534

commit: c869e09

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b19e2d82b6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread .changeset/511-terminal-capability.md Outdated
'@agent-bundle/runtime': patch
---

Expose the process's terminal capability to routes and scripts instead of each plugin probing `process.stdout` itself. `(await agent()).terminal` is a new `Observed<AgentTerminal>` axis beside `lineage` and `workspace`: `{ hostSurface: 'cli' | 'mcp' | 'hook' | 'script' | 'workbench', stdout, stderr, sharesTarget }`, where each stream reports `kind: 'tty' | 'pipe' | 'none'`, `color: 'none' | 'basic' | '256' | 'truecolor'`, and `columns`/`rows` when known. The routed-CLI executable (plain, rendered, and projected MCP commands) and rendered scripts probe their own process once, honouring `FORCE_COLOR`, `CLICOLOR_FORCE`, `NO_COLOR`, `CLICOLOR=0`, `TERM=dumb`, `COLORTERM`/`TERM` depth, and `COLUMNS`/`LINES` overrides, and select their `tty`/piped output mode from the same value; generated MCP servers, event routes (shared or standalone), and Workbench replays report `none` on both streams and never guess; a custom `runAgentRequest` host that mounts none observes `unavailable` (`not-provided`). The executable envelope now calls `main(argv, { terminal })` for plain `main`-exporting scripts and bins through the dependency-free `agent-bundle/terminal-capability` module (typed as `ExecutableMainContext` from `agent-bundle`); a one-parameter `main` is unaffected. `agent-bundle/cli-entry` accepts `terminal` on `runGeneratedCliEntry`/`runGeneratedRenderedScript` and hands it to `execute`, `render`, and `createSession`. The `agent-bundle/test` harness mounts a deterministic synthetic value from the `tty` knob of `invokeCli`/`runScript`, mounts `none` for MCP and event routes in `renderRoute` and the in-memory MCP level, and accepts `context.terminal` through the identity seam. `AGENT_REQUEST_STORE_VERSION` is now 4. Fixes #511

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Rewrite the changeset as a release summary

This entry is emitted into the release notes, but it includes internal implementation details such as AGENT_REQUEST_STORE_VERSION and harness/runtime wiring, and ends with Fixes #511 rather than the required (#511). Rewrite it as a concise, user-facing imperative summary naming the new public capability and ending with the parenthesized PR reference.

AGENTS.md reference: AGENTS.md:L102-L105

Useful? React with 👍 / 👎.

const lineage = snapshotObserved(init.lineage ?? unavailable<AgentLineage>());
const session = snapshotObserved(init.session ?? unavailable<AgentSessionIdentity>());
const signal = init.signal ?? new AbortController().signal;
const terminal = snapshotObserved(init.terminal ?? unavailable<AgentTerminal>());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Mount terminal data in the runtime compatibility adapters

When an operation calls agent() through the public runRscCli or createRscMcpServer adapters, neither adapter supplies terminal to its runAgentRequest initializer (checked src/cli.ts:37-52 and src/mcp-server.ts:42-56), so this fallback returns unavailable. This contradicts the new documentation promising a native CLI capability and a derived terminal-free MCP capability, leaving the feature absent from two documented production paths; both adapters should mount the appropriate value.

AGENTS.md reference: AGENTS.md:L78-L81

Useful? React with 👍 / 👎.

@ScriptedAlchemy
ScriptedAlchemy force-pushed the feat/511-terminal-capability branch 2 times, most recently from 430705f to 3c4ea4f Compare September 4, 2026 07:22
@ScriptedAlchemy
ScriptedAlchemy enabled auto-merge (squash) September 4, 2026 07:22
@ScriptedAlchemy
ScriptedAlchemy force-pushed the feat/511-terminal-capability branch from 3c4ea4f to 1029032 Compare September 4, 2026 07:48
…s request.terminal (#511)

`(await agent()).terminal` is a new `Observed<AgentTerminal>` axis: per-stream
TTY-ness, color depth, and columns/rows plus `sharesTarget` and the projection
(`hostSurface`). The routed CLI and rendered scripts probe their process once
(FORCE_COLOR, CLICOLOR_FORCE, NO_COLOR, CLICOLOR, TERM, COLORTERM, COLUMNS,
LINES) and pick their output mode from the same value; MCP servers, event
routes, and Workbench replays report `none` and never guess. The executable
envelope passes `{ terminal }` to plain `main` scripts and bins through the
dependency-free `agent-bundle/terminal-capability` module.
…pters; rewrite the changeset as a release summary (review)
@ScriptedAlchemy
ScriptedAlchemy force-pushed the feat/511-terminal-capability branch from 1029032 to eb5ce95 Compare September 4, 2026 08:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Expose a terminal/IO capability to routes and rendered scripts (TTY-ness, color, columns) instead of each plugin re-detecting it

1 participant