# Pier-X — Code Rules for Claude Pier-X is a cross-platform terminal / Git / SSH / database management tool, aiming for an IntelliJ-grade IDE experience on macOS and Windows. The stack is **Rust backend + Tauri 2 + React + TypeScript**: the previous Qt/QML shell is archived, and the Rust/GPUI experiment on `backup/rust-gpui` is abandoned. ## Authoritative sources | Concern | File | |---|---| | **What the product is & which features exist** | [docs/PRODUCT-SPEC.md](docs/PRODUCT-SPEC.md) — only source of truth for panels, tools, default behaviors, non-goals | | Backend design → impl gap tracker | [docs/BACKEND-GAPS.md](docs/BACKEND-GAPS.md) | | Build / frontend / review rules (short form) | [AGENTS.md](AGENTS.md) | | **Visual design tokens & rules** | [.agents/skills/pier-design-system/SKILL.md](.agents/skills/pier-design-system/SKILL.md) — only source of truth for colors, typography, spacing, radius, shadow | When SKILL.md and this file overlap, SKILL.md wins for visual values; this file wins for code structure. ## Architecture boundaries - **Cargo workspace**: root [`Cargo.toml`](Cargo.toml) with two members — [`pier-core`](pier-core/) (UI-framework-agnostic backend) and [`src-tauri`](src-tauri/) (the Tauri runtime glue). - **Frontend**: repo root — Vite + React 19 + TypeScript under [`src/`](src/). State via `zustand`; terminals via `@xterm/xterm`; panels via `react-resizable-panels`; icons from `lucide-react`. - `pier-core` **must stay UI-agnostic**. No `tauri`, `gpui`, `qt`, or any UI crate dependency. Public API returns plain Rust types. - `src-tauri` **calls `pier-core` directly** as Rust functions and exposes them to the frontend as Tauri commands. React code calls those commands via `@tauri-apps/api`'s `invoke`. The frontend **must not** bypass Tauri to reach pier-core. - **Do not reintroduce**: `qt6-*`, `qml`, `cmake`, `qmake`, `corrosion`, any C-ABI bridge, or the `pier-ui-gpui` crate. The Qt and GPUI shells are gone on purpose — propose a new feature, not a third UI runtime. ## Frontend code rules (`src/`) ### Rule 1 — Design tokens, never literals Every color, font family, font size, spacing, radius, and shadow used in a component or panel **must** reference a CSS custom property defined in [`src/styles/tokens.css`](src/styles/tokens.css) — or a shared atom class from [`src/styles/atoms.css`](src/styles/atoms.css). **Forbidden in `src/shell/`, `src/panels/`, `src/components/`, and any stylesheet under `src/styles/` other than `tokens.css`:** - Hex / rgb / rgba / hsl color literals in `.css` or inline styles (`color: "#0e1116"`, `background: rgba(...)`, etc.) - Hardcoded pixel values for spacing, radius, or typography when a token exists (use `--sp-X` / `--radius-X` / `--size-X` / `--ui-fs*`) - Hardcoded font family strings like `"IBM Plex Sans"`, `"Inter"`, `"JetBrains Mono"` - Bypassing `IconButton` / `.btn` / `PanelHeader` / `DbConnRow` / `StatusDot` / `Badge` to roll your own button or panel chrome **Allowed:** - Backgrounds: `var(--bg)` / `var(--surface)` / `var(--surface-2)` / `var(--panel)` / `var(--panel-2)` / `var(--elev)` - Text: `var(--ink)` / `var(--ink-2)` / `var(--muted)` / `var(--dim)` - Borders: `var(--line)` / `var(--line-2)` / `var(--line-3)` - Accent: `var(--accent)` / `var(--accent-dim)` / `var(--accent-subtle)` / `var(--accent-hover)` / `var(--accent-ink)` - Status: `var(--pos)` / `var(--neg)` / `var(--warn)` / `var(--info)` + their `-dim` variants - Diff: `var(--add)` / `var(--del)` / `var(--mod)` - Spacing/radius: `var(--sp-0..sp-12)` / `var(--radius-xs..radius-pill)` - Typography: `var(--size-display..size-small)` / `var(--ui-fs)` / `var(--ui-fs-sm)` / `var(--ui-fs-lg)` / `var(--size-micro)` - Font families: `var(--sans)` / `var(--mono)` / `var(--serif)` - Elevation/scrim: `var(--shadow-app)` / `var(--shadow-popover)` / `var(--shadow-dialog)` / `var(--stage-gradient)` / `var(--overlay-scrim)` Legacy aliases (`--text-primary`, `--bg-canvas`, `--border-subtle`, `--font-ui`, etc.) remain for transitional code but new code should use the primary names above. See [`.agents/skills/pier-design-system/SKILL.md`](.agents/skills/pier-design-system/SKILL.md) §8 for the shared-atom catalog. If a token is missing, **add it to `tokens.css` first** (dark + light, plus any accent variants), then consume it. Do not "just this once" inline a raw value. ### Rule 2 — Module layout ``` src/ ├── main.tsx # entrypoint; mounts ├── App.tsx # top-level routing / layout shell ├── shell/ # chrome: TopBar, Sidebar, StatusBar, TabBar, WelcomeView, dialogs ├── panels/ # one file per tool panel (Git, Terminal, Sftp, MySql, …) ├── components/ # reusable UI atoms (ContextMenu, PreviewTable, ResizeHandle, …) ├── stores/ # zustand stores — UI state, never business logic ├── lib/ # Tauri-command wrappers, pure helpers ├── i18n/ # locale resources └── styles/ # tokens.css (single source of truth) + shell.css + scoped css ``` When adding code, follow this split: - A new tool surface → a file in `src/panels/`. - A new piece of shell chrome → a file in `src/shell/`. - A reusable atom used by ≥2 panels → a file in `src/components/`. - Shared layout / chrome styling → `src/styles/shell.css` (or a new scoped sheet), not inline across panels. ### Rule 3 — State in stores, not in panels Cross-panel state (connections, active tab, selected host, pending diffs) lives in a `zustand` store under `src/stores/`. Panels subscribe to slices they need. Keep stores focused on UI state; don't put business logic there — that belongs in `pier-core`. ### Rule 4 — Tauri IPC is the only bridge - React components call backend behavior by invoking a Tauri command declared in [`src-tauri/src/lib.rs`](src-tauri/src/lib.rs) (or a sibling module like `git_panel.rs`). - Wrap `invoke` calls in typed helpers under `src/lib/` so panels stay free of raw `invoke("...")` strings. - New backend capability: add it to `pier-core` first, expose a thin command in `src-tauri`, then a typed wrapper in `src/lib/`, then consume it from the panel. Do not grow `src-tauri` into a business-logic layer. ### Rule 5 — Render is paint-only React render paths (component bodies, `useMemo` deps, JSX children) **must not** call `invoke` synchronously or block on IO. Load data in `useEffect` / event handlers, store it in a zustand store or local state, and render from the cache. Tauri commands that can be slow (SSH connect, DB connect, directory walks) must stream/return via awaited calls off the render path. ## Review gate Reject a change if any of these are true: 1. It adds a color/size/font literal in `shell/`, `panels/`, or `components/` instead of a `tokens.css` var. 2. It inlines a new visual atom in a panel instead of adding a component in `src/components/`. 3. It reintroduces Qt / QML / CMake / Corrosion / `pier-ui-gpui` in any form. 4. It adds a `pier-core` dependency on `tauri`, `gpui`, or any UI crate. 5. It calls pier-core from React without going through a Tauri command. 6. It violates one of the SKILL.md non-negotiables (see SKILL.md §1). 7. It invokes a backend command synchronously inside a render body (Rule 5). 8. It adds / removes / re-purposes a right-side tool, changes a panel's default safety stance (e.g. DB read-only default), or alters the default `rightTool` for any backend, **without first updating the relevant section in [docs/PRODUCT-SPEC.md](docs/PRODUCT-SPEC.md)**. ## Build & run ```sh npm install # first-time frontend deps (run at repo root) npm run tauri dev # dev: vite + tauri dev npm run tauri build # release: vite build + tauri build cargo build -p pier-core # backend only npm run bump # sync version across manifests + tag ``` Node + npm and the Rust toolchain are required; no Qt, CMake, or GPUI toolchain is needed. If a step asks you to install Qt or to run `cargo build -p pier-ui-gpui`, it is out of date. Releases are tag-driven: - Push a `v*.*.*` tag → `.github/workflows/release.yml` builds Linux / Windows x64 / Windows ARM64 / macOS universal bundles and publishes them to the GitHub release for that tag. - The same tag pushed to a Gitea remote → `.gitea/workflows/release.yml` builds Linux `.deb` / `.rpm` / `.AppImage` bundles and uploads them to the Gitea release via the Gitea API.