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

Claude Code 的一个 mod。
Claude 干活时,终端里每一步都会打出完整的命令和输出:路径、参数、JSON,一屏接一屏。要知道它做得对不对,你得自己读完这些。
plain-steps 替你读完。每一步只留一行:在做什么、用了多久、成没成。出错的那一步下面留着最后几行报错,你可以直接判断要不要插手,也能顺着报错找原因。想看细节,输入 /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 把它转到后台继续跑 |
在后台跑 | 这一步一开始就在后台运行 |
已中断 | 你中断了这一步 |
✓ 只表示命令正常结束。如果测试脚本在测试失败时仍然正常退出,这里也会是 ✓。输入 /steps:每一步切回完整命令和输出。再输入一次:切回一行。
MIT
hooks/register.tsx 99 lines1import { 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}
99types/index.d.ts 8 lines1export type Plain = boolean
2
3declare module 'claude-code' {
4 interface PluginState {
5 'plain-steps': { plain: Plain }
6 }
7}
8