From 70743a81ccc62bc622d12680beabfb6ad931c5a9 Mon Sep 17 00:00:00 2001 From: Jerry Yuan Date: Fri, 14 Aug 2026 14:25:05 +0800 Subject: [PATCH] feat(agentos): complete ordinary app lifecycle --- ARCHITECTURE.md | 18 +- README.md | 106 +++-- apps/pi-agent/app.tsx | 59 ++- apps/pi-agent/dist/app.js | 23 +- apps/pi-agent/dist/app.pak | Bin 431312 -> 431376 bytes crates/pocket-pi-agentos/src/lib.rs | 373 ++++++++++++++++-- docs/agentos-architecture.md | 80 ++-- docs/esp32-p4-port.md | 49 ++- docs/pocket-pi-design-system.md | 2 +- firmware/esp32-p4/Cargo.lock | 1 + firmware/esp32-p4/Cargo.toml | 1 + firmware/esp32-p4/src/agentos_main.rs | 223 +++++++++-- firmware/esp32-p4/src/app_services.rs | 10 +- firmware/esp32-p4/src/main.rs | 10 +- firmware/esp32-p4/src/transport/mod.rs | 2 +- .../esp32-p4/src/transport/provisioning.rs | 75 +++- hosts/esp32-p4-sim/src/main.rs | 37 +- tools/uart-install.py | 72 ++++ tools/uart-model-bridge.py | 113 +----- tools/uart-provision.py | 93 +++++ tools/uart_io.py | 70 ++++ 21 files changed, 1107 insertions(+), 310 deletions(-) create mode 100755 tools/uart-install.py create mode 100755 tools/uart-provision.py create mode 100644 tools/uart_io.py diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 17ca472..0992636 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -27,9 +27,9 @@ The authoritative detailed design is FS/SQLite mounts. There is no separate persistent App Catalog service. - `tools/xtask` packages ordinary Apps without moving their product logic into the AgentOS runtime or host adapters. -- HTTP is the current package ingress. It hands the complete `.pocketapp` to the - Installer, which alone validates, stores credentials, creates `current` and - activates runtime metadata. A future UART ingress must use the same boundary. +- HTTP and UART are package ingress adapters. Both hand the complete `.pocketapp` + to the Installer, which alone validates, stores credentials, creates `current` + and activates runtime metadata. - `crates/pocket-pi-embedded` provides the bounded JavaScript Agent Loop bridge. In AgentOS hosts, the loop is loaded from the Pi Agent System App release into the same PocketJS Guest as its Root View. @@ -59,6 +59,11 @@ LRU caches. Only the foreground View ticks or renders. On ESP32-P4, App QuickJS heaps and large worker stacks allocate explicitly from PSRAM without changing the platform-wide `malloc()` policy. +Uninstall is the reverse of ordinary App activation inside the same +`AppSupervisor`: it removes the App's Tool routes, schedules, cached View/Data +Action Guests, native credentials/session state and complete App data root. It +does not introduce a second lifecycle manager or affect the resident Pi Agent. + Ordinary Apps receive capability-scoped data roots. Pi Agent alone owns the top-level `/workspace` and cross-App Tool Registry. @@ -72,8 +77,11 @@ crates/pocket-pi-tools/ native workspace/shell/time/schedule Tools crates/pocket-pi-protocols/ provider codecs hosts/esp32-p4-sim/ macOS development simulator for ESP32-P4 contracts firmware/esp32-p4/ first supported target and reference implementation -tools/uart_bridge/ Mac Codex/Claude streaming adapters -tools/uart-model-bridge.py UART framing and provisioning CLI +tools/uart_io.py shared raw UART read/write helpers +tools/uart-provision.py one-time wireless model provisioning +tools/uart-install.py App package ingress over UART +tools/uart_bridge/ development-only Codex/Claude adapters +tools/uart-model-bridge.py optional development-only model bridge ``` ## Build entry points diff --git a/README.md b/README.md index 6d33bfe..207ad1a 100644 --- a/README.md +++ b/README.md @@ -92,47 +92,65 @@ espflash flash --baud 921600 --port "$DEVICE_PORT" \ firmware/esp32-p4/target/riscv32imafc-esp-espidf/release/pocket-pi-p4 ``` -For development, the simplest model path is UART to a logged-in Mac Codex: +Provision the board once with a direct wireless model backend. DeepSeek is the +default provider and uses `deepseek-v4-flash` unless `--model` is supplied: ```sh -python3 tools/uart-model-bridge.py "$DEVICE_PORT" \ - --backend uart --provider codex +python3 tools/uart-provision.py "$DEVICE_PORT" \ + --provider deepseek --provision-wifi ``` -Claude Code can be used instead: +The command reads the DeepSeek key from macOS Keychain when available, otherwise +it prompts without echoing the value. It stores model provider, model, thinking +level and API key in native NVS. Wi-Fi credentials use the existing Wi-Fi NVS +store and can later be changed from Settings. Subsequent boots load both stores +directly and do not wait for a Mac or UART bridge. + +Other direct providers are selected explicitly: ```sh -python3 tools/uart-model-bridge.py "$DEVICE_PORT" \ - --backend uart --provider claude-code +python3 tools/uart-provision.py "$DEVICE_PORT" \ + --provider openai --model gpt-5-mini ``` -The bridge can also send a repeatable boot prompt: +For bring-up on an unprovisioned development board only, the optional model +bridge can route requests to a logged-in Mac Codex or Claude Code. It is not +part of standalone startup and is never stored as the device backend: ```sh -python3 tools/uart-model-bridge.py "$DEVICE_PORT" \ - --backend uart --provider codex \ +python3 tools/uart-model-bridge.py "$DEVICE_PORT" --provider codex \ --prompt 'Use write, read, schedule.set and schedule.list.' ``` -For standalone use, provision Wi-Fi and a direct model provider over UART: +Use `espflash monitor` when only serial logs are needed: ```sh -python3 tools/uart-model-bridge.py "$DEVICE_PORT" \ - --backend wireless --provider openai --model gpt-5-mini --provision-wifi +espflash monitor --port "$DEVICE_PORT" ``` -The provisioning command asks for the Wi-Fi credentials and API key -interactively. Wi-Fi can subsequently be changed from the device Settings UI -without reflashing. Credentials are kept out of Agent workspace files and -PocketJS UI state. +To install over the local network, open `http:///` from a computer or +phone and upload the `.pocketapp`. When local peer access is unavailable, upload +the same artifact over USB UART: -To install an ordinary App, open `http:///` from a computer or phone, -upload its `.pocketapp`, then confirm on the Pocket Pi screen. Every credential -declared by the App must be present in the package. Installer removes those values -from the package and stores them in native NVS; they are not exposed through the -App filesystem or Agent workspace. HTTP is the current upload ingress; activation -always goes through the same Installer. A future UART ingress may deliver the same -artifact, but may not write App storage, credentials or runtime state directly. +```sh +python3 tools/uart-install.py "$DEVICE_PORT" \ + target/pocketapps/exa.pocketapp +``` + +Both transports stop at the same review screen; installation starts only after +confirmation on Pocket Pi. Every credential declared by the App must be present +in the package. Installer removes those values from the package and stores them in +native NVS; they are not exposed through the App filesystem or Agent workspace. +Neither HTTP nor UART writes App storage, credentials or runtime state directly. +UART upload does not reset the board or change its model configuration. +The UART CLIs leave DTR/RTS inactive before closing the port so the USB serial +bridge does not reset a running Pocket Pi. + +To remove an ordinary App, open **Apps**, tap **UNINSTALL APP**, then tap the +`X` on that App's row. Uninstall removes the App release, SQLite/data files, +schedules, credentials, native session state and cached View/Data Action +Runtimes. It does not retain App data or provide rollback. The resident Pi Agent +System App cannot be uninstalled. ### 2. Develop with the ESP32-P4 simulator on macOS @@ -177,12 +195,13 @@ Backends belong to their host composition, not to the Agent core: | Host | Supported backends | |---|---| | ESP32 simulator | local Codex, OpenAI, OpenRouter, Anthropic, DeepSeek V4 | -| Physical ESP32-P4 | UART to Mac Codex or Claude Code; wireless OpenAI, OpenRouter, Anthropic or DeepSeek V4 | +| Physical ESP32-P4 | standalone wireless OpenAI, OpenRouter, Anthropic or DeepSeek V4 | -`UartBackend` and `WirelessBackend` implement the same model-completion -contract. Wireless providers may emit progress events internally; the current -UART bridge coalesces provider chunks into one final framed result before it -reaches the device. Provider request/streaming codecs live in +The optional development-only `UartBackend` and the standalone +`WirelessBackend` implement the same model-completion contract. Wireless +providers may emit progress events internally; the development bridge coalesces +provider chunks into one final framed result before it reaches an unprovisioned +device. Provider request/streaming codecs live in `pocket-pi-protocols`; serial framing, desktop CLIs and ESP-IDF HTTPS stay in their platform layers. @@ -216,6 +235,7 @@ includes: - Chat with provider-dependent incremental replies, recent-turn history and a full-message reader; - Files with workspace metadata, file viewing and scrolling; +- Apps with install discovery and destructive uninstall mode; - Settings with Wi-Fi scanning, selection and password entry; - touch keyboard and next-schedule status. @@ -233,8 +253,11 @@ crates/pocket-pi-protocols/ model request, response and streaming codecs crates/pocket-pi-agentos/ App Supervisor, System App lifecycle and App contracts hosts/esp32-p4-sim/ macOS development simulator for ESP32-P4 contracts firmware/esp32-p4/ first supported device and reference implementation -tools/uart_bridge/ Mac Codex and Claude Code streaming adapters -tools/uart-model-bridge.py UART framing and provisioning CLI +tools/uart_io.py shared raw UART read/write helpers +tools/uart-provision.py one-time wireless model provisioning +tools/uart-install.py App package ingress over UART +tools/uart_bridge/ development-only Codex/Claude adapters +tools/uart-model-bridge.py optional development-only model bridge ``` Dependencies point inward: hosts depend on shared runtimes, tools, UI and @@ -247,16 +270,25 @@ and [docs/esp32-p4-port.md](docs/esp32-p4-port.md) for board-specific details. ## Current validation -On **2026-08-14**, the workspace completed 34 Rust tests and 3 App text behavior +On **2026-08-14**, the workspace completed 35 Rust tests and 3 App text behavior tests with no failures, passed workspace Clippy with warnings denied, and built the ESP32-P4 release firmware. A simulator LAN smoke uploaded the generated Exa package through `POST /install` and received HTTP 202; the core install test -then proves activation, Tool routing, SQLite ownership and restart recovery for -a previously absent App. - -Fresh physical flash/boot, phone upload, Wi-Fi/DHCP, provider calls and -unattended memory pressure remain separate evidence tiers. Simulator and -cross-build success do not substitute for fresh physical-board acceptance. +proves activation, Tool routing, SQLite ownership and restart recovery for a +previously absent App. The uninstall lifecycle test proves complete App-owned +state removal, live Tool removal, restart absence and reinstall. A physical +ESP32-P4 then accepted the same Exa package over UART, stopped at the shared +review screen, installed after touch confirmation, uninstalled it from Apps, +kept it absent across restart, and accepted a clean UART reinstall. +The split UART tools were then verified on the same board: one-time DeepSeek +provisioning received `PPI-CONFIG-STORED`; a bridge-free reset logged +`loaded wireless model configuration from NVS`, restored Wi-Fi/DHCP on +the saved network, and reached an idle Pi Agent; the independent App uploader +transferred Exa to the shared review screen without resetting the board. + +Phone upload, fresh provider calls and unattended memory pressure remain +separate evidence tiers. Simulator and cross-build success do not substitute +for fresh physical-board acceptance. ## Development checks diff --git a/apps/pi-agent/app.tsx b/apps/pi-agent/app.tsx index 4fba444..72652c2 100644 --- a/apps/pi-agent/app.tsx +++ b/apps/pi-agent/app.tsx @@ -34,6 +34,8 @@ type Projection = { schedule?: { name?: string | null; prompt?: string; next?: string; everyMinutes?: number | null }; apps?: InstalledApp[]; install?: InstallProjection | null; + uninstallingApp?: string | null; + uninstallError?: string | null; settings?: { wifi?: { connectedSsid?: string | null; @@ -68,6 +70,7 @@ const [projection, setProjection] = createSignal({ messages: [{ role: "assistant", text: "BOOTING PI AGENT..." }], }); const [installDetail, setInstallDetail] = createSignal(null); +const [uninstallMode, setUninstallMode] = createSignal(false); const [screen, setScreen] = createSignal("chat"); const [activeTab, setActiveTab] = createSignal("chat"); const [chatScroll, setChatScroll] = createSignal(0); @@ -84,7 +87,6 @@ const [uppercase, setUppercase] = createSignal(false); const [keyboardPurpose, setKeyboardPurpose] = createSignal({ type: "prompt" }); const [pressedKey, setPressedKey] = createSignal(null); const [wifiOffset, setWifiOffset] = createSignal(0); -const fileCache = new Map(); const letterRows = ["qwertyuiop", "asdfghjkl", "zxcvbnm"]; const numberRows = ["1234567890", "-/:;()$&@", ".,?!'\"+"]; @@ -126,21 +128,13 @@ function moveViewerPage(direction: -1 | 1) { setViewer({ ...current, pageIndex: nextIndex, page: filePage(current.text, start) }); } -function refreshFiles(path = filePath(), force = false) { - const cached = fileCache.get(path); - if (!force && cached) { - setFiles(cached); - setFileError(""); - setFileOffset((value) => Math.min(value, Math.max(0, cached.length - 8))); - return; - } +function refreshFiles(path = filePath()) { try { const next = (readdirSync(path, { withFileTypes: true }) as DirEntry[]).map((entry) => ({ name: entry.name, kind: entry.isDirectory() ? "dir" as const : "file" as const, size: entry.size, })); - fileCache.set(path, next); setFiles(next); setFileError(""); setFileOffset((value) => Math.min(value, Math.max(0, next.length - 8))); @@ -285,10 +279,16 @@ function FilesScreen() { function AppsScreen() { const apps = () => projection().apps ?? []; + const uninstalling = () => projection().uninstallingApp; + const status = () => projection().uninstallError + ? "UNINSTALL FAILED · " + projection().uninstallError + : uninstalling() + ? "UNINSTALLING " + uninstalling()!.toUpperCase() + "..." + : "APP DATA STAYS ISOLATED. PI AGENT CAN USE EACH APP'S TOOLS."; return (
- + {String(apps().length) + " INSTALLED APPS"} 0} fallback={ NO OPTIONAL APPS INSTALLED @@ -297,14 +297,23 @@ function AppsScreen() { {app.title.slice(0, 1).toUpperCase()} - {app.title}{app.description}{"UPDATES EVERY " + String(app.scheduleEveryMinutes) + " MINUTES"} + {app.title}{app.description}{"UPDATES EVERY " + String(app.scheduleEveryMinutes) + " MINUTES"} - + ›}> + + )} - - {"APP DATA STAYS ISOLATED.\nPI AGENT CAN USE EACH APP'S TOOLS."} + + + + + @@ -522,9 +531,10 @@ function Root() { function openTab(tab: Tab) { batch(() => { + if (tab !== "apps") setUninstallMode(false); setActiveTab(tab); setScreen(tab); - if (tab === "files") refreshFiles(); + if (tab === "files") refreshFiles(filePath()); }); } @@ -600,7 +610,7 @@ function handleKeyboardTap(x: number, y: number): string { } mount(() => ); -queueMicrotask(() => refreshFiles("", true)); +queueMicrotask(() => refreshFiles("")); (globalThis as any).PocketPiApp = { tick() { @@ -630,6 +640,7 @@ queueMicrotask(() => refreshFiles("", true)); } return ""; } + if (projection().uninstallingApp) return ""; if (screen() === "keyboard") return handleKeyboardTap(x, y); if (screen() === "viewer") { const current = viewer(); @@ -713,8 +724,18 @@ queueMicrotask(() => refreshFiles("", true)); return ""; } if (screen() === "apps") { - const app = (projection().apps ?? [])[Math.floor((y - 162) / 166)]; - if (y >= 162 && app) return JSON.stringify({ type: "navigate", app: app.id }); + const apps = projection().apps ?? []; + if (y >= 1060 && y < 1140 && (uninstallMode() || apps.length > 0) && !projection().uninstallingApp) { + setUninstallMode(!uninstallMode()); + return ""; + } + const app = apps[Math.floor((y - 162) / 166)]; + if (y >= 162 && app && !projection().uninstallingApp) { + if (uninstallMode()) { + return x >= 604 && x < 696 ? JSON.stringify({ type: "uninstallApp", app: app.id }) : ""; + } + return JSON.stringify({ type: "navigate", app: app.id }); + } return ""; } if (screen() === "settings") { diff --git a/apps/pi-agent/dist/app.js b/apps/pi-agent/dist/app.js index 4acf6eb..e320bfd 100644 --- a/apps/pi-agent/dist/app.js +++ b/apps/pi-agent/dist/app.js @@ -1,17 +1,16 @@ -(()=>{var kJ={context:void 0,registry:void 0,effects:void 0,done:!1,getContextId(){return j1(this.context.count)},getNextContextId(){return j1(this.context.count++)}};function j1(J){let Q=String(J),$=Q.length-1;return kJ.context.id+($?String.fromCharCode(96+$):"")+Q}function K1(J){kJ.context=J}function G6(){return{...kJ.context,id:kJ.getNextContextId(),count:0}}var W6=!1,B6=(J,Q)=>J===Q,_0=Symbol("solid-proxy"),H6=typeof Proxy==="function",j6=Symbol("solid-track"),_Q=Symbol("solid-dev-component"),oJ={equals:B6},F1=null,K6=O1,u=1,TJ=2,M1={owned:null,cleanups:null,context:null,owner:null},S=null,F=null,fJ=null,SJ=null,b=null,E=null,T=null,nJ=0;function tJ(J,Q){let $=b,Z=S,q=J.length===0,X=Q===void 0?Z:Q,z=q?M1:{owned:null,cleanups:null,context:X?X.context:null,owner:X},W=q?J:()=>J(()=>QJ(()=>$J(z)));S=z,b=null;try{return d(W,!0)}finally{b=$,S=Z}}function w(J,Q){Q=Q?Object.assign({},oJ,Q):oJ;let $={value:J,observers:null,observerSlots:null,comparator:Q.equals||void 0},Z=(q)=>{if(typeof q==="function")if(F&&F.running&&F.sources.has($))q=q($.tValue);else q=q($.value);return D1($,q)};return[P1.bind($),Z]}function JJ(J,Q,$){let Z=L1(J,Q,!1,u);if(fJ&&F&&F.running)E.push(Z);else eJ(Z)}function bJ(J,Q,$){$=$?Object.assign({},oJ,$):oJ;let Z=L1(J,Q,!0,0);if(Z.observers=null,Z.observerSlots=null,Z.comparator=$.equals||void 0,fJ&&F&&F.running)Z.tState=u,E.push(Z);else eJ(Z);return P1.bind(Z)}function U1(J){return d(J,!1)}function QJ(J){if(!SJ&&b===null)return J();let Q=b;b=null;try{if(SJ)return SJ.untrack(J);return J()}finally{b=Q}}function k0(J){if(S===null);else if(S.cleanups===null)S.cleanups=[J];else S.cleanups.push(J);return J}function F6(J){if(F&&F.running)return J(),F.done;let Q=b,$=S;return Promise.resolve().then(()=>{b=Q,S=$;let Z;if(fJ||M6)Z=F||(F={sources:new Set,effects:[],promises:new Set,disposed:new Set,queue:new Set,running:!0}),Z.done||(Z.done=new Promise((q)=>Z.resolve=q)),Z.running=!0;return d(J,!1),b=S=null,Z?Z.done:void 0})}var[kQ,R1]=w(!1),M6;function P1(){let J=F&&F.running;if(this.sources&&(J?this.tState:this.state))if((J?this.tState:this.state)===u)eJ(this);else{let Q=E;E=null,d(()=>J0(this),!1),E=Q}if(b){let Q=this.observers;if(!Q||Q[Q.length-1]!==b){let $=Q?Q.length:0;if(!b.sources)b.sources=[this],b.sourceSlots=[$];else b.sources.push(this),b.sourceSlots.push($);if(!Q)this.observers=[b],this.observerSlots=[b.sources.length-1];else Q.push(b),this.observerSlots.push(b.sources.length-1)}}if(J&&F.sources.has(this))return this.tValue;return this.value}function D1(J,Q,$){let Z=F&&F.running&&F.sources.has(J)?J.tValue:J.value;if(!J.comparator||!J.comparator(Z,Q)){if(F){let q=F.running;if(q||!$&&F.sources.has(J))F.sources.add(J),J.tValue=Q;if(!q)J.value=Q}else J.value=Q;if(J.observers&&J.observers.length)d(()=>{for(let q=0;q1e6)throw E=[],Error()},!1)}return Q}function eJ(J){if(!J.fn)return;$J(J);let Q=nJ;if(V1(J,F&&F.running&&F.sources.has(J)?J.tValue:J.value,Q),F&&!F.running&&F.sources.has(J))queueMicrotask(()=>{d(()=>{F&&(F.running=!0),b=S=J,V1(J,J.tValue,Q),b=S=null},!1)})}function V1(J,Q,$){let Z,q=S,X=b;b=S=J;try{Z=J.fn(Q)}catch(z){if(J.pure)if(F&&F.running)J.tState=u,J.tOwned&&J.tOwned.forEach($J),J.tOwned=void 0;else J.state=u,J.owned&&J.owned.forEach($J),J.owned=null;return J.updatedAt=$+1,b0(z)}finally{b=X,S=q}if(!J.updatedAt||J.updatedAt<=$){if(J.updatedAt!=null&&"observers"in J)D1(J,Z,!0);else if(F&&F.running&&J.pure){if(!F.sources.has(J))J.value=Z;F.sources.add(J),J.tValue=Z}else J.value=Z;J.updatedAt=$}}function L1(J,Q,$,Z=u,q){let X={fn:J,state:Z,updatedAt:null,owned:null,sources:null,sourceSlots:null,cleanups:null,value:Q,owner:S,context:S?S.context:null,pure:$};if(F&&F.running)X.state=0,X.tState=Z;if(S===null);else if(S!==M1)if(F&&F.running&&S.pure)if(!S.tOwned)S.tOwned=[X];else S.tOwned.push(X);else if(!S.owned)S.owned=[X];else S.owned.push(X);if(SJ&&X.fn){let z=X.fn,[W,R]=w(void 0,{equals:!1}),D=SJ.factory(z,R);k0(()=>D.dispose());let V,B=()=>F6(R).then(()=>{if(V)V.dispose(),V=void 0});X.fn=(_)=>{if(W(),F&&F.running){if(!V)V=SJ.factory(z,B);return V.track(_)}return D.track(_)}}return X}function S0(J){let Q=F&&F.running;if((Q?J.tState:J.state)===0)return;if((Q?J.tState:J.state)===TJ)return J0(J);if(J.suspense&&QJ(J.suspense.inFallback))return J.suspense.effects.push(J);let $=[J];while((J=J.owner)&&(!J.updatedAt||J.updatedAt=0;Z--){if(J=$[Z],Q){let q=J,X=$[Z+1];while((q=q.owner)&&q!==X)if(F.disposed.has(q))return}if((Q?J.tState:J.state)===u)eJ(J);else if((Q?J.tState:J.state)===TJ){let q=E;E=null,d(()=>J0(J,$[0]),!1),E=q}}}function d(J,Q){if(E)return J();let $=!1;if(!Q)E=[];if(T)$=!0;else T=[];nJ++;try{let Z=J();return U6($),Z}catch(Z){if(!$)T=null;E=null,b0(Z)}}function U6(J){if(E){if(fJ&&F&&F.running)R6(E);else O1(E);E=null}if(J)return;let Q;if(F){if(!F.promises.size&&!F.queue.size){let{sources:Z,disposed:q}=F;T.push.apply(T,F.effects),Q=F.resolve;for(let X of T)"tState"in X&&(X.state=X.tState),delete X.tState;F=null,d(()=>{for(let X of q)$J(X);for(let X of Z){if(X.value=X.tValue,X.owned)for(let z=0,W=X.owned.length;zK6($),!1);if(Q)Q()}function O1(J){for(let Q=0;Q{Z.delete($),d(()=>{F.running=!0,S0($)},!1),F&&(F.running=!1)})}}function J0(J,Q){let $=F&&F.running;if($)J.tState=0;else J.state=0;for(let Z=0;Z=0;Q--)$J(J.tOwned[Q]);delete J.tOwned}if(F&&F.running&&J.pure)A1(J,!0);else if(J.owned){for(Q=J.owned.length-1;Q>=0;Q--)$J(J.owned[Q]);J.owned=null}if(J.cleanups){for(Q=J.cleanups.length-1;Q>=0;Q--)J.cleanups[Q]();J.cleanups=null}if(F&&F.running)J.tState=0;else J.state=0}function A1(J,Q){if(!Q)J.tState=0,F.disposed.add(J);if(J.owned)for(let $=0;$1?[]:null;return k0(()=>k1(X)),()=>{let R=J()||[],D=R.length,V,B;return R[j6],QJ(()=>{let g,C,r,HJ,IJ,M,K,H,L;if(D===0){if(z!==0)k1(X),X=[],Z=[],q=[],z=0,W&&(W=[]);if($.fallback)Z=[D6],q[0]=tJ((A)=>{return X[0]=A,$.fallback()}),z=1}else if(z===0){q=Array(D);for(B=0;B=M&&H>=M&&Z[K]===R[H];K--,H--)r[H]=q[K],HJ[H]=X[K],W&&(IJ[H]=W[K]);g=new Map,C=Array(H+1);for(B=H;B>=M;B--)L=R[B],V=g.get(L),C[B]=V===void 0?-1:V,g.set(L,B);for(V=M;V<=K;V++)if(L=Z[V],B=g.get(L),B!==void 0&&B!==-1)r[B]=q[V],HJ[B]=X[V],W&&(IJ[B]=W[V]),B=C[B],g.set(L,B);else X[V]();for(B=M;BJ(Q||{}));return K1($),Z}}return QJ(()=>J(Q||{}))}function Q0(){return!0}var I6={get(J,Q,$){if(Q===_0)return $;return J.get(Q)},has(J,Q){if(Q===_0)return!0;return J.has(Q)},set:Q0,deleteProperty:Q0,getOwnPropertyDescriptor(J,Q){return{configurable:!0,enumerable:!0,get(){return J.get(Q)},set:Q0,deleteProperty:Q0}},ownKeys(J){return J.keys()}};function g0(J){return!(J=typeof J==="function"?J():J)?{}:J}function A6(){for(let J=0,Q=this.length;J=0;W--){let R=g0(J[W])[z];if(R!==void 0)return R}},has(z){for(let W=J.length-1;W>=0;W--)if(z in g0(J[W]))return!0;return!1},keys(){let z=[];for(let W=0;W=0;z--){let W=J[z];if(!W)continue;let R=Object.getOwnPropertyNames(W);for(let D=R.length-1;D>=0;D--){let V=R[D];if(V==="__proto__"||V==="constructor")continue;let B=Object.getOwnPropertyDescriptor(W,V);if(!Z[V])Z[V]=B.get?{enumerable:!0,configurable:!0,get:A6.bind($[V]=[B.get.bind(W)])}:B.value!==void 0?B:void 0;else{let _=$[V];if(_){if(B.get)_.push(B.get.bind(W));else if(B.value!==void 0)_.push(()=>B.value)}}}}let q={},X=Object.keys(Z);for(let z=X.length-1;z>=0;z--){let W=X[z],R=Z[W];if(R&&R.get)Object.defineProperty(q,W,R);else q[W]=R?R.value:void 0}return q}var _6=(J)=>`Stale read from <${J}>.`;function jJ(J){let Q="fallback"in J&&{fallback:()=>J.fallback};return bJ(V6(()=>J.each,J.children,Q||void 0))}function ZJ(J){let Q=J.keyed,$=bJ(()=>J.when,void 0,void 0),Z=Q?$:bJ($,void 0,{equals:(q,X)=>!q===!X});return bJ(()=>{let q=Z();if(q){let X=J.children;return typeof X==="function"&&X.length>0?QJ(()=>X(Q?q:()=>{if(!QJ(Z))throw _6("Show");return $()})):X}return J.fallback},void 0,void 0)}var k6=(J)=>bJ(()=>J());function S6({createElement:J,createTextNode:Q,isTextNode:$,replaceText:Z,insertNode:q,removeNode:X,setProperty:z,getParentNode:W,getFirstChild:R,getNextSibling:D}){function V(M,K,H,L){if(H!==void 0&&!L)L=[];if(typeof K!=="function")return B(M,K,L,H);JJ((A)=>B(M,K(),A,H),L)}function B(M,K,H,L,A){while(typeof H==="function")H=H();if(K===H)return H;let k=typeof K,I=L!==void 0;if(k==="string"||k==="number"){if(k==="number")K=K.toString();if(I){let O=H[0];if(O&&$(O))Z(O,K);else O=Q(K);H=C(M,H,L,O)}else if(H!==""&&typeof H==="string")Z(R(M),H=K);else C(M,H,L,Q(K)),H=K}else if(K==null||k==="boolean")H=C(M,H,L);else if(k==="function")return JJ(()=>{let O=K();while(typeof O==="function")O=O();H=B(M,O,H,L)}),()=>H;else if(Array.isArray(K)){let O=[];if(_(O,K,A))return JJ(()=>H=B(M,O,H,L,!0)),()=>H;if(O.length===0){let yJ=C(M,H,L);if(I)return H=yJ}else if(Array.isArray(H))if(H.length===0)r(M,O,L);else g(M,H,O);else if(H==null||H==="")r(M,O);else g(M,I&&H||[R(M)],O);H=O}else{if(Array.isArray(H)){if(I)return H=C(M,H,L,K);C(M,H,null,K)}else if(H==null||H===""||!R(M))q(M,K);else HJ(M,K,R(M));H=K}return H}function _(M,K,H){let L=!1;for(let A=0,k=K.length;Ao-O){let AQ=K[I];while(O=0;I--){let O=K[I];if(A!==O){let yJ=W(O)===M;if(!k&&!I)yJ?HJ(M,A,O):q(M,A,H);else yJ&&X(M,O)}else k=!0}}else q(M,A,H);return[A]}function r(M,K,H){for(let L=0,A=K.length;LH.children=B(M,K.children,H.children));return JJ(()=>K.ref&&K.ref(M)),JJ(()=>{for(let A in K){if(A==="children"||A==="ref")continue;let k=K[A];if(k===H[A])continue;z(M,A,k,H[A]),H[A]=k}}),H}return{render(M,K){let H;return tJ((L)=>{H=L,V(K,M())}),H},insert:V,spread(M,K,H){if(typeof K==="function")JJ((L)=>IJ(M,K(),L,H));else IJ(M,K,void 0,H)},createElement:J,createTextNode:Q,insertNode:q,setProp(M,K,H,L){return z(M,K,H,L),H},mergeProps:S1,effect:JJ,memo:k6,createComponent:O6,use(M,K,H){return QJ(()=>M(K,H))}}}function b6(J){let Q=S6(J);return Q.mergeProps=S1,Q}var b1=480,g1=272,KJ={view:0,text:1,image:2},g6=1,w6=-1,C6={width:1,height:2,minW:3,minH:4,maxW:5,maxH:6,paddingT:8,paddingR:9,paddingB:10,paddingL:11,marginT:12,marginR:13,marginB:14,marginL:15,gap:16,flexDir:17,justify:18,align:19,grow:20,shrink:21,basis:22,flexWrap:23,posType:24,insetT:25,insetR:26,insetB:27,insetL:28,display:29,overflow:30,zIndex:31,hitPass:32,bgColor:64,gradFrom:65,gradTo:66,gradDir:67,radius:68,opacity:69,borderColor:70,borderWidth:71,shadow:72,bevelOuterLight:77,bevelOuterDark:78,bevelInnerLight:79,bevelInnerDark:80,bevelWidth:81,textColor:96,fontSlot:97,textAlign:98,lineHeight:99,tracking:100,translateX:128,translateY:129,scale:130,rotate:131,scaleX:132,scaleY:133,originX:134,originY:135,rotateX:136,rotateY:137,translateZ:138,perspective:139,arcStart:140,arcSweep:141,arcWidth:142},P={f32:0,color:1,int:2},N6={width:P.f32,height:P.f32,minW:P.f32,minH:P.f32,maxW:P.f32,maxH:P.f32,paddingT:P.f32,paddingR:P.f32,paddingB:P.f32,paddingL:P.f32,marginT:P.f32,marginR:P.f32,marginB:P.f32,marginL:P.f32,gap:P.f32,flexDir:P.int,justify:P.int,align:P.int,grow:P.f32,shrink:P.f32,basis:P.f32,flexWrap:P.int,posType:P.int,insetT:P.f32,insetR:P.f32,insetB:P.f32,insetL:P.f32,display:P.int,overflow:P.int,zIndex:P.int,hitPass:P.int,bgColor:P.color,gradFrom:P.color,gradTo:P.color,gradDir:P.int,radius:P.f32,opacity:P.f32,borderColor:P.color,borderWidth:P.f32,shadow:P.int,bevelOuterLight:P.color,bevelOuterDark:P.color,bevelInnerLight:P.color,bevelInnerDark:P.color,bevelWidth:P.f32,textColor:P.color,fontSlot:P.int,textAlign:P.int,lineHeight:P.f32,tracking:P.f32,translateX:P.f32,translateY:P.f32,scale:P.f32,rotate:P.f32,scaleX:P.f32,scaleY:P.f32,originX:P.f32,originY:P.f32,rotateX:P.f32,rotateY:P.f32,translateZ:P.f32,perspective:P.f32,arcStart:P.f32,arcSweep:P.f32,arcWidth:P.f32},l={FlexDir:{Row:0,Col:1},Justify:{Start:0,Center:1,End:2,Between:3,Around:4},Align:{Start:0,Center:1,End:2,Stretch:3},PosType:{Relative:0,Absolute:1},Display:{Flex:0,None:1},Overflow:{Visible:0,Hidden:1},TextAlign:{Left:0,Center:1,Right:2},GradDir:{ToTop:0,ToBottom:1,ToLeft:2,ToRight:3},Easing:{Linear:0,EaseIn:1,EaseOut:2,EaseInOut:3,OutBack:4,Spring:5,SpringBouncy:6,CubicBezier:7}},E6={PSM_5650:0,PSM_4444:2,PSM_8888:3,PSM_T8:5},h6=1,SQ=2,bQ=1,gQ=2,wQ=131072,CQ=1,NQ=262144,EQ=1,hQ=1,yQ=1,TQ=2,fQ=4,xQ=8,vQ=16,mQ=1,uQ=2;function w1(J,Q,$,Z=255){return((Z&255)<<24|($&255)<<16|(Q&255)<<8|J&255)>>>0}var lQ=1,y6=1263551300,T6=1,f6=32,x6=24,i={SELECT:1,START:8,UP:16,RIGHT:32,DOWN:64,LEFT:128,LTRIGGER:256,RTRIGGER:512,TRIANGLE:4096,CIRCLE:8192,CROSS:16384,SQUARE:32768},FJ=32896,cQ=0.016666666666666666;function C1(J){return J.__viewport??null}var w0=null;function v6(){return null}function N1(J,Q=v6()){if(!Q)return;if(typeof J.__host!=="string")throw Error(`PocketJS: this bundle targets "${Q.target}" but the native host predates platform `+"contracts — add __host/__hostAbi to its ui namespace (see framework/src/host.ts HostOps)");if(J.__host!==Q.target)throw Error(`PocketJS: native target mismatch (bundle=${Q.target}, host=${J.__host})`);if(J.__hostAbi!==Q.hostAbi)throw Error(`PocketJS: native host ABI mismatch (bundle=${Q.hostAbi}, host=${J.__hostAbi??"missing"})`)}function m6(J){let Q=globalThis.ui,$=Q!==void 0&&(typeof Q.__host==="string"||Q.__textures!==void 0);if(J){if(Q!==void 0&&J===Q&&$)return N1(Q),{ops:J,kind:"native",target:Q.__host??"unknown",strict:!1};return{ops:J,kind:"injected",target:J.__host??"injected",strict:!0}}if(Q!==void 0&&$)return N1(Q),{ops:Q,kind:"native",target:Q.__host??"unknown",strict:!1};if(Q)return{ops:Q,kind:"injected",target:"injected",strict:!0};throw Error("PocketJS: no host — pass HostOps to render() (web/test) or run under a native runtime (globalThis.ui)")}function u6(J){w0=J}function gJ(){if(!w0)throw Error("PocketJS: host not installed — call render() first");return w0}function h(){return gJ().ops}function l6(J){globalThis.frame=J}function c6(J){let Q=globalThis,$=Q.__pocketResizeViewport,Z=(q,X)=>J(q,X);return Q.__pocketResizeViewport=Z,()=>{if(Q.__pocketResizeViewport!==Z)return;if($)Q.__pocketResizeViewport=$;else delete Q.__pocketResizeViewport}}function p6(J){let Q=J.slice(1);if(Q.length===3)Q=Q[0]+Q[0]+Q[1]+Q[1]+Q[2]+Q[2];if(Q.length!==6&&Q.length!==8)throw Error(`PocketJS: bad color '${J}' (expected #rgb/#rrggbb/#rrggbbaa)`);if(!/^[0-9a-fA-F]+$/.test(Q))throw Error(`PocketJS: bad color '${J}'`);let $=parseInt(Q,16);if(Q.length===6)return w1($>>>16&255,$>>>8&255,$&255,255);return w1($>>>24&255,$>>>16&255,$>>>8&255,$&255)}function d6(J,Q){let $=N6[J];if(typeof Q==="string"){if($===P.color)return p6(Q);let Z=Number(Q);if(Number.isNaN(Z))throw Error(`PocketJS: non-numeric value '${Q}' for prop '${J}'`);Q=Z}if($===P.color||$===P.int)return Q>>>0;return Q}var $0=60,E1=[1,2,3,4,5,6,10,12,15,20,30,60],C0=$0,qJ=-1,i6=0,xJ=[];function s6(J){if(!Number.isFinite(J)||J<=0)return $0;let Q=E1[0];for(let $ of E1)if(Math.abs($-J)Q.at<=qJ).sort((Q,$)=>Q.at-$.at||Q.seq-$.seq);if(J.length===0)return;xJ=xJ.filter((Q)=>Q.at>qJ);for(let Q of J)Q.cb()}var N0=0.12,Z0=FJ;function n6(J){Z0=J===void 0?FJ:J&65535}function t6(){Z0=FJ}function T1(J){let Q=Math.max(-1,Math.min(1,(J-128)/127)),$=Math.abs(Q);if($>8&255)}function J8(){return T1(Z0&255)}var f1=new Set,Q8=0;function $8(){f1.clear(),Q8=0,t6()}function Z8(J){for(let Q of[...f1])Q(J)}var MJ=null,x1=null;function q8(J,Q,$){let Z="";for(let q=0;q<$;q++)Z+=String.fromCharCode(J[Q+q]);return Z}function v1(J){let Q=new DataView(J);if(J.byteLength=J.length&&$.slice(0,J.length)===J)Q.push($);return Q.sort(),Q}function vJ(J){E0();let Q=MJ?MJ.get(J):void 0;if(!Q)throw Error("pak: missing key "+J+" (no __pak provided, or the pack is incomplete)");return x1.slice(Q.off,Q.off+Q.len)}var mJ=null,N=null,UJ=null,q0=0,RJ=[],y0=[],T0=[];function u1(J){if(mJ=J,N=null,UJ=null,q0=0,RJ.length=0,y0.length=0,T0.length=0,c){if(c.pressTarget=null,c.target=null,c.spriteDirty=!0,c.fresh=!0,c.vw=0,c.tex>=0){let Q=h();Q.setCursor?.(-1,0,0,0,0),Q.freeTexture?.(c.tex),c.tex=-1}}}function Y8(J,Q){J.onPress=Q??void 0}function z8(J,Q){if(J.focusable=Q,i1(),!Q&&N===J)v(null)}function v(J){if(UJ&&UJ!==J)PJ(null);N=J,h().setFocus(J?J.id:0)}function PJ(J){if(UJ===J)return;let Q=h();if(UJ)Q.setActive?.(UJ.id,0);if(UJ=J,J)Q.setActive?.(J.id,1)}function f0(){return RJ.length>0?RJ[RJ.length-1]:mJ}function x0(J,Q){if(!J)return;if(J.focusable)Q.push(J);if(!Array.isArray(J.children))return;for(let $=0;$=$.length)return;v($[q])}function B8(){if(!N)return null;let J=f0();if(J&&!s(N,J))return null;for(let Q=y0.length-1;Q>=0;Q--){let $=y0[Q];if(J&&!s($.node,J)&&!s(J,$.node))continue;if(s(N,$.node))return $}return null}function H8(J){let Q=B8();if(!Q)return!1;let $=[];if(x0(Q.node,$),$.length===0){if(N)v(null);return!0}let Z=Q.columns,q=N?$.indexOf(N):-1;if(q<0)return v(l1(J)===1?$[0]:$[$.length-1]),!0;let X=q;switch(J){case"right":if(q+1<$.length&&q%Z0)X=q-1;else if(Q.wrap)X=Math.min($.length-1,Math.floor(q/Z)*Z+Z-1);break;case"down":if(q+Z<$.length)X=q+Z;else if(Q.wrap)X=q%Z;break;case"up":if(q-Z>=0)X=q-Z;else if(Q.wrap){X=q%Z;while(X+Z<$.length)X+=Z}break}if(X!==q)v($[X]);return!0}function j8(){if(!N)return null;let J=f0();if(J&&!s(N,J))return null;for(let Q=T0.length-1;Q>=0;Q--){let $=T0[Q];if(J&&!s($.node,J)&&!s(J,$.node))continue;if(s(N,$.node))return $}return null}function X0(J){let Q=j8();if(Q&&Q.move(J))return;if(H8(J))return;W8(J)}function c1(){p1(N)}function p1(J){let Q=J;while(Q){if(Q.onPress){Q.onPress();return}Q=Q.parent}}function Y0(J){PJ(J)}function K8(J){v(J),p1(J)}function s(J,Q){if(!J||!Q)return!1;let $=J;while($){if($===Q)return!0;$=$.parent}return!1}function v0(J){if(!J)return null;if(J.focusable)return J;if(!Array.isArray(J.children))return null;for(let Q=0;Q=0;q--){let X=v0(Q.children[q]);if(X){v(X);return}}let Z=Q;while(Z){if(Z.focusable){v(Z);return}Z=Z.parent}}v(null)}var c=null,d1=0;function i1(){d1++}var M8=[1,3,5,9,17,33,65,129,257,513,1985,73,149,147,288,480],U8=[0,0,2,6,14,30,62,126,254,510,62,54,98,96,192,0];function R8(){let J=new Uint8Array(1024);for(let Q=0;Q<16;Q++)for(let $=0;$<16;$++){let Z=M8[Q]>>$&1,q=U8[Q]>>$&1;if(!Z&&!q)continue;let X=(Q*16+$)*4,z=q?255:0;J[X]=z,J[X+1]=z,J[X+2]=z,J[X+3]=255}return J}function P8(J,Q){let $=J.sprite;J.spriteDirty=!1;let Z=J.tex,q=-1,X=null;if(typeof $.image==="string")try{X=vJ($.image)}catch(z){if(gJ().strict)throw z;X=null}else if($.image)X=$.image;if(X){if(q=Q.uploadImgEntry?Q.uploadImgEntry(X):D8(Q,X),q<0&&gJ().strict)throw Error("enableCursor: cursor image rejected (malformed or RLE-only IMG entry)")}if(q<0)q=Q.uploadTexture(R8(),16,16,E6.PSM_8888);if(J.tex=q,Q.setCursor(q,$.hotspot[0],$.hotspot[1],$.size[0],$.size[1]),Z>=0&&Z!==q)Q.freeTexture?.(Z)}function D8(J,Q){if(Q.length<8)return-1;let $=new DataView(Q.buffer,Q.byteOffset,Q.byteLength);if(Q[5]&h6)return-1;return J.uploadTexture(Q.subarray(8),$.getUint16(0,!0),$.getUint16(2,!0),Q[4])}function z0(J,Q){if(!J||Q===0)return null;if(J.id===Q)return J;let $=J.children;if(!Array.isArray($))return null;for(let Z=0;Z<$.length;Z++){let q=z0($[Z],Q);if(q)return q}return null}var G0=null;function s1(J){G0=J}function a1(J){let Q=RJ.length>0?RJ[RJ.length-1]:null,$=J;while($){if($.focusable&&(!Q||s($,Q)))return $;$=$.parent}return null}function r1(J,Q,$){return a1(o1(J,Q,$))}function o1(J,Q,$){if($!==void 0)return $===0?null:z0(G0??mJ,$);let Z=h(),q=Z.hitTestBounds??Z.hitTest;if(!q)return null;return z0(G0??mJ,q(J,Q))}function V8(J,Q,$){let Z=c,q=h();if(!q.hitTest||!q.setCursor||!q.setCursorPos)return!1;if(Z.vw===0){let B=C1(q);if(Z.vw=B?B.w:b1,Z.vh=B?B.h:g1,Z.x<0)Z.x=Math.floor(Z.vw/2),Z.y=Math.floor(Z.vh/2)}if(Z.spriteDirty)P8(Z,q);let X=e6()*Z.speed,z=J8()*Z.speed;if(Z.dpadSpeed>0&&X===0&&z===0){if(J&i.LEFT)X=-Z.dpadSpeed;if(J&i.RIGHT)X=Z.dpadSpeed;if(J&i.UP)z=-Z.dpadSpeed;if(J&i.DOWN)z=Z.dpadSpeed}let W=Z.fresh;if(X!==0||z!==0){let B=a6()/60,_=Math.min(Math.max(Z.x+X*B,0),Z.vw-1),g=Math.min(Math.max(Z.y+z*B,0),Z.vh-1);if(_!==Z.x||g!==Z.y)Z.x=_,Z.y=g,W=!0}if(W)q.setCursorPos(Z.x,Z.y);let R=(Q|$)&Z.button,D=d1;if(W||R!==0||D!==Z.gen)Z.gen=D,Z.fresh=!1,Z.target=a1(z0(G0??mJ,q.hitTest(Z.x,Z.y)));let V=Z.target;if(V!==N)v(V);if(Q&Z.button&&V)Z.pressTarget=V;if(Z.pressTarget){if(PJ(V===Z.pressTarget?Z.pressTarget:null),$&Z.button){let B=V===Z.pressTarget;if(Z.pressTarget=null,PJ(null),B)c1()}}else if($&Z.button)PJ(null);return!0}function L8(J){let Q=J&~q0,$=q0&~J;if(q0=J,c&&V8(J,Q,$))return;if($&i.CIRCLE)PJ(null);if(Q===0)return;if(Q&i.DOWN)X0("down");if(Q&i.RIGHT)X0("right");if(Q&i.UP)X0("up");if(Q&i.LEFT)X0("left");if(Q&i.CIRCLE)PJ(N),c1()}var m0=null;function n1(J){m0=J}function wJ(){if(i1(),m0)m0()}function O8(J,Q){J.debugName=Q||void 0,wJ()}var XJ={id:g6,type:KJ.view,parent:null,children:[],domNodeType:1,domTag:"root"},t1=Symbol.for("pocketjs.native-node"),u0=1,uJ=3,n=8,I8=new Set(["class","className","style","src","onPress","on:press","focusable","debugName","ref","nodeRef","key","children"]);function W0(J){return J.domAttrs??={}}function e1(J,Q){let $=J.domNodeType??(lJ(J)?uJ:u0),Z=$===uJ?B0(J.text??""):$===n?k8(J.domData??""):Y4(J.domTag??j0(J));for(let q of Object.keys(J.domAttrs??{}))l0(Z,q,J.domAttrs[q]);if(Q)for(let q of J.children)YJ(Z,e1(q,!0));return Z}function l0(J,Q,$){if(I8.has(Q)){NJ(J,Q,$,J.domAttrs?.[Q]);return}if($==null)delete W0(J)[Q];else W0(J)[Q]=$}function c0(J){if(J[t1]===!0)return J;return Object.defineProperty(J,t1,{value:!0}),Object.defineProperties(J,{nodeType:{configurable:!0,get(){return J.domNodeType??(lJ(J)?uJ:u0)}},nodeValue:{configurable:!0,get(){return J.domNodeType===n?J.domData??"":J.text??""},set($){if(J.domNodeType===n)J.domData=String($??"");else H0(J,String($??""))}},data:{configurable:!0,get(){return J.domNodeType===n?J.domData??"":J.text??""},set($){if(J.domNodeType===n)J.domData=String($??"");else H0(J,String($??""))}},textContent:{configurable:!0,get(){if(J.domNodeType===n)return J.domData??"";if(lJ(J))return J.text??"";return J.children.map(($)=>$.text??"").join("")},set($){let Z=String($??"");if(J.domNodeType===n)J.domData=Z;else if(lJ(J))H0(J,Z);else if(E8(J),Z)YJ(J,B0(Z))}},parentNode:{configurable:!0,get(){return J.parent}},parentElement:{configurable:!0,get(){return J.parent}},childNodes:{configurable:!0,get(){return J.children}},firstChild:{configurable:!0,get(){return J.children[0]??null}},lastChild:{configurable:!0,get(){return J.children[J.children.length-1]??null}},nextSibling:{configurable:!0,get(){return G4(J)??null}},previousSibling:{configurable:!0,get(){let $=J.parent;if(!$)return null;let Z=$.children.indexOf(J);return Z>0?$.children[Z-1]:null}},tagName:{configurable:!0,get(){return(J.domTag??j0(J)).toUpperCase()}},nodeName:{configurable:!0,get(){if(J.domNodeType===uJ)return"#text";if(J.domNodeType===n)return"#comment";return(J.domTag??j0(J)).toUpperCase()}},className:{configurable:!0,get(){return String(J.domAttrs?.class??"")},set($){NJ(J,"class",$,J.domAttrs?.class)}},isConnected:{configurable:!0,get(){let $=J;while($){if($===XJ)return!0;$=$.parent}return!1}}}),Object.assign(J,{appendChild($){return YJ(J,$),$},insertBefore($,Z){return YJ(J,$,Z??null),$},removeChild($){return cJ(J,$),$},replaceChild($,Z){return YJ(J,$,Z),cJ(J,Z),Z},cloneNode($=!1){return e1(J,!!$)},remove(){if(J.parent)cJ(J.parent,J)},setAttribute($,Z){l0(J,$,Z)},removeAttribute($){l0(J,$,void 0)},getAttribute($){let Z=J.domAttrs?.[$];return Z==null?null:String(Z)},hasAttribute($){return J.domAttrs?.[$]!=null},hasChildNodes(){return J.children.length>0},contains($){let Z=$??null;while(Z){if(Z===J)return!0;Z=Z.parent}return!1},addEventListener(){},removeEventListener(){}},{style:{length:0,item:()=>""},classList:{add(){},remove(){}}}),J}c0(XJ);var p0=null;function A8(J){p0=J}var d0={unknownClass:0,unknownTexture:0},J4=new Map;function Q4(J,Q){J4.set(J,Q)}var $4=new Map;function Z4(J,Q){$4.set(J,Q)}var CJ=new Set,_8=new Set;function q4(J){if(!J)return!1;if(_8.has(J))return!0;if(J.children){for(let Q=0;Q - only view/text/image exist`);return c0({id:h().createNode(Q),type:Q,parent:null,children:[],domNodeType:u0,domTag:J})}function B0(J){let Q=h(),$=Q.createNode(KJ.text);return Q.setText($,J),c0({id:$,type:KJ.text,parent:null,children:[],text:J,domNodeType:uJ,domTag:"#text"})}function k8(J=""){let Q=B0("");return Q.domNodeType=n,Q.domTag="#comment",Q.domData=J,Q}function H0(J,Q){h().replaceText(J.id,Q),J.text=Q,wJ()}function lJ(J){return J.type===KJ.text}function z4(J){let Q=J.parent;if(!Q)return;let $=Q.children.indexOf(J);if($>=0)Q.children.splice($,1);J.parent=null}function YJ(J,Q,$){let Z=h();if(z4(Q),CJ.delete(Q),Z.insertBefore(J.id,Q.id,$?$.id:0),$){let q=J.children.indexOf($);if(q<0)throw Error("PocketJS: insert anchor is not a child of parent");J.children.splice(q,0,Q)}else J.children.push(Q);Q.parent=J,wJ()}function cJ(J,Q){if(!Q)return;F8(Q),h().removeChild(J.id,Q.id),z4(Q),CJ.add(Q),wJ()}function S8(J){return J.parent??void 0}function b8(J){return J.children[0]}function G4(J){let Q=J.parent;if(!Q)return;let $=Q.children.indexOf(J);return $>=0?Q.children[$+1]:void 0}function g8(J,Q){let $=h();if(wJ(),Q==null||Q===""){$.setStyle(J.id,w6);return}if(typeof Q!=="string")throw Error("PocketJS: class must be a string literal of utilities");let Z=p0?p0(Q):void 0;if(Z===void 0){if(gJ().strict)throw Error(`PocketJS: unknown class "${Q}" - not in the compiled style table (dynamic classes must be ternaries of full literals)`);d0.unknownClass++;return}$.setStyle(J.id,Z)}function w8(J,Q){let $=h();if(Q==null||Q===""){$.setImage(J.id,-1);return}if(typeof Q!=="string")throw Error("PocketJS: src must be a string key");let Z=J4.get(Q);if(Z===void 0){if(gJ().strict)throw Error(`PocketJS: unknown image src "${Q}" - no texture registered under that key`);d0.unknownTexture++;return}$.setImage(J.id,Z)}function C8(J,Q){let $=h();if(Q==null||Q===""){$.setSprite(J.id,-1,0,0,0);return}if(typeof Q!=="string")throw Error("PocketJS: sprite must be a string key");let Z=$4.get(Q);if(Z===void 0){if(gJ().strict)throw Error(`PocketJS: unknown sprite "${Q}" - no sprite atlas registered under that key`);d0.unknownTexture++;return}$.setSprite(J.id,Z.handle,Z.frames,Z.cols,Z.step)}function N8(J,Q,$){let Z=h(),q=Q??{},X=$??{},z=!1;for(let W in q){let R=q[W];if(X[W]===R)continue;let D=C6[W];if(D===void 0)throw Error(`PocketJS: unknown style prop '${W}' (see spec PROP)`);Z.setProp(J.id,D,d6(W,R)),z=!0}if(z)wJ()}function NJ(J,Q,$,Z){if($===Z&&Q!=="style")return $;if(Q==="className")Q="class";if(Q!=="children"&&Q!=="key"&&Q!=="ref"&&Q!=="nodeRef")if($==null)delete W0(J)[Q];else W0(J)[Q]=$;switch(Q){case"class":return g8(J,$),$;case"onPress":case"on:press":return Y8(J,$),$;case"src":return w8(J,$),$;case"sprite":return C8(J,$),$;case"style":return N8(J,$,Z),$;case"focusable":return z8(J,!!$),$;case"debugName":return O8(J,$==null?void 0:String($)),$;case"ref":case"nodeRef":case"key":case"children":return $;default:break}if(Q==="classList")throw Error("PocketJS: classList is not supported - use ternaries of full class literals");if(Q.startsWith("on:")||Q.startsWith("bool:")||Q.startsWith("prop:"))throw Error(`PocketJS: unsupported namespaced attribute '${Q}'`);throw Error(`PocketJS: unknown property '${Q}' on <${j0(J)}>`)}function E8(J){for(let Q of[...J.children])cJ(J,Q)}function j0(J){for(let Q of Object.keys(KJ))if(KJ[Q]===J.type)return Q;return String(J.type)}function h8(J,Q,$,Z){if(Q==="ref"&&typeof $==="function"){$(J);return}NJ(J,Q,$,Z)}var y8=b6({createElement:Y4,createTextNode:B0,replaceText:H0,isTextNode:lJ,setProperty:h8,insertNode(J,Q,$){YJ(J,Q,$)},removeNode(J,Q){cJ(J,Q)},getParentNode:S8,getFirstChild:b8,getNextSibling:G4}),{render:T8,effect:pQ,memo:y,createComponent:Y,createElement:W4,insert:dQ,spread:f8,mergeProps:iQ,use:sQ}=y8,aQ={linear:l.Easing.Linear,in:l.Easing.EaseIn,out:l.Easing.EaseOut,"in-out":l.Easing.EaseInOut,"out-back":l.Easing.OutBack,spring:l.Easing.Spring,"spring-bouncy":l.Easing.SpringBouncy},x8=null;function B4(J){x8=J}function v8(J,Q){if(!J)return;if(typeof J==="function")J(Q);else if("current"in J)J.current=Q}function H4(J,Q){let $=W4(J);return f8($,Q,!1),v8(Q.nodeRef,$),$}function j(J){return H4("view",J)}function U(J){return H4("text",J)}var rQ=new WeakMap,oQ=new WeakMap,m8={}!==null?Object.freeze({...{}}):Object.freeze({}),nQ=Object.freeze({target:"",pixelRatio:Number.isInteger(1)?1:1,features:m8}),tQ=new Map,zJ=36000,i0=30,u8=30,G={ops:null,transport:null,app:void 0,frame:0,tape:new Uint16Array(zJ),tapeAnalog:new Uint16Array(zJ),tapeTouch:null,tapeStart:0,tapeLen:0,tapeFirstFrame:0,replayMasks:null,replayAnalog:null,replayTouch:null,replayAt:0,paused:!1,stepQueued:0,inspectReportId:null,inspectAskedAt:0,treeDirty:!0,treeSentAt:-i0,saidHello:!1,hostCalls:0};function l8(J){let Q=globalThis;if(!Q.console)Q.console={log(){},warn(){},error(){}};G.ops=J,G.frame=0,G.tapeStart=0,G.tapeLen=0,G.tapeFirstFrame=0,G.tapeTouch=null,G.replayMasks=null,G.replayAnalog=null,G.replayTouch=null,G.paused=!1,G.stepQueued=0,G.inspectReportId=null,G.inspectAskedAt=0,G.treeDirty=!0,G.treeSentAt=-i0,G.saidHello=!1,G.hostCalls=0,G.app=globalThis.__pocketApp;let $=globalThis.__pocketDevtoolsTransport;if($)G.transport=$;else if(J.__dbgActive?.()&&J.__dbgPoll&&J.__dbgSend)G.transport={send:(Z)=>J.__dbgSend(Z),recv:()=>J.__dbgPoll(),everyFrames:10};else G.transport=null;if(G.transport)n1(()=>{G.treeDirty=!0}),o8();else n1(null);globalThis.__pocketDevtools=t8}function c8(J){return(Q,$,Z,q)=>{if(G.hostCalls++,G.transport)d8(),a8();let X=Q,z=$===void 0?FJ:$&65535,W=Z,R=q;if(G.replayMasks)if(G.replayAt0?$.slice(0,8):null;if(Z&&!G.tapeTouch)G.tapeTouch=Array(zJ).fill(null);if(G.tapeLen1||Q.length===1&&Q[0][0]!==FJ)J.analog=Q;if(G.tapeTouch){let $=[];for(let Z=0;Z0)J.v=2,J.touch=$}return J}function F4(J,Q,$){let Z=new Uint16Array($).fill(Q),q=0;for(let[X,z]of J)Z.fill(X,q,Math.min(q+z,$)),q+=z;return Z}function M4(J){let Q=0;for(let[,$]of J.masks)Q+=$;return F4(J.masks,0,Q)}function U4(J){let Q=0;for(let[,$]of J.masks)Q+=$;return F4(J.analog??[],FJ,Q)}function R4(J){let Q=0;for(let[,Z]of J.masks)Q+=Z;let $=Array(Q).fill(void 0);for(let[Z,q]of J.touch??[])if(Z>=0&&Z1&&G.hostCalls%Q!==0)return;if(!G.saidHello)G.saidHello=!0,f({t:"hello",app:G.app,host:n8(),frame:G.frame});for(let $=0;$<64;$++){let Z=J.recv();if(!Z)break;for(let q of Z.split(` -`))if(q.trim())i8(q)}}function i8(J){let Q;try{Q=JSON.parse(J)}catch{return}let $=G.ops;switch(Q.t){case"inspect":{let Z=typeof Q.id==="number"?Q.id:0;if($?.debugInspect?.(Z),G.inspectReportId=Z||null,G.inspectAskedAt=G.hostCalls,!Z)f({t:"inspect",id:0,rect:null});break}case"pause":G.paused=!0,G.stepQueued=0,$?.debugPause?.(!0),s0();break;case"resume":G.paused=!1,$?.debugPause?.(!1),s0();break;case"step":G.stepQueued+=typeof Q.n==="number"&&Q.n>0?Q.n:1;break;case"getTree":P4();break;case"eval":{let Z=!0,q;try{q=K0((0,eval)(String(Q.code)))}catch(X){Z=!1,q=X instanceof Error?`${X.name}: ${X.message}`:String(X)}f({t:"evalResult",id:Q.id,ok:Z,value:q});break}case"dumpTape":f({t:"tape",tape:K4()});break;case"devStats":{let Z=null,q=$?.debugStats?.();if(q)try{Z=JSON.parse(q)}catch{Z=null}f({t:"devStats",frame:G.frame,data:Z});break}case"screenshot":{if($?.__dbgShot?.())f({t:"screenshotRaw",file:"shot.raw",w:480,h:272,stride:512,frame:G.frame});else f({t:"log",level:"warn",args:["screenshot: not supported on this host"]});break}case"replay":{let Z=Q.tape;if(Z&&Array.isArray(Z.masks))G.replayMasks=M4(Z),G.replayAnalog=Z.analog?U4(Z):null,G.replayTouch=Z.touch?R4(Z):null,G.replayAt=0;break}default:break}}function s8(){if(G.treeDirty&&G.frame-G.treeSentAt>=i0)P4();if(G.frame%u8===0)s0()}function a8(){let J=G.inspectReportId;if(J==null)return;let Q=G.ops;if(!Q?.debugRectXY||!Q.debugRectWH){G.inspectReportId=null;return}let $=Q.debugRectXY();if($===-1){if(G.hostCalls-G.inspectAskedAt>60)G.inspectReportId=null,f({t:"inspect",id:J,rect:null});return}let Z=Q.debugRectWH();G.inspectReportId=null,f({t:"inspect",id:J,rect:[$<<16>>16,$>>16,Z&65535,Z>>16&65535]})}function s0(){f({t:"stats",frame:G.frame,nodes:L4(XJ),tapeLen:G.tapeLen,paused:G.paused})}function P4(){G.treeDirty=!1,G.treeSentAt=G.frame,f({t:"tree",frame:G.frame,root:V4(XJ)})}function r8(J){if(J==null||typeof J!=="object")return!1;let Q=J;return typeof Q.id==="number"&&typeof Q.type==="number"}function a0(J,Q){if(Array.isArray(J)){for(let $ of J)a0($,Q);return}if(r8(J)){Q(J);return}if(J!=null&&typeof J==="object"){let $=J.nodes;if($!==void 0)a0($,Q)}}function D4(J,Q){let $=Array.isArray(J.children)?J.children:[];for(let Z of $)a0(Z,Q)}function V4(J){let Q={i:J.id,t:J.domTag??String(J.type)};if(J.debugName)Q.n=J.debugName;let $=J.domAttrs?.class;if(typeof $==="string"&&$)Q.c=$;if(J.text)Q.x=J.text.length>80?J.text.slice(0,79)+"…":J.text;let Z=[];if(D4(J,(q)=>{if(q.domNodeType===8)return;Z.push(V4(q))}),Z.length)Q.k=Z;return Q}function L4(J){let Q=1;return D4(J,($)=>{Q+=L4($)}),Q}function o8(){let J=globalThis;if(!J.console)J.console={};let Q=J.console;if(Q.__pocketBridged)return;Q.__pocketBridged=!0;for(let $ of["log","warn","error"]){let Z=Q[$];Q[$]=(...q)=>{f({t:"log",level:$,args:q.map((X)=>K0(X))}),Z?.apply(Q,q)}}}function K0(J,Q=0){if(J===void 0)return"undefined";if(J===null)return"null";let $=typeof J;if($==="string"){let X=J;return Q===0?O4(X):JSON.stringify(O4(X))}if($==="number"||$==="boolean"||$==="bigint")return String(J);if($==="function"){let X=J.name;return X?`[function ${X}]`:"[function]"}if(Q>=3)return Array.isArray(J)?"[…]":"{…}";if(Array.isArray(J)){let X=J.slice(0,20).map((z)=>K0(z,Q+1));if(J.length>20)X.push(`… ${J.length-20} more`);return`[${X.join(", ")}]`}if(J instanceof Error)return`${J.name}: ${J.message}`;return`{${Object.entries(J).slice(0,20).map(([X,z])=>`${X}: ${K0(z,Q+1)}`).join(", ")}}`}function O4(J){return J.length>200?J.slice(0,199)+"…":J}function n8(){let J=G.ops;if(typeof J?.__host==="string")return J.__host;if(J?.__textures!==void 0)return"psp";if(typeof globalThis.document<"u")return"web";return"headless"}var t8={get frame(){return G.frame},dumpTape:()=>K4(),replay:(J)=>{G.replayMasks=M4(J),G.replayAnalog=J.analog?U4(J):null,G.replayTouch=J.touch?R4(J):null,G.replayAt=0}},I4=new Map,r0=new Map;function A4(J){return J.trim().replace(/\s+/g," ")}function _4(J){return J.split(" ").sort().join(" ")}var k4=-1;function e8(J){for(let Q of Object.keys(J)){let $=J[Q],Z=A4(Q);I4.set(Z,$);let q=_4(Z),X=r0.get(q);r0.set(q,X!==void 0&&X!==$?k4:$)}}function J2(J){let Q=A4(J),$=I4.get(Q);if($!==void 0)return $;let Z=r0.get(_4(Q));return Z===k4?void 0:Z}var o0=9,Q2=(1<{let q=($&Z2)!==0,X=q?n0:o0,z=q?q2:Q2;return Object.freeze({id:$>>>(q?X2:$2)&255,x:$&z,y:$>>>X&z,hit:Q?.[Z]})}))}function z2(){return F0}function G2(){F0=t0}var S4=8,GJ=8,W2=3,B2=8,H2=6,j2=0.5,a=1,pJ=2,e0=4,DJ=8,EJ=[],b4=0,M0=0,WJ=Array.from({length:S4},(J,Q)=>({slot:Q,used:!1,present:!1,id:0,x:0,y:0,startX:0,startY:0,dx:0,dy:0,fdx:0,fdy:0,vx:0,vy:0,downFrame:0,frames:0,histX:new Int16Array(GJ),histY:new Int16Array(GJ),histHead:0,histLen:0,owners:[],claimedBy:null}));function K2(J,Q){let $=J;while($){if($===Q)return!0;$=$.parent}return!1}function F2(J,Q,$,Z){let q=J.opts.region;if(!q)return!0;let X=q.node?.();if(X){if(Z.hit===void 0)Z.hit=o1(Q,$,Z.fact);let W=Z.hit;if(W)return K2(W,X)}let z=q.rect?.();if(!z)return!1;return Q>=z.x&&Q=z.y&&$=0;z--){let W=EJ[z];if(W.disposed)continue;if(b4>0&&!W.opts.allowWhenBlocked)continue;if(!F2(W,$,Z,X))continue;W.flags[J.slot]=a,J.owners.push(W)}for(let z of J.owners)z.opts.onDown?.(J)}function R2(J,Q,$){if(J.present=!0,J.fdx=Q-J.x,J.fdy=$-J.y,J.x=Q,J.y=$,J.dx=Q-J.startX,J.dy=$-J.startY,J.frames++,J.histX[J.histHead]=Q,J.histY[J.histHead]=$,J.histHead=(J.histHead+1)%GJ,J.histLenz||Z>z))q.flags[J.slot]|=pJ}if(!J.claimedBy)for(let q of J.owners){let X=q.flags[J.slot];if(!(X&a)||X&(pJ|e0))continue;if(!q.opts.onLongPress)continue;let z=Math.max(1,Math.round((q.opts.longPressSeconds??j2)*h1()));if(J.frames=0)EJ.splice($,1)},cancel(){if(!Q.disposed)C4(Q)},get panning(){for(let $ of WJ)if($.used&&Q.flags[$.slot]&DJ)return!0;return!1}}}function O2(J){let Q=L2(J);return k0(()=>Q.dispose()),Q}function N4(){EJ.length=0,b4=0,M0=0;for(let J of WJ)J.used=!1,J.present=!1,J.owners.length=0,J.claimedBy=null}function I2(){return O2({onDown:(J)=>{let Q=r1(J.x,J.y,J.hit);if(Q)Y0(Q)},onTap:(J)=>{Y0(null);let Q=r1(J.x,J.y,J.hit);if(Q)K8(Q)},onUp:()=>Y0(null),onCancel:()=>Y0(null)})}var A2=1,J1=new Map,U0=[];function _2(){let J=globalThis.__pocketEffectTrace;return typeof J==="function"?J:null}function k2(){A2=1,J1.clear(),U0=[]}function S2(){if(U0.length===0)return;let J=U0;U0=[];for(let{id:Q,result:$}of J){let Z=J1.get(Q);if(!Z)continue;J1.delete(Q),_2()?.({t:"delivery",frame:y1(),id:Q,kind:Z.kind}),Z.onResult($)}}var E4=new Set;function b2(){if(E4.size===0)return;for(let J of E4)J()}var g2={"h-[108] px-[10] py-4 flex-row gap-[10] bg-slate-950":0,"w-[165] h-[76] items-center justify-center bg-orange-600":1,"w-[165] h-[76] items-center justify-center bg-slate-900":2,"text-base text-white font-bold":3,"flex-col w-full h-full bg-slate-50":4,"h-[686] px-6 pt-7 flex-col gap-[22]":5,"w-[584] h-[318] px-6 py-5 flex-col rounded-xl shadow bg-white border-slate-100":6,"h-[30] flex-row items-center gap-3":7,"w-[10] h-[10] rounded bg-orange-500":8,"text-lg text-orange-600 font-bold":9,"h-[84] pt-3 text-xl text-slate-900":10,"h-[2] mx-1 my-4 bg-slate-100":11,"w-[10] h-[10] rounded bg-emerald-500":12,"text-lg text-emerald-700 font-bold":13,"absolute left-[628] top-[28] w-[68] h-[132] items-center justify-center bg-orange-100":14,"absolute left-[628] top-[512] w-[68] h-[132] items-center justify-center bg-orange-100":15,"h-[228] mx-6 px-6 py-5 flex-col rounded-xl shadow bg-white border-slate-100":16,"text-base text-slate-600 font-bold":17,"pt-6 text-lg text-slate-500":18,"pt-6 text-lg text-slate-900 font-bold":19,"h-[146] px-6 pt-7 flex-col bg-slate-50":20,"h-[80]":21,"h-[1060] px-6 pt-5 flex-col":22,"h-[48] px-4 justify-center bg-slate-100":23,"text-base text-slate-600":24,"pt-[10] flex-col gap-[12]":25,"w-[584] h-[92] px-[18] flex-row items-center gap-5 bg-white":26,"w-[48] h-[48] items-center justify-center bg-blue-100":27,"w-[48] h-[48] items-center justify-center bg-emerald-100":28,"text-lg text-blue-700 font-bold":29,"w-[474] flex-col gap-2":30,"text-lg text-slate-900 font-bold":31,"text-base text-slate-500":32,"pt-20 pl-10 text-lg text-slate-500":33,"absolute left-[628] top-[78] w-[68] h-[132] items-center justify-center bg-orange-100":34,"absolute left-[628] top-[828] w-[68] h-[132] items-center justify-center bg-orange-100":35,"h-[1060] px-6 pt-7 flex-col gap-4":36,"text-base text-slate-500 font-bold":37,"h-[214] px-7 items-center justify-center bg-slate-100":38,"text-lg text-slate-500 font-bold":39,"w-[672] h-[150] px-6 flex-row items-center justify-between bg-white":40,"flex-row items-center gap-5":41,"w-[68] h-[68] items-center justify-center bg-orange-100":42,"text-xl text-orange-700 font-bold":43,"w-[500] flex-col gap-2":44,"text-xl text-slate-900 font-bold":45,"text-lg text-slate-600":46,"text-2xl text-orange-600":47,"mt-4 h-[112] px-6 justify-center bg-slate-100":48,"relative h-[154] px-5 pt-5 flex-col bg-white":49,"pt-3 text-lg text-orange-600 font-bold":50,"pt-3 text-base text-slate-500":51,"absolute left-[456] top-[14] w-[196] h-[72] items-center justify-center bg-orange-600":52,"text-lg text-white font-bold":53,"h-[40] pt-3 text-base text-slate-500":54,"h-[470] flex-col gap-2":55,"w-[584] h-[84] px-5 flex-row items-center justify-between bg-white":56,"absolute left-[604] top-[0] w-[68] h-[132] items-center justify-center bg-orange-100":57,"absolute left-[604] top-[320] w-[68] h-[132] items-center justify-center bg-orange-100":58,"h-[160] px-5 pt-5 flex-col bg-white":59,"pt-3 text-lg text-slate-900 font-bold":60,"pt-3 text-base text-slate-600":61,"h-[108] pt-7 flex-row gap-4":62,"w-[316] h-[80] items-center justify-center bg-slate-100":63,"w-[340] h-[80] items-center justify-center bg-red-100":64,"text-lg text-red-500 font-bold":65,"h-[1052] px-6 pt-5 flex-col":66,"h-[270] px-[22] pt-6 bg-white":67,"text-lg text-slate-900":68,"text-lg text-slate-400":69,"h-[86] px-1 flex-row items-center justify-between":70,"w-[132] h-[58] items-center justify-center bg-slate-300":71,"w-[132] h-[58] items-center justify-center bg-slate-100":72,"text-base text-slate-900 font-bold":73,"h-[140] flex-row gap-2":74,"grow h-[120] items-center justify-center bg-slate-300":75,"grow h-[120] items-center justify-center bg-white":76,"w-[104] h-[120] items-center justify-center bg-slate-300":77,"w-[104] h-[120] items-center justify-center bg-slate-100":78,"h-[176] flex-row gap-2":79,"w-[92] h-[156] items-center justify-center bg-slate-300":80,"w-[92] h-[156] items-center justify-center bg-slate-100":81,"w-[300] h-[156] items-center justify-center bg-slate-300":82,"w-[300] h-[156] items-center justify-center bg-slate-100":83,"w-[144] h-[156] items-center justify-center bg-slate-300":84,"w-[144] h-[156] items-center justify-center bg-slate-100":85,"w-[112] h-[156] items-center justify-center bg-emerald-700":86,"w-[112] h-[156] items-center justify-center bg-emerald-500":87,"text-base text-slate-950 font-bold":88,"h-[108] px-6 py-3 flex-col bg-slate-50":89,"h-[84] items-center justify-center bg-slate-300":90,"h-[84] items-center justify-center bg-slate-100":91,"h-[1168] px-6 pt-5 flex-col":92,"h-[82] px-4 justify-center bg-white":93,"w-[584] h-[900] px-5 pt-5 bg-slate-950":94,"text-base text-slate-200":95,"absolute left-[628] top-[58] w-[68] h-[132] items-center justify-center bg-orange-100":96,"w-[584] h-[900] px-5 pt-5 bg-white":97,"text-xl text-slate-900":98,"w-full h-[150] px-6 flex-col justify-center gap-3 rounded-xl bg-red-100":99,"w-full h-[150] px-6 flex-col justify-center gap-3 rounded-xl bg-emerald-100":100,"w-full h-[150] px-6 flex-col justify-center gap-3 rounded-xl bg-amber-100":101,"w-full h-[150] px-6 flex-col justify-center gap-3 rounded-xl bg-orange-100":102,"text-xl text-red-700 font-bold":103,"text-xl text-emerald-700 font-bold":104,"text-xl text-amber-700 font-bold":105,"h-[602] px-6 flex-col":106,"h-[526] px-6 py-6 flex-col gap-5 rounded-xl shadow bg-white border-slate-100":107,"h-[2] bg-slate-100":108,"h-[166] px-6 pt-4":109,"h-[168] px-6 pt-6 pb-6":110,"w-full h-[120]":111,"h-[66]":112,"relative flex-col w-full h-full bg-slate-50 overflow-hidden":113,"absolute inset-0 z-50 flex-col items-center justify-center":114,"absolute inset-0 bg-slate-950":115,"flex-col gap-2 w-[328] p-3 rounded-xl shadow-lg bg-white border-slate-200":116,"absolute left-3 right-3 bottom-3 flex-row items-center justify-between px-2 py-1 rounded-lg shadow-md bg-white border-slate-200":117,"flex-row flex-wrap":118,grow:119,"text-2xl text-white font-bold":120,"text-2xl text-slate-950 font-bold":121,"px-3 py-2 rounded-lg bg-slate-100":122,"px-3 py-2 rounded-lg bg-indigo-100":123,"text-base text-indigo-700 font-bold":124,"px-3 py-2 rounded-lg bg-emerald-100":125,"text-base text-emerald-700 font-bold":126,"px-3 py-2 rounded-lg bg-amber-100":127,"text-base text-amber-700 font-bold":128,"px-3 py-2 rounded-lg bg-red-100":129,"text-base text-red-500 font-bold":130,"w-[34] h-[34] rounded-lg bg-amber-400":131,"w-[34] h-[34] rounded-lg bg-red-500":132,"w-[34] h-[34] rounded-lg bg-slate-800":133,"w-[34] h-[34] rounded-lg bg-emerald-500":134,"h-[112] px-6 flex-row items-center justify-between bg-slate-950":135,"flex-row items-center gap-4":136,"w-[34] text-2xl text-white font-bold":137,"w-[332] flex-col items-end gap-2":138,"text-base text-slate-300 font-bold":139,"text-base text-slate-400":140,"h-[166] px-6 pt-6 flex-col gap-3":141,"text-base text-orange-600 font-bold":142,"h-[44] px-1 flex-row items-center justify-between":143,"w-full h-full items-center justify-center rounded-xl bg-slate-200":144,"w-full h-full items-center justify-center rounded-xl bg-red-100":145,"w-full h-full items-center justify-center rounded-xl bg-slate-100":146,"w-full h-full items-center justify-center rounded-xl bg-orange-600":147,"w-full h-[150] px-5 flex-col items-center justify-center rounded-xl shadow bg-white border-slate-100":148,"w-full h-[430] px-12 flex-col items-center justify-center rounded-xl shadow bg-white border-slate-100":149,"w-[88] h-[88] items-center justify-center rounded-xl bg-indigo-100":150,"w-[88] h-[88] items-center justify-center rounded-xl bg-slate-100":151,"text-2xl text-indigo-700 font-bold":152,"text-2xl text-slate-600 font-bold":153,"pt-7 text-2xl text-slate-900 font-bold":154,"pt-4 text-lg text-slate-500":155,"text-xl text-emerald-600 font-bold":156,"text-xl text-red-500 font-bold":157,"w-full h-full px-5 py-4 flex-col gap-3 rounded-xl shadow bg-white border-slate-100":158,"text-base text-red-300":159,"text-base text-slate-300":160,"text-base text-red-500":161,"w-full h-full px-6 flex-row items-center bg-slate-950":162,"w-full h-full flex-row items-center":163};if(typeof globalThis.queueMicrotask!=="function")globalThis.queueMicrotask=(J)=>{Promise.resolve().then(J)};var w2="ui:styles",C2="ui:font.",h4="ui:img.",y4="ui:sprite.";function N2(){return globalThis.ui}function E2(J){if(J.__textures)return;for(let Q of h0(h4)){let $=vJ(Q),Z;if(J.uploadImgEntry)Z=J.uploadImgEntry($);else{let q=new DataView($.buffer,$.byteOffset,$.byteLength);Z=J.uploadTexture($.subarray(8),q.getUint16(0,!0),q.getUint16(2,!0),$[4])}if(Z>=0)Q4(Q.slice(h4.length),Z)}}function h2(J){if(J.__sprites)return;for(let Q of h0(y4)){let $=vJ(Q),Z=new DataView($.buffer,$.byteOffset,$.byteLength),q=Z.getUint16(0,!0),X=Z.getUint16(2,!0),z=$[4],W=Z.getUint16(6,!0),R=Z.getUint16(8,!0),D=Z.getUint16(10,!0),V=J.uploadTexture($.subarray(16),q,X,z);if(V>=0)Z4(Q.slice(y4.length),{handle:V,frames:W,cols:R,step:D})}}function T4(J){let Q=W4("view");return NJ(Q,"style",J,void 0),Q}var R0=null,P0=null;function y2(J,Q){if(!R0||!P0)return;NJ(R0,"style",{width:J,height:Q,overflow:l.Overflow.Hidden},void 0),NJ(P0,"style",{width:J,height:Q,posType:l.PosType.Absolute,insetT:0,insetR:0,insetB:0,insetL:0,zIndex:1000},void 0);let $=h();$.__viewport={w:J,h:Q}}function T2(J,Q={}){let $=m6(Q.ops);if(u6($),A8(J2),Q.styles)e8(Q.styles);let Z=$.kind==="native"?$.ops.__textures:void 0;if($.kind==="native"){if(Z)for(let _ in Z)Q4(_,Z[_]);let B=$.ops.__sprites;if(B)for(let _ in B)Z4(_,B[_])}if($.kind==="injected"||Z===void 0){if(Q.pak)m1(Q.pak);if(X8()){for(let B of h0())if(B===w2)$.ops.loadStyles?.(vJ(B));else if(B.startsWith(C2))$.ops.loadFontAtlas?.(vJ(B))}}let q=C1($.ops),X=q?.w??b1,z=q?.h??g1,W=T4({width:X,height:z,overflow:l.Overflow.Hidden}),R=T4({width:X,height:z,posType:l.PosType.Absolute,insetT:0,insetR:0,insetB:0,insetL:0,zIndex:1000,hitPass:1});YJ(XJ,W),YJ(XJ,R),B4(R),R0=W,P0=R,u1(W),s1(XJ),$8(),N4(),I2(),r6(),k2(),l8($.ops),l6(c8((B,_,g,C)=>{o6(),n6(_),Y2(g,C),b2(),S2(),V2(),Z8(B),L8(B),X4()}));let D=T8(J,W),V=c6(y2);return()=>{V(),G2(),N4(),D(),u1(null),s1(null),B4(null),R0=null,P0=null;for(let B of XJ.children.splice(0))B.parent=null,$.ops.destroyNode(B.id);X4()}}function f2(J,Q={}){let $=Q.ops??N2();if(!$)throw Error("PocketJS: mount() requires globalThis.ui or opts.ops");if(Q.pak)m1(Q.pak);return E2($),h2($),T2(J,{ops:$,styles:Q.styles??g2,pak:Q.pak})}var x2="$b",v2=65536,f4="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",dJ={};for(let J=0;JJ.length)throw Error("invalid UTF-8");for(let z=0;z1114111||q>=55296&&q<=57343||X===1&&q<128||X===2&&q<2048||X===3&&q<65536)throw Error("invalid UTF-8");if(q<65536)Q+=String.fromCharCode(q);else q-=65536,Q+=String.fromCharCode(55296+(q>>10),56320+(q&1023))}return Q}function l2(){let J=globalThis.fs;if(!J||typeof J!=="object")return null;return typeof J.read==="function"?J:null}function x4(){let J=l2();if(!J)throw Error("fs: globalThis.fs is not mounted — declare `data.fs` in pocket.json requires");return J}function c2(J,Q){let $=[],Z=0;for(;;){let z=JSON.parse(J.read(Q,Z,v2));if(z.error!==void 0)throw Error(`fs: read ${Q}: ${z.error}`);let W=m2(z.data[x2]);if($.push(W),Z+=W.length,z.eof)break}if($.length===1)return $[0];let q=new Uint8Array(Z),X=0;for(let z of $)q.set(z,X),X+=z.length;return q}function p2(J,Q){let $=c2(x4(),J);return Q==="utf8"||Q==="utf-8"?u2($):$}function d2(J,Q){let $=x4(),Z=[],q=0;for(;;){let X=JSON.parse($.list(J,q));if(X.error!==void 0)throw Error(`fs: readdir ${J}: ${X.error}`);for(let z of X.entries)Z.push({...z,isFile:()=>z.kind==="file",isDirectory:()=>z.kind==="dir"});if(q+=X.entries.length,X.eof)break}return Q?.withFileTypes?Z:Z.map((X)=>X.name)}var D0={appTitle:"text-2xl text-white font-bold",pageTitle:"text-2xl text-slate-950 font-bold",heading:"text-xl text-slate-900 font-bold",label:"text-base text-slate-600 font-bold",captionStrong:"text-base text-slate-500 font-bold"};function v4(J){let Q=()=>J.accent==="busy"?"w-[34] h-[34] rounded-lg bg-amber-400":J.accent==="danger"?"w-[34] h-[34] rounded-lg bg-red-500":J.accent==="none"?"w-[34] h-[34] rounded-lg bg-slate-800":"w-[34] h-[34] rounded-lg bg-emerald-500";return Y(j,{class:"h-[112] px-6 flex-row items-center justify-between bg-slate-950",get children(){return[Y(j,{class:"flex-row items-center gap-4",get children(){return[y(()=>y(()=>!!J.back)()?Y(U,{class:"w-[34] text-2xl text-white font-bold",children:"‹"}):Y(j,{get["class"](){return Q()}})),Y(U,{get["class"](){return D0.appTitle},get children(){return J.title}})]}}),Y(j,{class:"w-[332] flex-col items-end gap-2",get children(){return[Y(U,{class:"text-base text-slate-300 font-bold",get children(){return J.metaTop??""}}),Y(U,{class:"text-base text-slate-400",get children(){return J.metaBottom??""}})]}})]}})}function i2(J){return Y(j,{class:"h-[166] px-6 pt-6 flex-col gap-3",get children(){return[Y(U,{get["class"](){return J.tone==="info"?"text-base text-indigo-700 font-bold":"text-base text-orange-600 font-bold"},get children(){return J.eyebrow}}),Y(U,{get["class"](){return D0.pageTitle},get children(){return J.title}}),Y(U,{class:"text-lg text-slate-600",get children(){return J.description}})]}})}function s2(J){let Q=()=>J.action?J.detail?J.detail+" · VIEW ALL ›":"VIEW ALL ›":J.detail??"";return Y(j,{class:"h-[44] px-1 flex-row items-center justify-between",get children(){return[Y(U,{get["class"](){return D0.heading},get children(){return J.title}}),Y(U,{get["class"](){return D0.captionStrong},get children(){return Q()}})]}})}function m4(J){let Q=()=>J.disabled?"w-full h-full items-center justify-center rounded-xl bg-slate-200":J.tone==="danger"?"w-full h-full items-center justify-center rounded-xl bg-red-100":J.tone==="neutral"?"w-full h-full items-center justify-center rounded-xl bg-slate-100":"w-full h-full items-center justify-center rounded-xl bg-orange-600",$=()=>J.disabled?"text-lg text-slate-500 font-bold":J.tone==="danger"?"text-lg text-red-500 font-bold":J.tone==="neutral"?"text-lg text-slate-900 font-bold":"text-lg text-white font-bold";return Y(j,{get["class"](){return Q()},get children(){return Y(U,{get["class"](){return $()},get children(){return J.label}})}})}function a2(J){let Q=()=>J.dark?J.tone==="danger"?"text-base text-red-300":"text-base text-slate-300":J.tone==="danger"?"text-base text-red-500":"text-base text-slate-500";return Y(j,{get["class"](){return J.dark?"w-full h-full px-6 flex-row items-center bg-slate-950":"w-full h-full flex-row items-center"},get children(){return Y(U,{get["class"](){return Q()},get children(){return J.text}})}})}function iJ(J){return[Y(j,{get["class"](){return J.top},get children(){return Y(U,{class:"text-base text-orange-600 font-bold",children:"UP"})}}),Y(j,{get["class"](){return J.bottom},get children(){return Y(U,{class:"text-base text-orange-600 font-bold",children:"DN"})}})]}var eQ="‘’“”‑–—…•",r2=512;function o2(J,Q,$){let Z=[],q="",X=0;for(let z of J){let W=Q(z);if(q&&X+W>$)Z.push(q),q="",X=0;q+=z,X+=W}if(q)Z.push(q);return Z}function u4(J,Q,$){let Z=new Map,q=(W)=>{let R=Z.get(W);if(R===void 0)R=h().measureText(W,Q),Z.set(W,R);return R},X=[],z=q(" ");for(let W of J.split(` -`)){let R=W.split(" ").flatMap((B)=>B&&q(B)>$?o2(B,q,$):[B]),D="",V=0;for(let B of R){let _=q(B);if(!D)D=B,V=_;else if(V+z+_<=$)D+=" "+B,V+=z+_;else X.push(D),D=B,V=_}X.push(D)}return X}function Q1(J,Q,$,Z){let q=J.slice(0,r2),X=u4(q,Q,$);if(q.length===J.length&&X.length<=Z)return X.join(` +(()=>{var SJ={context:void 0,registry:void 0,effects:void 0,done:!1,getContextId(){return K1(this.context.count)},getNextContextId(){return K1(this.context.count++)}};function K1(J){let Q=String(J),$=Q.length-1;return SJ.context.id+($?String.fromCharCode(96+$):"")+Q}function F1(J){SJ.context=J}function H6(){return{...SJ.context,id:SJ.getNextContextId(),count:0}}var B6=!1,j6=(J,Q)=>J===Q,k0=Symbol("solid-proxy"),K6=typeof Proxy==="function",F6=Symbol("solid-track"),kQ=Symbol("solid-dev-component"),nJ={equals:j6},M1=null,M6=I1,l=1,yJ=2,U1={owned:null,cleanups:null,context:null,owner:null},S=null,F=null,fJ=null,bJ=null,b=null,E=null,f=null,tJ=0;function eJ(J,Q){let $=b,Z=S,q=J.length===0,X=Q===void 0?Z:Q,z=q?U1:{owned:null,cleanups:null,context:X?X.context:null,owner:X},W=q?J:()=>J(()=>$J(()=>ZJ(z)));S=z,b=null;try{return d(W,!0)}finally{b=$,S=Z}}function w(J,Q){Q=Q?Object.assign({},nJ,Q):nJ;let $={value:J,observers:null,observerSlots:null,comparator:Q.equals||void 0},Z=(q)=>{if(typeof q==="function")if(F&&F.running&&F.sources.has($))q=q($.tValue);else q=q($.value);return V1($,q)};return[D1.bind($),Z]}function QJ(J,Q,$){let Z=O1(J,Q,!1,l);if(fJ&&F&&F.running)E.push(Z);else J0(Z)}function gJ(J,Q,$){$=$?Object.assign({},nJ,$):nJ;let Z=O1(J,Q,!0,0);if(Z.observers=null,Z.observerSlots=null,Z.comparator=$.equals||void 0,fJ&&F&&F.running)Z.tState=l,E.push(Z);else J0(Z);return D1.bind(Z)}function R1(J){return d(J,!1)}function $J(J){if(!bJ&&b===null)return J();let Q=b;b=null;try{if(bJ)return bJ.untrack(J);return J()}finally{b=Q}}function S0(J){if(S===null);else if(S.cleanups===null)S.cleanups=[J];else S.cleanups.push(J);return J}function U6(J){if(F&&F.running)return J(),F.done;let Q=b,$=S;return Promise.resolve().then(()=>{b=Q,S=$;let Z;if(fJ||R6)Z=F||(F={sources:new Set,effects:[],promises:new Set,disposed:new Set,queue:new Set,running:!0}),Z.done||(Z.done=new Promise((q)=>Z.resolve=q)),Z.running=!0;return d(J,!1),b=S=null,Z?Z.done:void 0})}var[SQ,P1]=w(!1),R6;function D1(){let J=F&&F.running;if(this.sources&&(J?this.tState:this.state))if((J?this.tState:this.state)===l)J0(this);else{let Q=E;E=null,d(()=>Q0(this),!1),E=Q}if(b){let Q=this.observers;if(!Q||Q[Q.length-1]!==b){let $=Q?Q.length:0;if(!b.sources)b.sources=[this],b.sourceSlots=[$];else b.sources.push(this),b.sourceSlots.push($);if(!Q)this.observers=[b],this.observerSlots=[b.sources.length-1];else Q.push(b),this.observerSlots.push(b.sources.length-1)}}if(J&&F.sources.has(this))return this.tValue;return this.value}function V1(J,Q,$){let Z=F&&F.running&&F.sources.has(J)?J.tValue:J.value;if(!J.comparator||!J.comparator(Z,Q)){if(F){let q=F.running;if(q||!$&&F.sources.has(J))F.sources.add(J),J.tValue=Q;if(!q)J.value=Q}else J.value=Q;if(J.observers&&J.observers.length)d(()=>{for(let q=0;q1e6)throw E=[],Error()},!1)}return Q}function J0(J){if(!J.fn)return;ZJ(J);let Q=tJ;if(L1(J,F&&F.running&&F.sources.has(J)?J.tValue:J.value,Q),F&&!F.running&&F.sources.has(J))queueMicrotask(()=>{d(()=>{F&&(F.running=!0),b=S=J,L1(J,J.tValue,Q),b=S=null},!1)})}function L1(J,Q,$){let Z,q=S,X=b;b=S=J;try{Z=J.fn(Q)}catch(z){if(J.pure)if(F&&F.running)J.tState=l,J.tOwned&&J.tOwned.forEach(ZJ),J.tOwned=void 0;else J.state=l,J.owned&&J.owned.forEach(ZJ),J.owned=null;return J.updatedAt=$+1,g0(z)}finally{b=X,S=q}if(!J.updatedAt||J.updatedAt<=$){if(J.updatedAt!=null&&"observers"in J)V1(J,Z,!0);else if(F&&F.running&&J.pure){if(!F.sources.has(J))J.value=Z;F.sources.add(J),J.tValue=Z}else J.value=Z;J.updatedAt=$}}function O1(J,Q,$,Z=l,q){let X={fn:J,state:Z,updatedAt:null,owned:null,sources:null,sourceSlots:null,cleanups:null,value:Q,owner:S,context:S?S.context:null,pure:$};if(F&&F.running)X.state=0,X.tState=Z;if(S===null);else if(S!==U1)if(F&&F.running&&S.pure)if(!S.tOwned)S.tOwned=[X];else S.tOwned.push(X);else if(!S.owned)S.owned=[X];else S.owned.push(X);if(bJ&&X.fn){let z=X.fn,[W,R]=w(void 0,{equals:!1}),D=bJ.factory(z,R);S0(()=>D.dispose());let V,H=()=>U6(R).then(()=>{if(V)V.dispose(),V=void 0});X.fn=(_)=>{if(W(),F&&F.running){if(!V)V=bJ.factory(z,H);return V.track(_)}return D.track(_)}}return X}function b0(J){let Q=F&&F.running;if((Q?J.tState:J.state)===0)return;if((Q?J.tState:J.state)===yJ)return Q0(J);if(J.suspense&&$J(J.suspense.inFallback))return J.suspense.effects.push(J);let $=[J];while((J=J.owner)&&(!J.updatedAt||J.updatedAt=0;Z--){if(J=$[Z],Q){let q=J,X=$[Z+1];while((q=q.owner)&&q!==X)if(F.disposed.has(q))return}if((Q?J.tState:J.state)===l)J0(J);else if((Q?J.tState:J.state)===yJ){let q=E;E=null,d(()=>Q0(J,$[0]),!1),E=q}}}function d(J,Q){if(E)return J();let $=!1;if(!Q)E=[];if(f)$=!0;else f=[];tJ++;try{let Z=J();return P6($),Z}catch(Z){if(!$)f=null;E=null,g0(Z)}}function P6(J){if(E){if(fJ&&F&&F.running)D6(E);else I1(E);E=null}if(J)return;let Q;if(F){if(!F.promises.size&&!F.queue.size){let{sources:Z,disposed:q}=F;f.push.apply(f,F.effects),Q=F.resolve;for(let X of f)"tState"in X&&(X.state=X.tState),delete X.tState;F=null,d(()=>{for(let X of q)ZJ(X);for(let X of Z){if(X.value=X.tValue,X.owned)for(let z=0,W=X.owned.length;zM6($),!1);if(Q)Q()}function I1(J){for(let Q=0;Q{Z.delete($),d(()=>{F.running=!0,b0($)},!1),F&&(F.running=!1)})}}function Q0(J,Q){let $=F&&F.running;if($)J.tState=0;else J.state=0;for(let Z=0;Z=0;Q--)ZJ(J.tOwned[Q]);delete J.tOwned}if(F&&F.running&&J.pure)_1(J,!0);else if(J.owned){for(Q=J.owned.length-1;Q>=0;Q--)ZJ(J.owned[Q]);J.owned=null}if(J.cleanups){for(Q=J.cleanups.length-1;Q>=0;Q--)J.cleanups[Q]();J.cleanups=null}if(F&&F.running)J.tState=0;else J.state=0}function _1(J,Q){if(!Q)J.tState=0,F.disposed.add(J);if(J.owned)for(let $=0;$1?[]:null;return S0(()=>S1(X)),()=>{let R=J()||[],D=R.length,V,H;return R[F6],$J(()=>{let g,C,r,jJ,AJ,M,K,B,L;if(D===0){if(z!==0)S1(X),X=[],Z=[],q=[],z=0,W&&(W=[]);if($.fallback)Z=[L6],q[0]=eJ((A)=>{return X[0]=A,$.fallback()}),z=1}else if(z===0){q=Array(D);for(H=0;H=M&&B>=M&&Z[K]===R[B];K--,B--)r[B]=q[K],jJ[B]=X[K],W&&(AJ[B]=W[K]);g=new Map,C=Array(B+1);for(H=B;H>=M;H--)L=R[H],V=g.get(L),C[H]=V===void 0?-1:V,g.set(L,H);for(V=M;V<=K;V++)if(L=Z[V],H=g.get(L),H!==void 0&&H!==-1)r[H]=q[V],jJ[H]=X[V],W&&(AJ[H]=W[V]),H=C[H],g.set(L,H);else X[V]();for(H=M;HJ(Q||{}));return F1($),Z}}return $J(()=>J(Q||{}))}function $0(){return!0}var _6={get(J,Q,$){if(Q===k0)return $;return J.get(Q)},has(J,Q){if(Q===k0)return!0;return J.has(Q)},set:$0,deleteProperty:$0,getOwnPropertyDescriptor(J,Q){return{configurable:!0,enumerable:!0,get(){return J.get(Q)},set:$0,deleteProperty:$0}},ownKeys(J){return J.keys()}};function w0(J){return!(J=typeof J==="function"?J():J)?{}:J}function k6(){for(let J=0,Q=this.length;J=0;W--){let R=w0(J[W])[z];if(R!==void 0)return R}},has(z){for(let W=J.length-1;W>=0;W--)if(z in w0(J[W]))return!0;return!1},keys(){let z=[];for(let W=0;W=0;z--){let W=J[z];if(!W)continue;let R=Object.getOwnPropertyNames(W);for(let D=R.length-1;D>=0;D--){let V=R[D];if(V==="__proto__"||V==="constructor")continue;let H=Object.getOwnPropertyDescriptor(W,V);if(!Z[V])Z[V]=H.get?{enumerable:!0,configurable:!0,get:k6.bind($[V]=[H.get.bind(W)])}:H.value!==void 0?H:void 0;else{let _=$[V];if(_){if(H.get)_.push(H.get.bind(W));else if(H.value!==void 0)_.push(()=>H.value)}}}}let q={},X=Object.keys(Z);for(let z=X.length-1;z>=0;z--){let W=X[z],R=Z[W];if(R&&R.get)Object.defineProperty(q,W,R);else q[W]=R?R.value:void 0}return q}var S6=(J)=>`Stale read from <${J}>.`;function KJ(J){let Q="fallback"in J&&{fallback:()=>J.fallback};return gJ(O6(()=>J.each,J.children,Q||void 0))}function n(J){let Q=J.keyed,$=gJ(()=>J.when,void 0,void 0),Z=Q?$:gJ($,void 0,{equals:(q,X)=>!q===!X});return gJ(()=>{let q=Z();if(q){let X=J.children;return typeof X==="function"&&X.length>0?$J(()=>X(Q?q:()=>{if(!$J(Z))throw S6("Show");return $()})):X}return J.fallback},void 0,void 0)}var b6=(J)=>gJ(()=>J());function g6({createElement:J,createTextNode:Q,isTextNode:$,replaceText:Z,insertNode:q,removeNode:X,setProperty:z,getParentNode:W,getFirstChild:R,getNextSibling:D}){function V(M,K,B,L){if(B!==void 0&&!L)L=[];if(typeof K!=="function")return H(M,K,L,B);QJ((A)=>H(M,K(),A,B),L)}function H(M,K,B,L,A){while(typeof B==="function")B=B();if(K===B)return B;let k=typeof K,I=L!==void 0;if(k==="string"||k==="number"){if(k==="number")K=K.toString();if(I){let O=B[0];if(O&&$(O))Z(O,K);else O=Q(K);B=C(M,B,L,O)}else if(B!==""&&typeof B==="string")Z(R(M),B=K);else C(M,B,L,Q(K)),B=K}else if(K==null||k==="boolean")B=C(M,B,L);else if(k==="function")return QJ(()=>{let O=K();while(typeof O==="function")O=O();B=H(M,O,B,L)}),()=>B;else if(Array.isArray(K)){let O=[];if(_(O,K,A))return QJ(()=>B=H(M,O,B,L,!0)),()=>B;if(O.length===0){let TJ=C(M,B,L);if(I)return B=TJ}else if(Array.isArray(B))if(B.length===0)r(M,O,L);else g(M,B,O);else if(B==null||B==="")r(M,O);else g(M,I&&B||[R(M)],O);B=O}else{if(Array.isArray(B)){if(I)return B=C(M,B,L,K);C(M,B,null,K)}else if(B==null||B===""||!R(M))q(M,K);else jJ(M,K,R(M));B=K}return B}function _(M,K,B){let L=!1;for(let A=0,k=K.length;Ao-O){let _Q=K[I];while(O=0;I--){let O=K[I];if(A!==O){let TJ=W(O)===M;if(!k&&!I)TJ?jJ(M,A,O):q(M,A,B);else TJ&&X(M,O)}else k=!0}}else q(M,A,B);return[A]}function r(M,K,B){for(let L=0,A=K.length;LB.children=H(M,K.children,B.children));return QJ(()=>K.ref&&K.ref(M)),QJ(()=>{for(let A in K){if(A==="children"||A==="ref")continue;let k=K[A];if(k===B[A])continue;z(M,A,k,B[A]),B[A]=k}}),B}return{render(M,K){let B;return eJ((L)=>{B=L,V(K,M())}),B},insert:V,spread(M,K,B){if(typeof K==="function")QJ((L)=>AJ(M,K(),L,B));else AJ(M,K,void 0,B)},createElement:J,createTextNode:Q,insertNode:q,setProp(M,K,B,L){return z(M,K,B,L),B},mergeProps:b1,effect:QJ,memo:b6,createComponent:A6,use(M,K,B){return $J(()=>M(K,B))}}}function w6(J){let Q=g6(J);return Q.mergeProps=b1,Q}var g1=480,w1=272,FJ={view:0,text:1,image:2},C6=1,N6=-1,E6={width:1,height:2,minW:3,minH:4,maxW:5,maxH:6,paddingT:8,paddingR:9,paddingB:10,paddingL:11,marginT:12,marginR:13,marginB:14,marginL:15,gap:16,flexDir:17,justify:18,align:19,grow:20,shrink:21,basis:22,flexWrap:23,posType:24,insetT:25,insetR:26,insetB:27,insetL:28,display:29,overflow:30,zIndex:31,hitPass:32,bgColor:64,gradFrom:65,gradTo:66,gradDir:67,radius:68,opacity:69,borderColor:70,borderWidth:71,shadow:72,bevelOuterLight:77,bevelOuterDark:78,bevelInnerLight:79,bevelInnerDark:80,bevelWidth:81,textColor:96,fontSlot:97,textAlign:98,lineHeight:99,tracking:100,translateX:128,translateY:129,scale:130,rotate:131,scaleX:132,scaleY:133,originX:134,originY:135,rotateX:136,rotateY:137,translateZ:138,perspective:139,arcStart:140,arcSweep:141,arcWidth:142},P={f32:0,color:1,int:2},h6={width:P.f32,height:P.f32,minW:P.f32,minH:P.f32,maxW:P.f32,maxH:P.f32,paddingT:P.f32,paddingR:P.f32,paddingB:P.f32,paddingL:P.f32,marginT:P.f32,marginR:P.f32,marginB:P.f32,marginL:P.f32,gap:P.f32,flexDir:P.int,justify:P.int,align:P.int,grow:P.f32,shrink:P.f32,basis:P.f32,flexWrap:P.int,posType:P.int,insetT:P.f32,insetR:P.f32,insetB:P.f32,insetL:P.f32,display:P.int,overflow:P.int,zIndex:P.int,hitPass:P.int,bgColor:P.color,gradFrom:P.color,gradTo:P.color,gradDir:P.int,radius:P.f32,opacity:P.f32,borderColor:P.color,borderWidth:P.f32,shadow:P.int,bevelOuterLight:P.color,bevelOuterDark:P.color,bevelInnerLight:P.color,bevelInnerDark:P.color,bevelWidth:P.f32,textColor:P.color,fontSlot:P.int,textAlign:P.int,lineHeight:P.f32,tracking:P.f32,translateX:P.f32,translateY:P.f32,scale:P.f32,rotate:P.f32,scaleX:P.f32,scaleY:P.f32,originX:P.f32,originY:P.f32,rotateX:P.f32,rotateY:P.f32,translateZ:P.f32,perspective:P.f32,arcStart:P.f32,arcSweep:P.f32,arcWidth:P.f32},c={FlexDir:{Row:0,Col:1},Justify:{Start:0,Center:1,End:2,Between:3,Around:4},Align:{Start:0,Center:1,End:2,Stretch:3},PosType:{Relative:0,Absolute:1},Display:{Flex:0,None:1},Overflow:{Visible:0,Hidden:1},TextAlign:{Left:0,Center:1,Right:2},GradDir:{ToTop:0,ToBottom:1,ToLeft:2,ToRight:3},Easing:{Linear:0,EaseIn:1,EaseOut:2,EaseInOut:3,OutBack:4,Spring:5,SpringBouncy:6,CubicBezier:7}},T6={PSM_5650:0,PSM_4444:2,PSM_8888:3,PSM_T8:5},y6=1,bQ=2,gQ=1,wQ=2,CQ=131072,NQ=1,EQ=262144,hQ=1,TQ=1,yQ=1,fQ=2,xQ=4,vQ=8,mQ=16,uQ=1,lQ=2;function C1(J,Q,$,Z=255){return((Z&255)<<24|($&255)<<16|(Q&255)<<8|J&255)>>>0}var cQ=1,f6=1263551300,x6=1,v6=32,m6=24,i={SELECT:1,START:8,UP:16,RIGHT:32,DOWN:64,LEFT:128,LTRIGGER:256,RTRIGGER:512,TRIANGLE:4096,CIRCLE:8192,CROSS:16384,SQUARE:32768},MJ=32896,pQ=0.016666666666666666;function N1(J){return J.__viewport??null}var C0=null;function u6(){return null}function E1(J,Q=u6()){if(!Q)return;if(typeof J.__host!=="string")throw Error(`PocketJS: this bundle targets "${Q.target}" but the native host predates platform `+"contracts — add __host/__hostAbi to its ui namespace (see framework/src/host.ts HostOps)");if(J.__host!==Q.target)throw Error(`PocketJS: native target mismatch (bundle=${Q.target}, host=${J.__host})`);if(J.__hostAbi!==Q.hostAbi)throw Error(`PocketJS: native host ABI mismatch (bundle=${Q.hostAbi}, host=${J.__hostAbi??"missing"})`)}function l6(J){let Q=globalThis.ui,$=Q!==void 0&&(typeof Q.__host==="string"||Q.__textures!==void 0);if(J){if(Q!==void 0&&J===Q&&$)return E1(Q),{ops:J,kind:"native",target:Q.__host??"unknown",strict:!1};return{ops:J,kind:"injected",target:J.__host??"injected",strict:!0}}if(Q!==void 0&&$)return E1(Q),{ops:Q,kind:"native",target:Q.__host??"unknown",strict:!1};if(Q)return{ops:Q,kind:"injected",target:"injected",strict:!0};throw Error("PocketJS: no host — pass HostOps to render() (web/test) or run under a native runtime (globalThis.ui)")}function c6(J){C0=J}function wJ(){if(!C0)throw Error("PocketJS: host not installed — call render() first");return C0}function h(){return wJ().ops}function p6(J){globalThis.frame=J}function d6(J){let Q=globalThis,$=Q.__pocketResizeViewport,Z=(q,X)=>J(q,X);return Q.__pocketResizeViewport=Z,()=>{if(Q.__pocketResizeViewport!==Z)return;if($)Q.__pocketResizeViewport=$;else delete Q.__pocketResizeViewport}}function i6(J){let Q=J.slice(1);if(Q.length===3)Q=Q[0]+Q[0]+Q[1]+Q[1]+Q[2]+Q[2];if(Q.length!==6&&Q.length!==8)throw Error(`PocketJS: bad color '${J}' (expected #rgb/#rrggbb/#rrggbbaa)`);if(!/^[0-9a-fA-F]+$/.test(Q))throw Error(`PocketJS: bad color '${J}'`);let $=parseInt(Q,16);if(Q.length===6)return C1($>>>16&255,$>>>8&255,$&255,255);return C1($>>>24&255,$>>>16&255,$>>>8&255,$&255)}function s6(J,Q){let $=h6[J];if(typeof Q==="string"){if($===P.color)return i6(Q);let Z=Number(Q);if(Number.isNaN(Z))throw Error(`PocketJS: non-numeric value '${Q}' for prop '${J}'`);Q=Z}if($===P.color||$===P.int)return Q>>>0;return Q}var Z0=60,h1=[1,2,3,4,5,6,10,12,15,20,30,60],N0=Z0,qJ=-1,a6=0,xJ=[];function r6(J){if(!Number.isFinite(J)||J<=0)return Z0;let Q=h1[0];for(let $ of h1)if(Math.abs($-J)Q.at<=qJ).sort((Q,$)=>Q.at-$.at||Q.seq-$.seq);if(J.length===0)return;xJ=xJ.filter((Q)=>Q.at>qJ);for(let Q of J)Q.cb()}var E0=0.12,q0=MJ;function e6(J){q0=J===void 0?MJ:J&65535}function J8(){q0=MJ}function f1(J){let Q=Math.max(-1,Math.min(1,(J-128)/127)),$=Math.abs(Q);if($>8&255)}function $8(){return f1(q0&255)}var x1=new Set,Z8=0;function q8(){x1.clear(),Z8=0,J8()}function X8(J){for(let Q of[...x1])Q(J)}var UJ=null,v1=null;function Y8(J,Q,$){let Z="";for(let q=0;q<$;q++)Z+=String.fromCharCode(J[Q+q]);return Z}function m1(J){let Q=new DataView(J);if(J.byteLength=J.length&&$.slice(0,J.length)===J)Q.push($);return Q.sort(),Q}function vJ(J){h0();let Q=UJ?UJ.get(J):void 0;if(!Q)throw Error("pak: missing key "+J+" (no __pak provided, or the pack is incomplete)");return v1.slice(Q.off,Q.off+Q.len)}var mJ=null,N=null,RJ=null,X0=0,PJ=[],y0=[],f0=[];function l1(J){if(mJ=J,N=null,RJ=null,X0=0,PJ.length=0,y0.length=0,f0.length=0,p){if(p.pressTarget=null,p.target=null,p.spriteDirty=!0,p.fresh=!0,p.vw=0,p.tex>=0){let Q=h();Q.setCursor?.(-1,0,0,0,0),Q.freeTexture?.(p.tex),p.tex=-1}}}function G8(J,Q){J.onPress=Q??void 0}function W8(J,Q){if(J.focusable=Q,s1(),!Q&&N===J)m(null)}function m(J){if(RJ&&RJ!==J)DJ(null);N=J,h().setFocus(J?J.id:0)}function DJ(J){if(RJ===J)return;let Q=h();if(RJ)Q.setActive?.(RJ.id,0);if(RJ=J,J)Q.setActive?.(J.id,1)}function x0(){return PJ.length>0?PJ[PJ.length-1]:mJ}function v0(J,Q){if(!J)return;if(J.focusable)Q.push(J);if(!Array.isArray(J.children))return;for(let $=0;$=$.length)return;m($[q])}function j8(){if(!N)return null;let J=x0();if(J&&!s(N,J))return null;for(let Q=y0.length-1;Q>=0;Q--){let $=y0[Q];if(J&&!s($.node,J)&&!s(J,$.node))continue;if(s(N,$.node))return $}return null}function K8(J){let Q=j8();if(!Q)return!1;let $=[];if(v0(Q.node,$),$.length===0){if(N)m(null);return!0}let Z=Q.columns,q=N?$.indexOf(N):-1;if(q<0)return m(c1(J)===1?$[0]:$[$.length-1]),!0;let X=q;switch(J){case"right":if(q+1<$.length&&q%Z0)X=q-1;else if(Q.wrap)X=Math.min($.length-1,Math.floor(q/Z)*Z+Z-1);break;case"down":if(q+Z<$.length)X=q+Z;else if(Q.wrap)X=q%Z;break;case"up":if(q-Z>=0)X=q-Z;else if(Q.wrap){X=q%Z;while(X+Z<$.length)X+=Z}break}if(X!==q)m($[X]);return!0}function F8(){if(!N)return null;let J=x0();if(J&&!s(N,J))return null;for(let Q=f0.length-1;Q>=0;Q--){let $=f0[Q];if(J&&!s($.node,J)&&!s(J,$.node))continue;if(s(N,$.node))return $}return null}function Y0(J){let Q=F8();if(Q&&Q.move(J))return;if(K8(J))return;B8(J)}function p1(){d1(N)}function d1(J){let Q=J;while(Q){if(Q.onPress){Q.onPress();return}Q=Q.parent}}function z0(J){DJ(J)}function M8(J){m(J),d1(J)}function s(J,Q){if(!J||!Q)return!1;let $=J;while($){if($===Q)return!0;$=$.parent}return!1}function m0(J){if(!J)return null;if(J.focusable)return J;if(!Array.isArray(J.children))return null;for(let Q=0;Q=0;q--){let X=m0(Q.children[q]);if(X){m(X);return}}let Z=Q;while(Z){if(Z.focusable){m(Z);return}Z=Z.parent}}m(null)}var p=null,i1=0;function s1(){i1++}var R8=[1,3,5,9,17,33,65,129,257,513,1985,73,149,147,288,480],P8=[0,0,2,6,14,30,62,126,254,510,62,54,98,96,192,0];function D8(){let J=new Uint8Array(1024);for(let Q=0;Q<16;Q++)for(let $=0;$<16;$++){let Z=R8[Q]>>$&1,q=P8[Q]>>$&1;if(!Z&&!q)continue;let X=(Q*16+$)*4,z=q?255:0;J[X]=z,J[X+1]=z,J[X+2]=z,J[X+3]=255}return J}function V8(J,Q){let $=J.sprite;J.spriteDirty=!1;let Z=J.tex,q=-1,X=null;if(typeof $.image==="string")try{X=vJ($.image)}catch(z){if(wJ().strict)throw z;X=null}else if($.image)X=$.image;if(X){if(q=Q.uploadImgEntry?Q.uploadImgEntry(X):L8(Q,X),q<0&&wJ().strict)throw Error("enableCursor: cursor image rejected (malformed or RLE-only IMG entry)")}if(q<0)q=Q.uploadTexture(D8(),16,16,T6.PSM_8888);if(J.tex=q,Q.setCursor(q,$.hotspot[0],$.hotspot[1],$.size[0],$.size[1]),Z>=0&&Z!==q)Q.freeTexture?.(Z)}function L8(J,Q){if(Q.length<8)return-1;let $=new DataView(Q.buffer,Q.byteOffset,Q.byteLength);if(Q[5]&y6)return-1;return J.uploadTexture(Q.subarray(8),$.getUint16(0,!0),$.getUint16(2,!0),Q[4])}function G0(J,Q){if(!J||Q===0)return null;if(J.id===Q)return J;let $=J.children;if(!Array.isArray($))return null;for(let Z=0;Z<$.length;Z++){let q=G0($[Z],Q);if(q)return q}return null}var W0=null;function a1(J){W0=J}function r1(J){let Q=PJ.length>0?PJ[PJ.length-1]:null,$=J;while($){if($.focusable&&(!Q||s($,Q)))return $;$=$.parent}return null}function o1(J,Q,$){return r1(n1(J,Q,$))}function n1(J,Q,$){if($!==void 0)return $===0?null:G0(W0??mJ,$);let Z=h(),q=Z.hitTestBounds??Z.hitTest;if(!q)return null;return G0(W0??mJ,q(J,Q))}function O8(J,Q,$){let Z=p,q=h();if(!q.hitTest||!q.setCursor||!q.setCursorPos)return!1;if(Z.vw===0){let H=N1(q);if(Z.vw=H?H.w:g1,Z.vh=H?H.h:w1,Z.x<0)Z.x=Math.floor(Z.vw/2),Z.y=Math.floor(Z.vh/2)}if(Z.spriteDirty)V8(Z,q);let X=Q8()*Z.speed,z=$8()*Z.speed;if(Z.dpadSpeed>0&&X===0&&z===0){if(J&i.LEFT)X=-Z.dpadSpeed;if(J&i.RIGHT)X=Z.dpadSpeed;if(J&i.UP)z=-Z.dpadSpeed;if(J&i.DOWN)z=Z.dpadSpeed}let W=Z.fresh;if(X!==0||z!==0){let H=o6()/60,_=Math.min(Math.max(Z.x+X*H,0),Z.vw-1),g=Math.min(Math.max(Z.y+z*H,0),Z.vh-1);if(_!==Z.x||g!==Z.y)Z.x=_,Z.y=g,W=!0}if(W)q.setCursorPos(Z.x,Z.y);let R=(Q|$)&Z.button,D=i1;if(W||R!==0||D!==Z.gen)Z.gen=D,Z.fresh=!1,Z.target=r1(G0(W0??mJ,q.hitTest(Z.x,Z.y)));let V=Z.target;if(V!==N)m(V);if(Q&Z.button&&V)Z.pressTarget=V;if(Z.pressTarget){if(DJ(V===Z.pressTarget?Z.pressTarget:null),$&Z.button){let H=V===Z.pressTarget;if(Z.pressTarget=null,DJ(null),H)p1()}}else if($&Z.button)DJ(null);return!0}function I8(J){let Q=J&~X0,$=X0&~J;if(X0=J,p&&O8(J,Q,$))return;if($&i.CIRCLE)DJ(null);if(Q===0)return;if(Q&i.DOWN)Y0("down");if(Q&i.RIGHT)Y0("right");if(Q&i.UP)Y0("up");if(Q&i.LEFT)Y0("left");if(Q&i.CIRCLE)DJ(N),p1()}var u0=null;function t1(J){u0=J}function CJ(){if(s1(),u0)u0()}function A8(J,Q){J.debugName=Q||void 0,CJ()}var XJ={id:C6,type:FJ.view,parent:null,children:[],domNodeType:1,domTag:"root"},e1=Symbol.for("pocketjs.native-node"),l0=1,uJ=3,t=8,_8=new Set(["class","className","style","src","onPress","on:press","focusable","debugName","ref","nodeRef","key","children"]);function H0(J){return J.domAttrs??={}}function J4(J,Q){let $=J.domNodeType??(lJ(J)?uJ:l0),Z=$===uJ?B0(J.text??""):$===t?b8(J.domData??""):z4(J.domTag??K0(J));for(let q of Object.keys(J.domAttrs??{}))c0(Z,q,J.domAttrs[q]);if(Q)for(let q of J.children)YJ(Z,J4(q,!0));return Z}function c0(J,Q,$){if(_8.has(Q)){EJ(J,Q,$,J.domAttrs?.[Q]);return}if($==null)delete H0(J)[Q];else H0(J)[Q]=$}function p0(J){if(J[e1]===!0)return J;return Object.defineProperty(J,e1,{value:!0}),Object.defineProperties(J,{nodeType:{configurable:!0,get(){return J.domNodeType??(lJ(J)?uJ:l0)}},nodeValue:{configurable:!0,get(){return J.domNodeType===t?J.domData??"":J.text??""},set($){if(J.domNodeType===t)J.domData=String($??"");else j0(J,String($??""))}},data:{configurable:!0,get(){return J.domNodeType===t?J.domData??"":J.text??""},set($){if(J.domNodeType===t)J.domData=String($??"");else j0(J,String($??""))}},textContent:{configurable:!0,get(){if(J.domNodeType===t)return J.domData??"";if(lJ(J))return J.text??"";return J.children.map(($)=>$.text??"").join("")},set($){let Z=String($??"");if(J.domNodeType===t)J.domData=Z;else if(lJ(J))j0(J,Z);else if(T8(J),Z)YJ(J,B0(Z))}},parentNode:{configurable:!0,get(){return J.parent}},parentElement:{configurable:!0,get(){return J.parent}},childNodes:{configurable:!0,get(){return J.children}},firstChild:{configurable:!0,get(){return J.children[0]??null}},lastChild:{configurable:!0,get(){return J.children[J.children.length-1]??null}},nextSibling:{configurable:!0,get(){return W4(J)??null}},previousSibling:{configurable:!0,get(){let $=J.parent;if(!$)return null;let Z=$.children.indexOf(J);return Z>0?$.children[Z-1]:null}},tagName:{configurable:!0,get(){return(J.domTag??K0(J)).toUpperCase()}},nodeName:{configurable:!0,get(){if(J.domNodeType===uJ)return"#text";if(J.domNodeType===t)return"#comment";return(J.domTag??K0(J)).toUpperCase()}},className:{configurable:!0,get(){return String(J.domAttrs?.class??"")},set($){EJ(J,"class",$,J.domAttrs?.class)}},isConnected:{configurable:!0,get(){let $=J;while($){if($===XJ)return!0;$=$.parent}return!1}}}),Object.assign(J,{appendChild($){return YJ(J,$),$},insertBefore($,Z){return YJ(J,$,Z??null),$},removeChild($){return cJ(J,$),$},replaceChild($,Z){return YJ(J,$,Z),cJ(J,Z),Z},cloneNode($=!1){return J4(J,!!$)},remove(){if(J.parent)cJ(J.parent,J)},setAttribute($,Z){c0(J,$,Z)},removeAttribute($){c0(J,$,void 0)},getAttribute($){let Z=J.domAttrs?.[$];return Z==null?null:String(Z)},hasAttribute($){return J.domAttrs?.[$]!=null},hasChildNodes(){return J.children.length>0},contains($){let Z=$??null;while(Z){if(Z===J)return!0;Z=Z.parent}return!1},addEventListener(){},removeEventListener(){}},{style:{length:0,item:()=>""},classList:{add(){},remove(){}}}),J}p0(XJ);var d0=null;function k8(J){d0=J}var i0={unknownClass:0,unknownTexture:0},Q4=new Map;function $4(J,Q){Q4.set(J,Q)}var Z4=new Map;function q4(J,Q){Z4.set(J,Q)}var NJ=new Set,S8=new Set;function X4(J){if(!J)return!1;if(S8.has(J))return!0;if(J.children){for(let Q=0;Q - only view/text/image exist`);return p0({id:h().createNode(Q),type:Q,parent:null,children:[],domNodeType:l0,domTag:J})}function B0(J){let Q=h(),$=Q.createNode(FJ.text);return Q.setText($,J),p0({id:$,type:FJ.text,parent:null,children:[],text:J,domNodeType:uJ,domTag:"#text"})}function b8(J=""){let Q=B0("");return Q.domNodeType=t,Q.domTag="#comment",Q.domData=J,Q}function j0(J,Q){h().replaceText(J.id,Q),J.text=Q,CJ()}function lJ(J){return J.type===FJ.text}function G4(J){let Q=J.parent;if(!Q)return;let $=Q.children.indexOf(J);if($>=0)Q.children.splice($,1);J.parent=null}function YJ(J,Q,$){let Z=h();if(G4(Q),NJ.delete(Q),Z.insertBefore(J.id,Q.id,$?$.id:0),$){let q=J.children.indexOf($);if(q<0)throw Error("PocketJS: insert anchor is not a child of parent");J.children.splice(q,0,Q)}else J.children.push(Q);Q.parent=J,CJ()}function cJ(J,Q){if(!Q)return;U8(Q),h().removeChild(J.id,Q.id),G4(Q),NJ.add(Q),CJ()}function g8(J){return J.parent??void 0}function w8(J){return J.children[0]}function W4(J){let Q=J.parent;if(!Q)return;let $=Q.children.indexOf(J);return $>=0?Q.children[$+1]:void 0}function C8(J,Q){let $=h();if(CJ(),Q==null||Q===""){$.setStyle(J.id,N6);return}if(typeof Q!=="string")throw Error("PocketJS: class must be a string literal of utilities");let Z=d0?d0(Q):void 0;if(Z===void 0){if(wJ().strict)throw Error(`PocketJS: unknown class "${Q}" - not in the compiled style table (dynamic classes must be ternaries of full literals)`);i0.unknownClass++;return}$.setStyle(J.id,Z)}function N8(J,Q){let $=h();if(Q==null||Q===""){$.setImage(J.id,-1);return}if(typeof Q!=="string")throw Error("PocketJS: src must be a string key");let Z=Q4.get(Q);if(Z===void 0){if(wJ().strict)throw Error(`PocketJS: unknown image src "${Q}" - no texture registered under that key`);i0.unknownTexture++;return}$.setImage(J.id,Z)}function E8(J,Q){let $=h();if(Q==null||Q===""){$.setSprite(J.id,-1,0,0,0);return}if(typeof Q!=="string")throw Error("PocketJS: sprite must be a string key");let Z=Z4.get(Q);if(Z===void 0){if(wJ().strict)throw Error(`PocketJS: unknown sprite "${Q}" - no sprite atlas registered under that key`);i0.unknownTexture++;return}$.setSprite(J.id,Z.handle,Z.frames,Z.cols,Z.step)}function h8(J,Q,$){let Z=h(),q=Q??{},X=$??{},z=!1;for(let W in q){let R=q[W];if(X[W]===R)continue;let D=E6[W];if(D===void 0)throw Error(`PocketJS: unknown style prop '${W}' (see spec PROP)`);Z.setProp(J.id,D,s6(W,R)),z=!0}if(z)CJ()}function EJ(J,Q,$,Z){if($===Z&&Q!=="style")return $;if(Q==="className")Q="class";if(Q!=="children"&&Q!=="key"&&Q!=="ref"&&Q!=="nodeRef")if($==null)delete H0(J)[Q];else H0(J)[Q]=$;switch(Q){case"class":return C8(J,$),$;case"onPress":case"on:press":return G8(J,$),$;case"src":return N8(J,$),$;case"sprite":return E8(J,$),$;case"style":return h8(J,$,Z),$;case"focusable":return W8(J,!!$),$;case"debugName":return A8(J,$==null?void 0:String($)),$;case"ref":case"nodeRef":case"key":case"children":return $;default:break}if(Q==="classList")throw Error("PocketJS: classList is not supported - use ternaries of full class literals");if(Q.startsWith("on:")||Q.startsWith("bool:")||Q.startsWith("prop:"))throw Error(`PocketJS: unsupported namespaced attribute '${Q}'`);throw Error(`PocketJS: unknown property '${Q}' on <${K0(J)}>`)}function T8(J){for(let Q of[...J.children])cJ(J,Q)}function K0(J){for(let Q of Object.keys(FJ))if(FJ[Q]===J.type)return Q;return String(J.type)}function y8(J,Q,$,Z){if(Q==="ref"&&typeof $==="function"){$(J);return}EJ(J,Q,$,Z)}var f8=w6({createElement:z4,createTextNode:B0,replaceText:j0,isTextNode:lJ,setProperty:y8,insertNode(J,Q,$){YJ(J,Q,$)},removeNode(J,Q){cJ(J,Q)},getParentNode:g8,getFirstChild:w8,getNextSibling:W4}),{render:x8,effect:dQ,memo:T,createComponent:Y,createElement:H4,insert:iQ,spread:v8,mergeProps:sQ,use:aQ}=f8,rQ={linear:c.Easing.Linear,in:c.Easing.EaseIn,out:c.Easing.EaseOut,"in-out":c.Easing.EaseInOut,"out-back":c.Easing.OutBack,spring:c.Easing.Spring,"spring-bouncy":c.Easing.SpringBouncy},m8=null;function B4(J){m8=J}function u8(J,Q){if(!J)return;if(typeof J==="function")J(Q);else if("current"in J)J.current=Q}function j4(J,Q){let $=H4(J);return v8($,Q,!1),u8(Q.nodeRef,$),$}function j(J){return j4("view",J)}function U(J){return j4("text",J)}var oQ=new WeakMap,nQ=new WeakMap,l8={}!==null?Object.freeze({...{}}):Object.freeze({}),tQ=Object.freeze({target:"",pixelRatio:Number.isInteger(1)?1:1,features:l8}),eQ=new Map,zJ=36000,s0=30,c8=30,G={ops:null,transport:null,app:void 0,frame:0,tape:new Uint16Array(zJ),tapeAnalog:new Uint16Array(zJ),tapeTouch:null,tapeStart:0,tapeLen:0,tapeFirstFrame:0,replayMasks:null,replayAnalog:null,replayTouch:null,replayAt:0,paused:!1,stepQueued:0,inspectReportId:null,inspectAskedAt:0,treeDirty:!0,treeSentAt:-s0,saidHello:!1,hostCalls:0};function p8(J){let Q=globalThis;if(!Q.console)Q.console={log(){},warn(){},error(){}};G.ops=J,G.frame=0,G.tapeStart=0,G.tapeLen=0,G.tapeFirstFrame=0,G.tapeTouch=null,G.replayMasks=null,G.replayAnalog=null,G.replayTouch=null,G.paused=!1,G.stepQueued=0,G.inspectReportId=null,G.inspectAskedAt=0,G.treeDirty=!0,G.treeSentAt=-s0,G.saidHello=!1,G.hostCalls=0,G.app=globalThis.__pocketApp;let $=globalThis.__pocketDevtoolsTransport;if($)G.transport=$;else if(J.__dbgActive?.()&&J.__dbgPoll&&J.__dbgSend)G.transport={send:(Z)=>J.__dbgSend(Z),recv:()=>J.__dbgPoll(),everyFrames:10};else G.transport=null;if(G.transport)t1(()=>{G.treeDirty=!0}),t8();else t1(null);globalThis.__pocketDevtools=J2}function d8(J){return(Q,$,Z,q)=>{if(G.hostCalls++,G.transport)s8(),o8();let X=Q,z=$===void 0?MJ:$&65535,W=Z,R=q;if(G.replayMasks)if(G.replayAt0?$.slice(0,8):null;if(Z&&!G.tapeTouch)G.tapeTouch=Array(zJ).fill(null);if(G.tapeLen1||Q.length===1&&Q[0][0]!==MJ)J.analog=Q;if(G.tapeTouch){let $=[];for(let Z=0;Z0)J.v=2,J.touch=$}return J}function M4(J,Q,$){let Z=new Uint16Array($).fill(Q),q=0;for(let[X,z]of J)Z.fill(X,q,Math.min(q+z,$)),q+=z;return Z}function U4(J){let Q=0;for(let[,$]of J.masks)Q+=$;return M4(J.masks,0,Q)}function R4(J){let Q=0;for(let[,$]of J.masks)Q+=$;return M4(J.analog??[],MJ,Q)}function P4(J){let Q=0;for(let[,Z]of J.masks)Q+=Z;let $=Array(Q).fill(void 0);for(let[Z,q]of J.touch??[])if(Z>=0&&Z1&&G.hostCalls%Q!==0)return;if(!G.saidHello)G.saidHello=!0,x({t:"hello",app:G.app,host:e8(),frame:G.frame});for(let $=0;$<64;$++){let Z=J.recv();if(!Z)break;for(let q of Z.split(` +`))if(q.trim())a8(q)}}function a8(J){let Q;try{Q=JSON.parse(J)}catch{return}let $=G.ops;switch(Q.t){case"inspect":{let Z=typeof Q.id==="number"?Q.id:0;if($?.debugInspect?.(Z),G.inspectReportId=Z||null,G.inspectAskedAt=G.hostCalls,!Z)x({t:"inspect",id:0,rect:null});break}case"pause":G.paused=!0,G.stepQueued=0,$?.debugPause?.(!0),a0();break;case"resume":G.paused=!1,$?.debugPause?.(!1),a0();break;case"step":G.stepQueued+=typeof Q.n==="number"&&Q.n>0?Q.n:1;break;case"getTree":D4();break;case"eval":{let Z=!0,q;try{q=F0((0,eval)(String(Q.code)))}catch(X){Z=!1,q=X instanceof Error?`${X.name}: ${X.message}`:String(X)}x({t:"evalResult",id:Q.id,ok:Z,value:q});break}case"dumpTape":x({t:"tape",tape:F4()});break;case"devStats":{let Z=null,q=$?.debugStats?.();if(q)try{Z=JSON.parse(q)}catch{Z=null}x({t:"devStats",frame:G.frame,data:Z});break}case"screenshot":{if($?.__dbgShot?.())x({t:"screenshotRaw",file:"shot.raw",w:480,h:272,stride:512,frame:G.frame});else x({t:"log",level:"warn",args:["screenshot: not supported on this host"]});break}case"replay":{let Z=Q.tape;if(Z&&Array.isArray(Z.masks))G.replayMasks=U4(Z),G.replayAnalog=Z.analog?R4(Z):null,G.replayTouch=Z.touch?P4(Z):null,G.replayAt=0;break}default:break}}function r8(){if(G.treeDirty&&G.frame-G.treeSentAt>=s0)D4();if(G.frame%c8===0)a0()}function o8(){let J=G.inspectReportId;if(J==null)return;let Q=G.ops;if(!Q?.debugRectXY||!Q.debugRectWH){G.inspectReportId=null;return}let $=Q.debugRectXY();if($===-1){if(G.hostCalls-G.inspectAskedAt>60)G.inspectReportId=null,x({t:"inspect",id:J,rect:null});return}let Z=Q.debugRectWH();G.inspectReportId=null,x({t:"inspect",id:J,rect:[$<<16>>16,$>>16,Z&65535,Z>>16&65535]})}function a0(){x({t:"stats",frame:G.frame,nodes:O4(XJ),tapeLen:G.tapeLen,paused:G.paused})}function D4(){G.treeDirty=!1,G.treeSentAt=G.frame,x({t:"tree",frame:G.frame,root:L4(XJ)})}function n8(J){if(J==null||typeof J!=="object")return!1;let Q=J;return typeof Q.id==="number"&&typeof Q.type==="number"}function r0(J,Q){if(Array.isArray(J)){for(let $ of J)r0($,Q);return}if(n8(J)){Q(J);return}if(J!=null&&typeof J==="object"){let $=J.nodes;if($!==void 0)r0($,Q)}}function V4(J,Q){let $=Array.isArray(J.children)?J.children:[];for(let Z of $)r0(Z,Q)}function L4(J){let Q={i:J.id,t:J.domTag??String(J.type)};if(J.debugName)Q.n=J.debugName;let $=J.domAttrs?.class;if(typeof $==="string"&&$)Q.c=$;if(J.text)Q.x=J.text.length>80?J.text.slice(0,79)+"…":J.text;let Z=[];if(V4(J,(q)=>{if(q.domNodeType===8)return;Z.push(L4(q))}),Z.length)Q.k=Z;return Q}function O4(J){let Q=1;return V4(J,($)=>{Q+=O4($)}),Q}function t8(){let J=globalThis;if(!J.console)J.console={};let Q=J.console;if(Q.__pocketBridged)return;Q.__pocketBridged=!0;for(let $ of["log","warn","error"]){let Z=Q[$];Q[$]=(...q)=>{x({t:"log",level:$,args:q.map((X)=>F0(X))}),Z?.apply(Q,q)}}}function F0(J,Q=0){if(J===void 0)return"undefined";if(J===null)return"null";let $=typeof J;if($==="string"){let X=J;return Q===0?I4(X):JSON.stringify(I4(X))}if($==="number"||$==="boolean"||$==="bigint")return String(J);if($==="function"){let X=J.name;return X?`[function ${X}]`:"[function]"}if(Q>=3)return Array.isArray(J)?"[…]":"{…}";if(Array.isArray(J)){let X=J.slice(0,20).map((z)=>F0(z,Q+1));if(J.length>20)X.push(`… ${J.length-20} more`);return`[${X.join(", ")}]`}if(J instanceof Error)return`${J.name}: ${J.message}`;return`{${Object.entries(J).slice(0,20).map(([X,z])=>`${X}: ${F0(z,Q+1)}`).join(", ")}}`}function I4(J){return J.length>200?J.slice(0,199)+"…":J}function e8(){let J=G.ops;if(typeof J?.__host==="string")return J.__host;if(J?.__textures!==void 0)return"psp";if(typeof globalThis.document<"u")return"web";return"headless"}var J2={get frame(){return G.frame},dumpTape:()=>F4(),replay:(J)=>{G.replayMasks=U4(J),G.replayAnalog=J.analog?R4(J):null,G.replayTouch=J.touch?P4(J):null,G.replayAt=0}},A4=new Map,o0=new Map;function _4(J){return J.trim().replace(/\s+/g," ")}function k4(J){return J.split(" ").sort().join(" ")}var S4=-1;function Q2(J){for(let Q of Object.keys(J)){let $=J[Q],Z=_4(Q);A4.set(Z,$);let q=k4(Z),X=o0.get(q);o0.set(q,X!==void 0&&X!==$?S4:$)}}function $2(J){let Q=_4(J),$=A4.get(Q);if($!==void 0)return $;let Z=o0.get(k4(Q));return Z===S4?void 0:Z}var n0=9,Z2=(1<{let q=($&X2)!==0,X=q?t0:n0,z=q?Y2:Z2;return Object.freeze({id:$>>>(q?z2:q2)&255,x:$&z,y:$>>>X&z,hit:Q?.[Z]})}))}function W2(){return M0}function H2(){M0=e0}var b4=8,GJ=8,B2=3,j2=8,K2=6,F2=0.5,a=1,pJ=2,J1=4,VJ=8,hJ=[],g4=0,U0=0,WJ=Array.from({length:b4},(J,Q)=>({slot:Q,used:!1,present:!1,id:0,x:0,y:0,startX:0,startY:0,dx:0,dy:0,fdx:0,fdy:0,vx:0,vy:0,downFrame:0,frames:0,histX:new Int16Array(GJ),histY:new Int16Array(GJ),histHead:0,histLen:0,owners:[],claimedBy:null}));function M2(J,Q){let $=J;while($){if($===Q)return!0;$=$.parent}return!1}function U2(J,Q,$,Z){let q=J.opts.region;if(!q)return!0;let X=q.node?.();if(X){if(Z.hit===void 0)Z.hit=n1(Q,$,Z.fact);let W=Z.hit;if(W)return M2(W,X)}let z=q.rect?.();if(!z)return!1;return Q>=z.x&&Q=z.y&&$=0;z--){let W=hJ[z];if(W.disposed)continue;if(g4>0&&!W.opts.allowWhenBlocked)continue;if(!U2(W,$,Z,X))continue;W.flags[J.slot]=a,J.owners.push(W)}for(let z of J.owners)z.opts.onDown?.(J)}function D2(J,Q,$){if(J.present=!0,J.fdx=Q-J.x,J.fdy=$-J.y,J.x=Q,J.y=$,J.dx=Q-J.startX,J.dy=$-J.startY,J.frames++,J.histX[J.histHead]=Q,J.histY[J.histHead]=$,J.histHead=(J.histHead+1)%GJ,J.histLenz||Z>z))q.flags[J.slot]|=pJ}if(!J.claimedBy)for(let q of J.owners){let X=q.flags[J.slot];if(!(X&a)||X&(pJ|J1))continue;if(!q.opts.onLongPress)continue;let z=Math.max(1,Math.round((q.opts.longPressSeconds??F2)*T1()));if(J.frames=0)hJ.splice($,1)},cancel(){if(!Q.disposed)N4(Q)},get panning(){for(let $ of WJ)if($.used&&Q.flags[$.slot]&VJ)return!0;return!1}}}function A2(J){let Q=I2(J);return S0(()=>Q.dispose()),Q}function E4(){hJ.length=0,g4=0,U0=0;for(let J of WJ)J.used=!1,J.present=!1,J.owners.length=0,J.claimedBy=null}function _2(){return A2({onDown:(J)=>{let Q=o1(J.x,J.y,J.hit);if(Q)z0(Q)},onTap:(J)=>{z0(null);let Q=o1(J.x,J.y,J.hit);if(Q)M8(Q)},onUp:()=>z0(null),onCancel:()=>z0(null)})}var k2=1,Q1=new Map,R0=[];function S2(){let J=globalThis.__pocketEffectTrace;return typeof J==="function"?J:null}function b2(){k2=1,Q1.clear(),R0=[]}function g2(){if(R0.length===0)return;let J=R0;R0=[];for(let{id:Q,result:$}of J){let Z=Q1.get(Q);if(!Z)continue;Q1.delete(Q),S2()?.({t:"delivery",frame:y1(),id:Q,kind:Z.kind}),Z.onResult($)}}var h4=new Set;function w2(){if(h4.size===0)return;for(let J of h4)J()}var C2={"h-[108] px-[10] py-4 flex-row gap-[10] bg-slate-950":0,"w-[165] h-[76] items-center justify-center bg-orange-600":1,"w-[165] h-[76] items-center justify-center bg-slate-900":2,"text-base text-white font-bold":3,"flex-col w-full h-full bg-slate-50":4,"h-[686] px-6 pt-7 flex-col gap-[22]":5,"w-[584] h-[318] px-6 py-5 flex-col rounded-xl shadow bg-white border-slate-100":6,"h-[30] flex-row items-center gap-3":7,"w-[10] h-[10] rounded bg-orange-500":8,"text-lg text-orange-600 font-bold":9,"h-[84] pt-3 text-xl text-slate-900":10,"h-[2] mx-1 my-4 bg-slate-100":11,"w-[10] h-[10] rounded bg-emerald-500":12,"text-lg text-emerald-700 font-bold":13,"absolute left-[628] top-[28] w-[68] h-[132] items-center justify-center bg-orange-100":14,"absolute left-[628] top-[512] w-[68] h-[132] items-center justify-center bg-orange-100":15,"h-[228] mx-6 px-6 py-5 flex-col rounded-xl shadow bg-white border-slate-100":16,"text-base text-slate-600 font-bold":17,"pt-6 text-lg text-slate-500":18,"pt-6 text-lg text-slate-900 font-bold":19,"h-[146] px-6 pt-7 flex-col bg-slate-50":20,"h-[80]":21,"h-[1060] px-6 pt-5 flex-col":22,"h-[48] px-4 justify-center bg-slate-100":23,"text-base text-slate-600":24,"pt-[10] flex-col gap-[12]":25,"w-[584] h-[92] px-[18] flex-row items-center gap-5 bg-white":26,"w-[48] h-[48] items-center justify-center bg-blue-100":27,"w-[48] h-[48] items-center justify-center bg-emerald-100":28,"text-lg text-blue-700 font-bold":29,"w-[474] flex-col gap-2":30,"text-lg text-slate-900 font-bold":31,"text-base text-slate-500":32,"pt-20 pl-10 text-lg text-slate-500":33,"absolute left-[628] top-[78] w-[68] h-[132] items-center justify-center bg-orange-100":34,"absolute left-[628] top-[828] w-[68] h-[132] items-center justify-center bg-orange-100":35,"relative h-[1060] px-6 pt-7 flex-col gap-4":36,"text-base text-slate-500 font-bold":37,"h-[214] px-7 items-center justify-center bg-slate-100":38,"text-lg text-slate-500 font-bold":39,"w-[672] h-[150] px-6 flex-row items-center justify-between bg-white":40,"flex-row items-center gap-5":41,"w-[68] h-[68] items-center justify-center bg-orange-100":42,"text-xl text-orange-700 font-bold":43,"w-[448] flex-col gap-2":44,"text-xl text-slate-900 font-bold":45,"text-lg text-slate-600":46,"text-2xl text-orange-600":47,"w-[68] h-[68]":48,"absolute left-[24] top-[820] w-[672] h-[112] px-6 bg-slate-100":49,"absolute left-[24] top-[948] w-[672] h-[80]":50,"relative h-[154] px-5 pt-5 flex-col bg-white":51,"pt-3 text-lg text-orange-600 font-bold":52,"pt-3 text-base text-slate-500":53,"absolute left-[456] top-[14] w-[196] h-[72] items-center justify-center bg-orange-600":54,"text-lg text-white font-bold":55,"h-[40] pt-3 text-base text-slate-500":56,"h-[470] flex-col gap-2":57,"w-[584] h-[84] px-5 flex-row items-center justify-between bg-white":58,"absolute left-[604] top-[0] w-[68] h-[132] items-center justify-center bg-orange-100":59,"absolute left-[604] top-[320] w-[68] h-[132] items-center justify-center bg-orange-100":60,"h-[160] px-5 pt-5 flex-col bg-white":61,"pt-3 text-lg text-slate-900 font-bold":62,"pt-3 text-base text-slate-600":63,"h-[108] pt-7 flex-row gap-4":64,"w-[316] h-[80] items-center justify-center bg-slate-100":65,"w-[340] h-[80] items-center justify-center bg-red-100":66,"text-lg text-red-500 font-bold":67,"h-[1052] px-6 pt-5 flex-col":68,"h-[270] px-[22] pt-6 bg-white":69,"text-lg text-slate-900":70,"text-lg text-slate-400":71,"h-[86] px-1 flex-row items-center justify-between":72,"w-[132] h-[58] items-center justify-center bg-slate-300":73,"w-[132] h-[58] items-center justify-center bg-slate-100":74,"text-base text-slate-900 font-bold":75,"h-[140] flex-row gap-2":76,"grow h-[120] items-center justify-center bg-slate-300":77,"grow h-[120] items-center justify-center bg-white":78,"w-[104] h-[120] items-center justify-center bg-slate-300":79,"w-[104] h-[120] items-center justify-center bg-slate-100":80,"h-[176] flex-row gap-2":81,"w-[92] h-[156] items-center justify-center bg-slate-300":82,"w-[92] h-[156] items-center justify-center bg-slate-100":83,"w-[300] h-[156] items-center justify-center bg-slate-300":84,"w-[300] h-[156] items-center justify-center bg-slate-100":85,"w-[144] h-[156] items-center justify-center bg-slate-300":86,"w-[144] h-[156] items-center justify-center bg-slate-100":87,"w-[112] h-[156] items-center justify-center bg-emerald-700":88,"w-[112] h-[156] items-center justify-center bg-emerald-500":89,"text-base text-slate-950 font-bold":90,"h-[108] px-6 py-3 flex-col bg-slate-50":91,"h-[84] items-center justify-center bg-slate-300":92,"h-[84] items-center justify-center bg-slate-100":93,"h-[1168] px-6 pt-5 flex-col":94,"h-[82] px-4 justify-center bg-white":95,"w-[584] h-[900] px-5 pt-5 bg-slate-950":96,"text-base text-slate-200":97,"absolute left-[628] top-[58] w-[68] h-[132] items-center justify-center bg-orange-100":98,"w-[584] h-[900] px-5 pt-5 bg-white":99,"text-xl text-slate-900":100,"w-full h-[150] px-6 flex-col justify-center gap-3 rounded-xl bg-red-100":101,"w-full h-[150] px-6 flex-col justify-center gap-3 rounded-xl bg-emerald-100":102,"w-full h-[150] px-6 flex-col justify-center gap-3 rounded-xl bg-amber-100":103,"w-full h-[150] px-6 flex-col justify-center gap-3 rounded-xl bg-orange-100":104,"text-xl text-red-700 font-bold":105,"text-xl text-emerald-700 font-bold":106,"text-xl text-amber-700 font-bold":107,"h-[602] px-6 flex-col":108,"h-[526] px-6 py-6 flex-col gap-5 rounded-xl shadow bg-white border-slate-100":109,"h-[2] bg-slate-100":110,"h-[166] px-6 pt-4":111,"h-[168] px-6 pt-6 pb-6":112,"w-full h-[120]":113,"h-[66]":114,"relative flex-col w-full h-full bg-slate-50 overflow-hidden":115,"absolute inset-0 z-50 flex-col items-center justify-center":116,"absolute inset-0 bg-slate-950":117,"flex-col gap-2 w-[328] p-3 rounded-xl shadow-lg bg-white border-slate-200":118,"absolute left-3 right-3 bottom-3 flex-row items-center justify-between px-2 py-1 rounded-lg shadow-md bg-white border-slate-200":119,"flex-row flex-wrap":120,grow:121,"text-2xl text-white font-bold":122,"text-2xl text-slate-950 font-bold":123,"px-3 py-2 rounded-lg bg-slate-100":124,"px-3 py-2 rounded-lg bg-indigo-100":125,"text-base text-indigo-700 font-bold":126,"px-3 py-2 rounded-lg bg-emerald-100":127,"text-base text-emerald-700 font-bold":128,"px-3 py-2 rounded-lg bg-amber-100":129,"text-base text-amber-700 font-bold":130,"px-3 py-2 rounded-lg bg-red-100":131,"text-base text-red-500 font-bold":132,"w-[34] h-[34] rounded-lg bg-amber-400":133,"w-[34] h-[34] rounded-lg bg-red-500":134,"w-[34] h-[34] rounded-lg bg-slate-800":135,"w-[34] h-[34] rounded-lg bg-emerald-500":136,"h-[112] px-6 flex-row items-center justify-between bg-slate-950":137,"flex-row items-center gap-4":138,"w-[34] text-2xl text-white font-bold":139,"w-[332] flex-col items-end gap-2":140,"text-base text-slate-300 font-bold":141,"text-base text-slate-400":142,"h-[166] px-6 pt-6 flex-col gap-3":143,"text-base text-orange-600 font-bold":144,"h-[44] px-1 flex-row items-center justify-between":145,"w-full h-full items-center justify-center rounded-xl bg-slate-200":146,"w-full h-full items-center justify-center rounded-xl bg-red-100":147,"w-full h-full items-center justify-center rounded-xl bg-slate-100":148,"w-full h-full items-center justify-center rounded-xl bg-orange-600":149,"w-full h-[150] px-5 flex-col items-center justify-center rounded-xl shadow bg-white border-slate-100":150,"w-full h-[430] px-12 flex-col items-center justify-center rounded-xl shadow bg-white border-slate-100":151,"w-[88] h-[88] items-center justify-center rounded-xl bg-indigo-100":152,"w-[88] h-[88] items-center justify-center rounded-xl bg-slate-100":153,"text-2xl text-indigo-700 font-bold":154,"text-2xl text-slate-600 font-bold":155,"pt-7 text-2xl text-slate-900 font-bold":156,"pt-4 text-lg text-slate-500":157,"text-xl text-emerald-600 font-bold":158,"text-xl text-red-500 font-bold":159,"w-full h-full px-5 py-4 flex-col gap-3 rounded-xl shadow bg-white border-slate-100":160,"text-base text-red-300":161,"text-base text-slate-300":162,"text-base text-red-500":163,"w-full h-full px-6 flex-row items-center bg-slate-950":164,"w-full h-full flex-row items-center":165};if(typeof globalThis.queueMicrotask!=="function")globalThis.queueMicrotask=(J)=>{Promise.resolve().then(J)};var N2="ui:styles",E2="ui:font.",T4="ui:img.",y4="ui:sprite.";function h2(){return globalThis.ui}function T2(J){if(J.__textures)return;for(let Q of T0(T4)){let $=vJ(Q),Z;if(J.uploadImgEntry)Z=J.uploadImgEntry($);else{let q=new DataView($.buffer,$.byteOffset,$.byteLength);Z=J.uploadTexture($.subarray(8),q.getUint16(0,!0),q.getUint16(2,!0),$[4])}if(Z>=0)$4(Q.slice(T4.length),Z)}}function y2(J){if(J.__sprites)return;for(let Q of T0(y4)){let $=vJ(Q),Z=new DataView($.buffer,$.byteOffset,$.byteLength),q=Z.getUint16(0,!0),X=Z.getUint16(2,!0),z=$[4],W=Z.getUint16(6,!0),R=Z.getUint16(8,!0),D=Z.getUint16(10,!0),V=J.uploadTexture($.subarray(16),q,X,z);if(V>=0)q4(Q.slice(y4.length),{handle:V,frames:W,cols:R,step:D})}}function f4(J){let Q=H4("view");return EJ(Q,"style",J,void 0),Q}var P0=null,D0=null;function f2(J,Q){if(!P0||!D0)return;EJ(P0,"style",{width:J,height:Q,overflow:c.Overflow.Hidden},void 0),EJ(D0,"style",{width:J,height:Q,posType:c.PosType.Absolute,insetT:0,insetR:0,insetB:0,insetL:0,zIndex:1000},void 0);let $=h();$.__viewport={w:J,h:Q}}function x2(J,Q={}){let $=l6(Q.ops);if(c6($),k8($2),Q.styles)Q2(Q.styles);let Z=$.kind==="native"?$.ops.__textures:void 0;if($.kind==="native"){if(Z)for(let _ in Z)$4(_,Z[_]);let H=$.ops.__sprites;if(H)for(let _ in H)q4(_,H[_])}if($.kind==="injected"||Z===void 0){if(Q.pak)u1(Q.pak);if(z8()){for(let H of T0())if(H===N2)$.ops.loadStyles?.(vJ(H));else if(H.startsWith(E2))$.ops.loadFontAtlas?.(vJ(H))}}let q=N1($.ops),X=q?.w??g1,z=q?.h??w1,W=f4({width:X,height:z,overflow:c.Overflow.Hidden}),R=f4({width:X,height:z,posType:c.PosType.Absolute,insetT:0,insetR:0,insetB:0,insetL:0,zIndex:1000,hitPass:1});YJ(XJ,W),YJ(XJ,R),B4(R),P0=W,D0=R,l1(W),a1(XJ),q8(),E4(),_2(),n6(),b2(),p8($.ops),p6(d8((H,_,g,C)=>{t6(),e6(_),G2(g,C),w2(),g2(),O2(),X8(H),I8(H),Y4()}));let D=x8(J,W),V=d6(f2);return()=>{V(),H2(),E4(),D(),l1(null),a1(null),B4(null),P0=null,D0=null;for(let H of XJ.children.splice(0))H.parent=null,$.ops.destroyNode(H.id);Y4()}}function v2(J,Q={}){let $=Q.ops??h2();if(!$)throw Error("PocketJS: mount() requires globalThis.ui or opts.ops");if(Q.pak)u1(Q.pak);return T2($),y2($),x2(J,{ops:$,styles:Q.styles??C2,pak:Q.pak})}var m2="$b",u2=65536,x4="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",dJ={};for(let J=0;JJ.length)throw Error("invalid UTF-8");for(let z=0;z1114111||q>=55296&&q<=57343||X===1&&q<128||X===2&&q<2048||X===3&&q<65536)throw Error("invalid UTF-8");if(q<65536)Q+=String.fromCharCode(q);else q-=65536,Q+=String.fromCharCode(55296+(q>>10),56320+(q&1023))}return Q}function p2(){let J=globalThis.fs;if(!J||typeof J!=="object")return null;return typeof J.read==="function"?J:null}function v4(){let J=p2();if(!J)throw Error("fs: globalThis.fs is not mounted — declare `data.fs` in pocket.json requires");return J}function d2(J,Q){let $=[],Z=0;for(;;){let z=JSON.parse(J.read(Q,Z,u2));if(z.error!==void 0)throw Error(`fs: read ${Q}: ${z.error}`);let W=l2(z.data[m2]);if($.push(W),Z+=W.length,z.eof)break}if($.length===1)return $[0];let q=new Uint8Array(Z),X=0;for(let z of $)q.set(z,X),X+=z.length;return q}function i2(J,Q){let $=d2(v4(),J);return Q==="utf8"||Q==="utf-8"?c2($):$}function s2(J,Q){let $=v4(),Z=[],q=0;for(;;){let X=JSON.parse($.list(J,q));if(X.error!==void 0)throw Error(`fs: readdir ${J}: ${X.error}`);for(let z of X.entries)Z.push({...z,isFile:()=>z.kind==="file",isDirectory:()=>z.kind==="dir"});if(q+=X.entries.length,X.eof)break}return Q?.withFileTypes?Z:Z.map((X)=>X.name)}var V0={appTitle:"text-2xl text-white font-bold",pageTitle:"text-2xl text-slate-950 font-bold",heading:"text-xl text-slate-900 font-bold",label:"text-base text-slate-600 font-bold",captionStrong:"text-base text-slate-500 font-bold"};function m4(J){let Q=()=>J.accent==="busy"?"w-[34] h-[34] rounded-lg bg-amber-400":J.accent==="danger"?"w-[34] h-[34] rounded-lg bg-red-500":J.accent==="none"?"w-[34] h-[34] rounded-lg bg-slate-800":"w-[34] h-[34] rounded-lg bg-emerald-500";return Y(j,{class:"h-[112] px-6 flex-row items-center justify-between bg-slate-950",get children(){return[Y(j,{class:"flex-row items-center gap-4",get children(){return[T(()=>T(()=>!!J.back)()?Y(U,{class:"w-[34] text-2xl text-white font-bold",children:"‹"}):Y(j,{get["class"](){return Q()}})),Y(U,{get["class"](){return V0.appTitle},get children(){return J.title}})]}}),Y(j,{class:"w-[332] flex-col items-end gap-2",get children(){return[Y(U,{class:"text-base text-slate-300 font-bold",get children(){return J.metaTop??""}}),Y(U,{class:"text-base text-slate-400",get children(){return J.metaBottom??""}})]}})]}})}function a2(J){return Y(j,{class:"h-[166] px-6 pt-6 flex-col gap-3",get children(){return[Y(U,{get["class"](){return J.tone==="info"?"text-base text-indigo-700 font-bold":"text-base text-orange-600 font-bold"},get children(){return J.eyebrow}}),Y(U,{get["class"](){return V0.pageTitle},get children(){return J.title}}),Y(U,{class:"text-lg text-slate-600",get children(){return J.description}})]}})}function r2(J){let Q=()=>J.action?J.detail?J.detail+" · VIEW ALL ›":"VIEW ALL ›":J.detail??"";return Y(j,{class:"h-[44] px-1 flex-row items-center justify-between",get children(){return[Y(U,{get["class"](){return V0.heading},get children(){return J.title}}),Y(U,{get["class"](){return V0.captionStrong},get children(){return Q()}})]}})}function L0(J){let Q=()=>J.disabled?"w-full h-full items-center justify-center rounded-xl bg-slate-200":J.tone==="danger"?"w-full h-full items-center justify-center rounded-xl bg-red-100":J.tone==="neutral"?"w-full h-full items-center justify-center rounded-xl bg-slate-100":"w-full h-full items-center justify-center rounded-xl bg-orange-600",$=()=>J.disabled?"text-lg text-slate-500 font-bold":J.tone==="danger"?"text-lg text-red-500 font-bold":J.tone==="neutral"?"text-lg text-slate-900 font-bold":"text-lg text-white font-bold";return Y(j,{get["class"](){return Q()},get children(){return Y(U,{get["class"](){return $()},get children(){return J.label}})}})}function u4(J){let Q=()=>J.dark?J.tone==="danger"?"text-base text-red-300":"text-base text-slate-300":J.tone==="danger"?"text-base text-red-500":"text-base text-slate-500";return Y(j,{get["class"](){return J.dark?"w-full h-full px-6 flex-row items-center bg-slate-950":"w-full h-full flex-row items-center"},get children(){return Y(U,{get["class"](){return Q()},get children(){return J.text}})}})}function iJ(J){return[Y(j,{get["class"](){return J.top},get children(){return Y(U,{class:"text-base text-orange-600 font-bold",children:"UP"})}}),Y(j,{get["class"](){return J.bottom},get children(){return Y(U,{class:"text-base text-orange-600 font-bold",children:"DN"})}})]}var J$="‘’“”‑–—…•",o2=512;function n2(J,Q,$){let Z=[],q="",X=0;for(let z of J){let W=Q(z);if(q&&X+W>$)Z.push(q),q="",X=0;q+=z,X+=W}if(q)Z.push(q);return Z}function l4(J,Q,$){let Z=new Map,q=(W)=>{let R=Z.get(W);if(R===void 0)R=h().measureText(W,Q),Z.set(W,R);return R},X=[],z=q(" ");for(let W of J.split(` +`)){let R=W.split(" ").flatMap((H)=>H&&q(H)>$?n2(H,q,$):[H]),D="",V=0;for(let H of R){let _=q(H);if(!D)D=H,V=_;else if(V+z+_<=$)D+=" "+H,V+=z+_;else X.push(D),D=H,V=_}X.push(D)}return X}function $1(J,Q,$,Z){let q=J.slice(0,o2),X=l4(q,Q,$);if(q.length===J.length&&X.length<=Z)return X.join(` `);let z=X.slice(0,Z),W=z.length-1;return z[W]=z[W].replace(/[\s.]+$/,"")+"…",z.join(` -`)}function n2(J,Q,$,Z){let q=$;while(qQ){if(R>X)return{text:z.slice(0,D).replace(/\s+$/,""),nextOffset:R,sourceLineEnded:!1};return{text:z,nextOffset:q,sourceLineEnded:!1}}if(z+=V,W+=B,q+=1,V===" ")R=q,D=z.length-1}return{text:z.replace(/\s+$/,""),nextOffset:q,sourceLineEnded:!1}}function t2(J,Q,$,Z,q,X){let z=Math.max(0,Math.floor(X)),W=[],R=Math.max(0,Math.min(Math.floor(Z),J.length)),D=Math.max(0,Math.floor(q)),V=D,B=new Map,_=(g)=>{let C=B.get(g);if(C===void 0)C=h().measureText(g,Q),B.set(g,C);return C};while(R0&&!Q.page.hasMore)return;let Z=Q.pageStarts[$];if(!Z)Z={offset:Q.page.nextOffset,sourceLine:Q.page.nextSourceLine},Q.pageStarts.push(Z);z1({...Q,pageIndex:$,page:Z6(Q.text,Z)})}function I0(J=VJ(),Q=!1){let $=e4.get(J);if(!Q&&$){X1($),V0(""),hJ((Z)=>Math.min(Z,Math.max(0,$.length-8)));return}try{let Z=d2(J,{withFileTypes:!0}).map((q)=>({name:q.name,kind:q.isDirectory()?"dir":"file",size:q.size}));e4.set(J,Z),X1(Z),V0(""),hJ((q)=>Math.min(q,Math.max(0,Z.length-8)))}catch(Z){X1([]),V0(Z instanceof Error?Z.message:String(Z))}}function HQ(){let J=[];for(let Q of p().messages??[])if(Q.role==="user")J.push({user:Q.text,assistant:"THINKING..."});else if(J.length===0)J.push({user:"TYPE A MESSAGE",assistant:Q.text||"THINKING..."});else J[J.length-1].assistant=Q.text||"THINKING...";return J.length?J:[{user:"TYPE A MESSAGE",assistant:"BOOTING PI AGENT..."}]}function X6(){let J=HQ(),Q=Math.max(0,J.length-2),$=Math.min(q1(),Q),Z=J.length-$;return J.slice(Math.max(0,Z-2),Z)}function OJ(J){return Y(v4,{get title(){return J.title},get accent(){return y(()=>p().agent==="FAULTED")()?"danger":p().agent==="THINKING"?"busy":"ready"}})}function A0(){return Y(j,{class:"h-[108] px-[10] py-4 flex-row gap-[10] bg-slate-950",get children(){return Y(jJ,{each:$6,children:(J)=>Y(j,{get["class"](){return Z1()===J?"w-[165] h-[76] items-center justify-center bg-orange-600":"w-[165] h-[76] items-center justify-center bg-slate-900"},get children(){return Y(U,{class:"text-base text-white font-bold",get children(){return J.toUpperCase()}})}})})}})}function jQ(){let J=()=>p().schedule,Q=()=>{let $=J();return Q1(String($?.name)+" "+String($?.next??"")+` +`?q+2:q+1;return{text:z.replace(/\s+$/,""),nextOffset:_,sourceLineEnded:!0}}let H=Z(V);if(z&&W+H>Q){if(R>X)return{text:z.slice(0,D).replace(/\s+$/,""),nextOffset:R,sourceLineEnded:!1};return{text:z,nextOffset:q,sourceLineEnded:!1}}if(z+=V,W+=H,q+=1,V===" ")R=q,D=z.length-1}return{text:z.replace(/\s+$/,""),nextOffset:q,sourceLineEnded:!1}}function e2(J,Q,$,Z,q,X){let z=Math.max(0,Math.floor(X)),W=[],R=Math.max(0,Math.min(Math.floor(Z),J.length)),D=Math.max(0,Math.floor(q)),V=D,H=new Map,_=(g)=>{let C=H.get(g);if(C===void 0)C=h().measureText(g,Q),H.set(g,C);return C};while(R0&&!Q.page.hasMore)return;let Z=Q.pageStarts[$];if(!Z)Z={offset:Q.page.nextOffset,sourceLine:Q.page.nextSourceLine},Q.pageStarts.push(Z);G1({...Q,pageIndex:$,page:X6(Q.text,Z)})}function A0(J=HJ()){try{let Q=s2(J,{withFileTypes:!0}).map(($)=>({name:$.name,kind:$.isDirectory()?"dir":"file",size:$.size}));r4(Q),Y1(""),aJ(($)=>Math.min($,Math.max(0,Q.length-8)))}catch(Q){r4([]),Y1(Q instanceof Error?Q.message:String(Q))}}function jQ(){let J=[];for(let Q of y().messages??[])if(Q.role==="user")J.push({user:Q.text,assistant:"THINKING..."});else if(J.length===0)J.push({user:"TYPE A MESSAGE",assistant:Q.text||"THINKING..."});else J[J.length-1].assistant=Q.text||"THINKING...";return J.length?J:[{user:"TYPE A MESSAGE",assistant:"BOOTING PI AGENT..."}]}function z6(){let J=jQ(),Q=Math.max(0,J.length-2),$=Math.min(X1(),Q),Z=J.length-$;return J.slice(Math.max(0,Z-2),Z)}function IJ(J){return Y(m4,{get title(){return J.title},get accent(){return T(()=>y().agent==="FAULTED")()?"danger":y().agent==="THINKING"?"busy":"ready"}})}function _0(){return Y(j,{class:"h-[108] px-[10] py-4 flex-row gap-[10] bg-slate-950",get children(){return Y(KJ,{each:q6,children:(J)=>Y(j,{get["class"](){return q1()===J?"w-[165] h-[76] items-center justify-center bg-orange-600":"w-[165] h-[76] items-center justify-center bg-slate-900"},get children(){return Y(U,{class:"text-base text-white font-bold",get children(){return J.toUpperCase()}})}})})}})}function KQ(){let J=()=>y().schedule,Q=()=>{let $=J();return $1(String($?.name)+" "+String($?.next??"")+` -`+String($?.prompt??""),e2,$1,5)};return Y(j,{class:"flex-col w-full h-full bg-slate-50",get children(){return[Y(OJ,{title:"ESP32 PI AGENT"}),Y(j,{class:"h-[686] px-6 pt-7 flex-col gap-[22]",get children(){return[Y(jJ,{get each(){return X6()},children:($)=>Y(j,{class:"w-[584] h-[318] px-6 py-5 flex-col rounded-xl shadow bg-white border-slate-100",get children(){return[Y(j,{class:"h-[30] flex-row items-center gap-3",get children(){return[Y(j,{class:"w-[10] h-[10] rounded bg-orange-500"}),Y(U,{class:"text-lg text-orange-600 font-bold",children:"YOU"})]}}),Y(U,{class:"h-[84] pt-3 text-xl text-slate-900",get children(){return Q1($.user,l4,$1,3)}}),Y(j,{class:"h-[2] mx-1 my-4 bg-slate-100"}),Y(j,{class:"h-[30] flex-row items-center gap-3",get children(){return[Y(j,{class:"w-[10] h-[10] rounded bg-emerald-500"}),Y(U,{class:"text-lg text-emerald-700 font-bold",children:"PI"})]}}),Y(U,{class:"h-[84] pt-3 text-xl text-slate-900",get children(){return Q1($.assistant,l4,$1,3)}})]}})}),Y(iJ,{top:"absolute left-[628] top-[28] w-[68] h-[132] items-center justify-center bg-orange-100",bottom:"absolute left-[628] top-[512] w-[68] h-[132] items-center justify-center bg-orange-100"})]}}),Y(j,{class:"h-[228] mx-6 px-6 py-5 flex-col rounded-xl shadow bg-white border-slate-100",get children(){return[Y(U,{class:"text-base text-slate-600 font-bold",children:"NEXT WAKE"}),Y(ZJ,{get when(){return J()?.name},get fallback(){return Y(U,{class:"pt-6 text-lg text-slate-500",children:`NO WAKE SCHEDULED +`+String($?.prompt??""),JQ,Z1,5)};return Y(j,{class:"flex-col w-full h-full bg-slate-50",get children(){return[Y(IJ,{title:"ESP32 PI AGENT"}),Y(j,{class:"h-[686] px-6 pt-7 flex-col gap-[22]",get children(){return[Y(KJ,{get each(){return z6()},children:($)=>Y(j,{class:"w-[584] h-[318] px-6 py-5 flex-col rounded-xl shadow bg-white border-slate-100",get children(){return[Y(j,{class:"h-[30] flex-row items-center gap-3",get children(){return[Y(j,{class:"w-[10] h-[10] rounded bg-orange-500"}),Y(U,{class:"text-lg text-orange-600 font-bold",children:"YOU"})]}}),Y(U,{class:"h-[84] pt-3 text-xl text-slate-900",get children(){return $1($.user,c4,Z1,3)}}),Y(j,{class:"h-[2] mx-1 my-4 bg-slate-100"}),Y(j,{class:"h-[30] flex-row items-center gap-3",get children(){return[Y(j,{class:"w-[10] h-[10] rounded bg-emerald-500"}),Y(U,{class:"text-lg text-emerald-700 font-bold",children:"PI"})]}}),Y(U,{class:"h-[84] pt-3 text-xl text-slate-900",get children(){return $1($.assistant,c4,Z1,3)}})]}})}),Y(iJ,{top:"absolute left-[628] top-[28] w-[68] h-[132] items-center justify-center bg-orange-100",bottom:"absolute left-[628] top-[512] w-[68] h-[132] items-center justify-center bg-orange-100"})]}}),Y(j,{class:"h-[228] mx-6 px-6 py-5 flex-col rounded-xl shadow bg-white border-slate-100",get children(){return[Y(U,{class:"text-base text-slate-600 font-bold",children:"NEXT WAKE"}),Y(n,{get when(){return J()?.name},get fallback(){return Y(U,{class:"pt-6 text-lg text-slate-500",children:`NO WAKE SCHEDULED -ASK PI TO CREATE ONE WITH SCHEDULE.SET`})},get children(){return Y(U,{class:"pt-6 text-lg text-slate-900 font-bold",get children(){return Q()}})}})]}}),Y(j,{class:"h-[146] px-6 pt-7 flex-col bg-slate-50",get children(){return Y(j,{class:"h-[80]",get children(){return Y(m4,{label:"TYPE A MESSAGE"})}})}}),Y(A0,{})]}})}function KQ(){let J=()=>aJ().slice(sJ(),sJ()+8);return Y(j,{class:"flex-col w-full h-full bg-slate-50",get children(){return[Y(OJ,{get title(){return VJ()?"< WORKSPACE FILES":"WORKSPACE FILES"}}),Y(j,{class:"h-[1060] px-6 pt-5 flex-col",get children(){return[Y(j,{class:"h-[48] px-4 justify-center bg-slate-100",get children(){return Y(U,{class:"text-base text-slate-600",get children(){return"/workspace"+(VJ()?"/"+VJ():"")}})}}),Y(j,{class:"pt-[10] flex-col gap-[12]",get children(){return Y(jJ,{get each(){return J()},children:(Q)=>Y(j,{class:"w-[584] h-[92] px-[18] flex-row items-center gap-5 bg-white",get children(){return[Y(j,{get["class"](){return Q.kind==="dir"?"w-[48] h-[48] items-center justify-center bg-blue-100":"w-[48] h-[48] items-center justify-center bg-emerald-100"},get children(){return Y(U,{get["class"](){return Q.kind==="dir"?"text-lg text-blue-700 font-bold":"text-lg text-emerald-700 font-bold"},get children(){return Q.kind==="dir"?"D":"F"}})}}),Y(j,{class:"w-[474] flex-col gap-2",get children(){return[Y(U,{class:"text-lg text-slate-900 font-bold",get children(){return(Q.name+(Q.kind==="dir"?"/":"")).slice(0,52)}}),Y(U,{class:"text-base text-slate-500",get children(){return y(()=>Q.kind==="dir")()?"FOLDER":GQ(Q.size)}})]}})]}})})}}),Y(ZJ,{get when(){return aJ().length===0},get children(){return Y(U,{class:"pt-20 pl-10 text-lg text-slate-500",get children(){return zQ()||"THIS DIRECTORY IS EMPTY"}})}}),Y(ZJ,{get when(){return aJ().length>8},get children(){return Y(iJ,{top:"absolute left-[628] top-[78] w-[68] h-[132] items-center justify-center bg-orange-100",bottom:"absolute left-[628] top-[828] w-[68] h-[132] items-center justify-center bg-orange-100"})}})]}}),Y(A0,{})]}})}function FQ(){let J=()=>p().apps??[];return Y(j,{class:"flex-col w-full h-full bg-slate-50",get children(){return[Y(OJ,{title:"APPS"}),Y(j,{class:"h-[1060] px-6 pt-7 flex-col gap-4",get children(){return[Y(U,{class:"text-base text-slate-500 font-bold",get children(){return String(J().length)+" INSTALLED APPS"}}),Y(ZJ,{get when(){return J().length>0},get fallback(){return Y(j,{class:"h-[214] px-7 items-center justify-center bg-slate-100",get children(){return Y(U,{class:"text-lg text-slate-500 font-bold",children:"NO OPTIONAL APPS INSTALLED"})}})},get children(){return Y(jJ,{get each(){return J()},children:(Q)=>Y(j,{class:"w-[672] h-[150] px-6 flex-row items-center justify-between bg-white",get children(){return[Y(j,{class:"flex-row items-center gap-5",get children(){return[Y(j,{class:"w-[68] h-[68] items-center justify-center bg-orange-100",get children(){return Y(U,{class:"text-xl text-orange-700 font-bold",get children(){return Q.title.slice(0,1).toUpperCase()}})}}),Y(j,{class:"w-[500] flex-col gap-2",get children(){return[Y(U,{class:"text-xl text-slate-900 font-bold",get children(){return Q.title}}),Y(U,{class:"text-lg text-slate-600",get children(){return Q.description}}),Y(ZJ,{get when(){return Q.scheduleEveryMinutes},get children(){return Y(U,{class:"text-base text-slate-500",get children(){return"UPDATES EVERY "+String(Q.scheduleEveryMinutes)+" MINUTES"}})}})]}})]}}),Y(U,{class:"text-2xl text-orange-600",children:"›"})]}})})}}),Y(j,{class:"mt-4 h-[112] px-6 justify-center bg-slate-100",get children(){return Y(U,{class:"text-base text-slate-600",children:`APP DATA STAYS ISOLATED. -PI AGENT CAN USE EACH APP'S TOOLS.`})}})]}}),Y(A0,{})]}})}function MQ(){let J=()=>p().settings??{},Q=()=>J().wifi??{},$=()=>(Q().networks??[]).slice(rJ(),rJ()+5),Z=()=>Q().ipAddress?"IP "+Q().ipAddress+" RSSI "+String(Q().rssiDbm??"--")+" DBM":Q().status||"SCAN AND SELECT A NETWORK";return Y(j,{class:"flex-col w-full h-full bg-slate-50",get children(){return[Y(OJ,{title:"SETTINGS"}),Y(j,{class:"h-[1060] px-6 pt-5 flex-col",get children(){return[Y(j,{class:"relative h-[154] px-5 pt-5 flex-col bg-white",get children(){return[Y(U,{class:"text-lg text-slate-900 font-bold",children:"WI-FI"}),Y(U,{class:"pt-3 text-lg text-orange-600 font-bold",get children(){return Q().connectedSsid||"NOT CONNECTED"}}),Y(U,{class:"pt-3 text-base text-slate-500",get children(){return Z()}}),Y(j,{class:"absolute left-[456] top-[14] w-[196] h-[72] items-center justify-center bg-orange-600",get children(){return Y(U,{class:"text-lg text-white font-bold",get children(){return Q().scanning?"SCANNING":"SCAN"}})}})]}}),Y(U,{class:"h-[40] pt-3 text-base text-slate-500",children:"AVAILABLE NETWORKS"}),Y(j,{class:"h-[470] flex-col gap-2",get children(){return[Y(ZJ,{get when(){return $().length>0},get fallback(){return Y(j,{class:"h-[214] px-7 items-center justify-center bg-slate-100",get children(){return Y(U,{class:"text-lg text-slate-500 font-bold",children:`NO NETWORK LIST YET +ASK PI TO CREATE ONE WITH SCHEDULE.SET`})},get children(){return Y(U,{class:"pt-6 text-lg text-slate-900 font-bold",get children(){return Q()}})}})]}}),Y(j,{class:"h-[146] px-6 pt-7 flex-col bg-slate-50",get children(){return Y(j,{class:"h-[80]",get children(){return Y(L0,{label:"TYPE A MESSAGE"})}})}}),Y(_0,{})]}})}function FQ(){let J=()=>rJ().slice(sJ(),sJ()+8);return Y(j,{class:"flex-col w-full h-full bg-slate-50",get children(){return[Y(IJ,{get title(){return HJ()?"< WORKSPACE FILES":"WORKSPACE FILES"}}),Y(j,{class:"h-[1060] px-6 pt-5 flex-col",get children(){return[Y(j,{class:"h-[48] px-4 justify-center bg-slate-100",get children(){return Y(U,{class:"text-base text-slate-600",get children(){return"/workspace"+(HJ()?"/"+HJ():"")}})}}),Y(j,{class:"pt-[10] flex-col gap-[12]",get children(){return Y(KJ,{get each(){return J()},children:(Q)=>Y(j,{class:"w-[584] h-[92] px-[18] flex-row items-center gap-5 bg-white",get children(){return[Y(j,{get["class"](){return Q.kind==="dir"?"w-[48] h-[48] items-center justify-center bg-blue-100":"w-[48] h-[48] items-center justify-center bg-emerald-100"},get children(){return Y(U,{get["class"](){return Q.kind==="dir"?"text-lg text-blue-700 font-bold":"text-lg text-emerald-700 font-bold"},get children(){return Q.kind==="dir"?"D":"F"}})}}),Y(j,{class:"w-[474] flex-col gap-2",get children(){return[Y(U,{class:"text-lg text-slate-900 font-bold",get children(){return(Q.name+(Q.kind==="dir"?"/":"")).slice(0,52)}}),Y(U,{class:"text-base text-slate-500",get children(){return T(()=>Q.kind==="dir")()?"FOLDER":WQ(Q.size)}})]}})]}})})}}),Y(n,{get when(){return rJ().length===0},get children(){return Y(U,{class:"pt-20 pl-10 text-lg text-slate-500",get children(){return GQ()||"THIS DIRECTORY IS EMPTY"}})}}),Y(n,{get when(){return rJ().length>8},get children(){return Y(iJ,{top:"absolute left-[628] top-[78] w-[68] h-[132] items-center justify-center bg-orange-100",bottom:"absolute left-[628] top-[828] w-[68] h-[132] items-center justify-center bg-orange-100"})}})]}}),Y(_0,{})]}})}function MQ(){let J=()=>y().apps??[],Q=()=>y().uninstallingApp,$=()=>y().uninstallError?"UNINSTALL FAILED · "+y().uninstallError:Q()?"UNINSTALLING "+Q().toUpperCase()+"...":"APP DATA STAYS ISOLATED. PI AGENT CAN USE EACH APP'S TOOLS.";return Y(j,{class:"flex-col w-full h-full bg-slate-50",get children(){return[Y(IJ,{title:"APPS"}),Y(j,{class:"relative h-[1060] px-6 pt-7 flex-col gap-4",get children(){return[Y(U,{class:"text-base text-slate-500 font-bold",get children(){return String(J().length)+" INSTALLED APPS"}}),Y(n,{get when(){return J().length>0},get fallback(){return Y(j,{class:"h-[214] px-7 items-center justify-center bg-slate-100",get children(){return Y(U,{class:"text-lg text-slate-500 font-bold",children:"NO OPTIONAL APPS INSTALLED"})}})},get children(){return Y(KJ,{get each(){return J()},children:(Z)=>Y(j,{class:"w-[672] h-[150] px-6 flex-row items-center justify-between bg-white",get children(){return[Y(j,{class:"flex-row items-center gap-5",get children(){return[Y(j,{class:"w-[68] h-[68] items-center justify-center bg-orange-100",get children(){return Y(U,{class:"text-xl text-orange-700 font-bold",get children(){return Z.title.slice(0,1).toUpperCase()}})}}),Y(j,{class:"w-[448] flex-col gap-2",get children(){return[Y(U,{class:"text-xl text-slate-900 font-bold",get children(){return Z.title}}),Y(U,{class:"text-lg text-slate-600",get children(){return Z.description}}),Y(n,{get when(){return Z.scheduleEveryMinutes},get children(){return Y(U,{class:"text-base text-slate-500",get children(){return"UPDATES EVERY "+String(Z.scheduleEveryMinutes)+" MINUTES"}})}})]}})]}}),Y(n,{get when(){return LJ()},get fallback(){return Y(U,{class:"text-2xl text-orange-600",children:"›"})},get children(){return Y(j,{class:"w-[68] h-[68]",get children(){return Y(L0,{get label(){return Q()===Z.id?"...":"X"},get disabled(){return Boolean(Q())},tone:"danger"})}})}})]}})})}}),Y(j,{class:"absolute left-[24] top-[820] w-[672] h-[112] px-6 bg-slate-100",get children(){return Y(u4,{get text(){return $()},get tone(){return y().uninstallError?"danger":"neutral"}})}}),Y(j,{class:"absolute left-[24] top-[948] w-[672] h-[80]",get children(){return Y(L0,{get label(){return LJ()?"DONE":"UNINSTALL APP"},get disabled(){return T(()=>!LJ())()&&J().length===0||Boolean(Q())},get tone(){return LJ()?"neutral":"danger"}})}})]}}),Y(_0,{})]}})}function UQ(){let J=()=>y().settings??{},Q=()=>J().wifi??{},$=()=>(Q().networks??[]).slice(oJ(),oJ()+5),Z=()=>Q().ipAddress?"IP "+Q().ipAddress+" RSSI "+String(Q().rssiDbm??"--")+" DBM":Q().status||"SCAN AND SELECT A NETWORK";return Y(j,{class:"flex-col w-full h-full bg-slate-50",get children(){return[Y(IJ,{title:"SETTINGS"}),Y(j,{class:"h-[1060] px-6 pt-5 flex-col",get children(){return[Y(j,{class:"relative h-[154] px-5 pt-5 flex-col bg-white",get children(){return[Y(U,{class:"text-lg text-slate-900 font-bold",children:"WI-FI"}),Y(U,{class:"pt-3 text-lg text-orange-600 font-bold",get children(){return Q().connectedSsid||"NOT CONNECTED"}}),Y(U,{class:"pt-3 text-base text-slate-500",get children(){return Z()}}),Y(j,{class:"absolute left-[456] top-[14] w-[196] h-[72] items-center justify-center bg-orange-600",get children(){return Y(U,{class:"text-lg text-white font-bold",get children(){return Q().scanning?"SCANNING":"SCAN"}})}})]}}),Y(U,{class:"h-[40] pt-3 text-base text-slate-500",children:"AVAILABLE NETWORKS"}),Y(j,{class:"h-[470] flex-col gap-2",get children(){return[Y(n,{get when(){return $().length>0},get fallback(){return Y(j,{class:"h-[214] px-7 items-center justify-center bg-slate-100",get children(){return Y(U,{class:"text-lg text-slate-500 font-bold",children:`NO NETWORK LIST YET -TAP SCAN TO FIND WI-FI`})}})},get children(){return Y(jJ,{get each(){return $()},children:(q)=>Y(j,{class:"w-[584] h-[84] px-5 flex-row items-center justify-between bg-white",get children(){return[Y(U,{class:"text-lg text-slate-900 font-bold",get children(){return q.ssid}}),Y(U,{class:"text-base text-slate-500",get children(){return String(q.rssiDbm)+" DBM "+(q.secured?"LOCK":"OPEN")}})]}})})}}),Y(ZJ,{get when(){return(Q().networks??[]).length>5},get children(){return Y(iJ,{top:"absolute left-[604] top-[0] w-[68] h-[132] items-center justify-center bg-orange-100",bottom:"absolute left-[604] top-[320] w-[68] h-[132] items-center justify-center bg-orange-100"})}})]}}),Y(j,{class:"h-[160] px-5 pt-5 flex-col bg-white",get children(){return[Y(U,{class:"text-base text-slate-500",children:"MODEL BACKEND"}),Y(U,{class:"pt-3 text-lg text-slate-900 font-bold",get children(){return p().model??"UNKNOWN"}}),Y(U,{class:"pt-3 text-base text-slate-600",get children(){return"FIRMWARE "+String(J().firmwareVersion??"0.1.0")+" · WORKSPACE FREE "+String(J().workspaceFree??"--")}})]}}),Y(j,{class:"h-[108] pt-7 flex-row gap-4",get children(){return[Y(j,{class:"w-[316] h-[80] items-center justify-center bg-slate-100",get children(){return Y(U,{class:"text-lg text-slate-900 font-bold",children:"FORGET WI-FI"})}}),Y(j,{class:"w-[340] h-[80] items-center justify-center bg-red-100",get children(){return Y(U,{class:"text-lg text-red-500 font-bold",children:"RESTART DEVICE"})}})]}})]}}),Y(A0,{})]}})}function UQ(){let J=()=>B1(),Q=()=>LJ()==="letters"?J6:Q6,$=()=>J().type==="wifi"?"*".repeat(m().length):m();return Y(j,{class:"flex-col w-full h-full bg-slate-50",get children(){return[Y(OJ,{get title(){return J().type==="wifi"?"WIFI PASSWORD":"NEW MESSAGE"}}),Y(j,{class:"h-[1052] px-6 pt-5 flex-col",get children(){return[Y(j,{class:"h-[270] px-[22] pt-6 bg-white",get children(){return Y(U,{get["class"](){return m()?"text-lg text-slate-900":"text-lg text-slate-400"},get children(){return $()||(J().type==="wifi"?"ENTER NETWORK PASSWORD...":"TYPE YOUR MESSAGE...")}})}}),Y(j,{class:"h-[86] px-1 flex-row items-center justify-between",get children(){return[Y(U,{class:"text-base text-slate-500",get children(){return String(m().length)+" / "+(J().type==="wifi"?"63":"256")+" CHARACTERS"}}),Y(j,{get["class"](){return BJ()==="clear"?"w-[132] h-[58] items-center justify-center bg-slate-300":"w-[132] h-[58] items-center justify-center bg-slate-100"},get children(){return Y(U,{class:"text-base text-slate-900 font-bold",children:"CLEAR"})}})]}}),Y(jJ,{get each(){return Q()},children:(Z,q)=>Y(j,{class:"h-[140] flex-row gap-2",get children(){return[Y(jJ,{get each(){return Z.split("")},children:(X)=>Y(j,{get["class"](){return BJ()==="char:"+X?"grow h-[120] items-center justify-center bg-slate-300":"grow h-[120] items-center justify-center bg-white"},get children(){return Y(U,{class:"text-xl text-slate-900 font-bold",get children(){return y(()=>!!G1())()?X.toUpperCase():X}})}})}),Y(ZJ,{get when(){return q()===2},get children(){return Y(j,{get["class"](){return BJ()==="delete"?"w-[104] h-[120] items-center justify-center bg-slate-300":"w-[104] h-[120] items-center justify-center bg-slate-100"},get children(){return Y(U,{class:"text-base text-slate-900 font-bold",children:"DEL"})}})}})]}})}),Y(j,{class:"h-[176] flex-row gap-2",get children(){return[Y(j,{get["class"](){return BJ()==="mode"?"w-[92] h-[156] items-center justify-center bg-slate-300":"w-[92] h-[156] items-center justify-center bg-slate-100"},get children(){return Y(U,{class:"text-base text-slate-900 font-bold",get children(){return LJ()==="letters"?"123":"ABC"}})}}),Y(j,{get["class"](){return BJ()==="space"?"w-[300] h-[156] items-center justify-center bg-slate-300":"w-[300] h-[156] items-center justify-center bg-slate-100"},get children(){return Y(U,{class:"text-base text-slate-900 font-bold",children:"SPACE"})}}),Y(j,{get["class"](){return BJ()==="shift"?"w-[144] h-[156] items-center justify-center bg-slate-300":"w-[144] h-[156] items-center justify-center bg-slate-100"},get children(){return Y(U,{class:"text-base text-slate-900 font-bold",get children(){return LJ()==="letters"?"SHIFT":". ?"}})}}),Y(j,{get["class"](){return BJ()==="submit"?"w-[112] h-[156] items-center justify-center bg-emerald-700":"w-[112] h-[156] items-center justify-center bg-emerald-500"},get children(){return Y(U,{class:"text-base text-slate-950 font-bold",get children(){return J().type==="wifi"?"JOIN":"SEND"}})}})]}})]}}),Y(j,{class:"h-[108] px-6 py-3 flex-col bg-slate-50",get children(){return Y(j,{get["class"](){return BJ()==="close"?"h-[84] items-center justify-center bg-slate-300":"h-[84] items-center justify-center bg-slate-100"},get children(){return Y(U,{class:"text-base text-slate-900 font-bold",children:"CLOSE KEYBOARD"})}})}})]}})}function RQ(){let J=()=>Y1();return Y(j,{class:"flex-col w-full h-full bg-slate-50",get children(){return[Y(OJ,{title:"< FILE VIEWER"}),Y(j,{class:"h-[1168] px-6 pt-5 flex-col",get children(){return[Y(j,{class:"h-[82] px-4 justify-center bg-white",get children(){return Y(U,{class:"text-lg text-slate-900 font-bold",get children(){return J()?.path??"NO FILE OPEN"}})}}),Y(j,{class:"w-[584] h-[900] px-5 pt-5 bg-slate-950",get children(){return Y(U,{class:"text-base text-slate-200",get children(){return J()?.page.text??""}})}}),Y(U,{class:"pt-3 text-base text-slate-500",get children(){return y(()=>!!J())()?"PAGE "+String(J().pageIndex+1)+" · SOURCE LINES "+String(J().page.startSourceLine+1)+"-"+String(J().page.lastSourceLine+1):"NO FILE OPEN"}}),Y(iJ,{top:"absolute left-[628] top-[58] w-[68] h-[132] items-center justify-center bg-orange-100",bottom:"absolute left-[628] top-[828] w-[68] h-[132] items-center justify-center bg-orange-100"})]}})]}})}function PQ(){let J=()=>s4();return Y(j,{class:"flex-col w-full h-full bg-slate-50",get children(){return[Y(OJ,{title:"< MESSAGE READER"}),Y(j,{class:"h-[1168] px-6 pt-5 flex-col",get children(){return[Y(j,{class:"h-[82] px-4 justify-center bg-white",get children(){return Y(U,{class:"text-lg text-orange-600 font-bold",get children(){return J()?.author??"PI"}})}}),Y(j,{class:"w-[584] h-[900] px-5 pt-5 bg-white",get children(){return Y(U,{class:"text-xl text-slate-900",get children(){return(J()?.lines??[]).slice(L0(),L0()+p4).join(` -`)}})}}),Y(iJ,{top:"absolute left-[628] top-[58] w-[68] h-[132] items-center justify-center bg-orange-100",bottom:"absolute left-[628] top-[828] w-[68] h-[132] items-center justify-center bg-orange-100"})]}})]}})}function DQ(){let J=()=>qQ(),Q=()=>J().state==="review"?"REVIEW APP":J().state==="installing"?"INSTALLING":J().state==="success"?"APP INSTALLED":"INSTALL FAILED",$=()=>J().state==="review"?"Ready to install":J().state==="installing"?"Installing App...":J().state==="success"?"Installation complete":"Installation failed",Z=()=>J().state==="review"?"Confirm this package on the device.":J().state==="installing"?"Do not operate the device until installation finishes.":J().state==="success"?"The App is available to you and Pi Agent.":J().error||"The package could not be installed.",q=()=>J().state==="failed"?"w-full h-[150] px-6 flex-col justify-center gap-3 rounded-xl bg-red-100":J().state==="success"?"w-full h-[150] px-6 flex-col justify-center gap-3 rounded-xl bg-emerald-100":J().state==="installing"?"w-full h-[150] px-6 flex-col justify-center gap-3 rounded-xl bg-amber-100":"w-full h-[150] px-6 flex-col justify-center gap-3 rounded-xl bg-orange-100",X=()=>J().state==="failed"?"text-xl text-red-700 font-bold":J().state==="success"?"text-xl text-emerald-700 font-bold":J().state==="installing"?"text-xl text-amber-700 font-bold":"text-xl text-orange-700 font-bold";return Y(j,{class:"flex-col w-full h-full bg-slate-50",get children(){return[Y(v4,{get title(){return Q()},get accent(){return y(()=>J().state==="failed")()?"danger":J().state==="success"?"ready":"busy"},metaTop:"LOCAL INSTALL",metaBottom:"PHYSICAL CONFIRMATION"}),Y(i2,{eyebrow:"APP PACKAGE",get title(){return J().name},get description(){return"VERSION "+J().version+" · "+String(J().tools)+" TOOLS · "+String(J().schedules)+" SCHEDULES"}}),Y(j,{class:"h-[602] px-6 flex-col",get children(){return[Y(s2,{title:"REQUESTED ACCESS",detail:"PACKAGE MANIFEST"}),Y(j,{class:"h-[526] px-6 py-6 flex-col gap-5 rounded-xl shadow bg-white border-slate-100",get children(){return[Y(U,{class:"text-base text-slate-500 font-bold",children:"NETWORK"}),Y(U,{class:"text-lg text-slate-900",get children(){return y(()=>!!J().network.length)()?J().network.slice(0,2).join(` -`):"NO NETWORK ACCESS"}}),Y(j,{class:"h-[2] bg-slate-100"}),Y(U,{class:"text-base text-slate-500 font-bold",children:"CREDENTIALS"}),Y(U,{class:"text-lg text-slate-900",get children(){return y(()=>!!J().credentials.length)()?J().credentials.slice(0,3).join(", "):"NONE"}}),Y(j,{class:"h-[2] bg-slate-100"}),Y(U,{class:"text-base text-slate-500 font-bold",children:"CAPABILITIES"}),Y(U,{class:"text-lg text-slate-900",get children(){return String(J().tools)+" TOOLS · "+String(J().schedules)+" SCHEDULES"}})]}})]}}),Y(j,{class:"h-[166] px-6 pt-4",get children(){return Y(j,{get["class"](){return q()},get children(){return[Y(U,{get["class"](){return X()},get children(){return $()}}),Y(U,{class:"text-base text-slate-600",get children(){return Z()}})]}})}}),Y(j,{class:"h-[168] px-6 pt-6 pb-6",get children(){return Y(j,{class:"w-full h-[120]",get children(){return Y(m4,{get label(){return y(()=>J().state==="review")()?"INSTALL":J().state==="installing"?"INSTALLING...":"DONE"},get disabled(){return J().state==="installing"},get tone(){return J().state==="review"?"primary":"neutral"}})}})}}),Y(j,{class:"h-[66]",get children(){return Y(a2,{text:"Package received over your local network",dark:!0})}})]}})}function VQ(){return Y(j,{class:"flex-col w-full h-full bg-slate-50",get children(){return y(()=>!!p().install)()?Y(DQ,{}):y(()=>x()==="chat")()?Y(jQ,{}):y(()=>x()==="files")()?Y(KQ,{}):y(()=>x()==="apps")()?Y(FQ,{}):y(()=>x()==="settings")()?Y(MQ,{}):y(()=>x()==="keyboard")()?Y(UQ,{}):y(()=>x()==="viewer")()?Y(RQ,{}):Y(PQ,{})}})}function LQ(J){U1(()=>{if(YQ(J),t(J),J==="files")I0()})}function Y6(J,Q){let $=LJ()==="letters"?J6:Q6,Z=[488,628,768];for(let q=0;q<3;q++){if(Q=Z[q]+120)continue;let X=$[q],W=(q===2?560:672)/X.length,R=Math.floor((J-24)/W);if(R>=0&&R=1164)return"close";if(J>=548&&Q>=402&&Q<=482)return"clear";let $=Y6(J,Q);if($)return"char:"+$;if(J>=592&&Q>=768&&Q<=888)return"delete";if(Q<908||Q>1064)return null;if(J<=116)return"mode";if(J<=424)return"space";if(J<=576)return"shift";return"submit"}function IQ(J,Q){if(Q>=1164)return e(""),t(Z1()),"";if(J>=548&&Q>=402&&Q<=482)return e(""),"";let $=Y6(J,Q);if($){let Z=LJ()==="letters"&&G1()?$.toUpperCase():$,q=B1().type==="wifi"?63:256;if(m().length=592&&Q>=768&&Q<=888)return e(m().slice(0,-1)),"";if(Q>=908&&Q<=1064){if(J<=116)r4(LJ()==="letters"?"numbers":"letters"),W1(!1);else if(J<=424){if(m())e(m()+" ")}else if(J<=576)if(LJ()==="letters")W1(!G1());else e(m()+(J<=500?".":"?"));else if(m().trim()){let Z=m().trim(),q=B1();if(e(""),r4("letters"),W1(!1),t(Z1()),q.type==="wifi")return JSON.stringify({type:"settings",command:"connect",ssid:q.ssid,password:Z});return JSON.stringify({type:"submitPrompt",prompt:Z})}}return""}f2(()=>Y(VQ,{})),queueMicrotask(()=>I0("",!0)),globalThis.PocketPiApp={tick(){return""},update(J){let Q=JSON.parse(J);return U1(()=>{if(Q.install)XQ(Q.install);ZQ(Q)}),""},pointerDown(J,Q){return n4(x()==="keyboard"?OQ(J,Q):null),""},pointerUp(){return n4(null),""},tap(J,Q){let $=p().install;if($){if($.state!=="installing"&&Q>=1046&&Q<1214)return JSON.stringify({type:$.state==="review"?"installApp":"dismissInstall"});return""}if(x()==="keyboard")return IQ(J,Q);if(x()==="viewer"){let Z=Y1();if(J<104&&Q<112)z1(null),t("files");else if(Z&&J>=620&&Q>=170&&Q<=340)q6(-1);else if(Z&&J>=620&&Q>=920&&Q<=1100)q6(1);return""}if(x()==="reader"){if(J<104&&Q<112)a4(null),O0(0),t("chat");else if(J>=620&&Q>=170&&Q<=340)O0(Math.max(0,L0()-18));else if(J>=620&&Q>=920&&Q<=1100){let Z=s4()?.lines.length??0;O0(Math.min(Math.max(0,Z-p4),L0()+18))}return""}if(Q>=1172)return LQ($6[Math.min(3,Math.floor(J/180))]),"";if(x()==="chat"){if(J>=620&&Q>=140&&Q<=272)d4(q1()+2);else if(J>=620&&Q>=624&&Q<=756)d4(Math.max(0,q1()-2));else if(J<610&&Q>=140&&Q<798){let Z=Math.floor((Q-140)/340),q=X6()[Z];if(q){let X=Q>=140+Z*340+160,z=X?q.assistant:q.user;a4({author:X?"PI":"YOU",lines:u4(z,JQ,c4)}),O0(0),t("reader")}}else if(Q>=1070&&Q<=1150)o4({type:"prompt"}),e(""),t("keyboard");return""}if(x()==="files"){if(J<104&&Q<112&&VJ()){let Z=VJ().split("/");Z.pop();let q=Z.join("/");i4(q),hJ(0),I0(q)}else if(J>=620&&Q>=170&&Q<=340)hJ(Math.max(0,sJ()-4));else if(J>=620&&Q>=920&&Q<=1100)hJ(Math.min(Math.max(0,aJ().length-8),sJ()+4));else if(J<610&&Q>=190){let Z=Math.floor((Q-190)/104),q=aJ()[sJ()+Z];if(q){let X=WQ(VJ(),q.name);if(q.kind==="dir")i4(X),hJ(0),I0(X);else try{BQ("/workspace/"+X,p2(X,"utf8"))}catch(z){V0(z instanceof Error?z.message:String(z))}}}return""}if(x()==="apps"){let Z=(p().apps??[])[Math.floor((Q-162)/166)];if(Q>=162&&Z)return JSON.stringify({type:"navigate",app:Z.id});return""}if(x()==="settings"){let Z=p().settings?.wifi?.networks??[];if(J>=480&&Q>=126&&Q<=218)return JSON.stringify({type:"settings",command:"scan"});if(J>=620&&Q>=330&&Q<=462)t4(Math.max(0,rJ()-4));else if(J>=620&&Q>=650&&Q<=782)t4(Math.min(Math.max(0,Z.length-5),rJ()+4));else if(J<610&&Q>=330&&Q<790){let q=Math.floor((Q-330)/92),X=Z[rJ()+q];if(X){if(!X.secured)return JSON.stringify({type:"settings",command:"connect",ssid:X.ssid,password:""});o4({type:"wifi",ssid:X.ssid}),e(""),t("keyboard")}}else if(J<=340&&Q>=1010&&Q<=1090)return JSON.stringify({type:"settings",command:"forget"});else if(J>=356&&Q>=1010&&Q<=1090)return JSON.stringify({type:"settings",command:"restart"})}return""}}})(); +TAP SCAN TO FIND WI-FI`})}})},get children(){return Y(KJ,{get each(){return $()},children:(q)=>Y(j,{class:"w-[584] h-[84] px-5 flex-row items-center justify-between bg-white",get children(){return[Y(U,{class:"text-lg text-slate-900 font-bold",get children(){return q.ssid}}),Y(U,{class:"text-base text-slate-500",get children(){return String(q.rssiDbm)+" DBM "+(q.secured?"LOCK":"OPEN")}})]}})})}}),Y(n,{get when(){return(Q().networks??[]).length>5},get children(){return Y(iJ,{top:"absolute left-[604] top-[0] w-[68] h-[132] items-center justify-center bg-orange-100",bottom:"absolute left-[604] top-[320] w-[68] h-[132] items-center justify-center bg-orange-100"})}})]}}),Y(j,{class:"h-[160] px-5 pt-5 flex-col bg-white",get children(){return[Y(U,{class:"text-base text-slate-500",children:"MODEL BACKEND"}),Y(U,{class:"pt-3 text-lg text-slate-900 font-bold",get children(){return y().model??"UNKNOWN"}}),Y(U,{class:"pt-3 text-base text-slate-600",get children(){return"FIRMWARE "+String(J().firmwareVersion??"0.1.0")+" · WORKSPACE FREE "+String(J().workspaceFree??"--")}})]}}),Y(j,{class:"h-[108] pt-7 flex-row gap-4",get children(){return[Y(j,{class:"w-[316] h-[80] items-center justify-center bg-slate-100",get children(){return Y(U,{class:"text-lg text-slate-900 font-bold",children:"FORGET WI-FI"})}}),Y(j,{class:"w-[340] h-[80] items-center justify-center bg-red-100",get children(){return Y(U,{class:"text-lg text-red-500 font-bold",children:"RESTART DEVICE"})}})]}})]}}),Y(_0,{})]}})}function RQ(){let J=()=>B1(),Q=()=>OJ()==="letters"?$6:Z6,$=()=>J().type==="wifi"?"*".repeat(u().length):u();return Y(j,{class:"flex-col w-full h-full bg-slate-50",get children(){return[Y(IJ,{get title(){return J().type==="wifi"?"WIFI PASSWORD":"NEW MESSAGE"}}),Y(j,{class:"h-[1052] px-6 pt-5 flex-col",get children(){return[Y(j,{class:"h-[270] px-[22] pt-6 bg-white",get children(){return Y(U,{get["class"](){return u()?"text-lg text-slate-900":"text-lg text-slate-400"},get children(){return $()||(J().type==="wifi"?"ENTER NETWORK PASSWORD...":"TYPE YOUR MESSAGE...")}})}}),Y(j,{class:"h-[86] px-1 flex-row items-center justify-between",get children(){return[Y(U,{class:"text-base text-slate-500",get children(){return String(u().length)+" / "+(J().type==="wifi"?"63":"256")+" CHARACTERS"}}),Y(j,{get["class"](){return BJ()==="clear"?"w-[132] h-[58] items-center justify-center bg-slate-300":"w-[132] h-[58] items-center justify-center bg-slate-100"},get children(){return Y(U,{class:"text-base text-slate-900 font-bold",children:"CLEAR"})}})]}}),Y(KJ,{get each(){return Q()},children:(Z,q)=>Y(j,{class:"h-[140] flex-row gap-2",get children(){return[Y(KJ,{get each(){return Z.split("")},children:(X)=>Y(j,{get["class"](){return BJ()==="char:"+X?"grow h-[120] items-center justify-center bg-slate-300":"grow h-[120] items-center justify-center bg-white"},get children(){return Y(U,{class:"text-xl text-slate-900 font-bold",get children(){return T(()=>!!W1())()?X.toUpperCase():X}})}})}),Y(n,{get when(){return q()===2},get children(){return Y(j,{get["class"](){return BJ()==="delete"?"w-[104] h-[120] items-center justify-center bg-slate-300":"w-[104] h-[120] items-center justify-center bg-slate-100"},get children(){return Y(U,{class:"text-base text-slate-900 font-bold",children:"DEL"})}})}})]}})}),Y(j,{class:"h-[176] flex-row gap-2",get children(){return[Y(j,{get["class"](){return BJ()==="mode"?"w-[92] h-[156] items-center justify-center bg-slate-300":"w-[92] h-[156] items-center justify-center bg-slate-100"},get children(){return Y(U,{class:"text-base text-slate-900 font-bold",get children(){return OJ()==="letters"?"123":"ABC"}})}}),Y(j,{get["class"](){return BJ()==="space"?"w-[300] h-[156] items-center justify-center bg-slate-300":"w-[300] h-[156] items-center justify-center bg-slate-100"},get children(){return Y(U,{class:"text-base text-slate-900 font-bold",children:"SPACE"})}}),Y(j,{get["class"](){return BJ()==="shift"?"w-[144] h-[156] items-center justify-center bg-slate-300":"w-[144] h-[156] items-center justify-center bg-slate-100"},get children(){return Y(U,{class:"text-base text-slate-900 font-bold",get children(){return OJ()==="letters"?"SHIFT":". ?"}})}}),Y(j,{get["class"](){return BJ()==="submit"?"w-[112] h-[156] items-center justify-center bg-emerald-700":"w-[112] h-[156] items-center justify-center bg-emerald-500"},get children(){return Y(U,{class:"text-base text-slate-950 font-bold",get children(){return J().type==="wifi"?"JOIN":"SEND"}})}})]}})]}}),Y(j,{class:"h-[108] px-6 py-3 flex-col bg-slate-50",get children(){return Y(j,{get["class"](){return BJ()==="close"?"h-[84] items-center justify-center bg-slate-300":"h-[84] items-center justify-center bg-slate-100"},get children(){return Y(U,{class:"text-base text-slate-900 font-bold",children:"CLOSE KEYBOARD"})}})}})]}})}function PQ(){let J=()=>z1();return Y(j,{class:"flex-col w-full h-full bg-slate-50",get children(){return[Y(IJ,{title:"< FILE VIEWER"}),Y(j,{class:"h-[1168] px-6 pt-5 flex-col",get children(){return[Y(j,{class:"h-[82] px-4 justify-center bg-white",get children(){return Y(U,{class:"text-lg text-slate-900 font-bold",get children(){return J()?.path??"NO FILE OPEN"}})}}),Y(j,{class:"w-[584] h-[900] px-5 pt-5 bg-slate-950",get children(){return Y(U,{class:"text-base text-slate-200",get children(){return J()?.page.text??""}})}}),Y(U,{class:"pt-3 text-base text-slate-500",get children(){return T(()=>!!J())()?"PAGE "+String(J().pageIndex+1)+" · SOURCE LINES "+String(J().page.startSourceLine+1)+"-"+String(J().page.lastSourceLine+1):"NO FILE OPEN"}}),Y(iJ,{top:"absolute left-[628] top-[58] w-[68] h-[132] items-center justify-center bg-orange-100",bottom:"absolute left-[628] top-[828] w-[68] h-[132] items-center justify-center bg-orange-100"})]}})]}})}function DQ(){let J=()=>o4();return Y(j,{class:"flex-col w-full h-full bg-slate-50",get children(){return[Y(IJ,{title:"< MESSAGE READER"}),Y(j,{class:"h-[1168] px-6 pt-5 flex-col",get children(){return[Y(j,{class:"h-[82] px-4 justify-center bg-white",get children(){return Y(U,{class:"text-lg text-orange-600 font-bold",get children(){return J()?.author??"PI"}})}}),Y(j,{class:"w-[584] h-[900] px-5 pt-5 bg-white",get children(){return Y(U,{class:"text-xl text-slate-900",get children(){return(J()?.lines??[]).slice(O0(),O0()+d4).join(` +`)}})}}),Y(iJ,{top:"absolute left-[628] top-[58] w-[68] h-[132] items-center justify-center bg-orange-100",bottom:"absolute left-[628] top-[828] w-[68] h-[132] items-center justify-center bg-orange-100"})]}})]}})}function VQ(){let J=()=>XQ(),Q=()=>J().state==="review"?"REVIEW APP":J().state==="installing"?"INSTALLING":J().state==="success"?"APP INSTALLED":"INSTALL FAILED",$=()=>J().state==="review"?"Ready to install":J().state==="installing"?"Installing App...":J().state==="success"?"Installation complete":"Installation failed",Z=()=>J().state==="review"?"Confirm this package on the device.":J().state==="installing"?"Do not operate the device until installation finishes.":J().state==="success"?"The App is available to you and Pi Agent.":J().error||"The package could not be installed.",q=()=>J().state==="failed"?"w-full h-[150] px-6 flex-col justify-center gap-3 rounded-xl bg-red-100":J().state==="success"?"w-full h-[150] px-6 flex-col justify-center gap-3 rounded-xl bg-emerald-100":J().state==="installing"?"w-full h-[150] px-6 flex-col justify-center gap-3 rounded-xl bg-amber-100":"w-full h-[150] px-6 flex-col justify-center gap-3 rounded-xl bg-orange-100",X=()=>J().state==="failed"?"text-xl text-red-700 font-bold":J().state==="success"?"text-xl text-emerald-700 font-bold":J().state==="installing"?"text-xl text-amber-700 font-bold":"text-xl text-orange-700 font-bold";return Y(j,{class:"flex-col w-full h-full bg-slate-50",get children(){return[Y(m4,{get title(){return Q()},get accent(){return T(()=>J().state==="failed")()?"danger":J().state==="success"?"ready":"busy"},metaTop:"LOCAL INSTALL",metaBottom:"PHYSICAL CONFIRMATION"}),Y(a2,{eyebrow:"APP PACKAGE",get title(){return J().name},get description(){return"VERSION "+J().version+" · "+String(J().tools)+" TOOLS · "+String(J().schedules)+" SCHEDULES"}}),Y(j,{class:"h-[602] px-6 flex-col",get children(){return[Y(r2,{title:"REQUESTED ACCESS",detail:"PACKAGE MANIFEST"}),Y(j,{class:"h-[526] px-6 py-6 flex-col gap-5 rounded-xl shadow bg-white border-slate-100",get children(){return[Y(U,{class:"text-base text-slate-500 font-bold",children:"NETWORK"}),Y(U,{class:"text-lg text-slate-900",get children(){return T(()=>!!J().network.length)()?J().network.slice(0,2).join(` +`):"NO NETWORK ACCESS"}}),Y(j,{class:"h-[2] bg-slate-100"}),Y(U,{class:"text-base text-slate-500 font-bold",children:"CREDENTIALS"}),Y(U,{class:"text-lg text-slate-900",get children(){return T(()=>!!J().credentials.length)()?J().credentials.slice(0,3).join(", "):"NONE"}}),Y(j,{class:"h-[2] bg-slate-100"}),Y(U,{class:"text-base text-slate-500 font-bold",children:"CAPABILITIES"}),Y(U,{class:"text-lg text-slate-900",get children(){return String(J().tools)+" TOOLS · "+String(J().schedules)+" SCHEDULES"}})]}})]}}),Y(j,{class:"h-[166] px-6 pt-4",get children(){return Y(j,{get["class"](){return q()},get children(){return[Y(U,{get["class"](){return X()},get children(){return $()}}),Y(U,{class:"text-base text-slate-600",get children(){return Z()}})]}})}}),Y(j,{class:"h-[168] px-6 pt-6 pb-6",get children(){return Y(j,{class:"w-full h-[120]",get children(){return Y(L0,{get label(){return T(()=>J().state==="review")()?"INSTALL":J().state==="installing"?"INSTALLING...":"DONE"},get disabled(){return J().state==="installing"},get tone(){return J().state==="review"?"primary":"neutral"}})}})}}),Y(j,{class:"h-[66]",get children(){return Y(u4,{text:"Package received over your local network",dark:!0})}})]}})}function LQ(){return Y(j,{class:"flex-col w-full h-full bg-slate-50",get children(){return T(()=>!!y().install)()?Y(VQ,{}):T(()=>v()==="chat")()?Y(KQ,{}):T(()=>v()==="files")()?Y(FQ,{}):T(()=>v()==="apps")()?Y(MQ,{}):T(()=>v()==="settings")()?Y(UQ,{}):T(()=>v()==="keyboard")()?Y(RQ,{}):T(()=>v()==="viewer")()?Y(PQ,{}):Y(DQ,{})}})}function OQ(J){R1(()=>{if(J!=="apps")i4(!1);if(zQ(J),e(J),J==="files")A0(HJ())})}function G6(J,Q){let $=OJ()==="letters"?$6:Z6,Z=[488,628,768];for(let q=0;q<3;q++){if(Q=Z[q]+120)continue;let X=$[q],W=(q===2?560:672)/X.length,R=Math.floor((J-24)/W);if(R>=0&&R=1164)return"close";if(J>=548&&Q>=402&&Q<=482)return"clear";let $=G6(J,Q);if($)return"char:"+$;if(J>=592&&Q>=768&&Q<=888)return"delete";if(Q<908||Q>1064)return null;if(J<=116)return"mode";if(J<=424)return"space";if(J<=576)return"shift";return"submit"}function AQ(J,Q){if(Q>=1164)return JJ(""),e(q1()),"";if(J>=548&&Q>=402&&Q<=482)return JJ(""),"";let $=G6(J,Q);if($){let Z=OJ()==="letters"&&W1()?$.toUpperCase():$,q=B1().type==="wifi"?63:256;if(u().length=592&&Q>=768&&Q<=888)return JJ(u().slice(0,-1)),"";if(Q>=908&&Q<=1064){if(J<=116)t4(OJ()==="letters"?"numbers":"letters"),H1(!1);else if(J<=424){if(u())JJ(u()+" ")}else if(J<=576)if(OJ()==="letters")H1(!W1());else JJ(u()+(J<=500?".":"?"));else if(u().trim()){let Z=u().trim(),q=B1();if(JJ(""),t4("letters"),H1(!1),e(q1()),q.type==="wifi")return JSON.stringify({type:"settings",command:"connect",ssid:q.ssid,password:Z});return JSON.stringify({type:"submitPrompt",prompt:Z})}}return""}v2(()=>Y(LQ,{})),queueMicrotask(()=>A0("")),globalThis.PocketPiApp={tick(){return""},update(J){let Q=JSON.parse(J);return R1(()=>{if(Q.install)YQ(Q.install);qQ(Q)}),""},pointerDown(J,Q){return J6(v()==="keyboard"?IQ(J,Q):null),""},pointerUp(){return J6(null),""},tap(J,Q){let $=y().install;if($){if($.state!=="installing"&&Q>=1046&&Q<1214)return JSON.stringify({type:$.state==="review"?"installApp":"dismissInstall"});return""}if(y().uninstallingApp)return"";if(v()==="keyboard")return AQ(J,Q);if(v()==="viewer"){let Z=z1();if(J<104&&Q<112)G1(null),e("files");else if(Z&&J>=620&&Q>=170&&Q<=340)Y6(-1);else if(Z&&J>=620&&Q>=920&&Q<=1100)Y6(1);return""}if(v()==="reader"){if(J<104&&Q<112)n4(null),I0(0),e("chat");else if(J>=620&&Q>=170&&Q<=340)I0(Math.max(0,O0()-18));else if(J>=620&&Q>=920&&Q<=1100){let Z=o4()?.lines.length??0;I0(Math.min(Math.max(0,Z-d4),O0()+18))}return""}if(Q>=1172)return OQ(q6[Math.min(3,Math.floor(J/180))]),"";if(v()==="chat"){if(J>=620&&Q>=140&&Q<=272)s4(X1()+2);else if(J>=620&&Q>=624&&Q<=756)s4(Math.max(0,X1()-2));else if(J<610&&Q>=140&&Q<798){let Z=Math.floor((Q-140)/340),q=z6()[Z];if(q){let X=Q>=140+Z*340+160,z=X?q.assistant:q.user;n4({author:X?"PI":"YOU",lines:l4(z,QQ,p4)}),I0(0),e("reader")}}else if(Q>=1070&&Q<=1150)e4({type:"prompt"}),JJ(""),e("keyboard");return""}if(v()==="files"){if(J<104&&Q<112&&HJ()){let Z=HJ().split("/");Z.pop();let q=Z.join("/");a4(q),aJ(0),A0(q)}else if(J>=620&&Q>=170&&Q<=340)aJ(Math.max(0,sJ()-4));else if(J>=620&&Q>=920&&Q<=1100)aJ(Math.min(Math.max(0,rJ().length-8),sJ()+4));else if(J<610&&Q>=190){let Z=Math.floor((Q-190)/104),q=rJ()[sJ()+Z];if(q){let X=HQ(HJ(),q.name);if(q.kind==="dir")a4(X),aJ(0),A0(X);else try{BQ("/workspace/"+X,i2(X,"utf8"))}catch(z){Y1(z instanceof Error?z.message:String(z))}}}return""}if(v()==="apps"){let Z=y().apps??[];if(Q>=1060&&Q<1140&&(LJ()||Z.length>0)&&!y().uninstallingApp)return i4(!LJ()),"";let q=Z[Math.floor((Q-162)/166)];if(Q>=162&&q&&!y().uninstallingApp){if(LJ())return J>=604&&J<696?JSON.stringify({type:"uninstallApp",app:q.id}):"";return JSON.stringify({type:"navigate",app:q.id})}return""}if(v()==="settings"){let Z=y().settings?.wifi?.networks??[];if(J>=480&&Q>=126&&Q<=218)return JSON.stringify({type:"settings",command:"scan"});if(J>=620&&Q>=330&&Q<=462)Q6(Math.max(0,oJ()-4));else if(J>=620&&Q>=650&&Q<=782)Q6(Math.min(Math.max(0,Z.length-5),oJ()+4));else if(J<610&&Q>=330&&Q<790){let q=Math.floor((Q-330)/92),X=Z[oJ()+q];if(X){if(!X.secured)return JSON.stringify({type:"settings",command:"connect",ssid:X.ssid,password:""});e4({type:"wifi",ssid:X.ssid}),JJ(""),e("keyboard")}}else if(J<=340&&Q>=1010&&Q<=1090)return JSON.stringify({type:"settings",command:"forget"});else if(J>=356&&Q>=1010&&Q<=1090)return JSON.stringify({type:"settings",command:"restart"})}return""}}})(); diff --git a/apps/pi-agent/dist/app.pak b/apps/pi-agent/dist/app.pak index 4c3cc0e6e3628f1bdb1d5315f28f466233e63c80..c68360600a58d4b9ab7e3449c0bcfe3e317e36e1 100644 GIT binary patch delta 176 zcmcbxQfk5~sR$28IqNCNRm!0pe-6fOrp_IDzB= rM{Y1F!2ptx1Y%zo8L)^0!^f{b|1+{8bS!W}sLF+^+Ww=B?FKIZQD-k+ delta 109 zcmbQRO6tN&sR%Vo__FmSW9v(%)|bq!FIie&vTl9JmfOI%WP5lcTM`Q+ z+w@(nY%0^Aw6g7C{55@H8=HkH8xsQq!viNyAbG%%8%**5$p%Lu21X$5!0_?w&;Q#k J+SzXK0svxXCqe)K diff --git a/crates/pocket-pi-agentos/src/lib.rs b/crates/pocket-pi-agentos/src/lib.rs index a331869..9426337 100644 --- a/crates/pocket-pi-agentos/src/lib.rs +++ b/crates/pocket-pi-agentos/src/lib.rs @@ -28,7 +28,7 @@ pub const POCKETJS_REVISION: &str = "e12cf12f82cc60b636368119d49a06eb9ed2a3d5"; const VIEWPORT: (f32, f32) = (720.0, 1280.0); pub const MAX_POCKETAPP_BYTES: usize = 2 * 1024 * 1024; const VIEW_RUNTIME_LIMIT: usize = 3; -const DATA_RUNTIME_LIMIT: usize = 3; +const ACTION_RUNTIME_LIMIT: usize = 3; fn take_runtime(runtimes: &mut Vec<(String, T)>, app_id: &str) -> Option { runtimes @@ -598,6 +598,13 @@ impl InstalledAppIndex { add_app_tools(&app, &mut index.tool_owner); index.apps.insert(app.descriptor.id.clone(), app); } + + fn remove(&self, app_id: &str) -> Option { + let mut index = self.inner.write().expect("installed App index lock"); + let app = index.apps.remove(app_id)?; + index.tool_owner.retain(|_, owner| owner != app_id); + Some(app) + } } fn validate_app_tools(app: &InstalledApp, owners: &BTreeMap) -> Result<()> { @@ -796,7 +803,7 @@ pub trait AppServiceHost: Send + Sync { ) -> Result; /// Execute one policy-checked PocketJS HTTP request. This is called only - /// from the native NET worker, never from the QuickJS/App Data thread. + /// from the native NET worker, never from the QuickJS Data Action thread. fn http( &self, _app_id: &str, @@ -825,7 +832,11 @@ pub trait AppServiceHost: Send + Sync { } } - fn remove_credentials(&self, _ids: &[String]) -> std::result::Result<(), String> { + fn remove_app_state( + &self, + _app_id: &str, + _credential_ids: &[String], + ) -> std::result::Result<(), String> { Ok(()) } } @@ -858,8 +869,16 @@ struct DataActionRequest { response: Option>, } +enum DataActionMessage { + Run(DataActionRequest), + RemoveApp { + app_id: String, + done: mpsc::Sender>, + }, +} + #[derive(Clone)] -struct DataAppConfig { +struct DataActionConfig { app_id: String, source_path: PathBuf, database: SharedDb, @@ -968,7 +987,7 @@ struct DataActionRuntime { } impl DataActionRuntime { - fn load(config: &DataAppConfig, services: Arc) -> Result { + fn load(config: &DataActionConfig, services: Arc) -> Result { let guest = new_app_guest().context("create App Data Action Guest")?; mount_shared_db(&guest, config.database.clone())?; let action_deadline = Rc::new(Cell::new(None)); @@ -1094,16 +1113,16 @@ fn parse_data_result(line: &str) -> Result { }) } -struct AppDataRunner { - tx: mpsc::SyncSender, - configs: Arc>>, +struct DataActionRunner { + tx: mpsc::SyncSender, + configs: Arc>>, next_run_id: AtomicU32, busy: Arc, } -impl AppDataRunner { - fn start(configs: Vec, services: Arc) -> Result { - let (tx, rx) = mpsc::sync_channel::(DATA_ACTION_QUEUE); +impl DataActionRunner { + fn start(configs: Vec, services: Arc) -> Result { + let (tx, rx) = mpsc::sync_channel::(DATA_ACTION_QUEUE); let configs = Arc::new(Mutex::new( configs .into_iter() @@ -1114,17 +1133,33 @@ impl AppDataRunner { let busy = Arc::new(AtomicBool::new(false)); let worker_busy = busy.clone(); std::thread::Builder::new() - .name("app-data".to_owned()) + .name("data-actions".to_owned()) .stack_size(DATA_ACTION_STACK_BYTES) .spawn(move || { let mut runtimes = Vec::<(String, DataActionRuntime)>::new(); - while let Ok(request) = rx.recv() { + while let Ok(message) = rx.recv() { worker_busy.store(true, Ordering::Release); + let request = match message { + DataActionMessage::Run(request) => request, + DataActionMessage::RemoveApp { app_id, done } => { + let result = worker_configs + .lock() + .map_err(|_| anyhow!("App Data Action config lock was poisoned")) + .map(|mut configs| { + configs.remove(&app_id); + runtimes + .retain(|(runtime_app_id, _)| runtime_app_id != &app_id); + }); + let _ = done.send(result); + worker_busy.store(false, Ordering::Release); + continue; + } + }; let result = (|| -> Result { let runtime = match take_runtime(&mut runtimes, &request.app_id) { Some(runtime) => runtime, None => { - make_runtime_room(&mut runtimes, DATA_RUNTIME_LIMIT, None); + make_runtime_room(&mut runtimes, ACTION_RUNTIME_LIMIT, None); let config = worker_configs .lock() .map_err(|_| { @@ -1192,7 +1227,7 @@ impl AppDataRunner { ) -> Result { let run_id = u64::from(self.next_run_id.fetch_add(1, Ordering::Relaxed)); self.tx - .try_send(DataActionRequest { + .try_send(DataActionMessage::Run(DataActionRequest { run_id, app_id: app_id.to_owned(), kind, @@ -1200,7 +1235,7 @@ impl AppDataRunner { args, deadline, response, - }) + })) .map_err(|error| anyhow!("queue App Data Action: {error}"))?; Ok(run_id) } @@ -1209,7 +1244,7 @@ impl AppDataRunner { self.busy.load(Ordering::Acquire) } - fn register(&self, config: DataAppConfig) -> Result<()> { + fn register(&self, config: DataActionConfig) -> Result<()> { let mut configs = self .configs .lock() @@ -1228,9 +1263,20 @@ impl AppDataRunner { configs.remove(app_id); } } + + fn remove_app(&self, app_id: &str) -> Result<()> { + let (done, response) = mpsc::channel(); + self.tx + .send(DataActionMessage::RemoveApp { + app_id: app_id.to_owned(), + done, + }) + .context("stop App Data Action runtime")?; + response.recv().context("App Data Action runner stopped")? + } } -struct AppRuntime { +struct ViewRuntime { guest: Guest, surface: UiSurface, _fs: Rc>, @@ -1243,7 +1289,7 @@ struct AppRuntime { ticks: Cell, } -impl AppRuntime { +impl ViewRuntime { fn load( app: &InstalledApp, fs_root: &Path, @@ -1545,16 +1591,16 @@ pub struct AppSupervisor { workspace: PathBuf, catalog: InstalledAppIndex, services: Arc, - data_runner: AppDataRunner, + action_runner: DataActionRunner, databases: BTreeMap, revisions: BTreeMap, /// The Pi Agent System App is booted once and remains resident for the /// entire supervisor lifetime. Foreground navigation never replaces it. - system: AppRuntime, + system: ViewRuntime, agent: Option, agent_tools: Option>, // Least recently used first. Only the selected View advances. - runtimes: Vec<(String, AppRuntime)>, + runtimes: Vec<(String, ViewRuntime)>, active_app: Option, schedules: AppScheduleStore, } @@ -1585,7 +1631,7 @@ impl AppSupervisor { .filter_map(|app| { let descriptor = &app.descriptor; let source_path = app.release_dir.join("data-action.js"); - source_path.is_file().then(|| DataAppConfig { + source_path.is_file().then(|| DataActionConfig { app_id: descriptor.id.clone(), source_path, database: databases @@ -1606,7 +1652,7 @@ impl AppSupervisor { }) }) .collect(); - let data_runner = AppDataRunner::start(data_configs, services.clone())?; + let action_runner = DataActionRunner::start(data_configs, services.clone())?; log::info!("loading Pi Agent System App"); let system = load_runtime(&workspace, &catalog, &databases, &revisions, ROOT_APP_ID)?; log::info!("Pi Agent System App loaded"); @@ -1614,7 +1660,7 @@ impl AppSupervisor { workspace, catalog, services, - data_runner, + action_runner, databases, revisions, system, @@ -1631,7 +1677,7 @@ impl AppSupervisor { } pub fn services_busy(&self) -> bool { - self.services.busy() || self.data_runner.busy() + self.services.busy() || self.action_runner.busy() } pub fn activate_new_app( @@ -1668,9 +1714,9 @@ impl AppSupervisor { self.activate_new_app_inner(staging_release, descriptor, credentials, &app_root); if result.is_err() { if let Some(app_id) = app_root.file_name().and_then(|value| value.to_str()) { - self.data_runner.unregister(app_id); + self.action_runner.unregister(app_id); + let _ = self.services.remove_app_state(app_id, &credential_ids); } - let _ = self.services.remove_credentials(&credential_ids); let _ = std::fs::remove_dir_all(&app_root); } result @@ -1697,7 +1743,7 @@ impl AppSupervisor { VIEW_RUNTIME_LIMIT, self.active_app.as_deref(), ); - let runtime = AppRuntime::load( + let runtime = ViewRuntime::load( &app, &fs_root, &tmp_root, @@ -1711,7 +1757,7 @@ impl AppSupervisor { .is_some_and(|capabilities| capabilities.iter().any(|value| value == "net.http")); let staged_data = staging_release.join("data-action.js"); if staged_data.is_file() { - let candidate = DataAppConfig { + let candidate = DataActionConfig { app_id: descriptor.id.clone(), source_path: staged_data, database: database.clone(), @@ -1733,7 +1779,7 @@ impl AppSupervisor { let has_data_action = release_dir.join("data-action.js").is_file(); if has_data_action { - self.data_runner.register(DataAppConfig { + self.action_runner.register(DataActionConfig { app_id: descriptor.id.clone(), source_path: release_dir.join("data-action.js"), database: database.clone(), @@ -1753,7 +1799,7 @@ impl AppSupervisor { .map_err(anyhow::Error::msg)?; } if let Err(error) = atomic_write(&app_root.join("current"), descriptor.version.as_bytes()) { - self.data_runner.unregister(&descriptor.id); + self.action_runner.unregister(&descriptor.id); if let (Some(agent), Some(prior)) = (&self.agent, prior_tools) { let _ = agent.replace_tools(&self.system.guest, prior); } @@ -1769,6 +1815,59 @@ impl AppSupervisor { Ok(descriptor) } + pub fn uninstall_app(&mut self, app_id: &str) -> Result { + anyhow::ensure!(app_id != ROOT_APP_ID, "cannot uninstall the System App"); + anyhow::ensure!(!self.services_busy(), "App services are busy"); + let app = self + .catalog + .app(app_id) + .ok_or_else(|| anyhow!("unknown App: {app_id}"))?; + + let removed_tools = app + .descriptor + .tools + .iter() + .filter_map(|tool| tool.get("name").and_then(Value::as_str)) + .collect::>(); + if let (Some(agent), Some(tools)) = (&self.agent, &self.agent_tools) { + let next = tools + .definitions() + .into_iter() + .filter(|tool| { + tool.get("name") + .and_then(Value::as_str) + .is_none_or(|name| !removed_tools.contains(name)) + }) + .collect(); + agent + .replace_tools(&self.system.guest, next) + .map_err(anyhow::Error::msg)?; + } + + self.schedules.remove(app_id); + self.action_runner.remove_app(app_id)?; + if self.active_app.as_deref() == Some(app_id) { + self.active_app = None; + } + self.runtimes + .retain(|(runtime_app_id, _)| runtime_app_id != app_id); + self.databases.remove(app_id); + self.revisions.remove(app_id); + + let credential_ids = descriptor_credential_ids(&app.descriptor) + .into_iter() + .collect::>(); + self.services + .remove_app_state(app_id, &credential_ids) + .map_err(anyhow::Error::msg)?; + std::fs::remove_dir_all(self.workspace.join("apps").join(app_id)) + .with_context(|| format!("delete App {app_id}"))?; + self.catalog + .remove(app_id) + .expect("installed App remains indexed during uninstall"); + Ok(app.descriptor) + } + pub fn active_id(&self) -> &str { self.active_app.as_deref().unwrap_or(ROOT_APP_ID) } @@ -1908,7 +2007,7 @@ impl AppSupervisor { return; }; let args = serde_json::from_str(args_json).unwrap_or(Value::Null); - match self.data_runner.enqueue( + match self.action_runner.enqueue( &app_id, DataActionKind::Tool, name, @@ -1928,7 +2027,7 @@ impl AppSupervisor { /// native services never hide touch feedback. pub fn invoke_active_task(&mut self, name: &str, args: &Value) -> ToolResult { let app_id = self.active_id().to_owned(); - match self.data_runner.enqueue( + match self.action_runner.enqueue( &app_id, DataActionKind::Task, name, @@ -1950,7 +2049,7 @@ impl AppSupervisor { let mut results = Vec::new(); while let Some(due) = self.schedules.claim_due() { let label = format!("{}.{}", due.app_id, due.task); - let result = match self.data_runner.enqueue( + let result = match self.action_runner.enqueue( &due.app_id, DataActionKind::Task, &due.task, @@ -1972,14 +2071,14 @@ impl AppSupervisor { results } - fn active(&self) -> &AppRuntime { + fn active(&self) -> &ViewRuntime { self.active_app .as_deref() .and_then(|app_id| self.cached_view(app_id)) .unwrap_or(&self.system) } - fn cached_view(&self, app_id: &str) -> Option<&AppRuntime> { + fn cached_view(&self, app_id: &str) -> Option<&ViewRuntime> { self.runtimes .iter() .find(|(cached_id, _)| cached_id == app_id) @@ -1993,7 +2092,7 @@ fn load_runtime( databases: &BTreeMap, revisions: &BTreeMap, app_id: &str, -) -> Result { +) -> Result { let app = catalog .app(app_id) .ok_or_else(|| anyhow!("unknown App: {app_id}"))?; @@ -2006,7 +2105,7 @@ fn load_runtime( .get(app_id) .cloned() .ok_or_else(|| anyhow!("App {app_id} has no revision owner"))?; - AppRuntime::load(&app, &fs_root, &tmp_root, db, revision) + ViewRuntime::load(&app, &fs_root, &tmp_root, db, revision) .with_context(|| format!("load App {app_id}")) } @@ -2303,6 +2402,11 @@ impl AppScheduleStore { self.schedules.extend(schedules); } + fn remove(&mut self, app_id: &str) { + self.paths.remove(app_id); + self.schedules.retain(|schedule| schedule.app_id != app_id); + } + fn finish(&mut self, due: &DueTask, ok: bool) { if let Some(item) = self .schedules @@ -2384,6 +2488,69 @@ mod tests { } } + #[derive(Default)] + struct TrackingServices { + credentials: Mutex>, + removed_apps: Mutex>, + } + + impl AppServiceHost for TrackingServices { + fn call( + &self, + _app_id: &str, + _service: &str, + _operation: &str, + _args: &Value, + _deadline: Instant, + ) -> std::result::Result { + Err("simulated service failure".into()) + } + + fn store_credentials( + &self, + credentials: &BTreeMap, + ) -> std::result::Result<(), String> { + self.credentials.lock().unwrap().extend(credentials.clone()); + Ok(()) + } + + fn remove_app_state( + &self, + app_id: &str, + credential_ids: &[String], + ) -> std::result::Result<(), String> { + let mut credentials = self.credentials.lock().unwrap(); + for id in credential_ids { + credentials.remove(id); + } + self.removed_apps.lock().unwrap().push(app_id.to_owned()); + Ok(()) + } + } + + struct RecordingBackend(Arc>>); + + impl ModelBackend for RecordingBackend { + fn complete( + &self, + request_json: &str, + _on_event: &mut dyn FnMut(pocket_pi_embedded::ModelStreamEvent), + ) -> std::result::Result { + self.0 + .lock() + .unwrap() + .push(serde_json::from_str(request_json).unwrap()); + Ok(json!({ + "thinking":"", + "text":"done", + "toolCalls":[], + "usage":{}, + "stopReason":"stop" + }) + .to_string()) + } + } + struct NoTools; impl ToolHost for NoTools { @@ -2624,6 +2791,130 @@ mod tests { assert_eq!(supervisor.active_id(), "exa"); } + #[test] + fn uninstall_removes_app_owned_state_and_allows_reinstall() { + let temp = tempfile::tempdir().unwrap(); + let workspace = temp.path().join("device"); + let services = Arc::new(TrackingServices::default()); + let catalog = InstalledAppIndex::load(&workspace, system_app_bundle()).unwrap(); + let mut supervisor = + AppSupervisor::new(&workspace, catalog.clone(), services.clone()).unwrap(); + let (tools, _requests) = RoutedToolHost::new(Arc::new(NoTools), catalog); + let model_requests = Arc::new(Mutex::new(Vec::new())); + supervisor + .boot_agent( + r#"{"model":"offline"}"#, + Arc::new(RecordingBackend(model_requests.clone())), + Arc::new(tools), + ) + .unwrap(); + + let staging = temp.path().join("staged-robinhood"); + stage_checked_in_app(&staging, "robinhood"); + let credentials = BTreeMap::from([( + "robinhood.oauth-access-token".to_owned(), + "secret".to_owned(), + )]); + supervisor + .activate_new_app(&staging, credentials.clone()) + .unwrap(); + supervisor.open("robinhood").unwrap(); + std::fs::write( + workspace.join("apps/robinhood/data/user-state"), + "delete me", + ) + .unwrap(); + + let (response, result) = mpsc::channel(); + supervisor + .action_runner + .enqueue( + "robinhood", + DataActionKind::Task, + "refreshPortfolio", + Value::Null, + new_action_deadline(), + Some(response), + ) + .unwrap(); + let _ = result.recv_timeout(Duration::from_secs(2)).unwrap(); + while supervisor.services_busy() { + std::thread::yield_now(); + } + + assert_eq!(supervisor.schedules.schedules.len(), 1); + assert!(supervisor.cached_view("robinhood").is_some()); + assert_eq!(*services.credentials.lock().unwrap(), credentials); + assert!(supervisor.uninstall_app(ROOT_APP_ID).is_err()); + + supervisor.uninstall_app("robinhood").unwrap(); + + assert_eq!(supervisor.active_id(), ROOT_APP_ID); + assert!(supervisor.catalog().descriptor("robinhood").is_none()); + assert!(supervisor.cached_view("robinhood").is_none()); + assert!(!supervisor.databases.contains_key("robinhood")); + assert!(!supervisor.revisions.contains_key("robinhood")); + assert!(supervisor.schedules.schedules.is_empty()); + assert!(!workspace.join("apps/robinhood").exists()); + assert!(services.credentials.lock().unwrap().is_empty()); + assert_eq!( + *services.removed_apps.lock().unwrap(), + vec!["robinhood".to_owned()] + ); + + let (response, result) = mpsc::channel(); + supervisor + .action_runner + .enqueue( + "robinhood", + DataActionKind::Task, + "refreshPortfolio", + Value::Null, + new_action_deadline(), + Some(response), + ) + .unwrap(); + assert!( + result + .recv_timeout(Duration::from_secs(2)) + .unwrap() + .is_error + ); + + supervisor.prompt_agent("check available tools").unwrap(); + let mut done = false; + for _ in 0..200 { + done |= supervisor + .frame() + .unwrap() + .iter() + .any(|event| matches!(event, AgentEvent::Done)); + if done { + break; + } + std::thread::sleep(Duration::from_millis(2)); + } + assert!(done); + let requests = model_requests.lock().unwrap(); + assert!(requests[0]["context"]["tools"] + .as_array() + .unwrap() + .iter() + .all(|tool| !tool["name"] + .as_str() + .unwrap_or("") + .starts_with("portfolio."))); + drop(requests); + + let restarted = InstalledAppIndex::load(&workspace, system_app_bundle()).unwrap(); + assert!(restarted.descriptor("robinhood").is_none()); + + let staging = temp.path().join("reinstall-robinhood"); + stage_checked_in_app(&staging, "robinhood"); + supervisor.activate_new_app(&staging, credentials).unwrap(); + assert!(supervisor.catalog().descriptor("robinhood").is_some()); + } + #[test] fn app_revisions_coalesce_at_the_foreground_frame_boundary() { let temp = tempfile::tempdir().unwrap(); @@ -2787,7 +3078,7 @@ globalThis.PocketPiData = { temp.path().join(app_id), )))); databases.insert(app_id, database.clone()); - DataAppConfig { + DataActionConfig { app_id: app_id.into(), source_path: source.clone(), database, @@ -2796,7 +3087,7 @@ globalThis.PocketPiData = { } }) .collect(); - let runner = AppDataRunner::start(configs, Arc::new(NoServices)).unwrap(); + let runner = DataActionRunner::start(configs, Arc::new(NoServices)).unwrap(); for app_id in ["one", "two", "three", "one", "four", "two"] { let (response, rx) = mpsc::channel(); diff --git a/docs/agentos-architecture.md b/docs/agentos-architecture.md index d2fa8f7..75ea10d 100644 --- a/docs/agentos-architecture.md +++ b/docs/agentos-architecture.md @@ -65,7 +65,7 @@ Pi Agent 常驻 System App、Core/Bundle ownership 和前后台 Guest 生命周 | Pocket Pi AgentOS | `AppSupervisor`、System/ordinary App 生命周期、Tool Catalog/Router、Data Action queue、AppTask Schedule、revision delivery、App data ownership | 不重新实现 PocketJS 的 UI、JS engine、DB/FS/net module 形状 | | Host adapter | 把 portable contract 接到 LittleFS/SQLite、ESP HTTP/TLS、MCP、Keychain/NVS、LCD/Touch 和资源限制 | 不拥有 Robinhood/Exa 的 schema、provider mapping 或 View | -因此 `AppSupervisor`、`RoutedToolHost` 和 `AppDataRunner` 不是 PocketJS 缺失的基础能力, +因此 `AppSupervisor`、`RoutedToolHost` 和 `DataActionRunner` 不是 PocketJS 缺失的基础能力, 而是建立在 PocketJS Guest/module primitives 之上的 **AgentOS 跨 App 产品语义**。 PocketJS 类似可移植的嵌入式 application/UI runtime;它不应替具体产品决定 App catalog、Agent Tool ownership 或后台任务生命周期。如果其中某个 primitive 将来被 @@ -89,7 +89,7 @@ catalog、Agent Tool ownership 或后台任务生命周期。如果其中某个 ## 3. 第一版明确不做什么 -- 不做 Marketplace、云端 Registry、自动更新、卸载或 rollback。 +- 不做 Marketplace、云端 Registry、自动更新或 rollback。 - ESP32 不修改 App/Root View 源码,不编译 PocketJS Bundle。 - 不做 Agent 动态创建 Tools 或 workflow interpreter。 - 不允许 Agent 调用 Installer;安装必须由设备屏幕上的用户确认。 @@ -100,8 +100,8 @@ catalog、Agent Tool ownership 或后台任务生命周期。如果其中某个 `pi-agent` 是 Firmware 中唯一的 `SystemAppBundle`。所有 ordinary App,包括 Robinhood 和 Exa,都由构建机预编译为同一种 `.pocketapp`,并且只能由统一 Installer 校验、写入和激活;设备端不编译源码,也不存在启动时自动安装或其他 ordinary App -side door。HTTP 是当前唯一实现的上传 ingress,未来 UART 也只能把完整的同一种 -artifact 交给 Installer,不能直接写 App 目录、credential、`current` 或 Runtime。 +side door。HTTP 和 UART 都只把完整的同一种 artifact 交给 Installer,不能直接写 +App 目录、credential、`current` 或 Runtime。 第一版局域网上传允许明文 HTTP 和明文 credential,暂不增加 TLS、签名、配对或证书 系统。 @@ -327,8 +327,8 @@ flowchart TB ``` Pi Agent 的 release id 仍由 Firmware 管理;ordinary App 的 release id 来自 App -版本。`InstalledAppIndex` 在启动时只扫描每个 App 的 `current` 一次,之后仅在安装 -成功时插入内存,不存在持久 catalog、目录 watcher 或逐帧扫描。当前 +版本。`InstalledAppIndex` 在启动时只扫描每个 App 的 `current` 一次,之后只在安装 +或卸载成功时修改内存,不存在持久 catalog、目录 watcher 或逐帧扫描。当前 `agent-app.json.nativeServices` 保存 endpoint/credential-reference policy; credential 可以随上传包交付,但 Installer 只将其写入 Native NVS,绝不写入 active release、App DB、`/workspace` projection、日志或 Agent context。 @@ -410,14 +410,15 @@ catalog。 - artifact hashes。 启动时只加载常驻 Pi Agent;ordinary View 在第一次打开时加载。最近使用的 ordinary -View 和 Data Runtime 各自最多保留 3 个,超出时淘汰最久未使用者。安装成功的新 +View Runtime 和 Data Action Runtime 各自最多保留 3 个,超出时淘汰最久未使用者。安装成功的新 View 进入 View cache,但前台仍停留在 Pi Agent 的安装界面。只有用户之后主动打开 该 App 时才切换前台。只有前台 View 执行 `tick()`;Data Action 只更新 SQLite 和 revision,重新打开时合并为一次 bounded projection refresh。 -安装校验会短暂构造 candidate View 和 candidate Data Runtime,以证明 bundle 能够 -加载;成功后保留 View Runtime,丢弃 candidate Data Runtime。真正执行第一次 Tool、 -Schedule 或 UI action 时,串行 Data worker 才按需创建 operational Data Runtime。 +安装校验会短暂构造 candidate View Runtime 和 candidate Data Action Runtime,以证明 +bundle 能够加载;成功后保留 View Runtime,丢弃 candidate Data Action Runtime。 +真正执行第一次 Tool、Schedule 或 UI action 时,串行 Data Action worker 才按需创建 +operational Data Action Runtime。 ### App 内部依赖方向 @@ -451,8 +452,10 @@ UI refresh ─────┘ │ - 路由 App Tool/AppTask,并用一个 bounded Data Action queue 串行执行; - 注册、持久化并推进 App Schedules; - 共享每个 App 的 SQLite owner 和 revision counter。 +- 在用户从 Apps 页面确认卸载后,释放该 App 的 Runtime、Tools、Schedules、 + credentials、MCP session 和整个磁盘目录。 -更新、卸载、迁移、签名、上一版本回退和 recovery UI 不属于当前实现。 +更新、迁移、签名、上一版本回退、数据保留和 recovery UI 不属于当前实现。 ### 常驻 System App 与前台 View @@ -490,7 +493,7 @@ cache miss 才重新加载 bundle,随后前台 frame 只读取一次当前 bou 实例,而是同一个 App runtime 的 data plane 与 view plane。 当前 v1 让所有 App 共用一个全局串行 Data Action worker,并在该 worker 内保留最多 -3 个 Data Runtime。这是资源与执行语义取舍:它换取 bounded concurrency、固定 +3 个 Data Action Runtime。这是资源与执行语义取舍:它换取 bounded concurrency、固定 QuickJS 资源上限和简单 completion ordering。只有实测出现不可接受的跨 App 阻塞后, 才需要重新讨论并发执行。 @@ -1025,8 +1028,8 @@ isolation 和 lifecycle。App Bundle 拥有名称、schema、provider mapping、 release;普通 App 位于 `/workspace/apps/`。 3. App Supervisor 只 seed Pi Agent `SystemAppBundle`;ordinary App 从磁盘 `current` 恢复,也可以从局域网上传的 `.pocketapp` 动态激活。普通 View Guest 被限制在 - 自己的 `data/` 和 `tmp/`,切换它们不会替换 System App。普通 View/Data Runtime - 各使用容量为 3 的简单 LRU;这里没有 Marketplace、pinning 或额外 residency + 自己的 `data/` 和 `tmp/`,切换它们不会替换 System App。普通 View Runtime 和 + Data Action Runtime 各使用容量为 3 的简单 LRU;这里没有 Marketplace、pinning 或额外 residency service。 4. Tool Catalog/Router 合并 Native Tools 与 namespaced App Tools;Agent Loop 和 Root View 已挂载在同一个 PocketJS Guest。模型请求由一个常驻 worker 顺序 @@ -1037,7 +1040,7 @@ isolation 和 lifecycle。App Bundle 拥有名称、schema、provider mapping、 AppTask store 持久化在 `apps/robinhood/data/.system/schedules.json`; refresh 是 `AppTask`,只 enqueue Data Action,不启动模型。 6. 每个有后台数据能力的 App release 包含可选 `data-action.js`。一个 bounded - `AppDataRunner` 顺序执行 Tool/Schedule/UI refresh;Robinhood 和 Exa View + `DataActionRunner` 顺序执行 Tool/Schedule/UI refresh;Robinhood 和 Exa View bundles 已删除网络调用和业务表写入。 7. 每个 App 有一个共享 `DbModule` owner。View Guest 与 Data Action Guest 使用 同一 connection owner,避免 ESP32 `unix-none` VFS 上两个 connection 竞争同一 @@ -1064,7 +1067,7 @@ isolation 和 lifecycle。App Bundle 拥有名称、schema、provider mapping、 typography/recipes/components,不包含 App-specific View 或 native UI logic。 12. ESP32-P4 使用 4 KiB PSRAM launcher,待 ESP-IDF entry task 退出后创建 64 KiB internal AgentOS runtime stack;所有 App QuickJS Guest 使用显式 PSRAM - allocator,不修改全局 `malloc()` policy。App Data pthread 使用 128 KiB PSRAM + allocator,不修改全局 `malloc()` policy。App Data Action pthread 使用 128 KiB PSRAM stack,并把 PSRAM allocation caps 继承给按需创建的 96 KiB NET worker;热安装 candidate Data 校验也使用相同配置。常驻 model worker 使用 64 KiB PSRAM stack。 13. AgentOS 核心 contract tests 已证明:Agent turn 进行中打开 Robinhood,仍能收到完整回复 @@ -1108,30 +1111,40 @@ isolation 和 lifecycle。App Bundle 拥有名称、schema、provider mapping、 transaction、resident Agent lifecycle、Tool routing、revision coalescing 和 3-entry LRU 等核心 contract;不保留 UI 坐标、按压视觉状态或重复 smoke tests。 `dataVersion` reset 只发生一次,正常启动不重复 DDL。 -21. 实机断网 Tool 验证已证明:Agent 调用 `research.search` 后,Data Action 在 +21. 卸载 contract test 使用真实 Robinhood bundle,覆盖 live Agent Tool snapshot、 + cached View/Data Action Runtime、Schedule、credential、整个 App data root、重启后 + Catalog 和同一 App 重新安装;`pi-agent` System App 永远不可卸载。 +22. 实机断网 Tool 验证已证明:Agent 调用 `research.search` 后,Data Action 在 native connection-ready gate 处 fail-fast,写一条 terminal error search,随后 删除不安全的 30 秒 ESP-Hosted 自动 reconnect 后,连续观察 120 秒没有 assertion 或重启;用户仍可从 Settings 明确发起重连。 -22. Exa schema v5 只保存固定 View 消费的 bounded search-history row:query、时间、 +23. Exa schema v5 只保存固定 View 消费的 bounded search-history row:query、时间、 terminal status、result count、top title 和 error;provider 原始 JSON、其余结果 和 fetched document 都只返回 Agent,不进入 SQLite。每次 search transaction 删除 7 天前的 searches;不自动执行 `VACUUM`。 -23. 正式 `pocket-net` 路径曾在 schema v4 镜像的实体 ESP32-P4 上完成 Exa provider +24. 正式 `pocket-net` 路径曾在 schema v4 镜像的实体 ESP32-P4 上完成 Exa provider success、SQLite 持久化、重启恢复和 30 秒稳定性观察。schema v5 只收窄 App-owned projection,不改变已验证的 `/search`、`/contents` transport contract。 -24. App credential 只由 Installer 从 package 读取并写入 Native NVS;UART 不提供 - credential 注入 side door。既有 Robinhood provider E2E 只作为 transport 历史 - 证据,不代表新的 package credential 路径已完成实体板验收。 -25. Agent 发起 App Tool 时,`RoutedToolHost` 与 Data Action 共用从 Router 创建的 +25. App credential 只由 Installer 从 package 读取并写入 Native NVS;UART 只传输 + 完整 package,不提供 package 外的 credential 注入 side door。既有 Robinhood + provider E2E 只作为 transport 历史证据。本轮实体板已验证带 credential 的 Exa + package 经 UART 进入同一 review/install、卸载、重启不恢复和干净重装路径;未在 + 本轮重复 provider call。 +26. Agent 发起 App Tool 时,`RoutedToolHost` 与 Data Action 共用从 Router 创建的 80 秒绝对 deadline;worker 直接把真实结果返回等待方,不再经过 Supervisor frame 转发,也不把 queued receipt 当作模型结果。PocketJS HTTP 和 native MCP 都只能使用剩余 budget,超时的 Data Action Guest 会被丢弃,避免 pending Promise 在后续调用中恢复。Exa 和 Robinhood 的 provider payload 都只放进 ToolResult `text`,不再同时复制到 `details`。 -26. 最新实机链路已完成 Exa search → fetch → DeepSeek 最终总结;模型 worker 内复用 +27. 最新实机链路已完成 Exa search → fetch → DeepSeek 最终总结;模型 worker 内复用 HTTPS client 后,该轮没有 TLS `-0x3000`、pthread 创建失败或重启。大 Tool Result 同步进入 QuickJS context 时仍出现 task-watchdog warning,所以持续无人值守稳定性 不能视为已经闭合。 +28. Physical ESP32-P4 的正常 model path 是持久化 wireless backend:首次 + `uart-provision.py` 把 provider、model、thinking level 和 API key 写入 Native NVS, + 后续启动直接读取,不等待 UART,也不静默回落到 Codex。`uart-install.py` 只向运行中 + 的板子传输完整 `.pocketapp`,不重启、不改 model config;HTTP/UART 最终都进入同一个 + Installer。通用串口诊断继续使用 `espflash monitor`,没有另建 debug framework。 ### 22.2 待补齐,不能视为已实现 @@ -1158,16 +1171,25 @@ isolation 和 lifecycle。App Bundle 拥有名称、schema、provider mapping、 `dataChanged` 和 account/span 的 bounded cache miss,但所有 projection cache 的 `loadedRevision` 仍需继续显式化,随后再用板上 query 计数确认交互路径为零重读。 -### 22.3 App 热安装边界 +### 22.3 App 安装与卸载边界 当前 v1 的安装只有一个固定业务流程:Installer 接收一个完整、预编译的 `.pocketapp`,屏幕显示 App、Tools、network policy 和 schedules,用户确认后进入 Installing 锁定状态;candidate 校验通过后创建 `current`,并在同一个 Pi Agent Guest 中更新 Tool snapshot。Robinhood、Exa 和任何新 ordinary App 都只能通过 Installer -进入设备。HTTP 是当前唯一实现的 ingress;未来网络或 UART ingress 只负责输送同一 -artifact,不得成为第二条安装路径。 - -明确延后:Marketplace、Registry、签名、TLS/配对、update/rollback/uninstall、migration、 +进入设备。HTTP 和 UART ingress 都只负责输送同一 artifact,不得成为第二条安装 +路径。 + +卸载只从 Pi Agent 的 Apps 页面发起。用户先进入 `UNINSTALL APP` mode,App row 的 +右侧 navigation chevron 才变成 danger `X`;点击 `X` 后,Host 先显示卸载中状态, +再由同一个 `AppSupervisor` 删除 Tool routing、Schedule、View/Data Action Runtime、 +credential、MCP session 和 `/workspace/apps/`。卸载不保留 App data,也没有 +rollback、trash 或第二套 lifecycle manager;System App `pi-agent` 不进入该列表。 +实体 ESP32-P4 已验证 Exa 的 UART upload → review → install → uninstall → restart +absence → UART reinstall;卸载和重装后 Files 页面会在用户进入时读取真实目录,而不 +复用安装前的根目录缓存。 + +明确延后:Marketplace、Registry、签名、TLS/配对、update/rollback、migration、 credential rotation UI、ESP32 source editing、Agent-authored Tools 和不经编译的 declarative View schema。 diff --git a/docs/esp32-p4-port.md b/docs/esp32-p4-port.md index 65c088c..86c4893 100644 --- a/docs/esp32-p4-port.md +++ b/docs/esp32-p4-port.md @@ -23,11 +23,10 @@ The physical host selects `UartBackend` for a Mac Codex/Claude Code bridge or `WirelessBackend` for direct OpenAI/OpenRouter/Anthropic/DeepSeek HTTPS. These remain host adapters; they do not belong in the UI or embedded Agent core. -The ESP host also exposes the current HTTP ingress for ordinary App packages. -That adapter only receives a complete `.pocketapp` and hands it to the shared -Installer in `pocket-pi-agentos`; it does not write App releases, credentials, -`current`, or runtime state itself. A future UART ingress must use the same -Installer boundary. +The ESP host exposes HTTP and UART ingress for ordinary App packages. Both +adapters only receive a complete `.pocketapp` and hand it to the shared Installer +in `pocket-pi-agentos`; neither writes App releases, credentials, `current`, or +runtime state itself. ```sh cargo xtask build esp32-p4 @@ -75,30 +74,40 @@ App contracts, but may use simpler macOS adapters. ESP-IDF, PSRAM, PPA, LittleFS capacity, MIPI-DSI, touch-controller and Wi-Fi/NVS behavior require a physical-board test. -## Physical UART bridge +## Physical provisioning, install and diagnostics -The thin bridge CLI provisions the boot-time model choice and routes framed -model decisions. Its `uart_bridge` adapters reuse a logged-in Codex Coding Plan -through the persistent Codex app-server, or Claude Code through `stream-json`. -Both paths consume provider deltas internally, then coalesce them into one -final `PPI-RPC-STREAM` result for the current device-side `UartBackend`. -Wi-Fi can be selected later from the on-device Settings page. +The physical board is provisioned once with a standalone wireless model backend. +Model provider, model, thinking level and API key are stored in native NVS; +Wi-Fi SSID/password use the existing Wi-Fi NVS store. Normal boots load those +values directly and do not wait for UART. ```sh -tools/uart-model-bridge.py /dev/cu.usbserial-... --backend uart --provider codex +tools/uart-provision.py /dev/cu.usbserial-... \ + --provider deepseek --provision-wifi -# Submit one boot-time prompt for a repeatable physical E2E test -tools/uart-model-bridge.py /dev/cu.usbserial-... --backend uart \ - --provider codex --prompt 'Use write, read, schedule.set and schedule.list.' +tools/uart-install.py /dev/cu.usbserial-... \ + target/pocketapps/exa.pocketapp -tools/uart-model-bridge.py /dev/cu.usbserial-... --backend wireless \ - --provider openai --model gpt-5-mini --provision-wifi +espflash monitor --port /dev/cu.usbserial-... +``` + +The App uploader does not reset the board or configure a model. It only transfers +one complete package to the same Installer used by HTTP. For unprovisioned +development-board bring-up, the optional model bridge can still route requests +to a logged-in Mac Codex or Claude Code without persisting that backend: + +All UART CLIs leave DTR/RTS inactive before closing the port so the WCH USB +serial bridge does not reset a running board. + +```sh +tools/uart-model-bridge.py /dev/cu.usbserial-... --provider codex \ + --prompt 'Use write, read, schedule.set and schedule.list.' ``` The Settings page scans visible networks, opens the shared PocketJS keyboard for secured-network passwords, connects, forgets credentials, and requests a -restart. The ESP host persists only Wi-Fi SSID/password in NVS. Model API keys -arrive through provisioning and remain outside UI projections. +restart. Model and Wi-Fi credentials remain outside Agent workspace and UI +projections. UART provisioning also seeds the board clock from the Mac for development. Standalone operation uses ESP-IDF SNTP after Wi-Fi connects. Both feed the same diff --git a/docs/pocket-pi-design-system.md b/docs/pocket-pi-design-system.md index bbd9306..dc2d76f 100644 --- a/docs/pocket-pi-design-system.md +++ b/docs/pocket-pi-design-system.md @@ -28,7 +28,7 @@ body 16→18px;不会引入 runtime font loader 或第二套字体。 | `PocketHeader` | 112px shell header、back/status affordance、App title、两行 metadata | navigation side effect、App data loading | Pi Agent、Exa、Robinhood | | `PageIntro` | eyebrow、page title、一行说明 | 页面 query、filter、scroll | Pi Agent installer、Exa | | `SectionHeading` | section title、optional detail、optional `VIEW ALL` affordance | list data、tap behavior | Pi Agent installer、Robinhood | -| `ActionButton` | primary/neutral/danger/disabled visual state、统一 18px label | action execution、loading state machine | Pi Agent、Robinhood | +| `ActionButton` | primary/neutral/danger/disabled visual state、统一 18px label | action execution、loading state machine | Pi Agent installer/Apps uninstall、Robinhood | | `statusBadge` recipe | 短 label 的 neutral/info/success/warning/danger surface + text classes | 长状态文案、业务判断 | Exa result、Robinhood account status | | `EmptyState` | optional icon、title、detail、regular/compact layout | empty 条件、retry action | Exa、Robinhood | | `MetricCard` | metric label、formatted value、optional positive/negative tone | 数值计算、currency formatting、time range | Robinhood value/cash/buying power | diff --git a/firmware/esp32-p4/Cargo.lock b/firmware/esp32-p4/Cargo.lock index dd68a8b..4b337ff 100644 --- a/firmware/esp32-p4/Cargo.lock +++ b/firmware/esp32-p4/Cargo.lock @@ -1201,6 +1201,7 @@ name = "pocket-pi-p4" version = "0.1.0" dependencies = [ "anyhow", + "base64", "embedded-svc", "embuild", "esp-idf-svc", diff --git a/firmware/esp32-p4/Cargo.toml b/firmware/esp32-p4/Cargo.toml index cb7901b..be8a86b 100644 --- a/firmware/esp32-p4/Cargo.toml +++ b/firmware/esp32-p4/Cargo.toml @@ -23,6 +23,7 @@ opt-level = "z" [dependencies] anyhow = "1" +base64 = "0.23.1" log = "0.4" serde_json = "1" pocket-pi-agentos = { path = "../../crates/pocket-pi-agentos" } diff --git a/firmware/esp32-p4/src/agentos_main.rs b/firmware/esp32-p4/src/agentos_main.rs index ef0e71b..1c94db1 100644 --- a/firmware/esp32-p4/src/agentos_main.rs +++ b/firmware/esp32-p4/src/agentos_main.rs @@ -1,13 +1,16 @@ use core::time::Duration; +use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::mpsc::{self, Receiver}; +use std::sync::mpsc::{self, SyncSender}; use std::sync::Arc; use std::time::Instant; use anyhow::Context as _; +use base64::prelude::{Engine as _, BASE64_STANDARD}; use embedded_svc::http::{Headers as _, Method}; use embedded_svc::io::{Read as _, Write as _}; use esp_idf_svc::http::server::{Configuration as ServerConfiguration, EspHttpServer}; +use esp_idf_svc::nvs::EspDefaultNvsPartition; use pocket_pi_agentos::{ stage_pocketapp_bytes, system_app_bundle, AppDescriptor, AppServiceHost, AppSupervisor, InstalledAppIndex, RoutedToolHost, StagedApp, DATA_ACTION_STACK_BYTES, MAX_POCKETAPP_BYTES, @@ -22,11 +25,17 @@ use super::{ app_services::EspAppServices, backend, device_state::{SettingsProjection, WifiSettingsProjection}, - esp_result, init_wifi, storage, transport, DisplayProbe, EspPlatform, BOARD_NAME, PANEL_HEIGHT, - PANEL_WIDTH, + esp_result, init_wifi, storage, transport, transport::LineTransport as _, DisplayProbe, + EspPlatform, BOARD_NAME, PANEL_HEIGHT, PANEL_WIDTH, }; const AGENTOS_FREERTOS_HZ: u32 = 100; +const UART_INSTALL_BEGIN: &str = "PPI-INSTALL-BEGIN:"; +const UART_INSTALL_CHUNK: &str = "PPI-INSTALL-CHUNK:"; +const UART_INSTALL_READY: &str = "PPI-INSTALL-READY"; +const UART_INSTALL_ACK: &str = "PPI-INSTALL-ACK"; +const UART_INSTALL_UPLOADED: &str = "PPI-INSTALL-UPLOADED"; +const UART_INSTALL_ERROR: &str = "PPI-INSTALL-ERROR:"; const _: () = assert!(esp_idf_svc::sys::configTICK_RATE_HZ == AGENTOS_FREERTOS_HZ); #[derive(Clone)] @@ -43,19 +52,33 @@ struct InstallUi { pub fn run() -> anyhow::Result<()> { let _workspace = storage::mount_workspace()?; + let nvs = EspDefaultNvsPartition::take()?; let uart = Arc::new( transport::UartLineTransport::new() .map_err(|error| anyhow::anyhow!("initialize UART transport: {error}"))?, ); - let runtime_config = - match transport::request_runtime_config(uart.as_ref(), Duration::from_secs(5)) { - Ok(config) => config, - Err(error) => { - log::warn!("No UART runtime config received: {error}"); - transport::RuntimeConfig::default() + let runtime_config = match transport::load_runtime_config(nvs.clone()) + .map_err(anyhow::Error::msg)? + { + Some(config) => { + log::info!("loaded wireless model configuration from NVS"); + config + } + None => { + let config = transport::request_runtime_config(uart.as_ref(), Duration::from_secs(20)) + .map_err(|error| anyhow::anyhow!( + "Pocket Pi is not provisioned; run tools/uart-provision.py: {error}" + ))?; + if matches!(config.model.backend, ModelBackendSettings::Wireless { .. }) { + transport::persist_runtime_config(nvs.clone(), &config) + .map_err(anyhow::Error::msg)?; + uart.write_line("PPI-CONFIG-STORED"); + log::info!("stored wireless model configuration in NVS"); } - }; + config + } + }; if let Some(seconds) = runtime_config.unix_time_seconds { let time = esp_idf_svc::sys::timeval { tv_sec: seconds as i64, @@ -67,6 +90,7 @@ pub fn run() -> anyhow::Result<()> { } let mut wifi = match init_wifi( + nvs.clone(), runtime_config.wifi_ssid.as_deref(), runtime_config.wifi_password.as_deref(), ) { @@ -119,7 +143,7 @@ pub fn run() -> anyhow::Result<()> { let uart_poc = matches!(model_settings.backend, ModelBackendSettings::Uart { .. }); let backend: Arc = match model_settings.backend { ModelBackendSettings::Uart { .. } => { - let transport: Arc = uart; + let transport: Arc = uart.clone(); Arc::new(backend::UartBackend::new(transport)) } ModelBackendSettings::Wireless { provider } => Arc::new( @@ -143,7 +167,7 @@ pub fn run() -> anyhow::Result<()> { let services: Arc = Arc::new(EspAppServices::new( network_ready.clone(), catalog.clone(), - wifi.as_ref().map(|wifi| wifi.nvs.clone()), + Some(nvs), )); let mut supervisor = with_psram_pthread_config(DATA_ACTION_STACK_BYTES, || { AppSupervisor::new(storage::WORKSPACE_ROOT, catalog, services) @@ -182,7 +206,14 @@ pub fn run() -> anyhow::Result<()> { .boot_agent(&config.to_string(), backend, Arc::new(routed_tools)) .map_err(anyhow::Error::msg) })?; - let (_install_server, install_rx, install_slot) = start_install_server()?; + let install_root = prepare_install_root()?; + let (install_tx, install_rx) = mpsc::sync_channel(1); + let install_slot = Arc::new(AtomicBool::new(false)); + let _install_server = start_install_server( + install_root.clone(), + install_tx.clone(), + install_slot.clone(), + )?; let mut messages = vec![Message { role: "assistant", @@ -200,15 +231,39 @@ pub fn run() -> anyhow::Result<()> { let mut pending_install: Option = None; let mut install_ui: Option = None; let mut install_requested = false; + let mut pending_uninstall: Option = None; + let mut uninstall_error: Option = None; let mut last_tick = Instant::now(); let mut last_runtime_pump = Instant::now(); let mut last_projection_refresh = Instant::now(); let mut last_heartbeat = Instant::now(); let mut last_wifi_poll = Instant::now(); + let mut last_uart_install_poll = Instant::now(); let mut last_wifi_connected = wifi.as_ref().is_some_and(|wifi| wifi.is_connected()); loop { - if pending_install.is_none() && !busy && !supervisor.services_busy() { + if last_uart_install_poll.elapsed() >= Duration::from_millis(250) + && pending_install.is_none() + && pending_uninstall.is_none() + && !busy + && !supervisor.services_busy() + { + last_uart_install_poll = Instant::now(); + if let Ok(begin) = uart.read_frame(UART_INSTALL_BEGIN, Duration::from_millis(1)) { + receive_uart_install( + uart.as_ref(), + &begin, + &install_root, + &install_tx, + install_slot.as_ref(), + ); + } + } + if pending_install.is_none() + && pending_uninstall.is_none() + && !busy + && !supervisor.services_busy() + { if let Ok(staged) = install_rx.try_recv() { install_ui = Some(InstallUi { state: "review", @@ -256,6 +311,17 @@ pub fn run() -> anyhow::Result<()> { pending_ui_task = Some(task.to_owned()); } } + Some("uninstallApp") => { + if let Some(app_id) = action.get("app").and_then(Value::as_str) { + if busy || supervisor.services_busy() { + uninstall_error = Some("APP SERVICES ARE BUSY".into()); + } else { + pending_uninstall = Some(app_id.to_owned()); + uninstall_error = None; + } + projection_dirty = true; + } + } Some("installApp") if install_ui.as_ref().is_some_and(|ui| ui.state == "review") => { @@ -401,7 +467,12 @@ pub fn run() -> anyhow::Result<()> { } if last_tick.elapsed() >= Duration::from_secs(1) { - if install_ui.is_none() && !busy && agent_status == "IDLE" && initial_prompt.is_none() { + if install_ui.is_none() + && pending_uninstall.is_none() + && !busy + && agent_status == "IDLE" + && initial_prompt.is_none() + { if let Some(wake) = native_tools.claim_due() { submit_prompt( wake.prompt, @@ -447,6 +518,8 @@ pub fn run() -> anyhow::Result<()> { native_tools.as_ref(), supervisor.catalog(), install_ui.as_ref(), + pending_uninstall.as_deref(), + uninstall_error.as_deref(), ); supervisor.update_root(&projection)?; projection_dirty = false; @@ -511,6 +584,7 @@ pub fn run() -> anyhow::Result<()> { if agent_status == "IDLE" && !busy && install_ui.is_none() + && pending_uninstall.is_none() && initial_prompt.is_some() && Instant::now() >= initial_prompt_not_before && (!wireless_model || network_ready.load(Ordering::Acquire)) @@ -574,9 +648,21 @@ pub fn run() -> anyhow::Result<()> { } } + if let Some(app_id) = pending_uninstall.take() { + match supervisor.uninstall_app(&app_id) { + Ok(_) => log::info!("App uninstall succeeded: {app_id}"), + Err(error) => { + log::error!("App uninstall failed: {error:#}"); + uninstall_error = Some(format!("{error:#}")); + } + } + projection_dirty = true; + redraw = true; + } + // A UI-requested task runs only after its loading state has reached the // panel. This preserves immediate touch feedback even when HTTPS is slow. - if install_ui.is_none() && !busy { + if install_ui.is_none() && pending_uninstall.is_none() && !busy { if let Some(task) = pending_ui_task.take() { let started = Instant::now(); let result = supervisor.invoke_active_task(&task, &Value::Null); @@ -611,15 +697,22 @@ pub fn run() -> anyhow::Result<()> { } } -fn start_install_server( -) -> anyhow::Result<(EspHttpServer<'static>, Receiver, Arc)> { - let install_root = std::path::Path::new(storage::WORKSPACE_ROOT).join(".system/install"); +fn prepare_install_root() -> anyhow::Result { + let install_root = Path::new(storage::WORKSPACE_ROOT).join(".system/install"); if let Err(error) = std::fs::remove_dir_all(&install_root) { if error.kind() != std::io::ErrorKind::NotFound { return Err(error).context("clear stale App install staging"); } } std::fs::create_dir_all(&install_root)?; + Ok(install_root) +} + +fn start_install_server( + install_root: PathBuf, + tx: SyncSender, + slot: Arc, +) -> anyhow::Result> { let mut server = EspHttpServer::new(&ServerConfiguration { stack_size: 12 * 1024, ..Default::default() @@ -631,8 +724,6 @@ fn start_install_server( Ok(()) })?; - let (tx, rx) = mpsc::sync_channel(1); - let slot = Arc::new(AtomicBool::new(false)); let handler_slot = slot.clone(); server.fn_handler::("/install", Method::Post, move |mut request| { if handler_slot @@ -644,13 +735,7 @@ fn start_install_server( .write_all(b"another install is pending")?; return Ok(()); } - let job = install_root.join( - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|duration| duration.as_nanos()) - .unwrap_or_default() - .to_string(), - ); + let job = install_job(&install_root); let result = (|| -> anyhow::Result { let length = usize::try_from( request @@ -694,14 +779,88 @@ fn start_install_server( Ok(()) })?; log::info!("App installer listening on http:///"); - Ok((server, rx, slot)) + Ok(server) +} + +fn receive_uart_install( + transport: &dyn transport::LineTransport, + begin: &str, + install_root: &Path, + tx: &SyncSender, + slot: &AtomicBool, +) { + if slot + .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire) + .is_err() + { + transport.write_line(&format!("{UART_INSTALL_ERROR}installer is busy")); + return; + } + + let job = install_job(install_root); + let result = (|| -> anyhow::Result<()> { + let length = begin + .strip_prefix(UART_INSTALL_BEGIN) + .context("invalid upload header")? + .parse::() + .context("invalid package length")?; + anyhow::ensure!(length > 0, "package is empty"); + anyhow::ensure!(length <= MAX_POCKETAPP_BYTES, "package exceeds 2 MiB"); + + let mut bytes = Vec::with_capacity(length); + transport.write_line(UART_INSTALL_READY); + while bytes.len() < length { + let frame = transport + .read_frame(UART_INSTALL_CHUNK, Duration::from_secs(10)) + .map_err(anyhow::Error::msg)?; + let payload = frame + .strip_prefix(UART_INSTALL_CHUNK) + .context("invalid chunk header")?; + let chunk = BASE64_STANDARD + .decode(payload) + .context("invalid chunk encoding")?; + anyhow::ensure!(!chunk.is_empty(), "empty chunk"); + anyhow::ensure!( + bytes.len() + chunk.len() <= length, + "package exceeds declared length" + ); + bytes.extend_from_slice(&chunk); + transport.write_line(UART_INSTALL_ACK); + } + + std::fs::create_dir_all(&job)?; + let staged = stage_pocketapp_bytes(&bytes, &job.join("release"))?; + tx.try_send(staged) + .map_err(|_| anyhow::anyhow!("installer is busy"))?; + Ok(()) + })(); + + match result { + Ok(()) => transport.write_line(UART_INSTALL_UPLOADED), + Err(error) => { + let _ = std::fs::remove_dir_all(&job); + slot.store(false, Ordering::Release); + let message = format!("{error:#}").replace(['\r', '\n'], " "); + transport.write_line(&format!("{UART_INSTALL_ERROR}{message}")); + } + } +} + +fn install_job(install_root: &Path) -> PathBuf { + install_root.join( + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|duration| duration.as_nanos()) + .unwrap_or_default() + .to_string(), + ) } fn with_psram_pthread_config( stack_size: usize, action: impl FnOnce() -> anyhow::Result, ) -> anyhow::Result { - // Large persistent workers use PSRAM stacks. App Data passes this setting + // Large persistent workers use PSRAM stacks. App Data Action passes this setting // to its NET child; the platform default is restored after each spawn. let default = unsafe { esp_idf_svc::sys::esp_pthread_get_default_config() }; let mut config = default; @@ -758,6 +917,8 @@ fn root_projection( tools: &CoreToolHost, catalog: &InstalledAppIndex, install: Option<&InstallUi>, + uninstalling_app: Option<&str>, + uninstall_error: Option<&str>, ) -> Value { let schedule = tools.schedule_projection(); let schedule_text = match schedule.next_in_seconds { @@ -827,6 +988,8 @@ fn root_projection( "scheduleEveryMinutes":app.schedules.first().map(|schedule| schedule.every_minutes), })).collect::>(), "install":install, + "uninstallingApp":uninstalling_app, + "uninstallError":uninstall_error, "settings":{ "wifi":{ "connectedSsid":settings.wifi.connected_ssid, diff --git a/firmware/esp32-p4/src/app_services.rs b/firmware/esp32-p4/src/app_services.rs index d46574f..149da60 100644 --- a/firmware/esp32-p4/src/app_services.rs +++ b/firmware/esp32-p4/src/app_services.rs @@ -409,18 +409,24 @@ impl AppServiceHost for EspAppServices { Ok(()) } - fn remove_credentials(&self, ids: &[String]) -> Result<(), String> { + fn remove_app_state(&self, app_id: &str, credential_ids: &[String]) -> Result<(), String> { let mut stored = self .inner .credentials .lock() .map_err(|_| "credential store lock was poisoned".to_owned())?; - for id in ids { + for id in credential_ids { stored.remove(id); } if let Some(partition) = &self.inner.nvs { persist_credentials(partition.clone(), &stored)?; } + drop(stored); + self.inner + .mcp + .lock() + .map_err(|_| "MCP state lock was poisoned".to_owned())? + .retain(|(owner, _), _| owner != app_id); Ok(()) } } diff --git a/firmware/esp32-p4/src/main.rs b/firmware/esp32-p4/src/main.rs index 6b0b88f..578d288 100644 --- a/firmware/esp32-p4/src/main.rs +++ b/firmware/esp32-p4/src/main.rs @@ -22,7 +22,7 @@ use device_state::{SettingsProjection, WifiNetworkProjection}; const BOARD_NAME: &str = "Waveshare ESP32-P4-WIFI6-Touch-LCD-5"; const PANEL_WIDTH: u32 = 720; const PANEL_HEIGHT: u32 = 1280; -const WIFI_NVS_NAMESPACE: &str = "pocket_pi"; +const DEVICE_NVS_NAMESPACE: &str = "pocket_pi"; const WIFI_NVS_SSID_KEY: &str = "wifi_ssid"; const WIFI_NVS_PASSWORD_KEY: &str = "wifi_pass"; const AGENTOS_LAUNCHER_STACK_BYTES: u32 = 4 * 1024; @@ -144,12 +144,12 @@ impl PlatformTools for EspPlatform { } fn init_wifi( + nvs: EspDefaultNvsPartition, provisioned_ssid: Option<&str>, provisioned_password: Option<&str>, ) -> anyhow::Result { let peripherals = Peripherals::take()?; let system_loop = EspSystemEventLoop::take()?; - let nvs = EspDefaultNvsPartition::take()?; let mut driver = WifiDriver::new(peripherals.modem, system_loop, Some(nvs.clone()))?; driver.set_configuration(&Configuration::Client(ClientConfiguration::default()))?; driver.start()?; @@ -274,7 +274,7 @@ impl WifiConnection { if pending.persist_on_success { let result = (|| { let storage = - EspDefaultNvs::new(self.nvs.clone(), WIFI_NVS_NAMESPACE, true)?; + EspDefaultNvs::new(self.nvs.clone(), DEVICE_NVS_NAMESPACE, true)?; storage.set_str(WIFI_NVS_SSID_KEY, &pending.ssid)?; storage.set_str(WIFI_NVS_PASSWORD_KEY, &pending.password)?; Ok::<(), anyhow::Error>(()) @@ -313,7 +313,7 @@ impl WifiConnection { if self.driver.is_connected()? { self.driver.disconnect()?; } - let storage = EspDefaultNvs::new(self.nvs.clone(), WIFI_NVS_NAMESPACE, true)?; + let storage = EspDefaultNvs::new(self.nvs.clone(), DEVICE_NVS_NAMESPACE, true)?; storage.remove(WIFI_NVS_SSID_KEY)?; storage.remove(WIFI_NVS_PASSWORD_KEY)?; Ok(()) @@ -353,7 +353,7 @@ fn load_wifi_credentials( provisioned_ssid: Option<&str>, provisioned_password: Option<&str>, ) -> anyhow::Result<(String, String)> { - let storage = EspDefaultNvs::new(partition, WIFI_NVS_NAMESPACE, true)?; + let storage = EspDefaultNvs::new(partition, DEVICE_NVS_NAMESPACE, true)?; if let (Some(ssid), Some(password)) = (provisioned_ssid, provisioned_password) { validate_wifi_ssid(ssid)?; validate_wifi_password(password)?; diff --git a/firmware/esp32-p4/src/transport/mod.rs b/firmware/esp32-p4/src/transport/mod.rs index eff47e8..5835da4 100644 --- a/firmware/esp32-p4/src/transport/mod.rs +++ b/firmware/esp32-p4/src/transport/mod.rs @@ -3,7 +3,7 @@ mod uart; use std::time::Duration; -pub use provisioning::{request_runtime_config, RuntimeConfig}; +pub use provisioning::{load_runtime_config, persist_runtime_config, request_runtime_config}; pub use uart::UartLineTransport; pub trait LineTransport: Send + Sync { diff --git a/firmware/esp32-p4/src/transport/provisioning.rs b/firmware/esp32-p4/src/transport/provisioning.rs index 451e211..efc11ba 100644 --- a/firmware/esp32-p4/src/transport/provisioning.rs +++ b/firmware/esp32-p4/src/transport/provisioning.rs @@ -1,15 +1,18 @@ use std::time::Duration; +use esp_idf_svc::nvs::{EspDefaultNvs, EspDefaultNvsPartition}; use pocket_pi_protocols::model::{ ModelBackendSettings, ModelSettings, ThinkingLevel, UartProvider, WirelessProvider, }; +use serde_json::{json, Value}; use super::LineTransport; +use crate::DEVICE_NVS_NAMESPACE; const CONFIG_REQUEST: &str = "PPI-CONFIG-REQUEST"; const CONFIG_RESPONSE: &str = "PPI-CONFIG:"; +const MODEL_CONFIG_KEY: &str = "model"; -#[derive(Default)] pub struct RuntimeConfig { pub wifi_ssid: Option, pub wifi_password: Option, @@ -26,15 +29,71 @@ pub fn request_runtime_config( ) -> Result { transport.write_line(CONFIG_REQUEST); let frame = transport.read_frame(CONFIG_RESPONSE, timeout)?; - let value: serde_json::Value = serde_json::from_str( + let value: Value = serde_json::from_str( frame .strip_prefix(CONFIG_RESPONSE) .ok_or_else(|| "invalid runtime config frame".to_owned())?, ) .map_err(|error| format!("runtime config JSON: {error}"))?; - let backend = text(&value, "modelBackend", 16)?.unwrap_or_else(|| "uart".into()); - let provider = text(&value, "modelProvider", 32)?.unwrap_or_else(|| "codex".into()); - let model_api_key = secret(&value, "modelApiKey", 512)?; + parse_runtime_config(&value) +} + +pub fn load_runtime_config( + partition: EspDefaultNvsPartition, +) -> Result, String> { + let storage = EspDefaultNvs::new(partition, DEVICE_NVS_NAMESPACE, true) + .map_err(|error| format!("open model config NVS: {error}"))?; + let Some(length) = storage + .blob_len(MODEL_CONFIG_KEY) + .map_err(|error| format!("read model config length: {error}"))? + else { + return Ok(None); + }; + let mut bytes = vec![0; length]; + let bytes = storage + .get_blob(MODEL_CONFIG_KEY, &mut bytes) + .map_err(|error| format!("read model config: {error}"))? + .ok_or_else(|| "model config disappeared while reading".to_owned())?; + let value: Value = serde_json::from_slice(bytes) + .map_err(|error| format!("parse stored model config: {error}"))?; + let config = parse_runtime_config(&value)?; + if !matches!(config.model.backend, ModelBackendSettings::Wireless { .. }) { + return Err("stored model config must use the wireless backend".into()); + } + Ok(Some(config)) +} + +pub fn persist_runtime_config( + partition: EspDefaultNvsPartition, + config: &RuntimeConfig, +) -> Result<(), String> { + let ModelBackendSettings::Wireless { provider } = config.model.backend else { + return Err("only wireless model config can be persisted".into()); + }; + let mut value = json!({ + "modelBackend": "wireless", + "modelProvider": provider.id(), + "modelApiKey": config.model_api_key, + "thinkingLevel": config.model.thinking_level.id(), + }); + if let Some(model) = &config.model.model { + value["model"] = Value::String(model.clone()); + } + let bytes = serde_json::to_vec(&value) + .map_err(|error| format!("encode model config: {error}"))?; + let storage = EspDefaultNvs::new(partition, DEVICE_NVS_NAMESPACE, true) + .map_err(|error| format!("open model config NVS: {error}"))?; + storage + .set_blob(MODEL_CONFIG_KEY, &bytes) + .map_err(|error| format!("store model config: {error}")) +} + +fn parse_runtime_config(value: &Value) -> Result { + let backend = text(value, "modelBackend", 16)? + .ok_or_else(|| "modelBackend is required".to_owned())?; + let provider = text(value, "modelProvider", 32)? + .ok_or_else(|| "modelProvider is required".to_owned())?; + let model_api_key = secret(value, "modelApiKey", 512)?; let model_backend = match (backend.as_str(), provider.as_str()) { ("uart", "codex") => ModelBackendSettings::Uart { provider: UartProvider::Codex, @@ -92,7 +151,7 @@ pub fn request_runtime_config( } fn text( - value: &serde_json::Value, + value: &Value, field: &str, max_bytes: usize, ) -> Result, String> { @@ -105,7 +164,7 @@ fn text( Ok(Some(value.to_owned())) } -fn string<'a>(value: &'a serde_json::Value, field: &str) -> Result, String> { +fn string<'a>(value: &'a Value, field: &str) -> Result, String> { let Some(value) = value.get(field) else { return Ok(None); }; @@ -119,7 +178,7 @@ fn string<'a>(value: &'a serde_json::Value, field: &str) -> Result Result, String> { diff --git a/hosts/esp32-p4-sim/src/main.rs b/hosts/esp32-p4-sim/src/main.rs index f84cec2..287c8a4 100644 --- a/hosts/esp32-p4-sim/src/main.rs +++ b/hosts/esp32-p4-sim/src/main.rs @@ -349,6 +349,8 @@ struct Product { pending_install: Option, install_ui: Option, install_requested: bool, + pending_uninstall: Option, + uninstall_error: Option, } struct InstallUi { @@ -405,6 +407,8 @@ impl Product { pending_install: None, install_ui: None, install_requested: false, + pending_uninstall: None, + uninstall_error: None, }) } @@ -449,6 +453,17 @@ impl Product { self.pending_ui_task = Some(task.to_owned()); } } + Some("uninstallApp") => { + if let Some(app_id) = action.get("app").and_then(Value::as_str) { + if self.busy || self.supervisor.services_busy() { + self.uninstall_error = Some("APP SERVICES ARE BUSY".into()); + } else { + self.pending_uninstall = Some(app_id.to_owned()); + self.uninstall_error = None; + } + self.projection_dirty = true; + } + } Some("installApp") if self .install_ui @@ -555,8 +570,22 @@ impl Product { self.projection_dirty = true; } + fn run_pending_uninstall(&mut self) { + let Some(app_id) = self.pending_uninstall.take() else { + return; + }; + if let Err(error) = self.supervisor.uninstall_app(&app_id) { + self.uninstall_error = Some(format!("{error:#}")); + } + self.projection_dirty = true; + } + fn poll(&mut self) -> Result<()> { - if self.pending_install.is_none() && !self.busy && !self.supervisor.services_busy() { + if self.pending_install.is_none() + && self.pending_uninstall.is_none() + && !self.busy + && !self.supervisor.services_busy() + { if let Ok(staged) = self.install_rx.try_recv() { self.install_ui = Some(InstallUi { state: "review", @@ -573,7 +602,7 @@ impl Product { self.projection_dirty = true; } if self.last_schedule_poll.elapsed() >= Duration::from_secs(1) { - if self.install_ui.is_none() && !self.busy { + if self.install_ui.is_none() && self.pending_uninstall.is_none() && !self.busy { if let Some(wake) = self.native_tools.claim_due() { self.send_prompt(wake.prompt); } @@ -685,6 +714,8 @@ impl Product { "scheduleEveryMinutes":app.schedules.first().map(|schedule| schedule.every_minutes), })).collect::>(), "install":install, + "uninstallingApp":self.pending_uninstall, + "uninstallError":self.uninstall_error, "settings":{ "wifi":{ "connectedSsid":self.wifi_connected, @@ -800,6 +831,7 @@ fn headless( for frame in 0..7_500 { product.poll()?; product.run_pending_install(); + product.run_pending_uninstall(); // Give PocketJS a few ticks to settle reactive insertions and layout // before taking a deterministic screenshot. if (!wait_for_turn || !product.busy) && !product.supervisor.services_busy() { @@ -954,6 +986,7 @@ impl WindowApp { frame.present(); state.product.run_pending_ui_task(); state.product.run_pending_install(); + state.product.run_pending_uninstall(); state.window.request_redraw(); Ok(()) } diff --git a/tools/uart-install.py b/tools/uart-install.py new file mode 100755 index 0000000..2b8c141 --- /dev/null +++ b/tools/uart-install.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 +"""Upload one .pocketapp to a running Pocket Pi over UART.""" + +from __future__ import annotations + +import argparse +import base64 +from pathlib import Path +import time + +from uart_io import close_port, open_port, read_lines, write_line + +BEGIN = "PPI-INSTALL-BEGIN:" +CHUNK = "PPI-INSTALL-CHUNK:" +READY = "PPI-INSTALL-READY" +ACK = "PPI-INSTALL-ACK" +UPLOADED = "PPI-INSTALL-UPLOADED" +ERROR = "PPI-INSTALL-ERROR:" +CHUNK_BYTES = 3 * 1024 + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("port") + parser.add_argument("package", type=Path) + args = parser.parse_args() + payload = args.package.read_bytes() + + fd = open_port(args.port) + pending = b"" + started = False + sent = 0 + next_begin = 0.0 + last_progress = time.monotonic() + try: + while time.monotonic() - last_progress < 15: + if not started and time.monotonic() >= next_begin: + write_line(fd, f"{BEGIN}{len(payload)}") + next_begin = time.monotonic() + 1 + lines, pending = read_lines(fd, pending, 0.2) + for line in lines: + if line.endswith(READY): + started = True + chunk = payload[:CHUNK_BYTES] + write_line(fd, CHUNK + base64.b64encode(chunk).decode()) + sent = len(chunk) + last_progress = time.monotonic() + print(f"Uploading {args.package.name} ({len(payload)} bytes)", flush=True) + elif line.startswith(ACK) and started: + last_progress = time.monotonic() + if sent < len(payload): + chunk = payload[sent : sent + CHUNK_BYTES] + write_line(fd, CHUNK + base64.b64encode(chunk).decode()) + sent += len(chunk) + else: + print("Package transferred; device is validating it", flush=True) + elif line.startswith(UPLOADED): + print("Upload complete; confirm installation on Pocket Pi", flush=True) + return 0 + elif line.startswith(ERROR): + print(f"Install failed: {line[len(ERROR):]}", flush=True) + return 1 + print("Install failed: device did not respond", flush=True) + return 1 + except (KeyboardInterrupt, OSError): + return 1 + finally: + close_port(fd) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/uart-model-bridge.py b/tools/uart-model-bridge.py index d8d02a0..3fa0424 100755 --- a/tools/uart-model-bridge.py +++ b/tools/uart-model-bridge.py @@ -1,20 +1,15 @@ #!/usr/bin/env python3 -"""Bridge ESP32 Pocket Pi model decisions to a logged-in Mac CLI.""" +"""Development-only bridge from Pocket Pi model requests to Codex or Claude Code.""" from __future__ import annotations import argparse -import getpass import json -import os -import select import signal -import subprocess -import termios import time -import tty from uart_bridge import create_backend +from uart_io import close_port, open_port, read_lines, reset_device, write_line CONFIG_REQUEST = "PPI-CONFIG-REQUEST" CONFIG_RESPONSE = "PPI-CONFIG:" @@ -22,39 +17,6 @@ WAITING = "PPI-RPC-WAITING" REQUEST = "PPI-RPC-REQUEST:" STREAM = "PPI-RPC-STREAM:" -DEEPSEEK_KEYCHAIN_SERVICE = "Pocket Pi Credentials" -DEEPSEEK_KEYCHAIN_ACCOUNT = "deepseek-api-key" - - -def keychain_secret(service: str, account: str) -> str | None: - """Read one generic-password value without ever printing it.""" - try: - result = subprocess.run( - ["security", "find-generic-password", "-s", service, "-a", account, "-w"], - capture_output=True, - text=True, - check=False, - ) - except FileNotFoundError: - return None - value = result.stdout.strip() - return value if result.returncode == 0 and value else None - - -def deepseek_api_key() -> str | None: - """Load the user-provided DeepSeek key from macOS Keychain.""" - return keychain_secret(DEEPSEEK_KEYCHAIN_SERVICE, DEEPSEEK_KEYCHAIN_ACCOUNT) - - -def write_line(fd: int, line: str) -> None: - payload = memoryview(f"{line}\r\n".encode()) - while payload: - try: - count = os.write(fd, payload) - except BlockingIOError: - select.select([], [fd], [], 0.5) - continue - payload = payload[count:] def log_tool_failure(request: dict[str, object]) -> None: @@ -81,25 +43,12 @@ def log_tool_failure(request: dict[str, object]) -> None: def config(args: argparse.Namespace) -> dict[str, object]: - provider = args.provider or ("codex" if args.backend == "uart" else "openai") value: dict[str, object] = { - "modelBackend": args.backend, - "modelProvider": provider, + "modelBackend": "uart", + "modelProvider": args.provider, "unixTimeSeconds": int(time.time()), + "thinkingLevel": args.thinking_level, } - if args.model: - value["model"] = args.model - value["thinkingLevel"] = args.thinking_level - if args.provision_wifi: - value["wifiSsid"] = input("Wi-Fi SSID: ").strip() - value["wifiPassword"] = getpass.getpass("Wi-Fi password: ") - if args.backend == "wireless": - key = deepseek_api_key() if provider == "deepseek" else None - if key: - value["modelApiKey"] = key - print("DeepSeek: reusing Keychain API key (RAM only on device)", flush=True) - else: - value["modelApiKey"] = getpass.getpass(f"{provider} API key: ") if args.prompt: value["initialPrompt"] = args.prompt if args.prompt_delay_seconds: @@ -110,12 +59,7 @@ def config(args: argparse.Namespace) -> dict[str, object]: def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("port") - parser.add_argument("--backend", choices=("uart", "wireless"), default="uart") - parser.add_argument( - "--provider", - choices=("codex", "claude-code", "openai", "openrouter", "anthropic", "deepseek"), - ) - parser.add_argument("--model") + parser.add_argument("--provider", choices=("codex", "claude-code"), default="codex") parser.add_argument("--thinking-level", choices=("high", "xhigh"), default="high") parser.add_argument("--prompt", help="submit one prompt after the board agent is ready") parser.add_argument( @@ -126,48 +70,24 @@ def main() -> int: metavar="0..120", help="delay the repeatable boot prompt while device services settle", ) - parser.add_argument("--provision-wifi", action="store_true") args = parser.parse_args() - provider = args.provider or ("codex" if args.backend == "uart" else "openai") - if args.backend == "uart" and provider not in ("codex", "claude-code"): - parser.error("UART provider must be codex or claude-code") - if args.backend == "wireless" and provider not in ("openai", "openrouter", "anthropic", "deepseek"): - parser.error("wireless provider must be openai, openrouter, anthropic or deepseek") runtime_config = config(args) - model_backend = create_backend(provider) if args.backend == "uart" else None + model_backend = create_backend(args.provider) + + reset_device(args.port) + fd = open_port(args.port) - try: - subprocess.run( - ["espflash", "reset", "--port", args.port, "--non-interactive"], - check=False, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - ) - except FileNotFoundError: - pass - - fd = os.open(args.port, os.O_RDWR | os.O_NOCTTY | os.O_NONBLOCK) - tty.setraw(fd) - attributes = termios.tcgetattr(fd) - attributes[4] = termios.B115200 - attributes[5] = termios.B115200 - termios.tcsetattr(fd, termios.TCSANOW, attributes) def stop(_signum: int, _frame: object) -> None: raise KeyboardInterrupt signal.signal(signal.SIGINT, stop) signal.signal(signal.SIGTERM, stop) pending = b"" - print(f"Pocket Pi UART bridge ready: {provider} on {args.port}", flush=True) + print(f"Pocket Pi development model bridge ready: {args.provider} on {args.port}", flush=True) try: while True: - readable, _, _ = select.select([fd], [], [], 0.2) - if not readable: - continue - pending += os.read(fd, 4096) - while b"\n" in pending: - raw, pending = pending.split(b"\n", 1) - line = raw.decode(errors="replace").strip() + lines, pending = read_lines(fd, pending, 0.2) + for line in lines: if line.endswith(CONFIG_REQUEST): frame = CONFIG_RESPONSE + json.dumps(runtime_config, separators=(",", ":")) for _ in range(3): @@ -178,8 +98,6 @@ def stop(_signum: int, _frame: object) -> None: write_line(fd, READY) elif line.startswith(REQUEST): try: - if model_backend is None: - raise RuntimeError("wireless mode does not provide a UART model backend") stream_stats = [0, 0] request_payload = json.loads(line[len(REQUEST) :]) log_tool_failure(request_payload) @@ -223,9 +141,8 @@ def emit_delta(delta: str) -> None: except (KeyboardInterrupt, OSError): return 0 finally: - if model_backend is not None: - model_backend.close() - os.close(fd) + model_backend.close() + close_port(fd) if __name__ == "__main__": diff --git a/tools/uart-provision.py b/tools/uart-provision.py new file mode 100755 index 0000000..ccf364a --- /dev/null +++ b/tools/uart-provision.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 +"""Persist Pocket Pi wireless model configuration over UART.""" + +from __future__ import annotations + +import argparse +import getpass +import json +import subprocess +import time + +from uart_io import close_port, open_port, read_lines, reset_device, write_line + +CONFIG_REQUEST = "PPI-CONFIG-REQUEST" +CONFIG_RESPONSE = "PPI-CONFIG:" +CONFIG_STORED = "PPI-CONFIG-STORED" +DEEPSEEK_KEYCHAIN_SERVICE = "Pocket Pi Credentials" +DEEPSEEK_KEYCHAIN_ACCOUNT = "deepseek-api-key" + + +def keychain_secret(service: str, account: str) -> str | None: + try: + result = subprocess.run( + ["security", "find-generic-password", "-s", service, "-a", account, "-w"], + capture_output=True, + text=True, + check=False, + ) + except FileNotFoundError: + return None + value = result.stdout.strip() + return value if result.returncode == 0 and value else None + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("port") + parser.add_argument( + "--provider", + choices=("openai", "openrouter", "anthropic", "deepseek"), + default="deepseek", + ) + parser.add_argument("--model") + parser.add_argument("--thinking-level", choices=("high", "xhigh"), default="high") + parser.add_argument("--provision-wifi", action="store_true") + args = parser.parse_args() + + api_key = ( + keychain_secret(DEEPSEEK_KEYCHAIN_SERVICE, DEEPSEEK_KEYCHAIN_ACCOUNT) + if args.provider == "deepseek" + else None + ) + if api_key is None: + api_key = getpass.getpass(f"{args.provider} API key: ") + config: dict[str, object] = { + "modelBackend": "wireless", + "modelProvider": args.provider, + "modelApiKey": api_key, + "thinkingLevel": args.thinking_level, + "unixTimeSeconds": int(time.time()), + } + if args.model: + config["model"] = args.model + if args.provision_wifi: + config["wifiSsid"] = input("Wi-Fi SSID: ").strip() + config["wifiPassword"] = getpass.getpass("Wi-Fi password: ") + + reset_device(args.port) + fd = open_port(args.port) + pending = b"" + deadline = time.monotonic() + 20 + try: + while time.monotonic() < deadline: + lines, pending = read_lines(fd, pending, 0.2) + for line in lines: + if line.endswith(CONFIG_REQUEST): + frame = CONFIG_RESPONSE + json.dumps(config, separators=(",", ":")) + for _ in range(3): + time.sleep(0.1) + write_line(fd, frame) + elif line.endswith(CONFIG_STORED): + print("Pocket Pi configuration stored", flush=True) + return 0 + print("Provisioning failed: device did not confirm storage", flush=True) + return 1 + except (KeyboardInterrupt, OSError): + return 1 + finally: + close_port(fd) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/uart_io.py b/tools/uart_io.py new file mode 100644 index 0000000..bd3c2a5 --- /dev/null +++ b/tools/uart_io.py @@ -0,0 +1,70 @@ +"""Small shared helpers for Pocket Pi UART command-line tools.""" + +from __future__ import annotations + +import fcntl +import os +import select +import struct +import subprocess +import termios +import tty + + +def open_port(path: str) -> int: + fd = os.open(path, os.O_RDWR | os.O_NOCTTY | os.O_NONBLOCK) + tty.setraw(fd) + attributes = termios.tcgetattr(fd) + attributes[4] = termios.B115200 + attributes[5] = termios.B115200 + termios.tcsetattr(fd, termios.TCSANOW, attributes) + return fd + + +def close_port(fd: int) -> None: + try: + termios.tcdrain(fd) + attributes = termios.tcgetattr(fd) + attributes[2] &= ~termios.HUPCL + attributes[2] |= termios.CLOCAL + termios.tcsetattr(fd, termios.TCSANOW, attributes) + lines = struct.pack("I", termios.TIOCM_DTR | termios.TIOCM_RTS) + fcntl.ioctl(fd, termios.TIOCMBIC, lines) + finally: + os.close(fd) + + +def reset_device(port: str) -> None: + try: + subprocess.run( + ["espflash", "reset", "--port", port, "--non-interactive"], + check=False, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + except FileNotFoundError: + pass + + +def write_line(fd: int, line: str) -> None: + payload = memoryview(f"{line}\r\n".encode()) + while payload: + try: + count = os.write(fd, payload) + except BlockingIOError: + select.select([], [fd], [], 0.5) + continue + payload = payload[count:] + + +def read_lines(fd: int, pending: bytes, timeout: float) -> tuple[list[str], bytes]: + readable, _, _ = select.select([fd], [], [], timeout) + if readable: + pending += os.read(fd, 4096) + lines: list[str] = [] + while b"\n" in pending: + raw, pending = pending.split(b"\n", 1) + line = raw.decode(errors="replace").strip() + if line: + lines.append(line) + return lines, pending