A live forecast of the context window, drawn above the prompt.

五个 Claude Code mod,打包成一个本地 marketplace my-mods。
| Mod | 作用 |
|---|---|
| token-weather | 在输入框上方用“天气预报”显示上下文窗口用量 |
| blast-radius | 拦下有风险的 Bash 命令,先显示会影响什么,再等你选 Proceed 或 Cancel |
| replay-theater | 记录上一轮的文件编辑,逐个 diff 回放 |
| context-bar | 在输入框上方用一张卡片显示 /context 的分类明细,可折叠 |
| agents-panel | 侧边面板列出项目的 .claude/agents,每个带 run 按钮 |
需要 Claude Code 2.1.287 或更高版本(claude --version)。
git clone https://github.com/TLOBillyQ/claude-mods.git
claude plugin marketplace add ./claude-mods
claude plugin install token-weather@my-mods --scope user
claude plugin install blast-radius@my-mods --scope user
claude plugin install replay-theater@my-mods --scope user
claude plugin install context-bar@my-mods --scope user
claude plugin install agents-panel@my-mods --scope user
装好后新开一个会话才会加载。
停用某一个:
claude plugin disable blast-radius@my-mods
拉取新版本后更新:
claude plugin marketplace update my-mods
只想临时试用、不安装:
claude --plugin-dir ./claude-mods/token-weather --plugin-dir ./claude-mods/blast-radius --plugin-dir ./claude-mods/replay-theater --plugin-dir ./claude-mods/context-bar --plugin-dir ./claude-mods/agents-panel
每轮结束后读取上下文用量,在输入框上方画一行:天气图标、百分比、已用/总 token、最近 12 轮的迷你图,以及上一轮增加了多少。
| 用量 | 预报 |
|---|---|
| < 25% | ☀ Clear |
| 25–49% | ☁ Cloudy |
| 50–74% | ☂ Showers |
| 75–89% | ☇ Storm |
| ≥ 90% | ↯ Compact soon |
☀ Clear 18% of context 36.1k / 200k last turns ▁▂▅ ▲ +12.4k last turn
Claude 调用下面这些 Bash 命令时,mod 会先拦住,空跑一遍算出影响范围,再打开一个面板:
| 命令 | 怎么算影响范围 |
|---|---|
rm -r / rm -rf | find + du:会删哪些文件、总大小 |
git reset --hard | git status --porcelain:会丢掉哪些未提交改动 |
git clean | git clean -n:会删哪些未跟踪文件 |
强推(--force、--force-with-lease、+ref) | git log HEAD..<upstream>:远端会丢哪些提交 |
| 数据库迁移(Django、Rails、Prisma、Alembic、Knex、Sequelize、Flyway) | Django 用 showmigrations --plan 列出待执行迁移,其他只做提示 |
按 1 Proceed,命令照原样执行;按 2 Cancel,Claude 会收到拒绝和原因。关掉面板或按 Esc 等于 Cancel。终端太窄放不下面板时,同样的内容会画在输入框上方。
它只读命令文本,
$(…)、别名、调用rm的脚本都拦不住。它是安全网,不是权限系统;要硬性禁止请用权限规则。
一轮对话里,mod 记录每次成功的 Edit、MultiEdit 和 Write(Write 会在写入前读取旧内容,所以 diff 是真实的)。回合结束后,输入框上方会出现提示:
↻ 3 edits across 2 files last turn r: Replay or /replay
运行 /replay(或聚焦提示条后按 r)打开回放面板:顶部是步骤条,下面是当前这一步的文件和 diff,p / n / c 对应 Prev / Next / Close,也可以点步骤条上的编号直接跳转。它只观察,不会阻止或修改任何编辑。
把 /context 的分类明细做成输入框上方的一张卡片:
◆ context,右边是已用 / 窗口、自动压缩阈值和一个百分比徽章(<50% 薄荷、<80% 柠檬杏、其余珊瑚红)╭──────────────────────────────────────────────────────────────╮
│ ◆ ▾ context 90k of 1M · compacts at 987k 9% │
│ ██████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░│ │
│ ■ system prompt 4.2k 0% ■ tools 17k 2% ■ messages 0 0% │
│ ■ free 897k │
╰──────────────────────────────────────────────────────────────╯
默认折叠,只显示标题行;点 ▸ context 展开,再点 ▾ context 收起。手动选择在当前会话的后续回复和热重载时保持;键盘可以 ctrl+x tab 聚焦后回车。/context-bar 显示或隐藏整张卡片。会话开始和每轮结束后刷新,用 summary 模式在本地估算,不额外发 token 计数请求;按需加载的 deferred 工具不占窗口,不显示。终端窄于 60 列时不画图例。
/agents-panel 在侧边打开面板,列出项目 .claude/agents/*.md 里定义的所有 agent:名字、模型、一行描述,右侧 ▶ run 按钮用该 agent 启动一个子 agent(提示词为“Run your default task on this project.”)。再次运行 /agents-panel 关闭。只读项目目录,不读 ~/.claude/agents;每次打开面板时重新读取。
每个 mod 的结构:
<mod>/
├── .claude-plugin/plugin.json 清单,types 指向类型契约
├── hooks/hooks.json 指向 hooks 模块
├── hooks/<mod>.mjs 导出 register(on)
├── types/index.d.ts $.state 的类型契约
└── tests/<mod>.test.ts
校验和测试:
claude plugin validate ./blast-radius
claude plugin test ./blast-radiushooks/token-weather.mjs 84 lines1// Token Weather: a live forecast of the context window, above the prompt.
2
3const HISTORY = 12;
4const BARS = "▁▂▃▄▅▆▇█";
5const FORECAST = [
6 { upTo: 25, icon: "☀", word: "Clear", color: "yellow" },
7 { upTo: 50, icon: "☁", word: "Cloudy", color: "cyan" },
8 { upTo: 75, icon: "☂", word: "Showers", color: "blue" },
9 { upTo: 90, icon: "☇", word: "Storm", color: "magenta" },
10 { upTo: Infinity, icon: "↯", word: "Compact soon", color: "red" },
11];
12
13// Held by the host, so the history survives a hot reload of this file.
14const readings = { plugin: "token-weather", key: "readings" };
15
16export function register(on) {
17 on("session.start", async ($, e, next) => {
18 const result = await next(e);
19 await takeReading($);
20 return result;
21 });
22
23 on("turn.complete", async ($, e, next) => {
24 const result = await next(e);
25 if (!e.agentId) {
26 await takeReading($); // main-loop turns only, not subagents
27 }
28 return result;
29 });
30
31 on("ui.render", { component: "AbovePrompt" }, async ($, e, next) => {
32 const { value: history = [] } = await $.state.get(readings);
33 if (e.props.hasSurvey || history.length === 0) {
34 return next(e);
35 }
36 const { Box, Text } = $.ui.resolve(e);
37 return band(Box, Text, history, e.props.bodyColumns);
38 });
39}
40
41async function takeReading($) {
42 const { context } = await $.session.usage();
43 if (!context?.window) return;
44 const tokens = context.tokens ?? 0;
45 const percent = context.percent ?? Math.round((tokens / context.window) * 100);
46 const { value: history = [] } = await $.state.get(readings);
47 await $.state.set(readings, [...history, { tokens, window: context.window, percent }].slice(-HISTORY));
48}
49
50function band(Box, Text, history, columns) {
51 const now = history[history.length - 1];
52 const f = FORECAST.find((b) => now.percent < b.upTo);
53 const parts = [
54 Text({ color: f.color, bold: true, children: `${f.icon} ${f.word}` }),
55 Text({ children: ` ${now.percent}% of context` }),
56 Text({ dimColor: true, children: ` ${short(now.tokens)} / ${short(now.window)}` }),
57 ];
58 if (columns >= 60) {
59 parts.push(Text({ dimColor: true, children: " last turns " }));
60 parts.push(Text({ color: f.color, children: sparkline(history) }));
61 if (history.length > 1) {
62 parts.push(Text({ dimColor: true, children: trend(history) }));
63 }
64 }
65 return Box({ flexDirection: "row", paddingX: 1, children: parts });
66}
67
68function sparkline(history) {
69 const top = Math.max(...history.map((r) => r.tokens), 1);
70 return history.map((r) => BARS[Math.floor((r.tokens / top) * (BARS.length - 1))]).join("");
71}
72
73function trend(history) {
74 const delta = history[history.length - 1].tokens - history[history.length - 2].tokens;
75 if (delta === 0) return " steady";
76 return delta > 0 ? ` ▲ +${short(delta)} last turn` : ` ▼ ${short(-delta)} last turn`;
77}
78
79function short(n) {
80 if (n >= 1_000_000) return `${+(n / 1_000_000).toFixed(1)}M`;
81 if (n >= 1_000) return `${+(n / 1_000).toFixed(1)}k`;
82 return String(n);
83}
84types/index.d.ts 8 lines1export type TokenWeatherReading = { tokens: number; window: number; percent: number };
2
3declare module "claude-code" {
4 interface PluginState {
5 "token-weather": { readings: TokenWeatherReading[] };
6 }
7}
8