diff --git a/.prettierignore b/.prettierignore
index 101e4990..378817ef 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -2,6 +2,8 @@ CHANGELOG.md
pnpm-lock.yaml
**/node_modules
+**/.svelte-kit
+**/.vercel
**/dist
**/build
**/test-results
diff --git a/.prettierrc b/.prettierrc
index ed855ae2..d8ff547d 100644
--- a/.prettierrc
+++ b/.prettierrc
@@ -3,7 +3,14 @@
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
+ "plugins": ["prettier-plugin-svelte"],
"overrides": [
+ {
+ "files": ["*.svelte"],
+ "options": {
+ "bracketSameLine": false
+ }
+ },
{
"files": ["README.md", "**/README.md", "**/package.json"],
"options": {
diff --git a/package.json b/package.json
index 297b3a6a..02e06035 100644
--- a/package.json
+++ b/package.json
@@ -24,6 +24,7 @@
"@js-temporal/polyfill": "^0.5.1",
"@types/node": "^24.12.0",
"prettier": "^3.2.4",
+ "prettier-plugin-svelte": "^3.4.0",
"publint": "^0.3.12",
"typescript": "^7.0.2",
"vite": "8.3.0",
diff --git a/playground/uneval-stream/.gitignore b/playground/uneval-stream/.gitignore
new file mode 100644
index 00000000..3b462cb0
--- /dev/null
+++ b/playground/uneval-stream/.gitignore
@@ -0,0 +1,23 @@
+node_modules
+
+# Output
+.output
+.vercel
+.netlify
+.wrangler
+/.svelte-kit
+/build
+
+# OS
+.DS_Store
+Thumbs.db
+
+# Env
+.env
+.env.*
+!.env.example
+!.env.test
+
+# Vite
+vite.config.js.timestamp-*
+vite.config.ts.timestamp-*
diff --git a/playground/uneval-stream/.npmrc b/playground/uneval-stream/.npmrc
new file mode 100644
index 00000000..b6f27f13
--- /dev/null
+++ b/playground/uneval-stream/.npmrc
@@ -0,0 +1 @@
+engine-strict=true
diff --git a/playground/uneval-stream/.vscode/extensions.json b/playground/uneval-stream/.vscode/extensions.json
new file mode 100644
index 00000000..42ca69cb
--- /dev/null
+++ b/playground/uneval-stream/.vscode/extensions.json
@@ -0,0 +1,3 @@
+{
+ "recommendations": ["svelte.svelte-vscode", "bradlc.vscode-tailwindcss"]
+}
diff --git a/playground/uneval-stream/.vscode/settings.json b/playground/uneval-stream/.vscode/settings.json
new file mode 100644
index 00000000..bc31e155
--- /dev/null
+++ b/playground/uneval-stream/.vscode/settings.json
@@ -0,0 +1,5 @@
+{
+ "files.associations": {
+ "*.css": "tailwindcss"
+ }
+}
diff --git a/playground/uneval-stream/README.md b/playground/uneval-stream/README.md
new file mode 100644
index 00000000..9a3081b2
--- /dev/null
+++ b/playground/uneval-stream/README.md
@@ -0,0 +1,55 @@
+# unevalStream protocol lab
+
+Interactive Svelte 5 playground for observing `devalue`'s streamed executable graph protocol.
+
+Pretty formats blocks for display only, while Raw shows the exact source received over the wire.
+
+## Setup and commands
+
+Install once from the repository root:
+
+```sh
+pnpm install
+pnpm --dir playground/uneval-stream dev
+pnpm --dir playground/uneval-stream check
+pnpm --dir playground/uneval-stream build
+```
+
+## Architecture
+
+- `+page.svelte` owns immutable worker-message snapshots and the block timeline.
+- `CodeEditor.svelte` embeds CodeMirror 6 with TypeScript mode and Mod-Enter execution.
+- `stream.worker.ts` transpiles TypeScript to CommonJS, evaluates the user module, calls workspace `unevalStream`, and evaluates head/tail source in order in the worker realm.
+- `snapshot.ts` turns the reconstructed graph into a structured-cloneable identity/reference tree without JSON serialization.
+- `ObjectTree.svelte` renders recursive expandable nodes and aliases.
+
+## User file contract
+
+The editor is a single TypeScript CommonJS-transpiled module. It must provide a graph as its default export, and may provide an `unevalStream` replacer as a named `replacer` export. The replacer receives the value and a `js` template tag; interpolated values are serialized by devalue (preserving identity), and returning `undefined` falls back to the default serialization:
+
+```ts
+export class Point {
+ constructor(
+ readonly x: number,
+ readonly y: number
+ ) {}
+}
+
+export const replacer = (
+ value: unknown,
+ js: (strings: TemplateStringsArray, ...values: unknown[]) => unknown
+) => {
+ if (value instanceof Point) return js`new Point(${value.x},${value.y})`;
+};
+
+const graph = { answer: Promise.resolve(new Point(3, 4)) };
+export default graph;
+```
+
+All other named exports are promoted onto the worker's global scope before head/tail blocks are evaluated, so custom constructor source emitted by the replacer (e.g. `new Point(…)`) can revive. The worker is discarded after every run, so promoted globals never leak across runs.
+
+Static/dynamic imports and dependencies are not exposed to the user module.
+
+## Security and limitations
+
+Execution is isolated from the page in a dedicated module Web Worker and Stop terminates it. This is an isolation boundary for UI responsiveness, **not a hardened sandbox**: code can use worker-global browser APIs, consume CPU/memory, and initiate network requests allowed by the browser/CSP. Infinite synchronous work requires termination. TypeScript uses `transpileModule`, so it strips types but does not perform full project type-checking. Snapshots enforce depth/item/byte-preview limits and represent functions, symbols, and inaccessible properties as markers.
diff --git a/playground/uneval-stream/components.json b/playground/uneval-stream/components.json
new file mode 100644
index 00000000..8a170a35
--- /dev/null
+++ b/playground/uneval-stream/components.json
@@ -0,0 +1,20 @@
+{
+ "$schema": "https://shadcn-svelte.com/schema.json",
+ "tailwind": {
+ "css": "src/app.css",
+ "baseColor": "neutral"
+ },
+ "aliases": {
+ "components": "$lib/components",
+ "utils": "$lib/utils",
+ "ui": "$lib/components/ui",
+ "hooks": "$lib/hooks",
+ "lib": "$lib"
+ },
+ "typescript": true,
+ "registry": "https://shadcn-svelte.com/registry",
+ "style": "nova",
+ "iconLibrary": "remixicon",
+ "menuColor": "default",
+ "menuAccent": "subtle"
+}
diff --git a/playground/uneval-stream/package.json b/playground/uneval-stream/package.json
new file mode 100644
index 00000000..9f3a7841
--- /dev/null
+++ b/playground/uneval-stream/package.json
@@ -0,0 +1,43 @@
+{
+ "name": "uneval-stream",
+ "private": true,
+ "version": "0.0.1",
+ "type": "module",
+ "scripts": {
+ "dev": "vite dev",
+ "build": "vite build",
+ "preview": "vite preview",
+ "prepare": "svelte-kit sync || echo ''",
+ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
+ "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
+ },
+ "dependencies": {
+ "devalue": "workspace:*"
+ },
+ "devDependencies": {
+ "@codemirror/lang-javascript": "^6.2.4",
+ "@codemirror/language": "^6.12.4",
+ "@codemirror/state": "^6.5.2",
+ "@codemirror/view": "^6.38.6",
+ "@fontsource-variable/roboto": "^5.3.0",
+ "@internationalized/date": "^3.12.3",
+ "@lezer/highlight": "^1.2.3",
+ "@sveltejs/adapter-vercel": "^6.3.4",
+ "@sveltejs/kit": "^2.63.0",
+ "@sveltejs/vite-plugin-svelte": "^7.1.2",
+ "@tailwindcss/vite": "^4.3.0",
+ "bits-ui": "^2.18.1",
+ "clsx": "^2.1.1",
+ "prettier": "^3.9.6",
+ "remixicon-svelte": "^0.0.5",
+ "shadcn-svelte": "^1.5.0",
+ "svelte": "^5.56.1",
+ "svelte-check": "^4.6.0",
+ "tailwind-merge": "^3.6.0",
+ "tailwind-variants": "^3.3.1",
+ "tailwindcss": "^4.3.0",
+ "tw-animate-css": "^1.4.0",
+ "typescript": "^6.0.3",
+ "vite": "^8.0.16"
+ }
+}
diff --git a/playground/uneval-stream/src/app.css b/playground/uneval-stream/src/app.css
new file mode 100644
index 00000000..5ca9f4c1
--- /dev/null
+++ b/playground/uneval-stream/src/app.css
@@ -0,0 +1,700 @@
+@import 'tailwindcss';
+@import 'tw-animate-css';
+@import 'shadcn-svelte/tailwind.css';
+@import '@fontsource-variable/roboto';
+
+@custom-variant dark (&:is(.dark *));
+
+:root {
+ --background: oklch(0.12 0.008 255);
+ --foreground: oklch(0.91 0.012 90);
+ --card: oklch(1 0 0);
+ --card-foreground: oklch(0.145 0 0);
+ --popover: oklch(1 0 0);
+ --popover-foreground: oklch(0.145 0 0);
+ --primary: oklch(0.841 0.238 128.85);
+ --primary-foreground: oklch(0.405 0.101 131.063);
+ --secondary: oklch(0.967 0.001 286.375);
+ --secondary-foreground: oklch(0.21 0.006 285.885);
+ --muted: oklch(0.97 0 0);
+ --muted-foreground: oklch(0.556 0 0);
+ --accent: oklch(0.97 0 0);
+ --accent-foreground: oklch(0.205 0 0);
+ --destructive: oklch(0.577 0.245 27.325);
+ --border: oklch(0.922 0 0);
+ --input: oklch(0.922 0 0);
+ --ring: oklch(0.708 0 0);
+ --chart-1: oklch(0.87 0 0);
+ --chart-2: oklch(0.556 0 0);
+ --chart-3: oklch(0.439 0 0);
+ --chart-4: oklch(0.371 0 0);
+ --chart-5: oklch(0.269 0 0);
+ --radius: 0.625rem;
+ --sidebar: oklch(0.985 0 0);
+ --sidebar-foreground: oklch(0.145 0 0);
+ --sidebar-primary: oklch(0.648 0.2 131.684);
+ --sidebar-primary-foreground: oklch(0.986 0.031 120.757);
+ --sidebar-accent: oklch(0.97 0 0);
+ --sidebar-accent-foreground: oklch(0.205 0 0);
+ --sidebar-border: oklch(0.922 0 0);
+ --sidebar-ring: oklch(0.708 0 0);
+}
+
+.dark {
+ --background: oklch(0.145 0 0);
+ --foreground: oklch(0.985 0 0);
+ --card: oklch(0.205 0 0);
+ --card-foreground: oklch(0.985 0 0);
+ --popover: oklch(0.205 0 0);
+ --popover-foreground: oklch(0.985 0 0);
+ --primary: oklch(0.768 0.233 130.85);
+ --primary-foreground: oklch(0.405 0.101 131.063);
+ --secondary: oklch(0.274 0.006 286.033);
+ --secondary-foreground: oklch(0.985 0 0);
+ --muted: oklch(0.269 0 0);
+ --muted-foreground: oklch(0.708 0 0);
+ --accent: oklch(0.269 0 0);
+ --accent-foreground: oklch(0.985 0 0);
+ --destructive: oklch(0.704 0.191 22.216);
+ --border: oklch(1 0 0 / 10%);
+ --input: oklch(1 0 0 / 15%);
+ --ring: oklch(0.556 0 0);
+ --chart-1: oklch(0.87 0 0);
+ --chart-2: oklch(0.556 0 0);
+ --chart-3: oklch(0.439 0 0);
+ --chart-4: oklch(0.371 0 0);
+ --chart-5: oklch(0.269 0 0);
+ --sidebar: oklch(0.205 0 0);
+ --sidebar-foreground: oklch(0.985 0 0);
+ --sidebar-primary: oklch(0.768 0.233 130.85);
+ --sidebar-primary-foreground: oklch(0.274 0.072 132.109);
+ --sidebar-accent: oklch(0.269 0 0);
+ --sidebar-accent-foreground: oklch(0.985 0 0);
+ --sidebar-border: oklch(1 0 0 / 10%);
+ --sidebar-ring: oklch(0.556 0 0);
+}
+
+@theme inline {
+ --font-sans: 'Roboto Variable', sans-serif;
+ --color-sidebar-ring: var(--sidebar-ring);
+ --color-sidebar-border: var(--sidebar-border);
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
+ --color-sidebar-accent: var(--sidebar-accent);
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
+ --color-sidebar-primary: var(--sidebar-primary);
+ --color-sidebar-foreground: var(--sidebar-foreground);
+ --color-sidebar: var(--sidebar);
+ --color-chart-5: var(--chart-5);
+ --color-chart-4: var(--chart-4);
+ --color-chart-3: var(--chart-3);
+ --color-chart-2: var(--chart-2);
+ --color-chart-1: var(--chart-1);
+ --color-ring: var(--ring);
+ --color-input: var(--input);
+ --color-border: var(--border);
+ --color-destructive: var(--destructive);
+ --color-accent-foreground: var(--accent-foreground);
+ --color-accent: var(--accent);
+ --color-muted-foreground: var(--muted-foreground);
+ --color-muted: var(--muted);
+ --color-secondary-foreground: var(--secondary-foreground);
+ --color-secondary: var(--secondary);
+ --color-primary-foreground: var(--primary-foreground);
+ --color-primary: var(--primary);
+ --color-popover-foreground: var(--popover-foreground);
+ --color-popover: var(--popover);
+ --color-card-foreground: var(--card-foreground);
+ --color-card: var(--card);
+ --color-foreground: var(--foreground);
+ --color-background: var(--background);
+ --radius-sm: calc(var(--radius) * 0.6);
+ --radius-md: calc(var(--radius) * 0.8);
+ --radius-lg: var(--radius);
+ --radius-xl: calc(var(--radius) * 1.4);
+ --radius-2xl: calc(var(--radius) * 1.8);
+ --radius-3xl: calc(var(--radius) * 2.2);
+ --radius-4xl: calc(var(--radius) * 2.6);
+}
+
+@layer base {
+ * {
+ @apply border-border outline-ring/50;
+ }
+ body {
+ @apply bg-background text-foreground;
+ margin: 0;
+ min-width: 320px;
+ background:
+ radial-gradient(circle at 78% -10%, rgba(55, 72, 60, 0.16), transparent 31rem), #0d0f11;
+ font-family: 'Roboto Variable', system-ui, sans-serif;
+ }
+ html {
+ @apply font-sans;
+ }
+}
+
+* {
+ box-sizing: border-box;
+}
+button,
+a {
+ -webkit-tap-highlight-color: transparent;
+}
+button:focus-visible,
+a:focus-visible {
+ outline: 2px solid #d49a45;
+ outline-offset: 2px;
+}
+::selection {
+ background: #a66c2f66;
+}
+::-webkit-scrollbar {
+ width: 9px;
+ height: 9px;
+}
+::-webkit-scrollbar-thumb {
+ background: #32373b;
+ border: 2px solid #111416;
+ border-radius: 8px;
+}
+::-webkit-scrollbar-track {
+ background: #111416;
+}
+main {
+ min-height: 100svh;
+ display: flex;
+ flex-direction: column;
+ padding: 0 20px;
+}
+.topbar {
+ min-height: 72px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 20px;
+}
+.brand {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+.brand-mark {
+ color: #e0a558;
+ border: 1px solid #765b35;
+ background: #231c13;
+ padding: 6px 8px;
+ border-radius: 5px;
+ font:
+ 700 14px ui-monospace,
+ monospace;
+}
+h1 {
+ font:
+ 650 16px ui-monospace,
+ monospace;
+ letter-spacing: -0.02em;
+ margin: 0;
+}
+.brand p {
+ color: #727a80;
+ font:
+ 10px ui-monospace,
+ monospace;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+ margin: 3px 0 0;
+}
+.header-stats {
+ display: flex;
+ gap: 16px;
+ align-items: center;
+ color: #747c82;
+ font:
+ 11px ui-monospace,
+ monospace;
+}
+.workspace {
+ flex: 1;
+ display: grid;
+ grid-template-columns: minmax(360px, 0.95fr) minmax(420px, 1.05fr);
+ gap: 12px;
+ padding: 12px 0;
+ min-height: 0;
+ height: calc(100svh - 111px);
+}
+.panel {
+ min-height: 0;
+ overflow: hidden;
+ border: 1px solid #292d30;
+ border-radius: 8px;
+ background: #111416;
+ box-shadow: 0 12px 38px #0005;
+}
+.editor-panel {
+ display: flex;
+ flex-direction: column;
+}
+.panel-header,
+.result-header {
+ min-height: 47px;
+ border-bottom: 1px solid #292d30;
+ background: #15181a;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0 12px;
+ gap: 10px;
+}
+.file-label,
+.actions,
+.block-meta {
+ display: flex;
+ align-items: center;
+ gap: 9px;
+}
+.ts-icon {
+ font:
+ 700 10px ui-monospace,
+ monospace;
+ color: #7ab4d8;
+ background: #15242d;
+ border: 1px solid #254252;
+ border-radius: 3px;
+ padding: 3px;
+}
+.file-label strong {
+ font:
+ 12px ui-monospace,
+ monospace;
+}
+.dirty-dot,
+.live-dot {
+ width: 5px;
+ height: 5px;
+ border-radius: 50%;
+ background: #d99c4b;
+}
+.status {
+ font:
+ 10px ui-monospace,
+ monospace;
+ color: #7e878d;
+ text-transform: uppercase;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+.status i,
+.pending-row i,
+.page-footer i {
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+ background: #62696d;
+}
+.status-streaming i,
+.status-complete i {
+ background: #55ad78;
+ box-shadow: 0 0 8px #55ad78aa;
+}
+.status-compiling i,
+.status-stopped i {
+ background: #d49a45;
+}
+.status-error i {
+ background: #e06c69;
+}
+kbd {
+ font:
+ 9px ui-monospace,
+ monospace;
+ opacity: 0.7;
+ border-left: 1px solid #0004;
+ padding-left: 6px;
+}
+.editor-wrap {
+ flex: 1;
+ min-height: 0;
+}
+.editor-shell {
+ height: 100%;
+}
+.editor-footer {
+ min-height: 30px;
+ border-top: 1px solid #292d30;
+ display: flex;
+ align-items: center;
+ gap: 18px;
+ padding: 0 12px;
+ color: #687177;
+ font:
+ 10px ui-monospace,
+ monospace;
+}
+.cm-editor {
+ background: #101315;
+ color: #c9ccca;
+ font:
+ 12px/1.7 ui-monospace,
+ SFMono-Regular,
+ Consolas,
+ monospace;
+}
+.cm-editor.cm-focused {
+ outline: none;
+}
+.cm-gutters {
+ background: #101315 !important;
+ color: #454c51 !important;
+ border: 0 !important;
+}
+.cm-activeLine,
+.cm-activeLineGutter {
+ background: #1a1e2055 !important;
+}
+.cm-content {
+ padding: 12px 0;
+ caret-color: #e0a558;
+}
+.cm-selectionBackground {
+ background: #37424b !important;
+}
+.results-tabs {
+ height: 100%;
+ gap: 0 !important;
+}
+.result-header {
+ padding-left: 6px;
+}
+.result-header-tools {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+.protocol {
+ color: #596167;
+ font:
+ 9px ui-monospace,
+ monospace;
+ text-transform: uppercase;
+ letter-spacing: 0.06em;
+}
+.display-toggle {
+ display: flex;
+ align-items: center;
+ padding: 2px;
+ border: 1px solid #303539;
+ border-radius: 7px;
+ background: #0f1214;
+}
+.display-toggle button {
+ height: 22px;
+ min-width: 48px;
+ border-radius: 5px;
+ font:
+ 10px ui-monospace,
+ monospace;
+}
+.tab-content {
+ flex: 1;
+ min-height: 0;
+ margin: 0 !important;
+}
+.result-scroll {
+ height: 100%;
+}
+.timeline,
+.object-pane {
+ padding: 14px;
+ min-height: 100%;
+}
+.stream-block {
+ border: 1px solid #292e31;
+ border-left: 2px solid #3b4145;
+ border-radius: 6px;
+ margin-bottom: 10px;
+ background: #0e1113;
+ overflow: hidden;
+ transition:
+ border-color 0.2s,
+ box-shadow 0.2s;
+}
+.stream-block.latest {
+ border-left-color: #d69a49;
+ box-shadow: inset 0 0 24px #bd7f2210;
+}
+.stream-block header {
+ min-height: 38px;
+ background: #15191b;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0 8px;
+}
+.block-toggle {
+ color: #c4c8c6;
+ border: 0;
+ background: none;
+ font:
+ 11px ui-monospace,
+ monospace;
+ display: flex;
+ gap: 8px;
+ cursor: pointer;
+}
+.block-toggle span {
+ color: #858d92;
+}
+.block-meta {
+ color: #6e777c;
+ font:
+ 10px ui-monospace,
+ monospace;
+}
+.formatting-state {
+ color: #8c795e;
+}
+.code-viewer {
+ border-top: 1px solid #24282b;
+ background: #0e1113;
+}
+.code-viewer .cm-editor {
+ height: auto;
+ background: transparent;
+ font-size: 11px;
+ line-height: 1.6;
+}
+.code-viewer .cm-content {
+ padding: 13px;
+ caret-color: transparent;
+ white-space: pre-wrap;
+ word-break: break-word;
+}
+.code-viewer .cm-line {
+ padding: 0;
+}
+.code-viewer .cm-gutters {
+ background: transparent !important;
+ border-right: 1px solid #24282b !important;
+}
+.code-viewer .cm-scroller {
+ overflow: visible;
+}
+.error-box pre {
+ margin: 0;
+ padding: 13px;
+ overflow: auto;
+ color: #aebbb4;
+ font:
+ 11px/1.6 ui-monospace,
+ monospace;
+ border-top: 1px solid #24282b;
+ white-space: pre-wrap;
+ word-break: break-word;
+}
+.pending-row {
+ color: #767f84;
+ display: flex;
+ align-items: center;
+ gap: 9px;
+ font:
+ 10px ui-monospace,
+ monospace;
+ padding: 12px;
+}
+.pending-row i {
+ background: #d49a45;
+ animation: pulse 1.2s infinite;
+}
+.error-box {
+ border: 1px solid #6f3738;
+ background: #211415;
+ color: #ef8f8c;
+ border-radius: 6px;
+ padding-top: 10px;
+}
+.error-box strong {
+ padding: 12px;
+ font-size: 12px;
+}
+.empty {
+ min-height: 360px;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ text-align: center;
+ color: #666e73;
+}
+.empty > span {
+ font:
+ 30px ui-monospace,
+ monospace;
+ color: #9c7541;
+}
+.empty h2 {
+ color: #a2a8a8;
+ font:
+ 500 13px ui-monospace,
+ monospace;
+ margin: 12px 0 4px;
+}
+.empty p {
+ max-width: 360px;
+ font-size: 12px;
+ line-height: 1.6;
+}
+.empty code {
+ color: #c8904d;
+}
+.object-note {
+ border: 1px solid #2d3932;
+ background: #101913;
+ color: #748078;
+ border-radius: 5px;
+ padding: 9px 11px;
+ margin-bottom: 12px;
+ font:
+ 10px/1.5 ui-monospace,
+ monospace;
+}
+.object-note span {
+ color: #64a87a;
+ margin-right: 8px;
+}
+.tree-node {
+ font:
+ 11px/1.8 ui-monospace,
+ monospace;
+}
+.tree-row {
+ min-height: 25px;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+.tree-toggle {
+ width: 18px;
+ height: 18px;
+ border: 0;
+ color: #7f898e;
+ background: transparent;
+ cursor: pointer;
+}
+.tree-spacer {
+ width: 18px;
+}
+.tree-key {
+ color: #9aaab3;
+}
+.punct,
+.tree-meta {
+ color: #515b61;
+}
+.tree-value {
+ color: #c5c7bf;
+}
+.kind-string {
+ color: #80ab78;
+}
+.kind-number,
+.kind-bigint,
+.kind-boolean {
+ color: #d1a363;
+}
+.kind-ref {
+ color: #d79b4e;
+ text-decoration: underline;
+}
+.kind-undefined,
+.kind-null {
+ color: #797f83;
+ font-style: italic;
+}
+.identity {
+ color: #d49a45 !important;
+ border-color: #57452e !important;
+ height: 17px !important;
+ padding: 0 5px !important;
+}
+.tree-children {
+ margin-left: 9px;
+ padding-left: 13px;
+ border-left: 1px solid #292e31;
+}
+.page-footer {
+ min-height: 27px;
+ display: flex;
+ align-items: center;
+ gap: 18px;
+ color: #5f686d;
+ font:
+ 9px ui-monospace,
+ monospace;
+ text-transform: uppercase;
+}
+.page-footer span {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+.page-footer .green {
+ background: #54a873;
+}
+.page-footer .amber {
+ background: #d49a45;
+}
+.page-footer .spacer {
+ flex: 1;
+}
+.page-footer a {
+ color: #747d82;
+ text-decoration: none;
+}
+@keyframes pulse {
+ 50% {
+ opacity: 0.3;
+ }
+}
+@media (max-width: 850px) {
+ main {
+ padding: 0 10px;
+ }
+ .header-stats span:not(:first-child),
+ .protocol {
+ display: none;
+ }
+ .workspace {
+ height: auto;
+ grid-template-columns: 1fr;
+ }
+ .panel {
+ min-height: 560px;
+ }
+ .results-panel {
+ min-height: 650px;
+ }
+ .page-footer {
+ flex-wrap: wrap;
+ }
+ .topbar {
+ min-height: 64px;
+ }
+ .brand p {
+ display: none;
+ }
+ .result-header {
+ padding-right: 6px;
+ }
+ .display-toggle button {
+ min-width: 42px;
+ }
+}
+@media (prefers-reduced-motion: reduce) {
+ *,
+ *::before,
+ *::after {
+ animation-duration: 0.01ms !important;
+ transition-duration: 0.01ms !important;
+ }
+}
diff --git a/playground/uneval-stream/src/app.d.ts b/playground/uneval-stream/src/app.d.ts
new file mode 100644
index 00000000..da08e6da
--- /dev/null
+++ b/playground/uneval-stream/src/app.d.ts
@@ -0,0 +1,13 @@
+// See https://svelte.dev/docs/kit/types#app.d.ts
+// for information about these interfaces
+declare global {
+ namespace App {
+ // interface Error {}
+ // interface Locals {}
+ // interface PageData {}
+ // interface PageState {}
+ // interface Platform {}
+ }
+}
+
+export {};
diff --git a/playground/uneval-stream/src/app.html b/playground/uneval-stream/src/app.html
new file mode 100644
index 00000000..6a2bb582
--- /dev/null
+++ b/playground/uneval-stream/src/app.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+ %sveltekit.head%
+
+
+ %sveltekit.body%
+
+
diff --git a/playground/uneval-stream/src/lib/CodeEditor.svelte b/playground/uneval-stream/src/lib/CodeEditor.svelte
new file mode 100644
index 00000000..7a9d99ec
--- /dev/null
+++ b/playground/uneval-stream/src/lib/CodeEditor.svelte
@@ -0,0 +1,56 @@
+
+
+
diff --git a/playground/uneval-stream/src/lib/CodeViewer.svelte b/playground/uneval-stream/src/lib/CodeViewer.svelte
new file mode 100644
index 00000000..f0330e69
--- /dev/null
+++ b/playground/uneval-stream/src/lib/CodeViewer.svelte
@@ -0,0 +1,51 @@
+
+
+
diff --git a/playground/uneval-stream/src/lib/ObjectTree.svelte b/playground/uneval-stream/src/lib/ObjectTree.svelte
new file mode 100644
index 00000000..30acba7d
--- /dev/null
+++ b/playground/uneval-stream/src/lib/ObjectTree.svelte
@@ -0,0 +1,61 @@
+
+
+
+
+ {#if expandable}
+
+ {:else}{/if}
+ {name}:
+ {node.ref ? `↗ #${node.ref}` : (node.value ?? node.kind)}
+ {#if node.id}#{node.id}{/if}
+ {#if node.state}{node.state}{/if}
+ {#if node.meta}{node.meta}{/if}
+
+ {#if open && node.children}
+
+ {#each node.children as child, index (`${child.key}-${index}`)}
+
+ {/each}
+
+ {/if}
+
+
+
diff --git a/playground/uneval-stream/src/lib/StreamBlock.svelte b/playground/uneval-stream/src/lib/StreamBlock.svelte
new file mode 100644
index 00000000..f6aee3b9
--- /dev/null
+++ b/playground/uneval-stream/src/lib/StreamBlock.svelte
@@ -0,0 +1,95 @@
+
+
+
+
+
+ {#if mode === 'pretty'}
+ {#await prettySource}
+
+ formatting…{block.bytes} B+{block.elapsed.toFixed(0)} ms{copied ? 'Copied' : 'Copy source'}
+
+ {:then formatted}
+
+ {block.bytes} B+{block.elapsed.toFixed(0)} ms{copied ? 'Copied' : 'Copy source'}
+
+ {/await}
+ {:else}
+
+ {block.bytes} B+{block.elapsed.toFixed(0)} ms{copied ? 'Copied' : 'Copy source'}
+
+ {/if}
+
+ {#if open}
+ {#if mode === 'pretty'}
+ {#await prettySource}{:then formatted}{/await}
+ {:else}{/if}
+ {/if}
+
diff --git a/playground/uneval-stream/src/lib/assets/favicon.svg b/playground/uneval-stream/src/lib/assets/favicon.svg
new file mode 100644
index 00000000..cc5dc66a
--- /dev/null
+++ b/playground/uneval-stream/src/lib/assets/favicon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/playground/uneval-stream/src/lib/codemirror.ts b/playground/uneval-stream/src/lib/codemirror.ts
new file mode 100644
index 00000000..cade90c9
--- /dev/null
+++ b/playground/uneval-stream/src/lib/codemirror.ts
@@ -0,0 +1,24 @@
+import { HighlightStyle, syntaxHighlighting } from '@codemirror/language';
+import { tags } from '@lezer/highlight';
+
+export const javascriptHighlightStyle = HighlightStyle.define([
+ { tag: [tags.keyword, tags.controlKeyword, tags.moduleKeyword], color: '#f5a524' },
+ { tag: [tags.typeName, tags.className, tags.namespace], color: '#5eead4' },
+ { tag: [tags.string, tags.special(tags.string)], color: '#86d98b' },
+ { tag: [tags.number, tags.bool, tags.null], color: '#f6c85f' },
+ {
+ tag: [tags.lineComment, tags.blockComment, tags.docComment],
+ color: '#7f8c9f',
+ fontStyle: 'italic'
+ },
+ { tag: [tags.function(tags.variableName), tags.function(tags.propertyName)], color: '#c4b5fd' },
+ { tag: tags.propertyName, color: '#7dd3fc' },
+ { tag: [tags.variableName, tags.definition(tags.variableName)], color: '#e5e7eb' },
+ {
+ tag: [tags.operator, tags.updateOperator, tags.logicOperator, tags.compareOperator],
+ color: '#fb7185'
+ },
+ { tag: [tags.punctuation, tags.bracket, tags.separator], color: '#94a3b8' }
+]);
+
+export const javascriptSyntaxHighlighting = syntaxHighlighting(javascriptHighlightStyle);
diff --git a/playground/uneval-stream/src/lib/components/ui/badge/badge.svelte b/playground/uneval-stream/src/lib/components/ui/badge/badge.svelte
new file mode 100644
index 00000000..9018911d
--- /dev/null
+++ b/playground/uneval-stream/src/lib/components/ui/badge/badge.svelte
@@ -0,0 +1,50 @@
+
+
+
+
+
+ {@render children?.()}
+
diff --git a/playground/uneval-stream/src/lib/components/ui/badge/index.ts b/playground/uneval-stream/src/lib/components/ui/badge/index.ts
new file mode 100644
index 00000000..f05fb87f
--- /dev/null
+++ b/playground/uneval-stream/src/lib/components/ui/badge/index.ts
@@ -0,0 +1,2 @@
+export { default as Badge } from './badge.svelte';
+export { badgeVariants, type BadgeVariant } from './badge.svelte';
diff --git a/playground/uneval-stream/src/lib/components/ui/button/button.svelte b/playground/uneval-stream/src/lib/components/ui/button/button.svelte
new file mode 100644
index 00000000..c6c43b0d
--- /dev/null
+++ b/playground/uneval-stream/src/lib/components/ui/button/button.svelte
@@ -0,0 +1,89 @@
+
+
+
+
+{#if href}
+
+ {@render children?.()}
+
+{:else}
+
+{/if}
diff --git a/playground/uneval-stream/src/lib/components/ui/button/index.ts b/playground/uneval-stream/src/lib/components/ui/button/index.ts
new file mode 100644
index 00000000..5414d9d3
--- /dev/null
+++ b/playground/uneval-stream/src/lib/components/ui/button/index.ts
@@ -0,0 +1,17 @@
+import Root, {
+ type ButtonProps,
+ type ButtonSize,
+ type ButtonVariant,
+ buttonVariants
+} from './button.svelte';
+
+export {
+ Root,
+ type ButtonProps as Props,
+ //
+ Root as Button,
+ buttonVariants,
+ type ButtonProps,
+ type ButtonSize,
+ type ButtonVariant
+};
diff --git a/playground/uneval-stream/src/lib/components/ui/scroll-area/index.ts b/playground/uneval-stream/src/lib/components/ui/scroll-area/index.ts
new file mode 100644
index 00000000..d5468067
--- /dev/null
+++ b/playground/uneval-stream/src/lib/components/ui/scroll-area/index.ts
@@ -0,0 +1,10 @@
+import Scrollbar from './scroll-area-scrollbar.svelte';
+import Root from './scroll-area.svelte';
+
+export {
+ Root,
+ Scrollbar,
+ //,
+ Root as ScrollArea,
+ Scrollbar as ScrollAreaScrollbar
+};
diff --git a/playground/uneval-stream/src/lib/components/ui/scroll-area/scroll-area-scrollbar.svelte b/playground/uneval-stream/src/lib/components/ui/scroll-area/scroll-area-scrollbar.svelte
new file mode 100644
index 00000000..9c257230
--- /dev/null
+++ b/playground/uneval-stream/src/lib/components/ui/scroll-area/scroll-area-scrollbar.svelte
@@ -0,0 +1,30 @@
+
+
+
+ {@render children?.()}
+
+
diff --git a/playground/uneval-stream/src/lib/components/ui/scroll-area/scroll-area.svelte b/playground/uneval-stream/src/lib/components/ui/scroll-area/scroll-area.svelte
new file mode 100644
index 00000000..ef3a87d8
--- /dev/null
+++ b/playground/uneval-stream/src/lib/components/ui/scroll-area/scroll-area.svelte
@@ -0,0 +1,43 @@
+
+
+
+
+ {@render children?.()}
+
+ {#if orientation === 'vertical' || orientation === 'both'}
+
+ {/if}
+ {#if orientation === 'horizontal' || orientation === 'both'}
+
+ {/if}
+
+
diff --git a/playground/uneval-stream/src/lib/components/ui/separator/index.ts b/playground/uneval-stream/src/lib/components/ui/separator/index.ts
new file mode 100644
index 00000000..768efac9
--- /dev/null
+++ b/playground/uneval-stream/src/lib/components/ui/separator/index.ts
@@ -0,0 +1,7 @@
+import Root from './separator.svelte';
+
+export {
+ Root,
+ //
+ Root as Separator
+};
diff --git a/playground/uneval-stream/src/lib/components/ui/separator/separator.svelte b/playground/uneval-stream/src/lib/components/ui/separator/separator.svelte
new file mode 100644
index 00000000..40dcb92a
--- /dev/null
+++ b/playground/uneval-stream/src/lib/components/ui/separator/separator.svelte
@@ -0,0 +1,23 @@
+
+
+
diff --git a/playground/uneval-stream/src/lib/components/ui/tabs/index.ts b/playground/uneval-stream/src/lib/components/ui/tabs/index.ts
new file mode 100644
index 00000000..c5a15a0b
--- /dev/null
+++ b/playground/uneval-stream/src/lib/components/ui/tabs/index.ts
@@ -0,0 +1,18 @@
+import Content from './tabs-content.svelte';
+import Trigger from './tabs-trigger.svelte';
+import Root from './tabs.svelte';
+import List, { tabsListVariants, type TabsListVariant } from './tabs-list.svelte';
+
+export {
+ Root,
+ Content,
+ List,
+ Trigger,
+ tabsListVariants,
+ type TabsListVariant,
+ //
+ Root as Tabs,
+ Content as TabsContent,
+ List as TabsList,
+ Trigger as TabsTrigger
+};
diff --git a/playground/uneval-stream/src/lib/components/ui/tabs/tabs-content.svelte b/playground/uneval-stream/src/lib/components/ui/tabs/tabs-content.svelte
new file mode 100644
index 00000000..b3f21369
--- /dev/null
+++ b/playground/uneval-stream/src/lib/components/ui/tabs/tabs-content.svelte
@@ -0,0 +1,17 @@
+
+
+
diff --git a/playground/uneval-stream/src/lib/components/ui/tabs/tabs-list.svelte b/playground/uneval-stream/src/lib/components/ui/tabs/tabs-list.svelte
new file mode 100644
index 00000000..74e39a0e
--- /dev/null
+++ b/playground/uneval-stream/src/lib/components/ui/tabs/tabs-list.svelte
@@ -0,0 +1,40 @@
+
+
+
+
+
diff --git a/playground/uneval-stream/src/lib/components/ui/tabs/tabs-trigger.svelte b/playground/uneval-stream/src/lib/components/ui/tabs/tabs-trigger.svelte
new file mode 100644
index 00000000..81916fae
--- /dev/null
+++ b/playground/uneval-stream/src/lib/components/ui/tabs/tabs-trigger.svelte
@@ -0,0 +1,23 @@
+
+
+
diff --git a/playground/uneval-stream/src/lib/components/ui/tabs/tabs.svelte b/playground/uneval-stream/src/lib/components/ui/tabs/tabs.svelte
new file mode 100644
index 00000000..4adebd07
--- /dev/null
+++ b/playground/uneval-stream/src/lib/components/ui/tabs/tabs.svelte
@@ -0,0 +1,19 @@
+
+
+
diff --git a/playground/uneval-stream/src/lib/components/ui/tooltip/index.ts b/playground/uneval-stream/src/lib/components/ui/tooltip/index.ts
new file mode 100644
index 00000000..b98d5771
--- /dev/null
+++ b/playground/uneval-stream/src/lib/components/ui/tooltip/index.ts
@@ -0,0 +1,19 @@
+import Content from './tooltip-content.svelte';
+import Portal from './tooltip-portal.svelte';
+import Provider from './tooltip-provider.svelte';
+import Trigger from './tooltip-trigger.svelte';
+import Root from './tooltip.svelte';
+
+export {
+ Root,
+ Trigger,
+ Content,
+ Provider,
+ Portal,
+ //
+ Root as Tooltip,
+ Content as TooltipContent,
+ Trigger as TooltipTrigger,
+ Provider as TooltipProvider,
+ Portal as TooltipPortal
+};
diff --git a/playground/uneval-stream/src/lib/components/ui/tooltip/tooltip-content.svelte b/playground/uneval-stream/src/lib/components/ui/tooltip/tooltip-content.svelte
new file mode 100644
index 00000000..4a687949
--- /dev/null
+++ b/playground/uneval-stream/src/lib/components/ui/tooltip/tooltip-content.svelte
@@ -0,0 +1,52 @@
+
+
+
+
+ {@render children?.()}
+
+ {#snippet child({ props })}
+
+ {/snippet}
+
+
+
diff --git a/playground/uneval-stream/src/lib/components/ui/tooltip/tooltip-portal.svelte b/playground/uneval-stream/src/lib/components/ui/tooltip/tooltip-portal.svelte
new file mode 100644
index 00000000..7d328f01
--- /dev/null
+++ b/playground/uneval-stream/src/lib/components/ui/tooltip/tooltip-portal.svelte
@@ -0,0 +1,7 @@
+
+
+
diff --git a/playground/uneval-stream/src/lib/components/ui/tooltip/tooltip-provider.svelte b/playground/uneval-stream/src/lib/components/ui/tooltip/tooltip-provider.svelte
new file mode 100644
index 00000000..fde5bb28
--- /dev/null
+++ b/playground/uneval-stream/src/lib/components/ui/tooltip/tooltip-provider.svelte
@@ -0,0 +1,7 @@
+
+
+
diff --git a/playground/uneval-stream/src/lib/components/ui/tooltip/tooltip-trigger.svelte b/playground/uneval-stream/src/lib/components/ui/tooltip/tooltip-trigger.svelte
new file mode 100644
index 00000000..0f041627
--- /dev/null
+++ b/playground/uneval-stream/src/lib/components/ui/tooltip/tooltip-trigger.svelte
@@ -0,0 +1,7 @@
+
+
+
diff --git a/playground/uneval-stream/src/lib/components/ui/tooltip/tooltip.svelte b/playground/uneval-stream/src/lib/components/ui/tooltip/tooltip.svelte
new file mode 100644
index 00000000..16eeb706
--- /dev/null
+++ b/playground/uneval-stream/src/lib/components/ui/tooltip/tooltip.svelte
@@ -0,0 +1,7 @@
+
+
+
diff --git a/playground/uneval-stream/src/lib/format-source.ts b/playground/uneval-stream/src/lib/format-source.ts
new file mode 100644
index 00000000..5960e055
--- /dev/null
+++ b/playground/uneval-stream/src/lib/format-source.ts
@@ -0,0 +1,36 @@
+import type { Plugin } from 'prettier';
+
+let prettierPromise:
+ | Promise<{
+ format: (source: string, options: Record) => Promise;
+ babel: Plugin;
+ estree: Plugin;
+ }>
+ | undefined;
+
+function loadPrettier() {
+ return (prettierPromise ??= Promise.all([
+ import('prettier/standalone'),
+ import('prettier/plugins/babel'),
+ import('prettier/plugins/estree')
+ ]).then(([prettier, babel, estree]) => ({
+ format: prettier.format,
+ babel: babel.default,
+ estree: estree.default
+ })));
+}
+
+export async function formatSource(source: string): Promise {
+ try {
+ const prettier = await loadPrettier();
+ return await prettier.format(source, {
+ parser: 'babel',
+ plugins: [prettier.babel, prettier.estree],
+ useTabs: true,
+ singleQuote: true,
+ printWidth: 90
+ });
+ } catch {
+ return source;
+ }
+}
diff --git a/playground/uneval-stream/src/lib/index.ts b/playground/uneval-stream/src/lib/index.ts
new file mode 100644
index 00000000..856f2b6c
--- /dev/null
+++ b/playground/uneval-stream/src/lib/index.ts
@@ -0,0 +1 @@
+// place files you want to import through the `$lib` alias in this folder.
diff --git a/playground/uneval-stream/src/lib/snapshot.ts b/playground/uneval-stream/src/lib/snapshot.ts
new file mode 100644
index 00000000..34c4d9a5
--- /dev/null
+++ b/playground/uneval-stream/src/lib/snapshot.ts
@@ -0,0 +1,178 @@
+import type { SnapshotEntry, SnapshotNode } from './stream-types';
+
+const MAX_DEPTH = 9;
+const MAX_ITEMS = 80;
+
+export interface SnapshotContext {
+ identities: WeakMap