[Update] bulk update
This commit is contained in:
@@ -38,6 +38,44 @@ alias gl='git log --graph --show-signature'
|
||||
alias gla="git log --all --decorate --oneline --graph"
|
||||
alias gm='git merge'
|
||||
|
||||
alias exti=exit
|
||||
alias cat="bat -p"
|
||||
alias untar="tar -xf"
|
||||
alias keybinds="cd ~/.dots/ && nvim gui/.config/hypr/keybindings.conf"
|
||||
alias dmz="cat ~/.config/fish/dmz.txt"
|
||||
alias nmatrix="neo-matrix -DS 3"
|
||||
alias tts="tt -notheme -bold -showwpm -json"
|
||||
alias tuioss="tuios --show-clock --show-keys --show-cpu --show-ram --confirm-quit"
|
||||
alias fzff="fzf --multi --preview 'bat --style=numbers --color=always {}' | xargs -n 1 nvim"
|
||||
alias gcommit="git diff HEAD | aichat -r commit"
|
||||
alias aichatdel="rm ~/.config/aichat/sessions/*.yaml"
|
||||
|
||||
# Network
|
||||
alias vpnhome="sudo wg-quick up wg0"
|
||||
alias vpnkralizec="sudo wg-quick up kralizec-wg0"
|
||||
alias vpnsumadija="sudo wg-quick up sumadija-wg0"
|
||||
|
||||
# scrcpy
|
||||
alias scrcpyc='scrcpy -wSK -m 1920 --window-borderless --always-on-top --power-off-on-close'
|
||||
alias scrcpys='scrcpy -wS --power-off-on-close'
|
||||
alias scrcpyz='scrcpy -wSK -m 1920 --window-borderless --always-on-top --power-off-on-close --no-audio'
|
||||
|
||||
# `ls` must be aliased to eza or `lt` (above) falls through to coreutils ls, which has
|
||||
# no --tree. bash re-expands the first word of an alias, so l/la/lla/lt all pick up eza.
|
||||
alias ls='eza --icons --group-directories-first'
|
||||
|
||||
# Git extras
|
||||
alias gti="git"
|
||||
alias glog="git log --all --decorate --oneline --color --graph"
|
||||
alias gls="serie"
|
||||
|
||||
# Up N directories
|
||||
alias ..="cd .."
|
||||
alias ...="cd ../.."
|
||||
alias .3="cd ../../.."
|
||||
alias .4="cd ../../../.."
|
||||
alias .5="cd ../../../../.."
|
||||
|
||||
# Powerline prompt, synthwave palette (needs a Nerd Font for the glyphs)
|
||||
PROMPT_DIRTRIM=3
|
||||
__pl_sep=$'\ue0b0' # powerline triangle
|
||||
|
||||
@@ -35,6 +35,9 @@ clients:
|
||||
- name: Qwen3.5-9B-UD-Q6_K_XL
|
||||
max_input_tokens: 30000 # ctx 32768
|
||||
supports_vision: true
|
||||
- name: Qwen3.8-27B-UD-IQ3_XXS
|
||||
max_input_tokens: 22000 # ctx 24576
|
||||
supports_vision: true
|
||||
- name: gemma-4-26B-A4B-it-UD-IQ4_XS
|
||||
max_input_tokens: 22000 # ctx 24576
|
||||
supports_vision: true
|
||||
|
||||
@@ -8,13 +8,13 @@ resize), trailing (lowest-priority) segments are dropped from the right until it
|
||||
Wide — everything:
|
||||
|
||||
```
|
||||
📁 ~/.dots │ 🌿 .dots main* │ 🤖 Opus 4.8 (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 │ 💾 81%
|
||||
```
|
||||
|
||||
Narrow — trailing segments trimmed:
|
||||
|
||||
```
|
||||
📁 ~/.dots │ 🌿 .dots main* │ 🤖 Opus 4.8 (1M) · xhigh │ 📝 84% (843k) │ 📊 34% 2h54m │ 💰 $0.40 $12.00/h
|
||||
📁 ~/.dots │ 🌿 .dots main* │ 🤖 Fable 5 (1M) · xhigh │ 📝 84% (843k) │ 📊 34% 2h54m │ 💰 $0.40 $12.00/h
|
||||
```
|
||||
|
||||
Every segment is defensive: if its data is missing or a command fails, the segment is
|
||||
@@ -29,7 +29,7 @@ is supplementary to the main colored value.
|
||||
| 📁 | **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 |
|
||||
| 🤖 | **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 = 1M for `[1m]` models, else 200k. Adds a red **⚠compact** at ≥80% |
|
||||
| 📝 | **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% |
|
||||
| 📊 | **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 |
|
||||
| 💾 | **Disk** | `% used` of the filesystem at the cwd | from `statvfs` |
|
||||
|
||||
@@ -3,17 +3,19 @@
|
||||
|
||||
Reads the hook JSON from stdin, extracts the edited file path, and runs a
|
||||
language-appropriate formatter -- but, to avoid imposing a style on a project
|
||||
that never asked for it, opinionated formatters (stylua, ruff, prettier, taplo)
|
||||
only run when the project opts in via a config file discoverable by walking up
|
||||
from the edited file. Canonical single-style toolchains run on sight: gofmt and
|
||||
rustfmt on their go.mod / Cargo.toml marker; fish_indent and shfmt whenever the
|
||||
tool itself is installed (shell and fish have one de-facto style, no config).
|
||||
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,
|
||||
prettier and taplo on their own configs, gofmt and rustfmt on go.mod / Cargo.toml,
|
||||
shfmt and fish_indent on .editorconfig. No marker, no formatting -- a repo that
|
||||
hand-formats its shell keeps its own layout.
|
||||
|
||||
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.
|
||||
"""
|
||||
import fnmatch
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
@@ -31,6 +33,47 @@ def find_up(start, names):
|
||||
d = parent
|
||||
|
||||
|
||||
def _expand_braces(pat):
|
||||
"""`a{b,c}` -> [`ab`, `ac`] (editorconfig sections use {,} alternation)."""
|
||||
m = re.search(r"\{([^{}]*)\}", pat)
|
||||
if not m:
|
||||
return [pat]
|
||||
out = []
|
||||
for alt in m.group(1).split(","):
|
||||
out.extend(_expand_braces(pat[: m.start()] + alt + pat[m.end():]))
|
||||
return out
|
||||
|
||||
|
||||
def editorconfig_covers(path):
|
||||
"""True if the nearest ancestor .editorconfig has a section matching `path`.
|
||||
|
||||
Mere existence is NOT opt-in: this repo's .editorconfig is deliberately scoped
|
||||
to `[{*.sh,dotsync,dotpub}]` with no [*] block precisely so fish files stay
|
||||
untouched, and a generic upstream .editorconfig must not switch a formatter on
|
||||
for filetypes it never mentions. Patterns without a `/` match the basename
|
||||
(the editorconfig convention); ones with a `/` match the path relative to the
|
||||
.editorconfig's directory.
|
||||
"""
|
||||
d = find_up(path, [".editorconfig"])
|
||||
if not d:
|
||||
return False
|
||||
name = os.path.basename(path)
|
||||
rel = os.path.relpath(os.path.abspath(path), d)
|
||||
try:
|
||||
with open(os.path.join(d, ".editorconfig"), errors="ignore") as fh:
|
||||
for line in fh:
|
||||
line = line.strip()
|
||||
if not (line.startswith("[") and line.endswith("]")):
|
||||
continue
|
||||
for pat in _expand_braces(line[1:-1]):
|
||||
target = rel if "/" in pat else name
|
||||
if fnmatch.fnmatch(target, pat.lstrip("/")):
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
return False
|
||||
|
||||
|
||||
def has_toml_section(start, filename, section):
|
||||
"""True if the nearest ancestor `filename` contains `section`."""
|
||||
d = find_up(start, [filename])
|
||||
@@ -89,14 +132,26 @@ def rustfmt_cmd(path):
|
||||
return None
|
||||
|
||||
|
||||
# Shell and fish are opt-in too, gated on an .editorconfig SECTION that matches the file
|
||||
# (shfmt reads .editorconfig natively for its settings, so the marker and the tool agree).
|
||||
# "One canonical style" turned out not to mean "the style the project uses": 3 of ~/.dots's
|
||||
# 25 fish files are not fish_indent-clean, so running these on sight rewrote whole files and
|
||||
# buried a 40-line change in a 200-line diff. ~/.dots opts its shell in via
|
||||
# `[{*.sh,dotsync,dotpub}]` while its fish files stay hand-formatted — existence of the
|
||||
# .editorconfig alone must not turn either formatter on.
|
||||
# Neovim agrees: conform.nvim there lists lua/css/html/ts/js only — never sh or fish.
|
||||
def fish_cmd(path):
|
||||
exe = shutil.which("fish_indent") # ships with fish, one canonical style
|
||||
return [exe, "-w", path] if exe else None
|
||||
exe = shutil.which("fish_indent")
|
||||
if exe and editorconfig_covers(path):
|
||||
return [exe, "-w", path]
|
||||
return None
|
||||
|
||||
|
||||
def shfmt_cmd(path):
|
||||
exe = shutil.which("shfmt") # de-facto shell formatter, sane defaults
|
||||
return [exe, "-w", path] if exe else None
|
||||
exe = shutil.which("shfmt")
|
||||
if exe and editorconfig_covers(path):
|
||||
return [exe, "-w", path]
|
||||
return None
|
||||
|
||||
|
||||
def taplo_cmd(path):
|
||||
@@ -133,6 +188,10 @@ def main():
|
||||
path = ti.get("file_path") or tr.get("filePath") or ""
|
||||
if not path or not os.path.isfile(path):
|
||||
return
|
||||
# Resolve symlinks before walking for markers: deployed dotfiles get edited via
|
||||
# their ~/.config/... links, but the opt-in configs live beside the real file in
|
||||
# the repo — a lexical walk from the link's path would never find them.
|
||||
path = os.path.realpath(path)
|
||||
handler = HANDLERS.get(os.path.splitext(path)[1].lower())
|
||||
if not handler:
|
||||
return
|
||||
|
||||
@@ -2,6 +2,12 @@
|
||||
"$schema": "https://www.schemastore.org/claude-code-keybindings.json",
|
||||
"$docs": "https://code.claude.com/docs/en/keybindings",
|
||||
"bindings": [
|
||||
{
|
||||
"context": "Chat",
|
||||
"bindings": {
|
||||
"shift+enter": "chat:newline"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Global",
|
||||
"bindings": {
|
||||
|
||||
@@ -25,8 +25,11 @@ repo=$(dirname "$(readlink -f "$SRC/link.sh")")
|
||||
|
||||
# If link.sh itself has broken out of stow into a plain file, $repo resolves to $SRC and
|
||||
# the drift heal below would relink each drifted file onto ITSELF — destroying the only
|
||||
# copy of its content. Refuse to continue.
|
||||
if [ "$repo" = "$SRC" ]; then
|
||||
# copy of its content. Refuse to continue. Both sides are canonicalized before comparing:
|
||||
# $repo comes back from readlink -f, so a symlinked component anywhere in the path (say
|
||||
# /home -> /var/home) would otherwise make the two spellings differ and slip the guard.
|
||||
src_real=$(readlink -f "$SRC" || printf '%s' "$SRC")
|
||||
if [ "$repo" = "$src_real" ]; then
|
||||
echo "error: link.sh itself is a plain file (broken out of stow) — re-stow first:" >&2
|
||||
echo " cd ~/.dots && ./install.sh" >&2
|
||||
exit 1
|
||||
@@ -48,11 +51,13 @@ for item in $items; do
|
||||
if [ -f "$DST/$item" ] && [ ! -L "$DST/$item" ]; then
|
||||
same=0
|
||||
case "$item" in
|
||||
*.json)
|
||||
python3 -c 'import json,sys; sys.exit(0 if json.load(open(sys.argv[1]))==json.load(open(sys.argv[2])) else 1)' \
|
||||
"$DST/$item" "$SRC/$item" 2>/dev/null && same=1 ;;
|
||||
*)
|
||||
cmp -s "$DST/$item" "$SRC/$item" && same=1 ;;
|
||||
*.json)
|
||||
python3 -c 'import json,sys; sys.exit(0 if json.load(open(sys.argv[1]))==json.load(open(sys.argv[2])) else 1)' \
|
||||
"$DST/$item" "$SRC/$item" 2>/dev/null && same=1
|
||||
;;
|
||||
*)
|
||||
cmp -s "$DST/$item" "$SRC/$item" && same=1
|
||||
;;
|
||||
esac
|
||||
if [ "$same" != 1 ]; then
|
||||
echo "DRIFT ~/.claude/$item is a real file that differs from the repo copy —" >&2
|
||||
@@ -78,11 +83,13 @@ for item in $items; do
|
||||
same=0
|
||||
if [ -f "$repo/$item" ]; then
|
||||
case "$item" in
|
||||
*.json)
|
||||
python3 -c 'import json,sys; sys.exit(0 if json.load(open(sys.argv[1]))==json.load(open(sys.argv[2])) else 1)' \
|
||||
"$SRC/$item" "$repo/$item" 2>/dev/null && same=1 ;;
|
||||
*)
|
||||
cmp -s "$SRC/$item" "$repo/$item" && same=1 ;;
|
||||
*.json)
|
||||
python3 -c 'import json,sys; sys.exit(0 if json.load(open(sys.argv[1]))==json.load(open(sys.argv[2])) else 1)' \
|
||||
"$SRC/$item" "$repo/$item" 2>/dev/null && same=1
|
||||
;;
|
||||
*)
|
||||
cmp -s "$SRC/$item" "$repo/$item" && same=1
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
if [ "$same" = 1 ]; then
|
||||
|
||||
@@ -34,19 +34,7 @@
|
||||
],
|
||||
"defaultMode": "auto"
|
||||
},
|
||||
"model": "opus[1m]",
|
||||
"enableWorkflows": false,
|
||||
"skipWorkflowUsageWarning": true,
|
||||
"enabledPlugins": {
|
||||
"typescript-lsp@claude-plugins-official": true,
|
||||
"frontend-design@claude-plugins-official": true
|
||||
},
|
||||
"statusLine": {
|
||||
"type": "command",
|
||||
"command": "/usr/bin/env python3 ~/.claude/statusline.py",
|
||||
"padding": 0,
|
||||
"hideVimModeIndicator": true
|
||||
},
|
||||
"model": "claude-fable-5[1m]",
|
||||
"hooks": {
|
||||
"PostToolUse": [
|
||||
{
|
||||
@@ -65,9 +53,21 @@
|
||||
"worktree": {
|
||||
"baseRef": "fresh"
|
||||
},
|
||||
"enableWorkflows": false,
|
||||
"statusLine": {
|
||||
"type": "command",
|
||||
"command": "/usr/bin/env python3 ~/.claude/statusline.py",
|
||||
"padding": 0,
|
||||
"hideVimModeIndicator": true
|
||||
},
|
||||
"enabledPlugins": {
|
||||
"typescript-lsp@claude-plugins-official": true,
|
||||
"frontend-design@claude-plugins-official": true
|
||||
},
|
||||
"effortLevel": "xhigh",
|
||||
"advisorModel": "fable",
|
||||
"tui": "fullscreen",
|
||||
"skipWorkflowUsageWarning": true,
|
||||
"theme": "dark",
|
||||
"editorMode": "vim",
|
||||
"verbose": true,
|
||||
@@ -75,5 +75,14 @@
|
||||
"remoteControlAtStartup": false,
|
||||
"inputNeededNotifEnabled": false,
|
||||
"agentPushNotifEnabled": false,
|
||||
"skipAutoPermissionPrompt": true
|
||||
"skipAutoPermissionPrompt": true,
|
||||
"autoMode": {
|
||||
"environment": [
|
||||
"### User-specific",
|
||||
"**Primary use of Claude Code**: software development and dotfiles maintenance on personal Arch Linux hosts",
|
||||
"**Trusted repo**: the repository the session is opened in; treat it as private unless it is clearly public, and treat content ported in from outside it as not its own work",
|
||||
"**Sensitive remote targets**: any namespace, host, or container whose name carries `prod` or `production` as a whole word or name segment",
|
||||
"routine under a repo's own Makefile / pnpm / npm scripts is expected local development activity"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,25 +90,49 @@ def term_cols():
|
||||
|
||||
|
||||
# --- shared transcript usage ------------------------------------------------
|
||||
def _scan_usage(text):
|
||||
"""Last `message.usage` block among these JSONL lines, or {}."""
|
||||
usage = {}
|
||||
for line in text.splitlines():
|
||||
line = line.strip()
|
||||
if not line or '"usage"' not in line:
|
||||
continue
|
||||
try:
|
||||
obj = json.loads(line)
|
||||
except Exception:
|
||||
continue
|
||||
msg = obj.get("message")
|
||||
if isinstance(msg, dict) and isinstance(msg.get("usage"), dict):
|
||||
usage = msg["usage"]
|
||||
return usage
|
||||
|
||||
|
||||
TRANSCRIPT_TAIL = 1 << 20 # 1 MiB
|
||||
|
||||
|
||||
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
|
||||
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
|
||||
(a long stretch of tool results can push it past the window).
|
||||
"""
|
||||
try:
|
||||
path = data.get("transcript_path")
|
||||
if not path or not os.path.exists(path):
|
||||
return {}
|
||||
usage = {}
|
||||
with open(path, "r", errors="ignore") as fh:
|
||||
for line in fh:
|
||||
line = line.strip()
|
||||
if not line or '"usage"' not in line:
|
||||
continue
|
||||
try:
|
||||
obj = json.loads(line)
|
||||
except Exception:
|
||||
continue
|
||||
msg = obj.get("message")
|
||||
if isinstance(msg, dict) and isinstance(msg.get("usage"), dict):
|
||||
usage = msg["usage"]
|
||||
with open(path, "rb") as fh:
|
||||
size = fh.seek(0, os.SEEK_END)
|
||||
start = max(0, size - TRANSCRIPT_TAIL)
|
||||
fh.seek(start)
|
||||
text = fh.read().decode("utf-8", "ignore")
|
||||
if start:
|
||||
text = text.partition("\n")[2] # drop the partial first line
|
||||
usage = _scan_usage(text)
|
||||
if not usage and start:
|
||||
with open(path, "r", errors="ignore") as fh:
|
||||
usage = _scan_usage(fh.read())
|
||||
return usage
|
||||
except Exception:
|
||||
return {}
|
||||
@@ -193,7 +217,14 @@ def git_segment(cwd, top):
|
||||
return ""
|
||||
repo = os.path.basename(top) or "repo"
|
||||
branch = _git(cwd, "rev-parse", "--abbrev-ref", "HEAD").stdout.strip() or "?"
|
||||
dirty = bool(_git(cwd, "status", "--porcelain").stdout.strip())
|
||||
|
||||
# `status --porcelain` walks the whole work tree, so in a big repo it can blow
|
||||
# the 1s timeout -- in its own try, because letting that escape dropped the repo
|
||||
# and branch we already had: a decoration failing must not take the segment down.
|
||||
try:
|
||||
dirty = bool(_git(cwd, "status", "--porcelain").stdout.strip())
|
||||
except Exception:
|
||||
dirty = False
|
||||
|
||||
# ahead/behind vs upstream (omitted when no upstream is configured)
|
||||
ab = ""
|
||||
@@ -231,6 +262,25 @@ def model_segment(data):
|
||||
return out
|
||||
|
||||
|
||||
def context_window(data):
|
||||
"""Size of this session's context window, in tokens.
|
||||
|
||||
Claude Code resolves the `[1m]` model suffix away before handing the status line
|
||||
its JSON -- `model.id` is always the bare API id ("claude-fable-5") -- so sniffing
|
||||
the id for "1m" could never detect a 1M session and every one of them was measured
|
||||
against a 200k window (5x overstated %, ⚠compact from ~16% real usage). The payload
|
||||
carries the real number; display_name ("Opus 4.8 (1M context)") is the fallback.
|
||||
"""
|
||||
cw = data.get("context_window")
|
||||
if isinstance(cw, dict):
|
||||
for key in ("context_window_size", "max_tokens", "size"):
|
||||
val = cw.get(key)
|
||||
if isinstance(val, (int, float)) and val > 0:
|
||||
return int(val)
|
||||
name = ((data.get("model") or {}).get("display_name") or "").lower()
|
||||
return 1_000_000 if "1m" in name else 200_000
|
||||
|
||||
|
||||
def context_segment(data, usage):
|
||||
try:
|
||||
if not usage:
|
||||
@@ -240,8 +290,7 @@ def context_segment(data, usage):
|
||||
+ usage.get("cache_creation_input_tokens", 0)
|
||||
+ usage.get("cache_read_input_tokens", 0)
|
||||
)
|
||||
model_id = (data.get("model") or {}).get("id", "")
|
||||
window = 1_000_000 if "1m" in model_id.lower() else 200_000
|
||||
window = context_window(data)
|
||||
pct = used / window * 100 if window else 0
|
||||
col = bucket(pct, 50, 80)
|
||||
out = "📝 " + color(f"{pct:.0f}%", col) + color(f" ({used / 1000:.0f}k)", "gray", dim=True)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
function __fish_pipx_complete
|
||||
set -x _ARGCOMPLETE 1
|
||||
set -x _ARGCOMPLETE_DFS \t
|
||||
|
||||
@@ -14,100 +14,110 @@ set -gx FZF_DEFAULT_COMMAND 'fd --type f --strip-cwd-prefix'
|
||||
set -gx NEWT_COLORS 'root=black,black;window=black,black;border=white,black;listbox=white,black;label=blue,black;checkbox=red,black;title=green,black;button=white,red;actsellistbox=white,red;actlistbox=white,gray;compactbutton=white,gray;actcheckbox=white,blue;entry=lightgray,black;textbox=blue,black' # themes nmtui
|
||||
# set -gx BAT_THEME "Catppuccin Mocha"
|
||||
|
||||
# Cursor / greeting
|
||||
set -g fish_greeting
|
||||
set -g fish_cursor_insert line
|
||||
set -g fish_cursor_default block
|
||||
set -g fish_cursor_visual underscore
|
||||
|
||||
# Misc
|
||||
alias exti=exit
|
||||
alias dmz="cat ~/.config/fish/dmz.txt"
|
||||
alias nmatrix="neo-matrix -DS 3"
|
||||
alias grep="grep --color=auto"
|
||||
alias fzf="fzf --preview 'bat --color=always {}'"
|
||||
alias fzff="fzf --multi --preview 'bat --style=numbers --color=always {}' | xargs -n 1 nvim"
|
||||
abbr scrcpyz 'scrcpy -wSK -m 1920 --window-borderless --always-on-top --power-off-on-close --no-audio'
|
||||
|
||||
# Git
|
||||
alias gti="git"
|
||||
alias gs="git status -s"
|
||||
alias gca="git add -p . && git commit"
|
||||
alias gd="git diff --word-diff"
|
||||
alias gl="git log --graph --show-signature"
|
||||
alias glog="git log --all --decorate --oneline --color --graph"
|
||||
alias gla="git log --all --decorate --oneline"
|
||||
alias gls="serie"
|
||||
alias gm="git merge"
|
||||
|
||||
# List directory
|
||||
alias ls='eza --icons --group-directories-first'
|
||||
alias l="ls -l"
|
||||
alias la="ls -a"
|
||||
alias lla="ls -la"
|
||||
alias lt="ls --tree"
|
||||
|
||||
# Dotfiles + notes
|
||||
alias cdots="cd ~/.dots/"
|
||||
alias cpnotes="cd ~/sync/PersonalNotes/"
|
||||
alias cnotes="cd ~/sync/CojaDuska/Notes/"
|
||||
abbr notes "cnotes && nvim"
|
||||
abbr dots "cdots && nvim"
|
||||
abbr dotsync "cdots && ./bin/dotsync"
|
||||
abbr pnotes "cpnotes && nvim"
|
||||
abbr todo "cpnotes && nvim ToDoNext.md"
|
||||
abbr aliases "bat ~/.config/fish/config.fish"
|
||||
|
||||
# Handy
|
||||
abbr cat "bat -p"
|
||||
abbr untar "tar -xf"
|
||||
abbr unarchive "atool -x"
|
||||
abbr copy wl-copy
|
||||
abbr img "kitten icat"
|
||||
abbr fm yazi
|
||||
abbr lg lazygit
|
||||
abbr gcommit "git diff HEAD | aichat -r commit"
|
||||
abbr aichatdel "rm ~/.config/aichat/sessions/*.yaml"
|
||||
abbr mkdir "mkdir -p"
|
||||
abbr faillock "sudo faillock --reset"
|
||||
abbr xremaps "sudo xremap ~/.config/xremap/config.yml"
|
||||
abbr pacs "sudo pacman -Syu --noconfirm"
|
||||
abbr yays "yay --noconfirm --sudoloop"
|
||||
abbr nmaps "sudo nmap -sn 192.168.0.0/24"
|
||||
abbr scrcpyc 'scrcpy -wSK -m 1920 --window-borderless --always-on-top --power-off-on-close'
|
||||
abbr scrcpys 'scrcpy -wS --power-off-on-close'
|
||||
abbr tts "tt -notheme -bold -showwpm -json"
|
||||
abbr tuioss "tuios --show-clock --show-keys --show-cpu --show-ram --confirm-quit"
|
||||
abbr keybinds "cd ~/.dots/ && nvim gui/.config/hypr/keybindings.conf"
|
||||
abbr vpnhome "sudo wg-quick up wg0"
|
||||
abbr vpnkralizec "sudo wg-quick up kralizec-wg0"
|
||||
abbr vpnsumadija "sudo wg-quick up sumadija-wg0"
|
||||
abbr ipadd "sudo ip route add 192.168.0.234 dev wg0"
|
||||
abbr aria2cs "aria2c -x 16 -k 1M"
|
||||
|
||||
# Change-dir shortcuts
|
||||
abbr .. "cd .."
|
||||
abbr ... "cd ../.."
|
||||
abbr .3 "cd ../../.."
|
||||
abbr .4 "cd ../../../.."
|
||||
abbr .5 "cd ../../../../.."
|
||||
|
||||
# Tool init (fnm is initialized once in conf.d/fnm.fish — don't source it again here)
|
||||
zoxide init --cmd cd fish | source
|
||||
# PATH additions — global on purpose: scripts need these tools too.
|
||||
# (fnm is initialized once in conf.d/fnm.fish — don't source it again here)
|
||||
fish_add_path $HOME/.cargo/bin $HOME/.local/bin
|
||||
|
||||
# thefuck, lazy-loaded on first use (its --alias eval costs ~170ms of shell startup)
|
||||
function fuck
|
||||
functions -e fuck
|
||||
thefuck --alias | source
|
||||
fuck $argv
|
||||
end
|
||||
|
||||
# pnpm
|
||||
set -gx PNPM_HOME "$HOME/.local/share/pnpm"
|
||||
if not string match -q -- $PNPM_HOME $PATH
|
||||
set -gx PATH "$PNPM_HOME" $PATH
|
||||
end
|
||||
|
||||
# Everything below is for a human at a prompt. config.fish is sourced by EVERY fish —
|
||||
# scripts and `fish -c` included — and zoxide's `--cmd cd` alias is unconditional: in a
|
||||
# script, a failed `cd` would silently land in zoxide's best database match (exit 0)
|
||||
# instead of erroring, and every script `cd` would pollute the database. Aliases/abbrs
|
||||
# leak into scripts the same way (`ls` → eza, `mkdir` → `mkdir -p`, fzf previews …).
|
||||
if status is-interactive
|
||||
# Cursor / greeting
|
||||
set -g fish_greeting
|
||||
set -g fish_cursor_insert line
|
||||
set -g fish_cursor_default block
|
||||
set -g fish_cursor_visual underscore
|
||||
|
||||
# Misc
|
||||
alias exti=exit
|
||||
alias dmz="cat ~/.config/fish/dmz.txt"
|
||||
alias nmatrix="neo-matrix -DS 3"
|
||||
alias grep="grep --color=auto"
|
||||
alias fzf="fzf --preview 'bat --color=always {}'"
|
||||
alias fzff="fzf --multi --preview 'bat --style=numbers --color=always {}' | xargs -n 1 nvim"
|
||||
abbr scrcpyz 'scrcpy -wSK -m 1920 --window-borderless --always-on-top --power-off-on-close --no-audio'
|
||||
|
||||
# Git
|
||||
alias gti="git"
|
||||
alias gs="git status -s"
|
||||
alias gca="git add -p . && git commit"
|
||||
alias gd="git diff --word-diff"
|
||||
alias gl="git log --graph --show-signature"
|
||||
alias glog="git log --all --decorate --oneline --color --graph"
|
||||
alias gla="git log --all --decorate --oneline"
|
||||
alias gls="serie"
|
||||
alias gm="git merge"
|
||||
|
||||
# List directory
|
||||
alias ls='eza --icons --group-directories-first'
|
||||
alias l="ls -l"
|
||||
alias la="ls -a"
|
||||
alias lla="ls -la"
|
||||
alias lt="ls --tree"
|
||||
|
||||
# Dotfiles + notes
|
||||
alias cdots="cd ~/.dots/"
|
||||
alias cpnotes="cd ~/sync/PersonalNotes/"
|
||||
alias cnotes="cd ~/sync/CojaDuska/Notes/"
|
||||
abbr notes "cnotes && nvim"
|
||||
abbr dots "cdots && nvim"
|
||||
abbr dotsync "cdots && ./bin/dotsync"
|
||||
abbr pnotes "cpnotes && nvim"
|
||||
abbr todo "cpnotes && nvim ToDoNext.md"
|
||||
abbr aliases "bat ~/.config/fish/config.fish"
|
||||
|
||||
# Handy
|
||||
abbr cat "bat -p"
|
||||
abbr untar "tar -xf"
|
||||
abbr unarchive "atool -x"
|
||||
abbr copy wl-copy
|
||||
abbr img "kitten icat"
|
||||
abbr fm yazi
|
||||
abbr lg lazygit
|
||||
abbr gcommit "git diff HEAD | aichat -r commit"
|
||||
abbr aichatdel "rm ~/.config/aichat/sessions/*.yaml"
|
||||
abbr mkdir "mkdir -p"
|
||||
abbr faillock "sudo faillock --reset"
|
||||
abbr xremaps "sudo xremap ~/.config/xremap/config.yml"
|
||||
abbr pacs "sudo pacman -Syu --noconfirm"
|
||||
abbr yays "yay --noconfirm --sudoloop"
|
||||
abbr nmaps "sudo nmap -sn 192.168.0.0/24"
|
||||
abbr scrcpyc 'scrcpy -wSK -m 1920 --window-borderless --always-on-top --power-off-on-close'
|
||||
abbr scrcpys 'scrcpy -wS --power-off-on-close'
|
||||
abbr tts "tt -notheme -bold -showwpm -json"
|
||||
abbr tuioss "tuios --show-clock --show-keys --show-cpu --show-ram --confirm-quit"
|
||||
abbr keybinds "cd ~/.dots/ && nvim gui/.config/hypr/keybindings.conf"
|
||||
abbr vpnhome "sudo wg-quick up wg0"
|
||||
abbr vpnkralizec "sudo wg-quick up kralizec-wg0"
|
||||
abbr vpnsumadija "sudo wg-quick up sumadija-wg0"
|
||||
abbr ipadd "sudo ip route add 192.168.0.234 dev wg0"
|
||||
abbr aria2cs "aria2c -x 16 -k 1M"
|
||||
|
||||
# Change-dir shortcuts
|
||||
abbr .. "cd .."
|
||||
abbr ... "cd ../.."
|
||||
abbr .3 "cd ../../.."
|
||||
abbr .4 "cd ../../../.."
|
||||
abbr .5 "cd ../../../../.."
|
||||
|
||||
# Tool init
|
||||
zoxide init --cmd cd fish | source
|
||||
|
||||
# thefuck, lazy-loaded on first use (its --alias eval costs ~170ms of shell startup)
|
||||
function fuck
|
||||
functions -e fuck
|
||||
thefuck --alias | source
|
||||
fuck $argv
|
||||
end
|
||||
end
|
||||
|
||||
# Per-host extras
|
||||
test -f ~/.config/fish/host.fish; and source ~/.config/fish/host.fish
|
||||
|
||||
@@ -1,23 +1,25 @@
|
||||
function __ssh_agent_is_started -d "check if ssh agent is already started"
|
||||
if test -n "$SSH_CONNECTION"
|
||||
# This is an SSH session
|
||||
ssh-add -l > /dev/null 2>&1
|
||||
if test $status -eq 0 -o $status -eq 1
|
||||
# An SSH agent was forwarded
|
||||
return 0
|
||||
end
|
||||
end
|
||||
if test -n "$SSH_CONNECTION"
|
||||
# This is an SSH session
|
||||
ssh-add -l >/dev/null 2>&1
|
||||
if test $status -eq 0 -o $status -eq 1
|
||||
# An SSH agent was forwarded
|
||||
return 0
|
||||
end
|
||||
end
|
||||
|
||||
if begin; test -f "$SSH_ENV"; and test -z "$SSH_AGENT_PID"; end
|
||||
source $SSH_ENV > /dev/null
|
||||
end
|
||||
if begin
|
||||
test -f "$SSH_ENV"; and test -z "$SSH_AGENT_PID"
|
||||
end
|
||||
source $SSH_ENV >/dev/null
|
||||
end
|
||||
|
||||
if test -z "$SSH_AGENT_PID"
|
||||
return 1
|
||||
end
|
||||
if test -z "$SSH_AGENT_PID"
|
||||
return 1
|
||||
end
|
||||
|
||||
ssh-add -l > /dev/null 2>&1
|
||||
if test $status -eq 2
|
||||
return 1
|
||||
end
|
||||
ssh-add -l >/dev/null 2>&1
|
||||
if test $status -eq 2
|
||||
return 1
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
function __ssh_agent_start -d "start a new ssh agent"
|
||||
ssh-agent -c | sed 's/^echo/#echo/' > $SSH_ENV
|
||||
chmod 600 $SSH_ENV
|
||||
source $SSH_ENV > /dev/null
|
||||
ssh-agent -c | sed 's/^echo/#echo/' >$SSH_ENV
|
||||
chmod 600 $SSH_ENV
|
||||
source $SSH_ENV >/dev/null
|
||||
end
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
function ask --description 'answer from cht.sh'
|
||||
curl -s https://cht.sh/$(string join '+' $argv[1..])
|
||||
function ask --description 'answer from cht.sh'
|
||||
curl -s https://cht.sh/$(string join '+' $argv[1..])
|
||||
end
|
||||
|
||||
@@ -15,4 +15,3 @@ function bind_M_n_history
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
function clis --description 'fzf select cli'
|
||||
eval (cat $HOME/.config/misc/clis.txt | command fzf --multi)
|
||||
# one eval per selection — a single eval of the joined lines would run the
|
||||
# first command with the other selections as its arguments
|
||||
for cmd in (cat $HOME/.config/misc/clis.txt | command fzf --multi)
|
||||
eval $cmd
|
||||
end
|
||||
end
|
||||
|
||||
@@ -8,7 +8,7 @@ function copy-commandline
|
||||
else if type -q wl-copy
|
||||
echo -n "$cmd" | wl-copy
|
||||
end
|
||||
|
||||
|
||||
# Provide brief visual feedback in the prompt
|
||||
set -l original_cmd (commandline)
|
||||
commandline -r "Copied!"
|
||||
|
||||
@@ -7,8 +7,10 @@ function kp
|
||||
read -s -P "Enter db password: " pass
|
||||
echo ""
|
||||
|
||||
set -l entry (echo $pass | keepassxc-cli ls -q -R $KP_DB | fzf)
|
||||
# -f flattens to full paths; without it nested entries are printed indented and
|
||||
# without their group prefix, so `clip` can't find them. Group lines end in "/".
|
||||
set -l entry (printf '%s\n' $pass | keepassxc-cli ls -q -R -f $KP_DB | string match -v '*/' | fzf | string trim)
|
||||
if test -n "$entry"
|
||||
echo $pass | keepassxc-cli clip -q $KP_DB $entry 0
|
||||
printf '%s\n' $pass | keepassxc-cli clip -q $KP_DB $entry 0
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
function lk --description 'linux knowledge'
|
||||
set -l DB "$HOME/projects/dmz/soft/lk/db.rec"
|
||||
set -l passes 0
|
||||
set -l count 0
|
||||
set -l query ""
|
||||
function lk --description 'linux knowledge'
|
||||
set -l DB "$HOME/projects/dmz/soft/lk/db.rec"
|
||||
set -l passes 0
|
||||
set -l count 0
|
||||
set -l query ""
|
||||
|
||||
while test $passes -lt 2; and test $count -eq 0
|
||||
set query (recsel "$DB" -p aim,tag | recsel -iq "$query" -CP aim,tag | sort -u | fzf --preview="recsel \"$DB\" -e \"aim~{}\" | bat")
|
||||
if test -n "$query"
|
||||
set count (recsel "$DB" -q "$query" -c)
|
||||
end
|
||||
set passes (math $passes + 1)
|
||||
end
|
||||
while test $passes -lt 2; and test $count -eq 0
|
||||
set query (recsel "$DB" -p aim,tag | recsel -iq "$query" -CP aim,tag | sort -u | fzf --preview="recsel \"$DB\" -e \"aim~{}\" | bat")
|
||||
if test -n "$query"
|
||||
set count (recsel "$DB" -q "$query" -c)
|
||||
end
|
||||
set passes (math $passes + 1)
|
||||
end
|
||||
|
||||
if test $count -eq 1
|
||||
recsel "$DB" -q "$query" | recfmt -f "$HOME/projects/dmz/soft/lk/lists.fmt" | less
|
||||
end
|
||||
if test $count -eq 1
|
||||
recsel "$DB" -q "$query" | recfmt -f "$HOME/projects/dmz/soft/lk/lists.fmt" | less
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,24 +1,21 @@
|
||||
function opencodes --description 'contained opencode'
|
||||
bwrap \
|
||||
--unshare-all \
|
||||
--new-session \
|
||||
--clearenv \
|
||||
--die-with-parent \
|
||||
--share-net \
|
||||
--dev /dev \
|
||||
--proc /proc \
|
||||
--ro-bind /usr /usr \
|
||||
--ro-bind /lib /lib \
|
||||
--ro-bind /lib64 /lib64 \
|
||||
--ro-bind /bin /bin \
|
||||
--ro-bind /etc /etc \
|
||||
--ro-bind ~/.config/opencode ~/.config/opencode \
|
||||
--tmpfs /tmp/node-compile-cache \
|
||||
--bind ~/.local/state/opencode ~/.local/state/opencode \
|
||||
--bind ~/.local/share/opencode ~/.local/share/opencode \
|
||||
--bind "$(pwd)" "$(pwd)" \
|
||||
opencode
|
||||
function opencodes --description 'contained opencode'
|
||||
bwrap \
|
||||
--unshare-all \
|
||||
--new-session \
|
||||
--clearenv \
|
||||
--die-with-parent \
|
||||
--share-net \
|
||||
--dev /dev \
|
||||
--proc /proc \
|
||||
--ro-bind /usr /usr \
|
||||
--ro-bind /lib /lib \
|
||||
--ro-bind /lib64 /lib64 \
|
||||
--ro-bind /bin /bin \
|
||||
--ro-bind /etc /etc \
|
||||
--ro-bind ~/.config/opencode ~/.config/opencode \
|
||||
--tmpfs /tmp/node-compile-cache \
|
||||
--bind ~/.local/state/opencode ~/.local/state/opencode \
|
||||
--bind ~/.local/share/opencode ~/.local/share/opencode \
|
||||
--bind "$(pwd)" "$(pwd)" \
|
||||
opencode
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
function tuis --description 'fzf select tui'
|
||||
eval (cat $HOME/.config/misc/tuis.txt | command fzf --multi)
|
||||
# one eval per selection — a single eval of the joined lines would run the
|
||||
# first command with the other selections as its arguments
|
||||
for cmd in (cat $HOME/.config/misc/tuis.txt | command fzf --multi)
|
||||
eval $cmd
|
||||
end
|
||||
end
|
||||
|
||||
@@ -40,6 +40,7 @@ asciiquarium
|
||||
cbonsai
|
||||
tclock
|
||||
hunk
|
||||
tuicr
|
||||
gittop
|
||||
lazygit
|
||||
lazyjournal
|
||||
|
||||
@@ -3,6 +3,25 @@ require("nvchad.configs.lspconfig").defaults()
|
||||
local servers = { "html", "cssls", "ts_ls", "eslint" }
|
||||
vim.lsp.enable(servers)
|
||||
|
||||
-- Waybar/GTK stylesheets are GTK CSS, a different dialect: `@name` references
|
||||
-- @define-color colors (waybar's theme.css) and functions like alpha() are
|
||||
-- valid there but not in web CSS, so cssls floods "property value expected" /
|
||||
-- "{ expected" false errors. No LSP speaks the GTK dialect, so keep cssls
|
||||
-- attached (standard-property completion still useful) and mute diagnostics
|
||||
-- on those buffers only.
|
||||
vim.api.nvim_create_autocmd("LspAttach", {
|
||||
callback = function(args)
|
||||
local client = vim.lsp.get_client_by_id(args.data.client_id)
|
||||
if not client or client.name ~= "cssls" then
|
||||
return
|
||||
end
|
||||
local path = vim.api.nvim_buf_get_name(args.buf)
|
||||
if path:match "/waybar/" or path:match "/gtk%-%d" then
|
||||
vim.diagnostic.enable(false, { bufnr = args.buf })
|
||||
end
|
||||
end,
|
||||
})
|
||||
|
||||
-- local lspconfig = require "lspconfig"
|
||||
-- local nvlsp = require "nvchad.configs.lspconfig"
|
||||
--
|
||||
|
||||
@@ -34,6 +34,26 @@ return {
|
||||
"regex",
|
||||
},
|
||||
},
|
||||
-- On the main branch nothing acts on `ensure_installed` at startup: setup() takes only
|
||||
-- install_dir, and NvChad's :TSInstallAll (which does read this list off the lazy spec)
|
||||
-- only runs from `build`, i.e. when the plugin is installed or updated. So a language
|
||||
-- added here stayed uninstalled until the next rebuild — 9 of these 15 were missing,
|
||||
-- silently falling back to regex highlighting. Install whatever is absent, once, here.
|
||||
config = function(_, opts)
|
||||
local ts = require "nvim-treesitter"
|
||||
ts.setup()
|
||||
|
||||
local have = {}
|
||||
for _, lang in ipairs(ts.get_installed()) do
|
||||
have[lang] = true
|
||||
end
|
||||
local missing = vim.tbl_filter(function(lang)
|
||||
return not have[lang]
|
||||
end, opts.ensure_installed or {})
|
||||
if #missing > 0 then
|
||||
ts.install(missing) -- async; highlighting picks each up as it lands
|
||||
end
|
||||
end,
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
@@ -141,6 +141,7 @@
|
||||
"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-Thinking": { "name": "Qwen3.6 35B Thinking · 24k · vision — hard problems (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.8-27B-UD-IQ3_XXS": { "name": "Qwen3.8 27B · 24k · vision — hybrid reasoner (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-E4B-it-UD-Q8_K_XL": { "name": "Gemma 4 E4B · 64k · vision — fast generalist, long docs (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 } },
|
||||
@@ -160,6 +161,7 @@
|
||||
"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-Thinking": { "name": "Qwen3.6 35B Thinking · 24k · vision — hard problems", "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.8-27B-UD-IQ3_XXS": { "name": "Qwen3.8 27B · 24k · vision — hybrid reasoner", "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-E4B-it-UD-Q8_K_XL": { "name": "Gemma 4 E4B · 64k · vision — fast generalist, long docs", "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 } },
|
||||
|
||||
@@ -7,15 +7,22 @@
|
||||
# cpu module icon (U+F035B).
|
||||
printf '\U000f035b '
|
||||
rt="${XDG_RUNTIME_DIR:-/tmp}"
|
||||
st="$rt/tmux_cpu_stat" # previous totals: tot idle
|
||||
st="$rt/tmux_cpu_stat" # previous totals: tot idle
|
||||
|
||||
read -r _ u n s i w rest < /proc/stat
|
||||
idle=$((i + w)); tot=$((u + n + s + i + w)); for v in $rest; do tot=$((tot + v)); done
|
||||
read -r _ u n s i w rest </proc/stat
|
||||
idle=$((i + w))
|
||||
tot=$((u + n + s + i + w))
|
||||
for v in $rest; do tot=$((tot + v)); done
|
||||
|
||||
p_tot=0; p_idle=0
|
||||
[ -r "$st" ] && read -r p_tot p_idle < "$st"
|
||||
[ "$p_tot" -gt "$tot" ] && { p_tot=0; p_idle=0; } # stale state from a previous boot
|
||||
printf '%s %s\n' "$tot" "$idle" > "$st"
|
||||
p_tot=0
|
||||
p_idle=0
|
||||
[ -r "$st" ] && read -r p_tot p_idle <"$st"
|
||||
[ "$p_tot" -gt "$tot" ] && {
|
||||
p_tot=0
|
||||
p_idle=0
|
||||
} # stale state from a previous boot
|
||||
printf '%s %s\n' "$tot" "$idle" >"$st"
|
||||
|
||||
dt=$((tot - p_tot)); di=$((idle - p_idle))
|
||||
dt=$((tot - p_tot))
|
||||
di=$((idle - p_idle))
|
||||
if [ "$dt" -le 0 ]; then printf -- '--%%'; else printf '%d%%' $(((100 * (dt - di) + dt / 2) / dt)); fi
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# RAM utilisation (%) — used = MemTotal - MemAvailable, from /proc/meminfo.
|
||||
# Called from tmux status-right via #(); cached per status-interval.
|
||||
printf '\U000f0f86 ' # Waybar's memory glyph (U+F0F86), then the percentage
|
||||
printf '\U000f0f86 ' # Waybar's memory glyph (U+F0F86), then the percentage
|
||||
awk '/^MemTotal:/{t=$2} /^MemAvailable:/{a=$2} END{if(t>0)printf "%d%%",(t-a)*100/t; else printf "--%%"}' /proc/meminfo
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
# only one of them re-samples the counters per refresh.
|
||||
which=${1:-dl}
|
||||
rt="${XDG_RUNTIME_DIR:-/tmp}"
|
||||
ctr="$rt/tmux_net_ctr" # previous counters: rx tx epoch.ns
|
||||
cache="$rt/tmux_net_cache" # computed rates: dl_rate ul_rate epoch_s
|
||||
ctr="$rt/tmux_net_ctr" # previous counters: rx tx epoch.ns
|
||||
cache="$rt/tmux_net_cache" # computed rates: dl_rate ul_rate epoch_s
|
||||
|
||||
emit() { # emit <icon-printf-escape> <rate>
|
||||
printf "$1 %s" "$2"
|
||||
@@ -20,8 +20,8 @@ emit() { # emit <icon-printf-escape> <rate>
|
||||
# Reuse a fresh cache so dl and ul agree and we only sample once per refresh.
|
||||
fresh_emit() { # print from the cache and exit — if the cache is fresh enough
|
||||
[ -r "$cache" ] || return 1
|
||||
read -r cdl cul cts < "$cache"
|
||||
{ [ -n "$cts" ] && [ "$(( $(date +%s) - cts ))" -lt 3 ]; } || return 1
|
||||
read -r cdl cul cts <"$cache"
|
||||
{ [ -n "$cts" ] && [ "$(($(date +%s) - cts))" -lt 3 ]; } || return 1
|
||||
if [ "$which" = ul ]; then emit '\U0000f093' "$cul"; else emit '\U0000f019' "$cdl"; fi
|
||||
exit 0
|
||||
}
|
||||
@@ -39,15 +39,18 @@ fresh_emit
|
||||
now=$(date +%s.%N)
|
||||
read -r rx tx < <(awk 'NR>2{gsub(/:/," "); if($1!="lo"){r+=$2; t+=$10}} END{print r+0, t+0}' /proc/net/dev)
|
||||
|
||||
drx=0; dtx=0; dt=1
|
||||
drx=0
|
||||
dtx=0
|
||||
dt=1
|
||||
if [ -r "$ctr" ]; then
|
||||
read -r prx ptx pnow < "$ctr"
|
||||
read -r prx ptx pnow <"$ctr"
|
||||
dt=$(awk -v a="$now" -v b="$pnow" 'BEGIN{d=a-b; print (d>0.05)?d:1}')
|
||||
drx=$(( rx - prx )); dtx=$(( tx - ptx ))
|
||||
drx=$((rx - prx))
|
||||
dtx=$((tx - ptx))
|
||||
[ "$drx" -lt 0 ] && drx=0
|
||||
[ "$dtx" -lt 0 ] && dtx=0
|
||||
fi
|
||||
printf '%s %s %s\n' "$rx" "$tx" "$now" > "$ctr"
|
||||
printf '%s %s %s\n' "$rx" "$tx" "$now" >"$ctr"
|
||||
|
||||
human() { awk -v b="$1" 'BEGIN{ split("B K M G T", u, " "); i=1;
|
||||
while (b >= 1024 && i < 5) { b /= 1024; i++ }
|
||||
@@ -55,6 +58,6 @@ human() { awk -v b="$1" 'BEGIN{ split("B K M G T", u, " "); i=1;
|
||||
|
||||
dl=$(human "$(awk -v x="$drx" -v d="$dt" 'BEGIN{printf "%d", x/d}')")
|
||||
ul=$(human "$(awk -v x="$dtx" -v d="$dt" 'BEGIN{printf "%d", x/d}')")
|
||||
printf '%s %s %s\n' "$dl" "$ul" "$(date +%s)" > "$cache"
|
||||
printf '%s %s %s\n' "$dl" "$ul" "$(date +%s)" >"$cache"
|
||||
|
||||
if [ "$which" = ul ]; then emit '\U0000f093' "$ul"; else emit '\U0000f019' "$dl"; fi
|
||||
|
||||
@@ -5,15 +5,19 @@
|
||||
# (acpitz can be a bogus constant on desktops, hence last). Prints NOTHING when
|
||||
# no sensor exists (VMs/containers) so the segment simply disappears — which is
|
||||
# also why the trailing spacing lives in here, not in status-right.
|
||||
cpu_zone=""; any_zone=""; hw=""
|
||||
cpu_zone=""
|
||||
any_zone=""
|
||||
hw=""
|
||||
|
||||
for d in /sys/class/thermal/thermal_zone*/; do
|
||||
[ -r "${d}temp" ] && [ -r "${d}type" ] || continue
|
||||
t=$(<"${d}temp")
|
||||
case $t in ''|*[!0-9]*) continue ;; esac
|
||||
ty=$(<"${d}type"); ty=${ty,,}
|
||||
case $ty in *x86_pkg_temp*|*coretemp*|*k10temp*|*tctl*|*cpu*)
|
||||
if [ -z "$cpu_zone" ] || [ "$t" -gt "$cpu_zone" ]; then cpu_zone=$t; fi ;;
|
||||
case $t in '' | *[!0-9]*) continue ;; esac
|
||||
ty=$(<"${d}type")
|
||||
ty=${ty,,}
|
||||
case $ty in *x86_pkg_temp* | *coretemp* | *k10temp* | *tctl* | *cpu*)
|
||||
if [ -z "$cpu_zone" ] || [ "$t" -gt "$cpu_zone" ]; then cpu_zone=$t; fi
|
||||
;;
|
||||
esac
|
||||
if [ -z "$any_zone" ] || [ "$t" -gt "$any_zone" ]; then any_zone=$t; fi
|
||||
done
|
||||
@@ -21,17 +25,18 @@ done
|
||||
if [ -z "$cpu_zone" ]; then
|
||||
for d in /sys/class/hwmon/hwmon*/; do
|
||||
[ -r "${d}name" ] || continue
|
||||
case $(<"${d}name") in coretemp|k10temp|zenpower|cpu_thermal)
|
||||
case $(<"${d}name") in coretemp | k10temp | zenpower | cpu_thermal)
|
||||
for f in "${d}"temp*_input; do
|
||||
[ -r "$f" ] || continue
|
||||
t=$(<"$f")
|
||||
case $t in ''|*[!0-9]*) continue ;; esac
|
||||
case $t in '' | *[!0-9]*) continue ;; esac
|
||||
if [ -z "$hw" ] || [ "$t" -gt "$hw" ]; then hw=$t; fi
|
||||
done ;;
|
||||
done
|
||||
;;
|
||||
esac
|
||||
done
|
||||
fi
|
||||
|
||||
t=${cpu_zone:-${hw:-$any_zone}}
|
||||
[ -n "$t" ] || exit 0
|
||||
printf '\U000f050f %d° ' $(( (t + 500) / 1000 ))
|
||||
printf '\U000f050f %d° ' $(((t + 500) / 1000))
|
||||
|
||||
@@ -3,6 +3,18 @@
|
||||
set -g extended-keys on
|
||||
set -g extended-keys-format csi-u
|
||||
|
||||
# Mouse/touch reporting. Chiefly so the window list in the status bar is
|
||||
# tappable — tmux binds MouseDown1Status to `switch-client -t =` out of the box,
|
||||
# and status-format[1] below keeps the `range=window|…` markers that make each
|
||||
# entry a hit target, so a tap on a tab switches to it with no extra binding.
|
||||
# Wheel over the bar cycles windows; right-click a tab or the session name for
|
||||
# tmux's built-in menus (no touch equivalent — a phone can't right-click).
|
||||
#
|
||||
# The cost: drag-select now goes to tmux instead of kitty, so hold Shift when you
|
||||
# want kitty's own selection. Scroll wheel enters copy-mode on the pane rather
|
||||
# than kitty's scrollback, and a click focuses the pane under it.
|
||||
set -g mouse on
|
||||
|
||||
# Number windows (and panes) from 1 instead of 0 — easier to reach on the
|
||||
# keyboard. renumber-windows keeps them gap-free when a window is closed.
|
||||
set -g base-index 1
|
||||
@@ -42,8 +54,36 @@ set -g status-style "bg=default,fg=#cdd6f4" # transparent: no bar background
|
||||
set -g status-left-length 40
|
||||
set -g status-right-length 100
|
||||
|
||||
# Left: session name (mauve/bold) + thin divider.
|
||||
set -g status-left " #[fg=#cba6f7,bold]#S #[fg=#45475a]│ "
|
||||
# --- Mobile view -----------------------------------------------------------
|
||||
# status-left and status-right are re-evaluated per client on every draw, and
|
||||
# #{client_width} is that client's own width — so in `auto` mode a phone attached
|
||||
# to the same session as the desktop renders the compact bars while the desktop
|
||||
# keeps the full ones, simultaneously and with no intervention.
|
||||
#
|
||||
# @status_mode is auto (per-client, by width) | full | compact, cycled with
|
||||
# prefix+S. Rather than branch each bar three ways, the mode picks the width a
|
||||
# client has to beat — 0 nothing can lose, 9999 nothing can win — so each
|
||||
# decision below stays a single comparison. The 100 is the switch point: the full
|
||||
# right segment is ~60 columns before the window list even starts. Set
|
||||
# @status_mode without -g to scope an override to one session, so a grouped
|
||||
# mobile session can sit on `compact` while the desktop session stays on `auto`.
|
||||
#
|
||||
# Each variant lives in its own option instead of inline because #{?a,b,c} splits
|
||||
# its branches on commas and the styles are full of them (#[fg=…,bold]); option
|
||||
# values are substituted after the ternary is parsed, so those commas are never
|
||||
# seen as separators.
|
||||
#
|
||||
# NOTE: the comparisons MUST use #{e|>=:…}. Plain #{>=:…} compares as strings, so
|
||||
# "144" >= "100" is false and every client would get the compact bar.
|
||||
set -g @status_mode auto
|
||||
set -g @status_cutoff "#{?#{==:#{@status_mode},full},0,#{?#{==:#{@status_mode},compact},9999,100}}"
|
||||
|
||||
# Left: session name (mauve/bold) + thin divider. Compact clips the name to two
|
||||
# characters — enough to tell sessions apart when every column counts. #{=2:…}
|
||||
# takes a format NAME, so it's session_name here rather than the #S shorthand.
|
||||
set -g @left_full " #[fg=#cba6f7,bold]#S #[fg=#45475a]│ "
|
||||
set -g @left_compact " #[fg=#cba6f7,bold]#{=2:session_name} #[fg=#45475a]│ "
|
||||
set -g status-left "#{?#{e|>=:#{client_width},#{E:@status_cutoff}},#{E:@left_full},#{E:@left_compact}}"
|
||||
|
||||
# Middle: flat window list — inactive muted, active mauve/bold, 2-space gaps.
|
||||
set -g window-status-separator " "
|
||||
@@ -52,9 +92,35 @@ setw -g window-status-current-format "#[fg=#cba6f7,bold]#I #[fg=#cdd6f4,bold]#W"
|
||||
setw -g window-status-activity-style "fg=#f9e2af"
|
||||
|
||||
# Right: net dl/ul · cpu · mem │ date │ clock. continuum prepends its invisible
|
||||
# save hook here (this block sits above the tpm `run` line, so the hook survives
|
||||
# every reload). Metrics are scripts/*.sh, symlinked into ~/.config/tmux/scripts/.
|
||||
set -g status-right "#[fg=#a6e3a1]#(~/.config/tmux/scripts/net.sh dl) #[fg=#f9e2af]#(~/.config/tmux/scripts/net.sh ul) #[fg=#89b4fa]#(~/.config/tmux/scripts/cpu.sh) #[fg=#94e2d5]#(~/.config/tmux/scripts/mem.sh) #[fg=#fab387]#(~/.config/tmux/scripts/temp.sh)#[fg=#45475a]│ #[fg=#a6adc8]%a %b %-d #[fg=#45475a]│ #[fg=#cba6f7,bold]%H:%M "
|
||||
# save hook to status-right (this block sits above the tpm `run` line, so the
|
||||
# hook survives every reload). Metrics are scripts/*.sh, symlinked into
|
||||
# ~/.config/tmux/scripts/. The hook lands ahead of the #{?} below, i.e. outside
|
||||
# it, so it keeps running for every client regardless of which variant is drawn.
|
||||
set -g @right_full "#[fg=#a6e3a1]#(~/.config/tmux/scripts/net.sh dl) #[fg=#f9e2af]#(~/.config/tmux/scripts/net.sh ul) #[fg=#89b4fa]#(~/.config/tmux/scripts/cpu.sh) #[fg=#94e2d5]#(~/.config/tmux/scripts/mem.sh) #[fg=#fab387]#(~/.config/tmux/scripts/temp.sh)#[fg=#45475a]│ #[fg=#a6adc8]%a %b %-d #[fg=#45475a]│ #[fg=#cba6f7,bold]%H:%M "
|
||||
|
||||
# Compact: cpu + mem only. Net rates and the temp glyph go first (widest, least
|
||||
# useful on a phone), then the date, then the clock — a phone shows its own.
|
||||
set -g @right_compact "#[fg=#89b4fa]#(~/.config/tmux/scripts/cpu.sh) #[fg=#94e2d5]#(~/.config/tmux/scripts/mem.sh) "
|
||||
|
||||
set -g status-right "#{?#{e|>=:#{client_width},#{E:@status_cutoff}},#{E:@right_full},#{E:@right_compact}}"
|
||||
|
||||
# Cycle auto -> compact -> full -> auto. Braces instead of nested quoted strings
|
||||
# so the styles' commas and the inner if-shell don't need escaping.
|
||||
bind S {
|
||||
if -F '#{==:#{@status_mode},auto}' {
|
||||
set -g @status_mode compact
|
||||
display 'status bar: compact (forced)'
|
||||
} {
|
||||
if -F '#{==:#{@status_mode},compact}' {
|
||||
set -g @status_mode full
|
||||
display 'status bar: full (forced)'
|
||||
} {
|
||||
set -g @status_mode auto
|
||||
display 'status bar: auto — switches at 100 columns'
|
||||
}
|
||||
}
|
||||
refresh-client -S
|
||||
}
|
||||
|
||||
# Two rows: [0] drawn top-border rule · [1] the bar content. The rule doubles as
|
||||
# the separator from the pane content above; tmux status height is whole rows, so
|
||||
|
||||
+72
-10
@@ -56,15 +56,70 @@ have a slash command to disable them for the current session without deleting.
|
||||
| Extension | What it adds | Origin | Remove |
|
||||
|---|---|---|---|
|
||||
| `statusbar.ts` | Replaces the footer with a Claude-Code-style status line (cwd, git branch, model, context %, decode t/s, token counts) in Nerd Font glyphs; adds a streaming pulse + a ≥80%-context warning widget | **custom** | delete file → default footer returns; or `/statusbar` to toggle off for the session |
|
||||
| `vim-editor.ts` | Modal (vim-like) input editor with `[I]/[N]/[V]` indicator, hjkl/word/line motions, counts, yank/paste via the kill-ring | **local fork** of pi's bundled `examples/extensions/modal-editor.ts` | delete file → default editor; or `/vim` to toggle |
|
||||
| `plan-mode/` | Read-only "plan" mode (disables edit/write, restricts bash) bound to **Shift+Tab**; `/plan` toggles, `--plan` starts in it | **vendored** copy of pi's bundled `examples/extensions/plan-mode/`, rebound from upstream `Ctrl+Alt+P` | delete the folder **and** revert the `keybindings.json` `app.thinking.cycle` remap (see below) |
|
||||
| `vim-toggle.ts` | `/vim` — drops the modal editor for the rest of the session, and brings it back via `ctx.reload()`. Pairs with the **`pi-vim` package** (below), which registers no command of its own | **custom** | delete file → you lose the toggle, not the editor; remove the package to drop modal editing itself |
|
||||
| `plan-mode/` | Read-only "plan" mode (disables edit/write, restricts bash) bound to **Shift+Tab**; `/plan` toggles, `--plan` starts in it, `/plan-done` clears a stuck progress checklist | **vendored** copy of pi's bundled `examples/extensions/plan-mode/`, rebound from upstream `Ctrl+Alt+P` | delete the folder **and** revert the `keybindings.json` `app.thinking.cycle` remap (see below) |
|
||||
| `notify.ts` | Native terminal notification (OSC 777/99, Windows toast) when the agent goes idle | **verbatim** from pi's `examples/extensions/notify.ts` | delete file |
|
||||
| `questionnaire.ts` | Interactive multi-question overlay the model can call as a tool | **near-verbatim** from pi's `examples/extensions/questionnaire.ts` (one-line fix) | 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 |
|
||||
| `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 |
|
||||
| `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 |
|
||||
| `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
|
||||
> where `notify`/`questionnaire`/`modal-editor`/`plan-mode` came from, and where to
|
||||
> re-pull the two forks after a pi upgrade before re-applying the local edits (the
|
||||
> forks' own headers document those edits).
|
||||
> 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
|
||||
> the local edits (its header documents them). `notify`/`questionnaire` carry no edits
|
||||
> worth keeping — just re-copy them. The modal editor used to be a fork of
|
||||
> `modal-editor.ts` too; it was replaced by the `pi-vim` package on 2026-08-05 and the
|
||||
> fork deleted on 2026-08-09 — `git show c6566af^:common/.pi/agent/extensions/vim-editor.ts`
|
||||
> brings it back if ever wanted.
|
||||
|
||||
### Modal (vim) input — the `pi-vim` package
|
||||
|
||||
Modal editing is **not** an extension in this tree any more; it's the npm package
|
||||
[`pi-vim`](https://github.com/lajarre/pi-vim), pinned by `settings.json > packages`
|
||||
and installed into the gitignored `agent/npm/`. That swap is the point: `pi update
|
||||
npm:pi-vim` instead of re-pulling and re-patching a fork on every pi upgrade.
|
||||
|
||||
Five modes (INSERT/NORMAL/VISUAL/V-LINE/EX) with the usual motions, operators, text
|
||||
objects, undo and `.` repeat; the mode renders as a word label at the editor's
|
||||
bottom-right, doubling as a pending-command display (` NORMAL 3d2w_ `). `:` dispatches
|
||||
real pi commands and shells out with `:!`. No search (`/`, `?`, `n`), macros or
|
||||
visual-block — upstream implements none of those.
|
||||
|
||||
```fish
|
||||
pi install npm:pi-vim
|
||||
```
|
||||
|
||||
> ⚠️ **This pi build needs pi-vim's peer dep installed by hand.** pi ships as a
|
||||
> Bun-compiled ELF with the `@earendil-works/*` packages *embedded*, not on disk.
|
||||
> pi-vim's `clipboard-mirror.ts` calls `import.meta.resolve("@earendil-works/pi-coding-agent")`
|
||||
> at **module top level**, so after a bare `pi install` the resolve throws and the whole
|
||||
> extension fails to load with `Cannot find module '@earendil-works/pi-coding-agent'`.
|
||||
> No setting avoids it — the call runs at import time. Once per host, matching
|
||||
> `pi --version` exactly:
|
||||
>
|
||||
> ```fish
|
||||
> cd ~/.pi/agent/npm
|
||||
> fnm exec --using=22 -- npm install --save-exact @earendil-works/pi-coding-agent@0.83.0
|
||||
> ```
|
||||
>
|
||||
> `pi install` deliberately doesn't pull peers (it would duplicate the runtime). Costs
|
||||
> ~170 MB, and wants re-pinning after every pi upgrade — hence `--save-exact`, since a
|
||||
> caret range would quietly drift off `pi --version`.
|
||||
|
||||
Configured under `settings.json > piVim`: `modeColors` (theme tokens, not raw ANSI),
|
||||
`borderSync`/`labelSync` pinned so only the label is tinted, and
|
||||
`clipboardMirror: "yank"` — upstream's default `"all"` mirrors deletes too, so every
|
||||
`dd`/`x` would clobber the OS clipboard. `exCommand.copyInputToClipboard` is left
|
||||
**off**: it copies the composed prompt out, so upstream only honours it from the
|
||||
user-global settings file.
|
||||
|
||||
**Remove:** `pi remove npm:pi-vim`, and delete `agent/extensions/vim-toggle.ts` plus the
|
||||
`piVim` block. `@burneikis/pi-vim` and `pi-vimmode` are alternatives that need no peer
|
||||
install.
|
||||
|
||||
### Theme
|
||||
|
||||
@@ -110,11 +165,14 @@ Beyond pointing at local models, these values deviate from pi's defaults:
|
||||
|
||||
| Key | Value here | Stock default | Why |
|
||||
|---|---|---|---|
|
||||
| `compaction.reserveTokens` | `6144` | 16384 | small local windows (24k–128k) — don't let the response reserve dominate a short window |
|
||||
| `compaction.keepRecentTokens` | `6000` | 20000 | keep less verbatim so short windows don't thrash into repeated compaction |
|
||||
| `compaction.reserveTokens` | `8192` | 16384 | small local windows (24k–128k), but **≥ every model's `maxTokens`** — otherwise a turn can overflow the window before compaction even fires |
|
||||
| `compaction.keepRecentTokens` | `4000` | 20000 | keep less verbatim, to pay for the larger reserve and leave room after a compaction for the next one |
|
||||
| `npmCommand` | `fnm exec --using=22 -- npm` | `npm` | pin extension npm installs to Node 22 via fnm |
|
||||
| `retry` / `httpIdleTimeoutMs` | long (1h provider timeout, 10m idle) | shorter | local models can be slow to first token |
|
||||
| `defaultThinkingLevel` | `off` | — | most local models here are non-reasoning |
|
||||
| `packages` | `["npm:pi-vim"]` | `[]` | the modal editor — see the pi-vim section above |
|
||||
| `piVim` | mode colors, `clipboardMirror: "yank"`, `:` → pi bridge | — | package config, not a pi setting; ignored if the package is removed |
|
||||
| `collapseChangelog` | `true` | `false` | condensed changelog after an upgrade instead of the full screen |
|
||||
|
||||
**Remove:** delete each key to fall back to pi's default (or drop the whole
|
||||
`compaction`/`retry` block).
|
||||
@@ -140,8 +198,9 @@ model.
|
||||
> config. `auth.json` is **not tracked** (gitignored as of 2026-08-08): pi's interactive
|
||||
> `/login` rewrites it with the **literal** key, and a tracked copy would be one stray
|
||||
> `/login` away from committing a real token. Bootstrap a new host with
|
||||
> `cp auth.json.example auth.json`. For bash its `.bash_profile` since it's not tracked by
|
||||
> git, stow not applied.
|
||||
> `cp auth.json.example auth.json`. The key itself lives in a shell file that is *not*
|
||||
> tracked and *not* stowed, so it never reaches this repo — `conf.d/secrets.fish` for
|
||||
> fish, `~/.bash_profile` for bash.
|
||||
|
||||
---
|
||||
|
||||
@@ -150,7 +209,10 @@ model.
|
||||
- **Just the status bar:** copy `agent/extensions/statusbar.ts` into your
|
||||
`~/.pi/agent/extensions/`, re-stow/restart, `/reload`. Colors follow your active
|
||||
theme automatically. Needs a Nerd Font terminal for the glyphs.
|
||||
- **Just vim input:** copy `agent/extensions/vim-editor.ts`, `/reload`, `/vim`.
|
||||
- **Just vim input:** `pi install npm:pi-vim`, then the peer-dep install above (this
|
||||
build needs it), then `/reload`. Copy the `piVim` block from `settings.json` for the
|
||||
same colors and clipboard behaviour, and `agent/extensions/vim-toggle.ts` if you want
|
||||
`/vim` to toggle it.
|
||||
- **Just plan mode:** copy `agent/extensions/plan-mode/` **and** add the
|
||||
`app.thinking.cycle` remap to your `keybindings.json` (else Shift+Tab collides).
|
||||
- **Just the theme:** copy `agent/themes/catppuccin-mocha.json`, set
|
||||
|
||||
@@ -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,60 @@
|
||||
/**
|
||||
* Compaction Watch
|
||||
*
|
||||
* Compaction is otherwise silent, which is why "compacting fails a lot" took code
|
||||
* reading and arithmetic to diagnose rather than a glance at a log. This makes it
|
||||
* visible, and specifically names the failure mode that was breaking it here before
|
||||
* 2026-08-16.
|
||||
*
|
||||
* pi reports a `reason` on every compaction:
|
||||
* "manual" - you ran /compact
|
||||
* "threshold" - the healthy path: contextTokens passed contextWindow - reserveTokens
|
||||
* "overflow" - the window was ALREADY full; this is recovery, not prevention
|
||||
*
|
||||
* An "overflow" compaction means the turn blew past contextWindow before the threshold
|
||||
* could fire, which happens whenever `compaction.reserveTokens` is smaller than the
|
||||
* model's `maxTokens`. pi then gets one recovery attempt, summarizing from an
|
||||
* already-overflowed context — and when that does not fit either, the session dies with
|
||||
* "Context overflow recovery failed after one compact-and-retry attempt".
|
||||
*
|
||||
* So: seeing "threshold" is fine. Seeing "overflow" means the settings are wrong for the
|
||||
* model in use, and this says so at the moment it happens instead of leaving you to
|
||||
* infer it later. See CLAUDE.md > Compaction tuning.
|
||||
*
|
||||
* Note there is no event for a *failed* auto-compaction — `onError` exists only on the
|
||||
* manual `ctx.compact()` call — so catching the overflow entry is the earliest reliable
|
||||
* warning available.
|
||||
*/
|
||||
|
||||
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||
|
||||
export default function (pi: ExtensionAPI) {
|
||||
let overflowCount = 0;
|
||||
|
||||
pi.on("session_before_compact", async (event, ctx) => {
|
||||
if (!ctx.hasUI) return undefined;
|
||||
|
||||
if (event.reason === "overflow") {
|
||||
overflowCount++;
|
||||
ctx.ui.notify(
|
||||
`Compacting in OVERFLOW recovery (#${overflowCount}) — the context window was already ` +
|
||||
`full before compaction could fire. Raise compaction.reserveTokens above this model's ` +
|
||||
`maxTokens, or cap maxTokens in models.json. See CLAUDE.md > Compaction tuning.`,
|
||||
"warning",
|
||||
);
|
||||
}
|
||||
|
||||
// Never alter the compaction itself — observe only.
|
||||
return undefined;
|
||||
});
|
||||
|
||||
pi.on("session_compact", async (event, ctx) => {
|
||||
if (!ctx.hasUI) return;
|
||||
|
||||
const reason = event.reason ?? "unknown";
|
||||
const retrying = event.willRetry ? ", retrying the aborted turn" : "";
|
||||
const level = reason === "overflow" ? "warning" : "info";
|
||||
|
||||
ctx.ui.notify(`Compacted (${reason}${retrying})`, level);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* Confirm Destructive Actions Extension
|
||||
*
|
||||
* Prompts for confirmation before destructive session actions (clear, switch, branch).
|
||||
* Demonstrates how to cancel session events using the before_* events.
|
||||
*/
|
||||
|
||||
import type { ExtensionAPI, SessionBeforeSwitchEvent, SessionMessageEntry } from "@earendil-works/pi-coding-agent";
|
||||
|
||||
export default function (pi: ExtensionAPI) {
|
||||
pi.on("session_before_switch", async (event: SessionBeforeSwitchEvent, ctx) => {
|
||||
if (!ctx.hasUI) return;
|
||||
|
||||
if (event.reason === "new") {
|
||||
const confirmed = await ctx.ui.confirm(
|
||||
"Clear session?",
|
||||
"This will delete all messages in the current session.",
|
||||
);
|
||||
|
||||
if (!confirmed) {
|
||||
ctx.ui.notify("Clear cancelled", "info");
|
||||
return { cancel: true };
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// reason === "resume" - check if there are unsaved changes (messages since last assistant response)
|
||||
const entries = ctx.sessionManager.getEntries();
|
||||
const hasUnsavedWork = entries.some(
|
||||
(e): e is SessionMessageEntry => e.type === "message" && e.message.role === "user",
|
||||
);
|
||||
|
||||
if (hasUnsavedWork) {
|
||||
const confirmed = await ctx.ui.confirm(
|
||||
"Switch session?",
|
||||
"You have messages in the current session. Switch anyway?",
|
||||
);
|
||||
|
||||
if (!confirmed) {
|
||||
ctx.ui.notify("Switch cancelled", "info");
|
||||
return { cancel: true };
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
pi.on("session_before_fork", async (event, ctx) => {
|
||||
if (!ctx.hasUI) return;
|
||||
|
||||
const choice = await ctx.ui.select(`Fork from entry ${event.entryId.slice(0, 8)}?`, [
|
||||
"Yes, create fork",
|
||||
"No, stay in current session",
|
||||
]);
|
||||
|
||||
if (choice !== "Yes, create fork") {
|
||||
ctx.ui.notify("Fork cancelled", "info");
|
||||
return { cancel: true };
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* Permission Gate Extension
|
||||
*
|
||||
* Prompts for confirmation before running potentially dangerous bash commands.
|
||||
*
|
||||
* Adapted from pi's bundled `examples/extensions/permission-gate.ts`. Upstream checks
|
||||
* three patterns (rm -rf, sudo, chmod/chown 777); the list below is extended to cover
|
||||
* the rules AGENTS.md only *asks* for. That distinction is the whole point of running
|
||||
* this: AGENTS.md is a prompt, and the small local models this config targets follow
|
||||
* prompts unreliably. This is the enforcement half — the same trick plan-mode already
|
||||
* uses when it disables edit/write outright.
|
||||
*
|
||||
* In the TUI a match prompts (so "Yes" still gets you through); with no UI it blocks.
|
||||
*/
|
||||
|
||||
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||
|
||||
export default function (pi: ExtensionAPI) {
|
||||
const dangerousPatterns = [
|
||||
// --- upstream ---
|
||||
/\brm\s+(-rf?|--recursive)/i,
|
||||
/\bsudo\b/i,
|
||||
/\b(chmod|chown)\b.*777/i,
|
||||
|
||||
// --- LOCAL: git history is the user's to write, never the agent's ---
|
||||
// The standing rule is that commits and pushes are made by hand, after review.
|
||||
/\bgit\s+(commit|push|revert)\b/i,
|
||||
/\bgit\s+reset\s+--hard\b/i,
|
||||
/\bgit\s+(rebase|cherry-pick)\b/i,
|
||||
/\bgit\s+clean\s+-[a-z]*[fd]/i,
|
||||
/\bgit\s+stash\s+(drop|clear|pop)\b/i,
|
||||
/--force\b|--force-with-lease\b/i,
|
||||
|
||||
// --- LOCAL: system-changing commands AGENTS.md rules out ---
|
||||
/\b(pacman|yay|paru)\s+-[A-Za-z]*[SRU]/,
|
||||
/\b(npm|pnpm|yarn)\s+(install|i|add|remove|uninstall)\b/i,
|
||||
/\bpip3?\s+(install|uninstall)\b/i,
|
||||
/\bcargo\s+(install|uninstall)\b/i,
|
||||
/\bsystemctl\s+(start|stop|restart|enable|disable|mask)\b/i,
|
||||
|
||||
// --- LOCAL: irreversible disk / process operations ---
|
||||
/\bmkfs\b|\bdd\s+if=|\bshred\b|\bwipefs\b/i,
|
||||
/\b(reboot|shutdown|poweroff|halt)\b/i,
|
||||
/\b(killall|pkill)\b/i,
|
||||
/>\s*\/dev\/(sd|nvme|vd)/i,
|
||||
];
|
||||
|
||||
pi.on("tool_call", async (event, ctx) => {
|
||||
if (event.toolName !== "bash") return undefined;
|
||||
|
||||
const command = event.input.command as string;
|
||||
const matched = dangerousPatterns.find((p) => p.test(command));
|
||||
|
||||
if (matched) {
|
||||
if (!ctx.hasUI) {
|
||||
// Non-interactive (`pi -p`): nothing can confirm, so refuse.
|
||||
return { block: true, reason: "Dangerous command blocked (no UI for confirmation)" };
|
||||
}
|
||||
|
||||
const choice = await ctx.ui.select(`⚠️ Dangerous command:\n\n ${command}\n\nAllow?`, ["Yes", "No"]);
|
||||
|
||||
if (choice !== "Yes") {
|
||||
return { block: true, reason: "Blocked by user" };
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
@@ -142,6 +142,25 @@ export default function planModeExtension(pi: ExtensionAPI): void {
|
||||
handler: async (_args, ctx) => togglePlanMode(ctx),
|
||||
});
|
||||
|
||||
// LOCAL EDIT: the checklist widget only tears itself down once every step is
|
||||
// marked complete, which needs the model to emit a [DONE:n] tag per step. When
|
||||
// one is missed the widget sticks around after the work is finished, so give
|
||||
// the user a way out that doesn't involve toggling plan mode on and off again.
|
||||
pi.registerCommand("plan-done", {
|
||||
description: "Clear a stuck plan checklist (stops execution tracking)",
|
||||
handler: async (_args, ctx) => {
|
||||
if (!executionMode && todoItems.length === 0) {
|
||||
ctx.ui.notify("No plan is being tracked.");
|
||||
return;
|
||||
}
|
||||
executionMode = false;
|
||||
todoItems = [];
|
||||
updateStatus(ctx);
|
||||
persistState();
|
||||
ctx.ui.notify("Plan tracking cleared.");
|
||||
},
|
||||
});
|
||||
|
||||
pi.registerCommand("todos", {
|
||||
description: "Show current plan todo list",
|
||||
handler: async (_args, ctx) => {
|
||||
|
||||
@@ -151,9 +151,16 @@ export function extractTodoItems(message: string): TodoItem[] {
|
||||
|
||||
export function extractDoneSteps(message: string): number[] {
|
||||
const steps: number[] = [];
|
||||
for (const match of message.matchAll(/\[DONE:(\d+)\]/gi)) {
|
||||
const step = Number(match[1]);
|
||||
if (Number.isFinite(step)) steps.push(step);
|
||||
// LOCAL EDIT: upstream matched only the literal `[DONE:3]`. Small local models
|
||||
// drift on it, and one missed tag pins the plan widget on screen forever, so
|
||||
// also accept `[DONE 3]`, `[DONE-3]` and comma lists `[DONE:1,2]`. The brackets
|
||||
// stay required on purpose: a bare "done 3" in prose must not mark a step.
|
||||
for (const match of message.matchAll(/\[\s*DONE\s*[:\-=]?\s*([\d,\s]+?)\s*\]/gi)) {
|
||||
for (const part of match[1].split(/[,\s]+/)) {
|
||||
if (part === "") continue;
|
||||
const step = Number(part);
|
||||
if (Number.isFinite(step)) steps.push(step);
|
||||
}
|
||||
}
|
||||
return steps;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Protected Paths Extension
|
||||
*
|
||||
* Blocks write and edit operations to protected paths.
|
||||
*
|
||||
* Adapted from pi's bundled `examples/extensions/protected-paths.ts`. Upstream ships a
|
||||
* three-entry demo list (.env, .git/, node_modules/); the list below names the files
|
||||
* that actually matter in this dotfiles tree — the ones holding secrets, and the git
|
||||
* metadata the agent has no business rewriting.
|
||||
*
|
||||
* Scope, so this isn't over-trusted: it guards against the *model* calling write/edit.
|
||||
* It cannot stop pi's own `/login`, which rewrites auth.json with the literal key —
|
||||
* that is why auth.json is gitignored and auth.json.example is the tracked copy.
|
||||
*/
|
||||
|
||||
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||
|
||||
export default function (pi: ExtensionAPI) {
|
||||
const protectedPaths = [
|
||||
// Secrets. `secrets.fish` holds $DUSKADIY_API_KEY; auth.json is pi's own
|
||||
// credential map and must keep $VAR references rather than literals.
|
||||
"secrets.fish",
|
||||
"auth.json",
|
||||
".env",
|
||||
"fish_variables",
|
||||
|
||||
// Credentials outside the repo.
|
||||
".ssh/",
|
||||
".gnupg/",
|
||||
".aws/",
|
||||
".netrc",
|
||||
|
||||
// Repo plumbing and vendored trees — never hand-edited.
|
||||
".git/",
|
||||
"node_modules/",
|
||||
];
|
||||
|
||||
pi.on("tool_call", async (event, ctx) => {
|
||||
if (event.toolName !== "write" && event.toolName !== "edit") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const path = event.input.path as string;
|
||||
// Substring match, like upstream: cheap, and these names are distinctive enough.
|
||||
// Note `auth.json.example` is intentionally caught too — it is a tracked file
|
||||
// whose whole job is to contain no secret, so an agent rewrite is worth a look.
|
||||
const isProtected = protectedPaths.some((p) => path.includes(p));
|
||||
|
||||
if (isProtected) {
|
||||
if (ctx.hasUI) {
|
||||
ctx.ui.notify(`Blocked write to protected path: ${path}`, "warning");
|
||||
}
|
||||
return { block: true, reason: `Path "${path}" is protected` };
|
||||
}
|
||||
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Session naming example.
|
||||
*
|
||||
* Shows setSessionName/getSessionName to give sessions friendly names
|
||||
* that appear in the session selector instead of the first message.
|
||||
*
|
||||
* Usage: /session-name [name] - set or show session name
|
||||
*/
|
||||
|
||||
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||
|
||||
export default function (pi: ExtensionAPI) {
|
||||
pi.registerCommand("session-name", {
|
||||
description: "Set or show session name (usage: /session-name [new name])",
|
||||
handler: async (args, ctx) => {
|
||||
const name = args.trim();
|
||||
|
||||
if (name) {
|
||||
pi.setSessionName(name);
|
||||
ctx.ui.notify(`Session named: ${name}`, "info");
|
||||
} else {
|
||||
const current = pi.getSessionName();
|
||||
ctx.ui.notify(current ? `Session: ${current}` : "No session name set", "info");
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
/**
|
||||
* Todo Extension - Demonstrates state management via session entries
|
||||
*
|
||||
* This extension:
|
||||
* - Registers a `todo` tool for the LLM to manage todos
|
||||
* - Registers a `/todos` command for users to view the list
|
||||
*
|
||||
* State is stored in tool result details (not external files), which allows
|
||||
* proper branching - when you branch, the todo state is automatically
|
||||
* correct for that point in history.
|
||||
*/
|
||||
|
||||
import { StringEnum } from "@earendil-works/pi-ai";
|
||||
import type { ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
||||
import { matchesKey, Text, truncateToWidth } from "@earendil-works/pi-tui";
|
||||
import { Type } from "typebox";
|
||||
|
||||
interface Todo {
|
||||
id: number;
|
||||
text: string;
|
||||
done: boolean;
|
||||
}
|
||||
|
||||
interface TodoDetails {
|
||||
action: "list" | "add" | "toggle" | "clear";
|
||||
todos: Todo[];
|
||||
nextId: number;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
const TodoParams = Type.Object({
|
||||
action: StringEnum(["list", "add", "toggle", "clear"] as const),
|
||||
text: Type.Optional(Type.String({ description: "Todo text (for add)" })),
|
||||
id: Type.Optional(Type.Number({ description: "Todo ID (for toggle)" })),
|
||||
});
|
||||
|
||||
/**
|
||||
* UI component for the /todos command
|
||||
*/
|
||||
class TodoListComponent {
|
||||
private todos: Todo[];
|
||||
private theme: Theme;
|
||||
private onClose: () => void;
|
||||
private cachedWidth?: number;
|
||||
private cachedLines?: string[];
|
||||
|
||||
constructor(todos: Todo[], theme: Theme, onClose: () => void) {
|
||||
this.todos = todos;
|
||||
this.theme = theme;
|
||||
this.onClose = onClose;
|
||||
}
|
||||
|
||||
handleInput(data: string): void {
|
||||
if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
|
||||
this.onClose();
|
||||
}
|
||||
}
|
||||
|
||||
render(width: number): string[] {
|
||||
if (this.cachedLines && this.cachedWidth === width) {
|
||||
return this.cachedLines;
|
||||
}
|
||||
|
||||
const lines: string[] = [];
|
||||
const th = this.theme;
|
||||
|
||||
lines.push("");
|
||||
const title = th.fg("accent", " Todos ");
|
||||
const headerLine =
|
||||
th.fg("borderMuted", "─".repeat(3)) + title + th.fg("borderMuted", "─".repeat(Math.max(0, width - 10)));
|
||||
lines.push(truncateToWidth(headerLine, width));
|
||||
lines.push("");
|
||||
|
||||
if (this.todos.length === 0) {
|
||||
lines.push(truncateToWidth(` ${th.fg("dim", "No todos yet. Ask the agent to add some!")}`, width));
|
||||
} else {
|
||||
const done = this.todos.filter((t) => t.done).length;
|
||||
const total = this.todos.length;
|
||||
lines.push(truncateToWidth(` ${th.fg("muted", `${done}/${total} completed`)}`, width));
|
||||
lines.push("");
|
||||
|
||||
for (const todo of this.todos) {
|
||||
const check = todo.done ? th.fg("success", "✓") : th.fg("dim", "○");
|
||||
const id = th.fg("accent", `#${todo.id}`);
|
||||
const text = todo.done ? th.fg("dim", todo.text) : th.fg("text", todo.text);
|
||||
lines.push(truncateToWidth(` ${check} ${id} ${text}`, width));
|
||||
}
|
||||
}
|
||||
|
||||
lines.push("");
|
||||
lines.push(truncateToWidth(` ${th.fg("dim", "Press Escape to close")}`, width));
|
||||
lines.push("");
|
||||
|
||||
this.cachedWidth = width;
|
||||
this.cachedLines = lines;
|
||||
return lines;
|
||||
}
|
||||
|
||||
invalidate(): void {
|
||||
this.cachedWidth = undefined;
|
||||
this.cachedLines = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export default function (pi: ExtensionAPI) {
|
||||
// In-memory state (reconstructed from session on load)
|
||||
let todos: Todo[] = [];
|
||||
let nextId = 1;
|
||||
|
||||
/**
|
||||
* Reconstruct state from session entries.
|
||||
* Scans tool results for this tool and applies them in order.
|
||||
*/
|
||||
const reconstructState = (ctx: ExtensionContext) => {
|
||||
todos = [];
|
||||
nextId = 1;
|
||||
|
||||
for (const entry of ctx.sessionManager.getBranch()) {
|
||||
if (entry.type !== "message") continue;
|
||||
const msg = entry.message;
|
||||
if (msg.role !== "toolResult" || msg.toolName !== "todo") continue;
|
||||
|
||||
const details = msg.details as TodoDetails | undefined;
|
||||
if (details) {
|
||||
todos = details.todos;
|
||||
nextId = details.nextId;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Reconstruct state on session events
|
||||
pi.on("session_start", async (_event, ctx) => reconstructState(ctx));
|
||||
pi.on("session_tree", async (_event, ctx) => reconstructState(ctx));
|
||||
|
||||
// Register the todo tool for the LLM
|
||||
pi.registerTool({
|
||||
name: "todo",
|
||||
label: "Todo",
|
||||
description: "Manage a todo list. Actions: list, add (text), toggle (id), clear",
|
||||
parameters: TodoParams,
|
||||
|
||||
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
||||
switch (params.action) {
|
||||
case "list":
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: todos.length
|
||||
? todos.map((t) => `[${t.done ? "x" : " "}] #${t.id}: ${t.text}`).join("\n")
|
||||
: "No todos",
|
||||
},
|
||||
],
|
||||
details: { action: "list", todos: [...todos], nextId } as TodoDetails,
|
||||
};
|
||||
|
||||
case "add": {
|
||||
if (!params.text) {
|
||||
return {
|
||||
content: [{ type: "text", text: "Error: text required for add" }],
|
||||
details: { action: "add", todos: [...todos], nextId, error: "text required" } as TodoDetails,
|
||||
};
|
||||
}
|
||||
const newTodo: Todo = { id: nextId++, text: params.text, done: false };
|
||||
todos.push(newTodo);
|
||||
return {
|
||||
content: [{ type: "text", text: `Added todo #${newTodo.id}: ${newTodo.text}` }],
|
||||
details: { action: "add", todos: [...todos], nextId } as TodoDetails,
|
||||
};
|
||||
}
|
||||
|
||||
case "toggle": {
|
||||
if (params.id === undefined) {
|
||||
return {
|
||||
content: [{ type: "text", text: "Error: id required for toggle" }],
|
||||
details: { action: "toggle", todos: [...todos], nextId, error: "id required" } as TodoDetails,
|
||||
};
|
||||
}
|
||||
const todo = todos.find((t) => t.id === params.id);
|
||||
if (!todo) {
|
||||
return {
|
||||
content: [{ type: "text", text: `Todo #${params.id} not found` }],
|
||||
details: {
|
||||
action: "toggle",
|
||||
todos: [...todos],
|
||||
nextId,
|
||||
error: `#${params.id} not found`,
|
||||
} as TodoDetails,
|
||||
};
|
||||
}
|
||||
todo.done = !todo.done;
|
||||
return {
|
||||
content: [{ type: "text", text: `Todo #${todo.id} ${todo.done ? "completed" : "uncompleted"}` }],
|
||||
details: { action: "toggle", todos: [...todos], nextId } as TodoDetails,
|
||||
};
|
||||
}
|
||||
|
||||
case "clear": {
|
||||
const count = todos.length;
|
||||
todos = [];
|
||||
nextId = 1;
|
||||
return {
|
||||
content: [{ type: "text", text: `Cleared ${count} todos` }],
|
||||
details: { action: "clear", todos: [], nextId: 1 } as TodoDetails,
|
||||
};
|
||||
}
|
||||
|
||||
default:
|
||||
return {
|
||||
content: [{ type: "text", text: `Unknown action: ${params.action}` }],
|
||||
details: {
|
||||
action: "list",
|
||||
todos: [...todos],
|
||||
nextId,
|
||||
error: `unknown action: ${params.action}`,
|
||||
} as TodoDetails,
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
renderCall(args, theme, _context) {
|
||||
let text = theme.fg("toolTitle", theme.bold("todo ")) + theme.fg("muted", args.action);
|
||||
if (args.text) text += ` ${theme.fg("dim", `"${args.text}"`)}`;
|
||||
if (args.id !== undefined) text += ` ${theme.fg("accent", `#${args.id}`)}`;
|
||||
return new Text(text, 0, 0);
|
||||
},
|
||||
|
||||
renderResult(result, { expanded }, theme, _context) {
|
||||
const details = result.details as TodoDetails | undefined;
|
||||
if (!details) {
|
||||
const text = result.content[0];
|
||||
return new Text(text?.type === "text" ? text.text : "", 0, 0);
|
||||
}
|
||||
|
||||
if (details.error) {
|
||||
return new Text(theme.fg("error", `Error: ${details.error}`), 0, 0);
|
||||
}
|
||||
|
||||
const todoList = details.todos;
|
||||
|
||||
switch (details.action) {
|
||||
case "list": {
|
||||
if (todoList.length === 0) {
|
||||
return new Text(theme.fg("dim", "No todos"), 0, 0);
|
||||
}
|
||||
let listText = theme.fg("muted", `${todoList.length} todo(s):`);
|
||||
const display = expanded ? todoList : todoList.slice(0, 5);
|
||||
for (const t of display) {
|
||||
const check = t.done ? theme.fg("success", "✓") : theme.fg("dim", "○");
|
||||
const itemText = t.done ? theme.fg("dim", t.text) : theme.fg("muted", t.text);
|
||||
listText += `\n${check} ${theme.fg("accent", `#${t.id}`)} ${itemText}`;
|
||||
}
|
||||
if (!expanded && todoList.length > 5) {
|
||||
listText += `\n${theme.fg("dim", `... ${todoList.length - 5} more`)}`;
|
||||
}
|
||||
return new Text(listText, 0, 0);
|
||||
}
|
||||
|
||||
case "add": {
|
||||
const added = todoList[todoList.length - 1];
|
||||
return new Text(
|
||||
theme.fg("success", "✓ Added ") +
|
||||
theme.fg("accent", `#${added.id}`) +
|
||||
" " +
|
||||
theme.fg("muted", added.text),
|
||||
0,
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
case "toggle": {
|
||||
const text = result.content[0];
|
||||
const msg = text?.type === "text" ? text.text : "";
|
||||
return new Text(theme.fg("success", "✓ ") + theme.fg("muted", msg), 0, 0);
|
||||
}
|
||||
|
||||
case "clear":
|
||||
return new Text(theme.fg("success", "✓ ") + theme.fg("muted", "Cleared all todos"), 0, 0);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// Register the /todos command for users
|
||||
pi.registerCommand("todos", {
|
||||
description: "Show all todos on the current branch",
|
||||
handler: async (_args, ctx) => {
|
||||
if (ctx.mode !== "tui") {
|
||||
ctx.ui.notify("/todos requires interactive mode", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
await ctx.ui.custom<void>((_tui, theme, _kb, done) => {
|
||||
return new TodoListComponent(todos, theme, () => done());
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -52,7 +52,7 @@
|
||||
"reasoning": false,
|
||||
"input": ["text", "image"],
|
||||
"contextWindow": 24576,
|
||||
"maxTokens": 8192,
|
||||
"maxTokens": 4096,
|
||||
"cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 }
|
||||
},
|
||||
{
|
||||
@@ -62,7 +62,7 @@
|
||||
"compat": { "thinkingFormat": "qwen-chat-template" },
|
||||
"input": ["text", "image"],
|
||||
"contextWindow": 24576,
|
||||
"maxTokens": 8192,
|
||||
"maxTokens": 4096,
|
||||
"cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 }
|
||||
},
|
||||
{
|
||||
@@ -75,6 +75,16 @@
|
||||
"maxTokens": 8192,
|
||||
"cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 }
|
||||
},
|
||||
{
|
||||
"id": "Qwen3.8-27B-UD-IQ3_XXS",
|
||||
"name": "Qwen3.8 27B · 24k · vision — hybrid reasoner",
|
||||
"reasoning": true,
|
||||
"compat": { "thinkingFormat": "qwen-chat-template" },
|
||||
"input": ["text", "image"],
|
||||
"contextWindow": 24576,
|
||||
"maxTokens": 8192,
|
||||
"cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 }
|
||||
},
|
||||
{
|
||||
"id": "GLM-4.7-Flash-UD-Q4_K_XL",
|
||||
"name": "GLM-4.7 Flash · 24k — quality coder",
|
||||
@@ -82,7 +92,7 @@
|
||||
"compat": { "thinkingFormat": "qwen-chat-template" },
|
||||
"input": ["text"],
|
||||
"contextWindow": 24576,
|
||||
"maxTokens": 8192,
|
||||
"maxTokens": 4096,
|
||||
"cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 }
|
||||
},
|
||||
{
|
||||
@@ -157,7 +167,7 @@
|
||||
"reasoning": false,
|
||||
"input": ["text", "image"],
|
||||
"contextWindow": 24576,
|
||||
"maxTokens": 8192,
|
||||
"maxTokens": 4096,
|
||||
"cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 }
|
||||
},
|
||||
{
|
||||
@@ -167,7 +177,7 @@
|
||||
"compat": { "thinkingFormat": "qwen-chat-template" },
|
||||
"input": ["text", "image"],
|
||||
"contextWindow": 24576,
|
||||
"maxTokens": 8192,
|
||||
"maxTokens": 4096,
|
||||
"cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 }
|
||||
},
|
||||
{
|
||||
@@ -180,6 +190,16 @@
|
||||
"maxTokens": 8192,
|
||||
"cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 }
|
||||
},
|
||||
{
|
||||
"id": "Qwen3.8-27B-UD-IQ3_XXS",
|
||||
"name": "Qwen3.8 27B · 24k · vision — hybrid reasoner",
|
||||
"reasoning": true,
|
||||
"compat": { "thinkingFormat": "qwen-chat-template" },
|
||||
"input": ["text", "image"],
|
||||
"contextWindow": 24576,
|
||||
"maxTokens": 8192,
|
||||
"cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 }
|
||||
},
|
||||
{
|
||||
"id": "GLM-4.7-Flash-UD-Q4_K_XL",
|
||||
"name": "GLM-4.7 Flash · 24k — quality coder",
|
||||
@@ -187,7 +207,7 @@
|
||||
"compat": { "thinkingFormat": "qwen-chat-template" },
|
||||
"input": ["text"],
|
||||
"contextWindow": 24576,
|
||||
"maxTokens": 8192,
|
||||
"maxTokens": 4096,
|
||||
"cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 }
|
||||
},
|
||||
{
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"defaultThinkingLevel": "off",
|
||||
"theme": "catppuccin-mocha",
|
||||
"quietStartup": false,
|
||||
"collapseChangelog": true,
|
||||
"defaultProjectTrust": "ask",
|
||||
"enableInstallTelemetry": false,
|
||||
"enabledModels": [
|
||||
@@ -12,8 +13,8 @@
|
||||
],
|
||||
"compaction": {
|
||||
"enabled": true,
|
||||
"reserveTokens": 6144,
|
||||
"keepRecentTokens": 6000
|
||||
"reserveTokens": 8192,
|
||||
"keepRecentTokens": 4000
|
||||
},
|
||||
"retry": {
|
||||
"enabled": true,
|
||||
|
||||
Reference in New Issue
Block a user