A browser-only client for the
OmnyShell ecosystem. Connect to a Hub,
discover nodes, and manage interactive sessions — entirely from the browser, with
no custom backend. All communication is the browser talking directly to
existing OmnyShell infrastructure over a single WebSocket, using the real
omnyshell package APIs and wire protocol (no duplicated business logic).
Live app: https://omnygrid.github.io/omnyshell_web/ — installable as a PWA (see Install as an app).
Built as a plain Dart→JS web app (package:web + build_web_compilers), not
Flutter.
This repository is two things: the app above, and the package behind it. If you are building your own Dart web app, read the next section — you can embed a real remote shell without re-implementing any of this.
package:omnyshell_web exposes the reusable half of the app, so another web
project gets a live remote shell — with PTY sizing, soft-keyboard handling, line
editing, history and flow control already solved — instead of a blank canvas.
OmnyServer Web is the first consumer:
an OmnyServer Hub can host an OmnyShell broker on the same port, so its fleet
dashboard opens a shell on any node with one button.
dependencies:
omnyshell_web: ^1.15.0The browser assets are not code, and pub does not serve a dependency's web/
directory to your app, so install them once:
dart run omnyshell_web:copy_assets # xterm bundle + kit.css + terminal.css + boot.js → ./webThen wire the terminal — this is the whole thing:
import 'package:omnyshell_web/client.dart'; // OmnyShellService, AppError, …
import 'package:omnyshell_web/terminal.dart'; // the terminal stack
import 'package:omnyshell_web/ui_kit.dart'; // dom helpers, widgets, toasts
final service = OmnyShellService();
await service.connect(hubUri: 'hub.example.com', principal: 'alice', token: token);
final term = XtermTerminalView(hostElement);
final session = await service.openShell(nodeId: 'worker-01', cols: 80, rows: 24);
final shell = WebShellHost(
term: term,
session: session,
principal: 'alice',
nodeId: 'worker-01',
commands: null, // a plain remote shell — no `:ai`, no `:ide`
);
mount(accessorySlot, TerminalAccessoryBar(
keys: shell,
term: term,
clipboardRead: defaultClipboardRead,
clipboardWrite: defaultClipboardWrite,
onToast: toasts.show,
).element);
TerminalFitter(
term: term,
host: hostElement,
accessory: accessorySlot,
scaleTarget: screenRoot,
).attach();Notes worth knowing before you start:
- The AI agent and the IDE are opt-in.
WebShellHostnever imports them; passingcommands: null(orLocalCommandRegistry.withDefaults()) gives a plain shell.command_shieldis pulled in by the:idefactory alone. - Give your app its own storage prefix.
localStorageis per-origin and one Hub can serve two apps, soSettingsStore(kv, prefix: 'myapp.')keeps your theme, Hub and token from clobbering OmnyShell Web's. TerminalFitteris the part you do not want to rewrite. Sizing a terminal against a soft keyboard, a fullscreen toggle and a rotating phone is where the bodies are buried.- Style the widgets. The UI kit is styled by class names in
kit.css; ship it (the copy tool does) or the widgets render unstyled.
The public surface is three barrels — terminal.dart, ui_kit.dart and
client.dart. Everything under app/, ui/screens/ and ui/settings_panel.dart
is this app's own and is deliberately not exported; see
ARCHITECTURE.md.
- Authentication — bearer-token login, logout, session persistence, and automatic restore on reload.
- Nodes — list, view details (platform, capabilities, labels, online status), refresh, with cache-backed instant paint.
- Sessions — list active/detached sessions, view status, peek a screen snapshot, detach, resume, and kill. Open a fresh interactive shell.
- Interactive terminal — a real xterm.js terminal wired to the remote session (stdin/stdout/stderr, resize, flow control), with selectable terminal dimensions and adjustable text/key sizing.
- AI agent (
:ai) — drive the node in natural language from the terminal. Configure a provider/model and API key in Settings, or use the Hub's default provider (the key stays on the Hub). Commands run in the live session, gated by the samecommand_shieldsafety checks as the CLI, and each interaction ends with a token-usage stats line (tokens, tok/s, requests, duration). - Terminal IDE (
:ide, alias:edit) — open the full-screen TUI IDE on the connected node, right inside the browser terminal: a file-tree sidebar with git status, tabbed editing with syntax highlighting and a git-change gutter, an integrated terminal, and an AI agent panel (gated by the samecommand_shieldas:ai).:ide [path]opens a directory;Ctrl-Qreturns to the shell. The IDE operates on the remote node over the connected session. - Theming — light, dark, and system themes with a persistent selector and no flash of the wrong palette on load.
- Resilience — explicit loading/empty/error states everywhere, dropped- connection detection, and one-click reconnect.
These map to the omnyshell CLI's client commands: login/logout,
nodes list, connect, sessions list/peek/resume/detach/kill, and the
in-session :ai agent and :ide terminal IDE.
This app depends on the published omnyshell package (from pub.dev), which
includes a browser-compatible transport (see
ARCHITECTURE.md → omnyshell browser seam). To develop
against a local checkout, drop a git-ignored pubspec_overrides.yaml with
dependency_overrides: { omnyshell: { path: ../omnyshell } }.
dart pub get
# Dev server with hot rebuild:
dart pub global activate webdev
webdev serve # → http://localhost:8080
# Production build:
webdev build # → build/You'll need a running Hub and at least one Node to connect to. From the
omnyshell repo:
cd ../omnyshell
./run-hub.sh # starts a Hub and prints a grant token
dart run bin/omnyshell.dart node start --id web-01 # register a nodeThen open the web app, enter the Hub address (e.g. localhost:8443), your
principal, and the grant token.
The browser owns the TLS stack, so there is no in-app certificate bypass. To use a self-signed/dev Hub, trust its certificate at the OS/browser level first (or front the Hub with a publicly-trusted certificate). The login screen surfaces this guidance instead of a fake "insecure" toggle.
The app is live at https://omnygrid.github.io/omnyshell_web/.
OmnyShell Web is an installable Progressive Web App on Android and iOS — it adds a home-screen icon and launches standalone (no browser chrome), with the app shell cached by a service worker so it loads instantly and survives flaky networks.
- Android (Chrome/Edge): open https://omnygrid.github.io/omnyshell_web/ → menu → Install app / Add to Home screen.
- iOS/iPadOS (Safari): open https://omnygrid.github.io/omnyshell_web/ → Share → Add to Home Screen.
Installability requires the app to be served over HTTPS (or localhost for
testing). The status bar and theme colour follow the in-app light/dark theme,
and the layout respects device safe areas (notch / home indicator).
Icons are generated from a single source:
dart run tool/generate_icons.dart # regenerates web/icons/*.pngdart analyze
dart test # unit + integration (VM, fast)
dart test -p chrome test/ui test/e2e # widget/DOM + end-to-end (headless Chrome)- Unit — storage, state controllers, routing, error mapping, time formatting (pure, VM).
- Integration —
OmnyShellServicedriven through a fake Hub connection (test/support/fake_hub.dart) that speaks the real protocol, exercising the actualClientRuntime. - Widget/DOM — each screen and component rendered into the DOM and asserted
(
test/ui). - End-to-end — full user journeys over the fake Hub (
test/e2e): login → nodes → detail → sessions → peek → logout, session persistence across reload, and reconnection after a dropped connection.
- The bearer token, when "remember" is checked, is stored in
localStorageand is therefore reachable by any script on the page. Leave it unchecked to keep the token in memory only. Serve the app with a strict Content-Security-Policy in production. - No credentials are sent to anyone but the configured Hub.
See ARCHITECTURE.md for the design in depth.