Terax loads TERAX.md from the workspace root as agent memory (similar to AGENTS.md / CLAUDE.md). This file is also the project's living architecture doc - read it before making changes.
Terax: open-source AI-native terminal emulator. Tauri 2 + Rust (portable-pty) backend, React 19 + TypeScript + xterm.js (webgl) client, BYOK AI via Vercel AI SDK v6.
- Bundle id:
app.crynta.terax - Package manager: pnpm
- Platforms: macOS, Linux, Windows
- Frontend checks:
pnpm lint,pnpm check-types,pnpm test - Rust checks:
cd src-tauri && cargo clippy --all-targets --locked -- -D warnings,cd src-tauri && cargo nextest run --locked(local fallback:cargo test --locked)
Production-grade or it does not ship. Every change is judged against all of these, not just "it works":
- Correctness: edge cases, failure modes, concurrent access. No "works for now".
- Performance: ultra-lightweight is the product. ~7-8 MB bundle, high-performance terminal. For every change ask: how much RAM it costs, whether it adds IPC round-trips or redundant requests, whether it triggers extra re-renders or wasted work, whether it pulls a heavy dependency. Unused features consume zero resources.
- Security: no critical security holes. Validate at every boundary (IPC, fs, network, AI tool surface). The secret-path deny-list applies on both read and write and is never bypassed.
- UI/UX: polished, professional, premium. Every state and detail considered.
- Architecture: new or changed logic lives in pure, dependency-light functions (functional core); tauri commands and React components stay thin (imperative shell). Keeps it testable without a later rewrite.
Verify before claiming done:
- Frontend:
pnpm lint,pnpm check-types,pnpm test - Rust:
cd src-tauri && cargo clippy --all-targets --locked -- -D warnings,cd src-tauri && cargo nextest run --locked(orcargo test --locked)
A change to a core subsystem (terminal/shell spawn, workspace auth, git, fs, IPC or AI tool surface) needs a test that locks the invariant.
- Comments: default to none, the code should explain itself. If genuinely needed, 1-2 lines on why, never what. No AI-generic filler.
- No em-dash anywhere: code, comments, commits, docs.
- No emojis anywhere.
- Imports: always
@/...on the frontend, never relative across modules. - pnpm only, never npm/npx/yarn.
Rust (src-tauri/) owns all OS access. The webview never touches the FS, processes, or shells directly - everything goes through invoke() calls to commands registered in src-tauri/src/lib.rs:
pty::pty_*- long-lived interactive PTY sessions (xterm ↔ portable-pty), managed byPtyState(RwLock<HashMap<id, Session>>). Output streams via a TauriChannel<PtyEvent>.fs::tree::*(fs_read_dir,list_subdirs),fs::file::*(fs_read_file,fs_write_file,fs_stat,fs_canonicalize),fs::mutate::*(fs_create_file,fs_create_dir,fs_rename,fs_move,fs_delete,fs_delete_batch): file explorer + editor IO.fs::search::*(fs_search,fs_list_files),fs::grep::*(fs_grep,fs_glob): fuzzy file finder + content search (powered byignore+grep-*crates).git::commands::*: full source-control surface (git_status,git_diff,git_diff_content,git_stage,git_unstage,git_discard,git_commit,git_fetch,git_pull_ff_only,git_push,git_log,git_show_commit,git_commit_files,git_commit_file_diff,git_panel_snapshot,git_resolve_repo,git_remote_url). All gated through the workspace authorization registry.shell::shell_run_command: one-shot subshell exec used by AI tools. Distinct from PTY sessions; not the user's interactive terminal. On Windows via PowerShell (-NoProfile -Command), on Unix via$SHELL -lc. Shared helperbuild_oneshot_command.shell::shell_session_*: persistent agent shell with state across calls.shell::shell_bg_*(spawn,logs,kill,list): long-running background processes (dev servers etc.) with bounded ring-buffer log capture.workspace::*:workspace_authorize/workspace_current_dir(the spawn/git/AI cwd authorization registry) plus the WSL bridge (wsl_list_distros,wsl_default_distro,wsl_home).lsp::*(lsp_detect,lsp_host_pid,lsp_resolve_root,lsp_spawn,lsp_send,lsp_kill): language server process host. Dumb JSON-RPC pipe: Content-Length framing + process lifecycle in Rust (lsp/framing.rs, pure + tested), protocol intelligence on the frontend. Spawn cwd gated through the workspace registry; binaries resolve via the captured login-shell env (lsp/env.rs, GUI apps get a bare PATH on macOS); root detection walks up to markers but never to or above$HOME. Servers run in their own process group on Unix and are group-killed (cargo check / proc-macro children die with the server); Windows children get aproc::job::ProcessJob(kill-on-close, shared with pty). All sessions killed onRunEvent::Exit.net::*(ai_http_request,ai_http_stream,lm_ping): AI HTTP proxy with SSRF guard; keeps provider calls and local-model pings off the webview.secrets::secrets_*: OS keychain via thekeyringcrate. Service constantterax-ai. Linux uses a file-based fallback gated behind#[cfg(target_os = "linux")].open_settings_window: separate webview window for Settings (optionaltabarg deep-links a section).vibrancy::window_*: native window backdrop (window_backdrop_kind,window_set_backdrop). macOS getsNSVisualEffectMaterial::UnderWindowBackground, Windows 11 gets Mica (gated on build >= 22000 viaRtlGetVersion, sinceapply_micafails on Windows 10), Linux reportsnonebecause blur there belongs to the compositor. Thewindow-vibrancycrate is a macOS/Windows-only dependency so Linux builds never pull it.
PTY shells are bootstrapped via injected init scripts in src-tauri/src/modules/pty/scripts/:
- Unix (
zshenv.zsh,zprofile.zsh,zlogin.zsh,zshrc.zsh,bashrc.bash) for zsh/bash, plusinit.fishinstalled to~/.config/fish/conf.d/terax.fishfor fish. Emit OSC 7 (cwd) and OSC 133 A/B/C/D (prompt boundaries + exit code) so the host can track cwd and detect command boundaries without re-parsing the prompt. Fish 4.0+ writes its own OSC 133 prompt markers; Terax setsfish_features=no-mark-promptand re-asserts its own prompt via-Cto avoid doubling. - Windows (
profile.ps1) - passed viapwsh -NoLogo -NoExit -ExecutionPolicy Bypass -File <path>. Wraps the user's existingpromptfunction (after their$PROFILEruns) to emit OSC 7 + OSC 133 A/B/D. Shell priority:pwsh.exe(PS 7+) →powershell.exe(PS 5.1) →cmd.exe(no integration). cwd is normalized to backslashes before being passed to ConPTY (CreateProcessWmisbehaves with forward-slash cwd).
pty/shell_init.rs is split into #[cfg(unix)] / #[cfg(windows)] modules - keep new platform-specific code in the right cfg arm.
ConPTY on Windows requires SPAWN_LOCK (Mutex) around openpty + spawn_command in session.rs. Concurrent spawns leave one of the resulting PTYs with a stalled output pipe. Don't remove the lock without verifying first-tab stability under fast tab spam.
Each ConPTY child is also assigned to a per-session Job Object with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE (pty/job.rs). When the Job HANDLE drops - clean shutdown, panic, or even SIGKILL'd Terax process - the kernel kills every descendant of the shell (e.g. npm run dev spawned from inside pwsh). Without this Windows orphans the entire process subtree because TerminateProcess only kills the immediate child. macOS/Linux rely on Drop for Session → killer.kill(); on dev-Ctrl-C of cargo run destructors don't fire and orphans are possible there too - acceptable for now since dev only.
AiComposerProvider is mounted unconditionally at the App.tsx root: a conditional wrapper would change the parent element type when keys load, remounting the entire tree (and re-spawning every PTY) the moment getAllKeys() resolves. Production happened to dodge this because keychain reads can land in the same paint frame; dev didn't. Keep the unconditional wrap.
Single-window React app. Path alias @/* → src/*. Tabs are a tagged union (kind: terminal | editor | preview | markdown | ai-diff | git-diff | git-history | git-commit-file) and not unmounted on switch - they're hidden via invisible pointer-events-none so PTYs and dev servers keep streaming in the background.
App.tsx wires modules together - keep it a coordinator. New features go inside the appropriate modules/<area>/.
Each module is self-contained, exports a thin barrel via index.ts, and owns its hooks under lib/.
- terminal/ -
TerminalStackkeeps one mounted xterm per tab viauseTerminalSession+pty-bridge.osc-handlers.tsparses OSC 7 (with Windows drive-letter normalization:/C:/Users/foo→C:/Users/foo) and OSC 133 markers. The xterm color palette is driven by the central theme engine (modules/theme), not a local table. Renderer slots are pooled (rendererPool.ts, max 5): a hidden leaf with a foreground job (OSC 133 C..D, agent signal, orpty_has_foreground_job) keeps its live grid parked with rendering paused viadisplay:none; an idle hidden leaf releases its slot but the buffer is retained and serialized lazily only when another leaf steals it. TheDormantRing(1 MiB, no terminal reset on overflow) buffers bytes only for leaves whose slot was stolen or never bound. Never serialize a leaf that is mid-command: replaying incremental TUI repaints over a snapshot is what used to wipe Claude Code. - editor/ - CodeMirror 6 stack (
EditorStackmirrorsTerminalStack).extensions.tsconfigures language modes; supports vim mode. Buffers live in LF space and the original EOL (lib/eol.ts, majority-vote detection) is restored on save; indent unit/tab size are detected per file (lib/indent.ts) via a per-pane compartment. Saves are conflict-checked against the disk mtime returned byfs_read_file/fs_write_file(mismatch → warning toast with explicit Overwrite, never silent last-writer-wins); external format-on-save only applies the disk read-back if the doc is unchanged since the save snapshot. Files over 10 MB offer "Open anyway" (hard cap 50 MB,forcearg); above 4 MB syntax highlighting and LSP stay off. Cmd-F routes to CodeMirror's own search panel (find/replace/regex) when an editor tab is active, Ctrl-G opens go-to-line; both panels styled inchromeTheme.ts. Format-on-save formatters live inlib/externalFormat.ts(FORMATTERSregistry: biome, prettier, ruff, rustfmt, gofmt, clang-format, shfmt, zig fmt, plus a custom{file}command template);resolveFormatterapplies per-language overrides (editorFormatterByLang) over the global default, and a global external default only runs on languages its tool understands. Diff panes resolve the language before mounting CodeMirror: a late compartment reconfigure leaves the merge view's deleted-chunk widgets unhighlighted. AI inline completion (lib/autocomplete/) sends the buffer's indent unit with the request and normalizes unambiguous tab/space mismatches in responses (normalizeIndent.ts); triggering isautocompleteTriggerauto or manual, witheditor.aiComplete/editor.codeCompleteregistry shortcuts (guarded to editor tabs so the keys fall through to terminals), and Tab accepts an open completion popup before the ghost. Multi-line ghosts render first-line-inline plus a block widget below the line (never inline<br>s); a closers-only line-suffix (cursor insidefn(|)) is hidden and re-appended after the block so the preview equals the accept result, and a line-suffix with real code caps the ghost to one line (capToLineSuffix). Suggestions echoing the recent prefix are dropped, multi-line suggestions and closing brackets never start on a line that ends with;, and closer-only lines are reindented from the previous line (trimSuggestion/reindentClosers, all tested). Markdown editing is GFM (markdownLanguagebase) with fenced-code highlighting resolved through the shared lazy language registry, Cmd/Ctrl+Click URLs, and clickable task checkboxes (markdownExtras.ts, all inside the lazy markdown chunk; the eager-budget test enforces this). Dotenv files (.env,.env.*, and*.env) use the lazy shell grammar. Editor theme is decoupled from the app theme: theeditorThemepref is"auto" | EditorThemeId(default"auto"), resolved at render time byuseEditorThemeExtviaresolveEditorThemeId. Inautothe editor follows the active app theme'seditorTheme[mode]pairing (live, never stale); an explicit pick overrides. Theme ids + labels live insettings/store.ts(EDITOR_THEMES/EDITOR_THEME_LABELS); the matching extensions ineditor/lib/themes.ts(EDITOR_THEME_EXT). Prebuilt@uiwthemes plus locally-built ones ineditor/lib/cmThemes.ts(Kanagawa wave/lotus/dragon, Everforest, Dracula, Solarized, Catppuccin, Rosé Pine) viacreateTheme(no extra deps). The three CM surfaces (EditorPane,AiDiffPane,GitDiffPane) all read the theme throughuseEditorThemeExt. Editor code size is stored separately aseditorFontSizeand does not affectterminalFontSize. - explorer/ - file tree with Material/Catppuccin icons (
iconResolver.ts), fuzzy search, keyboard nav, inline rename, context actions. Backslash-awarebasename. - preview/ - auto-detected dev-server preview tab (status-bar pill suggests opening when a localhost URL is detected).
- tabs/ -
useTabsis the source of truth for tab list + active id.useWorkspaceCwdderives explorer root + inherited cwd for new tabs from active tab.basenamesplits on both/and\. - header/ - top bar + inline search (
SearchInlineadapts to terminal vs editor viaSearchTarget).WindowControlsrendered whenUSE_CUSTOM_WINDOW_CONTROLSis true (Linux + Windows; macOS uses native traffic lights). - statusbar/ - bottom bar,
CwdBreadcrumb(handles Unix paths, Windows drive letters, and home~segments viapathUtils.segmentsFromCwd), AI tools indicator. - shortcuts/ - keymap registry (
shortcuts.ts) +useGlobalShortcuts. Handlers live inApp.tsxand are passed in by id (tab.new,ai.toggle, …).metaKey || ctrlKeyfor cross-platform Cmd/Ctrl. - settings/ - settings store (
store.tsviatauri-plugin-store), preferences hook, settings window opener. - sidebar/ - activity bar + collapsible side panels (explorer, source control, git history).
- source-control/ - git status / stage / commit panel and diff workflow.
- git-history/ - commit graph rail, refs, per-commit file diffs.
- lsp/ - opt-in language server support, zero cost until enabled (no process, no PATH check, nothing in the eager bundle beyond a 14.5 kB shell). Statusbar pill offers Enable (binary found) or Install (with copyable command) per language; activation persists as
lspActivationin the settings store (enabled/dismissed/unset).sessionManager.tskeys sessions by (server, workspace root), refcounts open docs, idle-kills after 3 min, and crash-backoffs (cooldown before respawn; 3 in 5 min → give up + toast with the server's stderr tail). Resource invariants: no root marker → no session (a dirname fallback once spawned a server per directory and burned GBs), hard cap of 4 sessions per server, lean per-presetinitializationOptions(rust-analyzer:cachePrimingoff + boundedlru; tsls:maxTsServerMemory). Client iscodemirror-languageserverbehind a lazy import, subclassed (lib/client.ts) to add didClose/didSave/shutdown,textDocument/references(Shift-F12; multi-result definitions and references share thelocationsPanel.tspicker) and the publishDiagnostics capability the lib forgets (tsls sends no diagnostics without it);lib/transport.tsbridges to the Rust pipe and answers server-to-client requests the lib ignores.vscode-languageserver-protocolis aliased to a 4-enum shim in vite.config.ts (~117 kB saved). Presets: typescript, rust-analyzer, pyright, ruff, gopls and more; custom stdio servers via Settings. Several presets can claim one language (pyright and ruff both takepy):serverForLanguageprefers the enabled candidate, so enabling ruff while pyright is unset or dismissed routes Python to ruff. WSL workspaces excluded for now. - markdown/ - markdown preview renderer (backs the
markdowntab kind). - workspace/ - workspace environment switching (Local + WSL distros).
- theme/ - custom theme engine (no
next-themes).ThemeProvider+applyThemewrite CSS variables; built-in presets inthemes/(terax-default - colours live inglobals.csssince ThemeProvider clears rather than applies for that id - xcode, claude, kanagawa, kanagawa-dragon, tokyo-night, catppuccin, rose-pine, everforest, nord, gruvbox, dracula, solarized, tide, sage, caffeine), each optionally declaring aneditorThemepairing consumed byresolveEditorThemeId(see editor/). User themes viacustomThemes.ts+validateTheme.ts, optional background image viabgImageStore.ts+SurfaceLayer. - updater/ - auto-updater UI built on
tauri-plugin-updater. - agents/ - agent launching, notifications, and management for both the built-in Terax agent and terminal coding agents (Claude Code, Codex, Gemini CLI, Pi, OpenCode, Grok). The header launcher (
components/AgentLauncherPanel.tsx+lib/launcher.ts) persists per-agent start commands in preferences and atomically builds balanced one-to-four-pane tabs. Shared store (store/agentStore.ts: terminalsessions+localAgent+notifications) and a shared router (lib/route.ts: suppress when focused-and-visible, OS-notify when unfocused, in-app Sonner toast when focused-but-hidden) feed the headerNotificationBell(management surface, Terax agent listed first, per-agent hook enable rows). Toasts use Sonner (components/ui/sonner.tsx) themed via the central engine;lib/agentIcon.tsxrenders the per-agent brand mark. Terminal detection is Rust-side (pty/agent_detect.rs) on the PTY reader's byte filter, armed onOSC 133;C;<cmd>or self-armed by the marker, emittingterax:agent-signaltransitions (started/working/attention/finished/exited) driven only by OSC sequences (never raw output, so a repainting TUI never flaps) - zero cost when no agent runs. Hook-backed terminal agents converge on the sameOSC 777marker the detector reads, installed viaagent_enable_hooks(agent)/agent_hooks_status(agent)inmodules/agent.rs(data-drivenAgentSpecfor JSON-hook agents plus a Terax-owned Pi extension; atomic writes, foreign configuration preserved, idempotent; gated onTERAX_TERMINAL). OpenCode and Grok use OSC 133 process-lifecycle detection but do not install attention hooks. Delivery differs because only Claude's hook protocol can return terminal bytes in the hook response: Claude (~/.claude/settings.json,UserPromptSubmit/Notification/Stop) returns the marker via theterminalSequencefield (legacy 3-fieldnotify;Terax;<event>). Codex (~/.codex/hooks.json,UserPromptSubmit/PermissionRequest/Stop) and Gemini (~/.gemini/settings.json,BeforeAgent/Notification/AfterAgent,matcher:"*") can't, so the hook command emits the 4-fieldnotify;Terax;<agent>;<event>marker itself (printf > /dev/ttyon Unix, orterax __terax_notifywriting toCONOUT$afterAttachConsoleon Windows) and prints{}as a JSON stdout no-op (Codex'sStopand Gemini both reject empty/non-JSON stdout). Pi (~/.pi/agent/extensions/terax-notifications.ts) usesagent_start/agent_settledextension events and writes its named marker directly to stdout. The agent-named marker lets a self-arm name the right agent when no preexec fired (bash/tmux/Windows). The Terax agent path isai/components/LocalAgentNotificationsBridge.tsx, mappingchatStore.agentMeta(awaiting-approval→attention, busy→idle→finished,error) into the same router. - command-palette/ - modal command palette (
CommandPalette.tsx,commands.ts) for actions and navigation. - spaces/ - workspace spaces/projects (name, root, env, color, per-space tab persistence) via
useSpacesandSpaceSwitcher. - ai/ - see below.
BYOK. Cloud providers via @ai-sdk/*: OpenAI, Anthropic, Google, xAI, Cerebras, Groq, DeepSeek, Mistral, OpenRouter, plus OpenAI-compatible for any custom base URL. Local / offline providers (key-optional, model id supplied at runtime): LM Studio, MLX, Ollama. Provider list in config.ts (PROVIDERS); model registry includes DEFAULT_MODEL_ID + DEFAULT_AUTOCOMPLETE_MODEL.
- Key storage: OS keychain via
keyring(Rust). Frontend reads/writes throughsecrets_*commands. ServiceKEYRING_SERVICE = "terax-ai". Never persist keys to disk, settings store, orlocalStorage. - Agent (
lib/agent.ts):Experimental_AgentwithstopWhen: stepCountIs(MAX_AGENT_STEPS)and the system prompt fromconfig.ts. Provider branching happens here - keep theAgent/DirectChatTransportshape; the rest of the system depends on AI SDK v6 chat semantics. - Sub-agents (
agents/registry.ts,agents/runSubagent.ts): named sub-agents with their own system prompts and tool subsets, invoked by the main agent viarun_subagenttool. - Sessions (
lib/sessions.ts+store/chatStore.ts): conversations are organized into named sessions, persisted viatauri-plugin-storeatterax-ai-sessions.json(list +activeId+ per-sessionmessages:<id>keys).chatStore.tskeeps a module-scopedMap<sessionId, Chat<UIMessage>>;getOrCreateChat(apiKey, sessionId)lazily constructs aChat, seeded with messages from a hydration map populated byhydrateSessions()(called once fromApp.tsx).AgentRunBridgemirrors active-session messages to disk on every change and auto-derives titles from the first user message. Switching the API key wipes the chat map; sessions persist. - Composer (
lib/composer.tsx): React context providing shared input state (text, attachments, voice) for both the dockedAiInputBarand any other surface. Attachments include image, text-file, andselectionkinds - selections come fromuseChatStore.attachSelection(text, source)(drained into chips, not pasted into the textarea) and are wrapped as<selection source="terminal|editor">…</selection>blocks at submit. Composer derivesisBusyfromagentMeta.statusso it can mount safely before sessions hydrate. - Voice input: streamed transcription pipeline. Toggled from the composer.
- Live context bridge:
App.tsxcallssetLive({ getCwd, getTerminalContext, … })so tools can read the currently active terminal's cwd + last 300 lines of buffer. Lazy by design - don't pre-snapshot. - Tools (
tools/tools.ts):read_file,list_directory,fs_search,fs_grepauto-execute.write_file,create_directory,rename,delete,run_command,shell_session_run,shell_bg_spawnsetneedsApproval: trueand the AI SDK pauses for an in-UI confirmation card. Auto-send after approval useslastAssistantMessageIsCompleteWithApprovalResponses.lib/security.tsis a deny-list refusing obvious secret paths (.env*,.ssh/, credentials, keychain dirs) - apply on both read and write paths and don't bypass it. - Edit diffs: AI-proposed edits open in a side-by-side diff tab (
ai-difftab kind); user accepts/rejects per hunk before the write tool actually runs. - Prompt snippets (
#handle): reusable prompt fragments surfaced in the composer. Do not describe these as skills; a reusable tool-bundled skills system is not implemented yet.
- shadcn/ui is configured (
components.json, styleradix-luma, basemist, icon lib hugeicons). Primitives insrc/components/ui/- don't hand-edit; re-runpnpm dlx shadcn addto upgrade. - AI Elements (Vercel) live in
src/components/ai-elements/from the@ai-elementsregistry incomponents.json. Same rule: regenerate, don't hand-patch - composition wrappers belong inmodules/ai/components/. - Tailwind v4 - no
tailwind.config.*, config is insrc/App.cssvia@theme. Usecn()from@/lib/utils. - Animation:
motion(Framer Motion successor). Resizable layout:react-resizable-panels. - Window vibrancy: the
windowVibrancypref drivesWindowVibrancyBridge(main window only -window_set_backdroptargets its caller).html[data-vibrancy="on"]makes<html>/<body>transparent and redefines--framewith alpha, so only the chrome frosts; panes keep--backgroundso terminal text stays on a solid surface and the xterm canvas still matches its container. The opaque colour the pre-paint script parks on<html>would cover the backdrop, soapplyVibrancyclears it while the effect is on; there is deliberately no localStorage fast path, since pre-declaring the effect would show a see-through window on any launch where the native call has not landed yet. Repeat applications are deduped, and only Mica is rebuilt on a light/dark flip (NSVisualEffectView adapts on its own). - Floating panes: header and status bar are window chrome painted on
--frame(derived from--card, so no theme declares it); the sidebar and the tab surface are.terax-panecards on--background- same tone as the xterm canvas. Panes meet the chrome flush and are inset only horizontally, because the header centers its content and any vertical gutter would stack onto that padding and read as asymmetric..terax-panecarries no drop shadow:react-resizable-panelsclips panel content at the panel box, so a shadow would only render on the gutter sides. - Path imports: always
@/…, never relative across modules. - Cross-platform paths: anywhere a path may originate from OSC 7, the explorer, or the OS, normalize separators with
.split(/[\\/]/)rather than.split("/"). - Canonical path form on the frontend is forward-slash.
homeDir()returns backslashes on Windows; convert at the boundary (App.tsx setHome). OSC 7 already arrives as forward-slash. Equal canonical strings keepuseFileTreefrom wiping its tree and flashing the explorer whentab.cwdfirst arrives.
- macOS:
titleBarStyle: Overlay+hiddenTitle: trueintauri.conf.json(native traffic lights via overlay).transparent: true+macOSPrivateApi: trueintauri.conf.jsonare whatNSVisualEffectViewrequires; that also means the macOS build uses a private API and is not App Store eligible. - Linux:
decorations: false+transparent: truefromtauri.linux.conf.json; re-asserted post-realize for GNOME/Mutter CSD. - Windows: same as Linux via
tauri.windows.conf.json. React renders customWindowControls.
src-tauri/capabilities/default.json is the allowlist for plugin APIs available to the webview. New plugins (dialog, autostart, updater, window-state, store, opener, os, log are wired in lib.rs) typically need:
Cargo.tomldependency.plugin(...)call inlib.rsrun()- capability entry in
default.json
- HOME / cache dirs: use the
dirscrate (dirs::home_dir(),dirs::cache_dir()), never raw$HOME/%USERPROFILE%. - Shell init scripts: gate Unix-only logic behind
#[cfg(unix)]; Windows arm inpty::shell_init::windows. - Terminal input: send
\r(CR) for Enter, not\n(LF) - PowerShell on Windows requires CR.
bundle.targets: "all"plus per-platform sections intauri.conf.json:- macOS:
minimumSystemVersion: 10.15. - Linux: deb depends
libwebkit2gtk-4.1-0,libgtk-3-0; rpmwebkit2gtk4.1,gtk3; AppImage bundles its media framework. - Windows: NSIS installer in
currentUsermode (no admin required), WebView2 viaembedBootstrapper(offline install).
- macOS:
- Auto-updater configured with a public minisign key; release artifacts at
https://github.com/crynta/terax-ai/releases/latest/download/latest.json.
- React 19 strict mode double-mounts
useEffectin dev → terminals spawn twice on first render. The first PTY is cleaned up almost immediately. TheSPAWN_LOCKmutex serializes this; don't be alarmed bypty opened id=1followed bypty closed id=1in dev logs. - Windows PowerShell process lifecycle:
killer.kill()fromportable-ptyonly kills the immediate child. Descendants (e.g.npm run devstarted inside pwsh) survive unless something else takes them down. The Job Object inpty/job.rshandles this for the Terax-process-death case; an explicitpty_closefrom JS also kills only the immediate child + relies on the Job to take the rest. Don't disable the Job without a replacement. - Tab
cwdstorage: comes from OSC 7 with forward slashes (afterparseOsc7strips/C:→C:). Anything that consumestab.cwdand passes it to a Rust fs command on Windows must normalize separators or accept both forms -apply_commoninpty::shell_inithandles this for PTY spawn; other call sites must do their own.
Long-form contributor guides live under docs/. These guides elaborate on TERAX.md; if anything conflicts, TERAX.md wins.
docs/README.md- index of contributor guidesdocs/architecture/two-process-model.md- IPC boundary and command referencedocs/architecture/pty-shell-integration.md- PTY, shell init scripts, OSC, ConPTY, Job Objectdocs/architecture/security-model.md- consolidated security model and boundariesdocs/architecture/ai-subsystem.md- AI stack, sessions, tools, adding a providerdocs/architecture/terminal-renderer-pool.md- renderer pool and DormantRing invariantsdocs/contributing/testing.md- testing contract and core-subsystem invariants