SLOPSHOPPER

session-gauges

Band above the prompt: context-window fill bar and the count of background tasks still in flight.

bandguardtoast
v0.1.0MITupdated 2026-10-18aaraujo666/claude-mods/session-gauges
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · session-gauges
› 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 ██████████░░░░░░░░░░ 49% 97k / 200k ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
██████████░░░░░░░░░░ 49% 97k / 200k
README

claude-mods

Claude Code mods (plugins built from a function-hooks module) that change the desktop app's and the terminal's interface. This folder is a local plugin marketplace named anthony-mods. Each mod is a subfolder.

ModWhat it does
session-gaugesShows a band above the prompt with a context-window fill bar (42% 84k / 200k, green, then yellow at 60%, then red at 80%). It also shows a count of background tasks still running. Click the count to open the desktop app's tasks pane.

Install

The mods are published at https://github.com/aaraujo666/claude-mods. Install one from the prompt of a Claude Code terminal session:

/plugin install session-gauges --marketplace aaraujo666/claude-mods

From a shell, run claude plugin install session-gauges --marketplace aaraujo666/claude-mods.

In the Claude desktop app, where /plugin doesn't run: click + next to the prompt box, then Plugins, then Add marketplace with aaraujo666/claude-mods, then Add plugin.

Mods need a recent Claude Code with function-hooks support. These were built and tested on 2.1.299.

Working copy (the author's machine)

To work on a mod, install it from a local clone instead. A folder marketplace runs the plugin from that folder, not from a copy. After an edit, run /reload-plugins or start a new session. You don't need to reinstall.

$c = (Get-ChildItem "$env:APPDATA\Claude\claude-code\*\*\claude.exe" | Sort-Object LastWriteTime | Select-Object -Last 1).FullName
& $c plugin marketplace add <path-to-clone>
& $c plugin install session-gauges@anthony-mods --scope user

Use the desktop app's bundled engine. An older npm claude on PATH may be too old for mods.

Check a mod

& $c plugin validate .\session-gauges
& $c plugin test     .\session-gauges

Traps

  • A hook's arguments are ($, e, next). The first one is the engine, not the event.
  • Don't declare a local variable called next inside a hook. Validation refuses the shadowing.
  • The mod API is marked early access. After a Claude Code update, re-run validate and test. A broken hook is skipped with one dim line in the transcript, and the app carries on.
Source 2 files
hooks/register.tsx 160 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { BackgroundTask, ContextFill } from '../types'
5
6const context = atom({ plugin: 'session-gauges', key: 'context' } as const, null)
7const tasks = atom({ plugin: 'session-gauges', key: 'tasks' } as const, {})
8
9const BAR_CELLS = 20
10const FINISHED = new Set(['completed', 'failed', 'killed', 'stopped', 'cancelled'])
11const NOTIFICATION = /<task-notification>([\s\S]*?)<\/task-notification>/g
12
13type Usage = { tokens?: number; window: number; percent?: number }
14
15// The engine reports the fill once a response of the live window carried one.
16const setContext = async ($: EngineInterface, usage: Usage) => {
17  if (usage.tokens === undefined || usage.percent === undefined) return
18  const fill: ContextFill = { tokens: usage.tokens, window: usage.window, percent: usage.percent }
19  await update($, context, () => fill)
20}
21
22const addTask = ($: EngineInterface, id: string | undefined, task: BackgroundTask) =>
23  id ? update($, tasks, all => ({ ...all, [id]: task })) : undefined
24
25const dropTasks = ($: EngineInterface, ids: string[]) =>
26  ids.length === 0
27    ? undefined
28    : update($, tasks, all => {
29        const kept = { ...all }
30        for (const id of ids) delete kept[id]
31        return kept
32      })
33
34// A task notification names the task and how it ended; ids of finished ones.
35export const finishedIds = (text: string): string[] => {
36  const ids: string[] = []
37  for (const [, body = ''] of text.matchAll(NOTIFICATION)) {
38    const id = /<task-id>\s*([^<\s]+)\s*<\/task-id>/.exec(body)?.[1]
39    const status = /<status>\s*([^<\s]+)\s*<\/status>/.exec(body)?.[1] ?? 'completed'
40    if (id && FINISHED.has(status.toLowerCase())) ids.push(id)
41  }
42  return ids
43}
44
45const clip = (s: string, n = 40) => (s.length > n ? `${s.slice(0, n - 1)}…` : s)
46
47export const formatTokens = (n: number) =>
48  n >= 1_000_000 ? `${(n / 1_000_000).toFixed(1)}M` : n >= 1000 ? `${Math.round(n / 1000)}k` : `${n}`
49
50export const bar = (percent: number) => {
51  const filled = Math.min(BAR_CELLS, Math.round((Math.max(0, percent) / 100) * BAR_CELLS))
52  return { filled: '█'.repeat(filled), empty: '░'.repeat(BAR_CELLS - filled) }
53}
54
55const fillColor = (percent: number) => (percent >= 80 ? 'error' : percent >= 60 ? 'warning' : 'success')
56
57// Every gauge is display only: if its bookkeeping throws, the event goes on as
58// if the mod were not there (each gating hook's .catch passes it through).
59export const register: Register = on => {
60  on('session.start', async ($, e, next) => {
61    const started = await next(e)
62    await setContext($, (await $.session.usage()).context)
63    return started
64  })
65
66  on('session.measure', async ($, e, next) => {
67    await setContext($, e.context)
68    return next(e)
69  })
70
71  // Launches: each tool that can leave work running reports the task's id.
72  on('tool.call', async ($, e, next) => {
73    const ran = await next(e)
74    if (ran.deny !== undefined || ran.isError) return ran
75
76    if (e.tool === 'Bash' || e.tool === 'PowerShell') {
77      const result = ran.result as { backgroundTaskId?: string } | undefined
78      await addTask($, result?.backgroundTaskId, { kind: 'shell', label: clip(e.description ?? e.command) })
79    } else if (e.tool === 'Agent') {
80      const result = ran.result as { status?: string; agentId?: string; taskId?: string } | undefined
81      if (result?.status === 'async_launched') {
82        await addTask($, result.agentId, { kind: 'agent', label: clip(e.description) })
83      } else if (result?.status === 'remote_launched') {
84        await addTask($, result.taskId, { kind: 'remote', label: clip(e.description) })
85      }
86    } else if (e.tool === 'Monitor' || e.tool === 'Workflow') {
87      const result = ran.result as { taskId?: string } | undefined
88      await addTask($, result?.taskId, { kind: e.tool.toLowerCase(), label: e.tool })
89    } else if (e.tool === 'TaskStop') {
90      await dropTasks($, [e.task_id ?? e.shell_id].filter((id): id is string => !!id))
91    }
92    return ran
93  }).catch(($, e, next) => next(e))
94
95  // Completions: the task notification lands in the conversation as a row.
96  on('session.append', async ($, e, next) => {
97    const text = e.message.content
98      .map(block => (block.type === 'text' ? block.text : ''))
99      .join('\n')
100    if (text.includes('<task-notification>')) await dropTasks($, finishedIds(text))
101    return next(e)
102  }).catch(($, e, next) => next(e))
103
104  // At each stop the engine lists what is still in flight: take it as truth,
105  // which also picks up a command the person backgrounded with ctrl+b.
106  on('classic.Stop', async ($, e, next) => {
107    const live = e.background_tasks
108    if (live) {
109      await update($, tasks, all => {
110        const inFlight: Record<string, BackgroundTask> = {}
111        for (const t of live) {
112          inFlight[t.id] = all[t.id] ?? { kind: t.type, label: clip(t.description || t.command || t.type) }
113        }
114        return inFlight
115      })
116    }
117    return next(e)
118  }).catch(($, e, next) => next(e))
119
120  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
121    if (e.props.hasSurvey) return next(e)
122
123    const fill = await read($, context)
124    const running = Object.values(await read($, tasks))
125    if (fill === null && running.length === 0) return next(e)
126
127    const { Box, Button, Text } = $.ui.resolve(e)
128    const cells = fill ? bar(fill.percent) : null
129
130    // The desktop app's own view tool opens its background-task pane.
131    const openTasks = async () => {
132      const shown = await $.tool.call({ tool: 'mcp__ccd_view__show_pane', pane: 'tasks' } as never)
133      if (shown.deny !== undefined || shown.isError) $.ui.toast('Could not open the tasks pane')
134    }
135
136    return (
137      <Box flexDirection="row" gap={1}>
138        {fill && cells ? (
139          <Text key="context">
140            <Text color={fillColor(fill.percent)}>{cells.filled}</Text>
141            <Text dimColor>{cells.empty}</Text>
142            <Text> {fill.percent}% </Text>
143            <Text dimColor>
144              {formatTokens(fill.tokens)} / {formatTokens(fill.window)}
145            </Text>
146          </Text>
147        ) : null}
148        {running.length > 0 ? (
149          <Button key="tasks" plain onPress={openTasks}>
150            <Text color="warning">
151              {fill ? '· ' : ''}⏳ {running.length} background {running.length === 1 ? 'task' : 'tasks'}:{' '}
152              {running.map(t => `${t.kind} (${t.label})`).join(', ')}
153            </Text>
154          </Button>
155        ) : null}
156      </Box>
157    )
158  })
159}
160
types/index.d.ts 13 lines
1export type ContextFill = { tokens: number; window: number; percent: number }
2
3export type BackgroundTask = { kind: string; label: string }
4
5declare module 'claude-code' {
6  interface PluginState {
7    'session-gauges': {
8      context: ContextFill | null
9      tasks: Record<string, BackgroundTask>
10    }
11  }
12}
13