Pi with a face. A native desktop shell where the agent decides what you see.
Status legend: [x] done · [~] partial / in progress · [ ] not started.
Last reviewed: 2026-06-25 (full doc-sync pass at v0.10.2: README / website / docs/aethon-agent / CLAUDE.md / AGENTS.md / RELEASING.md brought back into sync with the shipped surface; the release pipeline has narrowed to macOS Apple Silicon only via release-please — see RELEASING.md; M8 — Workspaces, MCP, Subagents & Scheduling added below to record the features shipped since M7). Previous review: 2026-05-28 (doc-sync pass: M7 — Voice Input & Multi-Account Auth added below to record shipped voice-to-text dictation, multi-account auth profiles, and the env.rs command-PATH resolver; README / website / docs/aethon-agent / CLAUDE.md / AGENTS.md brought into sync). Previous review: 2026-05-24 (workstation/project polish: git worktrees are first-class sidebar/project-dashboard entries, issue "Send to agent" launches fresh workspaces with metadata-aware branch names, frontend code HMR reloads the app shell after persisting state, and package.json is the app-version source of truth mirrored into Tauri/Cargo by scripts/sync-version.mjs). Previous review: 2026-04-29 (forward-looking audit pass: M1–M5 complete; M6 — Interactive Terminals & Workspace Polish added below to scope real PTY-backed user shell tabs, an opt-in agent-sharing model, hotkey expansion, a graphical settings panel, drag-and-drop into the composer, OS notifications, bridge crash recovery, and a handful of polish items uncovered by the audit). Recent additions: native A2UI canvas windows for separate restorable OS-window workpads, viewport-compensated UI zoom (--app-ui-scale + measured viewport tokens), project/git status badges in sidebar and palette, Cmd/Ctrl+K clear-chat and Cmd/Ctrl+. stop-prompt wiring, project-local .aethon/extensions/ discovery from cwd to git root, in-app /extensions install <npm-package|git-url> extension-package installation, layout-slot contract (slots.json + canonical area names + slotMap), generic extension_lifecycle feedback channel, extension-deletion state pruning via extensionStateKeys, cargo + vitest unit-test scaffolding, ESLint with react-hooks rules wired into check, a Nix distribution package + overlay, tag-driven GitHub release publishing for v0.2.0, and Windows x64 NSIS release bundles.
Aethon is a cross-platform desktop application that embeds the pi coding agent and renders its output as rich, interactive UI via the A2UI protocol. Instead of a fixed IDE layout, the interface is a canvas the agent populates dynamically — extensions bring their own UI components, themes control the look, and the agent decides the layout based on what you're doing.
The name comes from Greek mythology: Αἴθων, one of the horses that pulled Helios's sun chariot. The blazing one that shapes what you see.
| Decision | Choice | Rationale |
|---|---|---|
| Agent runtime | Pi SDK (embedded) | Pi runs in a bun subprocess bridge (JSON-lines over stdio); createAgentSession() lives in the bridge, not the Rust shell |
| Primary language | TypeScript (agent + UI) / Rust (OS shim) | Native pi integration, single language for extensions |
| Desktop framework | Tauri 2 | Native binary, ~5MB shell, system webview |
| UI protocol | A2UI v0.9 (full spec) | Agent-generated declarative UI, framework-agnostic |
| LLM providers | Multi-provider via pi-ai | Anthropic, OpenAI, Google, any OpenAI-compatible endpoint. BYOK. |
| Agent model | Opinionated default layout + full canvas flexibility | Ships with a Claudette-style layout as the default, but the layout itself is A2UI — users and extensions can replace or extend it |
| Packaging | Compiled pi binary (bun build --compile) + Tauri shell in single .app | No runtime dependencies for end users |
| License | MIT | Open source under utensils org |
| Relation to Claudette | None | Independent project, no shared code |
┌─────────────────────────────────────────────────────┐
│ Aethon (.app) │
│ │
│ ┌───────────────┐ stdio/socket ┌───────────┐ │
│ │ Tauri Shell │◄──────────────────►│ Pi Agent │ │
│ │ (Rust) │ │ (compiled │ │
│ │ │ │ Bun bin) │ │
│ │ - Window mgmt │ A2UI JSON │ │ │
│ │ - File access │◄──────────────────│ - pi-ai │ │
│ │ - System tray │ │ - tools │ │
│ │ - Menus │ Events/cmds │ - extensions │ │
│ │ │──────────────────►│ - exts │ │
│ └───────┬───────┘ └───────────┘ │
│ │ │
│ │ Tauri IPC │
│ ▼ │
│ ┌─────────────────────────────────────────────┐ │
│ │ React Frontend │ │
│ │ │ │
│ │ ┌──────────────┐ ┌──────────────────────┐ │ │
│ │ │ A2UI Renderer │ │ Host Shell │ │ │
│ │ │ │ │ - Boot/hydration │ │ │
│ │ │ Renders │ │ - Menus/shortcuts │ │ │
│ │ │ agent + │ │ - Persistence │ │ │
│ │ │ extension UI │ │ - Root overlays │ │ │
│ │ │ │ │ │ │ │
│ │ └──────────────┘ └──────────────────────┘ │ │
│ └─────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
Tauri Shell (Rust) — Owns the OS boundary. Window management, filesystem access, system tray, native menus, notifications, auto-updater. No business logic. No agent awareness beyond spawning + I/O. Exposes Tauri commands for filesystem, clipboard, shell execution.
Pi Agent (Compiled Bun Binary) — The brain. Runs as a compiled standalone binary, communicates with the Tauri shell via stdio or local socket. Handles all LLM interaction, tool execution, session management, extension loading. Emits A2UI JSON payloads describing what the UI should show. Receives user input events (chat messages, button clicks, form submissions) from the frontend.
React Frontend — The face. Two parts:
- A2UI Renderer consumes the agent's A2UI JSON stream, maps component types to React components, handles data binding and event dispatch. This is the dynamic canvas.
- Host shell owns bootstrapping, persistence, native menu/shortcut
dispatch, and root overlays. The visible workspace chrome (tabs, sidebar,
canvas, composer, terminal, status, palette, notifications) is provided by
the
default-layoutA2UI extension and can be replaced or extended.
The default workspace is itself an A2UI payload, shipped by the
default-layout extension. Loading it goes through the same renderer that
handles agent-emitted UI, so users (and other extensions) can replace, modify,
or extend it without touching React. The extension currently ships one layout:
workstation (boot default). Earlier sibling variations (live-layout,
editorial, command-deck) were trimmed while polish focuses on a single
surface; extensions can introduce more via aethon.registerLayout.
┌──────────┬────────────────────────────────────┐
│ Sidebar │ Header / tabs / layout controls │
│ ├────────────────────────────────────┤
│ Projects │ Canvas │
│ Models │ - chat history │
│ Themes │ - agent-emitted A2UI components │
│ Layouts │ - empty state / recent sessions │
│ Exts ├────────────────────────────────────┤
│ │ Terminal (optional) │
│ ├────────────────────────────────────┤
│ │ Composer + slash picker │
└──────────┴────────────────────────────────────┘
Status
The canonical boot payload and slot catalogue are bundled from
src/extensions/default-layout/*.a2ui.json and slots.json (or release
resources). Users switch layouts via /layout, the appearance menu, or
window.aethon.activateLayout(id). Extensions can register layouts at runtime
with aethon.registerLayout({ id, name, payload }) or replace/patch the active
layout with setLayout / patchLayout. Layout state is live runtime state,
not a persisted ~/.aethon/layouts/default.a2ui.json file.
- Tauri 2 project scaffold (
src-tauri/, identifiercom.utensils.aethon) - Pi agent bridge spawned as a
bun run agent/main.tssubprocess from the Rust shell - JSON-lines stdio protocol (Tauri ↔ pi agent)
- Chat input/output text round-trip
- Streaming text deltas in the UI (response feels live, not blocking)
- Real model name surfaced in the status bar (replaces hardcoded label)
- Model picker (sidebar) — switch model at runtime via
session.setModel() - Errors from the agent surface as visible chat messages, not silent hangs
- Stop button — chat input swaps Send → Stop while
state.waitingis true; Stop callssession.abort()via the bridgestopcommand. Bridge does not awaitsession.prompt()so subsequent stdin messages (the stop) aren't queued behind it; verified killing in-flight bash within ~3s. - Hot reload of the agent — Rust watches
agent/(dev only) plus~/.aethon/extensions/,~/.aethon/extensions/node_modules/, and~/.pi/agent/extensions/(dev AND release). File changes request a graceful bridge reload: active prompts drain,_reload_doneis emitted, and the next IPC call respawns the child. Trailing-edge debounce via a single mpsc worker thread collapses install bursts. See M3 "Extension hot-reload" for the full description. - Theme system — three ship-ready palettes (
emberwarm dark default,papercream light,aetherink-blue signature) behinddata-themeon<html>, switcher in sidebar, persisted to~/.aethon/theme(legacylocalStoragemigrates on first read). Legacy ids map assignature→aether,dark→ember,light→paper. Boots from disk →config.toml[ui] theme→ OSprefers-color-scheme(paperfor light,emberfor dark) →ember. Extensions and loose JSON themes register additional palettes viaaethon.registerTheme. - Theme registry —
aethon.registerTheme({id, label?, vars})ships extension-supplied CSS custom-property maps; full details in M3 below. Demo atexamples/pi-extensions/aethon-theme.ts. - Compiled
aethon-agentbinary —src-tauri/build.rsinvokesbun build --compilefor the active Cargo target with mtime-gated rebuilds. Bundled via TauriexternalBin. Full details in M4 below. - Filter model picker to user's
enabledModelspatterns from~/.pi/agent/settings.json(compiled glob patterns; falls back to authed models if none configured)
- A2UI React renderer with built-in component set
- Primitives:
text,heading,paragraph,code,card,container,divider,button,text-input,date-picker,select,checkbox,slider,table,list,image,icon,form,form-field - Extension components (default-layout):
layout,sidebar,chat-history,chat-input,status-bar,tab-strip,terminal,main-canvas,empty-state,command-palette,notification-stack, plus layout-variation composites (command-bar,vertical-tab-rail,inspector-pane,model-picker,appearance-menu, etc.)
- Primitives:
- Data binding via JSON Pointer (
{"$ref": "/path"}) —DynamicString/Number/Boolean - Event dispatch (button clicks, form submissions → agent via Tauri IPC)
- Optimistic state updates for
change/submitevents on$ref-bound inputs - Agent emits A2UI payloads — tool execution surfaces as
cardcomponents with summarized args + result - Tool execution surfaced as A2UI cards (read/bash/edit/write/grep/find/ls events → visible UI). Cards are emitted with a stable
tool-<callId>message id, so the "running…" state updates in place to the final result instead of duplicating bubbles. - Image content from tool results renders in the card via the
imageprimitive (data URLs, capped at 4 per result). Persisted history strips the base64 to avoid blowing the localStorage quota. - Streaming text bubbles survive intervening tool cards — bridge stamps each text delta with a stable
messageId(piAssistantMessage.timestamp) so post-tool deltas land in the original bubble instead of a new one - Streaming progressive component renders — text deltas amend by stable messageId, tool cards replace by stable id, and state-driven live A2UI subtrees can be seeded at
/canvasthen patched mid-turn through array-preserving JSON Pointer writes (e.g./canvas/components/0/props/title) without corruptingcomponents/childrenarrays.
- Extension registry primitive (
ExtensionRegistry, exposed via React context) - Default-layout shipped as a registered extension (eats its own dog food)
- Runtime API on
window.aethon(frontend-only, in addition to the bridge-sideglobalThis.aethon):setLayout,resetLayout,getLayout,registerExtension,listExtensions,newTab,closeTab(tabId),switchTab(tabId),listTabs. Used by the aethon-debug skill, the system menu / tray, and any in-webview script. Do not confuse with the bridge-side surface inagent/main.ts— that one is for extensions and addssetState,patchLayout,registerComponent,registerTheme,onEvent,windows,getRuntimeSnapshot, etc. - Pi extensions reach the Aethon UI surface via
globalThis.aethon(set beforecreateAgentSessionso pi's loader sees it). SameregisterComponent/setState/onEventAPI as Aethon-side extensions, and the global is absent outside Aethon so pi-TUI extensions stay functional. Examples + types underexamples/pi-extensions/. - Extension-registered components are interactive —
aethon.onEvent({templateRootType, componentType, descendantId, eventType, surfaceId, windowId}, handler)runs handlers when an A2UI control inside an extension template fires an event. Handlers can callsetState/registerComponentto drive UI without an LLM round-trip; events from native canvas windows also receivectx.windowhelpers scoped to that OS window. Renderer threadstemplateRootTypethrough descendant dispatches and the bridge extractsdescendantIdfrom the host-prefixed componentId. Demo atexamples/pi-extensions/aethon-counter.ts. - Aethon-side extensions via
~/.aethon/extensions/*.{ts,js}exportingregister(api)(same API surface as pi-side).loadAethonExtensionsin the bridge discovers + dynamic-imports them at boot; missing dir is the no-op default. Bridge retains state as a tree and replays onready; frontend hydrates templates into the ExtensionRegistry and the renderer expands them inline with host-prefixed ids. - Project-local Aethon extensions via
<project>/.aethon/extensions/*.{ts,js,mjs}— when a tab opens or the active project cwd changes, the bridge walks from that cwd up to the nearest git root, loads each existing.aethon/extensionsdirectory root-first, dedupes files for the bridge process, refreshes the resource loader, and marks entries asproject-directoryinlistExtensions()/RuntimeSnapshot.extensions. This mirrors pi's project-local extension ergonomics while keeping Aethon's UI API surface identical. - Extensions can mutate the entire UI:
aethon.setLayout(payload)replaces the active layout wholesale,aethon.patchLayout(path, value)JSON-Pointer patches it (array-preserving),aethon.registerSidebarSection({id,title,items})is a convenience wrapper that appends into the sidebar'sextraSections. Bridge retains both the layout and pending pre-setLayout patches for ready/report replay. Frontend treats layout state as boot defaults so live runtime fields (model, status, messages, draft) survive reload. Demo atexamples/pi-extensions/aethon-sidebar-panel.ts. -
ctx.pinamespace for handlers —aethon.onEventhandlers receive a typed pi-coding-agent surface scoped for UI work:ctx.pi.prompt(text)fires an LLM turn from a click (frontend flips waiting/Stop via aprompt_startedoutbound message, just like a user-typed prompt),ctx.pi.notify(message)pushes a non-terminal system bubble,ctx.pi.sessionexposes current model + last 50 messages read-only,ctx.pi.signalis pi's active turn AbortSignal so handler-side fetch/spawn cancels with Stop. The dispatch loop is fire-and-forget so handler awaits never block bridge IPC; handler errors emit asnoticeso they can't clobber waiting state for an in-flight prompt. Demo atexamples/pi-extensions/aethon-actions.ts(Quick Actions sidebar: Summarize commits, Explain README, Show current model). - Extensions can register color themes:
aethon.registerTheme({id, label?, vars})ships a CSS custom-property map (--bg,--text,--accent, …). Bridge sanitizes the id/keys, retains the theme map, emitsextension_themesdeltas, and includes the snapshot inreadyfor reload-replay. Frontend hydrates each theme into a<style>tag built via CSSOMsetPropertyso malformed values can't escape the declaration; stale tags are dropped when the list shrinks. Built-in idsember,paper,aether, and legacysignatureare reserved. Themes appear in the sidebar Themes section alongside the built-in palettes and persist to~/.aethon/theme. Demo atexamples/pi-extensions/aethon-theme.ts(Solarized Dark + Synthwave). - Agent self-awareness —
agent/system-prompt.tscomposes a layered prompt from the static template inagent/system-prompt/prompt-template.tsand theRuntimeSnapshotcontract inagent/system-prompt/types.ts: static Aethon base (API surface, A2UI primitives, anti-patterns, env-var contract) → optional~/.aethon/system-prompt.mdoverride /~/.aethon/system-prompt-append.mdappend → runtime snapshot (loaded extensions, registered themes, custom components, layout summary, open tabs). Snapshot rebuilds everyresourceLoader.reload(), and the bootstrap loads extensions BEFORE the default tab so the first session's prompt sees them. Bundled docs ship at$AETHON_DOCS_DIR(docs/aethon-agent/{api,components,extensions}.md) so the agent has authoritative reference material in any build. Live state mirrors to$AETHON_STATE_FILE(~/.aethon/state.json) —cat-able from the agent's bash tool, debounced 200 ms, regenerated on every register* call. Introspection methods onglobalThis.aethon(listExtensions,listComponents,listThemes,getLayout,getRuntimeSnapshot) cover the same surface for in-process queries. Pi extensions in~/.pi/agent/extensions/that touchglobalThis.aethonare discovered (grep-based, no execution) and listed alongside Aethon-direct ones so the snapshot covers all UI-driving sources. - Extension package manifest from
package.json#aethon—loadAethonExtensionPackagesin the bridge walks~/.aethon/extensions/node_modules/*(plus@scope/*), reads each package.json, and for any package with anaethon.entryfield dynamically imports the entry and calls itsregister(api)export with the same Aethon API surface directory extensions get. Lets usersnpm install --prefix ~/.aethon/extensions <pkg>to install third-party extension packages. Demo atexamples/extension-package/. - In-app extension install —
/extensions install <npm-package|git-url>runs the equivalent ofnpm install --prefix ~/.aethon/extensions <spec>from the Tauri shell (not the agent sidecar, so thenode_moduleswatcher cannot kill the installer mid-flight). The command accepts npm package specs, tarballs, GitHub shorthands, and git URLs, rejects shell-like option/whitespace input, uses the same macOS login-shell PATH recovery as the agent, and terminates the current sidecar after success so the next request loads the new extension package. - React-component extensions —
aethon.frontendEntryin an extension package'spackage.jsonpoints at a JS file whose body usesReact+extension.registerComponent(type, ReactComponent)to register real React components in the ExtensionRegistry. Bridge reads the file at boot, ships it to the webview as a string inextension_frontend_modules, frontend evaluates withnew Function("React", "extension", code)and reconciles wholesale (re-evaluating drops/replaces components from removed/changed packages). Errors per module are caught + surfaced as a system notice.RuntimeSnapshot.frontendModulescarries{name, entryPath, bytes}; code body lives on disk. Same trust model as bridge-side extension code (no sandbox; user installed the package). Demo:examples/extension-package/src/frontend.jsships apulse-cardcomponent usingReact.useEffect. Loader covered undersrc/extensions/extensionFrontendLoader.tsandsrc/hooks/bridgeMessageHandlers/extensionFrontendModules.test.ts. - Native A2UI canvas windows —
globalThis.aethon.windows.openCanvas({ id?, title?, components?, state?, width?, height?, x?, y?, focus?, restoreOnLaunch? })creates a normal native OS window atindex.html?surface=canvas-window&id=<id>using labelaethon-canvas-<id>. V1 supportskind: "canvas"records only, persists restorable open windows to~/.aethon/native-windows.json, reuses window geometry persistence for dynamic labels, and removes records on close. The lightweight canvas-window React root hydrates the record from Rust, rendersA2UIRendererin bare controlled mode, persists debounced local state, and replays extension templates,aethon.frontendEntryReact modules, themes, and highlight grammars without booting the main bridge hook. Bridge-side tools mirror the namespace (openA2uiCanvasWindow,emitA2uiWindowCanvas,setA2uiWindowState, etc.),RuntimeSnapshot.nativeWindowslists summaries, and the prompt now prefers native canvas windows for exploratory/custom UI so agents don't modify main chrome unless asked. - Extension hot-reload — bridge file watcher now runs in dev AND release, watches
~/.aethon/extensions/,~/.aethon/extensions/node_modules/, and~/.pi/agent/extensions/(when present), plus<project>/agent/in dev only. Trailing-edge debounce via a single dedicated worker thread (mpsc channel +recv_timeout) collapses npm-install bursts into one settled reload request.~/.aethon/extensionsis pre-created on boot so first-install Create events fire without a manual restart.
- Auto-updater wired via
tauri-plugin-updater— Cargo + bun deps, capabilities, plugin registration (gated on a non-emptyplugins.updater.pubkeyintauri.conf.jsonso unconfigured builds boot safely),updater_availableTauri command, and a manual "Check for Updates…" menu item (Aethon submenu on macOS, View on Linux/Windows) plus a tray entry. Reads thelatest.jsonmanifest from the GitHub Releases endpoint, downloads with progress as system messages, and relaunches viatauri-plugin-process. Release signing is activated: the updater public key is committed intauri.conf.json, the private key and passphrase are stored locally under~/.tauri/, and the matchingTAURI_SIGNING_PRIVATE_KEY/TAURI_SIGNING_PRIVATE_KEY_PASSWORDrepository secrets are configured so CI can sign updater bundles automatically. - System tray (status-bar) icon —
TrayIconBuilderreuses the bundled brand mark in full color (template image strips the orange so it's intentionally off). Left-click focuses the main window (callsAppHandle::show()on macOS so Cmd+H'd apps re-surface), and a small menu offers Show Aethon / New Tab (focuses + emits the same "menu" event the app menu /Cmd/Ctrl+Tfire) / Quit. Same code runs on Linux/Windows; menu pops on left-click on those platforms per their convention. - Native menus — full bar built with
tauri::menu::MenuBuilderreplaces Tauri's auto-generated default. Standard NS items (Quit, Hide, Cut/Copy/Paste, Minimize, …) come fromPredefinedMenuItemso they get free native behavior; app-specific items emit amenuTauri event whose payload routes into the same React dispatcherCmd/Ctrl+T,Cmd/Ctrl+],Cmd/Ctrl+[,Cmd/Ctrl+W,Cmd/Ctrl+\``,Cmd/Ctrl+K, andCmd/Ctrl+.use, so menu and shortcuts can never drift. Layout: Aethon (macOS) / File (New Tab, Close Tab) / Edit / View (Toggle Terminal, Clear Chat, Stop Prompt, Check for Updates… on non-macOS) / Tabs / Help (Documentation, Report Issue) / Window.Cmd+Wis reserved forclose_tab; the predefinedclose_windowis omitted so macOS doesn't route it to the wrong action. Help submenu opens external URLs viatauri-plugin-opener`. - Cross-platform release builds —
cargo tauri buildproduces self-contained macOS DMGs/updater tarballs, Linux.deb/.rpminstallers, and a Windows x64 NSIS setup executable from the GitHub release matrix. Theaethon-agentsidecar is compiled bysrc-tauri/build.rsviabun build --compile, bundled via Tauri'sexternalBin, and spawned in release withPI_PACKAGE_DIRpointing to a shippedpi/package.jsonresource. mtime-gated so incremental builds no-op when sources haven't changed; cross-target builds honorcargo --target/CARGO_BUILD_TARGET/TAURI_ENV_TARGET_TRIPLEacross macOS, Linux, and Windows. macOS-specific: the spawn path also runs<shell> -ilc envonce on first agent launch to recover the user's login-shell PATH (Homebrew, Nix profile,~/.npm-global/bin, …) because launchd-spawned.appprocesses inherit a minimal PATH that breaks pi'snpm root -gand similar package-resolver calls; result is cached for the process lifetime. Bundled resources includepi/package.json,docs/aethon-agent/*.md, andextensions/default-layout/*.a2ui.jsonplusslots.json. - Brand mark —
assets/brand/aethon-logo.svg(cream Bodoni Æ + orange π badge on dark tile) andaethon-brand-marks.svg(6-format reference sheet). Rasterized into every Tauri target viabun tauri icon. In-app header shows the logo alongside "Aethon" via Vite?urlimport +$ref-bound state. - macOS About dialog metadata —
bundle.{publisher,homepage,copyright,category,shortDescription,longDescription}+bundle.macOS.minimumSystemVersionpopulateInfo.plist(NSHumanReadableCopyright,LSApplicationCategoryType, etc.). Shows the proper icon + version + "Copyright © 2026 James Brink. MIT License." attribution. Dev binary still shows the generic icon (it's a raw Mach-O without a.appwrapper). - Nix flake overlay for distribution —
flake.nixnow exportspackages.aethon,packages.default, andoverlays.default(pkgs.aethon). The package follows nixpkgs' Tauri packaging path withcargo-tauri.hook, the pinned Rust 1.92 toolchain,fetchNpmDepsbacked bypackage-lock.json, Linux WebKitGTK build inputs, and a macOS$out/bin/aethonwrapper around the generated.app. Nix builds disable updater artifact generation inside the build copy so distribution packages don't require release signing secrets. - First public release —
CHANGELOG.mdcarries dated release sections (0.1.0onward). The early0.1.0/0.2.0releases shipped a multi-platform matrix, but the pipeline has since moved to release-please (.github/workflows/release-please.yml, triggered on merge tomainrather than av*tag) and currently builds macOS Apple Silicon only —app+dmg+ updaterlatest.json, signed viaTAURI_SIGNING_PRIVATE_KEY. The release PR is authored with theaethon-release-botGitHub App token so CI fires on it. Theaethoncrate still publishes to crates.io viaCARGO_REGISTRY_TOKEN.cargo tauri builditself can still produce Linux.deb/.rpmand Windows NSIS bundles locally; they are simply not in the release matrix today. SeeRELEASING.md. - Version source-of-truth guard —
package.jsonowns the app version shown in the sidebar via direct JSON import, andscripts/sync-version.mjsmirrors it intopackage-lock.json,tauri.conf.json,Cargo.toml, andCargo.lock. CI/release runbun run version:check.
Aethon's stated vision is "the agent decides what you see — A2UI is the entire UI, no fixed IDE chrome." Today the agent can register components, themes, sidebar sections, layouts, and event handlers, but several hardcoded behaviors in the frontend and gaps in the bridge → frontend contract still prevent full agent control. M5 is the work to close those gaps so any UI surface can be inspected, mutated, and overridden by an extension without touching React source.
-
getLayout()returns the active rendered layout — bridge now preloads the canonical boot layout SYNCHRONOUSLY from$AETHON_BOOT_LAYOUT_FILE(set by the Tauri shell, pointing atsrc/extensions/default-layout/layout.a2ui.jsonin dev or the bundled resource in release) BEFORE any extension'sregister(api)runs._getLayout()returnsextensionLayout ?? (bootLayout + pendingLayoutPatches folded), and aboot_layoutinbound message lets the frontend refresh the value when the active layout extension changes. Verified end-to-end:right-sidebar-model-pickerextension's prior bailout (if (!layout) return) now succeeds and applies its 5 patches; layoutSummary correctly reportssidebar=rightafter register-time. Bundled boot layout added totauri.conf.jsonbundle.resourcesso release builds get it too. - Bridge-readable frontend state — frontend pushes
frontend_state_patch { path, value }whenever an allowlisted slice changes (/sidebar/models,/sidebar/themes,/connection,/status,/tabs,/draft,/messagesCount). Bridge stores infrontendStateMap;aethon.getFrontendState(path?)returns the live value;getRuntimeSnapshot().uiStateincludes the full mirror so the system prompt's runtime section +~/.aethon/state.jsonreflect what's actually on screen. Frame-debounced (≈16 ms) + diff-on-frontend coalesces a flurry of state changes — typing into the composer is one patch after the user pauses, not one per keystroke. Best-effort mirror, no ack. - Mutation feedback channel — every mutating outbound message (
state_patch,layout_set,layout_patch,extension_components,extension_themes) carries amutationId. Frontend acks viamutation_ack { mutationId, success, error? }. Bridge resolves a per-mutation Promise so the API now returnsPromise<{ok: boolean, error?: string}>. Sync use is unchanged (Promises just GC if not awaited). Pre-frontend-ready calls resolve immediately with{ok: true}(covered by retained-state replay). 5-second timeout guards stuck Promises.registerThemevalidation failures emit anotice(noterror) so they don't clobber waiting state, and the Promise carries the same detail. - Boot-layout structural snapshot in
RuntimeSnapshot—RuntimeSnapshot.layoutStructureships{rootId, rootType, columns?, rows?, areas?, children: [{id, type, area?}]}extracted from the active layout (extension-set or boot+patches). Agent answers "what's in the layout?" without paying for the fullgetLayout()round-trip; the system prompt's runtime section emits a one-liner showing the root's children + areas.
- Array-iteration template primitive (
for-each) —{type: "for-each", props: {items: {$ref: "/some/array"}, key?: "<field>"}, children: [<template>]}. Renderer expandschildrenonce per array element, suffixing every nested id with__$idx<n>so React keys stay stable. Auto-rerenders on array mutation. -
$refresolution scope insidefor-each— iteration injects three special keys into the scoped state map:/$item(current element),/$index(position),/$parent(surrounding state). Standard JSON Pointer resolution finds them — no resolver changes needed. Templates address values via{ $ref: "/$item/label" }. Documented in bundled docs (components.md) and the system prompt's iteration section.
- Registerable keyboard shortcuts —
aethon.registerKeybinding({combo, action?, description?})andaethon.unregisterKeybinding(combo)are wired. Combos accept any human-readable form (Cmd/Meta/Ctrl/Alt/Option/Shift); bridge and frontend normalize to a canonical key for matching/unregister. Invocations dispatch througha2ui_eventas{componentType: "keybinding", componentId: "keybinding__tpl__<combo>", data: {action, combo}}so a pairedaethon.onEventhandler fires. Registered bindings run before built-ins, so extensions can intentionally override default chrome shortcuts (Cmd/Ctrl+P,Cmd/Ctrl+Shift+P,Cmd/Ctrl+T,Cmd/Ctrl+W,Cmd/Ctrl+],Cmd/Ctrl+[,Cmd/Ctrl+\``,Cmd/Ctrl+K,Cmd/Ctrl+., plus UI zoom); unregistering restores the built-in behavior. Surfaced inRuntimeSnapshot.keybindings`. - Registerable slash commands —
aethon.registerSlashCommand({name, description, usage?})records metadata; pair withaethon.onEvent({componentType: "slash-command", descendantId: "<name>"}, handler)to wire the action. Frontend merges with built-ins for the picker, dispatches invocations through the existinga2ui_eventroute as{componentType: "slash-command", componentId: "slash-command__tpl__<name>", data: {args}}so per-tab attribution + handler dedup work the same as any other event. Built-in collisions (clear,help,theme,model,reset,terminal,sidebar,layout,extensions,project) are rejected with a notice. Replayed onreadyso reload restores the picker; surfaced inRuntimeSnapshot.slashCommands. - Registerable menu items —
aethon.registerMenuItem({label, action, location?, id?, parent?})andaethon.unregisterMenuItem(id)are wired. Bridge shipsextension_menu_itemsevents to the frontend, which forwards to aset_extension_menu_itemsTauri command that rebuilds both the App menu (extension entries appear under an "Extensions" submenu) and the tray menu. Click events emitmenuevents with idext:<action>; the React dispatcher routes them viaa2ui_eventso a pairedaethon.onEvent({componentType: "menu-item", descendantId: "<action>"}, handler)matcher fires. Replayed onreadyso reload restores the menu. Surfaced inRuntimeSnapshot.menuItems. - Compositional sidebar items — each
SidebarItemcan carrycomponentType. When set, the sidebar resolves it through the ExtensionRegistry and renders the registered template per item with/$item,/$index,/$parentscope keys (same convention as thefor-eachprimitive). NewBuiltinComponentProps.renderChildWithState(child, overlay)helper exposes the renderer's scoped expansion to composites. Click semantics unchanged (selectevent with{sectionId, itemId}+ descendantId). Documented in bundled docs (components.md). - Compositional terminal subscription — bash output now lands in three places: (1)
Tab.terminalBufferper-tab record (existing; tab-switch replay), (2)/terminal/buffer/<tabId>state path bindable via$reffrom any A2UI component, (3)aethon:terminal-tapwindow event withdetail = {tabId, content}firing for every chunk regardless of active tab — multi-subscriber friendly, no monkey-patching. The originalaethon:terminalevent is preserved for the active-tab xterm pump. Documented in bundledcomponents.md. - Pluggable
onEventrouting — extensions can intercept or replace built-in events.aethon.registerEventRoute({componentId?, eventType?})andunregisterEventRoute(...)intercept matching App-dispatched events so they flow througha2ui_eventto pairedaethon.onEvent({componentType, descendantId})handlers. Wildcards via omitted fields (componentId-only or eventType-only).aethon.listEventRoutes()returns the registered intercepts.aethon.setEventRoutingMode("extension")now switches to full replacement mode: every layout event bypasses the App built-in switch and goes to the bridge;setEventRoutingMode("builtin")restores default built-ins for unmatched events. Surfaced inRuntimeSnapshot.eventRoutingMode. - Built-in
buttonfiresclickunconditionally —Buttonno longer gates onprops.onClick; the prop is gone from the schema. Disabled buttons remain inert. Pre-existingexamples/that passonClickcontinue to work because the renderer ignores unknown props. - Sidebar emits per-item
descendantId—BuiltinComponentProps.onEventnow accepts an optional 3rd argument; when supplied the renderer rewrites the outbound componentId to<host>__tpl__<descendantId>, so the bridge's existing__tpl__parser pulls it intomatch.descendantId. The documentedaethon.onEvent({componentType:"sidebar", descendantId:"open-readme"}, …)recipe now matches. - Handler
ctx.pi.prompterrors emitnotice(non-terminal) —handler prompt: …now sendstype:"notice"so the frontend's Stop button stays visible for the surrounding turn. The error still rethrows so the calling handler can see it. -
registerComponentaccepts both bare and wrapper shapes — bridge auto-unwraps{components:[<single component>]}to the single component for renderer expansion. Docs (docs/aethon-agent/api.md:57-78) updated to show the bare-component form as canonical. - A2UI primitive coverage — the full SPEC-enumerated primitive set now ships:
text,heading,paragraph,code,card,container,divider,button,text-input,date-picker,select,checkbox,slider,table,list,image,icon,form, andform-field.listandtableuse the same scoped-render mechanism asfor-each;formserializes named child controls into{values}; input-like controls optimistically update$ref-boundvalueprops. - Multi-tab restore on relaunch — discovery + manual or automatic restore. Bridge's
discoverPersistedTabs()walks~/.aethon/sessions/at boot, reads the newest JSONL metadata for each tab, and returns[{tabId, lastModified, cwd?}]sorted descending. Shipped inreadyasdiscoveredTabs. Frontend scopes/recentSessionsto the active project cwd, filters out already-open tabIds, and the empty-state/sidebar/palette render clickable "Recent sessions" rows (relative-time labels). Clicking a row callsnewTab(sessionId, label, {cwd, restoredSession:true}), reusing the sametabId; the bridge replays the saved visible transcript from JSONL viasession_historyand usesSessionManager.continueRecent(cwd, dir)so pi resumes the same LLM history.[ui] restore_tabs = trueauto-opens only the active project's discovered sessions after projects load, dedupes repeatedreadyevents, opens oldest first so the newest restored session is active, and leaves manual restore available when the setting is false. - Concurrent setState attribution race fixed for handler-driven writes.
dispatch_a2ui_eventhandler dispatch now wraps the handler intabContext.run(handlerTabId, …)so any setState (including microtask/promise chains the handler kicks off) inherits the originating tab via AsyncLocalStorage. Pi prompts already had this. setIntervals registered at module-load time still have no ALS context — those fall back tocurrentAgentTabId(best-effort active-tab attribution; documented in the priority-list comment). - State file regenerates on
onEventregistration — fixed inagent/main.ts:_onEvent:scheduleStateFileWriteis now called after each handler is registered. - Registered event handlers in runtime snapshot —
RuntimeSnapshot.eventHandlersships the match shape only (templateRootType / componentType / descendantId / eventType), no function bodies. Surfaced in the live~/.aethon/state.jsonand the system-prompt runtime section so the agent can answer "what handlers are wired?" without invoking JS. - Hardcoded chat-input chrome lifted to props —
chat-inputnow acceptssendLabel,stopLabel,stopTitle, andqueueBadgeFormat(with{n}placeholder for the queue count). All accept$refs. Defaults preserve existing UX. Documented indocs/aethon-agent/components.md. - Hardcoded chrome strings lifted to composite props —
main-canvasacceptsemptyHint;terminalacceptsheaderLabelandbootGreeting(re-applied on tab-switch replay too). Extensions overriding brand/voice no longer need to fork the composite. - Theme discovery: loose-file loader implemented. Bridge now loads
~/.aethon/themes/*.jsonat boot vialoadAethonThemeDirectory; each file goes through the samenormalizeThemevalidation as extension-supplied themes. Watcher pre-creates the directory and watches it for hot-reload. SPEC's Themes section updated with the four discovery sources. Lowest-friction path for non-coder users to ship a theme.
- Default-layout slot contract —
src/extensions/default-layout/slots.jsondeclares the canonical slot catalogue (header,sidebar,tabs,canvas,terminal,composer,status,empty-state) with descriptions, default composite types, and arequiredflag (canvas+composerare required for a complete workspace). Composites slot via their existingareaprop; the Layout component honors an optionalslotMapon the root<layout>so a non-canonical layout can host the standard composites under different CSS area names. The current built-in layout (workstation) uses the canonical slot names —chat-inputarea was renamed tocomposer. (Earlier siblingseditorial/command-deck/live-layoutalso conformed before being trimmed from the catalogue.) Catalogue exposed onwindow.aethon.layoutSlots(withinspectLayoutSlotCoverage(payload?)for tooling) and on the bridge asglobalThis.aethon.getLayoutSlots(). Bridge loads it synchronously at boot from$AETHON_LAYOUT_SLOTS_FILE(set by the Tauri shell, bundled asslots.jsonin release) and surfaces it inRuntimeSnapshot.layoutSlotsso the system prompt prints the canonical slot list. Documented indocs/aethon-agent/components.md"Layout-slot contract" with aslotMapexample. - Default-layout sidebar opt-in — sidebar presence binds to
/layout/sidebarVisible, with/layout/columnsand/layout/areasalso state-driven so the grid template-areas adapts on toggle.Layoutcomponent now resolvesareasvia$reftoo. New/sidebarslash command,Cmd/Ctrl+B, andtoggleSidebarslash context method flip all three keys atomically. - Layout extension catalogue —
default-layoutextension currently ships one built-in layout (workstation) as an.a2ui.jsonpayload with chrome composites (sidebar, tab-strip, status-bar, chat-input) plus the workstation-specificagent-pulse/model-picker/appearance-menuchrome. (Earliereditorial/command-deck/live-layoutsiblings were trimmed in this branch to focus polish — re-add their JSON payloads + catalogue entries to bring them back;aethon.registerLayoutis unchanged.)window.aethon.listLayouts(),window.aethon.activateLayout(id), andwindow.aethon.registerLayout({id, name, payload})form the frontend catalogue API. Bridge mirror:globalThis.aethon.registerLayout({id, name, description?, payload}),unregisterLayout(id), andlistLayouts()ship on the agent side too — agent-side extensions and extension packages can append to the catalogue without injecting through the webview. Reserved built-in id:workstation. The catalogue replays onready(so reload restores extension layouts) andRuntimeSnapshot.layoutscarries id+name+description (payloads omitted to keep the snapshot small)./layout <id>slash command swaps via the catalogue;/layoutlists available ids.workstationis the boot default. - Closing the last tab shows an empty-state composite from default-layout —
closeTabno longer guards ontabs.length <= 1ortabId === "default"; every tab is closable. When the tab list reaches zero, the layout swaps the canvas/composer/tab-strip cells (hidden viavisible: { $ref: "/hasTabs" }) for anempty-statecomposite (visible: { $ref: "/empty" }) that renders a welcome card with a "New Tab" button, quick-start tips, and a recent-sessions slot. The composite is a registered component ondefault-layoutextension (empty-state→EmptyStateReact component) — extensions can override it by re-registering the same type. Layout JSON owns the visibility wiring (/emptyand/hasTabsboot tofalse/trueand flip on tab transitions). Bridgetab_closenow allows closing "default" too and gracefully handles an emptytabsmap (clearscurrentAgentTabId;ensureTablazily recreates whatever tab the next inbound message references). New-tab gestures (Cmd/Ctrl+T, system-tray "New Tab", menu,empty-state:new-tabevent) all work from the empty state.
- System prompt covers
for-eachiteration — replaces the prior "no array iteration" warning. Documents{$item, $index, $parent}scope keys with a worked example. - System prompt covers the mutation feedback contract —
agent/system-prompt/prompt-template.tsnow ships a "Knowing whether a mutation succeeded" section explaining the Promise return, the failure modes (timeout,frontend_rejected: …, validation), and the pre-connect immediate-resolve behavior. Bundleddocs/aethon-agent/api.mdcarries the same contract at the head of the Mutation section. -
docs/aethon-agent/{api,components,extensions}.mdupdated. Each shipped M5 item updated the bundled docs alongside the code change —registerSlashCommand,registerKeybinding,registerMenuItem,registerEventRoute,getFrontendState,getRuntimeSnapshot.layoutStructure,for-each,empty-state, and the full primitive set (heading,paragraph,divider,checkbox,select,slider,list,table,date-picker,icon,form,form-field). Composites' new prop schemas (emptyHint,headerLabel,bootGreeting,sendLabel,stopLabel,queueBadgeFormat) are documented too.
The agent-control surface (M1–M5) is shipped. M6 turns Aethon into a
proper desktop terminal-class app: real PTY-backed user shell tabs that
look and feel like Terminal.app/iTerm2, an opt-in agent-sharing model so
the agent can see (and optionally drive) the user's shell, hotkey
expansion to terminal-app conventions, a graphical settings panel, and
the rest of the polish bundle the audit surfaced (drag-and-drop,
notifications, crash recovery, tool-card timing, reopen closed tab,
cross-session search). Phase order is tab kinds + PTY + Cmd+T first,
then sharing, then polish.
- Tab kinds —
Tabinterface gainskind: "agent" | "shell". Agent tabs keep today's chat-history / draft / waiting / queueCount fields; shell tabs carrycwd,command,args,shareMode,shellState,exitCode?,scrollback. Tab strip renders a kind icon and the bottom panel surfaces shell sub-tabs alongside the read-only Agent bash view. Tab metadata persists across restarts; PTYs are re-spawned fresh on relaunch (scrollback is not persisted to disk in v1). - PTY backend (Rust) —
portable-ptycrate. Tauri commands:shell_open(withinheritEnvknob),shell_input,shell_resize,shell_close. Per-tabMutex<HashMap<TabId, ShellSlot>>in app state holds(child, master_writer, reader_thread_handle, scrollback, share). Reader thread emitsshell-output {tabId, content}events; child exit emitsshell-exit {tabId, code}. Reader is UTF-8 carry-buffered + char-boundary safe. - Interactive xterm.js —
ShellCanvasis a fresh composite (separate from the read-onlyTerminal). Bidirectional input viaterm.onData → shell_input, FitAddon resize firesshell_resize, status line under the canvas showscwd · command · share-mode-badge · cols×rows. -
Cmd+T= focus-aware — focus inside the bottom panel → new shell sub-tab, focus elsewhere → new agent tab.Cmd+Shift+Talways opens the alternate kind. Override via[shortcuts] new_tab_kind = "shell"to make Cmd+T always open a shell. - Spawn defaults — command falls back to
$SHELL; cwd inherits the active project else$HOME; env inherits the host process and stampsTERM=xterm-256color,COLORTERM=truecolor,AETHON=1.[shell] inherit_env = falseclears host env before stamping our baseline. Override the program via[shell] default_commandand append argv via[shell] default_args. - Theme-aware ANSI palette —
ember,paper,aethership--ansi-{black,red,…,bright-white}plus--terminal-{bg,fg,cursor,selection}. BothTerminalandShellCanvasread these at xterm construction and re-apply ondata-themeattribute change via aMutationObserver. - Process title via OSC — instead of cross-platform foreground-PID detection, the reader thread parses OSC 0/1/2 title-set sequences out of the PTY stream and emits
shell-title {tabId, title}events. Vim, zsh's PROMPT_COMMAND, ssh, htop all already emit these, so the sub-tab label tracksvim · README.md/user@host/htopwithout per-platform plumbing. Strict foreground-PID detection remains a future lift. - Close-confirm for running shell tabs —
Cmd+W/ X-click on arunningshell tab pops an Allow / Cancel notification (reusing the shell-write consent UI). User can disable via[shell] prompt_before_close = false. Strict child-vs-shell foreground detection deferred — for v1 we prompt for any running shell; users opt out via the setting. - Drag-drop file → shell-quoted paste — drops onto the bottom panel while a shell sub-tab is active, or anywhere when the active top tab is a shell, paste the absolute path with
shellQuoteescaping (single-quote-wrapped, embedded quotes escaped). Drops elsewhere route to the agent composer as@pathtokens.src/utils/shellQuote.tscovers the helper + tests.
- Per-tab share mode — four-value enum:
private(default) |read(agent reads scrollback, no writes) |read-write(agent writes with per-write user confirmation) |read-write-trusted(agent writes without confirmation). Mode changes are user-only — only the status-bar badge click (or future palette entry) can flip a mode. There is no agent-sidesetShareModeAPI on purpose: a discoverable tab id from/tabsplus a setter would defeat the opt-in boundary. The default for new shell tabs is configurable via[shell] default_share_mode; unknown values fall back to"private"so a typo can't widen access. Mode is per-tab; the badge is always visible — no hidden state. Right-click menu / palette entries deferred to a follow-up. -
aethon.shells.{list,read,write}API —aethon.shells.list()returns shareable-tab metadata only (shareMode !== "private");aethon.shells.read({tabId, sinceTotal?, maxBytes?})returns scrollback bytes clamped to ≥ the privacy floor (forward-paging from the supplied cursor; cold-start with no cursor returns the latestmax_bytes, default 8 KiB / hard cap 64 KiB; subsequent calls feed back the returnedtotal_appendedto walk the stream);aethon.shells.write({tabId, text})injects keystrokes (gated toread-writeandread-write-trusted). Round-trips through the existing mutation-ack channel asshell_query; queries await frontend readiness so pre-readycallers don't see{ok:true, data: undefined}. The write op uses an extended 5-minute ack timeout because it can wait on user confirmation. Pi-tool registration (listShells/readShell/writeShell) deferred to a follow-up — the global API is reachable viaglobalThis.aethon.shells.*from any extension today. - Per-write user confirmation in
read-writemode — eachaethon.shells.writecall inread-writemode pushes a sticky notification ("Agent wants to type in<tab>:<preview>. [Allow] [Deny]") and only invokes the Rustshell_writecommand on Allow; Deny / dismiss rejects the bridge promise with "user denied agent write".read-write-trustedskips the prompt — that's the only behavioral difference between the two writable modes. The confirmation reuses the existing notification primitive (no new component). Allow-once vs Allow-forever distinction collapses to a single Allow button; users who want "no prompts" upgrade toread-write-trustedvia the badge. - Privacy guardrail: mode flips don't expose old scrollback —
ShareStateinsrc-tauri/src/shell.rstracks(mode, floor)atomically. On every private→shareable transition,flooris bumped to the liveScrollback::total_appendedcursor, andshell_read_scrollbackclamps any caller cursor to ≥ floor. Cargo tests cover the round-trip-through-private case explicitly so the in-between window can never re-expose.aethon.shells.readreturnsshare_floor+share_modealongside content so callers can see the live state without a second round-trip.
-
Cmd/Ctrl+1…Cmd/Ctrl+8— jump to tab N (matches every browser). -
Cmd/Ctrl+9— jump to last tab (Chrome/Firefox convention). -
Cmd/Ctrl+Shift+]/Cmd/Ctrl+Shift+[— move active tab right / left. -
Cmd/Ctrl+,— open the Settings panel (macOS native preferences convention). -
Cmd/Ctrl+L— focus composer (agent tabs) / terminal (shell tabs). -
Cmd+Ctrl+F(mac) /F11— toggle fullscreen via Tauriset_fullscreen. -
Cmd/Ctrl+Shift+S— export current chat as Markdown to~/Downloads/(agent tabs only). -
Cmd/Ctrl+Shift+F— search across past sessions. -
F12— toggle dev tools (debug builds only). Release builds surface a "DevTools unavailable" toast. -
Cmd/Ctrl+Opt+T— reopen most-recently-closed tab from the in-memory stack (cap 10). Agent tabs spawn fresh; shell tabs spawn a new PTY at the original cwd / command / args. Session-history replay for restored agent tabs is a follow-up. - Sub-tab navigation when focus is in the panel —
Cmd+1..9jumps between sub-tabs (1 = agent-bash).Cmd+Shift+]/[cycles sub-tabs;Cmd+Opt+]/[reorders shell sub-tabs (agent-bash is pinned first). - Native menu accelerators in
src-tauri/src/lib.rsextended in lockstep so menu and shortcut paths never drift. ExtensionregisterKeybindingpriority is unchanged: extensions still run first and may override built-ins.
- Settings UI — A2UI overlay registered by the
default-layoutextension (settings-panelcomposite). Sections: Appearance, Notifications, Agent (default model + system-prompt override path), Shell (default share mode, auto-restart, default command, inherit env), Behavior (prompt-before-close, Cmd+T action, ANSI palette preview), Updater (channel placeholder for M7), Advanced (openconfig.tomldirectly). Writes viawrite_configTauri command, atomic-write through tempfile + rename.toml_editround-trips comments + unknown keys so hand edits survive Save.clearConfigCache()invalidates the in-memorygetConfig()cache so theme / font / refs apply without reload. - Drag-and-drop file paths into composer — Tauri 2's
onDragDropEvent. Drops onto the bottom panel while a shell sub-tab is active route shell-quoted paths into the PTY; drops elsewhere into an agent tab insert@<absolute-path>tokens. Cmd+V image paste persists the clipboard image to~/.aethon/pastes/<uuid>.<ext>viasave_paste_imageand inserts the resulting@pathtoken so the agent's read tool can pick it up. - Native OS notifications —
tauri-plugin-notificationregistered. Notification fires when an agent turn ends while the window is unfocused OR the originating tab isn't active, AND turn duration ≥[ui] notify_min_duration_seconds(default 8). Permission requested lazily on first fire. Settings[ui] notify_on_completion(default true) +notify_min_duration_seconds(default 8) configurable in~/.aethon/config.toml. - Bridge crash recovery — Rust spawn-with-supervisor pattern: on bun child exit, emit
agent-crashed {exitCode, stderr}event. Frontend clearswaitingandqueueCountfor all tabs, appends a system notice with a Restart button (or auto-restart per[shell] auto_restart_agent = true, default). Per-tabSessionManager.continueRecentresumes context on the next message. - Tool-card elapsed-time + long-running warning —
tool-cardbuiltin component (replaces plaincardfor tool calls). Shows live elapsed clock at 4 Hz whilestartedAtis set andendedAtisn't; on end shows total duration. At 30 s, the title shifts amber and the card surfaces a "Long-running command — press ⌘. to stop" hint. The bridge sendsstartedAtontool_execution_startandendedAtontool_execution_end; the React component owns the timer locally. - Reopen closed tab stack — in-memory stack of last 10 closed tabs (id, kind, label, cwd, projectId, command/args for shell, restoredSessionId for agent).
Cmd/Ctrl+Opt+Tpops; palette gains "Reopen closed tab → <label>" entries. Agent tabs restore via the existing JSONL replay path; shell tabs spawn fresh PTYs at the same cwd. - Cross-session search —
Cmd/Ctrl+Shift+Fopens an A2UI overlay (search-panelcomposite). Bridge commandsearch_sessions(query, limit?)scans~/.aethon/sessions/<tabId>/*.jsonland returns hits with{snippetBefore, snippetMatch, snippetAfter}. The frontend wraps the match in<mark>, scopes results to current/all projects via a top-of-panel toggle, surfaces project labels via tab cross-reference, and on result-click restores the tab and scrolls to + flashes the matching message.
-
[shell] default_share_mode(default"private", allowed:private | read | read-write | read-write-trusted, anything else clamped toprivate) — surfaced from~/.aethon/config.tomland seeded intonewShellTabso new tabs open at the configured mode. - Remaining
[shell]keys —default_command(override$SHELL),default_args(extra argv appended after the platform default),inherit_env(default true;falseclears the host env before stampingTERM/COLORTERM/AETHON),prompt_before_close(default true),auto_restart_agent(default true). -
[shortcuts] new_tab_kind—"agent"(default — focus-aware) |"shell"(always shell). Lets users force Cmd+T to always open a shell. -
[ui] notify_on_completion+notify_min_duration_seconds— surface the OS-notification gate as user-tunable.
- App-root overlays go through the registry —
command-palette,notification-stack,settings-panel, andsearch-panelare mounted at App root via<RegistryComponent type="…">; theshare-mode-badgeis mounted inline insideshell/canvas.tsx(also via<RegistryComponent>, so extensions can still replace it withaethon.registerComponent("share-mode-badge", custom)). Previously the App-root overlays were direct-imported inApp.tsx, silently defeating the documented override path. - Built-in event routes dispatch by type, not id — every
chrome composite (
sidebar,command-palette,settings-panel,search-panel,notification-stack,chat-input,empty-state,terminal-panel,tab-strip,model-picker,appearance-menu,share-mode-badge,shell-canvas) has its built-in handler registered undertype:<componentType>insrc/eventRoutes/index.ts. A custom layout payload may rename the instance ({id: "primary-side-rail", type: "sidebar"}) and events still route correctly. Previously each handler also had a defensiveif (component.id !== "<literal>") return falseguard that broke the override contract; those guards are gone andindex.test.tscovers eight renamed-instance regression cases. -
main-canvaschat mode is opt-in — bindingmessagesenables chat-history rendering, the empty hint, and the "↓ latest" pill; omittingmessages(and binding onlyslot) makes the canvas a pure scroll viewport so non-chat extension hosts (image gallery, dashboard, …) don't inherit chat chrome. - Hot-reload no longer kills in-flight prompts — the Rust
file-watcher now sends
{type:"reload_request"}over the bridge's stdin instead of SIGKILLing the bun child. The bridge drains active prompts (tab.promptInFlight), writes a{"type":"_reload_done"}sentinel, and exits cleanly. The supervisor's stdout reader sees the sentinel, sets the reload-in-progress flag, emitsagent-reloaded, and the frontend respawns lazily on the next request. Previous behaviour aborted the user's LLM turn whenever an extension file changed.
-
docs/aethon-agent/{api,components,extensions}.mdupdated —aethon.shells.{list,read,write},ctx.shells.*,settings-panel/search-panel/terminal-panel/shell-canvas/tool-card/ share-mode badge composites, the new keybinding set. -
SPEC.mdkeyboard summary + Configuration schema updated alongside each shipped item. - CLAUDE.md keyboard summary mirrored.
- Tests: vitest for
formatToolDuration+ share-badge cycling integration (src/extensions/default-layout/components.test.ts);src/utils/shellQuote.test.tsfor file-drop quoting;agent/shell-tools.test.tsforlistShells/readShell/writeShelltool dispatch + privacy-floor refusal propagation; cargo tests for PTY spawn/resize/exit-code/cleanup, scrollback paging, share-state floor, and OSC title parsing. All wired into thecheckgate.
Polish bundle layered on top of the M6 workstation: hands-free composer dictation and the ability to hold several provider accounts at once and pick which one each tab speaks as.
- Cross-platform capture + transcription — the
src-tauri/src/voice/module records via acpalpipeline (voice/audio.rs, level metering + 16-bit PCM WAV) and transcribes through one of several providers behind theplatform_speech/PlatformSpeechEnginetrait: a bundled local Whisper model (candle-transformers, weights downloaded on demand), macOSSFSpeechRecognizer/SpeechAnalyzer(Swift static lib compiled inbuild.rs), and Windows SAPI 5.4 via COM (windowscrate; no .NET / PowerShell). Linux falls back to the local-Whisper provider. - IPC + chrome —
commands/voice.rsexposes provider list/select/enable, modelprepare/remove, andstart_recording/stop_and_transcribe/cancel_recording. Push-to-talk toggle isCmd+Shift+Mwith an optional hold-to-record key; both configurable via[voice](toggle_hotkey,hold_hotkey) and surfaced in Settings → Voice.
- Per-tab login identities —
agent/auth-profiles/(store + manager) persistsoauth | api_keyprofiles scoped per provider, with a per-tab active profile and a per-provider default. Profile ids are sanitized (isSafeProfileId/sanitizeProfileId) before touching disk; credentials live underauthProfilesDir()in~/.aethon/. Login streams anAuthProfileLoginEvent(started → auth → progress → prompt → complete). Frontend mirror insrc/auth-profiles/; the active profile selects which model registry a tab sees. Driven by/login [list | use <account> | default <account>].
- Command PATH resolution —
src-tauri/src/env.rscentralizes external-binary lookup (COMMON_TOOL_DIRS) so every Rust IPC command resolves tools identically even when the.appis launched from Finder/Dock with a minimal PATH. - Docs synced — README feature list,
website/(voice config, keyboard reference,/login),docs/aethon-agent/api.md, CLAUDE.md, and AGENTS.md updated alongside the shipped surface.
Shipped across the 0.3.0–0.10.2 line (see CHANGELOG.md for the
per-release detail). Recorded here so the checklist tracks the current
surface.
- First-class workspaces (BREAKING,
#259) — a project has one or more workspaces (the main checkoutisMain: trueor a git worktree), each with its own tabs, agent sessions, git state, and devshell. The sidebar tree is host → project → workspace; tabs bucket per workspace; git-status polling is tiered (hot/warm/cold).projects.jsonis schemaVersion 5 (older schemas migrate on read). Concept-level copy says "workspace"; "worktree" survives only at the git-mechanics layer. - MCP setup + config flows (
#407) — Model Context Protocol support with/mcp [status|setup]and/mcp-auth [server], host policy ([mcp] enabled/project_configs), and repo-owned config import from.aethon/mcp.toml/.mcp.jsonbehind an approval gate. - Subagents + parallel background delegation (
#208,#299) — configurable multi-model subagents, session rollback/fork, and parallel background task delegation through the task tool. - Native scheduler — loops & scheduled tasks —
/loop,/tasks(Cmd+Shift+L), and a native loop scheduler for recurring agent runs. - GitHub issue → session linking (
fc869bf4) — sending an issue to the agent targets a new or the current workspace and re-opens an existing linked session instead of forking; per-repo.aethon/issues.tomlhandoff templates and inferred branch names. - Workspace startup commands — per-project
.aethon/startup.tomlcommands gated by[startup] auto_approve(opt-in execution boundary). - Native A2UI canvas windows (
#318) — separate restorable OS windows rendering bare A2UI canvas content with window-local state. - Multi-account auth (
#323) — multiple accounts per provider with usage stats, isolation, switching, and auto-switch;/login [list | use <account> | default <account>]. - Voice — LFM2-Audio third mode + auto-listen (
#289,#293,#294) — push-to-talk (Cmd+Shift+M) and hold-to-record dictation plus an end-to-end ASR+TTS conversational mode (speak-aloud replies, hands-free auto-listen, off by default). - Enforced plan mode + reasoning controls (
929c9cc,#319,8eaab89) —/plan/Shift+Tabplanning-only mode the agent enforces, a compact plan-mode toggle, and Codex reasoning-effort controls ([agent] thinking_level), including model-specific GPT-5.6 Sol/Terra/Luna effort sets, Max, and Ultra delegation. - Memory (
#270) —/memory; per-user and resolved-project memory the agent can read and write. - Editor + dashboards (
#71,db89c19) — Monaco editor, file tree, and media/text viewers; project/host dashboards with a host-level task launcher. - Always-on VCS surface — consolidated working-tree/branch/PR/CI
status (
/vcs),.gitwatching, periodic remote fetch;@filecompletion picker, clickable terminal URLs, markdown-preview parity. - Window-state persistence (
#69) and mDNS networked discovery (#74) — position/size/multi-monitor restore; LAN host advertise/browse gated by[server] enabled.
- Terminal panel —
xterm.jswith WebGL renderer, toggled from sidebar - Bash tool output streams into the terminal panel via the
aethon:terminalwindow event (default-layout terminal opts in viasubscribeToBash). Command echo ($ <cmd>) is emitted attool_execution_start; stdout/stderr now stream from pi'stool_execution_updaterolling tail buffer through a per-toolCallIddiff so overlapping bash calls stay isolated and the finaltool_execution_endpayload does not replay duplicate output. Covered by stream-diff tests for append-only output, tail truncation overlap, final replay suppression, and interleaved bash calls. - Per-tab persistent sessions — each tab uses
SessionManager.continueRecent(cwd, $AETHON_SESSIONS_DIR/<tabId>), so pi context survives bun restarts (file-watcher in dev, app relaunch in release). Session discovery reads each JSONL session record'scwdand the frontend scopes Chat History / recent-session restore to the active project. Restored tabs replay their visible user/assistant transcript from JSONL, then pi resumes the underlying LLM context from the same tab directory. New tabs (no prior file) start fresh. Falls back toinMemory()if the per-tab dir can't be created.tabIdis sanitized against[A-Za-z0-9_-]{1,128}before use as a directory name (defense against malformed external callers). -
aethon-debugskill — TCP eval server (127.0.0.1:19433in dev) + slash command for driving the running app from Claude (eval, send, set-model, screenshot, wait, status). Mirrors Claudette'sclaudette-debugpattern. - Project workspace switching — recent projects persist to
~/.aethon/projects.json; selecting a project switches the active cwd for new agent sessions and maintains a separate visible tab bucket per project. The sidebar shows basename + full-path tooltip, plus best-effort git branch and dirty badges from the Taurigit_statuscommand. Command palette project rows prioritize full path hints, then git branch tags./projectsupports picker, id switch, and raw absolute/~paths. - Multi-tab — per-tab pi sessions (
Map<TabId, AgentSession>sharing one auth/registry/resourceLoader), per-tab message history, draft, canvas, queue counter, terminal buffer, and model. AsyncLocalStorage carries the active turn's tabId through the agent's async chain soglobalThis.aethon.setStatecalls route to the right tab even under concurrent prompts. Tab strip in the layout,Cmd/Ctrl+Tnew,Cmd/Ctrl+]/Cmd/Ctrl+[next/prev,Cmd/Ctrl+Wclose. New tabs inherit the active tab's model; bridgetab_openaccepts amodelso the pi session boots with it (no race window). Per-tab terminal buffer with replay on switch viaaethon:terminal-replay. - UI scaling — app-wide browser zoom persists to
~/.aethon/ui_zoom, driven byCmd/Ctrl+=,Cmd/Ctrl+-, andCmd/Ctrl+0.App.tsxwrites--app-ui-scale,--app-viewport-width, and--app-viewport-heightso viewport-bound layouts, popovers, palette, slash picker, and terminal sizes divide by the zoom instead of expanding past the visible window. Shipped layouts/components useminmax(0, 1fr), bounded overlays, truncation, and shrink constraints so zoomed UI stays inside the viewport acrossember,paper,aether, and extension themes. - Persistent state — legacy default-tab chat history (
~/.aethon/messages.json, capped at 200 messages / 8KB per text field, image data URLs stripped before persist), theme (~/.aethon/theme), UI zoom (~/.aethon/ui_zoom), projects (~/.aethon/projects.json), per-tab pi sessions and restorable visible transcripts (~/.aethon/sessions/<tabId>), and runtime snapshot (~/.aethon/state.json) persist to disk via Tauri commands or the bridge. Cross-platform via Tauri'shome_dir(). Legacy localStorage values migrate on first read; legacy entries are removed only after a confirmed disk write. - Client-side slash commands (
/clear,/help,/theme,/model,/reset,/terminal,/sidebar,/layout,/extensions,/project). Unknown commands fall through to the agent so pi-side handling and prompt templates aren't blocked.//fooescapes to send a literal/foo. - Slash command picker UI — autocomplete dropdown above the chat input when the draft starts with
/. Prefix-filters; ↑/↓/Tab/Enter navigate+insert; Esc dismisses; click inserts. Portalled todocument.bodyso the layout cell'soverflow:hiddendoesn't clip it. Bound via thecommandsprop onchat-input(inline array or$ref). - Configuration file (
~/.aethon/config.toml) —read_configparses TOML into[ui](theme,font_size,restore_tabs) +[agent](model) sections. Frontend reads it during boot to seed theme;[ui] font_sizeclamps to 10–24 px and writes the--app-font-sizeCSS custom property the body rule consumes;[ui] restore_tabs = trueauto-restores discovered per-tab sessions on launch;[agent] modelseeds the picker default when no per-session model is saved (bridge's session model still wins onreadyhydration). Pi settings (~/.pi/agent/settings.json) still drive model picker filtering and provider/auth — the Aethon config is layered on top. - Bridge env-var contract — Tauri shell passes
AETHON_DOCS_DIR(bundled reference docs),AETHON_USER_DIR(~/.aethon/),AETHON_STATE_FILE(snapshot path),AETHON_SESSIONS_DIR(per-tab pi sessions),AETHON_RELEASE_MODE(1/0), andAETHON_PROJECT_ROOT(dev only). Bundled docs live in the binary viatauri.conf.jsonbundle.resources(docs/aethon-agent/*.md→<resource_dir>/docs/aethon-agent/). System prompt branches on these so release builds don't tell the agent to read source files that aren't there. -
dispatch_a2ui_eventroutes to extension handlers — bridge extractsdescendantIdfrom host-prefixedcomponentIdand matches againstaethon.onEvent({templateRootType, componentType, descendantId, eventType})predicates. Handlers run fire-and-forget so a slow handler can't block the bridge IPC loop; errors emit asnotice(noterror) so they don't clobber thewaitingflag for an in-flight prompt. - Window opens maximized via
tauri.conf.jsonapp.windows[0].maximized: true(existing center/width/height stay as the unmaximized fallback). - Extension lifecycle feedback channel. Bridge emits
extension_lifecycle { name, source, status: "loaded"|"failed"|"skipped", error?, path }for every load attempt fromloadAethonExtensionsandloadAethonExtensionPackages. Frontend dispatches a cancellableaethon:extension-lifecyclewindow CustomEvent, then (default) appends a system-notice chat bubble. Layouts and extensions can listen on the window event and calle.preventDefault()to substitute alternative UX (toast, sidebar pulse, status pill) — the channel is decoupled from chat-history rendering by design. - Extension-deletion UI cleanup. Bridge tracks
extensionStateKeys: Set<string>of every JSON Pointer path written via extensionsetStateand reports the list inready. Frontend keeps a ref of the previous ready's set; on each new ready, paths in (previous − new) are deleted from live state viadeletePointer. So when an extension is uninstalled (file deleted), the watcher respawns the bridge → new ready has a smaller key set → leftover sidebar sections / canvas cards / state slices vanish without a page reload. - Test coverage + linting.
cargo test --libcovershelpers::{validate_state_name, parse_config_toml, clamp_font_size}(14 tests).bun run test/bunx vitest runcoversutils/jsonPointer,utils/dataBinding,slashCommands, project storage, palette item selection, primitive rendering, and the slot catalogue / inspector (77 tests).bunx eslint .enforces 0 errors with type-aware rules + react-hooks plugin (warnings tracked:react-hooks/set-state-in-effect,react-hooks/refsinApp.tsx/ChatInput). All wired into thecheckdevshell command as a single CI gate. - Nix devshell first-class support. Project roots containing
flake.nix,.envrc(withuse_flake/use_nix+direnvon PATH), orshell.nixare detected automatically and the resolved env is layered over every PTY shell tab AND the agent's pibashtool. Precedence: direnv > flake > shell.nix; gated capability-aware (nonix/direnvon PATH → returnsNone, shells still spawn unwrapped). One resolver per(root, flake.lock+marker hash)runs intokio::task::spawn_blockingviatokio::sync::OnceCell-style de-dup; result persisted to~/.aethon/devshell-cache/<short-hash>/{env,meta}.jsonwith 30-day GC. Rust intercept lives insrc-tauri/src/shell/lifecycle/open.rs(aftereffective_command, beforeargs.envso explicit overrides still win). Agent intercept is acustomToolsentry namedbashthat overrides pi's built-in via the registry's later-wins-by-name rule, attaching aBashSpawnHookthat consults a process-local cache (warmed via thedevshell_querybridge IPC; invalidated bydevshell-ready/devshell-failedpush events). Status badge in the status bar (⬡ <kind>) + Settings → Nix devshell section withenabled,mode,cache_ttl_hours,refresh_on_lockfile_change, and a live "Refresh now" button. 49 Rust + TS unit tests; live UAT verified non-interactive/bin/sh -cshells pick upCARGO=/nix/store/.../cargo,IN_NIX_SHELL=impure,DEVSHELL_DIR=/nix/store/..., etc. Per-project override at<project>/.aethon/devshell.toml. Seesrc-tauri/src/devshell/andagent/devshell/.
Built-in (primitives) — Hardcoded in src/components/A2UIRenderer.tsx's
PRIMITIVE_REGISTRY. Cannot be overridden by extensions. The current shipped set
is text, heading, paragraph, code, card, container, divider,
button, text-input, date-picker, select, checkbox, slider,
table, list, image, icon, form, and form-field. list supports
scoped child templates via /$item, /$index, and /$parent; table cell
templates expose /$row, /$index, /$parent, /$column (column metadata
— field, header, width), and /$cell (resolved value at the column's
field path on the row); form emits named child control values on submit.
Extension components — Registered via ExtensionRegistry.register(extension). An
extension declares its custom component types in its manifest:
{
"name": "git-extension",
"a2ui": {
"components": {
"branch-graph": "./components/BranchGraph.tsx",
"commit-list": "./components/CommitList.tsx",
"diff-viewer": "./components/DiffViewer.tsx"
}
}
}When the agent uses this extension and emits a branch-graph component, the
renderer resolves it through the registry.
User components — Installed from ~/.aethon/components/ or via packages.
Users can create and share custom A2UI components.
A2UI's DynamicString / DynamicNumber / DynamicBoolean types enable
reactive data binding. The frontend maintains a single state store; components
bind to state paths via JSON Pointer ({"$ref": "/path/to/value"}). When
state updates, bound components re-render automatically.
User interactions (clicks, form submissions, selections) are dispatched back
to the pi agent as structured events (dispatch_a2ui_event Tauri command).
The agent handles them like tool results — processing the interaction and
optionally updating the UI.
Aethon extends pi's existing extension system with UI capabilities.
registerTool()— LLM-callable functionsregisterCommand()— Slash commandson('tool_call', ...)— Event hooksctx.ui.confirm(),ctx.ui.select()— User prompts
Pi extensions reach the Aethon UI via globalThis.aethon — see
examples/pi-extensions/aethon-types.d.ts for full ambient types.
Aethon-only extensions live in ~/.aethon/extensions/*.{ts,js} and
receive the same surface as the first arg to their register(api).
registerComponent(type, template)— Register an A2UI subtree as a custom component typesetState(jsonPointer, value)— Mutate live layout state at a pathonEvent(match, handler)— Route component events to extension handlers (no LLM round-trip)setLayout(payload)— Replace the active layout wholesalepatchLayout(jsonPointer, value)— JSON-Pointer patch the active layout (array-preserving)registerSidebarSection({id, title, items})— Convenience wrapper for/sidebar/extraSectionsregisterTheme({id, label?, vars})— Register a color scheme (CSS custom properties)registerSlashCommand({name, description, usage?})— Add slash picker metadata and pair it with an event handlerregisterKeybinding({combo, action?, description?})/unregisterKeybinding(combo)— Add or override keyboard shortcutsregisterMenuItem({label, action, location?, id?, parent?})/unregisterMenuItem(id)— Add menu metadata for A2UI-driven menusregisterEventRoute({componentId?, eventType?})/unregisterEventRoute(route)— Route matching built-in events through extension handlersregisterLayout({id, name, description?, payload})/unregisterLayout(id)/listLayouts()— Append to the runtime layout catalogue from the agent side; activation still goes throughsetLayout(payload)(or, on the frontend,window.aethon.activateLayout(id))canvas.{emit, append, clear, patch}— Programmatic canvas push API; sugar oversetState("/canvas", …). Also surfaced on the handlerctx(tab-pinned variant) so a sidebar click canctx.canvas.append(...)without rebuilding the envelope or losing tab attribution mid-handler. Implementation inagent/canvas.ts(covered by 23 vitest cases underagent/canvas.test.ts).windows.{openCanvas, list, focus, close, setTitle, emitCanvas, appendCanvas, patchCanvas, clearCanvas, setState}— Native OS windows hosting bare A2UI canvas payloads with window-local state. Events includesurfaceId/windowId, and handlers invoked from these windows receivectx.window.getFrontendState(path?),getRuntimeSnapshot(),getLayout(),getLayoutSlots()— Introspection helpers used by the system prompt and agent-side diagnostics
(none — M1–M5 surface complete.)
Today: bridge loads ~/.aethon/extensions/*.{ts,js,mjs} (Aethon-direct,
loose files), project-local .aethon/extensions/*.{ts,js,mjs} discovered
from the selected cwd up to its nearest git root (root-first, loaded once
per bridge process), ~/.aethon/extensions/node_modules/* (npm-distributed
extension packages with an aethon.entry field in package.json), and
discovers (without loading — pi does that) any
~/.pi/agent/extensions/* that references globalThis.aethon, tagging
them in the runtime snapshot.
Backlog: no active discovery/install backlog items.
Themes control the visual layer without affecting functionality. A theme defines:
- Color palette (background, foreground, accent, semantic colors)
- Typography (font family, sizes, weights)
- Spacing scale
- Border radii
- Component-specific overrides (card shadow, button style, code block theme)
- Dark/light mode variants
Themes are CSS custom properties applied globally. The A2UI renderer reads these when rendering components. Themes can be switched at runtime.
Discovery: themes can come from any of these sources, all registered
through the same normalizeTheme validation path so reserved built-ins
(ember, paper, aether, legacy signature) and malformed CSS variable
names are rejected uniformly:
~/.aethon/themes/*.json— loose-file JSON themes loaded by the bridge at boot. Each file is{ id, label?, vars: { "--bg": "...", ... } }. Pre-created on first launch and watched for hot reload — drop a JSON file and it appears in the sidebar Themes section without restarting.- Aethon extensions under
~/.aethon/extensions/*.{ts,js,mjs}callingglobalThis.aethon.registerTheme(…). - npm-distributed extension packages under
~/.aethon/extensions/node_modules/<pkg>(manifestaethon.entry) calling the same registration. - pi extensions under
~/.pi/agent/extensions/*that touchglobalThis.aethon.registerTheme(…).
Loose-file themes are the lowest-friction option for end users — no code required.
The key differentiator. An Aethon extension can include:
- Agent tools (standard pi) — Functions the LLM can call
- A2UI components (new) — Custom React components the agent can render
- Prompt templates (standard pi) — Pre-built prompts
- Default layout (new) — A2UI JSON describing the extension's preferred canvas layout
Example: A "Kubernetes" extension ships with:
- Tools:
kubectl_get,kubectl_describe,kubectl_logs - Components:
PodList,ServiceMap,LogViewer - Default layout: When activated, renders a
PodList+LogViewerside by side
The agent can override or extend the default layout based on conversation.
aethon/
├── src-tauri/ # Rust Tauri shell (thin)
├── src/ # React frontend + A2UI renderer
├── agent/ # Pi agent entry point + Aethon extensions
├── components/ # Built-in A2UI component implementations
├── themes/ # Built-in themes
├── extensions/ # Bundled extensions
└── package.json
bun build --compile agent/main.ts→aethon-agentbinary (~50MB)tauri build→ Bundles Rust shell + React frontend +aethon-agentinto.app
- macOS:
.dmg(Apple Silicon) + updater tarball/latest.json— the only target the release CI builds today - Linux:
.deb/.rpmvia localcargo tauri build(not in the release matrix today) - Windows: NSIS via local
cargo tauri build(not in the release matrix today) - Nix: Flake with
packages.aethon,packages.default, andoverlays.default(pkgs.aethon)
The canonical schema, defaults, and clamps live in
src-tauri/src/helpers/config.rs (parse_config_toml) and src/config.ts;
the full per-key reference is website/reference/config-reference.md.
Parsed sections: [ui], [agent], [shell], [shortcuts], [voice],
[extensions], [updates], [devshell], [server], [startup], [mcp],
[guardrails].
# ~/.aethon/config.toml
[ui]
theme = "ember" # built-ins: ember | paper | aether | brink |
# daylight | mist | nocturne ("signature" is a
# back-compat alias for aether); extension/
# loose-file themes are accepted once registered
font_size = 14 # clamps to 10-24 and writes --app-font-size
# thinking_visibility / tool_calls_visibility default to "hide" when unset
# restore_tabs is a deprecated no-op compat key (session restore is per-workspace)
[agent]
model = "anthropic/claude-sonnet-4-6" # seeds the picker/default display
thinking_level = "medium" # off|minimal|low|medium|high|xhigh|max|ultra
# codex_fast_mode, provider/bash/subagent timeouts, idle_retire_minutes
[mcp]
enabled = true # host-level Model Context Protocol support
project_configs = "require-approval" # auto-load | require-approval | never
[startup]
auto_approve = false # trust per-project .aethon/startup.toml commands
[devshell]
enabled = "auto" # "auto" | "always" | "never"
mode = "auto" # "auto" | "direnv" | "nix" | "nix-shell"
cache_ttl_hours = 720 # GC ceiling for on-disk snapshots; 0 disables
refresh_on_lockfile_change = truePer-project override at <project>/.aethon/devshell.toml (same shape; merges over the global section). Used to mark one repo as enabled = "never" while keeping devshell on globally. [mcp] and [startup] also read per-project files (.aethon/mcp.toml, .aethon/startup.toml).
Pi settings (~/.pi/agent/settings.json) drive provider/auth, model
discovery, and enabledModels filtering for the picker. Aethon's
config layers on top, not a replacement.
- A2UI layout engine — A2UI Container components with CSS Grid/Flexbox layout props. The layout extension uses these to define the default workspace. Agents and extensions use the same Container primitives to modify layout dynamically.
- Persistent state — Yes. Legacy frontend default-tab chat history
(
~/.aethon/messages.json), theme (~/.aethon/theme), UI zoom (~/.aethon/ui_zoom), and project list (~/.aethon/projects.json) persist across launches. Pi LLM context and restorable visible transcripts persist per-tab under~/.aethon/sessions/<tabId>/*.jsonlvia pi's own session manager.~/.aethon/state.jsonis a live snapshot of the bridge's runtime registry (extensions, themes, components, layout summary, tabs) — rewritten on every registration so the agent cancatit for an up-to-date view; not a layout persistence file. - Multiple canvases — Yes. Tabs provide one canvas per agent session, and native canvas windows provide separate restorable OS-window canvases for exploratory/custom UI that should not replace the main workspace chrome.
- Pi upstream — No. The A2UI extension API stays Aethon-specific. If pi wants it later, they can pull from our implementation. We don't gate our progress on upstream acceptance.
- Component sandboxing — None. Same-process, no iframe, no shadow DOM. Like pi itself: full trust, full speed. Users install what they choose to install. No permission system by default — yolo mode, matching pi's philosophy.
- Terminal integration — Embedded
xterm.jswith the WebGL renderer for GPU-accelerated terminal output. Pi's tools get shell access through the agent-tab terminal panel (read-only sink for bash output). M6 extends this with a second terminal mode — interactive PTY-backed user shell tabs (Tab kind ="shell") usingportable-ptyon the Rust side, with an opt-in per-tab sharing model so the agent can read or drive the user's shell on request. Both modes share the samexterm.jscomposite —readOnly: truein agent tabs,readOnly: falsein shell tabs.
Author: James Brink brink.james@gmail.com Project: utensils/aethon License: MIT