Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
91cf7f4
feat: add typed WebView navigation command
marcusschiesser Aug 7, 2026
f7651e0
ci: tolerate shared-runner GPU input latency
marcusschiesser Aug 7, 2026
2c75e57
feat: wire Agent Wars previews through navigateWebView
marcusschiesser Aug 7, 2026
6eaf0bf
ci: make Linux canvas Xvfb startup resilient
marcusschiesser Aug 7, 2026
956efde
Merge upstream main into PR branch
marcusschiesser Aug 9, 2026
da55833
Merge upstream main into PR branch
marcusschiesser Aug 10, 2026
4c0aa0b
Resolve WebView navigation opcode collision
marcusschiesser Aug 10, 2026
0ac237b
Merge upstream main into PR branch
marcusschiesser Aug 11, 2026
4d898c0
fix: avoid streaming fetch opcode collision
marcusschiesser Aug 11, 2026
3e2ff7a
Merge upstream main into PR branch
marcusschiesser Aug 11, 2026
dd5a1c3
fix: encode non-ASCII status text as UTF-8
marcusschiesser Aug 11, 2026
f401264
Merge upstream main into PR branch
marcusschiesser Aug 12, 2026
01d8cfe
Fix duplicate system service binding
marcusschiesser Aug 12, 2026
1cd8612
Merge upstream main into PR branch
marcusschiesser Aug 13, 2026
a306479
Merge upstream main into PR branch
marcusschiesser Aug 13, 2026
5adcc12
Merge upstream main and resolve WebView opcode collision
marcusschiesser Aug 14, 2026
983f886
Update Agent Wars to Native SDK 0.9.0
marcusschiesser Aug 14, 2026
a30296b
Fix WebView and file append wire opcodes
marcusschiesser Aug 14, 2026
dc2e3d3
Merge upstream main
marcusschiesser Aug 15, 2026
6f65f7a
Update Agent Wars to Native SDK 0.9.1
marcusschiesser Aug 15, 2026
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
39 changes: 27 additions & 12 deletions .github/scripts/linux-canvas-smoke.sh
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,14 @@ set -u
# Accessibility is not what this smoke tests.
export GTK_A11Y="${GTK_A11Y:-none}"

# Allocate the display with xvfb-run when the runner does not provide one.
# Re-exec the whole smoke script (rather than only the app) so the app,
# xdotool, and xwininfo all use the same DISPLAY. This also avoids depending
# on Xvfb's -displayfd support, which is not consistent across runner images.
if [ -z "${DISPLAY:-}" ] && command -v xvfb-run >/dev/null 2>&1; then
exec xvfb-run -a --server-args="-screen 0 1280x800x24" "$0" "$@"
fi

repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
app_dir="$repo_root/examples/ui-inbox"
snap="$app_dir/.zig-cache/native-sdk-automation/snapshot.txt"
Expand Down Expand Up @@ -128,18 +136,25 @@ assert_no_webkit() {
echo "== native-only ELF audit ok"

# ---- launch ---------------------------------------------------------------
# The script owns its Xvfb (instead of wrapping the app in xvfb-run) so
# the xdotool step below shares the app's display. -displayfd picks a
# free display number, the modern equivalent of xvfb-run -a's probing.
display_file="$(mktemp)"
Xvfb -displayfd 4 -screen 0 1280x800x24 4>"$display_file" &
xvfb_pid=$!
for _ in $(seq 1 100); do
[ -s "$display_file" ] && break
sleep 0.1
done
[ -s "$display_file" ] || fail "Xvfb never reported a display number"
export DISPLAY=":$(cat "$display_file")"
# The normal CI path reaches this point with DISPLAY set by xvfb-run. Keep a
# direct-Xvfb fallback for local environments that have Xvfb but not the
# wrapper, while retaining the same display-sharing behavior.
if [ -z "${DISPLAY:-}" ]; then
display_file="$(mktemp)"
xvfb_log="$(mktemp)"
Xvfb -displayfd 4 -screen 0 1280x800x24 4>"$display_file" 2>"$xvfb_log" &
xvfb_pid=$!
for _ in $(seq 1 100); do
[ -s "$display_file" ] && break
sleep 0.1
done
if [ ! -s "$display_file" ]; then
echo "-- Xvfb stderr:"
sed 's/^/ /' "$xvfb_log" 2>/dev/null
fail "Xvfb never reported a display number"
fi
export DISPLAY=":$(cat "$display_file")"
fi
echo "== Xvfb on $DISPLAY"

cd "$app_dir" || fail "missing $app_dir"
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ jobs:
- run: zig build test-gpu-components-smoke
env:
NATIVE_SDK_SMOKE_BUDGET_MS: "1500"
NATIVE_SDK_INPUT_LATENCY_BUDGET_MS: "500"

macos-gpu-perf:
name: macOS GPU Perf
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,4 @@ packages/core/node_modules/
# TS scaffold writes into a new app's .gitignore)
examples/soundboard-ts/node_modules/
examples/system-monitor-ts/node_modules/
examples/agent-wars/node_modules/
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ The apps pictured above live in [examples/](./examples), most as zero-config pro
| Example | What it shows |
| --- | --- |
| [`chatbot`](./examples/chatbot) | TypeScript + Native markup end to end: modules, a text editor, streaming fetch effects, and replay-safe configuration. |
| [`agent-wars`](./examples/agent-wars) | A two-model Pi harness comparison app: native controls and progress around side-by-side WebView results. |
| [`soundboard-ts`](./examples/soundboard-ts) | The full music-player showcase in TypeScript + Native markup: audio, search, assets, timers, and context menus. |
| [`system-monitor-ts`](./examples/system-monitor-ts) | A live process monitor in TypeScript + Native markup: subprocess effects, tables, charts, and timers. |
| [`calculator`](./examples/calculator) | A complete small app: markup keypad, keyboard input, chrome shortcuts, theming. |
Expand Down
1 change: 1 addition & 0 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1647,6 +1647,7 @@ pub fn build(b: *std.Build) void {
addExampleTestStep(b, host_cli_exe, native_examples_step, "test-example-terminal", "Run terminal example tests", "examples/terminal", .owned),
addExampleTestStep(b, host_cli_exe, native_examples_step, "test-example-workbench", "Run workbench example tests", "examples/workbench", .owned),
addExampleTestStep(b, host_cli_exe, native_examples_step, "test-example-system-monitor-ts", "Run system-monitor-ts example tests", "examples/system-monitor-ts", .managed),
addExampleTestStep(b, host_cli_exe, native_examples_step, "test-example-agent-wars", "Run agent-wars example tests", "examples/agent-wars", .managed),
addExampleTestStep(b, host_cli_exe, native_examples_step, "test-example-effects-probe", "Run effects probe example tests", "examples/effects-probe", .managed),
addExampleTestStep(b, host_cli_exe, native_examples_step, "test-example-channel-monitor", "Run channel monitor example tests", "examples/channel-monitor", .managed),
addExampleTestStep(b, host_cli_exe, native_examples_step, "test-example-menu-bar", "Run menu-bar lifecycle example tests", "examples/menu-bar", .managed),
Expand Down
3 changes: 2 additions & 1 deletion examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ TypeScript is the primary app-authoring language. A new `native init my_app` pro
| Example | Shows |
| --- | --- |
| `chatbot` | Multi-module TypeScript core, text editing, streaming `Cmd.fetch`, environment messages, and deterministic replay. |
| `agent-wars` | Two editable Pi harness models, a shared task, spawn-streamed status, and side-by-side WebView previews. |
| `service-feed-reader` | The complete services loop: `Cmd.fetch`, a parsing service reached through the generated `@native-sdk/services` client, shared record shapes, and recorded replay without the service. |
| `relational-notes` | Append-only SQLite migrations, build-time checked SQL, generated typed transactions and page decoders, FTS5, and live queries. |
| `gpu-components` | Isolated interactive Native UI specimens, disclosure trees, anchored menus, and controlled component state. |
Expand Down Expand Up @@ -62,4 +63,4 @@ The `-ts` suffix is historical: `soundboard-ts` and `system-monitor-ts` distingu

`mobile-shell`, `ios`, and `android` are mobile host projects (Xcode/Gradle shells plus shared `app.zon` metadata) rather than desktop app directories.

Start with `native init` for a small TypeScript + Native markup app, then use `chatbot`, `gpu-components`, `soundboard-ts`, or `system-monitor-ts` according to the feature you need. Use `habits` when you specifically want the smallest Zig-core equivalent, `hello` for the lower-level WebView path, `webview` for native commands or WebView policy, `capabilities` for guarded OS services, and `gpu-surface` or `gpu-dashboard` for custom-rendered or retained-canvas panes.
Start with `native init` for a small TypeScript + Native markup app, then use `chatbot`, `agent-wars`, `gpu-components`, `soundboard-ts`, or `system-monitor-ts` according to the feature you need. Use `habits` when you specifically want the smallest Zig-core equivalent, `hello` for the lower-level WebView path, `webview` for native commands or WebView policy, `capabilities` for guarded OS services, and `gpu-surface` or `gpu-dashboard` for custom-rendered or retained-canvas panes.
84 changes: 84 additions & 0 deletions examples/agent-wars/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Native SDK Agent Wars example

A deliberately small, native-rendered comparison bench for exactly two coding
models. The app shell is TypeScript + Native markup; a single long-lived Node
sidecar uses the [AI SDK Pi harness](https://ai-sdk.dev/providers/ai-sdk-harnesses/pi)
with `@ai-sdk/sandbox-just-bash` to run both agents concurrently.

The example keeps the architecture visible:

- `src/core.ts` owns the Native state machine, editable comboboxes, shared task,
compare/stop HTTP effects, and the coarse line-streamed status protocol.
- `src/app.native` is the complete native UI. Progress appears only once per
model, immediately above its preview.
- `sidecar/coordinator.ts` owns one local server, two isolated Pi sessions,
the versioned preview results, and the viewer bootstrap served to both child
WebViews.
- `Cmd.navigateWebView` navigates those two declared child WebViews after the
coordinator announces that it is ready; the Native core never navigates the
reserved `main` WebView.

## Requirements

- macOS
- Node.js 22 or newer
- an `AI_GATEWAY_API_KEY` available to the app process

Every comparison is routed through Vercel AI Gateway; provider-specific keys
are neither read nor classified. The eight built-in choices are current model
ids from Pi's [Vercel AI Gateway model catalog](https://pi.dev/models?provider=vercel-ai-gateway)
that appear among recent [Terminal-Bench v2.1](https://artificialanalysis.ai/evaluations/terminalbench-v2-1)
results: DeepSeek V4 Flash, GPT-5.6 Luna, GPT-5.6 Sol, Claude Opus 5,
Claude Fable 5, Claude Opus 4.8, Kimi K3, and Grok 4.5. DeepSeek V4 Flash
and GPT-5.6 Luna remain the defaults.

The compact menus arrange those choices in two horizontal rows below each
combobox. The native layout reserves that 64px surface before the platform
WebViews begin, so every option receives real pointer clicks.
Add another built-in choice to one of the two `MODEL_OPTIONS_*` arrays in
`src/core.ts`; keep each row compact. The comboboxes remain editable, so any
other model id from Pi's Vercel AI Gateway section can be entered directly.

## Run

```sh
cd examples/agent-wars
npm install
AI_GATEWAY_API_KEY=... npm run dev
```

The key must be exported into the app process. The example never reads dotenv
files. If the key is stored in one, export it in the shell before running the
app (for example, `set -a; source ~/.env; set +a`).

The Native core starts exactly one sidecar with `Cmd.spawn`. The sidecar listens
on `127.0.0.1:43110` for `POST /compare`, `POST /stop`, and each slot's
viewer/version/result routes. A compare sends the task as its plain-text body and the
small run/model metadata as query parameters, so the Native core needs no JSON
encoder and the sidecar needs no JSON request parser.
Sidecar stdout is reserved for bounded tab-separated status records, which the
core receives through the spawn's line message. Each slot reports only
Starting, Working, and a detailed Ready/Failed terminal state. There is no SSE
endpoint and no browser-to-native coordinator channel.

Each Pi agent receives a separate in-memory just-bash filesystem. It must write
one `index.html`; CSS, application code, and visual assets stay inline or
procedural. A task may use a requested browser library such as Three.js through
a version-pinned jsDelivr or unpkg ESM URL. To keep this example focused on the
Native shell and sidecar boundary, the coordinator publishes that file verbatim:
it does not validate or rewrite the document and it does not attach a content
security policy. A missing `index.html` still fails the slot explicitly.
Both declared child WebViews start with `zero://inline`, then the Native core
uses `Cmd.navigateWebView` to load `http://127.0.0.1:43110/preview/A/viewer`
and `/preview/B/viewer` once the coordinator is ready. The viewer page polls
its slot's version route once per second, then places the completed page in an
opaque iframe sandbox. It keeps the previous completed result visible while the
next comparison runs. The poll only swaps preview documents; agent status and
progress remain on the Native sidecar channel.

## Check

```sh
npm run check
npm test
```
40 changes: 40 additions & 0 deletions examples/agent-wars/app.zon
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
.{
.id = "dev.native_sdk.agent_wars",
.name = "agent-wars",
.display_name = "Agent Wars",
.description = "Compare two Pi harness coding agents in a native-rendered visual evaluation bench.",
.version = "0.1.0",
.platforms = .{"macos"},
.permissions = .{ "view", "command", "network" },
.capabilities = .{ "native_views", "gpu_surfaces", "webview" },
.shell = .{
.windows = .{
.{
.label = "main",
.title = "Agent Wars",
.width = 1380,
.height = 820,
.resizable = false,
.restore_state = false,
.restore_policy = "center_on_primary",
.views = .{
.{ .label = "agent-wars-canvas", .kind = "gpu_surface", .fill = true, .role = "Agent Wars controls", .accessibility_label = "Agent Wars", .gpu_backend = "metal", .gpu_pixel_format = "bgra8_unorm", .gpu_present_mode = "timer", .gpu_alpha_mode = "opaque", .gpu_color_space = "srgb", .gpu_vsync = true },
// app.native's fixed geometry: 16px outer padding, a
// 668px column, and a 1px inset that leaves its border.
// Both panes are child WebViews. They start with the
// inline blank page and the TypeScript core navigates
// them to the sidecar viewer after it is ready.
.{ .label = "preview-a", .kind = "webview", .parent = "agent-wars-canvas", .url = "zero://inline", .x = 17, .y = 205, .width = 666, .height = 598, .layer = 20 },
.{ .label = "preview-b", .kind = "webview", .parent = "agent-wars-canvas", .url = "zero://inline", .x = 697, .y = 205, .width = 666, .height = 598, .layer = 20 },
},
},
},
},
.security = .{
.navigation = .{
.allowed_origins = .{ "zero://inline", "http://127.0.0.1:43110" },
.external_links = .{ .action = "deny" },
},
},
.web_engine = "system",
}
33 changes: 33 additions & 0 deletions examples/agent-wars/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"name": "agent-wars",
"version": "0.1.0",
"private": true,
"type": "module",
"description": "A two-model Pi harness comparison example for Native SDK.",
"engines": {
"node": ">=22"
},
"scripts": {
"dev": "native dev",
"build": "native build",
"check": "native check && npm run typecheck:sidecar",
"typecheck:sidecar": "tsc -p tsconfig.sidecar.json",
"test": "native test -Dplatform=null && npm run test:sidecar",
"test:sidecar": "node --import tsx --test sidecar/coordinator.test.ts",
"sidecar": "node --import tsx sidecar/coordinator.ts"
},
"dependencies": {
"@ai-sdk/harness": "1.0.62",
"@ai-sdk/harness-pi": "1.0.62",
"@ai-sdk/sandbox-just-bash": "1.0.62",
"@native-sdk/core": "0.9.1",
"ai": "7.0.56",
"ws": "8.21.2",
"zod": "4.4.3"
},
"devDependencies": {
"@types/node": "20.19.43",
"tsx": "4.23.8",
"typescript": "7.0.2"
}
}
74 changes: 74 additions & 0 deletions examples/agent-wars/preview/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Agent preview</title>
<style>
* { box-sizing: border-box; }
html, body, #preview { width: 100%; height: 100%; margin: 0; }
body { overflow: hidden; background: white; }
#empty {
position: absolute;
inset: 0;
display: grid;
place-items: center;
padding: 32px;
color: #737373;
font: 13px -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
text-align: center;
}
iframe {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
border: 0;
background: white;
}
</style>
</head>
<body>
<div id="preview"><div id="empty">The completed preview will appear here.</div></div>
<script>
(() => {
const slot = new URLSearchParams(location.search).get("slot") === "B" ? "B" : "A";
const baseUrl = `http://127.0.0.1:43110/preview/${slot}`;
const root = document.querySelector("#preview");
let displayedVersion = 0;
let loadingVersion = 0;

function showVersion(value) {
const version = Number(value);
if (!Number.isSafeInteger(version) || version <= displayedVersion || version === loadingVersion) return;

loadingVersion = version;
const frame = document.createElement("iframe");
frame.title = `Completed model ${slot} preview`;
frame.sandbox = "allow-scripts allow-forms allow-modals allow-popups";
frame.style.visibility = "hidden";
frame.src = `${baseUrl}/site?run=${version}`;
frame.addEventListener("load", () => {
frame.style.visibility = "visible";
root.replaceChildren(frame);
displayedVersion = version;
loadingVersion = 0;
}, { once: true });
root.append(frame);
}

async function checkVersion() {
try {
const response = await fetch(`${baseUrl}/version`, { cache: "no-store" });
if (response.ok) showVersion((await response.json()).version);
} catch {
// The bundled page starts before the coordinator and retries.
}
}

void checkVersion();
setInterval(checkVersion, 1000);
})();
</script>
</body>
</html>
Loading
Loading