Files
dots/common/.pi/CLAUDE.md
T
coja a700e23e0a [Sync] adopt unified stow layout from the private repo
Mirrors the private dots tree at 900bdda: one shared base plus per-host
overlays, replacing the old flat .config/ layout (last synced 2026-06-28).

- packages: common/ gui/ wm/ lw/ fl/ + install.sh and bin/ tooling (dotsync,
  reconcile-hyde.sh)
- new README (layout, deploy order, HyDE dependency), plus ToDo.md and
  HYDE-UPDATE.md
- current HyDE waybar rig (layouts/, cava), pi agent extensions, claude/
  config, tmux, presenterm, aichat roles
- drops stale duplicates and generated cruft that should never have been
  tracked: the second top-level .pi/ copy, btop.log, zellij config.kdl.bak,
  fish_variables, nvim codecompanion.lua
- .pi/agent/auth.json is gitignored now; auth.json.example ships instead
- fl/ and wm/ hypr themes/ stay untracked (HyDE-generated per machine, per
  the root .gitignore)
2026-08-12 22:50:18 +02:00

192 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## What this is
This is **not application source** — it's the on-disk configuration tree for `pi`, the TUI coding agent from [earendil-works/pi](https://github.com/earendil-works/pi) (binary at `/usr/bin/pi`). It lives inside the user's dots repo (`~/.dots`) and is surfaced into `$HOME` via GNU Stow's per-file symlinks (`--no-folding`), e.g. `~/.pi/agent/extensions/notify.ts -> ../../../.dots/common/.pi/agent/extensions/notify.ts`. **Editing** an existing file edits the live config directly — no deploy step. But **adding a new file** (e.g. a new extension) is not surfaced until you re-stow: run `./install.sh` (or `stow --no-folding -R -d ~/.dots -t ~ common`) to create its symlink, on each machine.
Because it's JSON + markdown config, there are no tests/lint/build. The only useful "validation" is JSON well-formedness, e.g. `jq . settings.json` or `python -m json.tool < settings.json`.
## Config directory — single source of truth
**All live config is `.pi/agent/`** (surfaced as `~/.pi/agent`). The pi coding agent reads `$PI_CODING_AGENT_DIR`, which defaults to `~/.pi/agent` and is not overridden here; per pi's docs, settings/auth/trust/sessions/extensions/AGENTS.md all resolve under that one directory. Nothing reads `~/.pi/*` at the top level or `~/.config/pi`.
> History: there used to be three drifted copies of this config (`.pi/*` top-level, `.pi/agent/`, and `~/.config/pi/`). The two unread mirrors were deleted and the dangling `~/.config/pi` stow symlink removed, leaving `.pi/agent/` as the only copy. Don't reintroduce mirrors — edit `.pi/agent/` directly.
## Layout of `.pi/agent/` (the live config)
- `settings.json` — runtime settings: `defaultProvider`/`defaultModel`/`defaultThinkingLevel`, `theme`, `enabledModels` (glob allowlist for the Ctrl+P model picker), `compaction` (auto-summarize long sessions), `retry`, HTTP timeouts, `npmCommand` (pinned to `fnm exec --using=22 -- npm`), `packages` (installed extension packages, currently `npm:pi-vim`) and their config blocks (`piVim` — see Vim input editor).
- `models.json` — the provider + model catalog (see below).
- `auth.json` — maps each provider to its credential. Values are **references, not secrets** (`$DUSKADIY_API_KEY`, `no-key-required`); pi resolves `$VAR` from the environment. **Gitignored** — the tracked copy is `auth.json.example`; a new host needs `cp auth.json.example auth.json` before pi can resolve a provider (stow links whatever is present, tracked or not, so the live file keeps working here).
- `prompts/*.md` — custom slash commands (pi "prompt templates"). `/commit` and `/review` are defined here. Format: YAML frontmatter (`description`, `argument-hint`) + body, with `${1:-default}` positional-arg substitution.
- `themes/*.json` — color themes following the schema at `earendil-works/pi .../theme/theme-schema.json`: a `vars` palette referenced by semantic `colors` keys, plus an `export` block for HTML session export.
- `sessions/` — runtime session transcripts (`.jsonl`), **gitignored**. One subdir per project cwd; each line is an event (`session`, `model_change`, `message`, …).
- `extensions/*/index.ts` — auto-discovered TypeScript extensions, loaded via [jiti](https://github.com/unjs/jiti) (no build step). `import type` from `@earendil-works/*` is erased at runtime; value imports resolve against pi's own bundled packages, so a vendored extension needs no `node_modules`. Editor TS "cannot find module" warnings on these imports are therefore expected noise.
- `npm/` — extension **packages** installed by `pi install` (currently `pi-vim`), **gitignored**; `settings.json > packages` is the tracked source of truth. On a fresh host run `pi install npm:pi-vim` once — plus pi-vim's manual peer-dep install (see Vim input editor), which `pi install` does not do — then `pi update npm:pi-vim` to bump. A package needs no re-stow — unlike a new file under `extensions/`.
- `keybindings.json` — key remaps. A user entry **replaces** the default keys for that action (it does not merge). Action ids and defaults are listed in `/opt/pi-coding-agent/docs/keybindings.md`.
## Plan mode (Shift+Tab)
`extensions/plan-mode/` is pi's bundled plan-mode example (pi has no built-in plan mode), **vendored here and rebound from its upstream `Ctrl+Alt+P` to Shift+Tab**. In plan mode it disables `edit`/`write` and restricts `bash` to a read-only allowlist (footer shows `⏸ plan`); `/plan` also toggles it, `--plan` starts in it. The rebind is two coupled edits — keep them together:
- `extensions/plan-mode/index.ts` (~line 157): `pi.registerShortcut("shift+tab", …)`, and the upstream `import { Key } … }` is removed. This file is a **local fork** of `/opt/pi-coding-agent/examples/extensions/plan-mode/`; on a pi upgrade, re-pull from there and re-apply these two edits.
- `keybindings.json` moves `app.thinking.cycle` off Shift+Tab to `ctrl+shift+t`, so the toggle fires deterministically (otherwise it collides with the built-in thinking-cycle binding).
After editing an extension or `keybindings.json`, run `/reload` in pi to apply without restarting.
## Status bar (custom footer)
`extensions/statusbar.ts` replaces pi's default footer via `ctx.ui.setFooter()`. It
mirrors the Claude Code status line (`~/.config/claude/statusline.py`) but swaps the
emoji for **Nerd Font (Material Design) glyphs** — the same family already used in
tmux (waybar cpu/mem/net glyphs) and nvim — so it renders natively in kitty
(CaskaydiaCove Nerd Font Mono). Colors come from the active theme, not raw ANSI.
Icon legend (each glyph echoes the Claude emoji it stands in for):
| Glyph | Codepoint | Segment | ~ Claude |
|---|---|---|---|
| `󰉋` | `U+F024B` nf-md-folder | cwd (`~`-collapsed) | 📁 |
| `󰘬` | `U+F062C` nf-md-source_branch | git branch | 🌿 |
| `󰚩` | `U+F06A9` nf-md-robot | model id + `· thinking` | 🤖 |
| `󰈚` | `U+F021A` nf-md-text_box | context-window % used (+ tokens) | 📝 |
| `󰓅` | `U+F04C5` nf-md-speedometer | last response's decode throughput (t/s) | — |
| `󰀪` | `U+F002A` nf-md-alert_outline | context-budget warning widget (above editor, ≥80%) | — |
| `↑ ↓` | — | session input / output tokens | 💰 |
Two extras beyond the footer line: a themed "breathing" pulse **working-indicator**
(the streaming spinner), and a **context-budget warning widget** above the editor
that appears once the window is ≥80% full (`warning`, then `error` ≥90%) nudging a
`/compact`. Both are reset when the footer is toggled off.
Segments render left→right and truncate at the terminal edge. git only shows inside
a git repo; context, t/s and tokens only appear after the first response (t/s is
`usage.output ÷ (message_end first streamed token)`, so prompt-eval time is
excluded). Extension statuses (e.g. plan-mode's `⏸ plan`) are preserved at the far
left. The Claude bar's system row (RAM/CPU/temp/disk) is intentionally omitted —
that data isn't in the footer API, and the tmux bar below pi already shows
cpu/mem/net. `/statusbar` toggles it off (restores the built-in footer) and back on;
`/reload` picks up edits to this file (a *new* extension needs a re-stow first — see
top).
## Vim input editor
The modal (vim-like) input editor is the npm package
**[`pi-vim`](https://github.com/lajarre/pi-vim)** (pinned by `settings.json > packages`,
installed into the gitignored `.pi/agent/npm/`, configured under `settings.json > piVim`;
v0.14.1 at adoption). It replaced the hand-rolled local fork on 2026-08-05; the fork was parked
as `extensions/vim-editor.ts.disabled` and **deleted 2026-08-09** once the package had proven
itself in daily use. `git show c6566af^:common/.pi/agent/extensions/vim-editor.ts` brings it back
if it is ever wanted.
**That swap is the whole point: no more re-pull-and-re-apply on every pi upgrade** — use
`pi update npm:pi-vim`, and `pi remove npm:pi-vim` to back out.
⚠️ **This pi build needs pi-vim's peer dep installed by hand.** `pi` here is the AUR
`pi-coding-agent`: a Bun-compiled ELF at `/opt/pi-coding-agent/pi` with the
`@earendil-works/*` packages *embedded* as virtual modules, not present on disk
(`/opt/pi-coding-agent/node_modules/` holds only `@mariozechner`). pi-vim's
`clipboard-mirror.ts` calls `import.meta.resolve("@earendil-works/pi-coding-agent")` at
**module top level** — it bakes that URL into the source of a spawned clipboard helper —
so after a bare `pi install npm:pi-vim` the resolve throws and the whole extension fails
to load: `Cannot find module '@earendil-works/pi-coding-agent'`. No setting dodges it;
`clipboardMirror: "never"` can't, because the call runs at import time. Fix, once per host:
```fish
cd ~/.pi/agent/npm
fnm exec --using=22 -- npm install --save-exact @earendil-works/pi-coding-agent@0.83.0 # match `pi --version`
```
pi's own `pi install` deliberately does *not* pull peers (it would duplicate the runtime),
so this is manual, costs ~170 MB inside the gitignored `npm/`, and wants re-pinning after a
pi upgrade (hence `--save-exact`: npm's default `^` range would let a later `npm install`
drift the peer off `pi --version` on its own). The static
`CustomEditor`/`SettingsManager`/`matchesKey` imports still bind to pi's embedded copy —
verified in the TUI — so the on-disk copy stays inert apart from the spawned helper. Upstream has no issue filed for this (checked 2026-08-05); the proper fix is
making that resolve lazy. `@burneikis/pi-vim` and `pi-vimmode` use no `import.meta.resolve`
and need no peer install, if this ever gets annoying.
Five modes — INSERT, NORMAL, VISUAL, V-LINE, EX. The active one renders as a word label
(` NORMAL `) at the **bottom-right** of the editor border, doubling as a pending-command
display: `3d2w`, `ci"`, `25gg` appear as you type them (` NORMAL 3d2w_ `). That's a
**position change from the fork**, which put a single `[N]` tag bottom-*left*.
- **motions** — `hjkl` · `w`/`b`/`e` + `W`/`B`/`E` · `0`/`^`/`$` · `{`/`}` paragraph ·
`gg`/`G` with counts (`25gg`) · `f`/`F`/`t`/`T` + `;` repeat · `%` matching pair
- **operators** — `d`/`c`/`y` + any motion · `dd`/`cc`/`yy`/`Y` · `x`/`X`/`s`/`S`/`C`/`D` ·
`o`/`O` open line · `r` replace char · `J`/`gJ` join · `p`/`P` put
- **text objects** — `iw`/`aw` · `iW`/`aW` · `i"`/`a"` · `i(`/`a(` · `i[`/`a[` · `i{`/`a{`
- **undo/repeat** — `u` and `Ctrl+R`, scoped to vim changes · `.` repeats the last edit
- **EX** — `:` dispatches real pi commands (`:model`, `:tree`) and shells out via
`:!git status`. pi exposes no command-dispatch API, so it re-submits the line as if
typed, snapshotting and restoring the prompt around it.
- cursor shape follows the mode (bar in INSERT, block elsewhere) via DECSCUSR.
Upstream implements no search (`/`, `?`, `n`, `N`), no macros (`q`/`@`) and no visual-block.
The fork had none of those either, so nothing regressed; `pi-vimmode` has them but is
rougher (5★/18 open issues vs 75★/2 at the time of choosing).
### Config (`settings.json > piVim`)
- `modeColors` — theme tokens, **ported from the fork**: `insert: success` (green),
`normal: accent` (mauve), `visual: warning`. ⚠️ the fork's header comment claimed visual
was "peach", but `warning` resolves to **yellow** `#f9e2af` in catppuccin-mocha — the port
keeps what actually rendered and hands the *peach* (`bashMode`) to the new EX mode.
- `borderSync` all `host`, `labelSync` all `mode` — also the upstream defaults, but pinned
explicitly so an upstream default change can't start repainting the input border. Matches
the fork: only the label is tinted, the border is left to the host.
- `clipboardMirror: "yank"` — only an explicit `y` reaches the **OS** clipboard. Upstream's
default `"all"` mirrors deletes too, so every `dd`/`x` would clobber the Wayland clipboard;
`"never"` is exact fork parity (pi's kill-ring only). Writes go through
`@mariozechner/clipboard` in a spawned helper, not a direct `wl-copy`.
- `exCommand.piDispatch: true` — the `:` bridge above. `copyInputToClipboard: false`: it
copies the composed prompt out, an exfiltration path, so upstream only honors it from the
user-global file (never project settings) — leave it off.
- `modeChange` (unset) — would run a shell command on every INSERT/NORMAL transition.
`/vim` still toggles the editor, now from **`extensions/vim-toggle.ts`** (pi-vim registers no
commands of its own): off drops the editor component for the rest of the session, on calls
`ctx.reload()` — the same flow as `/reload` — which re-runs discovery and reinstalls it.
No clash with `statusbar.ts`: pi-vim draws its label inside its own editor component, not
through `setFooter()`.
## Compaction tuning (small local windows)
`settings.json > compaction` is set for the small local context windows (24k128k;
several presets are only 24k). `reserveTokens: 6144` (headroom for the
response; auto-compaction fires at `contextTokens > contextWindow reserveTokens`)
and `keepRecentTokens: 6000` (kept verbatim, not summarized) — both well below pi's
16384/20000 defaults so short windows aren't dominated by the reserve or thrash into
repeated compaction. These are **global** (pi has no per-model compaction): on the
128k model you could raise `keepRecentTokens` for richer retained context; if you
see responses truncate near a full window, raise `reserveTokens` toward the models'
`maxTokens` (8192). All model `contextWindow`s are verified to match the server
`ctx-size` in `fl/.config/llamacpp/config.ini`.
## Providers and models
Two OpenAI-compatible providers are configured, both serving the same catalog of small local/self-hosted models (Qwen3-Coder-30B, Gemma 4, GLM-4.7-Flash, etc.). All have `cost: 0`:
- **`duskadiy`** — remote, `https://llm.duskadiy.com/api/v1`, key `$DUSKADIY_API_KEY`.
- **`localcpp`** — LAN llama.cpp server at `http://192.168.0.204:11343/v1`, no key.
⚠️ **`duskadiy` is parked out of the model picker** (2026-08-05): `settings.json > enabledModels` lists only `localcpp/*`. pi resolves `auth.json`'s `$DUSKADIY_API_KEY` from the environment, and this host has no `~/.config/fish/conf.d/secrets.fish`, so the credential resolves empty, pi drops the whole provider, and the `duskadiy/*` glob then matched nothing — printing `Warning: No models match pattern "duskadiy/*"` on every single start. Its box is down independently of the missing key: `llm.duskadiy.com` still resolves (`24.135.113.16`) but TCP 443 times out, as does the apex. Provider, catalog and auth entry are all left **intact** — re-add `"duskadiy/*"` to `enabledModels` once the key is back on the host and the box answers.
`defaultProvider` stays `duskadiy` on purpose. With the provider dropped pi falls back to the same model id under `localcpp` (the two catalogs are mirrors), so startup resolves fine today — and when duskadiy returns it is the provider that works **off**-LAN, unlike `localcpp`'s `192.168.0.204`. Don't "simplify" it to `localcpp` without weighing that.
When adding/editing a model, keep `id` exactly matching the server's model id (the section name in `fl/.config/llamacpp/config.ini`), and set `contextWindow` to match the `ctx-size` the model is actually loaded with server-side.
## Secrets
`$DUSKADIY_API_KEY` is the only real secret. It is defined in `.config/fish/conf.d/secrets.fish` (gitignored) and referenced — never inlined — in tracked config. Keep it that way: tracked files (`auth.json`, `models.json`) must contain `$DUSKADIY_API_KEY`, not the literal token.
`auth.json` is **no longer tracked** (2026-08-08, ahead of making the repo public): pi's interactive `/login` rewrites it with the **literal** key, so a tracked copy was one stray `/login` away from committing a real token. `auth.json.example` carries the same `$VAR` references. Every historical version of the old tracked file was audited and only ever contained references — nothing leaked. Installed-package and runtime artifacts (`.pi/agent/npm/`, `git/`, `trust.json`, `sessions/`) are gitignored.
## Running pi
- `pi` — interactive TUI. `pi -p "<prompt>"` — non-interactive, print and exit.
- `pi -c` / `pi -r` — continue / pick a session to resume.
- `pi config` — TUI to enable/disable discovered resources. `pi --list-models [search]` — list available models.
- `--provider` / `--model` / `--thinking` override the `settings.json` defaults per-run.
- pi also auto-discovers `CLAUDE.md`/`AGENTS.md` as context files (disable with `-nc`) — i.e. pi reads this very file too, not just Claude Code.