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

16 KiB
Raw Blame History

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

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 (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:

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.

  • motionshjkl · w/b/e + W/B/E · 0/^/$ · {/} paragraph · gg/G with counts (25gg) · f/F/t/T + ; repeat · % matching pair
  • operatorsd/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 objectsiw/aw · iW/aW · i"/a" · i(/a( · i[/a[ · i{/a{
  • undo/repeatu 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 contextWindows 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.