diff --git a/console/web/src/demo/LandingDemo.tsx b/console/web/src/demo/LandingDemo.tsx
index 9bb72d16f..2a9397d69 100644
--- a/console/web/src/demo/LandingDemo.tsx
+++ b/console/web/src/demo/LandingDemo.tsx
@@ -408,16 +408,18 @@ function DemoChrome({
replay
{/* Embedded only: the host page listens for iii-demo-close and scrolls
- the console away. Inverted so the way out is unmissable. */}
+ the console away. Red, the loudest thing in the frame, because a
+ reader who wants out should not have to look for it. The exact red
+ and its text color are in demo.css. */}
{window.self !== window.top && (
)}
diff --git a/console/web/src/demo/demo.css b/console/web/src/demo/demo.css
index 545c93c66..94b7f2c3d 100644
--- a/console/web/src/demo/demo.css
+++ b/console/web/src/demo/demo.css
@@ -62,3 +62,12 @@
transform: none;
}
}
+
+/* The way out of the embedded console. Taken down from the raw alert red, so
+ it reads as an exit rather than as something having gone wrong, and dark
+ enough that white holds AA at the 10px the chrome bar uses. Derived from the
+ token, identical in both themes: the button means the same thing in each. */
+.demo-close {
+ background: color-mix(in oklab, var(--color-alert) 72%, #1a0206);
+ color: #fff;
+}
diff --git a/console/web/src/demo/scenario.ts b/console/web/src/demo/scenario.ts
index 1d7b08ad9..da0d16497 100644
--- a/console/web/src/demo/scenario.ts
+++ b/console/web/src/demo/scenario.ts
@@ -132,6 +132,11 @@ function readingDwellMs(body: string): number {
return Math.min(2800, Math.max(600, tokenize(body).length * 10))
}
+/** Mean per-token delay that streams `body` in about `totalMs`. */
+function paceOver(body: string, totalMs: number): number {
+ return totalMs / tokenize(body).length
+}
+
async function* thought(
body: string,
signal?: AbortSignal,
@@ -1139,6 +1144,10 @@ would have resumed from the last completed step, into the same trace.`
/* ── the script ───────────────────────────────────────────────────────── */
+/** Streamed on a 1.1s budget: the reader is waiting on the first tool call. */
+const OPENING_THOUGHT =
+ 'The user wants a payments ledger with durable storage. Before I write a line of it I should look at what is already connected to this engine. iii keeps a live catalog of every running worker, so a durable database may already be here. If it is, there is nothing to scaffold and nothing to deploy alongside.'
+
export async function* runScenario(
opts: ScenarioOptions,
): AsyncGenerator {
@@ -1153,10 +1162,7 @@ export async function* runScenario(
/* step 1 — look at what is already running */
yield* step(
1,
- thought(
- 'The user wants a payments ledger with durable storage. Before I write a line of it I should look at what is already connected to this engine. iii keeps a live catalog of every running worker, so a durable database may already be here. If it is, there is nothing to scaffold and nothing to deploy alongside.',
- signal,
- ),
+ thought(OPENING_THOUGHT, signal, paceOver(OPENING_THOUGHT, 1100)),
signal,
(stepSpan) =>
call({
diff --git a/console/web/src/demo/usePlayer.ts b/console/web/src/demo/usePlayer.ts
index 3ca43b367..1b46ef301 100644
--- a/console/web/src/demo/usePlayer.ts
+++ b/console/web/src/demo/usePlayer.ts
@@ -41,7 +41,8 @@ export type Phase = 'idle' | 'typing' | 'streaming' | 'done'
/** How long the finished turn stays up before the loop restarts. */
const HOLD_MS = 14000
-const TYPE_MS_PER_CHAR = 42
+/** Fast: the prompt is the preamble, the run is what the reader came for. */
+const TYPE_MS_PER_CHAR = 18
/** Repaint cadence while a span is still open, so its bar grows. */
const PENDING_TICK_MS = 120
/** The gate releases itself if nobody clicks approve. */