Cut comments back to the non-obvious

Rationale that explains a decision rather than the code below it belongs in
the sandbox README or the PR, not in a header paragraph on every file.
This commit is contained in:
Gregory Schier
2026-08-17 22:10:12 -07:00
parent 8d10a5d879
commit 45adc2d023
23 changed files with 141 additions and 529 deletions
+12 -41
View File
@@ -1,15 +1,9 @@
/**
* `ctx`, as a plugin sees it, built once for every runtime that has one.
* `ctx`, built once for every runtime that has one. A runtime supplies only how
* a payload reaches its host.
*
* Two runtimes host plugins today — the Node sidecar over a WebSocket, and the
* QuickJS sandbox over a message port — and a third will when the sandbox is
* embedded in Rust. What `ctx.httpRequest.send(...)` *means* is the same in all
* of them, so it is built here, and the only thing a runtime supplies is how a
* payload gets to its host and back.
*
* `stream` and `form` are optional because they are the two places where a host
* genuinely differs: both need a conversation rather than one reply, and a
* runtime that cannot hold one degrades honestly instead of pretending.
* `stream` and `form` are optional because they are the two places a host
* genuinely differs: both need a conversation rather than one reply.
*/
import type {
@@ -53,19 +47,14 @@ import { createResponseBody, decodeBase64Chunk } from "./responseBody";
import { applyFormInputDefaults } from "./templateFunction";
export interface PluginTransport {
/** One request out, one reply back. Rejects if the host couldn't answer. */
request(
context: PluginContext,
payload: InternalEventPayload,
): Promise<Record<string, unknown>>;
/** Send with no reply expected. */
notify(context: PluginContext, payload: InternalEventPayload): void;
/**
* Send once and keep receiving. Used by windows, which report navigation
* until they close. Absent where a host has no windows to open.
*/
/** Send once, keep receiving. Windows report navigation until they close. */
stream?(
context: PluginContext,
payload: InternalEventPayload,
@@ -73,11 +62,8 @@ export interface PluginTransport {
): void;
/**
* Show a form that may re-render before it settles.
*
* `onChange` is called with the values entered so far and answers with the
* form to show next, so inputs that compute themselves from other inputs stay
* live. A host without it gets a form drawn once from its defaults.
* A form that may re-render before it settles: `onChange` answers with the
* form to show next. Without it, a form is drawn once from its defaults.
*/
form?(
context: PluginContext,
@@ -88,14 +74,7 @@ export interface PluginTransport {
): Promise<PromptFormResponse>;
}
/**
* A response as a plugin should see it.
*
* `bodyPath` names a file on a host's disk: meaningless to a plugin, absent
* once bodies move off the filesystem, impossible in a browser. Plugins address
* bodies by response id, so it is dropped rather than left for one to grow a
* dependency on.
*/
/** `bodyPath` names a file on a host's disk; plugins address bodies by id. */
function forPlugin(httpResponse: HttpResponse): HttpResponse {
const { bodyPath: _bodyPath, ...rest } = httpResponse as HttpResponse & {
bodyPath?: string | null;
@@ -110,7 +89,6 @@ export function createPluginContext(
const send = <T>(payload: InternalEventPayload): Promise<T> =>
transport.request(context, payload) as Promise<T>;
/** Read a body the host has stored, a chunk at a time, following it if it is still arriving. */
const storedBody = async (responseId: string) => {
const bodyInfo = () =>
send<GetHttpResponseBodyInfoResponse>({
@@ -191,9 +169,8 @@ export function createPluginContext(
return reply.value;
},
form: async (args) => {
// Inputs may compute themselves from the values entered so far, and a
// function cannot cross to a host — so they are resolved against the
// defaults before the form is drawn, then stripped.
// Inputs may compute from the values entered so far, and a function
// cannot cross to a host.
const resolve = async (values: Record<string, unknown>) => {
const callArgs: CallPromptFormDynamicArgs = { values } as CallPromptFormDynamicArgs;
const resolved = await applyDynamicFormInput(
@@ -217,8 +194,7 @@ export function createPluginContext(
}
const reply = await transport.form(context, payload, async (values) => {
// Fired on mount before any interaction, when there is nothing to
// recompute from.
// Fired on mount, before there is anything to recompute from.
if (values == null || Object.keys(values).length === 0) return null;
return { type: "prompt_form_request", ...args, inputs: await resolve(values) };
});
@@ -259,8 +235,7 @@ export function createPluginContext(
});
// A send with no request behind it saves nothing, so the reply carries
// the only copy of its body. A saved one is read back from the host like
// any other. Callers get the same thing either way.
// the only copy of its body.
if (body == null) {
return { httpResponse: forPlugin(httpResponse), body: await storedBody(httpResponse.id) };
}
@@ -366,10 +341,6 @@ export function createPluginContext(
},
},
templates: {
/**
* Invoke Yaak's template engine to render a value. If the value is a nested
* type (eg. object), it will be recursively rendered.
*/
render: async (args: TemplateRenderRequest) => {
const result = await send<TemplateRenderResponse>({
type: "template_render_request",
+1 -16
View File
@@ -1,13 +1,3 @@
/**
* The form handling every plugin runtime does, wherever it runs.
*
* A plugin declares its inputs as data, but any of them may compute itself
* from the values entered so far — so a runtime has to resolve those callbacks
* before a host can draw the form, then strip them, because a function cannot
* cross a process, a worker, or a sandbox boundary. That is the same work for
* the Node runtime and the QuickJS one, so it lives here rather than in either.
*/
import type {
CallPromptFormDynamicArgs,
Context,
@@ -86,12 +76,7 @@ export async function applyDynamicFormInput(
return resolvedArgs;
}
/**
* Drop the `dynamic` callbacks, recursively, leaving inputs that serialize.
*
* Called on the way out of a runtime, after [`applyDynamicFormInput`] has run
* them: what a host receives has to be data all the way down.
*/
/** What a host receives has to be data all the way down. */
export function stripDynamicCallbacks(inputs: { dynamic?: unknown }[]): FormInput[] {
return inputs.map((input) => {
// oxlint-disable-next-line no-explicit-any -- stripping dynamic from union type