Skip to content

Latest commit

 

History

History
177 lines (130 loc) · 29.5 KB

File metadata and controls

177 lines (130 loc) · 29.5 KB

TERAX.md

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.

Project

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)

Quality bar

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 (or cargo 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.

Conventions

  • 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.

Architecture

Two-process model

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 by PtyState (RwLock<HashMap<id, Session>>). Output streams via a Tauri Channel<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 by ignore + 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 helper build_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 a proc::job::ProcessJob (kill-on-close, shared with pty). All sessions killed on RunEvent::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 the keyring crate. Service constant terax-ai. Linux uses a file-based fallback gated behind #[cfg(target_os = "linux")].
  • open_settings_window: separate webview window for Settings (optional tab arg deep-links a section).
  • vibrancy::window_*: native window backdrop (window_backdrop_kind, window_set_backdrop). macOS gets NSVisualEffectMaterial::UnderWindowBackground, Windows 11 gets Mica (gated on build >= 22000 via RtlGetVersion, since apply_mica fails on Windows 10), Linux reports none because blur there belongs to the compositor. The window-vibrancy crate is a macOS/Windows-only dependency so Linux builds never pull it.

PTY shell integration

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, plus init.fish installed to ~/.config/fish/conf.d/terax.fish for 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 sets fish_features=no-mark-prompt and re-asserts its own prompt via -C to avoid doubling.
  • Windows (profile.ps1) - passed via pwsh -NoLogo -NoExit -ExecutionPolicy Bypass -File <path>. Wraps the user's existing prompt function (after their $PROFILE runs) 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 (CreateProcessW misbehaves 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.

Frontend (src/)

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>/.

Module layout (src/modules/)

Each module is self-contained, exports a thin barrel via index.ts, and owns its hooks under lib/.

  • terminal/ - TerminalStack keeps one mounted xterm per tab via useTerminalSession + pty-bridge. osc-handlers.ts parses OSC 7 (with Windows drive-letter normalization: /C:/Users/fooC:/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, or pty_has_foreground_job) keeps its live grid parked with rendering paused via display:none; an idle hidden leaf releases its slot but the buffer is retained and serialized lazily only when another leaf steals it. The DormantRing (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 (EditorStack mirrors TerminalStack). extensions.ts configures 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 by fs_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, force arg); 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 in chromeTheme.ts. Format-on-save formatters live in lib/externalFormat.ts (FORMATTERS registry: biome, prettier, ruff, rustfmt, gofmt, clang-format, shfmt, zig fmt, plus a custom {file} command template); resolveFormatter applies 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 is autocompleteTrigger auto or manual, with editor.aiComplete / editor.codeComplete registry 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 inside fn(|)) 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 (markdownLanguage base) 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: the editorTheme pref is "auto" | EditorThemeId (default "auto"), resolved at render time by useEditorThemeExt via resolveEditorThemeId. In auto the editor follows the active app theme's editorTheme[mode] pairing (live, never stale); an explicit pick overrides. Theme ids + labels live in settings/store.ts (EDITOR_THEMES/EDITOR_THEME_LABELS); the matching extensions in editor/lib/themes.ts (EDITOR_THEME_EXT). Prebuilt @uiw themes plus locally-built ones in editor/lib/cmThemes.ts (Kanagawa wave/lotus/dragon, Everforest, Dracula, Solarized, Catppuccin, Rosé Pine) via createTheme (no extra deps). The three CM surfaces (EditorPane, AiDiffPane, GitDiffPane) all read the theme through useEditorThemeExt. Editor code size is stored separately as editorFontSize and does not affect terminalFontSize.
  • explorer/ - file tree with Material/Catppuccin icons (iconResolver.ts), fuzzy search, keyboard nav, inline rename, context actions. Backslash-aware basename.
  • preview/ - auto-detected dev-server preview tab (status-bar pill suggests opening when a localhost URL is detected).
  • tabs/ - useTabs is the source of truth for tab list + active id. useWorkspaceCwd derives explorer root + inherited cwd for new tabs from active tab. basename splits on both / and \.
  • header/ - top bar + inline search (SearchInline adapts to terminal vs editor via SearchTarget). WindowControls rendered when USE_CUSTOM_WINDOW_CONTROLS is true (Linux + Windows; macOS uses native traffic lights).
  • statusbar/ - bottom bar, CwdBreadcrumb (handles Unix paths, Windows drive letters, and home ~ segments via pathUtils.segmentsFromCwd), AI tools indicator.
  • shortcuts/ - keymap registry (shortcuts.ts) + useGlobalShortcuts. Handlers live in App.tsx and are passed in by id (tab.new, ai.toggle, …). metaKey || ctrlKey for cross-platform Cmd/Ctrl.
  • settings/ - settings store (store.ts via tauri-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 lspActivation in the settings store (enabled/dismissed/unset). sessionManager.ts keys 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-preset initializationOptions (rust-analyzer: cachePriming off + bounded lru; tsls: maxTsServerMemory). Client is codemirror-languageserver behind a lazy import, subclassed (lib/client.ts) to add didClose/didSave/shutdown, textDocument/references (Shift-F12; multi-result definitions and references share the locationsPanel.ts picker) and the publishDiagnostics capability the lib forgets (tsls sends no diagnostics without it); lib/transport.ts bridges to the Rust pipe and answers server-to-client requests the lib ignores. vscode-languageserver-protocol is 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 take py): serverForLanguage prefers 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 markdown tab kind).
  • workspace/ - workspace environment switching (Local + WSL distros).
  • theme/ - custom theme engine (no next-themes). ThemeProvider + applyTheme write CSS variables; built-in presets in themes/ (terax-default - colours live in globals.css since 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 an editorTheme pairing consumed by resolveEditorThemeId (see editor/). User themes via customThemes.ts + validateTheme.ts, optional background image via bgImageStore.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: terminal sessions + 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 header NotificationBell (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.tsx renders the per-agent brand mark. Terminal detection is Rust-side (pty/agent_detect.rs) on the PTY reader's byte filter, armed on OSC 133;C;<cmd> or self-armed by the marker, emitting terax:agent-signal transitions (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 same OSC 777 marker the detector reads, installed via agent_enable_hooks(agent) / agent_hooks_status(agent) in modules/agent.rs (data-driven AgentSpec for JSON-hook agents plus a Terax-owned Pi extension; atomic writes, foreign configuration preserved, idempotent; gated on TERAX_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 the terminalSequence field (legacy 3-field notify;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-field notify;Terax;<agent>;<event> marker itself (printf > /dev/tty on Unix, or terax __terax_notify writing to CONOUT$ after AttachConsole on Windows) and prints {} as a JSON stdout no-op (Codex's Stop and Gemini both reject empty/non-JSON stdout). Pi (~/.pi/agent/extensions/terax-notifications.ts) uses agent_start/agent_settled extension 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 is ai/components/LocalAgentNotificationsBridge.tsx, mapping chatStore.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 useSpaces and SpaceSwitcher.
  • ai/ - see below.

AI subsystem (src/modules/ai/)

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 through secrets_* commands. Service KEYRING_SERVICE = "terax-ai". Never persist keys to disk, settings store, or localStorage.
  • Agent (lib/agent.ts): Experimental_Agent with stopWhen: stepCountIs(MAX_AGENT_STEPS) and the system prompt from config.ts. Provider branching happens here - keep the Agent / DirectChatTransport shape; 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 via run_subagent tool.
  • Sessions (lib/sessions.ts + store/chatStore.ts): conversations are organized into named sessions, persisted via tauri-plugin-store at terax-ai-sessions.json (list + activeId + per-session messages:<id> keys). chatStore.ts keeps a module-scoped Map<sessionId, Chat<UIMessage>>; getOrCreateChat(apiKey, sessionId) lazily constructs a Chat, seeded with messages from a hydration map populated by hydrateSessions() (called once from App.tsx). AgentRunBridge mirrors 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 docked AiInputBar and any other surface. Attachments include image, text-file, and selection kinds - selections come from useChatStore.attachSelection(text, source) (drained into chips, not pasted into the textarea) and are wrapped as <selection source="terminal|editor">…</selection> blocks at submit. Composer derives isBusy from agentMeta.status so it can mount safely before sessions hydrate.
  • Voice input: streamed transcription pipeline. Toggled from the composer.
  • Live context bridge: App.tsx calls setLive({ 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_grep auto-execute. write_file, create_directory, rename, delete, run_command, shell_session_run, shell_bg_spawn set needsApproval: true and the AI SDK pauses for an in-UI confirmation card. Auto-send after approval uses lastAssistantMessageIsCompleteWithApprovalResponses. lib/security.ts is 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-diff tab 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.

UI conventions

  • shadcn/ui is configured (components.json, style radix-luma, base mist, icon lib hugeicons). Primitives in src/components/ui/ - don't hand-edit; re-run pnpm dlx shadcn add to upgrade.
  • AI Elements (Vercel) live in src/components/ai-elements/ from the @ai-elements registry in components.json. Same rule: regenerate, don't hand-patch - composition wrappers belong in modules/ai/components/.
  • Tailwind v4 - no tailwind.config.*, config is in src/App.css via @theme. Use cn() from @/lib/utils.
  • Animation: motion (Framer Motion successor). Resizable layout: react-resizable-panels.
  • Window vibrancy: the windowVibrancy pref drives WindowVibrancyBridge (main window only - window_set_backdrop targets its caller). html[data-vibrancy="on"] makes <html>/<body> transparent and redefines --frame with alpha, so only the chrome frosts; panes keep --background so 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, so applyVibrancy clears 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-pane cards 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-pane carries no drop shadow: react-resizable-panels clips 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 keep useFileTree from wiping its tree and flashing the explorer when tab.cwd first arrives.

Window styling

  • macOS: titleBarStyle: Overlay + hiddenTitle: true in tauri.conf.json (native traffic lights via overlay). transparent: true + macOSPrivateApi: true in tauri.conf.json are what NSVisualEffectView requires; that also means the macOS build uses a private API and is not App Store eligible.
  • Linux: decorations: false + transparent: true from tauri.linux.conf.json; re-asserted post-realize for GNOME/Mutter CSD.
  • Windows: same as Linux via tauri.windows.conf.json. React renders custom WindowControls.

Tauri capabilities

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:

  1. Cargo.toml dependency
  2. .plugin(...) call in lib.rs run()
  3. capability entry in default.json

Cross-platform conventions

  • HOME / cache dirs: use the dirs crate (dirs::home_dir(), dirs::cache_dir()), never raw $HOME / %USERPROFILE%.
  • Shell init scripts: gate Unix-only logic behind #[cfg(unix)]; Windows arm in pty::shell_init::windows.
  • Terminal input: send \r (CR) for Enter, not \n (LF) - PowerShell on Windows requires CR.

Bundle config

  • bundle.targets: "all" plus per-platform sections in tauri.conf.json:
    • macOS: minimumSystemVersion: 10.15.
    • Linux: deb depends libwebkit2gtk-4.1-0, libgtk-3-0; rpm webkit2gtk4.1, gtk3; AppImage bundles its media framework.
    • Windows: NSIS installer in currentUser mode (no admin required), WebView2 via embedBootstrapper (offline install).
  • Auto-updater configured with a public minisign key; release artifacts at https://github.com/crynta/terax-ai/releases/latest/download/latest.json.

Known gotchas

  • React 19 strict mode double-mounts useEffect in dev → terminals spawn twice on first render. The first PTY is cleaned up almost immediately. The SPAWN_LOCK mutex serializes this; don't be alarmed by pty opened id=1 followed by pty closed id=1 in dev logs.
  • Windows PowerShell process lifecycle: killer.kill() from portable-pty only kills the immediate child. Descendants (e.g. npm run dev started inside pwsh) survive unless something else takes them down. The Job Object in pty/job.rs handles this for the Terax-process-death case; an explicit pty_close from JS also kills only the immediate child + relies on the Job to take the rest. Don't disable the Job without a replacement.
  • Tab cwd storage: comes from OSC 7 with forward slashes (after parseOsc7 strips /C:C:). Anything that consumes tab.cwd and passes it to a Rust fs command on Windows must normalize separators or accept both forms - apply_common in pty::shell_init handles this for PTY spawn; other call sites must do their own.

Further reading

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 guides
  • docs/architecture/two-process-model.md - IPC boundary and command reference
  • docs/architecture/pty-shell-integration.md - PTY, shell init scripts, OSC, ConPTY, Job Object
  • docs/architecture/security-model.md - consolidated security model and boundaries
  • docs/architecture/ai-subsystem.md - AI stack, sessions, tools, adding a provider
  • docs/architecture/terminal-renderer-pool.md - renderer pool and DormantRing invariants
  • docs/contributing/testing.md - testing contract and core-subsystem invariants