Add a bundled local WebForm component for schema-backed user input #195

Description

@taras

Motivation

Executable Markdown workflows need a minimal human-input checkpoint before the
full <Elicit> interaction model is designed. The workflow should be able to
open a local schema-generated form, wait for one validated response, capture
that structured value, and continue in the same execution.

This story introduces the behavior as a general web component rather than
hiding browser and server semantics inside <Elicit>.

Implementation architecture and parallel coordination

PR #237 provides the merged package and deterministic bundled-client foundation. The remaining WebForm work proceeds as a small PR stack in parallel with #202.

<WebForm> is an ordinary TypeScript function component:

  • @executablemd/web registers it as a non-reserved default through the scope-local registration API delivered by Register function components and migrate legacy component handlers #202 PR 1;
  • core resolves expression props, validates the static props schema, removes reserved props, requires and validates as, validates the return, creates the invocation boundary, and applies the binding;
  • the component never receives a raw ComponentElement, expression map, binding environment, or as value;
  • the component calls content() and completes schema parsing, schema compilation, UI-schema parsing, and body sanitization before starting a listener, printing a URL, launching a browser, or journaling a live response;
  • an uncaught ContentError prevents all live WebForm effects through the function-component boundary established by Make content() the failure boundary for function components #232;
  • the component declares a broad JSON return schema, returns the one server-validated response, and emits no document output;
  • repository-local WebForm.md or WebForm.ts overrides the package default under Register function components and migrate legacy component handlers #202's resolution contract.
  • the component delegates live interaction to one package-owned operation that accepts the normalized schema, optional UI schema, and sanitized rendered content and returns the server-validated JSON response; the WebForm adapter for Add provider-neutral Elicit through the core Context API #197 reuses this operation without receiving component props, as, or journal state.

Do not implement WebForm through Component.expand, Component.expandSegments, a private expression resolver, or component-owned as handling. The earlier expansion-handler design is obsolete.

The remaining stack is:

  1. Declaration parsing, shared Ajv policy, and sanitization. Parse structured or captured-JSON schema and uiSchema; compile draft-07 with the shared browser/server policy; render Markdown through remark → remark-rehype → rehype-sanitize → rehype-stringify; drop raw HTML and active markup; preserve the fixed same-origin image policy. This begins from merged ✨ feat: @executablemd/web foundation + deterministic browser asset pipeline (#195) #237 and has no Register function components and migrate legacy component handlers #202 dependency.
  2. Loopback form server. Bind only 127.0.0.1; implement token-scoped routes, exact Host/Origin checks, the fixed headers and CSP, the streaming 1 MiB limit, authoritative validation, atomic first-valid reservation, concurrent-submission coverage, 409 after reservation, and cleanup on success, failure, and interruption. This depends only on the preceding WebForm slice.
  3. Form opener and responder seams. Add the package-owned contextual opener and internal test responder; keep opener failure warning-only; own and observe the opener task; preserve the live URL fallback. This depends only on the preceding WebForm slice.
  4. Registered <WebForm>, reusable provider operation, durability, and specification. Base this slice on the preceding WebForm work plus Register function components and migrate legacy component handlers #202 PR 1 only. Establish the package-owned live-form operation and the provider adapter boundary consumed later by Add provider-neutral Elicit through the core Context API #197; register the function component as one caller of that operation; call content() before live effects; return the validated response through core's declared-return/as path; and journal only that response with the deterministic schema/UI-schema/sanitized-body fingerprint. Prove content failure performs zero listener, opener, responder, URL-printing, and journal work. Register function components and migrate legacy component handlers #202 PRs 2–6 are not dependencies.
  5. CLI integration, browser CI, documentation, and publication. Install the web registration in the CLI, run the real Playwright security/form suite, update release configuration and its specification together, publish the completed package, and perform the one manual keyboard/browser smoke check.

Each slice stops for review and passes the four repository checks plus the relevant Node, Bun, browser, compiled-CLI, or site checks. PRs from the two issues must not share implementation branches: rebase the WebForm registration slice onto the merged #202 registration foundation when it is ready.

Authoring contract

<WebForm
schema={responseSchema}
uiSchema={responseUiSchema}
as="response"
># Review required
Review the implementation plan and provide your decision.
</WebForm>
  • <WebForm> lives in a separate @executablemd/web package.
  • schema and as are required.
  • schema accepts captured JSON text or an already structured draft-07 JSON
    Schema value.
  • uiSchema is optional and accepts captured JSON text or a structured RJSF UI
    schema.
  • Invocation content expands through content() and its rendered Markdown appears above the form.
  • A valid submission binds the structured form data to as.
  • The component emits nothing into the surrounding document.
  • The initial presentation is the bundled RJSF Shadcn theme. It is the fixed
    default and is not exposed as a component option.

Local browser lifecycle

On a live execution, <WebForm>:

  1. renders its body and compiles the supplied schema;
  2. starts a server bound only to 127.0.0.1 on an ephemeral port;
  3. creates an unguessable, execution-local form URL;
  4. prints the URL and attempts to open it in the default browser;
  5. waits for exactly one valid form submission;
  6. shuts down the server and binds the validated response.

Failure to launch the browser produces a warning but does not fail the
component; the printed URL remains usable. Closing the tab does not cancel the
component. It continues waiting until a valid submission or workflow
interruption.

The submitted page attempts to close itself after success. When the browser
does not permit that, it displays a safe-to-close confirmation.

The component owns the server, browser-launch operation, and related background
tasks through one structured-concurrency scope. Success, failure, and
interruption all stop the server and release its port. There is no component
timeout; the surrounding workflow controls interruption.

Bundled client

The browser client bundles React, RJSF, the Ajv 8 validator, the Shadcn theme,
and every required script, stylesheet, font, and icon into assets shipped by
@executablemd/web.

  • The page makes no CDN or other external network requests.
  • The loopback server serves only the assets for the active form URL.
  • Ports, tokens, URLs, and browser process details are ephemeral runtime state
    and are not part of the authoring API.
  • The implementation records the generated client bundle size so the
    dependency cost is visible before the package is accepted.

RJSF provides immediate browser validation. The loopback server validates the
submitted data again with equivalent draft-07 Ajv configuration and treats the
server result as authoritative. Malformed or schema-invalid requests never
enter workflow state and do not consume the one successful submission.

Durability

Only the validated structured response is journaled.

On replay, <WebForm> restores that response immediately without allocating a
port, starting a server, printing a URL, or launching a browser. Transport
details never appear in the durable result.

Testing

Add a colocated WebForm.test.md document that uses an internally injected test
responder. It supplies predetermined form data and asserts the structured
captured value without adding a test-only prop to the public <WebForm> API.

Add lower-level automated tests for:

  • required props and both accepted schema representations;
  • optional uiSchema parsing and application;
  • normal invocation-content rendering above the generated form;
  • structured capture with no surrounding rendered output;
  • loopback-only binding and unguessable URL authorization;
  • delivery of every required client asset without external requests;
  • representative Shadcn widgets, field errors, and submit behavior;
  • equivalent browser and server validation;
  • malformed and schema-invalid submissions remaining unresolved;
  • accepting only the first valid submission;
  • successful page-close response and the safe-to-close fallback;
  • browser-launch success and warning-only launch failure;
  • server and background-task cleanup after success, failure, and interruption;
  • journal contents excluding transport details;
  • full replay returning the recorded value without live browser or server work;
    and
  • Deno, Node, Bun, and compiled CLI packaging where supported by the workspace.

The browser-facing integration test uses a representative review schema with a
required enum, boolean, optional text, and textarea. A manual smoke check
confirms that the default browser opens and the Shadcn page is usable with a
keyboard. No automated test depends on a user's installed browser.

Documentation

  • Specify the component props, lifecycle, capture behavior, validation, and
    interruption semantics.
  • Document that the page is loopback-only, single-use, and fully bundled.
  • Add a website example for a human review checkpoint using schema and
    uiSchema.
  • Explain that Shadcn is the fixed initial presentation rather than a public
    theme-selection API.
  • Record the generated client bundle size and asset-generation process.

Acceptance criteria

  • <WebForm> captures exactly one server-validated structured response.
  • Execution remains local and requires no network access after installation.
  • The default browser opens when possible and the printed URL always provides a
    fallback.
  • Closing the tab does not resolve or cancel the component.
  • All owned resources are released on every exit path.
  • Replay restores the response without repeating browser or server effects.
  • RJSF Shadcn presentation is bundled as the fixed default.
  • Markdown and lower-level tests cover public behavior, security boundaries,
    cleanup, validation, packaging, and replay.
  • Specification and website documentation ship in the same PR.

Not included

  • The complete <Elicit> interaction and focus-stack model.
  • Runtime-selectable themes or author-supplied templates and widgets.
  • A general-purpose web server component.
  • Remote assets, CDN fallback, or externally reachable forms.
  • Treating browser-tab closure as cancellation.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all \u003cpre\u003e\u003ccode\u003e 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

      Add a bundled local WebForm component for schema-backed user input #195

      Description

      @taras

      Motivation

      Executable Markdown workflows need a minimal human-input checkpoint before the
      full <Elicit> interaction model is designed. The workflow should be able to
      open a local schema-generated form, wait for one validated response, capture
      that structured value, and continue in the same execution.

      This story introduces the behavior as a general web component rather than
      hiding browser and server semantics inside <Elicit>.

      Implementation architecture and parallel coordination

      PR #237 provides the merged package and deterministic bundled-client foundation. The remaining WebForm work proceeds as a small PR stack in parallel with #202.

      <WebForm> is an ordinary TypeScript function component:

      • @executablemd/web registers it as a non-reserved default through the scope-local registration API delivered by Register function components and migrate legacy component handlers #202 PR 1;
      • core resolves expression props, validates the static props schema, removes reserved props, requires and validates as, validates the return, creates the invocation boundary, and applies the binding;
      • the component never receives a raw ComponentElement, expression map, binding environment, or as value;
      • the component calls content() and completes schema parsing, schema compilation, UI-schema parsing, and body sanitization before starting a listener, printing a URL, launching a browser, or journaling a live response;
      • an uncaught ContentError prevents all live WebForm effects through the function-component boundary established by Make content() the failure boundary for function components #232;
      • the component declares a broad JSON return schema, returns the one server-validated response, and emits no document output;
      • repository-local WebForm.md or WebForm.ts overrides the package default under Register function components and migrate legacy component handlers #202's resolution contract.
      • the component delegates live interaction to one package-owned operation that accepts the normalized schema, optional UI schema, and sanitized rendered content and returns the server-validated JSON response; the WebForm adapter for Add provider-neutral Elicit through the core Context API #197 reuses this operation without receiving component props, as, or journal state.

      Do not implement WebForm through Component.expand, Component.expandSegments, a private expression resolver, or component-owned as handling. The earlier expansion-handler design is obsolete.

      The remaining stack is:

      1. Declaration parsing, shared Ajv policy, and sanitization. Parse structured or captured-JSON schema and uiSchema; compile draft-07 with the shared browser/server policy; render Markdown through remark → remark-rehype → rehype-sanitize → rehype-stringify; drop raw HTML and active markup; preserve the fixed same-origin image policy. This begins from merged ✨ feat: @executablemd/web foundation + deterministic browser asset pipeline (#195) #237 and has no Register function components and migrate legacy component handlers #202 dependency.
      2. Loopback form server. Bind only 127.0.0.1; implement token-scoped routes, exact Host/Origin checks, the fixed headers and CSP, the streaming 1 MiB limit, authoritative validation, atomic first-valid reservation, concurrent-submission coverage, 409 after reservation, and cleanup on success, failure, and interruption. This depends only on the preceding WebForm slice.
      3. Form opener and responder seams. Add the package-owned contextual opener and internal test responder; keep opener failure warning-only; own and observe the opener task; preserve the live URL fallback. This depends only on the preceding WebForm slice.
      4. Registered <WebForm>, reusable provider operation, durability, and specification. Base this slice on the preceding WebForm work plus Register function components and migrate legacy component handlers #202 PR 1 only. Establish the package-owned live-form operation and the provider adapter boundary consumed later by Add provider-neutral Elicit through the core Context API #197; register the function component as one caller of that operation; call content() before live effects; return the validated response through core's declared-return/as path; and journal only that response with the deterministic schema/UI-schema/sanitized-body fingerprint. Prove content failure performs zero listener, opener, responder, URL-printing, and journal work. Register function components and migrate legacy component handlers #202 PRs 2–6 are not dependencies.
      5. CLI integration, browser CI, documentation, and publication. Install the web registration in the CLI, run the real Playwright security/form suite, update release configuration and its specification together, publish the completed package, and perform the one manual keyboard/browser smoke check.

      Each slice stops for review and passes the four repository checks plus the relevant Node, Bun, browser, compiled-CLI, or site checks. PRs from the two issues must not share implementation branches: rebase the WebForm registration slice onto the merged #202 registration foundation when it is ready.

      Authoring contract

      <WebForm
      schema={responseSchema}
      uiSchema={responseUiSchema}
      as="response"
      ># Review required
      Review the implementation plan and provide your decision.
      </WebForm>
      • <WebForm> lives in a separate @executablemd/web package.
      • schema and as are required.
      • schema accepts captured JSON text or an already structured draft-07 JSON
        Schema value.
      • uiSchema is optional and accepts captured JSON text or a structured RJSF UI
        schema.
      • Invocation content expands through content() and its rendered Markdown appears above the form.
      • A valid submission binds the structured form data to as.
      • The component emits nothing into the surrounding document.
      • The initial presentation is the bundled RJSF Shadcn theme. It is the fixed
        default and is not exposed as a component option.

      Local browser lifecycle

      On a live execution, <WebForm>:

      1. renders its body and compiles the supplied schema;
      2. starts a server bound only to 127.0.0.1 on an ephemeral port;
      3. creates an unguessable, execution-local form URL;
      4. prints the URL and attempts to open it in the default browser;
      5. waits for exactly one valid form submission;
      6. shuts down the server and binds the validated response.

      Failure to launch the browser produces a warning but does not fail the
      component; the printed URL remains usable. Closing the tab does not cancel the
      component. It continues waiting until a valid submission or workflow
      interruption.

      The submitted page attempts to close itself after success. When the browser
      does not permit that, it displays a safe-to-close confirmation.

      The component owns the server, browser-launch operation, and related background
      tasks through one structured-concurrency scope. Success, failure, and
      interruption all stop the server and release its port. There is no component
      timeout; the surrounding workflow controls interruption.

      Bundled client

      The browser client bundles React, RJSF, the Ajv 8 validator, the Shadcn theme,
      and every required script, stylesheet, font, and icon into assets shipped by
      @executablemd/web.

      • The page makes no CDN or other external network requests.
      • The loopback server serves only the assets for the active form URL.
      • Ports, tokens, URLs, and browser process details are ephemeral runtime state
        and are not part of the authoring API.
      • The implementation records the generated client bundle size so the
        dependency cost is visible before the package is accepted.

      RJSF provides immediate browser validation. The loopback server validates the
      submitted data again with equivalent draft-07 Ajv configuration and treats the
      server result as authoritative. Malformed or schema-invalid requests never
      enter workflow state and do not consume the one successful submission.

      Durability

      Only the validated structured response is journaled.

      On replay, <WebForm> restores that response immediately without allocating a
      port, starting a server, printing a URL, or launching a browser. Transport
      details never appear in the durable result.

      Testing

      Add a colocated WebForm.test.md document that uses an internally injected test
      responder. It supplies predetermined form data and asserts the structured
      captured value without adding a test-only prop to the public <WebForm> API.

      Add lower-level automated tests for:

      • required props and both accepted schema representations;
      • optional uiSchema parsing and application;
      • normal invocation-content rendering above the generated form;
      • structured capture with no surrounding rendered output;
      • loopback-only binding and unguessable URL authorization;
      • delivery of every required client asset without external requests;
      • representative Shadcn widgets, field errors, and submit behavior;
      • equivalent browser and server validation;
      • malformed and schema-invalid submissions remaining unresolved;
      • accepting only the first valid submission;
      • successful page-close response and the safe-to-close fallback;
      • browser-launch success and warning-only launch failure;
      • server and background-task cleanup after success, failure, and interruption;
      • journal contents excluding transport details;
      • full replay returning the recorded value without live browser or server work;
        and
      • Deno, Node, Bun, and compiled CLI packaging where supported by the workspace.

      The browser-facing integration test uses a representative review schema with a
      required enum, boolean, optional text, and textarea. A manual smoke check
      confirms that the default browser opens and the Shadcn page is usable with a
      keyboard. No automated test depends on a user's installed browser.

      Documentation

      • Specify the component props, lifecycle, capture behavior, validation, and
        interruption semantics.
      • Document that the page is loopback-only, single-use, and fully bundled.
      • Add a website example for a human review checkpoint using schema and
        uiSchema.
      • Explain that Shadcn is the fixed initial presentation rather than a public
        theme-selection API.
      • Record the generated client bundle size and asset-generation process.

      Acceptance criteria

      • <WebForm> captures exactly one server-validated structured response.
      • Execution remains local and requires no network access after installation.
      • The default browser opens when possible and the printed URL always provides a
        fallback.
      • Closing the tab does not resolve or cancel the component.
      • All owned resources are released on every exit path.
      • Replay restores the response without repeating browser or server effects.
      • RJSF Shadcn presentation is bundled as the fixed default.
      • Markdown and lower-level tests cover public behavior, security boundaries,
        cleanup, validation, packaging, and replay.
      • Specification and website documentation ship in the same PR.

      Not included

      • The complete <Elicit> interaction and focus-stack model.
      • Runtime-selectable themes or author-supplied templates and widgets.
      • A general-purpose web server component.
      • Remote assets, CDN fallback, or externally reachable forms.
      • Treating browser-tab closure as cancellation.

      Activity

      Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

      Metadata

      Metadata

      Assignees

      No one assigned

        Labels

        No labels
        No labels

        Projects

        No projects

          Milestone

          No milestone

          Relationships

          None yet

          Development

          No branches or pull requests

          Issue actions

          , '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

          Add a bundled local WebForm component for schema-backed user input #195

          Description

          @taras

          Motivation

          Executable Markdown workflows need a minimal human-input checkpoint before the
          full <Elicit> interaction model is designed. The workflow should be able to
          open a local schema-generated form, wait for one validated response, capture
          that structured value, and continue in the same execution.

          This story introduces the behavior as a general web component rather than
          hiding browser and server semantics inside <Elicit>.

          Implementation architecture and parallel coordination

          PR #237 provides the merged package and deterministic bundled-client foundation. The remaining WebForm work proceeds as a small PR stack in parallel with #202.

          <WebForm> is an ordinary TypeScript function component:

          • @executablemd/web registers it as a non-reserved default through the scope-local registration API delivered by Register function components and migrate legacy component handlers #202 PR 1;
          • core resolves expression props, validates the static props schema, removes reserved props, requires and validates as, validates the return, creates the invocation boundary, and applies the binding;
          • the component never receives a raw ComponentElement, expression map, binding environment, or as value;
          • the component calls content() and completes schema parsing, schema compilation, UI-schema parsing, and body sanitization before starting a listener, printing a URL, launching a browser, or journaling a live response;
          • an uncaught ContentError prevents all live WebForm effects through the function-component boundary established by Make content() the failure boundary for function components #232;
          • the component declares a broad JSON return schema, returns the one server-validated response, and emits no document output;
          • repository-local WebForm.md or WebForm.ts overrides the package default under Register function components and migrate legacy component handlers #202's resolution contract.
          • the component delegates live interaction to one package-owned operation that accepts the normalized schema, optional UI schema, and sanitized rendered content and returns the server-validated JSON response; the WebForm adapter for Add provider-neutral Elicit through the core Context API #197 reuses this operation without receiving component props, as, or journal state.

          Do not implement WebForm through Component.expand, Component.expandSegments, a private expression resolver, or component-owned as handling. The earlier expansion-handler design is obsolete.

          The remaining stack is:

          1. Declaration parsing, shared Ajv policy, and sanitization. Parse structured or captured-JSON schema and uiSchema; compile draft-07 with the shared browser/server policy; render Markdown through remark → remark-rehype → rehype-sanitize → rehype-stringify; drop raw HTML and active markup; preserve the fixed same-origin image policy. This begins from merged ✨ feat: @executablemd/web foundation + deterministic browser asset pipeline (#195) #237 and has no Register function components and migrate legacy component handlers #202 dependency.
          2. Loopback form server. Bind only 127.0.0.1; implement token-scoped routes, exact Host/Origin checks, the fixed headers and CSP, the streaming 1 MiB limit, authoritative validation, atomic first-valid reservation, concurrent-submission coverage, 409 after reservation, and cleanup on success, failure, and interruption. This depends only on the preceding WebForm slice.
          3. Form opener and responder seams. Add the package-owned contextual opener and internal test responder; keep opener failure warning-only; own and observe the opener task; preserve the live URL fallback. This depends only on the preceding WebForm slice.
          4. Registered <WebForm>, reusable provider operation, durability, and specification. Base this slice on the preceding WebForm work plus Register function components and migrate legacy component handlers #202 PR 1 only. Establish the package-owned live-form operation and the provider adapter boundary consumed later by Add provider-neutral Elicit through the core Context API #197; register the function component as one caller of that operation; call content() before live effects; return the validated response through core's declared-return/as path; and journal only that response with the deterministic schema/UI-schema/sanitized-body fingerprint. Prove content failure performs zero listener, opener, responder, URL-printing, and journal work. Register function components and migrate legacy component handlers #202 PRs 2–6 are not dependencies.
          5. CLI integration, browser CI, documentation, and publication. Install the web registration in the CLI, run the real Playwright security/form suite, update release configuration and its specification together, publish the completed package, and perform the one manual keyboard/browser smoke check.

          Each slice stops for review and passes the four repository checks plus the relevant Node, Bun, browser, compiled-CLI, or site checks. PRs from the two issues must not share implementation branches: rebase the WebForm registration slice onto the merged #202 registration foundation when it is ready.

          Authoring contract

          <WebForm
          schema={responseSchema}
          uiSchema={responseUiSchema}
          as="response"
          ># Review required
          Review the implementation plan and provide your decision.
          </WebForm>
          • <WebForm> lives in a separate @executablemd/web package.
          • schema and as are required.
          • schema accepts captured JSON text or an already structured draft-07 JSON
            Schema value.
          • uiSchema is optional and accepts captured JSON text or a structured RJSF UI
            schema.
          • Invocation content expands through content() and its rendered Markdown appears above the form.
          • A valid submission binds the structured form data to as.
          • The component emits nothing into the surrounding document.
          • The initial presentation is the bundled RJSF Shadcn theme. It is the fixed
            default and is not exposed as a component option.

          Local browser lifecycle

          On a live execution, <WebForm>:

          1. renders its body and compiles the supplied schema;
          2. starts a server bound only to 127.0.0.1 on an ephemeral port;
          3. creates an unguessable, execution-local form URL;
          4. prints the URL and attempts to open it in the default browser;
          5. waits for exactly one valid form submission;
          6. shuts down the server and binds the validated response.

          Failure to launch the browser produces a warning but does not fail the
          component; the printed URL remains usable. Closing the tab does not cancel the
          component. It continues waiting until a valid submission or workflow
          interruption.

          The submitted page attempts to close itself after success. When the browser
          does not permit that, it displays a safe-to-close confirmation.

          The component owns the server, browser-launch operation, and related background
          tasks through one structured-concurrency scope. Success, failure, and
          interruption all stop the server and release its port. There is no component
          timeout; the surrounding workflow controls interruption.

          Bundled client

          The browser client bundles React, RJSF, the Ajv 8 validator, the Shadcn theme,
          and every required script, stylesheet, font, and icon into assets shipped by
          @executablemd/web.

          • The page makes no CDN or other external network requests.
          • The loopback server serves only the assets for the active form URL.
          • Ports, tokens, URLs, and browser process details are ephemeral runtime state
            and are not part of the authoring API.
          • The implementation records the generated client bundle size so the
            dependency cost is visible before the package is accepted.

          RJSF provides immediate browser validation. The loopback server validates the
          submitted data again with equivalent draft-07 Ajv configuration and treats the
          server result as authoritative. Malformed or schema-invalid requests never
          enter workflow state and do not consume the one successful submission.

          Durability

          Only the validated structured response is journaled.

          On replay, <WebForm> restores that response immediately without allocating a
          port, starting a server, printing a URL, or launching a browser. Transport
          details never appear in the durable result.

          Testing

          Add a colocated WebForm.test.md document that uses an internally injected test
          responder. It supplies predetermined form data and asserts the structured
          captured value without adding a test-only prop to the public <WebForm> API.

          Add lower-level automated tests for:

          • required props and both accepted schema representations;
          • optional uiSchema parsing and application;
          • normal invocation-content rendering above the generated form;
          • structured capture with no surrounding rendered output;
          • loopback-only binding and unguessable URL authorization;
          • delivery of every required client asset without external requests;
          • representative Shadcn widgets, field errors, and submit behavior;
          • equivalent browser and server validation;
          • malformed and schema-invalid submissions remaining unresolved;
          • accepting only the first valid submission;
          • successful page-close response and the safe-to-close fallback;
          • browser-launch success and warning-only launch failure;
          • server and background-task cleanup after success, failure, and interruption;
          • journal contents excluding transport details;
          • full replay returning the recorded value without live browser or server work;
            and
          • Deno, Node, Bun, and compiled CLI packaging where supported by the workspace.

          The browser-facing integration test uses a representative review schema with a
          required enum, boolean, optional text, and textarea. A manual smoke check
          confirms that the default browser opens and the Shadcn page is usable with a
          keyboard. No automated test depends on a user's installed browser.

          Documentation

          • Specify the component props, lifecycle, capture behavior, validation, and
            interruption semantics.
          • Document that the page is loopback-only, single-use, and fully bundled.
          • Add a website example for a human review checkpoint using schema and
            uiSchema.
          • Explain that Shadcn is the fixed initial presentation rather than a public
            theme-selection API.
          • Record the generated client bundle size and asset-generation process.

          Acceptance criteria

          • <WebForm> captures exactly one server-validated structured response.
          • Execution remains local and requires no network access after installation.
          • The default browser opens when possible and the printed URL always provides a
            fallback.
          • Closing the tab does not resolve or cancel the component.
          • All owned resources are released on every exit path.
          • Replay restores the response without repeating browser or server effects.
          • RJSF Shadcn presentation is bundled as the fixed default.
          • Markdown and lower-level tests cover public behavior, security boundaries,
            cleanup, validation, packaging, and replay.
          • Specification and website documentation ship in the same PR.

          Not included

          • The complete <Elicit> interaction and focus-stack model.
          • Runtime-selectable themes or author-supplied templates and widgets.
          • A general-purpose web server component.
          • Remote assets, CDN fallback, or externally reachable forms.
          • Treating browser-tab closure as cancellation.

          Activity

          Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

          Metadata

          Metadata

          Assignees

          No one assigned

            Labels

            No labels
            No labels

            Projects

            No projects

              Milestone

              No milestone

              Relationships

              None yet

              Development

              No branches or pull requests

              Issue actions

              , '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 \u003e 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

              Add a bundled local WebForm component for schema-backed user input #195

              Description

              @taras

              Motivation

              Executable Markdown workflows need a minimal human-input checkpoint before the
              full <Elicit> interaction model is designed. The workflow should be able to
              open a local schema-generated form, wait for one validated response, capture
              that structured value, and continue in the same execution.

              This story introduces the behavior as a general web component rather than
              hiding browser and server semantics inside <Elicit>.

              Implementation architecture and parallel coordination

              PR #237 provides the merged package and deterministic bundled-client foundation. The remaining WebForm work proceeds as a small PR stack in parallel with #202.

              <WebForm> is an ordinary TypeScript function component:

              • @executablemd/web registers it as a non-reserved default through the scope-local registration API delivered by Register function components and migrate legacy component handlers #202 PR 1;
              • core resolves expression props, validates the static props schema, removes reserved props, requires and validates as, validates the return, creates the invocation boundary, and applies the binding;
              • the component never receives a raw ComponentElement, expression map, binding environment, or as value;
              • the component calls content() and completes schema parsing, schema compilation, UI-schema parsing, and body sanitization before starting a listener, printing a URL, launching a browser, or journaling a live response;
              • an uncaught ContentError prevents all live WebForm effects through the function-component boundary established by Make content() the failure boundary for function components #232;
              • the component declares a broad JSON return schema, returns the one server-validated response, and emits no document output;
              • repository-local WebForm.md or WebForm.ts overrides the package default under Register function components and migrate legacy component handlers #202's resolution contract.
              • the component delegates live interaction to one package-owned operation that accepts the normalized schema, optional UI schema, and sanitized rendered content and returns the server-validated JSON response; the WebForm adapter for Add provider-neutral Elicit through the core Context API #197 reuses this operation without receiving component props, as, or journal state.

              Do not implement WebForm through Component.expand, Component.expandSegments, a private expression resolver, or component-owned as handling. The earlier expansion-handler design is obsolete.

              The remaining stack is:

              1. Declaration parsing, shared Ajv policy, and sanitization. Parse structured or captured-JSON schema and uiSchema; compile draft-07 with the shared browser/server policy; render Markdown through remark → remark-rehype → rehype-sanitize → rehype-stringify; drop raw HTML and active markup; preserve the fixed same-origin image policy. This begins from merged ✨ feat: @executablemd/web foundation + deterministic browser asset pipeline (#195) #237 and has no Register function components and migrate legacy component handlers #202 dependency.
              2. Loopback form server. Bind only 127.0.0.1; implement token-scoped routes, exact Host/Origin checks, the fixed headers and CSP, the streaming 1 MiB limit, authoritative validation, atomic first-valid reservation, concurrent-submission coverage, 409 after reservation, and cleanup on success, failure, and interruption. This depends only on the preceding WebForm slice.
              3. Form opener and responder seams. Add the package-owned contextual opener and internal test responder; keep opener failure warning-only; own and observe the opener task; preserve the live URL fallback. This depends only on the preceding WebForm slice.
              4. Registered <WebForm>, reusable provider operation, durability, and specification. Base this slice on the preceding WebForm work plus Register function components and migrate legacy component handlers #202 PR 1 only. Establish the package-owned live-form operation and the provider adapter boundary consumed later by Add provider-neutral Elicit through the core Context API #197; register the function component as one caller of that operation; call content() before live effects; return the validated response through core's declared-return/as path; and journal only that response with the deterministic schema/UI-schema/sanitized-body fingerprint. Prove content failure performs zero listener, opener, responder, URL-printing, and journal work. Register function components and migrate legacy component handlers #202 PRs 2–6 are not dependencies.
              5. CLI integration, browser CI, documentation, and publication. Install the web registration in the CLI, run the real Playwright security/form suite, update release configuration and its specification together, publish the completed package, and perform the one manual keyboard/browser smoke check.

              Each slice stops for review and passes the four repository checks plus the relevant Node, Bun, browser, compiled-CLI, or site checks. PRs from the two issues must not share implementation branches: rebase the WebForm registration slice onto the merged #202 registration foundation when it is ready.

              Authoring contract

              <WebForm
              schema={responseSchema}
              uiSchema={responseUiSchema}
              as="response"
              ># Review required
              Review the implementation plan and provide your decision.
              </WebForm>
              • <WebForm> lives in a separate @executablemd/web package.
              • schema and as are required.
              • schema accepts captured JSON text or an already structured draft-07 JSON
                Schema value.
              • uiSchema is optional and accepts captured JSON text or a structured RJSF UI
                schema.
              • Invocation content expands through content() and its rendered Markdown appears above the form.
              • A valid submission binds the structured form data to as.
              • The component emits nothing into the surrounding document.
              • The initial presentation is the bundled RJSF Shadcn theme. It is the fixed
                default and is not exposed as a component option.

              Local browser lifecycle

              On a live execution, <WebForm>:

              1. renders its body and compiles the supplied schema;
              2. starts a server bound only to 127.0.0.1 on an ephemeral port;
              3. creates an unguessable, execution-local form URL;
              4. prints the URL and attempts to open it in the default browser;
              5. waits for exactly one valid form submission;
              6. shuts down the server and binds the validated response.

              Failure to launch the browser produces a warning but does not fail the
              component; the printed URL remains usable. Closing the tab does not cancel the
              component. It continues waiting until a valid submission or workflow
              interruption.

              The submitted page attempts to close itself after success. When the browser
              does not permit that, it displays a safe-to-close confirmation.

              The component owns the server, browser-launch operation, and related background
              tasks through one structured-concurrency scope. Success, failure, and
              interruption all stop the server and release its port. There is no component
              timeout; the surrounding workflow controls interruption.

              Bundled client

              The browser client bundles React, RJSF, the Ajv 8 validator, the Shadcn theme,
              and every required script, stylesheet, font, and icon into assets shipped by
              @executablemd/web.

              • The page makes no CDN or other external network requests.
              • The loopback server serves only the assets for the active form URL.
              • Ports, tokens, URLs, and browser process details are ephemeral runtime state
                and are not part of the authoring API.
              • The implementation records the generated client bundle size so the
                dependency cost is visible before the package is accepted.

              RJSF provides immediate browser validation. The loopback server validates the
              submitted data again with equivalent draft-07 Ajv configuration and treats the
              server result as authoritative. Malformed or schema-invalid requests never
              enter workflow state and do not consume the one successful submission.

              Durability

              Only the validated structured response is journaled.

              On replay, <WebForm> restores that response immediately without allocating a
              port, starting a server, printing a URL, or launching a browser. Transport
              details never appear in the durable result.

              Testing

              Add a colocated WebForm.test.md document that uses an internally injected test
              responder. It supplies predetermined form data and asserts the structured
              captured value without adding a test-only prop to the public <WebForm> API.

              Add lower-level automated tests for:

              • required props and both accepted schema representations;
              • optional uiSchema parsing and application;
              • normal invocation-content rendering above the generated form;
              • structured capture with no surrounding rendered output;
              • loopback-only binding and unguessable URL authorization;
              • delivery of every required client asset without external requests;
              • representative Shadcn widgets, field errors, and submit behavior;
              • equivalent browser and server validation;
              • malformed and schema-invalid submissions remaining unresolved;
              • accepting only the first valid submission;
              • successful page-close response and the safe-to-close fallback;
              • browser-launch success and warning-only launch failure;
              • server and background-task cleanup after success, failure, and interruption;
              • journal contents excluding transport details;
              • full replay returning the recorded value without live browser or server work;
                and
              • Deno, Node, Bun, and compiled CLI packaging where supported by the workspace.

              The browser-facing integration test uses a representative review schema with a
              required enum, boolean, optional text, and textarea. A manual smoke check
              confirms that the default browser opens and the Shadcn page is usable with a
              keyboard. No automated test depends on a user's installed browser.

              Documentation

              • Specify the component props, lifecycle, capture behavior, validation, and
                interruption semantics.
              • Document that the page is loopback-only, single-use, and fully bundled.
              • Add a website example for a human review checkpoint using schema and
                uiSchema.
              • Explain that Shadcn is the fixed initial presentation rather than a public
                theme-selection API.
              • Record the generated client bundle size and asset-generation process.

              Acceptance criteria

              • <WebForm> captures exactly one server-validated structured response.
              • Execution remains local and requires no network access after installation.
              • The default browser opens when possible and the printed URL always provides a
                fallback.
              • Closing the tab does not resolve or cancel the component.
              • All owned resources are released on every exit path.
              • Replay restores the response without repeating browser or server effects.
              • RJSF Shadcn presentation is bundled as the fixed default.
              • Markdown and lower-level tests cover public behavior, security boundaries,
                cleanup, validation, packaging, and replay.
              • Specification and website documentation ship in the same PR.

              Not included

              • The complete <Elicit> interaction and focus-stack model.
              • Runtime-selectable themes or author-supplied templates and widgets.
              • A general-purpose web server component.
              • Remote assets, CDN fallback, or externally reachable forms.
              • Treating browser-tab closure as cancellation.

              Activity

              Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

              Metadata

              Metadata

              Assignees

              No one assigned

                Labels

                No labels
                No labels

                Projects

                No projects

                  Milestone

                  No milestone

                  Relationships

                  None yet

                  Development

                  No branches or pull requests

                  Issue actions

                  , '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

                  Add a bundled local WebForm component for schema-backed user input #195

                  Description

                  @taras

                  Motivation

                  Executable Markdown workflows need a minimal human-input checkpoint before the
                  full <Elicit> interaction model is designed. The workflow should be able to
                  open a local schema-generated form, wait for one validated response, capture
                  that structured value, and continue in the same execution.

                  This story introduces the behavior as a general web component rather than
                  hiding browser and server semantics inside <Elicit>.

                  Implementation architecture and parallel coordination

                  PR #237 provides the merged package and deterministic bundled-client foundation. The remaining WebForm work proceeds as a small PR stack in parallel with #202.

                  <WebForm> is an ordinary TypeScript function component:

                  • @executablemd/web registers it as a non-reserved default through the scope-local registration API delivered by Register function components and migrate legacy component handlers #202 PR 1;
                  • core resolves expression props, validates the static props schema, removes reserved props, requires and validates as, validates the return, creates the invocation boundary, and applies the binding;
                  • the component never receives a raw ComponentElement, expression map, binding environment, or as value;
                  • the component calls content() and completes schema parsing, schema compilation, UI-schema parsing, and body sanitization before starting a listener, printing a URL, launching a browser, or journaling a live response;
                  • an uncaught ContentError prevents all live WebForm effects through the function-component boundary established by Make content() the failure boundary for function components #232;
                  • the component declares a broad JSON return schema, returns the one server-validated response, and emits no document output;
                  • repository-local WebForm.md or WebForm.ts overrides the package default under Register function components and migrate legacy component handlers #202's resolution contract.
                  • the component delegates live interaction to one package-owned operation that accepts the normalized schema, optional UI schema, and sanitized rendered content and returns the server-validated JSON response; the WebForm adapter for Add provider-neutral Elicit through the core Context API #197 reuses this operation without receiving component props, as, or journal state.

                  Do not implement WebForm through Component.expand, Component.expandSegments, a private expression resolver, or component-owned as handling. The earlier expansion-handler design is obsolete.

                  The remaining stack is:

                  1. Declaration parsing, shared Ajv policy, and sanitization. Parse structured or captured-JSON schema and uiSchema; compile draft-07 with the shared browser/server policy; render Markdown through remark → remark-rehype → rehype-sanitize → rehype-stringify; drop raw HTML and active markup; preserve the fixed same-origin image policy. This begins from merged ✨ feat: @executablemd/web foundation + deterministic browser asset pipeline (#195) #237 and has no Register function components and migrate legacy component handlers #202 dependency.
                  2. Loopback form server. Bind only 127.0.0.1; implement token-scoped routes, exact Host/Origin checks, the fixed headers and CSP, the streaming 1 MiB limit, authoritative validation, atomic first-valid reservation, concurrent-submission coverage, 409 after reservation, and cleanup on success, failure, and interruption. This depends only on the preceding WebForm slice.
                  3. Form opener and responder seams. Add the package-owned contextual opener and internal test responder; keep opener failure warning-only; own and observe the opener task; preserve the live URL fallback. This depends only on the preceding WebForm slice.
                  4. Registered <WebForm>, reusable provider operation, durability, and specification. Base this slice on the preceding WebForm work plus Register function components and migrate legacy component handlers #202 PR 1 only. Establish the package-owned live-form operation and the provider adapter boundary consumed later by Add provider-neutral Elicit through the core Context API #197; register the function component as one caller of that operation; call content() before live effects; return the validated response through core's declared-return/as path; and journal only that response with the deterministic schema/UI-schema/sanitized-body fingerprint. Prove content failure performs zero listener, opener, responder, URL-printing, and journal work. Register function components and migrate legacy component handlers #202 PRs 2–6 are not dependencies.
                  5. CLI integration, browser CI, documentation, and publication. Install the web registration in the CLI, run the real Playwright security/form suite, update release configuration and its specification together, publish the completed package, and perform the one manual keyboard/browser smoke check.

                  Each slice stops for review and passes the four repository checks plus the relevant Node, Bun, browser, compiled-CLI, or site checks. PRs from the two issues must not share implementation branches: rebase the WebForm registration slice onto the merged #202 registration foundation when it is ready.

                  Authoring contract

                  <WebForm
                  schema={responseSchema}
                  uiSchema={responseUiSchema}
                  as="response"
                  ># Review required
                  Review the implementation plan and provide your decision.
                  </WebForm>
                  • <WebForm> lives in a separate @executablemd/web package.
                  • schema and as are required.
                  • schema accepts captured JSON text or an already structured draft-07 JSON
                    Schema value.
                  • uiSchema is optional and accepts captured JSON text or a structured RJSF UI
                    schema.
                  • Invocation content expands through content() and its rendered Markdown appears above the form.
                  • A valid submission binds the structured form data to as.
                  • The component emits nothing into the surrounding document.
                  • The initial presentation is the bundled RJSF Shadcn theme. It is the fixed
                    default and is not exposed as a component option.

                  Local browser lifecycle

                  On a live execution, <WebForm>:

                  1. renders its body and compiles the supplied schema;
                  2. starts a server bound only to 127.0.0.1 on an ephemeral port;
                  3. creates an unguessable, execution-local form URL;
                  4. prints the URL and attempts to open it in the default browser;
                  5. waits for exactly one valid form submission;
                  6. shuts down the server and binds the validated response.

                  Failure to launch the browser produces a warning but does not fail the
                  component; the printed URL remains usable. Closing the tab does not cancel the
                  component. It continues waiting until a valid submission or workflow
                  interruption.

                  The submitted page attempts to close itself after success. When the browser
                  does not permit that, it displays a safe-to-close confirmation.

                  The component owns the server, browser-launch operation, and related background
                  tasks through one structured-concurrency scope. Success, failure, and
                  interruption all stop the server and release its port. There is no component
                  timeout; the surrounding workflow controls interruption.

                  Bundled client

                  The browser client bundles React, RJSF, the Ajv 8 validator, the Shadcn theme,
                  and every required script, stylesheet, font, and icon into assets shipped by
                  @executablemd/web.

                  • The page makes no CDN or other external network requests.
                  • The loopback server serves only the assets for the active form URL.
                  • Ports, tokens, URLs, and browser process details are ephemeral runtime state
                    and are not part of the authoring API.
                  • The implementation records the generated client bundle size so the
                    dependency cost is visible before the package is accepted.

                  RJSF provides immediate browser validation. The loopback server validates the
                  submitted data again with equivalent draft-07 Ajv configuration and treats the
                  server result as authoritative. Malformed or schema-invalid requests never
                  enter workflow state and do not consume the one successful submission.

                  Durability

                  Only the validated structured response is journaled.

                  On replay, <WebForm> restores that response immediately without allocating a
                  port, starting a server, printing a URL, or launching a browser. Transport
                  details never appear in the durable result.

                  Testing

                  Add a colocated WebForm.test.md document that uses an internally injected test
                  responder. It supplies predetermined form data and asserts the structured
                  captured value without adding a test-only prop to the public <WebForm> API.

                  Add lower-level automated tests for:

                  • required props and both accepted schema representations;
                  • optional uiSchema parsing and application;
                  • normal invocation-content rendering above the generated form;
                  • structured capture with no surrounding rendered output;
                  • loopback-only binding and unguessable URL authorization;
                  • delivery of every required client asset without external requests;
                  • representative Shadcn widgets, field errors, and submit behavior;
                  • equivalent browser and server validation;
                  • malformed and schema-invalid submissions remaining unresolved;
                  • accepting only the first valid submission;
                  • successful page-close response and the safe-to-close fallback;
                  • browser-launch success and warning-only launch failure;
                  • server and background-task cleanup after success, failure, and interruption;
                  • journal contents excluding transport details;
                  • full replay returning the recorded value without live browser or server work;
                    and
                  • Deno, Node, Bun, and compiled CLI packaging where supported by the workspace.

                  The browser-facing integration test uses a representative review schema with a
                  required enum, boolean, optional text, and textarea. A manual smoke check
                  confirms that the default browser opens and the Shadcn page is usable with a
                  keyboard. No automated test depends on a user's installed browser.

                  Documentation

                  • Specify the component props, lifecycle, capture behavior, validation, and
                    interruption semantics.
                  • Document that the page is loopback-only, single-use, and fully bundled.
                  • Add a website example for a human review checkpoint using schema and
                    uiSchema.
                  • Explain that Shadcn is the fixed initial presentation rather than a public
                    theme-selection API.
                  • Record the generated client bundle size and asset-generation process.

                  Acceptance criteria

                  • <WebForm> captures exactly one server-validated structured response.
                  • Execution remains local and requires no network access after installation.
                  • The default browser opens when possible and the printed URL always provides a
                    fallback.
                  • Closing the tab does not resolve or cancel the component.
                  • All owned resources are released on every exit path.
                  • Replay restores the response without repeating browser or server effects.
                  • RJSF Shadcn presentation is bundled as the fixed default.
                  • Markdown and lower-level tests cover public behavior, security boundaries,
                    cleanup, validation, packaging, and replay.
                  • Specification and website documentation ship in the same PR.

                  Not included

                  • The complete <Elicit> interaction and focus-stack model.
                  • Runtime-selectable themes or author-supplied templates and widgets.
                  • A general-purpose web server component.
                  • Remote assets, CDN fallback, or externally reachable forms.
                  • Treating browser-tab closure as cancellation.

                  Activity

                  Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

                  Metadata

                  Metadata

                  Assignees

                  No one assigned

                    Labels

                    No labels
                    No labels

                    Projects

                    No projects

                      Milestone

                      No milestone

                      Relationships

                      None yet

                      Development

                      No branches or pull requests

                      Issue actions

                      , '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

                      Add a bundled local WebForm component for schema-backed user input #195

                      Description

                      @taras

                      Motivation

                      Executable Markdown workflows need a minimal human-input checkpoint before the
                      full <Elicit> interaction model is designed. The workflow should be able to
                      open a local schema-generated form, wait for one validated response, capture
                      that structured value, and continue in the same execution.

                      This story introduces the behavior as a general web component rather than
                      hiding browser and server semantics inside <Elicit>.

                      Implementation architecture and parallel coordination

                      PR #237 provides the merged package and deterministic bundled-client foundation. The remaining WebForm work proceeds as a small PR stack in parallel with #202.

                      <WebForm> is an ordinary TypeScript function component:

                      • @executablemd/web registers it as a non-reserved default through the scope-local registration API delivered by Register function components and migrate legacy component handlers #202 PR 1;
                      • core resolves expression props, validates the static props schema, removes reserved props, requires and validates as, validates the return, creates the invocation boundary, and applies the binding;
                      • the component never receives a raw ComponentElement, expression map, binding environment, or as value;
                      • the component calls content() and completes schema parsing, schema compilation, UI-schema parsing, and body sanitization before starting a listener, printing a URL, launching a browser, or journaling a live response;
                      • an uncaught ContentError prevents all live WebForm effects through the function-component boundary established by Make content() the failure boundary for function components #232;
                      • the component declares a broad JSON return schema, returns the one server-validated response, and emits no document output;
                      • repository-local WebForm.md or WebForm.ts overrides the package default under Register function components and migrate legacy component handlers #202's resolution contract.
                      • the component delegates live interaction to one package-owned operation that accepts the normalized schema, optional UI schema, and sanitized rendered content and returns the server-validated JSON response; the WebForm adapter for Add provider-neutral Elicit through the core Context API #197 reuses this operation without receiving component props, as, or journal state.

                      Do not implement WebForm through Component.expand, Component.expandSegments, a private expression resolver, or component-owned as handling. The earlier expansion-handler design is obsolete.

                      The remaining stack is:

                      1. Declaration parsing, shared Ajv policy, and sanitization. Parse structured or captured-JSON schema and uiSchema; compile draft-07 with the shared browser/server policy; render Markdown through remark → remark-rehype → rehype-sanitize → rehype-stringify; drop raw HTML and active markup; preserve the fixed same-origin image policy. This begins from merged ✨ feat: @executablemd/web foundation + deterministic browser asset pipeline (#195) #237 and has no Register function components and migrate legacy component handlers #202 dependency.
                      2. Loopback form server. Bind only 127.0.0.1; implement token-scoped routes, exact Host/Origin checks, the fixed headers and CSP, the streaming 1 MiB limit, authoritative validation, atomic first-valid reservation, concurrent-submission coverage, 409 after reservation, and cleanup on success, failure, and interruption. This depends only on the preceding WebForm slice.
                      3. Form opener and responder seams. Add the package-owned contextual opener and internal test responder; keep opener failure warning-only; own and observe the opener task; preserve the live URL fallback. This depends only on the preceding WebForm slice.
                      4. Registered <WebForm>, reusable provider operation, durability, and specification. Base this slice on the preceding WebForm work plus Register function components and migrate legacy component handlers #202 PR 1 only. Establish the package-owned live-form operation and the provider adapter boundary consumed later by Add provider-neutral Elicit through the core Context API #197; register the function component as one caller of that operation; call content() before live effects; return the validated response through core's declared-return/as path; and journal only that response with the deterministic schema/UI-schema/sanitized-body fingerprint. Prove content failure performs zero listener, opener, responder, URL-printing, and journal work. Register function components and migrate legacy component handlers #202 PRs 2–6 are not dependencies.
                      5. CLI integration, browser CI, documentation, and publication. Install the web registration in the CLI, run the real Playwright security/form suite, update release configuration and its specification together, publish the completed package, and perform the one manual keyboard/browser smoke check.

                      Each slice stops for review and passes the four repository checks plus the relevant Node, Bun, browser, compiled-CLI, or site checks. PRs from the two issues must not share implementation branches: rebase the WebForm registration slice onto the merged #202 registration foundation when it is ready.

                      Authoring contract

                      <WebForm
                      schema={responseSchema}
                      uiSchema={responseUiSchema}
                      as="response"
                      ># Review required
                      Review the implementation plan and provide your decision.
                      </WebForm>
                      • <WebForm> lives in a separate @executablemd/web package.
                      • schema and as are required.
                      • schema accepts captured JSON text or an already structured draft-07 JSON
                        Schema value.
                      • uiSchema is optional and accepts captured JSON text or a structured RJSF UI
                        schema.
                      • Invocation content expands through content() and its rendered Markdown appears above the form.
                      • A valid submission binds the structured form data to as.
                      • The component emits nothing into the surrounding document.
                      • The initial presentation is the bundled RJSF Shadcn theme. It is the fixed
                        default and is not exposed as a component option.

                      Local browser lifecycle

                      On a live execution, <WebForm>:

                      1. renders its body and compiles the supplied schema;
                      2. starts a server bound only to 127.0.0.1 on an ephemeral port;
                      3. creates an unguessable, execution-local form URL;
                      4. prints the URL and attempts to open it in the default browser;
                      5. waits for exactly one valid form submission;
                      6. shuts down the server and binds the validated response.

                      Failure to launch the browser produces a warning but does not fail the
                      component; the printed URL remains usable. Closing the tab does not cancel the
                      component. It continues waiting until a valid submission or workflow
                      interruption.

                      The submitted page attempts to close itself after success. When the browser
                      does not permit that, it displays a safe-to-close confirmation.

                      The component owns the server, browser-launch operation, and related background
                      tasks through one structured-concurrency scope. Success, failure, and
                      interruption all stop the server and release its port. There is no component
                      timeout; the surrounding workflow controls interruption.

                      Bundled client

                      The browser client bundles React, RJSF, the Ajv 8 validator, the Shadcn theme,
                      and every required script, stylesheet, font, and icon into assets shipped by
                      @executablemd/web.

                      • The page makes no CDN or other external network requests.
                      • The loopback server serves only the assets for the active form URL.
                      • Ports, tokens, URLs, and browser process details are ephemeral runtime state
                        and are not part of the authoring API.
                      • The implementation records the generated client bundle size so the
                        dependency cost is visible before the package is accepted.

                      RJSF provides immediate browser validation. The loopback server validates the
                      submitted data again with equivalent draft-07 Ajv configuration and treats the
                      server result as authoritative. Malformed or schema-invalid requests never
                      enter workflow state and do not consume the one successful submission.

                      Durability

                      Only the validated structured response is journaled.

                      On replay, <WebForm> restores that response immediately without allocating a
                      port, starting a server, printing a URL, or launching a browser. Transport
                      details never appear in the durable result.

                      Testing

                      Add a colocated WebForm.test.md document that uses an internally injected test
                      responder. It supplies predetermined form data and asserts the structured
                      captured value without adding a test-only prop to the public <WebForm> API.

                      Add lower-level automated tests for:

                      • required props and both accepted schema representations;
                      • optional uiSchema parsing and application;
                      • normal invocation-content rendering above the generated form;
                      • structured capture with no surrounding rendered output;
                      • loopback-only binding and unguessable URL authorization;
                      • delivery of every required client asset without external requests;
                      • representative Shadcn widgets, field errors, and submit behavior;
                      • equivalent browser and server validation;
                      • malformed and schema-invalid submissions remaining unresolved;
                      • accepting only the first valid submission;
                      • successful page-close response and the safe-to-close fallback;
                      • browser-launch success and warning-only launch failure;
                      • server and background-task cleanup after success, failure, and interruption;
                      • journal contents excluding transport details;
                      • full replay returning the recorded value without live browser or server work;
                        and
                      • Deno, Node, Bun, and compiled CLI packaging where supported by the workspace.

                      The browser-facing integration test uses a representative review schema with a
                      required enum, boolean, optional text, and textarea. A manual smoke check
                      confirms that the default browser opens and the Shadcn page is usable with a
                      keyboard. No automated test depends on a user's installed browser.

                      Documentation

                      • Specify the component props, lifecycle, capture behavior, validation, and
                        interruption semantics.
                      • Document that the page is loopback-only, single-use, and fully bundled.
                      • Add a website example for a human review checkpoint using schema and
                        uiSchema.
                      • Explain that Shadcn is the fixed initial presentation rather than a public
                        theme-selection API.
                      • Record the generated client bundle size and asset-generation process.

                      Acceptance criteria

                      • <WebForm> captures exactly one server-validated structured response.
                      • Execution remains local and requires no network access after installation.
                      • The default browser opens when possible and the printed URL always provides a
                        fallback.
                      • Closing the tab does not resolve or cancel the component.
                      • All owned resources are released on every exit path.
                      • Replay restores the response without repeating browser or server effects.
                      • RJSF Shadcn presentation is bundled as the fixed default.
                      • Markdown and lower-level tests cover public behavior, security boundaries,
                        cleanup, validation, packaging, and replay.
                      • Specification and website documentation ship in the same PR.

                      Not included

                      • The complete <Elicit> interaction and focus-stack model.
                      • Runtime-selectable themes or author-supplied templates and widgets.
                      • A general-purpose web server component.
                      • Remote assets, CDN fallback, or externally reachable forms.
                      • Treating browser-tab closure as cancellation.

                      Activity

                      Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

                      Metadata

                      Metadata

                      Assignees

                      No one assigned

                        Labels

                        No labels
                        No labels

                        Projects

                        No projects

                          Milestone

                          No milestone

                          Relationships

                          None yet

                          Development

                          No branches or pull requests

                          Issue actions

                          , '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

                          Add a bundled local WebForm component for schema-backed user input #195

                          Description

                          @taras

                          Motivation

                          Executable Markdown workflows need a minimal human-input checkpoint before the
                          full <Elicit> interaction model is designed. The workflow should be able to
                          open a local schema-generated form, wait for one validated response, capture
                          that structured value, and continue in the same execution.

                          This story introduces the behavior as a general web component rather than
                          hiding browser and server semantics inside <Elicit>.

                          Implementation architecture and parallel coordination

                          PR #237 provides the merged package and deterministic bundled-client foundation. The remaining WebForm work proceeds as a small PR stack in parallel with #202.

                          <WebForm> is an ordinary TypeScript function component:

                          • @executablemd/web registers it as a non-reserved default through the scope-local registration API delivered by Register function components and migrate legacy component handlers #202 PR 1;
                          • core resolves expression props, validates the static props schema, removes reserved props, requires and validates as, validates the return, creates the invocation boundary, and applies the binding;
                          • the component never receives a raw ComponentElement, expression map, binding environment, or as value;
                          • the component calls content() and completes schema parsing, schema compilation, UI-schema parsing, and body sanitization before starting a listener, printing a URL, launching a browser, or journaling a live response;
                          • an uncaught ContentError prevents all live WebForm effects through the function-component boundary established by Make content() the failure boundary for function components #232;
                          • the component declares a broad JSON return schema, returns the one server-validated response, and emits no document output;
                          • repository-local WebForm.md or WebForm.ts overrides the package default under Register function components and migrate legacy component handlers #202's resolution contract.
                          • the component delegates live interaction to one package-owned operation that accepts the normalized schema, optional UI schema, and sanitized rendered content and returns the server-validated JSON response; the WebForm adapter for Add provider-neutral Elicit through the core Context API #197 reuses this operation without receiving component props, as, or journal state.

                          Do not implement WebForm through Component.expand, Component.expandSegments, a private expression resolver, or component-owned as handling. The earlier expansion-handler design is obsolete.

                          The remaining stack is:

                          1. Declaration parsing, shared Ajv policy, and sanitization. Parse structured or captured-JSON schema and uiSchema; compile draft-07 with the shared browser/server policy; render Markdown through remark → remark-rehype → rehype-sanitize → rehype-stringify; drop raw HTML and active markup; preserve the fixed same-origin image policy. This begins from merged ✨ feat: @executablemd/web foundation + deterministic browser asset pipeline (#195) #237 and has no Register function components and migrate legacy component handlers #202 dependency.
                          2. Loopback form server. Bind only 127.0.0.1; implement token-scoped routes, exact Host/Origin checks, the fixed headers and CSP, the streaming 1 MiB limit, authoritative validation, atomic first-valid reservation, concurrent-submission coverage, 409 after reservation, and cleanup on success, failure, and interruption. This depends only on the preceding WebForm slice.
                          3. Form opener and responder seams. Add the package-owned contextual opener and internal test responder; keep opener failure warning-only; own and observe the opener task; preserve the live URL fallback. This depends only on the preceding WebForm slice.
                          4. Registered <WebForm>, reusable provider operation, durability, and specification. Base this slice on the preceding WebForm work plus Register function components and migrate legacy component handlers #202 PR 1 only. Establish the package-owned live-form operation and the provider adapter boundary consumed later by Add provider-neutral Elicit through the core Context API #197; register the function component as one caller of that operation; call content() before live effects; return the validated response through core's declared-return/as path; and journal only that response with the deterministic schema/UI-schema/sanitized-body fingerprint. Prove content failure performs zero listener, opener, responder, URL-printing, and journal work. Register function components and migrate legacy component handlers #202 PRs 2–6 are not dependencies.
                          5. CLI integration, browser CI, documentation, and publication. Install the web registration in the CLI, run the real Playwright security/form suite, update release configuration and its specification together, publish the completed package, and perform the one manual keyboard/browser smoke check.

                          Each slice stops for review and passes the four repository checks plus the relevant Node, Bun, browser, compiled-CLI, or site checks. PRs from the two issues must not share implementation branches: rebase the WebForm registration slice onto the merged #202 registration foundation when it is ready.

                          Authoring contract

                          <WebForm
                          schema={responseSchema}
                          uiSchema={responseUiSchema}
                          as="response"
                          ># Review required
                          Review the implementation plan and provide your decision.
                          </WebForm>
                          • <WebForm> lives in a separate @executablemd/web package.
                          • schema and as are required.
                          • schema accepts captured JSON text or an already structured draft-07 JSON
                            Schema value.
                          • uiSchema is optional and accepts captured JSON text or a structured RJSF UI
                            schema.
                          • Invocation content expands through content() and its rendered Markdown appears above the form.
                          • A valid submission binds the structured form data to as.
                          • The component emits nothing into the surrounding document.
                          • The initial presentation is the bundled RJSF Shadcn theme. It is the fixed
                            default and is not exposed as a component option.

                          Local browser lifecycle

                          On a live execution, <WebForm>:

                          1. renders its body and compiles the supplied schema;
                          2. starts a server bound only to 127.0.0.1 on an ephemeral port;
                          3. creates an unguessable, execution-local form URL;
                          4. prints the URL and attempts to open it in the default browser;
                          5. waits for exactly one valid form submission;
                          6. shuts down the server and binds the validated response.

                          Failure to launch the browser produces a warning but does not fail the
                          component; the printed URL remains usable. Closing the tab does not cancel the
                          component. It continues waiting until a valid submission or workflow
                          interruption.

                          The submitted page attempts to close itself after success. When the browser
                          does not permit that, it displays a safe-to-close confirmation.

                          The component owns the server, browser-launch operation, and related background
                          tasks through one structured-concurrency scope. Success, failure, and
                          interruption all stop the server and release its port. There is no component
                          timeout; the surrounding workflow controls interruption.

                          Bundled client

                          The browser client bundles React, RJSF, the Ajv 8 validator, the Shadcn theme,
                          and every required script, stylesheet, font, and icon into assets shipped by
                          @executablemd/web.

                          • The page makes no CDN or other external network requests.
                          • The loopback server serves only the assets for the active form URL.
                          • Ports, tokens, URLs, and browser process details are ephemeral runtime state
                            and are not part of the authoring API.
                          • The implementation records the generated client bundle size so the
                            dependency cost is visible before the package is accepted.

                          RJSF provides immediate browser validation. The loopback server validates the
                          submitted data again with equivalent draft-07 Ajv configuration and treats the
                          server result as authoritative. Malformed or schema-invalid requests never
                          enter workflow state and do not consume the one successful submission.

                          Durability

                          Only the validated structured response is journaled.

                          On replay, <WebForm> restores that response immediately without allocating a
                          port, starting a server, printing a URL, or launching a browser. Transport
                          details never appear in the durable result.

                          Testing

                          Add a colocated WebForm.test.md document that uses an internally injected test
                          responder. It supplies predetermined form data and asserts the structured
                          captured value without adding a test-only prop to the public <WebForm> API.

                          Add lower-level automated tests for:

                          • required props and both accepted schema representations;
                          • optional uiSchema parsing and application;
                          • normal invocation-content rendering above the generated form;
                          • structured capture with no surrounding rendered output;
                          • loopback-only binding and unguessable URL authorization;
                          • delivery of every required client asset without external requests;
                          • representative Shadcn widgets, field errors, and submit behavior;
                          • equivalent browser and server validation;
                          • malformed and schema-invalid submissions remaining unresolved;
                          • accepting only the first valid submission;
                          • successful page-close response and the safe-to-close fallback;
                          • browser-launch success and warning-only launch failure;
                          • server and background-task cleanup after success, failure, and interruption;
                          • journal contents excluding transport details;
                          • full replay returning the recorded value without live browser or server work;
                            and
                          • Deno, Node, Bun, and compiled CLI packaging where supported by the workspace.

                          The browser-facing integration test uses a representative review schema with a
                          required enum, boolean, optional text, and textarea. A manual smoke check
                          confirms that the default browser opens and the Shadcn page is usable with a
                          keyboard. No automated test depends on a user's installed browser.

                          Documentation

                          • Specify the component props, lifecycle, capture behavior, validation, and
                            interruption semantics.
                          • Document that the page is loopback-only, single-use, and fully bundled.
                          • Add a website example for a human review checkpoint using schema and
                            uiSchema.
                          • Explain that Shadcn is the fixed initial presentation rather than a public
                            theme-selection API.
                          • Record the generated client bundle size and asset-generation process.

                          Acceptance criteria

                          • <WebForm> captures exactly one server-validated structured response.
                          • Execution remains local and requires no network access after installation.
                          • The default browser opens when possible and the printed URL always provides a
                            fallback.
                          • Closing the tab does not resolve or cancel the component.
                          • All owned resources are released on every exit path.
                          • Replay restores the response without repeating browser or server effects.
                          • RJSF Shadcn presentation is bundled as the fixed default.
                          • Markdown and lower-level tests cover public behavior, security boundaries,
                            cleanup, validation, packaging, and replay.
                          • Specification and website documentation ship in the same PR.

                          Not included

                          • The complete <Elicit> interaction and focus-stack model.
                          • Runtime-selectable themes or author-supplied templates and widgets.
                          • A general-purpose web server component.
                          • Remote assets, CDN fallback, or externally reachable forms.
                          • Treating browser-tab closure as cancellation.

                          Activity

                          Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

                          Metadata

                          Metadata

                          Assignees

                          No one assigned

                            Labels

                            No labels
                            No labels

                            Projects

                            No projects

                              Milestone

                              No milestone

                              Relationships

                              None yet

                              Development

                              No branches or pull requests

                              Issue actions

                              , '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

                              Add a bundled local WebForm component for schema-backed user input #195

                              Description

                              @taras

                              Motivation

                              Executable Markdown workflows need a minimal human-input checkpoint before the
                              full <Elicit> interaction model is designed. The workflow should be able to
                              open a local schema-generated form, wait for one validated response, capture
                              that structured value, and continue in the same execution.

                              This story introduces the behavior as a general web component rather than
                              hiding browser and server semantics inside <Elicit>.

                              Implementation architecture and parallel coordination

                              PR #237 provides the merged package and deterministic bundled-client foundation. The remaining WebForm work proceeds as a small PR stack in parallel with #202.

                              <WebForm> is an ordinary TypeScript function component:

                              • @executablemd/web registers it as a non-reserved default through the scope-local registration API delivered by Register function components and migrate legacy component handlers #202 PR 1;
                              • core resolves expression props, validates the static props schema, removes reserved props, requires and validates as, validates the return, creates the invocation boundary, and applies the binding;
                              • the component never receives a raw ComponentElement, expression map, binding environment, or as value;
                              • the component calls content() and completes schema parsing, schema compilation, UI-schema parsing, and body sanitization before starting a listener, printing a URL, launching a browser, or journaling a live response;
                              • an uncaught ContentError prevents all live WebForm effects through the function-component boundary established by Make content() the failure boundary for function components #232;
                              • the component declares a broad JSON return schema, returns the one server-validated response, and emits no document output;
                              • repository-local WebForm.md or WebForm.ts overrides the package default under Register function components and migrate legacy component handlers #202's resolution contract.
                              • the component delegates live interaction to one package-owned operation that accepts the normalized schema, optional UI schema, and sanitized rendered content and returns the server-validated JSON response; the WebForm adapter for Add provider-neutral Elicit through the core Context API #197 reuses this operation without receiving component props, as, or journal state.

                              Do not implement WebForm through Component.expand, Component.expandSegments, a private expression resolver, or component-owned as handling. The earlier expansion-handler design is obsolete.

                              The remaining stack is:

                              1. Declaration parsing, shared Ajv policy, and sanitization. Parse structured or captured-JSON schema and uiSchema; compile draft-07 with the shared browser/server policy; render Markdown through remark → remark-rehype → rehype-sanitize → rehype-stringify; drop raw HTML and active markup; preserve the fixed same-origin image policy. This begins from merged ✨ feat: @executablemd/web foundation + deterministic browser asset pipeline (#195) #237 and has no Register function components and migrate legacy component handlers #202 dependency.
                              2. Loopback form server. Bind only 127.0.0.1; implement token-scoped routes, exact Host/Origin checks, the fixed headers and CSP, the streaming 1 MiB limit, authoritative validation, atomic first-valid reservation, concurrent-submission coverage, 409 after reservation, and cleanup on success, failure, and interruption. This depends only on the preceding WebForm slice.
                              3. Form opener and responder seams. Add the package-owned contextual opener and internal test responder; keep opener failure warning-only; own and observe the opener task; preserve the live URL fallback. This depends only on the preceding WebForm slice.
                              4. Registered <WebForm>, reusable provider operation, durability, and specification. Base this slice on the preceding WebForm work plus Register function components and migrate legacy component handlers #202 PR 1 only. Establish the package-owned live-form operation and the provider adapter boundary consumed later by Add provider-neutral Elicit through the core Context API #197; register the function component as one caller of that operation; call content() before live effects; return the validated response through core's declared-return/as path; and journal only that response with the deterministic schema/UI-schema/sanitized-body fingerprint. Prove content failure performs zero listener, opener, responder, URL-printing, and journal work. Register function components and migrate legacy component handlers #202 PRs 2–6 are not dependencies.
                              5. CLI integration, browser CI, documentation, and publication. Install the web registration in the CLI, run the real Playwright security/form suite, update release configuration and its specification together, publish the completed package, and perform the one manual keyboard/browser smoke check.

                              Each slice stops for review and passes the four repository checks plus the relevant Node, Bun, browser, compiled-CLI, or site checks. PRs from the two issues must not share implementation branches: rebase the WebForm registration slice onto the merged #202 registration foundation when it is ready.

                              Authoring contract

                              <WebForm
                              schema={responseSchema}
                              uiSchema={responseUiSchema}
                              as="response"
                              ># Review required
                              Review the implementation plan and provide your decision.
                              </WebForm>
                              • <WebForm> lives in a separate @executablemd/web package.
                              • schema and as are required.
                              • schema accepts captured JSON text or an already structured draft-07 JSON
                                Schema value.
                              • uiSchema is optional and accepts captured JSON text or a structured RJSF UI
                                schema.
                              • Invocation content expands through content() and its rendered Markdown appears above the form.
                              • A valid submission binds the structured form data to as.
                              • The component emits nothing into the surrounding document.
                              • The initial presentation is the bundled RJSF Shadcn theme. It is the fixed
                                default and is not exposed as a component option.

                              Local browser lifecycle

                              On a live execution, <WebForm>:

                              1. renders its body and compiles the supplied schema;
                              2. starts a server bound only to 127.0.0.1 on an ephemeral port;
                              3. creates an unguessable, execution-local form URL;
                              4. prints the URL and attempts to open it in the default browser;
                              5. waits for exactly one valid form submission;
                              6. shuts down the server and binds the validated response.

                              Failure to launch the browser produces a warning but does not fail the
                              component; the printed URL remains usable. Closing the tab does not cancel the
                              component. It continues waiting until a valid submission or workflow
                              interruption.

                              The submitted page attempts to close itself after success. When the browser
                              does not permit that, it displays a safe-to-close confirmation.

                              The component owns the server, browser-launch operation, and related background
                              tasks through one structured-concurrency scope. Success, failure, and
                              interruption all stop the server and release its port. There is no component
                              timeout; the surrounding workflow controls interruption.

                              Bundled client

                              The browser client bundles React, RJSF, the Ajv 8 validator, the Shadcn theme,
                              and every required script, stylesheet, font, and icon into assets shipped by
                              @executablemd/web.

                              • The page makes no CDN or other external network requests.
                              • The loopback server serves only the assets for the active form URL.
                              • Ports, tokens, URLs, and browser process details are ephemeral runtime state
                                and are not part of the authoring API.
                              • The implementation records the generated client bundle size so the
                                dependency cost is visible before the package is accepted.

                              RJSF provides immediate browser validation. The loopback server validates the
                              submitted data again with equivalent draft-07 Ajv configuration and treats the
                              server result as authoritative. Malformed or schema-invalid requests never
                              enter workflow state and do not consume the one successful submission.

                              Durability

                              Only the validated structured response is journaled.

                              On replay, <WebForm> restores that response immediately without allocating a
                              port, starting a server, printing a URL, or launching a browser. Transport
                              details never appear in the durable result.

                              Testing

                              Add a colocated WebForm.test.md document that uses an internally injected test
                              responder. It supplies predetermined form data and asserts the structured
                              captured value without adding a test-only prop to the public <WebForm> API.

                              Add lower-level automated tests for:

                              • required props and both accepted schema representations;
                              • optional uiSchema parsing and application;
                              • normal invocation-content rendering above the generated form;
                              • structured capture with no surrounding rendered output;
                              • loopback-only binding and unguessable URL authorization;
                              • delivery of every required client asset without external requests;
                              • representative Shadcn widgets, field errors, and submit behavior;
                              • equivalent browser and server validation;
                              • malformed and schema-invalid submissions remaining unresolved;
                              • accepting only the first valid submission;
                              • successful page-close response and the safe-to-close fallback;
                              • browser-launch success and warning-only launch failure;
                              • server and background-task cleanup after success, failure, and interruption;
                              • journal contents excluding transport details;
                              • full replay returning the recorded value without live browser or server work;
                                and
                              • Deno, Node, Bun, and compiled CLI packaging where supported by the workspace.

                              The browser-facing integration test uses a representative review schema with a
                              required enum, boolean, optional text, and textarea. A manual smoke check
                              confirms that the default browser opens and the Shadcn page is usable with a
                              keyboard. No automated test depends on a user's installed browser.

                              Documentation

                              • Specify the component props, lifecycle, capture behavior, validation, and
                                interruption semantics.
                              • Document that the page is loopback-only, single-use, and fully bundled.
                              • Add a website example for a human review checkpoint using schema and
                                uiSchema.
                              • Explain that Shadcn is the fixed initial presentation rather than a public
                                theme-selection API.
                              • Record the generated client bundle size and asset-generation process.

                              Acceptance criteria

                              • <WebForm> captures exactly one server-validated structured response.
                              • Execution remains local and requires no network access after installation.
                              • The default browser opens when possible and the printed URL always provides a
                                fallback.
                              • Closing the tab does not resolve or cancel the component.
                              • All owned resources are released on every exit path.
                              • Replay restores the response without repeating browser or server effects.
                              • RJSF Shadcn presentation is bundled as the fixed default.
                              • Markdown and lower-level tests cover public behavior, security boundaries,
                                cleanup, validation, packaging, and replay.
                              • Specification and website documentation ship in the same PR.

                              Not included

                              • The complete <Elicit> interaction and focus-stack model.
                              • Runtime-selectable themes or author-supplied templates and widgets.
                              • A general-purpose web server component.
                              • Remote assets, CDN fallback, or externally reachable forms.
                              • Treating browser-tab closure as cancellation.

                              Activity

                              Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

                              Metadata

                              Metadata

                              Assignees

                              No one assigned

                                Labels

                                No labels
                                No labels

                                Projects

                                No projects

                                  Milestone

                                  No milestone

                                  Relationships

                                  None yet

                                  Development

                                  No branches or pull requests

                                  Issue actions