From 4b77da7ed4f5dbd96bcca98abf151428afdc106a Mon Sep 17 00:00:00 2001 From: Venkatesh prasath Date: Mon, 29 Jun 2026 10:08:24 +0530 Subject: [PATCH] docs: align verify commands with CI, fix doc drift, add architecture guides --- .github/PULL_REQUEST_TEMPLATE.md | 11 +- CODE_OF_CONDUCT.md | 6 +- CONTRIBUTING.md | 61 ++++++--- README.md | 12 +- SECURITY.md | 14 +- TERAX.md | 99 ++++++++------ docs/README.md | 22 ++++ docs/architecture/ai-subsystem.md | 97 ++++++++++++++ docs/architecture/pty-shell-integration.md | 104 +++++++++++++++ docs/architecture/security-model.md | 96 ++++++++++++++ docs/architecture/terminal-renderer-pool.md | 65 ++++++++++ docs/architecture/two-process-model.md | 135 ++++++++++++++++++++ docs/contributing/testing.md | 82 ++++++++++++ 13 files changed, 726 insertions(+), 78 deletions(-) create mode 100644 docs/README.md create mode 100644 docs/architecture/ai-subsystem.md create mode 100644 docs/architecture/pty-shell-integration.md create mode 100644 docs/architecture/security-model.md create mode 100644 docs/architecture/terminal-renderer-pool.md create mode 100644 docs/architecture/two-process-model.md create mode 100644 docs/contributing/testing.md diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 9dcc208b4..e5503a711 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,5 +1,5 @@ @@ -13,12 +13,15 @@ Examples: feat(terminal): add split panes / fix(explorer): close button alignmen ## Testing - -- [ ] `pnpm exec tsc --noEmit` clean +- [ ] `pnpm lint` clean +- [ ] `pnpm check-types` clean +- [ ] `pnpm test` clean - [ ] Manual smoke-test of the affected feature -- [ ] (If you touched `src-tauri/`) `cargo test --locked` and `cargo clippy --all-targets --locked -- -D warnings` clean +- [ ] (If you touched `src-tauri/`) `cargo clippy --all-targets --locked -- -D warnings` clean +- [ ] (If you touched `src-tauri/`) `cargo nextest run --locked` clean (or `cargo test --locked`) - [ ] (If you changed a `#[tauri::command]` signature) called out below so the FE caller can be updated in lockstep - [ ] (If UI) tested in `pnpm tauri dev` - [ ] Platforms tested: diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 045963df7..bc660b0cc 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -5,16 +5,16 @@ Terax is a small open-source project and we want it to stay a place people enjoy ## The rules, briefly - **Be respectful.** Disagreement is fine; rudeness, condescension, and personal attacks are not. -- **Assume good faith.** Most miscommunication isn't malicious — clarify before escalating. +- **Assume good faith.** Most miscommunication isn't malicious - clarify before escalating. - **Stay on topic.** Issues, PRs, and discussions are about Terax. Take off-topic conversations elsewhere. -- **No harassment.** Targeted insults, slurs, sustained disruption, sexualized comments, doxxing, or threats are not tolerated — anywhere, against anyone. +- **No harassment.** Targeted insults, slurs, sustained disruption, sexualized comments, doxxing, or threats are not tolerated - anywhere, against anyone. - **No spam.** That includes promotional links, irrelevant cross-posting, and AI-generated noise that doesn't engage with the actual conversation. This applies to everything inside the project: issues, PRs, discussions, commits, and any community space we create later (Discord, etc.). ## Enforcement -If you see a violation — or experience one — email **crynta.dev@gmail.com** with subject `[Terax conduct]`. Include links and context. +If you see a violation - or experience one - email **crynta.dev@gmail.com** with subject `[Terax conduct]`. Include links and context. Maintainers may, at their discretion: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4bef29c0b..2ac198fc2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -22,6 +22,8 @@ pnpm tauri dev Prereqs: Rust (stable), Node 20+, pnpm, plus your platform's [Tauri prerequisites](https://tauri.app/start/prerequisites/). +For the architecture and how to contribute safely, see [TERAX.md](TERAX.md) and the [docs/ index](docs/README.md). + ## Where to discuss Discord: [Crynta OS](https://discord.gg/tyveTUyEp7) @@ -74,9 +76,12 @@ A 10-minute conversation saves a 500-line PR that doesn't fit the roadmap. Terax positions itself as **lightweight, fast, production-grade**. Every PR is reviewed against: -- `pnpm exec tsc --noEmit` clean -- `cargo clippy` clean, `cargo fmt` applied -- `pnpm test` and `cargo test` pass +- `pnpm lint` clean +- `pnpm check-types` clean +- `pnpm test` clean +- `cargo clippy --all-targets --locked -- -D warnings` clean +- `cargo nextest run --locked` clean (or `cargo test --locked`) +- `cargo fmt` applied before pushing - No perf regressions in known hot paths: terminal renderer, PTY stream, AI streaming, source control, file explorer - No new heavy dependencies (>50KB gzip in client bundle, >5MB compiled on Rust side) without justification - Platform parity preserved (macOS / Linux / Windows / WSL still work) @@ -179,27 +184,45 @@ Within a PR, individual commit messages can be free-form (they get squashed or g ``` src-tauri/ Rust backend - src/modules/ - pty/ Terminal sessions, shell integration, DA filter - fs/ File system commands - git/ Source control - net/ AI HTTP proxy with SSRF guard - workspace/ WSL bridge, workspace env - -src/ + src/ + lib.rs Tauri command registration + modules/ + agent.rs Terminal coding-agent hook installer/status + fs/ File system commands (read/write/search/grep) + git/ Source control commands + history/ Shell history integration + mod.rs Module exports + net.rs AI HTTP proxy with SSRF guard + proc.rs Process utilities + pty/ Terminal sessions, shell integration, DA filter + secrets.rs OS keychain access + shell/ Oneshot/session/background shell commands + workspace.rs WSL bridge, workspace env, authorization registry + +src/ React frontend + App.tsx Top-level coordinator + components/ shadcn/ui + AI Elements modules/ - terminal/ xterm.js sessions, OSC handlers, renderer pool + agents/ Agent notifications and management + ai/ Agents, sessions, tools, providers, composer + command-palette/ Modal command palette and actions editor/ CodeMirror stack, AI autocomplete explorer/ File tree - tabs/ Tab/split model - ai/ Agents, sessions, tools, providers, mini-window git-history/ Git graph and history pane - source-control/ Source control panel - preview/ Image / Markdown / web preview + header/ Top bar, search, window controls + markdown/ Markdown preview renderer + preview/ Dev server, image, and web preview settings/ Settings UI and preferences store - shortcuts/ Keymap - app/ Top-level App.tsx - components/ shadcn/ui + AI Elements + shortcuts/ Keymap registry + sidebar/ Activity bar and side panels + source-control/ Source control panel + spaces/ Workspace spaces/projects with per-space tab persistence + statusbar/ Bottom bar and cwd breadcrumb + tabs/ Tab/split model + terminal/ xterm.js sessions, OSC handlers, renderer pool + theme/ Custom theme engine and presets + updater/ Auto-updater UI + workspace/ Workspace environment switching ``` ## FAQ diff --git a/README.md b/README.md index 6371752cc..42a5e1499 100644 --- a/README.md +++ b/README.md @@ -106,7 +106,7 @@ Latest installers are on the [Releases](https://github.com/crynta/terax-ai/relea ### Linux notes - **Arch / AUR:** `yay -S terax-bin` (or `paru`, etc.). Tracks the latest release. -- **NixOS / Nix**: use the official flake — `nix profile install github:crynta/terax-ai` (non-NixOS), or import the flake and add `inputs.terax.packages.${pkgs.system}.terax` to `environment.systemPackages` (NixOS). The `nixosModules.terax` output is also available for a simpler setup. +- **NixOS / Nix**: use the official flake - `nix profile install github:crynta/terax-ai` (non-NixOS), or import the flake and add `inputs.terax.packages.${pkgs.system}.terax` to `environment.systemPackages` (NixOS). The `nixosModules.terax` output is also available for a simpler setup. - **AppImage:** needs FUSE. Without it: `./Terax_*.AppImage --appimage-extract-and-run`. On Wayland with rendering glitches, try `WEBKIT_DISABLE_DMABUF_RENDERER=1`. Otherwise the `.deb` / `.rpm` packages link against the system GTK stack and tend to be smoother. ## Configure AI @@ -131,9 +131,11 @@ pnpm tauri build # production bundle **Checks** ```bash -pnpm exec tsc --noEmit # frontend type-check -cd src-tauri && cargo clippy --all-targets --locked -D warnings # Rust lint (matches CI) -cd src-tauri && cargo test --locked # Rust tests +pnpm lint +pnpm check-types +pnpm test +cd src-tauri && cargo clippy --all-targets --locked -- -D warnings # Rust lint (matches CI) +cd src-tauri && cargo nextest run --locked # or: cargo test --locked ``` ## Tech stack @@ -142,7 +144,7 @@ Tauri 2, Rust, `portable-pty`, React 19, TypeScript, Vite, xterm.js, CodeMirror ## Contributing -Issues and PRs are welcome! Feel free to open issues, suggest features, or submit pull requests. See [CONTRIBUTING.md](CONTRIBUTING.md) for more details. +Issues and PRs are welcome! Feel free to open issues, suggest features, or submit pull requests. See [CONTRIBUTING.md](CONTRIBUTING.md) and the [architecture docs](docs/README.md) for more details. ## License diff --git a/SECURITY.md b/SECURITY.md index 4af0634da..ed2992dd7 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,6 +1,6 @@ # Security -Terax runs shells, reads/writes files, and talks to AI providers — so security bugs matter. If you find one, please tell us before posting it publicly. +Terax runs shells, reads/writes files, and talks to AI providers, so security bugs matter. If you find one, please tell us before posting it publicly. ## Reporting @@ -10,30 +10,30 @@ Email **security@terax.app**. Include: - Steps to reproduce (a small PoC is great) - Version, OS, arch -We'll get back to you within a few days. Once it's fixed, we'll credit you in the release notes — unless you'd rather stay anonymous. +We'll get back to you within a few days. Once it's fixed, we'll credit you in the release notes - unless you'd rather stay anonymous. Please **don't** open a public GitHub issue for security reports. ## Supported versions -Until `1.0.0`, only the latest minor gets security fixes. Right now that's `0.5.x`. +Until `1.0.0`, only the latest minor gets security fixes. See the current version in `package.json` or on the [Releases page](https://github.com/crynta/terax-ai/releases). ## What's in scope - The Rust backend in `src-tauri/` (PTY, FS, IPC, plugins) -- The frontend in `src/` — anywhere untrusted input lands (terminal output, file content, AI tool results, credentials) +- The frontend in `src/` - anywhere untrusted input lands (terminal output, file content, AI tool results, credentials) - Release artifacts on GitHub and `terax.app` - The auto-updater ## What's not -- Bugs in upstream deps (Tauri, xterm.js, CodeMirror, AI SDKs…) — report those upstream. We'll ship the fix once it's released. +- Bugs in upstream deps (Tauri, xterm.js, CodeMirror, AI SDKs…) - report those upstream. We'll ship the fix once it's released. - Anything that needs an already-compromised machine or a local attacker with shell access - Older versions (`< 0.5`) ## What we do to keep things safe -- **API keys** live in the OS keychain via `keyring` — not on disk, not in `localStorage`, not in logs. +- **API keys** live in the OS keychain via `keyring` - not on disk, not in `localStorage`, not in logs. - **No telemetry.** Terax only talks to the network when you ask it to (AI requests, update checks, web preview). - **AI tool approval.** File writes and shell commands from the agent need your OK before they run. - **No Node in the renderer.** The frontend only reaches the host through the allow-listed Tauri commands. @@ -43,4 +43,4 @@ Until `1.0.0`, only the latest minor gets security fixes. Right now that's `0.5. - Terax runs whatever you (or the agent) tell it to run, with your permissions. That's kind of the point of a terminal. - AI providers see whatever you send them. Read their retention policies. -- Local LLM endpoints (LM Studio, OpenAI-compatible) are trusted at the network level — only point Terax at servers you control. +- Local LLM endpoints (LM Studio, OpenAI-compatible) are trusted at the network level - only point Terax at servers you control. diff --git a/TERAX.md b/TERAX.md index 6fd78b3a1..c6763cb13 100644 --- a/TERAX.md +++ b/TERAX.md @@ -1,16 +1,16 @@ # 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. +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. +**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 && cargo test --locked` +- 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 @@ -22,7 +22,12 @@ Production-grade or it does not ship. Every change is judged against all of thes - **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: `pnpm lint`, `pnpm check-types`, `pnpm test`, `cargo clippy`, `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. +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 @@ -36,9 +41,9 @@ Verify before claiming done: `pnpm lint`, `pnpm check-types`, `pnpm test`, `carg ### 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`: +**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>`). Output streams via a Tauri `Channel`. +- `pty::pty_*` - long-lived interactive PTY sessions (xterm ↔ portable-pty), managed by `PtyState` (`RwLock>`). Output streams via a Tauri `Channel`. - `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_delete`): 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. @@ -53,66 +58,68 @@ Verify before claiming done: `pnpm lint`, `pnpm check-types`, `pnpm test`, `carg 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`) — installed via `ZDOTDIR` (zsh) or `--rcfile` (bash). 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. -- **Windows** (`profile.ps1`) — passed via `pwsh -NoLogo -NoExit -ExecutionPolicy Bypass -File `. 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). +- **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 `. 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. +`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. +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. +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//`. +`App.tsx` wires modules together - keep it a coordinator. New features go inside the appropriate `modules//`. ### 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/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, 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. 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`. -- **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. -- **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, 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 notifications + management for both the built-in Terax agent and terminal coding-agents (Claude Code, Codex, Gemini CLI). 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 (Terax logo, Claude/ChatGPT/Gemini hugeicon). Terminal detection is Rust-side (`pty/agent_detect.rs`) on the PTY reader's byte filter, armed on `OSC 133;C;` 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. All three 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` per agent; atomic write, never clobbers invalid JSON, prunes empty groups, idempotent; gated on `TERAX_TERMINAL`). 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;`). **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;;` 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). 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. -- **ai/** — see below. +- **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/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, 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. 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`. +- **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. +- **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, 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 notifications + management for both the built-in Terax agent and terminal coding-agents (Claude Code, Codex, Gemini CLI). 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 (Terax logo, Claude/ChatGPT/Gemini hugeicon). Terminal detection is Rust-side (`pty/agent_detect.rs`) on the PTY reader's byte filter, armed on `OSC 133;C;` 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. All three 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` per agent; atomic write, never clobbers invalid JSON, prunes empty groups, idempotent; gated on `TERAX_TERMINAL`). 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;`). **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;;` 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). 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**, 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`. +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. +- **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:` keys). `chatStore.ts` keeps a module-scoped `Map>`; `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 `` blocks at submit. Composer derives `isBusy` from `agentMeta.status` so it can mount safely before sessions hydrate. +- **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 `` 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. +- **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. - **Skills / snippets**: reusable prompt fragments + tool-bundles surfaced in the composer. ### 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`. +- **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`. - 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("/")`. @@ -135,7 +142,7 @@ BYOK. Cloud providers via `@ai-sdk/*`: **OpenAI, Anthropic, Google, xAI, Cerebra - 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. +- Terminal input: send `\r` (CR) for Enter, not `\n` (LF) - PowerShell on Windows requires CR. ### Bundle config @@ -149,4 +156,16 @@ BYOK. Cloud providers via `@ai-sdk/*`: **OpenAI, Anthropic, Google, xAI, Cerebra - **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. +- **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 diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 000000000..a00e18d75 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,22 @@ +# Terax contributor documentation + +This directory holds long-form contributor and maintainer guides. `TERAX.md` at the repo root is the living architecture doc and the source of truth; these guides elaborate on specific areas without duplicating it. + +If a guide conflicts with `TERAX.md`, `TERAX.md` wins. + +## Getting started + +- [TERAX.md](../TERAX.md) - the architecture source of truth; read this first +- [CONTRIBUTING.md](../CONTRIBUTING.md) - how to contribute, quality bar, project layout + +## Architecture guides + +- [Two-process model and IPC command reference](architecture/two-process-model.md) - Rust owns all OS access; the webview talks through `invoke()`. Command catalog and how to add a new command. +- [PTY shell integration](architecture/pty-shell-integration.md) - PTY sessions, shell init scripts, OSC 7 / 133, ConPTY, SPAWN_LOCK, Job Object, WSL. +- [Security model](architecture/security-model.md) - deny-list, SSRF guard, workspace authorization, AI tool approval, IPC allowlist, OSC trust, keychain handling. +- [AI subsystem](architecture/ai-subsystem.md) - providers, agent, sub-agents, sessions, composer, tools, edit diffs, live context bridge. Includes a walkthrough for adding a new provider. +- [Terminal renderer pool](architecture/terminal-renderer-pool.md) - slot pooling, the DormantRing, and the never-serialize-mid-command invariant. + +## Contributing guides + +- [Testing](contributing/testing.md) - the testing contract, how to run checks, and what makes a good core-subsystem test. diff --git a/docs/architecture/ai-subsystem.md b/docs/architecture/ai-subsystem.md new file mode 100644 index 000000000..544278f7b --- /dev/null +++ b/docs/architecture/ai-subsystem.md @@ -0,0 +1,97 @@ +# AI subsystem + +This guide elaborates on `TERAX.md`. If anything here conflicts with `TERAX.md`, `TERAX.md` wins. + +## Overview + +The AI subsystem is BYOK (bring your own key). It supports cloud providers via `@ai-sdk/*` and local / offline providers via OpenAI-compatible endpoints. The agent layer is built on Vercel AI SDK v6 chat semantics: `streamText`, tool definitions, and `stopWhen` step limits. + +Main entry point: `runAgentStream` in `src/modules/ai/lib/agent.ts`. + +## Providers + +Cloud providers are defined in `src/modules/ai/config.ts`: + +- OpenAI, Anthropic, Google, xAI, Cerebras, Groq, DeepSeek, Mistral, OpenRouter +- `openai-compatible` for any custom base URL +- Local: LM Studio, MLX, Ollama + +`buildLanguageModel` in `src/modules/ai/lib/agent.ts:76` branches on `provider` to construct the correct AI SDK provider instance. Local providers use `createOpenAICompatible` with a `localProxyFetch` that allows private-network access, while cloud providers use their dedicated SDK constructors. + +Model metadata (context limits, costs, reasoning behavior) lives in the model registry in `config.ts`. `resolveModel` maps a model id to its provider and defaults. + +### Adding a new provider + +1. Add a `ProviderInfo` entry to `PROVIDERS` in `src/modules/ai/config.ts`. +2. Add model ids and metadata to the model registry in the same file. +3. Add a branch in `buildLanguageModel` (`src/modules/ai/lib/agent.ts:99`) that constructs the provider instance. For OpenAI-compatible APIs you can often reuse `createOpenAICompatible`. +4. If the provider requires an API key, update `providerNeedsKey` in `config.ts` and the keyring service mapping. +5. If it needs a dedicated `@ai-sdk/*` package, add it to `package.json` and justify the bundle cost (see `CONTRIBUTING.md`). +6. New built-ins must justify unique value beyond `openai-compatible` and OpenRouter; `CONTRIBUTING.md` calls this out explicitly. + +Keys are never persisted outside the OS keychain / Linux secrets file. + +## Agent run loop + +`runAgentStream` (`agent.ts:391`): + +1. Resolves the model via `buildConfiguredLanguageModel`. +2. Builds a stable system prompt from `selectSystemPrompt(modelId)` plus optional persona, custom instructions, and `TERAX.md` project memory. +3. Converts UI messages to model messages, prunes reasoning content if the model does not keep it, and compacts old messages if the context limit is exceeded. +4. Streams via `streamText` with the tool set from `buildTools(ctx)` and `stopWhen: stepCountIs(MAX_AGENT_STEPS)`. +5. Emits step labels, usage deltas, and finish metadata. + +The tool set is assembled in `src/modules/ai/tools/tools.ts` from `fs`, `edit`, `search`, `shell`, `subagent`, `terminal`, `todo`, and `managedAgent` builders. + +## Sub-agents + +`src/modules/ai/agents/registry.ts` defines read-only sub-agents: `explore`, `code-review`, `security`, and `general`. Each has a whitelist of tools and its own system prompt. `run_subagent` cannot recurse (the subagent tool set excludes `run_subagent` itself). + +## Sessions + +Conversations are organized into sessions. Persistence lives in `terax-ai-sessions.json` via `tauri-plugin-store` (`src/modules/ai/lib/sessions.ts`): + +- `sessions` key: list of session metadata +- `activeId` key: active session id +- `messages:` keys: per-session messages, loaded lazily + +`AgentRunBridge` mirrors active-session messages to disk on every change and auto-derives titles from the first user message. + +## Composer + +`AiComposerProvider` (`src/modules/ai/lib/composer.tsx`) is a React context that holds shared input state (text, attachments, voice) for the docked input bar and any other surface. Attachments can be images, text files, or `selection` chips from the terminal or editor. Selections are wrapped as `` blocks at submit time and are not pasted into the textarea. + +The composer derives `isBusy` from `agentMeta.status` so it can mount safely before sessions hydrate. + +## Tools and approval + +Tool definitions live under `src/modules/ai/tools/`: + +- Read-only tools (`read_file`, `list_directory`, `grep`, `glob`) auto-execute after passing the security deny-list. +- Mutating tools (`write_file`, `edit`, `multi_edit`, `create_directory`, `bash_run`, `bash_background`, `bash_session_run`) set `needsApproval: true`. The AI SDK pauses and the UI renders an approval card. +- `edit` / `multi_edit` enforce a read-before-edit invariant: the model must have read the file earlier in the session. +- In plan mode, mutating tools queue edits for batch review instead of applying them immediately. + +Auto-send after approval uses `lastAssistantMessageIsCompleteWithApprovalResponses`. + +## Edit diffs + +AI-proposed file edits open in an `ai-diff` tab. The user accepts or rejects per hunk. Only after acceptance does the `write_file` or `edit` tool actually run. This keeps the approval UI decoupled from the tool execution. + +## Live context bridge + +`App.tsx` calls `setLive({ getCwd, getTerminalContext, … })` so tools can read the currently active terminal's cwd and the last 300 lines of buffer. It is lazy by design - tools call for it only when needed rather than pre-snapshotting every turn. + +## Invariants + +- Keep the Vercel AI SDK v6 chat shape (`streamText`, tools, step limits); the rest of the UI depends on it. +- Keys only via `secrets_*` commands; never disk, settings store, or `localStorage`. +- New providers must justify their bundle cost and unique value. +- Mutating tools require approval; read-only tools still pass the deny-list. + +## See also + +- [`TERAX.md`](../../TERAX.md) - the architecture source of truth +- [`docs/README.md`](../README.md) - index of contributor guides +- [Two-process model](two-process-model.md) - IPC boundary and command catalog +- [Security model](security-model.md) - the boundaries every tool must respect diff --git a/docs/architecture/pty-shell-integration.md b/docs/architecture/pty-shell-integration.md new file mode 100644 index 000000000..f81a96721 --- /dev/null +++ b/docs/architecture/pty-shell-integration.md @@ -0,0 +1,104 @@ +# PTY shell integration + +This guide elaborates on `TERAX.md`. If anything here conflicts with `TERAX.md`, `TERAX.md` wins. + +## Session model + +A terminal tab maps to one PTY session. Sessions live in `PtyState` (`src-tauri/src/modules/pty/mod.rs:20`): + +```rust +pub struct PtyState { + sessions: RwLock>>, + next_id: AtomicU32, +} +``` + +IDs start at 1 and monotonically increase; they are never reused so the frontend can treat `0` as unset. + +`pty_open` (`mod.rs:44`) spawns a session on a blocking thread, inserts it into the map, and returns the id. Output streams through a `Channel`; exit codes stream through a separate `Channel`. `pty_write` (`mod.rs:100`) accepts raw bytes with an `x-pty-id` header to avoid JSON serialization on every keystroke. + +## Reader / flusher / waiter threads + +`session::spawn` (`session.rs:102`) starts three threads per session: + +1. **Reader** - reads bytes from the PTY master, runs the DA filter and agent detector, and pushes filtered bytes into a pending buffer. +2. **Flusher** - coalesces output and sends it to the frontend over the data channel. +3. **Waiter** - waits for the child process to exit, flushes the tail, and emits the exit code. + +The pending buffer is capped at 4 MiB; on overflow it is discarded and replaced with an SGR-reset notice so xterm state is not corrupted by a sliced CSI sequence. + +## Shell bootstrapping + +`shell_init::build_command` (`shell_init.rs:53`) builds the `CommandBuilder` used to spawn the shell. The path and arguments depend on the platform and the selected workspace environment (Local or a WSL distro). + +### Unix + +Integration scripts live in `src-tauri/src/modules/pty/scripts/`: + +- `zshenv.zsh`, `zprofile.zsh`, `zlogin.zsh`, `zshrc.zsh` for zsh +- `bashrc.bash` for bash +- `init.fish` for fish, installed to `~/.config/fish/conf.d/terax.fish` + +Zsh is launched with `ZDOTDIR` pointing at a temp directory that sources our scripts and then the user's real configs. Bash uses `--rcfile` with a wrapper that sources the user's `~/.bashrc` after Terax's. Fish uses `conf.d` so no user file is replaced. + +All integrated shells emit **OSC 7** (cwd) and **OSC 133 A/B/C/D** (prompt boundaries and exit code) so Terax can track cwd and detect command boundaries without parsing the user's prompt. + +### Windows + +On Windows the shell priority is: + +1. `pwsh.exe` (PowerShell 7+) +2. `powershell.exe` (Windows PowerShell 5.1) +3. `cmd.exe` (no integration) + +PowerShell loads `profile.ps1` via: + +```text +pwsh -NoLogo -NoExit -ExecutionPolicy Bypass -File +``` + +The profile wraps the user's existing `prompt` function to emit OSC 7 + OSC 133 A/B/D after `$PROFILE` runs. The cwd is normalized to backslashes before being passed to ConPTY because `CreateProcessW` misbehaves with forward slashes. + +### Fish 4.0+ + +Fish 4.0 writes its own OSC 133 prompt markers. To avoid doubling, Terax sets `fish_features=no-mark-prompt` and re-asserts its own prompt via `-C` after `config.fish` runs. + +## Concurrency and process lifetime on Windows + +### `CONPTY_LIFECYCLE_LOCK` + +`openpty + spawn_command` and the corresponding close are serialized by a static mutex in `session.rs:71`. Concurrent ConPTY lifecycle calls corrupt the new console so its shell never pumps output. + +### Job Object + +Each ConPTY child is assigned to a Windows Job Object with `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE` (`job.rs:34`). When the Job HANDLE drops - clean shutdown, panic, or even a SIGKILL'd Terax process - the kernel kills every descendant of the shell. Without this, `TerminateProcess` only kills the immediate child and `npm run dev` started inside pwsh would be orphaned. + +On macOS and Linux, `Drop for Session` calls `killer.kill()`. Dev `Ctrl-C` of `cargo run` can still leave orphans because destructors may not run; that is acceptable for development only. + +## Input and escape-sequence handling + +### DA filter + +PowerShell / PSReadLine sends a cursor-position query (`ESC[6n`) at startup and blocks until it gets an answer. The `DaFilter` (`da_filter.rs`) intercepts that query and replies on the PTY input so the shell does not hang. + +### Agent detection + +The reader thread runs an `AgentDetector` (`agent_detect.rs`) over the byte stream. It is armed by `OSC 133;C;` or by a self-armed `OSC 777` marker and emits `terax:agent-signal` transitions (`started`, `working`, `attention`, `finished`, `exited`). Detection is driven only by OSC sequences, never by raw output, so a repainting TUI never flaps. + +### Enter key + +Terminal input sends `\r` (CR), not `\n` (LF). PowerShell on Windows requires CR. + +## Invariants + +- Do not remove `CONPTY_LIFECYCLE_LOCK` without verifying first-tab stability under fast tab spam. +- Do not disable the Job Object without a replacement orphan guard on Windows. +- Keep platform-specific shell logic in the matching `#[cfg(unix)]` or `#[cfg(windows)]` arm of `shell_init.rs`. +- cwd passed to ConPTY must use backslashes; OSC 7 cwd arriving at the frontend is forward-slash canonical. + +## See also + +- [`TERAX.md`](../../TERAX.md) - the architecture source of truth +- [`docs/README.md`](../README.md) - index of contributor guides +- [Two-process model](two-process-model.md) - IPC boundary and command catalog +- [Terminal renderer pool](terminal-renderer-pool.md) - slot pooling and the DormantRing diff --git a/docs/architecture/security-model.md b/docs/architecture/security-model.md new file mode 100644 index 000000000..20b92f576 --- /dev/null +++ b/docs/architecture/security-model.md @@ -0,0 +1,96 @@ +# Security model + +This guide elaborates on `TERAX.md`. If anything here conflicts with `TERAX.md`, `TERAX.md` wins. + +Terax runs shells, reads and writes files, and sends data to AI providers. The security model is defense-in-depth: no single guard is enough, so every boundary validates input before acting on it. + +## Boundaries + +The main trust boundaries are: + +1. **IPC boundary** - commands registered in `src-tauri/src/lib.rs`, gated by `src-tauri/capabilities/default.json`. +2. **File-system boundary** - AI tools go through `src/modules/ai/lib/security.ts`; PTY spawn goes through the workspace authorization registry. +3. **Network boundary** - AI HTTP proxy in `src-tauri/src/modules/net.rs` with SSRF and DNS-rebinding defenses. +4. **Secret-storage boundary** - keys live in the OS keychain, never on disk or in `localStorage`. +5. **Terminal escape-sequence boundary** - OSC sequences are parsed and acted on, but never blindly trusted to mutate state. + +## Secret-path deny-list + +`src/modules/ai/lib/security.ts` refuses reads and writes of obvious secret paths. This applies **on both read and write** and must never be bypassed. + +Blocked categories include: + +- Files: `.env*`, `*.pem`, `*.key`, `*.p12`, `id_rsa*`, `known_hosts`, `credentials`, `service-account*.json`, and similar. +- Directories: `~/.ssh`, `~/.gnupg`, `~/.aws`, `~/.kube`, `~/.config/gh`, `~/.git`, system dirs (`/etc`, `/proc`, `/sys`), and Windows credential stores. +- System write prefixes: `/etc/`, `/var/db/`, `/usr/bin/`, `/windows/`, `/program files/`, etc. + +The comparison surface normalizes paths: backslashes to forward slashes, strips Windows drive letters, strips NTFS alternate data streams, strips trailing dots/spaces, lowercases, and collapses duplicate slashes. Protected directories are matched as exact path or descendant, not raw substring. + +`checkReadableCanonical` and `checkWritableCanonical` also canonicalize the path and re-check the resolved form so a symlink at an innocent path pointing into `~/.ssh` is caught. + +## Workspace authorization registry + +`WorkspaceRegistry` (`src-tauri/src/modules/workspace.rs:20`) tracks directories that PTY spawn, git commands, and AI tools are allowed to operate in. + +- `workspace_authorize` adds a directory. +- `authorize_spawn_cwd` rejects a spawn cwd outside an authorized root. +- `authorize_user_spawn_cwd` registers the user's chosen cwd as a new root instead of rejecting it. +- The registry is bootstrapped with the launch directory and the user's home directory (`workspace.rs:135`). + +This is the allow side of the file-system boundary. Any new feature that spawns a shell or mutates files outside the current workspace must interact with this registry. + +## AI tool approval flow + +In `src/modules/ai/tools/tools.ts`: + +- Read-only tools (`read_file`, `list_directory`, `grep`, `glob`) auto-execute after passing the deny-list. +- Mutating tools (`write_file`, `edit`, `multi_edit`, `create_directory`, `run_command`, `shell_session_run`, `shell_bg_spawn`) set `needsApproval: true`. The AI SDK pauses and surfaces a `tool-approval-request` part rendered as a confirmation card. +- `edit` / `multi_edit` enforce a read-before-edit invariant: the model must have read the file earlier in the session. + +Auto-send after approval uses `lastAssistantMessageIsCompleteWithApprovalResponses`. + +## SSRF and DNS rebinding defense + +`src-tauri/src/modules/net.rs` proxies AI provider requests and local-model pings. Before connecting: + +1. Resolve the hostname once (`resolve_and_classify`). +2. Classify every resolved IP as public, private, loopback, or blocked metadata. +3. Block cloud metadata endpoints (`169.254.169.254`, `metadata.google.internal`, AWS IPv6 metadata, etc.). +4. Pin reqwest to the resolved IPs so a second DNS lookup cannot return a different address (DNS rebinding). + +Local LLM endpoints are explicitly allowed because the user opted in by pointing Terax at them, but they are still classified and logged. + +## Secret storage + +API keys are stored via `secrets_*` commands (`src-tauri/src/modules/secrets.rs`): + +- macOS: Keychain via `keyring` +- Windows: Credential Manager via `keyring` +- Linux: a JSON file in the app's local data dir with mode `0600` (atomic write to `.tmp` then rename) + +Service constant: `terax-ai`. Keys never touch disk outside the keychain/Linux secrets file, never go in `localStorage`, and never appear in logs. + +## OSC trust gating + +The terminal parses OSC sequences from the PTY byte stream: + +- **OSC 7** updates the tab cwd. +- **OSC 133 A/B/C/D** marks prompt/command boundaries. +- **OSC 777** is used by the agent detector to signal coding-agent state transitions. + +The agent detector (`src-tauri/src/modules/pty/agent_detect.rs`) is armed by `OSC 133;C;` or by a self-armed marker and emits `terax:agent-signal` events. It is driven **only by OSC sequences**, never by raw output, so a repainting TUI never flaps. + +## Invariants + +- The deny-list in `security.ts` applies on both read and write. Never bypass it. +- New file-system-touching commands must respect the workspace authorization registry. +- New network-facing commands must go through the `net.rs` proxy or reimplement the same classification and DNS pinning. +- New plugin APIs must be added to `src-tauri/capabilities/default.json`. +- Keys, tokens, and credentials stay in the keychain / Linux secrets file. + +## See also + +- [`TERAX.md`](../../TERAX.md) - the architecture source of truth +- [`docs/README.md`](../README.md) - index of contributor guides +- [Two-process model](two-process-model.md) - IPC boundary and command catalog +- [AI subsystem](ai-subsystem.md) - tools, approval flow, and provider handling diff --git a/docs/architecture/terminal-renderer-pool.md b/docs/architecture/terminal-renderer-pool.md new file mode 100644 index 000000000..285b33066 --- /dev/null +++ b/docs/architecture/terminal-renderer-pool.md @@ -0,0 +1,65 @@ +# Terminal renderer pool + +This guide elaborates on `TERAX.md`. If anything here conflicts with `TERAX.md`, `TERAX.md` wins. + +## Why a pool exists + +Terminal tabs are kept mounted and hidden on switch so PTYs and dev servers keep streaming in the background. Creating an unbounded number of live xterm + WebGL renderer instances would blow the memory budget, so Terax pools renderer slots. + +The pool lives in `src/modules/terminal/lib/rendererPool.ts`. + +## Slot lifecycle + +- `POOL_MAX_SIZE` is 5 (`rendererPool.ts:22`). Each slot owns one xterm `Terminal`, `FitAddon`, `SearchAddon`, `SerializeAddon`, and optionally a `WebglAddon`. +- A slot is created on demand and assigned to a leaf on bind. +- `releaseSlot` detaches a slot from a leaf. If the leaf is idle, the slot is parked with `display:none` so xterm stops rendering but keeps parsing PTY bytes. +- After a grace period, idle slots may be reaped to keep the pool size down. + +## Parking vs releasing + +When a leaf becomes hidden: + +1. `parkLeafSlot` sets the host to `display:none`. Rendering pauses but the live buffer keeps receiving bytes. +2. If the leaf is **busy** (foreground command, agent signal, alt-screen TUI, or block-shell running mode), it keeps the slot parked indefinitely. +3. If the leaf is **idle**, `releaseSlot` is called after `HIDDEN_RELEASE_DELAY_MS`. The slot's `currentLeafId` is cleared and `retainedLeafId` is set so the buffer stays live. + +When the leaf becomes visible again, `acquireSlot` looks for: + +1. A slot already bound to this leaf. +2. A retained slot for this leaf (`retainedLeafId === leafId`) - fast path, no snapshot replay. +3. A clean idle slot. +4. If the pool is at max size, the lowest-scoring slot is evicted. Eviction serializes the retained buffer to a snapshot via `SerializeAddon` before stealing the slot. + +## The DormantRing + +`src/modules/terminal/lib/dormantRing.ts` buffers PTY bytes for leaves that have no slot at all (stolen or never bound). It is capped at 1 MiB and drops oldest blocks on overflow. On drain it resumes from the next line boundary rather than resetting the terminal, so a mid-line escape sequence is not replayed from the middle. + +## The never-serialize-mid-command invariant + +This is the most important rule in the pool. A leaf that is in the middle of a command must **never** be serialized. Replaying incremental TUI repaints over a stale snapshot is what used to wipe Claude Code. + +The code enforces this by checking `isLeafBusy` before eviction and by keeping slots parked (not released) while `commandRunning`, `isAgentActivePty`, or alt-screen is true. + +## Fast path and snapshot replay + +If a retained slot exists for a leaf, `bindSlot` skips `term.clear()` / `term.reset()` and simply drains the DormantRing into the live buffer. This avoids re-rendering a large snapshot. + +If only a snapshot exists, `bindSlot` clears the terminal, resizes, writes the snapshot, then drains the ring. For alt-screen TUIs, the snapshot is skipped and a SIGWINCH kick is sent so the TUI repaints from scratch. + +## WebGL lifecycle + +WebGL addons are created when a slot becomes visible and reaped after a grace period when parked. The addon recovers from context loss on sleep/wake or GPU reset. + +## Invariants + +- Never allow the pool to grow without bound; max is `POOL_MAX_SIZE`. +- Never serialize or evict a leaf that is mid-command or in alt-screen. +- A hidden busy leaf keeps its live grid parked with `display:none`. +- An idle hidden leaf releases its slot but the buffer continues parsing bytes. +- The DormantRing only buffers bytes for leaves without any slot. + +## See also + +- [`TERAX.md`](../../TERAX.md) - the architecture source of truth +- [`docs/README.md`](../README.md) - index of contributor guides +- [PTY shell integration](pty-shell-integration.md) - sessions, OSC sequences, and ConPTY diff --git a/docs/architecture/two-process-model.md b/docs/architecture/two-process-model.md new file mode 100644 index 000000000..f93f695a7 --- /dev/null +++ b/docs/architecture/two-process-model.md @@ -0,0 +1,135 @@ +# Two-process model and IPC command reference + +This guide elaborates on `TERAX.md`. If anything here conflicts with `TERAX.md`, `TERAX.md` wins. + +## The split + +Terax is two processes: the Rust backend (`src-tauri/`) and the webview frontend (`src/`). + +- **Rust owns all OS access**: PTY, file system, git, shell spawn, network, secrets, workspace authorization. +- **The webview never touches the FS, processes, or shells directly**. Every host operation goes through an `invoke()` call to a command registered in `src-tauri/src/lib.rs`. + +This boundary is the root of the security model. Untrusted input (terminal escape sequences, file content, AI tool results) is parsed and validated in Rust or in carefully scoped frontend code, never executed by the renderer. + +## Adding a new IPC command + +1. Write the `#[tauri::command]` async function in the appropriate `src-tauri/src/modules//` module. +2. Register it in `src-tauri/src/lib.rs` inside the `tauri::generate_handler![...]` block (`src-tauri/src/lib.rs:191`). +3. If the command uses a Tauri plugin API (window, clipboard, dialog, etc.), add the plugin permission to `src-tauri/capabilities/default.json`. +4. Add a typed frontend wrapper in the matching `src/modules//lib/` directory and call it through Tauri's `invoke()` API. +5. If the command touches the file system, network, or shell, it must go through the existing guards (`security.ts` deny-list, workspace authorization registry, SSRF guard, AI tool approval). + +Custom commands do not need to be listed one-by-one in `default.json`; the capability covers the window. Plugin permissions do. + +## Command catalog + +The commands registered in `src-tauri/src/lib.rs` are grouped below by module. Names are the Rust function names as seen by the frontend. + +### PTY (`src-tauri/src/modules/pty/`) + +Long-lived interactive terminal sessions. + +- `pty_open` - create a new PTY session +- `pty_write` - send input bytes (text or control sequences) +- `pty_resize` - resize the PTY +- `pty_close` / `pty_close_all` - destroy one or all sessions +- `pty_has_foreground_process` / `pty_has_foreground_job` - detect whether a command is running +- `pty_shell_name` / `pty_list_shells` - shell detection and enumeration + +Output streams from `pty_open` via a Tauri `Channel`. + +### File system (`src-tauri/src/modules/fs/`) + +#### Tree + +- `list_subdirs` - list subdirectories +- `fs_read_dir` - read a directory + +#### File + +- `fs_read_file` - read file contents +- `fs_write_file` - write file contents +- `fs_stat` - file metadata +- `fs_canonicalize` - canonical path + +#### Mutate + +- `fs_create_file` / `fs_create_dir` +- `fs_rename` / `fs_delete` / `fs_copy` + +#### Watch + +- `fs_watch_add` / `fs_watch_remove` - filesystem change notifications + +#### Search + +- `fs_search` - fuzzy file finder +- `fs_list_files` - recursive file listing + +#### Grep + +- `fs_grep` - content search +- `fs_grep_interactive` - interactive content search +- `fs_glob` - glob matching + +### Git (`src-tauri/src/modules/git/`) + +All git commands are gated through the workspace authorization registry. + +- `git_resolve_repo` / `git_panel_snapshot` +- `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_remote_url` +- `git_list_branches` / `git_checkout_branch` + +### Shell (`src-tauri/src/modules/shell/`) + +Three distinct surfaces: + +- `shell_run_command` - one-shot subshell exec for AI tools +- `shell_session_open` / `shell_session_run` / `shell_session_close` - persistent agent shell with state across calls +- `shell_bg_spawn` / `shell_bg_logs` / `shell_bg_kill` / `shell_bg_list` - long-running background processes with bounded ring-buffer log capture + +### Workspace (`src-tauri/src/modules/workspace.rs`) + +- `workspace_authorize` / `workspace_current_dir` - the spawn/git/AI cwd authorization registry +- `wsl_list_distros` / `wsl_default_distro` / `wsl_home` - WSL bridge + +### Network (`src-tauri/src/modules/net.rs`) + +- `ai_http_request` / `ai_http_stream` - AI HTTP proxy with SSRF guard +- `lm_ping` - local-model ping + +### Secrets (`src-tauri/src/modules/secrets.rs`) + +- `secrets_get` / `secrets_set` / `secrets_delete` / `secrets_get_all` - OS keychain access, service `terax-ai` + +### Agent hooks (`src-tauri/src/modules/agent.rs`) + +- `agent_enable_hooks` / `agent_hooks_status` - install/status terminal coding-agent hooks (Claude Code, Codex, Gemini CLI) + +### History (`src-tauri/src/modules/history/`) + +- `history_suggest` / `history_commands` / `history_record` / `history_list` - shell history integration + +### Settings window + +- `get_launch_dir` - CLI launch directory, drained on first read +- `open_settings_window` - open the separate settings webview (optional `tab` deep-link) + +## Invariants + +- The webview must not spawn processes, read files, or make network calls except through the commands above. +- New commands must be registered in `lib.rs` and guarded at the boundary (workspace auth, deny-list, SSRF, approval flow). +- Plugin permissions must be added to `src-tauri/capabilities/default.json` if the command uses a plugin API. + +## See also + +- [`TERAX.md`](../../TERAX.md) - the architecture source of truth +- [`docs/README.md`](../README.md) - index of contributor guides +- [PTY shell integration](pty-shell-integration.md) - how sessions and shell integration work +- [Security model](security-model.md) - the boundaries every command must respect diff --git a/docs/contributing/testing.md b/docs/contributing/testing.md new file mode 100644 index 000000000..23dc16e53 --- /dev/null +++ b/docs/contributing/testing.md @@ -0,0 +1,82 @@ +# Testing + +This guide elaborates on `TERAX.md` and `CONTRIBUTING.md`. If anything conflicts with those files, they win. + +## Running checks locally + +The canonical commands are what CI runs (`.github/workflows/ci.yml`): + +```bash +pnpm lint +pnpm check-types +pnpm test + +cd src-tauri +cargo clippy --all-targets --locked -- -D warnings +cargo nextest run --locked # CI uses nextest +``` + +If you do not have `cargo-nextest` installed, `cargo test --locked` is the local fallback. Install nextest with `cargo install cargo-nextest`. + +## What must have a test + +`CONTRIBUTING.md` requires a test for any change that touches behavior in these load-bearing paths: + +- Shell / terminal spawn (what shell launches, with which cwd, env, and login flags) +- Workspace authorization (both the allow and deny side) +- Git command layer (repo-root resolution, pathspec/argument guards, status parsing) +- Filesystem mutation (atomic writes, symlink handling, no-data-loss on partial failure) +- IPC command surface and AI tool surface +- Pure logic with wide reach (cwd inheritance, tab/split tree transforms, OSC/prompt parsing, command guard) + +The bar is real coverage of the contract, not a placeholder. Test the edge, the deny path, the "what happens one level above home". + +## What does not need a test + +UI rendering, themes, syntax-highlight tables, and anything the type-checker already guarantees do not need tests. + +## Writing a good test + +A good test locks the invariant you are relying on. Examples from the codebase: + +- `src-tauri/src/modules/workspace.rs` `auth_tests` verify that an authorized path, a subdir of an authorized root, an unauthorized path, a missing path, and a symlink escape all behave correctly. +- `src-tauri/src/modules/pty/job.rs` tests verify that dropping the Job Object kills the assigned process tree on Windows. +- `src-tauri/src/modules/pty/session.rs` tests verify that dropping a `Session` kills the child process. +- `src-tauri/src/modules/pty/shell_init.rs` tests verify shell classification and WSL fish launch specs. +- `src/modules/ai/lib/security.ts` is exercised by tests that assert specific paths are refused and that canonicalization catches symlink traversal. + +## Cross-platform PTY tests + +Platform-specific behavior must be gated: + +```rust +#[cfg(unix)] +fn shell_has_children(shell_pid: u32) -> bool { ... } + +#[cfg(windows)] +fn shell_has_children(shell_pid: u32) -> bool { ... } +``` + +Tests for ConPTY/Job Object belong behind `#[cfg(windows)]`; tests for Unix PTY lifecycle belong behind `#[cfg(unix)]`. Do not assume a helper that works on one platform works on the other. + +## Security function tests + +When testing `src/modules/ai/lib/security.ts` or the Rust equivalents, cover: + +1. The literal path is refused. +2. The canonicalized path is re-refused (symlink case). +3. Case variants match on case-insensitive filesystems. +4. NTFS alternate data streams and trailing dot/space variants are normalized. +5. Write-only deny prefixes block writes but allow reads where appropriate. + +## Invariants + +- A local fix with global blast radius must be caught by a test; review alone is not enough. +- Test the deny path and the edge, not just the happy path. +- Keep platform-specific tests behind the right `#[cfg(...)]` gate. + +## See also + +- [`TERAX.md`](../../TERAX.md) - the architecture source of truth +- [`CONTRIBUTING.md`](../../CONTRIBUTING.md) - quality bar, project layout, how to contribute +- [`docs/README.md`](../README.md) - index of contributor guides