SLOPSHOPPER

plain-steps

每一步工具调用在对话里只显示一行中文说明和结果;成功的输出不展开,失败只露最后几行;/steps 切回完整显示

newrowsguardcommand
★ 1v0.1.0MITupdated 2026-10-15yishu-ziyu/plain-steps
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · plain-steps
› 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 › /steps ⎿ plain-steps: 每一步显示完整命令和输出。 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

plain-steps

Claude Code 的一个 mod。

Claude 干活时,终端里每一步都会打出完整的命令和输出:路径、参数、JSON,一屏接一屏。要知道它做得对不对,你得自己读完这些。

plain-steps 替你读完。每一步只留一行:在做什么、用了多久、成没成。出错的那一步下面留着最后几行报错,你可以直接判断要不要插手,也能顺着报错找原因。想看细节,输入 /steps 切回完整输出。

plain-steps 演示

装上

在 Claude Code 的输入框里输入:

/plugin install plain-steps --marketplace yishu-ziyu/plain-steps

问是否添加 marketplace 时输入 y,再选安装范围(默认用户范围,回车即可)。装好后当场生效。

想改代码再试:克隆仓库后运行 claude --plugin-dir ./plain-steps。

在 Claude Code v2.1.292 上测试过。

一行里写什么

显示意思
✓这一步正常结束。超过 2 秒会写上耗时,比如 ✓ 38 秒
✗ 失败这一步报错退出,下面显示输出的最后 6 行
超时,转到后台命令超时,Claude Code 把它转到后台继续跑
在后台跑这一步一开始就在后台运行
已中断你中断了这一步
  • 说明文字来自 Claude 给每条命令写的描述,所以语言跟着 Claude 走。
  • ✓ 只表示命令正常结束。如果测试脚本在测试失败时仍然正常退出,这里也会是 ✓。
  • 只接管 Bash、Read、Edit、Write、Grep、Glob、Agent、SendMessage、ToolSearch、Skill 这几种工具,其他工具照原样显示。

切换

输入 /steps:每一步切回完整命令和输出。再输入一次:切回一行。

许可

MIT

Source 2 files
hooks/register.tsx 99 lines
1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4// true:每一步一行中文;false:引擎原样画(完整命令和输出)。/steps 切换。
5const plain = atom({ plugin: 'plain-steps', key: 'plain' } as const, true)
6
7// 每次调用花了多久(模块自己的变量,重载后从头记)。
8const took = new Map<string, number>()
9
10const field = (v: unknown, k: string): unknown => (typeof v === 'object' && v !== null ? (v as Record<string, unknown>)[k] : undefined)
11const str = (v: unknown) => (typeof v === 'string' ? v : '')
12const clip = (t: string, n: number) => {
13  const one = t.replace(/\s+/g, ' ').trim()
14  return one.length > n ? `${one.slice(0, n - 1)}…` : one
15}
16const fileName = (v: unknown) => str(v).split('/').filter(Boolean).at(-1) ?? ''
17
18// 只接手这些工具的行;别的工具照引擎原样画。
19const LABELS: Record<string, (input: unknown) => string> = {
20  Bash: i => clip(str(field(i, 'description')), 80) || clip(str(field(i, 'command')).replace(/\/Users\/[^\s"']*\//g, '…/'), 60),
21  Read: i => `读 ${fileName(field(i, 'file_path'))}`,
22  Edit: i => `改 ${fileName(field(i, 'file_path'))}`,
23  MultiEdit: i => `改 ${fileName(field(i, 'file_path'))}`,
24  Write: i => `写 ${fileName(field(i, 'file_path'))}`,
25  Grep: i => `搜 “${clip(str(field(i, 'pattern')), 40)}”`,
26  Glob: i => `找文件 ${clip(str(field(i, 'pattern')), 40)}`,
27  Agent: i => `派子代理:${clip(str(field(i, 'description')), 60)}`,
28  SendMessage: i => `发消息给 ${clip(str(field(i, 'to')), 30)}${str(field(i, 'summary')) ? `:${clip(str(field(i, 'summary')), 50)}` : ''}`,
29  ToolSearch: () => '加载工具',
30  Skill: i => `用技能 ${clip(str(field(i, 'skill')), 40)}`,
31}
32
33const duration = (ms: number) => (ms < 60_000 ? `${Math.round(ms / 1000)} 秒` : `${Math.floor(ms / 60_000)} 分 ${Math.round((ms % 60_000) / 1000)} 秒`)
34
35type Row = { tool_use_id: string; isRunning: boolean; isErrored: boolean; isInterrupted: boolean; output?: unknown }
36
37const statusOf = (row: Row) => {
38  if (row.isRunning) return '进行中…'
39  if (row.isInterrupted) return '已中断'
40  if (row.isErrored) return '✗ 失败'
41  if (field(row.output, 'timedOutAfterMs') !== undefined) return '超时,转到后台'
42  if (field(row.output, 'backgroundTaskId') !== undefined) return '在后台跑'
43  const ms = took.get(row.tool_use_id)
44  return ms !== undefined && ms >= 2000 ? `✓ ${duration(ms)}` : '✓'
45}
46
47// 失败时交给模型的那段文字(报错调用的 output 就是它),只露最后几行。
48const failureTail = (output: unknown) => {
49  const text = typeof output === 'string' ? output : str(field(output, 'stderr')) || str(field(output, 'stdout'))
50  return text.trim().split('\n').slice(-6).map(line => clip(line, 160)).join('\n')
51}
52
53export const register: Register = on => {
54  on('session.start', async ($, e, next) => {
55    await $.command.register({ name: 'steps', description: '切换每一步的显示:一行中文,或完整命令和输出' })
56    return next(e)
57  })
58
59  on('command.run', { command: 'steps' }, async $ => {
60    await update($, plain, v => !v)
61    return { text: (await read($, plain)) ? '每一步显示一行中文。' : '每一步显示完整命令和输出。' }
62  })
63
64  on('tool.call', async ($, e, next) => {
65    const started = await $.clock.now()
66    const ran = await next(e)
67    took.set(e.tool_use_id, (await $.clock.now()) - started)
68    return ran
69  }).catch(($, e, next) => next(e))
70
71  on('ui.render', { component: 'ToolUse' }, async ($, e, next) => {
72    const label = LABELS[e.props.tool]?.(e.props.input)
73    if (!label || !(await read($, plain))) return next(e)
74    const { Box, Text } = $.ui.resolve(e)
75    const bad = e.props.isErrored && !e.props.isInterrupted
76    return (
77      <Box>
78        {bad ? <Text color="red">● </Text> : e.props.isRunning ? <Text dimColor>● </Text> : <Text color="green">● </Text>}
79        <Text>{label}</Text>
80        {bad ? <Text color="red">{`  ${statusOf(e.props)}`}</Text> : <Text dimColor>{`  ${statusOf(e.props)}`}</Text>}
81      </Box>
82    )
83  })
84
85  on('ui.render', { component: 'ToolResult' }, async ($, e, next) => {
86    if (!LABELS[e.props.tool] || !(await read($, plain))) return next(e)
87    const { Box, Text } = $.ui.resolve(e)
88    if (!e.props.isErrored) return <Box />
89    const tail = failureTail(e.props.output)
90    return tail ? (
91      <Box>
92        <Text color="red" dimColor>{tail}</Text>
93      </Box>
94    ) : (
95      <Box />
96    )
97  })
98}
99
types/index.d.ts 8 lines
1export type Plain = boolean
2
3declare module 'claude-code' {
4  interface PluginState {
5    'plain-steps': { plain: Plain }
6  }
7}
8