oomi-ai 0.2.17 → 0.2.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +237 -202
  2. package/agent_instructions.md +209 -186
  3. package/bin/oomi-ai.js +3989 -3460
  4. package/bin/sessionBridgeState.js +78 -78
  5. package/lib/channelPluginClient.js +119 -0
  6. package/lib/personaApiClient.js +221 -0
  7. package/lib/personaJobExecutor.js +115 -0
  8. package/lib/personaJobPoller.js +112 -0
  9. package/lib/personaRuntimeProcess.js +152 -0
  10. package/lib/scaffold.js +108 -0
  11. package/lib/template.js +45 -0
  12. package/openclaw.extension.js +602 -602
  13. package/openclaw.plugin.json +17 -17
  14. package/package.json +67 -65
  15. package/skills/oomi/SKILL.md +191 -191
  16. package/skills/oomi/agent_instructions.md +80 -80
  17. package/skills/oomi/config.json +2 -2
  18. package/skills/oomi/scripts/get_avatar_capabilities.py +40 -40
  19. package/skills/oomi/scripts/get_data.py +49 -49
  20. package/skills/oomi/scripts/install_agent_instructions.py +78 -78
  21. package/skills/oomi/scripts/send_goal.py +53 -53
  22. package/skills/oomi/scripts/sync.py +46 -46
  23. package/skills/oomi/setup.py +41 -41
  24. package/templates/persona-app/.env.example +8 -0
  25. package/templates/persona-app/README.md +35 -0
  26. package/templates/persona-app/eslint.config.js +28 -0
  27. package/templates/persona-app/index.html +18 -0
  28. package/templates/persona-app/oomi.runtime.json +13 -0
  29. package/templates/persona-app/package.json +42 -0
  30. package/templates/persona-app/persona/brief.md +14 -0
  31. package/templates/persona-app/persona.json +14 -0
  32. package/templates/persona-app/public/manifest.webmanifest +8 -0
  33. package/templates/persona-app/public/oomi.health.json +6 -0
  34. package/templates/persona-app/src/App.css +180 -0
  35. package/templates/persona-app/src/App.tsx +14 -0
  36. package/templates/persona-app/src/index.css +32 -0
  37. package/templates/persona-app/src/main.tsx +10 -0
  38. package/templates/persona-app/src/pages/HomePage.tsx +73 -0
  39. package/templates/persona-app/src/pages/ScenePage.tsx +18 -0
  40. package/templates/persona-app/src/persona/config.ts +6 -0
  41. package/templates/persona-app/src/persona/notes.ts +5 -0
  42. package/templates/persona-app/src/vite-env.d.ts +3 -0
  43. package/templates/persona-app/template.json +13 -0
  44. package/templates/persona-app/tsconfig.app.json +23 -0
  45. package/templates/persona-app/tsconfig.json +7 -0
  46. package/templates/persona-app/tsconfig.node.json +21 -0
  47. package/templates/persona-app/vite.config.ts +18 -0
@@ -1,46 +1,46 @@
1
- import urllib.request
2
- import urllib.error
3
- import json
4
- import os
5
- import sys
6
-
7
- # Load config
8
- try:
9
- config_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'config.json')
10
- with open(config_path, 'r') as f:
11
- config = json.load(f)
12
- BASE_URL = config.get('api_url', 'http://localhost:3000/api/skill')
13
- except:
14
- BASE_URL = 'http://localhost:3000/api/skill'
15
-
16
- API_URL = f"{BASE_URL}/sync"
17
-
18
- def sync_context():
19
- # In a real scenario, this might read from stdin or a file provided by the agent
20
- # For now, we send a dummy context
21
- payload = {
22
- "agent_id": "nemu-agent",
23
- "context_summary": "User is actively working on coding tasks.",
24
- "suggested_mode": "working"
25
- }
26
-
27
- try:
28
- req = urllib.request.Request(
29
- API_URL,
30
- data=json.dumps(payload).encode('utf-8'),
31
- headers={'Content-Type': 'application/json'}
32
- )
33
-
34
- with urllib.request.urlopen(req) as response:
35
- result = json.loads(response.read().decode())
36
-
37
- print(json.dumps(result, indent=2))
38
-
39
- except urllib.error.URLError as e:
40
- print(json.dumps({
41
- "error": f"Failed to sync: {str(e)}"
42
- }))
43
- sys.exit(1)
44
-
45
- if __name__ == "__main__":
46
- sync_context()
1
+ import urllib.request
2
+ import urllib.error
3
+ import json
4
+ import os
5
+ import sys
6
+
7
+ # Load config
8
+ try:
9
+ config_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'config.json')
10
+ with open(config_path, 'r') as f:
11
+ config = json.load(f)
12
+ BASE_URL = config.get('api_url', 'http://localhost:3000/api/skill')
13
+ except:
14
+ BASE_URL = 'http://localhost:3000/api/skill'
15
+
16
+ API_URL = f"{BASE_URL}/sync"
17
+
18
+ def sync_context():
19
+ # In a real scenario, this might read from stdin or a file provided by the agent
20
+ # For now, we send a dummy context
21
+ payload = {
22
+ "agent_id": "nemu-agent",
23
+ "context_summary": "User is actively working on coding tasks.",
24
+ "suggested_mode": "working"
25
+ }
26
+
27
+ try:
28
+ req = urllib.request.Request(
29
+ API_URL,
30
+ data=json.dumps(payload).encode('utf-8'),
31
+ headers={'Content-Type': 'application/json'}
32
+ )
33
+
34
+ with urllib.request.urlopen(req) as response:
35
+ result = json.loads(response.read().decode())
36
+
37
+ print(json.dumps(result, indent=2))
38
+
39
+ except urllib.error.URLError as e:
40
+ print(json.dumps({
41
+ "error": f"Failed to sync: {str(e)}"
42
+ }))
43
+ sys.exit(1)
44
+
45
+ if __name__ == "__main__":
46
+ sync_context()
@@ -1,41 +1,41 @@
1
- import json
2
- import os
3
- import sys
4
-
5
- CONFIG_FILE = os.path.join(os.path.dirname(__file__), 'config.json')
6
-
7
- def setup():
8
- print("Oomi Skill Setup")
9
- print("================")
10
-
11
- # Load existing config or defaults
12
- config = {"api_url": "http://localhost:3000/api/skill"}
13
- if os.path.exists(CONFIG_FILE):
14
- try:
15
- with open(CONFIG_FILE, 'r') as f:
16
- config.update(json.load(f))
17
- except:
18
- pass
19
-
20
- # Prompt user
21
- print(f"\nCurrent API URL: {config.get('api_url')}")
22
- new_url = input("Enter new API URL (press Enter to keep current): ").strip()
23
-
24
- if new_url:
25
- # Remove trailing slash if present
26
- if new_url.endswith('/'):
27
- new_url = new_url[:-1]
28
- config['api_url'] = new_url
29
-
30
- # Save
31
- try:
32
- with open(CONFIG_FILE, 'w') as f:
33
- json.dump(config, f, indent=2)
34
- print(f"\nConfiguration saved to {CONFIG_FILE}")
35
- print("Setup complete!")
36
- except Exception as e:
37
- print(f"\nError saving configuration: {e}")
38
- sys.exit(1)
39
-
40
- if __name__ == "__main__":
41
- setup()
1
+ import json
2
+ import os
3
+ import sys
4
+
5
+ CONFIG_FILE = os.path.join(os.path.dirname(__file__), 'config.json')
6
+
7
+ def setup():
8
+ print("Oomi Skill Setup")
9
+ print("================")
10
+
11
+ # Load existing config or defaults
12
+ config = {"api_url": "http://localhost:3000/api/skill"}
13
+ if os.path.exists(CONFIG_FILE):
14
+ try:
15
+ with open(CONFIG_FILE, 'r') as f:
16
+ config.update(json.load(f))
17
+ except:
18
+ pass
19
+
20
+ # Prompt user
21
+ print(f"\nCurrent API URL: {config.get('api_url')}")
22
+ new_url = input("Enter new API URL (press Enter to keep current): ").strip()
23
+
24
+ if new_url:
25
+ # Remove trailing slash if present
26
+ if new_url.endswith('/'):
27
+ new_url = new_url[:-1]
28
+ config['api_url'] = new_url
29
+
30
+ # Save
31
+ try:
32
+ with open(CONFIG_FILE, 'w') as f:
33
+ json.dump(config, f, indent=2)
34
+ print(f"\nConfiguration saved to {CONFIG_FILE}")
35
+ print("Setup complete!")
36
+ except Exception as e:
37
+ print(f"\nError saving configuration: {e}")
38
+ sys.exit(1)
39
+
40
+ if __name__ == "__main__":
41
+ setup()
@@ -0,0 +1,8 @@
1
+ XR_DEV_SERVER=http://localhost:4789/webspatial/avp/
2
+ XR_PRE_SERVER=
3
+ XR_PROD_SERVER=
4
+ XR_BUNDLE_ID=
5
+ XR_TEAM_ID=
6
+ XR_VERSION=
7
+ XR_DEV_NAME=
8
+ XR_DEV_PASSWORD=
@@ -0,0 +1,35 @@
1
+ # __OOMI_PERSONA_NAME__
2
+
3
+ This project was scaffolded by `oomi personas scaffold`.
4
+
5
+ ## Purpose
6
+
7
+ This app is intended to run inside the Oomi client as a managed persona surface and uses the WebSpatial quick-example pattern as its base.
8
+
9
+ ## Editable Zones
10
+
11
+ Only customize files in these zones unless Oomi explicitly changes the scaffold contract:
12
+
13
+ - `src/persona/`
14
+ - `persona/`
15
+
16
+ ## Runtime Contract
17
+
18
+ - Template version: `__OOMI_TEMPLATE_VERSION__`
19
+ - Health document: `/oomi.health.json`
20
+ - Runtime metadata document: `/oomi.runtime.json`
21
+ - Manifest: `/manifest.webmanifest`
22
+ - Default dev port: `4789`
23
+
24
+ ## Local Development
25
+
26
+ ```bash
27
+ npm install
28
+ npm run dev
29
+ ```
30
+
31
+ ## Notes
32
+
33
+ - Preserve the WebSpatial/Vite shell and the files in `public/`.
34
+ - Keep the app safe to embed inside Oomi and future XR clients.
35
+ - Customize persona behavior in `src/persona/`.
@@ -0,0 +1,28 @@
1
+ import js from "@eslint/js";
2
+ import globals from "globals";
3
+ import reactHooks from "eslint-plugin-react-hooks";
4
+ import reactRefresh from "eslint-plugin-react-refresh";
5
+ import tseslint from "typescript-eslint";
6
+
7
+ export default tseslint.config(
8
+ { ignores: ["dist"] },
9
+ {
10
+ extends: [js.configs.recommended, ...tseslint.configs.recommended],
11
+ files: ["**/*.{ts,tsx}"],
12
+ languageOptions: {
13
+ ecmaVersion: 2020,
14
+ globals: globals.browser,
15
+ },
16
+ plugins: {
17
+ "react-hooks": reactHooks,
18
+ "react-refresh": reactRefresh,
19
+ },
20
+ rules: {
21
+ ...reactHooks.configs.recommended.rules,
22
+ "react-refresh/only-export-components": [
23
+ "warn",
24
+ { allowConstantExport: true },
25
+ ],
26
+ },
27
+ },
28
+ );
@@ -0,0 +1,18 @@
1
+ <!DOCTYPE html>
2
+ <%- XR_ENV === 'avp' ? `
3
+ <html lang="en" class="is-spatial">
4
+ ` : `
5
+ <html lang="en">
6
+ ` %>
7
+ <head>
8
+ <meta charset="UTF-8" />
9
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
10
+ <link rel="manifest" href="/manifest.webmanifest" />
11
+ <title>__OOMI_PERSONA_NAME__</title>
12
+ </head>
13
+ <body>
14
+ <div id="root"></div>
15
+ <script type="module" src="/src/main.tsx"></script>
16
+ </body>
17
+ </html>
18
+ </html>
@@ -0,0 +1,13 @@
1
+ {
2
+ "templateVersion": "__OOMI_TEMPLATE_VERSION__",
3
+ "appKind": "oomi-persona-app",
4
+ "healthPath": "/oomi.health.json",
5
+ "defaultPort": 4789,
6
+ "supportsRuntimeRegistration": true,
7
+ "renderMode": "webspatial",
8
+ "entryDocument": "/index.html",
9
+ "editableZones": [
10
+ "src/persona",
11
+ "persona"
12
+ ]
13
+ }
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "oomi-persona-__OOMI_PERSONA_SLUG__",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "install:clean": "npm install",
8
+ "dev": "vite --host 127.0.0.1 --port 4789",
9
+ "dev:avp": "cross-env XR_ENV=avp vite --host 127.0.0.1 --port 4789",
10
+ "build": "vite build && cross-env XR_ENV=avp vite build",
11
+ "preview": "vite preview --host 127.0.0.1 --port 4789",
12
+ "lint": "eslint ."
13
+ },
14
+ "dependencies": {
15
+ "@webspatial/core-sdk": "^0.1.16",
16
+ "@webspatial/react-sdk": "^0.1.16",
17
+ "react": "^19.0.0",
18
+ "react-dom": "^19.0.0",
19
+ "react-router-dom": "^7.4.0",
20
+ "three": "^0.170.0"
21
+ },
22
+ "devDependencies": {
23
+ "@eslint/js": "^9.21.0",
24
+ "@types/react": "^19.0.10",
25
+ "@types/react-dom": "^19.0.4",
26
+ "@vitejs/plugin-react": "^4.3.4",
27
+ "@webspatial/builder": "^0.1.16",
28
+ "@webspatial/platform-visionos": "^0.1.16",
29
+ "@webspatial/vite-plugin": "^0.1.7",
30
+ "cross-env": "^7.0.3",
31
+ "dotenv-cli": "^8.0.0",
32
+ "eslint": "^9.21.0",
33
+ "eslint-plugin-react-hooks": "^5.1.0",
34
+ "eslint-plugin-react-refresh": "^0.4.19",
35
+ "globals": "^15.15.0",
36
+ "typescript": "~5.7.2",
37
+ "typescript-eslint": "^8.24.1",
38
+ "vite": "^6.2.0",
39
+ "vite-plugin-html": "^3.2.2"
40
+ },
41
+ "packageManager": "pnpm@10.6.4"
42
+ }
@@ -0,0 +1,14 @@
1
+ # Persona Brief
2
+
3
+ Name: __OOMI_PERSONA_NAME__
4
+ Slug: __OOMI_PERSONA_SLUG__
5
+
6
+ ## User Intent
7
+
8
+ __OOMI_PERSONA_DESCRIPTION__
9
+
10
+ ## Notes For The Agent
11
+
12
+ - Start from the scaffolded shell.
13
+ - Keep the runtime and health documents intact.
14
+ - Add persona-specific data flows inside `src/persona/` or `persona/`.
@@ -0,0 +1,14 @@
1
+ {
2
+ "id": "__OOMI_PERSONA_SLUG__",
3
+ "name": "__OOMI_PERSONA_NAME__",
4
+ "summary": "__OOMI_PERSONA_DESCRIPTION__",
5
+ "status": "inactive",
6
+ "type": "persona",
7
+ "templateType": "persona-app",
8
+ "promptTemplateVersion": "__OOMI_TEMPLATE_VERSION__",
9
+ "capabilities": [],
10
+ "dataSources": [],
11
+ "tags": [
12
+ "managed"
13
+ ]
14
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "xr_main_scene": {
3
+ "default_size": {
4
+ "width": 860,
5
+ "height": 1100
6
+ }
7
+ }
8
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "ok": true,
3
+ "status": "healthy",
4
+ "appKind": "oomi-persona-app",
5
+ "templateVersion": "__OOMI_TEMPLATE_VERSION__"
6
+ }
@@ -0,0 +1,180 @@
1
+ #root {
2
+ width: 100%;
3
+ }
4
+
5
+ .persona-shell {
6
+ width: min(1120px, calc(100% - 48px));
7
+ margin: 0 auto;
8
+ padding: 32px 0 48px;
9
+ }
10
+
11
+ .persona-header {
12
+ display: grid;
13
+ grid-template-columns: 1.4fr 0.8fr;
14
+ gap: 20px;
15
+ align-items: start;
16
+ }
17
+
18
+ .persona-panel {
19
+ border: 1px solid rgba(117, 94, 59, 0.2);
20
+ background: rgba(255, 250, 242, 0.88);
21
+ backdrop-filter: blur(20px);
22
+ border-radius: 28px;
23
+ box-shadow: 0 24px 80px rgba(63, 48, 23, 0.1);
24
+ }
25
+
26
+ .persona-hero {
27
+ padding: 28px;
28
+ }
29
+
30
+ .persona-eyebrow {
31
+ margin: 0;
32
+ font-size: 11px;
33
+ letter-spacing: 0.28em;
34
+ text-transform: uppercase;
35
+ color: #6d6256;
36
+ }
37
+
38
+ .persona-title {
39
+ margin: 14px 0 0;
40
+ font-size: clamp(2.6rem, 5vw, 4.6rem);
41
+ line-height: 0.94;
42
+ letter-spacing: -0.04em;
43
+ }
44
+
45
+ .persona-description {
46
+ margin: 16px 0 0;
47
+ max-width: 52rem;
48
+ color: #4f463b;
49
+ font-size: 1rem;
50
+ line-height: 1.8;
51
+ }
52
+
53
+ .persona-runtime {
54
+ padding: 24px;
55
+ }
56
+
57
+ .runtime-list {
58
+ display: grid;
59
+ gap: 10px;
60
+ margin-top: 14px;
61
+ color: #4f463b;
62
+ font-size: 0.96rem;
63
+ }
64
+
65
+ .persona-grid {
66
+ display: grid;
67
+ grid-template-columns: 1.2fr 0.8fr;
68
+ gap: 20px;
69
+ margin-top: 20px;
70
+ }
71
+
72
+ .persona-card {
73
+ padding: 24px;
74
+ }
75
+
76
+ .persona-card h2 {
77
+ margin: 0;
78
+ font-size: 1rem;
79
+ text-transform: uppercase;
80
+ letter-spacing: 0.2em;
81
+ color: #6d6256;
82
+ }
83
+
84
+ .persona-card p,
85
+ .persona-card li {
86
+ color: #51483d;
87
+ line-height: 1.8;
88
+ }
89
+
90
+ .persona-actions {
91
+ display: flex;
92
+ flex-wrap: wrap;
93
+ gap: 12px;
94
+ margin-top: 18px;
95
+ }
96
+
97
+ .persona-button,
98
+ .persona-link {
99
+ display: inline-flex;
100
+ align-items: center;
101
+ justify-content: center;
102
+ min-height: 48px;
103
+ padding: 0 18px;
104
+ border-radius: 14px;
105
+ border: 1px solid rgba(117, 94, 59, 0.22);
106
+ background: rgba(250, 242, 228, 0.96);
107
+ color: #2f291f;
108
+ text-decoration: none;
109
+ font-weight: 600;
110
+ }
111
+
112
+ .persona-button:hover,
113
+ .persona-link:hover {
114
+ border-color: rgba(117, 94, 59, 0.42);
115
+ }
116
+
117
+ .persona-scene-card {
118
+ min-height: 240px;
119
+ }
120
+
121
+ .scene-shell {
122
+ display: grid;
123
+ place-items: center;
124
+ min-height: 100vh;
125
+ padding: 32px;
126
+ }
127
+
128
+ .scene-panel {
129
+ width: min(720px, 100%);
130
+ padding: 28px;
131
+ }
132
+
133
+ .scene-panel h1 {
134
+ margin: 0;
135
+ font-size: clamp(2rem, 4vw, 3.2rem);
136
+ }
137
+
138
+ .scene-panel p {
139
+ color: #51483d;
140
+ line-height: 1.8;
141
+ }
142
+
143
+ html.is-spatial {
144
+ background: transparent;
145
+ --xr-background-material: transparent;
146
+ }
147
+
148
+ html.is-spatial .persona-panel {
149
+ --xr-background-material: thick;
150
+ }
151
+
152
+ html.is-spatial .persona-runtime {
153
+ --xr-background-material: translucent;
154
+ --xr-back: 60;
155
+ transform: translateZ(18px) rotateX(12deg);
156
+ transform-origin: top right;
157
+ }
158
+
159
+ html.is-spatial .persona-scene-card {
160
+ --xr-background-material: thin;
161
+ --xr-back: 36;
162
+ transform: translateZ(24px) rotateX(14deg);
163
+ }
164
+
165
+ html.is-spatial .persona-link,
166
+ html.is-spatial .persona-button {
167
+ --xr-background-material: thick;
168
+ }
169
+
170
+ @media (max-width: 900px) {
171
+ .persona-shell {
172
+ width: min(100%, calc(100% - 28px));
173
+ padding-top: 18px;
174
+ }
175
+
176
+ .persona-header,
177
+ .persona-grid {
178
+ grid-template-columns: 1fr;
179
+ }
180
+ }
@@ -0,0 +1,14 @@
1
+ import { BrowserRouter as Router, Route, Routes } from "react-router-dom";
2
+ import { HomePage } from "./pages/HomePage";
3
+ import { ScenePage } from "./pages/ScenePage";
4
+
5
+ export default function App() {
6
+ return (
7
+ <Router basename={__XR_ENV_BASE__}>
8
+ <Routes>
9
+ <Route path="/" element={<HomePage />} />
10
+ <Route path="/scene" element={<ScenePage />} />
11
+ </Routes>
12
+ </Router>
13
+ );
14
+ }
@@ -0,0 +1,32 @@
1
+ :root {
2
+ font-family: "Segoe UI", system-ui, sans-serif;
3
+ line-height: 1.5;
4
+ font-weight: 400;
5
+ color: #2a251f;
6
+ background:
7
+ radial-gradient(circle at top, rgba(205, 183, 143, 0.32), transparent 36%),
8
+ linear-gradient(180deg, #f7f2e8 0%, #efe7d8 100%);
9
+ font-synthesis: none;
10
+ text-rendering: optimizeLegibility;
11
+ -webkit-font-smoothing: antialiased;
12
+ -moz-osx-font-smoothing: grayscale;
13
+ }
14
+
15
+ * {
16
+ box-sizing: border-box;
17
+ }
18
+
19
+ body {
20
+ margin: 0;
21
+ min-width: 320px;
22
+ min-height: 100vh;
23
+ }
24
+
25
+ button,
26
+ a {
27
+ font: inherit;
28
+ }
29
+
30
+ button {
31
+ cursor: pointer;
32
+ }
@@ -0,0 +1,10 @@
1
+ import { StrictMode } from "react";
2
+ import { createRoot } from "react-dom/client";
3
+ import "./index.css";
4
+ import App from "./App";
5
+
6
+ createRoot(document.getElementById("root")!).render(
7
+ <StrictMode>
8
+ <App />
9
+ </StrictMode>,
10
+ );
@@ -0,0 +1,73 @@
1
+ import { initScene } from "@webspatial/react-sdk";
2
+ import { Link } from "react-router-dom";
3
+ import "../App.css";
4
+ import { personaConfig } from "../persona/config";
5
+ import { personaNotes } from "../persona/notes";
6
+
7
+ export function HomePage() {
8
+ const openSpatialScene = () => {
9
+ initScene("personaScene", prevConfig => ({
10
+ ...prevConfig,
11
+ defaultSize: {
12
+ width: 720,
13
+ height: 720,
14
+ },
15
+ }));
16
+ window.open(`${__XR_ENV_BASE__}/scene`, "personaScene");
17
+ };
18
+
19
+ return (
20
+ <main className="persona-shell">
21
+ <section className="persona-header">
22
+ <div className="persona-panel persona-hero">
23
+ <p className="persona-eyebrow">WebSpatial Persona</p>
24
+ <h1 className="persona-title">{personaConfig.name}</h1>
25
+ <p className="persona-description">{personaConfig.description}</p>
26
+
27
+ <div className="persona-actions">
28
+ <button className="persona-button" onClick={openSpatialScene} enable-xr>
29
+ Open Spatial Scene
30
+ </button>
31
+ <Link className="persona-link" to="/scene" target="_blank" enable-xr>
32
+ Open Scene Route
33
+ </Link>
34
+ </div>
35
+ </div>
36
+
37
+ <aside className="persona-panel persona-runtime">
38
+ <p className="persona-eyebrow">Runtime</p>
39
+ <div className="runtime-list">
40
+ <div>Slug: {personaConfig.slug}</div>
41
+ <div>Template: {personaConfig.templateVersion}</div>
42
+ <div>Engine: WebSpatial + React + Vite</div>
43
+ <div>Mode: local/private</div>
44
+ </div>
45
+ </aside>
46
+ </section>
47
+
48
+ <section className="persona-grid">
49
+ <article className="persona-panel persona-card">
50
+ <h2>Editable Persona Notes</h2>
51
+ <p>
52
+ This scaffold follows the WebSpatial quick-example structure so the app can render
53
+ well in both the browser and future XR clients. Keep persona-specific logic in
54
+ <code> src/persona/</code>.
55
+ </p>
56
+ <ul>
57
+ {personaNotes.map(note => (
58
+ <li key={note}>{note}</li>
59
+ ))}
60
+ </ul>
61
+ </article>
62
+
63
+ <article className="persona-panel persona-card persona-scene-card" enable-xr>
64
+ <h2>Scene Surface</h2>
65
+ <p enable-xr>
66
+ Use this card as the main spatial handoff. It already uses WebSpatial router
67
+ basenames, scene launching, and `enable-xr` affordances taken from the quick example.
68
+ </p>
69
+ </article>
70
+ </section>
71
+ </main>
72
+ );
73
+ }