[Sync] llm stuff

This commit is contained in:
Coja
2026-09-17 20:58:21 +02:00
parent ed7e34552d
commit d8f6b30e2c
52 changed files with 2233 additions and 237 deletions
+6 -3
View File
@@ -168,9 +168,12 @@ track neither. HyDE-managed customization goes via:
- `gui/.config/waybar/modules/*.jsonc` — module defs, globbed into the generated config. - `gui/.config/waybar/modules/*.jsonc` — module defs, globbed into the generated config.
**The custom cava rig is NOT HyDE-managed**: `layouts/custom.jsonc` + `layouts/custom.css` form a **The custom cava rig is NOT HyDE-managed**: `layouts/custom.jsonc` + `layouts/custom.css` form a
self-contained two-bar config launched raw (`waybar -c …custom.jsonc -s …custom.css` — exec-once, self-contained two-bar config run by `wbar` (`gui/.local/bin/wbar`) as the supervised systemd
the Ctrl+Alt+W / Super+Shift+R binds, and the `wbar` abbr). **Never route it through user unit `dots-waybar.service` (`Restart=always` — waybar 0.15.0 dies on every DP-2 link
`hyde-shell waybar --set`** — waybar.py would clobber the repo's files as above. HyDE only reads drop/return, and as a bare process nothing relaunched it). The `exec-once` in `userprefs.conf`,
the Ctrl+Alt+W / Super+Shift+R binds and the `wbar` command all go through it; **never `killall
waybar`** — systemd brings it back in seconds and a raw relaunch beside it means two bars
(`wbar toggle|restart|reload|stop`). **Never route it through `hyde-shell waybar --set`** — waybar.py would clobber the repo's files as above. HyDE only reads
from `layouts/`, which is what makes them safe to track. On a rig host, HyDE's own bar must be from `layouts/`, which is what makes them safe to track. On a rig host, HyDE's own bar must be
disabled once, or **two bars appear at every login** — and the launcher differs by HyDE vintage: disabled once, or **two bars appear at every login** — and the launcher differs by HyDE vintage:
+13 -11
View File
@@ -1,3 +1,4 @@
# NOTE: the lw/ and wm/ host overlays carry full-file copies of this config — mirror changes there.
# see https://github.com/sigoden/aichat/blob/main/config.example.yaml # see https://github.com/sigoden/aichat/blob/main/config.example.yaml
keybindings: vi keybindings: vi
editor: nvim editor: nvim
@@ -22,34 +23,35 @@ clients:
name: local name: local
api_base: http://192.168.0.204:11343/v1 api_base: http://192.168.0.204:11343/v1
models: &lan_models models: &lan_models
# Speeds are benched decode t/s (clean-night sweeps) — see fl/.config/llamacpp/README.md roster.
- name: Qwen3-Coder-30B-Instruct-UD-Q3_K_XL - name: Qwen3-Coder-30B-Instruct-UD-Q3_K_XL
max_input_tokens: 30000 # ctx 32768 max_input_tokens: 30000 # ctx 32768 · ~30 t/s — main agent coder
- name: Qwen3-Coder-Next-UD-IQ3_XXS - name: Qwen3-Coder-Next-UD-IQ3_XXS
max_input_tokens: 128000 # ctx 131072 max_input_tokens: 128000 # ctx 131072 · ~16 t/s — long sessions (128k)
- name: Qwen3.6-35B-A3B-MTP-UD-IQ3_XXS - name: Qwen3.6-35B-A3B-MTP-UD-IQ3_XXS
max_input_tokens: 22000 # ctx 24576 max_input_tokens: 22000 # ctx 24576 · ~36 t/s — daily driver
supports_vision: true supports_vision: true
- name: Qwen3.6-35B-A3B-Thinking - name: Qwen3.6-35B-A3B-Thinking
max_input_tokens: 22000 # ctx 24576 max_input_tokens: 22000 # ctx 24576 · ~39 t/s — hard problems
supports_vision: true supports_vision: true
- name: Qwen3.5-9B-UD-Q6_K_XL - name: Qwen3.5-9B-UD-Q6_K_XL
max_input_tokens: 30000 # ctx 32768 max_input_tokens: 30000 # ctx 32768 · ~32 t/s — quick tasks
supports_vision: true supports_vision: true
- name: Qwen3.8-27B-UD-IQ3_XXS - name: Qwen3.8-27B-UD-IQ3_XXS
max_input_tokens: 22000 # ctx 24576 max_input_tokens: 22000 # ctx 24576 · ~15 t/s — hybrid reasoner, quiet desktop only
supports_vision: true supports_vision: true
- name: gemma-4-26B-A4B-it-UD-IQ4_XS - name: gemma-4-26B-A4B-it-UD-IQ4_XS
max_input_tokens: 22000 # ctx 24576 max_input_tokens: 22000 # ctx 24576 · ~34 t/s — quality generalist, best vision
supports_vision: true supports_vision: true
- name: gemma-4-E4B-it-UD-Q8_K_XL - name: gemma-4-E4B-it-UD-Q8_K_XL
max_input_tokens: 62000 # ctx 65536 max_input_tokens: 62000 # ctx 65536 · ~57 t/s — fast generalist, long docs
supports_vision: true supports_vision: true
- name: GLM-4.7-Flash-UD-Q4_K_XL - name: GLM-4.7-Flash-UD-Q4_K_XL
max_input_tokens: 22000 # ctx 24576 max_input_tokens: 22000 # ctx 24576 · ~21 t/s — quality coder
- name: gpt-oss-20b - name: gpt-oss-20b
max_input_tokens: 62000 # ctx 65536 max_input_tokens: 62000 # ctx 65536 · ~38 t/s — fast reasoning + tools
- name: gpt-oss-20b-low - name: gpt-oss-20b-low
max_input_tokens: 62000 # ctx 65536 max_input_tokens: 62000 # ctx 65536 · ~37 t/s — snappy answers
# Remote access via duskadiy.com (reachable from anywhere; requires an API key — # Remote access via duskadiy.com (reachable from anywhere; requires an API key —
# $DUSKADIY_API_KEY, from the gitignored conf.d/secrets.fish). The LAN endpoint above is keyless. # $DUSKADIY_API_KEY, from the gitignored conf.d/secrets.fish). The LAN endpoint above is keyless.
# Same server, same model ids — reuse the list above via a YAML anchor. # Same server, same model ids — reuse the list above via a YAML anchor.
+10 -4
View File
@@ -8,14 +8,20 @@
- The user self-hosts models on the LAN at `192.168.0.204`: - The user self-hosts models on the LAN at `192.168.0.204`:
- llama.cpp server — `http://192.168.0.204:11343/v1` (OpenAI-compatible). - llama.cpp server — `http://192.168.0.204:11343/v1` (OpenAI-compatible).
- Ollama — `192.168.0.204:11434`. - Ollama — `192.168.0.204:11434`.
- Recurring models: Qwen3-Coder-30B, gemma-4-26B, DeepSeek-Coder-V2-Lite, GLM-4.7-Flash. When configuring any AI/agent tool, default to these endpoints rather than a cloud API unless asked otherwise. - The available models are the section names in `~/.dots/fl/.config/llamacpp/config.ini` (the router's client model ids) — read that file rather than trusting any list written here; the roster changes often. When configuring any AI/agent tool, default to these endpoints rather than a cloud API unless asked otherwise.
## Git ## Git
- **Do not run `git commit` or `git push`, and never offer or ask to do the committing/pushing yourself** — the user reviews and commits entirely manually (these are also denied in settings.json). Stage/prepare changes and stop there. You may suggest a commit message for the user to use, but nothing more. - **Do not run `git commit` or `git push`, and never offer or ask to do the committing/pushing yourself** — the user reviews and commits entirely manually (denied in settings.json and enforced by the `~/.claude/hooks/git-guard.py` PreToolUse hook, which catches `git -C … push`-style spellings the prefix rules miss and also blocks history rewrites and working-tree discards — `revert`, `cherry-pick`, `rebase`, `merge`, `reset` that moves HEAD or unstages everything, `restore <paths>`, `checkout --`, `stash drop/pop/clear`, `branch -D` — plus `gh pr merge/create/close/edit`; several sessions share this checkout, so never discard changes you did not make). Stage/prepare changes and stop there. You may suggest a commit message for the user to use, but nothing more.
- To hand off a suggested commit message, write it to the per-worktree file `$(git rev-parse --git-path CLAUDE_COMMIT_MSG)`**not** `COMMIT_EDITMSG` (git overwrites that on `git commit`). A `prepare-commit-msg` hook prefills the editor from it (one-shot) on the next `git commit` / lazygit `C`. Just the raw message, no `#` comments. - To hand off a suggested commit message, feed it to `~/.config/git/claude-commit-msg save` **after staging** (it records the staged paths with the message), as a quoted heredoc so quotes and backticks in the body survive:
```sh
~/.config/git/claude-commit-msg save <<'EOF'
<message>
EOF
```
Never write `CLAUDE_COMMIT_MSG` or `COMMIT_EDITMSG` directly — several sessions share this checkout, and the single file got overwritten. A `prepare-commit-msg` hook prefills the next `git commit` / lazygit `C` with the message(s) whose recorded paths are part of that commit, then consumes them. Just the raw message, no `#` comments. `claude-commit-msg list` shows what is pending.
## Formatting ## Formatting
- Lua: format with `stylua` (2-space indent, 120 col, no call parentheses — see any `.stylua.toml`). - Lua: format with `stylua` (2-space indent, 120 col, no call parentheses — see any `.stylua.toml`).
## Dotfiles ## Dotfiles
- Configs live in the dots repo under `.config/` and are surfaced via per-directory symlinks into `~/.config/` (and `~/.claude/`). Editing a file under the repo's `.config/` is editing the live config — no deploy step. - Configs live in the dots repo under `.config/` and are surfaced via **per-file** symlinks (GNU Stow `--no-folding`; directories stay real) into `~/.config/`, and from there into `~/.claude/` by `~/.config/claude/link.sh`. Editing an existing file under the repo's `.config/` is editing the live config — no deploy step. A **new** file is not live until a re-stow creates its symlink (`cd ~/.dots && ./install.sh`), on each host.
+67 -9
View File
@@ -7,20 +7,65 @@ back into this repo.
## What's tracked here ## What's tracked here
- `settings.json` — global settings: permissions (deny git commit/push + read of secret - `settings.json` — global settings: permissions (deny git commit/push, `gh pr` write ops, and
files), default `model`, `enabledPlugins`, vim mode, dark theme, `effortLevel`, fullscreen reads of secret files), default `model`, `enabledPlugins`, vim mode, dark theme, `effortLevel`,
TUI, `statusLine`, `hooks`, `worktree` defaults, … fullscreen TUI, `statusLine`, `hooks`, `worktree` defaults, a generic `autoMode.environment`,
Nothing in here may name a specific project, path or remote: this file is published by the
public mirror, and the auto-mode classifier only reads `autoMode` from user/managed settings
anyway (never from a project's `.claude/settings*.json`).
- `statusline.py` — rich status line (dir, git, model, context %, cost, disk — system metrics live in the tmux bar). - `statusline.py` — rich status line (dir, git, model, context %, cost, disk — system metrics live in the tmux bar).
See [StatusBar.md](StatusBar.md) for what each segment and colour means. See [StatusBar.md](StatusBar.md) for what each segment and colour means.
- `keybindings.json` — custom keybindings (vim-style scroll/navigation). - `keybindings.json` — custom keybindings: vim-style scroll/navigation, plus `ctrl+x v` for the
verbose transcript view (`app:toggleTranscript`, same keystroke as opencode's `<leader>v`; the
default `ctrl+o` still works, and `ctrl+e` inside the view expands everything).
- `CLAUDE.md` — global user preferences applied to every project. - `CLAUDE.md` — global user preferences applied to every project.
- `hooks/format.py` — PostToolUse hook: formats edited files by extension (stylua / ruff / - `hooks/format.py` — PostToolUse hook: formats edited files by extension (stylua / ruff /
prettier / gofmt / rustfmt / fish_indent / shfmt / taplo). Opinionated formatters (stylua, ruff, prettier / gofmt / rustfmt / fish_indent / shfmt / taplo). Every formatter is opt-in: stylua,
prettier, taplo) only run where the project opts in via a config file found walking up; gofmt and ruff, prettier and taplo run only where the project has their config file (found walking up);
rustfmt gate on a project marker (`go.mod` / `Cargo.toml`); fish_indent and shfmt run on sight. gofmt and rustfmt gate on a project marker (`go.mod` / `Cargo.toml`); fish_indent and shfmt gate
No-ops if the formatter isn't installed, and always exits 0. on an `.editorconfig` *section* that matches the file. Wired to `Edit|Write` **and** `Bash`
under auto mode Claude edits through the shell, so the Bash branch scans the command for
path-like tokens and formats the ones modified in the last 30 s. No-ops if the formatter isn't
installed, and always exits 0.
- `hooks/git-guard.py` — PreToolUse hook on `Bash`: structurally blocks anything that commits,
rewrites history or discards working-tree changes — `git commit` / `push` / `revert` /
`cherry-pick` / `rebase` / `merge` (not `--abort`), `clean` / `reset` in any form that moves HEAD
or unstages everything (`reset -- <paths>`, `reset HEAD <path>` stay allowed) / `restore <paths>`
(`--staged <path>` is fine, `--staged .` is not) / `checkout --` / `checkout .` / `stash drop|pop|clear` /
`branch -D|-f|-M`, and `gh pr merge` / `create` / `close` / `edit` — wherever the flags sit
(`git -C . push`, `/usr/bin/git push`, `sh -c 'git push'`), which the prefix-matched deny rules
can't. The discard group is what protects parallel sessions sharing one checkout. Exit 2 + a
one-line reason. Heredoc bodies are skipped on purpose (file contents). Regression matrix:
`bash ~/.claude/hooks/git-guard-test.sh` (a file on purpose — typed inline, the commands under
test would trip the live hook).
- `hooks/notify.py` — Notification hook (`idle_prompt|permission_prompt`): `notify-send` desktop
notification when Claude is waiting on you; no-op on a headless host.
- `hooks/drift-check.sh` — SessionStart + UserPromptSubmit hook, read-only: fails loudly when a
`~/.config/claude/*` file has broken out of the repo into a plain file, or `~/.claude/*` no longer
resolves into the dots repo. That is what Claude Code's atomic settings rewrite does through the
stow layer — on 2026-09-15 it silently reverted the live config to a stale snapshot for an hour,
dropping four hook events. At session start the warning is shown to you (exit 2, session goes
on); on each prompt any drift is fed to Claude as context so it reports it. Fix is still
`link.sh` (heals identical copies, warns otherwise).
- `hooks/local-commands.py` — UserPromptExpansion hook: **TUI-side slash commands**. When you type
`/x` and `skills/x/local.py` exists, the hook runs it with the arguments, shows its output to you
and blocks the expansion (exit 2), so no prompt reaches the model and it costs nothing. Skills
without a `local.py` expand as usual. Caveat baked into the design: the hook fires *after* the
skill body expanded, so a skill with a `local.py` must not also act via a `!` inline command.
- `skills/` — custom [Agent Skills](https://code.claude.com/docs/en/skills). See **Skills** below. - `skills/` — custom [Agent Skills](https://code.claude.com/docs/en/skills). See **Skills** below.
- `link.sh` — idempotent bootstrap that creates the `~/.claude` symlinks below. - `link.sh` — idempotent bootstrap that creates the `~/.claude` symlinks below.
- `../git/claude-commit-msg` + `../git/hooks/prepare-commit-msg` — the commit-message handoff.
A session pipes its suggestion into `claude-commit-msg save`, which stores it as its own file under
`$(git rev-parse --git-path CLAUDE_COMMIT_MSG.d)/` **with the paths it staged**; on the next
`git commit` / lazygit `C` the hook prefills the editor with the message(s) whose paths are part of
that commit and consumes them (`used/`). Parallel sessions therefore never overwrite each other,
and committing one session's files gets that session's message. `claude-commit-msg list` shows
what is pending. The old single `CLAUDE_COMMIT_MSG` file is still honoured. Both tracked here
(deployed to `~/.config/git/`); the hook is installed **per repo** — see setup step 4 — and
deliberately not via a global `core.hooksPath`, which would override a project's own hooks.
The `settings.json` hook commands for the newer scripts are wrapped in `[ -f … ]`, so a host that
has pulled the repo but not yet re-stowed just skips them instead of blocking every Bash call.
Paths inside `settings.json` reference `~/.claude/...` via `$HOME`/`PATH` (`/usr/bin/env Paths inside `settings.json` reference `~/.claude/...` via `$HOME`/`PATH` (`/usr/bin/env
python3 ~/.claude/...`), so they work regardless of the username, python location, or where the python3 ~/.claude/...`), so they work regardless of the username, python location, or where the
@@ -43,7 +88,13 @@ claude
# 3. Reinstall the plugins from settings.json -> enabledPlugins # 3. Reinstall the plugins from settings.json -> enabledPlugins
# (typescript-lsp, frontend-design — anthropics/claude-plugins-official) via /plugin. # (typescript-lsp, frontend-design — anthropics/claude-plugins-official) via /plugin.
# Optional: install any formatters you want the hook to use (stylua, ruff, prettier, …). # Optional: install any formatters you want the hook to use (stylua, ruff, prettier, …),
# and libnotify (notify-send) on GUI hosts for hooks/notify.py.
```
```fish
# 4. In every repo you commit in: install the one-shot commit-message prefill hook
ln -sf ~/.config/git/hooks/prepare-commit-msg (git rev-parse --git-path hooks)/prepare-commit-msg
``` ```
`install.sh` deploys `~/.config/claude` (via stow) and then runs `link.sh` automatically. You only `install.sh` deploys `~/.config/claude` (via stow) and then runs `link.sh` automatically. You only
@@ -73,6 +124,13 @@ so Claude never auto-triggers them. Run them inside the repo whose branch you're
- **`/commit-msg [hint]`** — reads the **staged** diff, infers the repo's commit style from recent - **`/commit-msg [hint]`** — reads the **staged** diff, infers the repo's commit style from recent
`git log` (e.g. this repo's `[Scope] summary`), drafts a matching message, prints it, and copies `git log` (e.g. this repo's `[Scope] summary`), drafts a matching message, prints it, and copies
it to the clipboard with `wl-copy`. Never stages or commits — draft only. it to the clipboard with `wl-copy`. Never stages or commits — draft only.
- **`/verbose [on|off|status]`** — flips the *persisted* verbose view for the current project:
`viewMode` (`verbose` / `default` — the key that actually decides the view and beats a sticky
`/focus` choice for new sessions) plus `verbose` and `showThinkingSummaries`, written to the
project's `.claude/settings.local.json` (machine-local, never committed) rather than the tracked
`settings.json`, so a toggle never shows up as a dots diff. Runs **TUI-side**`local.py` via
`hooks/local-commands.py`, no model turn — with the SKILL.md body as the fallback on a host
where the hook isn't deployed. The *live* toggle is a keypress: `ctrl+x v` / `ctrl+o`.
- **`/pr-loop [pr]`** — babysits the current branch's PR in a self-paced loop: fixes review - **`/pr-loop [pr]`** — babysits the current branch's PR in a self-paced loop: fixes review
comments that don't need your input — from **any** reviewer (Codex, Claude, humans) plus any comments that don't need your input — from **any** reviewer (Codex, Claude, humans) plus any
`@claude` request — `git add`s them, drafts the commit message (`/commit-msg` style) and marks `@claude` request — `git add`s them, drafts the commit message (`/commit-msg` style) and marks
+4 -3
View File
@@ -8,7 +8,7 @@ resize), trailing (lowest-priority) segments are dropped from the right until it
Wide — everything: Wide — everything:
``` ```
📁 ~/.dots │ 🌿 .dots main* │ 🤖 Fable 5 (1M) · xhigh │ 📝 84% (843k) │ 📊 34% 2h54m │ 💰 $0.40 $12.00/h │ 💾 81% 📁 ~/.dots │ 🌿 .dots main* │ 🤖 Fable 5 (1M) · xhigh │ 📝 84% (843k) │ 📊 34% 2h54m │ 💰 $0.40 $12.00/h │ ♻️ 91% 1h │ 💾 81%
``` ```
Narrow — trailing segments trimmed: Narrow — trailing segments trimmed:
@@ -29,9 +29,10 @@ is supplementary to the main colored value.
| 📁 | **Directory** | Current working dir, with `~` for home | cyan | | 📁 | **Directory** | Current working dir, with `~` for home | cyan |
| 🌿 | **Git** | `repo branch` + `*` if dirty + `↑N`/`↓N` ahead/behind upstream | repo = magenta; branch = green when clean, yellow + red `*` when dirty; `↑` cyan, `↓` yellow. Omitted outside a repo | | 🌿 | **Git** | `repo branch` + `*` if dirty + `↑N`/`↓N` ahead/behind upstream | repo = magenta; branch = green when clean, yellow + red `*` when dirty; `↑` cyan, `↓` yellow. Omitted outside a repo |
| 🤖 | **Model** | Active model display name + `· effort` level (`low`/`medium`/`high`/`xhigh`) | name = blue, effort = light gray; effort omitted for models without the param | | 🤖 | **Model** | Active model display name + `· effort` level (`low`/`medium`/`high`/`xhigh`) | name = blue, effort = light gray; effort omitted for models without the param |
| 📝 | **Context** | `% of context window used` + `(Nk)` tokens | window comes from the payload's `context_window.context_window_size`, falling back to `(1M …)` in the display name, else 200k — **not** from `model.id`, which arrives with any `[1m]` suffix already stripped. Adds a red **⚠compact** at ≥80% | | 📝 | **Context** | `% of context window used` + `(Nk)` tokens | usage comes from the payload's `context_window` (`used_percentage` + `current_usage`, Claude Code 2.1.x); the transcript's last `message.usage` is only a fallback. Window from `context_window.context_window_size`, then `(1M …)` in the display name, else 200k — **not** from `model.id`, which arrives with any `[1m]` suffix already stripped. Adds a red **⚠compact** at ≥80% |
| 📊 | **5h usage** | `% of the 5-hour rolling limit used` + time until it resets | from `rate_limits.five_hour`; Pro/Max only, and absent until the first API response of a session | | 📊 | **5h usage** | `% of the 5-hour rolling limit used` + time until it resets | from `rate_limits.five_hour`; Pro/Max only, and absent until the first API response of a session |
| 💰 | **Cost** | `$` session cost so far + `$/h` burn rate | burn rate shown once the session exceeds ~30s | | 💰 | **Cost** | `$` session cost so far + `$/h` burn rate | burn rate shown once the session exceeds ~30s |
| ♻️ | **Prompt cache** | hit `%` (higher is better) + cache TTL (`5m` / `1h`), yellow `·cold` once it has expired | from the payload's `prompt_cache` (2.1.260+: `hit_ratio` 01, `ttl`, `warm`); computed from usage on older payloads. A `1h` TTL is what keeps a 10-min `/pr-loop` cadence warm |
| 💾 | **Disk** | `% used` of the filesystem at the cwd | from `statvfs` | | 💾 | **Disk** | `% used` of the filesystem at the cwd | from `statvfs` |
## Colour legend ## Colour legend
@@ -43,6 +44,7 @@ attention. Light gray = secondary detail. Thresholds (`green < … < yellow <
|---|---|---|---| |---|---|---|---|
| Context | `< 50%` | `5080%` | `≥ 80%` (⚠compact) | | Context | `< 50%` | `5080%` | `≥ 80%` (⚠compact) |
| 5h usage | `< 50%` | `5080%` | `≥ 80%` | | 5h usage | `< 50%` | `5080%` | `≥ 80%` |
| Prompt cache (inverted) | `≥ 80%` | `5080%` | `< 50%` |
| Disk | `< 75%` | `7590%` | `≥ 90%` | | Disk | `< 75%` | `7590%` | `≥ 90%` |
> Light gray is `\033[37m`; dimming (`\033[2m`) is disabled because it blended the gray > Light gray is `\033[37m`; dimming (`\033[2m`) is disabled because it blended the gray
@@ -64,7 +66,6 @@ events, so system metrics sat visibly stale between turns — the tmux status ba
| 🖥️ | `cpu_segment` | `% busy` (diffed `/proc/stat` snapshot) + loadavg + cores (green `<60%` / red `≥85%`) | | 🖥️ | `cpu_segment` | `% busy` (diffed `/proc/stat` snapshot) + loadavg + cores (green `<60%` / red `≥85%`) |
| 🌡️ | `temp_segment` | hottest CPU thermal zone °C (green `<60` / red `≥80`) | | 🌡️ | `temp_segment` | hottest CPU thermal zone °C (green `<60` / red `≥80`) |
| ✏️ | `velocity_segment` | `+added/-removed` lines this session + lines/min | | ✏️ | `velocity_segment` | `+added/-removed` lines this session + lines/min |
| ♻️ | `cache_segment` | prompt-cache hit % (higher is better) |
| ⚙️ | `api_segment` | share of wall-clock time spent in API/inference | | ⚙️ | `api_segment` | share of wall-clock time spent in API/inference |
| | `version_segment` | Claude Code version (`vX.Y.Z`) | | | `version_segment` | Claude Code version (`vX.Y.Z`) |
| 🎨 | `style_segment` | active output-style name | | 🎨 | `style_segment` | active output-style name |
+32
View File
@@ -0,0 +1,32 @@
#!/usr/bin/env bash
# SessionStart hook: refuse to let a severed dotfiles link go unnoticed.
#
# Claude Code writes settings with an atomic temp+rename. Through the stow layer that can
# replace ~/.config/claude/<file> (a symlink into the repo) with a plain FILE holding whatever
# snapshot the writing process had in memory -- silently freezing every later repo edit out
# of the live config. It happened on 2026-09-15 23:31: a stale copy dropped four hook events
# (git-guard among them) and nobody noticed for an hour. link.sh heals the identical case but
# only when run; this makes every session start say so instead.
#
# Read-only: it never relinks. Exit 2 on SessionStart shows stderr to the user and the session
# continues. Exit 0 when everything resolves into the repo.
set -u
src="$HOME/.config/claude"
bad=""
for f in settings.json statusline.py keybindings.json CLAUDE.md link.sh; do
p="$src/$f"
[ -e "$p" ] || continue
if [ ! -L "$p" ]; then bad="$bad $p is a plain file (should be a symlink into the dots repo)"$'\n'; fi
done
# the ~/.claude side too: link.sh's own guard covers it, but only when link.sh runs
for f in settings.json keybindings.json CLAUDE.md statusline.py; do
p="$HOME/.claude/$f"
[ -e "$p" ] || continue
case "$(readlink -f "$p")" in
*"/.dots/"*) ;;
*) bad="$bad $p resolves to $(readlink -f "$p"), not into the dots repo"$'\n' ;;
esac
done
[ -z "$bad" ] && exit 0
printf 'DRIFT: Claude config has broken out of the dots repo; the live config is a stale copy.\n%s fix: review with `diff`, then `bash ~/.config/claude/link.sh` (heals identical copies, warns otherwise)\n' "$bad" >&2
exit 2
+55 -9
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""PostToolUse hook: format an edited file with the right formatter. """PostToolUse hook: format an edited file with the right formatter.
Reads the hook JSON from stdin, extracts the edited file path, and runs a Reads the hook JSON from stdin, works out which file(s) were just edited, and runs a
language-appropriate formatter -- but, to avoid imposing a style on a project language-appropriate formatter -- but, to avoid imposing a style on a project
that never asked for it, every formatter here only runs when the project opts in that never asked for it, every formatter here only runs when the project opts in
via a config file discoverable by walking up from the edited file: stylua, ruff, via a config file discoverable by walking up from the edited file: stylua, ruff,
@@ -9,6 +9,15 @@ prettier and taplo on their own configs, gofmt and rustfmt on go.mod / Cargo.tom
shfmt and fish_indent on .editorconfig. No marker, no formatting -- a repo that shfmt and fish_indent on .editorconfig. No marker, no formatting -- a repo that
hand-formats its shell keeps its own layout. hand-formats its shell keeps its own layout.
Two entry points, both wired in settings.json > hooks > PostToolUse:
* Edit|Write -- the edited path is `tool_input.file_path`.
* Bash -- under `defaultMode: auto` Claude is steered to edit through the shell
(sed -i, heredocs, python), which the Edit|Write matcher never sees.
There is no per-file event for that (`FileChanged` only watches literal
filenames), so this scans `tool_input.command` for path-like tokens with a
handled extension and formats the ones modified in the last few seconds --
the mtime gate keeps a parallel session's dirty files out of it.
Best-effort: always exits 0 so a format hiccup never blocks an edit. jq is not Best-effort: always exits 0 so a format hiccup never blocks an edit. jq is not
guaranteed on the host, hence python for the stdin JSON parse. guaranteed on the host, hence python for the stdin JSON parse.
""" """
@@ -19,6 +28,7 @@ import re
import shutil import shutil
import subprocess import subprocess
import sys import sys
import time
def find_up(start, names): def find_up(start, names):
@@ -178,14 +188,13 @@ HANDLERS = {
} }
def main(): RECENT_SECS = 30 # a Bash edit is "ours" if the file changed this recently
try:
data = json.load(sys.stdin) _EXT_ALT = "|".join(re.escape(e[1:]) for e in HANDLERS)
except Exception: _PATHISH = re.compile(r"[\w.~/@+-]+\.(?:" + _EXT_ALT + r")\b")
return
ti = data.get("tool_input") or {}
tr = data.get("tool_response") or {} def format_one(path):
path = ti.get("file_path") or tr.get("filePath") or ""
if not path or not os.path.isfile(path): if not path or not os.path.isfile(path):
return return
# Resolve symlinks before walking for markers: deployed dotfiles get edited via # Resolve symlinks before walking for markers: deployed dotfiles get edited via
@@ -204,5 +213,42 @@ def main():
pass pass
def recent_paths_in_command(command, cwd):
"""Files a shell command plausibly just wrote: path-like tokens with a handled
extension, resolved against the tool's cwd, that exist and changed within RECENT_SECS.
Unchanged mentions (the source of a `cp`, a file merely grepped) fail the mtime gate."""
now = time.time()
seen, out = set(), []
for tok in _PATHISH.findall(command or ""):
p = os.path.expanduser(tok)
if not os.path.isabs(p):
p = os.path.join(cwd, p)
p = os.path.normpath(p)
if p in seen:
continue
seen.add(p)
try:
if os.path.isfile(p) and now - os.path.getmtime(p) <= RECENT_SECS:
out.append(p)
except OSError:
pass
return out
def main():
try:
data = json.load(sys.stdin)
except Exception:
return
ti = data.get("tool_input") or {}
tr = data.get("tool_response") or {}
if data.get("tool_name") == "Bash":
cwd = data.get("cwd") or os.getcwd()
for p in recent_paths_in_command(ti.get("command"), cwd):
format_one(p)
return
format_one(ti.get("file_path") or tr.get("filePath") or "")
if __name__ == "__main__": if __name__ == "__main__":
main() main()
+121
View File
@@ -0,0 +1,121 @@
#!/usr/bin/env bash
# Regression matrix for git-guard.py: `bash ~/.claude/hooks/git-guard-test.sh` (exit 1 on mismatch).
# Kept as a file on purpose: typed inline, the very commands under test would trip the live hook.
G="$(dirname "$(readlink -f "$0")")/git-guard.py"; fail=0
# `git reset <one arg>` is allowed only for an existing path: give the matrix one to point at.
tmp=$(mktemp -d) && cd "$tmp" && touch existing.py
trap 'rm -rf "$tmp"' EXIT
run() { printf '%s' "{\"tool_name\":\"Bash\",\"tool_input\":{\"command\":$(python3 -c 'import json,sys; print(json.dumps(sys.argv[1]))' "$2")}}" | python3 "$G" 2>/dev/null; rc=$?
got=allow; [ $rc -eq 2 ] && got=block; [ "$got" = "$1" ] && mark=ok || { mark=MISMATCH; fail=1; }; printf '%-8s %-6s %-6s %s\n' "$mark" "$1" "$got" "$(printf '%s' "$2" | head -1)"; }
echo "--- must block: commits / history ---"
run block 'git push'
run block 'git -C /home/coja/.dots push origin main'
run block 'git -c user.name=x commit -m fix'
run block '/usr/bin/git push'
run block 'env GIT_DIR=.git git push'
run block 'timeout 5 git push'
run block 'sudo -u me git push'
run block 'cd /tmp && git push'
run block 'git status; git commit -am wip'
run block 'x=1; git push'
run block 'git push 2>&1 | tee log'
run block "sh -c 'git push origin HEAD'"
run block 'eval "git push"'
run block 'git -C $(pwd) push'
run block 'echo "$(git push)"'
run block 'git --git-dir=/x/.git --work-tree=/x push'
run block 'git commit -m "fix: grep pattern; not a command"'
run block 'git revert HEAD'
run block 'git cherry-pick abc123'
run block 'git rebase -i HEAD~3'
run block 'git rebase main'
run block 'git merge feature'
run block 'git merge --no-ff feature'
echo "--- must block: discards working-tree changes ---"
run block 'git reset --hard HEAD~1'
run block 'git reset HEAD~1 --hard'
run block 'git reset --soft HEAD~1'
run block 'git reset --keep HEAD~2'
run block 'git reset --merge'
run block 'git reset'
run block 'git reset -q'
run block 'git reset HEAD'
run block 'git reset HEAD~1'
run block 'git reset abc1234'
run block 'git reset 0123456789abcdef0123456789abcdef01234567'
run block 'git reset origin/main'
run block 'git reset @{u}'
run block 'git restore --staged .'
run block 'git restore --staged :/'
run block 'git clean -fd'
run block 'git -C . clean -n'
run block 'git restore file.py'
run block 'git restore --staged --worktree file.py'
run block 'git restore -W file.py'
run block 'git checkout -- .'
run block 'git checkout -- file.py'
run block 'git checkout HEAD -- file.py'
run block 'git checkout .'
run block 'git checkout -f main'
run block 'git stash drop'
run block 'git stash pop'
run block 'git stash clear'
run block 'git branch -D feature'
run block 'git branch -f main HEAD~1'
run block 'git branch -M main trunk'
echo "--- must block: gh pr write ops ---"
run block 'gh pr merge 12 --squash'
run block 'gh -R o/r pr create --fill'
run block 'gh pr edit 3 --title x'
run block 'gh pr close 4'
echo "--- must allow ---"
run allow 'git status'
run allow 'git log -1 --oneline'
run allow 'git diff --cached --stat'
run allow 'git add -A common/.config/claude'
run allow 'git rev-parse --git-path CLAUDE_COMMIT_MSG'
run allow 'git -c core.pager=cat log -3'
run allow 'git reset -- file.txt'
run allow 'git reset HEAD file.txt'
run allow 'git reset HEAD existing.py other.py'
run allow 'git reset existing.py'
run allow 'git reset -p'
run allow 'git reset -- .'
run allow 'git restore --staged existing.py'
run allow 'git restore --staged file.py'
run allow 'git restore -S file.py'
run allow 'git checkout main'
run allow 'git checkout -b feature'
run allow 'git switch main'
run allow 'git merge --abort'
run allow 'git stash'
run allow 'git stash list'
run allow 'git stash push -m wip'
run allow 'git stash show -p'
run allow 'git branch -d merged-branch'
run allow 'git branch --list'
run allow 'git branch -vv'
run allow 'git commit-tree HEAD^{tree} -m x'
run allow 'git worktree list'
run allow 'grep -rn "git push" README.md'
run allow 'grep -n "foo; git commit" file'
run allow "echo 'git push'"
run allow 'echo "run git push"'
run allow "cat > README.md <<'EOF'
Deploy with:
git status; git commit -am x; git push origin main
EOF
echo written"
run allow 'python3 - <<EOF
print("git commit is manual")
EOF'
run allow 'gh pr view 12 --json updatedAt'
run allow 'gh api graphql -f query="mutation{...}"'
run allow 'gh auth status'
run allow 'ls -la HEAD#x; git status'
run allow 'git log --grep="git push" -5'
run allow '~/.config/git/claude-commit-msg list'
echo "--- fail-open ---"
printf '%s' '{"tool_name":"Edit","tool_input":{"command":"git push"}}' | python3 "$G"; [ $? -eq 0 ] && echo "ok non-Bash tool ignored" || { echo "MISMATCH non-Bash"; fail=1; }
printf 'not json' | python3 "$G"; [ $? -eq 0 ] && echo "ok garbage stdin passes" || { echo "MISMATCH garbage"; fail=1; }
[ $fail -eq 0 ] && echo "GIT-GUARD MATRIX: all ok" || { echo "GIT-GUARD MATRIX: mismatches above"; exit 1; }
+208
View File
@@ -0,0 +1,208 @@
#!/usr/bin/env python3
"""PreToolUse hook (Bash): enforce the no-commit / no-push contract structurally.
settings.json's deny rules (`Bash(git push:*)`, ...) match a literal *prefix* of the
command text, so `git -C . push`, `git -c k=v commit`, `/usr/bin/git push`,
`env X=1 git push`, `timeout 5 git push` and `sh -c 'git push'` all slip past them.
This hook parses each shell segment instead: finds the git/gh token wherever it sits,
skips git's global options (`-C dir`, `-c k=v`, `--git-dir=...`) and blocks
git commit | push | revert | cherry-pick | rebase | merge (not --abort/--quit/--continue)
git clean | reset (any form that moves HEAD or unstages everything; `reset -- <paths>`,
`reset HEAD <path>` and `reset <existing path>` are fine) | restore <paths> (--staged
alone is fine, except `--staged .` / `:/`) | checkout -- / checkout . / -f
git stash drop | pop | clear git branch -D / -f / -M
gh pr merge | create | close | edit
i.e. anything that commits, rewrites history, or discards working-tree changes -- the last
group matters most with several sessions sharing one checkout. Mirrors pi's permission-gate.
Exit 2 + one stderr line = the call is blocked and Claude reads the reason (stage and
stop; /commit-msg drafts the message). Mirrors pi's permission-gate.ts.
Tokenizing is quote-aware first (shlex in punctuation mode), and only then split at the
separators `;`, `|`, `&&`, `(`, `)`: a quoted word that merely *contains* a separator and a
git verb (a grep pattern, a commit message) stays one word and is not a command. `$(...)`
and backtick bodies are scanned on their own and then collapsed, so `git -C $(pwd) push`
still parses as a push. `sh -c '...'` and `eval` bodies recurse.
Deliberate gaps: heredoc bodies are skipped -- they are file/script *contents*, and a
README that documents pushing must stay writable -- so a script piped in through a heredoc
is not inspected. Aliases (`git ci`) are not expanded. The deny list, the auto-mode
classifier and the global CLAUDE.md still cover those; this is the belt for the prefix
rules' braces. Fails open: any parse error lets the command through (exit 0).
"""
import json
import os
import re
import shlex
import sys
GIT_BLOCK = {"commit", "push", "clean", "revert", "cherry-pick", "rebase"}
MERGE_OK = {"--abort", "--quit", "--continue"}
STASH_BLOCK = {"drop", "pop", "clear"}
BRANCH_FORCE = {"-D", "-f", "--force", "-M"}
RESET_MODES = {"--hard", "--soft", "--merge", "--keep"}
UNSTAGE_ALL = {".", ":/", "*", ":"}
_HEX = re.compile(r"^[0-9a-f]{7,40}$")
def looks_like_ref(a):
return (a in ("HEAD", "ORIG_HEAD", "FETCH_HEAD", "MERGE_HEAD") or a.startswith("refs/")
or "~" in a or "^" in a or "@{" in a or bool(_HEX.match(a)))
GH_PR_BLOCK = {"merge", "create", "close", "edit"}
# global options that take a *separate* argument; the `--opt=value` spelling is one token.
GIT_OPT_ARG = {"-C", "-c", "--git-dir", "--work-tree", "--exec-path", "--namespace",
"--super-prefix", "--config-env"}
GH_OPT_ARG = {"-R", "--repo"}
SHELLS = {"sh", "bash", "dash", "zsh", "ksh", "fish"}
# `<<'EOF' ... EOF` / `<<-EOF ... EOF`: drop the body, keep the command around it.
_HEREDOC = re.compile(r"<<-?\s*(['\"]?)(\w+)\1[^\n]*\n.*?\n[ \t]*\2[ \t]*(?=\n|$)", re.S)
# innermost `$(...)` / `...` command substitutions (peeled repeatedly for nesting).
_SUBST = re.compile(r"\$\(([^()]*)\)|`([^`]*)`")
# tokens that end one simple command and start the next.
_SEP = {";", ";;", "|", "||", "|&", "&", "&&", "(", ")"}
def _skip_opts(args, with_arg):
j = 0
while j < len(args) and args[j].startswith("-"):
j += 2 if args[j] in with_arg else 1
return j
def git_reason(args):
j = _skip_opts(args, GIT_OPT_ARG)
if j >= len(args):
return None
sub, rest = args[j], args[j + 1:]
if sub in GIT_BLOCK:
return f"git {sub}"
if sub == "reset":
opts = [a for a in rest if a.startswith("-") and a != "--"]
if RESET_MODES & set(opts):
return "git reset --hard/--soft/--merge/--keep (moves HEAD)"
if "-p" in opts or "--patch" in opts or "--" in rest:
return None # interactive, or the explicit path form
pos = [a for a in rest if not a.startswith("-")]
if not pos or pos == ["HEAD"]:
return "git reset (unstages everything, other sessions' rounds included)"
if len(pos) == 1 and (looks_like_ref(pos[0]) or not os.path.exists(pos[0])):
return "git reset <commit> (moves HEAD)"
return None # `reset HEAD <paths>` / `reset <paths>`
if sub == "merge" and not (MERGE_OK & set(rest)):
return "git merge (creates a commit)"
if sub == "restore":
staged = any(a in ("--staged", "-S") for a in rest)
worktree = any(a in ("--worktree", "-W") for a in rest)
if not staged or worktree:
return "git restore (discards working-tree changes)"
if UNSTAGE_ALL & {a for a in rest if not a.startswith("-")}:
return "git restore --staged . (unstages everything, other sessions' rounds included)"
if sub == "checkout" and ("--" in rest or "." in rest or "-f" in rest or "--force" in rest):
return "git checkout -- <paths> (discards working-tree changes)"
if sub == "stash" and rest and rest[0] in STASH_BLOCK:
return f"git stash {rest[0]}"
if sub == "branch" and BRANCH_FORCE & set(rest):
return "git branch -D/-f (force-deletes or force-moves a branch)"
return None
def gh_reason(args):
j = _skip_opts(args, GH_OPT_ARG)
if j >= len(args) or args[j] != "pr":
return None
rest = args[j + 1:]
k = _skip_opts(rest, GH_OPT_ARG)
if k < len(rest) and rest[k] in GH_PR_BLOCK:
return f"gh pr {rest[k]}"
return None
def tokenize(text):
"""Quote-aware shell words; the separator characters come out as their own tokens
(shlex punctuation mode). Comment handling is off: a `#` mid-word must not swallow
the rest of the line."""
lex = shlex.shlex(text, posix=True, punctuation_chars=True)
lex.whitespace_split = True
lex.commenters = ""
try:
return list(lex)
except ValueError: # unbalanced quotes: best effort
return text.split()
def segments(toks):
seg = []
for t in toks:
if t in _SEP:
if seg:
yield seg
seg = []
else:
seg.append(t)
if seg:
yield seg
def scan(text, depth=0):
"""Reason string for the first blocked invocation in `text`, else None."""
if depth > 4:
return None
text = _HEREDOC.sub(" ", text)
# command substitutions: scan each body on its own, then collapse it to a plain word so
# the enclosing command still parses (`git -C $(pwd) push` -> `git -C SUBST push`).
bodies = []
def _grab(m):
bodies.append(m.group(1) if m.group(1) is not None else m.group(2))
return " SUBST "
prev = None
while prev != text:
prev, text = text, _SUBST.sub(_grab, text)
for body in bodies:
r = scan(body, depth + 1)
if r:
return r
for toks in segments(tokenize(text)):
for i, tok in enumerate(toks):
base = os.path.basename(tok)
r = None
if base == "git":
r = git_reason(toks[i + 1:])
elif base == "gh":
r = gh_reason(toks[i + 1:])
elif base == "eval":
r = scan(" ".join(toks[i + 1:]), depth + 1)
elif base in SHELLS and "-c" in toks[i + 1:]:
k = toks.index("-c", i + 1) # `sh -c '<cmd>'`: the real command is one word
if k + 1 < len(toks):
r = scan(toks[k + 1], depth + 1)
if r:
return r
return None
def main():
try:
data = json.load(sys.stdin)
if data.get("tool_name", "Bash") != "Bash":
return
cmd = (data.get("tool_input") or {}).get("command") or ""
reason = scan(cmd) if cmd else None
except Exception:
return # fail open: the deny list still applies
if reason:
short = cmd.strip().splitlines()[0]
short = short if len(short) <= 80 else short[:77] + "..."
sys.stderr.write(
f"git-guard: blocked `{short}` ({reason}). The user commits, pushes and merges "
"manually (global CLAUDE.md): stage and stop, and draft the message with /commit-msg.\n"
)
sys.exit(2)
if __name__ == "__main__":
main()
+48
View File
@@ -0,0 +1,48 @@
#!/usr/bin/env python3
"""UserPromptExpansion hook: run a skill TUI-side, without a model turn.
Custom slash commands in Claude Code are prompts -- typing `/x` expands skills/x/SKILL.md
and sends it to the model. For a command that is really just a local action (flip a
setting, print a state) that round trip is waste. This hook gives any skill a TUI-side
implementation: if `~/.claude/skills/<command_name>/local.py` exists, it is executed with
the command's arguments, its output is shown to you, and the expansion is blocked (exit 2),
so nothing reaches the model. Skills without a local.py expand as usual (exit 0).
Order matters: the hook fires AFTER the skill body has been expanded, so any `!` inline
commands in that SKILL.md have already run by the time this executes. A skill with a
local.py must therefore not also apply its action via `!` -- it would happen twice.
Input (stdin JSON): command_name, command_args (list), expanded_prompt, cwd, ...
Fails open: any error here means exit 0 and the normal expansion goes ahead.
"""
import json
import os
import subprocess
import sys
def main():
try:
data = json.load(sys.stdin)
name = data.get("command_name") or ""
args = data.get("command_args") or []
if isinstance(args, str):
args = args.split()
if not name or "/" in name or name.startswith("."):
return
script = os.path.expanduser(f"~/.claude/skills/{name}/local.py")
if not os.path.isfile(script):
return
r = subprocess.run(
[sys.executable, script, *[str(a) for a in args]],
capture_output=True, text=True, timeout=20, cwd=data.get("cwd") or None,
)
except Exception:
return # fail open: let the skill expand normally
out = (r.stdout or "") + (r.stderr or "")
sys.stderr.write(out.rstrip("\n") + f"\n[/{name}: handled TUI-side by skills/{name}/local.py, no model turn]\n")
sys.exit(2)
if __name__ == "__main__":
main()
+51
View File
@@ -0,0 +1,51 @@
#!/usr/bin/env python3
"""Notification hook: a desktop notification when Claude is waiting on you.
settings.json wires it to `idle_prompt|permission_prompt` (the Notification matcher is the
`notification_type`). Push notifications are off in this setup, so this is the local
counterpart of pi's notify.ts. Uses notify-send where present (the Wayland/GUI hosts) and
silently no-ops on a headless box or without a session bus. Payload fields are read
defensively -- the docs do not pin the Notification input schema: `message`, `title`,
`notification_type`, `cwd`.
"""
import json
import os
import shutil
import subprocess
import sys
TITLES = {
"idle_prompt": "Claude Code is waiting for you",
"permission_prompt": "Claude Code needs a permission",
}
def main():
try:
data = json.load(sys.stdin)
except Exception:
return
exe = shutil.which("notify-send")
if not exe:
return
kind = data.get("notification_type") or "notification"
title = data.get("title") or TITLES.get(kind, "Claude Code")
body = (data.get("message") or "").strip() or kind
cwd = data.get("cwd") or ""
if cwd:
home = os.path.expanduser("~")
body += "\n" + (("~" + cwd[len(home):]) if cwd.startswith(home) else cwd)
try:
subprocess.run(
[exe, "--app-name=Claude Code", "--urgency=normal", "--expire-time=8000",
# same tag -> a newer notification replaces the previous one instead of stacking
f"--hint=string:x-canonical-private-synchronous:claude-code-{kind}",
title, body],
timeout=5, capture_output=True,
)
except Exception:
pass
if __name__ == "__main__":
main()
+1 -2
View File
@@ -13,8 +13,7 @@
"bindings": { "bindings": {
"alt+j": "scroll:lineDown", "alt+j": "scroll:lineDown",
"alt+k": "scroll:lineUp", "alt+k": "scroll:lineUp",
"ctrl+shift+j": "scroll:lineDown", "ctrl+x v": "app:toggleTranscript"
"ctrl+shift+k": "scroll:lineUp"
} }
}, },
{ {
+81 -3
View File
@@ -13,6 +13,14 @@
"Bash(git reset --hard:*)", "Bash(git reset --hard:*)",
"Bash(git clean)", "Bash(git clean)",
"Bash(git clean:*)", "Bash(git clean:*)",
"Bash(gh pr merge)",
"Bash(gh pr merge:*)",
"Bash(gh pr create)",
"Bash(gh pr create:*)",
"Bash(gh pr close)",
"Bash(gh pr close:*)",
"Bash(gh pr edit)",
"Bash(gh pr edit:*)",
"Read(**/.env)", "Read(**/.env)",
"Read(**/.env.*)", "Read(**/.env.*)",
"Read(**/*.env)", "Read(**/*.env)",
@@ -30,15 +38,63 @@
"Read(**/.claude/.credentials.json)", "Read(**/.claude/.credentials.json)",
"Read(**/secrets.fish)", "Read(**/secrets.fish)",
"Read(**/*.token)", "Read(**/*.token)",
"Read(**/.envrc)" "Read(**/.envrc)",
"Read(**/.gnupg/**)",
"Read(**/fish_variables)"
], ],
"defaultMode": "auto" "defaultMode": "auto"
}, },
"model": "claude-fable-5[1m]", "model": "claude-fable-5-1[1m]",
"hooks": { "hooks": {
"SessionStart": [
{
"matcher": "startup|resume",
"hooks": [
{
"type": "command",
"command": "f=\"$HOME/.claude/hooks/drift-check.sh\"; if [ -f \"$f\" ]; then exec bash \"$f\"; fi",
"timeout": 5
}
]
}
],
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "f=\"$HOME/.claude/hooks/drift-check.sh\"; if [ -f \"$f\" ]; then out=$(bash \"$f\" 2>&1); [ -n \"$out\" ] && printf \"%s\\n\" \"$out\"; fi; exit 0",
"timeout": 5
}
]
}
],
"UserPromptExpansion": [
{
"hooks": [
{
"type": "command",
"command": "f=\"$HOME/.claude/hooks/local-commands.py\"; if [ -f \"$f\" ]; then exec /usr/bin/env python3 \"$f\"; fi",
"timeout": 25
}
]
}
],
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "f=\"$HOME/.claude/hooks/git-guard.py\"; if [ -f \"$f\" ]; then exec /usr/bin/env python3 \"$f\"; fi",
"timeout": 5
}
]
}
],
"PostToolUse": [ "PostToolUse": [
{ {
"matcher": "Edit|Write|MultiEdit", "matcher": "Edit|Write",
"hooks": [ "hooks": [
{ {
"type": "command", "type": "command",
@@ -47,6 +103,28 @@
"statusMessage": "format" "statusMessage": "format"
} }
] ]
},
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "/usr/bin/env python3 ~/.claude/hooks/format.py",
"timeout": 20
}
]
}
],
"Notification": [
{
"matcher": "idle_prompt|permission_prompt",
"hooks": [
{
"type": "command",
"command": "f=\"$HOME/.claude/hooks/notify.py\"; if [ -f \"$f\" ]; then exec /usr/bin/env python3 \"$f\"; fi",
"timeout": 5
}
]
} }
] ]
}, },
@@ -2,7 +2,7 @@
description: Draft a commit message for the staged changes, matching this repo's commit style, then print it in the reply, save it to the repo's CLAUDE_COMMIT_MSG file, and copy it to the clipboard. Never stages or commits. Use once you've staged what you want to commit. description: Draft a commit message for the staged changes, matching this repo's commit style, then print it in the reply, save it to the repo's CLAUDE_COMMIT_MSG file, and copy it to the clipboard. Never stages or commits. Use once you've staged what you want to commit.
argument-hint: [optional focus/scope hint] argument-hint: [optional focus/scope hint]
disable-model-invocation: true disable-model-invocation: true
allowed-tools: Bash(git diff:*), Bash(git log:*), Bash(git status:*), Bash(git rev-parse:*), Bash(wl-copy:*), Read, Glob, Write allowed-tools: Bash(git diff:*), Bash(git log:*), Bash(git status:*), Bash(git rev-parse:*), Bash(wl-copy:*), Bash(~/.config/git/claude-commit-msg:*), Read, Glob
--- ---
# Draft a commit message for the staged changes # Draft a commit message for the staged changes
@@ -29,14 +29,20 @@ Context for the current commit — what's staged, plus this repo's recent style:
ready. This in-reply copy is the durable one: it lives in the transcript, so it survives even if ready. This in-reply copy is the durable one: it lives in the transcript, so it survives even if
the clipboard gets overwritten later (by me copying something else, or another Claude session the clipboard gets overwritten later (by me copying something else, or another Claude session
running `wl-copy`). Always print it; don't rely on the clipboard alone. running `wl-copy`). Always print it; don't rely on the clipboard alone.
5. **Save it to a file in the repo** so I can recover it any time: resolve the path with 5. **Hand it off** so I can recover it any time — feed the raw message to the CLI as a quoted
`git rev-parse --git-path CLAUDE_COMMIT_MSG` (per-worktree correct), then use the Write tool to heredoc (so quotes, backticks and `$` in the body survive untouched):
write the raw message there — **not** `COMMIT_EDITMSG`, which git overwrites on every ```sh
`git commit` before the editor opens. My `prepare-commit-msg` hook prefills the commit editor ~/.config/git/claude-commit-msg save <<'EOF'
from `CLAUDE_COMMIT_MSG` (one-shot) on the next `git commit` / lazygit `C`. The file lives <message>
inside the git dir so it never shows up in `git status`. EOF
6. **Also copy it to the clipboard** as a convenience: `wl-copy < <path from step 5>`. Then ```
tell me all three places it is: **printed above**, **saved to `CLAUDE_COMMIT_MSG`** (hook That stores it as its own file under `$(git rev-parse --git-path CLAUDE_COMMIT_MSG.d)/` **together with the staged paths
prefills the next commit), and **on the clipboard** — so I never lose it to a stray copy. it describes**, so several sessions can hand off in parallel without overwriting each other. My
`prepare-commit-msg` hook prefills the next `git commit` / lazygit `C` with the message(s) whose
paths are part of that commit, then consumes them. Never write `CLAUDE_COMMIT_MSG` or
`COMMIT_EDITMSG` directly. The `save` output names the saved id and file path.
6. **Also copy it to the clipboard** as a convenience: `wl-copy < <file path printed by save>`.
Then tell me all three places it is: **printed above**, **saved for the hook** (with its id),
and **on the clipboard** — so I never lose it to a stray copy.
7. **Never** run `git add`, `git commit`, or `git push`, and never offer to — I stage and commit 7. **Never** run `git add`, `git commit`, or `git push`, and never offer to — I stage and commit
manually. Draft only. manually. Draft only.
+19 -14
View File
@@ -2,7 +2,7 @@
description: Babysit the current branch's PR — address review comments that don't need my input, re-check every ~10 min, and stop when there are no open comments left or the automated reviewers (Codex, Claude) hit their limit. Prep-only — stages fixes and drafts the commit message; never commits, pushes, or merges. description: Babysit the current branch's PR — address review comments that don't need my input, re-check every ~10 min, and stop when there are no open comments left or the automated reviewers (Codex, Claude) hit their limit. Prep-only — stages fixes and drafts the commit message; never commits, pushes, or merges.
argument-hint: [pr number or url — optional; defaults to the current branch's PR] argument-hint: [pr number or url — optional; defaults to the current branch's PR]
disable-model-invocation: true disable-model-invocation: true
allowed-tools: Bash(gh:*), Bash(git status:*), Bash(git diff:*), Bash(git log:*), Bash(git rev-parse:*), Bash(git fetch:*), Bash(git add:*), Bash(wl-copy:*), Read, Edit, Write, Grep, Glob, ScheduleWakeup allowed-tools: Bash(gh:*), Bash(git status:*), Bash(git diff:*), Bash(git log:*), Bash(git rev-parse:*), Bash(git fetch:*), Bash(git add:*), Bash(wl-copy:*), Bash(~/.config/git/claude-commit-msg:*), Read, Edit, Write, Grep, Glob, ScheduleWakeup
--- ---
# Watch this PR and resolve review comments (prep-only) # Watch this PR and resolve review comments (prep-only)
@@ -36,11 +36,12 @@ comments left or the automated reviewers (Codex/Claude) hit a usage limit.
session stays open**. **To stop it: press `Esc` while I'm idle between cycles** (that clears the session stays open**. **To stop it: press `Esc` while I'm idle between cycles** (that clears the
queued wake-up); closing the session also stops it. A plain message does **not** cancel a pending queued wake-up); closing the session also stops it. A plain message does **not** cancel a pending
wake-up — use `Esc`. wake-up — use `Esc`.
- **Every token counts.** Each wake-up re-reads the whole conversation with a cold prompt cache - **Every token counts.** Each wake-up re-sends the whole conversation. On a Claude subscription
(the 10-min cadence outlives the 5-min cache TTL), so cost compounds with every cycle and every the prompt cache holds for **1 hour**, so the 10-min cadence stays warm; on an API key it drops to
extra line in context. Therefore: fetch **filtered** data only (use the `--jq` filters below — 5 min and every cycle is a cold read. Either way every extra line in context rides along on every
never let raw unfiltered JSON into the conversation), summarize deltas rather than restating later cycle. Therefore: fetch **filtered** data only (use the `--jq` filters below — never let raw
unchanged state, and keep idle cycles to **one cheap API call and ≤2 lines of output**. unfiltered JSON into the conversation), summarize deltas rather than restating unchanged state,
and keep idle cycles to **one cheap API call and ≤2 lines of output**.
Current state (auth + the current branch's PR): Current state (auth + the current branch's PR):
@@ -233,10 +234,11 @@ Current state (auth + the current branch's PR):
9. **Draft the commit message — exactly like `/commit-msg`:** from the staged diff and the repo's 9. **Draft the commit message — exactly like `/commit-msg`:** from the staged diff and the repo's
recent `git log` style (this repo uses `[Scope] summary`), write a message that matches. Then: recent `git log` style (this repo uses `[Scope] summary`), write a message that matches. Then:
- **(a) Print it** in your reply as a fenced ` ```text ` block — the durable copy. - **(a) Print it** in your reply as a fenced ` ```text ` block — the durable copy.
- **(b) Write it** with the Write tool to the path from `git rev-parse --git-path CLAUDE_COMMIT_MSG` - **(b) Hand it off:** `~/.config/git/claude-commit-msg save <<'EOF'``EOF` (the message as a
(**not** `COMMIT_EDITMSG`git overwrites that on `git commit`; the `prepare-commit-msg` hook quoted heredoc, so quotes and backticks survive) — it is stored with the paths you staged this cycle, so the `prepare-commit-msg` hook prefills it only
prefills the editor from `CLAUDE_COMMIT_MSG`). for the commit that contains them (other sessions' handoffs are not overwritten). Never write
- **(c) Copy it:** `wl-copy < <that path>`. `CLAUDE_COMMIT_MSG` / `COMMIT_EDITMSG` directly.
- **(c) Copy it:** `wl-copy < <file path printed by save>`.
- Tell me all three locations. **Never** run `git commit`. - Tell me all three locations. **Never** run `git commit`.
(Skip this step when nothing new was staged this cycle.) (Skip this step when nothing new was staged this cycle.)
@@ -252,9 +254,10 @@ Current state (auth + the current branch's PR):
needs-input questions are posted (reactions don't bump `updatedAt`, comments do — so your own needs-input questions are posted (reactions don't bump `updatedAt`, comments do — so your own
activity doesn't defeat the next cheap check), then save state activity doesn't defeat the next cheap check), then save state
to `<git-dir>/pr-loop-state.json`. Then schedule: to `<git-dir>/pr-loop-state.json`. Then schedule:
- **normal cycle:** `ScheduleWakeup(delaySeconds=600, prompt="/pr-loop", reason="recheck PR - **normal cycle:** `ScheduleWakeup(delaySeconds=600, noop=false, prompt="/pr-loop",
#<num> review comments")`; reason="recheck PR #<num> review comments")`;
- **idle cycle** (step 3 short-circuited): back off — `delaySeconds=1200`, I'm clearly away; - **idle cycle** (step 3 short-circuited): back off — `delaySeconds=1200, noop=true` (idle
ticks then collapse into one line in my terminal instead of stacking), I'm clearly away;
re-running `/pr-loop` checks immediately. re-running `/pr-loop` checks immediately.
End the turn with a sign-off that states when you'll run again and how to stop, e.g.: End the turn with a sign-off that states when you'll run again and how to stop, e.g.:
@@ -268,7 +271,9 @@ Current state (auth + the current branch's PR):
- **Never** `git commit`, `git push`, `gh pr merge`, `gh pr create`, `gh pr close`, or `gh pr edit` - **Never** `git commit`, `git push`, `gh pr merge`, `gh pr create`, `gh pr close`, or `gh pr edit`
— I do all of those. You stage, react, resolve, and draft; nothing more. The only comment you may — I do all of those. You stage, react, resolve, and draft; nothing more. The only comment you may
post is a needs-input decision question. post is a needs-input decision question. This is enforced, not just asked: `settings.json` denies
those commands and the `git-guard.py` PreToolUse hook blocks any spelling of them, so a slip is
refused with a reason rather than executed.
- Act only on the target PR. Don't touch unrelated files or other PRs. - Act only on the target PR. Don't touch unrelated files or other PRs.
- **Keep context lean:** no raw JSON dumps in replies, no re-listing threads that haven't changed, - **Keep context lean:** no raw JSON dumps in replies, no re-listing threads that haven't changed,
idle cycles ≤2 lines. Every line you emit is re-read (uncached) on every later cycle. idle cycles ≤2 lines. Every line you emit is re-read (uncached) on every later cycle.
@@ -2,7 +2,7 @@
description: Review all changes on the current branch (diff vs its base) for bugs, security issues, optimizations, and readability/scannability, then apply the readability + safe improvements after you approve. Use right before opening a PR. description: Review all changes on the current branch (diff vs its base) for bugs, security issues, optimizations, and readability/scannability, then apply the readability + safe improvements after you approve. Use right before opening a PR.
argument-hint: [base-branch] argument-hint: [base-branch]
disable-model-invocation: true disable-model-invocation: true
allowed-tools: Bash(git diff:*), Bash(git log:*), Bash(git status:*), Bash(git merge-base:*), Bash(git symbolic-ref:*), Bash(git rev-parse:*), Bash(git show-ref:*), Bash(git branch:*), Read, Grep, Glob allowed-tools: Bash(git diff:*), Bash(git log:*), Bash(git status:*), Bash(git merge-base:*), Bash(git symbolic-ref:*), Bash(git rev-parse:*), Bash(git show-ref:*), Bash(git branch:*), Read, Grep, Glob, Edit, Write
--- ---
# Review this branch # Review this branch
@@ -59,11 +59,10 @@ Context auto-collected for the current branch vs. its base:
surfaced, not a blanket dump. Only include the ones that actually fit: surfaced, not a blanket dump. Only include the ones that actually fit:
- `/security-review` — a deeper, whole-branch security pass; suggest whenever the 🔒 lens - `/security-review` — a deeper, whole-branch security pass; suggest whenever the 🔒 lens
flagged something or the diff touches auth, crypto, input handling, secrets, or network I/O. flagged something or the diff touches auth, crypto, input handling, secrets, or network I/O.
- `/code-review` (add `--fix` to apply, `--comment` for inline PR comments) — a second review - `/code-review` (alias `/review`; add `--fix` to apply, `--comment` for inline PR comments) —
of the working-tree diff at a chosen effort level. a second review of the working-tree diff, a PR number or a branch at a chosen effort level.
- `/simplify` — apply reuse / simplification / efficiency cleanups beyond the safe ones applied - `/simplify` — apply reuse / simplification / efficiency cleanups beyond the safe ones applied
here. here.
- `/verify` — run the app to confirm the change behaves, if this branch changed behavior. - `/verify` — run the app to confirm the change behaves, if this branch changed behavior.
- `/pr-description` — draft the PR text from this branch once the review is clean. - `/pr-description` — draft the PR text from this branch once the review is clean.
- `/review` — review the PR on GitHub after it's open.
8. **Never** run `git commit` or `git push` — I do that manually. 8. **Never** run `git commit` or `git push` — I do that manually.
@@ -0,0 +1,20 @@
---
description: Toggle the persisted verbose view for this project (viewMode verbose/default, plus verbose + showThinkingSummaries) by writing .claude/settings.local.json, and print the live-view keybind. Usage: /verbose [on|off|status]; no argument toggles. Runs TUI-side via the UserPromptExpansion hook; this body is only the fallback.
argument-hint: [on|off|status]
disable-model-invocation: true
allowed-tools: Bash(python3:*)
---
# /verbose (fallback path)
Normally you never see this: `hooks/local-commands.py` (UserPromptExpansion) runs
`skills/verbose/local.py` and blocks the expansion, so `/verbose` costs no model turn. This body
only loads on a host where that hook isn't deployed yet.
Run exactly this, then relay its output to me verbatim in a fenced ```text block with **at most one
line** of your own:
python3 ~/.claude/skills/verbose/local.py $ARGUMENTS
Don't edit any settings file yourself, don't run anything else, and don't offer follow-ups. If it
reports an error, quote it and stop.
+99
View File
@@ -0,0 +1,99 @@
#!/usr/bin/env python3
"""`/verbose`, TUI-side: flip Claude Code's *persisted* verbose view for the current project.
Run by hooks/local-commands.py from the UserPromptExpansion hook, i.e. when you type
`/verbose [on|off|status]` -- the hook executes this and blocks the skill expansion, so no
prompt ever reaches the model and the output below is shown to you directly. (On a host
without that hook, skills/verbose/SKILL.md falls back to having the model run it.)
Claude Code has no built-in runtime `/verbose`; the live toggle is a keypress (`ctrl+x v`
here, `ctrl+o` by default -- `app:toggleTranscript`). What can be scripted is the persisted
default: `viewMode` ("verbose" | "default" | "focus" -- this is what actually decides the view;
it also beats a sticky `/focus` choice for new sessions), plus `verbose` (full tool
input/output inline) and `showThinkingSummaries` for versions without viewMode. All accept any
settings scope, so this writes the project's
`.claude/settings.local.json` -- local to this machine, never committed -- instead of the
dots-tracked ~/.claude/settings.json, which would show a diff on every toggle. Project-local
settings win over user settings, so the user default stays `verbose: true` and a project can
be quietened individually.
usage: local.py [on|off|toggle|status] (no argument = toggle)
"""
import json
import os
import subprocess
import sys
KEYS = ("verbose", "showThinkingSummaries")
VIEW = "viewMode" # "verbose" | "default" | "focus"; overrides KEYS and the sticky /focus choice
LIVE = "live view: ctrl+x v (or ctrl+o) toggles the transcript; ctrl+e inside it shows everything"
def project_root():
try:
r = subprocess.run(["git", "rev-parse", "--show-toplevel"], capture_output=True, text=True, timeout=2)
if r.returncode == 0 and r.stdout.strip():
return r.stdout.strip()
except Exception:
pass
return os.getcwd()
def load(path):
try:
with open(path) as fh:
return json.load(fh)
except FileNotFoundError:
return {}
except Exception as e: # a broken local file must not be silently overwritten
sys.exit(f"error: cannot parse {path}: {e}")
def main():
raw = " ".join(sys.argv[1:]).strip()
mode = raw.lower() or "toggle"
if mode not in ("on", "off", "toggle", "status"):
sys.exit("usage: /verbose [on|off|status]")
root = project_root()
local_path = os.path.join(root, ".claude", "settings.local.json")
user = load(os.path.expanduser("~/.claude/settings.json"))
local = load(local_path)
effective = {k: bool(local.get(k, user.get(k, False))) for k in KEYS}
view = local.get(VIEW, user.get(VIEW))
def state_of(view, eff):
if view == "verbose":
return "ON"
if view in ("default", "focus"):
return f"OFF ({view} forced by settings)"
if all(eff.values()):
return "ON via the verbose key (viewMode unset: a sticky /focus choice still wins)"
return "OFF" if not any(eff.values()) else "MIXED"
if mode != "status":
current_on = state_of(view, effective).startswith("ON")
on = {"on": True, "off": False}.get(mode, not current_on)
if on:
local[VIEW] = "verbose" # forces verbose for new sessions, beating a sticky /focus
else:
local.pop(VIEW, None) # release: the sticky /focus choice (or default) applies again
for k in KEYS:
local[k] = on
os.makedirs(os.path.dirname(local_path), exist_ok=True)
with open(local_path, "w") as fh:
json.dump(local, fh, indent=2)
fh.write("\n")
effective = {k: on for k in KEYS}
view = local.get(VIEW, user.get(VIEW))
print(f"verbose view: {'ON' if on else 'OFF'} for {root}")
print(f" written to {local_path} (viewMode applies to new sessions; the current one: ctrl+x v)")
else:
print(f"verbose view: {state_of(view, effective)} for {root}")
print(f" {VIEW}: {view or '-'} (project-local: {local.get(VIEW, '-')}, user: {user.get(VIEW, '-')})")
for k in KEYS:
print(f" {k}: {str(effective[k]).lower()} (project-local: {local.get(k, '-')}, user: {user.get(k, '-')})")
print(LIVE)
if __name__ == "__main__":
main()
+52 -17
View File
@@ -3,7 +3,8 @@
One line: vim mode | dir (repo-relative, hidden at the root) | One line: vim mode | dir (repo-relative, hidden at the root) |
git(repo/branch +dirty +ahead/behind) | model (+effort) | git(repo/branch +dirty +ahead/behind) | model (+effort) |
context% (+compact warn) | 5h usage (+reset eta) | cost (+burn rate) | disk. If the context% (+compact warn) | 5h usage (+reset eta) | cost (+burn rate) |
prompt-cache hit% (+ttl, cold marker) | disk. If the
rendered line would overflow the terminal, trailing (lowest-priority) segments are rendered line would overflow the terminal, trailing (lowest-priority) segments are
dropped until it fits. dropped until it fits.
(Dormant, re-addable in main(): ram, cpu%, temp -- retired 2026-07-26 because the (Dormant, re-addable in main(): ram, cpu%, temp -- retired 2026-07-26 because the
@@ -89,7 +90,24 @@ def term_cols():
return 0 return 0
# --- shared transcript usage ------------------------------------------------ # --- context usage ------------------------------------------------------------
_USAGE_KEYS = ("input_tokens", "cache_creation_input_tokens", "cache_read_input_tokens")
def payload_usage(data):
"""`context_window.current_usage` from the status payload, shaped like `message.usage`.
Claude Code 2.1.x hands the status line its own token accounting, so the transcript
scan below is only a fallback for payloads that predate it (or lack a transcript).
"""
cw = data.get("context_window")
cu = cw.get("current_usage") if isinstance(cw, dict) else None
if isinstance(cu, dict) and any(isinstance(cu.get(k), (int, float)) for k in _USAGE_KEYS):
return cu
return {}
# --- shared transcript usage (fallback) ------------------------------------------
def _scan_usage(text): def _scan_usage(text):
"""Last `message.usage` block among these JSONL lines, or {}.""" """Last `message.usage` block among these JSONL lines, or {}."""
usage = {} usage = {}
@@ -113,7 +131,7 @@ TRANSCRIPT_TAIL = 1 << 20 # 1 MiB
def last_usage(data): def last_usage(data):
"""Most recent `message.usage` block from the transcript, or {}. """Most recent `message.usage` block from the transcript, or {}.
Reads only the tail: this runs on every status refresh and transcripts grow to Fallback only -- see payload_usage(). Reads only the tail: this runs on every status refresh and transcripts grow to
tens of MB, so re-parsing the whole file each time is unbounded work for a value tens of MB, so re-parsing the whole file each time is unbounded work for a value
that lives at the end. Falls back to a full scan if the tail holds no usage block that lives at the end. Falls back to a full scan if the tail holds no usage block
(a long stretch of tool results can push it past the window). (a long stretch of tool results can push it past the window).
@@ -292,6 +310,11 @@ def context_segment(data, usage):
) )
window = context_window(data) window = context_window(data)
pct = used / window * 100 if window else 0 pct = used / window * 100 if window else 0
# Prefer Claude Code's own percentage when the payload carries it (2.1.x) -- it
# already accounts for the auto-compact reserve and window quirks we can't see.
cw = data.get("context_window")
if isinstance(cw, dict) and isinstance(cw.get("used_percentage"), (int, float)):
pct = float(cw["used_percentage"])
col = bucket(pct, 50, 80) col = bucket(pct, 50, 80)
out = "📝 " + color(f"{pct:.0f}%", col) + color(f" ({used / 1000:.0f}k)", "gray", dim=True) out = "📝 " + color(f"{pct:.0f}%", col) + color(f" ({used / 1000:.0f}k)", "gray", dim=True)
if pct >= 80: if pct >= 80:
@@ -353,20 +376,31 @@ def velocity_segment(data):
return "" return ""
def cache_segment(usage): def cache_segment(usage, data=None):
"""Prompt-cache hit % (higher is better) + the cache TTL, `·cold` when the cache has
expired. Hit % from the 2.1.260+ payload `prompt_cache.hit_ratio` (a 0..1 fraction, null
while all counts are zero), else computed from the usage block; ttl/cold always from the
payload when present. A 1h TTL is what keeps a 10-minute pr-loop cadence warm."""
try: try:
if not usage: pc = (data or {}).get("prompt_cache")
pc = pc if isinstance(pc, dict) else {}
pct = None
if isinstance(pc.get("hit_ratio"), (int, float)):
pct = float(pc["hit_ratio"]) * 100
elif usage:
read = usage.get("cache_read_input_tokens", 0)
total = usage.get("input_tokens", 0) + usage.get("cache_creation_input_tokens", 0) + read
if total > 0:
pct = read / total * 100
if pct is None:
return "" return ""
read = usage.get("cache_read_input_tokens", 0) out = "♻️ " + color(f"{pct:.0f}%", bucket(pct, 50, 80, invert=True))
total = ( ttl = pc.get("ttl")
usage.get("input_tokens", 0) if isinstance(ttl, str) and ttl:
+ usage.get("cache_creation_input_tokens", 0) out += color(f" {ttl}", "gray")
+ read if pc.get("warm") is False:
) out += color(" ·cold", "yellow")
if total <= 0: return out
return ""
pct = read / total * 100
return "♻️ " + color(f"{pct:.0f}%", bucket(pct, 50, 80, invert=True))
except Exception: except Exception:
return "" return ""
@@ -532,7 +566,7 @@ def main():
or data.get("cwd") or data.get("cwd")
or os.getcwd() or os.getcwd()
) )
usage = last_usage(data) usage = payload_usage(data) or last_usage(data)
top = git_top(cwd) top = git_top(cwd)
# One row: work (left) then system (right), joined. If it would overflow the # One row: work (left) then system (right), joined. If it would overflow the
@@ -540,7 +574,7 @@ def main():
# Dormant helpers kept above for easy re-add: ram_segment, cpu_segment, # Dormant helpers kept above for easy re-add: ram_segment, cpu_segment,
# temp_segment (retired -- they only refresh on message events, so they sat # temp_segment (retired -- they only refresh on message events, so they sat
# stale; the tmux status bar owns system metrics now), velocity_segment, # stale; the tmux status bar owns system metrics now), velocity_segment,
# cache_segment, api_segment, version_segment, style_segment. # api_segment, version_segment, style_segment.
work = [s for s in ( work = [s for s in (
vim_segment(data), vim_segment(data),
dir_segment(cwd, top), dir_segment(cwd, top),
@@ -549,6 +583,7 @@ def main():
context_segment(data, usage), context_segment(data, usage),
usage_segment(data), usage_segment(data),
cost_segment(data), cost_segment(data),
cache_segment(usage, data),
) if s] ) if s]
system = [s for s in ( system = [s for s in (
disk_segment(cwd), disk_segment(cwd),
+6 -1
View File
@@ -5,7 +5,11 @@ set -gx EDITOR nvim
set -gx PAGER less set -gx PAGER less
set -gx QT_FONT_DPI 96 set -gx QT_FONT_DPI 96
set -gx PI_SKIP_VERSION_CHECK 1 set -gx PI_SKIP_VERSION_CHECK 1
set -gx OPENCODE_CONFIG "$HOME/.config/opencode/openai-gpt.jsonc" # opencode GPT/Codex OAuth overlay — only where `opencode auth login` stored an OpenAI credential,
# so hosts without the login don't get an unusable provider in the model picker.
if test -r ~/.local/share/opencode/auth.json; and grep -q '"openai"' ~/.local/share/opencode/auth.json
set -gx OPENCODE_CONFIG "$HOME/.config/opencode/openai-gpt.jsonc"
end
set -gx MANPAGER "nvim +Man!" set -gx MANPAGER "nvim +Man!"
# fzf's built-in walker is the one search tool that ignores .ignore files, so point it at # fzf's built-in walker is the one search tool that ignores .ignore files, so point it at
# fd, which honours them. fd still hides dotfiles by default everywhere else — this only # fd, which honours them. fd still hides dotfiles by default everywhere else — this only
@@ -48,6 +52,7 @@ if status is-interactive
# Git # Git
alias gti="git" alias gti="git"
alias gs="git status -s" alias gs="git status -s"
alias gp="git pull"
alias gca="git add -p . && git commit" alias gca="git add -p . && git commit"
alias gd="git diff --word-diff" alias gd="git diff --word-diff"
alias gl="git log --graph --show-signature" alias gl="git log --graph --show-signature"
+29 -5
View File
@@ -1,4 +1,25 @@
function opencodes --description 'contained opencode' function opencodes --description 'contained opencode (bwrap); extra args are passed to opencode'
set -l cfg ~/.config/opencode
# ~/.config/opencode is stowed as per-file symlinks into ~/.dots. Bind each link's real parent
# dir read-only too, or the sandbox sees dangling links and opencode starts with NO config.
set -l dirs
for f in $cfg/* $cfg/*/*
test -L $f; or continue
set -l d (path resolve $f | path dirname)
string match -q "$cfg*" $d; and continue
contains -- $d $dirs; or set -a dirs $d
end
set -l binds
for d in $dirs
set -a binds --ro-bind $d $d
end
# --clearenv drops everything; pass through only what the TUI and its tools need.
set -l env --setenv PATH /usr/local/bin:/usr/bin:/bin
for v in HOME TERM COLORTERM LANG LC_ALL EDITOR OPENCODE_CONFIG DUSKADIY_API_KEY
set -l val $$v
test -n "$val"; and set -a env --setenv $v $val
end
mkdir -p ~/.cache/opencode ~/.local/state/opencode ~/.local/share/opencode
bwrap \ bwrap \
--unshare-all \ --unshare-all \
--new-session \ --new-session \
@@ -12,10 +33,13 @@ function opencodes --description 'contained opencode'
--ro-bind /lib64 /lib64 \ --ro-bind /lib64 /lib64 \
--ro-bind /bin /bin \ --ro-bind /bin /bin \
--ro-bind /etc /etc \ --ro-bind /etc /etc \
--ro-bind ~/.config/opencode ~/.config/opencode \ --ro-bind $cfg $cfg \
--tmpfs /tmp/node-compile-cache \ $binds \
--tmpfs /tmp \
--bind ~/.cache/opencode ~/.cache/opencode \
--bind ~/.local/state/opencode ~/.local/state/opencode \ --bind ~/.local/state/opencode ~/.local/state/opencode \
--bind ~/.local/share/opencode ~/.local/share/opencode \ --bind ~/.local/share/opencode ~/.local/share/opencode \
--bind "$(pwd)" "$(pwd)" \ --bind $PWD $PWD \
opencode $env \
opencode $argv
end end
+256
View File
@@ -0,0 +1,256 @@
#!/usr/bin/env python3
"""claude-commit-msg -- hand suggested commit messages from Claude sessions to `git commit`.
Several Claude sessions work on one checkout at once, each staging its own round. The old
handoff was ONE file (`<git-dir>/CLAUDE_COMMIT_MSG`) that the prepare-commit-msg hook pasted
into the editor: the second session silently overwrote the first, and the hook could not
know which message described which files. Now every handoff is its own file under
`<git-dir>/CLAUDE_COMMIT_MSG.d/`, tagged with the paths it describes, and the hook prefills
only the message(s) whose paths are part of the commit being made:
<id>.msg the raw message
<id>.paths one repo-relative path per line (the staged set at save time)
used/ consumed messages, kept for recovery
save [--paths P ...] [--slug S] read the message on stdin; paths default to the index
list pending messages and how they match the current index
show ID | rm ID
prefill MSGFILE [SOURCE] hook entry point (prepare-commit-msg)
Matching in `prefill`: a message whose paths are all in the commit is a FULL match (prefilled,
then consumed); overlapping only partly = PARTIAL (prefilled, kept -- the rest may come in a
later commit); no paths recorded = WILDCARD (prefilled, consumed), which is also how a legacy
single CLAUDE_COMMIT_MSG file is treated. Several matches are all prefilled, separated by a
comment line git strips, so you pick in the editor. Nothing matching = editor untouched.
Everything lives inside the git dir, so it never shows in `git status`; `--git-path` keeps it
per-worktree. Always exits 0 from `prefill`: a hook failure must never abort a commit.
"""
import os
import re
import shutil
import subprocess
import sys
import time
def git(*args, check=True):
r = subprocess.run(["git", *args], capture_output=True, text=True)
if check and r.returncode != 0:
sys.exit(f"claude-commit-msg: git {' '.join(args)} failed: {r.stderr.strip()}")
return r.stdout
def store():
d = git("rev-parse", "--git-path", "CLAUDE_COMMIT_MSG.d").strip()
d = os.path.abspath(d)
os.makedirs(os.path.join(d, "used"), exist_ok=True)
return d
def staged_paths():
out = git("diff", "--cached", "--name-only", "-z", check=False)
return [p for p in out.split("\0") if p]
def comment_char():
c = git("config", "--get", "core.commentchar", check=False).strip()
return "#" if not c or c == "auto" else c[0]
def pending(d):
"""[(id, message, paths|None)] oldest first. paths None = wildcard."""
out = []
for name in sorted(os.listdir(d)):
if not name.endswith(".msg"):
continue
mid = name[:-4]
try:
with open(os.path.join(d, name)) as fh:
msg = fh.read()
except OSError:
continue
paths = None
pf = os.path.join(d, mid + ".paths")
if os.path.exists(pf):
with open(pf) as fh:
paths = [ln.rstrip("\n") for ln in fh if ln.strip()]
out.append((mid, msg, paths))
return out
def slugify(text):
m = re.match(r"\s*\[([^\]]+)\]", text) # this repo's `[Scope] summary` convention
base = m.group(1) if m else " ".join(text.split()[:3])
s = re.sub(r"[^a-z0-9]+", "-", base.lower()).strip("-")
return s[:32] or "msg"
def cmd_save(argv):
paths, slug = None, None
i = 0
while i < len(argv):
if argv[i] == "--paths":
paths = []
i += 1
while i < len(argv) and not argv[i].startswith("--"):
paths.append(argv[i])
i += 1
continue
if argv[i] == "--slug" and i + 1 < len(argv):
slug = argv[i + 1]
i += 2
continue
sys.exit(f"claude-commit-msg save: unknown argument {argv[i]!r}")
msg = sys.stdin.read().rstrip() + "\n"
if not msg.strip():
sys.exit("claude-commit-msg save: empty message on stdin")
if paths is None:
paths = staged_paths()
d = store()
mid = time.strftime("%Y%m%d-%H%M%S") + "-" + (slug or slugify(msg))
n = 1
while os.path.exists(os.path.join(d, f"{mid}.msg")):
n += 1
mid = f"{mid}-{n}"
with open(os.path.join(d, mid + ".msg"), "w") as fh:
fh.write(msg)
with open(os.path.join(d, mid + ".paths"), "w") as fh:
fh.write("".join(p + "\n" for p in paths))
print(f"saved {mid} ({len(paths)} path{'s' if len(paths) != 1 else ''}) -> {os.path.join(d, mid + '.msg')}")
if not paths:
print("warning: no paths recorded (nothing staged?) -- it will prefill on ANY commit", file=sys.stderr)
warn_if_hook_is_old()
def warn_if_hook_is_old():
"""A clone still running the legacy hook (or none) reads only the single file this tool
never writes -- the editor would come up empty with no error. Say so at save time."""
hook = git("rev-parse", "--git-path", "hooks/prepare-commit-msg", check=False).strip()
try:
with open(hook, errors="ignore") as fh:
ok = "claude-commit-msg" in fh.read()
except OSError:
ok = False
if not ok:
print(
"warning: this repo's prepare-commit-msg hook is missing or predates claude-commit-msg, so this\n"
" message will NOT be prefilled here. Install the tracked hook once per repo:\n"
" ln -sf ~/.config/git/hooks/prepare-commit-msg (git rev-parse --git-path hooks)/prepare-commit-msg",
file=sys.stderr,
)
def classify(paths, staged):
if paths is None or not paths:
return "wildcard"
ps = set(paths)
if ps <= staged:
return "full"
if ps & staged:
return "partial"
return "none"
def cmd_list(_argv):
d = store()
staged = set(staged_paths())
items = pending(d)
legacy = git("rev-parse", "--git-path", "CLAUDE_COMMIT_MSG").strip()
if not items and not os.path.exists(legacy):
print("no pending messages")
return
for mid, msg, paths in items:
n = len(paths) if paths else 0
print(f"{mid} [{classify(paths, staged):8}] {n} path{'s' if n != 1 else ''} {msg.splitlines()[0][:70]}")
if os.path.exists(legacy) and os.path.getsize(legacy) > 0:
with open(legacy) as fh:
first = fh.readline().rstrip()
print(f"(legacy CLAUDE_COMMIT_MSG) [wildcard] {first[:70]}")
def cmd_show(argv):
d = store()
for mid in argv:
with open(os.path.join(d, mid + ".msg")) as fh:
sys.stdout.write(fh.read())
def cmd_rm(argv):
d = store()
for mid in argv:
hit = False
for ext in (".msg", ".paths"):
p = os.path.join(d, mid + ext)
if os.path.exists(p):
os.remove(p)
hit = True
if hit:
print(f"removed {mid}")
elif os.path.exists(os.path.join(d, "used", mid + ".msg")):
print(f"{mid} was already consumed by a commit (see used/)")
else:
print(f"{mid}: no such pending message")
def consume(d, mid):
for ext in (".msg", ".paths"):
p = os.path.join(d, mid + ext)
if os.path.exists(p):
shutil.move(p, os.path.join(d, "used", mid + ext))
def cmd_prefill(argv):
if not argv:
return
msg_file = argv[0]
source = argv[1] if len(argv) > 1 else ""
if source not in ("", "template"): # -m/-F, merge, squash, amend: leave alone
return
d = store()
staged = set(staged_paths())
order = {"full": 0, "partial": 1, "wildcard": 2}
picked = []
for mid, msg, paths in pending(d):
kind = classify(paths, staged)
if kind != "none":
picked.append((order[kind], mid, msg, kind))
legacy = git("rev-parse", "--git-path", "CLAUDE_COMMIT_MSG").strip()
legacy_msg = None
if os.path.isfile(legacy) and os.path.getsize(legacy) > 0:
with open(legacy) as fh:
legacy_msg = fh.read()
picked.append((3, "legacy", legacy_msg, "wildcard"))
if not picked:
return
picked.sort(key=lambda t: (t[0], t[1]))
cc = comment_char()
sep = f"\n{cc} ---- another pending Claude message matched this commit; keep the right one ----\n\n"
body = sep.join(m.rstrip("\n") + "\n" for _, _, m, _ in picked)
with open(msg_file) as fh:
existing = fh.read()
tmp = msg_file + ".claude-tmp"
with open(tmp, "w") as fh:
fh.write(body + "\n" + existing)
os.replace(tmp, msg_file)
for _, mid, _, kind in picked:
if mid == "legacy":
os.replace(legacy, legacy + ".last")
elif kind in ("full", "wildcard"):
consume(d, mid)
def main():
cmds = {"save": cmd_save, "list": cmd_list, "show": cmd_show, "rm": cmd_rm, "prefill": cmd_prefill}
if len(sys.argv) < 2 or sys.argv[1] not in cmds:
sys.exit("usage: claude-commit-msg save [--paths P ...] [--slug S] < message | list | show ID | rm ID | prefill MSGFILE [SOURCE]")
if sys.argv[1] == "prefill":
try:
cmd_prefill(sys.argv[2:])
except Exception as e: # never abort a commit because of the helper
print(f"claude-commit-msg prefill: {e}", file=sys.stderr)
return
cmds[sys.argv[1]](sys.argv[2:])
if __name__ == "__main__":
main()
+36
View File
@@ -0,0 +1,36 @@
#!/bin/sh
# Prefill the commit editor from pending Claude commit messages (one-shot).
#
# Claude sessions save suggestions with `~/.config/git/claude-commit-msg save` -- one file per
# message under $(git rev-parse --git-path CLAUDE_COMMIT_MSG.d)/, tagged with the paths it
# describes. On the next `git commit` / lazygit `C` this pastes the message(s) whose paths are
# part of the commit above git's comment block, then consumes them (moved to used/). Several
# sessions can hand off in parallel without overwriting each other.
#
# Falls back to the legacy single file $(git rev-parse --git-path CLAUDE_COMMIT_MSG) when the
# CLI is not deployed on this host (not re-stowed yet). Never exits non-zero: a helper problem
# must not abort a commit.
msg_file=$1
source=$2
# Only prefill a fresh editor session -- skip -m/-F (message), merge, squash, amend (commit).
case "$source" in
""|template) ;;
*) exit 0 ;;
esac
cli="$HOME/.config/git/claude-commit-msg"
if [ -f "$cli" ] && command -v python3 >/dev/null 2>&1; then
python3 "$cli" prefill "$msg_file" "$source"
exit 0
fi
# --- legacy fallback: the single one-shot file ---
suggestion=$(git rev-parse --git-path CLAUDE_COMMIT_MSG)
[ -s "$suggestion" ] || exit 0
tmp=$(mktemp) || exit 0
{ cat "$suggestion"; printf '\n'; cat "$msg_file"; } > "$tmp" &&
mv "$tmp" "$msg_file" &&
mv "$suggestion" "$suggestion.last"
exit 0
+1
View File
@@ -30,6 +30,7 @@ nvim
vim vim
tetrigo tetrigo
pyradio pyradio
cliamp
px7-radio px7-radio
kew kew
cmatrix cmatrix
+22
View File
@@ -0,0 +1,22 @@
# Global agent instructions
You are a coding assistant running on small, locally-hosted models. Be precise and economical with tokens.
## Working style
- Act with tools instead of describing what you would do. Keep prose short.
- Do the task that was asked. Don't add unrequested changes, refactors, or files.
- When done, stop. A one- or two-line summary is enough; no recaps of obvious steps.
## Files & edits
- Read a file before you edit it. Never guess at file paths, function names, or APIs — verify first.
- Use the edit tool for changes. Make minimal, targeted diffs; never paste an entire file back to the user.
- Match the surrounding code's style, naming, and imports.
## Shell
- Run one command at a time and check its output before the next.
- Prefer `rg` and `fd` for search. Use read-only commands when exploring.
- Never run destructive or system-changing commands (`rm -rf`, `sudo`, package installs, force-push) unless explicitly asked.
## Honesty
- If you're unsure, say so and check rather than inventing an answer.
- Report failures plainly with the actual error; don't claim success you didn't verify.
@@ -0,0 +1,11 @@
---
description: Write a Conventional Commits message for the staged changes (does not commit)
agent: plan
---
Inspect the staged changes with `git diff --cached` (and `git status` for context). Then write a single Conventional Commits message:
- A `<type>(<scope>): <subject>` summary line, imperative mood, ≤72 chars (types: feat, fix, refactor, docs, chore, test, perf, build).
- An optional short body explaining the *why* only when it isn't obvious from the diff.
- Do NOT run `git commit`. Output only the message inside a code block so I can review it.
Extra context from me, if any: $ARGUMENTS
@@ -0,0 +1,14 @@
---
description: Review the current diff (staged + unstaged) for bugs and issues, read-only
agent: plan
---
Review the current changes. Check both `git diff` (unstaged) and `git diff --cached` (staged). Focus on:
- Correctness and logic bugs
- Error handling and missing edge cases
- Security issues (injection, secrets, unsafe input)
- Anything that doesn't match the surrounding code's conventions
List concrete findings as `file:line — issue — suggested fix`, ordered by severity. Be concise; skip praise. If nothing is wrong, say so.
Extra focus this round, if any (default: general correctness): $ARGUMENTS
+8
View File
@@ -14,6 +14,14 @@
], ],
"store": false "store": false
}, },
"whitelist": [
"gpt-5.2",
"gpt-5.2-codex",
"gpt-5.1-codex-max",
"gpt-5.1-codex",
"gpt-5.1-codex-mini",
"gpt-5.1"
],
"models": { "models": {
"gpt-5.2": { "gpt-5.2": {
"name": "GPT 5.2 (OAuth)", "name": "GPT 5.2 (OAuth)",
+134 -29
View File
@@ -1,10 +1,11 @@
{ {
"$schema": "https://opencode.ai/config.json", "$schema": "https://opencode.ai/config.json",
"model": "llama.cpp/gemma-4-26B-A4B-it-UD-IQ4_XS", "model": "llama.cpp/gemma-4-26B-A4B-it-UD-IQ4_XS",
"small_model": "llama.cpp/gemma-4-E4B-it-UD-Q8_K_XL",
"instructions": ["CLAUDE.md", "AGENTS.md"], "instructions": ["CLAUDE.md", "AGENTS.md"],
"share": "disabled", "share": "disabled",
"autoupdate": "notify", "autoupdate": "notify",
"tool_output": { "max_lines": 200, "max_bytes": 16384 },
"snapshot": false,
"disabled_providers": [ "disabled_providers": [
"gemini", "gemini",
"anthropic", "anthropic",
@@ -17,6 +18,7 @@
"webfetch": "ask", "webfetch": "ask",
"websearch": "ask", "websearch": "ask",
"external_directory": "ask", "external_directory": "ask",
"question": "allow",
"read": { "read": {
"*": "allow", "*": "allow",
"**/.env*": "deny", "**/.env*": "deny",
@@ -133,46 +135,51 @@
"npm": "@ai-sdk/openai-compatible", "npm": "@ai-sdk/openai-compatible",
"name": "DuskaDIY", "name": "DuskaDIY",
"options": { "options": {
"baseURL": "https://llm.duskadiy.com/api/v1" "baseURL": "https://llm.duskadiy.com/api/v1",
"apiKey": "{env:DUSKADIY_API_KEY}",
"timeout": 3600000
}, },
"models": { "models": {
"Qwen3-Coder-30B-Instruct-UD-Q3_K_XL": { "name": "Qwen3 Coder 30B · 32k — main agent coder (remote)", "limit": { "context": 32768, "output": 4096 } }, "Qwen3-Coder-30B-Instruct-UD-Q3_K_XL": { "name": "Qwen3 Coder 30B · 32k — main agent coder (~30 t/s, remote)", "limit": { "context": 32768, "output": 4096 } },
"Qwen3-Coder-Next-UD-IQ3_XXS": { "name": "Qwen3 Coder Next 80B · 128k — long sessions (remote)", "limit": { "context": 131072, "output": 16384 } }, "Qwen3-Coder-Next-UD-IQ3_XXS": { "name": "Qwen3 Coder Next 80B · 128k — long sessions (~16 t/s, remote)", "limit": { "context": 131072, "output": 16384 } },
"Qwen3.6-35B-A3B-MTP-UD-IQ3_XXS": { "name": "Qwen3.6 35B · 24k · vision — daily driver (remote)", "attachment": true, "limit": { "context": 24576, "output": 8192 } }, "Qwen3.6-35B-A3B-MTP-UD-IQ3_XXS": { "name": "Qwen3.6 35B · 24k · vision — daily driver (~36 t/s, remote)", "attachment": true, "limit": { "context": 24576, "output": 8192 } },
"Qwen3.6-35B-A3B-Thinking": { "name": "Qwen3.6 35B Thinking · 24k · vision — hard problems (remote)", "attachment": true, "limit": { "context": 24576, "output": 8192 } }, "Qwen3.6-35B-A3B-Thinking": { "reasoning": true, "name": "Qwen3.6 35B Thinking · 24k · vision — hard problems (~39 t/s, remote)", "attachment": true, "limit": { "context": 24576, "output": 8192 } },
"Qwen3.5-9B-UD-Q6_K_XL": { "name": "Qwen3.5 9B · 32k · vision — quick tasks (remote)", "attachment": true, "limit": { "context": 32768, "output": 8192 } }, "Qwen3.5-9B-UD-Q6_K_XL": { "reasoning": true, "name": "Qwen3.5 9B · 32k · vision — quick tasks (~32 t/s, remote)", "attachment": true, "limit": { "context": 32768, "output": 8192 } },
"Qwen3.8-27B-UD-IQ3_XXS": { "name": "Qwen3.8 27B · 24k · vision — hybrid reasoner (remote)", "attachment": true, "limit": { "context": 24576, "output": 8192 } }, "Qwen3.8-27B-UD-IQ3_XXS": { "reasoning": true, "name": "Qwen3.8 27B · 24k · vision — hybrid reasoner, quiet desktop only (~15 t/s, remote)", "attachment": true, "limit": { "context": 24576, "output": 8192 } },
"gemma-4-26B-A4B-it-UD-IQ4_XS": { "name": "Gemma 4 26B · 24k · vision — quality generalist (remote)", "attachment": true, "limit": { "context": 24576, "output": 4096 } }, "gemma-4-26B-A4B-it-UD-IQ4_XS": { "name": "Gemma 4 26B · 24k · vision — quality generalist (~34 t/s, remote)", "attachment": true, "limit": { "context": 24576, "output": 4096 } },
"gemma-4-E4B-it-UD-Q8_K_XL": { "name": "Gemma 4 E4B · 64k · vision — fast generalist, long docs (remote)", "attachment": true, "limit": { "context": 65536, "output": 16384 } }, "gemma-4-E4B-it-UD-Q8_K_XL": { "name": "Gemma 4 E4B · 64k · vision — fast generalist, long docs (~57 t/s, remote)", "attachment": true, "limit": { "context": 65536, "output": 16384 } },
"GLM-4.7-Flash-UD-Q4_K_XL": { "name": "GLM-4.7 Flash · 24k — quality coder (remote)", "limit": { "context": 24576, "output": 8192 } }, "GLM-4.7-Flash-UD-Q4_K_XL": { "reasoning": true, "interleaved": "reasoning_content", "name": "GLM-4.7 Flash · 24k — quality coder (~21 t/s, remote)", "limit": { "context": 24576, "output": 8192 } },
"gpt-oss-20b": { "name": "gpt-oss 20B · 64k — fast reasoning + tools (remote)", "limit": { "context": 65536, "output": 8192 } }, "gpt-oss-20b": { "reasoning": true, "name": "gpt-oss 20B · 64k — fast reasoning + tools (~38 t/s, remote)", "limit": { "context": 65536, "output": 8192 } },
"gpt-oss-20b-low": { "name": "gpt-oss 20B low · 64k — snappy answers (remote)", "limit": { "context": 65536, "output": 8192 } } "gpt-oss-20b-low": { "reasoning": true, "name": "gpt-oss 20B low · 64k — snappy answers (~37 t/s, remote)", "limit": { "context": 65536, "output": 8192 } }
} }
}, },
"llama.cpp": { "llama.cpp": {
"npm": "@ai-sdk/openai-compatible", "npm": "@ai-sdk/openai-compatible",
"name": "llamacpp", "name": "llamacpp",
"options": { "options": {
"baseURL": "http://192.168.0.204:11343/v1" "baseURL": "http://192.168.0.204:11343/v1",
"timeout": 3600000
}, },
"models": { "models": {
"Qwen3-Coder-Next-UD-IQ3_XXS": { "name": "Qwen3 Coder Next 80B · 128k — long sessions", "limit": { "context": 131072, "output": 16384 } }, "Qwen3-Coder-Next-UD-IQ3_XXS": { "name": "Qwen3 Coder Next 80B · 128k — long sessions (~16 t/s)", "limit": { "context": 131072, "output": 16384 } },
"Qwen3-Coder-30B-Instruct-UD-Q3_K_XL": { "name": "Qwen3 Coder 30B · 32k — main agent coder", "limit": { "context": 32768, "output": 4096 } }, "Qwen3-Coder-30B-Instruct-UD-Q3_K_XL": { "name": "Qwen3 Coder 30B · 32k — main agent coder (~30 t/s)", "limit": { "context": 32768, "output": 4096 } },
"Qwen3.6-35B-A3B-MTP-UD-IQ3_XXS": { "name": "Qwen3.6 35B · 24k · vision — daily driver", "attachment": true, "limit": { "context": 24576, "output": 8192 } }, "Qwen3.6-35B-A3B-MTP-UD-IQ3_XXS": { "name": "Qwen3.6 35B · 24k · vision — daily driver (~36 t/s)", "attachment": true, "limit": { "context": 24576, "output": 8192 } },
"Qwen3.6-35B-A3B-Thinking": { "name": "Qwen3.6 35B Thinking · 24k · vision — hard problems", "attachment": true, "limit": { "context": 24576, "output": 8192 } }, "Qwen3.6-35B-A3B-Thinking": { "reasoning": true, "name": "Qwen3.6 35B Thinking · 24k · vision — hard problems (~39 t/s)", "attachment": true, "limit": { "context": 24576, "output": 8192 } },
"Qwen3.5-9B-UD-Q6_K_XL": { "name": "Qwen3.5 9B · 32k · vision — quick tasks", "attachment": true, "limit": { "context": 32768, "output": 8192 } }, "Qwen3.5-9B-UD-Q6_K_XL": { "reasoning": true, "name": "Qwen3.5 9B · 32k · vision — quick tasks (~32 t/s)", "attachment": true, "limit": { "context": 32768, "output": 8192 } },
"Qwen3.8-27B-UD-IQ3_XXS": { "name": "Qwen3.8 27B · 24k · vision — hybrid reasoner", "attachment": true, "limit": { "context": 24576, "output": 8192 } }, "Qwen3.8-27B-UD-IQ3_XXS": { "reasoning": true, "name": "Qwen3.8 27B · 24k · vision — hybrid reasoner, quiet desktop only (~15 t/s)", "attachment": true, "limit": { "context": 24576, "output": 8192 } },
"gemma-4-26B-A4B-it-UD-IQ4_XS": { "name": "Gemma 4 26B · 24k · vision — quality generalist", "attachment": true, "limit": { "context": 24576, "output": 4096 } }, "gemma-4-26B-A4B-it-UD-IQ4_XS": { "name": "Gemma 4 26B · 24k · vision — quality generalist (~34 t/s)", "attachment": true, "limit": { "context": 24576, "output": 4096 } },
"gemma-4-E4B-it-UD-Q8_K_XL": { "name": "Gemma 4 E4B · 64k · vision — fast generalist, long docs", "attachment": true, "limit": { "context": 65536, "output": 16384 } }, "gemma-4-E4B-it-UD-Q8_K_XL": { "name": "Gemma 4 E4B · 64k · vision — fast generalist, long docs (~57 t/s)", "attachment": true, "limit": { "context": 65536, "output": 16384 } },
"GLM-4.7-Flash-UD-Q4_K_XL": { "name": "GLM-4.7 Flash · 24k — quality coder", "limit": { "context": 24576, "output": 8192 } }, "GLM-4.7-Flash-UD-Q4_K_XL": { "reasoning": true, "interleaved": "reasoning_content", "name": "GLM-4.7 Flash · 24k — quality coder (~21 t/s)", "limit": { "context": 24576, "output": 8192 } },
"gpt-oss-20b": { "name": "gpt-oss 20B · 64k — fast reasoning + tools", "limit": { "context": 65536, "output": 8192 } }, "gpt-oss-20b": { "reasoning": true, "name": "gpt-oss 20B · 64k — fast reasoning + tools (~38 t/s)", "limit": { "context": 65536, "output": 8192 } },
"gpt-oss-20b-low": { "name": "gpt-oss 20B low · 64k — snappy answers", "limit": { "context": 65536, "output": 8192 } } "gpt-oss-20b-low": { "reasoning": true, "name": "gpt-oss 20B low · 64k — snappy answers (~37 t/s)", "limit": { "context": 65536, "output": 8192 } }
} }
} }
}, },
"agent": { "agent": {
"plan": { "plan": {
"mode": "primary", "mode": "primary",
"color": "info",
"steps": 30,
"model": "llama.cpp/gemma-4-26B-A4B-it-UD-IQ4_XS", "model": "llama.cpp/gemma-4-26B-A4B-it-UD-IQ4_XS",
"permission": { "permission": {
"edit": "deny" "edit": "deny"
@@ -180,6 +187,8 @@
}, },
"build": { "build": {
"mode": "primary", "mode": "primary",
"color": "primary",
"steps": 30,
"model": "llama.cpp/gemma-4-26B-A4B-it-UD-IQ4_XS", "model": "llama.cpp/gemma-4-26B-A4B-it-UD-IQ4_XS",
"permission": { "permission": {
"edit": { "edit": {
@@ -197,11 +206,108 @@
} }
} }
}, },
"auto": {
"description": "Auto mode: acts without asking; only irreversible or outward-facing actions ask",
"mode": "primary",
"color": "#f9e2af",
"steps": 30,
"model": "llama.cpp/gemma-4-26B-A4B-it-UD-IQ4_XS",
"permission": {
"edit": {
"*": "allow",
"**/.env*": "deny",
"**/*.env": "deny",
"**/*.pem": "deny",
"**/*.key": "deny",
"**/id_rsa*": "deny",
"**/.ssh/**": "deny",
"**/.aws/**": "deny",
"**/.git/**": "deny",
"**/.npmrc": "deny",
"**/.netrc": "deny"
},
"webfetch": "allow",
"websearch": "allow",
"external_directory": "ask",
"question": "allow",
"bash": {
"*": "allow",
"git *": "allow",
"git commit*": "ask",
"git push*": "ask",
"git reset*": "ask",
"git checkout*": "ask",
"git switch*": "ask",
"git restore*": "ask",
"git rebase*": "ask",
"git merge*": "ask",
"git cherry-pick*": "ask",
"git stash*": "ask",
"git clean*": "ask",
"git branch -*": "ask",
"git tag*": "ask",
"git remote add*": "ask",
"git remote remove*": "ask",
"git remote set-url*": "ask",
"rm *": "ask",
"rmdir *": "ask",
"kill *": "ask",
"pkill *": "ask",
"killall *": "ask",
"systemctl *": "ask",
"curl *": "ask",
"wget *": "ask",
"ssh *": "ask",
"scp *": "ask",
"rsync *": "ask",
"npm install*": "ask",
"npm i": "ask",
"npm i *": "ask",
"npm ci*": "ask",
"pnpm install*": "ask",
"pnpm i": "ask",
"pnpm i *": "ask",
"pnpm add*": "ask",
"yarn add*": "ask",
"yarn install*": "ask",
"pip install*": "ask",
"pip3 install*": "ask",
"pipx install*": "ask",
"cargo install*": "ask",
"go install*": "ask",
"gem install*": "ask",
"yay *": "ask",
"sudo *": "deny",
"doas *": "deny",
"dd *": "deny",
"mkfs*": "deny",
"shred *": "deny",
"chmod -R *": "deny",
"rm -rf /*": "deny",
"rm -rf ~*": "deny",
"cat .env*": "deny",
"cat *.env": "deny",
"cat *.pem": "deny",
"cat *.key": "deny",
"cat *id_rsa*": "deny",
"cat *secret*": "deny",
"cat *credential*": "deny",
"cat ~/.ssh/*": "deny",
"cat ~/.aws/*": "deny",
"grep .env*": "deny",
"grep * .env*": "deny"
}
}
},
"code-reviewer": { "code-reviewer": {
"description": "Reviews code for best practices and potential issues", "description": "Reviews code for best practices and potential issues",
"prompt": "You are a code reviewer. Focus on security, performance, and maintainability.", "prompt": "You are a code reviewer. Focus on security, performance, and maintainability.",
"mode": "subagent", "mode": "subagent",
"model": "llama.cpp/GLM-4.7-Flash-UD-Q4_K_XL",
"permission": { "permission": {
"edit": "deny" "edit": "deny"
} }
@@ -210,7 +316,7 @@
"description": "Runs and interprets test suites", "description": "Runs and interprets test suites",
"prompt": "You are a test runner. Focus on identifying failing tests and providing clear reproduction steps.", "prompt": "You are a test runner. Focus on identifying failing tests and providing clear reproduction steps.",
"mode": "subagent", "mode": "subagent",
"model": "llama.cpp/GLM-4.7-Flash-UD-Q4_K_XL", "hidden": true,
"permission": { "permission": {
"edit": "deny" "edit": "deny"
} }
@@ -219,7 +325,6 @@
"description": "Maintains project documentation and docstrings", "description": "Maintains project documentation and docstrings",
"prompt": "You are a technical writer. Focus on clarity, accuracy, and keeping documentation in sync with code changes.", "prompt": "You are a technical writer. Focus on clarity, accuracy, and keeping documentation in sync with code changes.",
"mode": "subagent", "mode": "subagent",
"model": "llama.cpp/gemma-4-26B-A4B-it-UD-IQ4_XS",
"permission": { "permission": {
"edit": { "edit": {
"*": "deny", "*": "deny",
@@ -235,7 +340,7 @@
"description": "Scans for security vulnerabilities and hardcoded secrets", "description": "Scans for security vulnerabilities and hardcoded secrets",
"prompt": "You are a security auditor. Focus on OWASP principles, secret detection, and dependency vulnerabilities.", "prompt": "You are a security auditor. Focus on OWASP principles, secret detection, and dependency vulnerabilities.",
"mode": "subagent", "mode": "subagent",
"model": "llama.cpp/GLM-4.7-Flash-UD-Q4_K_XL", "hidden": true,
"permission": { "permission": {
"edit": "deny" "edit": "deny"
} }
@@ -0,0 +1,262 @@
{
"defs": {
"mocha_rosewater": "#f5e0dc",
"mocha_flamingo": "#f2cdcd",
"mocha_pink": "#f5c2e7",
"mocha_mauve": "#cba6f7",
"mocha_red": "#f38ba8",
"mocha_maroon": "#eba0ac",
"mocha_peach": "#fab387",
"mocha_yellow": "#f9e2af",
"mocha_green": "#a6e3a1",
"mocha_teal": "#94e2d5",
"mocha_sky": "#89dceb",
"mocha_sapphire": "#74c7ec",
"mocha_blue": "#89b4fa",
"mocha_lavender": "#b4befe",
"mocha_text": "#cdd6f4",
"mocha_subtext1": "#bac2de",
"mocha_subtext0": "#a6adc8",
"mocha_overlay2": "#9399b2",
"mocha_overlay1": "#7f849c",
"mocha_overlay0": "#6c7086",
"mocha_surface2": "#585b70",
"mocha_surface1": "#45475a",
"mocha_surface0": "#313244",
"mocha_base": "#1e1e2e",
"mocha_mantle": "#181825",
"mocha_crust": "#11111b",
"mocha_diffAddBg": "#1e2b22",
"mocha_diffDelBg": "#2b1e22",
"latte_rosewater": "#dc8a78",
"latte_flamingo": "#dd7878",
"latte_pink": "#ea76cb",
"latte_mauve": "#8839ef",
"latte_red": "#d20f39",
"latte_maroon": "#e64553",
"latte_peach": "#fe640b",
"latte_yellow": "#df8e1d",
"latte_green": "#40a02b",
"latte_teal": "#179299",
"latte_sky": "#04a5e5",
"latte_sapphire": "#209fb5",
"latte_blue": "#1e66f5",
"latte_lavender": "#7287fd",
"latte_text": "#4c4f69",
"latte_subtext1": "#5c5f77",
"latte_subtext0": "#6c6f85",
"latte_overlay2": "#7c7f93",
"latte_overlay1": "#8c8fa1",
"latte_overlay0": "#9ca0b0",
"latte_surface2": "#acb0be",
"latte_surface1": "#bcc0cc",
"latte_surface0": "#ccd0da",
"latte_base": "#eff1f5",
"latte_mantle": "#e6e9ef",
"latte_crust": "#dce0e8",
"latte_diffAddBg": "#e4f3e6",
"latte_diffDelBg": "#f9e1e6"
},
"theme": {
"primary": {
"dark": "mocha_mauve",
"light": "latte_mauve"
},
"secondary": {
"dark": "mocha_blue",
"light": "latte_blue"
},
"accent": {
"dark": "mocha_peach",
"light": "latte_peach"
},
"error": {
"dark": "mocha_red",
"light": "latte_red"
},
"warning": {
"dark": "mocha_yellow",
"light": "latte_yellow"
},
"success": {
"dark": "mocha_green",
"light": "latte_green"
},
"info": {
"dark": "mocha_sapphire",
"light": "latte_sapphire"
},
"text": {
"dark": "mocha_text",
"light": "latte_text"
},
"textMuted": {
"dark": "mocha_subtext0",
"light": "latte_subtext0"
},
"background": {
"dark": "mocha_base",
"light": "latte_base"
},
"backgroundPanel": {
"dark": "mocha_mantle",
"light": "latte_mantle"
},
"backgroundElement": {
"dark": "mocha_surface0",
"light": "latte_surface0"
},
"border": {
"dark": "mocha_surface1",
"light": "latte_surface1"
},
"borderActive": {
"dark": "mocha_mauve",
"light": "latte_mauve"
},
"borderSubtle": {
"dark": "mocha_surface0",
"light": "latte_surface0"
},
"diffAdded": {
"dark": "mocha_green",
"light": "latte_green"
},
"diffRemoved": {
"dark": "mocha_red",
"light": "latte_red"
},
"diffContext": {
"dark": "mocha_overlay0",
"light": "latte_overlay0"
},
"diffHunkHeader": {
"dark": "mocha_overlay1",
"light": "latte_overlay1"
},
"diffHighlightAdded": {
"dark": "mocha_green",
"light": "latte_green"
},
"diffHighlightRemoved": {
"dark": "mocha_red",
"light": "latte_red"
},
"diffAddedBg": {
"dark": "mocha_diffAddBg",
"light": "latte_diffAddBg"
},
"diffRemovedBg": {
"dark": "mocha_diffDelBg",
"light": "latte_diffDelBg"
},
"diffContextBg": {
"dark": "mocha_base",
"light": "latte_base"
},
"diffLineNumber": {
"dark": "mocha_overlay0",
"light": "latte_overlay0"
},
"diffAddedLineNumberBg": {
"dark": "mocha_diffAddBg",
"light": "latte_diffAddBg"
},
"diffRemovedLineNumberBg": {
"dark": "mocha_diffDelBg",
"light": "latte_diffDelBg"
},
"markdownText": {
"dark": "mocha_text",
"light": "latte_text"
},
"markdownHeading": {
"dark": "mocha_mauve",
"light": "latte_mauve"
},
"markdownLink": {
"dark": "mocha_sapphire",
"light": "latte_sapphire"
},
"markdownLinkText": {
"dark": "mocha_blue",
"light": "latte_blue"
},
"markdownCode": {
"dark": "mocha_green",
"light": "latte_green"
},
"markdownBlockQuote": {
"dark": "mocha_subtext0",
"light": "latte_subtext0"
},
"markdownEmph": {
"dark": "mocha_yellow",
"light": "latte_yellow"
},
"markdownStrong": {
"dark": "mocha_peach",
"light": "latte_peach"
},
"markdownHorizontalRule": {
"dark": "mocha_surface1",
"light": "latte_surface1"
},
"markdownListItem": {
"dark": "mocha_mauve",
"light": "latte_mauve"
},
"markdownListEnumeration": {
"dark": "mocha_mauve",
"light": "latte_mauve"
},
"markdownImage": {
"dark": "mocha_sapphire",
"light": "latte_sapphire"
},
"markdownImageText": {
"dark": "mocha_blue",
"light": "latte_blue"
},
"markdownCodeBlock": {
"dark": "mocha_text",
"light": "latte_text"
},
"syntaxComment": {
"dark": "mocha_overlay0",
"light": "latte_overlay0"
},
"syntaxKeyword": {
"dark": "mocha_mauve",
"light": "latte_mauve"
},
"syntaxFunction": {
"dark": "mocha_blue",
"light": "latte_blue"
},
"syntaxVariable": {
"dark": "mocha_text",
"light": "latte_text"
},
"syntaxString": {
"dark": "mocha_green",
"light": "latte_green"
},
"syntaxNumber": {
"dark": "mocha_peach",
"light": "latte_peach"
},
"syntaxType": {
"dark": "mocha_yellow",
"light": "latte_yellow"
},
"syntaxOperator": {
"dark": "mocha_sky",
"light": "latte_sky"
},
"syntaxPunctuation": {
"dark": "mocha_overlay2",
"light": "latte_overlay2"
}
}
}
@@ -0,0 +1,71 @@
// opencode TUI plugin: one key toggles the whole "verbose view".
//
// The session view keeps four independent display toggles in the TUI's persisted KV store
// (~/.local/state/opencode/kv.json). Built-in commands only flip one each. This plugin reads all
// four and drives them to the same state: if every one is on, turn all off; otherwise turn all on.
// Writing the KV keys directly is what the built-in toggles do too (kv.signal is a reactive
// store), so the open session re-renders immediately and the choice survives restarts.
//
// Declared in tui.json: "plugin": [["./tui-plugins/verbose.ts", { "key": "<leader>v" }]]
// (TUI plugins are never auto-discovered, and this dir is deliberately NOT ~/.config/opencode/plugins/,
// which the server-side plugin loader scans and would reject a TUI-only module.)
// Also available as /verbose and from the command palette.
import type { TuiPlugin, TuiPluginApi, TuiPluginModule } from "@opencode-ai/plugin/tui"
// KV keys + values as used by packages/tui/src/routes/session/index.tsx and context/thinking.ts (v1.18.29)
const KEYS = {
details: "tool_details_visibility", // boolean, default true
generic: "generic_tool_output_visibility", // boolean, default false
timestamps: "timestamps", // "show" | "hide", default "hide"
thinking: "thinking_mode", // "show" | "hide", default "hide"
} as const
const COMMAND = "verbose.toggle"
function current(api: TuiPluginApi) {
return {
details: api.kv.get<boolean>(KEYS.details, true) === true,
generic: api.kv.get<boolean>(KEYS.generic, false) === true,
timestamps: api.kv.get<string>(KEYS.timestamps, "hide") === "show",
thinking: api.kv.get<string>(KEYS.thinking, "hide") === "show",
}
}
function apply(api: TuiPluginApi, on: boolean) {
api.kv.set(KEYS.details, on)
api.kv.set(KEYS.generic, on)
api.kv.set(KEYS.timestamps, on ? "show" : "hide")
api.kv.set(KEYS.thinking, on ? "show" : "hide")
}
const tui: TuiPlugin = async (api, options) => {
const opts = (options ?? {}) as Record<string, unknown>
const key = typeof opts.key === "string" && opts.key.length > 0 ? opts.key : "<leader>v"
api.keymap.registerLayer({
mode: "base",
commands: [
{
name: COMMAND,
title: "Toggle verbose view (tool details, generic output, thinking, timestamps)",
category: "Session",
namespace: "palette",
slashName: "verbose",
run() {
const state = current(api)
const on = !Object.values(state).every(Boolean)
apply(api, on)
api.ui.toast({
variant: "info",
message: on ? "Verbose view on" : "Verbose view off",
duration: 1500,
})
},
},
],
bindings: [{ key, cmd: COMMAND, desc: "Toggle verbose view" }],
})
}
const plugin: TuiPluginModule & { id: string } = { id: "coja.verbose", tui }
export default plugin
+15 -1
View File
@@ -1,4 +1,18 @@
{ {
"$schema": "https://opencode.ai/tui.json", "$schema": "https://opencode.ai/tui.json",
"theme": "lucent-orng" "theme": "lucent-orng",
"plugin": [["./tui-plugins/verbose.ts", { "key": "<leader>v" }]],
"leader_timeout": 1500,
"diff_style": "stacked",
"cursor": { "style": "block", "blinking": false },
"attention": { "enabled": true, "notifications": true, "sound": false },
"keybinds": {
"prompt_stash": "<leader>p",
"prompt_stash_pop": "<leader>o",
"session_fork": "<leader>f",
"messages_last_user": "<leader>k",
"display_thinking": "<leader>i",
"tool_details": "<leader>d",
"session_toggle_timestamps": "<leader>w"
}
} }
+16 -1
View File
@@ -130,7 +130,22 @@ bind S {
# status-format[1] is tmux 3.7's default content template (status-left/windows/right). # status-format[1] is tmux 3.7's default content template (status-left/windows/right).
set -g status 2 set -g status 2
set -g status-format[0] "#[fg=#6c7086,bg=default]#(yes ─ | head -n 400 | paste -sd '')" set -g status-format[0] "#[fg=#6c7086,bg=default]#(yes ─ | head -n 400 | paste -sd '')"
set -g status-format[1] "#[align=left range=left #{E:status-left-style}]#[push-default]#{T;=/#{status-left-length}:status-left}#[pop-default]#[norange default]#[list=on align=#{status-justify}]#[list=left-marker]<#[list=right-marker]>#[list=on]#{W:#[range=window|#{window_index} #{E:window-status-style}#{?#{&&:#{window_last_flag},#{!=:#{E:window-status-last-style},default}}, #{E:window-status-last-style},}#{?#{&&:#{window_bell_flag},#{!=:#{E:window-status-bell-style},default}}, #{E:window-status-bell-style},#{?#{&&:#{||:#{window_activity_flag},#{window_silence_flag}},#{!=:#{E:window-status-activity-style},default}}, #{E:window-status-activity-style},}}]#[push-default]#{T:window-status-format}#[pop-default]#[norange default]#{?loop_last_flag,,#{E:window-status-separator}},#[range=window|#{window_index} list=focus #{?#{!=:#{E:window-status-current-style},default},#{E:window-status-current-style},#{E:window-status-style}}#{?#{&&:#{window_last_flag},#{!=:#{E:window-status-last-style},default}}, #{E:window-status-last-style},}#{?#{&&:#{window_bell_flag},#{!=:#{E:window-status-bell-style},default}}, #{E:window-status-bell-style},#{?#{&&:#{||:#{window_activity_flag},#{window_silence_flag}},#{!=:#{E:window-status-activity-style},default}}, #{E:window-status-activity-style},}}]#[push-default]#{T:window-status-current-format}#[pop-default]#[norange list=on default]#{?loop_last_flag,,#{E:window-status-separator}}}#[nolist align=right range=right #{E:status-right-style}]#[push-default]#{T;=/#{status-right-length}:status-right}#[pop-default]#[norange default]" set -g status-format[1] "#[align=left range=left #{E:status-left-style}]#[push-default]#{T;=/#{status-left-length}:status-left}#[pop-default]#[norange default]#[list=on align=#{status-justify}]#[list=left-marker]<#[list=right-marker]>#[list=on]#{W:#[range=window|#{window_index} #{E:window-status-style}#{?#{&&:#{window_last_flag},#{!=:#{E:window-status-last-style},default}}, #{E:window-status-last-style},}#{?#{&&:#{window_bell_flag},#{!=:#{E:window-status-bell-style},default}}, #{E:window-status-bell-style},#{?#{&&:#{||:#{window_activity_flag},#{window_silence_flag}},#{!=:#{E:window-status-activity-style},default}}, #{E:window-status-activity-style},}}]#[push-default]#{T:window-status-format}#[pop-default]#[norange default]#{?loop_last_flag,,#{E:window-status-separator}},#[range=window|#{window_index} list=focus #{?#{!=:#{E:window-status-current-style},default},#{E:window-status-current-style},#{E:window-status-style}}#{?#{&&:#{window_last_flag},#{!=:#{E:window-status-last-style},default}}, #{E:window-status-last-style},}#{?#{&&:#{window_bell_flag},#{!=:#{E:window-status-bell-style},default}}, #{E:window-status-bell-style},#{?#{&&:#{||:#{window_activity_flag},#{window_silence_flag}},#{!=:#{E:window-status-activity-style},default}}, #{E:window-status-activity-style},}}]#[push-default]#{T:window-status-current-format}#[pop-default]#[norange list=on default]#{?loop_last_flag,,#{E:window-status-separator}}}#[range=user|newwin] #[fg=#6c7086]+#[default] #[norange]#[nolist align=right range=right #{E:status-right-style}]#[push-default]#{T;=/#{status-right-length}:status-right}#[pop-default]#[norange default]"
# The `+` appended to the window list above sits in a `range=user|newwin`
# marker. Per tmux(1), a user range triggers the same `Status` mouse key as a
# window range and exposes its argument in #{mouse_status_range} — window ranges
# report the literal string "window" — so one binding can serve both: click a
# tab to select it (tmux's stock behaviour, kept in the else branch), click the
# `+` to open a new window. Padded with spaces on both sides so it's a wide
# enough target to hit with a thumb.
bind -n MouseDown1Status {
if -F '#{==:#{mouse_status_range},newwin}' {
new-window
} {
switch-client -t =
}
}
# Round out the theme so no default green leaks into borders/messages/modes. # Round out the theme so no default green leaks into borders/messages/modes.
set -g pane-border-style "fg=#313244" set -g pane-border-style "fg=#313244"
+10 -4
View File
@@ -63,11 +63,16 @@ have a slash command to disable them for the current session without deleting.
| `permission-gate.ts` | Prompts before dangerous `bash` commands (blocks outright with no UI). Local list covers git history rewrites, package managers, `systemctl` and irreversible disk ops on top of upstream's `rm -rf`/`sudo`/`chmod 777` | **adapted** from pi's `examples/extensions/permission-gate.ts` | delete file | | `permission-gate.ts` | Prompts before dangerous `bash` commands (blocks outright with no UI). Local list covers git history rewrites, package managers, `systemctl` and irreversible disk ops on top of upstream's `rm -rf`/`sudo`/`chmod 777` | **adapted** from pi's `examples/extensions/permission-gate.ts` | delete file |
| `protected-paths.ts` | Blocks `write`/`edit` to secrets and repo plumbing (`secrets.fish`, `auth.json`, `.env`, `.ssh/`, `.git/`, …) | **adapted** from pi's `examples/extensions/protected-paths.ts` | delete file | | `protected-paths.ts` | Blocks `write`/`edit` to secrets and repo plumbing (`secrets.fish`, `auth.json`, `.env`, `.ssh/`, `.git/`, …) | **adapted** from pi's `examples/extensions/protected-paths.ts` | delete file |
| `compaction-watch.ts` | Surfaces every compaction and its `reason`; loudly flags `overflow` (recovery rather than prevention), which means `reserveTokens` is below the model's `maxTokens` | **custom** | delete file | | `compaction-watch.ts` | Surfaces every compaction and its `reason`; loudly flags `overflow` (recovery rather than prevention), which means `reserveTokens` is below the model's `maxTokens` | **custom** | delete file |
| `verbose.ts` | `/verbose [on|off|status]` and **Alt+V** toggle the *persisted* verbose default (`hideThinkingBlock`, `showCacheMissNotices`) and report pi's live keys — `ctrl+o` tool output, `ctrl+t` thinking, which an extension cannot drive itself | **custom** | delete file; the live keys are built in and keep working |
| `confirm-destructive.ts` | Confirms before session clear / switch / fork | **verbatim** from pi's `examples/extensions/confirm-destructive.ts` | delete file | | `confirm-destructive.ts` | Confirms before session clear / switch / fork | **verbatim** from pi's `examples/extensions/confirm-destructive.ts` | delete file |
| `session-name.ts` | `/session-name [name]` — friendly names in the session picker instead of the first message | **verbatim** from pi's `examples/extensions/session-name.ts` | delete file | | `session-name.ts` | `/session-name [name]` — friendly names in the session picker instead of the first message | **verbatim** from pi's `examples/extensions/session-name.ts` | delete file |
| `todo.ts` | A `todo` tool the model calls (add/toggle/list/clear) plus `/todos` to view it; state lives in tool results, so forking rewinds it correctly. Distinct from plan-mode's checklist | **verbatim** from pi's `examples/extensions/todo.ts` | delete file (it also adds a tool schema to every request) | | `todo.ts` | A `todo` tool the model calls (add/toggle/list/clear) plus `/todos` to view it; state lives in tool results, so forking rewinds it correctly. Distinct from plan-mode's checklist | **verbatim** from pi's `examples/extensions/todo.ts` | delete file (it also adds a tool schema to every request) |
> The bundled examples live at `/opt/pi-coding-agent/examples/extensions/` — that's > The bundled examples live under pi's install prefix, which moves: it was
> `/opt/pi-coding-agent/` before 0.85 and is `/usr/lib/pi-coding-agent/` on current Arch
> packages. Rather than hardcode either, resolve it —
> `set -l PI_ROOT (dirname (readlink -f (which pi)))` — and read `$PI_ROOT/examples/extensions/`.
> That's
> where `notify`/`questionnaire`/`plan-mode` came from. `plan-mode/` is now the only > where `notify`/`questionnaire`/`plan-mode` came from. `plan-mode/` is now the only
> **fork** left, so it's the only one to re-pull after a pi upgrade before re-applying > **fork** left, so it's the only one to re-pull after a pi upgrade before re-applying
> the local edits (its header documents them). `notify`/`questionnaire` carry no edits > the local edits (its header documents them). `notify`/`questionnaire` carry no edits
@@ -103,7 +108,8 @@ pi install npm:pi-vim
> >
> ```fish > ```fish
> cd ~/.pi/agent/npm > cd ~/.pi/agent/npm
> fnm exec --using=22 -- npm install --save-exact @earendil-works/pi-coding-agent@0.83.0 > fnm exec --using=22 -- npm install --save-exact \
> "@earendil-works/pi-coding-agent@"(pi --version | string trim)
> ``` > ```
> >
> `pi install` deliberately doesn't pull peers (it would duplicate the runtime). Costs > `pi install` deliberately doesn't pull peers (it would duplicate the runtime). Costs
@@ -142,11 +148,11 @@ working diff). Both are **custom**. Format is YAML frontmatter + body with
`keybindings.json` overrides three actions. **A user entry replaces the default keys `keybindings.json` overrides three actions. **A user entry replaces the default keys
for that action — it does not merge.** Action ids and their stock defaults are in for that action — it does not merge.** Action ids and their stock defaults are in
`/opt/pi-coding-agent/docs/keybindings.md`. `$PI_ROOT/docs/keybindings.md` (see above for `$PI_ROOT`).
```jsonc ```jsonc
{ {
"app.thinking.cycle": ["ctrl+shift+t"], // moved OFF Shift+Tab so plan-mode can bind it "app.thinking.cycle": ["alt+t"], // moved OFF Shift+Tab so plan-mode can bind it (was ctrl+shift+t = kitty new_tab, never reached pi)
"tui.editor.cursorUp": ["up", "ctrl+p"], // adds Ctrl+P as up (Emacs-style) "tui.editor.cursorUp": ["up", "ctrl+p"], // adds Ctrl+P as up (Emacs-style)
"app.model.cycleForward": ["alt+p"] // Alt+P cycles models "app.model.cycleForward": ["alt+p"] // Alt+P cycles models
} }
@@ -0,0 +1,12 @@
# Upstream example files as last reviewed against pi 0.85.1.
# Written by pi-upgrade-check -a. A file is only re-raised when UPSTREAM moves,
# so our own local edits stop generating noise on every run.
55502f82109df07e922ef3ec41af1b1a5ed33d4d6d56076654ab2bc88e2e0888 confirm-destructive.ts
70e4333e09ce00d546c116fd2e918abf7616c70b5da6e88ba8ee21a326afd483 notify.ts
e32086ba9dd9b4ba0fd308b65241d9affa0b5ecd3f90ef7ea1b9f2b30cab8cc1 permission-gate.ts
9ca66b1f3b1b9a61cc5ddc660f92cca2ea66c4264a1bdb6b00e0da5844399fe2 protected-paths.ts
3f1f719d81152ff013f5d36a97cc3976388d43f2d89bff0f19827d3443d903e6 questionnaire.ts
a0b9fd94e2cc827931150cc409238b473da635ab1a1cddce633cae5937a3ae24 session-name.ts
e46824d00217e25242c186d41837cc84ca81b23f978500323448502a9a424ee2 todo.ts
90fae53e2fada97165f7488520bd54f3cbfe5d2c7e4cfd3bd2aa07ee796e61e8 plan-mode/index.ts
12ea28d57cc55e68e8d52a2f1d16df8b2e7f2749cd694d41453028002a2bdad9 plan-mode/utils.ts
+3 -1
View File
@@ -49,7 +49,9 @@ function notify(title: string, body: string): void {
} }
export default function (pi: ExtensionAPI) { export default function (pi: ExtensionAPI) {
pi.on("agent_end", async () => { // `agent_end` fires after each low-level run; Pi may still retry, compact,
// or continue with queued follow-ups. Notify only after the full run settles.
pi.on("agent_settled", async () => {
notify("Pi", "Ready for input"); notify("Pi", "Ready for input");
}); });
} }
+114
View File
@@ -0,0 +1,114 @@
/**
* Verbose view toggle `/verbose` and a bound key.
*
* The counterpart of the opencode TUI plugin and the Claude `/verbose` skill, adjusted for
* what pi actually allows. pi splits verbosity in two:
*
* LIVE `ctrl+o` (app.tools.expand) collapses/expands tool output and `ctrl+t`
* (app.thinking.toggle) does the same for thinking blocks. These are built-in
* keybindings. An extension cannot drive them there is no settings API and no
* way to dispatch a built-in action and a key maps to one action, so they
* cannot be merged into a single press the way opencode's four toggles were.
*
* PERSISTED `hideThinkingBlock` and `showCacheMissNotices` in settings.json, read at
* startup. That is what this toggles, i.e. the default you get on every new
* session, which is exactly the position the Claude skill is in.
*
* So: this sets the default and tells you the live keys. "on" shows thinking and the
* cache/compaction/recovery notices; "off" is deliberately quieter than stock pi (it hides
* thinking), matching how opencode's verbose-off state hides thinking too.
*
* pi writes settings.json itself (that is where `lastChangelogVersion` churn comes from),
* so a running pi may overwrite this on exit. If a toggle seems not to have stuck, re-run it
* after pi has exited.
*/
import { readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { getAgentDir } from "@earendil-works/pi-coding-agent";
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
type Settings = Record<string, unknown>;
const LIVE = "live now: ctrl+o tool output, ctrl+t thinking";
// Verbose-on values. Verbose-off is the inverse, not pi's defaults: stock pi already shows
// thinking (hideThinkingBlock defaults to false), so an "off" that merely restored defaults
// would be indistinguishable from "on" for the noisiest part.
const VERBOSE_ON: Settings = { hideThinkingBlock: false, showCacheMissNotices: true };
const VERBOSE_OFF: Settings = { hideThinkingBlock: true, showCacheMissNotices: false };
function settingsPath(): string {
return join(getAgentDir(), "settings.json");
}
function read(): Settings {
// A parse failure must never lead to a write — that would replace a hand-edited config
// with whatever this extension thinks it knows.
const raw = readFileSync(settingsPath(), "utf-8");
return JSON.parse(raw) as Settings;
}
function isOn(s: Settings): boolean {
return s.hideThinkingBlock === false && s.showCacheMissNotices === true;
}
function write(s: Settings): void {
// Plain writeFileSync, deliberately NOT write-temp-then-rename: this path is a stow
// symlink into the dots repo, and renaming over it would replace the link with a real
// file, silently detaching the live config from the repo.
writeFileSync(settingsPath(), `${JSON.stringify(s, null, 2)}\n`, "utf-8");
}
function describe(s: Settings): string {
return `hideThinkingBlock: ${s.hideThinkingBlock ?? false}, showCacheMissNotices: ${s.showCacheMissNotices ?? false}`;
}
function toggle(ctx: ExtensionContext, mode: "on" | "off" | "toggle" | "status"): void {
let current: Settings;
try {
current = read();
} catch (err) {
ctx.ui.notify(`verbose: cannot read settings.json — ${err}`, "error");
return;
}
if (mode === "status") {
ctx.ui.notify(`Verbose ${isOn(current) ? "ON" : "OFF"}${describe(current)}. ${LIVE}`, "info");
return;
}
const on = mode === "toggle" ? !isOn(current) : mode === "on";
const next = { ...current, ...(on ? VERBOSE_ON : VERBOSE_OFF) };
try {
write(next);
} catch (err) {
ctx.ui.notify(`verbose: cannot write settings.json — ${err}`, "error");
return;
}
ctx.ui.notify(
`Verbose ${on ? "ON" : "OFF"} — applies to new sessions (settings are read at startup). ${LIVE}`,
"info",
);
}
export default function (pi: ExtensionAPI) {
pi.registerCommand("verbose", {
description: "Toggle the persisted verbose view (usage: /verbose [on|off|status])",
handler: async (args, ctx) => {
const raw = args.trim().toLowerCase();
if (raw && raw !== "on" && raw !== "off" && raw !== "status") {
ctx.ui.notify("usage: /verbose [on|off|status]", "error");
return;
}
toggle(ctx, (raw || "toggle") as "on" | "off" | "toggle" | "status");
},
});
pi.registerShortcut("alt+v", {
description: "Toggle verbose view",
handler: async (ctx) => toggle(ctx, "toggle"),
});
}
+1 -1
View File
@@ -1,5 +1,5 @@
{ {
"app.thinking.cycle": ["ctrl+shift+t"], "app.thinking.cycle": ["alt+t"],
"tui.editor.cursorUp": ["up", "ctrl+p"], "tui.editor.cursorUp": ["up", "ctrl+p"],
"app.model.cycleForward": ["alt+p"] "app.model.cycleForward": ["alt+p"]
} }
+24 -24
View File
@@ -12,7 +12,7 @@
"models": [ "models": [
{ {
"id": "gemma-4-E4B-it-UD-Q8_K_XL", "id": "gemma-4-E4B-it-UD-Q8_K_XL",
"name": "Gemma 4 E4B · 64k · vision — fast generalist, long docs", "name": "Gemma 4 E4B · 64k · vision — fast generalist, long docs (~57 t/s)",
"reasoning": false, "reasoning": false,
"input": ["text", "image"], "input": ["text", "image"],
"contextWindow": 65536, "contextWindow": 65536,
@@ -21,7 +21,7 @@
}, },
{ {
"id": "gemma-4-26B-A4B-it-UD-IQ4_XS", "id": "gemma-4-26B-A4B-it-UD-IQ4_XS",
"name": "Gemma 4 26B · 24k · vision — quality generalist", "name": "Gemma 4 26B · 24k · vision — quality generalist (~34 t/s)",
"reasoning": false, "reasoning": false,
"input": ["text", "image"], "input": ["text", "image"],
"contextWindow": 24576, "contextWindow": 24576,
@@ -30,7 +30,7 @@
}, },
{ {
"id": "Qwen3-Coder-30B-Instruct-UD-Q3_K_XL", "id": "Qwen3-Coder-30B-Instruct-UD-Q3_K_XL",
"name": "Qwen3 Coder 30B · 32k — main agent coder", "name": "Qwen3 Coder 30B · 32k — main agent coder (~30 t/s)",
"reasoning": false, "reasoning": false,
"input": ["text"], "input": ["text"],
"contextWindow": 32768, "contextWindow": 32768,
@@ -39,7 +39,7 @@
}, },
{ {
"id": "Qwen3-Coder-Next-UD-IQ3_XXS", "id": "Qwen3-Coder-Next-UD-IQ3_XXS",
"name": "Qwen3 Coder Next 80B · 128k — long sessions", "name": "Qwen3 Coder Next 80B · 128k — long sessions (~16 t/s)",
"reasoning": false, "reasoning": false,
"input": ["text"], "input": ["text"],
"contextWindow": 131072, "contextWindow": 131072,
@@ -48,7 +48,7 @@
}, },
{ {
"id": "Qwen3.6-35B-A3B-MTP-UD-IQ3_XXS", "id": "Qwen3.6-35B-A3B-MTP-UD-IQ3_XXS",
"name": "Qwen3.6 35B · 24k · vision — daily driver", "name": "Qwen3.6 35B · 24k · vision — daily driver (~36 t/s)",
"reasoning": false, "reasoning": false,
"input": ["text", "image"], "input": ["text", "image"],
"contextWindow": 24576, "contextWindow": 24576,
@@ -57,7 +57,7 @@
}, },
{ {
"id": "Qwen3.6-35B-A3B-Thinking", "id": "Qwen3.6-35B-A3B-Thinking",
"name": "Qwen3.6 35B Thinking · 24k · vision — hard problems", "name": "Qwen3.6 35B Thinking · 24k · vision — hard problems (~39 t/s)",
"reasoning": true, "reasoning": true,
"compat": { "thinkingFormat": "qwen-chat-template" }, "compat": { "thinkingFormat": "qwen-chat-template" },
"input": ["text", "image"], "input": ["text", "image"],
@@ -67,7 +67,7 @@
}, },
{ {
"id": "Qwen3.5-9B-UD-Q6_K_XL", "id": "Qwen3.5-9B-UD-Q6_K_XL",
"name": "Qwen3.5 9B · 32k · vision — quick tasks", "name": "Qwen3.5 9B · 32k · vision — quick tasks (~32 t/s)",
"reasoning": true, "reasoning": true,
"compat": { "thinkingFormat": "qwen-chat-template" }, "compat": { "thinkingFormat": "qwen-chat-template" },
"input": ["text", "image"], "input": ["text", "image"],
@@ -77,17 +77,17 @@
}, },
{ {
"id": "Qwen3.8-27B-UD-IQ3_XXS", "id": "Qwen3.8-27B-UD-IQ3_XXS",
"name": "Qwen3.8 27B · 24k · vision — hybrid reasoner", "name": "Qwen3.8 27B · 24k · vision — hybrid reasoner, quiet desktop only (~15 t/s)",
"reasoning": true, "reasoning": true,
"compat": { "thinkingFormat": "qwen-chat-template" }, "compat": { "thinkingFormat": "qwen-chat-template" },
"input": ["text", "image"], "input": ["text", "image"],
"contextWindow": 24576, "contextWindow": 24576,
"maxTokens": 8192, "maxTokens": 4096,
"cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 } "cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 }
}, },
{ {
"id": "GLM-4.7-Flash-UD-Q4_K_XL", "id": "GLM-4.7-Flash-UD-Q4_K_XL",
"name": "GLM-4.7 Flash · 24k — quality coder", "name": "GLM-4.7 Flash · 24k — quality coder (~21 t/s)",
"reasoning": true, "reasoning": true,
"compat": { "thinkingFormat": "qwen-chat-template" }, "compat": { "thinkingFormat": "qwen-chat-template" },
"input": ["text"], "input": ["text"],
@@ -97,7 +97,7 @@
}, },
{ {
"id": "gpt-oss-20b", "id": "gpt-oss-20b",
"name": "gpt-oss 20B · 64k — fast reasoning + tools", "name": "gpt-oss 20B · 64k — fast reasoning + tools (~38 t/s)",
"reasoning": true, "reasoning": true,
"input": ["text"], "input": ["text"],
"contextWindow": 65536, "contextWindow": 65536,
@@ -106,7 +106,7 @@
}, },
{ {
"id": "gpt-oss-20b-low", "id": "gpt-oss-20b-low",
"name": "gpt-oss 20B low · 64k — snappy answers", "name": "gpt-oss 20B low · 64k — snappy answers (~37 t/s)",
"reasoning": true, "reasoning": true,
"input": ["text"], "input": ["text"],
"contextWindow": 65536, "contextWindow": 65536,
@@ -127,7 +127,7 @@
"models": [ "models": [
{ {
"id": "gemma-4-E4B-it-UD-Q8_K_XL", "id": "gemma-4-E4B-it-UD-Q8_K_XL",
"name": "Gemma 4 E4B · 64k · vision — fast generalist, long docs", "name": "Gemma 4 E4B · 64k · vision — fast generalist, long docs (~57 t/s)",
"reasoning": false, "reasoning": false,
"input": ["text", "image"], "input": ["text", "image"],
"contextWindow": 65536, "contextWindow": 65536,
@@ -136,7 +136,7 @@
}, },
{ {
"id": "gemma-4-26B-A4B-it-UD-IQ4_XS", "id": "gemma-4-26B-A4B-it-UD-IQ4_XS",
"name": "Gemma 4 26B · 24k · vision — quality generalist", "name": "Gemma 4 26B · 24k · vision — quality generalist (~34 t/s)",
"reasoning": false, "reasoning": false,
"input": ["text", "image"], "input": ["text", "image"],
"contextWindow": 24576, "contextWindow": 24576,
@@ -145,7 +145,7 @@
}, },
{ {
"id": "Qwen3-Coder-30B-Instruct-UD-Q3_K_XL", "id": "Qwen3-Coder-30B-Instruct-UD-Q3_K_XL",
"name": "Qwen3 Coder 30B · 32k — main agent coder", "name": "Qwen3 Coder 30B · 32k — main agent coder (~30 t/s)",
"reasoning": false, "reasoning": false,
"input": ["text"], "input": ["text"],
"contextWindow": 32768, "contextWindow": 32768,
@@ -154,7 +154,7 @@
}, },
{ {
"id": "Qwen3-Coder-Next-UD-IQ3_XXS", "id": "Qwen3-Coder-Next-UD-IQ3_XXS",
"name": "Qwen3 Coder Next 80B · 128k — long sessions", "name": "Qwen3 Coder Next 80B · 128k — long sessions (~16 t/s)",
"reasoning": false, "reasoning": false,
"input": ["text"], "input": ["text"],
"contextWindow": 131072, "contextWindow": 131072,
@@ -163,7 +163,7 @@
}, },
{ {
"id": "Qwen3.6-35B-A3B-MTP-UD-IQ3_XXS", "id": "Qwen3.6-35B-A3B-MTP-UD-IQ3_XXS",
"name": "Qwen3.6 35B · 24k · vision — daily driver", "name": "Qwen3.6 35B · 24k · vision — daily driver (~36 t/s)",
"reasoning": false, "reasoning": false,
"input": ["text", "image"], "input": ["text", "image"],
"contextWindow": 24576, "contextWindow": 24576,
@@ -172,7 +172,7 @@
}, },
{ {
"id": "Qwen3.6-35B-A3B-Thinking", "id": "Qwen3.6-35B-A3B-Thinking",
"name": "Qwen3.6 35B Thinking · 24k · vision — hard problems", "name": "Qwen3.6 35B Thinking · 24k · vision — hard problems (~39 t/s)",
"reasoning": true, "reasoning": true,
"compat": { "thinkingFormat": "qwen-chat-template" }, "compat": { "thinkingFormat": "qwen-chat-template" },
"input": ["text", "image"], "input": ["text", "image"],
@@ -182,7 +182,7 @@
}, },
{ {
"id": "Qwen3.5-9B-UD-Q6_K_XL", "id": "Qwen3.5-9B-UD-Q6_K_XL",
"name": "Qwen3.5 9B · 32k · vision — quick tasks", "name": "Qwen3.5 9B · 32k · vision — quick tasks (~32 t/s)",
"reasoning": true, "reasoning": true,
"compat": { "thinkingFormat": "qwen-chat-template" }, "compat": { "thinkingFormat": "qwen-chat-template" },
"input": ["text", "image"], "input": ["text", "image"],
@@ -192,17 +192,17 @@
}, },
{ {
"id": "Qwen3.8-27B-UD-IQ3_XXS", "id": "Qwen3.8-27B-UD-IQ3_XXS",
"name": "Qwen3.8 27B · 24k · vision — hybrid reasoner", "name": "Qwen3.8 27B · 24k · vision — hybrid reasoner, quiet desktop only (~15 t/s)",
"reasoning": true, "reasoning": true,
"compat": { "thinkingFormat": "qwen-chat-template" }, "compat": { "thinkingFormat": "qwen-chat-template" },
"input": ["text", "image"], "input": ["text", "image"],
"contextWindow": 24576, "contextWindow": 24576,
"maxTokens": 8192, "maxTokens": 4096,
"cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 } "cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 }
}, },
{ {
"id": "GLM-4.7-Flash-UD-Q4_K_XL", "id": "GLM-4.7-Flash-UD-Q4_K_XL",
"name": "GLM-4.7 Flash · 24k — quality coder", "name": "GLM-4.7 Flash · 24k — quality coder (~21 t/s)",
"reasoning": true, "reasoning": true,
"compat": { "thinkingFormat": "qwen-chat-template" }, "compat": { "thinkingFormat": "qwen-chat-template" },
"input": ["text"], "input": ["text"],
@@ -212,7 +212,7 @@
}, },
{ {
"id": "gpt-oss-20b", "id": "gpt-oss-20b",
"name": "gpt-oss 20B · 64k — fast reasoning + tools", "name": "gpt-oss 20B · 64k — fast reasoning + tools (~38 t/s)",
"reasoning": true, "reasoning": true,
"input": ["text"], "input": ["text"],
"contextWindow": 65536, "contextWindow": 65536,
@@ -221,7 +221,7 @@
}, },
{ {
"id": "gpt-oss-20b-low", "id": "gpt-oss-20b-low",
"name": "gpt-oss 20B low · 64k — snappy answers", "name": "gpt-oss 20B low · 64k — snappy answers (~37 t/s)",
"reasoning": true, "reasoning": true,
"input": ["text"], "input": ["text"],
"contextWindow": 65536, "contextWindow": 65536,
+1 -1
View File
@@ -58,7 +58,7 @@
"ex": "mode" "ex": "mode"
} }
}, },
"lastChangelogVersion": "0.83.0", "lastChangelogVersion": "0.85.1",
"packages": [ "packages": [
"npm:pi-vim" "npm:pi-vim"
] ]
+1 -1
View File
@@ -42,7 +42,7 @@ bind = $mainMod, F, exec, $yt # launch yt alternative
bind = $mainMod, B, exec, $browser # launch web browser bind = $mainMod, B, exec, $browser # launch web browser
bind = $mainMod, G, exec, gajim # launch messanger bind = $mainMod, G, exec, gajim # launch messanger
bind = $mainMod, K, exec, keepassxc # launch password manager bind = $mainMod, K, exec, keepassxc # launch password manager
bindd = $mainMod, N, Neovim, exec, $term -e nvim # bindd = $mainMod, N, Neovim, exec, $term -e nvim
bindd = $mainMod, R, $d pyradio, exec, kitty --session ~/.config/kitty/music.conf bindd = $mainMod, R, $d pyradio, exec, kitty --session ~/.config/kitty/music.conf
bindd = Ctrl+Shift, Escape, Btop, exec, kitty -e btop bindd = Ctrl+Shift, Escape, Btop, exec, kitty -e btop
bindd = Ctrl+Alt, Delete, Btop, exec, kitty -e btop bindd = Ctrl+Alt, Delete, Btop, exec, kitty -e btop
+9
View File
@@ -0,0 +1,9 @@
## gui package — put HyDE's script dir on the *session* PATH: the compositor and
## everything it spawns directly (raw exec binds, terminals) resolve bare hyde
## script names through it. Nothing else adds it — HyDE only self-bootstraps PATH
## inside its own wrappers (hyde-shell/app.sh), so a bare script name execed
## outside that lineage is "command not found" (TROUBLESHOOTING.md symptom 8).
## The systemd-unit side is covered by the twin
## gui/.config/environment.d/10-hyde-path.conf — both are needed.
## No ordering constraint vs hyde.fish (it never touches this dir).
fish_add_path --global ~/.local/lib/hyde
-8
View File
@@ -1,8 +0,0 @@
## gui package — waybar helpers. Lives in conf.d/ (fish sources every conf.d/*.fish),
## NOT in host.fish: each host package ships its own host.fish, which stow deploys last
## with --override — a gui/host.fish would be shadowed on every host that has one.
# Reload the custom waybar (kill + relaunch, detached so it survives this shell).
# Same invocation as userprefs.conf exec-once / the Ctrl+Alt+W bind. Killing waybar
# also stops its cava child (cava-waybar.sh is a child of the bar).
abbr wbar 'killall waybar; setsid -f waybar -c ~/.config/waybar/layouts/custom.jsonc -s ~/.config/waybar/layouts/custom.css'
@@ -18,7 +18,7 @@
## Nothing else from hyde.fish is undone here: its aliases lose to config.fish, ## Nothing else from hyde.fish is undone here: its aliases lose to config.fish,
## which fish loads after conf.d, and its `df` wrapper is a function we do not ## which fish loads after conf.d, and its `df` wrapper is a function we do not
## otherwise define. Delete this file if you ever decide you want starship. ## otherwise define. Delete this file if you ever decide you want starship.
for f in fish_prompt fish_right_prompt for f in fish_prompt fish_right_prompt fish_mode_prompt
if test -r $__fish_config_dir/functions/$f.fish if test -r $__fish_config_dir/functions/$f.fish
source $__fish_config_dir/functions/$f.fish source $__fish_config_dir/functions/$f.fish
end end
@@ -0,0 +1,3 @@
function fish_mode_prompt --description "Displays the current vi mode"
fish_default_mode_prompt
end
-3
View File
@@ -117,9 +117,6 @@ bindd = $mainMod Shift, T, $d terminal emulator , exec, alacritty
bindd = Control Alt, T, $d terminal emulator , exec, $TERMINAL bindd = Control Alt, T, $d terminal emulator , exec, $TERMINAL
# bindd = Control Shift, Alt, T, $d terminal emulator , exec, tilix # launch tilix terminal emulator - using for ssh # bindd = Control Shift, Alt, T, $d terminal emulator , exec, tilix # launch tilix terminal emulator - using for ssh
bindd = $mainMod Control, T, $d terminal emulator, exec, tilix # launch tilix terminal emulator bindd = $mainMod Control, T, $d terminal emulator, exec, tilix # launch tilix terminal emulator
# bindd = $mainMod, R, $d pyradio, exec, $TERMINAL pyradio #radio
bindd = $mainMod, I, $d work, exec, kitty --session ~/.config/kitty/c2.conf
bindd = $mainMod, O, $d work, exec, kitty --session ~/.config/kitty/runners.conf
bindd = $mainMod, R, $d pyradio, exec, kitty --session ~/.config/kitty/music.conf bindd = $mainMod, R, $d pyradio, exec, kitty --session ~/.config/kitty/music.conf
bindd = $mainMod, E, $d file explorer , exec, $EXPLORER bindd = $mainMod, E, $d file explorer , exec, $EXPLORER
bindd = $mainMod, C, $d neovim, exec, $TERMINAL nvim #editor bindd = $mainMod, C, $d neovim, exec, $TERMINAL nvim #editor
+3 -3
View File
@@ -200,13 +200,13 @@ window#waybar.empty #custom-sr_end {
} }
window#waybar.cavabar { window#waybar.cavabar {
background: transparent; /* background: transparent; */
background: alpha(@main-bg, 0.1);
} }
window#waybar.cavabar #custom-cava { window#waybar.cavabar #custom-cava {
font-size: 24px; font-size: 24px;
color: alpha(@main-fg, 0.7); color: alpha(@main-fg, 0.5);
background: rgba(0, 0, 0, 0.2);
margin: 0px; margin: 0px;
padding: 0px; padding: 0px;
} }
+12 -11
View File
@@ -48,34 +48,35 @@ clients:
name: local name: local
api_base: http://192.168.0.204:11343/v1 api_base: http://192.168.0.204:11343/v1
models: &lan_models models: &lan_models
# Speeds are benched decode t/s (clean-night sweeps) — see fl/.config/llamacpp/README.md roster.
- name: Qwen3-Coder-30B-Instruct-UD-Q3_K_XL - name: Qwen3-Coder-30B-Instruct-UD-Q3_K_XL
max_input_tokens: 30000 # ctx 32768 max_input_tokens: 30000 # ctx 32768 · ~30 t/s — main agent coder
- name: Qwen3-Coder-Next-UD-IQ3_XXS - name: Qwen3-Coder-Next-UD-IQ3_XXS
max_input_tokens: 128000 # ctx 131072 max_input_tokens: 128000 # ctx 131072 · ~16 t/s — long sessions (128k)
- name: Qwen3.6-35B-A3B-MTP-UD-IQ3_XXS - name: Qwen3.6-35B-A3B-MTP-UD-IQ3_XXS
max_input_tokens: 22000 # ctx 24576 max_input_tokens: 22000 # ctx 24576 · ~36 t/s — daily driver
supports_vision: true supports_vision: true
- name: Qwen3.6-35B-A3B-Thinking - name: Qwen3.6-35B-A3B-Thinking
max_input_tokens: 22000 # ctx 24576 max_input_tokens: 22000 # ctx 24576 · ~39 t/s — hard problems
supports_vision: true supports_vision: true
- name: Qwen3.5-9B-UD-Q6_K_XL - name: Qwen3.5-9B-UD-Q6_K_XL
max_input_tokens: 30000 # ctx 32768 max_input_tokens: 30000 # ctx 32768 · ~32 t/s — quick tasks
supports_vision: true supports_vision: true
- name: Qwen3.8-27B-UD-IQ3_XXS - name: Qwen3.8-27B-UD-IQ3_XXS
max_input_tokens: 22000 # ctx 24576 max_input_tokens: 22000 # ctx 24576 · ~15 t/s — hybrid reasoner, quiet desktop only
supports_vision: true supports_vision: true
- name: gemma-4-26B-A4B-it-UD-IQ4_XS - name: gemma-4-26B-A4B-it-UD-IQ4_XS
max_input_tokens: 22000 # ctx 24576 max_input_tokens: 22000 # ctx 24576 · ~34 t/s — quality generalist, best vision
supports_vision: true supports_vision: true
- name: gemma-4-E4B-it-UD-Q8_K_XL - name: gemma-4-E4B-it-UD-Q8_K_XL
max_input_tokens: 62000 # ctx 65536 max_input_tokens: 62000 # ctx 65536 · ~57 t/s — fast generalist, long docs
supports_vision: true supports_vision: true
- name: GLM-4.7-Flash-UD-Q4_K_XL - name: GLM-4.7-Flash-UD-Q4_K_XL
max_input_tokens: 22000 # ctx 24576 max_input_tokens: 22000 # ctx 24576 · ~21 t/s — quality coder
- name: gpt-oss-20b - name: gpt-oss-20b
max_input_tokens: 62000 # ctx 65536 max_input_tokens: 62000 # ctx 65536 · ~38 t/s — fast reasoning + tools
- name: gpt-oss-20b-low - name: gpt-oss-20b-low
max_input_tokens: 62000 # ctx 65536 max_input_tokens: 62000 # ctx 65536 · ~37 t/s — snappy answers
# Remote access via duskadiy.com (reachable from anywhere; requires an API key — # Remote access via duskadiy.com (reachable from anywhere; requires an API key —
# $DUSKADIY_API_KEY, from the gitignored conf.d/secrets.fish). The LAN endpoint above is keyless. # $DUSKADIY_API_KEY, from the gitignored conf.d/secrets.fish). The LAN endpoint above is keyless.
# Same server, same model ids — reuse the list above via a YAML anchor. # Same server, same model ids — reuse the list above via a YAML anchor.
+136 -30
View File
@@ -1,10 +1,11 @@
{ {
"$schema": "https://opencode.ai/config.json", "$schema": "https://opencode.ai/config.json",
"model": "llama.cpp/gemma-4-26B-A4B-it-UD-IQ4_XS", "model": "llama.cpp/gemma-4-26B-A4B-it-UD-IQ4_XS",
"small_model": "llama.cpp/gemma-4-E4B-it-UD-Q8_K_XL",
"instructions": ["CLAUDE.md", "AGENTS.md"], "instructions": ["CLAUDE.md", "AGENTS.md"],
"share": "disabled", "share": "disabled",
"autoupdate": "notify", "autoupdate": "notify",
"tool_output": { "max_lines": 200, "max_bytes": 16384 },
"snapshot": false,
"disabled_providers": [ "disabled_providers": [
"gemini", "gemini",
"anthropic", "anthropic",
@@ -17,6 +18,7 @@
"webfetch": "ask", "webfetch": "ask",
"websearch": "ask", "websearch": "ask",
"external_directory": "ask", "external_directory": "ask",
"question": "allow",
"read": { "read": {
"*": "allow", "*": "allow",
"**/.env*": "deny", "**/.env*": "deny",
@@ -133,7 +135,8 @@
"npm": "@ai-sdk/openai-compatible", "npm": "@ai-sdk/openai-compatible",
"name": "llamacpp (lw local)", "name": "llamacpp (lw local)",
"options": { "options": {
"baseURL": "http://127.0.0.1:11343/v1" "baseURL": "http://127.0.0.1:11343/v1",
"timeout": 3600000
}, },
"models": { "models": {
"gemma-4-E2B-it-UD-Q4_K_XL": { "name": "Gemma 4 E2B · 4k — fastest on lw (~6 t/s)", "limit": { "context": 4096, "output": 1024 } }, "gemma-4-E2B-it-UD-Q4_K_XL": { "name": "Gemma 4 E2B · 4k — fastest on lw (~6 t/s)", "limit": { "context": 4096, "output": 1024 } },
@@ -150,46 +153,51 @@
"npm": "@ai-sdk/openai-compatible", "npm": "@ai-sdk/openai-compatible",
"name": "DuskaDIY", "name": "DuskaDIY",
"options": { "options": {
"baseURL": "https://llm.duskadiy.com/api/v1" "baseURL": "https://llm.duskadiy.com/api/v1",
"apiKey": "{env:DUSKADIY_API_KEY}",
"timeout": 3600000
}, },
"models": { "models": {
"Qwen3-Coder-30B-Instruct-UD-Q3_K_XL": { "name": "Qwen3 Coder 30B · 32k — main agent coder (remote)", "limit": { "context": 32768, "output": 4096 } }, "Qwen3-Coder-30B-Instruct-UD-Q3_K_XL": { "name": "Qwen3 Coder 30B · 32k — main agent coder (~30 t/s, remote)", "limit": { "context": 32768, "output": 4096 } },
"Qwen3-Coder-Next-UD-IQ3_XXS": { "name": "Qwen3 Coder Next 80B · 128k — long sessions (remote)", "limit": { "context": 131072, "output": 16384 } }, "Qwen3-Coder-Next-UD-IQ3_XXS": { "name": "Qwen3 Coder Next 80B · 128k — long sessions (~16 t/s, remote)", "limit": { "context": 131072, "output": 16384 } },
"Qwen3.6-35B-A3B-MTP-UD-IQ3_XXS": { "name": "Qwen3.6 35B · 24k · vision — daily driver (remote)", "attachment": true, "limit": { "context": 24576, "output": 8192 } }, "Qwen3.6-35B-A3B-MTP-UD-IQ3_XXS": { "name": "Qwen3.6 35B · 24k · vision — daily driver (~36 t/s, remote)", "attachment": true, "limit": { "context": 24576, "output": 8192 } },
"Qwen3.6-35B-A3B-Thinking": { "name": "Qwen3.6 35B Thinking · 24k · vision — hard problems (remote)", "attachment": true, "limit": { "context": 24576, "output": 8192 } }, "Qwen3.6-35B-A3B-Thinking": { "reasoning": true, "name": "Qwen3.6 35B Thinking · 24k · vision — hard problems (~39 t/s, remote)", "attachment": true, "limit": { "context": 24576, "output": 8192 } },
"Qwen3.5-9B-UD-Q6_K_XL": { "name": "Qwen3.5 9B · 32k · vision — quick tasks (remote)", "attachment": true, "limit": { "context": 32768, "output": 8192 } }, "Qwen3.5-9B-UD-Q6_K_XL": { "reasoning": true, "name": "Qwen3.5 9B · 32k · vision — quick tasks (~32 t/s, remote)", "attachment": true, "limit": { "context": 32768, "output": 8192 } },
"Qwen3.8-27B-UD-IQ3_XXS": { "name": "Qwen3.8 27B · 24k · vision — hybrid reasoner (remote)", "attachment": true, "limit": { "context": 24576, "output": 8192 } }, "Qwen3.8-27B-UD-IQ3_XXS": { "reasoning": true, "name": "Qwen3.8 27B · 24k · vision — hybrid reasoner, quiet desktop only (~15 t/s, remote)", "attachment": true, "limit": { "context": 24576, "output": 8192 } },
"gemma-4-26B-A4B-it-UD-IQ4_XS": { "name": "Gemma 4 26B · 24k · vision — quality generalist (remote)", "attachment": true, "limit": { "context": 24576, "output": 4096 } }, "gemma-4-26B-A4B-it-UD-IQ4_XS": { "name": "Gemma 4 26B · 24k · vision — quality generalist (~34 t/s, remote)", "attachment": true, "limit": { "context": 24576, "output": 4096 } },
"gemma-4-E4B-it-UD-Q8_K_XL": { "name": "Gemma 4 E4B · 64k · vision — fast generalist, long docs (remote)", "attachment": true, "limit": { "context": 65536, "output": 16384 } }, "gemma-4-E4B-it-UD-Q8_K_XL": { "name": "Gemma 4 E4B · 64k · vision — fast generalist, long docs (~57 t/s, remote)", "attachment": true, "limit": { "context": 65536, "output": 16384 } },
"GLM-4.7-Flash-UD-Q4_K_XL": { "name": "GLM-4.7 Flash · 24k — quality coder (remote)", "limit": { "context": 24576, "output": 8192 } }, "GLM-4.7-Flash-UD-Q4_K_XL": { "reasoning": true, "interleaved": "reasoning_content", "name": "GLM-4.7 Flash · 24k — quality coder (~21 t/s, remote)", "limit": { "context": 24576, "output": 8192 } },
"gpt-oss-20b": { "name": "gpt-oss 20B · 64k — fast reasoning + tools (remote)", "limit": { "context": 65536, "output": 8192 } }, "gpt-oss-20b": { "reasoning": true, "name": "gpt-oss 20B · 64k — fast reasoning + tools (~38 t/s, remote)", "limit": { "context": 65536, "output": 8192 } },
"gpt-oss-20b-low": { "name": "gpt-oss 20B low · 64k — snappy answers (remote)", "limit": { "context": 65536, "output": 8192 } } "gpt-oss-20b-low": { "reasoning": true, "name": "gpt-oss 20B low · 64k — snappy answers (~37 t/s, remote)", "limit": { "context": 65536, "output": 8192 } }
} }
}, },
"llama.cpp": { "llama.cpp": {
"npm": "@ai-sdk/openai-compatible", "npm": "@ai-sdk/openai-compatible",
"name": "llamacpp", "name": "llamacpp",
"options": { "options": {
"baseURL": "http://192.168.0.204:11343/v1" "baseURL": "http://192.168.0.204:11343/v1",
"timeout": 3600000
}, },
"models": { "models": {
"Qwen3-Coder-Next-UD-IQ3_XXS": { "name": "Qwen3 Coder Next 80B · 128k — long sessions", "limit": { "context": 131072, "output": 16384 } }, "Qwen3-Coder-Next-UD-IQ3_XXS": { "name": "Qwen3 Coder Next 80B · 128k — long sessions (~16 t/s)", "limit": { "context": 131072, "output": 16384 } },
"Qwen3-Coder-30B-Instruct-UD-Q3_K_XL": { "name": "Qwen3 Coder 30B · 32k — main agent coder", "limit": { "context": 32768, "output": 4096 } }, "Qwen3-Coder-30B-Instruct-UD-Q3_K_XL": { "name": "Qwen3 Coder 30B · 32k — main agent coder (~30 t/s)", "limit": { "context": 32768, "output": 4096 } },
"Qwen3.6-35B-A3B-MTP-UD-IQ3_XXS": { "name": "Qwen3.6 35B · 24k · vision — daily driver", "attachment": true, "limit": { "context": 24576, "output": 8192 } }, "Qwen3.6-35B-A3B-MTP-UD-IQ3_XXS": { "name": "Qwen3.6 35B · 24k · vision — daily driver (~36 t/s)", "attachment": true, "limit": { "context": 24576, "output": 8192 } },
"Qwen3.6-35B-A3B-Thinking": { "name": "Qwen3.6 35B Thinking · 24k · vision — hard problems", "attachment": true, "limit": { "context": 24576, "output": 8192 } }, "Qwen3.6-35B-A3B-Thinking": { "reasoning": true, "name": "Qwen3.6 35B Thinking · 24k · vision — hard problems (~39 t/s)", "attachment": true, "limit": { "context": 24576, "output": 8192 } },
"Qwen3.5-9B-UD-Q6_K_XL": { "name": "Qwen3.5 9B · 32k · vision — quick tasks", "attachment": true, "limit": { "context": 32768, "output": 8192 } }, "Qwen3.5-9B-UD-Q6_K_XL": { "reasoning": true, "name": "Qwen3.5 9B · 32k · vision — quick tasks (~32 t/s)", "attachment": true, "limit": { "context": 32768, "output": 8192 } },
"Qwen3.8-27B-UD-IQ3_XXS": { "name": "Qwen3.8 27B · 24k · vision — hybrid reasoner", "attachment": true, "limit": { "context": 24576, "output": 8192 } }, "Qwen3.8-27B-UD-IQ3_XXS": { "reasoning": true, "name": "Qwen3.8 27B · 24k · vision — hybrid reasoner, quiet desktop only (~15 t/s)", "attachment": true, "limit": { "context": 24576, "output": 8192 } },
"gemma-4-26B-A4B-it-UD-IQ4_XS": { "name": "Gemma 4 26B · 24k · vision — quality generalist", "attachment": true, "limit": { "context": 24576, "output": 4096 } }, "gemma-4-26B-A4B-it-UD-IQ4_XS": { "name": "Gemma 4 26B · 24k · vision — quality generalist (~34 t/s)", "attachment": true, "limit": { "context": 24576, "output": 4096 } },
"gemma-4-E4B-it-UD-Q8_K_XL": { "name": "Gemma 4 E4B · 64k · vision — fast generalist, long docs", "attachment": true, "limit": { "context": 65536, "output": 16384 } }, "gemma-4-E4B-it-UD-Q8_K_XL": { "name": "Gemma 4 E4B · 64k · vision — fast generalist, long docs (~57 t/s)", "attachment": true, "limit": { "context": 65536, "output": 16384 } },
"GLM-4.7-Flash-UD-Q4_K_XL": { "name": "GLM-4.7 Flash · 24k — quality coder", "limit": { "context": 24576, "output": 8192 } }, "GLM-4.7-Flash-UD-Q4_K_XL": { "reasoning": true, "interleaved": "reasoning_content", "name": "GLM-4.7 Flash · 24k — quality coder (~21 t/s)", "limit": { "context": 24576, "output": 8192 } },
"gpt-oss-20b": { "name": "gpt-oss 20B · 64k — fast reasoning + tools", "limit": { "context": 65536, "output": 8192 } }, "gpt-oss-20b": { "reasoning": true, "name": "gpt-oss 20B · 64k — fast reasoning + tools (~38 t/s)", "limit": { "context": 65536, "output": 8192 } },
"gpt-oss-20b-low": { "name": "gpt-oss 20B low · 64k — snappy answers", "limit": { "context": 65536, "output": 8192 } } "gpt-oss-20b-low": { "reasoning": true, "name": "gpt-oss 20B low · 64k — snappy answers (~37 t/s)", "limit": { "context": 65536, "output": 8192 } }
} }
} }
}, },
"agent": { "agent": {
"plan": { "plan": {
"mode": "primary", "mode": "primary",
"color": "info",
"steps": 30,
"model": "llama.cpp/gemma-4-26B-A4B-it-UD-IQ4_XS", "model": "llama.cpp/gemma-4-26B-A4B-it-UD-IQ4_XS",
"permission": { "permission": {
"edit": "deny" "edit": "deny"
@@ -197,6 +205,8 @@
}, },
"build": { "build": {
"mode": "primary", "mode": "primary",
"color": "primary",
"steps": 30,
"model": "llama.cpp/gemma-4-26B-A4B-it-UD-IQ4_XS", "model": "llama.cpp/gemma-4-26B-A4B-it-UD-IQ4_XS",
"permission": { "permission": {
"edit": { "edit": {
@@ -214,11 +224,108 @@
} }
} }
}, },
"auto": {
"description": "Auto mode: acts without asking; only irreversible or outward-facing actions ask",
"mode": "primary",
"color": "#f9e2af",
"steps": 30,
"model": "llama.cpp/gemma-4-26B-A4B-it-UD-IQ4_XS",
"permission": {
"edit": {
"*": "allow",
"**/.env*": "deny",
"**/*.env": "deny",
"**/*.pem": "deny",
"**/*.key": "deny",
"**/id_rsa*": "deny",
"**/.ssh/**": "deny",
"**/.aws/**": "deny",
"**/.git/**": "deny",
"**/.npmrc": "deny",
"**/.netrc": "deny"
},
"webfetch": "allow",
"websearch": "allow",
"external_directory": "ask",
"question": "allow",
"bash": {
"*": "allow",
"git *": "allow",
"git commit*": "ask",
"git push*": "ask",
"git reset*": "ask",
"git checkout*": "ask",
"git switch*": "ask",
"git restore*": "ask",
"git rebase*": "ask",
"git merge*": "ask",
"git cherry-pick*": "ask",
"git stash*": "ask",
"git clean*": "ask",
"git branch -*": "ask",
"git tag*": "ask",
"git remote add*": "ask",
"git remote remove*": "ask",
"git remote set-url*": "ask",
"rm *": "ask",
"rmdir *": "ask",
"kill *": "ask",
"pkill *": "ask",
"killall *": "ask",
"systemctl *": "ask",
"curl *": "ask",
"wget *": "ask",
"ssh *": "ask",
"scp *": "ask",
"rsync *": "ask",
"npm install*": "ask",
"npm i": "ask",
"npm i *": "ask",
"npm ci*": "ask",
"pnpm install*": "ask",
"pnpm i": "ask",
"pnpm i *": "ask",
"pnpm add*": "ask",
"yarn add*": "ask",
"yarn install*": "ask",
"pip install*": "ask",
"pip3 install*": "ask",
"pipx install*": "ask",
"cargo install*": "ask",
"go install*": "ask",
"gem install*": "ask",
"yay *": "ask",
"sudo *": "deny",
"doas *": "deny",
"dd *": "deny",
"mkfs*": "deny",
"shred *": "deny",
"chmod -R *": "deny",
"rm -rf /*": "deny",
"rm -rf ~*": "deny",
"cat .env*": "deny",
"cat *.env": "deny",
"cat *.pem": "deny",
"cat *.key": "deny",
"cat *id_rsa*": "deny",
"cat *secret*": "deny",
"cat *credential*": "deny",
"cat ~/.ssh/*": "deny",
"cat ~/.aws/*": "deny",
"grep .env*": "deny",
"grep * .env*": "deny"
}
}
},
"code-reviewer": { "code-reviewer": {
"description": "Reviews code for best practices and potential issues", "description": "Reviews code for best practices and potential issues",
"prompt": "You are a code reviewer. Focus on security, performance, and maintainability.", "prompt": "You are a code reviewer. Focus on security, performance, and maintainability.",
"mode": "subagent", "mode": "subagent",
"model": "llama.cpp/GLM-4.7-Flash-UD-Q4_K_XL",
"permission": { "permission": {
"edit": "deny" "edit": "deny"
} }
@@ -227,7 +334,7 @@
"description": "Runs and interprets test suites", "description": "Runs and interprets test suites",
"prompt": "You are a test runner. Focus on identifying failing tests and providing clear reproduction steps.", "prompt": "You are a test runner. Focus on identifying failing tests and providing clear reproduction steps.",
"mode": "subagent", "mode": "subagent",
"model": "llama.cpp/GLM-4.7-Flash-UD-Q4_K_XL", "hidden": true,
"permission": { "permission": {
"edit": "deny" "edit": "deny"
} }
@@ -236,7 +343,6 @@
"description": "Maintains project documentation and docstrings", "description": "Maintains project documentation and docstrings",
"prompt": "You are a technical writer. Focus on clarity, accuracy, and keeping documentation in sync with code changes.", "prompt": "You are a technical writer. Focus on clarity, accuracy, and keeping documentation in sync with code changes.",
"mode": "subagent", "mode": "subagent",
"model": "llama.cpp/gemma-4-26B-A4B-it-UD-IQ4_XS",
"permission": { "permission": {
"edit": { "edit": {
"*": "deny", "*": "deny",
@@ -252,7 +358,7 @@
"description": "Scans for security vulnerabilities and hardcoded secrets", "description": "Scans for security vulnerabilities and hardcoded secrets",
"prompt": "You are a security auditor. Focus on OWASP principles, secret detection, and dependency vulnerabilities.", "prompt": "You are a security auditor. Focus on OWASP principles, secret detection, and dependency vulnerabilities.",
"mode": "subagent", "mode": "subagent",
"model": "llama.cpp/GLM-4.7-Flash-UD-Q4_K_XL", "hidden": true,
"permission": { "permission": {
"edit": "deny" "edit": "deny"
} }
+22 -22
View File
@@ -97,7 +97,7 @@
"models": [ "models": [
{ {
"id": "gemma-4-E4B-it-UD-Q8_K_XL", "id": "gemma-4-E4B-it-UD-Q8_K_XL",
"name": "Gemma 4 E4B · 64k · vision — fast generalist, long docs", "name": "Gemma 4 E4B · 64k · vision — fast generalist, long docs (~57 t/s)",
"reasoning": false, "reasoning": false,
"input": ["text", "image"], "input": ["text", "image"],
"contextWindow": 65536, "contextWindow": 65536,
@@ -106,7 +106,7 @@
}, },
{ {
"id": "gemma-4-26B-A4B-it-UD-IQ4_XS", "id": "gemma-4-26B-A4B-it-UD-IQ4_XS",
"name": "Gemma 4 26B · 24k · vision — quality generalist", "name": "Gemma 4 26B · 24k · vision — quality generalist (~34 t/s)",
"reasoning": false, "reasoning": false,
"input": ["text", "image"], "input": ["text", "image"],
"contextWindow": 24576, "contextWindow": 24576,
@@ -115,7 +115,7 @@
}, },
{ {
"id": "Qwen3-Coder-30B-Instruct-UD-Q3_K_XL", "id": "Qwen3-Coder-30B-Instruct-UD-Q3_K_XL",
"name": "Qwen3 Coder 30B · 32k — main agent coder", "name": "Qwen3 Coder 30B · 32k — main agent coder (~30 t/s)",
"reasoning": false, "reasoning": false,
"input": ["text"], "input": ["text"],
"contextWindow": 32768, "contextWindow": 32768,
@@ -124,7 +124,7 @@
}, },
{ {
"id": "Qwen3-Coder-Next-UD-IQ3_XXS", "id": "Qwen3-Coder-Next-UD-IQ3_XXS",
"name": "Qwen3 Coder Next 80B · 128k — long sessions", "name": "Qwen3 Coder Next 80B · 128k — long sessions (~16 t/s)",
"reasoning": false, "reasoning": false,
"input": ["text"], "input": ["text"],
"contextWindow": 131072, "contextWindow": 131072,
@@ -133,7 +133,7 @@
}, },
{ {
"id": "Qwen3.6-35B-A3B-MTP-UD-IQ3_XXS", "id": "Qwen3.6-35B-A3B-MTP-UD-IQ3_XXS",
"name": "Qwen3.6 35B · 24k · vision — daily driver", "name": "Qwen3.6 35B · 24k · vision — daily driver (~36 t/s)",
"reasoning": false, "reasoning": false,
"input": ["text", "image"], "input": ["text", "image"],
"contextWindow": 24576, "contextWindow": 24576,
@@ -142,7 +142,7 @@
}, },
{ {
"id": "Qwen3.6-35B-A3B-Thinking", "id": "Qwen3.6-35B-A3B-Thinking",
"name": "Qwen3.6 35B Thinking · 24k · vision — hard problems", "name": "Qwen3.6 35B Thinking · 24k · vision — hard problems (~39 t/s)",
"reasoning": true, "reasoning": true,
"compat": { "thinkingFormat": "qwen-chat-template" }, "compat": { "thinkingFormat": "qwen-chat-template" },
"input": ["text", "image"], "input": ["text", "image"],
@@ -152,7 +152,7 @@
}, },
{ {
"id": "Qwen3.5-9B-UD-Q6_K_XL", "id": "Qwen3.5-9B-UD-Q6_K_XL",
"name": "Qwen3.5 9B · 32k · vision — quick tasks", "name": "Qwen3.5 9B · 32k · vision — quick tasks (~32 t/s)",
"reasoning": true, "reasoning": true,
"compat": { "thinkingFormat": "qwen-chat-template" }, "compat": { "thinkingFormat": "qwen-chat-template" },
"input": ["text", "image"], "input": ["text", "image"],
@@ -162,7 +162,7 @@
}, },
{ {
"id": "Qwen3.8-27B-UD-IQ3_XXS", "id": "Qwen3.8-27B-UD-IQ3_XXS",
"name": "Qwen3.8 27B · 24k · vision — hybrid reasoner", "name": "Qwen3.8 27B · 24k · vision — hybrid reasoner, quiet desktop only (~15 t/s)",
"reasoning": true, "reasoning": true,
"compat": { "thinkingFormat": "qwen-chat-template" }, "compat": { "thinkingFormat": "qwen-chat-template" },
"input": ["text", "image"], "input": ["text", "image"],
@@ -172,7 +172,7 @@
}, },
{ {
"id": "GLM-4.7-Flash-UD-Q4_K_XL", "id": "GLM-4.7-Flash-UD-Q4_K_XL",
"name": "GLM-4.7 Flash · 24k — quality coder", "name": "GLM-4.7 Flash · 24k — quality coder (~21 t/s)",
"reasoning": true, "reasoning": true,
"compat": { "thinkingFormat": "qwen-chat-template" }, "compat": { "thinkingFormat": "qwen-chat-template" },
"input": ["text"], "input": ["text"],
@@ -182,7 +182,7 @@
}, },
{ {
"id": "gpt-oss-20b", "id": "gpt-oss-20b",
"name": "gpt-oss 20B · 64k — fast reasoning + tools", "name": "gpt-oss 20B · 64k — fast reasoning + tools (~38 t/s)",
"reasoning": true, "reasoning": true,
"input": ["text"], "input": ["text"],
"contextWindow": 65536, "contextWindow": 65536,
@@ -191,7 +191,7 @@
}, },
{ {
"id": "gpt-oss-20b-low", "id": "gpt-oss-20b-low",
"name": "gpt-oss 20B low · 64k — snappy answers", "name": "gpt-oss 20B low · 64k — snappy answers (~37 t/s)",
"reasoning": true, "reasoning": true,
"input": ["text"], "input": ["text"],
"contextWindow": 65536, "contextWindow": 65536,
@@ -212,7 +212,7 @@
"models": [ "models": [
{ {
"id": "gemma-4-E4B-it-UD-Q8_K_XL", "id": "gemma-4-E4B-it-UD-Q8_K_XL",
"name": "Gemma 4 E4B · 64k · vision — fast generalist, long docs", "name": "Gemma 4 E4B · 64k · vision — fast generalist, long docs (~57 t/s)",
"reasoning": false, "reasoning": false,
"input": ["text", "image"], "input": ["text", "image"],
"contextWindow": 65536, "contextWindow": 65536,
@@ -221,7 +221,7 @@
}, },
{ {
"id": "gemma-4-26B-A4B-it-UD-IQ4_XS", "id": "gemma-4-26B-A4B-it-UD-IQ4_XS",
"name": "Gemma 4 26B · 24k · vision — quality generalist", "name": "Gemma 4 26B · 24k · vision — quality generalist (~34 t/s)",
"reasoning": false, "reasoning": false,
"input": ["text", "image"], "input": ["text", "image"],
"contextWindow": 24576, "contextWindow": 24576,
@@ -230,7 +230,7 @@
}, },
{ {
"id": "Qwen3-Coder-30B-Instruct-UD-Q3_K_XL", "id": "Qwen3-Coder-30B-Instruct-UD-Q3_K_XL",
"name": "Qwen3 Coder 30B · 32k — main agent coder", "name": "Qwen3 Coder 30B · 32k — main agent coder (~30 t/s)",
"reasoning": false, "reasoning": false,
"input": ["text"], "input": ["text"],
"contextWindow": 32768, "contextWindow": 32768,
@@ -239,7 +239,7 @@
}, },
{ {
"id": "Qwen3-Coder-Next-UD-IQ3_XXS", "id": "Qwen3-Coder-Next-UD-IQ3_XXS",
"name": "Qwen3 Coder Next 80B · 128k — long sessions", "name": "Qwen3 Coder Next 80B · 128k — long sessions (~16 t/s)",
"reasoning": false, "reasoning": false,
"input": ["text"], "input": ["text"],
"contextWindow": 131072, "contextWindow": 131072,
@@ -248,7 +248,7 @@
}, },
{ {
"id": "Qwen3.6-35B-A3B-MTP-UD-IQ3_XXS", "id": "Qwen3.6-35B-A3B-MTP-UD-IQ3_XXS",
"name": "Qwen3.6 35B · 24k · vision — daily driver", "name": "Qwen3.6 35B · 24k · vision — daily driver (~36 t/s)",
"reasoning": false, "reasoning": false,
"input": ["text", "image"], "input": ["text", "image"],
"contextWindow": 24576, "contextWindow": 24576,
@@ -257,7 +257,7 @@
}, },
{ {
"id": "Qwen3.6-35B-A3B-Thinking", "id": "Qwen3.6-35B-A3B-Thinking",
"name": "Qwen3.6 35B Thinking · 24k · vision — hard problems", "name": "Qwen3.6 35B Thinking · 24k · vision — hard problems (~39 t/s)",
"reasoning": true, "reasoning": true,
"compat": { "thinkingFormat": "qwen-chat-template" }, "compat": { "thinkingFormat": "qwen-chat-template" },
"input": ["text", "image"], "input": ["text", "image"],
@@ -267,7 +267,7 @@
}, },
{ {
"id": "Qwen3.5-9B-UD-Q6_K_XL", "id": "Qwen3.5-9B-UD-Q6_K_XL",
"name": "Qwen3.5 9B · 32k · vision — quick tasks", "name": "Qwen3.5 9B · 32k · vision — quick tasks (~32 t/s)",
"reasoning": true, "reasoning": true,
"compat": { "thinkingFormat": "qwen-chat-template" }, "compat": { "thinkingFormat": "qwen-chat-template" },
"input": ["text", "image"], "input": ["text", "image"],
@@ -277,7 +277,7 @@
}, },
{ {
"id": "Qwen3.8-27B-UD-IQ3_XXS", "id": "Qwen3.8-27B-UD-IQ3_XXS",
"name": "Qwen3.8 27B · 24k · vision — hybrid reasoner", "name": "Qwen3.8 27B · 24k · vision — hybrid reasoner, quiet desktop only (~15 t/s)",
"reasoning": true, "reasoning": true,
"compat": { "thinkingFormat": "qwen-chat-template" }, "compat": { "thinkingFormat": "qwen-chat-template" },
"input": ["text", "image"], "input": ["text", "image"],
@@ -287,7 +287,7 @@
}, },
{ {
"id": "GLM-4.7-Flash-UD-Q4_K_XL", "id": "GLM-4.7-Flash-UD-Q4_K_XL",
"name": "GLM-4.7 Flash · 24k — quality coder", "name": "GLM-4.7 Flash · 24k — quality coder (~21 t/s)",
"reasoning": true, "reasoning": true,
"compat": { "thinkingFormat": "qwen-chat-template" }, "compat": { "thinkingFormat": "qwen-chat-template" },
"input": ["text"], "input": ["text"],
@@ -297,7 +297,7 @@
}, },
{ {
"id": "gpt-oss-20b", "id": "gpt-oss-20b",
"name": "gpt-oss 20B · 64k — fast reasoning + tools", "name": "gpt-oss 20B · 64k — fast reasoning + tools (~38 t/s)",
"reasoning": true, "reasoning": true,
"input": ["text"], "input": ["text"],
"contextWindow": 65536, "contextWindow": 65536,
@@ -306,7 +306,7 @@
}, },
{ {
"id": "gpt-oss-20b-low", "id": "gpt-oss-20b-low",
"name": "gpt-oss 20B low · 64k — snappy answers", "name": "gpt-oss 20B low · 64k — snappy answers (~37 t/s)",
"reasoning": true, "reasoning": true,
"input": ["text"], "input": ["text"],
"contextWindow": 65536, "contextWindow": 65536,
+30 -1
View File
@@ -4,6 +4,7 @@
"defaultThinkingLevel": "off", "defaultThinkingLevel": "off",
"theme": "catppuccin-mocha", "theme": "catppuccin-mocha",
"quietStartup": false, "quietStartup": false,
"collapseChangelog": true,
"defaultProjectTrust": "ask", "defaultProjectTrust": "ask",
"enableInstallTelemetry": false, "enableInstallTelemetry": false,
"enabledModels": [ "enabledModels": [
@@ -33,5 +34,33 @@
"npm" "npm"
], ],
"showHardwareCursor": true, "showHardwareCursor": true,
"lastChangelogVersion": "0.83.0" "piVim": {
"clipboardMirror": "yank",
"exCommand": {
"piDispatch": true,
"copyInputToClipboard": false
},
"modeColors": {
"insert": "success",
"normal": "accent",
"visual": "warning",
"ex": "bashMode"
},
"borderSync": {
"insert": "host",
"normal": "host",
"visual": "host",
"ex": "host"
},
"labelSync": {
"insert": "mode",
"normal": "mode",
"visual": "mode",
"ex": "mode"
}
},
"lastChangelogVersion": "0.83.0",
"packages": [
"npm:pi-vim"
]
} }
+64
View File
@@ -0,0 +1,64 @@
# WM HOST OVERLAY — full-file copy of common/.config/aichat/config.yaml with the default model
# flipped to the duskadiy client: wm is not on the home LAN, so the `local` endpoint
# (192.168.0.204) is unreachable here (kept below for VPN use). Stowed with --override so it
# shadows the common file on wm only. When the common config changes, mirror the change here.
# see https://github.com/sigoden/aichat/blob/main/config.example.yaml
keybindings: vi
editor: nvim
model: duskadiy:Qwen3-Coder-30B-Instruct-UD-Q3_K_XL
# Sessions: persist REPL sessions and keep more history before summarizing.
# The default compress_threshold (4000) summarizes far too early for 24k+ windows.
save_session: true
compress_threshold: 16000
# REPL prompts show live context usage (needs max_input_tokens, set per model below)
left_prompt: '{color.green}{?session {session}{?role /}}{role}{color.cyan}{?rag @{rag}}{color.reset}> '
right_prompt: '{color.purple}{?session {consume_tokens}/{max_input_tokens} }{color.reset}'
# NOTE: temperature/top_p are intentionally unset — the LAN server applies tuned
# per-model sampling via --jinja (e.g. GLM 0.6/0.95); a global value would clobber it.
# max_input_tokens = real ctx-size (from the router) minus output headroom.
clients:
# LAN access (fast; only reachable on the home network)
- type: openai-compatible
name: local
api_base: http://192.168.0.204:11343/v1
models: &lan_models
# Speeds are benched decode t/s (clean-night sweeps) — see fl/.config/llamacpp/README.md roster.
- name: Qwen3-Coder-30B-Instruct-UD-Q3_K_XL
max_input_tokens: 30000 # ctx 32768 · ~30 t/s — main agent coder
- name: Qwen3-Coder-Next-UD-IQ3_XXS
max_input_tokens: 128000 # ctx 131072 · ~16 t/s — long sessions (128k)
- name: Qwen3.6-35B-A3B-MTP-UD-IQ3_XXS
max_input_tokens: 22000 # ctx 24576 · ~36 t/s — daily driver
supports_vision: true
- name: Qwen3.6-35B-A3B-Thinking
max_input_tokens: 22000 # ctx 24576 · ~39 t/s — hard problems
supports_vision: true
- name: Qwen3.5-9B-UD-Q6_K_XL
max_input_tokens: 30000 # ctx 32768 · ~32 t/s — quick tasks
supports_vision: true
- name: Qwen3.8-27B-UD-IQ3_XXS
max_input_tokens: 22000 # ctx 24576 · ~15 t/s — hybrid reasoner, quiet desktop only
supports_vision: true
- name: gemma-4-26B-A4B-it-UD-IQ4_XS
max_input_tokens: 22000 # ctx 24576 · ~34 t/s — quality generalist, best vision
supports_vision: true
- name: gemma-4-E4B-it-UD-Q8_K_XL
max_input_tokens: 62000 # ctx 65536 · ~57 t/s — fast generalist, long docs
supports_vision: true
- name: GLM-4.7-Flash-UD-Q4_K_XL
max_input_tokens: 22000 # ctx 24576 · ~21 t/s — quality coder
- name: gpt-oss-20b
max_input_tokens: 62000 # ctx 65536 · ~38 t/s — fast reasoning + tools
- name: gpt-oss-20b-low
max_input_tokens: 62000 # ctx 65536 · ~37 t/s — snappy answers
# Remote access via duskadiy.com (reachable from anywhere; requires an API key —
# $DUSKADIY_API_KEY, from the gitignored conf.d/secrets.fish). The LAN endpoint above is keyless.
# Same server, same model ids — reuse the list above via a YAML anchor.
- type: openai-compatible
name: duskadiy
api_base: https://llm.duskadiy.com/api/v1
models: *lan_models