CanvasTTY follows Electron's three-layer model:
React renderer
│ typed window.canvasTTY API
▼
preload bridge (contextBridge)
│ allow-listed IPC channels
▼
Electron main process
├── SettingsStore → validated, atomic JSON persistence
├── TerminalSessionStore → opt-in, atomic terminal-window descriptors
├── TerminalManager → node-pty lifecycle, bounded scrollback, and output batching
├── LimitsService → sanitized provider-limit adapters and cache
├── PluginManager → GitHub install, manifest validation, assets, permissions, storage, hook trust registry
├── PluginServiceSupervisor → trusted plugin services as child processes, JSON-RPC over stdio
├── LaunchPipeline → asks chosen plugins' launch services, merges env/args/files or refuses
├── EnvironmentRegistry → plugin session environments: prepare, wrap, resume, release, describe
├── DecisionHooks → base protection (safety/*) then plugin canvastty.decide for PreToolUse / OpenCode tool calls
├── PluginAgentTools / PluginSessions / PluginCards → plugin tools in canvastty_agents, session events and owned-card control, card badges and actions
├── PluginSecretsService → OS-backed encrypted plugin credentials with fail-closed availability
├── PluginMediaService → user-granted music folders, ranged audio streams, playlist files
├── HermesHudService → permission-gated Hermes Desktop HUD lifecycle through a fixed control contract
├── BrowserService → tabs, shared persistent profile, downloads, presence, WebContentsView lifecycle
│ ├── BrowserStore / BrowserPolicyService / BrowserAuditStore
│ ├── BrowserCore / BrowserCommandDispatcher / BrowserAutomationService
│ └── AgentGateway → authenticated UDS/named pipe for the bundled stdio MCP helper
├── canvastty-plugin:// → CSP-constrained static plugin resources
├── canvastty-media:// → permission-checked local audio streams
└── native dialogs/window controls
src/shared/contracts.tsis the single public contract between processes. Add or change cross-process data here first.src/preload/index.tsexposes only the typed capabilities the renderer needs. Node integration stays disabled; context isolation and sandbox stay enabled.- Terminal file drops resolve native
Fileobjects through preload'swebUtils.getPathForFile, format paths for the host's default shell, and paste through xterm without submitting. File contents are not read and no new main-process IPC is exposed. src/main/ipc/registerIpc.tsowns native side effects and validates access to persisted media.src/main/services/TerminalManager.tsis the source of truth for live session state and PTY buffers. It keeps scrollback in a bounded chunk buffer and coalesces PTY data into 16ms IPC batches so clear/redraw sequences reach xterm together. A plain terminal startsidle; an agent staysunavailableuntil its provider emits a machine-readable lifecycle signal. Codex, Claude Code, Qwen Code, Kimi Code, OpenCode, Hermes, and Grok Build then transition throughidle,working, andneeds_approvalfrom provider hooks; exact Claude/Qwen OSC 0/2 markers remain a compatibility fallback. Human-readable terminal text and PTY existence are never treated as activity. Process exit provides onlydoneorfailed. An exited PTY may be restarted under the same session ID while preserving its card, bounds, title, and scrollback. Optional restart persistence (Settings → General, "Agent sessions after restart": Don't save / Reopen windows / Continue conversations) writes session records v2 throughTerminalSessionStore: the card descriptor, the state at quit or exit, the provider conversation id a lifecycle hook reported (threadId: Codex thread or Claude session UUID, OpenCodeses_id), the per-card restore flag, and two opaque plugin slots (launch options and an environment reference, at most 4 KB each). It never writes PTY scrollback, prompts, child environment, secrets, or capabilities. Plain terminals reopen as fresh shells in their saved folder.src/renderer/src/features/terminal/webglContextPool.tsdecides which terminal cards draw with xterm's WebGL renderer. Chromium keeps at most 16 WebGL contexts per renderer process, so the pool hands out 10: the focused card first, then by on-screen area (a busy holder keeps its context against a card less than 1.25× larger), then by most recent output. Camera and layout changes settle for 200 ms before contexts move; a card that leaves the screen, zooms above 1× or enters summary mode releases its context (and explicitly loses it) at once. Every other card uses the DOM renderer. A lost context puts the card back on DOM with its buffer intact and keeps it there for 30 s, doubling on repeats; returning to DOM refits the grid, because WebGL cells are snapped down to whole device pixels.src/main/services/LimitsService.tsreads Codex through the installed CLI's app-server protocol and Claude, Kimi, OpenCode Go, and Grok Build through their provider usage or billing endpoints. Qwen Code is multi-provider and exposes no provider-neutral read-only quota protocol, so its adapter reportscli-not-foundorunsupported-protocoland never invents percentages. Provider credentials are read only inside the trusted main process, sent only to the matching provider over HTTPS, and never logged or exposed over IPC. The service owns timeout, structural normalization, caching, stale fallback, and subprocess cleanup; raw provider responses never cross IPC.src/main/services/SettingsStore.tsnormalizes every update and persists through a serialized atomic write. Canvas regions and sticky notes have independent persistence gates: disabling one keeps its live objects for the current process but omits that collection from the disk snapshot and therefore from the next launch. The configurable canvas launcher and UI scale use the same boundary; transient window stacking does not.src/main/services/PluginManager.tsinstalls ready-to-run repositories without executing package scripts during install/update, rejects symlinks and oversized packages, persists the enabled registry, serves only contained package files, and enforces per-plugin permissions/storage quotas. Optional native agent-hook entries remain off by default; explicit per-hook trust is persisted in the plugin registry and compiled into a separate private atomic runtime registry. Update, module replacement, plugin disable, and uninstall revoke that trust before executable files change.src/main/services/PluginServiceSupervisor.tsruns theservicesof an apiVersion 2 plugin only after the separate per-plugin "native code" confirmation, which pins each entry's SHA-256 and is revoked like hook trust. Each service is aprocess.execPath+ELECTRON_RUN_AS_NODEchild in the plugin folder with an allow-listed environment (no provider keys, tokens,NODE_OPTIONS, orCANVASTTY_*), newline-delimited JSON-RPC 2.0 over stdio with 1 MB messages and 15 s request timeouts, restart with backoff (at most 5 in 10 minutes), and a bounded per-plugin log. Its host API islog, own-pluginstorage.*behind thestoragepermission, andeventto the plugin's own surfaces; surfaces reach only their own plugin's services (service.request).src/main/services/LaunchPipeline.tsruns before a card with plugin launch options is spawned (create, restart, restore). It sendscanvastty.launch.prepareto each chosen plugin's trusted launch service (5 s budget), validates the answers, merges them in plugin-id order, resolvessecretEnvfrom the plugin's own secrets in main (the values are masked byTerminalManager.redactSecretsin agent-readable text), writes per-run files, and refuses the launch on any refusal, timeout, error, conflict, reserved name or core-owned argument (coreOwnedLaunchArgument).TerminalManagerkeeps such a card waiting until the answer arrives and never launches it without the contribution; restore holds it stopped when the plugin is unavailable.src/main/services/EnvironmentRegistry.tstalks to trusted services that declareenvironments(environment:provide).TerminalManagercallscanvastty.environment.prepareonce for a card started in an environment and saves the opaque ref (≤4 KB) in the session record; before every start it callswrapand validates the answer (an absolute executable or a bare name resolved on PATH, never a shell string; launch-contributor env rules;secretEnvresolved in main and masked) and still spawns the PTY itself. Restore resumes all saved environments first (resume), then plans parents before children; an unavailable plugin, astoppedanswer or a timeout holds the card stopped with the reason and never runs it locally. Closing a card releases its environment with the person's "Keep environment data?" answer; quitting releases nothing unless saving is off (keepData: true).src/main/services/DecisionHooks.tsanswers the decision hook (src/agent-runtime/permission-gate.mjsas Claude Code/Codex/Qwen CodePreToolUse,opencode-decisions.mjsin OpenCode'stool.execute.before), which reachesRuntimeGatewayover the session's runtime capability. Base protection (safety/baseProtection.ts,commandFacts.ts,shellParse.ts: deny-only local rules, SettingsbaseProtectionEnabled) runs first; then trusted services that declaredecideanswercanvastty.decidein parallel (3 s). Any deny wins, a timeout or error is ask, an allow counts only with the plugin's separatedecisionsMayAllow. The hook fails closed: it is installed only when base protection is on or a decision plugin applies, and then a call it cannot check (no socket, refused, no answer within the helper deadline, an unreadable answer or hook input, or the gateway's own failure where the CLI cannot ask) is denied with "CanvasTTY safety check unavailable" instead of left to run.safety/SecretRedaction.tsis the redaction registry (vault values, launchsecretEnv,redaction.register, generic shapes) thatTerminalManager.redactSecretsapplies to every text one agent reads from another.src/main/services/PluginAgentTools.tslists trusted services'toolsper session role (the orchestration handler answers the helper'slist_tools;TerminalManagergives a non-orchestrator card thecanvastty_agentsbridge only when a plugin tool lists its role) and routes<pluginId>__<tool>calls tocanvastty.tools.call, masked and bounded.PluginSessions.tsturns terminal-manager events intocanvastty.sessions.eventnotifications (metadata; masked screen text only withsessions:read-screen) and runssessions.create/send/stopwith per-plugin ownership saved in the card's session record (ownerPluginId).PluginCards.tskeeps plain-text badges and declared card actions, pushes them to the renderer (plugins:card-decorations-changed) and callscanvastty.cards.invoke.src/main/services/PluginSecretsService.tsserializes per-plugin secret writes, encrypts the complete bounded payload through ElectronsafeStorage, rejects plaintext-only backends, and removes each encrypted file on uninstall.ProviderSecretsService.tsapplies the same architecture to provider API keys for BYOK-capable CLIs: values stay in the main process, and the renderer contract exposes only per-keyconfiguredflags plus set/clear actions.ApiProfilesettings entries name model backends (protocol, HTTPS base URL, secret reference) for the same BYOK runtimes; they are not agent providers, and the settings normalizer drops invalid profiles instead of repairing them.src/main/services/PluginMediaService.tspersists per-plugin grants only after a native folder choice, hides absolute paths, skips symlinks, and serves contained audio with HTTP Range semantics. Playlist reads stay inside granted libraries; writes are bounded and atomic under the library'sPlaylists/directory.src/main/services/HermesHudService.tsis the only plugin-facing native application controller. It resolves the installed Hermes CLI through the immutable provider registry, sends only the fixed--hud/--quitcontrol commands, and derives visible state from Hermes Desktop's validated live runtime record. It never accepts executable paths, arguments, PIDs, or arbitrary commands from plugin code.src/main/services/BrowserService.tsis the only owner of the built-in browser'sWebContentsViewtabs and shared persistent partition. Remote pages have no preload or Node access, keep context isolation and sandbox enabled, and cannot request hardware, location, notification, clipboard-read, certificate-bypass, or external-protocol capabilities. HTTP(S) popups are adopted as internal tabs; other schemes are rejected.src/main/services/browser/contains the browser kernel.BrowserStoreatomically persists only tab order, active tab, and safe restore URLs.BrowserPolicyServicecentralizes URL, permission, download, and upload rules; validated uploads are copied through an already-open no-follow file descriptor into private staging before Chromium sees them.BrowserAutomationServiceattaches Electron's internal debugger to the existing live tab without a remote-debugging port.BrowserCommandDispatcheradds revisions, revision-bound refs, mutation request deduplication, per-tab FIFO mutation lanes, bounded concurrency, typed errors, and redacted fail-closed audit for agent mutations.src/main/services/agent-browser/exposes the kernel only through an authenticated user-local Unix socket (0600) or Windows named pipe. The Windows pipe is created by the bundled native host with a protected DACL containing only the exact current-user SID and rejects remote clients. Each agent PTY receives a one-use bootstrap capability through its child environment. A successful authentication rotates it to a session-scoped reconnect capability held only in helper memory; duplicate bootstrap authentication is accepted only while the sameconnectionIdis already live, and every capability is revoked when the PTY ends. The bundled stdio MCP helper is the only protocol adapter; no TCP listener, cookie/storage endpoint, arbitrary evaluation tool, or raw CDP surface exists.src/main/services/agent-runtime/is a separate lifecycle boundary and is not controlled by the Browser access switch. When CanvasTTY status hooks are enabled, every agent PTY receives a distinct capability for a protected user-local socket/pipe. Provider command hooks and the OpenCode event plugin may report only the fixed status enum, bounded event name, and optional opaque turn/prompt ID; prompt text, responses, tool input, and arbitrary telemetry are rejected by the exact gateway schema. Electron helper commands carryELECTRON_RUN_AS_NODE=1inside the exact hook command only; the provider PTY never inherits that process-mode flag, so a provider cannot accidentally launch a second CanvasTTY GUI instance. The user can revoke this capability from Agents settings, immediately returning live agent status tounavailable; re-enabling requires a new/restarted PTY. Explicitly trusted plugin hooks use a separate process runner which re-checks the private PluginManager registry on every invocation and strips CanvasTTY internal capabilities before passing the provider payload to third-party code. Provider-native review remains an independent gate; CanvasTTY does not bypass Codex hook trust globally.- Lifecycle adapters use launch-only settings for Claude, Codex, Qwen, and OpenCode. Kimi, Hermes, and Grok, whose hook discovery is home-config based, receive ownership-checked temporary entries shared across live CanvasTTY sessions. Kimi and Hermes keep recovery journals and exact backups; Grok uses a dedicated owned hook file. Cleanup restores exact original bytes when no concurrent edit occurred and otherwise removes only CanvasTTY-owned entries.
TerminalManagerinjects the MCP helper per launch without leaving permanent provider configuration. Claude Code, Codex, and Qwen Code receive CLI arguments; Qwen gets one inline--mcp-configentry that overrides only the CanvasTTY server name and leaves unrelated user servers available. OpenCode receives a merged launch-onlyOPENCODE_CONFIG_CONTENTentry plus one scoped browser-tool permission; Kimi uses its per-run MCP configuration when supported. Older Kimi versions receive a compare-and-swap temporary CanvasTTY entry and one exact permission rule with an atomic recovery journal. Hermes receives a temporarymcp_servers.canvastty_browserentry inHERMES_HOME/config.yaml(defaulting to~/.hermes/config.yamlon POSIX or%LOCALAPPDATA%\hermes\config.yamlon Windows); sensitive capability values stay as child-environment placeholders. Temporary Kimi and Hermes configuration remains until the final owning PTY session ends, then exact original bytes are restored when safe. A journal repairs an interrupted Hermes launch at the next CanvasTTY startup, while compare-and-swap checks preserve concurrent user edits. Unrelated MCP entries, credentials, and file/shell permissions are preserved. Qwen, OpenCode, and Hermes YOLO remain launch-only and do not change persistent permission settings.src/main/services/providerCliRegistry.tsis the single owner of provider CLI discovery. During main-process startup it creates a shared snapshot for every provider inPROVIDER_CLI_DEFINITIONS— each definition declares the executable command names it may install as (which may differ from the provider ID, e.g. a provider shipping asmcode) and optional known home-relative or Windows LOCALAPPDATA-relative directories — by checking smoke-only overrides, the inheritedPATH, platform defaults, and those known directories in that order. Available entries retain an absolute executable, launcher kind, and supplemented childPATH; POSIX entries must be executable files and Windows entries must be supported native or batch launchers.TerminalManager,LimitsService, agent-browser probes, and provider smoke tests consume that same snapshot and never repeat command lookup. Missing entries produce a failed session with copyable checked-path diagnostics before PTY or temporary browser configuration creation, while the limit adapter reportscli-not-found; its HOME row is hidden until explicitly selected after CLI detection. Launchers keep a provider without a local CLI visible when an account bound to a remote computer or a saved container profile provides another route. CanvasTTY never reads shell startup scripts. Agents settings can recheck candidate paths, atomically replace the registry snapshot, reconcile saved launcher and limit selections, and refresh CLI-bound adapters without restarting the app. Existing sessions keep running; a newly found CLI remains disabled until selected.
The optional Web companion reuses EvenG2Controller and its SRP pairing, encrypted request channel, per-device grants and terminal/session access. Controls configures either an exact https://<device>.<tailnet>.ts.net origin or the Android USB loopback origin http://127.0.0.1:3481; both select a loopback-only 127.0.0.1:3481 listener instead of Even G2's LAN listener. Tailscale Serve terminates HTTPS and proxies to loopback; alternatively, an authorized Android USB debugging connection can use external adb reverse tcp:3481 tcp:3481 to forward the phone's loopback port, with broader device trust that should be revoked after testing. Host-local processes and browsers can also reach loopback, but SRP, encryption, desktop approval and grants still apply; neither mode needs a public listener, and CanvasTTY neither installs Tailscale nor enables Funnel. The packaged integrations/mobile/dist assets are served under /mobile/; /g2/api/mobile accepts only narrow session operations (not filesystem, browser, plugins, or raw agent control). Main supplies sanitized provider availability to the overview; TerminalManager still owns and validates creation. Main resolves the static directory from dev or packaged resources. See setup and limitations.
The primary BrowserWindow is created and shown with a lightweight local startup page before settings, plugins, media, and IPC services initialize. Successful initialization replaces that page with the trusted renderer; bootstrap failures replace it with a visible error page and retain a native-dialog fallback. The main process holds Electron's single-instance lock; a rejected second launch raises the running window through the second-instance handler so the app never appears to ignore a launch, while background plugin and browser requests never restore, show, or focus an existing window. Native browser contents are focused programmatically only while their owner BrowserWindow is already focused; explicit user pointer input remains the only cross-surface focus route.
Runtime plugin code is never imported into main or the trusted renderer bundle. HOME widgets and canvas apps run in sandboxed iframes with an opaque origin. Separate plugin windows use a dedicated narrow preload which forwards the same message SDK through an IPC handler that verifies the actual canvastty-plugin://<id>/<entry> sender URL. Explicitly trusted agent hooks and plugin services run only in isolated child processes, not in either trusted JavaScript context; they are privileged OS code rather than sandboxed web contributions. Arbitrary native OS windows are not embedded.
Plugin music access is capability-based rather than generic filesystem access. Media scans return library IDs, relative paths, metadata, and canvastty-media:// stream URLs; raw playlist text remains the only format-neutral file content exposed. A media URL is resolved only for the owning enabled plugin and only beneath a previously selected library root. Removing a plugin revokes its persisted folder grants.
The built-in browser is split across surfaces: BrowserCard renders trusted window chrome, tabs, navigation, agent badges, downloads, dialogs, and canvas geometry, while BrowserService positions the active native view over the measured viewport. The native view remains live while the card or camera moves and receives frame-coalesced geometry updates; it is hidden during semantic summary, HOME editing, trusted modal surfaces, or while a higher canvas layer overlaps its card, so native content cannot break the renderer-owned window stack. Fractional renderer bounds expand to enclosing device-independent pixels, and the active tab view is reparented only when the active tab actually changes. A typed pointer bridge reports native-page click and hover activity back to canvas selection and explicitly restores native page focus without preventing page input. A transparent trusted mouse-passthrough window draws optional live agent cursors above the native view; Wayland uses an isolated-world fallback. A connection or heartbeat alone never creates presence: badges appear only after an actual browser command, and cursors appear only after a real pointer position exists.
Renderer IPC and the agent gateway call the same BrowserCore.execute(actor, command, signal) boundary. Reads may run concurrently. Mutations are ordered FIFO per tab while different tabs remain independent; a repeated mutation request ID returns the recorded result. Navigation and document changes advance the revision, so stale accessibility refs fail before side effects. Agent activity is recorded as a redacted append-only hash chain: typed/page text, screenshots, URL query/fragment, credentials, headers, cookies, and tokens are not stored.
App.tsx is the orchestration boundary. It loads settings/sessions, subscribes to main-process events, and coordinates dialogs and persistence. Feature components do not call unrelated feature APIs.
App
├── WorkspaceCanvas camera, pan, zoom, spatial composition, context dispatch, and stacking
│ ├── HomeZone persisted resizable grid, visible boundary, and edit gestures
│ │ ├── homeModel pure derivation of limit/active-session rows
│ │ └── HomeMediaWidget independent pick/replace/remove control
│ ├── TerminalCard one live xterm view, selection, rename, drag, resize, and snap behavior
│ ├── CanvasRegion persisted named color field, drag/resize, and spatial window grouping
│ ├── StickyNoteCard persisted text/bounds with drag, eight-way resize, and deferred text writes
│ ├── CanvasContextMenu target-specific empty-canvas, region, and note commands
│ ├── CanvasCommandPalette searchable sessions and the same global creation/launch actions
│ ├── PluginCanvasCard sandboxed plugin app with canvas bounds and semantic summary
│ ├── BrowserCard trusted browser chrome and canvas geometry for the native WebContentsView
│ └── CanvasMinimap viewport/entity overview, camera recentering, and canvas-direction drag panning
├── AgentLaunchDialog fixed provider + folder + profile + launch
└── SettingsPanel two-pane icon-sidebar modal for General, Appearance, Agents, Controls, Browser, Plugins, and About
├── AgentHooksSettings built-in status revocation and explicit plugin-hook trust
├── PluginServicesSettings per-plugin native-code trust, service state and log
├── AboutSettings app identity and expandable hook/data/security FAQ
└── PluginSettingsSection install preview, permissions, registry, and contributions
Keep domain decisions in pure selectors such as homeModel.ts, orchestration in App.tsx, and rendering/local interaction in feature components. IPC calls belong in App.tsx or a feature that exclusively owns that capability.
WorkspaceCanvas is the sole trusted owner of canvas context-menu hit testing. It leaves terminal, Browser, plugin, and editable-text context menus native. It also owns one deterministic layer order for terminal, plugin, Browser, and note cards: every ordinary primary-pointer activation raises the hit card independently of camera-focus settings. Browser native-pointer callbacks enter the same path.
When session restore is on, startup loads validated records before the renderer and restores parents before children (sessionRestorePlan.ts). "Continue conversations" resumes the conversation id the card's lifecycle hook reported (codex resume <id>, claude --resume <id>, opencode --session <id>); without one Codex opens its own resume picker, and other CLIs use their "latest in this folder" flag only when that CLI has exactly one card in the folder, and otherwise start fresh and say so on the card. A plain Restart starts a new conversation and forgets the id; Continue on a stopped card resumes it. Cards that had already exited come back stopped with Restart / Continue, cards marked "Don't restore this card" do not come back, and a card placed in an environment that is unavailable comes back stopped with the reason and is never started locally. Stable CanvasTTY session IDs preserve card identity, while region membership remains spatial and requires the complete card bounds to be inside the region at region-drag start. Grok restoration still waits for the renderer-measured xterm grid before spawning. Turning restore off clears the descriptor store immediately; it remains off by default.
- Home requests a terminal or opens a provider-specific launch card.
Appsends a typedterminal:createrequest.TerminalManagervalidates the request, spawns the PTY, stores metadata and bounded chunked scrollback, then emits lifecycle events and 16ms-batched data events. Grok is the measured-grid exception: its card is created first and the PTY starts only after xterm reports the actual rows and columns.Appreconciles lifecycle snapshots by session ID and a main-owned monotonic metadata revision, so a delayed IPC response cannot overwrite a newer hook state.TerminalCardsubscribes to its PTY stream, sends PTY input/grid resize events, and commits typed canvas bounds after a drag or edge resize.
SessionMetadata owns both world-space position and card size. App reconciles those bounds, while TerminalCard may hold transient pointer-move geometry until pointer-up. The main process validates and clamps committed sizes before emitting a session snapshot. Camera wheel handling is limited to empty canvas; interactive surfaces keep their native scroll/input ownership.
A live TerminalCard owns one xterm instance for the lifetime of its session ID. Palette changes update terminal.options.theme in place; title and settings changes must never dispose the terminal or its renderer-side scrollback. Window titles are updated as session metadata through terminal:rename. PTY input and resize events that race with process exit are contained at the main-process boundary and never surface as uncaught Electron errors.
The optional native Codex frontend can be connected on macOS and Linux through explicit QA paths or bundled resources. Its POSIX launcher owns an official app-server and a patched TUI connected over a private Unix socket; ordinary CLI launches keep their existing path. The local Linux setup and manual checks are documented in docs/linux-native-editing.md.
Output batching is an IPC/rendering boundary, not a history boundary: every PTY chunk is appended to bounded scrollback immediately, while pending renderer output is flushed on the 16ms timer, before exit, and before disposal. One timer serves every session: the sessions with queued output flush together, and TerminalRendererOutbox sends the renderer's share of that flush as one terminal:data-batch message (session and removal events first flush what was collected before them, so order is kept). The preload hands each event only to the card of that session (TerminalDataRouter), not to every card. Scrollback trimming advances through chunks instead of rebuilding the entire buffer for every write; snapshots join only the retained suffix.
Terminal cards subscribe before requesting a fresh terminal:read-buffer snapshot. The snapshot and live batches carry the cumulative UTF-16 output offset, which survives history trimming and in-place restarts. The renderer removes their overlap before writing to xterm, so delayed batches cannot duplicate replayed history and output before subscription is recovered by the snapshot.
Terminal pointer coordinates are converted from the canvas's visually transformed rectangle back to xterm layout coordinates before selection or wheel handling. Terminal and canvas wheel direction are normalized independently from persisted settings. Clipboard, scrollback search, page scrolling and exited-session restart use the configured terminal shortcuts. Clipboard text enters xterm through Terminal.paste rather than synthetic keystrokes. Codex terminals preserve every combination of Shift, Alt, Ctrl, and Super on modified Enter through CSI-u on every platform; unbound plain Enter stays a carriage return, and the CLI decides what each key does.
General Settings owns macOS, Windows, Linux and Custom keyboard presets, with editable canvas, terminal and Codex shortcuts directly below the preset. New profiles use host defaults; existing bindings migrate to Custom. A named preset replaces bindings, and an individual edit selects Custom. SettingsStore validates bindings and duplicates by dispatch context. TerminalManager passes only native Codex editor bindings to the optional frontend through CANVASTTY_CODEX_KEYBOARD and publishes the launch snapshot in SessionMetadata.nativeEditor. TerminalCard uses that snapshot to encode editor keys with modifiers; changing settings updates canvas/terminal actions immediately but requires a new or restarted process for editor bindings. Ordinary CLI launches retain their own editor keymaps.
Application shortcuts are normalized in SettingsStore, matched in App, and rendered from the same persisted bindings in the canvas hint. App owns the exclusive selected canvas application and the selected terminal session used by window actions such as rename. TerminalCard owns xterm focus and only the inline editor; BrowserService owns native page focus. Alt+arrow canvas focus navigation yields to terminal surfaces and editable fields on every platform, before preventing the key event, so agent TUIs receive their own shortcuts. Pressing empty canvas clears either selection. Optional hover focus uses the same configured entry/exit delay for terminals and the built-in browser; focus-in/focus-out sequences produced by a terminal's programmatic transition are suppressed before PTY input so agent TUIs do not reset their history position.
Session counters, progress bars, and statuses must always derive from actual SessionSnapshot values. The UI must not synthesize telemetry.
Apprequests a sanitizedLimitsSnapshotat bootstrap and every 60 seconds.LimitsServicededuplicates refreshes and keeps a 60-second cache.- Codex is queried through
codex app-serverusingaccount/rateLimits/read. Claude, Kimi, OpenCode Go, and Grok Build use their read-only usage or billing endpoints with credentials already managed by each installed CLI. Qwen Code reports an explicit unavailable reason because one Qwen CLI session may use unrelated cloud or local providers and the CLI has no universal quota-read protocol. OpenCode Go contributes its real rolling, weekly, and monthly windows; Grok Build contributes its real shared billing period. Real responses are structurally validated and reduced to percentage, window, and reset time. - If a refresh fails after a successful read, the last valid snapshot is returned as stale. Missing or unsupported adapters return an explicit unavailable reason, never
0%. - Claude usage is requested with the OAuth token from the current user's Claude CLI credentials. Missing or unreadable credentials are
not-authenticated; CanvasTTY never infers a missing subscription from local credential state and never parses provider TUI screens.
- Add a provider in
ProviderId,providers.ts,TerminalManager.resolveLaunch, the official provider asset map, and an optional safe limit adapter. - Add a persisted setting to
AppSettings, defaults/normalization inSettingsStore, and the owning feature only. Settings owns user-facing canvas controls and shortcuts; camera math and snapping geometry remain pure renderer concerns. - Add a canvas entity as a separate feature component with an explicit position and callbacks; keep camera ownership in
WorkspaceCanvas. - Publish a runtime extension with
canvastty.plugin.jsonAPI v1 (or v2 forservices) and static HTML/CSS/JS entries. Contribution kinds arehome-widget,canvas-app, andwindow; capability access is restricted to declared permissions. See Runtime plugins.
Every extension should pass npm run typecheck, npm run build, and a real Electron interaction check.