diff --git a/.github/workflows/publish-packages.yml b/.github/workflows/publish-packages.yml
index c7918cb0e..f617f52d1 100644
--- a/.github/workflows/publish-packages.yml
+++ b/.github/workflows/publish-packages.yml
@@ -30,7 +30,7 @@ jobs:
- name: Validate the manifests declare this version
run: |
VERSION="${{ steps.resolve.outputs.value }}"
- for f in packages/durable-streams/deno.json packages/runtime/deno.json packages/terminal/deno.json packages/core/deno.json packages/acp/deno.json packages/terminal-tmux/deno.json packages/testing/deno.json packages/test-agent/deno.json packages/web/deno.json packages/workflow/deno.json packages/cli/deno.json packages/code-review-agent/deno.json; do
+ for f in packages/durable-streams/deno.json packages/grid/deno.json packages/runtime/deno.json packages/core/deno.json packages/acp/deno.json packages/grid-tmux/deno.json packages/testing/deno.json packages/test-agent/deno.json packages/web/deno.json packages/workflow/deno.json packages/cli/deno.json packages/code-review-agent/deno.json; do
declared="$(jq -r .version "$f")"
if [ "$declared" != "$VERSION" ]; then
echo "::error::$f declares $declared, not $VERSION — the tag does not match the manifests"
@@ -68,39 +68,39 @@ jobs:
package: packages/durable-streams
version: ${{ needs.version.outputs.value }}
- runtime:
- needs: [version]
+ grid:
+ needs: [version, durable-streams]
uses: ./.github/workflows/publish-one.yml
with:
- package: packages/runtime
+ package: packages/grid
version: ${{ needs.version.outputs.value }}
- terminal:
- needs: [version, durable-streams]
+ runtime:
+ needs: [version]
uses: ./.github/workflows/publish-one.yml
with:
- package: packages/terminal
+ package: packages/runtime
version: ${{ needs.version.outputs.value }}
core:
- needs: [version, durable-streams, runtime, terminal]
+ needs: [version, durable-streams, grid, runtime]
uses: ./.github/workflows/publish-one.yml
with:
package: packages/core
version: ${{ needs.version.outputs.value }}
acp:
- needs: [version, core, runtime, terminal]
+ needs: [version, core, grid, runtime]
uses: ./.github/workflows/publish-one.yml
with:
package: packages/acp
version: ${{ needs.version.outputs.value }}
- terminal-tmux:
- needs: [version, terminal]
+ grid-tmux:
+ needs: [version, grid]
uses: ./.github/workflows/publish-one.yml
with:
- package: packages/terminal-tmux
+ package: packages/grid-tmux
version: ${{ needs.version.outputs.value }}
testing:
@@ -111,7 +111,7 @@ jobs:
version: ${{ needs.version.outputs.value }}
test-agent:
- needs: [version, acp, core, durable-streams, runtime, terminal, testing]
+ needs: [version, acp, core, durable-streams, grid, runtime, testing]
uses: ./.github/workflows/publish-one.yml
with:
package: packages/test-agent
@@ -132,7 +132,7 @@ jobs:
version: ${{ needs.version.outputs.value }}
cli:
- needs: [version, acp, core, durable-streams, runtime, terminal, terminal-tmux, test-agent, testing, web, workflow]
+ needs: [version, acp, core, durable-streams, grid, grid-tmux, runtime, test-agent, testing, web, workflow]
uses: ./.github/workflows/publish-one.yml
with:
package: packages/cli
diff --git a/architecture.md b/architecture.md
index 90e980cac..50bd2e50b 100644
--- a/architecture.md
+++ b/architecture.md
@@ -108,10 +108,10 @@ Existing documents and code get aligned to this section retroactively.
| session materialization | the transition that makes a placement's chosen route and its backend history resumable. ACP-first materialization happens only when the backend reports that it accepted the session's first turn; client-native materialization is the native launch's existing retained construction. Nothing else promotes a placement — not a returning ensure, a first output, a terminal result, a checkpoint token, an error code or a diagnostic |
| established session | a placement whose immutable construction route and durable provider or native identity both already exist, and which is therefore validated eagerly: reattached, compared against its retained history, and refused when either is missing or names another conversation |
| instruction layer | the provider-native session, system or developer instructions a launch installs before the native UI accepts its first user turn. It is not a user message, and it is not conversation history |
-| foreground-terminal lease | the one exclusive claim on a document execution's foreground experience. A root native launch holds it for one inherited terminal; a terminal grid holds it for one composite presentation. A host with no terminal refuses it, and no second root launch or grid can hold it concurrently |
-| terminal grid | one provider-neutral foreground region whose direct terminal panes begin concurrently, remain independently interactive, and settle under one scope after complete provider and pane teardown |
-| terminal pane | one authored position in a terminal grid, identified structurally by its grid and ordinal and presented by its authored title. It owns one interactive terminal at a time; a paired pane expands its own document flow and a self-closing pane runs the host's default shell |
-| pane-terminal lease | the exclusive claim one live interactive operation holds on one terminal pane. Claims in different panes do not contend; two claims in one pane do. It is minted and validated by the host's terminal authority and grants no authority over an Agent session |
+| foreground-terminal lease | the one exclusive claim on a document execution's foreground experience. A root native launch holds it for one inherited terminal; a grid holds it for one composite presentation. A host with no terminal refuses it, and no second root launch or grid can hold it concurrently |
+| grid | one provider-neutral foreground presentation whose direct panes begin concurrently, remain independently usable, and settle under one scope after complete provider and pane teardown |
+| pane | one authored position in a grid, identified structurally by its grid and ordinal and presented by its authored title. A paired pane expands its own document flow and a self-closing pane runs the host's default shell; a pane acquires an interactive terminal only when its content requires one |
+| pane-terminal lease | the exclusive claim one live interactive operation holds on one pane's terminal capability. Claims in different panes do not contend; two claims in one pane do. It is minted and validated by the host's terminal authority and grants no authority over an Agent session |
| native launcher | the host-owned seam that reserves the foreground terminal or the current pane terminal, flushes what that terminal has pending, starts one native UI there, and reports its terminal status and nothing else. It is not `exec`, whose children are piped, captured and journaled |
| launch request | the frozen, one-use value public launch middleware routes. It carries the facts of one launch and `with()`, and nothing that can settle one. Identity is object identity: a rebuilt look-alike describes the same ask and authorizes none of it |
| provider authority | what core delivers to the provider factory it installs, as an argument that factory closes over. It validates the routed request, runs each absent phase once, cross-checks and retains what comes back, and derives the result. There is no reader for one, no context holding one, and no request member carrying one |
@@ -2873,47 +2873,46 @@ inside the run's existing deadline rather than opening a lifecycle of its own,
and a waiting read is cancellable: cancellation tears the reader down and stays
cancellation, never a read failure.
-## Interactive terminal grids
+## Interactive grids
-An executable document can replace its one foreground terminal with one
-provider-neutral grid of independently interactive terminal panes:
+An executable document can replace its one foreground presentation with one
+provider-neutral grid of independently usable panes:
```md
-
-
+
+
Implement the accepted plan.
-
-
+
+
Review the implementation against the plan.
-
-
-
+
+
+
```
-`Terminal` names the interactive endpoint the document requires. It does not
-name the presentation technology: a tmux integration, another terminal
-multiplexer, and a host-native composite UI are providers for the same
-contract. A component that elicits values through a terminal UI is a different
-abstraction, just as `` is one presentation for ``; it does not
-change what an interactive process requires here.
+`Grid` and `Pane` name the presentation structure the document requires. They
+do not name its technology: a tmux integration, another multiplexer, and a
+host-native composite UI are providers for the same contract. Terminal is a
+capability a pane acquires when an interactive process or shell requires a PTY;
+it is not the identity of the grid or every cell.
-The grid and its panes are core-owned structural syntax. `` is
-paired, requires a positive integer `columns`, and contains at least one direct
-`` child. Whitespace may separate those children, but ordinary text,
+The grid and its panes are core-owned structural syntax. `` is paired,
+requires a positive integer `columns`, and contains at least one direct
+`` child. Whitespace may separate those children, but ordinary text,
dynamic control structures, and every other direct element are invalid. A
pane requires a non-empty `title`; titles are display labels and need not be
unique. Its ordinal among the direct children is its structural identity.
Rows are derived in row-major order from the pane count and columns. A paired
pane expands ordinary document flow; a self-closing pane runs the host's
-default shell. A nested grid and a `` outside a grid are invalid.
+default shell. A nested grid and a `` outside a grid are invalid.
Neither form accepts a provider, executable, shell, layout identifier, or
`as`, and neither renders or returns document content.
@@ -2933,159 +2932,121 @@ observes the pane outcome and applies the grid's settlement rule after close.
### Package ownership
-The terminal domain is independent of both the document engine that invokes it
-and the presentation provider that implements it. Two publishable workspace
+The grid domain is independent of both the document engine that invokes it and
+the presentation provider that implements it. Two publishable workspace
packages make those boundaries explicit:
-- `@executablemd/terminal` owns native foreground-launch routing and
+- `@executablemd/grid` owns native foreground-launch routing and terminal
reservation; provider-neutral grid and pane requests, composites, states,
- errors, and row-major layout; `TerminalGrids` and `TerminalProviders` routing;
- provider registration and direct authority delivery; grid and pane claims,
- readiness, stale-authority refusal, lifecycle, reader-close settlement,
- retained outcomes, and replay; pane-scoped launch routing; the
- `TerminalProcesses` observation contract and quiescence operations; and the
- controlled launcher, composite, and log fixtures used to prove the contract.
-- `@executablemd/terminal-tmux` owns tmux capability probing and commands, the
+ errors, and row-major layout; `Grids` and `GridProviders` routing; provider
+ registration and direct authority delivery; claims, readiness,
+ stale-authority refusal, lifecycle, reader-close settlement, retained
+ outcomes, replay, pane-scoped launch routing, terminal process observation,
+ quiescence, and controlled test surfaces.
+- `@executablemd/grid-tmux` owns tmux capability probing and commands, the
hidden server and control clients, explicit layout and pane swaps, visible
attach, authenticated Unix-socket channels and their protocol, the persistent
pane worker and its child, worker invocation, and the provider's one ordered
teardown. No tmux command, type, identifier, protocol value, or host probe is
part of the neutral package.
-`@executablemd/terminal` exports its ordinary domain surface from the package
-root. Its `./lifecycle` entrypoint exports authority creation, provider
-installation, claims, readiness, grid execution, retained outcomes, and the
-reader-close boundary. Its `./processes` entrypoint exports
-`TerminalProcesses`, process facts and signals, snapshots, and quiescence. Its
-`./posix` entrypoint exports the POSIX process and terminal probes and the
-foreground-child adapter. Its `./test` entrypoint exports only controlled
-launchers, composites, logs, and signals. These entrypoints are facets of one
-package, not independent definitions: anything exported from more than one is
-the same object.
-
-`@executablemd/terminal-tmux` exports `TMUX_PROVIDER`,
+`@executablemd/grid` exports its ordinary domain surface from the package root.
+Its `./lifecycle` entrypoint exports authority creation, provider installation,
+claims, readiness, grid execution, retained outcomes, and reader close. Its
+`./processes` entrypoint exports `TerminalProcesses`, process facts and signals,
+snapshots, and quiescence. Its `./posix` entrypoint exports the POSIX process and
+terminal probes and foreground-child adapter. Its `./test` entrypoint exports
+only controlled launchers, composites, logs, and signals. Anything exported
+from more than one facet is the same object.
+
+`@executablemd/grid-tmux` exports `TMUX_PROVIDER`,
`TmuxProviderDependencies`, `tmuxGridProvider`, `installTmuxGridProvider`, the
unchanged `PANE_WORKER_COMMAND`, the hidden pane-worker invocation parser, the
pane-worker process runner, and the provider's documented refusal errors from
-its root. Protocol frames, channel
-handles, tmux process wrappers, layout mechanics, and teardown hooks stay
-private. Controlled low-level seams needed by the adapter's own tests are
-available only from its `./test` entrypoint and are not a second provider API.
+its root. Protocol frames, channel handles, tmux process wrappers, layout
+mechanics, and teardown hooks stay private. Controlled low-level seams exist
+only at its `./test` entrypoint.
The dependency graph points toward the neutral domain:
```text
-@executablemd/terminal-tmux ──> @executablemd/terminal
-@executablemd/core ──> @executablemd/terminal
-@executablemd/cli ──> core + runtime + terminal + terminal-tmux
+@executablemd/grid-tmux ──> @executablemd/grid
+@executablemd/core ──> @executablemd/grid
+@executablemd/cli ──> core + runtime + grid + grid-tmux
```
-The terminal package may depend on durable streams, Effection, and EffectionX;
-it never imports runtime, core, CLI, or terminal-tmux. The tmux package never
-imports runtime, core, or CLI. Moving the native-launch descriptor into the
-neutral package is load-bearing: leaving it in runtime would either reverse the
-domain dependency or make terminal depend on runtime. Core remains the owner of
-Markdown parse and expansion, `SourcePosition` journal descriptions,
-execution-profile installation, and Agent session behavior. Its
-`src/terminal/journal.ts` and `src/terminal/profile.ts` therefore stay in core;
-the neutral authority, provider API, layout, grid lifecycle, pane claim, and
-pane-launcher modules move. Runtime's launcher, terminal composite, process
-observation, and POSIX observer modules move. CLI's attach client, tmux layout,
-pane channel, child, protocol and worker, provider, grid, and tmux command
-modules move to terminal-tmux; CLI retains only entrypoint and execution
+The grid package may depend on durable streams, Effection, and EffectionX; it
+never imports runtime, core, CLI, or grid-tmux. Grid-tmux never imports runtime,
+core, or CLI. Runtime has no grid dependency. Core remains the owner of Markdown
+parsing and expansion, `SourcePosition` journal descriptions,
+execution-profile installation, and Agent session behavior. Its grid journal
+and profile adapters stay in core. CLI retains only entrypoint and execution
composition.
-The extraction applies to the current modules as follows:
-
-| Current module | Destination and responsibility |
-|---|---|
-| `packages/runtime/launcher.ts` | Split between terminal's neutral root, POSIX foreground-child adapter, and controlled test entrypoint |
-| `packages/runtime/terminal.ts` | Split between terminal's neutral root and controlled test entrypoint |
-| `packages/runtime/terminal-processes.ts` | `@executablemd/terminal/processes` |
-| `packages/runtime/deno-terminal-processes.ts` | `@executablemd/terminal/posix`; delete the old module after moving it |
-| `packages/core/src/terminal/authority.ts` | `@executablemd/terminal/lifecycle` |
-| `packages/core/src/terminal/provider-api.ts` | Terminal root and lifecycle entrypoints |
-| `packages/core/src/terminal/grid.ts` | `@executablemd/terminal/lifecycle` |
-| `packages/core/src/terminal/pane-launcher.ts` and `pane.ts` | Terminal's neutral pane and launcher surface |
-| `packages/core/src/terminal-grid.ts` | Split so neutral layout and grid lifecycle move to terminal while authored element scanning, expansion and source integration stay in core |
-| `packages/core/src/terminal/journal.ts` and `profile.ts` | Stay in core; they adapt terminal lifecycle to core journal descriptions and `Execution` |
-| `packages/cli/src/terminal/{attach-client,layout,pane-channel,pane-child,pane-protocol,pane-worker,provider,tmux-grid,tmux}.ts` | Move to `@executablemd/terminal-tmux` |
-| `packages/cli/src/terminal/host.ts` | Split: reusable provider and POSIX pieces move to their packages; the core `Execution` wrapper and entrypoint composition stay in a genuinely non-terminal CLI module, and the old terminal path is deleted |
-
-Tests follow the code whose contract they prove: neutral routing, authority,
-layout, lifecycle, replay and process-quiescence suites live under terminal;
-tmux topology, protocol, worker, host-process and teardown suites live under
-terminal-tmux; syntax, source integration and durable journal descriptions stay
-under core; cross-package Agent composition stays with test-agent; entrypoint
-selection and compiled-host evidence stay under CLI.
-
-The former `@executablemd/runtime` and `@executablemd/core` terminal exports and
-old `packages/cli/src/terminal` implementation paths are deleted. This stack is
-unmerged, so none is a compatibility surface. Every repository consumer imports
-the canonical terminal or terminal-tmux package entrypoint, and no forwarding
-barrel or alias preserves an old path. Each contextual API and error constructor
-therefore has one canonical definition and import path; stable contextual API
-names and `instanceof` behavior remain unchanged within that surface.
+The unmerged `packages/terminal` and `packages/terminal-tmux` trees become
+`packages/grid` and `packages/grid-tmux`. Their manifests, exports, tests,
+workspace declarations, generated publication entries, and consumers move with
+them. The former runtime and core terminal exports, old CLI terminal
+implementation paths, rejected package names, and old authored syntax are
+deleted. No compatibility component, package, module, alias, forwarding barrel,
+wrapper, subclass, or duplicate descriptor remains. Every repository import is
+canonical, and each contextual API and public error constructor has one
+definition and import path.
POSIX process-table, process-group, signal, reachability, and terminal-holder
-observation lives behind `@executablemd/terminal/posix`, not in the tmux
-adapter. A different POSIX presentation provider can reuse the same proof
-without depending on tmux. The Deno and compiled CLI entrypoints remain the
-host-composition boundary: they choose tmux, resolve self-reinvocation, terminal
-size and environment, translate host `SIGHUP` into structured cancellation, and
-install the POSIX observer both in the supervising run and inside each pane
-worker because contextual state does not cross a process boundary. Node and Bun
-continue to install neither observer nor grid provider.
-
-This extraction changes ownership, not behavior. It preserves the authored
-syntax, provider name `tmux`, hidden worker verb `terminal-worker`, worker
-protocol and authentication, durable records and identities, diagnostic text
-and normalization, readiness, close and replay semantics, and every provider
-identity. Event registrations remain owned by the Effection scope whose
-resource they observe and are removed when that scope settles. Both packages
-are ordinary lockstep-versioned workspace members. The
-generated publication graph places terminal after durable-streams,
-terminal-tmux and core after terminal, and CLI after terminal-tmux, terminal,
-core, and runtime. Runtime remains independent of terminal. Workspace,
-JSR, npm, compiled-host, and runtime-test discovery treat them like every other
+observation lives behind `@executablemd/grid/posix`, not in the tmux adapter. A
+different POSIX presentation provider can reuse the same proof without tmux.
+The Deno and compiled CLI entrypoints choose tmux, resolve self-reinvocation,
+terminal size and environment, translate host `SIGHUP` into structured
+cancellation, and install the POSIX observer in both the supervising run and
+each pane worker. Node and Bun install neither observer nor grid provider.
+
+This boundary change preserves the provider name `tmux`, hidden worker verb
+`terminal-worker`, worker protocol and authentication, durable behavior and
+identities, readiness, close and replay semantics, terminal capability, and
+every provider identity. It deliberately changes the authored names, canonical
+package and import paths, public grid descriptors and errors, and diagnostics
+that identify those authored constructs. Event registrations remain owned by
+the Effection scope whose resource they observe and are removed when that scope
+settles.
+
+Both packages are ordinary lockstep-versioned workspace members. The generated
+publication graph places grid after durable-streams, grid-tmux and core after
+grid, and CLI after grid-tmux, grid, core, and runtime. Workspace, JSR, npm,
+compiled-host, and runtime-test discovery treat them like every other
publishable package.
-The final extraction story is complete when this finite evidence passes:
-
-1. A static dependency test walks production imports and proves the four arrows
- above, including the absence of terminal-to-runtime/core/CLI/tmux and
- terminal-tmux-to-runtime/core/CLI edges.
-2. A package-boundary test proves the old runtime, core, and CLI terminal paths
- and exports are absent, every repository terminal import uses a canonical
- package surface, and each public contextual descriptor and error constructor
- has one definition.
-3. Relocated neutral tests prove foreground launching, provider routing and
- direct authority, claims and readiness, layout, close/cancellation/replay,
- process observation, and quiescence without tmux.
-4. Core tests prove the unchanged grammar, structural validation, source
- diagnostics, pane scope, durable identities and records, retained outcomes,
- and provider-neutral replay.
-5. Terminal-tmux tests prove exact authenticated worker transport, concurrent
- panes, sequential reuse, spawn readiness, display isolation, job control,
- explicit row-major layout, atomic attach, the three close signals, SIGHUP,
- scope-owned event registration, cancellation phases, and ordered bounded
- teardown with real workers and sockets under the existing fake-tmux host.
-6. The cross-package test Agent proves a pane-native launch reaches its physical
- endpoint while root launch and natural-key Agent session ownership remain
- unchanged.
-7. CLI evidence proves Deno and compiled hosts select tmux and dispatch the
- hidden worker with POSIX observation in both processes; Node, Bun, non-TTY,
- and missing-tmux paths install no partial provider and retain their exact
- refusals.
-8. Workspace and release evidence proves discovery of both packages, valid
- runtime exclusions, freshly measured corpus weights, generated dependency
- order, JSR publishability, a local-sibling npm CLI build, the compiled binary
- and hidden worker, and dependency-state cleanliness.
+The final boundary is established by finite evidence:
+
+1. A static dependency test proves the graph above and the absence of
+ grid-to-runtime/core/CLI/tmux and grid-tmux-to-runtime/core/CLI edges.
+2. A package-boundary test proves the rejected component names, packages,
+ runtime and core exports, and CLI implementation paths absent; all grid
+ imports are canonical; and every public contextual descriptor and error
+ constructor has one definition.
+3. Relocated neutral tests retain foreground launch, routing, authority,
+ readiness, layout, close, cancellation, replay, process observation, and
+ quiescence without tmux.
+4. Core tests retain grammar, structural validation, source diagnostics, pane
+ scope, durable identities and records, outcomes, and provider-neutral replay
+ under `` and ``.
+5. Grid-tmux tests retain exact authenticated transport, concurrent panes,
+ sequential reuse, spawn readiness, display isolation, job control, explicit
+ row-major layout, atomic attach, distinct close signals, SIGHUP, scope-owned
+ listeners, cancellation phases, and ordered bounded teardown.
+6. Cross-package Agent tests retain pane-native physical routing, root launch,
+ and natural-key Agent session ownership.
+7. CLI evidence retains Deno and compiled tmux assembly and hidden-worker POSIX
+ observation; Node, Bun, non-TTY, and missing-tmux paths install no partial
+ provider and keep their refusal boundaries.
+8. Workspace and release evidence proves both packages discovered and published
+ in dependency order, JSR and local-sibling npm consumption, compiled binary
+ and hidden worker, runtime-test discovery, and dependency-state cleanliness.
Tests use controlled signals and observable settlement for lifecycle success;
-elapsed time is not evidence. The focused feedback commit runs the smallest
-explicit tests that discriminate these boundaries. Runtime-wide matrices,
-lint, typecheck, JSR and clean composability remain delivery gates.
+elapsed time is not evidence. Focused feedback uses the smallest explicit tests
+that discriminate the boundary, while runtime matrices, lint, typecheck, JSR,
+and clean composability remain delivery gates.
### Terminal authority
@@ -3283,7 +3244,7 @@ observable ownership mechanism of its own instead of severing all three links.
### Durability and replay
-A terminal grid is a core-owned structured durable region. Its layout identity
+A grid is a core-owned structured durable region. Its layout identity
contains the columns and the ordered pane forms and titles, never a provider or
live terminal identifier. Each pane is a deterministic durable child coroutine,
so effects in paired content retain and replay under the same rules they use
@@ -4645,8 +4606,8 @@ Status is measured against main.
| testing harness (``) | runs another document as a real root under a production host profile, authorized by canonical `` alone: declarations installed before the root import, child output displayed progressively and collected only when asked, journal retention selected independently of observation, and the outcome published by the invocation's own terminal through a request public middleware composes around but cannot answer | built on the #454 stack for `host="run"`; the workflow profile and `` are unbuilt, and a host that offers no workflow profile refuses them |
| nested run-profile Agent and elicitation declarations | lets one `` declare one child-scoped `` scenario set and one non-delegating `` matcher set; only frozen test data crosses the harness request, the trusted host constructs both providers inside the isolated child, siblings share no session or provider state, ordinary component shadowing remains in force, and the child journal retains only the selected Prompt and Elicit components' ordinary results. A controlled `` may author an exact scenario label that this host alone maps to Plan's derived conversation identity; declaration selection uses the label while runtime state stays keyed by the opaque identity and child, with no matcher or fallback added to ordinary TestAgent sessions | built on the #641 stack; controlled Plan routing added on the #728 stack |
| `Config` run deadline / exec default / Fetch default / verbosity | three independently owned contextual timeouts, absent unless configured, each read by exactly one consumer, and contextual verbosity — a boolean that is false unless configured, seeded by the command line and overridable for a lexical subtree, bounding nothing and owning no authority | built on this stack |
-| terminal grid (`` / ``) | replaces the root foreground terminal with one provider-neutral composite whose statically declared direct panes begin concurrently, stay independently interactive, preserve their final statuses until the reader closes the composite, and tear down completely before document execution continues. A paired pane expands isolated document flow; a self-closing pane runs the host's default shell. The grid owns one foreground-terminal lease, each pane owns a separate pane-terminal lease, and a pane-scoped native launcher lets `` use that pane without weakening the independent Agent session coordinator. The launcher terminates at the composite's required provider-neutral pane-execution operation; the authored ordinal stays in core's live closure, and the native request carries no pane identity. Core validates the complete row-major layout before provider contact, attaches only after every pane is ready, contains post-attach pane failures until close, and records the ordered provider-neutral outcomes. Completed replay contacts no terminal or Agent provider; partial replay rebuilds a fresh composite, restores completed panes as statuses, and continues incomplete pane effects under their existing durable identities. Provider commands, sockets, process topology and layout identifiers remain live-only inside the provider closure | defined for #717; #726 proves the persistent tmux pane-worker topology and its observable teardown boundary on macOS; structure and layout built in #729, provider-neutral execution and durability in #730, pane claim admission and native-launch middleware in #731; the required composite pane-execution endpoint is specified and implemented in #732, which is what gives a pane's `` that pane's terminal rather than the root's; the controlled non-tmux provider remains the authority for core lifecycle semantics; the tmux provider is built in #732 for the Deno and compiled foreground hosts — one invocation-private server per grid, authenticated persistent pane workers carrying exact argv, cwd and environment outside tmux parsing, explicit row-major layout imposed by pane swaps, a required composite `launch()` that gives a pane's `` its own terminal rather than the root's, and one ordered teardown that proves worker quiescence, channel closure and server disappearance before the document continues; its evidence uses a fake tmux with real workers and real sockets, and real tmux behaviour on macOS remains #726's; Node and Bun catalog and validate the same grids and install neither the provider nor the process observer, refusing before pane start; DEC-016 specifies the final behavior-preserving extraction into `@executablemd/terminal` and `@executablemd/terminal-tmux`, with every repository import moved to the canonical packages and the unshipped old terminal paths deleted |
-| native session launch (`` / `launchAgentSession()`) | prepares one durable coding-agent session from the rendered body of `` and hands the provider's native UI the terminal for that exact session, then continues the document after it exits. The body renders completely first and only what it rendered crosses as the instruction layer; the launch performs no model turn; at the root it takes the run's foreground-terminal lease before an agent is resolved, while a launch inside `` takes that pane's lease through its pane-scoped native launcher. A host with no applicable terminal refuses without probing for an installed CLI. A session is constructed once, by one of two mechanisms, and its create-once construction route says which. Where the provider returns the identity, the ACPX provider creates the session, installs the layer at creation, releases ACP ownership before the spawn, and marks its handle stale so a later `` reattaches. Where the adapter names its own sessions, it allocates the identity inside ownership before any process exists, the native process creates the session under that name from a private mode-0600 instruction file, and ACP creates nothing — the instruction text reaches neither argv nor environment, and the file is removed on success, failure and cancellation alike while ownership is still held. Neither route converts into the other, and which one governs is chosen by the first operation that consumes the placement rather than by the `` that made it: a fresh `` publishes no route and establishes nothing, so a `` nested inside one constructs the session it placed, while a first subscribed `` publishes ACP-first before it ensures and keeps that account even if the turn that follows is never accepted. An established route is validated eagerly by a later ``, and a launch meeting a published ACP-first route refuses before an identity exists. A `` or `` meeting a bound client-allocated route attaches under the route's exact identity; a legacy unbound route or an unavailable attachment capability refuses before a turn and creates no substitute conversation. Phases are retained as `agent_session_launch` records under one expansion identity — `prepared` before ownership is released, then `detached`, then `exited` — so a completed replay launches nothing, a replay holding only `prepared` proves the handoff never began and may still create under the retained identity, and one holding `detached` resumes and never falls back. The public route carries an opaque one-use launch request and answers nothing; authority to run and retain a phase is delivered to the installed provider directly, so neither a returned completion nor a rebuilt request authors a launch. Every operation that can act on an advertised session takes exclusive ownership under one natural key first, through a coordinator the host built and passed in; contention refuses instead of queueing, and an owner that never proved it stopped leaves a recovery tombstone. A host that cannot say who owns a session refuses every advertised operation, and one that cannot say how a session was constructed additionally refuses an agent that names its own — before any provider effect. Every private setup or child-creation failure is normalized to `process-creation-failed` with fixed provider-owned text, carrying no path, argv, environment or host message. No launch path discards persistent provider state. A client-allocated session is bound to one executable build: the build is observed inside ownership before an identity is allocated, the binding is published with the V2 route and retained beside the prepared record, the native child runs the exact observed path in place of the launcher name, and every later create, resume, attachment and incomplete replay reobserves and compares before a process, an ensure or a turn. A `` or `` meeting a bound client-native route attaches to it: it reobserves the build, requires any retained provider arrangement to assert that same conversation, calls ensure with the route identity as `resumeSessionId`, and requires the provider to report that identity before a turn — refusing on missing capability, build drift, missing history or a differing assertion without creating a substitute conversation. ACP runtimes are partitioned by resolved agent command and binding, each handle is closed by the partition that created it, and a bound partition is torn down when its last handle closes. A legacy V1 client-native route keeps exactly the released native-only behavior and never attaches | built on the #517 stack, extended by the #519 and #561 stacks; Deno and the compiled binary assemble the host — coordinator, route store and executable observer — and Node and Bun keep the same advertised names while assembling none of it, so every advertised operation refuses before provider work; `claude` is advertised for native launch after passing the client-allocated gate at Claude Code 2.1.241 on macOS arm64 (#520) and separately for client-native attachment after passing the native-to-ACP marker gate (#561), and Codex remains unadvertised because nothing has run its provider-returned claims against an installed Codex; `Agent.AddDir` is unbuilt |
+| grid (`` / ``) | replaces the root foreground presentation with one provider-neutral composite whose statically declared direct panes begin concurrently, stay independently usable, preserve their final statuses until the reader closes the composite, and tear down completely before document execution continues. A paired pane expands isolated document flow; a self-closing pane runs the host's default shell. The grid owns one foreground-terminal lease when its content requires terminal presentation, each interactive pane owns a separate pane-terminal lease, and a pane-scoped native launcher lets `` use that pane without weakening the independent Agent session coordinator. The launcher terminates at the composite's required provider-neutral pane-execution operation; the authored ordinal stays in core's live closure, and the native request carries no pane identity. Core validates the complete row-major layout before provider contact, attaches only after every pane is ready, contains post-attach pane failures until close, and records the ordered provider-neutral outcomes. Completed replay contacts no grid, terminal, or Agent provider; partial replay rebuilds a fresh composite, restores completed panes as statuses, and continues incomplete pane effects under their existing durable identities. Provider commands, sockets, process topology and layout identifiers remain live-only inside the provider closure | defined for #717 and renamed before delivery by #781; #726 proves the persistent tmux pane-worker topology and its observable teardown boundary on macOS; structure and layout built in #729, provider-neutral execution and durability in #730, pane claim admission and native-launch middleware in #731; the required composite pane-execution endpoint is specified and implemented in #732, which is what gives a pane's `` that pane's terminal rather than the root's; the controlled non-tmux provider remains the authority for core lifecycle semantics; the tmux provider is built in #732 for the Deno and compiled foreground hosts — one invocation-private server per grid, authenticated persistent pane workers carrying exact argv, cwd and environment outside tmux parsing, explicit row-major layout imposed by pane swaps, a required composite `launch()` that gives a pane's `` its own terminal rather than the root's, and one ordered teardown that proves worker quiescence, channel closure and server disappearance before the document continues; its evidence uses a fake tmux with real workers and real sockets, and real tmux behaviour on macOS remains #726's; Node and Bun catalog and validate the same grids and install neither the provider nor the process observer, refusing before pane start; DEC-016 specifies the final extraction into `@executablemd/grid` and `@executablemd/grid-tmux`, with every repository import moved to the canonical packages and all rejected unshipped names and paths deleted |
+| native session launch (`` / `launchAgentSession()`) | prepares one durable coding-agent session from the rendered body of `` and hands the provider's native UI the terminal for that exact session, then continues the document after it exits. The body renders completely first and only what it rendered crosses as the instruction layer; the launch performs no model turn; at the root it takes the run's foreground-terminal lease before an agent is resolved, while a launch inside `` takes that pane's lease through its pane-scoped native launcher. A host with no applicable terminal refuses without probing for an installed CLI. A session is constructed once, by one of two mechanisms, and its create-once construction route says which. Where the provider returns the identity, the ACPX provider creates the session, installs the layer at creation, releases ACP ownership before the spawn, and marks its handle stale so a later `` reattaches. Where the adapter names its own sessions, it allocates the identity inside ownership before any process exists, the native process creates the session under that name from a private mode-0600 instruction file, and ACP creates nothing — the instruction text reaches neither argv nor environment, and the file is removed on success, failure and cancellation alike while ownership is still held. Neither route converts into the other, and which one governs is chosen by the first operation that consumes the placement rather than by the `` that made it: a fresh `` publishes no route and establishes nothing, so a `` nested inside one constructs the session it placed, while a first subscribed `` publishes ACP-first before it ensures and keeps that account even if the turn that follows is never accepted. An established route is validated eagerly by a later ``, and a launch meeting a published ACP-first route refuses before an identity exists. A `` or `` meeting a bound client-allocated route attaches under the route's exact identity; a legacy unbound route or an unavailable attachment capability refuses before a turn and creates no substitute conversation. Phases are retained as `agent_session_launch` records under one expansion identity — `prepared` before ownership is released, then `detached`, then `exited` — so a completed replay launches nothing, a replay holding only `prepared` proves the handoff never began and may still create under the retained identity, and one holding `detached` resumes and never falls back. The public route carries an opaque one-use launch request and answers nothing; authority to run and retain a phase is delivered to the installed provider directly, so neither a returned completion nor a rebuilt request authors a launch. Every operation that can act on an advertised session takes exclusive ownership under one natural key first, through a coordinator the host built and passed in; contention refuses instead of queueing, and an owner that never proved it stopped leaves a recovery tombstone. A host that cannot say who owns a session refuses every advertised operation, and one that cannot say how a session was constructed additionally refuses an agent that names its own — before any provider effect. Every private setup or child-creation failure is normalized to `process-creation-failed` with fixed provider-owned text, carrying no path, argv, environment or host message. No launch path discards persistent provider state. A client-allocated session is bound to one executable build: the build is observed inside ownership before an identity is allocated, the binding is published with the V2 route and retained beside the prepared record, the native child runs the exact observed path in place of the launcher name, and every later create, resume, attachment and incomplete replay reobserves and compares before a process, an ensure or a turn. A `` or `` meeting a bound client-native route attaches to it: it reobserves the build, requires any retained provider arrangement to assert that same conversation, calls ensure with the route identity as `resumeSessionId`, and requires the provider to report that identity before a turn — refusing on missing capability, build drift, missing history or a differing assertion without creating a substitute conversation. ACP runtimes are partitioned by resolved agent command and binding, each handle is closed by the partition that created it, and a bound partition is torn down when its last handle closes. A legacy V1 client-native route keeps exactly the released native-only behavior and never attaches | built on the #517 stack, extended by the #519 and #561 stacks; Deno and the compiled binary assemble the host — coordinator, route store and executable observer — and Node and Bun keep the same advertised names while assembling none of it, so every advertised operation refuses before provider work; `claude` is advertised for native launch after passing the client-allocated gate at Claude Code 2.1.241 on macOS arm64 (#520) and separately for client-native attachment after passing the native-to-ACP marker gate (#561), and Codex remains unadvertised because nothing has run its provider-returned claims against an installed Codex; `Agent.AddDir` is unbuilt |
| `` | performs one XMD-mediated HTTP read through contextual `API.Fetch`, admitting the whole request before transport, and retains the normalized request and the detached response as one `fetch` durable observation; capture decides whether a status is data or a failure, and the trusted host's destination ceiling sits below the component | built on the #456 stack; a generated fragment may name the pinned identity only for a request the trusted host stated exactly, on the #369 stack |
| `API.Files` | routes every document filesystem operation to the installed provider, with no host default and structural failure data. Its mandatory semantic operations include `ensureDirectory`, which recursively creates or adopts one directory and returns Unit; separately loaded copies compose through the stable Api name | built on the #227 stack; directory ensure added by #643 |
| `` | removes one file the document names, inside the contextual working directory. An ordinary overridable core default with a closed schema of one required non-empty `path`, **self-closing only** — a paired spelling never enters its body, because the component declares its one form and canonical invocation-form dispatch enters that body only for the form the scan recorded, before `Env.cwd` is read and before the provider is reached. Neither the composable `Component.hasContent()` chain nor a method on whatever object a caller handed over takes part. It renders the empty string, declares no `returns` and hands back no receipt, so an ordinary `as` captures that empty string; absence is the same success, so deleting a path twice succeeds twice. One regular file or one final symbolic link goes — the link rather than its target, inside or outside — and every directory is refused, an empty one included. Empty, absolute, lexically escaping and parent-link-escaping paths are refused before any removal, and a printed error names only the path the document wrote. One semantic `API.Files.deleteFile` call and no filesystem access of its own; under a workflow run it is one `workspace_file` effect retaining `{ kind: "deleted" }`. The standard Deno workflow profile admits it to generated XMD as the exact self-closing identity `@executablemd/core#File.Delete`, third in the write table, where it performs that same ordinary effect and contributes no evaluator result | built on the #567 stack |
diff --git a/bun.lock b/bun.lock
index 7980dedb1..90351ce57 100644
--- a/bun.lock
+++ b/bun.lock
@@ -10,7 +10,7 @@
"@effectionx/fetch": "0.2.1",
"@effectionx/fs": "0.3.0",
"@effectionx/middleware": "0.1.1",
- "@effectionx/node": "0.2.4",
+ "@effectionx/node": "0.2.5",
"@effectionx/process": "0.8.1",
"@effectionx/scope-eval": "0.1.3",
"@effectionx/stream-helpers": "0.8.3",
@@ -57,8 +57,8 @@
"dependencies": {
"@agentclientprotocol/sdk": "1.3.0",
"@executablemd/core": "workspace:*",
+ "@executablemd/grid": "workspace:*",
"@executablemd/runtime": "workspace:*",
- "@executablemd/terminal": "workspace:*",
"acpx": "0.12.0",
"effection": "4.1.0",
},
@@ -74,9 +74,9 @@
"@executablemd/acp": "workspace:*",
"@executablemd/core": "workspace:*",
"@executablemd/durable-streams": "workspace:*",
+ "@executablemd/grid": "workspace:*",
+ "@executablemd/grid-tmux": "workspace:*",
"@executablemd/runtime": "workspace:*",
- "@executablemd/terminal": "workspace:*",
- "@executablemd/terminal-tmux": "workspace:*",
"@executablemd/test-agent": "workspace:*",
"@executablemd/testing": "workspace:*",
"@executablemd/web": "workspace:*",
@@ -101,14 +101,14 @@
"@effectionx/fetch": "0.2.1",
"@effectionx/fs": "0.3.0",
"@effectionx/middleware": "0.1.1",
- "@effectionx/node": "0.2.4",
+ "@effectionx/node": "0.2.5",
"@effectionx/process": "0.8.1",
"@effectionx/scope-eval": "0.1.3",
"@effectionx/stream-helpers": "0.8.3",
"@effectionx/timebox": "0.4.3",
"@executablemd/durable-streams": "workspace:*",
+ "@executablemd/grid": "workspace:*",
"@executablemd/runtime": "workspace:*",
- "@executablemd/terminal": "workspace:*",
"@secretlint/core": "13.0.4",
"@secretlint/profiler": "13.0.4",
"@secretlint/secretlint-rule-preset-recommend": "13.0.4",
@@ -133,39 +133,39 @@
"effection": "4.1.0",
},
},
- "packages/runtime": {
- "name": "@executablemd/runtime",
+ "packages/grid": {
+ "name": "@executablemd/grid",
"version": "0.11.0",
"dependencies": {
"@effectionx/context-api": "0.6.0",
- "@effectionx/fetch": "0.2.1",
"@effectionx/fs": "0.3.0",
"@effectionx/node": "0.2.4",
"@effectionx/process": "0.8.1",
+ "@executablemd/durable-streams": "workspace:*",
"effection": "4.1.0",
},
},
- "packages/terminal": {
- "name": "@executablemd/terminal",
+ "packages/grid-tmux": {
+ "name": "@executablemd/grid-tmux",
"version": "0.11.0",
"dependencies": {
- "@effectionx/context-api": "0.6.0",
"@effectionx/fs": "0.3.0",
- "@effectionx/node": "0.2.4",
"@effectionx/process": "0.8.1",
- "@executablemd/durable-streams": "workspace:*",
+ "@executablemd/grid": "workspace:*",
"effection": "4.1.0",
+ "zod": "^4.3.6",
},
},
- "packages/terminal-tmux": {
- "name": "@executablemd/terminal-tmux",
+ "packages/runtime": {
+ "name": "@executablemd/runtime",
"version": "0.11.0",
"dependencies": {
+ "@effectionx/context-api": "0.6.0",
+ "@effectionx/fetch": "0.2.1",
"@effectionx/fs": "0.3.0",
+ "@effectionx/node": "0.2.5",
"@effectionx/process": "0.8.1",
- "@executablemd/terminal": "workspace:*",
"effection": "4.1.0",
- "zod": "^4.3.6",
},
},
"packages/test-agent": {
@@ -173,14 +173,14 @@
"version": "0.11.0",
"dependencies": {
"@agentclientprotocol/sdk": "1.3.0",
- "@effectionx/node": "0.2.4",
+ "@effectionx/node": "0.2.5",
"@effectionx/scope-eval": "0.1.3",
"@effectionx/stream-helpers": "0.8.3",
"@executablemd/acp": "workspace:*",
"@executablemd/core": "workspace:*",
"@executablemd/durable-streams": "workspace:*",
+ "@executablemd/grid": "workspace:*",
"@executablemd/runtime": "workspace:*",
- "@executablemd/terminal": "workspace:*",
"@executablemd/testing": "workspace:*",
"acorn": "^8.16.0",
"acpx": "0.12.0",
@@ -218,7 +218,7 @@
"name": "@executablemd/web",
"version": "0.11.0",
"dependencies": {
- "@effectionx/node": "0.2.4",
+ "@effectionx/node": "0.2.5",
"@executablemd/core": "workspace:*",
"@executablemd/durable-streams": "workspace:*",
"@executablemd/runtime": "workspace:*",
@@ -283,7 +283,7 @@
"@effectionx/middleware": ["@effectionx/middleware@0.1.1", "", {}, "sha512-ss/bZRkt/xzJNE59r8NR1+0K/xQcIyCm0y9n8FYC8jKdFn51SPe3m3t7EfPcK8zkdjCoTOU7k1UpIXRl26asYA=="],
- "@effectionx/node": ["@effectionx/node@0.2.4", "", { "peerDependencies": { "effection": "^3 || ^4" } }, "sha512-cPnp3fvfBKjGWekmBHdhZr5ScAr3Mg+x5IXpO8uKFe7AZ8EPAT9Di6skuB4kuGFJtRtS0Z1e5G4+2eJyapKhYA=="],
+ "@effectionx/node": ["@effectionx/node@0.2.5", "", { "peerDependencies": { "effection": "^3 || ^4" } }, "sha512-hL8mROda8Lx375MVS+Ubu86+yMht/I0wOZG5VR6Pel0XUA5ReObQDYvNS6ocW0cNFKnEmvlVLWGWbcjJ+VkVhA=="],
"@effectionx/process": ["@effectionx/process@0.8.1", "", { "dependencies": { "@effectionx/context-api": "0.6.0", "@effectionx/node": "0.2.4", "@effectionx/scope-eval": "0.1.3", "cross-spawn": "^7", "ctrlc-windows": "^2", "shellwords-ts": "^3.0.1" }, "peerDependencies": { "effection": "^3 || ^4" } }, "sha512-xyXlFja0Ill80lQ3IYfksXtJkqVmWuUOogRn/qlHWCAGlZj+MGGF8gOFbyzk/3Kx4pj14riVGgF/cyT5XCzqDw=="],
@@ -359,11 +359,11 @@
"@executablemd/durable-streams": ["@executablemd/durable-streams@workspace:packages/durable-streams"],
- "@executablemd/runtime": ["@executablemd/runtime@workspace:packages/runtime"],
+ "@executablemd/grid": ["@executablemd/grid@workspace:packages/grid"],
- "@executablemd/terminal": ["@executablemd/terminal@workspace:packages/terminal"],
+ "@executablemd/grid-tmux": ["@executablemd/grid-tmux@workspace:packages/grid-tmux"],
- "@executablemd/terminal-tmux": ["@executablemd/terminal-tmux@workspace:packages/terminal-tmux"],
+ "@executablemd/runtime": ["@executablemd/runtime@workspace:packages/runtime"],
"@executablemd/test-agent": ["@executablemd/test-agent@workspace:packages/test-agent"],
@@ -1091,11 +1091,15 @@
"@durable-streams/state/@durable-streams/client": ["@durable-streams/client@0.2.6", "", { "dependencies": { "@microsoft/fetch-event-source": "^2.0.1", "fastq": "^1.19.1" }, "bin": { "intent": "bin/intent.js" } }, "sha512-uHKKbWpsKLhFMeGjG0PgM6LXE3oEIi7FHKlJZkmYGxcqd4Yjjd/QEvnQnDzteRP4Av1uJVM8qjTL7kfKsgeS/w=="],
+ "@effectionx/process/@effectionx/node": ["@effectionx/node@0.2.4", "", { "peerDependencies": { "effection": "^3 || ^4" } }, "sha512-cPnp3fvfBKjGWekmBHdhZr5ScAr3Mg+x5IXpO8uKFe7AZ8EPAT9Di6skuB4kuGFJtRtS0Z1e5G4+2eJyapKhYA=="],
+
"@executablemd/cli/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
"@executablemd/durable-streams/@durable-streams/client": ["@durable-streams/client@0.2.6", "", { "dependencies": { "@microsoft/fetch-event-source": "^2.0.1", "fastq": "^1.19.1" }, "bin": { "intent": "bin/intent.js" } }, "sha512-uHKKbWpsKLhFMeGjG0PgM6LXE3oEIi7FHKlJZkmYGxcqd4Yjjd/QEvnQnDzteRP4Av1uJVM8qjTL7kfKsgeS/w=="],
- "@executablemd/terminal-tmux/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
+ "@executablemd/grid/@effectionx/node": ["@effectionx/node@0.2.4", "", { "peerDependencies": { "effection": "^3 || ^4" } }, "sha512-cPnp3fvfBKjGWekmBHdhZr5ScAr3Mg+x5IXpO8uKFe7AZ8EPAT9Di6skuB4kuGFJtRtS0Z1e5G4+2eJyapKhYA=="],
+
+ "@executablemd/grid-tmux/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
"@executablemd/test-agent/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
diff --git a/deno.lock b/deno.lock
index f04adcbe3..1f47f40b5 100644
--- a/deno.lock
+++ b/deno.lock
@@ -48,6 +48,7 @@
"npm:@effectionx/fetch@0.2.1": "0.2.1_effection@4.1.0",
"npm:@effectionx/fs@0.3.0": "0.3.0_effection@4.1.0",
"npm:@effectionx/middleware@0.1.1": "0.1.1",
+ "npm:@effectionx/node@0.2.4": "0.2.4_effection@4.1.0",
"npm:@effectionx/node@0.2.5": "0.2.5_effection@4.1.0",
"npm:@effectionx/process@0.8.1": "0.8.1_effection@4.1.0",
"npm:@effectionx/scope-eval@0.1.3": "0.1.3_effection@4.1.0",
@@ -4117,36 +4118,36 @@
]
}
},
- "packages/runtime": {
+ "packages/grid": {
"packageJson": {
"dependencies": [
"npm:@effectionx/context-api@0.6.0",
- "npm:@effectionx/fetch@0.2.1",
"npm:@effectionx/fs@0.3.0",
- "npm:@effectionx/node@0.2.5",
+ "npm:@effectionx/node@0.2.4",
"npm:@effectionx/process@0.8.1",
"npm:effection@4.1.0"
]
}
},
- "packages/terminal": {
+ "packages/grid-tmux": {
"packageJson": {
"dependencies": [
- "npm:@effectionx/context-api@0.6.0",
"npm:@effectionx/fs@0.3.0",
- "npm:@effectionx/node@0.2.4",
"npm:@effectionx/process@0.8.1",
- "npm:effection@4.1.0"
+ "npm:effection@4.1.0",
+ "npm:zod@^4.3.6"
]
}
},
- "packages/terminal-tmux": {
+ "packages/runtime": {
"packageJson": {
"dependencies": [
+ "npm:@effectionx/context-api@0.6.0",
+ "npm:@effectionx/fetch@0.2.1",
"npm:@effectionx/fs@0.3.0",
+ "npm:@effectionx/node@0.2.5",
"npm:@effectionx/process@0.8.1",
- "npm:effection@4.1.0",
- "npm:zod@^4.3.6"
+ "npm:effection@4.1.0"
]
}
},
diff --git a/packages/acp/package.json b/packages/acp/package.json
index 4fc639685..86d586b1f 100644
--- a/packages/acp/package.json
+++ b/packages/acp/package.json
@@ -10,8 +10,8 @@
"dependencies": {
"@agentclientprotocol/sdk": "1.3.0",
"@executablemd/core": "workspace:*",
+ "@executablemd/grid": "workspace:*",
"@executablemd/runtime": "workspace:*",
- "@executablemd/terminal": "workspace:*",
"acpx": "0.12.0",
"effection": "4.1.0"
}
diff --git a/packages/acp/src/provider.ts b/packages/acp/src/provider.ts
index 6eb0597a5..b1a3a4d16 100644
--- a/packages/acp/src/provider.ts
+++ b/packages/acp/src/provider.ts
@@ -89,7 +89,7 @@ import {
cwd,
ExecutableObservationError,
} from "@executablemd/runtime";
-import { nativeLaunch } from "@executablemd/terminal";
+import { nativeLaunch } from "@executablemd/grid";
import type {
AgentSessionCoordinator,
AgentSessionKey,
@@ -2760,7 +2760,7 @@ function* useAcpxProviderState(
//
// The launch runs in a scope of its own so that this owner can bring
// it down deliberately and watch how that goes. A cancelled launch —
- // the reader closing a terminal grid is one — unwinds past every
+ // the reader closing a grid is one — unwinds past every
// statement after it, so a decision written down here would never be
// reached; written as this scope's cleanup, it is reached on every
// path there is.
diff --git a/packages/acp/tests/native-launch.test.ts b/packages/acp/tests/native-launch.test.ts
index 6cdf80860..6961108fc 100644
--- a/packages/acp/tests/native-launch.test.ts
+++ b/packages/acp/tests/native-launch.test.ts
@@ -27,10 +27,10 @@ import type {
PreparedLaunchRecord,
Session,
} from "@executablemd/core";
-import { flushOutput, NativeLauncher, reserveTerminal } from "@executablemd/terminal";
-import { installControlledLauncher } from "@executablemd/terminal/test";
+import { flushOutput, NativeLauncher, reserveTerminal } from "@executablemd/grid";
+import { installControlledLauncher } from "@executablemd/grid/test";
import type { AgentSessionCoordinator } from "@executablemd/runtime";
-import type { NativeLaunchRequest } from "@executablemd/terminal";
+import type { NativeLaunchRequest } from "@executablemd/grid";
import { createAcpxProvider } from "../src/provider.ts";
import type { AcpxProviderDependencies } from "../src/provider.ts";
import {
diff --git a/packages/cli/package.json b/packages/cli/package.json
index 242ef51a5..de4fcaa53 100644
--- a/packages/cli/package.json
+++ b/packages/cli/package.json
@@ -15,9 +15,9 @@
"@executablemd/acp": "workspace:*",
"@executablemd/core": "workspace:*",
"@executablemd/durable-streams": "workspace:*",
+ "@executablemd/grid": "workspace:*",
+ "@executablemd/grid-tmux": "workspace:*",
"@executablemd/runtime": "workspace:*",
- "@executablemd/terminal": "workspace:*",
- "@executablemd/terminal-tmux": "workspace:*",
"@executablemd/test-agent": "workspace:*",
"@executablemd/testing": "workspace:*",
"@executablemd/web": "workspace:*",
diff --git a/packages/cli/src/agent-stack.ts b/packages/cli/src/agent-stack.ts
index d754acf5b..9ce1c5aa5 100644
--- a/packages/cli/src/agent-stack.ts
+++ b/packages/cli/src/agent-stack.ts
@@ -22,9 +22,9 @@ import {
} from "@executablemd/core";
import type { AgentProviderFactory, PermissionMode } from "@executablemd/core";
import { env as readEnv } from "@executablemd/runtime";
-import { installForegroundLauncher } from "@executablemd/terminal/posix";
-import { unsupportedTerminalGrid } from "./grid-host.ts";
-import type { TerminalGridInstaller } from "./grid-host.ts";
+import { installForegroundLauncher } from "@executablemd/grid/posix";
+import { unsupportedGrid } from "./grid-host.ts";
+import type { GridInstaller } from "./grid-host.ts";
import { createAcpxProvider, DEFAULT_AGENT_NAME } from "@executablemd/acp";
import type { AcpxProviderDependencies } from "@executablemd/acp";
// A separate entrypoint because the embedded adapters are temporary (#636) and
@@ -71,13 +71,13 @@ export interface AuthorshipStack {
/** What this host states about machine-wide agent sessions, if anything. */
sessions?: MachineSessionAssembly;
/**
- * What presents this host's terminal grids.
+ * What presents this host's grids.
*
* Deno and the compiled binary supply the tmux provider; Node and Bun supply
* the one that installs none, so those runtimes describe and validate the
* same grids and open none of them.
*/
- installTerminalGrid?: TerminalGridInstaller;
+ installGrid?: GridInstaller;
}
/** Everything one `xmd run` invocation settled about agents, resolved once. */
@@ -117,7 +117,7 @@ export function* resolveAuthorshipStack(
export function* resolveAgentStack(
flags: AgentFlags,
sessions: MachineSessionAssembly | undefined,
- installTerminalGrid?: TerminalGridInstaller,
+ installGrid?: GridInstaller,
): Operation> {
const config = resolveAgentConfig(flags);
if ("error" in config) {
@@ -133,7 +133,7 @@ export function* resolveAgentStack(
return Ok({
...authorship.value,
permissionMode: config.permissionMode,
- ...(installTerminalGrid === undefined ? {} : { installTerminalGrid }),
+ ...(installGrid === undefined ? {} : { installGrid }),
});
}
@@ -195,8 +195,8 @@ export function* installRunAgentStack(stack: AgentStack): Operation {
// document inspection and `xmd test` install no launcher, so a document that
// reaches under any of them refuses instead of spawning.
yield* installForegroundLauncher();
- // And whatever presents this host's terminal grids, which on a host that
+ // And whatever presents this host's grids, which on a host that
// presents none still opens the installation so a grid is validated — the
// refusal a document meets there is core's own.
- yield* (stack.installTerminalGrid ?? unsupportedTerminalGrid)();
+ yield* (stack.installGrid ?? unsupportedGrid)();
}
diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts
index 9ad13d20a..940c09b50 100755
--- a/packages/cli/src/cli.ts
+++ b/packages/cli/src/cli.ts
@@ -96,8 +96,8 @@ import { installWebComponents, installWebElicitation } from "@executablemd/web";
import { timebox } from "@effectionx/timebox";
import { timeout as runTimeout } from "@executablemd/runtime";
import { installRunAgentStack, resolveAgentStack, resolveAuthorshipStack } from "./agent-stack.ts";
-import { unsupportedTerminalGrid } from "./grid-host.ts";
-import type { TerminalGridInstaller } from "./grid-host.ts";
+import { unsupportedGrid } from "./grid-host.ts";
+import type { GridInstaller } from "./grid-host.ts";
import { planComponentDeclaration } from "./plan-component.ts";
import { planAgentContext } from "./authorship-profile.ts";
import { useVerboseComponent } from "./verbose-component.ts";
@@ -783,9 +783,9 @@ function* underRunDeadline(timeouts: RunTimeouts, body: () => Operation):
function* settleAgentStack(
flags: AgentFlags,
sessions: MachineSessionAssembly | undefined,
- installTerminalGrid: TerminalGridInstaller,
+ installGrid: GridInstaller,
): Operation {
- const stack = yield* resolveAgentStack(flags, sessions, installTerminalGrid);
+ const stack = yield* resolveAgentStack(flags, sessions, installGrid);
if (!stack.ok) {
console.error(stack.error.message);
yield* exit(1);
@@ -2356,7 +2356,7 @@ function* dispatch(
readStandardInput: StandardInputReader,
workflowHost: WorkflowHost | undefined,
sessions: MachineSessionAssembly | undefined,
- installTerminalGrid: TerminalGridInstaller,
+ installGrid: GridInstaller,
): Operation {
// Before the props phase, and before the help short-circuit below. `--help`
// is lifted out of argv early enough that a command's own grammar never sees
@@ -2455,7 +2455,7 @@ function* dispatch(
denyAll: config.denyAll,
},
sessions,
- installTerminalGrid,
+ installGrid,
);
if (runStack === undefined) {
break;
@@ -2511,7 +2511,6 @@ function* dispatch(
const authorship = yield* resolveAuthorshipStack(
{ agentProvider: config.agentProvider, defaultAgent: config.defaultAgent },
sessions,
- installTerminalGrid,
);
if (!authorship.ok) {
console.error(authorship.error.message);
@@ -2815,10 +2814,10 @@ export function* runXmd(
// owns the session or which build it belongs to. A caller that names none
// gets no machine sessions at all, which is the ordinary ACP behaviour.
sessions?: MachineSessionAssembly,
- // What presents a terminal grid on this host. Deno and the compiled binary
+ // What presents a grid on this host. Deno and the compiled binary
// supply the tmux provider; Node and Bun supply the one that installs none,
// so those runtimes describe and validate the same grids and open none.
- installTerminalGrid: TerminalGridInstaller = unsupportedTerminalGrid,
+ installGrid: GridInstaller = unsupportedGrid,
): Operation {
// Before every scanner, before command selection, and before anything reads a
// path. `prompt` names no command, and a first token that names none is a
@@ -2888,7 +2887,7 @@ export function* runXmd(
readStandardInput,
workflowHost,
sessions,
- installTerminalGrid,
+ installGrid,
);
}
@@ -2911,7 +2910,7 @@ export function* runXmd(
readStandardInput,
workflowHost,
sessions,
- installTerminalGrid,
+ installGrid,
),
);
}
diff --git a/packages/cli/src/compiled.ts b/packages/cli/src/compiled.ts
index c7d70d281..d15c67421 100644
--- a/packages/cli/src/compiled.ts
+++ b/packages/cli/src/compiled.ts
@@ -19,8 +19,8 @@ import {
isCredentialHelperMode,
runCredentialHelper,
} from "@executablemd/workflow/credential-helper";
-import { paneWorkerInvocation, runPaneWorkerProcess } from "@executablemd/terminal-tmux";
-import { foregroundTerminalGrid } from "./grid-host.ts";
+import { paneWorkerInvocation, runPaneWorkerProcess } from "@executablemd/grid-tmux";
+import { foregroundGrid } from "./grid-host.ts";
import type { HelperAssembly } from "@executablemd/workflow/credential-helper";
import { useCompiledService } from "./compiled-service.ts";
@@ -103,7 +103,7 @@ if (paneWorker !== undefined) {
useMachineSessions(),
// This host presents grids: it has a terminal to divide, and it can
// re-invoke itself for one pane.
- foregroundTerminalGrid(),
+ foregroundGrid(),
);
});
}
diff --git a/packages/cli/src/deno.ts b/packages/cli/src/deno.ts
index a1e7c9b5e..c3ae4e114 100644
--- a/packages/cli/src/deno.ts
+++ b/packages/cli/src/deno.ts
@@ -22,8 +22,8 @@ import {
isCredentialHelperMode,
runCredentialHelper,
} from "@executablemd/workflow/credential-helper";
-import { paneWorkerInvocation, runPaneWorkerProcess } from "@executablemd/terminal-tmux";
-import { foregroundTerminalGrid } from "./grid-host.ts";
+import { paneWorkerInvocation, runPaneWorkerProcess } from "@executablemd/grid-tmux";
+import { foregroundGrid } from "./grid-host.ts";
import type { HelperAssembly } from "@executablemd/workflow/credential-helper";
import { useDenoService } from "./deno-service.ts";
@@ -122,7 +122,7 @@ if (paneWorker !== undefined) {
useMachineSessions(),
// This host presents grids: it has a terminal to divide, and it can
// re-invoke itself for one pane.
- foregroundTerminalGrid(),
+ foregroundGrid(),
);
});
}
diff --git a/packages/cli/src/grid-host.ts b/packages/cli/src/grid-host.ts
index 591d773b3..05272fad5 100644
--- a/packages/cli/src/grid-host.ts
+++ b/packages/cli/src/grid-host.ts
@@ -1,10 +1,10 @@
/**
- * Which hosts open a terminal grid, and which only describe one
+ * Which hosts open a grid, and which only describe one
* (architecture.md §Package ownership).
*
* Host composition, not a terminal implementation — which is why it sits here
* rather than under a `terminal/` path. The domain is
- * `@executablemd/terminal`'s and the provider is `@executablemd/terminal-tmux`'s;
+ * `@executablemd/grid`'s and the provider is `@executablemd/grid-tmux`'s;
* what this module does is decide, per entrypoint, whether to install them.
*
* The Deno source entrypoint and the compiled binary present grids when the
@@ -14,7 +14,7 @@
* than part-way through one.
*
* That is a fact about the host, so the entrypoint states it rather than this
- * module inferring it. `unsupportedTerminalGrid` is the honest half of the same
+ * module inferring it. `unsupportedGrid` is the honest half of the same
* choice: it installs nothing, and the refusal a document meets is the one core
* already gives when no provider is installed.
*/
@@ -22,18 +22,18 @@
import { ensure, race, resource, withResolvers } from "effection";
import type { Operation } from "effection";
import process from "node:process";
-import { Execution, installTerminalGridProfile } from "@executablemd/core";
+import { Execution, installGridProfile } from "@executablemd/core";
import { command as hostCommand } from "@executablemd/runtime";
-import { installDenoTerminalProcesses } from "@executablemd/terminal/posix";
+import { installDenoTerminalProcesses } from "@executablemd/grid/posix";
import {
installTmuxGridProvider,
PANE_WORKER_COMMAND,
TMUX_PROVIDER,
-} from "@executablemd/terminal-tmux";
-import type { TmuxProviderDependencies } from "@executablemd/terminal-tmux";
+} from "@executablemd/grid-tmux";
+import type { TmuxProviderDependencies } from "@executablemd/grid-tmux";
-/** How a host installs whatever presents its terminal grids. */
-export type TerminalGridInstaller = () => Operation;
+/** How a host installs whatever presents its grids. */
+export type GridInstaller = () => Operation;
/**
* A host that describes grids and presents none.
@@ -42,8 +42,8 @@ export type TerminalGridInstaller = () => Operation;
* still validated, and core's own refusal is what a document meets when it asks
* for one to be shown.
*/
-export function* unsupportedTerminalGrid(): Operation {
- yield* installTerminalGridProfile();
+export function* unsupportedGrid(): Operation {
+ yield* installGridProfile();
}
/**
@@ -184,9 +184,7 @@ export function useHangup(): Operation> {
* executable is what makes a pane work in the compiled distribution, where
* there is no script to run.
*/
-export function foregroundTerminalGrid(
- overrides: Partial = {},
-): TerminalGridInstaller {
+export function foregroundGrid(overrides: Partial = {}): GridInstaller {
return function* (): Operation {
const hangup = yield* useHangup();
// The observer goes in beside the provider, in the same scope: a host that
@@ -201,7 +199,7 @@ export function foregroundTerminalGrid(
size: windowSize,
...overrides,
});
- yield* installTerminalGridProfile({ provider: TMUX_PROVIDER, label: TMUX_PROVIDER });
+ yield* installGridProfile({ provider: TMUX_PROVIDER, label: TMUX_PROVIDER });
yield* useHangupCancellation(hangup);
};
}
diff --git a/packages/cli/tests/agent-session-coordinator.test.ts b/packages/cli/tests/agent-session-coordinator.test.ts
index 7837bef20..78c1c4bf4 100644
--- a/packages/cli/tests/agent-session-coordinator.test.ts
+++ b/packages/cli/tests/agent-session-coordinator.test.ts
@@ -29,7 +29,7 @@ import {
createDenoAgentSessionCoordinator,
hasDenoAgentSessionCoordinator,
} from "@executablemd/runtime";
-import { installControlledLauncher } from "@executablemd/terminal/test";
+import { installControlledLauncher } from "@executablemd/grid/test";
import type { AgentSessionCoordinator } from "@executablemd/runtime";
import {
ADVERTISED_CLIENT_NATIVE_ATTACHMENT,
@@ -40,7 +40,7 @@ import {
} from "@executablemd/acp";
import type { AgentSessionRouteStore, NativeAdapter, NativeBinding } from "@executablemd/acp";
import type { ExecutableObserver } from "@executablemd/runtime";
-import type { NativeLaunchRequest } from "@executablemd/terminal";
+import type { NativeLaunchRequest } from "@executablemd/grid";
import { createFakeObserver } from "../../acp/tests/helpers.ts";
import {
sessionCoordinatorRoot,
diff --git a/packages/cli/tests/terminal-host.test.ts b/packages/cli/tests/grid-host.test.ts
similarity index 89%
rename from packages/cli/tests/terminal-host.test.ts
rename to packages/cli/tests/grid-host.test.ts
index a9e01686b..77dbc912b 100644
--- a/packages/cli/tests/terminal-host.test.ts
+++ b/packages/cli/tests/grid-host.test.ts
@@ -1,10 +1,10 @@
/**
- * Tier TH — which hosts open a terminal grid, and which only describe one
+ * Tier TH — which hosts open a grid, and which only describe one
* (architecture.md §Package ownership, issue #717).
*
* The host-composition boundary is CLI's, so its evidence is too. The tmux
* adapter's own topology, protocol, worker and teardown rows live with the
- * adapter in `@executablemd/terminal-tmux`; what is proved here is the part
+ * adapter in `@executablemd/grid-tmux`; what is proved here is the part
* only an entrypoint can answer — which runtime installs a provider and an
* observer, which installs neither, what a real document gets in each case,
* and that a terminal going away cancels the run rather than closing the grid.
@@ -26,18 +26,18 @@ import { randomUUID } from "node:crypto";
import { cliCommand } from "@executablemd/test-support/launch";
import { ensureDir, exists, readTextFile, rm, writeTextFile } from "@effectionx/fs";
import { execute } from "@executablemd/core";
-import { installTerminalProvider, useTerminalInstallation } from "@executablemd/terminal/lifecycle";
+import { installGridProvider, useGridInstallation } from "@executablemd/grid/lifecycle";
import type { Json } from "@executablemd/core";
import { InMemoryStream } from "@executablemd/durable-streams";
-import { registerTerminalProvider, TerminalGrids } from "@executablemd/terminal";
-import { installControlledLauncher } from "@executablemd/terminal/test";
-import { processReachable } from "@executablemd/terminal/processes";
-import { installDenoTerminalProcesses } from "@executablemd/terminal/posix";
-import { PANE_WORKER_COMMAND, tmuxGridProvider } from "@executablemd/terminal-tmux";
-import { foregroundSignalListeners } from "@executablemd/terminal-tmux/test";
-import { createFakeTmux } from "../../terminal-tmux/tests/fixtures/fake-tmux.ts";
-import { clientCommand } from "../../terminal-tmux/tests/fixtures/client-command.ts";
-import { foregroundTerminalGrid, unsupportedTerminalGrid } from "../src/grid-host.ts";
+import { registerGridProvider, Grids } from "@executablemd/grid";
+import { installControlledLauncher } from "@executablemd/grid/test";
+import { processReachable } from "@executablemd/grid/processes";
+import { installDenoTerminalProcesses } from "@executablemd/grid/posix";
+import { PANE_WORKER_COMMAND, tmuxGridProvider } from "@executablemd/grid-tmux";
+import { foregroundSignalListeners } from "@executablemd/grid-tmux/test";
+import { createFakeTmux } from "../../grid-tmux/tests/fixtures/fake-tmux.ts";
+import { clientCommand } from "../../grid-tmux/tests/fixtures/client-command.ts";
+import { foregroundGrid, unsupportedGrid } from "../src/grid-host.ts";
/** Where a fake server and its client fixtures meet. */
function useScript(): Operation {
@@ -63,8 +63,8 @@ function useProbedProvider(options: {
version?: string;
}): Operation {
return (function* (): Operation {
- const authority = yield* useTerminalInstallation();
- yield* registerTerminalProvider(
+ const authority = yield* useGridInstallation();
+ yield* registerGridProvider(
"tmux",
tmuxGridProvider({
isTerminal: options.isTerminal,
@@ -84,8 +84,8 @@ function useProbedProvider(options: {
}),
}),
);
- yield* installTerminalProvider("tmux", { label: "tmux" }, authority);
- yield* TerminalGrids.operations.open({
+ yield* installGridProvider("tmux", { label: "tmux" }, authority);
+ yield* Grids.operations.open({
columns: 1,
rows: 1,
panes: [{ ordinal: 0, title: "Only", row: 0, column: 0, form: "paired" }],
@@ -172,21 +172,16 @@ describe("Tier TH — host installation", () => {
});
yield* writeTextFile(
path.join(room, "doc.md"),
- [
- "",
- '',
- "",
- "",
- "AFTER_THE_GRID",
- "",
- ].join("\n"),
+ ["", '', "", "", "AFTER_THE_GRID", ""].join(
+ "\n",
+ ),
);
yield* installControlledLauncher({ outcome: () => ({ exitCode: 0 }) });
let outcome: Result | undefined;
let output = "";
yield* scoped(function* () {
- yield* foregroundTerminalGrid({
+ yield* foregroundGrid({
isTerminal: () => true,
createTmux: () => tmux,
env: { PATH: "/usr/bin:/bin", SHELL: shell },
@@ -249,7 +244,7 @@ describe("Tier TH — host installation", () => {
refusal = error instanceof Error ? error.message : String(error);
}
- expect(refusal).toContain("cannot open a terminal grid");
+ expect(refusal).toContain("cannot open a grid");
expect(refusal).toContain("no terminal");
// Before a directory, a socket, a token, a worker, a server or a pane: the
// host left nothing behind for having tried.
@@ -273,7 +268,7 @@ describe("Tier TH — host installation", () => {
} catch (error) {
refusal = error instanceof Error ? error.message : String(error);
}
- expect(refusal).toContain("cannot open a terminal grid");
+ expect(refusal).toContain("cannot open a grid");
expect(refusal).toContain("older than tmux");
});
@@ -288,14 +283,9 @@ describe("Tier TH — host installation", () => {
});
yield* writeTextFile(
path.join(room, "doc.md"),
- [
- "",
- '',
- "",
- "",
- "AFTER_THE_GRID",
- "",
- ].join("\n"),
+ ["", '', "", "", "AFTER_THE_GRID", ""].join(
+ "\n",
+ ),
);
// The run's foreground lease, which a grid takes before any provider.
yield* installControlledLauncher({ outcome: () => ({ exitCode: 0 }) });
@@ -306,7 +296,7 @@ describe("Tier TH — host installation", () => {
let outcome: Result | undefined;
let output = "";
yield* scoped(function* () {
- yield* foregroundTerminalGrid({
+ yield* foregroundGrid({
isTerminal: () => true,
createTmux: () => tmux,
env: { PATH: "/usr/bin:/bin", SHELL: shell },
@@ -393,14 +383,9 @@ describe("Tier TH — host installation", () => {
});
yield* writeTextFile(
path.join(room, "doc.md"),
- [
- "",
- '',
- "",
- "",
- "AFTER_THE_GRID",
- "",
- ].join("\n"),
+ ["", '', "", "", "AFTER_THE_GRID", ""].join(
+ "\n",
+ ),
);
yield* installControlledLauncher({ outcome: () => ({ exitCode: 0 }) });
@@ -408,7 +393,7 @@ describe("Tier TH — host installation", () => {
let outcome: Result | undefined;
let output = "";
yield* scoped(function* () {
- yield* foregroundTerminalGrid({
+ yield* foregroundGrid({
isTerminal: () => true,
createTmux: () => tmux,
env: { PATH: "/usr/bin:/bin", SHELL: shell },
@@ -477,16 +462,16 @@ describe("Tier TH — host installation", () => {
it("TH6: the Deno and compiled entrypoints present grids; Node and Bun do not", function* () {
for (const name of ["deno.ts", "compiled.ts"]) {
- expect((yield* entrypointSource(name)).includes("foregroundTerminalGrid()")).toBe(true);
+ expect((yield* entrypointSource(name)).includes("foregroundGrid()")).toBe(true);
}
for (const name of ["node.ts", "bun.ts"]) {
// Not a different grid: no grid at all, and therefore the default the
// shared entry declares — which is the installation that validates a grid
// and presents none.
- expect((yield* entrypointSource(name)).includes("foregroundTerminalGrid")).toBe(false);
+ expect((yield* entrypointSource(name)).includes("foregroundGrid")).toBe(false);
}
expect(yield* entrypointSource("cli.ts")).toContain(
- "installTerminalGrid: TerminalGridInstaller = unsupportedTerminalGrid",
+ "installGrid: GridInstaller = unsupportedGrid",
);
});
@@ -530,14 +515,12 @@ describe("Tier TH — host installation", () => {
yield* writeTextFile(
path.join(room, "doc.md"),
- ["", '', "", ""].join(
- "\n",
- ),
+ ["", '', "", ""].join("\n"),
);
yield* installControlledLauncher({ outcome: () => ({ exitCode: 0 }) });
yield* scoped(function* () {
- yield* foregroundTerminalGrid({
+ yield* foregroundGrid({
isTerminal: () => true,
createTmux: () => tmux,
// deno-lint-ignore require-yield
@@ -587,10 +570,10 @@ describe("Tier TH — host installation", () => {
it("TH3: a host that installs no provider still validates the grid", function* () {
// Node and Bun: the same language and the same validation, and core's own
// refusal rather than a provider that half-works.
- yield* unsupportedTerminalGrid();
+ yield* unsupportedGrid();
let refusal = "";
try {
- yield* TerminalGrids.operations.open({
+ yield* Grids.operations.open({
columns: 1,
rows: 1,
panes: [{ ordinal: 0, title: "Only", row: 0, column: 0, form: "paired" }],
@@ -598,6 +581,6 @@ describe("Tier TH — host installation", () => {
} catch (error) {
refusal = error instanceof Error ? error.message : String(error);
}
- expect(refusal).toContain("no terminal provider is installed");
+ expect(refusal).toContain("no grid provider is installed");
});
});
diff --git a/packages/cli/tests/run-composition-deno.test.ts b/packages/cli/tests/run-composition-deno.test.ts
index 8b47cf4e4..4ff8190b8 100644
--- a/packages/cli/tests/run-composition-deno.test.ts
+++ b/packages/cli/tests/run-composition-deno.test.ts
@@ -22,7 +22,7 @@ import { spawnSync } from "node:child_process";
import { join } from "node:path";
import process from "node:process";
import { API, useHostFiles } from "@executablemd/runtime";
-import { NativeLauncher } from "@executablemd/terminal";
+import { NativeLauncher } from "@executablemd/grid";
import { InMemoryStream } from "@executablemd/durable-streams";
import {
Agent,
diff --git a/packages/cli/tests/session-launch-cli.test.ts b/packages/cli/tests/session-launch-cli.test.ts
index d0cbbe278..c1ce66292 100644
--- a/packages/cli/tests/session-launch-cli.test.ts
+++ b/packages/cli/tests/session-launch-cli.test.ts
@@ -123,19 +123,17 @@ const ROLES = [
/** One authored grid, whose pane content must never run without a provider. */
const GRID = [
- "",
- '',
+ "",
+ '',
"PANE_MARKER",
- "",
- '',
- "",
+ "",
+ '',
+ "",
"",
].join("\n");
/** A grid the grammar refuses, wherever it is written. */
-const BAD_GRID = ["", '', "", ""].join(
- "\n",
-);
+const BAD_GRID = ["", '', "", ""].join("\n");
const NO_LAUNCH = "PLAIN_MARKER\n\nThis document launches nothing.\n";
@@ -240,12 +238,12 @@ describe(
// The concrete structural refusal, named and located — not merely the
// absence of a provider message, which an unrelated failure would also
// satisfy.
- expect(reported).toContain(' requires a "columns" prop');
+ expect(reported).toContain(' requires a "columns" prop');
expect(reported).toContain("bad.md:1:1");
// And it is the grammar's refusal, reached wherever the document is read
// rather than at a provider.
- expect(reported).not.toContain("cannot open a terminal grid");
- expect(reported).not.toContain("no terminal provider is installed");
+ expect(reported).not.toContain("cannot open a grid");
+ expect(reported).not.toContain("no grid provider is installed");
});
it("CL5: no behavior is keyed to the filename", function* () {
diff --git a/packages/cli/tests/syntax-cli.test.ts b/packages/cli/tests/syntax-cli.test.ts
index bf719c5ec..528608a0f 100644
--- a/packages/cli/tests/syntax-cli.test.ts
+++ b/packages/cli/tests/syntax-cli.test.ts
@@ -295,7 +295,7 @@ describe("Tier SX — the run profile the command describes", () => {
]);
});
- it("TG3: describes both terminal-grid constructs without probing for a terminal", function* () {
+ it("TG3: describes both grid constructs without probing for a terminal", function* () {
// Whatever this runtime can or cannot open, the language is the same, so
// the one boundary a capability probe would cross is a trap here.
const catalog = yield* scoped(function* () {
@@ -305,24 +305,21 @@ describe("Tier SX — the run profile the command describes", () => {
throw new Error(`describing the syntax ran ${JSON.stringify(options.command)}`);
},
});
- return yield* syntaxCatalog([]);
+ return yield* syntaxSymbols([]);
});
const [structural, builtIn] = catalog.categories;
- const grid = structural.entries.find((entry) => entry.name === "Terminal.Grid");
- const pane = structural.entries.find((entry) => entry.name === "Terminal");
- expect(grid?.origin).toEqual({ kind: "structural", construct: "Terminal.Grid" });
- expect(pane?.origin).toEqual({ kind: "structural", construct: "Terminal" });
- expect(grid?.syntax).toEqual(["…"]);
- expect(pane?.syntax).toEqual([
- '…',
- '',
- ]);
+ const grid = structural.entries.find((entry) => entry.name === "Grid");
+ const pane = structural.entries.find((entry) => entry.name === "Pane");
+ expect(grid?.origin).toEqual({ kind: "structural", construct: "Grid" });
+ expect(pane?.origin).toEqual({ kind: "structural", construct: "Pane" });
+ expect(grid?.syntax).toEqual(["…"]);
+ expect(pane?.syntax).toEqual(['…', '']);
expect(grid?.description ?? "").not.toBe("");
expect(pane?.description ?? "").not.toBe("");
// Reserved syntax, so neither name is a component this profile offers.
- expect(names(builtIn.entries)).not.toContain("Terminal.Grid");
- expect(names(builtIn.entries)).not.toContain("Terminal");
+ expect(names(builtIn.entries)).not.toContain("Grid");
+ expect(names(builtIn.entries)).not.toContain("Pane");
});
it("SX3: describes without minting an execution claimant", function* () {
@@ -563,19 +560,19 @@ describe("Tier SX — the command line", { sanitizeOps: false, sanitizeResources
});
});
- it("TG3: prints both terminal-grid constructs, in markdown and in JSON", function* () {
+ it("TG3: prints both grid constructs, in markdown and in JSON", function* () {
yield* useWorkspace(WORKSPACE, function* (cwd) {
const markdown = yield* runCli(["syntax"], { cwd }).expect();
- expect(markdown.stdout).toContain("### ``");
- expect(markdown.stdout).toContain("### ``");
- expect(markdown.stdout).toContain("…");
- expect(markdown.stdout).toContain('…');
- expect(markdown.stdout).toContain('');
+ expect(markdown.stdout).toContain("### ``");
+ expect(markdown.stdout).toContain("### ``");
+ expect(markdown.stdout).toContain("…");
+ expect(markdown.stdout).toContain('…');
+ expect(markdown.stdout).toContain('');
const json = yield* runCli(["syntax", "--json"], { cwd }).expect();
- const structural = parseCatalog(json.stdout).categories[0].entries;
- expect(names(structural)).toContain("Terminal.Grid");
- expect(names(structural)).toContain("Terminal");
+ const structural = parseSymbols(json.stdout).categories[0].entries;
+ expect(names(structural)).toContain("Grid");
+ expect(names(structural)).toContain("Pane");
});
});
diff --git a/packages/core/mod.ts b/packages/core/mod.ts
index 4e80efd47..4bac8e2c1 100644
--- a/packages/core/mod.ts
+++ b/packages/core/mod.ts
@@ -152,12 +152,12 @@ export { DocumentOutput } from "./src/api.ts";
export type { DocumentOutputApi } from "./src/api.ts";
export { useNormalizedOutput } from "./src/output/normalize.ts";
export { useTerminalOutput } from "./src/output/terminal.ts";
-// The terminal domain is `@executablemd/terminal`'s, and a caller names it
+// The terminal domain is `@executablemd/grid`'s, and a caller names it
// directly (DEC-016). What core exports here is only what core owns: the
// profile that composes a grid into an `Execution`, adapting the terminal
// lifecycle to this engine's journal descriptions and installation.
-export { installTerminalGridProfile } from "./src/terminal/profile.ts";
-export type { TerminalGridProfileOptions } from "./src/terminal/profile.ts";
+export { installGridProfile } from "./src/grid/profile.ts";
+export type { GridProfileOptions } from "./src/grid/profile.ts";
export { execute, Execution } from "./src/execute.ts";
export type {
diff --git a/packages/core/package.json b/packages/core/package.json
index 1fc4c93cf..b66e22f16 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -19,8 +19,8 @@
"@effectionx/stream-helpers": "0.8.3",
"@effectionx/timebox": "0.4.3",
"@executablemd/durable-streams": "workspace:*",
+ "@executablemd/grid": "workspace:*",
"@executablemd/runtime": "workspace:*",
- "@executablemd/terminal": "workspace:*",
"@secretlint/core": "13.0.4",
"@secretlint/profiler": "13.0.4",
"@secretlint/secretlint-rule-preset-recommend": "13.0.4",
diff --git a/packages/core/src/agent/function-components.ts b/packages/core/src/agent/function-components.ts
index c656e64f1..3a3389e85 100644
--- a/packages/core/src/agent/function-components.ts
+++ b/packages/core/src/agent/function-components.ts
@@ -24,7 +24,7 @@ import { sessionPlacement } from "./session-request.ts";
import type { ComponentInvocation, FunctionComponent } from "../types.ts";
import type { IdentityClaimant } from "../invocation-identity.ts";
import { cwd, parseDuration } from "@executablemd/runtime";
-import { flushOutput, reserveTerminal } from "@executablemd/terminal";
+import { flushOutput, reserveTerminal } from "@executablemd/grid";
import type { Json, PropsSchema } from "../types.ts";
import type { Expansion } from "../expansion.ts";
import { Agent } from "./agent-api.ts";
diff --git a/packages/core/src/agent/launch-owner.ts b/packages/core/src/agent/launch-owner.ts
index 3be70ec66..200741679 100644
--- a/packages/core/src/agent/launch-owner.ts
+++ b/packages/core/src/agent/launch-owner.ts
@@ -17,7 +17,7 @@ import { createApi } from "@effectionx/context-api";
import { scoped } from "effection";
import type { Operation, Stream } from "effection";
import { cwd } from "@executablemd/runtime";
-import { flushOutput, reserveTerminal } from "@executablemd/terminal";
+import { flushOutput, reserveTerminal } from "@executablemd/grid";
import { Agent, AGENT_API } from "./agent-api.ts";
import type {
AgentApi,
diff --git a/packages/core/src/document-validation.ts b/packages/core/src/document-validation.ts
index 9605a39ad..d24bad55b 100644
--- a/packages/core/src/document-validation.ts
+++ b/packages/core/src/document-validation.ts
@@ -74,9 +74,9 @@ import {
strayCaseMessage,
strayElseMessage,
strayStructuralMessage,
- strayTerminalMessage,
+ strayPaneMessage,
switchStructure,
- terminalGridStructure,
+ gridStructure,
} from "./structural-rules.ts";
import type { StructuralViolation } from "./structural-rules.ts";
import type {
@@ -316,8 +316,8 @@ interface LexicalContext {
readonly insideIf: boolean;
/** Whether a `` in this source lexically encloses this point. */
readonly insideSwitch: boolean;
- /** Whether a `` in this source lexically encloses this point. */
- readonly insideTerminalGrid: boolean;
+ /** Whether a `` in this source lexically encloses this point. */
+ readonly insideGrid: boolean;
/** Whether the immediate parent is an ``. */
readonly underAnswers: boolean;
}
@@ -496,7 +496,7 @@ class ValidationState {
insideLoop: false,
insideIf: false,
insideSwitch: false,
- insideTerminalGrid: false,
+ insideGrid: false,
underAnswers: false,
});
}
@@ -1107,22 +1107,22 @@ class ValidationState {
return context.insideSwitch
? []
: [{ code: "structural-usage-invalid", source: "Case", message: strayCaseMessage() }];
- case "Terminal.Grid":
+ case "Grid":
// The whole layout is decided from source, so every pane's own mistake
// is reported where it was written — and so is a construct written
// below the grid that the grid does not lay out.
- return terminalGridStructure(segment).violations;
- case "Terminal":
- // A well-placed `` is its grid's, and one placed wrongly
+ return gridStructure(segment).violations;
+ case "Pane":
+ // A well-placed `` is its grid's, and one placed wrongly
// under a grid is already reported by that grid's own structure. What
// is left is a pane with no grid above it at all.
- return context.insideTerminalGrid
+ return context.insideGrid
? []
: [
{
code: "structural-usage-invalid",
- source: "Terminal",
- message: strayTerminalMessage(),
+ source: "Pane",
+ message: strayPaneMessage(),
},
];
case "Else":
@@ -1295,7 +1295,7 @@ function childContext(segment: ComponentElement, context: LexicalContext): Lexic
insideLoop: context.insideLoop || segment.name === "Loop",
insideIf: context.insideIf || segment.name === "If",
insideSwitch: context.insideSwitch || segment.name === "Switch",
- insideTerminalGrid: context.insideTerminalGrid || segment.name === "Terminal.Grid",
+ insideGrid: context.insideGrid || segment.name === "Grid",
underAnswers: segment.name === "Answers",
};
}
diff --git a/packages/core/src/expand.ts b/packages/core/src/expand.ts
index b123391c5..debed6d80 100644
--- a/packages/core/src/expand.ts
+++ b/packages/core/src/expand.ts
@@ -57,24 +57,19 @@ import {
strayCaseMessage,
strayElseMessage,
strayStructuralMessage,
- strayTerminalMessage,
+ strayPaneMessage,
switchStructure,
- terminalColumns,
- terminalColumnsMissingMessage,
- terminalGridStructure,
- terminalTitle,
- terminalTitleMissingMessage,
+ gridColumns,
+ gridColumnsMissingMessage,
+ gridStructure,
+ paneTitle,
+ paneTitleMissingMessage,
} from "./structural-rules.ts";
-import type { StructuralViolation, SwitchCase, TerminalPane } from "./structural-rules.ts";
-import {
- durableGrid,
- openTerminalGrid,
- terminalGridLayout,
- toRequest,
-} from "@executablemd/terminal/lifecycle";
-import type { PaneWork, PlacedPane } from "@executablemd/terminal/lifecycle";
-import { usePaneNativeLauncher, usePaneTerminal } from "@executablemd/terminal";
-import { recordGridLayout } from "./terminal/journal.ts";
+import type { StructuralViolation, SwitchCase, Pane } from "./structural-rules.ts";
+import { durableGrid, openGrid, gridLayout, toRequest } from "@executablemd/grid/lifecycle";
+import type { PaneWork, PlacedPane } from "@executablemd/grid/lifecycle";
+import { usePaneNativeLauncher, usePaneTerminal } from "@executablemd/grid";
+import { recordGridLayout } from "./grid/journal.ts";
import {
asBindingViolation,
asExpressionViolation,
@@ -1188,10 +1183,10 @@ function* expandListSegments(
break;
}
- if (segment.name === "Terminal.Grid") {
- // No raise() here, like the branches above: expandTerminalGrid
+ if (segment.name === "Grid") {
+ // No raise() here, like the branches above: expandGrid
// reports every error it creates.
- yield* expandTerminalGrid(segment, result, {
+ yield* expandGrid(segment, result, {
parentMeta,
parentProps,
hideSet,
@@ -1202,16 +1197,16 @@ function* expandListSegments(
break;
}
- if (segment.name === "Terminal") {
- // A well-placed is consumed by its and
+ if (segment.name === "Pane") {
+ // A well-placed is consumed by its and
// never expanded on its own. Reaching this branch means the pane sits
// outside every grid, so it names no component and is diagnosed
// rather than resolved from the filesystem.
result.push(
yield* raise({
type: "error",
- message: positioned(strayTerminalMessage(), segment),
- source: "Terminal",
+ message: positioned(strayPaneMessage(), segment),
+ source: "Pane",
}),
);
break;
@@ -2072,16 +2067,16 @@ function* expandSwitch(
);
}
-function terminalGridError(segment: ComponentElement, message: string): ErrorSegment {
- return { type: "error", message: positioned(message, segment), source: "Terminal.Grid" };
+function gridError(segment: ComponentElement, message: string): ErrorSegment {
+ return { type: "error", message: positioned(message, segment), source: "Grid" };
}
-function terminalPaneError(segment: ComponentElement, message: string): ErrorSegment {
- return { type: "error", message: positioned(message, segment), source: "Terminal" };
+function paneError(segment: ComponentElement, message: string): ErrorSegment {
+ return { type: "error", message: positioned(message, segment), source: "Pane" };
}
/**
- * The value one prop of a terminal-grid construct produced, or why evaluating
+ * The value one prop of a grid construct produced, or why evaluating
* it failed. A missing prop is `undefined`, which is also what an expression
* evaluating to `undefined` leaves behind (§6.5) — absence either way, and the
* caller says what its construct requires instead.
@@ -2129,12 +2124,8 @@ interface GridSite {
readonly authority: ExpansionAuthority | undefined;
}
-function* expandTerminalGrid(
- segment: ComponentElement,
- owner: Segment[],
- site: GridSite,
-): Operation {
- const structure = terminalGridStructure(segment);
+function* expandGrid(segment: ComponentElement, owner: Segment[], site: GridSite): Operation {
+ const structure = gridStructure(segment);
if (structure.violations.length > 0) {
for (const violation of structure.violations) {
owner.push(yield* raise(structuralErrorSegment(violation, segment)));
@@ -2142,18 +2133,18 @@ function* expandTerminalGrid(
return;
}
- const columnsValue = yield* resolveStructuralProp(segment, "Terminal.Grid", "columns");
+ const columnsValue = yield* resolveStructuralProp(segment, "Grid", "columns");
if (!columnsValue.ok) {
- owner.push(yield* raise(terminalGridError(segment, columnsValue.error.message)));
+ owner.push(yield* raise(gridError(segment, columnsValue.error.message)));
return;
}
if (columnsValue.value === undefined) {
- owner.push(yield* raise(terminalGridError(segment, terminalColumnsMissingMessage())));
+ owner.push(yield* raise(gridError(segment, gridColumnsMissingMessage())));
return;
}
- const columns = terminalColumns(columnsValue.value);
+ const columns = gridColumns(columnsValue.value);
if (!columns.ok) {
- owner.push(yield* raise(terminalGridError(segment, columns.error.message)));
+ owner.push(yield* raise(gridError(segment, columns.error.message)));
return;
}
@@ -2161,15 +2152,15 @@ function* expandTerminalGrid(
for (const pane of structure.panes) {
const title = yield* resolvePaneTitle(pane);
if (!title.ok) {
- owner.push(yield* raise(terminalPaneError(pane.element, title.error.message)));
+ owner.push(yield* raise(paneError(pane.element, title.error.message)));
return;
}
placed.push({ title: title.value, form: pane.form });
}
- const layout = terminalGridLayout(columns.value, placed);
+ const layout = gridLayout(columns.value, placed);
// The grid renders nothing into the document: what a pane shows belongs to
- // that pane, and the sibling after `` renders to the root
+ // that pane, and the sibling after `` renders to the root
// again only once the provider has restored it.
const identity = {
path: site.path,
@@ -2187,18 +2178,16 @@ function* expandTerminalGrid(
const work = structure.panes.map((pane, index) =>
paneWork(pane, layout.cells[index]!.title, site),
);
- return yield* openTerminalGrid(layout, work, boundary);
+ return yield* openGrid(layout, work, boundary);
});
const failed = retained.panes.find((pane) => pane.status === "failed");
if (failed !== undefined) {
- owner.push(yield* raise(terminalGridError(segment, failed.reason)));
+ owner.push(yield* raise(gridError(segment, failed.reason)));
}
} catch (error) {
owner.push(
- yield* raise(
- terminalGridError(segment, error instanceof Error ? error.message : String(error)),
- ),
+ yield* raise(gridError(segment, error instanceof Error ? error.message : String(error))),
);
}
}
@@ -2214,7 +2203,7 @@ function* expandTerminalGrid(
* enclosing body, and a checked failure settles the pane rather than poisoning
* the root or a sibling.
*/
-function paneWork(pane: TerminalPane, title: string, site: GridSite): PaneWork {
+function paneWork(pane: Pane, title: string, site: GridSite): PaneWork {
if (pane.form === "self-closing") {
return {
ordinal: pane.ordinal,
@@ -2296,15 +2285,15 @@ function paneWork(pane: TerminalPane, title: string, site: GridSite): PaneWork {
}
/** The label one pane displays, from the value its own `title` prop produced. */
-function* resolvePaneTitle(pane: TerminalPane): Operation> {
- const value = yield* resolveStructuralProp(pane.element, "Terminal", "title");
+function* resolvePaneTitle(pane: Pane): Operation> {
+ const value = yield* resolveStructuralProp(pane.element, "Pane", "title");
if (!value.ok) {
return value;
}
if (value.value === undefined) {
- return Err(new Error(terminalTitleMissingMessage()));
+ return Err(new Error(paneTitleMissingMessage()));
}
- return terminalTitle(value.value);
+ return paneTitle(value.value);
}
function loopError(segment: ComponentElement, message: string): ErrorSegment {
diff --git a/packages/core/src/terminal/journal.ts b/packages/core/src/grid/journal.ts
similarity index 93%
rename from packages/core/src/terminal/journal.ts
rename to packages/core/src/grid/journal.ts
index d7d803e6c..0a12af6ac 100644
--- a/packages/core/src/terminal/journal.ts
+++ b/packages/core/src/grid/journal.ts
@@ -25,12 +25,12 @@ import {
StaleInputError,
} from "@executablemd/durable-streams";
import type { EffectDescription, Json, Workflow } from "@executablemd/durable-streams";
-import type { TerminalGridRequest } from "@executablemd/terminal";
+import type { GridRequest } from "@executablemd/grid";
import { sourceDescription } from "../source-position.ts";
import type { SourcePosition } from "../types.ts";
-import { retainedLayout } from "@executablemd/terminal/lifecycle";
-import type { RetainedGrid } from "@executablemd/terminal/lifecycle";
+import { retainedLayout } from "@executablemd/grid/lifecycle";
+import type { RetainedGrid } from "@executablemd/grid/lifecycle";
/** A grid's identity within one execution: where it was written. */
export interface GridIdentity {
@@ -180,10 +180,7 @@ function divergence(held: RetainedLayout, derived: RetainedLayout): string | und
*
* Expansion driven without a journal records nothing and behaves identically.
*/
-export function* recordGridLayout(
- identity: GridIdentity,
- request: TerminalGridRequest,
-): Operation {
+export function* recordGridLayout(identity: GridIdentity, request: GridRequest): Operation {
if (!(yield* durable())) {
return;
}
@@ -193,7 +190,7 @@ export function* recordGridLayout(
const held = readLayout(stored);
if (held === undefined) {
throw new StaleInputError(
- `The journal's record of "${description.name}" is not a terminal-grid layout. Re-run the ` +
+ `The journal's record of "${description.name}" is not a grid layout. Re-run the ` +
"document from the start rather than resuming from this journal.",
{ coroutineId: identity.path, description },
);
@@ -201,7 +198,7 @@ export function* recordGridLayout(
const changed = divergence(held, derived);
if (changed !== undefined) {
throw new StaleInputError(
- `The journal records this terminal grid as a grid with ${changed}. A grid whose layout ` +
+ `The journal records this grid as a grid with ${changed}. A grid whose layout ` +
"changed cannot be replayed onto this run. Re-run the document from the start rather " +
"than resuming from this journal.",
{ coroutineId: identity.path, description },
diff --git a/packages/core/src/terminal/profile.ts b/packages/core/src/grid/profile.ts
similarity index 77%
rename from packages/core/src/terminal/profile.ts
rename to packages/core/src/grid/profile.ts
index 7b687d981..106481b8a 100644
--- a/packages/core/src/terminal/profile.ts
+++ b/packages/core/src/grid/profile.ts
@@ -1,5 +1,5 @@
/**
- * Opening one terminal installation for a live document.
+ * Opening one grid installation for a live document.
*
* A grid needs two things before it can be durable at all: this execution's
* installation — which owns the generation every request belongs to and the
@@ -15,9 +15,9 @@ import { scoped } from "effection";
import type { Operation } from "effection";
import { Execution } from "../execute.ts";
-import { installTerminalProvider, useTerminalInstallation } from "@executablemd/terminal/lifecycle";
+import { installGridProvider, useGridInstallation } from "@executablemd/grid/lifecycle";
-export interface TerminalGridProfileOptions {
+export interface GridProfileOptions {
/**
* The registered provider to install for this execution.
*
@@ -31,22 +31,20 @@ export interface TerminalGridProfileOptions {
}
/**
- * Install the terminal-grid profile for the executions composed under it.
+ * Install the grid profile for the executions composed under it.
*
* The authority reaches the named provider's factory and nothing else: it is
* delivered through the installation handshake rather than published, so a
* handler that answers the install request itself installs no provider and the
* document is told so.
*/
-export function installTerminalGridProfile(
- options: TerminalGridProfileOptions = {},
-): Operation {
+export function installGridProfile(options: GridProfileOptions = {}): Operation {
return Execution.around({
*document([request], next) {
yield* scoped(function* () {
- const authority = yield* useTerminalInstallation();
+ const authority = yield* useGridInstallation();
if (options.provider !== undefined) {
- yield* installTerminalProvider(
+ yield* installGridProvider(
options.provider,
{ label: options.label ?? options.provider },
authority,
diff --git a/packages/core/src/structural-rules.ts b/packages/core/src/structural-rules.ts
index 24aa676bb..4a7895ad9 100644
--- a/packages/core/src/structural-rules.ts
+++ b/packages/core/src/structural-rules.ts
@@ -1038,18 +1038,18 @@ export function answerViolations(segment: ComponentElement): StructuralViolation
const TERMINAL_GRID_PROPS = new Set(["columns"]);
const TERMINAL_PROPS = new Set(["title"]);
-/** What a `` written outside the grid that lays it out says. */
-export function strayTerminalMessage(): string {
+/** What a `` written outside the grid that lays it out says. */
+export function strayPaneMessage(): string {
return (
- " must be a direct child of . is reserved: it never " +
+ " must be a direct child of . is reserved: it never " +
"resolves a component, and only the grid it belongs to can place it."
);
}
-/** What a `` written inside another grid says. */
-export function nestedTerminalGridMessage(): string {
+/** What a `` written inside another grid says. */
+export function nestedGridMessage(): string {
return (
- " cannot be written inside another . A grid lays out the " +
+ " cannot be written inside another . A grid lays out the " +
"panes it is written with, so one pane cannot become a grid of its own."
);
}
@@ -1061,18 +1061,16 @@ export function nestedTerminalGridMessage(): string {
* document is only being read, and an expression's answer is checked here too
* once expansion has evaluated it.
*/
-export function terminalColumns(columns: Json): Result {
+export function gridColumns(columns: Json): Result {
if (typeof columns !== "number") {
return Err(
- new Error(
- `Prop "columns" on must be a positive integer, not ${jsonKind(columns)}.`,
- ),
+ new Error(`Prop "columns" on must be a positive integer, not ${jsonKind(columns)}.`),
);
}
if (!Number.isInteger(columns) || columns < 1) {
return Err(
new Error(
- `Prop "columns" on must be a positive integer. Got: ` +
+ `Prop "columns" on must be a positive integer. Got: ` +
`${JSON.stringify(columns)}.`,
),
);
@@ -1080,31 +1078,31 @@ export function terminalColumns(columns: Json): Result {
return Ok(columns);
}
-/** What a `` naming no column count at all says. */
-export function terminalColumnsMissingMessage(): string {
- return ' requires a "columns" prop (a positive integer).';
+/** What a `` naming no column count at all says. */
+export function gridColumnsMissingMessage(): string {
+ return ' requires a "columns" prop (a positive integer).';
}
/** The label one pane displays, or why `title` rejects it. */
-export function terminalTitle(title: Json): Result {
+export function paneTitle(title: Json): Result {
if (typeof title !== "string") {
return Err(
- new Error(`Prop "title" on must be a non-empty string, not ${jsonKind(title)}.`),
+ new Error(`Prop "title" on must be a non-empty string, not ${jsonKind(title)}.`),
);
}
if (title.length === 0) {
- return Err(new Error('Prop "title" on must be a non-empty string. Got: "".'));
+ return Err(new Error('Prop "title" on must be a non-empty string. Got: "".'));
}
return Ok(title);
}
-/** What a `` naming no title at all says. */
-export function terminalTitleMissingMessage(): string {
- return ' requires a "title" prop (the label the pane displays).';
+/** What a `` naming no title at all says. */
+export function paneTitleMissingMessage(): string {
+ return ' requires a "title" prop (the label the pane displays).';
}
/** One pane a grid lays out, and where it sat among its siblings. */
-export interface TerminalPane {
+export interface Pane {
readonly element: ComponentElement;
/** The child index the pane was written at. */
readonly index: number;
@@ -1117,52 +1115,50 @@ export interface TerminalPane {
readonly form: "paired" | "self-closing";
}
-/** How a `` body divides into panes, and what the division got wrong. */
-export interface TerminalGridStructure {
+/** How a `` body divides into panes, and what the division got wrong. */
+export interface GridStructure {
readonly violations: StructuralViolation[];
/** The direct panes, in authored order. */
- readonly panes: TerminalPane[];
+ readonly panes: Pane[];
}
/** Which of a pane's two forms was written: its own markdown, or a shell. */
-function paneForm(segment: ComponentElement): TerminalPane["form"] {
+function paneForm(segment: ComponentElement): Pane["form"] {
return segment.selfClosing ? "self-closing" : "paired";
}
-/** Everything one `` pane decides from what the author wrote (spec §6.21). */
-function terminalPaneViolations(segment: ComponentElement): StructuralViolation[] {
+/** Everything one `` pane decides from what the author wrote (spec §6.21). */
+function paneViolations(segment: ComponentElement): StructuralViolation[] {
const found: StructuralViolation[] = [];
const unknownProp = authoredPropNames(segment).find((name) => !TERMINAL_PROPS.has(name));
if (unknownProp !== undefined) {
found.push(
violation(
"structural-usage-invalid",
- "Terminal",
- ` only accepts a "title" prop. Got: "${unknownProp}".`,
+ "Pane",
+ ` only accepts a "title" prop. Got: "${unknownProp}".`,
segment,
),
);
}
if ("title" in segment.props) {
- const title = terminalTitle(segment.props.title);
+ const title = paneTitle(segment.props.title);
if (!title.ok) {
- found.push(violation("structural-usage-invalid", "Terminal", title.error.message, segment));
+ found.push(violation("structural-usage-invalid", "Pane", title.error.message, segment));
}
} else if (!("title" in segment.expressions)) {
- found.push(
- violation("structural-usage-invalid", "Terminal", terminalTitleMissingMessage(), segment),
- );
+ found.push(violation("structural-usage-invalid", "Pane", paneTitleMissingMessage(), segment));
}
return found;
}
/**
- * Every `` and `` below a grid that the grid does not
+ * Every `` and `` below a grid that the grid does not
* lay out. The walk stops at a nested grid, which is reported where it sits and
* owns whatever is written beneath it.
*/
-function misplacedTerminalViolations(children: Segment[]): StructuralViolation[] {
+function misplacedPaneViolations(children: Segment[]): StructuralViolation[] {
const found: StructuralViolation[] = [];
const walk = (segments: Segment[], depth: number): void => {
@@ -1170,23 +1166,14 @@ function misplacedTerminalViolations(children: Segment[]): StructuralViolation[]
if (segment.type !== "component") {
continue;
}
- if (segment.name === "Terminal.Grid") {
+ if (segment.name === "Grid") {
if (depth > 0) {
- found.push(
- violation(
- "structural-usage-invalid",
- "Terminal.Grid",
- nestedTerminalGridMessage(),
- segment,
- ),
- );
+ found.push(violation("structural-usage-invalid", "Grid", nestedGridMessage(), segment));
}
continue;
}
- if (segment.name === "Terminal" && depth > 0) {
- found.push(
- violation("structural-usage-invalid", "Terminal", strayTerminalMessage(), segment),
- );
+ if (segment.name === "Pane" && depth > 0) {
+ found.push(violation("structural-usage-invalid", "Pane", strayPaneMessage(), segment));
}
walk(segment.children, depth + 1);
}
@@ -1197,48 +1184,44 @@ function misplacedTerminalViolations(children: Segment[]): StructuralViolation[]
}
/**
- * Divide a `` body into its panes and validate the division
+ * Divide a `` body into its panes and validate the division
* (spec §6.21). Everything here is read from source, so a grid whose layout the
* author got wrong is refused before `columns` is evaluated, before a pane's
- * content expands, and before any terminal provider is asked for anything.
+ * content expands, and before any grid provider is asked for anything.
*
* The panes are the grid's direct children and only they: a control structure
* that would produce panes as it ran cannot be one, because which panes exist
* is what the grid must know before it opens anything.
*/
-export function terminalGridStructure(segment: ComponentElement): TerminalGridStructure {
+export function gridStructure(segment: ComponentElement): GridStructure {
const violations: StructuralViolation[] = [];
- const panes: TerminalPane[] = [];
+ const panes: Pane[] = [];
const unknownProp = authoredPropNames(segment).find((name) => !TERMINAL_GRID_PROPS.has(name));
if (unknownProp !== undefined) {
violations.push(
violation(
"structural-usage-invalid",
- "Terminal.Grid",
- ` only accepts a "columns" prop. Got: "${unknownProp}".`,
+ "Grid",
+ ` only accepts a "columns" prop. Got: "${unknownProp}".`,
),
);
}
if ("columns" in segment.props) {
- const columns = terminalColumns(segment.props.columns);
+ const columns = gridColumns(segment.props.columns);
if (!columns.ok) {
- violations.push(
- violation("structural-usage-invalid", "Terminal.Grid", columns.error.message),
- );
+ violations.push(violation("structural-usage-invalid", "Grid", columns.error.message));
}
} else if (!("columns" in segment.expressions)) {
- violations.push(
- violation("structural-usage-invalid", "Terminal.Grid", terminalColumnsMissingMessage()),
- );
+ violations.push(violation("structural-usage-invalid", "Grid", gridColumnsMissingMessage()));
}
if (segment.selfClosing) {
violations.push(
violation(
"structural-usage-invalid",
- "Terminal.Grid",
- " holds the panes it lays out, so it is written paired: " +
- '.',
+ "Grid",
+ " holds the panes it lays out, so it is written paired: " +
+ '.',
),
);
}
@@ -1249,32 +1232,28 @@ export function terminalGridStructure(segment: ComponentElement): TerminalGridSt
continue;
}
substantive++;
- if (child.type !== "component" || child.name !== "Terminal") {
+ if (child.type !== "component" || child.name !== "Pane") {
violations.push(
violation(
"structural-usage-invalid",
- "Terminal.Grid",
- ` holds only panes. Found ${describeSegment(child)} ` +
+ "Grid",
+ ` holds only panes. Found ${describeSegment(child)} ` +
"directly inside it. Write control flow inside a pane instead.",
child.type === "component" ? child : undefined,
),
);
continue;
}
- violations.push(...terminalPaneViolations(child));
+ violations.push(...paneViolations(child));
panes.push({ element: child, index, ordinal: panes.length, form: paneForm(child) });
}
if (!segment.selfClosing && substantive === 0) {
violations.push(
- violation(
- "structural-usage-invalid",
- "Terminal.Grid",
- " requires at least one pane.",
- ),
+ violation("structural-usage-invalid", "Grid", " requires at least one pane."),
);
}
- violations.push(...misplacedTerminalViolations(segment.children));
+ violations.push(...misplacedPaneViolations(segment.children));
return { violations, panes };
}
diff --git a/packages/core/src/structural.ts b/packages/core/src/structural.ts
index 4823aca3d..7a7e43989 100644
--- a/packages/core/src/structural.ts
+++ b/packages/core/src/structural.ts
@@ -170,21 +170,21 @@ export const STRUCTURAL_DECLARATIONS: readonly StructuralDeclaration[] = [
context: "A multiline template, in place of the single-line `template` prop.",
},
{
- name: "Terminal.Grid",
- syntax: ["…"],
+ name: "Grid",
+ syntax: ["…"],
description:
- "Open several terminals in one view. " +
- '`…`',
+ "Open several panes in one view. " +
+ '`…`',
as: null,
- context: "The `` panes the grid lays out.",
+ context: "The `` panes the grid lays out.",
},
{
- name: "Terminal",
- syntax: ['…', ''],
+ name: "Pane",
+ syntax: ['…', ''],
description:
"Expand Markdown or open a shell in a pane. " +
- '`…` runs content; ' +
- '`` opens a shell.',
+ '`…` runs content; ' +
+ '`` opens a shell.',
as: null,
context: "Markdown the pane runs, in the paired form.",
},
diff --git a/packages/core/tests/agent-session-launch.test.ts b/packages/core/tests/agent-session-launch.test.ts
index 8ce019cd5..5dcc38a97 100644
--- a/packages/core/tests/agent-session-launch.test.ts
+++ b/packages/core/tests/agent-session-launch.test.ts
@@ -38,18 +38,18 @@ import {
NATIVE_LAUNCHER_UNAVAILABLE,
nativeLaunch,
reserveTerminal,
- TerminalGrids,
-} from "@executablemd/terminal";
+ Grids,
+} from "@executablemd/grid";
import {
installControlledLauncher,
prepareControlledComposite,
- terminalProviderLog,
-} from "@executablemd/terminal/test";
-import type { NativeLaunchOutcome, NativeLaunchRequest } from "@executablemd/terminal";
-import { createTerminalGridClaims } from "@executablemd/terminal/lifecycle";
-import { usePaneNativeLauncher } from "@executablemd/terminal";
-import { installTerminalGridProfile } from "../src/terminal/profile.ts";
-import { registerTerminalProvider } from "@executablemd/terminal";
+ gridProviderLog,
+} from "@executablemd/grid/test";
+import type { NativeLaunchOutcome, NativeLaunchRequest } from "@executablemd/grid";
+import { createGridClaims } from "@executablemd/grid/lifecycle";
+import { usePaneNativeLauncher } from "@executablemd/grid";
+import { installGridProfile } from "../src/grid/profile.ts";
+import { registerGridProvider } from "@executablemd/grid";
import type { Json } from "../src/types.ts";
const ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
@@ -226,7 +226,7 @@ interface RunOptions {
) => Operation;
secretDetection?: boolean;
/**
- * Install a controlled terminal provider, so the document can open a grid.
+ * Install a controlled grid provider, so the document can open a grid.
*
* The reader stays until every pane has settled, so a row about what a pane
* launched is not racing the close that would cancel it.
@@ -306,7 +306,7 @@ function* runDoc(doc: string, options: RunOptions = {}): Operation {
});
}
- const providerLog = terminalProviderLog();
+ const providerLog = gridProviderLog();
if (options.grid === true) {
// The reader leaves once every pane has settled. Leaving sooner is a real
// thing a reader does — TG12 owns that — but a row about what a pane
@@ -314,8 +314,8 @@ function* runDoc(doc: string, options: RunOptions = {}): Operation {
const settled = withResolvers();
let panes = 0;
let done = 0;
- yield* registerTerminalProvider("controlled", function* (_settings, authority) {
- yield* TerminalGrids.around(
+ yield* registerGridProvider("controlled", function* (_settings, authority) {
+ yield* Grids.around(
{
*open([request]) {
const composite = yield* prepareControlledComposite(request, {
@@ -360,7 +360,7 @@ function* runDoc(doc: string, options: RunOptions = {}): Operation {
{ at: "min" },
);
});
- yield* installTerminalGridProfile({ provider: "controlled" });
+ yield* installGridProfile({ provider: "controlled" });
}
yield* installAgentComponents({
@@ -853,7 +853,7 @@ describe("Tier SL — native session launch", () => {
/**
* Tier SP — `` inside a terminal pane
- * (specs/native-agent-session-launch-spec.md §Terminal-grid composition).
+ * (specs/native-agent-session-launch-spec.md §Grid composition).
*
* The launch is the same launch. Nothing here passes a pane to it, and its
* request, result and retained phases are the ones a root launch would have.
@@ -865,14 +865,14 @@ describe("Tier SL — native session launch", () => {
describe("Tier SP — a launch inside a terminal pane", () => {
/** Two panes, each launching a session of its own. */
const PANES = [
- "",
- '',
+ "",
+ '',
'left work',
- "",
- '',
+ "",
+ '',
'right work',
- "",
- "",
+ "",
+ "",
"",
].join("\n");
@@ -1009,7 +1009,7 @@ describe("Tier SP — a launch inside a terminal pane", () => {
}
it("SP5: a pane is held until both the child and the lease around it are done", function* () {
- const claims = createTerminalGridClaims({
+ const claims = createGridClaims({
columns: 1,
rows: 1,
panes: [{ ordinal: 0, title: "Only", row: 0, column: 0, form: "paired" }],
diff --git a/packages/core/tests/document-validation.test.ts b/packages/core/tests/document-validation.test.ts
index c37f8c92b..3c355f8dc 100644
--- a/packages/core/tests/document-validation.test.ts
+++ b/packages/core/tests/document-validation.test.ts
@@ -681,14 +681,14 @@ describe("Tier DV: branch selection", () => {
});
});
-describe("Tier DV: terminal grids", () => {
+describe("Tier DV: grids", () => {
const GRID_DOC = [
- "",
- '',
+ "",
+ '',
'',
- "",
- '',
- "",
+ "",
+ '',
+ "",
"",
].join("\n");
@@ -699,101 +699,96 @@ describe("Tier DV: terminal grids", () => {
expect(result.outcome).toBe("valid");
expect(result.diagnostics).toEqual([]);
- expect(names(result)).toEqual(["Terminal.Grid", "Terminal", "Widget", "Terminal"]);
- expect(named(result, "Terminal.Grid").origin).toEqual({
+ expect(names(result)).toEqual(["Grid", "Pane", "Widget", "Pane"]);
+ expect(named(result, "Grid").origin).toEqual({
kind: "structural",
- construct: "Terminal.Grid",
+ construct: "Grid",
});
- expect(named(result, "Terminal").origin).toEqual({
+ expect(named(result, "Pane").origin).toEqual({
kind: "structural",
- construct: "Terminal",
+ construct: "Pane",
});
// A pane's body is walked like any other region, and none of it — no
// shell, no command, no agent, no terminal — was reached to walk it.
expect(seen.effects).toEqual([]);
// Reserved means selection never looked for a file that could supply
// either construct.
- expect(seen.reads.some((read) => read.includes("Terminal"))).toBe(false);
+ expect(seen.reads.some((read) => read.includes("Pane"))).toBe(false);
});
it("TG3: reports each invalid authored form, with no execution", function* () {
const invalid: [string, string, string][] = [
[
"an unknown prop on the grid",
- '\n',
- ' only accepts a "columns" prop. Got: "layout".',
+ '\n',
+ ' only accepts a "columns" prop. Got: "layout".',
],
[
"a capture on the grid",
- '\n',
- ' only accepts a "columns" prop. Got: "as".',
+ '\n',
+ ' only accepts a "columns" prop. Got: "as".',
],
[
"no column count",
- '\n',
- ' requires a "columns" prop (a positive integer).',
+ '\n',
+ ' requires a "columns" prop (a positive integer).',
],
[
"a column count that is not a positive integer",
- '\n',
- 'Prop "columns" on must be a positive integer. Got: 0.',
+ '\n',
+ 'Prop "columns" on must be a positive integer. Got: 0.',
],
[
"an unknown prop on a pane",
- '\n',
- ' only accepts a "title" prop. Got: "shell".',
+ '\n',
+ ' only accepts a "title" prop. Got: "shell".',
],
[
"no title on a pane",
- "\n",
- ' requires a "title" prop (the label the pane displays).',
+ "\n",
+ ' requires a "title" prop (the label the pane displays).',
],
[
"an empty title",
- '\n',
- 'Prop "title" on must be a non-empty string. Got: "".',
- ],
- [
- "a self-closing grid",
- "\n",
- " holds the panes it lays out",
+ '\n',
+ 'Prop "title" on must be a non-empty string. Got: "".',
],
+ ["a self-closing grid", "\n", " holds the panes it lays out"],
[
"a grid with no pane",
- "\n",
- " requires at least one pane.",
+ "\n",
+ " requires at least one pane.",
],
[
"text written directly in a grid",
- 'a note\n',
- ' holds only panes. Found text "a note" directly inside it.',
+ 'a note\n',
+ ' holds only panes. Found text "a note" directly inside it.',
],
[
"a direct element that is not a pane",
- '\n',
- " holds only panes. Found directly inside it.",
+ '\n',
+ " holds only panes. Found directly inside it.",
],
[
"a pane produced by control flow",
- '\n',
- " holds only panes. Found directly inside it.",
+ '\n',
+ " holds only panes. Found directly inside it.",
],
[
"a nested grid",
- '' +
- '\n',
- " cannot be written inside another .",
+ '' +
+ '\n',
+ " cannot be written inside another .",
],
[
"a pane outside every grid",
- 'alone\n',
- " must be a direct child of .",
+ 'alone\n',
+ " must be a direct child of .",
],
[
"a pane below a grid that is not one of its panes",
- '' +
- "\n",
- " must be a direct child of .",
+ '' + "\n",
+ " must be a direct child of .",
],
];
@@ -813,17 +808,15 @@ describe("Tier DV: terminal grids", () => {
});
it("TG3: answers the same way twice", function* () {
- const first = yield* validateText("\n");
- const second = yield* validateText("\n");
+ const first = yield* validateText("\n");
+ const second = yield* validateText("\n");
expect(JSON.stringify(second.result)).toBe(JSON.stringify(first.result));
});
it("TG3: a dynamic column count and title are decided by expansion, not here", function* () {
const { result, seen } = yield* validateText(
- ["", "", "", ""].join(
- "\n",
- ),
+ ["", "", "", ""].join("\n"),
);
// Whether those expressions produce a positive integer and a non-empty
diff --git a/packages/core/tests/terminal-grid-structure.test.ts b/packages/core/tests/grid-structure.test.ts
similarity index 65%
rename from packages/core/tests/terminal-grid-structure.test.ts
rename to packages/core/tests/grid-structure.test.ts
index b2fc9b3b2..0b7f564dc 100644
--- a/packages/core/tests/terminal-grid-structure.test.ts
+++ b/packages/core/tests/grid-structure.test.ts
@@ -1,9 +1,9 @@
/**
- * Tier TG — the authored structure of a terminal grid (spec §6.21).
+ * Tier TG — the authored structure of a grid (spec §6.21).
*
* What an author may write, and where each pane lands, decided before anything
* opens. These rows drive the real expansion path: a grid the grammar accepts
- * runs until the point a terminal provider would be asked for one, and this
+ * runs until the point a grid provider would be asked for one, and this
* build installs none, so it refuses there and carries the layout it derived
* beside the refusal.
*
@@ -23,7 +23,7 @@ import { Component } from "../src/component-api.ts";
import { expandSegments } from "../src/expand.ts";
import { renderSegments } from "../src/render.ts";
import { scanSegments } from "../src/scanner.ts";
-import { terminalGridLayout } from "@executablemd/terminal/lifecycle";
+import { gridLayout } from "@executablemd/grid/lifecycle";
import type { Json, Segment } from "../src/types.ts";
interface GridRun {
@@ -98,10 +98,10 @@ function soleError(run: GridRun): string {
*/
function derivedLayout(run: GridRun): Json {
const refusal = run.segments.find(
- (segment) => segment.type === "error" && segment.source === "Terminal.Grid",
+ (segment) => segment.type === "error" && segment.source === "Grid",
);
if (refusal === undefined || refusal.type !== "error" || refusal.cause === undefined) {
- throw new Error(`no terminal-grid refusal carrying a layout: ${errorMessages(run.segments)}`);
+ throw new Error(`no grid refusal carrying a layout: ${errorMessages(run.segments)}`);
}
return refusal.cause;
}
@@ -131,117 +131,91 @@ const PANE_BODY = [
describe("Tier TG — the grid grammar", () => {
it("TG1: refuses an unknown prop and `as` on the grid", function* () {
- const unknown = yield* runGrid(
- '',
- );
- expect(soleError(unknown)).toContain(
- ' only accepts a "columns" prop. Got: "layout".',
- );
+ const unknown = yield* runGrid('');
+ expect(soleError(unknown)).toContain(' only accepts a "columns" prop. Got: "layout".');
- const captured = yield* runGrid(
- '',
- );
- expect(soleError(captured)).toContain(
- ' only accepts a "columns" prop. Got: "as".',
- );
+ const captured = yield* runGrid('');
+ expect(soleError(captured)).toContain(' only accepts a "columns" prop. Got: "as".');
reachedNothing(unknown);
reachedNothing(captured);
});
it("TG1: refuses an unknown prop and `as` on a pane", function* () {
- const unknown = yield* runGrid(
- '',
- );
- expect(soleError(unknown)).toContain(' only accepts a "title" prop. Got: "shell".');
+ const unknown = yield* runGrid('');
+ expect(soleError(unknown)).toContain(' only accepts a "title" prop. Got: "shell".');
- const captured = yield* runGrid(
- '',
- );
- expect(soleError(captured)).toContain(' only accepts a "title" prop. Got: "as".');
+ const captured = yield* runGrid('');
+ expect(soleError(captured)).toContain(' only accepts a "title" prop. Got: "as".');
reachedNothing(unknown);
reachedNothing(captured);
});
it("TG1: requires columns to be a positive integer, however it was written", function* () {
- const missing = yield* runGrid('');
- expect(soleError(missing)).toContain(
- ' requires a "columns" prop (a positive integer).',
- );
+ const missing = yield* runGrid('');
+ expect(soleError(missing)).toContain(' requires a "columns" prop (a positive integer).');
for (const literal of ["{0}", "{-1}", "{2.5}", '"2"', "{null}"]) {
- const run = yield* runGrid(
- ``,
- );
- expect(soleError(run)).toContain('Prop "columns" on must be a positive');
+ const run = yield* runGrid(``);
+ expect(soleError(run)).toContain('Prop "columns" on must be a positive');
reachedNothing(run);
}
// The same rule reaches a value the document computes, which the source
// could not have decided about.
- const computed = yield* runGrid(
- '',
- { size: 0 },
- );
+ const computed = yield* runGrid('', { size: 0 });
expect(soleError(computed)).toContain(
- 'Prop "columns" on must be a positive integer. Got: 0.',
+ 'Prop "columns" on must be a positive integer. Got: 0.',
);
reachedNothing(computed);
});
it("TG1: requires a non-empty title on every pane, however it was written", function* () {
- const missing = yield* runGrid("");
+ const missing = yield* runGrid("");
expect(soleError(missing)).toContain(
- ' requires a "title" prop (the label the pane displays).',
+ ' requires a "title" prop (the label the pane displays).',
);
for (const literal of ['""', "{3}", "{null}"]) {
- const run = yield* runGrid(
- ``,
- );
- expect(soleError(run)).toContain('Prop "title" on must be a non-empty string');
+ const run = yield* runGrid(``);
+ expect(soleError(run)).toContain('Prop "title" on must be a non-empty string');
reachedNothing(run);
}
- const computed = yield* runGrid(
- "",
- { label: "" },
- );
+ const computed = yield* runGrid("", {
+ label: "",
+ });
expect(soleError(computed)).toContain(
- 'Prop "title" on must be a non-empty string. Got: "".',
+ 'Prop "title" on must be a non-empty string. Got: "".',
);
reachedNothing(computed);
});
it("TG1: refuses a self-closing grid", function* () {
- const run = yield* runGrid("");
- expect(soleError(run)).toContain(" holds the panes it lays out");
+ const run = yield* runGrid("");
+ expect(soleError(run)).toContain(" holds the panes it lays out");
reachedNothing(run);
});
});
describe("Tier TG — structural placement", () => {
it("TG2: refuses a grid with no pane", function* () {
- const run = yield* runGrid("");
- expect(soleError(run)).toContain(" requires at least one pane.");
+ const run = yield* runGrid("");
+ expect(soleError(run)).toContain(" requires at least one pane.");
reachedNothing(run);
});
it("TG2: refuses ordinary text written directly in a grid", function* () {
- const run = yield* runGrid(
- 'a note',
- );
+ const run = yield* runGrid('a note');
expect(soleError(run)).toContain(
- ' holds only panes. Found text "a note" directly inside it.',
+ ' holds only panes. Found text "a note" directly inside it.',
);
reachedNothing(run);
});
it("TG2: refuses a direct element that is not a pane", function* () {
- const run = yield* runGrid(
- '',
- );
+ const run = yield* runGrid('');
expect(soleError(run)).toContain(
- " holds only panes. Found directly inside it.",
+ " holds only panes. Found directly inside it.",
);
// The element was refused as authored structure, so it was never resolved.
reachedNothing(run);
@@ -250,21 +224,19 @@ describe("Tier TG — structural placement", () => {
it("TG2: refuses a control structure that would produce the panes", function* () {
const run = yield* runGrid(
[
- "",
+ "",
'',
- '',
+ '',
"",
- "",
+ "",
].join("\n"),
);
const messages = errorMessages(run.segments);
expect(messages).toHaveLength(2);
- expect(messages[0]).toContain(
- " holds only panes. Found directly inside it.",
- );
+ expect(messages[0]).toContain(" holds only panes. Found directly inside it.");
expect(messages[0]).toContain("Write control flow inside a pane instead.");
- expect(messages[1]).toContain(" must be a direct child of .");
+ expect(messages[1]).toContain(" must be a direct child of .");
// The condition decides which panes would exist, and the grid must know
// that from the source, so it is never evaluated.
reachedNothing(run);
@@ -273,35 +245,33 @@ describe("Tier TG — structural placement", () => {
it("TG2: refuses a grid nested inside a pane", function* () {
const run = yield* runGrid(
[
- "",
- '',
- '',
- "",
- "",
+ "",
+ '',
+ '',
+ "",
+ "",
].join("\n"),
);
- expect(soleError(run)).toContain(
- " cannot be written inside another .",
- );
+ expect(soleError(run)).toContain(" cannot be written inside another .");
reachedNothing(run);
});
it("TG2: refuses a pane written outside every grid", function* () {
- const alone = yield* runGrid('Instructions.');
- expect(soleError(alone)).toContain(" must be a direct child of .");
+ const alone = yield* runGrid('Instructions.');
+ expect(soleError(alone)).toContain(" must be a direct child of .");
// Below a grid but not one of its panes is the same mistake, reported where
// the pane was written.
const buried = yield* runGrid(
[
- "",
- '',
- '',
- "",
- "",
+ "",
+ '',
+ '',
+ "",
+ "",
].join("\n"),
);
- expect(soleError(buried)).toContain(" must be a direct child of .");
+ expect(soleError(buried)).toContain(" must be a direct child of .");
reachedNothing(alone);
reachedNothing(buried);
});
@@ -309,7 +279,7 @@ describe("Tier TG — structural placement", () => {
describe("Tier TG — row-major layout", () => {
const positions = (columns: number, panes: number) =>
- terminalGridLayout(
+ gridLayout(
columns,
Array.from({ length: panes }, (_unused, index) => ({
title: `pane ${index}`,
@@ -342,7 +312,7 @@ describe("Tier TG — row-major layout", () => {
[2, 0],
]);
// The last row is left short rather than balanced or padded.
- expect([1, 2, 3, 4, 5].map((panes) => terminalGridLayout(2, filler(panes)).rows)).toEqual([
+ expect([1, 2, 3, 4, 5].map((panes) => gridLayout(2, filler(panes)).rows)).toEqual([
1, 1, 2, 2, 3,
]);
});
@@ -371,7 +341,7 @@ describe("Tier TG — row-major layout", () => {
[1, 0],
[1, 1],
]);
- expect([1, 2, 3, 4, 5].map((panes) => terminalGridLayout(3, filler(panes)).rows)).toEqual([
+ expect([1, 2, 3, 4, 5].map((panes) => gridLayout(3, filler(panes)).rows)).toEqual([
1, 1, 1, 2, 2,
]);
});
diff --git a/packages/core/tests/terminal-grid.test.ts b/packages/core/tests/grid.test.ts
similarity index 89%
rename from packages/core/tests/terminal-grid.test.ts
rename to packages/core/tests/grid.test.ts
index 424138056..1e1bd3779 100644
--- a/packages/core/tests/terminal-grid.test.ts
+++ b/packages/core/tests/grid.test.ts
@@ -1,5 +1,5 @@
/**
- * Tier TG — running a terminal grid through a replaceable provider
+ * Tier TG — running a grid through a replaceable provider
* (spec §6.21, architecture.md §Terminal authority, §Atomic presentation and
* settlement, §Durability and replay).
*
@@ -41,36 +41,32 @@ import { tmpdir } from "node:os";
import { join } from "node:path";
import { InMemoryStream } from "@executablemd/durable-streams";
import type { DurableEvent } from "@executablemd/durable-streams";
-import { reserveTerminal, TerminalGrids } from "@executablemd/terminal";
+import { reserveTerminal, Grids } from "@executablemd/grid";
import {
installControlledLauncher,
prepareControlledComposite,
- terminalProviderLog,
-} from "@executablemd/terminal/test";
-import type { TerminalComposite, TerminalGridRequest } from "@executablemd/terminal";
+ gridProviderLog,
+} from "@executablemd/grid/test";
+import type { GridComposite, GridRequest } from "@executablemd/grid";
import type {
ControlledCompositeOptions,
- TerminalProviderLog,
- TerminalProviderResources,
-} from "@executablemd/terminal/test";
+ GridProviderLog,
+ GridProviderResources,
+} from "@executablemd/grid/test";
import { Component } from "../src/component-api.ts";
import { execute } from "../src/execute.ts";
import { registerComponents } from "../src/components/registration.ts";
import {
- createTerminalGridClaims,
- TerminalAuthorityError,
- useTerminalInstallation,
-} from "@executablemd/terminal/lifecycle";
-import type { TerminalGridAuthority } from "@executablemd/terminal/lifecycle";
-import { installTerminalProvider } from "@executablemd/terminal/lifecycle";
-import {
- registerTerminalProvider,
- TerminalProviderInstallError,
- TerminalProviders,
-} from "@executablemd/terminal";
-import { installTerminalGridProfile } from "../src/terminal/profile.ts";
-import { paneTerminal } from "@executablemd/terminal";
+ createGridClaims,
+ GridAuthorityError,
+ useGridInstallation,
+} from "@executablemd/grid/lifecycle";
+import type { GridAuthority } from "@executablemd/grid/lifecycle";
+import { installGridProvider } from "@executablemd/grid/lifecycle";
+import { registerGridProvider, GridProviderInstallError, GridProviders } from "@executablemd/grid";
+import { installGridProfile } from "../src/grid/profile.ts";
+import { paneTerminal } from "@executablemd/grid";
import type { Json } from "../src/types.ts";
/** One document run against a controlled grid host. */
@@ -79,7 +75,7 @@ interface DocumentRun {
/** Text the consumer received — the root document's own output. */
output: string;
/** The grid the provider was actually asked to present. */
- requests: TerminalGridRequest[];
+ requests: GridRequest[];
/** What each pane displayed. */
shown: Map;
/** Everything the composite did, in order. */
@@ -91,7 +87,7 @@ interface DocumentRun {
/** The journal this run read and appended to. */
journal: DurableEvent[];
/** What the controlled provider still held when the run was over. */
- live: TerminalProviderResources;
+ live: GridProviderResources;
}
/**
@@ -130,7 +126,7 @@ function useGridComponents(
*fn() {
const pane = yield* paneTerminal();
if (pane === undefined) {
- throw new Error(" is written inside a pane");
+ throw new Error(" is written inside a pane");
}
yield* pane.interactive(function* (spawned) {
spawned();
@@ -162,7 +158,7 @@ function useGridComponents(
*fn() {
const pane = yield* paneTerminal();
if (pane === undefined) {
- throw new Error(" is written inside a pane");
+ throw new Error(" is written inside a pane");
}
yield* pane.interactive(function* (spawned) {
yield* sleep(25);
@@ -224,17 +220,17 @@ function useGridComponents(
function useControlledProvider(
options: ControlledCompositeOptions & {
/** Present something other than the request that was routed. */
- readonly substitute?: (request: TerminalGridRequest) => TerminalGridRequest;
+ readonly substitute?: (request: GridRequest) => GridRequest;
/** Answer the routed request without presenting anything at all. */
readonly shortCircuit?: boolean;
/** Keep the authority for a later, unrouted use. */
- readonly capture?: (authority: TerminalGridAuthority) => void;
+ readonly capture?: (authority: GridAuthority) => void;
} = {},
): Operation {
let generation = 0;
- return registerTerminalProvider("controlled", function* (_settings, authority) {
+ return registerGridProvider("controlled", function* (_settings, authority) {
options.capture?.(authority);
- yield* TerminalGrids.around(
+ yield* Grids.around(
{
*open([request]) {
if (options.shortCircuit === true) {
@@ -254,12 +250,12 @@ function useControlledProvider(
/** Everything a controlled grid host installs, for an in-process grid. */
function useGridHost(
options: Parameters[0] = {},
-): Operation {
- return (function* (): Operation {
+): Operation {
+ return (function* (): Operation {
yield* installControlledLauncher();
yield* useControlledProvider(options);
- const authority = yield* useTerminalInstallation();
- yield* installTerminalProvider("controlled", { label: "controlled" }, authority);
+ const authority = yield* useGridInstallation();
+ yield* installGridProvider("controlled", { label: "controlled" }, authority);
return authority;
})();
}
@@ -292,8 +288,8 @@ function runDocument(
return scoped(function* () {
const path = join(dir, "doc.md");
yield* writeTextFile(path, source);
- const requests: TerminalGridRequest[] = [];
- const log = terminalProviderLog();
+ const requests: GridRequest[] = [];
+ const log = gridProviderLog();
const ran: string[] = [];
const errors: string[] = [];
yield* Component.around({
@@ -335,7 +331,7 @@ function runDocument(
},
});
}
- yield* installTerminalGridProfile(options.provider === false ? {} : { provider: "controlled" });
+ yield* installGridProfile(options.provider === false ? {} : { provider: "controlled" });
const stream = options.stream ?? new InMemoryStream();
const execution = yield* execute({
@@ -370,15 +366,15 @@ function failureOf(run: DocumentRun): string {
/** A grid on its own, which a resumed run can carry to an outcome. */
function plainDocument(columns: number, panes: string[]): string {
- return [``, ...panes, "", ""].join("\n");
+ return [``, ...panes, "", ""].join("\n");
}
/** A grid, then a component that holds the run open so the root never settles. */
function heldDocument(columns: number, panes: string[]): string {
return [
- ``,
+ ``,
...panes,
- "",
+ "",
"",
// The sibling after the grid. It runs whether the grid ran or replayed, so
// a harness can wait for the document to have moved past the region.
@@ -430,7 +426,7 @@ function runInterrupted(
* A row reads those counters here to know they ever went up, which is what
* makes reading them again at the end mean something.
*/
- onTeardownEntered?: (live: TerminalProviderResources) => void;
+ onTeardownEntered?: (live: GridProviderResources) => void;
/**
* Called once that finalizer has left.
*
@@ -471,8 +467,8 @@ function runInterrupted(
} = {},
): Operation {
return scoped(function* () {
- const requests: TerminalGridRequest[] = [];
- const log = terminalProviderLog();
+ const requests: GridRequest[] = [];
+ const log = gridProviderLog();
const ran: string[] = [];
const errors: string[] = [];
// Three signals, kept apart because they mean different things. `attached`
@@ -578,7 +574,7 @@ function runInterrupted(
},
});
}
- yield* installTerminalGridProfile(options.provider === false ? {} : { provider: "controlled" });
+ yield* installGridProfile(options.provider === false ? {} : { provider: "controlled" });
const path = join(dir, "doc.md");
yield* writeTextFile(path, source);
@@ -636,13 +632,10 @@ function runInterrupted(
});
}
-const PANES = [
- 'left',
- '',
-];
+const PANES = ['left', ''];
describe("Tier TG — the terminal authority", () => {
- const GRID = ["", ...PANES, "", ""].join("\n");
+ const GRID = ["", ...PANES, "", ""].join("\n");
it("TA1: a handler that answers without presenting opens nothing", function* () {
const dir = yield* useDir();
@@ -657,7 +650,7 @@ describe("Tier TG — the terminal authority", () => {
yield* useGridComponents(ran);
yield* installControlledLauncher();
yield* useControlledProvider({ shortCircuit: true });
- yield* installTerminalGridProfile({ provider: "controlled" });
+ yield* installGridProfile({ provider: "controlled" });
const execution = yield* execute({ path, stream: new InMemoryStream(), includes: [dir] });
const outcome = yield* execution;
yield* forEach(function* (_chunk: string) {}, execution.output);
@@ -692,7 +685,7 @@ describe("Tier TG — the terminal authority", () => {
panes: request.panes.map((pane) => ({ ...pane })),
}),
});
- yield* installTerminalGridProfile({ provider: "controlled" });
+ yield* installGridProfile({ provider: "controlled" });
const execution = yield* execute({ path, stream: new InMemoryStream(), includes: [dir] });
const outcome = yield* execution;
yield* forEach(function* (_chunk: string) {}, execution.output);
@@ -714,7 +707,7 @@ describe("Tier TG — the terminal authority", () => {
yield* useControlledProvider({
substitute: (request) => ({ ...request, columns: request.columns + 1 }),
});
- yield* installTerminalGridProfile({ provider: "controlled" });
+ yield* installGridProfile({ provider: "controlled" });
const execution = yield* execute({ path, stream: new InMemoryStream(), includes: [dir] });
const outcome = yield* execution;
yield* forEach(function* (_chunk: string) {}, execution.output);
@@ -727,7 +720,7 @@ describe("Tier TG — the terminal authority", () => {
it("TA4: an authority kept past its grid authorizes nothing", function* () {
const dir = yield* useDir();
- let kept: TerminalGridAuthority | undefined;
+ let kept: GridAuthority | undefined;
const run = yield* runDocument(dir, GRID, {});
expect(run.outcome.ok).toBe(true);
@@ -737,7 +730,7 @@ describe("Tier TG — the terminal authority", () => {
yield* useGridComponents(ran);
yield* installControlledLauncher();
yield* useControlledProvider({ capture: (authority) => (kept = authority) });
- yield* installTerminalGridProfile({ provider: "controlled" });
+ yield* installGridProfile({ provider: "controlled" });
const execution = yield* execute({ path, stream: new InMemoryStream(), includes: [dir] });
yield* execution;
yield* forEach(function* (_chunk: string) {}, execution.output);
@@ -768,7 +761,7 @@ describe("Tier TG — the terminal authority", () => {
}
});
- expect(refusal).toBeInstanceOf(TerminalAuthorityError);
+ expect(refusal).toBeInstanceOf(GridAuthorityError);
expect(refusal instanceof Error ? refusal.message : "").toContain("is not live");
});
@@ -778,9 +771,9 @@ describe("Tier TG — the terminal authority", () => {
// Two installations in one scope: the second supersedes the first, so the
// first's authority names a generation the live registry no longer has.
const stale = yield* scoped(function* () {
- return yield* useTerminalInstallation();
+ return yield* useGridInstallation();
});
- yield* useTerminalInstallation();
+ yield* useGridInstallation();
const composite = yield* prepareControlledComposite(
{
columns: 1,
@@ -803,36 +796,36 @@ describe("Tier TG — the terminal authority", () => {
}
});
- expect(refusal).toBeInstanceOf(TerminalAuthorityError);
+ expect(refusal).toBeInstanceOf(GridAuthorityError);
expect(refusal instanceof Error ? refusal.message : "").toContain("is not live");
});
it("TA6: a provider that never acknowledges installs nothing", function* () {
let refusal: unknown;
yield* scoped(function* () {
- const authority = yield* useTerminalInstallation();
+ const authority = yield* useGridInstallation();
// A handler that answers the install request without delivering it to a
// registered provider.
- yield* registerTerminalProvider("real", function* () {});
- yield* TerminalProviders.around({
+ yield* registerGridProvider("real", function* () {});
+ yield* GridProviders.around({
// deno-lint-ignore require-yield
*install() {
return undefined;
},
});
try {
- yield* installTerminalProvider("real", { label: "real" }, authority);
+ yield* installGridProvider("real", { label: "real" }, authority);
} catch (error) {
refusal = error;
}
});
- expect(refusal).toBeInstanceOf(TerminalProviderInstallError);
+ expect(refusal).toBeInstanceOf(GridProviderInstallError);
expect(refusal instanceof Error ? refusal.message : "").toContain("did not install");
});
it("TA7: two claims from one grid do not contend; one pane admits one", function* () {
- const grid = createTerminalGridClaims({
+ const grid = createGridClaims({
columns: 2,
rows: 1,
panes: [
@@ -858,7 +851,7 @@ describe("Tier TG — the terminal authority", () => {
});
});
- expect(refusal).toBeInstanceOf(TerminalAuthorityError);
+ expect(refusal).toBeInstanceOf(GridAuthorityError);
expect(refusal instanceof Error ? refusal.message : "").toContain(
"one owns a pane terminal at a time",
);
@@ -871,8 +864,8 @@ describe("Tier TG — the terminal authority", () => {
rows: 1,
panes: [{ ordinal: 0, title: "a", row: 0, column: 0, form: "paired" as const }],
};
- const first = createTerminalGridClaims(request);
- const second = createTerminalGridClaims(request);
+ const first = createGridClaims(request);
+ const second = createGridClaims(request);
// Sealing one grid says nothing about the other: claims belong to the grid
// that minted them, not to a request shape.
first.seal();
@@ -895,7 +888,7 @@ describe("Tier TG — the terminal authority", () => {
});
it("TA9: readiness is the acknowledgement, and acknowledging twice is one event", function* () {
- const grid = createTerminalGridClaims({
+ const grid = createGridClaims({
columns: 1,
rows: 1,
panes: [{ ordinal: 0, title: "a", row: 0, column: 0, form: "paired" }],
@@ -917,7 +910,7 @@ describe("Tier TG — the terminal authority", () => {
it("TA10: a request whose ordinals are not its positions is refused", function* () {
let refusal: unknown;
try {
- createTerminalGridClaims({
+ createGridClaims({
columns: 2,
rows: 1,
panes: [
@@ -928,7 +921,7 @@ describe("Tier TG — the terminal authority", () => {
} catch (error) {
refusal = error;
}
- expect(refusal).toBeInstanceOf(TerminalAuthorityError);
+ expect(refusal).toBeInstanceOf(GridAuthorityError);
yield* sleep(0);
});
});
@@ -939,13 +932,13 @@ describe("Tier TG — a grid written in a document", () => {
const run = yield* runDocument(
dir,
[
- "",
- '',
- '',
- '',
- '',
- '',
- "",
+ "",
+ '',
+ '',
+ '',
+ '',
+ '',
+ "",
"",
].join("\n"),
);
@@ -970,11 +963,11 @@ describe("Tier TG — a grid written in a document", () => {
const run = yield* runDocument(
dir,
[
- "",
- 'first',
- '',
- 'third',
- "",
+ "",
+ 'first',
+ '',
+ 'third',
+ "",
"",
].join("\n"),
);
@@ -995,10 +988,10 @@ describe("Tier TG — a grid written in a document", () => {
[
"before",
"",
- "",
- 'left text',
- 'right text',
- "",
+ "",
+ 'left text',
+ 'right text',
+ "",
"",
"after",
"",
@@ -1035,8 +1028,8 @@ describe("Tier TG — a grid written in a document", () => {
[
'',
"",
- "",
- '',
+ "",
+ '',
"sees {shared}",
"",
'',
@@ -1044,13 +1037,13 @@ describe("Tier TG — a grid written in a document", () => {
"then {mine}",
"",
"",
- "",
- '',
+ "",
+ '',
"sees {shared} and {mine}",
"",
"",
- "",
- "",
+ "",
+ "",
"",
"after {mine}",
"",
@@ -1078,12 +1071,12 @@ describe("Tier TG — a grid written in a document", () => {
"returns:",
" type: string",
"---",
- "",
- '',
+ "",
+ '',
'',
"",
- "",
- "",
+ "",
+ "",
"",
'',
"",
@@ -1103,18 +1096,18 @@ describe("Tier TG — a grid written in a document", () => {
const run = yield* runDocument(
dir,
[
- "",
- '',
+ "",
+ '',
"",
'',
"",
"",
- "",
- '',
+ "",
+ '',
'',
"",
- "",
- "",
+ "",
+ "",
"",
].join("\n"),
);
@@ -1134,8 +1127,8 @@ describe("Tier TG — a grid written in a document", () => {
const run = yield* runInterrupted(
dir,
heldDocument(2, [
- '',
- '',
+ '',
+ '',
]),
stream,
{ close: true, closeWhenMarked: "second component" },
@@ -1149,19 +1142,19 @@ describe("Tier TG — a grid written in a document", () => {
const run = yield* runDocument(
dir,
[
- "",
- '',
+ "",
+ '',
'',
"",
- "",
- '',
- "",
+ "",
+ '',
+ "",
"",
].join("\n"),
{ provider: false },
);
- expect(failureOf(run)).toContain("no terminal provider is installed");
+ expect(failureOf(run)).toContain("no grid provider is installed");
// The pane held work; none of it was reached, and nothing was displayed.
expect(run.ran).toEqual([]);
expect(run.shown.size).toBe(0);
@@ -1169,7 +1162,7 @@ describe("Tier TG — a grid written in a document", () => {
});
describe("Tier TG — startup, settlement and teardown", () => {
- const TWO = ["", ...PANES, "", ""].join("\n");
+ const TWO = ["", ...PANES, "", ""].join("\n");
it("TG9: nothing attaches until every pane has reported a spawn", function* () {
const dir = yield* useDir();
@@ -1181,10 +1174,10 @@ describe("Tier TG — startup, settlement and teardown", () => {
const run = yield* runDocument(
dir,
[
- "",
- '',
- '',
- "",
+ "",
+ '',
+ '',
+ "",
"",
].join("\n"),
{
@@ -1215,10 +1208,10 @@ describe("Tier TG — startup, settlement and teardown", () => {
const run = yield* runDocument(
dir,
[
- "",
- 'nothing interactive here',
- '',
- "",
+ "",
+ 'nothing interactive here',
+ '',
+ "",
"",
].join("\n"),
);
@@ -1233,9 +1226,7 @@ describe("Tier TG — startup, settlement and teardown", () => {
const dir = yield* useDir();
const run = yield* runDocument(
dir,
- ["", '', "", ""].join(
- "\n",
- ),
+ ["", '', "", ""].join("\n"),
{
composite: {
// Reports its spawn and returns in the same breath.
@@ -1294,10 +1285,10 @@ describe("Tier TG — startup, settlement and teardown", () => {
const run = yield* runDocument(
dir,
[
- "",
- 'no interactive child',
- 'no interactive child either',
- "",
+ "",
+ 'no interactive child',
+ 'no interactive child either',
+ "",
"",
].join("\n"),
);
@@ -1313,9 +1304,9 @@ describe("Tier TG — startup, settlement and teardown", () => {
const run = yield* runDocument(
dir,
[
- "",
- '',
- "",
+ "",
+ '',
+ "",
"",
'',
"",
@@ -1344,12 +1335,12 @@ describe("Tier TG — startup, settlement and teardown", () => {
// The reader's close operation is where an active provider can fail.
// deno-lint-ignore require-yield
*close() {
- throw new Error("the terminal provider lost its server");
+ throw new Error("the grid provider lost its server");
},
},
});
- expect(failureOf(run)).toContain("the terminal provider lost its server");
+ expect(failureOf(run)).toContain("the grid provider lost its server");
expect(run.events).toContain("destroy:0");
});
});
@@ -1365,10 +1356,10 @@ describe("Tier TG — durability and replay", () => {
*/
const CONTAINED_FAILURE = [
"",
- "",
- '',
- '',
- "",
+ "",
+ '',
+ '',
+ "",
"",
"",
``,
@@ -1524,8 +1515,8 @@ describe("Tier TG — durability and replay", () => {
const dir = yield* useDir();
const stream = new InMemoryStream();
const source = heldDocument(2, [
- '',
- '',
+ '',
+ '',
]);
const holdingShell: ControlledCompositeOptions["shell"] = function* (_ordinal, spawned) {
spawned();
@@ -1568,10 +1559,10 @@ describe("Tier TG — durability and replay", () => {
" label:",
" type: string",
"---",
- "",
- "left",
- '',
- "",
+ "",
+ "left",
+ '',
+ "",
"",
``,
"",
@@ -1642,9 +1633,9 @@ describe("Tier TG — durability and replay", () => {
it("TG17: a continuation opens the retained structure, not the file's", function* () {
const structural: [string, string[]][] = [
- ["pane count", [...PANES, '']],
- ["pane order", ['', ...PANES.slice(0, 1)]],
- ["pane form", ['', '']],
+ ["pane count", [...PANES, '']],
+ ["pane order", ['', ...PANES.slice(0, 1)]],
+ ["pane form", ['', '']],
];
for (const [what, panes] of structural) {
@@ -1769,8 +1760,8 @@ describe("Tier TG — durability and replay", () => {
const dir = yield* useDir();
const stream = new InMemoryStream();
const source = heldDocument(2, [
- '',
- '',
+ '',
+ '',
]);
// Signals and counters, and nothing else. Every step below is an event this
@@ -1782,7 +1773,7 @@ describe("Tier TG — durability and replay", () => {
let entries = 0;
let exits = 0;
let leases = 0;
- let heldWhenBlocked: TerminalProviderResources | undefined;
+ let heldWhenBlocked: GridProviderResources | undefined;
const first = yield* runInterrupted(dir, source, stream, {
// 1. The live pane arms its blocking finalizer, and 2. only then does the
diff --git a/packages/core/tests/loop.test.ts b/packages/core/tests/loop.test.ts
index db40a891e..e5f4da4bf 100644
--- a/packages/core/tests/loop.test.ts
+++ b/packages/core/tests/loop.test.ts
@@ -1349,7 +1349,7 @@ describe("Tier LOOP — replay validates the terminal record", () => {
// No generic catch sits above the component, so the wrapper reaches the
// loop intact — which is what makes this observable. Registered rather
// than stubbed through importComponent: execute() installs its own
- // terminal provider at { at: "min" }, so an outer stub is never asked.
+ // grid provider at { at: "min" }, so an outer stub is never asked.
yield* registerComponents([
{
name: "Wrapped",
diff --git a/packages/core/tests/syntax-catalog.test.ts b/packages/core/tests/syntax-catalog.test.ts
index 83baec843..aab307e1a 100644
--- a/packages/core/tests/syntax-catalog.test.ts
+++ b/packages/core/tests/syntax-catalog.test.ts
@@ -348,55 +348,54 @@ describe("Tier SY: structural vocabulary", () => {
expect(find(entries, "Case").as).toBeUndefined();
});
- it("TG3: freezes the and entries the catalog publishes", function* () {
+ it("TG3: freezes the and entries the catalog publishes", function* () {
const catalog = yield* catalogFor({}, []);
const entries = structural(catalog);
- expect(catalog.version).toBe(1);
- expect(find(entries, "Terminal.Grid")).toEqual({
+ expect(catalog.version).toBe(2);
+ expect(find(entries, "Grid")).toEqual({
kind: "structural",
- name: "Terminal.Grid",
- origin: { kind: "structural", construct: "Terminal.Grid" },
- syntax: ["…"],
+ name: "Grid",
+ origin: { kind: "structural", construct: "Grid" },
+ syntax: ["…"],
description:
- "Open several terminals in one view. " +
- '`…`',
- context: "The `` panes the grid lays out.",
+ "Open several panes in one view. " +
+ '`…`',
+ context: "The `` panes the grid lays out.",
});
- expect(find(entries, "Terminal")).toEqual({
+ expect(find(entries, "Pane")).toEqual({
kind: "structural",
- name: "Terminal",
- origin: { kind: "structural", construct: "Terminal" },
- syntax: ['…', ''],
+ name: "Pane",
+ origin: { kind: "structural", construct: "Pane" },
+ syntax: ['…', ''],
description:
"Expand Markdown or open a shell in a pane. " +
- '`…` runs content; ' +
- '`` opens a shell.',
+ '`…` runs content; ' +
+ '`` opens a shell.',
context: "Markdown the pane runs, in the paired form.",
});
// Neither construct binds, so neither carries an `as` sentence at all.
- expect(find(entries, "Terminal.Grid").as).toBeUndefined();
- expect(find(entries, "Terminal").as).toBeUndefined();
+ expect(find(entries, "Grid").as).toBeUndefined();
+ expect(find(entries, "Pane").as).toBeUndefined();
});
it("TG3: a repository file cannot supply the grid or a pane, and neither can a registration", function* () {
const catalog = yield* catalogFor(
{
components: { kind: "directory" },
- "components/Terminal.md": markdown("a repository terminal\n"),
- "components/Terminal": { kind: "directory" },
- "components/Terminal/Grid.md": markdown("a repository grid\n"),
+ "components/Grid.md": markdown("a repository grid\n"),
+ "components/Pane.md": markdown("a repository pane\n"),
},
["components"],
);
- for (const name of ["Terminal.Grid", "Terminal"]) {
+ for (const name of ["Grid", "Pane"]) {
expect(names(structural(catalog))).toContain(name);
expect(names(userProvided(catalog))).not.toContain(name);
expect(names(builtIn(catalog))).not.toContain(name);
}
- for (const name of ["Terminal.Grid", "Terminal"]) {
+ for (const name of ["Grid", "Pane"]) {
let refused: unknown;
yield* scoped(function* () {
try {
diff --git a/packages/terminal-tmux/deno.json b/packages/grid-tmux/deno.json
similarity index 70%
rename from packages/terminal-tmux/deno.json
rename to packages/grid-tmux/deno.json
index 828e77aa5..9efe0de0c 100644
--- a/packages/terminal-tmux/deno.json
+++ b/packages/grid-tmux/deno.json
@@ -1,5 +1,5 @@
{
- "name": "@executablemd/terminal-tmux",
+ "name": "@executablemd/grid-tmux",
"version": "0.11.0",
"exports": {
".": "./mod.ts",
diff --git a/packages/terminal-tmux/mod.ts b/packages/grid-tmux/mod.ts
similarity index 81%
rename from packages/terminal-tmux/mod.ts
rename to packages/grid-tmux/mod.ts
index c98521662..ee6573c40 100644
--- a/packages/terminal-tmux/mod.ts
+++ b/packages/grid-tmux/mod.ts
@@ -1,9 +1,9 @@
/**
- * The tmux presentation provider for terminal grids
+ * The tmux presentation provider for grids
* (architecture.md §Package ownership).
*
* The first implementation of the provider-neutral domain in
- * `@executablemd/terminal`, and the only place tmux appears. A host that can
+ * `@executablemd/grid`, and the only place tmux appears. A host that can
* divide its terminal installs this; one that cannot installs nothing and the
* document meets core's own refusal rather than a provider that half-works.
*
@@ -25,4 +25,4 @@ export {
runPaneWorkerProcess,
} from "./src/pane-worker.ts";
-export { TerminalTeardownFailed, TMUX_UNAVAILABLE, TmuxUnavailableError } from "./src/tmux.ts";
+export { GridTeardownFailed, TMUX_UNAVAILABLE, TmuxUnavailableError } from "./src/tmux.ts";
diff --git a/packages/terminal-tmux/package.json b/packages/grid-tmux/package.json
similarity index 74%
rename from packages/terminal-tmux/package.json
rename to packages/grid-tmux/package.json
index f0464fcaa..93f71f1b7 100644
--- a/packages/terminal-tmux/package.json
+++ b/packages/grid-tmux/package.json
@@ -1,7 +1,7 @@
{
- "name": "@executablemd/terminal-tmux",
+ "name": "@executablemd/grid-tmux",
"version": "0.11.0",
- "description": "The tmux presentation provider for executable.md terminal grids.",
+ "description": "The tmux presentation provider for executable.md grids.",
"type": "module",
"exports": {
".": "./mod.ts",
@@ -10,7 +10,7 @@
"dependencies": {
"@effectionx/fs": "0.3.0",
"@effectionx/process": "0.8.1",
- "@executablemd/terminal": "workspace:*",
+ "@executablemd/grid": "workspace:*",
"effection": "4.1.0",
"zod": "^4.3.6"
}
diff --git a/packages/terminal-tmux/src/attach-client.ts b/packages/grid-tmux/src/attach-client.ts
similarity index 95%
rename from packages/terminal-tmux/src/attach-client.ts
rename to packages/grid-tmux/src/attach-client.ts
index 33eeda07d..225cac036 100644
--- a/packages/terminal-tmux/src/attach-client.ts
+++ b/packages/grid-tmux/src/attach-client.ts
@@ -1,6 +1,6 @@
/**
* The one visible client: the reader's own view of a grid
- * (architecture.md §Interactive terminal grids).
+ * (architecture.md §Interactive grids).
*
* Deliberately *not* a pane child. A pane's child is settled by sweeping the
* pane's process group and the pane's terminal, because a pane's terminal
@@ -20,8 +20,8 @@ import { spawn as spawnChild } from "node:child_process";
import type { ChildProcess } from "node:child_process";
import { ensure, race, resource, sleep, withResolvers } from "effection";
import type { Operation } from "effection";
-import { deliverSignal, processReachable } from "@executablemd/terminal/processes";
-import { TerminalTeardownFailed } from "./tmux.ts";
+import { deliverSignal, processReachable } from "@executablemd/grid/processes";
+import { GridTeardownFailed } from "./tmux.ts";
export interface AttachClient {
/** The client process, once the runtime says it started. */
@@ -107,7 +107,7 @@ export function useAttachClient(options: {
// the reader's terminal — so the document stops instead. Provider-neutral
// by construction: no socket, session, client name, argv, environment,
// terminal or host message goes into it.
- throw new TerminalTeardownFailed("the terminal grid's visible client did not stop");
+ throw new GridTeardownFailed("the grid's visible client did not stop");
}
function* leftWithin(limitMs: number, pid: number): Operation {
diff --git a/packages/terminal-tmux/src/layout.ts b/packages/grid-tmux/src/layout.ts
similarity index 99%
rename from packages/terminal-tmux/src/layout.ts
rename to packages/grid-tmux/src/layout.ts
index e1fa773f2..5e542d630 100644
--- a/packages/terminal-tmux/src/layout.ts
+++ b/packages/grid-tmux/src/layout.ts
@@ -1,6 +1,6 @@
/**
* The authored grid as explicit tmux geometry
- * (architecture.md §Interactive terminal grids).
+ * (architecture.md §Interactive grids).
*
* `select-layout tiled` picks its own column count from the window's
* dimensions, so it cannot implement a `columns` the author wrote: the same
diff --git a/packages/terminal-tmux/src/pane-channel.ts b/packages/grid-tmux/src/pane-channel.ts
similarity index 99%
rename from packages/terminal-tmux/src/pane-channel.ts
rename to packages/grid-tmux/src/pane-channel.ts
index 907781614..44cfd4652 100644
--- a/packages/terminal-tmux/src/pane-channel.ts
+++ b/packages/grid-tmux/src/pane-channel.ts
@@ -1,6 +1,6 @@
/**
* The parent's end of one grid's private worker channels
- * (architecture.md §Interactive terminal grids).
+ * (architecture.md §Interactive grids).
*
* One directory per grid, mode 0700, under `$TMPDIR` so the socket paths stay
* inside the 104-byte cap a Unix socket has. Inside it, one socket and one
diff --git a/packages/terminal-tmux/src/pane-child.ts b/packages/grid-tmux/src/pane-child.ts
similarity index 99%
rename from packages/terminal-tmux/src/pane-child.ts
rename to packages/grid-tmux/src/pane-child.ts
index d96b3db70..9c4322705 100644
--- a/packages/terminal-tmux/src/pane-child.ts
+++ b/packages/grid-tmux/src/pane-child.ts
@@ -1,6 +1,6 @@
/**
* One interactive child in a pane, and what its settlement establishes
- * (architecture.md §Interactive terminal grids).
+ * (architecture.md §Interactive grids).
*
* Two facts the pane topology needs kept apart:
*
@@ -31,7 +31,7 @@ import {
processReachable,
processTable,
terminalHolders,
-} from "@executablemd/terminal/processes";
+} from "@executablemd/grid/processes";
import type { Settlement } from "./pane-protocol.ts";
export interface PaneChildRequest {
diff --git a/packages/terminal-tmux/src/pane-protocol.ts b/packages/grid-tmux/src/pane-protocol.ts
similarity index 99%
rename from packages/terminal-tmux/src/pane-protocol.ts
rename to packages/grid-tmux/src/pane-protocol.ts
index 61ece9ffc..d45c7e545 100644
--- a/packages/terminal-tmux/src/pane-protocol.ts
+++ b/packages/grid-tmux/src/pane-protocol.ts
@@ -1,6 +1,6 @@
/**
* What the parent and one pane worker say to each other, and how
- * (architecture.md §Interactive terminal grids).
+ * (architecture.md §Interactive grids).
*
* The channel is invocation-private: one Unix socket per pane, inside a
* mode-0700 directory that exists for one grid. A worker proves which pane it
diff --git a/packages/terminal-tmux/src/pane-worker.ts b/packages/grid-tmux/src/pane-worker.ts
similarity index 98%
rename from packages/terminal-tmux/src/pane-worker.ts
rename to packages/grid-tmux/src/pane-worker.ts
index b4d7d283e..4dc7e7749 100644
--- a/packages/terminal-tmux/src/pane-worker.ts
+++ b/packages/grid-tmux/src/pane-worker.ts
@@ -1,6 +1,6 @@
/**
* The persistent pane worker: tmux's initial process in one pane
- * (architecture.md §Interactive terminal grids).
+ * (architecture.md §Interactive grids).
*
* It owns the pane's terminal for the pane's whole life, and everything it does
* is asked of it over the private socket — show this text, start this child,
@@ -28,8 +28,8 @@ import process from "node:process";
import { readTextFile, rm } from "@effectionx/fs";
import { ensure, resource, run, spawn, withResolvers } from "effection";
import type { Operation } from "effection";
-import { processTable } from "@executablemd/terminal/processes";
-import { installDenoTerminalProcesses } from "@executablemd/terminal/posix";
+import { processTable } from "@executablemd/grid/processes";
+import { installDenoTerminalProcesses } from "@executablemd/grid/posix";
import { sweepHolders, usePaneChild } from "./pane-child.ts";
import type { PaneChild, PaneChildRequest } from "./pane-child.ts";
import {
diff --git a/packages/terminal-tmux/src/provider.ts b/packages/grid-tmux/src/provider.ts
similarity index 92%
rename from packages/terminal-tmux/src/provider.ts
rename to packages/grid-tmux/src/provider.ts
index 3fc04e462..a85cbc21f 100644
--- a/packages/terminal-tmux/src/provider.ts
+++ b/packages/grid-tmux/src/provider.ts
@@ -1,6 +1,6 @@
/**
- * The tmux terminal-grid provider, and what a host must be to install it
- * (architecture.md §Interactive terminal grids).
+ * The tmux grid provider, and what a host must be to install it
+ * (architecture.md §Interactive grids).
*
* This is the one place the provider-neutral request from #730 meets tmux. The
* request names columns, rows and the authored panes; what comes back is a
@@ -23,22 +23,22 @@
import { ensure, resource, withResolvers } from "effection";
import process from "node:process";
import type { Operation } from "effection";
-import { registerTerminalProvider, TerminalGrids } from "@executablemd/terminal";
+import { registerGridProvider, Grids } from "@executablemd/grid";
import type {
NativeLaunchOutcome,
NativeLaunchRequest,
- TerminalComposite,
- TerminalGridRequest,
- TerminalPaneState,
- TerminalProviderFactory,
- TerminalShellOutcome,
-} from "@executablemd/terminal";
+ GridComposite,
+ GridRequest,
+ PaneState,
+ GridProviderFactory,
+ ShellOutcome,
+} from "@executablemd/grid";
import { usePaneChannels } from "./pane-channel.ts";
import { requireQuiescent } from "./pane-worker.ts";
import type { PaneLink } from "./pane-channel.ts";
import { useTmuxGrid } from "./tmux-grid.ts";
import type { TmuxGrid, VisibleClient } from "./tmux-grid.ts";
-import { probeTmux, TerminalTeardownFailed, tmuxAt, TmuxUnavailableError } from "./tmux.ts";
+import { probeTmux, GridTeardownFailed, tmuxAt, TmuxUnavailableError } from "./tmux.ts";
import type { Tmux } from "./tmux.ts";
/** The name a host installs this provider under. */
@@ -71,9 +71,9 @@ export interface TmuxProviderDependencies {
* request it was routed — a handler that answered without presenting would have
* presented nothing, which is what #730's handshake is for.
*/
-export function tmuxGridProvider(deps: TmuxProviderDependencies): TerminalProviderFactory {
+export function tmuxGridProvider(deps: TmuxProviderDependencies): GridProviderFactory {
return function* (_options, authority): Operation {
- yield* TerminalGrids.around(
+ yield* Grids.around(
{
*open([request]): Operation {
const composite = yield* usePresentedGrid(deps, request);
@@ -189,9 +189,9 @@ export function createGridTeardown(parts: GridParts): () => Operation {
function usePresentedGrid(
deps: TmuxProviderDependencies,
- request: TerminalGridRequest,
-): Operation {
- return resource(function* (provide) {
+ request: GridRequest,
+): Operation {
+ return resource(function* (provide) {
const probed = yield* probeTmux({
isTerminal: deps.isTerminal,
env: deps.env,
@@ -302,8 +302,8 @@ function usePresentedGrid(
function* label(
grid: TmuxGrid,
ordinal: number,
- request: TerminalGridRequest,
- state: TerminalPaneState,
+ request: GridRequest,
+ state: PaneState,
): Operation {
const pane = request.panes[ordinal];
if (pane === undefined) {
@@ -322,9 +322,7 @@ function* label(
*/
function* quiesceWorker(link: PaneLink): Operation {
if (!link.connected()) {
- throw new TerminalTeardownFailed(
- "a terminal pane's worker was gone before it was asked to stop",
- );
+ throw new GridTeardownFailed("a terminal pane's worker was gone before it was asked to stop");
}
yield* link.send({ type: "shutdown" });
let quiesced = false;
@@ -333,9 +331,7 @@ function* quiesceWorker(link: PaneLink): Operation {
const frame = yield* link.next();
if (frame === undefined) {
if (!quiesced || !farewelled) {
- throw new TerminalTeardownFailed(
- "a terminal pane stopped answering before it was proved free",
- );
+ throw new GridTeardownFailed("a terminal pane stopped answering before it was proved free");
}
return;
}
@@ -346,10 +342,10 @@ function* quiesceWorker(link: PaneLink): Operation {
}
if (frame.type === "bye") {
if (!quiesced) {
- throw new TerminalTeardownFailed("a terminal pane said goodbye before it was proved free");
+ throw new GridTeardownFailed("a terminal pane said goodbye before it was proved free");
}
if (frame.holders.some((holder) => !holder.gone)) {
- throw new TerminalTeardownFailed("something still holds a terminal pane");
+ throw new GridTeardownFailed("something still holds a terminal pane");
}
farewelled = true;
continue;
@@ -373,7 +369,7 @@ export function* runInPane(
if (link === undefined) {
// No fallback. A composite that cannot run this in the pane it was asked
// for refuses, rather than putting a native UI on the root terminal.
- throw new Error("this terminal grid cannot run that pane's launch");
+ throw new Error("this grid cannot run that pane's launch");
}
const id = `launch-${link.ordinal}-${++started}`;
let settled = false;
@@ -455,7 +451,7 @@ let started = 0;
/** Install the tmux provider for this host, when this host can present one. */
export function* installTmuxGridProvider(deps: TmuxProviderDependencies): Operation {
- yield* registerTerminalProvider(TMUX_PROVIDER, tmuxGridProvider(deps));
+ yield* registerGridProvider(TMUX_PROVIDER, tmuxGridProvider(deps));
}
export { TmuxUnavailableError };
diff --git a/packages/terminal-tmux/src/tmux-grid.ts b/packages/grid-tmux/src/tmux-grid.ts
similarity index 98%
rename from packages/terminal-tmux/src/tmux-grid.ts
rename to packages/grid-tmux/src/tmux-grid.ts
index 065dd2fba..1d96793f4 100644
--- a/packages/terminal-tmux/src/tmux-grid.ts
+++ b/packages/grid-tmux/src/tmux-grid.ts
@@ -1,6 +1,6 @@
/**
* One hidden, invocation-private tmux composite
- * (architecture.md §Interactive terminal grids, §Atomic presentation).
+ * (architecture.md §Interactive grids, §Atomic presentation).
*
* A grid is built entirely out of sight: its own server on its own socket, a
* pane per authored ordinal each running that pane's worker, the authored
@@ -30,12 +30,12 @@ import { exec } from "@effectionx/process";
import { lines } from "@effectionx/stream-helpers";
import { createSignal, ensure, resource, sleep, spawn } from "effection";
import type { Operation } from "effection";
-import { processReachable } from "@executablemd/terminal/processes";
+import { processReachable } from "@executablemd/grid/processes";
import { layoutString, swapsInto } from "./layout.ts";
import type { LayoutCell } from "./layout.ts";
import { useAttachClient } from "./attach-client.ts";
import type { AttachClient } from "./attach-client.ts";
-import { quietly, TerminalTeardownFailed } from "./tmux.ts";
+import { quietly, GridTeardownFailed } from "./tmux.ts";
import type { Tmux } from "./tmux.ts";
/** What one prepared pane is, from the composite's side. */
@@ -152,7 +152,7 @@ export function useTmuxGrid(tmux: Tmux, request: TmuxGridRequest): Operation {
return (function* (): Operation {
- const authority = yield* useTerminalInstallation();
- yield* registerTerminalProvider(
+ const authority = yield* useGridInstallation();
+ yield* registerGridProvider(
"tmux",
tmuxGridProvider({
isTerminal: options.isTerminal,
@@ -157,8 +157,8 @@ function useProbedProvider(options: {
}),
}),
);
- yield* installTerminalProvider("tmux", { label: "tmux" }, authority);
- yield* TerminalGrids.operations.open({
+ yield* installGridProvider("tmux", { label: "tmux" }, authority);
+ yield* Grids.operations.open({
columns: 1,
rows: 1,
panes: [{ ordinal: 0, title: "Only", row: 0, column: 0, form: "paired" }],
@@ -204,7 +204,7 @@ function useDeadObserver(): Operation {
}
/** A composite whose pane endpoint is the production one, over these links. */
-function paneComposite(links: readonly PaneLink[]): TerminalComposite {
+function paneComposite(links: readonly PaneLink[]): GridComposite {
const refuse = (): never => {
throw new Error("this row drives the pane endpoint only");
};
@@ -1706,7 +1706,7 @@ function untilEvent(grid: TmuxGrid, kind: ControlEvent["kind"]): Operation
describe("Tier TG20 — a pane launch reaches its own worker", () => {
/** A composite over a fake server that really starts its pane workers. */
function useLiveComposite(panes: number): Operation<{
- composite: TerminalComposite;
+ composite: GridComposite;
tmux: FakeTmux;
channels: PaneChannels;
}> {
@@ -1980,7 +1980,7 @@ describe("Tier TG20 — a pane launch reaches its own worker", () => {
/**
* Tier TH — which hosts open a grid, and which only describe one
- * (architecture.md §Interactive terminal grids).
+ * (architecture.md §Interactive grids).
*
* The Deno source entrypoint and the compiled binary present grids when the
* invocation has a terminal and a usable tmux. Node and Bun keep the same
diff --git a/packages/terminal/deno.json b/packages/grid/deno.json
similarity index 84%
rename from packages/terminal/deno.json
rename to packages/grid/deno.json
index d8685ae0c..708deabb4 100644
--- a/packages/terminal/deno.json
+++ b/packages/grid/deno.json
@@ -1,5 +1,5 @@
{
- "name": "@executablemd/terminal",
+ "name": "@executablemd/grid",
"version": "0.11.0",
"exports": {
".": "./mod.ts",
diff --git a/packages/terminal/lifecycle.ts b/packages/grid/lifecycle.ts
similarity index 66%
rename from packages/terminal/lifecycle.ts
rename to packages/grid/lifecycle.ts
index f0a699ce9..dee80051b 100644
--- a/packages/terminal/lifecycle.ts
+++ b/packages/grid/lifecycle.ts
@@ -5,36 +5,36 @@
* The direct authority a host installs, the claims and readiness a grid passes
* through before anything is shown, the row-major layout an author's `columns`
* implies, the live and durable grid itself, what it retains, and the
- * reader-close boundary that ends it. A facet of `@executablemd/terminal`: what
+ * reader-close boundary that ends it. A facet of `@executablemd/grid`: what
* it shares with the root is the same object, not a copy.
*/
export {
awaitReadiness,
createGridRegistry,
- createTerminalAuthority,
- createTerminalGridClaims,
+ createGridAuthority,
+ createGridClaims,
sealOnTeardown,
- TerminalAuthorityError,
- terminalInstallation,
- useTerminalInstallation,
+ GridAuthorityError,
+ gridInstallation,
+ useGridInstallation,
} from "./src/authority.ts";
export type {
GridRegistry,
LiveGrid,
PaneReadiness,
- TerminalGridAuthority,
- TerminalGridClaims,
- TerminalInstallation,
- TerminalPaneClaim,
+ GridAuthority,
+ GridClaims,
+ GridInstallation,
+ PaneClaim,
} from "./src/authority.ts";
-export { installTerminalProvider } from "./src/provider-api.ts";
+export { installGridProvider } from "./src/provider-api.ts";
export {
createCloseBoundary,
durableGrid,
- openTerminalGrid,
+ openGrid,
paneNeverStartedMessage,
retainedLayout,
toRequest,
@@ -49,5 +49,5 @@ export type {
RetainedPaneOutcome,
} from "./src/grid.ts";
-export { terminalGridLayout } from "./src/layout.ts";
-export type { PlacedPane, TerminalGridCell, TerminalGridLayout } from "./src/layout.ts";
+export { gridLayout } from "./src/layout.ts";
+export type { PlacedPane, GridCell, GridLayout } from "./src/layout.ts";
diff --git a/packages/terminal/mod.ts b/packages/grid/mod.ts
similarity index 73%
rename from packages/terminal/mod.ts
rename to packages/grid/mod.ts
index 6ed7a95db..683524dc0 100644
--- a/packages/terminal/mod.ts
+++ b/packages/grid/mod.ts
@@ -1,10 +1,11 @@
/**
- * The provider-neutral terminal domain (architecture.md §Package ownership).
+ * The provider-neutral grid domain (architecture.md §Package ownership).
*
- * Everything here is what a document means by a terminal, independent of what
+ * Everything here is what a document means by a grid, independent of what
* presents one: a native launch that wants the foreground, a grid of panes and
* the states they pass through, the routing that finds whichever provider a
- * host installed, and the errors a caller meets when none did. No multiplexer,
+ * host installed, and the errors a caller meets when none did. A terminal is a
+ * capability a pane acquires, not the identity of the grid. No multiplexer,
* socket, process topology or window identifier appears in this package.
*
* The lifecycle a provider is driven through lives in `./lifecycle`, process
@@ -37,32 +38,32 @@ export type {
} from "./src/native-launcher.ts";
export {
- TERMINAL_GRIDS_API,
- TERMINAL_PROVIDER_UNAVAILABLE,
- TerminalGrids,
- TerminalProviderUnavailableError,
+ GRIDS_API,
+ GRID_PROVIDER_UNAVAILABLE,
+ Grids,
+ GridProviderUnavailableError,
} from "./src/composite.ts";
export type {
- TerminalComposite,
- TerminalGridApi,
- TerminalGridRequest,
- TerminalPaneRequest,
- TerminalPaneState,
- TerminalShellOutcome,
+ GridComposite,
+ GridApi,
+ GridRequest,
+ PaneRequest,
+ PaneState,
+ ShellOutcome,
} from "./src/composite.ts";
export {
- registerTerminalProvider,
- TERMINAL_PROVIDERS_API,
- TerminalProviderInstallError,
- TerminalProviders,
+ registerGridProvider,
+ GRID_PROVIDERS_API,
+ GridProviderInstallError,
+ GridProviders,
} from "./src/provider-api.ts";
export type {
- TerminalProviderApi,
- TerminalProviderCall,
- TerminalProviderFactory,
- TerminalProviderInstallRequest,
- TerminalProviderOptions,
+ GridProviderApi,
+ GridProviderCall,
+ GridProviderFactory,
+ GridProviderInstallRequest,
+ GridProviderOptions,
} from "./src/provider-api.ts";
export { paneTerminal, usePaneTerminal } from "./src/pane.ts";
diff --git a/packages/terminal/package.json b/packages/grid/package.json
similarity index 79%
rename from packages/terminal/package.json
rename to packages/grid/package.json
index 568dd92ab..03a2cebcb 100644
--- a/packages/terminal/package.json
+++ b/packages/grid/package.json
@@ -1,7 +1,7 @@
{
- "name": "@executablemd/terminal",
+ "name": "@executablemd/grid",
"version": "0.11.0",
- "description": "The provider-neutral terminal domain for executable.md documents.",
+ "description": "The provider-neutral grid domain for executable.md documents.",
"type": "module",
"exports": {
".": "./mod.ts",
diff --git a/packages/terminal/posix.ts b/packages/grid/posix.ts
similarity index 100%
rename from packages/terminal/posix.ts
rename to packages/grid/posix.ts
diff --git a/packages/terminal/processes.ts b/packages/grid/processes.ts
similarity index 100%
rename from packages/terminal/processes.ts
rename to packages/grid/processes.ts
diff --git a/packages/terminal/src/authority.ts b/packages/grid/src/authority.ts
similarity index 81%
rename from packages/terminal/src/authority.ts
rename to packages/grid/src/authority.ts
index 29b15370c..c8d5d01f0 100644
--- a/packages/terminal/src/authority.ts
+++ b/packages/grid/src/authority.ts
@@ -21,10 +21,10 @@
import { all, createContext, ensure, withResolvers } from "effection";
import type { Context, Operation } from "effection";
-import type { TerminalComposite, TerminalGridRequest } from "./composite.ts";
+import type { GridComposite, GridRequest } from "./composite.ts";
-export class TerminalAuthorityError extends Error {
- override name = "TerminalAuthorityError";
+export class GridAuthorityError extends Error {
+ override name = "GridAuthorityError";
}
/**
@@ -35,7 +35,7 @@ export class TerminalAuthorityError extends Error {
* two ordinals do not contend at all, which is what lets panes be interactive at
* the same time.
*/
-export interface TerminalPaneClaim {
+export interface PaneClaim {
readonly ordinal: number;
/**
* Run one interactive operation as this pane's owner.
@@ -66,8 +66,8 @@ export interface PaneReadiness {
}
/** The claims one grid expansion holds, and what they are waiting on. */
-export interface TerminalGridClaims {
- readonly claims: readonly TerminalPaneClaim[];
+export interface GridClaims {
+ readonly claims: readonly PaneClaim[];
readonly readiness: readonly PaneReadiness[];
/**
* Stop admitting anything on every pane.
@@ -88,18 +88,18 @@ export interface TerminalGridClaims {
* presented, or one belonging to a superseded installation — authorizes
* nothing.
*/
-export interface TerminalGridAuthority {
- present(request: TerminalGridRequest, composite: TerminalComposite): Operation;
+export interface GridAuthority {
+ present(request: GridRequest, composite: GridComposite): Operation;
}
/** One grid this execution issued, from the authority's side. */
export interface LiveGrid {
/** The exact request object core issued. Compared by identity, never shape. */
- readonly request: TerminalGridRequest;
+ readonly request: GridRequest;
/** The installation this grid belongs to. */
readonly generation: object;
/** Run the grid on a presented composite, and keep what it settled to. */
- run(composite: TerminalComposite): Operation;
+ run(composite: GridComposite): Operation;
/** Whether this request has already been presented. */
used: boolean;
/** Whether the grid actually ran to a settlement. */
@@ -133,26 +133,26 @@ export function createGridRegistry(): GridRegistry {
* that kept an authority from a superseded installation presents into a
* generation that no longer has the grid it names.
*/
-export function createTerminalAuthority(
+export function createGridAuthority(
generation: object,
live: () => readonly LiveGrid[],
-): TerminalGridAuthority {
+): GridAuthority {
return {
*present(request, composite) {
const grid = live().find((candidate) => Object.is(candidate.request, request));
if (grid === undefined) {
- throw new TerminalAuthorityError(
+ throw new GridAuthorityError(
"this grid request is not live: it was copied, rebuilt, kept from another grid, or " +
"belongs to an execution that has finished",
);
}
if (!Object.is(grid.generation, generation)) {
- throw new TerminalAuthorityError(
- "this grid request belongs to another terminal provider installation",
+ throw new GridAuthorityError(
+ "this grid request belongs to another grid provider installation",
);
}
if (grid.used) {
- throw new TerminalAuthorityError(
+ throw new GridAuthorityError(
"this grid request has already been presented — one request opens one grid",
);
}
@@ -162,19 +162,19 @@ export function createTerminalAuthority(
};
}
-/** One execution's terminal installation: its registry and its generation. */
-export interface TerminalInstallation {
+/** One execution's grid installation: its registry and its generation. */
+export interface GridInstallation {
readonly registry: GridRegistry;
/** Identifies this execution's provider installation, and nothing else. */
readonly generation: object;
}
-const Installation: Context = createContext<
- TerminalInstallation | undefined
->("core.terminal.installation", undefined);
+const Installation: Context = createContext<
+ GridInstallation | undefined
+>("core.grid.installation", undefined);
/**
- * Open one terminal installation for a live document, and hand back the
+ * Open one grid installation for a live document, and hand back the
* authority its providers are installed with.
*
* What travels contextually is the installation — composition data, so a
@@ -183,15 +183,15 @@ const Installation: Context = createContext<
* therefore produces requests the real authority has never heard of, which is a
* refusal rather than a way in.
*/
-export function* useTerminalInstallation(): Operation {
+export function* useGridInstallation(): Operation {
const registry = createGridRegistry();
const generation = {};
yield* Installation.set({ registry, generation });
- return createTerminalAuthority(generation, () => registry.live());
+ return createGridAuthority(generation, () => registry.live());
}
-/** This execution's terminal installation, or `undefined` outside one. */
-export function terminalInstallation(): Operation {
+/** This execution's grid installation, or `undefined` outside one. */
+export function gridInstallation(): Operation {
return Installation.get();
}
@@ -203,11 +203,11 @@ export function terminalInstallation(): Operation(body: () => Operation): Operation {
if (sealed) {
- throw new TerminalAuthorityError(
+ throw new GridAuthorityError(
`pane ${pane.ordinal} is closed: its grid has stopped admitting interactive work`,
);
}
if (live) {
- throw new TerminalAuthorityError(
+ throw new GridAuthorityError(
`pane ${pane.ordinal} already has a live interactive operation — one owns a pane ` +
`terminal at a time`,
);
@@ -264,14 +264,14 @@ export function createTerminalGridClaims(request: TerminalGridRequest): Terminal
};
}
-function validate(request: TerminalGridRequest): void {
+function validate(request: GridRequest): void {
if (request.panes.length === 0) {
- throw new TerminalAuthorityError("a terminal grid request names no panes");
+ throw new GridAuthorityError("a grid request names no panes");
}
for (const [index, pane] of request.panes.entries()) {
if (pane.ordinal !== index) {
- throw new TerminalAuthorityError(
- `a terminal grid request names pane ordinal ${pane.ordinal} at position ${index}: ` +
+ throw new GridAuthorityError(
+ `a grid request names pane ordinal ${pane.ordinal} at position ${index}: ` +
`a pane's ordinal is its position among the grid's panes`,
);
}
@@ -295,7 +295,7 @@ function* allOf(waits: readonly Operation[]): Operation {
}
/** Seal the grid as soon as the enclosing scope begins to unwind. */
-export function sealOnTeardown(claims: TerminalGridClaims): Operation {
+export function sealOnTeardown(claims: GridClaims): Operation {
return ensure(() => {
claims.seal();
});
diff --git a/packages/terminal/src/composite.ts b/packages/grid/src/composite.ts
similarity index 85%
rename from packages/terminal/src/composite.ts
rename to packages/grid/src/composite.ts
index dc82e1e34..466163330 100644
--- a/packages/terminal/src/composite.ts
+++ b/packages/grid/src/composite.ts
@@ -1,5 +1,5 @@
/**
- * The terminal grid boundary — how a host presents one grid of interactive
+ * The grid boundary — how a host presents one grid of interactive
* panes, and what composing middleware around it may do.
*
* This is not the native launcher. A launch hands **one** child the whole
@@ -7,7 +7,7 @@
* several panes that stay interactive at the same time, each with its own
* lifetime. tmux is one way to do that, a host-native composite UI is another,
* and a test surface that opens no terminal at all is a third. None of them
- * appears in the document: `` asks for panes and their authored
+ * appears in the document: `` asks for panes and their authored
* layout, and the host chooses what presents them.
*
* **This surface is routing, and only routing.** Middleware here may observe,
@@ -29,7 +29,7 @@ import type { Operation } from "effection";
import type { NativeLaunchOutcome, NativeLaunchRequest } from "./native-launcher.ts";
/** One pane the provider is asked to present, by its authored ordinal. */
-export interface TerminalPaneRequest {
+export interface PaneRequest {
/** The pane's identity: its position among the grid's panes, from zero. */
readonly ordinal: number;
/** The label to display. Two panes may carry the same one. */
@@ -57,10 +57,10 @@ export interface TerminalPaneRequest {
* against the one it issued, so a request that was copied, rebuilt with the same
* members, kept from an earlier grid, or already used authorizes nothing.
*/
-export interface TerminalGridRequest {
+export interface GridRequest {
readonly columns: number;
readonly rows: number;
- readonly panes: readonly TerminalPaneRequest[];
+ readonly panes: readonly PaneRequest[];
}
/**
@@ -71,10 +71,10 @@ export interface TerminalGridRequest {
* cancelled solely because the reader closed the grid — which is not a failure
* and is deliberately spelled differently from one.
*/
-export type TerminalPaneState = "starting" | "running" | "succeeded" | "failed" | "closed";
+export type PaneState = "starting" | "running" | "succeeded" | "failed" | "closed";
/** How a pane's default shell ended. */
-export interface TerminalShellOutcome {
+export interface ShellOutcome {
exitCode?: number;
signal?: string;
}
@@ -86,7 +86,7 @@ export interface TerminalShellOutcome {
* is never reused across expansions, and a provider that hands the same one
* back twice has handed back a grid the second expansion did not ask for.
*/
-export interface TerminalComposite {
+export interface GridComposite {
/**
* Show the composite. Called once, and only after every pane is ready.
*
@@ -100,7 +100,7 @@ export interface TerminalComposite {
* Its return value is ignored on purpose: drawing a status is not a chance to
* change one.
*/
- update(ordinal: number, state: TerminalPaneState): Operation;
+ update(ordinal: number, state: PaneState): Operation;
/**
* Show text a pane's own content rendered.
*
@@ -124,7 +124,7 @@ export interface TerminalComposite {
* that starts and exits at once is both ready and settled, while a shell that
* never started leaves the latch alone and the grid never attaches.
*/
- shell(ordinal: number, spawned: () => void): Operation;
+ shell(ordinal: number, spawned: () => void): Operation;
/**
* Run one native launch in one pane, on that pane's terminal.
*
@@ -168,21 +168,21 @@ export interface TerminalComposite {
}
/** The stable name every loaded copy composes through. */
-export const TERMINAL_GRIDS_API = "TerminalGrids";
+export const GRIDS_API = "Grids";
-export const TERMINAL_PROVIDER_UNAVAILABLE =
- "no terminal provider is installed — this host does not present a grid of " +
+export const GRID_PROVIDER_UNAVAILABLE =
+ "no grid provider is installed — this host does not present a grid of " +
"interactive panes. `xmd run` installs one; a test or embedding host installs " +
"its own.";
-export class TerminalProviderUnavailableError extends Error {
- override name = "TerminalProviderUnavailableError";
- constructor(message: string = TERMINAL_PROVIDER_UNAVAILABLE) {
+export class GridProviderUnavailableError extends Error {
+ override name = "GridProviderUnavailableError";
+ constructor(message: string = GRID_PROVIDER_UNAVAILABLE) {
super(message);
}
}
-export interface TerminalGridApi {
+export interface GridApi {
/**
* Route one grid request to whatever presents it.
*
@@ -190,7 +190,7 @@ export interface TerminalGridApi {
* evidence that a grid was opened, and core reads what the authority settled
* instead of what a handler said.
*/
- open(request: TerminalGridRequest): Operation;
+ open(request: GridRequest): Operation;
}
/**
@@ -200,9 +200,9 @@ export interface TerminalGridApi {
* nothing was presented — which is the honest answer for a host that installs
* no provider at all.
*/
-export const TerminalGrids: Api = createApi(TERMINAL_GRIDS_API, {
+export const Grids: Api = createApi(GRIDS_API, {
// deno-lint-ignore require-yield
- *open(_request: TerminalGridRequest): Operation {
- throw new TerminalProviderUnavailableError();
+ *open(_request: GridRequest): Operation {
+ throw new GridProviderUnavailableError();
},
});
diff --git a/packages/terminal/src/controlled-composite.ts b/packages/grid/src/controlled-composite.ts
similarity index 89%
rename from packages/terminal/src/controlled-composite.ts
rename to packages/grid/src/controlled-composite.ts
index ec16db0b8..e7b8c8782 100644
--- a/packages/terminal/src/controlled-composite.ts
+++ b/packages/grid/src/controlled-composite.ts
@@ -9,17 +9,12 @@
* It lives apart from the contract for the same reason the controlled launcher
* does: production code must have no path to a fixture, and importing the
* domain must not load one. It is reachable only through
- * `@executablemd/terminal/test`.
+ * `@executablemd/grid/test`.
*/
import type { Operation } from "effection";
import type { NativeLaunchOutcome, NativeLaunchRequest } from "./native-launcher.ts";
-import type {
- TerminalComposite,
- TerminalGridRequest,
- TerminalPaneState,
- TerminalShellOutcome,
-} from "./composite.ts";
+import type { GridComposite, GridRequest, PaneState, ShellOutcome } from "./composite.ts";
/**
* Everything one controlled composite did, in the order it did it.
@@ -28,7 +23,7 @@ import type {
* before every pane started, that nothing attached before the readiness
* barrier, and that teardown destroyed exactly the composite it prepared.
*/
-export interface TerminalProviderLog {
+export interface GridProviderLog {
readonly events: string[];
/**
* What each pane displayed, by ordinal.
@@ -45,11 +40,11 @@ export interface TerminalProviderLog {
* including after a cancellation, where the ordering of the record alone
* would not say whether teardown finished.
*/
- readonly live: TerminalProviderResources;
+ readonly live: GridProviderResources;
}
/** What one controlled composite holds at a moment, by kind. */
-export interface TerminalProviderResources {
+export interface GridProviderResources {
/** Composites prepared and not yet destroyed. */
composites: number;
/** Composites attached and not yet destroyed. */
@@ -61,7 +56,7 @@ export interface TerminalProviderResources {
}
/** A fresh, empty record. */
-export function terminalProviderLog(): TerminalProviderLog {
+export function gridProviderLog(): GridProviderLog {
return {
events: [],
shown: new Map(),
@@ -80,8 +75,8 @@ export function terminalProviderLog(): TerminalProviderLog {
*/
export interface ControlledCompositeOptions {
/** Appended to as the composite works, so ordering is read rather than timed. */
- readonly log?: TerminalProviderLog;
- onPrepare?: (request: TerminalGridRequest) => Operation;
+ readonly log?: GridProviderLog;
+ onPrepare?: (request: GridRequest) => Operation;
onAttach?: () => Operation;
onDestroy?: () => Operation;
/**
@@ -90,8 +85,8 @@ export interface ControlledCompositeOptions {
* A suite watches it to react to something the grid decided — a pane that
* failed, a pane that became runnable — instead of waiting and hoping.
*/
- onUpdate?: (ordinal: number, state: TerminalPaneState) => void;
- shell?: (ordinal: number, spawned: () => void) => Operation;
+ onUpdate?: (ordinal: number, state: PaneState) => void;
+ shell?: (ordinal: number, spawned: () => void) => Operation;
/**
* What a pane launch does, in place of starting a native UI.
*
@@ -115,12 +110,12 @@ export interface ControlledCompositeOptions {
* multiplexer, or a process anywhere in it.
*/
export function prepareControlledComposite(
- request: TerminalGridRequest,
+ request: GridRequest,
options: ControlledCompositeOptions = {},
generation = 0,
-): Operation {
- return (function* (): Operation {
- const log = options.log ?? terminalProviderLog();
+): Operation {
+ return (function* (): Operation {
+ const log = options.log ?? gridProviderLog();
if (options.onPrepare) {
yield* options.onPrepare(request);
}
diff --git a/packages/terminal/src/controlled-launcher.ts b/packages/grid/src/controlled-launcher.ts
similarity index 98%
rename from packages/terminal/src/controlled-launcher.ts
rename to packages/grid/src/controlled-launcher.ts
index 8b314874f..ea7c0f75f 100644
--- a/packages/terminal/src/controlled-launcher.ts
+++ b/packages/grid/src/controlled-launcher.ts
@@ -6,7 +6,7 @@
* process and no host stream — a launch here is whatever the row says it is —
* and it lives in its own module so that importing the domain never loads a
* fixture. Production code has no path to it: it is reachable only through
- * `@executablemd/terminal/test`.
+ * `@executablemd/grid/test`.
*/
import { resource } from "effection";
diff --git a/packages/terminal/src/grid.ts b/packages/grid/src/grid.ts
similarity index 92%
rename from packages/terminal/src/grid.ts
rename to packages/grid/src/grid.ts
index f9a6750f6..799d45c80 100644
--- a/packages/terminal/src/grid.ts
+++ b/packages/grid/src/grid.ts
@@ -1,5 +1,5 @@
/**
- * One terminal grid, from the lease to the last finalizer (spec §6.21,
+ * One grid, from the lease to the last finalizer (spec §6.21,
* architecture.md §Atomic presentation and settlement, §Durability and replay).
*
* Opening a grid is atomic from the reader's side, and that is the whole shape
@@ -42,18 +42,18 @@ import {
ephemeral,
} from "@executablemd/durable-streams";
import type { Json, Workflow } from "@executablemd/durable-streams";
-import { TerminalGrids } from "./composite.ts";
+import { Grids } from "./composite.ts";
import { flushOutput, reserveTerminal } from "./native-launcher.ts";
-import type { TerminalComposite, TerminalGridRequest } from "./composite.ts";
+import type { GridComposite, GridRequest } from "./composite.ts";
import {
awaitReadiness,
- createTerminalGridClaims,
- TerminalAuthorityError,
- terminalInstallation,
+ createGridClaims,
+ GridAuthorityError,
+ gridInstallation,
} from "./authority.ts";
-import type { LiveGrid, TerminalPaneClaim } from "./authority.ts";
-import type { TerminalGridLayout } from "./layout.ts";
+import type { LiveGrid, PaneClaim } from "./authority.ts";
+import type { GridLayout } from "./layout.ts";
/**
* The live boundary reader close crosses (architecture.md §Atomic presentation
@@ -149,7 +149,7 @@ export interface RetainedGrid extends Record {
*/
export interface PaneWork {
readonly ordinal: number;
- run(claim: TerminalPaneClaim, composite: TerminalComposite): Operation;
+ run(claim: PaneClaim, composite: GridComposite): Operation;
}
/**
@@ -163,12 +163,12 @@ export function paneNeverStartedMessage(ordinal: number, title: string): string
return (
`pane ${ordinal} ("${title}") finished without starting anything interactive, so the ` +
`grid never opened. A pane runs an interactive child — a , or the ` +
- `default shell a self-closing starts.`
+ `default shell a self-closing starts.`
);
}
/** The provider-neutral request one derived layout asks for. */
-export function toRequest(layout: TerminalGridLayout): TerminalGridRequest {
+export function toRequest(layout: GridLayout): GridRequest {
return Object.freeze({
columns: layout.columns,
rows: layout.rows,
@@ -187,7 +187,7 @@ export function toRequest(layout: TerminalGridLayout): TerminalGridRequest {
}
/** The retained shape of one request. */
-export function retainedLayout(request: TerminalGridRequest): RetainedGrid["layout"] {
+export function retainedLayout(request: GridRequest): RetainedGrid["layout"] {
return {
columns: request.columns,
rows: request.rows,
@@ -211,17 +211,17 @@ export function retainedLayout(request: TerminalGridRequest): RetainedGrid["layo
* short-circuits or fabricates a return has presented nothing, and this says so
* rather than letting the document believe a grid opened.
*/
-export function openTerminalGrid(
- layout: TerminalGridLayout,
+export function openGrid(
+ layout: GridLayout,
work: readonly PaneWork[],
boundary: CloseBoundary,
): Operation {
return scoped(function* (): Operation {
- const installation = yield* terminalInstallation();
+ const installation = yield* gridInstallation();
if (installation === undefined) {
- throw new TerminalAuthorityError(
- "a terminal grid is available only inside a document execution with an installed " +
- "terminal provider — a grid outside one retains nothing and could not be resumed",
+ throw new GridAuthorityError(
+ "a grid is available only inside a document execution with an installed " +
+ "grid provider — a grid outside one retains nothing and could not be resumed",
);
}
@@ -252,11 +252,11 @@ export function openTerminalGrid(
yield* flushOutput();
// Routed, and the answer thrown away.
- yield* TerminalGrids.operations.open(request);
+ yield* Grids.operations.open(request);
if (!grid.settled || settled === undefined) {
- throw new TerminalAuthorityError(
- "no terminal provider opened this grid — a handler answered without delivering the " +
+ throw new GridAuthorityError(
+ "no grid provider opened this grid — a handler answered without delivering the " +
"request to a registered provider",
);
}
@@ -273,8 +273,8 @@ export function openTerminalGrid(
* it.
*/
function presentGrid(
- request: TerminalGridRequest,
- composite: TerminalComposite,
+ request: GridRequest,
+ composite: GridComposite,
work: readonly PaneWork[],
boundary: CloseBoundary,
): Operation {
@@ -283,7 +283,7 @@ function presentGrid(
// owed a destroy even if the next line is what fails.
yield* ensure(() => composite.destroy());
- const grid = createTerminalGridClaims(request);
+ const grid = createGridClaims(request);
// Nothing new is admitted once teardown begins, so a pane that was about to
// start an interactive child is refused rather than racing the close.
yield* ensure(() => {
@@ -355,7 +355,7 @@ function presentGrid(
} catch {
// Simultaneous startup failures are selected by authored ordinal, not by
// whichever rejected the race first.
- throw new Error(firstReason(outcomes) ?? "a terminal grid pane failed to start");
+ throw new Error(firstReason(outcomes) ?? "a grid pane failed to start");
}
// A pane that already settled keeps the status it settled to: overwriting
@@ -410,10 +410,10 @@ function presentGrid(
/** Run one pane's work and say what it came to. */
function runPane(
pane: PaneWork,
- claim: TerminalPaneClaim,
- composite: TerminalComposite,
+ claim: PaneClaim,
+ composite: GridComposite,
readiness: { readonly acknowledged: boolean },
- request: TerminalGridRequest,
+ request: GridRequest,
index: number,
closing: Operation,
): Operation {
@@ -462,7 +462,7 @@ function runPane(
/** The record one grid settled to. */
function retained(
- request: TerminalGridRequest,
+ request: GridRequest,
panes: readonly RetainedPaneOutcome[],
reason: string | undefined,
): RetainedGrid {
diff --git a/packages/terminal/src/layout.ts b/packages/grid/src/layout.ts
similarity index 87%
rename from packages/terminal/src/layout.ts
rename to packages/grid/src/layout.ts
index 8022dd3be..f16272b67 100644
--- a/packages/terminal/src/layout.ts
+++ b/packages/grid/src/layout.ts
@@ -1,5 +1,5 @@
/**
- * The concrete grid an authored `` derives (spec §6.21).
+ * The concrete grid an authored `` derives (spec §6.21).
*
* `structural-rules.ts` decides what the source says: which panes were written,
* in what order, and what is wrong with the way they were written. What it
@@ -22,7 +22,7 @@
export type PaneForm = "paired" | "self-closing";
/** One pane, placed. */
-export interface TerminalGridCell {
+export interface GridCell {
/** The pane's structural identity: its position among the panes, from zero. */
readonly ordinal: number;
/** The row it occupies, from zero. */
@@ -35,13 +35,13 @@ export interface TerminalGridCell {
readonly form: PaneForm;
}
-/** The complete grid one `` asked for. */
-export interface TerminalGridLayout {
+/** The complete grid one `` asked for. */
+export interface GridLayout {
readonly columns: number;
/** How many rows those columns take to hold every pane. */
readonly rows: number;
/** Every pane, in authored order, which is also row-major order. */
- readonly cells: readonly TerminalGridCell[];
+ readonly cells: readonly GridCell[];
}
/** One pane's placeable facts, once its title has been resolved. */
@@ -58,10 +58,7 @@ export interface PlacedPane {
* the last row unused. Nothing is reordered, padded, or balanced — the author's
* order is the layout, and a pane's ordinal is its identity wherever it lands.
*/
-export function terminalGridLayout(
- columns: number,
- panes: readonly PlacedPane[],
-): TerminalGridLayout {
+export function gridLayout(columns: number, panes: readonly PlacedPane[]): GridLayout {
return {
columns,
rows: Math.ceil(panes.length / columns),
diff --git a/packages/terminal/src/native-launcher.ts b/packages/grid/src/native-launcher.ts
similarity index 100%
rename from packages/terminal/src/native-launcher.ts
rename to packages/grid/src/native-launcher.ts
diff --git a/packages/terminal/src/pane-launcher.ts b/packages/grid/src/pane-launcher.ts
similarity index 95%
rename from packages/terminal/src/pane-launcher.ts
rename to packages/grid/src/pane-launcher.ts
index 0426800d9..628c3b5ad 100644
--- a/packages/terminal/src/pane-launcher.ts
+++ b/packages/grid/src/pane-launcher.ts
@@ -1,6 +1,6 @@
/**
* How a native UI reaches a pane's terminal instead of the run's
- * (architecture.md §Terminal authority, spec §Terminal-grid composition).
+ * (architecture.md §Terminal authority, spec §Grid composition).
*
* `` written at the root takes the one foreground-terminal
* lease, and every other launch waits for it. Written inside a pane it must
@@ -25,7 +25,7 @@ import type { Operation } from "effection";
import { NativeLauncher } from "./native-launcher.ts";
import type { NativeLaunchOutcome, NativeLaunchRequest } from "./native-launcher.ts";
-import type { TerminalPaneClaim } from "./authority.ts";
+import type { PaneClaim } from "./authority.ts";
/**
* Install one pane's native launcher for the scope that runs that pane's work.
@@ -47,7 +47,7 @@ export type RunInPane = (
) => Operation;
export function* usePaneNativeLauncher(
- claim: TerminalPaneClaim,
+ claim: PaneClaim,
flush: () => Operation,
runInPane: RunInPane,
): Operation {
diff --git a/packages/terminal/src/pane.ts b/packages/grid/src/pane.ts
similarity index 95%
rename from packages/terminal/src/pane.ts
rename to packages/grid/src/pane.ts
index f308de81b..603020f6c 100644
--- a/packages/terminal/src/pane.ts
+++ b/packages/grid/src/pane.ts
@@ -19,7 +19,7 @@
import { createContext } from "effection";
import type { Context, Operation } from "effection";
-import type { TerminalPaneClaim } from "./authority.ts";
+import type { PaneClaim } from "./authority.ts";
/** The pane the current work is running in. */
export interface PaneTerminal {
@@ -56,7 +56,7 @@ export function paneTerminal(): Operation {
* because panes do not nest. A grid written inside a pane is refused by the
* grammar, so the value a pane's scope holds is always its own.
*/
-export function* usePaneTerminal(claim: TerminalPaneClaim): Operation {
+export function* usePaneTerminal(claim: PaneClaim): Operation {
yield* PaneTerminalContext.set({
ordinal: claim.ordinal,
interactive(body) {
diff --git a/packages/terminal/src/posix-launcher.ts b/packages/grid/src/posix-launcher.ts
similarity index 99%
rename from packages/terminal/src/posix-launcher.ts
rename to packages/grid/src/posix-launcher.ts
index 75d6029ee..4f02f4968 100644
--- a/packages/terminal/src/posix-launcher.ts
+++ b/packages/grid/src/posix-launcher.ts
@@ -5,7 +5,7 @@
* the only one that reaches a process. It lives apart from that contract
* because a consumer that merely describes a launch must not load
* `node:child_process` to do it: the package root exports the contract, and
- * this module is reachable only through `@executablemd/terminal/posix`.
+ * this module is reachable only through `@executablemd/grid/posix`.
*
* XMD stays the parent. It does not replace itself with the child, because a
* process that has execed away cannot cancel the document, reap the child, own
diff --git a/packages/terminal/src/posix-processes.ts b/packages/grid/src/posix-processes.ts
similarity index 100%
rename from packages/terminal/src/posix-processes.ts
rename to packages/grid/src/posix-processes.ts
diff --git a/packages/terminal/src/processes.ts b/packages/grid/src/processes.ts
similarity index 98%
rename from packages/terminal/src/processes.ts
rename to packages/grid/src/processes.ts
index f962110b7..009b65297 100644
--- a/packages/terminal/src/processes.ts
+++ b/packages/grid/src/processes.ts
@@ -1,9 +1,9 @@
/**
* What the host can observe about processes and terminals
- * (architecture.md §Interactive terminal grids, "there is no implicit grid
+ * (architecture.md §Interactive grids, "there is no implicit grid
* timeout").
*
- * A terminal grid may not report a pane settled, admit the next launch into it,
+ * A grid may not report a pane settled, admit the next launch into it,
* or let the document continue while something a launch started can still act.
* Deciding that is not a matter of having sent a signal: a PID, a successful
* delivery, an attach client going away and an elapsed timeout each prove
diff --git a/packages/terminal/src/provider-api.ts b/packages/grid/src/provider-api.ts
similarity index 66%
rename from packages/terminal/src/provider-api.ts
rename to packages/grid/src/provider-api.ts
index f3537dd99..a28dc14a2 100644
--- a/packages/terminal/src/provider-api.ts
+++ b/packages/grid/src/provider-api.ts
@@ -1,5 +1,5 @@
/**
- * How a terminal provider is installed, and what installing one grants.
+ * How a grid provider is installed, and what installing one grants.
*
* A provider is the only thing that can present a grid, so *selecting* one is
* itself an authority decision. Returning a factory up the public chain would
@@ -29,35 +29,35 @@ import { type Api, createApi } from "@effectionx/context-api";
import { ensure } from "effection";
import type { Operation } from "effection";
-import type { TerminalGridAuthority } from "./authority.ts";
+import type { GridAuthority } from "./authority.ts";
/** What a host says about the provider it is installing. */
-export interface TerminalProviderOptions {
+export interface GridProviderOptions {
/** How the provider names itself in provider-neutral diagnostics. */
readonly label: string;
}
/**
- * A provider factory installs `TerminalGrids` middleware for its scope.
+ * A provider factory installs `Grids` middleware for its scope.
*
* The authority is the second argument because it is delivered, not published:
* there is no reader for it, no context holding one, and no request member
* carrying one. A factory closes over it, and only the handler that closed over
* it can pair a routed grid request with it.
*/
-export type TerminalProviderFactory = (
- options: TerminalProviderOptions,
- authority: TerminalGridAuthority,
+export type GridProviderFactory = (
+ options: GridProviderOptions,
+ authority: GridAuthority,
) => Operation;
/** The stable name every loaded copy composes through. */
-export const TERMINAL_PROVIDERS_API = "TerminalProviders";
+export const GRID_PROVIDERS_API = "GridProviders";
/** What public installation middleware sees: the name, and what it runs under. */
-export interface TerminalProviderInstallRequest {
+export interface GridProviderInstallRequest {
readonly intent: "install";
readonly name: string;
- readonly options: TerminalProviderOptions;
+ readonly options: GridProviderOptions;
}
/**
@@ -68,23 +68,23 @@ export interface TerminalProviderInstallRequest {
* own terminal through the continuation it captured; constructing one grants
* nothing, because the terminal is reachable from that continuation alone.
*/
-export type TerminalProviderCall =
- | TerminalProviderInstallRequest
- | { readonly intent: "inspect"; readonly install: TerminalProviderInstallRequest }
- | { readonly intent: "acknowledge"; readonly install: TerminalProviderInstallRequest };
+export type GridProviderCall =
+ | GridProviderInstallRequest
+ | { readonly intent: "inspect"; readonly install: GridProviderInstallRequest }
+ | { readonly intent: "acknowledge"; readonly install: GridProviderInstallRequest };
-export interface TerminalProviderApi {
+export interface GridProviderApi {
/**
* Install one provider.
*
* Answers nothing: a return value is not evidence a provider was installed,
* and the invocation that issued the request ignores it.
*/
- install(call: TerminalProviderCall): Operation;
+ install(call: GridProviderCall): Operation;
}
-export class TerminalProviderInstallError extends Error {
- override name = "TerminalProviderInstallError";
+export class GridProviderInstallError extends Error {
+ override name = "GridProviderInstallError";
}
/**
@@ -93,35 +93,29 @@ export class TerminalProviderInstallError extends Error {
* Invoking this descriptor with a captured request outside a live installation
* reaches this default and installs nothing.
*/
-export const TerminalProviders: Api = createApi(
- TERMINAL_PROVIDERS_API,
- {
- // deno-lint-ignore require-yield
- *install(call: TerminalProviderCall): Operation {
- const name = call.intent === "install" ? call.name : call.install.name;
- throw new TerminalProviderInstallError(`Unknown terminal provider "${name}"`);
- },
+export const GridProviders: Api = createApi(GRID_PROVIDERS_API, {
+ // deno-lint-ignore require-yield
+ *install(call: GridProviderCall): Operation {
+ const name = call.intent === "install" ? call.name : call.install.name;
+ throw new GridProviderInstallError(`Unknown grid provider "${name}"`);
},
-);
+});
/** Make `factory` installable as `name` for the current scope. */
-export function* registerTerminalProvider(
- name: string,
- factory: TerminalProviderFactory,
-): Operation {
+export function* registerGridProvider(name: string, factory: GridProviderFactory): Operation {
let registered = true;
yield* ensure(() => {
registered = false;
});
- yield* TerminalProviders.around(
+ yield* GridProviders.around(
{
*install([call], next): Operation {
if (call.intent !== "install" || call.name !== name) {
return yield* next(call);
}
if (!registered) {
- throw new TerminalProviderInstallError(
- `the "${name}" terminal provider registration is no longer live`,
+ throw new GridProviderInstallError(
+ `the "${name}" grid provider registration is no longer live`,
);
}
// Inspection first, and through the captured continuation: the terminal
@@ -145,34 +139,30 @@ export function* registerTerminalProvider(
* value, and reading it as a delivery is this side's decision.
*/
function deliveryOf(value: unknown): {
- options: TerminalProviderOptions;
- authority: TerminalGridAuthority;
+ options: GridProviderOptions;
+ authority: GridAuthority;
} {
if (typeof value !== "object" || value === null) {
- throw new TerminalProviderInstallError(
- "this terminal provider installation is not live, so nothing was delivered to it",
+ throw new GridProviderInstallError(
+ "this grid provider installation is not live, so nothing was delivered to it",
);
}
const options = Reflect.get(value, "options");
const authority = Reflect.get(value, "authority");
if (typeof options !== "object" || options === null) {
- throw new TerminalProviderInstallError(
- "the live terminal provider installation named no options",
- );
+ throw new GridProviderInstallError("the live grid provider installation named no options");
}
if (typeof authority !== "object" || authority === null) {
- throw new TerminalProviderInstallError(
- "the live terminal provider installation carried no authority",
- );
+ throw new GridProviderInstallError("the live grid provider installation carried no authority");
}
const label = Reflect.get(options, "label");
if (typeof label !== "string") {
- throw new TerminalProviderInstallError("the live terminal provider options are not readable");
+ throw new GridProviderInstallError("the live grid provider options are not readable");
}
const present = Reflect.get(authority, "present");
if (typeof present !== "function") {
- throw new TerminalProviderInstallError(
- "the live terminal provider installation carried no grid authority",
+ throw new GridProviderInstallError(
+ "the live grid provider installation carried no grid authority",
);
}
return {
@@ -192,13 +182,13 @@ function deliveryOf(value: unknown): {
* provider, and this refuses rather than leaving the caller believing one is
* there.
*/
-export function installTerminalProvider(
+export function installGridProvider(
name: string,
- options: TerminalProviderOptions,
- authority: TerminalGridAuthority,
+ options: GridProviderOptions,
+ authority: GridAuthority,
): Operation {
return (function* (): Operation {
- const request: TerminalProviderInstallRequest = Object.freeze({
+ const request: GridProviderInstallRequest = Object.freeze({
intent: "install",
name,
options: Object.freeze({ ...options }),
@@ -207,13 +197,13 @@ export function installTerminalProvider(
// Same stable name, so the shared middleware chain applies; own descriptor,
// so the chain ends in this invocation's terminal rather than in the public
// refusing default.
- const invocation = createApi(TERMINAL_PROVIDERS_API, {
+ const invocation = createApi(GRID_PROVIDERS_API, {
install: terminal.install,
});
yield* invocation.operations.install(request);
if (!terminal.acknowledged()) {
- throw new TerminalProviderInstallError(
- `the "${name}" terminal provider did not install — a handler answered without ` +
+ throw new GridProviderInstallError(
+ `the "${name}" grid provider did not install — a handler answered without ` +
`delivering the request to a registered provider`,
);
}
@@ -222,11 +212,11 @@ export function installTerminalProvider(
}
function installationTerminal(
- request: TerminalProviderInstallRequest,
- options: TerminalProviderOptions,
- authority: TerminalGridAuthority,
+ request: GridProviderInstallRequest,
+ options: GridProviderOptions,
+ authority: GridAuthority,
): {
- install: (call: TerminalProviderCall) => Operation;
+ install: (call: GridProviderCall) => Operation;
acknowledged: () => boolean;
close: () => void;
} {
@@ -234,30 +224,30 @@ function installationTerminal(
return {
// deno-lint-ignore require-yield
- *install(call: TerminalProviderCall): Operation {
+ *install(call: GridProviderCall): Operation {
if (call.intent === "install") {
// Reaching the terminal means no registered provider consumed it.
- throw new TerminalProviderInstallError(`Unknown terminal provider "${call.name}"`);
+ throw new GridProviderInstallError(`Unknown grid provider "${call.name}"`);
}
// Object identity, not shape: a request rebuilt with the same members
// describes the same ask and authorizes nothing.
if (!Object.is(call.install, request)) {
- throw new TerminalProviderInstallError(
- "the live terminal provider installation received a copied, substituted or foreign request",
+ throw new GridProviderInstallError(
+ "the live grid provider installation received a copied, substituted or foreign request",
);
}
if (call.intent === "inspect") {
if (state !== "available") {
- throw new TerminalProviderInstallError(
- "this terminal provider installation is reused, completed or stale",
+ throw new GridProviderInstallError(
+ "this grid provider installation is reused, completed or stale",
);
}
state = "inspected";
return { options, authority };
}
if (state !== "inspected") {
- throw new TerminalProviderInstallError(
- "this terminal provider acknowledgement is unsolicited, duplicated or stale",
+ throw new GridProviderInstallError(
+ "this grid provider acknowledgement is unsolicited, duplicated or stale",
);
}
state = "acknowledged";
diff --git a/packages/terminal/testing.ts b/packages/grid/testing.ts
similarity index 85%
rename from packages/terminal/testing.ts
rename to packages/grid/testing.ts
index 81b12e0be..7566419c6 100644
--- a/packages/terminal/testing.ts
+++ b/packages/grid/testing.ts
@@ -14,9 +14,9 @@
export { installControlledLauncher } from "./src/controlled-launcher.ts";
export type { ControlledLauncherOptions } from "./src/controlled-launcher.ts";
-export { prepareControlledComposite, terminalProviderLog } from "./src/controlled-composite.ts";
+export { prepareControlledComposite, gridProviderLog } from "./src/controlled-composite.ts";
export type {
ControlledCompositeOptions,
- TerminalProviderLog,
- TerminalProviderResources,
+ GridProviderLog,
+ GridProviderResources,
} from "./src/controlled-composite.ts";
diff --git a/packages/terminal/tests/terminal-provider.test.ts b/packages/grid/tests/grid-provider.test.ts
similarity index 83%
rename from packages/terminal/tests/terminal-provider.test.ts
rename to packages/grid/tests/grid-provider.test.ts
index 65a54accc..1dbd9e147 100644
--- a/packages/terminal/tests/terminal-provider.test.ts
+++ b/packages/grid/tests/grid-provider.test.ts
@@ -1,5 +1,5 @@
/**
- * Tier TG — the terminal grid routing surface and the composite contract
+ * Tier TG — the grid routing surface and the composite contract
* (architecture.md §Terminal authority, spec §6.21).
*
* Two things live here, and neither is an authority. The routing surface is
@@ -9,7 +9,7 @@
* ordering — prepared hidden, attached once, destroyed exactly once.
*
* Who may present a grid, and what presenting one authorizes, is core's, and is
- * proved in `packages/core/tests/terminal-grid.test.ts`.
+ * proved in `packages/core/tests/grid.test.ts`.
*
* Nothing here opens a terminal, looks for a multiplexer, or starts a process.
*/
@@ -20,15 +20,15 @@ import { scoped } from "effection";
import type { Operation } from "effection";
import {
- TERMINAL_PROVIDER_UNAVAILABLE,
- TerminalGrids,
- TerminalProviderUnavailableError,
+ GRID_PROVIDER_UNAVAILABLE,
+ Grids,
+ GridProviderUnavailableError,
} from "../src/composite.ts";
-import type { TerminalGridRequest } from "../src/composite.ts";
-import { prepareControlledComposite, terminalProviderLog } from "../src/controlled-composite.ts";
+import type { GridRequest } from "../src/composite.ts";
+import { prepareControlledComposite, gridProviderLog } from "../src/controlled-composite.ts";
/** A two-by-one grid: the smallest request that still has two ordinals. */
-function request(overrides: Partial = {}): TerminalGridRequest {
+function request(overrides: Partial = {}): GridRequest {
return {
columns: 2,
rows: 1,
@@ -45,21 +45,21 @@ describe("Tier TG — the routing surface", () => {
let refusal: unknown;
yield* scoped(function* () {
try {
- yield* TerminalGrids.operations.open(request());
+ yield* Grids.operations.open(request());
} catch (error) {
refusal = error;
}
});
- expect(refusal).toBeInstanceOf(TerminalProviderUnavailableError);
- expect(refusal instanceof Error ? refusal.message : "").toBe(TERMINAL_PROVIDER_UNAVAILABLE);
+ expect(refusal).toBeInstanceOf(GridProviderUnavailableError);
+ expect(refusal instanceof Error ? refusal.message : "").toBe(GRID_PROVIDER_UNAVAILABLE);
});
it("TP2: middleware observes a delegated request without changing it", function* () {
- const seen: TerminalGridRequest[] = [];
- const reached: TerminalGridRequest[] = [];
+ const seen: GridRequest[] = [];
+ const reached: GridRequest[] = [];
yield* scoped(function* () {
- yield* TerminalGrids.around(
+ yield* Grids.around(
{
// deno-lint-ignore require-yield
*open([asked]) {
@@ -70,13 +70,13 @@ describe("Tier TG — the routing surface", () => {
// The terminal end of the chain, where a registered provider sits.
{ at: "min" },
);
- yield* TerminalGrids.around({
+ yield* Grids.around({
*open([asked], next) {
seen.push(asked);
return yield* next(asked);
},
});
- yield* TerminalGrids.operations.open(request({ columns: 3, rows: 2 }));
+ yield* Grids.operations.open(request({ columns: 3, rows: 2 }));
});
expect(seen).toHaveLength(1);
@@ -86,9 +86,9 @@ describe("Tier TG — the routing surface", () => {
});
it("TP2: middleware narrows a request before anything below sees it", function* () {
- const reached: TerminalGridRequest[] = [];
+ const reached: GridRequest[] = [];
yield* scoped(function* () {
- yield* TerminalGrids.around(
+ yield* Grids.around(
{
// deno-lint-ignore require-yield
*open([asked]) {
@@ -99,12 +99,12 @@ describe("Tier TG — the routing surface", () => {
// The terminal end of the chain, where a registered provider sits.
{ at: "min" },
);
- yield* TerminalGrids.around({
+ yield* Grids.around({
*open([asked], next) {
return yield* next({ ...asked, columns: 1, rows: asked.panes.length });
},
});
- yield* TerminalGrids.operations.open(request());
+ yield* Grids.operations.open(request());
});
expect(reached[0]?.columns).toBe(1);
@@ -112,10 +112,10 @@ describe("Tier TG — the routing surface", () => {
});
it("TP2: middleware refuses a request, and nothing below is reached", function* () {
- const reached: TerminalGridRequest[] = [];
+ const reached: GridRequest[] = [];
let refusal: unknown;
yield* scoped(function* () {
- yield* TerminalGrids.around(
+ yield* Grids.around(
{
// deno-lint-ignore require-yield
*open([asked]) {
@@ -126,29 +126,27 @@ describe("Tier TG — the routing surface", () => {
// The terminal end of the chain, where a registered provider sits.
{ at: "min" },
);
- yield* TerminalGrids.around({
+ yield* Grids.around({
// deno-lint-ignore require-yield
*open(): Operation {
- throw new Error("this host does not open terminal grids");
+ throw new Error("this host does not open grids");
},
});
try {
- yield* TerminalGrids.operations.open(request());
+ yield* Grids.operations.open(request());
} catch (error) {
refusal = error;
}
});
- expect(refusal instanceof Error ? refusal.message : "").toBe(
- "this host does not open terminal grids",
- );
+ expect(refusal instanceof Error ? refusal.message : "").toBe("this host does not open grids");
expect(reached).toEqual([]);
});
});
describe("Tier TG — the composite contract", () => {
it("TP3: a prepared composite presents nothing until it is attached", function* () {
- const log = terminalProviderLog();
+ const log = gridProviderLog();
const events = yield* scoped(function* () {
yield* prepareControlledComposite(request(), { log });
return [...log.events];
@@ -161,7 +159,7 @@ describe("Tier TG — the composite contract", () => {
});
it("TP3: attach, update, display, shell and destroy record in order", function* () {
- const log = terminalProviderLog();
+ const log = gridProviderLog();
const spawns: number[] = [];
yield* scoped(function* () {
const composite = yield* prepareControlledComposite(request(), { log });
@@ -209,7 +207,7 @@ describe("Tier TG — the composite contract", () => {
});
it("TP4: a preparation failure leaves no composite to tear down", function* () {
- const log = terminalProviderLog();
+ const log = gridProviderLog();
let refusal: unknown;
yield* scoped(function* () {
try {
@@ -251,7 +249,7 @@ describe("Tier TG — the composite contract", () => {
});
it("TP5: each preparation is its own composite", function* () {
- const log = terminalProviderLog();
+ const log = gridProviderLog();
yield* scoped(function* () {
const first = yield* prepareControlledComposite(request(), { log }, 0);
const second = yield* prepareControlledComposite(request(), { log }, 1);
diff --git a/packages/terminal/tests/native-launcher.test.ts b/packages/grid/tests/native-launcher.test.ts
similarity index 99%
rename from packages/terminal/tests/native-launcher.test.ts
rename to packages/grid/tests/native-launcher.test.ts
index 76f4584ea..4662e2db0 100644
--- a/packages/terminal/tests/native-launcher.test.ts
+++ b/packages/grid/tests/native-launcher.test.ts
@@ -28,10 +28,9 @@ import {
nativeLaunch,
NativeLauncher,
NO_TERMINAL,
- reap,
reserveTerminal,
} from "../src/native-launcher.ts";
-import { installForegroundLauncher } from "../src/posix-launcher.ts";
+import { installForegroundLauncher, reap } from "../src/posix-launcher.ts";
const SENTINEL = "SENTINEL-PREPARED-CONTEXT-4b17";
diff --git a/packages/grid/tests/package-boundary.test.ts b/packages/grid/tests/package-boundary.test.ts
new file mode 100644
index 000000000..9aa1c51ba
--- /dev/null
+++ b/packages/grid/tests/package-boundary.test.ts
@@ -0,0 +1,709 @@
+/**
+ * Tier TG21 — the grid package boundary, the vocabulary it replaced, and the
+ * technical vocabulary it kept (architecture.md §Package ownership, DEC-016).
+ *
+ * The stack has not merged, so `Terminal.Grid`, ``, the terminal
+ * exports that used to sit in runtime, core and CLI, and the
+ * `@executablemd/terminal` packages were never a compatibility surface — they
+ * were the naming this rename removes. They are gone, and these rows are what
+ * keeps them gone.
+ *
+ * Five claims, each failing differently if the rename regresses.
+ *
+ * Structural: the dependency arrows point at the neutral domain, so a provider
+ * can be written without CLI or tmux and the domain consumed without either.
+ * A violation is an import statement, so the evidence is the import statements
+ * themselves — read from the production sources rather than inferred from a
+ * manifest, because a manifest records what was declared and a source records
+ * what is actually reached.
+ *
+ * Absence: the old directories, modules, exports, packages and authored names
+ * are not merely unused but not there. An unused forwarding barrel is exactly
+ * the thing that lets an import drift back, and a reserved alias is exactly
+ * what lets an author keep writing the rejected syntax.
+ *
+ * Discrimination: every absence row above is a claim over a set that could be
+ * empty for the wrong reason. One row plants each rejected name into the very
+ * scanners the others use and requires them to report it.
+ *
+ * Exactness: the public roots are pinned as complete sets rather than as
+ * required names, because a name that reached a root by being added to it is
+ * what a required-names check lets stay.
+ *
+ * Preservation: a terminal is still a real capability. `NO_TERMINAL`,
+ * `reserveTerminal`, `PaneTerminal` and `TerminalProcesses` describe a PTY, a
+ * lease and a process boundary, and this rename keeps every one of them — so
+ * the rows below prove they are still reachable while the presentation names
+ * that were rejected are not.
+ */
+import { describe, it } from "@executablemd/test-support/bdd";
+import { expect } from "@executablemd/test-support/expect";
+import { exists, readTextFile } from "@effectionx/fs";
+import { readdir } from "node:fs/promises";
+import * as path from "node:path";
+import { until } from "effection";
+import type { Operation } from "effection";
+
+/**
+ * What a canonical grid name used to be called.
+ *
+ * Derived rather than written out, so this file states the rejected spelling
+ * nowhere and the scans below can read it like any other source without
+ * reporting themselves.
+ */
+function rejected(canonical: string): string {
+ return canonical.replaceAll("grid", "terminal").replaceAll("Grid", "Terminal");
+}
+
+/** The two packages this rename replaced, by specifier and by directory. */
+const REJECTED_PACKAGES = [
+ rejected("@executablemd/grid-tmux"),
+ rejected("@executablemd/grid"),
+ rejected("packages/grid-tmux"),
+ rejected("packages/grid"),
+] as const;
+
+/** The authored names this rename replaced: `Terminal.Grid` and `Terminal`. */
+const REJECTED_CONSTRUCTS = [`${rejected("Grid")}.Grid`, rejected("Grid")] as const;
+
+/**
+ * Everything one entrypoint loads, transitively.
+ *
+ * Read from the module graph rather than from the entrypoint's own export
+ * list, because an export list is exactly what hid this: re-exporting three
+ * names out of a module that also spawns processes narrows what is *reachable
+ * by name* and nothing about what is *loaded*. A facade passes an export-shape
+ * check and fails this one.
+ */
+function* graphOf(entrypoint: string): Operation {
+ const seen = new Set();
+ const pending = [path.resolve("packages/grid", entrypoint)];
+ while (pending.length > 0) {
+ const file = pending.pop();
+ if (file === undefined || seen.has(file)) {
+ continue;
+ }
+ seen.add(file);
+ const source = yield* readTextFile(file);
+ for (const match of source.matchAll(/from\s+"([^"]+)"/g)) {
+ const specifier = match[1];
+ if (specifier === undefined) {
+ continue;
+ }
+ if (specifier.startsWith("node:")) {
+ seen.add(specifier);
+ continue;
+ }
+ if (specifier.startsWith(".")) {
+ pending.push(path.resolve(path.dirname(file), specifier));
+ }
+ }
+ }
+ return [...seen];
+}
+
+/**
+ * Trees that are an installer's rather than this repository's.
+ *
+ * `node_modules` has to go, and not only for speed: a workspace install links
+ * every dependency package under its dependents, so `packages/grid-tmux/
+ * node_modules/@executablemd/grid/src/...` is the *same file* reached through a
+ * link. Walking it would count one definition many times and would read a
+ * vendored copy's imports as if they were the importing package's own — so a
+ * package would appear to import whatever its dependencies import. Bun's layout
+ * creates those links and Deno's does not, which is why this was invisible
+ * until the Bun shard ran.
+ */
+const INSTALLED = new Set(["node_modules", "npm", "dist", "generated", "vendor"]);
+
+/** Whether any segment of `relative` names a tree this repository does not author. */
+function installed(relative: string): boolean {
+ return relative.split(path.sep).some((segment) => INSTALLED.has(segment));
+}
+
+/** Every production source of one workspace package, tests excluded. */
+function* productionSources(pkg: string): Operation {
+ const root = path.resolve("packages", pkg);
+ const files: string[] = [];
+ const entries = yield* until(readdir(root, { recursive: true, withFileTypes: true }));
+ for (const entry of entries) {
+ if (!entry.isFile() || !entry.name.endsWith(".ts")) {
+ continue;
+ }
+ const full = path.join(entry.parentPath ?? root, entry.name);
+ const relative = path.relative(root, full);
+ if (installed(relative)) {
+ continue;
+ }
+ // Tests prove the contract; they do not define the shipped graph. A row may
+ // reach across packages to drive a fixture without that being a dependency
+ // of the artifact.
+ if (relative.startsWith("tests/") || relative.includes(".test.")) {
+ continue;
+ }
+ files.push(full);
+ }
+ return files;
+}
+
+/** The package specifiers one source imports from, bare names only. */
+function specifiersOf(source: string): string[] {
+ const found: string[] = [];
+ for (const match of source.matchAll(/(?:^|\n)\s*(?:import|export)[^;]*?from\s+"([^"]+)"/g)) {
+ const specifier = match[1];
+ if (specifier !== undefined && !specifier.startsWith(".")) {
+ found.push(specifier);
+ }
+ }
+ return found;
+}
+
+/** Which workspace packages `pkg`'s production code actually imports. */
+function* importsOf(pkg: string): Operation> {
+ const reached = new Set();
+ for (const file of yield* productionSources(pkg)) {
+ for (const specifier of specifiersOf(yield* readTextFile(file))) {
+ if (specifier.startsWith("@executablemd/")) {
+ // `@executablemd/grid/posix` is the grid package.
+ reached.add(specifier.split("/").slice(0, 2).join("/"));
+ }
+ }
+ }
+ return reached;
+}
+
+/** Every `.ts` file in the repository's packages, tests included. */
+function* everySource(): Operation {
+ const root = path.resolve("packages");
+ const files: string[] = [];
+ const entries = yield* until(readdir(root, { recursive: true, withFileTypes: true }));
+ for (const entry of entries) {
+ if (!entry.isFile() || !entry.name.endsWith(".ts")) {
+ continue;
+ }
+ const full = path.join(entry.parentPath ?? root, entry.name);
+ if (installed(path.relative(root, full))) {
+ continue;
+ }
+ files.push(full);
+ }
+ return files;
+}
+
+/**
+ * The declared dependency state a rejected package name could survive in.
+ *
+ * A source that imports a deleted package fails loudly; a manifest, a lockfile
+ * or the generated publication workflow that still names one fails nothing at
+ * all until a release runs, which is why they are read here by name.
+ */
+function* declaredState(): Operation {
+ const files = [
+ "deno.json",
+ "deno.lock",
+ "package.json",
+ "pnpm-lock.yaml",
+ "bun.lock",
+ ".github/workflows/publish-packages.yml",
+ ];
+ const present: string[] = [];
+ for (const file of files) {
+ if (yield* exists(path.resolve(file))) {
+ present.push(file);
+ }
+ }
+ const root = path.resolve("packages");
+ const entries = yield* until(readdir(root, { recursive: true, withFileTypes: true }));
+ for (const entry of entries) {
+ if (!entry.isFile() || (entry.name !== "package.json" && entry.name !== "deno.json")) {
+ continue;
+ }
+ const full = path.join(entry.parentPath ?? root, entry.name);
+ if (installed(path.relative(root, full))) {
+ continue;
+ }
+ present.push(path.relative(path.resolve("."), full));
+ }
+ return present;
+}
+
+/** Where a rejected package name appears in the given text. */
+function namesRejectedPackage(text: string): string[] {
+ return REJECTED_PACKAGES.filter((name) => text.includes(name));
+}
+
+/** The names the grid domain owns, whatever path someone might reach for. */
+const GRID_EXPORTS = [
+ "NativeLauncher",
+ "nativeLaunch",
+ "reserveTerminal",
+ "flushOutput",
+ "installForegroundLauncher",
+ "installControlledLauncher",
+ "Grids",
+ "GridProviders",
+ "TerminalProcesses",
+ "registerGridProvider",
+ "installGridProvider",
+ "useGridInstallation",
+ "paneTerminal",
+ "prepareControlledComposite",
+ "gridProviderLog",
+ "installDenoTerminalProcesses",
+ "processTable",
+ "processReachable",
+] as const;
+
+/**
+ * Presentation names the rename rejected.
+ *
+ * Every one of them described the grid, the pane request, the provider or the
+ * lifecycle — never a PTY — so none of them may come back under any facet.
+ */
+const REJECTED_EXPORTS = [
+ "TerminalGrids",
+ "TerminalProviders",
+ "TerminalComposite",
+ "registerTerminalProvider",
+ "installTerminalProvider",
+ "useTerminalInstallation",
+ "createTerminalAuthority",
+ "createTerminalGridClaims",
+ "openTerminalGrid",
+ "terminalGridLayout",
+ "terminalProviderLog",
+ "TerminalProviderUnavailableError",
+ "TerminalProviderInstallError",
+ "TerminalAuthorityError",
+ "TerminalTeardownFailed",
+ "TERMINAL_GRIDS_API",
+ "TERMINAL_PROVIDERS_API",
+ "TERMINAL_PROVIDER_UNAVAILABLE",
+] as const;
+
+describe("Tier TG21 — the grid package boundary", () => {
+ it("TG21a: the neutral domain reaches no engine, host or provider", function* () {
+ const reached = yield* importsOf("grid");
+ // The whole point of the extraction: a provider or a consumer takes the
+ // domain without taking the document engine, the CLI, or tmux with it.
+ for (const forbidden of [
+ "@executablemd/runtime",
+ "@executablemd/core",
+ "@executablemd/cli",
+ "@executablemd/grid-tmux",
+ ]) {
+ expect([forbidden, reached.has(forbidden)]).toEqual([forbidden, false]);
+ }
+ });
+
+ it("TG21b: the tmux adapter reaches the domain and nothing above it", function* () {
+ const reached = yield* importsOf("grid-tmux");
+ expect(reached.has("@executablemd/grid")).toBe(true);
+ for (const forbidden of ["@executablemd/runtime", "@executablemd/core", "@executablemd/cli"]) {
+ expect([forbidden, reached.has(forbidden)]).toEqual([forbidden, false]);
+ }
+ });
+
+ it("TG21c: runtime owns no grid dependency, and only CLI composes both", function* () {
+ // The amendment's load-bearing change: runtime keeps no grid edge at all,
+ // in its sources or its manifest, because there is no unreleased path left
+ // for it to keep alive.
+ expect((yield* importsOf("runtime")).has("@executablemd/grid")).toBe(false);
+ const manifest = yield* readTextFile(path.resolve("packages/runtime/package.json"));
+ expect(manifest.includes("@executablemd/grid")).toBe(false);
+
+ expect((yield* importsOf("core")).has("@executablemd/grid")).toBe(true);
+ // Core is the document engine, not a host: it never selects a provider.
+ expect((yield* importsOf("core")).has("@executablemd/grid-tmux")).toBe(false);
+ const cli = yield* importsOf("cli");
+ for (const required of [
+ "@executablemd/core",
+ "@executablemd/runtime",
+ "@executablemd/grid",
+ "@executablemd/grid-tmux",
+ ]) {
+ expect([required, cli.has(required)]).toEqual([required, true]);
+ }
+ });
+
+ it("TG21i: the neutral entrypoints load no host process code and no fixture", function* () {
+ // The defect this replaced: the root re-exported a handful of neutral names
+ // from a module that also spawned children and carried a test double, so
+ // importing the domain loaded `node:child_process` and a fixture. Selective
+ // re-export narrows the names, never the load.
+ for (const entrypoint of ["mod.ts", "lifecycle.ts", "processes.ts"]) {
+ const graph = yield* graphOf(entrypoint);
+ const host = graph.filter(
+ (module) =>
+ module === "node:child_process" ||
+ module === "node:process" ||
+ module.endsWith("/posix-launcher.ts") ||
+ module.endsWith("/posix-processes.ts"),
+ );
+ const fixtures = graph.filter((module) => module.includes("/controlled-"));
+ expect([entrypoint, host]).toEqual([entrypoint, []]);
+ expect([entrypoint, fixtures]).toEqual([entrypoint, []]);
+ }
+ });
+
+ it("TG21j: the host and fixture facets are where that code actually lives", function* () {
+ // The complement, and the discriminator for the row above: if the split had
+ // simply deleted this code rather than moved it, TG21i would pass over an
+ // empty graph and prove nothing.
+ const posix = yield* graphOf("posix.ts");
+ expect(posix.some((module) => module.endsWith("/posix-launcher.ts"))).toBe(true);
+ expect(posix.some((module) => module.endsWith("/posix-processes.ts"))).toBe(true);
+ expect(posix.includes("node:child_process")).toBe(true);
+
+ // `testing.ts`, not `test.ts`: Deno's own test-file pattern matches a bare
+ // `test.ts`, so an entrypoint by that name would be loaded as a test file.
+ const fixtures = yield* graphOf("testing.ts");
+ expect(fixtures.some((module) => module.endsWith("/controlled-launcher.ts"))).toBe(true);
+ expect(fixtures.some((module) => module.endsWith("/controlled-composite.ts"))).toBe(true);
+ });
+
+ it("TG21l: an installer's linked copies are not read as a package's own source", function* () {
+ // A workspace install links each dependency under its dependents, so the
+ // same file is reachable at `packages/