Context Dashboard: a pane that breaks your context window down by category, with the biggest memory files, MCP tools and skills. Open with /context-dashboard.

A Claude Code mod that shows where your context window is going. Type /context-dashboard and a pane opens beside the transcript with:
/context category as a row, in its own colour, with a bar, a token count and its share of the window (free space and the compaction buffer drawn hollow);It refreshes when a session starts and after every turn, using the local estimate, so it makes no extra token-count calls and sends nothing anywhere.
At the prompt of a terminal session:
/plugin install context-dashboard --marketplace WashC-904/context-dashboard
From a shell:
claude plugin install context-dashboard --marketplace WashC-904/context-dashboard
In the desktop app, where /plugin does not run: click + next to the prompt box, then Plugins, Add marketplace with WashC-904/context-dashboard, then Add plugin.
Answer y to add the marketplace, then choose a scope.
Type /context-dashboard. Close the pane with its close mark.
Hooks session.start and turn.complete read $.session.usage({ breakdown: 'summary' }) and keep the result in the mod's own state; a ui.render hook draws the pane from it. Everything is in hooks/register.tsx and hooks/dash.ts.
MIT, Copyright (c) 2026 Norstride, Inc. See LICENSE.
hooks/register.tsx 122 lines1import type { Register, SessionUsage } from 'claude-code'
2
3import type { DashData } from '../types'
4import { bar, fmt, pct, rowsFor, shorten, top } from './dash'
5
6const PANE = 'context-dashboard'
7
8function toData(usage: SessionUsage, now: number): DashData | null {
9 const b = usage.context.breakdown
10 if (!b) return null
11 return {
12 model: b.model,
13 max: b.rawMaxTokens,
14 used: b.totalTokens,
15 percent: b.percentage,
16 isAutoCompact: b.isAutoCompactEnabled,
17 categories: b.categories.map(c => ({ name: c.name, tokens: c.tokens, color: c.color, kind: c.kind })),
18 memory: b.memoryFiles.map(f => ({ name: f.path, detail: f.type, tokens: f.tokens })),
19 mcp: b.mcpTools.map(t => ({ name: t.name, detail: t.serverName + (t.isLoaded ? '' : ' (on demand)'), tokens: t.tokens })),
20 skills: b.skills ? { total: b.skills.totalSkills, included: b.skills.includedSkills, tokens: b.skills.tokens } : null,
21 updatedAt: now,
22 }
23}
24
25export const register: Register = on => {
26 on('session.start', async ($, e, next) => {
27 await $.command.register({ name: 'context-dashboard', description: 'Open the Context Dashboard pane' })
28 const fresh = toData(await $.session.usage({ breakdown: 'summary' }), await $.clock.now())
29 if (fresh) await $.state.set({ plugin: 'context-dashboard', key: 'data' } as const, fresh)
30 return next(e)
31 })
32
33 on('command.run', { command: 'context-dashboard' }, async $ => {
34 const fresh = toData(await $.session.usage({ breakdown: 'summary' }), await $.clock.now())
35 if (fresh) await $.state.set({ plugin: 'context-dashboard', key: 'data' } as const, fresh)
36 await $.ui.open({ id: PANE, title: 'Context' })
37 return { text: 'Context Dashboard opened.' }
38 })
39
40 on('turn.complete', async ($, e, next) => {
41 const fresh = toData(await $.session.usage({ breakdown: 'summary' }), await $.clock.now())
42 if (fresh) await $.state.set({ plugin: 'context-dashboard', key: 'data' } as const, fresh)
43 return next(e)
44 })
45
46 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
47 const { Box, Text } = $.ui.resolve(e)
48 const { value: d = null } = await $.state.get({ plugin: 'context-dashboard', key: 'data' } as const)
49 if (d === null) return <Text dimColor>Measuring the context window…</Text>
50
51 const cols = Math.max(30, e.props.bodyColumns)
52 const wide = cols - 2
53 const total = bar(d.max > 0 ? d.used / d.max : 0, wide)
54 const catBar = Math.max(8, Math.min(24, cols - 46))
55 const mcp = top(d.mcp, 5)
56 const mem = top(d.memory, 5)
57
58 return (
59 <Box flexDirection="column">
60 <Text bold>
61 Context window <Text dimColor>{d.model}</Text>
62 </Text>
63 <Text>
64 <Text color="promptBorder">{total.filled}</Text>
65 <Text dimColor>{total.rest}</Text>
66 </Text>
67 <Text>
68 {fmt(d.used)} of {fmt(d.max)} used ({Math.round(d.percent)}%)
69 <Text dimColor> {d.isAutoCompact ? 'auto-compact on' : 'auto-compact off'}</Text>
70 </Text>
71 <Text> </Text>
72 <Text bold>By category</Text>
73 {rowsFor(d.categories).map(c => {
74 const b = bar(d.max > 0 ? c.tokens / d.max : 0, catBar)
75 const isUsed = c.kind === 'used'
76 return (
77 <Text key={c.name}>
78 <Text color={c.color}>{isUsed ? '●' : '○'} </Text>
79 {c.name.padEnd(22)}
80 <Text color={c.color} dimColor={!isUsed}>{b.filled}</Text>
81 <Text dimColor>{b.rest} {fmt(c.tokens).padStart(6)} {pct(c.tokens, d.max).padStart(4)}</Text>
82 </Text>
83 )
84 })}
85 {mem.shown.length > 0 && (
86 <Box flexDirection="column">
87 <Text> </Text>
88 <Text bold>Memory files</Text>
89 {mem.shown.map(m => (
90 <Text key={m.name} dimColor>
91 {fmt(m.tokens).padStart(6)} {shorten(m.name, cols - 12)}
92 </Text>
93 ))}
94 {mem.more > 0 && <Text dimColor> and {mem.more} more</Text>}
95 </Box>
96 )}
97 {mcp.shown.length > 0 && (
98 <Box flexDirection="column">
99 <Text> </Text>
100 <Text bold>Largest MCP tools</Text>
101 {mcp.shown.map(t => (
102 <Text key={t.name + t.detail} dimColor>
103 {fmt(t.tokens).padStart(6)} {shorten(t.name, 28)} <Text dimColor>{shorten(t.detail, cols - 40)}</Text>
104 </Text>
105 ))}
106 {mcp.more > 0 && <Text dimColor> and {mcp.more} more</Text>}
107 </Box>
108 )}
109 {d.skills !== null && (
110 <Box flexDirection="column">
111 <Text> </Text>
112 <Text bold>Skills</Text>
113 <Text dimColor>
114 {d.skills.included} of {d.skills.total} listed, {fmt(d.skills.tokens)} tokens
115 </Text>
116 </Box>
117 )}
118 </Box>
119 )
120 })
121}
122hooks/dash.ts 36 lines1import type { DashCategory, DashItem } from '../types'
2
3export function fmt(n: number): string {
4 return n >= 1_000_000 ? `${(n / 1_000_000).toFixed(1)}M` : n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n)
5}
6
7/** A bar `width` cells wide, `share` (0 to 1) of it filled; a non-zero share always shows at least one cell. */
8export function bar(share: number, width: number): { filled: string; rest: string } {
9 const clamped = Math.max(0, Math.min(1, share))
10 const cells = clamped > 0 ? Math.max(1, Math.round(clamped * width)) : 0
11 const n = Math.min(cells, width)
12 return { filled: '█'.repeat(n), rest: '░'.repeat(width - n) }
13}
14
15export function pct(part: number, whole: number): string {
16 if (whole <= 0) return '0%'
17 const p = (part / whole) * 100
18 return p > 0 && p < 1 ? '<1%' : `${Math.round(p)}%`
19}
20
21/** The categories worth a row, biggest first within the used ones; free space and the buffer follow, deferred tools are left out. */
22export function rowsFor(categories: readonly DashCategory[]): DashCategory[] {
23 const used = categories.filter(c => c.kind === 'used' && c.tokens > 0).sort((a, b) => b.tokens - a.tokens)
24 const rest = categories.filter(c => (c.kind === 'free' || c.kind === 'buffer') && c.tokens > 0)
25 return [...used, ...rest]
26}
27
28export function top(items: readonly DashItem[], n: number): { shown: DashItem[]; more: number } {
29 const sorted = [...items].sort((a, b) => b.tokens - a.tokens)
30 return { shown: sorted.slice(0, n), more: Math.max(0, sorted.length - n) }
31}
32
33export function shorten(s: string, max: number): string {
34 return s.length <= max ? s : '…' + s.slice(s.length - (max - 1))
35}
36types/index.d.ts 21 lines1export type DashCategory = { name: string; tokens: number; color: string; kind: 'used' | 'free' | 'buffer' | 'deferred' }
2export type DashItem = { name: string; detail: string; tokens: number }
3export type DashData = {
4 model: string
5 max: number
6 used: number
7 percent: number
8 isAutoCompact: boolean
9 categories: DashCategory[]
10 memory: DashItem[]
11 mcp: DashItem[]
12 skills: { total: number; included: number; tokens: number } | null
13 updatedAt: number
14}
15
16declare module 'claude-code' {
17 interface PluginState {
18 'context-dashboard': { data: DashData | null }
19 }
20}
21