SLOPSHOPPER

agents-panel

A side panel listing the project's .claude/agents, each with a run button.

newpanecommandtoast
v0.1.0no licenseupdated 2026-10-06TLOBillyQ/claude-mods/agents-panel
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · agents-panel
│ ┃ Agents ✕ › fix the failing auth test and add an audit log call │ ┃ ◆ Agents in this project · 0 defined │ ┃ .claude/agents ⏺ Read(src/auth.ts) │ ┃ No agent files found. ⎿ Read 6 lines │ ┃ ⏺ Update(src/auth.ts) │ ┃ click ▶ run to start one · /agents-panel ⎿ Added 2 lines, removed 1 line │ ┃ to hide ⏺ 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 │ │ › /agents-panel │ ⎿ agents-panel: Agents panel open. │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · Agents
◆ Agents in this project · 0 defined .claude/agents No agent files found. click ▶ run to start one · /agents-panel to hide
README

claude-mods

五个 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-weather

每轮结束后读取上下文用量,在输入框上方画一行:天气图标、百分比、已用/总 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

blast-radius

Claude 调用下面这些 Bash 命令时,mod 会先拦住,空跑一遍算出影响范围,再打开一个面板:

命令怎么算影响范围
rm -r / rm -rffind + du:会删哪些文件、总大小
git reset --hardgit status --porcelain:会丢掉哪些未提交改动
git cleangit 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 的脚本都拦不住。它是安全网,不是权限系统;要硬性禁止请用权限规则。

replay-theater

一轮对话里,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-bar

把 /context 的分类明细做成输入框上方的一张卡片:

  • 标题行:◆ context,右边是已用 / 窗口、自动压缩阈值和一个百分比徽章(<50% 薄荷、<80% 柠檬杏、其余珊瑚红)
  • 一条连续的彩色条,每个类别有一个固定的中间亮度粉彩色,深色和浅色终端背景上都看得清;剩余空间是浅灰色,深墨色竖线标出压缩点。条按格宽用色块画,desktop 上也和卡片等宽
  • 图例:每个类别的 token 数和占比
╭──────────────────────────────────────────────────────────────╮
│ ◆ ▾ 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

/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-radius
Source 2 files
hooks/register.tsx 110 lines
1// Agents Panel: the project's .claude/agents as a side panel, each with a run button.
2
3import { atom, read, update } from "claude-code";
4import type { Register } from "claude-code";
5
6import type { AgentsPanelAgent } from "../types";
7
8const PANE = "agents-panel";
9const DIR = ".claude/agents";
10const DOTS = ["blue", "cyan", "yellow", "red", "magenta", "green"];
11
12const agents = atom({ plugin: "agents-panel", key: "agents" } as const, [] as AgentsPanelAgent[]);
13
14export const register: Register = (on) => {
15  on("session.start", async ($, e, next) => {
16    await $.command.register({
17      name: "agents-panel",
18      description: "Show or hide the project's agents in a side panel",
19    });
20    return next(e);
21  });
22
23  on("command.run", { command: "agents-panel" }, async ($) => {
24    const panes = await $.ui.panes();
25    if (panes.some((p) => p.id === PANE)) {
26      await $.ui.close({ id: PANE });
27      return { text: "Agents panel hidden." };
28    }
29    await load($);
30    await $.ui.open({ id: PANE, title: "Agents" });
31    return { text: "Agents panel open." };
32  });
33
34  on("ui.render", { component: "Pane", requestId: PANE }, async ($, e) => {
35    const { Box, Text, Button } = $.ui.resolve(e);
36    const list = await read($, agents);
37
38    return (
39      <Box flexDirection="column" paddingX={1}>
40        <Box flexDirection="row">
41          <Text color="red">◆ </Text>
42          <Text bold>Agents</Text>
43          <Text dimColor> in this project  ·  {list.length} defined</Text>
44        </Box>
45        <Text dimColor>{DIR}</Text>
46        {list.length === 0 && <Text dimColor>No agent files found.</Text>}
47        {list.map((a, i) => (
48          <Box key={a.name} flexDirection="column" marginTop={1}>
49            <Box flexDirection="row">
50              <Text color={DOTS[i % DOTS.length]}>● </Text>
51              <Text bold>{a.name}</Text>
52              <Text dimColor> {a.model}</Text>
53              <Box flexGrow={1} />
54              <Button
55                key={`run-${a.name}`}
56                label="▶ run"
57                dimColor
58                onPress={() => run($, a)}
59              />
60            </Box>
61            <Text dimColor wrap="truncate-end">{a.description}</Text>
62          </Box>
63        ))}
64        <Box marginTop={1}>
65          <Text dimColor>click ▶ run to start one · /agents-panel to hide</Text>
66        </Box>
67      </Box>
68    );
69  });
70};
71
72async function load($) {
73  const entries = await $.fs.list(DIR).catch(() => []);
74  const found: AgentsPanelAgent[] = [];
75  for (const f of entries.filter((f) => f.kind === "file" && f.name.endsWith(".md"))) {
76    const text = await $.fs.read(`${DIR}/${f.name}`).catch(() => "");
77    const meta = frontmatter(text);
78    found.push({
79      name: meta.name || f.name.replace(/\.md$/, ""),
80      model: meta.model || "inherit",
81      description: meta.description || "",
82    });
83  }
84  found.sort((a, b) => a.name.localeCompare(b.name));
85  await update($, agents, () => found);
86}
87
88export function frontmatter(text: string): Record<string, string> {
89  const block = /^---\r?\n([\s\S]*?)\r?\n---/.exec(text)?.[1] ?? "";
90  const meta: Record<string, string> = {};
91  for (const line of block.split(/\r?\n/)) {
92    const m = /^([A-Za-z_-]+):\s*(.*)$/.exec(line);
93    if (m) meta[m[1]] = m[2].replace(/^["']|["']$/g, "").trim();
94  }
95  return meta;
96}
97
98async function run($, a: AgentsPanelAgent) {
99  try {
100    await $.agent.spawn({
101      subagentType: a.name,
102      description: a.name,
103      prompt: "Run your default task on this project.",
104    });
105    $.ui.toast(`${a.name}: started`);
106  } catch (err) {
107    $.ui.toast(`${a.name}: ${err instanceof Error ? err.message : "could not start"}`);
108  }
109}
110
types/index.d.ts 8 lines
1export type AgentsPanelAgent = { name: string; model: string; description: string };
2
3declare module "claude-code" {
4  interface PluginState {
5    "agents-panel": { agents: AgentsPanelAgent[] };
6  }
7}
8