Band above the prompt that shows the context window as a tide: a fill bar, the trend per turn, a sparkline of recent turns and how many turns are left before…

Shows the context window as a tide in the band above the prompt: a fill bar colored by phase, a sparkline of the last turns, how fast the window is filling per turn, and how many turns are left before the compaction threshold.
≈ ██████░░░░░░░░░░ ▁▂▂▃▄▄▅ 42% · mid tide · +3.5/turn · ~11 turns to 80% Hide
The phases are low tide (cyan), mid tide (green), high tide (yellow) and flood (red, at or past the threshold, where compaction is near). Crossing into flood shows one toast.
/plugin marketplace add Hula-Hoop-AI/supermods
/plugin install tide@supermods
The band appears after the first model response of the session and updates after each turn. /tide, or the band's Hide button (t while the band has focus), hides it; /tide again shows it.
| Option | Default | Effect |
|---|---|---|
compact_at | 80 | Context fill, in percent, that counts as full: the bar turns red there and the turns-left estimate counts down to it. |
history_turns | 12 | How many recent turns the sparkline shows. |
Events: session.start (registers /tide), session.measure (reads the context fill the engine reports after each turn), command.run (/tide), ui.render (the band above the prompt).
Capabilities: $.ui.toast, $.state. No file, process, network or model access.
/clear) starts the history over, so the sparkline and the trend take a few turns to fill in again.hooks/register.tsx 111 lines1import { atom, read, update } from 'claude-code';
2import type { Register } from 'claude-code';
3
4const COMMAND = 'tide';
5const BAR_CELLS = 16;
6const TREND_TURNS = 4;
7const SPARK = '▁▂▃▄▅▆▇█';
8// Each phase starts at this share of the compaction threshold; highest first.
9const PHASES: [number, { name: string; color: string }][] = [
10 [1, { name: 'flood', color: 'red' }],
11 [0.7, { name: 'high tide', color: 'yellow' }],
12 [0.35, { name: 'mid tide', color: 'green' }],
13 [0, { name: 'low tide', color: 'cyan' }],
14];
15
16const history = atom({ plugin: 'tide', key: 'history' } as const, []);
17const isHidden = atom({ plugin: 'tide', key: 'isHidden' } as const, false);
18
19export function phaseOf(percent: number, compactAt: number): { name: string; color: string } {
20 const share = percent / compactAt;
21 return PHASES.find(([from]) => share >= from)![1];
22}
23
24export function bar(percent: number, cells: number): string {
25 const filled = Math.round((Math.min(percent, 100) / 100) * cells);
26 return '█'.repeat(filled) + '░'.repeat(cells - filled);
27}
28
29export function sparkline(percents: number[]): string {
30 return percents.map(p => SPARK[Math.min(SPARK.length - 1, Math.floor((p / 100) * SPARK.length))]).join('');
31}
32
33/** Average change per turn over the last few turns, null before the second reading. */
34export function trend(percents: number[]): number | null {
35 const recent = percents.slice(-(TREND_TURNS + 1));
36 if (recent.length < 2) return null;
37 return ((recent.at(-1) ?? 0) - (recent[0] ?? 0)) / (recent.length - 1);
38}
39
40export function turnsLeft(percent: number, perTurn: number | null, compactAt: number): number | null {
41 if (perTurn === null || perTurn <= 0 || percent >= compactAt) return null;
42 return Math.ceil((compactAt - percent) / perTurn);
43}
44
45export function describe(percents: number[], compactAt: number): string {
46 const percent = percents.at(-1) ?? 0;
47 const perTurn = trend(percents);
48 const parts = [`${percent}%`, phaseOf(percent, compactAt).name];
49 if (perTurn !== null) parts.push(`${perTurn >= 0 ? '+' : ''}${perTurn.toFixed(1)}/turn`);
50 const left = turnsLeft(percent, perTurn, compactAt);
51 if (left !== null) parts.push(`~${left} turn${left === 1 ? '' : 's'} to ${compactAt}%`);
52 return parts.join(' · ');
53}
54
55export const register: Register = (on, options) => {
56 const compactAt = Number(options['compact_at']);
57 const historyTurns = Number(options['history_turns']);
58
59 on('session.start', async ($, e, next) => {
60 await $.command.register({ name: COMMAND, description: 'Show or hide the context tide band' });
61
62 return next(e);
63 });
64
65 on('session.measure', async ($, e, next) => {
66 const percent = e.context.percent;
67 if (percent !== undefined && e.changed.includes('context')) {
68 let previous: number | undefined;
69 await update($, history, list => {
70 previous = list[list.length - 1];
71 // A fill that fell is a new window (compaction, /clear): the old readings say nothing about it.
72 return previous !== undefined && percent < previous ? [percent] : [...list, percent].slice(-historyTurns);
73 });
74 if (previous !== undefined && previous < compactAt && percent >= compactAt) {
75 $.ui.toast(`${$.plugin.name}: flood, context at ${percent}%. Compaction is near.`);
76 }
77 }
78
79 return next(e);
80 });
81
82 on('command.run', { command: COMMAND }, async $ => {
83 const hidden = !(await read($, isHidden));
84 await update($, isHidden, () => hidden);
85
86 return { text: hidden ? 'Tide band hidden. /tide shows it again.' : 'Tide band shown.' };
87 });
88
89 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
90 const [percents, hidden] = await Promise.all([read($, history), read($, isHidden)]);
91 if (e.props.hasSurvey || percents.length === 0 || hidden) return next(e);
92
93 const { Box, Button, Text } = $.ui.resolve(e);
94 const percent = percents.at(-1) ?? 0;
95 const { color } = phaseOf(percent, compactAt);
96 const cells = Math.max(4, Math.min(BAR_CELLS, Math.floor(e.props.bodyColumns / 5)));
97
98 const below = await next(e);
99 const row = (
100 <Box>
101 <Text color={color}>≈ {bar(percent, cells)} </Text>
102 <Text dimColor>{sparkline(percents)} </Text>
103 <Text wrap="truncate-end">{describe(percents, compactAt)} </Text>
104 <Button key="tide-hide" label="Hide" hotkey="t" plain dimColor onPress={() => update($, isHidden, () => true)} />
105 </Box>
106 );
107
108 return below ? <Box flexDirection="column">{row}{below}</Box> : row;
109 });
110};
111types/index.d.ts 6 lines1declare module 'claude-code' {
2 interface PluginState {
3 'tide': { history: number[]; isHidden: boolean };
4 }
5}
6