Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 75 additions & 10 deletions architecture.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -2917,16 +2917,48 @@ The grid runs as one structured scope:
6. Once attached, each pane settles independently and keeps its final status
visible while siblings continue. The composite remains present after all
panes settle until the reader closes or leaves it.
7. Closing begins an ordered teardown: prevent new pane launches, cancel live
pane scopes, await every child and finalizer, detach and destroy the exact
provider composite, restore the root terminal, and only then release the
foreground lease and settle the grid. The document never continues while an
observable pane child or provider-owned process can still act through the
grid.
7. Reader close first crosses a live close boundary, then begins an ordered
teardown: prevent new pane launches, ask live pane children to close, await
every child and finalizer, detach and destroy the exact provider composite,
restore the root terminal, and only then release the foreground lease and
settle the grid. The document never continues while an observable pane child
or provider-owned process can still act through the grid.

The provider's `closed()` settlement proposes the live close boundary. The
boundary is crossed when the grid owner has entered a cancellation-deferred
await of the grid's durable child and acknowledges that proposal; only then may
the child signal pane close. That await ends only when the task has settled and
its durable `Close` has been acknowledged, not when the grid body has merely
chosen an outcome. This handshake has no provider identity and is not itself
journaled.

Reader-close intent becomes durable only as that completed grid `Close`, after
pane and provider teardown. There is no standalone durable "closing" state. The
gap between observing close and committing it is safe because ordinary parent
cancellation is held pending across the whole gap. A cancellation that arrives
before the owner acknowledges the close boundary cancels the active grid. One that arrives
afterward does not rewrite grid or pane outcomes: panes already settled keep
their outcomes, each then-live pane completes its own scope and retains
`closed`, and the grid retains the same `reader` or `failed` result it would
have retained without the cancellation. Once the grid child is durably closed,
the pending cancellation is delivered to the parent, so no following document
sibling runs in that attempt. A fatal or cleanup failure still takes its
existing precedence over cancellation.

Pane work and every finalizer it installs live inside that pane's durable child
scope. Reader close is cooperative at the durable boundary: it asks the pane to
close and awaits it; it never halts the pane's durable task. The pane may stop
its live nested work as part of its own scope teardown, but its durable child
does not settle as `closed` or write `Close(ok)` until that work and its finalizers
have settled. This preserves the pane's ordinal-derived identity and never
turns a deliberate reader close into a caller-cancelled durable child that a
later run could revive or wait on forever.

Parent cancellation follows the same teardown from preparation, readiness, or
the active grid and remains cancellation. A provider or host failure cancels
the whole grid and is the grid's canonical failure. An ordinary pane failure
the active grid and remains cancellation. Once reader close has crossed its
live boundary, the close result is committed first and that cancellation is
observed by the parent afterward. A provider or host failure cancels the whole
grid and is the grid's canonical failure. An ordinary pane failure
after attachment is contained as that pane's status and does not cancel its
siblings. When the reader closes the grid, core fails it with the first failed
pane in authored order; cancellation initiated by grid teardown is not a pane
Expand DownExpand Up@@ -2977,8 +3009,41 @@ a terminal provider, starting a shell, expanding pane content, acquiring an
Agent session, or launching a native UI. The structured durable boundary owns
that short circuit; a public replay context does not.

Partial replay first compares the exact authored layout and refuses divergence
before provider work. It rebuilds a fresh provider composite: completed pane
The reader-close handshake makes cancellation during teardown a completed-grid
case rather than a new partial-replay state. When a pane finalizer delays close
and parent cancellation arrives, the first attempt still finishes every pane
and provider finalizer, writes the pane outcomes and completed grid `Close`, and
only then reports cancellation to its parent. A continuation claims that
completed child and resumes after it without recreating the provider or
re-entering pane work. A host loss can still interrupt the unjournaled live
teardown; panes whose `Close` was acknowledged remain complete, while any pane
and grid without a completed record follow the existing partial-replay rules.

Partial replay compares the **resolved** layout and refuses divergence before
provider work.

What that can and cannot cover follows from where a resumed run gets its
document. A continuation executes the root the journal retained: the source the
new invocation supplies is not read, not compared and not refused. So the
authored structure of a grid — how many panes it has, their order, and whether
each was written paired or self-closing — is fixed for the whole life of a
journal, and cannot differ between runs. Comparing it would compare a value with
itself.

What can still differ is everything the retained source *resolves*: `columns`
and each `title` are expressions, and props are not restored across a
continuation, so a prop-borne or otherwise live value produces a different
resolved layout from the same retained document. Those are what the comparison
is for, and a change in either refuses before the foreground lease is taken and
before any provider is contacted.

Authored-structure change is therefore not a grid concern. A document whose body
changed under an existing journal is a root-definition compatibility question —
the retained root stays authoritative, and deciding whether a changed source
should be refused rather than ignored belongs to a versioned root boundary that
does not exist yet. Until it does, the grid's obligation is the narrower one it
can actually discharge: retain the complete authored structure, and open the
structure it retained rather than the one the file now shows. It rebuilds a fresh provider composite: completed pane
children are restored as settled statuses without re-running their effects,
while incomplete children replay or start their remaining work. An incomplete
`<Session.Launch>` preserves the prepared/detached identity rules of its own
Expand Down
30 changes: 30 additions & 0 deletions packages/core/mod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -152,6 +152,36 @@ 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";
export {
createTerminalAuthority,
createTerminalGridClaims,
TerminalAuthorityError,
terminalInstallation,
useTerminalInstallation,
} from "./src/terminal/authority.ts";
export type {
PaneReadiness,
TerminalGridAuthority,
TerminalGridClaims,
TerminalPaneClaim,
} from "./src/terminal/authority.ts";
export {
installTerminalProvider,
registerTerminalProvider,
TERMINAL_PROVIDERS_API,
TerminalProviderInstallError,
TerminalProviders,
} from "./src/terminal/provider-api.ts";
export type {
TerminalProviderFactory,
TerminalProviderInstallRequest,
TerminalProviderOptions,
} from "./src/terminal/provider-api.ts";
export { installTerminalGridProfile } from "./src/terminal/profile.ts";
export type { TerminalGridProfileOptions } from "./src/terminal/profile.ts";
export { paneTerminal } from "./src/terminal/pane.ts";
export type { PaneTerminal } from "./src/terminal/pane.ts";
export type { PaneStatus, RetainedGrid, RetainedPaneOutcome } from "./src/terminal/grid.ts";

export { execute, Execution } from "./src/execute.ts";
export type {
Expand Down
166 changes: 138 additions & 28 deletions packages/core/src/expand.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -68,6 +68,10 @@ import {
import type { StructuralViolation, SwitchCase, TerminalPane } from "./structural-rules.ts";
import { terminalGridLayout } from "./terminal-grid.ts";
import type { PlacedPane } from "./terminal-grid.ts";
import { durableGrid, openTerminalGrid, toRequest } from "./terminal/grid.ts";
import type { PaneWork } from "./terminal/grid.ts";
import { recordGridLayout } from "./terminal/journal.ts";
import { usePaneTerminal } from "./terminal/pane.ts";
import {
asBindingViolation,
asExpressionViolation,
Expand DownExpand Up@@ -139,7 +143,7 @@ import {
import { remark } from "remark";
import { select as cssSelect } from "unist-util-select";
import { toString as mdastToString } from "mdast-util-to-string";
import { liveEnvironment } from "./live-env.ts";
import { derivedEnvironment, liveEnvironment } from "./live-env.ts";
import { TestHarnessComponentDefinition } from "./test-harness.ts";
import type { TestHarnessBinding } from "./test-harness.ts";

Expand DownExpand Up@@ -1181,7 +1185,14 @@ function* expandListSegments(
if (segment.name === "Terminal.Grid") {
// No raise() here, like the branches above: expandTerminalGrid
// reports every error it creates.
yield* expandTerminalGrid(segment, result);
yield* expandTerminalGrid(segment, result, {
parentMeta,
parentProps,
hideSet,
path: elementPath,
checkedFailures,
authority,
});
break;
}

Expand DownExpand Up@@ -2102,7 +2113,21 @@ function* resolveStructuralProp(
* does, which is what makes the refusal a closed one rather than a partial grid
* left behind.
*/
function* expandTerminalGrid(segment: ComponentElement, owner: Segment[]): Operation<void> {
/** Everything a pane's own content needs to expand where the grid was written. */
interface GridSite {
readonly parentMeta: Record<string, unknown>;
readonly parentProps: Record<string, Json>;
readonly hideSet: Set<string>;
readonly path: string;
readonly checkedFailures: CheckedFailures | undefined;
readonly authority: ExpansionAuthority | undefined;
}

function* expandTerminalGrid(
segment: ComponentElement,
owner: Segment[],
site: GridSite,
): Operation<void> {
const structure = terminalGridStructure(segment);
if (structure.violations.length > 0) {
for (const violation of structure.violations) {
Expand DownExpand Up@@ -2137,23 +2162,117 @@ function* expandTerminalGrid(segment: ComponentElement, owner: Segment[]): Opera
}

const layout = terminalGridLayout(columns.value, placed);
owner.push(
yield* raise({
type: "error",
message: positioned(noTerminalProviderMessage(), segment),
source: "Terminal.Grid",
// The grid the author asked for, carried beside the sentence so an
// assertion is about the layout that was derived rather than about the
// wording of a refusal.
cause: {
layout: {
columns: layout.columns,
rows: layout.rows,
cells: layout.cells.map((cell) => ({ ...cell })),
},
// The grid renders nothing into the document: what a pane shows belongs to
// that pane, and the sibling after `</Terminal.Grid>` renders to the root
// again only once the provider has restored it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// again only once the provider has restored it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// again only once the provider has restored it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// again only once the provider has restored it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// again only once the provider has restored it.

const identity = {
path: site.path,
...(segment.position === undefined ? {} : { position: segment.position }),
};

try {
// Recorded in this coroutine, before the lease and before any provider is
// contacted: a resumed run whose grid changed is refused while nothing has
// been opened. It cannot live inside the grid child, because a completed
// child never runs.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// child never runs.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// child never runs.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// child never runs.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// child never runs.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// child never runs.

yield* recordGridLayout(identity, toRequest(layout));

const retained = yield* durableGrid(function* (boundary) {
const work = structure.panes.map((pane, index) =>
paneWork(pane, layout.cells[index]!.title, site),
);
return yield* openTerminalGrid(layout, work, boundary);
});

const failed = retained.panes.find((pane) => pane.status === "failed");
if (failed !== undefined) {
owner.push(yield* raise(terminalGridError(segment, failed.reason)));
}
} catch (error) {
owner.push(
yield* raise(
terminalGridError(segment, error instanceof Error ? error.message : String(error)),
),
);
}
}

/**
* What one authored pane does once the grid has minted its claim.
*
* A self-closing pane runs the host's default shell through its claim. A paired
* pane expands its own content in a scope of its own: it inherits the bindings,
* providers, configuration and working directory visible where the grid was
* written, and everything it creates afterwards stays inside the pane. Its
* `<Break>` cannot reach a loop outside the grid, its `<Return>` cannot claim an
* 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 {
if (pane.form === "self-closing") {
return {
ordinal: pane.ordinal,
*run(claim, composite) {
const outcome = yield* claim.admit(() =>
composite.shell(pane.ordinal, () => claim.ready()),
);
if (outcome.signal !== undefined) {
throw new Error(`pane ${pane.ordinal} ("${title}") shell ended on ${outcome.signal}`);
}
if (outcome.exitCode !== undefined && outcome.exitCode !== 0) {
throw new Error(
`pane ${pane.ordinal} ("${title}") shell exited with status ${outcome.exitCode}`,
);
}
},
}),
);
};
}

return {
ordinal: pane.ordinal,
*run(claim, composite) {
yield* scoped(function* () {
// A pane is not inside the loop the grid was written in, so a <Break>
// in its content has no loop to exit and says so.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// in its content has no loop to exit and says so.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// in its content has no loop to exit and says so.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// in its content has no loop to exit and says so.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// in its content has no loop to exit and says so.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// in its content has no loop to exit and says so.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// in its content has no loop to exit and says so.

yield* ActiveLoop.set(undefined);
yield* usePaneTerminal(claim);
const siteEnv = yield* env;
// Starts from what the grid site can see and keeps its own writes: a
// binding this pane makes is visible to later work in this pane and to
// nothing else.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// nothing else.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// nothing else.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// nothing else.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// nothing else.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// nothing else.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// nothing else.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// nothing else.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// nothing else.

yield* provideEnv(derivedEnvironment(siteEnv, { ...(siteEnv?.values ?? {}) }));

const shown: Segment[] = [];
yield* expandSegmentsWithin(
pane.element.children,
site.parentMeta,
site.parentProps,
site.hideSet,
// A counter of its own. Panes expand concurrently, and a shared
// mutable counter would hand two of them block identities that depend
// on which happened to run first.
createBlockCounter(),
shown,
extendPath(
site.path,
elementFrame(pane.element.name, elementSite(pane.element.position, pane.index)),
),
0,
// The pane's own ledger: a checked failure settles this pane and
// cannot reach the root or a sibling.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// cannot reach the root or a sibling.

containedLedger(site.checkedFailures),
site.authority,
// No enclosing value body: a <Return> written in a pane cannot claim
// one outside the grid.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// one outside the grid.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// one outside the grid.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// one outside the grid.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// one outside the grid.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// one outside the grid.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// one outside the grid.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// one outside the grid.

undefined,
);
const text = renderSegments(shown);
if (text.length > 0) {
yield* composite.display(pane.ordinal, text);
}
});
},
};
}

/** The label one pane displays, from the value its own `title` prop produced. */
Expand All@@ -2168,15 +2287,6 @@ function* resolvePaneTitle(pane: TerminalPane): Operation<Result<string>> {
return terminalTitle(value.value);
}

/** What a complete grid says on a host where nothing can open one. */
function noTerminalProviderMessage(): string {
return (
"no terminal provider opened this grid. A host installs the terminal-grid capability " +
"explicitly, and this one installs none, so no pane expanded its content and no default " +
"shell started."
);
}

function loopError(segment: ComponentElement, message: string): ErrorSegment {
return { type: "error", message: positioned(message, segment), source: "Loop" };
}
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 75 additions & 10 deletions architecture.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -2917,16 +2917,48 @@ The grid runs as one structured scope:
6. Once attached, each pane settles independently and keeps its final status
visible while siblings continue. The composite remains present after all
panes settle until the reader closes or leaves it.
7. Closing begins an ordered teardown: prevent new pane launches, cancel live
pane scopes, await every child and finalizer, detach and destroy the exact
provider composite, restore the root terminal, and only then release the
foreground lease and settle the grid. The document never continues while an
observable pane child or provider-owned process can still act through the
grid.
7. Reader close first crosses a live close boundary, then begins an ordered
teardown: prevent new pane launches, ask live pane children to close, await
every child and finalizer, detach and destroy the exact provider composite,
restore the root terminal, and only then release the foreground lease and
settle the grid. The document never continues while an observable pane child
or provider-owned process can still act through the grid.

The provider's `closed()` settlement proposes the live close boundary. The
boundary is crossed when the grid owner has entered a cancellation-deferred
await of the grid's durable child and acknowledges that proposal; only then may
the child signal pane close. That await ends only when the task has settled and
its durable `Close` has been acknowledged, not when the grid body has merely
chosen an outcome. This handshake has no provider identity and is not itself
journaled.

Reader-close intent becomes durable only as that completed grid `Close`, after
pane and provider teardown. There is no standalone durable "closing" state. The
gap between observing close and committing it is safe because ordinary parent
cancellation is held pending across the whole gap. A cancellation that arrives
before the owner acknowledges the close boundary cancels the active grid. One that arrives
afterward does not rewrite grid or pane outcomes: panes already settled keep
their outcomes, each then-live pane completes its own scope and retains
`closed`, and the grid retains the same `reader` or `failed` result it would
have retained without the cancellation. Once the grid child is durably closed,
the pending cancellation is delivered to the parent, so no following document
sibling runs in that attempt. A fatal or cleanup failure still takes its
existing precedence over cancellation.

Pane work and every finalizer it installs live inside that pane's durable child
scope. Reader close is cooperative at the durable boundary: it asks the pane to
close and awaits it; it never halts the pane's durable task. The pane may stop
its live nested work as part of its own scope teardown, but its durable child
does not settle as `closed` or write `Close(ok)` until that work and its finalizers
have settled. This preserves the pane's ordinal-derived identity and never
turns a deliberate reader close into a caller-cancelled durable child that a
later run could revive or wait on forever.

Parent cancellation follows the same teardown from preparation, readiness, or
the active grid and remains cancellation. A provider or host failure cancels
the whole grid and is the grid's canonical failure. An ordinary pane failure
the active grid and remains cancellation. Once reader close has crossed its
live boundary, the close result is committed first and that cancellation is
observed by the parent afterward. A provider or host failure cancels the whole
grid and is the grid's canonical failure. An ordinary pane failure
after attachment is contained as that pane's status and does not cancel its
siblings. When the reader closes the grid, core fails it with the first failed
pane in authored order; cancellation initiated by grid teardown is not a pane
Expand DownExpand Up@@ -2977,8 +3009,41 @@ a terminal provider, starting a shell, expanding pane content, acquiring an
Agent session, or launching a native UI. The structured durable boundary owns
that short circuit; a public replay context does not.

Partial replay first compares the exact authored layout and refuses divergence
before provider work. It rebuilds a fresh provider composite: completed pane
The reader-close handshake makes cancellation during teardown a completed-grid
case rather than a new partial-replay state. When a pane finalizer delays close
and parent cancellation arrives, the first attempt still finishes every pane
and provider finalizer, writes the pane outcomes and completed grid `Close`, and
only then reports cancellation to its parent. A continuation claims that
completed child and resumes after it without recreating the provider or
re-entering pane work. A host loss can still interrupt the unjournaled live
teardown; panes whose `Close` was acknowledged remain complete, while any pane
and grid without a completed record follow the existing partial-replay rules.

Partial replay compares the **resolved** layout and refuses divergence before
provider work.

What that can and cannot cover follows from where a resumed run gets its
document. A continuation executes the root the journal retained: the source the
new invocation supplies is not read, not compared and not refused. So the
authored structure of a grid — how many panes it has, their order, and whether
each was written paired or self-closing — is fixed for the whole life of a
journal, and cannot differ between runs. Comparing it would compare a value with
itself.

What can still differ is everything the retained source *resolves*: `columns`
and each `title` are expressions, and props are not restored across a
continuation, so a prop-borne or otherwise live value produces a different
resolved layout from the same retained document. Those are what the comparison
is for, and a change in either refuses before the foreground lease is taken and
before any provider is contacted.

Authored-structure change is therefore not a grid concern. A document whose body
changed under an existing journal is a root-definition compatibility question —
the retained root stays authoritative, and deciding whether a changed source
should be refused rather than ignored belongs to a versioned root boundary that
does not exist yet. Until it does, the grid's obligation is the narrower one it
can actually discharge: retain the complete authored structure, and open the
structure it retained rather than the one the file now shows. It rebuilds a fresh provider composite: completed pane
children are restored as settled statuses without re-running their effects,
while incomplete children replay or start their remaining work. An incomplete
`<Session.Launch>` preserves the prepared/detached identity rules of its own
Expand Down
30 changes: 30 additions & 0 deletions packages/core/mod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -152,6 +152,36 @@ 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";
export {
createTerminalAuthority,
createTerminalGridClaims,
TerminalAuthorityError,
terminalInstallation,
useTerminalInstallation,
} from "./src/terminal/authority.ts";
export type {
PaneReadiness,
TerminalGridAuthority,
TerminalGridClaims,
TerminalPaneClaim,
} from "./src/terminal/authority.ts";
export {
installTerminalProvider,
registerTerminalProvider,
TERMINAL_PROVIDERS_API,
TerminalProviderInstallError,
TerminalProviders,
} from "./src/terminal/provider-api.ts";
export type {
TerminalProviderFactory,
TerminalProviderInstallRequest,
TerminalProviderOptions,
} from "./src/terminal/provider-api.ts";
export { installTerminalGridProfile } from "./src/terminal/profile.ts";
export type { TerminalGridProfileOptions } from "./src/terminal/profile.ts";
export { paneTerminal } from "./src/terminal/pane.ts";
export type { PaneTerminal } from "./src/terminal/pane.ts";
export type { PaneStatus, RetainedGrid, RetainedPaneOutcome } from "./src/terminal/grid.ts";

export { execute, Execution } from "./src/execute.ts";
export type {
Expand Down
166 changes: 138 additions & 28 deletions packages/core/src/expand.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -68,6 +68,10 @@ import {
import type { StructuralViolation, SwitchCase, TerminalPane } from "./structural-rules.ts";
import { terminalGridLayout } from "./terminal-grid.ts";
import type { PlacedPane } from "./terminal-grid.ts";
import { durableGrid, openTerminalGrid, toRequest } from "./terminal/grid.ts";
import type { PaneWork } from "./terminal/grid.ts";
import { recordGridLayout } from "./terminal/journal.ts";
import { usePaneTerminal } from "./terminal/pane.ts";
import {
asBindingViolation,
asExpressionViolation,
Expand DownExpand Up@@ -139,7 +143,7 @@ import {
import { remark } from "remark";
import { select as cssSelect } from "unist-util-select";
import { toString as mdastToString } from "mdast-util-to-string";
import { liveEnvironment } from "./live-env.ts";
import { derivedEnvironment, liveEnvironment } from "./live-env.ts";
import { TestHarnessComponentDefinition } from "./test-harness.ts";
import type { TestHarnessBinding } from "./test-harness.ts";

Expand DownExpand Up@@ -1181,7 +1185,14 @@ function* expandListSegments(
if (segment.name === "Terminal.Grid") {
// No raise() here, like the branches above: expandTerminalGrid
// reports every error it creates.
yield* expandTerminalGrid(segment, result);
yield* expandTerminalGrid(segment, result, {
parentMeta,
parentProps,
hideSet,
path: elementPath,
checkedFailures,
authority,
});
break;
}

Expand DownExpand Up@@ -2102,7 +2113,21 @@ function* resolveStructuralProp(
* does, which is what makes the refusal a closed one rather than a partial grid
* left behind.
*/
function* expandTerminalGrid(segment: ComponentElement, owner: Segment[]): Operation<void> {
/** Everything a pane's own content needs to expand where the grid was written. */
interface GridSite {
readonly parentMeta: Record<string, unknown>;
readonly parentProps: Record<string, Json>;
readonly hideSet: Set<string>;
readonly path: string;
readonly checkedFailures: CheckedFailures | undefined;
readonly authority: ExpansionAuthority | undefined;
}

function* expandTerminalGrid(
segment: ComponentElement,
owner: Segment[],
site: GridSite,
): Operation<void> {
const structure = terminalGridStructure(segment);
if (structure.violations.length > 0) {
for (const violation of structure.violations) {
Expand DownExpand Up@@ -2137,23 +2162,117 @@ function* expandTerminalGrid(segment: ComponentElement, owner: Segment[]): Opera
}

const layout = terminalGridLayout(columns.value, placed);
owner.push(
yield* raise({
type: "error",
message: positioned(noTerminalProviderMessage(), segment),
source: "Terminal.Grid",
// The grid the author asked for, carried beside the sentence so an
// assertion is about the layout that was derived rather than about the
// wording of a refusal.
cause: {
layout: {
columns: layout.columns,
rows: layout.rows,
cells: layout.cells.map((cell) => ({ ...cell })),
},
// The grid renders nothing into the document: what a pane shows belongs to
// that pane, and the sibling after `</Terminal.Grid>` renders to the root
// again only once the provider has restored it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// again only once the provider has restored it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// again only once the provider has restored it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// again only once the provider has restored it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// again only once the provider has restored it.

const identity = {
path: site.path,
...(segment.position === undefined ? {} : { position: segment.position }),
};

try {
// Recorded in this coroutine, before the lease and before any provider is
// contacted: a resumed run whose grid changed is refused while nothing has
// been opened. It cannot live inside the grid child, because a completed
// child never runs.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// child never runs.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// child never runs.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// child never runs.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// child never runs.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// child never runs.

yield* recordGridLayout(identity, toRequest(layout));

const retained = yield* durableGrid(function* (boundary) {
const work = structure.panes.map((pane, index) =>
paneWork(pane, layout.cells[index]!.title, site),
);
return yield* openTerminalGrid(layout, work, boundary);
});

const failed = retained.panes.find((pane) => pane.status === "failed");
if (failed !== undefined) {
owner.push(yield* raise(terminalGridError(segment, failed.reason)));
}
} catch (error) {
owner.push(
yield* raise(
terminalGridError(segment, error instanceof Error ? error.message : String(error)),
),
);
}
}

/**
* What one authored pane does once the grid has minted its claim.
*
* A self-closing pane runs the host's default shell through its claim. A paired
* pane expands its own content in a scope of its own: it inherits the bindings,
* providers, configuration and working directory visible where the grid was
* written, and everything it creates afterwards stays inside the pane. Its
* `<Break>` cannot reach a loop outside the grid, its `<Return>` cannot claim an
* 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 {
if (pane.form === "self-closing") {
return {
ordinal: pane.ordinal,
*run(claim, composite) {
const outcome = yield* claim.admit(() =>
composite.shell(pane.ordinal, () => claim.ready()),
);
if (outcome.signal !== undefined) {
throw new Error(`pane ${pane.ordinal} ("${title}") shell ended on ${outcome.signal}`);
}
if (outcome.exitCode !== undefined && outcome.exitCode !== 0) {
throw new Error(
`pane ${pane.ordinal} ("${title}") shell exited with status ${outcome.exitCode}`,
);
}
},
}),
);
};
}

return {
ordinal: pane.ordinal,
*run(claim, composite) {
yield* scoped(function* () {
// A pane is not inside the loop the grid was written in, so a <Break>
// in its content has no loop to exit and says so.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// in its content has no loop to exit and says so.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// in its content has no loop to exit and says so.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// in its content has no loop to exit and says so.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// in its content has no loop to exit and says so.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// in its content has no loop to exit and says so.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// in its content has no loop to exit and says so.

yield* ActiveLoop.set(undefined);
yield* usePaneTerminal(claim);
const siteEnv = yield* env;
// Starts from what the grid site can see and keeps its own writes: a
// binding this pane makes is visible to later work in this pane and to
// nothing else.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// nothing else.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// nothing else.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// nothing else.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// nothing else.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// nothing else.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// nothing else.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// nothing else.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// nothing else.

yield* provideEnv(derivedEnvironment(siteEnv, { ...(siteEnv?.values ?? {}) }));

const shown: Segment[] = [];
yield* expandSegmentsWithin(
pane.element.children,
site.parentMeta,
site.parentProps,
site.hideSet,
// A counter of its own. Panes expand concurrently, and a shared
// mutable counter would hand two of them block identities that depend
// on which happened to run first.
createBlockCounter(),
shown,
extendPath(
site.path,
elementFrame(pane.element.name, elementSite(pane.element.position, pane.index)),
),
0,
// The pane's own ledger: a checked failure settles this pane and
// cannot reach the root or a sibling.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// cannot reach the root or a sibling.

containedLedger(site.checkedFailures),
site.authority,
// No enclosing value body: a <Return> written in a pane cannot claim
// one outside the grid.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// one outside the grid.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// one outside the grid.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// one outside the grid.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// one outside the grid.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// one outside the grid.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// one outside the grid.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// one outside the grid.

undefined,
);
const text = renderSegments(shown);
if (text.length > 0) {
yield* composite.display(pane.ordinal, text);
}
});
},
};
}

/** The label one pane displays, from the value its own `title` prop produced. */
Expand All@@ -2168,15 +2287,6 @@ function* resolvePaneTitle(pane: TerminalPane): Operation<Result<string>> {
return terminalTitle(value.value);
}

/** What a complete grid says on a host where nothing can open one. */
function noTerminalProviderMessage(): string {
return (
"no terminal provider opened this grid. A host installs the terminal-grid capability " +
"explicitly, and this one installs none, so no pane expanded its content and no default " +
"shell started."
);
}

function loopError(segment: ComponentElement, message: string): ErrorSegment {
return { type: "error", message: positioned(message, segment), source: "Loop" };
}
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 75 additions & 10 deletions architecture.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -2917,16 +2917,48 @@ The grid runs as one structured scope:
6. Once attached, each pane settles independently and keeps its final status
visible while siblings continue. The composite remains present after all
panes settle until the reader closes or leaves it.
7. Closing begins an ordered teardown: prevent new pane launches, cancel live
pane scopes, await every child and finalizer, detach and destroy the exact
provider composite, restore the root terminal, and only then release the
foreground lease and settle the grid. The document never continues while an
observable pane child or provider-owned process can still act through the
grid.
7. Reader close first crosses a live close boundary, then begins an ordered
teardown: prevent new pane launches, ask live pane children to close, await
every child and finalizer, detach and destroy the exact provider composite,
restore the root terminal, and only then release the foreground lease and
settle the grid. The document never continues while an observable pane child
or provider-owned process can still act through the grid.

The provider's `closed()` settlement proposes the live close boundary. The
boundary is crossed when the grid owner has entered a cancellation-deferred
await of the grid's durable child and acknowledges that proposal; only then may
the child signal pane close. That await ends only when the task has settled and
its durable `Close` has been acknowledged, not when the grid body has merely
chosen an outcome. This handshake has no provider identity and is not itself
journaled.

Reader-close intent becomes durable only as that completed grid `Close`, after
pane and provider teardown. There is no standalone durable "closing" state. The
gap between observing close and committing it is safe because ordinary parent
cancellation is held pending across the whole gap. A cancellation that arrives
before the owner acknowledges the close boundary cancels the active grid. One that arrives
afterward does not rewrite grid or pane outcomes: panes already settled keep
their outcomes, each then-live pane completes its own scope and retains
`closed`, and the grid retains the same `reader` or `failed` result it would
have retained without the cancellation. Once the grid child is durably closed,
the pending cancellation is delivered to the parent, so no following document
sibling runs in that attempt. A fatal or cleanup failure still takes its
existing precedence over cancellation.

Pane work and every finalizer it installs live inside that pane's durable child
scope. Reader close is cooperative at the durable boundary: it asks the pane to
close and awaits it; it never halts the pane's durable task. The pane may stop
its live nested work as part of its own scope teardown, but its durable child
does not settle as `closed` or write `Close(ok)` until that work and its finalizers
have settled. This preserves the pane's ordinal-derived identity and never
turns a deliberate reader close into a caller-cancelled durable child that a
later run could revive or wait on forever.

Parent cancellation follows the same teardown from preparation, readiness, or
the active grid and remains cancellation. A provider or host failure cancels
the whole grid and is the grid's canonical failure. An ordinary pane failure
the active grid and remains cancellation. Once reader close has crossed its
live boundary, the close result is committed first and that cancellation is
observed by the parent afterward. A provider or host failure cancels the whole
grid and is the grid's canonical failure. An ordinary pane failure
after attachment is contained as that pane's status and does not cancel its
siblings. When the reader closes the grid, core fails it with the first failed
pane in authored order; cancellation initiated by grid teardown is not a pane
Expand DownExpand Up@@ -2977,8 +3009,41 @@ a terminal provider, starting a shell, expanding pane content, acquiring an
Agent session, or launching a native UI. The structured durable boundary owns
that short circuit; a public replay context does not.

Partial replay first compares the exact authored layout and refuses divergence
before provider work. It rebuilds a fresh provider composite: completed pane
The reader-close handshake makes cancellation during teardown a completed-grid
case rather than a new partial-replay state. When a pane finalizer delays close
and parent cancellation arrives, the first attempt still finishes every pane
and provider finalizer, writes the pane outcomes and completed grid `Close`, and
only then reports cancellation to its parent. A continuation claims that
completed child and resumes after it without recreating the provider or
re-entering pane work. A host loss can still interrupt the unjournaled live
teardown; panes whose `Close` was acknowledged remain complete, while any pane
and grid without a completed record follow the existing partial-replay rules.

Partial replay compares the **resolved** layout and refuses divergence before
provider work.

What that can and cannot cover follows from where a resumed run gets its
document. A continuation executes the root the journal retained: the source the
new invocation supplies is not read, not compared and not refused. So the
authored structure of a grid — how many panes it has, their order, and whether
each was written paired or self-closing — is fixed for the whole life of a
journal, and cannot differ between runs. Comparing it would compare a value with
itself.

What can still differ is everything the retained source *resolves*: `columns`
and each `title` are expressions, and props are not restored across a
continuation, so a prop-borne or otherwise live value produces a different
resolved layout from the same retained document. Those are what the comparison
is for, and a change in either refuses before the foreground lease is taken and
before any provider is contacted.

Authored-structure change is therefore not a grid concern. A document whose body
changed under an existing journal is a root-definition compatibility question —
the retained root stays authoritative, and deciding whether a changed source
should be refused rather than ignored belongs to a versioned root boundary that
does not exist yet. Until it does, the grid's obligation is the narrower one it
can actually discharge: retain the complete authored structure, and open the
structure it retained rather than the one the file now shows. It rebuilds a fresh provider composite: completed pane
children are restored as settled statuses without re-running their effects,
while incomplete children replay or start their remaining work. An incomplete
`<Session.Launch>` preserves the prepared/detached identity rules of its own
Expand Down
30 changes: 30 additions & 0 deletions packages/core/mod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -152,6 +152,36 @@ 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";
export {
createTerminalAuthority,
createTerminalGridClaims,
TerminalAuthorityError,
terminalInstallation,
useTerminalInstallation,
} from "./src/terminal/authority.ts";
export type {
PaneReadiness,
TerminalGridAuthority,
TerminalGridClaims,
TerminalPaneClaim,
} from "./src/terminal/authority.ts";
export {
installTerminalProvider,
registerTerminalProvider,
TERMINAL_PROVIDERS_API,
TerminalProviderInstallError,
TerminalProviders,
} from "./src/terminal/provider-api.ts";
export type {
TerminalProviderFactory,
TerminalProviderInstallRequest,
TerminalProviderOptions,
} from "./src/terminal/provider-api.ts";
export { installTerminalGridProfile } from "./src/terminal/profile.ts";
export type { TerminalGridProfileOptions } from "./src/terminal/profile.ts";
export { paneTerminal } from "./src/terminal/pane.ts";
export type { PaneTerminal } from "./src/terminal/pane.ts";
export type { PaneStatus, RetainedGrid, RetainedPaneOutcome } from "./src/terminal/grid.ts";

export { execute, Execution } from "./src/execute.ts";
export type {
Expand Down
166 changes: 138 additions & 28 deletions packages/core/src/expand.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -68,6 +68,10 @@ import {
import type { StructuralViolation, SwitchCase, TerminalPane } from "./structural-rules.ts";
import { terminalGridLayout } from "./terminal-grid.ts";
import type { PlacedPane } from "./terminal-grid.ts";
import { durableGrid, openTerminalGrid, toRequest } from "./terminal/grid.ts";
import type { PaneWork } from "./terminal/grid.ts";
import { recordGridLayout } from "./terminal/journal.ts";
import { usePaneTerminal } from "./terminal/pane.ts";
import {
asBindingViolation,
asExpressionViolation,
Expand DownExpand Up@@ -139,7 +143,7 @@ import {
import { remark } from "remark";
import { select as cssSelect } from "unist-util-select";
import { toString as mdastToString } from "mdast-util-to-string";
import { liveEnvironment } from "./live-env.ts";
import { derivedEnvironment, liveEnvironment } from "./live-env.ts";
import { TestHarnessComponentDefinition } from "./test-harness.ts";
import type { TestHarnessBinding } from "./test-harness.ts";

Expand DownExpand Up@@ -1181,7 +1185,14 @@ function* expandListSegments(
if (segment.name === "Terminal.Grid") {
// No raise() here, like the branches above: expandTerminalGrid
// reports every error it creates.
yield* expandTerminalGrid(segment, result);
yield* expandTerminalGrid(segment, result, {
parentMeta,
parentProps,
hideSet,
path: elementPath,
checkedFailures,
authority,
});
break;
}

Expand DownExpand Up@@ -2102,7 +2113,21 @@ function* resolveStructuralProp(
* does, which is what makes the refusal a closed one rather than a partial grid
* left behind.
*/
function* expandTerminalGrid(segment: ComponentElement, owner: Segment[]): Operation<void> {
/** Everything a pane's own content needs to expand where the grid was written. */
interface GridSite {
readonly parentMeta: Record<string, unknown>;
readonly parentProps: Record<string, Json>;
readonly hideSet: Set<string>;
readonly path: string;
readonly checkedFailures: CheckedFailures | undefined;
readonly authority: ExpansionAuthority | undefined;
}

function* expandTerminalGrid(
segment: ComponentElement,
owner: Segment[],
site: GridSite,
): Operation<void> {
const structure = terminalGridStructure(segment);
if (structure.violations.length > 0) {
for (const violation of structure.violations) {
Expand DownExpand Up@@ -2137,23 +2162,117 @@ function* expandTerminalGrid(segment: ComponentElement, owner: Segment[]): Opera
}

const layout = terminalGridLayout(columns.value, placed);
owner.push(
yield* raise({
type: "error",
message: positioned(noTerminalProviderMessage(), segment),
source: "Terminal.Grid",
// The grid the author asked for, carried beside the sentence so an
// assertion is about the layout that was derived rather than about the
// wording of a refusal.
cause: {
layout: {
columns: layout.columns,
rows: layout.rows,
cells: layout.cells.map((cell) => ({ ...cell })),
},
// The grid renders nothing into the document: what a pane shows belongs to
// that pane, and the sibling after `</Terminal.Grid>` renders to the root
// again only once the provider has restored it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// again only once the provider has restored it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// again only once the provider has restored it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// again only once the provider has restored it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// again only once the provider has restored it.

const identity = {
path: site.path,
...(segment.position === undefined ? {} : { position: segment.position }),
};

try {
// Recorded in this coroutine, before the lease and before any provider is
// contacted: a resumed run whose grid changed is refused while nothing has
// been opened. It cannot live inside the grid child, because a completed
// child never runs.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// child never runs.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// child never runs.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// child never runs.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// child never runs.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// child never runs.

yield* recordGridLayout(identity, toRequest(layout));

const retained = yield* durableGrid(function* (boundary) {
const work = structure.panes.map((pane, index) =>
paneWork(pane, layout.cells[index]!.title, site),
);
return yield* openTerminalGrid(layout, work, boundary);
});

const failed = retained.panes.find((pane) => pane.status === "failed");
if (failed !== undefined) {
owner.push(yield* raise(terminalGridError(segment, failed.reason)));
}
} catch (error) {
owner.push(
yield* raise(
terminalGridError(segment, error instanceof Error ? error.message : String(error)),
),
);
}
}

/**
* What one authored pane does once the grid has minted its claim.
*
* A self-closing pane runs the host's default shell through its claim. A paired
* pane expands its own content in a scope of its own: it inherits the bindings,
* providers, configuration and working directory visible where the grid was
* written, and everything it creates afterwards stays inside the pane. Its
* `<Break>` cannot reach a loop outside the grid, its `<Return>` cannot claim an
* 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 {
if (pane.form === "self-closing") {
return {
ordinal: pane.ordinal,
*run(claim, composite) {
const outcome = yield* claim.admit(() =>
composite.shell(pane.ordinal, () => claim.ready()),
);
if (outcome.signal !== undefined) {
throw new Error(`pane ${pane.ordinal} ("${title}") shell ended on ${outcome.signal}`);
}
if (outcome.exitCode !== undefined && outcome.exitCode !== 0) {
throw new Error(
`pane ${pane.ordinal} ("${title}") shell exited with status ${outcome.exitCode}`,
);
}
},
}),
);
};
}

return {
ordinal: pane.ordinal,
*run(claim, composite) {
yield* scoped(function* () {
// A pane is not inside the loop the grid was written in, so a <Break>
// in its content has no loop to exit and says so.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// in its content has no loop to exit and says so.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// in its content has no loop to exit and says so.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// in its content has no loop to exit and says so.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// in its content has no loop to exit and says so.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// in its content has no loop to exit and says so.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// in its content has no loop to exit and says so.

yield* ActiveLoop.set(undefined);
yield* usePaneTerminal(claim);
const siteEnv = yield* env;
// Starts from what the grid site can see and keeps its own writes: a
// binding this pane makes is visible to later work in this pane and to
// nothing else.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// nothing else.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// nothing else.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// nothing else.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// nothing else.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// nothing else.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// nothing else.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// nothing else.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// nothing else.

yield* provideEnv(derivedEnvironment(siteEnv, { ...(siteEnv?.values ?? {}) }));

const shown: Segment[] = [];
yield* expandSegmentsWithin(
pane.element.children,
site.parentMeta,
site.parentProps,
site.hideSet,
// A counter of its own. Panes expand concurrently, and a shared
// mutable counter would hand two of them block identities that depend
// on which happened to run first.
createBlockCounter(),
shown,
extendPath(
site.path,
elementFrame(pane.element.name, elementSite(pane.element.position, pane.index)),
),
0,
// The pane's own ledger: a checked failure settles this pane and
// cannot reach the root or a sibling.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// cannot reach the root or a sibling.

containedLedger(site.checkedFailures),
site.authority,
// No enclosing value body: a <Return> written in a pane cannot claim
// one outside the grid.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// one outside the grid.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// one outside the grid.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// one outside the grid.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// one outside the grid.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// one outside the grid.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// one outside the grid.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// one outside the grid.

undefined,
);
const text = renderSegments(shown);
if (text.length > 0) {
yield* composite.display(pane.ordinal, text);
}
});
},
};
}

/** The label one pane displays, from the value its own `title` prop produced. */
Expand All@@ -2168,15 +2287,6 @@ function* resolvePaneTitle(pane: TerminalPane): Operation<Result<string>> {
return terminalTitle(value.value);
}

/** What a complete grid says on a host where nothing can open one. */
function noTerminalProviderMessage(): string {
return (
"no terminal provider opened this grid. A host installs the terminal-grid capability " +
"explicitly, and this one installs none, so no pane expanded its content and no default " +
"shell started."
);
}

function loopError(segment: ComponentElement, message: string): ErrorSegment {
return { type: "error", message: positioned(message, segment), source: "Loop" };
}
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 75 additions & 10 deletions architecture.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -2917,16 +2917,48 @@ The grid runs as one structured scope:
6. Once attached, each pane settles independently and keeps its final status
visible while siblings continue. The composite remains present after all
panes settle until the reader closes or leaves it.
7. Closing begins an ordered teardown: prevent new pane launches, cancel live
pane scopes, await every child and finalizer, detach and destroy the exact
provider composite, restore the root terminal, and only then release the
foreground lease and settle the grid. The document never continues while an
observable pane child or provider-owned process can still act through the
grid.
7. Reader close first crosses a live close boundary, then begins an ordered
teardown: prevent new pane launches, ask live pane children to close, await
every child and finalizer, detach and destroy the exact provider composite,
restore the root terminal, and only then release the foreground lease and
settle the grid. The document never continues while an observable pane child
or provider-owned process can still act through the grid.

The provider's `closed()` settlement proposes the live close boundary. The
boundary is crossed when the grid owner has entered a cancellation-deferred
await of the grid's durable child and acknowledges that proposal; only then may
the child signal pane close. That await ends only when the task has settled and
its durable `Close` has been acknowledged, not when the grid body has merely
chosen an outcome. This handshake has no provider identity and is not itself
journaled.

Reader-close intent becomes durable only as that completed grid `Close`, after
pane and provider teardown. There is no standalone durable "closing" state. The
gap between observing close and committing it is safe because ordinary parent
cancellation is held pending across the whole gap. A cancellation that arrives
before the owner acknowledges the close boundary cancels the active grid. One that arrives
afterward does not rewrite grid or pane outcomes: panes already settled keep
their outcomes, each then-live pane completes its own scope and retains
`closed`, and the grid retains the same `reader` or `failed` result it would
have retained without the cancellation. Once the grid child is durably closed,
the pending cancellation is delivered to the parent, so no following document
sibling runs in that attempt. A fatal or cleanup failure still takes its
existing precedence over cancellation.

Pane work and every finalizer it installs live inside that pane's durable child
scope. Reader close is cooperative at the durable boundary: it asks the pane to
close and awaits it; it never halts the pane's durable task. The pane may stop
its live nested work as part of its own scope teardown, but its durable child
does not settle as `closed` or write `Close(ok)` until that work and its finalizers
have settled. This preserves the pane's ordinal-derived identity and never
turns a deliberate reader close into a caller-cancelled durable child that a
later run could revive or wait on forever.

Parent cancellation follows the same teardown from preparation, readiness, or
the active grid and remains cancellation. A provider or host failure cancels
the whole grid and is the grid's canonical failure. An ordinary pane failure
the active grid and remains cancellation. Once reader close has crossed its
live boundary, the close result is committed first and that cancellation is
observed by the parent afterward. A provider or host failure cancels the whole
grid and is the grid's canonical failure. An ordinary pane failure
after attachment is contained as that pane's status and does not cancel its
siblings. When the reader closes the grid, core fails it with the first failed
pane in authored order; cancellation initiated by grid teardown is not a pane
Expand DownExpand Up@@ -2977,8 +3009,41 @@ a terminal provider, starting a shell, expanding pane content, acquiring an
Agent session, or launching a native UI. The structured durable boundary owns
that short circuit; a public replay context does not.

Partial replay first compares the exact authored layout and refuses divergence
before provider work. It rebuilds a fresh provider composite: completed pane
The reader-close handshake makes cancellation during teardown a completed-grid
case rather than a new partial-replay state. When a pane finalizer delays close
and parent cancellation arrives, the first attempt still finishes every pane
and provider finalizer, writes the pane outcomes and completed grid `Close`, and
only then reports cancellation to its parent. A continuation claims that
completed child and resumes after it without recreating the provider or
re-entering pane work. A host loss can still interrupt the unjournaled live
teardown; panes whose `Close` was acknowledged remain complete, while any pane
and grid without a completed record follow the existing partial-replay rules.

Partial replay compares the **resolved** layout and refuses divergence before
provider work.

What that can and cannot cover follows from where a resumed run gets its
document. A continuation executes the root the journal retained: the source the
new invocation supplies is not read, not compared and not refused. So the
authored structure of a grid — how many panes it has, their order, and whether
each was written paired or self-closing — is fixed for the whole life of a
journal, and cannot differ between runs. Comparing it would compare a value with
itself.

What can still differ is everything the retained source *resolves*: `columns`
and each `title` are expressions, and props are not restored across a
continuation, so a prop-borne or otherwise live value produces a different
resolved layout from the same retained document. Those are what the comparison
is for, and a change in either refuses before the foreground lease is taken and
before any provider is contacted.

Authored-structure change is therefore not a grid concern. A document whose body
changed under an existing journal is a root-definition compatibility question —
the retained root stays authoritative, and deciding whether a changed source
should be refused rather than ignored belongs to a versioned root boundary that
does not exist yet. Until it does, the grid's obligation is the narrower one it
can actually discharge: retain the complete authored structure, and open the
structure it retained rather than the one the file now shows. It rebuilds a fresh provider composite: completed pane
children are restored as settled statuses without re-running their effects,
while incomplete children replay or start their remaining work. An incomplete
`<Session.Launch>` preserves the prepared/detached identity rules of its own
Expand Down
30 changes: 30 additions & 0 deletions packages/core/mod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -152,6 +152,36 @@ 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";
export {
createTerminalAuthority,
createTerminalGridClaims,
TerminalAuthorityError,
terminalInstallation,
useTerminalInstallation,
} from "./src/terminal/authority.ts";
export type {
PaneReadiness,
TerminalGridAuthority,
TerminalGridClaims,
TerminalPaneClaim,
} from "./src/terminal/authority.ts";
export {
installTerminalProvider,
registerTerminalProvider,
TERMINAL_PROVIDERS_API,
TerminalProviderInstallError,
TerminalProviders,
} from "./src/terminal/provider-api.ts";
export type {
TerminalProviderFactory,
TerminalProviderInstallRequest,
TerminalProviderOptions,
} from "./src/terminal/provider-api.ts";
export { installTerminalGridProfile } from "./src/terminal/profile.ts";
export type { TerminalGridProfileOptions } from "./src/terminal/profile.ts";
export { paneTerminal } from "./src/terminal/pane.ts";
export type { PaneTerminal } from "./src/terminal/pane.ts";
export type { PaneStatus, RetainedGrid, RetainedPaneOutcome } from "./src/terminal/grid.ts";

export { execute, Execution } from "./src/execute.ts";
export type {
Expand Down
166 changes: 138 additions & 28 deletions packages/core/src/expand.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -68,6 +68,10 @@ import {
import type { StructuralViolation, SwitchCase, TerminalPane } from "./structural-rules.ts";
import { terminalGridLayout } from "./terminal-grid.ts";
import type { PlacedPane } from "./terminal-grid.ts";
import { durableGrid, openTerminalGrid, toRequest } from "./terminal/grid.ts";
import type { PaneWork } from "./terminal/grid.ts";
import { recordGridLayout } from "./terminal/journal.ts";
import { usePaneTerminal } from "./terminal/pane.ts";
import {
asBindingViolation,
asExpressionViolation,
Expand DownExpand Up@@ -139,7 +143,7 @@ import {
import { remark } from "remark";
import { select as cssSelect } from "unist-util-select";
import { toString as mdastToString } from "mdast-util-to-string";
import { liveEnvironment } from "./live-env.ts";
import { derivedEnvironment, liveEnvironment } from "./live-env.ts";
import { TestHarnessComponentDefinition } from "./test-harness.ts";
import type { TestHarnessBinding } from "./test-harness.ts";

Expand DownExpand Up@@ -1181,7 +1185,14 @@ function* expandListSegments(
if (segment.name === "Terminal.Grid") {
// No raise() here, like the branches above: expandTerminalGrid
// reports every error it creates.
yield* expandTerminalGrid(segment, result);
yield* expandTerminalGrid(segment, result, {
parentMeta,
parentProps,
hideSet,
path: elementPath,
checkedFailures,
authority,
});
break;
}

Expand DownExpand Up@@ -2102,7 +2113,21 @@ function* resolveStructuralProp(
* does, which is what makes the refusal a closed one rather than a partial grid
* left behind.
*/
function* expandTerminalGrid(segment: ComponentElement, owner: Segment[]): Operation<void> {
/** Everything a pane's own content needs to expand where the grid was written. */
interface GridSite {
readonly parentMeta: Record<string, unknown>;
readonly parentProps: Record<string, Json>;
readonly hideSet: Set<string>;
readonly path: string;
readonly checkedFailures: CheckedFailures | undefined;
readonly authority: ExpansionAuthority | undefined;
}

function* expandTerminalGrid(
segment: ComponentElement,
owner: Segment[],
site: GridSite,
): Operation<void> {
const structure = terminalGridStructure(segment);
if (structure.violations.length > 0) {
for (const violation of structure.violations) {
Expand DownExpand Up@@ -2137,23 +2162,117 @@ function* expandTerminalGrid(segment: ComponentElement, owner: Segment[]): Opera
}

const layout = terminalGridLayout(columns.value, placed);
owner.push(
yield* raise({
type: "error",
message: positioned(noTerminalProviderMessage(), segment),
source: "Terminal.Grid",
// The grid the author asked for, carried beside the sentence so an
// assertion is about the layout that was derived rather than about the
// wording of a refusal.
cause: {
layout: {
columns: layout.columns,
rows: layout.rows,
cells: layout.cells.map((cell) => ({ ...cell })),
},
// The grid renders nothing into the document: what a pane shows belongs to
// that pane, and the sibling after `</Terminal.Grid>` renders to the root
// again only once the provider has restored it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// again only once the provider has restored it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// again only once the provider has restored it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// again only once the provider has restored it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// again only once the provider has restored it.

const identity = {
path: site.path,
...(segment.position === undefined ? {} : { position: segment.position }),
};

try {
// Recorded in this coroutine, before the lease and before any provider is
// contacted: a resumed run whose grid changed is refused while nothing has
// been opened. It cannot live inside the grid child, because a completed
// child never runs.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// child never runs.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// child never runs.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// child never runs.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// child never runs.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// child never runs.

yield* recordGridLayout(identity, toRequest(layout));

const retained = yield* durableGrid(function* (boundary) {
const work = structure.panes.map((pane, index) =>
paneWork(pane, layout.cells[index]!.title, site),
);
return yield* openTerminalGrid(layout, work, boundary);
});

const failed = retained.panes.find((pane) => pane.status === "failed");
if (failed !== undefined) {
owner.push(yield* raise(terminalGridError(segment, failed.reason)));
}
} catch (error) {
owner.push(
yield* raise(
terminalGridError(segment, error instanceof Error ? error.message : String(error)),
),
);
}
}

/**
* What one authored pane does once the grid has minted its claim.
*
* A self-closing pane runs the host's default shell through its claim. A paired
* pane expands its own content in a scope of its own: it inherits the bindings,
* providers, configuration and working directory visible where the grid was
* written, and everything it creates afterwards stays inside the pane. Its
* `<Break>` cannot reach a loop outside the grid, its `<Return>` cannot claim an
* 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 {
if (pane.form === "self-closing") {
return {
ordinal: pane.ordinal,
*run(claim, composite) {
const outcome = yield* claim.admit(() =>
composite.shell(pane.ordinal, () => claim.ready()),
);
if (outcome.signal !== undefined) {
throw new Error(`pane ${pane.ordinal} ("${title}") shell ended on ${outcome.signal}`);
}
if (outcome.exitCode !== undefined && outcome.exitCode !== 0) {
throw new Error(
`pane ${pane.ordinal} ("${title}") shell exited with status ${outcome.exitCode}`,
);
}
},
}),
);
};
}

return {
ordinal: pane.ordinal,
*run(claim, composite) {
yield* scoped(function* () {
// A pane is not inside the loop the grid was written in, so a <Break>
// in its content has no loop to exit and says so.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// in its content has no loop to exit and says so.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// in its content has no loop to exit and says so.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// in its content has no loop to exit and says so.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// in its content has no loop to exit and says so.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// in its content has no loop to exit and says so.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// in its content has no loop to exit and says so.

yield* ActiveLoop.set(undefined);
yield* usePaneTerminal(claim);
const siteEnv = yield* env;
// Starts from what the grid site can see and keeps its own writes: a
// binding this pane makes is visible to later work in this pane and to
// nothing else.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// nothing else.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// nothing else.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// nothing else.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// nothing else.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// nothing else.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// nothing else.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// nothing else.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// nothing else.

yield* provideEnv(derivedEnvironment(siteEnv, { ...(siteEnv?.values ?? {}) }));

const shown: Segment[] = [];
yield* expandSegmentsWithin(
pane.element.children,
site.parentMeta,
site.parentProps,
site.hideSet,
// A counter of its own. Panes expand concurrently, and a shared
// mutable counter would hand two of them block identities that depend
// on which happened to run first.
createBlockCounter(),
shown,
extendPath(
site.path,
elementFrame(pane.element.name, elementSite(pane.element.position, pane.index)),
),
0,
// The pane's own ledger: a checked failure settles this pane and
// cannot reach the root or a sibling.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// cannot reach the root or a sibling.

containedLedger(site.checkedFailures),
site.authority,
// No enclosing value body: a <Return> written in a pane cannot claim
// one outside the grid.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// one outside the grid.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// one outside the grid.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// one outside the grid.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// one outside the grid.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// one outside the grid.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// one outside the grid.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// one outside the grid.

undefined,
);
const text = renderSegments(shown);
if (text.length > 0) {
yield* composite.display(pane.ordinal, text);
}
});
},
};
}

/** The label one pane displays, from the value its own `title` prop produced. */
Expand All@@ -2168,15 +2287,6 @@ function* resolvePaneTitle(pane: TerminalPane): Operation<Result<string>> {
return terminalTitle(value.value);
}

/** What a complete grid says on a host where nothing can open one. */
function noTerminalProviderMessage(): string {
return (
"no terminal provider opened this grid. A host installs the terminal-grid capability " +
"explicitly, and this one installs none, so no pane expanded its content and no default " +
"shell started."
);
}

function loopError(segment: ComponentElement, message: string): ErrorSegment {
return { type: "error", message: positioned(message, segment), source: "Loop" };
}
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 75 additions & 10 deletions architecture.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -2917,16 +2917,48 @@ The grid runs as one structured scope:
6. Once attached, each pane settles independently and keeps its final status
visible while siblings continue. The composite remains present after all
panes settle until the reader closes or leaves it.
7. Closing begins an ordered teardown: prevent new pane launches, cancel live
pane scopes, await every child and finalizer, detach and destroy the exact
provider composite, restore the root terminal, and only then release the
foreground lease and settle the grid. The document never continues while an
observable pane child or provider-owned process can still act through the
grid.
7. Reader close first crosses a live close boundary, then begins an ordered
teardown: prevent new pane launches, ask live pane children to close, await
every child and finalizer, detach and destroy the exact provider composite,
restore the root terminal, and only then release the foreground lease and
settle the grid. The document never continues while an observable pane child
or provider-owned process can still act through the grid.

The provider's `closed()` settlement proposes the live close boundary. The
boundary is crossed when the grid owner has entered a cancellation-deferred
await of the grid's durable child and acknowledges that proposal; only then may
the child signal pane close. That await ends only when the task has settled and
its durable `Close` has been acknowledged, not when the grid body has merely
chosen an outcome. This handshake has no provider identity and is not itself
journaled.

Reader-close intent becomes durable only as that completed grid `Close`, after
pane and provider teardown. There is no standalone durable "closing" state. The
gap between observing close and committing it is safe because ordinary parent
cancellation is held pending across the whole gap. A cancellation that arrives
before the owner acknowledges the close boundary cancels the active grid. One that arrives
afterward does not rewrite grid or pane outcomes: panes already settled keep
their outcomes, each then-live pane completes its own scope and retains
`closed`, and the grid retains the same `reader` or `failed` result it would
have retained without the cancellation. Once the grid child is durably closed,
the pending cancellation is delivered to the parent, so no following document
sibling runs in that attempt. A fatal or cleanup failure still takes its
existing precedence over cancellation.

Pane work and every finalizer it installs live inside that pane's durable child
scope. Reader close is cooperative at the durable boundary: it asks the pane to
close and awaits it; it never halts the pane's durable task. The pane may stop
its live nested work as part of its own scope teardown, but its durable child
does not settle as `closed` or write `Close(ok)` until that work and its finalizers
have settled. This preserves the pane's ordinal-derived identity and never
turns a deliberate reader close into a caller-cancelled durable child that a
later run could revive or wait on forever.

Parent cancellation follows the same teardown from preparation, readiness, or
the active grid and remains cancellation. A provider or host failure cancels
the whole grid and is the grid's canonical failure. An ordinary pane failure
the active grid and remains cancellation. Once reader close has crossed its
live boundary, the close result is committed first and that cancellation is
observed by the parent afterward. A provider or host failure cancels the whole
grid and is the grid's canonical failure. An ordinary pane failure
after attachment is contained as that pane's status and does not cancel its
siblings. When the reader closes the grid, core fails it with the first failed
pane in authored order; cancellation initiated by grid teardown is not a pane
Expand DownExpand Up@@ -2977,8 +3009,41 @@ a terminal provider, starting a shell, expanding pane content, acquiring an
Agent session, or launching a native UI. The structured durable boundary owns
that short circuit; a public replay context does not.

Partial replay first compares the exact authored layout and refuses divergence
before provider work. It rebuilds a fresh provider composite: completed pane
The reader-close handshake makes cancellation during teardown a completed-grid
case rather than a new partial-replay state. When a pane finalizer delays close
and parent cancellation arrives, the first attempt still finishes every pane
and provider finalizer, writes the pane outcomes and completed grid `Close`, and
only then reports cancellation to its parent. A continuation claims that
completed child and resumes after it without recreating the provider or
re-entering pane work. A host loss can still interrupt the unjournaled live
teardown; panes whose `Close` was acknowledged remain complete, while any pane
and grid without a completed record follow the existing partial-replay rules.

Partial replay compares the **resolved** layout and refuses divergence before
provider work.

What that can and cannot cover follows from where a resumed run gets its
document. A continuation executes the root the journal retained: the source the
new invocation supplies is not read, not compared and not refused. So the
authored structure of a grid — how many panes it has, their order, and whether
each was written paired or self-closing — is fixed for the whole life of a
journal, and cannot differ between runs. Comparing it would compare a value with
itself.

What can still differ is everything the retained source *resolves*: `columns`
and each `title` are expressions, and props are not restored across a
continuation, so a prop-borne or otherwise live value produces a different
resolved layout from the same retained document. Those are what the comparison
is for, and a change in either refuses before the foreground lease is taken and
before any provider is contacted.

Authored-structure change is therefore not a grid concern. A document whose body
changed under an existing journal is a root-definition compatibility question —
the retained root stays authoritative, and deciding whether a changed source
should be refused rather than ignored belongs to a versioned root boundary that
does not exist yet. Until it does, the grid's obligation is the narrower one it
can actually discharge: retain the complete authored structure, and open the
structure it retained rather than the one the file now shows. It rebuilds a fresh provider composite: completed pane
children are restored as settled statuses without re-running their effects,
while incomplete children replay or start their remaining work. An incomplete
`<Session.Launch>` preserves the prepared/detached identity rules of its own
Expand Down
30 changes: 30 additions & 0 deletions packages/core/mod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -152,6 +152,36 @@ 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";
export {
createTerminalAuthority,
createTerminalGridClaims,
TerminalAuthorityError,
terminalInstallation,
useTerminalInstallation,
} from "./src/terminal/authority.ts";
export type {
PaneReadiness,
TerminalGridAuthority,
TerminalGridClaims,
TerminalPaneClaim,
} from "./src/terminal/authority.ts";
export {
installTerminalProvider,
registerTerminalProvider,
TERMINAL_PROVIDERS_API,
TerminalProviderInstallError,
TerminalProviders,
} from "./src/terminal/provider-api.ts";
export type {
TerminalProviderFactory,
TerminalProviderInstallRequest,
TerminalProviderOptions,
} from "./src/terminal/provider-api.ts";
export { installTerminalGridProfile } from "./src/terminal/profile.ts";
export type { TerminalGridProfileOptions } from "./src/terminal/profile.ts";
export { paneTerminal } from "./src/terminal/pane.ts";
export type { PaneTerminal } from "./src/terminal/pane.ts";
export type { PaneStatus, RetainedGrid, RetainedPaneOutcome } from "./src/terminal/grid.ts";

export { execute, Execution } from "./src/execute.ts";
export type {
Expand Down
166 changes: 138 additions & 28 deletions packages/core/src/expand.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -68,6 +68,10 @@ import {
import type { StructuralViolation, SwitchCase, TerminalPane } from "./structural-rules.ts";
import { terminalGridLayout } from "./terminal-grid.ts";
import type { PlacedPane } from "./terminal-grid.ts";
import { durableGrid, openTerminalGrid, toRequest } from "./terminal/grid.ts";
import type { PaneWork } from "./terminal/grid.ts";
import { recordGridLayout } from "./terminal/journal.ts";
import { usePaneTerminal } from "./terminal/pane.ts";
import {
asBindingViolation,
asExpressionViolation,
Expand DownExpand Up@@ -139,7 +143,7 @@ import {
import { remark } from "remark";
import { select as cssSelect } from "unist-util-select";
import { toString as mdastToString } from "mdast-util-to-string";
import { liveEnvironment } from "./live-env.ts";
import { derivedEnvironment, liveEnvironment } from "./live-env.ts";
import { TestHarnessComponentDefinition } from "./test-harness.ts";
import type { TestHarnessBinding } from "./test-harness.ts";

Expand DownExpand Up@@ -1181,7 +1185,14 @@ function* expandListSegments(
if (segment.name === "Terminal.Grid") {
// No raise() here, like the branches above: expandTerminalGrid
// reports every error it creates.
yield* expandTerminalGrid(segment, result);
yield* expandTerminalGrid(segment, result, {
parentMeta,
parentProps,
hideSet,
path: elementPath,
checkedFailures,
authority,
});
break;
}

Expand DownExpand Up@@ -2102,7 +2113,21 @@ function* resolveStructuralProp(
* does, which is what makes the refusal a closed one rather than a partial grid
* left behind.
*/
function* expandTerminalGrid(segment: ComponentElement, owner: Segment[]): Operation<void> {
/** Everything a pane's own content needs to expand where the grid was written. */
interface GridSite {
readonly parentMeta: Record<string, unknown>;
readonly parentProps: Record<string, Json>;
readonly hideSet: Set<string>;
readonly path: string;
readonly checkedFailures: CheckedFailures | undefined;
readonly authority: ExpansionAuthority | undefined;
}

function* expandTerminalGrid(
segment: ComponentElement,
owner: Segment[],
site: GridSite,
): Operation<void> {
const structure = terminalGridStructure(segment);
if (structure.violations.length > 0) {
for (const violation of structure.violations) {
Expand DownExpand Up@@ -2137,23 +2162,117 @@ function* expandTerminalGrid(segment: ComponentElement, owner: Segment[]): Opera
}

const layout = terminalGridLayout(columns.value, placed);
owner.push(
yield* raise({
type: "error",
message: positioned(noTerminalProviderMessage(), segment),
source: "Terminal.Grid",
// The grid the author asked for, carried beside the sentence so an
// assertion is about the layout that was derived rather than about the
// wording of a refusal.
cause: {
layout: {
columns: layout.columns,
rows: layout.rows,
cells: layout.cells.map((cell) => ({ ...cell })),
},
// The grid renders nothing into the document: what a pane shows belongs to
// that pane, and the sibling after `</Terminal.Grid>` renders to the root
// again only once the provider has restored it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// again only once the provider has restored it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// again only once the provider has restored it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// again only once the provider has restored it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// again only once the provider has restored it.

const identity = {
path: site.path,
...(segment.position === undefined ? {} : { position: segment.position }),
};

try {
// Recorded in this coroutine, before the lease and before any provider is
// contacted: a resumed run whose grid changed is refused while nothing has
// been opened. It cannot live inside the grid child, because a completed
// child never runs.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// child never runs.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// child never runs.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// child never runs.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// child never runs.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// child never runs.

yield* recordGridLayout(identity, toRequest(layout));

const retained = yield* durableGrid(function* (boundary) {
const work = structure.panes.map((pane, index) =>
paneWork(pane, layout.cells[index]!.title, site),
);
return yield* openTerminalGrid(layout, work, boundary);
});

const failed = retained.panes.find((pane) => pane.status === "failed");
if (failed !== undefined) {
owner.push(yield* raise(terminalGridError(segment, failed.reason)));
}
} catch (error) {
owner.push(
yield* raise(
terminalGridError(segment, error instanceof Error ? error.message : String(error)),
),
);
}
}

/**
* What one authored pane does once the grid has minted its claim.
*
* A self-closing pane runs the host's default shell through its claim. A paired
* pane expands its own content in a scope of its own: it inherits the bindings,
* providers, configuration and working directory visible where the grid was
* written, and everything it creates afterwards stays inside the pane. Its
* `<Break>` cannot reach a loop outside the grid, its `<Return>` cannot claim an
* 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 {
if (pane.form === "self-closing") {
return {
ordinal: pane.ordinal,
*run(claim, composite) {
const outcome = yield* claim.admit(() =>
composite.shell(pane.ordinal, () => claim.ready()),
);
if (outcome.signal !== undefined) {
throw new Error(`pane ${pane.ordinal} ("${title}") shell ended on ${outcome.signal}`);
}
if (outcome.exitCode !== undefined && outcome.exitCode !== 0) {
throw new Error(
`pane ${pane.ordinal} ("${title}") shell exited with status ${outcome.exitCode}`,
);
}
},
}),
);
};
}

return {
ordinal: pane.ordinal,
*run(claim, composite) {
yield* scoped(function* () {
// A pane is not inside the loop the grid was written in, so a <Break>
// in its content has no loop to exit and says so.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// in its content has no loop to exit and says so.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// in its content has no loop to exit and says so.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// in its content has no loop to exit and says so.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// in its content has no loop to exit and says so.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// in its content has no loop to exit and says so.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// in its content has no loop to exit and says so.

yield* ActiveLoop.set(undefined);
yield* usePaneTerminal(claim);
const siteEnv = yield* env;
// Starts from what the grid site can see and keeps its own writes: a
// binding this pane makes is visible to later work in this pane and to
// nothing else.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// nothing else.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// nothing else.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// nothing else.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// nothing else.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// nothing else.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// nothing else.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// nothing else.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// nothing else.

yield* provideEnv(derivedEnvironment(siteEnv, { ...(siteEnv?.values ?? {}) }));

const shown: Segment[] = [];
yield* expandSegmentsWithin(
pane.element.children,
site.parentMeta,
site.parentProps,
site.hideSet,
// A counter of its own. Panes expand concurrently, and a shared
// mutable counter would hand two of them block identities that depend
// on which happened to run first.
createBlockCounter(),
shown,
extendPath(
site.path,
elementFrame(pane.element.name, elementSite(pane.element.position, pane.index)),
),
0,
// The pane's own ledger: a checked failure settles this pane and
// cannot reach the root or a sibling.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// cannot reach the root or a sibling.

containedLedger(site.checkedFailures),
site.authority,
// No enclosing value body: a <Return> written in a pane cannot claim
// one outside the grid.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// one outside the grid.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// one outside the grid.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// one outside the grid.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// one outside the grid.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// one outside the grid.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// one outside the grid.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// one outside the grid.

undefined,
);
const text = renderSegments(shown);
if (text.length > 0) {
yield* composite.display(pane.ordinal, text);
}
});
},
};
}

/** The label one pane displays, from the value its own `title` prop produced. */
Expand All@@ -2168,15 +2287,6 @@ function* resolvePaneTitle(pane: TerminalPane): Operation<Result<string>> {
return terminalTitle(value.value);
}

/** What a complete grid says on a host where nothing can open one. */
function noTerminalProviderMessage(): string {
return (
"no terminal provider opened this grid. A host installs the terminal-grid capability " +
"explicitly, and this one installs none, so no pane expanded its content and no default " +
"shell started."
);
}

function loopError(segment: ComponentElement, message: string): ErrorSegment {
return { type: "error", message: positioned(message, segment), source: "Loop" };
}
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 75 additions & 10 deletions architecture.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -2917,16 +2917,48 @@ The grid runs as one structured scope:
6. Once attached, each pane settles independently and keeps its final status
visible while siblings continue. The composite remains present after all
panes settle until the reader closes or leaves it.
7. Closing begins an ordered teardown: prevent new pane launches, cancel live
pane scopes, await every child and finalizer, detach and destroy the exact
provider composite, restore the root terminal, and only then release the
foreground lease and settle the grid. The document never continues while an
observable pane child or provider-owned process can still act through the
grid.
7. Reader close first crosses a live close boundary, then begins an ordered
teardown: prevent new pane launches, ask live pane children to close, await
every child and finalizer, detach and destroy the exact provider composite,
restore the root terminal, and only then release the foreground lease and
settle the grid. The document never continues while an observable pane child
or provider-owned process can still act through the grid.

The provider's `closed()` settlement proposes the live close boundary. The
boundary is crossed when the grid owner has entered a cancellation-deferred
await of the grid's durable child and acknowledges that proposal; only then may
the child signal pane close. That await ends only when the task has settled and
its durable `Close` has been acknowledged, not when the grid body has merely
chosen an outcome. This handshake has no provider identity and is not itself
journaled.

Reader-close intent becomes durable only as that completed grid `Close`, after
pane and provider teardown. There is no standalone durable "closing" state. The
gap between observing close and committing it is safe because ordinary parent
cancellation is held pending across the whole gap. A cancellation that arrives
before the owner acknowledges the close boundary cancels the active grid. One that arrives
afterward does not rewrite grid or pane outcomes: panes already settled keep
their outcomes, each then-live pane completes its own scope and retains
`closed`, and the grid retains the same `reader` or `failed` result it would
have retained without the cancellation. Once the grid child is durably closed,
the pending cancellation is delivered to the parent, so no following document
sibling runs in that attempt. A fatal or cleanup failure still takes its
existing precedence over cancellation.

Pane work and every finalizer it installs live inside that pane's durable child
scope. Reader close is cooperative at the durable boundary: it asks the pane to
close and awaits it; it never halts the pane's durable task. The pane may stop
its live nested work as part of its own scope teardown, but its durable child
does not settle as `closed` or write `Close(ok)` until that work and its finalizers
have settled. This preserves the pane's ordinal-derived identity and never
turns a deliberate reader close into a caller-cancelled durable child that a
later run could revive or wait on forever.

Parent cancellation follows the same teardown from preparation, readiness, or
the active grid and remains cancellation. A provider or host failure cancels
the whole grid and is the grid's canonical failure. An ordinary pane failure
the active grid and remains cancellation. Once reader close has crossed its
live boundary, the close result is committed first and that cancellation is
observed by the parent afterward. A provider or host failure cancels the whole
grid and is the grid's canonical failure. An ordinary pane failure
after attachment is contained as that pane's status and does not cancel its
siblings. When the reader closes the grid, core fails it with the first failed
pane in authored order; cancellation initiated by grid teardown is not a pane
Expand DownExpand Up@@ -2977,8 +3009,41 @@ a terminal provider, starting a shell, expanding pane content, acquiring an
Agent session, or launching a native UI. The structured durable boundary owns
that short circuit; a public replay context does not.

Partial replay first compares the exact authored layout and refuses divergence
before provider work. It rebuilds a fresh provider composite: completed pane
The reader-close handshake makes cancellation during teardown a completed-grid
case rather than a new partial-replay state. When a pane finalizer delays close
and parent cancellation arrives, the first attempt still finishes every pane
and provider finalizer, writes the pane outcomes and completed grid `Close`, and
only then reports cancellation to its parent. A continuation claims that
completed child and resumes after it without recreating the provider or
re-entering pane work. A host loss can still interrupt the unjournaled live
teardown; panes whose `Close` was acknowledged remain complete, while any pane
and grid without a completed record follow the existing partial-replay rules.

Partial replay compares the **resolved** layout and refuses divergence before
provider work.

What that can and cannot cover follows from where a resumed run gets its
document. A continuation executes the root the journal retained: the source the
new invocation supplies is not read, not compared and not refused. So the
authored structure of a grid — how many panes it has, their order, and whether
each was written paired or self-closing — is fixed for the whole life of a
journal, and cannot differ between runs. Comparing it would compare a value with
itself.

What can still differ is everything the retained source *resolves*: `columns`
and each `title` are expressions, and props are not restored across a
continuation, so a prop-borne or otherwise live value produces a different
resolved layout from the same retained document. Those are what the comparison
is for, and a change in either refuses before the foreground lease is taken and
before any provider is contacted.

Authored-structure change is therefore not a grid concern. A document whose body
changed under an existing journal is a root-definition compatibility question —
the retained root stays authoritative, and deciding whether a changed source
should be refused rather than ignored belongs to a versioned root boundary that
does not exist yet. Until it does, the grid's obligation is the narrower one it
can actually discharge: retain the complete authored structure, and open the
structure it retained rather than the one the file now shows. It rebuilds a fresh provider composite: completed pane
children are restored as settled statuses without re-running their effects,
while incomplete children replay or start their remaining work. An incomplete
`<Session.Launch>` preserves the prepared/detached identity rules of its own
Expand Down
30 changes: 30 additions & 0 deletions packages/core/mod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -152,6 +152,36 @@ 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";
export {
createTerminalAuthority,
createTerminalGridClaims,
TerminalAuthorityError,
terminalInstallation,
useTerminalInstallation,
} from "./src/terminal/authority.ts";
export type {
PaneReadiness,
TerminalGridAuthority,
TerminalGridClaims,
TerminalPaneClaim,
} from "./src/terminal/authority.ts";
export {
installTerminalProvider,
registerTerminalProvider,
TERMINAL_PROVIDERS_API,
TerminalProviderInstallError,
TerminalProviders,
} from "./src/terminal/provider-api.ts";
export type {
TerminalProviderFactory,
TerminalProviderInstallRequest,
TerminalProviderOptions,
} from "./src/terminal/provider-api.ts";
export { installTerminalGridProfile } from "./src/terminal/profile.ts";
export type { TerminalGridProfileOptions } from "./src/terminal/profile.ts";
export { paneTerminal } from "./src/terminal/pane.ts";
export type { PaneTerminal } from "./src/terminal/pane.ts";
export type { PaneStatus, RetainedGrid, RetainedPaneOutcome } from "./src/terminal/grid.ts";

export { execute, Execution } from "./src/execute.ts";
export type {
Expand Down
166 changes: 138 additions & 28 deletions packages/core/src/expand.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -68,6 +68,10 @@ import {
import type { StructuralViolation, SwitchCase, TerminalPane } from "./structural-rules.ts";
import { terminalGridLayout } from "./terminal-grid.ts";
import type { PlacedPane } from "./terminal-grid.ts";
import { durableGrid, openTerminalGrid, toRequest } from "./terminal/grid.ts";
import type { PaneWork } from "./terminal/grid.ts";
import { recordGridLayout } from "./terminal/journal.ts";
import { usePaneTerminal } from "./terminal/pane.ts";
import {
asBindingViolation,
asExpressionViolation,
Expand DownExpand Up@@ -139,7 +143,7 @@ import {
import { remark } from "remark";
import { select as cssSelect } from "unist-util-select";
import { toString as mdastToString } from "mdast-util-to-string";
import { liveEnvironment } from "./live-env.ts";
import { derivedEnvironment, liveEnvironment } from "./live-env.ts";
import { TestHarnessComponentDefinition } from "./test-harness.ts";
import type { TestHarnessBinding } from "./test-harness.ts";

Expand DownExpand Up@@ -1181,7 +1185,14 @@ function* expandListSegments(
if (segment.name === "Terminal.Grid") {
// No raise() here, like the branches above: expandTerminalGrid
// reports every error it creates.
yield* expandTerminalGrid(segment, result);
yield* expandTerminalGrid(segment, result, {
parentMeta,
parentProps,
hideSet,
path: elementPath,
checkedFailures,
authority,
});
break;
}

Expand DownExpand Up@@ -2102,7 +2113,21 @@ function* resolveStructuralProp(
* does, which is what makes the refusal a closed one rather than a partial grid
* left behind.
*/
function* expandTerminalGrid(segment: ComponentElement, owner: Segment[]): Operation<void> {
/** Everything a pane's own content needs to expand where the grid was written. */
interface GridSite {
readonly parentMeta: Record<string, unknown>;
readonly parentProps: Record<string, Json>;
readonly hideSet: Set<string>;
readonly path: string;
readonly checkedFailures: CheckedFailures | undefined;
readonly authority: ExpansionAuthority | undefined;
}

function* expandTerminalGrid(
segment: ComponentElement,
owner: Segment[],
site: GridSite,
): Operation<void> {
const structure = terminalGridStructure(segment);
if (structure.violations.length > 0) {
for (const violation of structure.violations) {
Expand DownExpand Up@@ -2137,23 +2162,117 @@ function* expandTerminalGrid(segment: ComponentElement, owner: Segment[]): Opera
}

const layout = terminalGridLayout(columns.value, placed);
owner.push(
yield* raise({
type: "error",
message: positioned(noTerminalProviderMessage(), segment),
source: "Terminal.Grid",
// The grid the author asked for, carried beside the sentence so an
// assertion is about the layout that was derived rather than about the
// wording of a refusal.
cause: {
layout: {
columns: layout.columns,
rows: layout.rows,
cells: layout.cells.map((cell) => ({ ...cell })),
},
// The grid renders nothing into the document: what a pane shows belongs to
// that pane, and the sibling after `</Terminal.Grid>` renders to the root
// again only once the provider has restored it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// again only once the provider has restored it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// again only once the provider has restored it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// again only once the provider has restored it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// again only once the provider has restored it.

const identity = {
path: site.path,
...(segment.position === undefined ? {} : { position: segment.position }),
};

try {
// Recorded in this coroutine, before the lease and before any provider is
// contacted: a resumed run whose grid changed is refused while nothing has
// been opened. It cannot live inside the grid child, because a completed
// child never runs.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// child never runs.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// child never runs.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// child never runs.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// child never runs.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// child never runs.

yield* recordGridLayout(identity, toRequest(layout));

const retained = yield* durableGrid(function* (boundary) {
const work = structure.panes.map((pane, index) =>
paneWork(pane, layout.cells[index]!.title, site),
);
return yield* openTerminalGrid(layout, work, boundary);
});

const failed = retained.panes.find((pane) => pane.status === "failed");
if (failed !== undefined) {
owner.push(yield* raise(terminalGridError(segment, failed.reason)));
}
} catch (error) {
owner.push(
yield* raise(
terminalGridError(segment, error instanceof Error ? error.message : String(error)),
),
);
}
}

/**
* What one authored pane does once the grid has minted its claim.
*
* A self-closing pane runs the host's default shell through its claim. A paired
* pane expands its own content in a scope of its own: it inherits the bindings,
* providers, configuration and working directory visible where the grid was
* written, and everything it creates afterwards stays inside the pane. Its
* `<Break>` cannot reach a loop outside the grid, its `<Return>` cannot claim an
* 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 {
if (pane.form === "self-closing") {
return {
ordinal: pane.ordinal,
*run(claim, composite) {
const outcome = yield* claim.admit(() =>
composite.shell(pane.ordinal, () => claim.ready()),
);
if (outcome.signal !== undefined) {
throw new Error(`pane ${pane.ordinal} ("${title}") shell ended on ${outcome.signal}`);
}
if (outcome.exitCode !== undefined && outcome.exitCode !== 0) {
throw new Error(
`pane ${pane.ordinal} ("${title}") shell exited with status ${outcome.exitCode}`,
);
}
},
}),
);
};
}

return {
ordinal: pane.ordinal,
*run(claim, composite) {
yield* scoped(function* () {
// A pane is not inside the loop the grid was written in, so a <Break>
// in its content has no loop to exit and says so.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// in its content has no loop to exit and says so.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// in its content has no loop to exit and says so.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// in its content has no loop to exit and says so.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// in its content has no loop to exit and says so.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// in its content has no loop to exit and says so.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// in its content has no loop to exit and says so.

yield* ActiveLoop.set(undefined);
yield* usePaneTerminal(claim);
const siteEnv = yield* env;
// Starts from what the grid site can see and keeps its own writes: a
// binding this pane makes is visible to later work in this pane and to
// nothing else.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// nothing else.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// nothing else.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// nothing else.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// nothing else.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// nothing else.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// nothing else.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// nothing else.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// nothing else.

yield* provideEnv(derivedEnvironment(siteEnv, { ...(siteEnv?.values ?? {}) }));

const shown: Segment[] = [];
yield* expandSegmentsWithin(
pane.element.children,
site.parentMeta,
site.parentProps,
site.hideSet,
// A counter of its own. Panes expand concurrently, and a shared
// mutable counter would hand two of them block identities that depend
// on which happened to run first.
createBlockCounter(),
shown,
extendPath(
site.path,
elementFrame(pane.element.name, elementSite(pane.element.position, pane.index)),
),
0,
// The pane's own ledger: a checked failure settles this pane and
// cannot reach the root or a sibling.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// cannot reach the root or a sibling.

containedLedger(site.checkedFailures),
site.authority,
// No enclosing value body: a <Return> written in a pane cannot claim
// one outside the grid.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// one outside the grid.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// one outside the grid.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// one outside the grid.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// one outside the grid.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// one outside the grid.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// one outside the grid.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// one outside the grid.

undefined,
);
const text = renderSegments(shown);
if (text.length > 0) {
yield* composite.display(pane.ordinal, text);
}
});
},
};
}

/** The label one pane displays, from the value its own `title` prop produced. */
Expand All@@ -2168,15 +2287,6 @@ function* resolvePaneTitle(pane: TerminalPane): Operation<Result<string>> {
return terminalTitle(value.value);
}

/** What a complete grid says on a host where nothing can open one. */
function noTerminalProviderMessage(): string {
return (
"no terminal provider opened this grid. A host installs the terminal-grid capability " +
"explicitly, and this one installs none, so no pane expanded its content and no default " +
"shell started."
);
}

function loopError(segment: ComponentElement, message: string): ErrorSegment {
return { type: "error", message: positioned(message, segment), source: "Loop" };
}
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 75 additions & 10 deletions architecture.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -2917,16 +2917,48 @@ The grid runs as one structured scope:
6. Once attached, each pane settles independently and keeps its final status
visible while siblings continue. The composite remains present after all
panes settle until the reader closes or leaves it.
7. Closing begins an ordered teardown: prevent new pane launches, cancel live
pane scopes, await every child and finalizer, detach and destroy the exact
provider composite, restore the root terminal, and only then release the
foreground lease and settle the grid. The document never continues while an
observable pane child or provider-owned process can still act through the
grid.
7. Reader close first crosses a live close boundary, then begins an ordered
teardown: prevent new pane launches, ask live pane children to close, await
every child and finalizer, detach and destroy the exact provider composite,
restore the root terminal, and only then release the foreground lease and
settle the grid. The document never continues while an observable pane child
or provider-owned process can still act through the grid.

The provider's `closed()` settlement proposes the live close boundary. The
boundary is crossed when the grid owner has entered a cancellation-deferred
await of the grid's durable child and acknowledges that proposal; only then may
the child signal pane close. That await ends only when the task has settled and
its durable `Close` has been acknowledged, not when the grid body has merely
chosen an outcome. This handshake has no provider identity and is not itself
journaled.

Reader-close intent becomes durable only as that completed grid `Close`, after
pane and provider teardown. There is no standalone durable "closing" state. The
gap between observing close and committing it is safe because ordinary parent
cancellation is held pending across the whole gap. A cancellation that arrives
before the owner acknowledges the close boundary cancels the active grid. One that arrives
afterward does not rewrite grid or pane outcomes: panes already settled keep
their outcomes, each then-live pane completes its own scope and retains
`closed`, and the grid retains the same `reader` or `failed` result it would
have retained without the cancellation. Once the grid child is durably closed,
the pending cancellation is delivered to the parent, so no following document
sibling runs in that attempt. A fatal or cleanup failure still takes its
existing precedence over cancellation.

Pane work and every finalizer it installs live inside that pane's durable child
scope. Reader close is cooperative at the durable boundary: it asks the pane to
close and awaits it; it never halts the pane's durable task. The pane may stop
its live nested work as part of its own scope teardown, but its durable child
does not settle as `closed` or write `Close(ok)` until that work and its finalizers
have settled. This preserves the pane's ordinal-derived identity and never
turns a deliberate reader close into a caller-cancelled durable child that a
later run could revive or wait on forever.

Parent cancellation follows the same teardown from preparation, readiness, or
the active grid and remains cancellation. A provider or host failure cancels
the whole grid and is the grid's canonical failure. An ordinary pane failure
the active grid and remains cancellation. Once reader close has crossed its
live boundary, the close result is committed first and that cancellation is
observed by the parent afterward. A provider or host failure cancels the whole
grid and is the grid's canonical failure. An ordinary pane failure
after attachment is contained as that pane's status and does not cancel its
siblings. When the reader closes the grid, core fails it with the first failed
pane in authored order; cancellation initiated by grid teardown is not a pane
Expand DownExpand Up@@ -2977,8 +3009,41 @@ a terminal provider, starting a shell, expanding pane content, acquiring an
Agent session, or launching a native UI. The structured durable boundary owns
that short circuit; a public replay context does not.

Partial replay first compares the exact authored layout and refuses divergence
before provider work. It rebuilds a fresh provider composite: completed pane
The reader-close handshake makes cancellation during teardown a completed-grid
case rather than a new partial-replay state. When a pane finalizer delays close
and parent cancellation arrives, the first attempt still finishes every pane
and provider finalizer, writes the pane outcomes and completed grid `Close`, and
only then reports cancellation to its parent. A continuation claims that
completed child and resumes after it without recreating the provider or
re-entering pane work. A host loss can still interrupt the unjournaled live
teardown; panes whose `Close` was acknowledged remain complete, while any pane
and grid without a completed record follow the existing partial-replay rules.

Partial replay compares the **resolved** layout and refuses divergence before
provider work.

What that can and cannot cover follows from where a resumed run gets its
document. A continuation executes the root the journal retained: the source the
new invocation supplies is not read, not compared and not refused. So the
authored structure of a grid — how many panes it has, their order, and whether
each was written paired or self-closing — is fixed for the whole life of a
journal, and cannot differ between runs. Comparing it would compare a value with
itself.

What can still differ is everything the retained source *resolves*: `columns`
and each `title` are expressions, and props are not restored across a
continuation, so a prop-borne or otherwise live value produces a different
resolved layout from the same retained document. Those are what the comparison
is for, and a change in either refuses before the foreground lease is taken and
before any provider is contacted.

Authored-structure change is therefore not a grid concern. A document whose body
changed under an existing journal is a root-definition compatibility question —
the retained root stays authoritative, and deciding whether a changed source
should be refused rather than ignored belongs to a versioned root boundary that
does not exist yet. Until it does, the grid's obligation is the narrower one it
can actually discharge: retain the complete authored structure, and open the
structure it retained rather than the one the file now shows. It rebuilds a fresh provider composite: completed pane
children are restored as settled statuses without re-running their effects,
while incomplete children replay or start their remaining work. An incomplete
`<Session.Launch>` preserves the prepared/detached identity rules of its own
Expand Down
30 changes: 30 additions & 0 deletions packages/core/mod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -152,6 +152,36 @@ 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";
export {
createTerminalAuthority,
createTerminalGridClaims,
TerminalAuthorityError,
terminalInstallation,
useTerminalInstallation,
} from "./src/terminal/authority.ts";
export type {
PaneReadiness,
TerminalGridAuthority,
TerminalGridClaims,
TerminalPaneClaim,
} from "./src/terminal/authority.ts";
export {
installTerminalProvider,
registerTerminalProvider,
TERMINAL_PROVIDERS_API,
TerminalProviderInstallError,
TerminalProviders,
} from "./src/terminal/provider-api.ts";
export type {
TerminalProviderFactory,
TerminalProviderInstallRequest,
TerminalProviderOptions,
} from "./src/terminal/provider-api.ts";
export { installTerminalGridProfile } from "./src/terminal/profile.ts";
export type { TerminalGridProfileOptions } from "./src/terminal/profile.ts";
export { paneTerminal } from "./src/terminal/pane.ts";
export type { PaneTerminal } from "./src/terminal/pane.ts";
export type { PaneStatus, RetainedGrid, RetainedPaneOutcome } from "./src/terminal/grid.ts";

export { execute, Execution } from "./src/execute.ts";
export type {
Expand Down
166 changes: 138 additions & 28 deletions packages/core/src/expand.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -68,6 +68,10 @@ import {
import type { StructuralViolation, SwitchCase, TerminalPane } from "./structural-rules.ts";
import { terminalGridLayout } from "./terminal-grid.ts";
import type { PlacedPane } from "./terminal-grid.ts";
import { durableGrid, openTerminalGrid, toRequest } from "./terminal/grid.ts";
import type { PaneWork } from "./terminal/grid.ts";
import { recordGridLayout } from "./terminal/journal.ts";
import { usePaneTerminal } from "./terminal/pane.ts";
import {
asBindingViolation,
asExpressionViolation,
Expand DownExpand Up@@ -139,7 +143,7 @@ import {
import { remark } from "remark";
import { select as cssSelect } from "unist-util-select";
import { toString as mdastToString } from "mdast-util-to-string";
import { liveEnvironment } from "./live-env.ts";
import { derivedEnvironment, liveEnvironment } from "./live-env.ts";
import { TestHarnessComponentDefinition } from "./test-harness.ts";
import type { TestHarnessBinding } from "./test-harness.ts";

Expand DownExpand Up@@ -1181,7 +1185,14 @@ function* expandListSegments(
if (segment.name === "Terminal.Grid") {
// No raise() here, like the branches above: expandTerminalGrid
// reports every error it creates.
yield* expandTerminalGrid(segment, result);
yield* expandTerminalGrid(segment, result, {
parentMeta,
parentProps,
hideSet,
path: elementPath,
checkedFailures,
authority,
});
break;
}

Expand DownExpand Up@@ -2102,7 +2113,21 @@ function* resolveStructuralProp(
* does, which is what makes the refusal a closed one rather than a partial grid
* left behind.
*/
function* expandTerminalGrid(segment: ComponentElement, owner: Segment[]): Operation<void> {
/** Everything a pane's own content needs to expand where the grid was written. */
interface GridSite {
readonly parentMeta: Record<string, unknown>;
readonly parentProps: Record<string, Json>;
readonly hideSet: Set<string>;
readonly path: string;
readonly checkedFailures: CheckedFailures | undefined;
readonly authority: ExpansionAuthority | undefined;
}

function* expandTerminalGrid(
segment: ComponentElement,
owner: Segment[],
site: GridSite,
): Operation<void> {
const structure = terminalGridStructure(segment);
if (structure.violations.length > 0) {
for (const violation of structure.violations) {
Expand DownExpand Up@@ -2137,23 +2162,117 @@ function* expandTerminalGrid(segment: ComponentElement, owner: Segment[]): Opera
}

const layout = terminalGridLayout(columns.value, placed);
owner.push(
yield* raise({
type: "error",
message: positioned(noTerminalProviderMessage(), segment),
source: "Terminal.Grid",
// The grid the author asked for, carried beside the sentence so an
// assertion is about the layout that was derived rather than about the
// wording of a refusal.
cause: {
layout: {
columns: layout.columns,
rows: layout.rows,
cells: layout.cells.map((cell) => ({ ...cell })),
},
// The grid renders nothing into the document: what a pane shows belongs to
// that pane, and the sibling after `</Terminal.Grid>` renders to the root
// again only once the provider has restored it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// again only once the provider has restored it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// again only once the provider has restored it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// again only once the provider has restored it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// again only once the provider has restored it.

const identity = {
path: site.path,
...(segment.position === undefined ? {} : { position: segment.position }),
};

try {
// Recorded in this coroutine, before the lease and before any provider is
// contacted: a resumed run whose grid changed is refused while nothing has
// been opened. It cannot live inside the grid child, because a completed
// child never runs.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// child never runs.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// child never runs.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// child never runs.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// child never runs.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// child never runs.

yield* recordGridLayout(identity, toRequest(layout));

const retained = yield* durableGrid(function* (boundary) {
const work = structure.panes.map((pane, index) =>
paneWork(pane, layout.cells[index]!.title, site),
);
return yield* openTerminalGrid(layout, work, boundary);
});

const failed = retained.panes.find((pane) => pane.status === "failed");
if (failed !== undefined) {
owner.push(yield* raise(terminalGridError(segment, failed.reason)));
}
} catch (error) {
owner.push(
yield* raise(
terminalGridError(segment, error instanceof Error ? error.message : String(error)),
),
);
}
}

/**
* What one authored pane does once the grid has minted its claim.
*
* A self-closing pane runs the host's default shell through its claim. A paired
* pane expands its own content in a scope of its own: it inherits the bindings,
* providers, configuration and working directory visible where the grid was
* written, and everything it creates afterwards stays inside the pane. Its
* `<Break>` cannot reach a loop outside the grid, its `<Return>` cannot claim an
* 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 {
if (pane.form === "self-closing") {
return {
ordinal: pane.ordinal,
*run(claim, composite) {
const outcome = yield* claim.admit(() =>
composite.shell(pane.ordinal, () => claim.ready()),
);
if (outcome.signal !== undefined) {
throw new Error(`pane ${pane.ordinal} ("${title}") shell ended on ${outcome.signal}`);
}
if (outcome.exitCode !== undefined && outcome.exitCode !== 0) {
throw new Error(
`pane ${pane.ordinal} ("${title}") shell exited with status ${outcome.exitCode}`,
);
}
},
}),
);
};
}

return {
ordinal: pane.ordinal,
*run(claim, composite) {
yield* scoped(function* () {
// A pane is not inside the loop the grid was written in, so a <Break>
// in its content has no loop to exit and says so.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// in its content has no loop to exit and says so.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// in its content has no loop to exit and says so.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// in its content has no loop to exit and says so.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// in its content has no loop to exit and says so.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// in its content has no loop to exit and says so.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// in its content has no loop to exit and says so.

yield* ActiveLoop.set(undefined);
yield* usePaneTerminal(claim);
const siteEnv = yield* env;
// Starts from what the grid site can see and keeps its own writes: a
// binding this pane makes is visible to later work in this pane and to
// nothing else.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// nothing else.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// nothing else.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// nothing else.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// nothing else.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// nothing else.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// nothing else.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// nothing else.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// nothing else.

yield* provideEnv(derivedEnvironment(siteEnv, { ...(siteEnv?.values ?? {}) }));

const shown: Segment[] = [];
yield* expandSegmentsWithin(
pane.element.children,
site.parentMeta,
site.parentProps,
site.hideSet,
// A counter of its own. Panes expand concurrently, and a shared
// mutable counter would hand two of them block identities that depend
// on which happened to run first.
createBlockCounter(),
shown,
extendPath(
site.path,
elementFrame(pane.element.name, elementSite(pane.element.position, pane.index)),
),
0,
// The pane's own ledger: a checked failure settles this pane and
// cannot reach the root or a sibling.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// cannot reach the root or a sibling.

containedLedger(site.checkedFailures),
site.authority,
// No enclosing value body: a <Return> written in a pane cannot claim
// one outside the grid.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// one outside the grid.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// one outside the grid.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// one outside the grid.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// one outside the grid.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// one outside the grid.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// one outside the grid.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// one outside the grid.

undefined,
);
const text = renderSegments(shown);
if (text.length > 0) {
yield* composite.display(pane.ordinal, text);
}
});
},
};
}

/** The label one pane displays, from the value its own `title` prop produced. */
Expand All@@ -2168,15 +2287,6 @@ function* resolvePaneTitle(pane: TerminalPane): Operation<Result<string>> {
return terminalTitle(value.value);
}

/** What a complete grid says on a host where nothing can open one. */
function noTerminalProviderMessage(): string {
return (
"no terminal provider opened this grid. A host installs the terminal-grid capability " +
"explicitly, and this one installs none, so no pane expanded its content and no default " +
"shell started."
);
}

function loopError(segment: ComponentElement, message: string): ErrorSegment {
return { type: "error", message: positioned(message, segment), source: "Loop" };
}
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 75 additions & 10 deletions architecture.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -2917,16 +2917,48 @@ The grid runs as one structured scope:
6. Once attached, each pane settles independently and keeps its final status
visible while siblings continue. The composite remains present after all
panes settle until the reader closes or leaves it.
7. Closing begins an ordered teardown: prevent new pane launches, cancel live
pane scopes, await every child and finalizer, detach and destroy the exact
provider composite, restore the root terminal, and only then release the
foreground lease and settle the grid. The document never continues while an
observable pane child or provider-owned process can still act through the
grid.
7. Reader close first crosses a live close boundary, then begins an ordered
teardown: prevent new pane launches, ask live pane children to close, await
every child and finalizer, detach and destroy the exact provider composite,
restore the root terminal, and only then release the foreground lease and
settle the grid. The document never continues while an observable pane child
or provider-owned process can still act through the grid.

The provider's `closed()` settlement proposes the live close boundary. The
boundary is crossed when the grid owner has entered a cancellation-deferred
await of the grid's durable child and acknowledges that proposal; only then may
the child signal pane close. That await ends only when the task has settled and
its durable `Close` has been acknowledged, not when the grid body has merely
chosen an outcome. This handshake has no provider identity and is not itself
journaled.

Reader-close intent becomes durable only as that completed grid `Close`, after
pane and provider teardown. There is no standalone durable "closing" state. The
gap between observing close and committing it is safe because ordinary parent
cancellation is held pending across the whole gap. A cancellation that arrives
before the owner acknowledges the close boundary cancels the active grid. One that arrives
afterward does not rewrite grid or pane outcomes: panes already settled keep
their outcomes, each then-live pane completes its own scope and retains
`closed`, and the grid retains the same `reader` or `failed` result it would
have retained without the cancellation. Once the grid child is durably closed,
the pending cancellation is delivered to the parent, so no following document
sibling runs in that attempt. A fatal or cleanup failure still takes its
existing precedence over cancellation.

Pane work and every finalizer it installs live inside that pane's durable child
scope. Reader close is cooperative at the durable boundary: it asks the pane to
close and awaits it; it never halts the pane's durable task. The pane may stop
its live nested work as part of its own scope teardown, but its durable child
does not settle as `closed` or write `Close(ok)` until that work and its finalizers
have settled. This preserves the pane's ordinal-derived identity and never
turns a deliberate reader close into a caller-cancelled durable child that a
later run could revive or wait on forever.

Parent cancellation follows the same teardown from preparation, readiness, or
the active grid and remains cancellation. A provider or host failure cancels
the whole grid and is the grid's canonical failure. An ordinary pane failure
the active grid and remains cancellation. Once reader close has crossed its
live boundary, the close result is committed first and that cancellation is
observed by the parent afterward. A provider or host failure cancels the whole
grid and is the grid's canonical failure. An ordinary pane failure
after attachment is contained as that pane's status and does not cancel its
siblings. When the reader closes the grid, core fails it with the first failed
pane in authored order; cancellation initiated by grid teardown is not a pane
Expand DownExpand Up@@ -2977,8 +3009,41 @@ a terminal provider, starting a shell, expanding pane content, acquiring an
Agent session, or launching a native UI. The structured durable boundary owns
that short circuit; a public replay context does not.

Partial replay first compares the exact authored layout and refuses divergence
before provider work. It rebuilds a fresh provider composite: completed pane
The reader-close handshake makes cancellation during teardown a completed-grid
case rather than a new partial-replay state. When a pane finalizer delays close
and parent cancellation arrives, the first attempt still finishes every pane
and provider finalizer, writes the pane outcomes and completed grid `Close`, and
only then reports cancellation to its parent. A continuation claims that
completed child and resumes after it without recreating the provider or
re-entering pane work. A host loss can still interrupt the unjournaled live
teardown; panes whose `Close` was acknowledged remain complete, while any pane
and grid without a completed record follow the existing partial-replay rules.

Partial replay compares the **resolved** layout and refuses divergence before
provider work.

What that can and cannot cover follows from where a resumed run gets its
document. A continuation executes the root the journal retained: the source the
new invocation supplies is not read, not compared and not refused. So the
authored structure of a grid — how many panes it has, their order, and whether
each was written paired or self-closing — is fixed for the whole life of a
journal, and cannot differ between runs. Comparing it would compare a value with
itself.

What can still differ is everything the retained source *resolves*: `columns`
and each `title` are expressions, and props are not restored across a
continuation, so a prop-borne or otherwise live value produces a different
resolved layout from the same retained document. Those are what the comparison
is for, and a change in either refuses before the foreground lease is taken and
before any provider is contacted.

Authored-structure change is therefore not a grid concern. A document whose body
changed under an existing journal is a root-definition compatibility question —
the retained root stays authoritative, and deciding whether a changed source
should be refused rather than ignored belongs to a versioned root boundary that
does not exist yet. Until it does, the grid's obligation is the narrower one it
can actually discharge: retain the complete authored structure, and open the
structure it retained rather than the one the file now shows. It rebuilds a fresh provider composite: completed pane
children are restored as settled statuses without re-running their effects,
while incomplete children replay or start their remaining work. An incomplete
`<Session.Launch>` preserves the prepared/detached identity rules of its own
Expand Down
30 changes: 30 additions & 0 deletions packages/core/mod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -152,6 +152,36 @@ 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";
export {
createTerminalAuthority,
createTerminalGridClaims,
TerminalAuthorityError,
terminalInstallation,
useTerminalInstallation,
} from "./src/terminal/authority.ts";
export type {
PaneReadiness,
TerminalGridAuthority,
TerminalGridClaims,
TerminalPaneClaim,
} from "./src/terminal/authority.ts";
export {
installTerminalProvider,
registerTerminalProvider,
TERMINAL_PROVIDERS_API,
TerminalProviderInstallError,
TerminalProviders,
} from "./src/terminal/provider-api.ts";
export type {
TerminalProviderFactory,
TerminalProviderInstallRequest,
TerminalProviderOptions,
} from "./src/terminal/provider-api.ts";
export { installTerminalGridProfile } from "./src/terminal/profile.ts";
export type { TerminalGridProfileOptions } from "./src/terminal/profile.ts";
export { paneTerminal } from "./src/terminal/pane.ts";
export type { PaneTerminal } from "./src/terminal/pane.ts";
export type { PaneStatus, RetainedGrid, RetainedPaneOutcome } from "./src/terminal/grid.ts";

export { execute, Execution } from "./src/execute.ts";
export type {
Expand Down
166 changes: 138 additions & 28 deletions packages/core/src/expand.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -68,6 +68,10 @@ import {
import type { StructuralViolation, SwitchCase, TerminalPane } from "./structural-rules.ts";
import { terminalGridLayout } from "./terminal-grid.ts";
import type { PlacedPane } from "./terminal-grid.ts";
import { durableGrid, openTerminalGrid, toRequest } from "./terminal/grid.ts";
import type { PaneWork } from "./terminal/grid.ts";
import { recordGridLayout } from "./terminal/journal.ts";
import { usePaneTerminal } from "./terminal/pane.ts";
import {
asBindingViolation,
asExpressionViolation,
Expand DownExpand Up@@ -139,7 +143,7 @@ import {
import { remark } from "remark";
import { select as cssSelect } from "unist-util-select";
import { toString as mdastToString } from "mdast-util-to-string";
import { liveEnvironment } from "./live-env.ts";
import { derivedEnvironment, liveEnvironment } from "./live-env.ts";
import { TestHarnessComponentDefinition } from "./test-harness.ts";
import type { TestHarnessBinding } from "./test-harness.ts";

Expand DownExpand Up@@ -1181,7 +1185,14 @@ function* expandListSegments(
if (segment.name === "Terminal.Grid") {
// No raise() here, like the branches above: expandTerminalGrid
// reports every error it creates.
yield* expandTerminalGrid(segment, result);
yield* expandTerminalGrid(segment, result, {
parentMeta,
parentProps,
hideSet,
path: elementPath,
checkedFailures,
authority,
});
break;
}

Expand DownExpand Up@@ -2102,7 +2113,21 @@ function* resolveStructuralProp(
* does, which is what makes the refusal a closed one rather than a partial grid
* left behind.
*/
function* expandTerminalGrid(segment: ComponentElement, owner: Segment[]): Operation<void> {
/** Everything a pane's own content needs to expand where the grid was written. */
interface GridSite {
readonly parentMeta: Record<string, unknown>;
readonly parentProps: Record<string, Json>;
readonly hideSet: Set<string>;
readonly path: string;
readonly checkedFailures: CheckedFailures | undefined;
readonly authority: ExpansionAuthority | undefined;
}

function* expandTerminalGrid(
segment: ComponentElement,
owner: Segment[],
site: GridSite,
): Operation<void> {
const structure = terminalGridStructure(segment);
if (structure.violations.length > 0) {
for (const violation of structure.violations) {
Expand DownExpand Up@@ -2137,23 +2162,117 @@ function* expandTerminalGrid(segment: ComponentElement, owner: Segment[]): Opera
}

const layout = terminalGridLayout(columns.value, placed);
owner.push(
yield* raise({
type: "error",
message: positioned(noTerminalProviderMessage(), segment),
source: "Terminal.Grid",
// The grid the author asked for, carried beside the sentence so an
// assertion is about the layout that was derived rather than about the
// wording of a refusal.
cause: {
layout: {
columns: layout.columns,
rows: layout.rows,
cells: layout.cells.map((cell) => ({ ...cell })),
},
// The grid renders nothing into the document: what a pane shows belongs to
// that pane, and the sibling after `</Terminal.Grid>` renders to the root
// again only once the provider has restored it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// again only once the provider has restored it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// again only once the provider has restored it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// again only once the provider has restored it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// again only once the provider has restored it.

const identity = {
path: site.path,
...(segment.position === undefined ? {} : { position: segment.position }),
};

try {
// Recorded in this coroutine, before the lease and before any provider is
// contacted: a resumed run whose grid changed is refused while nothing has
// been opened. It cannot live inside the grid child, because a completed
// child never runs.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// child never runs.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// child never runs.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// child never runs.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// child never runs.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// child never runs.

yield* recordGridLayout(identity, toRequest(layout));

const retained = yield* durableGrid(function* (boundary) {
const work = structure.panes.map((pane, index) =>
paneWork(pane, layout.cells[index]!.title, site),
);
return yield* openTerminalGrid(layout, work, boundary);
});

const failed = retained.panes.find((pane) => pane.status === "failed");
if (failed !== undefined) {
owner.push(yield* raise(terminalGridError(segment, failed.reason)));
}
} catch (error) {
owner.push(
yield* raise(
terminalGridError(segment, error instanceof Error ? error.message : String(error)),
),
);
}
}

/**
* What one authored pane does once the grid has minted its claim.
*
* A self-closing pane runs the host's default shell through its claim. A paired
* pane expands its own content in a scope of its own: it inherits the bindings,
* providers, configuration and working directory visible where the grid was
* written, and everything it creates afterwards stays inside the pane. Its
* `<Break>` cannot reach a loop outside the grid, its `<Return>` cannot claim an
* 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 {
if (pane.form === "self-closing") {
return {
ordinal: pane.ordinal,
*run(claim, composite) {
const outcome = yield* claim.admit(() =>
composite.shell(pane.ordinal, () => claim.ready()),
);
if (outcome.signal !== undefined) {
throw new Error(`pane ${pane.ordinal} ("${title}") shell ended on ${outcome.signal}`);
}
if (outcome.exitCode !== undefined && outcome.exitCode !== 0) {
throw new Error(
`pane ${pane.ordinal} ("${title}") shell exited with status ${outcome.exitCode}`,
);
}
},
}),
);
};
}

return {
ordinal: pane.ordinal,
*run(claim, composite) {
yield* scoped(function* () {
// A pane is not inside the loop the grid was written in, so a <Break>
// in its content has no loop to exit and says so.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// in its content has no loop to exit and says so.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// in its content has no loop to exit and says so.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// in its content has no loop to exit and says so.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// in its content has no loop to exit and says so.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// in its content has no loop to exit and says so.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// in its content has no loop to exit and says so.

yield* ActiveLoop.set(undefined);
yield* usePaneTerminal(claim);
const siteEnv = yield* env;
// Starts from what the grid site can see and keeps its own writes: a
// binding this pane makes is visible to later work in this pane and to
// nothing else.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// nothing else.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// nothing else.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// nothing else.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// nothing else.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// nothing else.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// nothing else.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// nothing else.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// nothing else.

yield* provideEnv(derivedEnvironment(siteEnv, { ...(siteEnv?.values ?? {}) }));

const shown: Segment[] = [];
yield* expandSegmentsWithin(
pane.element.children,
site.parentMeta,
site.parentProps,
site.hideSet,
// A counter of its own. Panes expand concurrently, and a shared
// mutable counter would hand two of them block identities that depend
// on which happened to run first.
createBlockCounter(),
shown,
extendPath(
site.path,
elementFrame(pane.element.name, elementSite(pane.element.position, pane.index)),
),
0,
// The pane's own ledger: a checked failure settles this pane and
// cannot reach the root or a sibling.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// cannot reach the root or a sibling.

containedLedger(site.checkedFailures),
site.authority,
// No enclosing value body: a <Return> written in a pane cannot claim
// one outside the grid.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// one outside the grid.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// one outside the grid.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// one outside the grid.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// one outside the grid.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// one outside the grid.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// one outside the grid.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Redundant comment — restates what the code does.

Suggested change
// one outside the grid.

undefined,
);
const text = renderSegments(shown);
if (text.length > 0) {
yield* composite.display(pane.ordinal, text);
}
});
},
};
}

/** The label one pane displays, from the value its own `title` prop produced. */
Expand All@@ -2168,15 +2287,6 @@ function* resolvePaneTitle(pane: TerminalPane): Operation<Result<string>> {
return terminalTitle(value.value);
}

/** What a complete grid says on a host where nothing can open one. */
function noTerminalProviderMessage(): string {
return (
"no terminal provider opened this grid. A host installs the terminal-grid capability " +
"explicitly, and this one installs none, so no pane expanded its content and no default " +
"shell started."
);
}

function loopError(segment: ComponentElement, message: string): ErrorSegment {
return { type: "error", message: positioned(message, segment), source: "Loop" };
}
Expand Down
Loading
Loading