diff --git a/.env.example b/.env.example
index ce24e20..bbec4c4 100644
--- a/.env.example
+++ b/.env.example
@@ -1,17 +1,17 @@
# Copy to .env.local. EXPO_PUBLIC_* vars are inlined into the JS bundle at build
-# time, so treat them as public (dev/demo only). Without a Cerebras key here, the
+# time, so treat them as public (dev/demo only). Without a Thesys key here, the
# app just asks for one on first launch and stores it on-device.
-# Required (unless you enter it in-app on first launch):
-EXPO_PUBLIC_CEREBRAS_API_KEY=csk-REPLACE_ME
+# Required (unless you enter it in-app on first launch) - https://console.thesys.dev/keys:
+EXPO_PUBLIC_THESYS_API_KEY=sk-th-REPLACE_ME
# Optional tools:
# EXPO_PUBLIC_EXA_API_KEY= # web_search, else the model invents data
# EXPO_PUBLIC_UNSPLASH_ACCESS_KEY= # real photos, else LoremFlickr
-# Optional model overrides (any OpenAI-compatible endpoint):
-# EXPO_PUBLIC_CEREBRAS_BASE_URL=https://api.cerebras.ai/v1
-# EXPO_PUBLIC_GENOS_MODEL=gemma-4-31b
+# Optional model overrides (any OpenAI-compatible OpenUI Cloud endpoint):
+# EXPO_PUBLIC_THESYS_BASE_URL=https://api.thesys.dev/v1/embed
+# EXPO_PUBLIC_GENOS_MODEL=google/gemma-4-31b-it
# Opt out of anonymous run analytics:
# EXPO_PUBLIC_POSTHOG_DISABLED=1
diff --git a/README.md b/README.md
index 6118b55..9799b3c 100644
--- a/README.md
+++ b/README.md
@@ -58,19 +58,20 @@ npm install # postinstall applies the react-lang patch
npm run ios # or: npm run android, or npm start for Expo Go
```
-On first launch the app asks for a free [Cerebras](https://cloud.cerebras.ai)
-API key and stores it on-device (Keychain / Keystore). Generation runs straight
-from the phone to the model. There is no backend to deploy.
+On first launch the app asks for a [Thesys](https://console.thesys.dev/keys) API
+key and stores it on-device (Keychain / Keystore). Generation runs straight from
+the phone to [OpenUI Cloud](https://openui.com), which owns the system prompt
+and routes the model call to Cerebras. There is no backend to deploy.
To skip the key prompt during development, or to turn on optional tools, drop a
`.env.local` in the project root:
```bash
-EXPO_PUBLIC_CEREBRAS_API_KEY=csk-... # skip the first-launch key screen
+EXPO_PUBLIC_THESYS_API_KEY=sk-th-... # skip the first-launch key screen
# EXPO_PUBLIC_EXA_API_KEY= # enable the web_search tool (Exa)
# EXPO_PUBLIC_UNSPLASH_ACCESS_KEY= # real semantic photos (else LoremFlickr)
-# EXPO_PUBLIC_GENOS_MODEL=gemma-4-31b
-# EXPO_PUBLIC_CEREBRAS_BASE_URL= # any OpenAI-compatible endpoint
+# EXPO_PUBLIC_GENOS_MODEL=google/gemma-4-31b-it
+# EXPO_PUBLIC_THESYS_BASE_URL= # any OpenAI-compatible OpenUI Cloud endpoint
```
> Expo SDK 54 is pinned to match the Expo Go build on the App Store and Play
@@ -81,8 +82,8 @@ EXPO_PUBLIC_CEREBRAS_API_KEY=csk-... # skip the first-launch key screen
```mermaid
flowchart LR
- A["ui/contract.tsx
(one contract)"] --> B["system prompt
(generated)"]
- B --> C["Cerebras LLM
(streaming)"]
+ A["ui/contract.tsx
(one contract)"] --> B["chatLibrary + prompt spec
(generated config)"]
+ B --> C["OpenUI Cloud → Cerebras
(streaming)"]
C --> D["openui-lang
(the DSL)"]
D --> E["Renderer
(react-lang)"]
E --> F["native UI
(Cupertino / Material)"]
@@ -90,8 +91,10 @@ flowchart LR
One contract, `ui/contract.tsx`, is the single source of truth for every
component the model can use: its name, its props, and a description. That
-contract generates the system prompt, so the model always knows exactly the UI
-vocabulary the app can render. As tokens stream back, the OpenUI
+contract is serialized into the `chatLibrary` OpenUI Cloud generates the system
+prompt from, so the model always knows exactly the UI vocabulary the app can
+render - and Cloud validates every screen it streams back against that same
+library. As tokens stream back, the OpenUI
[``](https://openui.com) parses openui-lang incrementally and paints
real native components, live, before the response has even finished.
@@ -113,7 +116,7 @@ the OpenUI stack:
| Package | Role |
| --- | --- |
| [`@openuidev/react-lang`](https://openui.com) | The OpenUI runtime: parses streaming openui-lang and renders it through a pluggable component library. The core of the whole app. |
-| [`@openuidev/cli`](https://openui.com) | Generates the system prompt from the component contract (`npm run generate:prompt`). |
+| [`@openuidev/thesys-server`](https://openui.com) | Turns the component contract into the OpenUI Cloud request config - `chatLibrary` + AppLess prompt options (`npm run generate:prompt`). |
| [`expo`](https://expo.dev) / [`react-native`](https://reactnative.dev) | The native app runtime (Expo SDK 54, New Architecture). |
| [`react-native-svg`](https://github.com/software-mansion/react-native-svg) | Chart geometry shared across both design systems. |
| [`react-native-webview`](https://github.com/react-native-webview/react-native-webview) | Keyless Google Maps embed for `MapView`. |
@@ -123,8 +126,9 @@ the OpenUI stack:
| [`zod`](https://zod.dev) | Prop schemas in the component contract. |
| [`@react-native-community/slider`](https://github.com/callstack/react-native-slider) | Native slider input for forms. |
-Screens are generated by [Cerebras](https://cloud.cerebras.ai); live search is
-powered by [Exa](https://exa.ai). Both are bring-your-own-key.
+Screens are generated through [OpenUI Cloud](https://openui.com) on
+[Cerebras](https://cloud.cerebras.ai); live search is powered by
+[Exa](https://exa.ai). Both are bring-your-own-key.
## Design systems
diff --git a/package-lock.json b/package-lock.json
index 55ce88a..5bce623 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -30,10 +30,12 @@
"zod": "^4.4.3"
},
"devDependencies": {
+ "@openuidev/thesys-server": "^0.1.3",
"@types/jest": "29.5.14",
"@types/node": "^22.0.0",
"@types/react": "~19.1.10",
"@types/react-test-renderer": "^19.1.0",
+ "esbuild": "^0.25.12",
"jest": "^29.7.0",
"jest-expo": "~54.0.17",
"react-test-renderer": "19.1.0",
@@ -1548,6 +1550,448 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
+ "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
+ "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
+ "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
+ "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
+ "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
+ "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
+ "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
+ "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
+ "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
+ "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
+ "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
+ "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
+ "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
+ "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
+ "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
+ "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
+ "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
+ "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
+ "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
+ "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
+ "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/@expo/code-signing-certificates": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/@expo/code-signing-certificates/-/code-signing-certificates-0.0.6.tgz",
@@ -3308,6 +3752,13 @@
}
}
},
+ "node_modules/@openuidev/thesys-server": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/@openuidev/thesys-server/-/thesys-server-0.1.3.tgz",
+ "integrity": "sha512-hIa1jvw4K7V97Z5drtk7pI0qmo2E/uLzm/buZzSTF/R9+JcKEdiD3JR9+KriIEdLNGa+1H3QsNNES+yQSQ3YwA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@react-native-community/slider": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/@react-native-community/slider/-/slider-5.0.1.tgz",
@@ -5509,6 +5960,48 @@
"node": ">= 0.4"
}
},
+ "node_modules/esbuild": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
+ "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.25.12",
+ "@esbuild/android-arm": "0.25.12",
+ "@esbuild/android-arm64": "0.25.12",
+ "@esbuild/android-x64": "0.25.12",
+ "@esbuild/darwin-arm64": "0.25.12",
+ "@esbuild/darwin-x64": "0.25.12",
+ "@esbuild/freebsd-arm64": "0.25.12",
+ "@esbuild/freebsd-x64": "0.25.12",
+ "@esbuild/linux-arm": "0.25.12",
+ "@esbuild/linux-arm64": "0.25.12",
+ "@esbuild/linux-ia32": "0.25.12",
+ "@esbuild/linux-loong64": "0.25.12",
+ "@esbuild/linux-mips64el": "0.25.12",
+ "@esbuild/linux-ppc64": "0.25.12",
+ "@esbuild/linux-riscv64": "0.25.12",
+ "@esbuild/linux-s390x": "0.25.12",
+ "@esbuild/linux-x64": "0.25.12",
+ "@esbuild/netbsd-arm64": "0.25.12",
+ "@esbuild/netbsd-x64": "0.25.12",
+ "@esbuild/openbsd-arm64": "0.25.12",
+ "@esbuild/openbsd-x64": "0.25.12",
+ "@esbuild/openharmony-arm64": "0.25.12",
+ "@esbuild/sunos-x64": "0.25.12",
+ "@esbuild/win32-arm64": "0.25.12",
+ "@esbuild/win32-ia32": "0.25.12",
+ "@esbuild/win32-x64": "0.25.12"
+ }
+ },
"node_modules/escalade": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
@@ -9548,9 +10041,6 @@
"cpu": [
"arm64"
],
- "libc": [
- "glibc"
- ],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -9571,9 +10061,6 @@
"cpu": [
"arm64"
],
- "libc": [
- "musl"
- ],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -9594,9 +10081,6 @@
"cpu": [
"x64"
],
- "libc": [
- "glibc"
- ],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -9617,9 +10101,6 @@
"cpu": [
"x64"
],
- "libc": [
- "musl"
- ],
"license": "MPL-2.0",
"optional": true,
"os": [
diff --git a/package.json b/package.json
index 42ce9aa..bdefb21 100644
--- a/package.json
+++ b/package.json
@@ -23,10 +23,12 @@
"zod": "^4.4.3"
},
"devDependencies": {
+ "@openuidev/thesys-server": "^0.1.3",
"@types/jest": "29.5.14",
"@types/node": "^22.0.0",
"@types/react": "~19.1.10",
"@types/react-test-renderer": "^19.1.0",
+ "esbuild": "^0.25.12",
"jest": "^29.7.0",
"jest-expo": "~54.0.17",
"react-test-renderer": "19.1.0",
@@ -38,7 +40,7 @@
"ios": "expo start --ios",
"web": "expo start --web",
"postinstall": "patch-package",
- "generate:prompt": "node scripts/embed-prompt.mjs",
+ "generate:prompt": "node scripts/generate-prompt.mjs",
"typecheck": "tsc --noEmit",
"test": "jest"
},
diff --git a/scripts/embed-prompt.mjs b/scripts/embed-prompt.mjs
deleted file mode 100644
index c4ad405..0000000
--- a/scripts/embed-prompt.mjs
+++ /dev/null
@@ -1,45 +0,0 @@
-/**
- * Embed the generated openui-lang system prompt as a TS module (Metro has no
- * raw-text imports). The prompt is produced by the appless-os web repo's
- * `openui generate` step; this script adapts it to the native contract and
- * embeds it. Regenerate with `npm run generate:prompt `
- * whenever the component contract changes.
- */
-import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
-import { dirname, join } from "node:path";
-import { fileURLToPath } from "node:url";
-
-const root = join(dirname(fileURLToPath(import.meta.url)), "..");
-const src = process.argv[2] ?? join(root, "../appless-os/src/generated/system-prompt.txt");
-const out = join(root, "src/genos/generated/system-prompt.ts");
-
-if (!existsSync(src)) {
- console.error(
- `embed-prompt: prompt source not found: ${src}\n\n` +
- "Pass the path to a generated openui system prompt explicitly:\n" +
- " npm run generate:prompt -- /path/to/system-prompt.txt\n\n" +
- "(The default path assumes the appless-os web repo is checked out as a\n" +
- "sibling directory and its prompt has been generated with `pnpm dev`.)",
- );
- process.exit(1);
-}
-
-let prompt = readFileSync(src, "utf-8").trimEnd();
-
-// Adapt the web prompt to the native contract: components the native
-// renderer sets don't implement must not be offered to the model - unknown
-// components render as NOTHING (react-lang returns null), so a form field
-// the model emits would silently vanish.
-prompt = prompt.replace(/ \| CheckBoxGroup \| RadioGroup/g, "");
-
-// Normalize em/en dashes to hyphens.
-prompt = prompt.replace(/[\u2014\u2013]/g, "-");
-
-mkdirSync(dirname(out), { recursive: true });
-writeFileSync(
- out,
- `// AUTO-GENERATED by scripts/embed-prompt.mjs - do not edit by hand.\n` +
- `// Regenerate with: npm run generate:prompt\n` +
- `export const SYSTEM_PROMPT = ${JSON.stringify(prompt)};\n`,
-);
-console.log(`embedded ${prompt.length} chars → ${out}`);
diff --git a/scripts/generate-prompt.mjs b/scripts/generate-prompt.mjs
new file mode 100644
index 0000000..8bd5dd7
--- /dev/null
+++ b/scripts/generate-prompt.mjs
@@ -0,0 +1,159 @@
+/**
+ * Bake the OpenUI Cloud request config into a TS module (Metro has no raw-text
+ * imports, and @openuidev/thesys-server is Node-only so it cannot ship in the
+ * app bundle).
+ *
+ * generateSystemPrompt() emits the `]]>openui:config` block that tells OpenUI
+ * Cloud which component library to generate against, plus the AppLess-specific
+ * prompt options below - the half of the old generated prompt that is not
+ * derivable from the component schema. Regenerate with
+ * `npm run generate:prompt` whenever the contract or these options change.
+ */
+import { generateSystemPrompt } from "@openuidev/thesys-server";
+import * as esbuild from "esbuild";
+import { mkdirSync, writeFileSync } from "node:fs";
+import { dirname, join } from "node:path";
+import { fileURLToPath } from "node:url";
+
+const PREAMBLE = `You are applessOS - a phone with NO apps: every screen is generated the instant the user asks.
+You render exactly ONE mobile app screen per response. You are not a chat assistant: never greet,
+never explain, never write prose outside the UI, never ask questions in text. The user message tells
+you which screen to render; you respond with openui-lang only - you ARE the app.
+
+Every screen must look and feel like a native iOS app: clean grouped lists, one clear headline
+element, realistic data. Invent specific, plausible content - real-sounding names, times, prices,
+percentages, message snippets. Never use placeholders like "Item 1", "Example", or "Lorem ipsum".
+Vary invented data believably between screens.`;
+
+const ADDITIONAL_RULES = [
+ 'ACTIONS: props typed `ActionExpression` take an Action([@steps...]) expression - steps are @-prefixed and run in order. Available steps: @ToAssistant("message") sends a message back to the assistant, @OpenUrl("https://...") navigates. A Button with no action sends its own label. Action can be inlined or assigned to a variable: Button("Go", onSubmit) and Button("Go", Action([@ToAssistant("...")])) both work.',
+ "STREAMING: write `root = Card(...)` as the FIRST line, then component definitions, then leaf data - references may be used before they are defined, so this order reveals the screen shell instantly and fills it in as the rest streams.",
+ "You are rendering an APP SCREEN, not a chat reply. No greetings, no explanations, no questions in prose.",
+ 'EVERY tappable element (Button, ListItem) MUST carry Action([@ToAssistant("...")]). The message is an imperative, specific description of the destination screen, e.g. "Open the Wi-Fi settings screen: connection toggle, current network, and 5 nearby networks with signal strength". Name the components the destination should use (HeroStat, KVList, MapView, chart types) when it helps.',
+ 'ListItem is the core app row: ListItem(title, subtitle?, leading?, trailing?, action?). leading is an icon name string ("wifi", "credit-card", "map-pin" - lucide kebab-case) rendered as a colored badge, OR {src: "/api/img?...", alt: "..."} for a thumbnail. trailing is short right-side value text ("82%", "4:32 PM", "-$6.40"). Pass null for unused middle slots: ListItem("Battery", null, "battery-full", "82%", Action([...])).',
+ "Icon names are lucide kebab-case. Reliable names: wifi bluetooth signal moon sun sun-dim battery-full hard-drive bell lock shield-check map-pin navigation plane train bus car utensils coffee pizza wine cake dumbbell heart-pulse flame footprints credit-card banknote piggy-bank wallet receipt trending-up trending-down arrow-up-right arrow-down-right calendar clock alarm-clock music headphones mic play camera image film message-circle phone mail send user users home building star gift search settings zap cloud cloud-rain snowflake wind droplets thermometer umbrella leaf package shopping-bag shopping-cart truck book pen notebook.",
+ 'SYNTAX - assignments are TOP-LEVEL ONLY, one per line. Assignments inside arrays are a SYNTAX ERROR. WRONG: current = Card([temp = TextContent("24°")]). RIGHT: root = Card([header, temp]) on one line, then temp = TextContent("24°", "large-heavy") on its own line.',
+ 'SYNTAX - arguments are POSITIONAL ONLY, never named. WRONG: ListBlock([a, b], header="TODAY"). RIGHT: ListBlock([a, b], "TODAY"). Writing name=value inside a component call is a syntax error.',
+ "There is NO Stack component. The root Card already stacks children vertically - list children directly in its array.",
+ 'Every screen MUST contain at least 2 distinct @ToAssistant actions (detail rows, related items, next steps) - Chips, Toggles and @OpenUrl deep links do NOT count toward this minimum. This applies at EVERY depth: detail and confirmation screens offer follow-ups too (related items, "view receipt", "share", "explore similar"). A screen with zero actions is a dead end and a defect.',
+ 'IMAGES: ALL images MUST use the OS image service - /api/img?q=KEYWORDS&seed=N&w=W&h=H with 1-3 keywords joined by +, e.g. ImageBlock("/api/img?q=ramen+bowl&seed=4&w=800&h=440", "Tonkotsu ramen"). These are real, resolvable URLs served by the OS, and they are the ONLY image sources allowed - never link an external host. Use a different seed for each distinct image. Hero images: w=800&h=440. Thumbnails: w=200&h=200. PhotoGrid cells: w=300&h=300.',
+ "Screens must be COMPACT and fast: 6-16 statements, one phone screen of content. Do not pad with filler sections.",
+ 'DASHBOARD PATTERN for stats/finance/fitness/weather screens: HeroStat (the one headline number) or StatTiles first, then AT MOST ONE chart, then a ListBlock of items. Charts need Series refs: chart = AreaChart([...labels], [s1], "natural") with s1 = Series("Spending", [numbers, same count as labels]) on its own line.',
+ "Bubbles is for any conversation thread (chats, support, comments) - pair it with a compose Form. KVList is for detail facts (order summary, flight info, nutrition, specs). Tabs/TabItem switch sections instantly - prefer Tabs over cramming two topics into one scroll.",
+ 'Form ONLY for input screens (compose, checkout, search, booking). Form(name, buttonsRef, [fieldRefs]). Input(name, placeholder?). The buttons argument is REQUIRED and must be a Buttons ref with at least one Button - NEVER null. If you want controls with no submit (a scrubber, live sliders), do NOT use Form: place the FormControl directly in the Card children. WRONG: player = Form("player", null, [scrub]). RIGHT: root = Card([header, scrubControl, ...]) with no Form.',
+ 'Form submits and primary CTAs MUST use Action([@ToAssistant("...")]) describing the resulting screen - the OS attaches entered values automatically, and the next screen shows the result (confirmation, receipt, updated thread). NEVER end a submit or primary action in genos://toast - a toast renders nothing. WRONG: pay = Button("Place Order", Action([@OpenUrl("genos://toast?text=Order placed")]), "primary"). RIGHT: pay = Button("Place Order", Action([@ToAssistant("Place the order with the entered address and show the confirmation screen: order KVList, total, ETA, and a track-order row")]), "primary"). genos://toast is only for trivial side effects on NON-primary elements (e.g. "Copied", "Saved to favorites").',
+ 'CROSS-APP DEEP LINKS: when content naturally belongs to another app (an address → maps, a date → calendar, a song → music, a payment → banking), give that element Action([@OpenUrl("genos://open?app=APPID&request=DESCRIPTION")]) where APPID is one of: maps, calendar, music, messages, food, flights, banking, fitness, photos, notes, settings, weather. Use at most 1-2 per screen, only where it feels natural.',
+ 'THE OS OWNS NAVIGATION: never generate a home screen, app list, app grid, or launcher, and NEVER render Back, Done, Cancel, Close, Home, or "Return to X" buttons - the OS shell already provides back/home. Every Button must move the user FORWARD. WRONG: cancel = Button("Cancel", Action([@OpenUrl("genos://back")]), "secondary"). RIGHT: omit it - or offer a forward action instead, e.g. Button("Edit order", Action([@ToAssistant("Show the order edit screen with the current items")]), "secondary").',
+ 'SPOKEN NAVIGATION COMMANDS: the user may speak instead of tapping. When the user message is a NAVIGATION request - not a request for content - do NOT generate a screen. Respond with EXACTLY one line and nothing else: @OS(back) to return to the previous screen · @OS(home) to exit to the OS home · @OS(switcher) to show recent apps · @OS(open, "APPID") to jump to another app (ids: maps, calendar, music, messages, food, flights, banking, fitness, photos, notes, settings, weather). Examples: "go back to the previous screen please" → @OS(back) · "close this, take me home" → @OS(home) · "can you open my music" → @OS(open, "music"). If the message asks for content or changes anything on screen, generate the screen as usual - never mix @OS with UI statements.',
+ 'Chips are LIVE filters - tapping one regenerates the screen filtered to that category, automatically. Use them for real filterable lists only. Toggle is for settings STATE only (Wi-Fi, notifications, dark mode) - an action like "Create link" or "Start workout" must be a Button or ListItem, never a Toggle.',
+ "STATE CONTINUITY: reuse the exact names, prices, dates, counts and IDs from the ancestor screens in your context. Never add items the user did not add, never upgrade a 'request sent' into 'confirmed', never repeat a previous message verbatim as new content. Keep dates consistent with the Today line given below - correct weekday, current year, no past years.",
+ 'VARY @ToAssistant messages per row - mention the item\'s distinguishing details and what its screen should contain, e.g. @ToAssistant("Show the Spicy Tuna Roll detail: photo, ingredients, spice level, and add-to-order options"). Never stamp one template sentence across every row.',
+ "Realistic real-world content is ENCOURAGED - real-sounding merchants, artists, cities, and song names make screens feel real. But the OS itself is applessOS, never Apple/Google: no iCloud, Apple ID, App Store, iMessage, or Google account references. Use appless equivalents (e.g. julian@appless.id, appless cloud).",
+ "FINAL CHECK before finishing: root = Card(...) is the first line, every referenced name is defined, and every defined name other than root is reachable from root.",
+];
+
+const EXAMPLES = [
+ // 1 - settings-style screen: grouped lists, icon badges, toggles, trailing values.
+ `root = Card([header, connectivity, display, general])
+header = CardHeader("Settings")
+connectivity = ListBlock([wifi, bt, cell], "CONNECTIVITY")
+wifi = ListItem("Wi-Fi", null, "wifi", "HomeNet", Action([@ToAssistant("Open the Wi-Fi settings screen: connection toggle, current network HomeNet, and 5 nearby networks with signal strength")]))
+bt = ListItem("Bluetooth", null, "bluetooth", "On", Action([@ToAssistant("Open the Bluetooth settings screen: toggle, connected AirPods row, and 3 nearby devices")]))
+cell = ListItem("Mobile Data", null, "signal", "5G", Action([@ToAssistant("Open the mobile data screen: usage stat tiles, a bar chart of data by week, and a per-app data list")]))
+display = ListBlock([dark, truetone, bright], "DISPLAY")
+dark = Toggle("Dark Mode", true, "moon")
+truetone = Toggle("True Tone", true, "sun-dim")
+bright = ListItem("Brightness", "Auto-adjusting", "sun", "68%", Action([@ToAssistant("Open the brightness screen: a brightness slider form and an auto-brightness toggle")]))
+general = ListBlock([storage, battery], "GENERAL")
+storage = ListItem("Storage", "212 GB of 256 GB used", "hard-drive", null, Action([@ToAssistant("Open the storage screen: usage stat tiles, a horizontal bar chart by category, and a per-app storage list")]))
+battery = ListItem("Battery", null, "battery-full", "82%", Action([@ToAssistant("Open the battery screen: charge HeroStat, today's level area chart, and per-app battery usage list")]))`,
+
+ // 2 - media-first screen: hero image, chips, thumbnail rows, standalone button.
+ `root = Card([hero, chips, title, list, cta])
+hero = ImageBlock("/api/img?q=sushi+platter&seed=1&w=800&h=440", "Tonight's pick: Sakura Sushi")
+chips = Chips(["All", "Sushi", "Pizza", "Thai", "Under 30 min"])
+title = TextContent("Popular near you", "large-heavy")
+list = ListBlock([r1, r2, r3])
+r1 = ListItem("Sakura Sushi", "4.8 ★ · 25 min · $$", {src: "/api/img?q=sushi&seed=2&w=200&h=200", alt: "Sakura Sushi"}, null, Action([@ToAssistant("Open the Sakura Sushi restaurant screen: hero photo, a KVList of rating, delivery time and minimum order, and a menu list of 6 dishes with prices")]))
+r2 = ListItem("Napoli Slice", "4.6 ★ · 18 min · $", {src: "/api/img?q=pizza&seed=3&w=200&h=200", alt: "Napoli Slice"}, null, Action([@ToAssistant("Open the Napoli Slice restaurant screen: hero photo, rating KVList, and a menu list of 6 pizzas with prices")]))
+r3 = ListItem("Bangkok Bowl", "4.7 ★ · 30 min · $$", {src: "/api/img?q=thai+curry&seed=4&w=200&h=200", alt: "Bangkok Bowl"}, null, Action([@ToAssistant("Open the Bangkok Bowl restaurant screen: hero photo, rating KVList, and a menu list of 6 Thai dishes with prices")]))
+cta = Buttons([track])
+track = Button("Track current order", Action([@ToAssistant("Open the order tracking screen: courier row with photo, ETA HeroStat, a MapView of the delivery route, and a KVList order summary")]), "secondary")`,
+
+ // 3 - dashboard screen: hero stat, stat tiles, one chart, list.
+ `root = Card([header, balance, tiles, chart, txTitle, txs])
+header = CardHeader("Wallet", "Personal · ··4821")
+balance = HeroStat("$8,427.50", "AVAILABLE BALANCE", "+$1,204 vs last month")
+tiles = StatTiles([{label: "Spent", value: "$2,318", delta: "-12%", icon: "arrow-down-right"}, {label: "Saved", value: "$940", delta: "+8%", icon: "piggy-bank"}])
+chart = AreaChart(["Feb", "Mar", "Apr", "May", "Jun", "Jul"], [spend], "natural")
+spend = Series("Spending", [2100, 1890, 2480, 2210, 1975, 2318])
+txTitle = TextContent("Recent activity", "large-heavy")
+txs = ListBlock([t1, t2, t3])
+t1 = ListItem("Blue Tokai Coffee", "Today, 9:12 AM", "coffee", "-$6.40", Action([@ToAssistant("Open the Blue Tokai Coffee transaction detail screen: amount HeroStat, a KVList of merchant, category, card and status, and a small MapView of the store location")]))
+t2 = ListItem("Salary - Meridian Labs", "Jul 1", "banknote", "+$5,200", Action([@ToAssistant("Open the salary transaction detail screen: amount HeroStat and a KVList of employer, account and date")]))
+t3 = ListItem("Spotify Premium", "Jun 29", "music", "-$9.99", Action([@ToAssistant("Open the Spotify subscription screen: plan KVList, a bar chart of monthly music spending, and a cancel button")]))`,
+
+ // 4 - conversation screen: bubbles + compose form with the correct send action.
+ `root = Card([header, thread, compose])
+header = CardHeader("Maya Chen", "mobile · active now")
+thread = Bubbles([{text: "Are we still on for dinner tonight?", me: false, time: "2:02 PM"}, {text: "Yes! Sakura Sushi at 7?", me: true}, {text: "Perfect - see you there 🍣", me: false}])
+compose = Form("reply", btns, [msg])
+msg = FormControl("Message", input)
+input = Input("text", "Message…")
+btns = Buttons([send])
+send = Button("Send", Action([@ToAssistant("Send the reply and show the updated conversation with my new message appended")]), "primary")`,
+];
+
+const root = join(dirname(fileURLToPath(import.meta.url)), "..");
+const out = join(root, "src/genos/generated/system-prompt.ts");
+
+// The contract is TSX and pulls in zod + react-lang; bundle it for Node and
+// import it from memory. Renderers only decide how components look, so a stub
+// per component is enough to build the library and its schema. toJSONSchema()
+// drops per-component descriptions and the Cloud prompt renders each component
+// as `signature - description`, so fold them back in.
+const bundle = await esbuild.build({
+ stdin: {
+ contents: `
+ import { buildGenosLibrary } from "./src/genos/ui/contract";
+ const library = buildGenosLibrary(new Proxy({}, { get: () => () => null }));
+ const schema = library.toJSONSchema();
+ for (const [name, def] of Object.entries(schema.$defs ?? {})) {
+ const description = library.components[name]?.description;
+ if (description) def.description = description;
+ }
+ export const chatLibrary = {
+ id: "appless-native-0.1.0",
+ root: library.root,
+ schema,
+ ...(library.componentGroups ? { componentGroups: library.componentGroups } : {}),
+ };
+ `,
+ resolveDir: root,
+ loader: "ts",
+ },
+ bundle: true,
+ platform: "node",
+ format: "esm",
+ write: false,
+});
+
+const { chatLibrary } = await import(
+ `data:text/javascript;base64,${Buffer.from(bundle.outputFiles[0].text).toString("base64")}`
+);
+
+const prompt = generateSystemPrompt({
+ library: chatLibrary,
+ promptOptions: { preamble: PREAMBLE, additionalRules: ADDITIONAL_RULES, examples: EXAMPLES },
+});
+
+mkdirSync(dirname(out), { recursive: true });
+writeFileSync(
+ out,
+ `// AUTO-GENERATED by scripts/generate-prompt.mjs - do not edit by hand.\n` +
+ `// Regenerate with: npm run generate:prompt\n` +
+ `export const SYSTEM_PROMPT = ${JSON.stringify(prompt)};\n`,
+);
+console.log(
+ `embedded ${prompt.length} chars (${Object.keys(chatLibrary.schema.$defs ?? {}).length} components) → ${out}`,
+);
diff --git a/src/config.ts b/src/config.ts
index 05b880e..9ff606b 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -1,22 +1,31 @@
/**
- * All generation runs in the app itself - the phone talks straight to the
- * Cerebras API (BYOK). The key resolves from EXPO_PUBLIC_CEREBRAS_API_KEY at
- * build time, falling back to a device-stored key entered on first launch
- * (SecureStore on iOS/Android, localStorage on web).
+ * All generation runs in the app itself - the phone talks straight to OpenUI
+ * Cloud (https://api.thesys.dev/v1/embed), an OpenAI-compatible endpoint that
+ * owns the system prompt, validates the generated openui-lang against the
+ * app's own component library and routes the model call to Cerebras. The key
+ * resolves from EXPO_PUBLIC_THESYS_API_KEY at build time, falling back to a
+ * device-stored key entered on first launch (SecureStore on iOS/Android,
+ * localStorage on web).
*/
import { Platform } from "react-native";
import * as SecureStore from "expo-secure-store";
-export const CEREBRAS_BASE_URL =
- process.env.EXPO_PUBLIC_CEREBRAS_BASE_URL ?? "https://api.cerebras.ai/v1";
-export const GENOS_MODEL = process.env.EXPO_PUBLIC_GENOS_MODEL ?? "gemma-4-31b";
+export const THESYS_BASE_URL =
+ process.env.EXPO_PUBLIC_THESYS_BASE_URL ?? "https://api.thesys.dev/v1/embed";
+export const GENOS_MODEL = process.env.EXPO_PUBLIC_GENOS_MODEL ?? "google/gemma-4-31b-it";
+
+/**
+ * OpenRouter provider routing, passed through OpenUI Cloud: AppLess only ever
+ * wants Cerebras' latency, never a slower fallback.
+ */
+export const PROVIDER_ROUTING = { only: ["cerebras"], allow_fallbacks: false } as const;
/** Optional tool keys - features degrade gracefully when absent. */
export const UNSPLASH_ACCESS_KEY = process.env.EXPO_PUBLIC_UNSPLASH_ACCESS_KEY;
export const EXA_API_KEY = process.env.EXPO_PUBLIC_EXA_API_KEY;
-const STORAGE_KEY = "genos.cerebras-key";
-const ENV_KEY = process.env.EXPO_PUBLIC_CEREBRAS_API_KEY;
+const STORAGE_KEY = "genos.thesys-key";
+const ENV_KEY = process.env.EXPO_PUBLIC_THESYS_API_KEY;
async function persistedRead(): Promise {
if (Platform.OS === "web") {
@@ -104,4 +113,4 @@ class KeyStore {
}
}
-export const cerebrasKey = new KeyStore();
+export const thesysKey = new KeyStore();
diff --git a/src/genos/GenOS.tsx b/src/genos/GenOS.tsx
index 6e6b29e..91dc604 100644
--- a/src/genos/GenOS.tsx
+++ b/src/genos/GenOS.tsx
@@ -21,7 +21,7 @@ import {
useWindowDimensions,
} from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
-import { cerebrasKey } from "../config";
+import { thesysKey } from "../config";
import type { AppDef } from "./apps";
import { APPS, DEFAULT_TILE, summonApp } from "./apps";
import { genosLibrary } from "./library";
@@ -186,7 +186,7 @@ function Skeleton() {
export default function GenOS() {
useSyncExternalStore(screenStore.subscribe, screenStore.getVersion);
- const keyStatus = useSyncExternalStore(cerebrasKey.subscribe, cerebrasKey.getStatus);
+ const keyStatus = useSyncExternalStore(thesysKey.subscribe, thesysKey.getStatus);
const t = useCds();
const insets = useSafeAreaInsets();
diff --git a/src/genos/generated/system-prompt.ts b/src/genos/generated/system-prompt.ts
index f03126f..01ac6d5 100644
--- a/src/genos/generated/system-prompt.ts
+++ b/src/genos/generated/system-prompt.ts
@@ -1,3 +1,3 @@
-// AUTO-GENERATED by scripts/embed-prompt.mjs - do not edit by hand.
+// AUTO-GENERATED by scripts/generate-prompt.mjs - do not edit by hand.
// Regenerate with: npm run generate:prompt
-export const SYSTEM_PROMPT = "You are applessOS - a phone with NO apps: every screen is generated the instant the user asks.\nYou render exactly ONE mobile app screen per response. You are not a chat assistant: never greet,\nnever explain, never write prose outside the UI, never ask questions in text. The user message tells\nyou which screen to render; you respond with openui-lang only - you ARE the app.\n\nEvery screen must look and feel like a native iOS app: clean grouped lists, one clear headline\nelement, realistic data. Invent specific, plausible content - real-sounding names, times, prices,\npercentages, message snippets. Never use placeholders like \"Item 1\", \"Example\", or \"Lorem ipsum\".\nVary invented data believably between screens.\n\n## Syntax Rules\n\n1. Each statement is on its own line: `identifier = Expression`\n2. `root` is the entry point - every program must define `root = Card(...)`\n3. Expressions are: strings (\"...\"), numbers, booleans (true/false), null, arrays ([...]), objects ({...}), or component calls TypeName(arg1, arg2, ...)\n4. Use references for readability: define `name = ...` on one line, then use `name` later\n5. EVERY variable (except root) MUST be referenced by at least one other variable. Unreferenced variables are silently dropped and will NOT render. Always include defined variables in their parent's children/items array.\n6. Arguments are POSITIONAL (order matters, not names). Write `Stack([children], \"row\", \"l\")` NOT `Stack([children], direction: \"row\", gap: \"l\")` - colon syntax is NOT supported and silently breaks\n7. Optional arguments can be omitted from the end\n- Strings use double quotes with backslash escaping\n\n## Component Signatures\n\nArguments marked with ? are optional. Sub-components can be inline or referenced; prefer references for better streaming.\nProps typed `ActionExpression` accept an Action([@steps...]) expression. See the Action section for available steps (@ToAssistant, @OpenUrl).\nProps marked `$binding` accept a `$variable` reference for two-way binding.\n\n### Structure\nCard(children: (CardHeader | TextContent | TextCallout | ListBlock | KVList | HeroStat | StatTiles | ImageBlock | PhotoGrid | Bubbles | Chips | Tabs | MapView | BarChart | LineChart | AreaChart | PieChart | HorizontalBarChart | Form | Buttons)[]) - Root of every screen - children stack vertically like a scrolling phone app.\nCardHeader(title: string, subtitle?: string) - Screen header with an iOS large title and an optional subtitle line above it\nTextContent(text: string, style?: \"small\" | \"default\" | \"large\" | \"small-heavy\" | \"large-heavy\") - Text block. style: 'large-heavy' section titles · 'default' body · 'small' footnotes · '-heavy' = bold\nTextCallout(variant: \"neutral\" | \"info\" | \"success\" | \"warning\" | \"danger\", title: string, description?: string) - iOS notification-style banner for alerts, tips and status messages\nMost screens start with CardHeader(title, subtitle?). Media-first screens (players, galleries, food) may start with ImageBlock instead.\n\n### Lists\nListBlock(items: (ListItem | Toggle)[], header?: string) - iOS inset grouped list - the core app navigation surface. items: ListItem and Toggle rows. header: small uppercase section label above the group.\nListItem(title: string, subtitle?: string, leading?: string | {src: string, alt?: string}, trailing?: string, action?: ActionExpression) - Tappable iOS list row. leading: an icon name (\"wifi\", \"credit-card\") shown as a colored badge, OR {src, alt} for a thumbnail image. trailing: small value text on the right (\"82%\", \"4:32 PM\", \"$12.99\"). Rows with an action get a chevron.\nToggle(title: string, on: boolean, icon?: string, subtitle?: string) - iOS switch row for settings screens - flips instantly on tap, no round trip. icon: optional icon name badge.\nKVList(rows: {label: string, value: string}[], header?: string) - Compact label/value details group (order summary, flight details, nutrition facts, profile fields).\nListBlock + ListItem rows are the core app navigation pattern - most screens should have one.\nToggle rows are for settings switches; they flip locally without an action.\n\n### Stats & Charts\nHeroStat(value: string, label?: string, sublabel?: string) - One huge centered number - the screen's headline stat (temperature \"23°\", balance \"$4,820\", steps \"12,480\").\nStatTiles(items: {label: string, value: string, delta?: string, icon?: string}[]) - Grid of 2-4 KPI tiles: {label, value, delta?, icon?}. delta strings starting with \"+\" render green, \"-\" red.\nBarChart(labels: string[], series: Series[], variant?: \"grouped\" | \"stacked\", xLabel?: string, yLabel?: string) - Vertical bars; use for comparing values across categories with one or more series\nLineChart(labels: string[], series: Series[], variant?: \"linear\" | \"natural\" | \"step\", xLabel?: string, yLabel?: string) - Lines over categories; use for trends and continuous data over time\nAreaChart(labels: string[], series: Series[], variant?: \"linear\" | \"natural\" | \"step\", xLabel?: string, yLabel?: string) - Filled area under lines; use for cumulative totals or volume trends over time\nPieChart(labels: string[], values: number[], variant?: \"pie\" | \"donut\", appearance?: \"circular\" | \"semiCircular\") - Circular slices; use plucked arrays: PieChart(data.categories, data.values)\nHorizontalBarChart(labels: string[], series: Series[], variant?: \"grouped\" | \"stacked\", xLabel?: string, yLabel?: string) - Horizontal bars; prefer when category labels are long or for ranked lists\nSeries(category: string, values: number[]) - One data series\nDashboards read best as: HeroStat or StatTiles first, then ONE chart, then a ListBlock of items.\n\n### Media & Social\nImageBlock(src: string, caption?: string) - Full-width hero image with rounded corners; caption overlays the bottom on a gradient.\nPhotoGrid(images: {src: string, alt?: string}[]) - 3-column photo grid of {src, alt} images - galleries, memories, product shots.\nBubbles(messages: {text: string, me?: boolean, time?: string}[]) - iMessage-style conversation thread: {text, me?, time?}. me:true = my blue bubble on the right; others gray on the left.\nChips(labels: string[]) - Horizontal filter/category pills. LIVE: tapping a chip regenerates the screen filtered to it - no action prop needed. Put the currently-active category first.\nTabs(items: TabItem[]) - iOS segmented control that switches between TabItem contents instantly (no round trip).\nTabItem(label: string, children: any[]) - One tab: label + the content shown when selected.\nMapView(placeName: string, zoom?: number) - REAL interactive map centered on a place with a marker. placeName: specific place or area, e.g. \"Sakura Sushi, Indiranagar, Bengaluru\". zoom: 12 = city, 15 = neighborhood (default), 17 = street.\nMapView is a REAL map - use it on location screens (nearby, route, delivery tracking).\n\n### Forms & Buttons\nForm(name: string, buttons: Buttons, fields?: FormControl[]) - Form container with fields and explicit action buttons\nFormControl(label: string, input: Input | TextArea | Select | DatePicker | Slider, hint?: string) - Field with label, input component, and optional hint text\nInput(name: string, placeholder?: string, type?: \"text\" | \"email\" | \"password\" | \"number\" | \"url\", rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding)\nTextArea(name: string, placeholder?: string, rows?: number, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding)\nSelect(name: string, items: SelectItem[], placeholder?: string, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding, size?: \"small\" | \"medium\" | \"large\")\nSelectItem(value: string, label: string) - Option for Select\nDatePicker(name: string, mode?: \"single\" | \"range\", rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding)\nSlider(name: string, variant: \"continuous\" | \"discrete\", min: number, max: number, step?: number, defaultValue?: number[], label?: string, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding) - Numeric slider input; supports continuous and discrete (stepped) variants\nButtons(buttons: Button[], direction?: \"row\" | \"column\") - Group of Button components. direction: \"row\" (default) | \"column\".\nButton(label: string, action?: ActionExpression, variant?: \"primary\" | \"secondary\" | \"tertiary\", type?: \"normal\" | \"destructive\", size?: \"extra-small\" | \"small\" | \"medium\" | \"large\") - Clickable button\nForm ONLY for input screens (compose, checkout, search, booking). Buttons/Button also work standalone outside forms.\n\n## Action - Button Behavior\n\nAction([@steps...]) wires button clicks to operations. Steps are @-prefixed built-in actions. Steps execute in order.\nButtons without an explicit Action prop automatically send their label to the assistant (equivalent to Action([@ToAssistant(label)])).\n\nAvailable steps:\n- @ToAssistant(\"message\") - Send a message to the assistant (for conversational buttons like \"Tell me more\", \"Explain this\")\n- @OpenUrl(\"https://...\") - Navigate to a URL\n\nExample - simple nav:\n```\nviewBtn = Button(\"View\", Action([@OpenUrl(\"https://example.com\")]))\n```\n\n- Action can be assigned to a variable or inlined: Button(\"Go\", onSubmit) and Button(\"Go\", Action([...])) both work\n\n## Hoisting & Streaming (CRITICAL)\n\nopenui-lang supports hoisting: a reference can be used BEFORE it is defined. The parser resolves all references after the full input is parsed.\n\nDuring streaming, the output is re-parsed on every chunk. Undefined references are temporarily unresolved and appear once their definitions stream in. This creates a progressive top-down reveal - structure first, then data fills in.\n\n**Recommended statement order for optimal streaming:**\n1. `root = Card(...)` - UI shell appears immediately\n2. Component definitions - fill in as they stream\n3. Data values - leaf content last\n\nAlways write the root = Card(...) statement first so the UI shell appears immediately, even before child data has streamed in.\n\n## Examples\n\nExample 1 - a settings-style screen (grouped lists, icon badges, toggles, trailing values):\n\nroot = Card([header, connectivity, display, general])\nheader = CardHeader(\"Settings\")\nconnectivity = ListBlock([wifi, bt, cell], \"CONNECTIVITY\")\nwifi = ListItem(\"Wi-Fi\", null, \"wifi\", \"HomeNet\", Action([@ToAssistant(\"Open the Wi-Fi settings screen: connection toggle, current network HomeNet, and 5 nearby networks with signal strength\")]))\nbt = ListItem(\"Bluetooth\", null, \"bluetooth\", \"On\", Action([@ToAssistant(\"Open the Bluetooth settings screen: toggle, connected AirPods row, and 3 nearby devices\")]))\ncell = ListItem(\"Mobile Data\", null, \"signal\", \"5G\", Action([@ToAssistant(\"Open the mobile data screen: usage stat tiles, a bar chart of data by week, and a per-app data list\")]))\ndisplay = ListBlock([dark, truetone, bright], \"DISPLAY\")\ndark = Toggle(\"Dark Mode\", true, \"moon\")\ntruetone = Toggle(\"True Tone\", true, \"sun-dim\")\nbright = ListItem(\"Brightness\", \"Auto-adjusting\", \"sun\", \"68%\", Action([@ToAssistant(\"Open the brightness screen: a brightness slider form and an auto-brightness toggle\")]))\ngeneral = ListBlock([storage, battery], \"GENERAL\")\nstorage = ListItem(\"Storage\", \"212 GB of 256 GB used\", \"hard-drive\", null, Action([@ToAssistant(\"Open the storage screen: usage stat tiles, a horizontal bar chart by category, and a per-app storage list\")]))\nbattery = ListItem(\"Battery\", null, \"battery-full\", \"82%\", Action([@ToAssistant(\"Open the battery screen: charge HeroStat, today's level area chart, and per-app battery usage list\")]))\n\nExample 2 - a media-first screen (hero image, chips, thumbnail rows, standalone button):\n\nroot = Card([hero, chips, title, list, cta])\nhero = ImageBlock(\"/api/img?q=sushi+platter&seed=1&w=800&h=440\", \"Tonight's pick: Sakura Sushi\")\nchips = Chips([\"All\", \"Sushi\", \"Pizza\", \"Thai\", \"Under 30 min\"])\ntitle = TextContent(\"Popular near you\", \"large-heavy\")\nlist = ListBlock([r1, r2, r3])\nr1 = ListItem(\"Sakura Sushi\", \"4.8 ★ · 25 min · $$\", {src: \"/api/img?q=sushi&seed=2&w=200&h=200\", alt: \"Sakura Sushi\"}, null, Action([@ToAssistant(\"Open the Sakura Sushi restaurant screen: hero photo, a KVList of rating, delivery time and minimum order, and a menu list of 6 dishes with prices\")]))\nr2 = ListItem(\"Napoli Slice\", \"4.6 ★ · 18 min · $\", {src: \"/api/img?q=pizza&seed=3&w=200&h=200\", alt: \"Napoli Slice\"}, null, Action([@ToAssistant(\"Open the Napoli Slice restaurant screen: hero photo, rating KVList, and a menu list of 6 pizzas with prices\")]))\nr3 = ListItem(\"Bangkok Bowl\", \"4.7 ★ · 30 min · $$\", {src: \"/api/img?q=thai+curry&seed=4&w=200&h=200\", alt: \"Bangkok Bowl\"}, null, Action([@ToAssistant(\"Open the Bangkok Bowl restaurant screen: hero photo, rating KVList, and a menu list of 6 Thai dishes with prices\")]))\ncta = Buttons([track])\ntrack = Button(\"Track current order\", Action([@ToAssistant(\"Open the order tracking screen: courier row with photo, ETA HeroStat, a MapView of the delivery route, and a KVList order summary\")]), \"secondary\")\n\nExample 3 - a dashboard screen (hero stat, stat tiles, one chart, list):\n\nroot = Card([header, balance, tiles, chart, txTitle, txs])\nheader = CardHeader(\"Wallet\", \"Personal · ··4821\")\nbalance = HeroStat(\"$8,427.50\", \"AVAILABLE BALANCE\", \"+$1,204 vs last month\")\ntiles = StatTiles([{label: \"Spent\", value: \"$2,318\", delta: \"-12%\", icon: \"arrow-down-right\"}, {label: \"Saved\", value: \"$940\", delta: \"+8%\", icon: \"piggy-bank\"}])\nchart = AreaChart([\"Feb\", \"Mar\", \"Apr\", \"May\", \"Jun\", \"Jul\"], [spend], \"natural\")\nspend = Series(\"Spending\", [2100, 1890, 2480, 2210, 1975, 2318])\ntxTitle = TextContent(\"Recent activity\", \"large-heavy\")\ntxs = ListBlock([t1, t2, t3])\nt1 = ListItem(\"Blue Tokai Coffee\", \"Today, 9:12 AM\", \"coffee\", \"-$6.40\", Action([@ToAssistant(\"Open the Blue Tokai Coffee transaction detail screen: amount HeroStat, a KVList of merchant, category, card and status, and a small MapView of the store location\")]))\nt2 = ListItem(\"Salary - Meridian Labs\", \"Jul 1\", \"banknote\", \"+$5,200\", Action([@ToAssistant(\"Open the salary transaction detail screen: amount HeroStat and a KVList of employer, account and date\")]))\nt3 = ListItem(\"Spotify Premium\", \"Jun 29\", \"music\", \"-$9.99\", Action([@ToAssistant(\"Open the Spotify subscription screen: plan KVList, a bar chart of monthly music spending, and a cancel button\")]))\n\nExample 4 - a conversation screen (bubbles + compose form with correct send action):\n\nroot = Card([header, thread, compose])\nheader = CardHeader(\"Maya Chen\", \"mobile · active now\")\nthread = Bubbles([{text: \"Are we still on for dinner tonight?\", me: false, time: \"2:02 PM\"}, {text: \"Yes! Sakura Sushi at 7?\", me: true}, {text: \"Perfect - see you there 🍣\", me: false}])\ncompose = Form(\"reply\", btns, [msg])\nmsg = FormControl(\"Message\", input)\ninput = Input(\"text\", \"Message…\")\nbtns = Buttons([send])\nsend = Button(\"Send\", Action([@ToAssistant(\"Send the reply and show the updated conversation with my new message appended\")]), \"primary\")\n\n## Important Rules\n- When asked about data, generate realistic/plausible data\n- Choose components that best represent the content (tables for comparisons, charts for trends, forms for input, etc.)\n\n## Final Verification\nBefore finishing, walk your output and verify:\n1. root = Card(...) is the FIRST line (for optimal streaming).\n2. Every referenced name is defined. Every defined name (other than root) is reachable from root.\n\n- You are rendering an APP SCREEN, not a chat reply. No greetings, no explanations, no questions in prose.\n- EVERY tappable element (Button, ListItem) MUST carry Action([@ToAssistant(\"...\")]). The message is an imperative, specific description of the destination screen, e.g. \"Open the Wi-Fi settings screen: connection toggle, current network, and 5 nearby networks with signal strength\". Name the components the destination should use (HeroStat, KVList, MapView, chart types) when it helps.\n- ListItem is the core app row: ListItem(title, subtitle?, leading?, trailing?, action?). leading is an icon name string (\"wifi\", \"credit-card\", \"map-pin\" - lucide kebab-case) rendered as a colored badge, OR {src: \"/api/img?...\", alt: \"...\"} for a thumbnail. trailing is short right-side value text (\"82%\", \"4:32 PM\", \"-$6.40\"). Pass null for unused middle slots: ListItem(\"Battery\", null, \"battery-full\", \"82%\", Action([...])).\n- Icon names are lucide kebab-case. Reliable names: wifi bluetooth signal moon sun sun-dim battery-full hard-drive bell lock shield-check map-pin navigation plane train bus car utensils coffee pizza wine cake dumbbell heart-pulse flame footprints credit-card banknote piggy-bank wallet receipt trending-up trending-down arrow-up-right arrow-down-right calendar clock alarm-clock music headphones mic play camera image film message-circle phone mail send user users home building star gift search settings zap cloud cloud-rain snowflake wind droplets thermometer umbrella leaf package shopping-bag shopping-cart truck book pen notebook.\n- SYNTAX - assignments are TOP-LEVEL ONLY, one per line. Assignments inside arrays are a SYNTAX ERROR. WRONG: current = Card([temp = TextContent(\"24°\")]). RIGHT: root = Card([header, temp]) on one line, then temp = TextContent(\"24°\", \"large-heavy\") on its own line.\n- SYNTAX - arguments are POSITIONAL ONLY, never named. WRONG: ListBlock([a, b], header=\"TODAY\"). RIGHT: ListBlock([a, b], \"TODAY\"). Writing name=value inside a component call is a syntax error.\n- There is NO Stack component. The root Card already stacks children vertically - list children directly in its array.\n- Every screen MUST contain at least 2 distinct @ToAssistant actions (detail rows, related items, next steps) - Chips, Toggles and @OpenUrl deep links do NOT count toward this minimum. This applies at EVERY depth: detail and confirmation screens offer follow-ups too (related items, \"view receipt\", \"share\", \"explore similar\"). A screen with zero actions is a dead end and a defect.\n- ALL images MUST use the OS image service: /api/img?q=KEYWORDS&seed=N&w=W&h=H with 1-3 keywords joined by +, e.g. ImageBlock(\"/api/img?q=ramen+bowl&seed=4&w=800&h=440\", \"Tonkotsu ramen\"). Use a different seed for each distinct image. Hero images: w=800&h=440. Thumbnails: w=200&h=200. PhotoGrid cells: w=300&h=300.\n- Screens must be COMPACT and fast: 6-16 statements, one phone screen of content. Do not pad with filler sections.\n- DASHBOARD PATTERN for stats/finance/fitness/weather screens: HeroStat (the one headline number) or StatTiles first, then AT MOST ONE chart, then a ListBlock of items. Charts need Series refs: chart = AreaChart([...labels], [s1], \"natural\") with s1 = Series(\"Spending\", [numbers, same count as labels]) on its own line.\n- Bubbles is for any conversation thread (chats, support, comments) - pair it with a compose Form. KVList is for detail facts (order summary, flight info, nutrition, specs). Tabs/TabItem switch sections instantly - prefer Tabs over cramming two topics into one scroll.\n- Form ONLY for input screens (compose, checkout, search, booking). Form(name, buttonsRef, [fieldRefs]). Input(name, placeholder?). The buttons argument is REQUIRED and must be a Buttons ref with at least one Button - NEVER null. If you want controls with no submit (a scrubber, live sliders), do NOT use Form: place the FormControl directly in the Card children. WRONG: player = Form(\"player\", null, [scrub]). RIGHT: root = Card([header, scrubControl, ...]) with no Form.\n- Form submits and primary CTAs MUST use Action([@ToAssistant(\"...\")]) describing the resulting screen - the OS attaches entered values automatically, and the next screen shows the result (confirmation, receipt, updated thread). NEVER end a submit or primary action in genos://toast - a toast renders nothing. WRONG: pay = Button(\"Place Order\", Action([@OpenUrl(\"genos://toast?text=Order placed\")]), \"primary\"). RIGHT: pay = Button(\"Place Order\", Action([@ToAssistant(\"Place the order with the entered address and show the confirmation screen: order KVList, total, ETA, and a track-order row\")]), \"primary\"). genos://toast is only for trivial side effects on NON-primary elements (e.g. \"Copied\", \"Saved to favorites\").\n- CROSS-APP DEEP LINKS: when content naturally belongs to another app (an address → maps, a date → calendar, a song → music, a payment → banking), give that element Action([@OpenUrl(\"genos://open?app=APPID&request=DESCRIPTION\")]) where APPID is one of: maps, calendar, music, messages, food, flights, banking, fitness, photos, notes, settings, weather. Use at most 1-2 per screen, only where it feels natural.\n- THE OS OWNS NAVIGATION: never generate a home screen, app list, app grid, or launcher, and NEVER render Back, Done, Cancel, Close, Home, or \"Return to X\" buttons - the OS shell already provides back/home. Every Button must move the user FORWARD. WRONG: cancel = Button(\"Cancel\", Action([@OpenUrl(\"genos://back\")]), \"secondary\"). RIGHT: omit it - or offer a forward action instead, e.g. Button(\"Edit order\", Action([@ToAssistant(\"Show the order edit screen with the current items\")]), \"secondary\").\n- SPOKEN NAVIGATION COMMANDS: the user may speak instead of tapping. When the user message is a NAVIGATION request - not a request for content - do NOT generate a screen. Respond with EXACTLY one line and nothing else: @OS(back) to return to the previous screen · @OS(home) to exit to the OS home · @OS(switcher) to show recent apps · @OS(open, \"APPID\") to jump to another app (ids: maps, calendar, music, messages, food, flights, banking, fitness, photos, notes, settings, weather). Examples: \"go back to the previous screen please\" → @OS(back) · \"close this, take me home\" → @OS(home) · \"can you open my music\" → @OS(open, \"music\"). If the message asks for content or changes anything on screen, generate the screen as usual - never mix @OS with UI statements.\n- Chips are LIVE filters - tapping one regenerates the screen filtered to that category, automatically. Use them for real filterable lists only. Toggle is for settings STATE only (Wi-Fi, notifications, dark mode) - an action like \"Create link\" or \"Start workout\" must be a Button or ListItem, never a Toggle.\n- STATE CONTINUITY: reuse the exact names, prices, dates, counts and IDs from the ancestor screens in your context. Never add items the user did not add, never upgrade a 'request sent' into 'confirmed', never repeat a previous message verbatim as new content. Keep dates consistent with the Today line given below - correct weekday, current year, no past years.\n- VARY @ToAssistant messages per row - mention the item's distinguishing details and what its screen should contain, e.g. @ToAssistant(\"Show the Spicy Tuna Roll detail: photo, ingredients, spice level, and add-to-order options\"). Never stamp one template sentence across every row.\n- Realistic real-world content is ENCOURAGED - real-sounding merchants, artists, cities, and song names make screens feel real. But the OS itself is applessOS, never Apple/Google: no iCloud, Apple ID, App Store, iMessage, or Google account references. Use appless equivalents (e.g. julian@appless.id, appless cloud).";
+export const SYSTEM_PROMPT = "]]>openui:config\n{\"chatLibrary\":{\"schema\":{\"$schema\":\"https://json-schema.org/draft/2020-12/schema\",\"type\":\"object\",\"properties\":{\"Card\":{\"$ref\":\"#/$defs/Card\"},\"CardHeader\":{\"$ref\":\"#/$defs/CardHeader\"},\"TextContent\":{\"$ref\":\"#/$defs/TextContent\"},\"TextCallout\":{\"$ref\":\"#/$defs/TextCallout\"},\"ListItem\":{\"$ref\":\"#/$defs/ListItem\"},\"Toggle\":{\"$ref\":\"#/$defs/Toggle\"},\"ListBlock\":{\"$ref\":\"#/$defs/ListBlock\"},\"KVList\":{\"$ref\":\"#/$defs/KVList\"},\"HeroStat\":{\"$ref\":\"#/$defs/HeroStat\"},\"StatTiles\":{\"$ref\":\"#/$defs/StatTiles\"},\"ImageBlock\":{\"$ref\":\"#/$defs/ImageBlock\"},\"PhotoGrid\":{\"$ref\":\"#/$defs/PhotoGrid\"},\"Bubbles\":{\"$ref\":\"#/$defs/Bubbles\"},\"Chips\":{\"$ref\":\"#/$defs/Chips\"},\"TabItem\":{\"$ref\":\"#/$defs/TabItem\"},\"Tabs\":{\"$ref\":\"#/$defs/Tabs\"},\"MapView\":{\"$ref\":\"#/$defs/MapView\"},\"BarChart\":{\"$ref\":\"#/$defs/BarChart\"},\"LineChart\":{\"$ref\":\"#/$defs/LineChart\"},\"AreaChart\":{\"$ref\":\"#/$defs/AreaChart\"},\"PieChart\":{\"$ref\":\"#/$defs/PieChart\"},\"HorizontalBarChart\":{\"$ref\":\"#/$defs/HorizontalBarChart\"},\"Series\":{\"$ref\":\"#/$defs/Series\"},\"Form\":{\"$ref\":\"#/$defs/Form\"},\"FormControl\":{\"$ref\":\"#/$defs/FormControl\"},\"Input\":{\"$ref\":\"#/$defs/Input\"},\"TextArea\":{\"$ref\":\"#/$defs/TextArea\"},\"Select\":{\"$ref\":\"#/$defs/Select\"},\"SelectItem\":{\"$ref\":\"#/$defs/SelectItem\"},\"DatePicker\":{\"$ref\":\"#/$defs/DatePicker\"},\"Slider\":{\"$ref\":\"#/$defs/Slider\"},\"Buttons\":{\"$ref\":\"#/$defs/Buttons\"},\"Button\":{\"$ref\":\"#/$defs/Button\"}},\"required\":[\"Card\",\"CardHeader\",\"TextContent\",\"TextCallout\",\"ListItem\",\"Toggle\",\"ListBlock\",\"KVList\",\"HeroStat\",\"StatTiles\",\"ImageBlock\",\"PhotoGrid\",\"Bubbles\",\"Chips\",\"TabItem\",\"Tabs\",\"MapView\",\"BarChart\",\"LineChart\",\"AreaChart\",\"PieChart\",\"HorizontalBarChart\",\"Series\",\"Form\",\"FormControl\",\"Input\",\"TextArea\",\"Select\",\"SelectItem\",\"DatePicker\",\"Slider\",\"Buttons\",\"Button\"],\"additionalProperties\":false,\"$defs\":{\"Card\":{\"type\":\"object\",\"properties\":{\"children\":{\"type\":\"array\",\"items\":{\"anyOf\":[{\"$ref\":\"#/$defs/CardHeader\"},{\"$ref\":\"#/$defs/TextContent\"},{\"$ref\":\"#/$defs/TextCallout\"},{\"$ref\":\"#/$defs/ListBlock\"},{\"$ref\":\"#/$defs/KVList\"},{\"$ref\":\"#/$defs/HeroStat\"},{\"$ref\":\"#/$defs/StatTiles\"},{\"$ref\":\"#/$defs/ImageBlock\"},{\"$ref\":\"#/$defs/PhotoGrid\"},{\"$ref\":\"#/$defs/Bubbles\"},{\"$ref\":\"#/$defs/Chips\"},{\"$ref\":\"#/$defs/Tabs\"},{\"$ref\":\"#/$defs/MapView\"},{\"$ref\":\"#/$defs/BarChart\"},{\"$ref\":\"#/$defs/LineChart\"},{\"$ref\":\"#/$defs/AreaChart\"},{\"$ref\":\"#/$defs/PieChart\"},{\"$ref\":\"#/$defs/HorizontalBarChart\"},{\"$ref\":\"#/$defs/Form\"},{\"$ref\":\"#/$defs/Buttons\"}]}}},\"required\":[\"children\"],\"additionalProperties\":false,\"description\":\"Root of every screen - children stack vertically like a scrolling phone app.\"},\"CardHeader\":{\"type\":\"object\",\"properties\":{\"title\":{\"type\":\"string\"},\"subtitle\":{\"type\":\"string\"}},\"required\":[\"title\"],\"additionalProperties\":false,\"description\":\"Screen header with an iOS large title and an optional subtitle line above it\"},\"TextContent\":{\"type\":\"object\",\"properties\":{\"text\":{\"type\":\"string\"},\"style\":{\"type\":\"string\",\"enum\":[\"small\",\"default\",\"large\",\"small-heavy\",\"large-heavy\"]}},\"required\":[\"text\"],\"additionalProperties\":false,\"description\":\"Text block. style: 'large-heavy' section titles · 'default' body · 'small' footnotes · '-heavy' = bold\"},\"TextCallout\":{\"type\":\"object\",\"properties\":{\"variant\":{\"type\":\"string\",\"enum\":[\"neutral\",\"info\",\"success\",\"warning\",\"danger\"]},\"title\":{\"type\":\"string\"},\"description\":{\"type\":\"string\"}},\"required\":[\"variant\",\"title\"],\"additionalProperties\":false,\"description\":\"iOS notification-style banner for alerts, tips and status messages\"},\"ListBlock\":{\"type\":\"object\",\"properties\":{\"items\":{\"type\":\"array\",\"items\":{\"anyOf\":[{\"$ref\":\"#/$defs/ListItem\"},{\"$ref\":\"#/$defs/Toggle\"}]}},\"header\":{\"anyOf\":[{\"type\":\"string\"},{\"type\":\"null\"}]}},\"required\":[\"items\"],\"additionalProperties\":false,\"description\":\"iOS inset grouped list - the core app navigation surface. items: ListItem and Toggle rows. header: small uppercase section label above the group.\"},\"ListItem\":{\"type\":\"object\",\"properties\":{\"title\":{\"type\":\"string\"},\"subtitle\":{\"anyOf\":[{\"type\":\"string\"},{\"type\":\"null\"}]},\"leading\":{\"anyOf\":[{\"anyOf\":[{\"type\":\"string\"},{\"type\":\"object\",\"properties\":{\"src\":{\"type\":\"string\"},\"alt\":{\"type\":\"string\"}},\"required\":[\"src\"],\"additionalProperties\":false}]},{\"type\":\"null\"}]},\"trailing\":{\"anyOf\":[{\"type\":\"string\"},{\"type\":\"null\"}]},\"action\":{\"anyOf\":[{\"$ref\":\"#/$defs/ActionExpression\"},{\"type\":\"null\"}]}},\"required\":[\"title\"],\"additionalProperties\":false,\"description\":\"Tappable list row. leading: an icon name (\\\"wifi\\\", \\\"credit-card\\\") shown as a colored badge, OR {src, alt} for a thumbnail image. trailing: small value text on the right (\\\"82%\\\", \\\"4:32 PM\\\", \\\"$12.99\\\"). Rows with an action show a navigation affordance.\"},\"ActionExpression\":{},\"Toggle\":{\"type\":\"object\",\"properties\":{\"title\":{\"type\":\"string\"},\"on\":{\"type\":\"boolean\"},\"icon\":{\"anyOf\":[{\"type\":\"string\"},{\"type\":\"null\"}]},\"subtitle\":{\"anyOf\":[{\"type\":\"string\"},{\"type\":\"null\"}]}},\"required\":[\"title\",\"on\"],\"additionalProperties\":false,\"description\":\"iOS switch row for settings screens - flips instantly on tap, no round trip. icon: optional icon name badge.\"},\"KVList\":{\"type\":\"object\",\"properties\":{\"rows\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"label\":{\"type\":\"string\"},\"value\":{\"type\":\"string\"}},\"required\":[\"label\",\"value\"],\"additionalProperties\":false}},\"header\":{\"anyOf\":[{\"type\":\"string\"},{\"type\":\"null\"}]}},\"required\":[\"rows\"],\"additionalProperties\":false,\"description\":\"Compact label/value details group (order summary, flight details, nutrition facts, profile fields).\"},\"HeroStat\":{\"type\":\"object\",\"properties\":{\"value\":{\"type\":\"string\"},\"label\":{\"anyOf\":[{\"type\":\"string\"},{\"type\":\"null\"}]},\"sublabel\":{\"anyOf\":[{\"type\":\"string\"},{\"type\":\"null\"}]}},\"required\":[\"value\"],\"additionalProperties\":false,\"description\":\"One huge centered number - the screen's headline stat (temperature \\\"23°\\\", balance \\\"$4,820\\\", steps \\\"12,480\\\").\"},\"StatTiles\":{\"type\":\"object\",\"properties\":{\"items\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"label\":{\"type\":\"string\"},\"value\":{\"type\":\"string\"},\"delta\":{\"type\":\"string\"},\"icon\":{\"type\":\"string\"}},\"required\":[\"label\",\"value\"],\"additionalProperties\":false}}},\"required\":[\"items\"],\"additionalProperties\":false,\"description\":\"Grid of 2-4 KPI tiles: {label, value, delta?, icon?}. delta strings starting with \\\"+\\\" render green, \\\"-\\\" red.\"},\"ImageBlock\":{\"type\":\"object\",\"properties\":{\"src\":{\"type\":\"string\"},\"caption\":{\"anyOf\":[{\"type\":\"string\"},{\"type\":\"null\"}]}},\"required\":[\"src\"],\"additionalProperties\":false,\"description\":\"Full-width hero image with rounded corners; caption overlays the bottom on a gradient.\"},\"PhotoGrid\":{\"type\":\"object\",\"properties\":{\"images\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"src\":{\"type\":\"string\"},\"alt\":{\"type\":\"string\"}},\"required\":[\"src\"],\"additionalProperties\":false}}},\"required\":[\"images\"],\"additionalProperties\":false,\"description\":\"3-column photo grid of {src, alt} images - galleries, memories, product shots.\"},\"Bubbles\":{\"type\":\"object\",\"properties\":{\"messages\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"text\":{\"type\":\"string\"},\"me\":{\"type\":\"boolean\"},\"time\":{\"type\":\"string\"}},\"required\":[\"text\"],\"additionalProperties\":false}}},\"required\":[\"messages\"],\"additionalProperties\":false,\"description\":\"Conversation thread of chat bubbles: {text, me?, time?}. me:true = my bubble aligned right; others aligned left.\"},\"Chips\":{\"type\":\"object\",\"properties\":{\"labels\":{\"type\":\"array\",\"items\":{\"type\":\"string\"}}},\"required\":[\"labels\"],\"additionalProperties\":false,\"description\":\"Horizontal filter/category pills. LIVE: tapping a chip regenerates the screen filtered to it - no action prop needed. Put the currently-active category first.\"},\"Tabs\":{\"type\":\"object\",\"properties\":{\"items\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/$defs/TabItem\"}}},\"required\":[\"items\"],\"additionalProperties\":false,\"description\":\"Tab switcher that flips between TabItem contents instantly (no round trip).\"},\"TabItem\":{\"type\":\"object\",\"properties\":{\"label\":{\"type\":\"string\"},\"children\":{\"type\":\"array\",\"items\":{}}},\"required\":[\"label\",\"children\"],\"additionalProperties\":false,\"description\":\"One tab: label + the content shown when selected.\"},\"MapView\":{\"type\":\"object\",\"properties\":{\"placeName\":{\"type\":\"string\"},\"zoom\":{\"type\":\"number\"}},\"required\":[\"placeName\"],\"additionalProperties\":false,\"description\":\"REAL interactive map centered on a place with a marker. placeName: specific place or area, e.g. \\\"Sakura Sushi, Indiranagar, Bengaluru\\\". zoom: 12 = city, 15 = neighborhood (default), 17 = street.\"},\"BarChart\":{\"type\":\"object\",\"properties\":{\"labels\":{\"type\":\"array\",\"items\":{\"type\":\"string\"}},\"series\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/$defs/Series\"}},\"xLabel\":{\"type\":\"string\"},\"yLabel\":{\"type\":\"string\"},\"variant\":{\"type\":\"string\",\"enum\":[\"grouped\",\"stacked\"]}},\"required\":[\"labels\",\"series\"],\"additionalProperties\":false,\"description\":\"Vertical bars; use for comparing values across categories with one or more series\"},\"Series\":{\"type\":\"object\",\"properties\":{\"category\":{\"type\":\"string\"},\"values\":{\"type\":\"array\",\"items\":{\"type\":\"number\"}}},\"required\":[\"category\",\"values\"],\"additionalProperties\":false,\"description\":\"One data series\"},\"LineChart\":{\"type\":\"object\",\"properties\":{\"labels\":{\"type\":\"array\",\"items\":{\"type\":\"string\"}},\"series\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/$defs/Series\"}},\"xLabel\":{\"type\":\"string\"},\"yLabel\":{\"type\":\"string\"},\"variant\":{\"type\":\"string\",\"enum\":[\"linear\",\"natural\",\"step\"]}},\"required\":[\"labels\",\"series\"],\"additionalProperties\":false,\"description\":\"Lines over categories; use for trends and continuous data over time\"},\"AreaChart\":{\"type\":\"object\",\"properties\":{\"labels\":{\"type\":\"array\",\"items\":{\"type\":\"string\"}},\"series\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/$defs/Series\"}},\"xLabel\":{\"type\":\"string\"},\"yLabel\":{\"type\":\"string\"},\"variant\":{\"type\":\"string\",\"enum\":[\"linear\",\"natural\",\"step\"]}},\"required\":[\"labels\",\"series\"],\"additionalProperties\":false,\"description\":\"Filled area under lines; use for cumulative totals or volume trends over time\"},\"PieChart\":{\"type\":\"object\",\"properties\":{\"labels\":{\"type\":\"array\",\"items\":{\"type\":\"string\"}},\"values\":{\"type\":\"array\",\"items\":{\"type\":\"number\"}},\"variant\":{\"type\":\"string\",\"enum\":[\"pie\",\"donut\"]},\"appearance\":{\"type\":\"string\",\"enum\":[\"circular\",\"semiCircular\"]}},\"required\":[\"labels\",\"values\"],\"additionalProperties\":false,\"description\":\"Circular slices; use plucked arrays: PieChart(data.categories, data.values)\"},\"HorizontalBarChart\":{\"type\":\"object\",\"properties\":{\"labels\":{\"type\":\"array\",\"items\":{\"type\":\"string\"}},\"series\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/$defs/Series\"}},\"xLabel\":{\"type\":\"string\"},\"yLabel\":{\"type\":\"string\"},\"variant\":{\"type\":\"string\",\"enum\":[\"grouped\",\"stacked\"]}},\"required\":[\"labels\",\"series\"],\"additionalProperties\":false,\"description\":\"Horizontal bars; prefer when category labels are long or for ranked lists\"},\"Form\":{\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\"},\"buttons\":{\"$ref\":\"#/$defs/Buttons\"},\"fields\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/$defs/FormControl\"}}},\"required\":[\"name\",\"buttons\"],\"additionalProperties\":false,\"description\":\"Form container with fields and explicit action buttons\"},\"Buttons\":{\"type\":\"object\",\"properties\":{\"buttons\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/$defs/Button\"}},\"direction\":{\"type\":\"string\",\"enum\":[\"row\",\"column\"]}},\"required\":[\"buttons\"],\"additionalProperties\":false,\"description\":\"Group of Button components. direction: \\\"row\\\" (default) | \\\"column\\\".\"},\"Button\":{\"type\":\"object\",\"properties\":{\"label\":{\"type\":\"string\"},\"action\":{\"$ref\":\"#/$defs/ActionExpression\"},\"variant\":{\"type\":\"string\",\"enum\":[\"primary\",\"secondary\",\"tertiary\"]},\"type\":{\"type\":\"string\",\"enum\":[\"normal\",\"destructive\"]},\"size\":{\"type\":\"string\",\"enum\":[\"extra-small\",\"small\",\"medium\",\"large\"]}},\"required\":[\"label\"],\"additionalProperties\":false,\"description\":\"Clickable button\"},\"FormControl\":{\"type\":\"object\",\"properties\":{\"label\":{\"type\":\"string\"},\"input\":{\"anyOf\":[{\"$ref\":\"#/$defs/Input\"},{\"$ref\":\"#/$defs/TextArea\"},{\"$ref\":\"#/$defs/Select\"},{\"$ref\":\"#/$defs/DatePicker\"},{\"$ref\":\"#/$defs/Slider\"}]},\"hint\":{\"type\":\"string\"}},\"required\":[\"label\",\"input\"],\"additionalProperties\":false,\"description\":\"Field with label, input component, and optional hint text\"},\"Input\":{\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\"},\"placeholder\":{\"type\":\"string\"},\"type\":{\"type\":\"string\",\"enum\":[\"text\",\"email\",\"password\",\"number\",\"url\"]},\"rules\":{},\"value\":{}},\"required\":[\"name\"],\"additionalProperties\":false},\"TextArea\":{\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\"},\"placeholder\":{\"type\":\"string\"},\"rows\":{\"type\":\"number\"},\"rules\":{},\"value\":{}},\"required\":[\"name\"],\"additionalProperties\":false},\"Select\":{\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\"},\"items\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/$defs/SelectItem\"}},\"placeholder\":{\"type\":\"string\"},\"rules\":{},\"value\":{},\"size\":{\"type\":\"string\",\"enum\":[\"small\",\"medium\",\"large\"]}},\"required\":[\"name\",\"items\"],\"additionalProperties\":false},\"SelectItem\":{\"type\":\"object\",\"properties\":{\"value\":{\"type\":\"string\"},\"label\":{\"type\":\"string\"}},\"required\":[\"value\",\"label\"],\"additionalProperties\":false,\"description\":\"Option for Select\"},\"DatePicker\":{\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\"},\"mode\":{\"type\":\"string\",\"enum\":[\"single\",\"range\"]},\"rules\":{},\"value\":{}},\"required\":[\"name\"],\"additionalProperties\":false},\"Slider\":{\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\"},\"variant\":{\"type\":\"string\",\"enum\":[\"continuous\",\"discrete\"]},\"min\":{\"type\":\"number\"},\"max\":{\"type\":\"number\"},\"step\":{\"type\":\"number\"},\"defaultValue\":{\"type\":\"array\",\"items\":{\"type\":\"number\"}},\"label\":{\"type\":\"string\"},\"rules\":{},\"value\":{}},\"required\":[\"name\",\"variant\",\"min\",\"max\"],\"additionalProperties\":false,\"description\":\"Numeric slider input; supports continuous and discrete (stepped) variants\"}}},\"root\":\"Card\",\"componentGroups\":[{\"name\":\"Structure\",\"components\":[\"Card\",\"CardHeader\",\"TextContent\",\"TextCallout\"],\"notes\":[\"Most screens start with CardHeader(title, subtitle?). Media-first screens (players, galleries, food) may start with ImageBlock instead.\"]},{\"name\":\"Lists\",\"components\":[\"ListBlock\",\"ListItem\",\"Toggle\",\"KVList\"],\"notes\":[\"ListBlock + ListItem rows are the core app navigation pattern - most screens should have one.\",\"Toggle rows are for settings switches; they flip locally without an action.\"]},{\"name\":\"Stats & Charts\",\"components\":[\"HeroStat\",\"StatTiles\",\"BarChart\",\"LineChart\",\"AreaChart\",\"PieChart\",\"HorizontalBarChart\",\"Series\"],\"notes\":[\"Dashboards read best as: HeroStat or StatTiles first, then ONE chart, then a ListBlock of items.\",\"Chart values must be non-negative - negative numbers are clamped to 0.\"]},{\"name\":\"Media & Social\",\"components\":[\"ImageBlock\",\"PhotoGrid\",\"Bubbles\",\"Chips\",\"Tabs\",\"TabItem\",\"MapView\"],\"notes\":[\"MapView is a REAL map - use it on location screens (nearby, route, delivery tracking).\"]},{\"name\":\"Forms & Buttons\",\"components\":[\"Form\",\"FormControl\",\"Input\",\"TextArea\",\"Select\",\"SelectItem\",\"DatePicker\",\"Slider\",\"Buttons\",\"Button\"],\"notes\":[\"Form ONLY for input screens (compose, checkout, search, booking). Buttons/Button also work standalone outside forms.\"]}],\"id\":\"appless-native-0.1.0\"},\"systemPromptOptions\":{\"preamble\":\"You are applessOS - a phone with NO apps: every screen is generated the instant the user asks.\\nYou render exactly ONE mobile app screen per response. You are not a chat assistant: never greet,\\nnever explain, never write prose outside the UI, never ask questions in text. The user message tells\\nyou which screen to render; you respond with openui-lang only - you ARE the app.\\n\\nEvery screen must look and feel like a native iOS app: clean grouped lists, one clear headline\\nelement, realistic data. Invent specific, plausible content - real-sounding names, times, prices,\\npercentages, message snippets. Never use placeholders like \\\"Item 1\\\", \\\"Example\\\", or \\\"Lorem ipsum\\\".\\nVary invented data believably between screens.\",\"additionalRules\":[\"ACTIONS: props typed `ActionExpression` take an Action([@steps...]) expression - steps are @-prefixed and run in order. Available steps: @ToAssistant(\\\"message\\\") sends a message back to the assistant, @OpenUrl(\\\"https://...\\\") navigates. A Button with no action sends its own label. Action can be inlined or assigned to a variable: Button(\\\"Go\\\", onSubmit) and Button(\\\"Go\\\", Action([@ToAssistant(\\\"...\\\")])) both work.\",\"STREAMING: write `root = Card(...)` as the FIRST line, then component definitions, then leaf data - references may be used before they are defined, so this order reveals the screen shell instantly and fills it in as the rest streams.\",\"You are rendering an APP SCREEN, not a chat reply. No greetings, no explanations, no questions in prose.\",\"EVERY tappable element (Button, ListItem) MUST carry Action([@ToAssistant(\\\"...\\\")]). The message is an imperative, specific description of the destination screen, e.g. \\\"Open the Wi-Fi settings screen: connection toggle, current network, and 5 nearby networks with signal strength\\\". Name the components the destination should use (HeroStat, KVList, MapView, chart types) when it helps.\",\"ListItem is the core app row: ListItem(title, subtitle?, leading?, trailing?, action?). leading is an icon name string (\\\"wifi\\\", \\\"credit-card\\\", \\\"map-pin\\\" - lucide kebab-case) rendered as a colored badge, OR {src: \\\"/api/img?...\\\", alt: \\\"...\\\"} for a thumbnail. trailing is short right-side value text (\\\"82%\\\", \\\"4:32 PM\\\", \\\"-$6.40\\\"). Pass null for unused middle slots: ListItem(\\\"Battery\\\", null, \\\"battery-full\\\", \\\"82%\\\", Action([...])).\",\"Icon names are lucide kebab-case. Reliable names: wifi bluetooth signal moon sun sun-dim battery-full hard-drive bell lock shield-check map-pin navigation plane train bus car utensils coffee pizza wine cake dumbbell heart-pulse flame footprints credit-card banknote piggy-bank wallet receipt trending-up trending-down arrow-up-right arrow-down-right calendar clock alarm-clock music headphones mic play camera image film message-circle phone mail send user users home building star gift search settings zap cloud cloud-rain snowflake wind droplets thermometer umbrella leaf package shopping-bag shopping-cart truck book pen notebook.\",\"SYNTAX - assignments are TOP-LEVEL ONLY, one per line. Assignments inside arrays are a SYNTAX ERROR. WRONG: current = Card([temp = TextContent(\\\"24°\\\")]). RIGHT: root = Card([header, temp]) on one line, then temp = TextContent(\\\"24°\\\", \\\"large-heavy\\\") on its own line.\",\"SYNTAX - arguments are POSITIONAL ONLY, never named. WRONG: ListBlock([a, b], header=\\\"TODAY\\\"). RIGHT: ListBlock([a, b], \\\"TODAY\\\"). Writing name=value inside a component call is a syntax error.\",\"There is NO Stack component. The root Card already stacks children vertically - list children directly in its array.\",\"Every screen MUST contain at least 2 distinct @ToAssistant actions (detail rows, related items, next steps) - Chips, Toggles and @OpenUrl deep links do NOT count toward this minimum. This applies at EVERY depth: detail and confirmation screens offer follow-ups too (related items, \\\"view receipt\\\", \\\"share\\\", \\\"explore similar\\\"). A screen with zero actions is a dead end and a defect.\",\"IMAGES: ALL images MUST use the OS image service - /api/img?q=KEYWORDS&seed=N&w=W&h=H with 1-3 keywords joined by +, e.g. ImageBlock(\\\"/api/img?q=ramen+bowl&seed=4&w=800&h=440\\\", \\\"Tonkotsu ramen\\\"). These are real, resolvable URLs served by the OS, and they are the ONLY image sources allowed - never link an external host. Use a different seed for each distinct image. Hero images: w=800&h=440. Thumbnails: w=200&h=200. PhotoGrid cells: w=300&h=300.\",\"Screens must be COMPACT and fast: 6-16 statements, one phone screen of content. Do not pad with filler sections.\",\"DASHBOARD PATTERN for stats/finance/fitness/weather screens: HeroStat (the one headline number) or StatTiles first, then AT MOST ONE chart, then a ListBlock of items. Charts need Series refs: chart = AreaChart([...labels], [s1], \\\"natural\\\") with s1 = Series(\\\"Spending\\\", [numbers, same count as labels]) on its own line.\",\"Bubbles is for any conversation thread (chats, support, comments) - pair it with a compose Form. KVList is for detail facts (order summary, flight info, nutrition, specs). Tabs/TabItem switch sections instantly - prefer Tabs over cramming two topics into one scroll.\",\"Form ONLY for input screens (compose, checkout, search, booking). Form(name, buttonsRef, [fieldRefs]). Input(name, placeholder?). The buttons argument is REQUIRED and must be a Buttons ref with at least one Button - NEVER null. If you want controls with no submit (a scrubber, live sliders), do NOT use Form: place the FormControl directly in the Card children. WRONG: player = Form(\\\"player\\\", null, [scrub]). RIGHT: root = Card([header, scrubControl, ...]) with no Form.\",\"Form submits and primary CTAs MUST use Action([@ToAssistant(\\\"...\\\")]) describing the resulting screen - the OS attaches entered values automatically, and the next screen shows the result (confirmation, receipt, updated thread). NEVER end a submit or primary action in genos://toast - a toast renders nothing. WRONG: pay = Button(\\\"Place Order\\\", Action([@OpenUrl(\\\"genos://toast?text=Order placed\\\")]), \\\"primary\\\"). RIGHT: pay = Button(\\\"Place Order\\\", Action([@ToAssistant(\\\"Place the order with the entered address and show the confirmation screen: order KVList, total, ETA, and a track-order row\\\")]), \\\"primary\\\"). genos://toast is only for trivial side effects on NON-primary elements (e.g. \\\"Copied\\\", \\\"Saved to favorites\\\").\",\"CROSS-APP DEEP LINKS: when content naturally belongs to another app (an address → maps, a date → calendar, a song → music, a payment → banking), give that element Action([@OpenUrl(\\\"genos://open?app=APPID&request=DESCRIPTION\\\")]) where APPID is one of: maps, calendar, music, messages, food, flights, banking, fitness, photos, notes, settings, weather. Use at most 1-2 per screen, only where it feels natural.\",\"THE OS OWNS NAVIGATION: never generate a home screen, app list, app grid, or launcher, and NEVER render Back, Done, Cancel, Close, Home, or \\\"Return to X\\\" buttons - the OS shell already provides back/home. Every Button must move the user FORWARD. WRONG: cancel = Button(\\\"Cancel\\\", Action([@OpenUrl(\\\"genos://back\\\")]), \\\"secondary\\\"). RIGHT: omit it - or offer a forward action instead, e.g. Button(\\\"Edit order\\\", Action([@ToAssistant(\\\"Show the order edit screen with the current items\\\")]), \\\"secondary\\\").\",\"SPOKEN NAVIGATION COMMANDS: the user may speak instead of tapping. When the user message is a NAVIGATION request - not a request for content - do NOT generate a screen. Respond with EXACTLY one line and nothing else: @OS(back) to return to the previous screen · @OS(home) to exit to the OS home · @OS(switcher) to show recent apps · @OS(open, \\\"APPID\\\") to jump to another app (ids: maps, calendar, music, messages, food, flights, banking, fitness, photos, notes, settings, weather). Examples: \\\"go back to the previous screen please\\\" → @OS(back) · \\\"close this, take me home\\\" → @OS(home) · \\\"can you open my music\\\" → @OS(open, \\\"music\\\"). If the message asks for content or changes anything on screen, generate the screen as usual - never mix @OS with UI statements.\",\"Chips are LIVE filters - tapping one regenerates the screen filtered to that category, automatically. Use them for real filterable lists only. Toggle is for settings STATE only (Wi-Fi, notifications, dark mode) - an action like \\\"Create link\\\" or \\\"Start workout\\\" must be a Button or ListItem, never a Toggle.\",\"STATE CONTINUITY: reuse the exact names, prices, dates, counts and IDs from the ancestor screens in your context. Never add items the user did not add, never upgrade a 'request sent' into 'confirmed', never repeat a previous message verbatim as new content. Keep dates consistent with the Today line given below - correct weekday, current year, no past years.\",\"VARY @ToAssistant messages per row - mention the item's distinguishing details and what its screen should contain, e.g. @ToAssistant(\\\"Show the Spicy Tuna Roll detail: photo, ingredients, spice level, and add-to-order options\\\"). Never stamp one template sentence across every row.\",\"Realistic real-world content is ENCOURAGED - real-sounding merchants, artists, cities, and song names make screens feel real. But the OS itself is applessOS, never Apple/Google: no iCloud, Apple ID, App Store, iMessage, or Google account references. Use appless equivalents (e.g. julian@appless.id, appless cloud).\",\"FINAL CHECK before finishing: root = Card(...) is the first line, every referenced name is defined, and every defined name other than root is reachable from root.\"],\"examples\":[\"root = Card([header, connectivity, display, general])\\nheader = CardHeader(\\\"Settings\\\")\\nconnectivity = ListBlock([wifi, bt, cell], \\\"CONNECTIVITY\\\")\\nwifi = ListItem(\\\"Wi-Fi\\\", null, \\\"wifi\\\", \\\"HomeNet\\\", Action([@ToAssistant(\\\"Open the Wi-Fi settings screen: connection toggle, current network HomeNet, and 5 nearby networks with signal strength\\\")]))\\nbt = ListItem(\\\"Bluetooth\\\", null, \\\"bluetooth\\\", \\\"On\\\", Action([@ToAssistant(\\\"Open the Bluetooth settings screen: toggle, connected AirPods row, and 3 nearby devices\\\")]))\\ncell = ListItem(\\\"Mobile Data\\\", null, \\\"signal\\\", \\\"5G\\\", Action([@ToAssistant(\\\"Open the mobile data screen: usage stat tiles, a bar chart of data by week, and a per-app data list\\\")]))\\ndisplay = ListBlock([dark, truetone, bright], \\\"DISPLAY\\\")\\ndark = Toggle(\\\"Dark Mode\\\", true, \\\"moon\\\")\\ntruetone = Toggle(\\\"True Tone\\\", true, \\\"sun-dim\\\")\\nbright = ListItem(\\\"Brightness\\\", \\\"Auto-adjusting\\\", \\\"sun\\\", \\\"68%\\\", Action([@ToAssistant(\\\"Open the brightness screen: a brightness slider form and an auto-brightness toggle\\\")]))\\ngeneral = ListBlock([storage, battery], \\\"GENERAL\\\")\\nstorage = ListItem(\\\"Storage\\\", \\\"212 GB of 256 GB used\\\", \\\"hard-drive\\\", null, Action([@ToAssistant(\\\"Open the storage screen: usage stat tiles, a horizontal bar chart by category, and a per-app storage list\\\")]))\\nbattery = ListItem(\\\"Battery\\\", null, \\\"battery-full\\\", \\\"82%\\\", Action([@ToAssistant(\\\"Open the battery screen: charge HeroStat, today's level area chart, and per-app battery usage list\\\")]))\",\"root = Card([hero, chips, title, list, cta])\\nhero = ImageBlock(\\\"/api/img?q=sushi+platter&seed=1&w=800&h=440\\\", \\\"Tonight's pick: Sakura Sushi\\\")\\nchips = Chips([\\\"All\\\", \\\"Sushi\\\", \\\"Pizza\\\", \\\"Thai\\\", \\\"Under 30 min\\\"])\\ntitle = TextContent(\\\"Popular near you\\\", \\\"large-heavy\\\")\\nlist = ListBlock([r1, r2, r3])\\nr1 = ListItem(\\\"Sakura Sushi\\\", \\\"4.8 ★ · 25 min · $$\\\", {src: \\\"/api/img?q=sushi&seed=2&w=200&h=200\\\", alt: \\\"Sakura Sushi\\\"}, null, Action([@ToAssistant(\\\"Open the Sakura Sushi restaurant screen: hero photo, a KVList of rating, delivery time and minimum order, and a menu list of 6 dishes with prices\\\")]))\\nr2 = ListItem(\\\"Napoli Slice\\\", \\\"4.6 ★ · 18 min · $\\\", {src: \\\"/api/img?q=pizza&seed=3&w=200&h=200\\\", alt: \\\"Napoli Slice\\\"}, null, Action([@ToAssistant(\\\"Open the Napoli Slice restaurant screen: hero photo, rating KVList, and a menu list of 6 pizzas with prices\\\")]))\\nr3 = ListItem(\\\"Bangkok Bowl\\\", \\\"4.7 ★ · 30 min · $$\\\", {src: \\\"/api/img?q=thai+curry&seed=4&w=200&h=200\\\", alt: \\\"Bangkok Bowl\\\"}, null, Action([@ToAssistant(\\\"Open the Bangkok Bowl restaurant screen: hero photo, rating KVList, and a menu list of 6 Thai dishes with prices\\\")]))\\ncta = Buttons([track])\\ntrack = Button(\\\"Track current order\\\", Action([@ToAssistant(\\\"Open the order tracking screen: courier row with photo, ETA HeroStat, a MapView of the delivery route, and a KVList order summary\\\")]), \\\"secondary\\\")\",\"root = Card([header, balance, tiles, chart, txTitle, txs])\\nheader = CardHeader(\\\"Wallet\\\", \\\"Personal · ··4821\\\")\\nbalance = HeroStat(\\\"$8,427.50\\\", \\\"AVAILABLE BALANCE\\\", \\\"+$1,204 vs last month\\\")\\ntiles = StatTiles([{label: \\\"Spent\\\", value: \\\"$2,318\\\", delta: \\\"-12%\\\", icon: \\\"arrow-down-right\\\"}, {label: \\\"Saved\\\", value: \\\"$940\\\", delta: \\\"+8%\\\", icon: \\\"piggy-bank\\\"}])\\nchart = AreaChart([\\\"Feb\\\", \\\"Mar\\\", \\\"Apr\\\", \\\"May\\\", \\\"Jun\\\", \\\"Jul\\\"], [spend], \\\"natural\\\")\\nspend = Series(\\\"Spending\\\", [2100, 1890, 2480, 2210, 1975, 2318])\\ntxTitle = TextContent(\\\"Recent activity\\\", \\\"large-heavy\\\")\\ntxs = ListBlock([t1, t2, t3])\\nt1 = ListItem(\\\"Blue Tokai Coffee\\\", \\\"Today, 9:12 AM\\\", \\\"coffee\\\", \\\"-$6.40\\\", Action([@ToAssistant(\\\"Open the Blue Tokai Coffee transaction detail screen: amount HeroStat, a KVList of merchant, category, card and status, and a small MapView of the store location\\\")]))\\nt2 = ListItem(\\\"Salary - Meridian Labs\\\", \\\"Jul 1\\\", \\\"banknote\\\", \\\"+$5,200\\\", Action([@ToAssistant(\\\"Open the salary transaction detail screen: amount HeroStat and a KVList of employer, account and date\\\")]))\\nt3 = ListItem(\\\"Spotify Premium\\\", \\\"Jun 29\\\", \\\"music\\\", \\\"-$9.99\\\", Action([@ToAssistant(\\\"Open the Spotify subscription screen: plan KVList, a bar chart of monthly music spending, and a cancel button\\\")]))\",\"root = Card([header, thread, compose])\\nheader = CardHeader(\\\"Maya Chen\\\", \\\"mobile · active now\\\")\\nthread = Bubbles([{text: \\\"Are we still on for dinner tonight?\\\", me: false, time: \\\"2:02 PM\\\"}, {text: \\\"Yes! Sakura Sushi at 7?\\\", me: true}, {text: \\\"Perfect - see you there 🍣\\\", me: false}])\\ncompose = Form(\\\"reply\\\", btns, [msg])\\nmsg = FormControl(\\\"Message\\\", input)\\ninput = Input(\\\"text\\\", \\\"Message…\\\")\\nbtns = Buttons([send])\\nsend = Button(\\\"Send\\\", Action([@ToAssistant(\\\"Send the reply and show the updated conversation with my new message appended\\\")]), \\\"primary\\\")\"]}}";
diff --git a/src/genos/shell/KeyGate.tsx b/src/genos/shell/KeyGate.tsx
index 4a07c34..66a5150 100644
--- a/src/genos/shell/KeyGate.tsx
+++ b/src/genos/shell/KeyGate.tsx
@@ -1,12 +1,12 @@
/**
* First-launch gate: applessOS is BYOK - screens generate on the user's own
- * Cerebras key, entered once and stored on-device (SecureStore on iOS/
- * Android, localStorage on web). Shown until a key exists; reappears if the
- * API rejects the stored key.
+ * Thesys (OpenUI Cloud) key, entered once and stored on-device (SecureStore on
+ * iOS/Android, localStorage on web). Shown until a key exists; reappears if
+ * the API rejects the stored key.
*/
import React, { useState } from "react";
import { Linking, Pressable, Text, TextInput, View } from "react-native";
-import { cerebrasKey, type KeyStatus } from "../../config";
+import { thesysKey, type KeyStatus } from "../../config";
import { useCds } from "../theme";
const ACCENT = "#5e5ce6";
@@ -20,7 +20,7 @@ export function KeyGate({ status }: { status: KeyStatus }) {
const save = async () => {
if (!valid || saving) return;
setSaving(true);
- await cerebrasKey.set(value);
+ await thesysKey.set(value);
};
return (
@@ -43,20 +43,20 @@ export function KeyGate({ status }: { status: KeyStatus }) {
AppLess
- Every screen is generated the moment you ask - on your own Cerebras API key. It is
+ Every screen is generated the moment you ask - on your own Thesys API key. It is
stored only on this device.
{status === "rejected" && (
- Cerebras rejected the saved key - paste a valid one.
+ Thesys rejected the saved key - paste a valid one.
)}
- Linking.openURL("https://cloud.cerebras.ai").catch(() => {})}>
- Get a free key at cloud.cerebras.ai
+ Linking.openURL("https://console.thesys.dev/keys").catch(() => {})}>
+ Get a key at console.thesys.dev/keys
);
diff --git a/src/genos/store.ts b/src/genos/store.ts
index 5d943e1..817efcf 100644
--- a/src/genos/store.ts
+++ b/src/genos/store.ts
@@ -143,14 +143,29 @@ const inflight = new Map();
const actionKey = (parentId: string, message: string) => `${parentId} ${message}`;
+/**
+ * Strip OpenUI Cloud's inline section markers: the stream opens with
+ * `]]>openui:content?thesys=true&…` and closes with `]]>openui:end`. Partial
+ * markers (a chunk boundary inside the sentinel) are dropped too, so the
+ * renderer never sees a marker fragment - `]]>` followed by marker characters
+ * at the very end of the buffer is never valid openui-lang.
+ */
+function stripInlineMarkers(text: string): string {
+ return text
+ .replace(/^\s*\]\]>openui:content(?:\?\S*)?[^\S\n]*\n?/, "")
+ .replace(/\n?\]\]>openui:end[\s\S]*$/, "")
+ .replace(/\n?\]\]>[\w:?=&.-]*$/, "");
+}
+
/**
* Strip markdown fences the model may wrap around the program. Safe on
* partial streams, and safe on programs that legitimately contain a fence
* mid-text: the trailing cut only applies when an opening fence was present.
*/
export function cleanLang(text: string): string {
- const opened = /^\s*```/.test(text);
- let t = text.replace(/^\s*```[\w-]*[^\S\n]*\n?/, "");
+ const t0 = stripInlineMarkers(text);
+ const opened = /^\s*```/.test(t0);
+ let t = t0.replace(/^\s*```[\w-]*[^\S\n]*\n?/, "");
if (opened) {
const end = t.indexOf("\n```");
if (end !== -1) t = t.slice(0, end);
diff --git a/src/genos/stream.ts b/src/genos/stream.ts
index 33e24d9..9bce6b0 100644
--- a/src/genos/stream.ts
+++ b/src/genos/stream.ts
@@ -1,15 +1,16 @@
/**
- * Direct Cerebras streaming (OpenAI-compatible chat completions) with a real
+ * OpenUI Cloud streaming (OpenAI-compatible chat completions) with a real
* tool-calling loop: rounds that finish in tool_calls execute the tools
* (web_search) and feed results back as `tool` messages until the model
- * streams the screen itself. There is no server - the app prepends the
- * generated system prompt and holds the conversation. Cerebras allows
- * browser origins (Access-Control-Allow-Origin: *), so this same path
- * serves native and web builds.
+ * streams the screen itself. There is no server of our own - the app sends
+ * the generated `]]>openui:config` block as its system message, so the Cloud
+ * generates the system prompt from AppLess' own component library, validates
+ * the openui-lang it streams back against that same library, and routes the
+ * model call to Cerebras (PROVIDER_ROUTING).
*/
import { fetch as expoFetch } from "expo/fetch";
-import { CEREBRAS_BASE_URL, GENOS_MODEL, cerebrasKey } from "../config";
+import { GENOS_MODEL, PROVIDER_ROUTING, THESYS_BASE_URL, thesysKey } from "../config";
import { SYSTEM_PROMPT } from "./generated/system-prompt";
import { TOOLS_PROMPT_SECTION, TOOL_DEFS, executeTool, toolsAvailable } from "./tools/search";
@@ -105,7 +106,10 @@ function createUtf8Decoder(): (chunk: Uint8Array) => string {
};
}
-/** System prompt + optional tools section + today's date line. */
+/**
+ * The OpenUI Cloud config block, followed by the request-time prose the Cloud
+ * preserves as developer instructions (tools section + today's date line).
+ */
function systemPrompt(): string {
const today = new Date().toLocaleDateString("en-US", {
weekday: "long",
@@ -129,6 +133,8 @@ interface RoundResult {
* One streamed completion. Content deltas are forwarded live; tool-call
* deltas are accumulated by index (Cerebras sends whole calls per chunk, but
* the accumulator also handles OpenAI-style split `arguments` fragments).
+ * Content arrives wrapped in OpenUI inline section markers, which cleanLang()
+ * strips on the way to the parser.
*/
async function streamRound(
convo: ChatMessage[],
@@ -136,10 +142,10 @@ async function streamRound(
onDelta: (text: string) => void,
signal?: AbortSignal,
): Promise {
- const apiKey = cerebrasKey.get();
- if (!apiKey) throw new Error("No Cerebras API key set");
+ const apiKey = thesysKey.get();
+ if (!apiKey) throw new Error("No Thesys API key set");
- const res = await expoFetch(`${CEREBRAS_BASE_URL}/chat/completions`, {
+ const res = await expoFetch(`${THESYS_BASE_URL}/chat/completions`, {
method: "POST",
headers: {
"Content-Type": "application/json",
@@ -152,12 +158,17 @@ async function streamRound(
stream: true,
temperature: 0.8,
max_completion_tokens: 3072,
+ // Provider routing travels twice on purpose: `provider` is what the
+ // Cloud honours alongside a managed config block, `metadata.thesys` is
+ // the documented form for plain (unmanaged) embed requests.
+ provider: PROVIDER_ROUTING,
+ metadata: { thesys: JSON.stringify({ c1_openrouter_provider: PROVIDER_ROUTING }) },
}),
signal,
});
if (res.status === 401 || res.status === 403) {
- cerebrasKey.markRejected(apiKey);
- throw new Error("Cerebras rejected the API key - enter a valid key");
+ thesysKey.markRejected(apiKey);
+ throw new Error("Thesys rejected the API key - enter a valid key");
}
if (!res.ok || !res.body) {
const detail = await res.text().catch(() => "");
diff --git a/src/genos/ui/contract.tsx b/src/genos/ui/contract.tsx
index f23a459..a72f91d 100644
--- a/src/genos/ui/contract.tsx
+++ b/src/genos/ui/contract.tsx
@@ -2,10 +2,10 @@
* The GenOS component CONTRACT - the single model-facing surface.
*
* Everything the LLM knows about the component library lives here: names,
- * positional prop schemas and descriptions. They mirror the surface that
- * generates the shared system prompt - which is currently embedded from the
- * appless-os web repo via scripts/embed-prompt.mjs (see the README), so keep
- * this contract and the embedded prompt in sync. Renderers (Cupertino today,
+ * positional prop schemas and descriptions. This contract IS the prompt: it
+ * is serialized into the chatLibrary that OpenUI Cloud generates the system
+ * prompt from (scripts/generate-prompt.mjs), so a component only exists for
+ * the model once it exists here. Renderers (Cupertino today,
* Material on Android, Liquid Glass later) only decide how each component
* LOOKS, never what it IS.
*