ไทม์ไลน์การทำงานของ Claude: จับเวลาทุก tool โชว์เวลาใต้แต่ละแถว บอกตัวที่ช้าสุดของรอบ และกราฟแกนต์ในแผง

ไทม์ไลน์การทำงานของ Claude แบบ Chrome DevTools: จับเวลาทุก tool เติมเวลาและแท่งเทียบใต้แถว tool ในแชต (ตัวที่ช้าสุดของรอบเป็นสีส้ม) เติมสรุปใต้บรรทัด "Baked for ..." ว่าเวลาหายไปกับอะไร และมีกราฟแกนต์ในแผงให้เห็นว่าอะไรรันพร้อมกัน อะไรรอกัน
| คำสั่ง | ทำอะไร | |
|---|---|---|
/timeline | กราฟแกนต์ของรอบล่าสุด (เปิดแผง) | |
/timeline stats | สถิติแยกตาม tool ทั้ง session | |
| `/timeline on\ | off` | เปิด/ปิดการเติมเวลาใต้แถว tool |
/plugin install tool-timeline-th --marketplace tksoft11/claude-code-mods-th
ต้องตั้ง CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 ก่อนเปิด claude (ดู README หลัก)
เปิด hooks/register.tsx แล้วอ่านตามบทที่เขียนไว้ในคอมเมนต์:
เติมเวลาเฉพาะ tool ที่ใช้ตั้งแต่ 1 วินาที เพื่อไม่ให้แชตรก
hooks/register.tsx 241 lines1/**
2 * ============================================================================
3 * MOD 50: Tool Timeline TH - ไทม์ไลน์การทำงานของ Claude (ฉบับสอนทำ)
4 * ============================================================================
5 * แรงบันดาลใจจาก: แท็บ Network/Performance ใน Chrome DevTools
6 *
7 * หน้าที่ของ Mod นี้:
8 * 1. จับเวลาทุก tool ที่ Claude เรียก (Bash, Read, Edit, Grep, MCP ...)
9 * 2. แต่งแถว tool ในแชต: เติมเวลาที่ใช้และแท่งเทียบกับตัวที่ช้าสุดของรอบ
10 * 3. แต่งบรรทัด "Baked for 1m 4s" ท้ายรอบ: บอกว่า tool ไหนกินเวลามากสุด
11 * 4. /timeline - กราฟแกนต์ของรอบล่าสุด เห็นว่าอะไรรันพร้อมกัน อะไรรอกัน
12 * 5. /timeline stats - สถิติแยกตามชนิด tool ทั้ง session
13 */
14
15import type { EngineInterface, Register } from 'claude-code'
16
17interface Span {
18 id: string
19 tool: string
20 label: string
21 start: number
22 end?: number
23 error?: boolean
24 agent?: boolean
25}
26
27const PANE = 'tool-timeline-th'
28const MIN_SHOW_MS = 1000 // เติมเวลาใต้แถวเฉพาะ tool ที่ใช้เวลาตั้งแต่ 1 วินาที
29let current: Span[] = [] // รอบที่กำลังทำ
30let lastTurn: Span[] = [] // รอบที่จบล่าสุด
31let byId = new Map<string, Span>()
32let totals = new Map<string, { count: number; ms: number; max: number }>()
33let turnStart = 0
34let enabled = true
35
36// ==========================================================================
37// บทที่ 1: ป้ายชื่อสั้นๆ ของแต่ละ tool (คำสั่ง Bash / ชื่อไฟล์ / pattern)
38// ==========================================================================
39function labelOf(e: Record<string, unknown>): string {
40 const pick = (k: string) => (typeof e[k] === 'string' ? (e[k] as string) : '')
41 const file = pick('file_path') || pick('path') || pick('notebook_path')
42 const raw = pick('command') || (file ? file.split(/[\\/]/).pop() ?? file : '') || pick('pattern') || pick('url') || pick('description') || pick('query')
43 const oneLine = raw.replace(/\s+/g, ' ').trim()
44 return oneLine.length > 40 ? `${oneLine.slice(0, 39)}…` : oneLine
45}
46
47function secs(ms: number): string {
48 return ms >= 60_000 ? `${Math.floor(ms / 60_000)}m ${Math.round((ms % 60_000) / 1000)}s` : `${(ms / 1000).toFixed(1)}s`
49}
50
51function duration(s: Span): number {
52 return (s.end ?? s.start) - s.start
53}
54
55function slowest(spans: Span[]): Span | undefined {
56 return spans.filter(s => s.end).sort((a, b) => duration(b) - duration(a))[0]
57}
58
59// ==========================================================================
60// บทที่ 2: กราฟแกนต์ - แกนเวลาเดียวกัน แต่ละแถวคือหนึ่ง tool
61// ==========================================================================
62function gantt(spans: Span[], width: number): { label: string; bar: string; time: string; error?: boolean }[] {
63 const done = spans.filter(s => s.end)
64 if (done.length === 0) return []
65 const t0 = Math.min(...done.map(s => s.start))
66 const t1 = Math.max(...done.map(s => s.end ?? s.start))
67 const scale = Math.max(1, t1 - t0) / width
68 return done.map(s => {
69 const from = Math.floor((s.start - t0) / scale)
70 const len = Math.max(1, Math.round(duration(s) / scale))
71 return {
72 label: `${s.agent ? '↳' : ''}${s.tool} ${s.label}`.slice(0, 34),
73 bar: ' '.repeat(Math.min(from, width - 1)) + '█'.repeat(Math.min(len, width - Math.min(from, width - 1))),
74 time: secs(duration(s)),
75 error: s.error,
76 }
77 })
78}
79
80const HELP = `⏱️ Tool Timeline TH (Mod 50)
81========================================
82/timeline กราฟแกนต์ของรอบล่าสุด (เปิดแผง)
83/timeline stats สถิติแยกตาม tool ทั้ง session
84/timeline on|off เปิด/ปิดการเติมเวลาใต้แถว tool (ตอนนี้: STATE)`
85
86function statsText(): string {
87 if (totals.size === 0) return 'ยังไม่มีข้อมูล ให้ Claude ทำงานสักรอบก่อน'
88 const rows = [...totals.entries()].sort((a, b) => b[1].ms - a[1].ms)
89 const all = rows.reduce((s, [, v]) => s + v.ms, 0)
90 return [
91 `📊 เวลาที่ใช้ใน tool ทั้ง session: ${secs(all)}`,
92 `${'tool'.padEnd(14)} ${'ครั้ง'.padStart(5)} ${'รวม'.padStart(8)} ${'เฉลี่ย'.padStart(8)} ${'นานสุด'.padStart(8)}`,
93 ...rows.map(([tool, v]) => `${tool.slice(0, 14).padEnd(14)} ${String(v.count).padStart(5)} ${secs(v.ms).padStart(8)} ${secs(v.ms / v.count).padStart(8)} ${secs(v.max).padStart(8)} ${'▇'.repeat(Math.max(1, Math.round((v.ms / all) * 20)))}`),
94 ].join('\n')
95}
96
97function finishTurn($: EngineInterface): void {
98 if (current.length) lastTurn = current
99 current = []
100 $.ui.invalidate('ui.render')
101}
102
103export const register: Register = on => {
104 on('session.start', async ($, e, next) => {
105 enabled = ((await $.store.get('enabled')) as boolean | undefined) ?? true
106 current = []
107 lastTurn = []
108 byId = new Map()
109 totals = new Map()
110 await $.command.register({ name: 'timeline', description: 'ไทม์ไลน์ของ tool ที่ Claude ใช้: เวลาแต่ละตัว ตัวที่ช้าสุด กราฟแกนต์ (/timeline help)', argumentHint: 'stats|on|off' })
111 return next(e)
112 })
113
114 on('turn.start', async ($, e, next) => {
115 if (!e.agentId) {
116 finishTurn($)
117 turnStart = await $.clock.now()
118 }
119 return next(e)
120 })
121
122 // ==========================================================================
123 // บทที่ 3: จับเวลา - เริ่มก่อน next(e) จบหลัง next(e)
124 // ==========================================================================
125 on('tool.call', async ($, e, next) => {
126 const span: Span = { id: e.tool_use_id, tool: e.tool, label: labelOf(e as unknown as Record<string, unknown>), start: await $.clock.now(), agent: !!e.agentId }
127 current.push(span)
128 byId.set(span.id, span)
129 if (byId.size > 2000) byId = new Map([...byId].slice(-1000))
130 try {
131 const r = await next(e)
132 span.error = !!r.isError || r.deny !== undefined
133 return r
134 } finally {
135 span.end = await $.clock.now()
136 const t = totals.get(span.tool) ?? { count: 0, ms: 0, max: 0 }
137 totals.set(span.tool, { count: t.count + 1, ms: t.ms + duration(span), max: Math.max(t.max, duration(span)) })
138 $.ui.invalidate('ui.render')
139 }
140 })
141
142 // ==========================================================================
143 // บทที่ 4: แต่งแถว tool - ห่อของเดิมไว้ แล้วเติมเวลาต่อท้าย
144 // ==========================================================================
145 on('ui.render', { component: 'ToolUse' }, async ($, e, next) => {
146 const span = byId.get(e.props.tool_use_id)
147 if (!enabled || !span?.end || e.props.isRunning || duration(span) < MIN_SHOW_MS) return next(e)
148 const { Box, Text } = $.ui.resolve(e)
149 const peers = current.includes(span) ? current : lastTurn
150 const max = Math.max(...peers.filter(s => s.end).map(duration), 1)
151 const isSlowest = slowest(peers) === span && peers.length > 1
152 return (
153 <Box flexDirection="column">
154 {await next(e)}
155 <Text dimColor>
156 {' ⏱️ '}
157 {secs(duration(span))} <Text color={isSlowest ? '#ff7043' : '#78909c'}>{'▇'.repeat(Math.max(1, Math.round((duration(span) / max) * 12)))}</Text>
158 {isSlowest ? ' ช้าสุดของรอบ' : ''}
159 </Text>
160 </Box>
161 )
162 })
163
164 // บรรทัดท้ายรอบ "Baked for 1m 4s" - เติมสรุปว่าเวลาหายไปไหน
165 on('ui.render', { component: 'TurnDuration' }, async ($, e, next) => {
166 const spans = lastTurn.length ? lastTurn : current
167 const worst = slowest(spans)
168 if (!enabled || !worst || e.props.waitingFor) return next(e)
169 const { Box, Text } = $.ui.resolve(e)
170 const toolMs = spans.reduce((s, x) => s + duration(x), 0)
171 return (
172 <Box flexDirection="column">
173 {await next(e)}
174 <Text dimColor>
175 {` 🔧 ${spans.length} tool · รวม ${secs(toolMs)} · 🐢 ช้าสุด ${worst.tool} ${worst.label} ${secs(duration(worst))} · /timeline`}
176 </Text>
177 </Box>
178 )
179 })
180
181 on('turn.complete', async ($, e, next) => {
182 const r = await next(e)
183 if (!e.agentId) finishTurn($)
184 return r
185 })
186
187 // ==========================================================================
188 // บทที่ 5: แผงกราฟแกนต์
189 // ==========================================================================
190 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
191 const { Box, Text } = $.ui.resolve(e)
192 const spans = current.length ? current : lastTurn
193 const width = Math.max(10, Math.min(50, e.props.bodyColumns - 50))
194 const rows = gantt(spans, width)
195 const now = await $.clock.now()
196 return (
197 <Box flexDirection="column">
198 <Text bold>{current.length ? `▶️ รอบนี้ (ผ่านไป ${secs(now - turnStart)})` : '⏹️ รอบล่าสุด'}</Text>
199 {rows.length === 0 && <Text dimColor>ยังไม่มี tool ในรอบนี้</Text>}
200 {rows.map(r => (
201 <Text>
202 {r.label.padEnd(35)}
203 <Text color={r.error ? '#e53935' : '#4fc3f7'}>{r.bar.padEnd(width)}</Text> {r.time}
204 </Text>
205 ))}
206 </Box>
207 )
208 })
209
210 on('command.run', { command: 'timeline' }, async ($, e) => {
211 const [sub = ''] = e.args.trim().split(/\s+/).filter(Boolean)
212 switch (sub) {
213 case '': {
214 const spans = current.length ? current : lastTurn
215 await $.ui.open({ id: PANE, title: '⏱️ Tool Timeline' })
216 const rows = gantt(spans, 30)
217 return { text: rows.length ? rows.map(r => `${r.label.padEnd(35)} ${r.bar.padEnd(30)} ${r.time}${r.error ? ' ❌' : ''}`).join('\n') : 'ยังไม่มีข้อมูล ให้ Claude ทำงานสักรอบก่อน' }
218 }
219 case 'stats':
220 return { text: statsText() }
221 case 'on':
222 case 'off':
223 enabled = sub === 'on'
224 await $.store.set('enabled', enabled)
225 $.ui.invalidate('ui.render')
226 return { text: enabled ? '⏱️ เติมเวลาใต้แถว tool แล้ว' : 'ปิดการเติมเวลาแล้ว (แผงและ /timeline ยังใช้ได้)' }
227 default:
228 return { text: HELP.replace('STATE', enabled ? 'เปิด' : 'ปิด') }
229 }
230 })
231}
232
233/**
234 * ========================================================================
235 * การบ้าน / ต่อยอด
236 * ========================================================================
237 * 1. เตือนเมื่อ tool เดิมช้าผิดปกติ (นานกว่าค่าเฉลี่ย 3 เท่า)
238 * 2. ส่งออกไทม์ไลน์เป็นไฟล์ Chrome Trace (JSON) เปิดใน chrome://tracing
239 * 3. แยกสีตาม subagent ให้เห็นงานที่รันขนานกัน
240 */
241