GearShell Architecture Map

Web Native Agent Sandbox — the browser is the sandbox

GearShell runs CLI agents, tools and SDKs inside a stock browser with zero install: a buildless React SPA that boots a Wanix WASM microkernel providing VFS isolation, PTY terminals and multi-language runtimes. Click any layer for details.

static CDN-only zero npm runtime Wanix v0.4.8 UI React 18 · Dockview 8.2 storage OPFS
UI shell panels modules runtime core persistence submodules external CDN
L0 · HOST

Web Browser

Chrome / Safari / Firefox — plain HTTPS tab, no extension details ▾
gear.sh SPA ?debug → eruda vercel.json SPA rewrite + COOP/COEP
  • The browser's own security model is the root of trust: VFS isolation + sandboxed processes, data never leaves the browser.
  • vercel.json ships SPA rewrites plus COOP/COEP headers for cross-origin isolation.
loads
L1 · SHELL UI

Shell UI — index.html + app.js

React 18 · Dockview 8.2 · importmap · lucide details ▾
index.html app.js?v=20260826.42 app.css importmap esm.sh DockviewReact dockview-theme-github-dark
  • Body is a single #wanix-host div; the only module script is app.js?v=… — top-level module code is the bootstrap, no DOMContentLoaded.
  • Dependencies resolved via <script type="importmap">: react/react-dom 18.2, dockview(-react/-enterprise) 8.2, lucide-react 0.468 — all from esm.sh.
  • Classic CDN scripts: marked.min.js, reveal.js 6.0.1, xterm CSS, eruda (debug only).
  • All 9 stylesheets carry ?v= cache-busting versions; CSS is flat (no custom properties, colors hard-coded GitHub-dark: #0d1117 / #58a6ff…).
renders
L2 · PANEL SYSTEM

Dockview Panel Grid

unified dispatch via PANEL_ADDERS + addPanelByComponent() details ▾
home files runtime deck reveal.js settings crush-runner terminal sessions workbench VS Code OSS vm v86 browser iframe group / task
  • Static panels (home, deck, settings, files, runtime, crush-runner) vs session panels keyed by sessionId (terminal, workbench, vm, task, group, iframe).
  • Default startup panel: ['home']. PANEL_CREATION_OPTIONS catalog has 16 entries.
  • Top-right AddTerminalButton: click = terminal, long-press/right-click = menu with terminal profiles + Wagi Dog + all panels.
  • iframe sub-panels: browser → /browser/, bonsai → /bonsai/, codigo → codigo.dev, crush → justwasm.github.io/crush, rickroll → YouTube.
imports
L3 · SHELL MODULES

Vanilla JS module families

buildless · 500-line rule · ?v= version discipline details ▾
app-* 17 files files-* 16 files settings-* 11 files crush-* 10 files panels.js home / deck / runtime / launcher
  • 500-line rule: any JS file over 500 lines is split into cohesive modules; every function stays under 50 lines.
  • Module URL consistency: every internal import must carry the SAME ?v= — a version split loads a module twice, breaking DI/singleton state (e.g. files-registry.js "initFiles has not been called").
  • DI-shim pattern: each submodule exports initX(deps) + lazy xDep(name); wiring is centralized at the bottom of app.js.
  • No global namespace (window.app doesn't exist); sessions live in module-level Maps.
bootstrap
L4 · RUNTIME CORE

Wanix WASM microkernel v0.4.8

dynamically imported · the terminal, VFS & tasks all live here details ▾
<wanix-namespace> <wanix-term> <wanix-task> bash /bash w9y binaries VFS 9p drivers gojs · wasi · js fetch binds w9y.io
  • Shell code never touches the wasm directly: loadActiveWorkspace() dynamically imports the runtime ES module, then createWanixSystem() builds <wanix-namespace id="wanix-system" wasm="…"> inside #wanix-host.
  • wasm + module pinned to @v0.4.8 (semver only — legacy commit-hash/@main URLs are auto-migrated by app-normalize.js).
  • Terminals are native <wanix-term> elements backed by the kernel — no xterm.js JS, no WebSocket/PTY code in the shell (xterm is imported as CSS only).
  • Terminal sessions: createTerminalSession() = <wanix-task id="repl-*"> + <wanix-term path="#task/…">, overlaid via attachOverlayTerminalSession.
  • The shell binary is a hush build mounted at /bash; default command bash -rcfile /profile.
persists to
L5 · PERSISTENCE & STATE

OPFS + localStorage

no IndexedDB · no service worker (yet) details ▾
/opfs/wanix /opfs/home localStorage gear-shell-config /tmp memfs events gear-shell-workspace-change
  • OPFS mounted through an opfs bind; WANIX='/opfs/wanix', HOME='/opfs/home'. No IndexedDB anywhere.
  • Workspace layout / config is JSON in localStorage (e.g. gear-shell-config); legacy hush-shell keys are kept as stable ids.
  • Crush-runner scratch state (crushrc) is written to the in-memory /tmp/crush-runner-<id> — never touches OPFS.
  • State sync via custom events: gear-shell-workspace-change on window, gear-shell-task-status on task elements.
plus
L6 · GIT SUBMODULES

Five embedded apps

mounted as static panels · pointers updated deliberately details ▾
bonsai/ 27B WebGPU LLM browser/ Browser.js · Scramjet proxy isolation/ Cloudflare Worker wanix-workbench/ VS Code OSS web-pet/ Wagi Dog
  • bonsai — btwiuse/bonsai-webgpu-kernels: buildless 27B LLM, streams GGUF from Hugging Face.
  • browser — btwiuse/gear-browser: Browser.js static build (Scramjet), mounted at /browser/.
  • isolation — btwiuse/gear-isolation: Cloudflare Worker providing the Browser.js isolation sandbox on *.greggang.com.
  • wanix-workbench — btwiuse/gear-wanix-workbench: Code-OSS static build + GearShell Wanix extension.
  • web-pet — justwasm/wagi-web-pet: Wagi Dog desktop pet (drag / click / right-click chat).
fetches from
L7 · EXTERNAL CDN / CLOUD

Everything is fetched at runtime — nothing is bundled

importmap + CDN is the dependency strategy details ▾
esm.sh react · dockview · lucide jsDelivr marked · reveal.js · xterm.css · eruda w9y.io wanix wasm · bash · w9y Hugging Face GGUF models agw.up.railway.app Crush AGW
  • Runtime wasm comes from w9y.io pinned by commit → verified byte-identical with the old railway URL (sha256 07c81e6c…).
  • The only network URL baked into Crush's default crushrc is AGW=https://agw.up.railway.app.
  • Bonsai loads GGUF weights on demand from Hugging Face (requires HF token).
TABLE Dependency graph at a glance
SourceProvidesDelivered via
esm.shreact 18.2 · react-dom 18.2 · dockview 8.2 · dockview-react 8.2 · dockview-enterprise 8.2 · lucide-react 0.468importmap (ES modules)
jsDelivrmarked.min.js · reveal.js 6.0.1 · @xterm/xterm css · erudaclassic scripts / stylesheets
w9y.iowanix runtime module + wasm (v0.4.8) · bash (hush build) · w9yfetch binds + dynamic import
Hugging FaceBonsai 27B GGUF weightsstreamed on demand
railway (agw)Crush agent gatewaycrushrc env

Boot sequence & data flow

From a blank tab to a fully wired multi-panel agent sandbox — the exact bootstrap order from index.html and the bottom of app.js.

BOOT Startup order
1

index.html parses — <base href="/">, importmap, 9 versioned stylesheets

Body is only <div id="wanix-host"></div>. Eruda attaches when the URL contains ?debug.

2

app.js?v=20260826.42 loads as the single module script

Top-level module code is the bootstrap — no DOMContentLoaded listener. Imports react, dockview, lucide and every panel module.

3

loadActiveWorkspace() → dynamic import of the Wanix runtime

Default https://w9y.io/…/wanix.min.js@v0.4.8 (wasm pinned to the same semver tag).

4

createWanixSystem() mounts the microkernel

Builds <wanix-namespace id="wanix-system" wasm="…"> inside #wanix-host — the kernel now owns terminal, VFS and tasks.

5

React mounts the Dockview grid

createRoot(#app-root) renders <DockviewReact className="dockview-theme-github-dark">; startup panel is ['home'].

6

DI wiring at the bottom of app.js

initLauncher → initPanels → initDeck → initRuntime → initFiles → initSettings → initHome → initCrushRunner — every panel receives its deps via initX(deps) + lazy xDep(name).

DATA Runtime wiring & communication patterns

Terminal session anatomy

  • createTerminalSession() → <wanix-task id="repl-*"> + <wanix-term path="#task/…">
  • Sessions overlay inside #terminal-layer via attachOverlayTerminalSession (absolute positioning, not Dockview content)
  • Default cmd bash -rcfile /profile; profiles from BUILTIN_TERMINAL_PROFILES (bash, crush)
  • The same <wanix-term> machinery powers Workbench terminals and VM serial console

State & events

  • Module-level Maps: terminalSessions, workspaceTaskSessions, iframeSessions, vmSessions, workbenchSessions
  • Events: gear-shell-workspace-change (window) · gear-shell-task-status (task element)
  • Workspace JSON in localStorage; shell/wasm state in OPFS via the opfs bind
  • No window.app global — everything flows through the DI shim

URL & version discipline

  • Runtime URLs only allow @v<semver> — commit-hash / @main pins are auto-migrated to the current default by app-normalize.js
  • Every JS module import carries ?v=; a version split loads the module twice and breaks singletons
  • Legacy hush terminal profiles migrate automatically: cmd hush -rcfile /tmp/profile → bash -rcfile /profile

Deck panel

  • loadSlidesMarkdown() fetches slides.md?v=…, splits on /^\s*--\s*$/m, renders via marked.parse into reveal.js <section>s
  • reveal.js 6.0.1 + marked are global CDN scripts, passed through DI

Crush Runner pipeline & module families

How CLI agents (Crush) boot inside the browser, and how the buildless codebase is organized under the 500-line rule.

PIPE Crush Runner: detect → install → launch

Entirely on the Wanix kernel — no Worker, no wasm fetch, no WebSocket.

STEP 1 · DETECT

which crush

Runs bash script which crush (CRUSH_DETECT_SCRIPT) and polls a /tmp marker file; on failure checks ${HOME}/.w9y/crush.

STEP 2 · INSTALL

/w9y mod apply crush

Spawns /w9y mod apply crush, polls the marker directory for up to 90s; on timeout shows the install log. Runtimes: auto | gojs | wasi | js.

STEP 3 · LAUNCH

crushrc + terminal

prepareCrushLaunch() writes /tmp/crush-runner-<id>/crushrc (in-memory fs, never OPFS), forces CRUSH_GLOBAL_CONFIG, then opens a terminal panel or inline overlay.

MODULES Module families after the 500-line split

Each original giant was carved into cohesive files (<500 lines); facades keep the entry import lines stable. Internal imports carry matching ?v= versions.

app.js → 17 files

app.js stays the entry: bootstrap side effects, App component, DI wiring
app.js            // entry + DI wiring
├─ app-constants.js        // data, DEFAULT_CMD, binds
├─ app-normalize.js        // legacy config migration
├─ app-storage.js
├─ app-state.js           // dependency-free session state
├─ app-workspace.js       // + presets / store / binds
├─ app-panels.js          // + panels-store
├─ app-sessions.js        // session Maps
├─ app-terminal-*.js      // profiles + sessions
└─ app-wanix.js

crush-runner.js → 10 files

crush-runner.js is a re-export facade so app.js's import stays stable
crush-runner.js        // facade
├─ crush-deps.js          // dep registry + id counters
├─ crush-presets.js       // default profile + builtins
├─ crush-config.js        // dirs + launch prep
├─ crush-install.js      // detect + install
├─ crush-json-edit.js
├─ crush-preset-crud.js
├─ crush-panel-controller.js // ctl state/handlers
├─ crush-panel-config.js // config section JSX
└─ crush-panel.js        // component + addCrushRunnerPanel

settings.js → 11 files

settings.js is a facade; 9 panel sections
settings.js           // facade
├─ settings-deps.js
├─ settings-template.js
├─ settings-config.js
├─ settings-launcher.js
├─ settings-preset-library.js
├─ settings-workspace.js
├─ settings-system.js
├─ settings-binds.js
├─ settings-task.js
├─ settings-terminal-editor.js
├─ settings-icons.js / settings-panel.js

files-* → 16 files

Files panel: OPFS + VFS explorer
files.js             // facade
├─ files-registry.js     // DI (initFiles singleton)
├─ files-panel-*.js      // sections / hooks
├─ files-tree.js         // + path / ui / parts
├─ files-editor.js
├─ files-mounts.js       // local/remote mounts
├─ files-favorites.js    // + favorites-ui
├─ files-context-menu.js // + context-menu-ui
├─ files-info.js / files-topbar.js / files-resize.js
RULES Engineering constraints that shape the architecture
RuleWhy it existsWhere
500-line / 50-line capsForces cohesive, reviewable modulesall JS
Matching ?v= on every importES modules cache by full URL; a split loads two instances and breaks DI singletonsall importers
Runtime pins are @v<semver> onlyLegacy hash/@main URLs auto-migrate to the current defaultapp-normalize.js · app-constants.js
Buildless: importmap + CDNZero install for users, cacheable static host (vercel)index.html
Shell never touches wasm directlyAll kernel work goes through Wanix elements/tasksapp-wanix.js
ESM check with node --input-type=modulenode --check misses strict-mode errors on .js filesscripts/verify-static.mjs
Created by GearShell
×