Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions fixtures/react-parity/runtime/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,73 @@
# Installed native runtime consumers

## Manual review

From the repository root, install dependencies and build the artifacts used by
the review runner:

```sh
npm ci
NX_DAEMON=false npx nx run-many -t build -p core,angular,react --skip-nx-cache
npx playwright install chromium
node scripts/react-parity/review-runtime.mjs
```

On Linux, install browser system dependencies with
`npx playwright install --with-deps chromium`. The runner requires the existing
`dist/libs/{core,angular,react}` artifacts and reports the build command if they
are missing. `node scripts/react-parity/review-runtime.mjs --help` prints the
prerequisite and review sequence.

The runner packs those artifacts, installs and strictly type-checks isolated
React and Angular consumers, builds each app, and runs all eleven browser
scenarios on fresh fixture servers. Only after those checks pass does it print
two new, untouched loopback URLs. Open each URL manually; no browser opens
automatically. The review servers have made no SDK requests at that point.
Separate preparation processes keep each framework's install/build environment
isolated. The shared stylesheet and labelled panels are fixture review aids.

The printed source HEAD and dirty source paths identify the checkout used for
the private runtime bundle and fixture UI. They do **not** establish which commit
built the preexisting package artifacts. Printed SHA-256 identifiers separately
identify the actual packed bytes installed in each consumer. Rebuild the
prerequisites when changing core or either native binding.

Use this order once per fresh server, waiting for each expected state:

| Action | Expected visible state |
| --- | --- |
| Open | Idle; zero loads, submissions and handler calls; values `unobserved`; interrupts `[]`. Mount performs no SDK I/O. |
| Load | Loads finished `1`, empty Load error, saved transcript and profile, two saved interrupt payloads, delivery `complete:paused`. No tool handler executes. |
| Load again | Loads finished `2`, empty Load error; the same saved content, values, interrupts and paused delivery. |
| Load a third time | Loads finished `3`, empty Load error; empty transcript, values `unobserved`, interrupts `[]`. |
| Send | `Hello 🌍.`, idle, delivery `complete:success`; values show the completed stage. |
| Tool | `20 degrees`, a completed weather result for Paris, handler calls `1`; delivery `complete:success`. |
| Error | Status `error` and a protected error message without the private backend diagnostic. Prior delivery remains `complete:success`; this fixture error does not replace it. |
| Hold | `Held partial` appears while delivery is `streaming`. |
| Stop | Delivery becomes `complete:aborted`; the native held response closes and partial text remains visible. |
| Pause | `Waiting for approvals`, both live interrupt payloads, idle, delivery `complete:paused`. |
| Stop again | Both interrupts and `complete:paused` remain; no extra request is made. |
| Send again | Another successful greeting, interrupts `[]`, delivery `complete:success`; submissions `6`, handler calls `1`. |
| Unmount | Component panels disappear; the separate owner controls report `unmounted`. |
| Dispose | Owner reports `disposed`. |
| Send after dispose | Owner reports `aborted`; no request is made. |

This sequence makes three history reads with `{ limit: 10 }` and seven run
requests, including the tool continuation. Each server permits only three Load
requests. Restart the CLI for a fresh sequence; reloading the page does not reset
server history. Unmount releases the framework observer, while the application
owns the session and explicitly disposes it.

Keep the CLI running during review. Ctrl+C or SIGTERM closes its servers and
connections, stops and awaits preparation process groups, and removes this
invocation's temporary consumers. Startup failures use the same cleanup path;
if a preparation process group cannot be stopped, its temporary files are
retained and an error is reported. Process-group cleanup targets POSIX macOS and
Linux. The local HTTP/SSE fixture is not a production backend, interrupt resume
API, SSR demonstration, or complete React migration.

## Automated installed-consumer verification

Run the owned commands from the repository root. On a fresh checkout, install
dependencies, build the private artifacts, and install Playwright Chromium:

Expand Down
172 changes: 145 additions & 27 deletions fixtures/react-parity/runtime/angular-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,50 +2,166 @@ import { Component, signal } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { observeAgent } from '@threadplane/angular';
import { createFixtureSession } from './runtime-entry.js';
import { attachOwner, display } from './scenarios';
import { attachOwner, display, reviewInstructions } from './scenarios';

let handlerCalls = 0;
let submissions = 0;
const session = createFixtureSession('/api', 'fixture-thread', () => { handlerCalls += 1; });
const submit = (input: string) => { submissions += 1; return session.submit(input); };
const session = createFixtureSession('/api', 'fixture-thread', () => {
handlerCalls += 1;
});
const submit = (input: string) => {
submissions += 1;
return session.submit(input);
};

@Component({
selector: 'app-root',
standalone: true,
template: `
<main>
<button [disabled]="!canLoad" (click)="load()">Load</button>
<button (click)="submit('Send')">Send</button>
<button (click)="submit('Tool')">Tool</button>
<button (click)="submit('Error')">Error</button>
<button (click)="submit('Hold')">Hold</button>
<button (click)="submit('Pause')">Pause</button>
<button (click)="stop()">Stop</button>
<output aria-label="Status" data-testid="status">{{ snapshot().status }}</output>
<output aria-label="Text" data-testid="text">{{ view().text }}</output>
<output aria-label="Transcript" data-testid="transcript">{{ view().transcript }}</output>
<output aria-label="Application values" data-testid="values">{{ view().values }}</output>
<output aria-label="Interrupts" data-testid="interrupts">{{ view().interrupts }}</output>
<output aria-label="Loads finished" data-testid="loads-finished">{{ loadsFinished() }}</output>
<output aria-label="Load error" data-testid="load-error">{{ loadError() }}</output>
<output aria-label="Error" data-testid="error">{{ view().error }}</output>
<output aria-label="Tool result" data-testid="tool">{{ view().tool }}</output>
<output aria-label="Delivery" data-testid="delivery">{{ view().delivery }}</output>
<output aria-label="Handler calls" data-testid="handler-calls">{{ handlerCalls() }}</output>
<output aria-label="Submissions" data-testid="submissions">{{ submissions() }}</output>
<main class="review-shell">
<header>
<p class="eyebrow">Installed package review · Angular</p>
<h1>Session runtime</h1>
<p>
Observe the application-owned session through the native Angular
binding.
</p>
</header>
<section class="panel instructions" aria-label="Review instructions">
<h2>Review sequence</h2>
<p>{{ reviewInstructions }}</p>
</section>
<section class="panel" aria-label="Session controls">
<h2>Session controls</h2>
<div class="controls">
<button [disabled]="!canLoad" (click)="load()">Load</button
><button (click)="submit('Send')">Send</button
><button (click)="submit('Tool')">Tool</button
><button (click)="submit('Error')">Error</button
><button (click)="submit('Hold')">Hold</button
><button (click)="submit('Pause')">Pause</button
><button (click)="stop()">Stop</button>
</div>
</section>
<div class="review-grid">
<section class="panel" aria-label="Request state panel">
<h2>Request state</h2>
<div class="state-grid">
<div class="field">
<h3>Status</h3>
<output aria-label="Status" data-testid="status">{{
snapshot().status
}}</output>
</div>
<div class="field">
<h3>Delivery</h3>
<output aria-label="Delivery" data-testid="delivery">{{
view().delivery
}}</output>
</div>
<div class="field">
<h3>Loads finished</h3>
<output
aria-label="Loads finished"
data-testid="loads-finished"
>{{ loadsFinished() }}</output
>
</div>
<div class="field">
<h3>Submissions</h3>
<output aria-label="Submissions" data-testid="submissions">{{
submissions()
}}</output>
</div>
<div class="field">
<h3>Handler calls</h3>
<output aria-label="Handler calls" data-testid="handler-calls">{{
handlerCalls()
}}</output>
</div>
<div class="field">
<h3>Load error</h3>
<output aria-label="Load error" data-testid="load-error">{{
loadError()
}}</output>
</div>
<div class="field">
<h3>Error</h3>
<output aria-label="Error" data-testid="error">{{
view().error
}}</output>
</div>
</div>
</section>
<section class="panel" aria-label="Conversation panel">
<h2>Conversation</h2>
<div class="fields">
<div class="field">
<h3>Text</h3>
<output aria-label="Text" data-testid="text">{{
view().text
}}</output>
</div>
<div class="field">
<h3>Transcript</h3>
<output aria-label="Transcript" data-testid="transcript">{{
view().transcript
}}</output>
</div>
</div>
</section>
<section class="panel" aria-label="Application values panel">
<h2>Application values</h2>
<div class="fields">
<div class="field">
<h3>Application values</h3>
<output aria-label="Application values" data-testid="values">{{
view().values
}}</output>
</div>
</div>
</section>
<section class="panel" aria-label="Interrupts panel">
<h2>Interrupts</h2>
<div class="fields">
<div class="field">
<h3>Interrupts</h3>
<output aria-label="Interrupts" data-testid="interrupts">{{
view().interrupts
}}</output>
</div>
</div>
</section>
<section class="panel" aria-label="Tools panel">
<h2>Tools</h2>
<div class="fields">
<div class="field">
<h3>Tool result</h3>
<output aria-label="Tool result" data-testid="tool">{{
view().tool
}}</output>
</div>
</div>
</section>
</div>
</main>
`,
})
class App {
readonly reviewInstructions = reviewInstructions;
readonly canLoad = !!session.load;
readonly loadsFinished = signal(0);
readonly loadError = signal('');
async load() {
if (!session.load) return;
this.loadError.set('');
try { await session.load(); }
catch { this.loadError.set('History unavailable'); }
finally { this.loadsFinished.update((count) => count + 1); }
try {
await session.load();
} catch {
this.loadError.set('History unavailable');
} finally {
this.loadsFinished.update((count) => count + 1);
}
}
readonly snapshot = observeAgent(session);
readonly view = () => display(this.snapshot());
Expand All @@ -55,4 +171,6 @@ class App {
readonly stop = () => session.stop();
}

void bootstrapApplication(App).then((application) => attachOwner(session, () => application.destroy()));
void bootstrapApplication(App).then((application) =>
attachOwner(session, () => application.destroy())
);
Loading
Loading