SLOPSHOPPER

sprag-statusline

Draws sprag's statusline above the prompt in the Claude desktop app, which never runs the statusLine command, and keeps the snapshots the statusline leaves…

newbandprocesstimer
★ 3v0.1.0Apache-2.0updated 2026-10-12rootstudioyaml/sprag/mod/sprag-statusline
A shopper browsing a rack in a slop shop
README

<img alt="Sprag" src="https://raw.githubusercontent.com/rootstudioyaml/sprag/main/site/assets/logo/sprag-lockup-light.svg" width="220">

Cut the tokens Claude Code and Codex spend, from the first session.

npm downloads license

English · 한국어

sprag.io · Benchmark

<a href="https://sprag.io/assets/video/sprag-compare-en.mp4"><img src="https://raw.githubusercontent.com/rootstudioyaml/sprag/main/docs/sprag-compare-en.gif" width="760" alt="Two terminals receive the same twelve requests. With Sprag, lookups and test runs go to cheaper models and documents go in as Markdown; the day ends at $1.83 against $6.68 without Sprag."></a>

Same 12 requests, two terminals: $6.68 → $1.83 (−72.6%) with Sprag.<br> <sub>Example requests priced at public list rates. <a href="https://sprag.io/assets/video/sprag-compare-en.mp4">Full video (41s)</a></sub>


Sprag saves tokens without changing how you work. Lookups, checks and test runs go to a cheaper model, documents reach the context as compact Markdown (a 540k-token deck becomes 23k tokens), and the statusline shows what each session spent and saved while it runs. npm i -g sprag-cli sets it all up; nothing to configure per project.

Sprag is named after the sprag clutch: forward motion passes, backspin locks. Your agent keeps working, it just stops regressing.

It reads the sessions you already ran, and what it finds there holds for every session after. Repeated failures become rules loaded at session start. Work that a cheaper tier has never got wrong goes to a sub-agent on that tier, and every delegated run writes its own price difference to a ledger. Cache and rate-limit trouble reaches your statusline while you can still act on it.

Session analysis runs locally on the logs Claude Code and Codex already write. No extra model calls or API key are needed for analysis. Optional Codex gateway budget and price lookups contact your configured LiteLLM endpoint using its existing credentials. The CLI requires Node.js 18+ and one TOML parser dependency.

npm i -g sprag-cli   # Claude Code, plus Codex when it is installed

After you install

  1. Run npm i -g sprag-cli. Hooks, the statusline and the starter rules are set up for every project; there is nothing to configure per project. If you already have a statusline, the install asks whether to show both, replace yours, or keep yours.
  2. Open Claude Code in any project and work as usual.
  3. Watch the statusline. It shows what the session spent and saved, and a warning chip leads the line when the cache, the context or a rate limit needs attention:
🔀 Routing saved $188  |  opus→sonnet ×67 $93.4 · fable→sonnet ×26 $83.7 · opus→haiku ×25 $7.41 · fable→haiku ×6 $3.73
🧠 Cache hit 98.8% · ⏳ Cache expires 59:46 · 🅷 5/5 · 📦 Ctx 47% of 1M

Sprag never swaps the model you picked. Analysis reads your local session logs only, with no extra model calls and no API key. sprag uninstall removes everything it added and restores a statusline it merged with.

Using Codex? See Codex below.

How it works

<img src="https://raw.githubusercontent.com/rootstudioyaml/sprag/main/docs/how-it-works.png" alt="What goes in: your past sessions, the task you just asked for, a document you would attach, Korean prose being written, and the tokens this turn is spending. What sprag does with it: writes a ratchet rule loaded every session, converts the document, checks the Korean as you write, prints the cost in your prompt, and routes a matched task to a haiku or sonnet sub-agent whose output the main agent reviews.">

Everything on the left is something your session already produces. Nothing is sent anywhere to read it. The tier decision at the bottom is the whole routing argument in three tests: where the answer lives, whether a mistake surfaces, and what error rate the rule was measured at.

Sprag does not swap the model you asked for. It reads what you have already run, finds the kinds of task that never went wrong on a cheaper tier, and spawns a sub-agent for exactly those. The main agent reviews what comes back before anything counts as done, and the work returns to it the moment a rule stops holding.

Measured results

MetricResultEvidence
Public benchmark accuracy59.1% vs 57.9% best single model▰▰▰▰▰▰▰▰▰▰▰▰benchmark
Cost for the same queries$268 vs gpt-5 $388, gemini-2.5-pro $734▰▰▰▰▰▱▱▱▱▱▱▱benchmark
Tokens for the same documents−95.9%, 2,011,178 → 82,209 over 12 files▰▱▱▱▱▱▱▱▱▱▱▱doc2md
Delegation savings, from the ledger$188.27 over 124 delegated runs▰▰▰▰▰▰▰▰▰▰▰▱sprag route-scan savings
Cheap tier on the same prompts24% of the cost, one answer worse out of eight▰▰▰▱▱▱▱▱▱▱▱▱A/B run

The first two rows answer the objection that comes up first: that moving work to a cheaper model has to cost you accuracy. It did not. The same criteria, wired up as a router, scored 1.2 points above the best single model at a third of the cost. Nothing is best at everything, which is why choosing per task beats choosing once.

Measured offline on LLMRouterBench (Findings of ACL 2026): 11,696 instances, 13 models whose answers, scores and costs ship with the dataset. No model was called and nothing was spent. The router chose; the numbers were looked up. Run it again and you get the same figures. It validates the criteria, not sprag's own routing: the dataset has no Claude models, so deepseek-v3, qwen3-235b and gpt-5 stood in for cheap, mid and flagship.

The last three rows are ledgers on this machine. They grow as it runs. The routing figures were read on 2026-10-09, the document figures on 2026-09-18. Model names are the ones each run actually used.

Current, on one machine, last 30 days

🔀 Routing saved $188.27 total        (last 7d $13.31 · last 30d $150.64)

   fable-5 → sonnet-5      24 runs    $81.19   ▰▰▰▰▰▰▰▰▰▰▰▰
   opus-5  → sonnet-5      54 runs    $68.85   ▰▰▰▰▰▰▰▰▰▰▱▱
   opus-5  → sonnet-5.5    13 runs    $24.58   ▰▰▰▰▱▱▱▱▱▱▱▱
   opus-5  → haiku-4.5     25 runs     $7.41   ▰▱▱▱▱▱▱▱▱▱▱▱
   fable-5 → haiku-4.5      5 runs     $3.22   ▰▱▱▱▱▱▱▱▱▱▱▱
   fable-5 → sonnet-5.5     2 runs     $2.51   ▰▱▱▱▱▱▱▱▱▱▱▱
   fable-5 → haiku-5.5      1 run      $0.51   ▰▱▱▱▱▱▱▱▱▱▱▱
Also measuredOver 30 days
Documents converted12 files, 2,011,178 → 82,209 tokens▰▱▱▱▱▱▱▱▱▱▱▱
Cache hit rate92.2%▰▰▰▰▰▰▰▰▰▰▰▱
Volume behind both175 sessions · 14,682 API calls · 3.81B input tokens

Real output, from sprag route-scan savings, sprag --days 30, and the doc2md ledger. Nothing here is a projection.

Every delegated run records its own price difference, so sprag route-scan savings traces any amount back to the rule that caused it. What the ledger deliberately leaves out is written down in the command reference.

Research behind the tier rules

The tier criteria are not a hunch. Each one traces back to published routing research, and the percentages quoted in the rules (28% vs 54% recall, a +63pp gap on chained tool work) are sprag's own offline measurement on the datasets below.

CriterionWhat the paper foundPaper
Delegate down, conservativelyAs budgets grow, learned routers default to the most expensive model even when a cheaper one already suffices.Lai & Ye, When Routing Collapses: On the Degenerate Convergence of LLM Routers (2026)
Judge difficulty by outcomeEvery router on the board falls short of the oracle, mainly because it cannot tell when a smaller model is enough. Query difficulty is defined by how many of 42 models actually answered it.Lu et al., RouterArena: An Open Platform for Comprehensive Comparison of LLM Routers (2025)
Thresholds from your own logsRouting compares a predicted win rate against a cost threshold, and results are read by the share of calls sent to the strong model. sprag sets that threshold from your last 14 days at every scan.Ong et al., RouteLLM: Learning to Route LLMs with Preference Data (2024)
Escalate on failureA cascade tries the cheap model first and lets a check on the answer decide whether to go up. It matched GPT-4 with up to 98% lower cost.Chen, Zaharia & Zou, FrugalGPT (2023)
Where the answer livesShort-form factual recall measured on its own. The 28% vs 54% recall gap comes from this class of set.Wei et al., Measuring short-form factuality in large language models (SimpleQA) (2024)
Tool orchestration stays upAgents must coordinate tool calls with a user in a shared environment. The +63pp gap was measured here.Barres et al., τ²-Bench (2025)
The public benchmark run400K instances, 21 datasets, 33 models. Several routers, commercial ones included, fail to beat a simple baseline.Li et al., LLMRouterBench (Findings of ACL 2026)
Routing vs cascadingA taxonomy of when a routing decision is made, what it reads, and how it is computed.Moslem & Kelleher, Dynamic Model Routing and Cascading for Efficient LLM Inference: A Survey (2026)

Two criteria have no paper behind them. Batching tasks that read the same files and splitting design from code writing come from sprag's own cost accounting: on a sub-agent, loading context costs more than generating. The full reasoning, in Korean, is in Tier criteria.

Everything ships in one install, working from day one

What it doesMore
⚙️ Ratchet rulesRepeated failures become one-line rules loaded every session. Candidates are detected from your logs, and you choose project or global scope.harness
🔀 Model fittingLog-driven delegation rules carrying the error rate they were measured at and the savings they reported, written to ratchet-model.md.route-scan
🅷 Harness scoreFive operating principles checked live. Skip the verify step and 🅷 4/5 says so before you report done.harness
📊 Token telemetryCache hit rate, TTL, context size, output spikes and both rate-limit windows, in the prompt every turn.statusline
📄 doc2mdpptx, xlsx, pdf, docx and fig converted on demand, so a document costs one read instead of your whole context.doc2md
🇰🇷 Style gatesWrite-time prose lint enforced by hook. Shipping today for Korean technical writing: double passives, translationese, cohesion.style

The two savings figures are never added together. They measure different things: the routing ledger records the price difference on delegated runs, and the conversion ledger records tokens a document did not cost.

Neither one counts what the harness and the ratchet rules save. That saving arrives as fewer round-trips rather than as cheaper ones.

The statusline in one line

🔀 Routing saved $188  |  opus→sonnet ×67 $93.4 · fable→sonnet ×26 $83.7 · opus→haiku ×25 $7.41 · fable→haiku ×6 $3.73
🚨 5H ▰▰▰▰▰▰▰▰▰▰▰▱ 94% 🔄 12:36 · 🅷 5/5 · 🤖 Opus 5.5 · 🧠 Cache hit 98.8% · ⏳ Cache expires 59:46 · 📅 weekly ▰▰▰▰▰▱▱▱▱▱▱▱ 38% · 💵 Sep $42 · 📦 Ctx 47% of 1M

When something is wrong the warning chip leads the line: 🚨 5H/7D NN%, ⚠ Ctx 500k+, ⚠ Cache miss, ⚠ Input spike, ⚠ Output heavy, ⚠ Call surge, ⚠ Rebuild churn, ⚠ 5m TTL. Paste the chip text into Claude and the Skill names the root-cause code and the fix. Every segment is explained in the statusline reference.

Inside a JetBrains IDE terminal the chips render as single-cell glyphs instead of emoji (◉ Cache hit 98.8% · ◧ Ctx 47% of 1M). The IDE's default font has no emoji glyphs, so emoji leave characters from the previous frame on screen, like Cache expires 4:545. Label modes covers the detail, plus sprag mode narrow and mode icon-force.

The cheap tier on the same prompts

The benchmark uses public data. This asks the same question of my own work: eight prompts from real sessions in this repository, sent to both tiers, graded against an answer written before the run (2026-09-17, raw data in video/out/measure2.json).

flagship (opus-5)cheap tier (haiku-4.5)
Cost for all eight$2.5652$0.6132 (23.9%)
Graded correct7 / 86 / 8
Gave the same answer6 / 8

The prompt both tiers missed is the useful one. It asked which files reference process.env without saying whether subdirectories counted, and the two read it differently. Adding that one condition made their answers identical. The question was ambiguous, not hard.

The remaining miss is the cheap tier leaving entries out of a file listing. A wrong listing is one you can see is wrong, which is the second criterion doing its job.

So the cheap tier is not free. On this sample one lookup in eight comes back worse, for a quarter of the price. That is why rules only send work whose failure surfaces, and never send design or diagnosis.

What the method has produced here

MeasuredValueSampleWindow
Documents converted2,011,178 → 82,209 tokens12 files (xlsx, pptx, pdf)to 2026-09-18
Delegated runs$188.27 saved, $1.52 per run124 runsto 2026-10-09
Ratchet rules accumulated37 (30 global, 7 per-project)4 repositories2026-05-08 to 09-16

The two counts grow differently. Ratchet rules come from mistakes a person made, so they keep coming. Delegation rules stop: the criteria recognize a fixed set of work types, and once each has a rule there is nothing left to promote.

Codex

Native hooks, an AGENTS.md harness, scoped ratchet rules, Korean lint, document conversion, token reports, and a session-bound companion panel. npm i -g sprag-cli sets Codex up too when it is installed, with the rule presets registered globally and nothing asked (sprag install --manual asks at each step). For a Codex-only install that leaves Claude Code alone:

npm i -g sprag-cli --ignore-scripts
sprag install --agent codex
sprag --agent codex --days 7
sprag panel --agent codex  # live companion in a separate terminal
sprag --statusline --agent codex --text
sprag doctor --agent codex  # troubleshooting only
sprag route-scan --agent codex
sprag seed --agent codex

Review and trust the new hooks in Codex's /hooks menu; Codex requires that step. Commands still default to Claude Code; add --agent codex to pick Codex. Codex also supports session briefings, warning history, handoffs, and opt-in model routing and subagent guidance (sprag delegate on --agent codex). The native Codex footer does not accept shell commands; Sprag provides a terminal statusline and panel. Recurring simple turns become route candidates; repeated tool failures become ratchet candidates. Both require your approval before a rule is registered. seed offers compatible starter rules with separate Codex decisions. sprag route-scan savings --agent codex records only Sprag-routed child runs and estimates their price difference when matching LiteLLM prices are available. It is not a billing measurement. Unknown prices remain n/a, and Codex document counts stay separate from Claude estimates. The historical dollar figures below are from Claude Code, not a Codex benchmark. See Codex support and limits, or run sprag capabilities --agent codex for the support matrix.

WorkflowClaude CodeCodex
Session instructionsCLAUDE.md and hooksAGENTS.md and native hooks
Live telemetryNative statusline commandCompanion panel or tmux layout
Rule candidatesClaude transcripts and tier presetsCodex rollouts and compatible ratchet presets
Model routingHaiku/Sonnet tier rulesExplicit provider model IDs and scoped rules
Routing ledgerClaude run estimatesAttributed child estimates with LiteLLM prices

Both integrations coexist. Commands without --agent codex retain Claude behavior.

statusline example: routing savings on row 1, document conversion savings on row 2, diagnostics on row 3

Day-to-day commands

CommandWhat it does
spragLast-30-day diagnostic report
sprag handoffBack work up to markdown before a cap blocks you
sprag route-scanFind delegation candidates in your own history (0 LLM calls)
sprag route-scan savingsThe routing-savings ledger
sprag harness checkCurrent 🅷 harness score
sprag doc2md <file>Convert one document to Markdown

Every subcommand, flag and the output-language setting: command reference.

Documentation

DocCovers
CodexInstallation, panel, native hooks, automatic candidates, presets, and accounting limits
InstallWhat the install turns on, what stays manual, how to turn each part off
StatuslineSegments, warning chips, spike issue codes, update notifications
CommandsFull CLI surface
HarnessThe five principles, rule promotion, compact-window pinning, seed presets
route-scanTier verdicts, rule-file mechanics, scan triggers, subagent setup
Tier criteriaWhy T0/T1/T2 split where they do, with sources (Korean)
BenchmarkThe 11,696-instance LLMRouterBench run
doc2mdSupported formats, savings evidence, editing documents, DRM cases
Korean styleWhat gets injected, the write-time check, before/after
Not a routerWhy realtime routing breaks the cache and costs more
Gateways & environmentBedrock, Vertex, LiteLLM budgets, pricing table, FAQ, how it works
Reduce Claude Code tokensCompact/clear timing, subagent exploration, document conversion, cache TTL
Claude Code usage limitsThe 5-hour and 7-day windows, watching both, cutting per-turn cost
Codex token usageSession scoping, native subagent delegation, document conversion for Codex
CompareSprag vs ccusage, claude-usage, and statusline tools like CCometixLine

Behind a LiteLLM gateway, sprag profile-map --refresh now reads model aliases straight from the gateway's own GET /model/info, so route-scan no longer needs to wait on learned votes or a hand-edited profile-map.json. The refresh uses the same 5-minute check and 24-hour cache as the LiteLLM budget gauge. It never stores your auth token: each refresh reads it from apiKeyHelper at call time and lets it expire on the helper's own schedule. Details: route-scan.

Release notes

The full history moved to CHANGELOG.md (Korean; version headings and command names are language-neutral). Recent changes:

  • v3.54.0: Codex integration adds native hooks, AGENTS.md harness rules, a session-bound companion panel, document conversion, diagnostics, and opt-in delegation. Codex usage accounting stays separate from Claude Code. See the Codex guide.
  • v3.39.0: feedback subcommand for filing bug reports and feature requests straight from the terminal or a Claude session, via the gh CLI, an anonymous no-login form (auto-filed as a GitHub issue by an Apps Script relay), or a local fallback. install now asks before replacing an existing statusline instead of silently skipping.
  • v3.38.0: cohesion on turns the language-neutral cohesion rules from the Korean supplement into a standalone English injection (given-before-new, one referent per pronoun, subject consistency, bridging, merging choppy sentences). Opt-in, ~0.5k tokens per session, suppressed while korean on already carries them.
  • v3.37.0: Korean guidance grows a conservative supplement (translationese, AI-writing tics, a research-backed cohesion section whose principles apply to English prose too) and the write-time lint gains 5 translationese patterns, validated at 1 false positive across 255 real files.
  • v3.35.0: A 💵 Sep $42 segment now shows estimated spend since 00:00 on the 1st of the current month, always on, including gateway setups with no 5h/7d caps. LiteLLM gateway users get a 🔑 budget ▰▱ 34% $34/$100 gauge built from the key's budget (GET /key/info + GET /user/info, team-membership budget first, then key, then internal user, verified against a Dockerized LiteLLM).
  • v3.34.0: seed presets offered one at a time, output-language choice at install, context warning raised to 500k.

Feedback

Bugs and feature requests go in the issue tracker: https://github.com/rootstudioyaml/sprag/issues

With no browser or GitHub login at hand (corporate network, mid-session), submit straight from the terminal instead, or ask Claude to do it for you:

sprag feedback "the 5m TTL chip never clears on Bedrock"

It files a GitHub issue through the gh CLI when one is authenticated. Otherwise

Source 2 files
hooks/register.tsx 177 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register, SessionUsage } from 'claude-code'
3
4/**
5 * sprag-statusline: sprag's statusline in the Claude desktop app.
6 *
7 * The desktop app's Code tab runs claude in stream-json mode, which never runs
8 * settings.json's statusLine.command. Hooks and mods do run there, so this mod
9 * runs the same sprag command itself and draws its text in the band above the
10 * prompt. Running the real CLI also leaves what a statusline render leaves: the
11 * rate-limit snapshot and the context window size that the delegation briefing
12 * reads back.
13 *
14 * The terminal has its own statusline, so the band is drawn on the desktop
15 * surface only. Nothing here may slow or break a session: a run is skipped
16 * while another is in flight, cut off after a few seconds, and a failure keeps
17 * the last text on screen.
18 */
19
20const lines = atom({ plugin: 'sprag-statusline', key: 'lines' } as const, [] as string[])
21
22const REFRESH_MS = 5000
23const RUN_TIMEOUT_MS = 3000
24const DEFAULT_ARGV = ['sprag', '--statusline', '--no-color']
25
26// The executable names src/installer.js's argsAfterOurExecutable accepts
27const OUR_EXECUTABLES = ['sprag', 'claude-token-saver', 'sprag-cli']
28const RUNNERS = /^(npx|bunx|pnpx)$/
29const RUNNER_SUBCOMMANDS: Record<string, string[]> = {
30  pnpm: ['dlx', 'exec'],
31  yarn: ['dlx', 'run'],
32  bun: ['x', 'run'],
33  npm: ['exec'],
34}
35
36/** Whether a statusLine command starts with sprag's executable, as the installer judges it. */
37function isOurCommand(tokens: string[]): boolean {
38  let i = 0
39  if (RUNNERS.test(tokens[0] ?? '')) i = 1
40  else if (RUNNER_SUBCOMMANDS[tokens[0] ?? '']?.includes(tokens[1] ?? '')) i = 2
41  const token = tokens[i]
42  if (!token) return false
43  const exe = (token.split(/[/\\]/).pop() ?? '').replace(/\.(cmd|exe|ps1)$/i, '')
44  return OUR_EXECUTABLES.includes(exe)
45}
46
47/**
48 * The command to run: the user's own sprag statusLine command with --no-color
49 * added, or the default one. This module has no shell, so the command is split
50 * on whitespace, the same way the installer reads it; a command with quoting
51 * falls back to the default rather than being run in pieces.
52 */
53export function statuslineArgv(settings: Readonly<Record<string, unknown>>): string[] {
54  const statusLine = settings.statusLine as { command?: unknown } | undefined
55  const command = typeof statusLine?.command === 'string' ? statusLine.command.trim() : ''
56  if (!command || /["'`$;|&<>\\]/.test(command)) return DEFAULT_ARGV
57  const tokens = command.split(/\s+/)
58  if (!isOurCommand(tokens)) return DEFAULT_ARGV
59  return tokens.includes('--no-color') ? tokens : [...tokens, '--no-color']
60}
61
62/**
63 * The JSON Claude Code's statusline would have been fed. Fields the engine has
64 * no value for are left out, never zeroed: sprag reads an absent rate_limits as
65 * "no data" but a 0 as "0% used". transcript_path is left out because the mod
66 * cannot know it, and sprag takes it only for the effort chip.
67 */
68export function statuslinePayload(input: {
69  sessionId: string
70  cwd: string
71  model: string
72  usage: SessionUsage
73}) {
74  const { usage } = input
75  const payload: Record<string, unknown> = {
76    session_id: input.sessionId,
77    cwd: input.cwd,
78    workspace: { current_dir: input.cwd, project_dir: input.cwd },
79    model: { id: input.model, display_name: input.model },
80  }
81  if (usage.context.percent !== undefined) {
82    payload.context_window = {
83      context_window_size: usage.context.window,
84      used_percentage: usage.context.percent,
85    }
86  }
87  if (usage.rateLimits.length) {
88    const limits: Record<string, { used_percentage: number; resets_at?: number }> = {}
89    for (const limit of usage.rateLimits) {
90      const at = limit.resetsAt ? Math.floor(Date.parse(limit.resetsAt) / 1000) : NaN
91      limits[limit.kind] = {
92        used_percentage: limit.percentUsed,
93        ...(Number.isFinite(at) ? { resets_at: at } : {}),
94      }
95    }
96    payload.rate_limits = limits
97  }
98  if (usage.cost) payload.cost = { total_cost_usd: usage.cost.usd }
99  return payload
100}
101
102/** Output with stray ANSI escapes removed and blank lines dropped. */
103export function cleanOutput(text: string): string[] {
104  return text
105    .replace(/\u001b\[[0-9;?]*[ -/]*[@-~]/g, '')
106    .replace(/\u001b\][^\u0007]*(\u0007|\u001b\\)/g, '')
107    .split(/\r?\n/)
108    .map(line => line.trimEnd())
109    .filter(line => line.trim() !== '')
110}
111
112let isRunning = false
113let timer: { cancel: () => void } | undefined
114
115/** Runs the statusline once and stores its text; any failure leaves the last text as it was. */
116async function refresh($: EngineInterface): Promise<void> {
117  if (isRunning) return
118  isRunning = true
119  try {
120    if (!(await $.session.surfaces()).includes('desktop')) return
121    const [sessionId, cwd, model, usage, settings] = await Promise.all([
122      $.session.id(),
123      $.session.cwd(),
124      $.session.model(),
125      $.session.usage(),
126      $.settings.read(),
127    ])
128    const run = await $.process.run(statuslineArgv(settings), {
129      stdin: JSON.stringify(statuslinePayload({ sessionId, cwd, model, usage })),
130      timeoutMs: RUN_TIMEOUT_MS,
131    })
132    if (run.exitCode !== 0) return
133    const next = cleanOutput(run.stdout)
134    if (!next.length) return
135    // An unchanged text is not written, so the band is not redrawn every tick
136    const same = JSON.stringify(await read($, lines)) === JSON.stringify(next)
137    if (!same) await update($, lines, () => next)
138  } catch {
139    // The statusline is a convenience; a refused or timed-out run is not worth an error
140  } finally {
141    isRunning = false
142  }
143}
144
145export const register: Register = on => {
146  on('session.start', ($, e, next) => {
147    timer?.cancel()
148    timer = $.clock.every(REFRESH_MS, () => void refresh($))
149    void refresh($)
150    return next(e)
151  })
152
153  // Not awaited: a slow statusline must never hold a turn open
154  on('turn.complete', ($, e, next) => {
155    void refresh($)
156    return next(e)
157  })
158
159  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
160    if (e.surface !== 'desktop' || e.props.hasSurvey) return next(e)
161    const shown = (await read($, lines)).slice(0, Math.max(0, e.props.maxRows))
162    if (!shown.length) return next(e)
163
164    const { Box, Text } = $.ui.resolve(e)
165    // One statusline line per row; a Box lays its children out in a row unless told otherwise
166    return (
167      <Box flexDirection="column">
168        {shown.map((line, i) => (
169          <Text key={`line-${i}`} dimColor>
170            {line}
171          </Text>
172        ))}
173      </Box>
174    )
175  })
176}
177
types/index.d.ts 6 lines
1declare module 'claude-code' {
2  interface PluginState {
3    'sprag-statusline': { lines: string[] }
4  }
5}
6