SLOPSHOPPER

Limit Watch

Draws the 5-hour and weekly usage limits above the prompt, coloured by use, with reset countdowns and toasts at 80% and 95%.

newbandtoasttimer
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · limit-watch
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM 5h 31% · NaNm ⟨Claude Code's own drawing⟩ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
5h 31% · NaNm ⟨Claude Code's own drawing⟩
README

🧩 Claude Code Mods

Live panes, prompt bands, status line entries, toasts, and hooks for Claude Code, each shipped as a plugin.

Release CI License: MIT

🚀 Install

/plugin marketplace add tartinerlabs/claude-code-mods
/plugin install <mod> --marketplace tartinerlabs/claude-code-mods

🧰 Mods

| | Mod | Where it shows | In one line | | :-: | --- | --- | --- | | ⏸ | auto-resume | Status line, toast | Picks a turn back up once a usage limit resets | | 🏷 | auto-session-name | Session title | Names each session from its first prompt | | ⏱ | cache-clock | Band above the prompt | Counts down to the prompt cache going cold | | 📊 | limit-watch | Band above the prompt, toasts | Shows the 5-hour and weekly usage limits | | 🛫 | pr-pilot | Band above the prompt, toasts | Shows the branch's PR checks and merges it with /merge | | 🗜 | smart-compact | Toast | Compacts between turns, sooner at checkpoints or when the cache is cooling | | ⛅ | token-weather | Band above the prompt | Forecasts how full the context window is |


⏸ auto-resume

When a turn stops on a usage limit, waits until the limit resets and sends continue. Typing a prompt yourself or running /auto-resume cancel stops the wait.

⏸ auto-resume in 1h 12m                                    ← status line
Usage limit reset, resuming                                 ← toast

<sub>📁 plugins/auto-resume</sub>

🏷 auto-session-name

Names each session from its first prompt using Haiku and can give it a prompt bar colour, so it is easy to find with claude -r after a crash. Sessions already named with -n or /rename are left alone. Set the color option to red, blue, green, yellow, purple, orange, pink or cyan to colour named sessions; default leaves the colour alone.

> fix the flaky retry test in the upload worker
                                    → session titled "Fix flaky upload retry test"

<sub>📁 plugins/auto-session-name</sub>

⏱ cache-clock

Draws a countdown above the prompt to when the prompt cache goes cold, with the last request's hit rate. Once cold, it shows how much the next turn re-writes. Set the ttl option to 1h when extended caching is on.

cache ● 3:42 left · 96% hit                                 ← green, yellow under a minute
cache ○ cold · next turn re-writes 84.3k                    ← red

<sub>📁 plugins/cache-clock</sub>

📊 limit-watch

Draws the 5-hour and weekly usage limits above the prompt with live reset countdowns, green under 60%, yellow to 80% and red past it. Toasts once a window passes 80% and again at 95%. Any other window the API reports, such as a gateway spend limit, shows under its own name.

5h 42% · 2h17m  7d 81% · 3d4h                               ← band
7d limit at 81%, resets in 3d4h                             ← toast

<sub>📁 plugins/limit-watch</sub>

🛫 pr-pilot

Draws the current branch's open pull request above the prompt with its checks and whether it can merge, refreshed each minute and after every turn. Toasts when a run of checks finishes. /merge merges it with gh and deletes the branch, but only once every check has passed and nothing blocks it. On a gh-stack layer the band shows its place in the stack, and /merge merges the whole stack with gh stack merge once every unmerged layer has passed its checks and has no unpushed commits, then switches to the trunk and deletes every layer's branch, local and remote, once all of them have merged. Set the method option to merge (default), squash or rebase. Needs the GitHub CLI, signed in.

PR #42  ◐ 3/5 checks  waiting                                ← band
PR #42  ✗ 1 failing: SonarCloud  blocked                     ← red
PR #42  ✓ 5/5 checks  ready · /merge                         ← green
PR #42  stack 2/3  ✓ 5/5 checks  ready · /merge              ← a gh-stack layer
PR #42 checks passed, ready to /merge                        ← toast

<sub>📁 plugins/pr-pilot</sub>

🗜 smart-compact

Compacts the conversation between turns, never during one, weighing several factors against the context's fill. Past the threshold option (60, 70 default, or 80%) it compacts about ten seconds after a turn ends. It compacts sooner when compacting is cheapest or most worth it:

  • Checkpoints, 20 points lower: a turn that committed, opened or merged a PR (gh pr, gh stack), had a plan approved, finished its todo list, or switched branch or worktree. The summary is told what to keep for each, such as the approved plan verbatim. A checkpoint counts only until the next turn starts. Set checkpoints to off to ignore them.
  • Cache cooling, 20 points lower in the last minute before the prompt cache goes cold, so the summary still reads the conversation from cache; 10 lower once it is cold, so the next turn re-writes a short prefix instead of a long one.
  • 5-hour limit, 10 points lower while it is at 80% or more.

Nothing takes it under the floor option (30, 40 default, or 50%), and it tries at most once per idle stretch. Set ttl to 1h when extended prompt caching is on.

Compacted at 52%: committed                                  ← toast
Compacted at 55%: cache cooling                              ← toast
Compacted at 74%: context high, 5h limit at 86%              ← toast

<sub>📁 plugins/smart-compact</sub>

⛅ token-weather

Draws a forecast of the context window above the prompt, with tokens used, a sparkline of the last 12 turns and the change since the last turn.

☁  Cloudy  38% of context  76.4k / 200k   last turns ▁▂▂▃▄▄▅▆  ▲ +4.1k last turn

| ☀ Clear | ☁ Cloudy | ☂ Showers | ☇ Storm | ↯ Compact soon | | :-: | :-: | :-: | :-: | :-: | | under 25% | under 50% | under 75% | under 90% | 90% and up |

<sub>📁 plugins/token-weather</sub>


MIT © Tartiner Labs

Source 2 files
hooks/register.tsx 101 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register, SessionRateLimit } from 'claude-code'
3
4import type { Limit, Warned } from '../types'
5
6const LEVELS = [95, 80]
7const LABELS: Record<string, string> = { five_hour: '5h', seven_day: '7d', spend_limit: 'spend' }
8
9// Held by the host, so a hot reload keeps the band and does not toast the same threshold twice.
10const limits = atom({ plugin: 'limit-watch', key: 'limits' } as const, [] as Limit[])
11const warned = atom({ plugin: 'limit-watch', key: 'warned' } as const, {} as Warned)
12
13async function refresh($: EngineInterface, latest: readonly SessionRateLimit[]) {
14  await update($, limits, () => [...latest])
15
16  const now = await $.clock.now()
17  const seen = await read($, warned)
18  for (const limit of latest) {
19    const level = LEVELS.find(l => limit.percentUsed >= l)
20    const resetsAt = limit.resetsAt ?? ''
21    const last = seen[limit.kind]
22    if (level === undefined || (last?.resetsAt === resetsAt && last.level >= level)) continue
23
24    const reset = limit.resetsAt ? `, resets in ${span(Date.parse(limit.resetsAt) - now)}` : ''
25    $.ui.toast(`${label(limit.kind)} limit at ${limit.percentUsed}%${reset}`, { timeoutMs: 8000 })
26    await update($, warned, w => ({ ...w, [limit.kind]: { resetsAt, level } }))
27  }
28}
29
30export const register: Register = (on) => {
31  on('session.start', async ($, e, next) => {
32    const result = await next(e)
33    await refresh($, (await $.session.usage()).rateLimits)
34    // Redraw each minute so the countdowns tick between measurements.
35    $.clock.every(60_000, () => $.ui.invalidate('ui.render'))
36
37    return result
38  })
39
40  on('session.measure', async ($, e, next) => {
41    if (e.changed.includes('rateLimits')) await refresh($, e.rateLimits)
42
43    return next(e)
44  })
45
46  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
47    const latest = await read($, limits)
48
49    if (e.props.hasSurvey || latest.length === 0) {
50      return next(e)
51    }
52
53    const { Box, Text } = $.ui.resolve(e)
54    const now = await $.clock.now()
55    // Drop the reset countdowns when the full row would not fit the band.
56    const width = latest.reduce((n, l) => n + label(l.kind).length + 1 + entry(l, now).length, 0) + 2 * (latest.length - 1)
57    const isWide = width <= e.props.bodyColumns - 2
58    // Stack above whatever the plugins beneath draw, so other bands stay.
59    const below = await next(e)
60
61    return (
62      <Box flexDirection="column">
63        <Box flexDirection="row" paddingX={1} gap={2}>
64          {latest.map(limit => (
65            <Text key={limit.kind} wrap="truncate-end">
66              <Text dimColor>{label(limit.kind)} </Text>
67              <Text color={colour(limit.percentUsed)}>{isWide ? entry(limit, now) : `${limit.percentUsed}%`}</Text>
68            </Text>
69          ))}
70        </Box>
71        {below}
72      </Box>
73    )
74  })
75}
76
77/** Green under 60%, yellow to 80%, red past it, as the status line colours them. */
78export function colour(percent: number) {
79  return percent >= 80 ? 'red' : percent >= 60 ? 'yellow' : 'green'
80}
81
82export function entry(limit: SessionRateLimit, now: number) {
83  const reset = limit.resetsAt ? ` · ${span(Date.parse(limit.resetsAt) - now)}` : ''
84
85  return `${limit.percentUsed}%${reset}`
86}
87
88function label(kind: string) {
89  return LABELS[kind] ?? kind
90}
91
92export function span(ms: number) {
93  const m = Math.max(0, Math.ceil(ms / 60_000))
94  const d = Math.floor(m / 1440)
95  const h = Math.floor((m % 1440) / 60)
96  if (d > 0) return `${d}d${h}h`
97  if (h > 0) return `${h}h${m % 60}m`
98
99  return `${m}m`
100}
101
types/index.d.ts 12 lines
1/** One rate-limit window as the API last reported it. */
2export type Limit = { kind: string; percentUsed: number; resetsAt?: string }
3
4/** The highest threshold already toasted for each window, keyed by `kind`, with the reset it belongs to. */
5export type Warned = Record<string, { resetsAt: string; level: number }>
6
7declare module 'claude-code' {
8  interface PluginState {
9    'limit-watch': { limits: Limit[]; warned: Warned }
10  }
11}
12