feat(statusline): add user-extensible statusline sections#1011
feat(statusline): add user-extensible statusline sections#1011gehnster wants to merge 2 commits intodanielmiessler:mainfrom
Conversation
Users can add custom sections to the statusline by creating PAI/USER/STATUSLINE/extensions.sh with two functions: user_statusline_prefetch and user_statusline_display. Includes a self-healing SessionStart hook that re-injects extension wiring if statusline-command.sh is overwritten during upgrades. Adds 23 automated tests. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds an upgrade-safe user extension architecture to the PAI statusline, allowing users to inject custom statusline sections via PAI/USER/STATUSLINE/extensions.sh, with a SessionStart hook that re-applies wiring after upgrades overwrite statusline-command.sh.
Changes:
- Adds extension wiring points to
statusline-command.sh(source user extensions, prefetch in parallel block, source prefetch results, display hook). - Introduces
StatuslineExtensions.hook.tsto self-heal wiring on SessionStart, and registers it insettings.json. - Adds documentation and a bun test suite validating the extension contract, wiring, and hook idempotency.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| Releases/v4.0.3/.claude/tests/statusline-extensions.test.ts | New bun:test suite covering extension contract + hook patching behavior |
| Releases/v4.0.3/.claude/statusline-command.sh | Adds extension source/prefetch/results/display wiring points |
| Releases/v4.0.3/.claude/settings.json | Registers StatuslineExtensions SessionStart hook |
| Releases/v4.0.3/.claude/PAI/USER/STATUSLINE/README.md | Documentation for authoring extensions + examples |
| Releases/v4.0.3/.claude/hooks/StatuslineExtensions.hook.ts | New self-healing hook that injects wiring if missing |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const PAI_DIR = process.env.PAI_DIR || join(process.env.HOME!, '.claude'); | ||
| const EXTENSIONS_DIR = join(PAI_DIR, 'PAI/USER/STATUSLINE'); | ||
| const EXTENSIONS_PATH = join(EXTENSIONS_DIR, 'extensions.sh'); | ||
| const STATUSLINE_PATH = join(PAI_DIR, 'statusline-command.sh'); | ||
| const HOOK_PATH = join(PAI_DIR, 'hooks/StatuslineExtensions.hook.ts'); |
There was a problem hiding this comment.
This test file defaults PAI_DIR to "$HOME/.claude" when PAI_DIR isn’t set, and then reads/writes real files under that directory (extensions.sh, statusline-command.sh). Running the test locally could overwrite or delete a user’s actual PAI config/statusline. Update the tests to run against a temporary isolated PAI_DIR (mkdtemp), copying in fixture versions of statusline-command.sh and the hook, so the suite never touches real user state.
| const ENV_SOURCE_LINE = '[ -f "${PAI_CONFIG_DIR:-$HOME/.config/PAI}/.env" ] && source "${PAI_CONFIG_DIR:-$HOME/.config/PAI}/.env"'; | ||
|
|
||
| function main() { | ||
| // No extensions file — nothing to wire | ||
| if (!existsSync(EXTENSIONS_PATH)) { | ||
| process.exit(0); | ||
| } | ||
|
|
||
| if (!existsSync(STATUSLINE_PATH)) { | ||
| console.error('[StatuslineExtensions] statusline-command.sh not found'); | ||
| process.exit(0); | ||
| } | ||
|
|
||
| let content = readFileSync(STATUSLINE_PATH, 'utf-8'); | ||
| let modified = false; | ||
|
|
||
| // 1. Check for source line | ||
| if (!content.includes(SOURCE_MARKER)) { | ||
| const envIdx = content.indexOf(ENV_SOURCE_LINE); | ||
| if (envIdx === -1) { | ||
| console.error('[StatuslineExtensions] Could not find .env source line to anchor injection'); | ||
| process.exit(0); | ||
| } | ||
| const insertAfter = envIdx + ENV_SOURCE_LINE.length; | ||
| const injection = `\n\n# Source user statusline extensions (upgrade-safe customizations)\n_USER_EXTENSIONS="$PAI_DIR/PAI/USER/STATUSLINE/extensions.sh"\n[ -f "$_USER_EXTENSIONS" ] && source "$_USER_EXTENSIONS"`; | ||
| content = content.slice(0, insertAfter) + injection + content.slice(insertAfter); | ||
| modified = true; |
There was a problem hiding this comment.
The hook anchors injection by searching for an exact, full-string match of the .env source line. Any small formatting change in statusline-command.sh (spacing, quoting, variable name, comment) will cause the hook to fail to inject and silently exit. Consider matching more robustly (e.g., regex for sourcing the .env file, or anchoring on a nearby comment block) so the “self-healing” behavior survives upstream script edits.
There was a problem hiding this comment.
dont know about the system enough to know what to do here
| // 3. Check for user-ext.sh source in parallel results | ||
| if (!content.includes(USER_EXT_SOURCE)) { | ||
| const lastSource = content.lastIndexOf('" ] && source "$_parallel_tmp/'); | ||
| if (lastSource !== -1) { | ||
| const lineEnd = content.indexOf('\n', lastSource); | ||
| const injection = `\n[ -f "$_parallel_tmp/user-ext.sh" ] && source "$_parallel_tmp/user-ext.sh"`; | ||
| content = content.slice(0, lineEnd) + injection + content.slice(lineEnd); | ||
| modified = true; | ||
| console.error('[StatuslineExtensions] Injected user-ext.sh source'); | ||
| } |
There was a problem hiding this comment.
The user-ext.sh source injection relies on lastIndexOf('" ] && source "$_parallel_tmp/') to find the insertion point. This is brittle to minor formatting changes in statusline-command.sh and could insert in the wrong place or not at all. Prefer a more stable anchor (e.g., insert before the rm -rf "$_parallel_tmp" line or after a named marker comment) and/or use a regex that matches the parallel results block structure.
There was a problem hiding this comment.
i honestly dont know anything about this so dont know how to move forward
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
|
sorry but it seems this might not be a great PR. I just started using AI and PAI recently but found this to be a missing part of the statusline and thought others may want it too. This was all generated with Claude and i tried to be mindful of coding standards and exisiting PRs but it seems even copilot found faults |
Summary
Adds a user extension architecture to the statusline so users can add custom sections without modifying
statusline-command.shdirectly. Extensions survive PAI upgrades.PAI/USER/STATUSLINE/extensions.shwith two functions:user_statusline_prefetch(fetch data in parallel) anduser_statusline_display(render output)SessionStarthook (StatuslineExtensions.hook.ts) auto-injects the extension wiring ifstatusline-command.shgets overwritten during an upgradebun:test) covering the extension contract, pipeline, display modes, self-healing hook, idempotency, and graceful degradationChanges to
statusline-command.sh(13 lines added)4 insertion points, zero changes to existing logic:
extensions.shif it exists (after.envsource)user_statusline_prefetchin the parallel block (guarded bytype -t)user-ext.shfrom parallel tmp alongside other resultsuser_statusline_displaybetween usage and git sections (guarded bytype -t)All calls are guarded — if no extensions file exists, zero overhead, zero output.
Why one PR instead of splitting architecture + example
The ElevenLabs voice quota example in the README demonstrates the architecture on an already-optional PAI feature (voice/TTS). Shipping the architecture without a working example makes it harder to evaluate. The example is documentation, not code in the repo — users create their own
extensions.shinPAI/USER/.Example: ElevenLabs voice quota in statusline
The README includes a complete example that shows remaining TTS characters. When
ELEVENLABS_API_KEYis set:When no API key is configured, the section produces zero output.
Self-healing hook
After an upgrade overwrites
statusline-command.sh, theStatuslineExtensions.hook.tshook runs at nextSessionStartand re-injects the 4 extension wiring points. It is:extensions.shexistsTest plan
23 automated tests via
bun test tests/statusline-extensions.test.ts:bash -nsyntax check🤖 Generated with Claude Code