SLOPSHOPPER

tool-timeline-th

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

newpanespinnerrowsguardcommand
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · tool-timeline-th
│ ┃ ⏱️ Tool Timeline ✕ › fix the failing auth test and add an audit log call │ ┃ ⏹️ รอบล่าสุด │ ┃ Read auth.ts █ ⏺ Read(src/auth.ts) │ ┃ 0.0s ⎿ Read 6 lines │ ┃ Grep src █ ⏺ Update(src/auth.ts) │ ┃ 0.0s ⎿ Added 2 lines, removed 1 line │ ┃ Edit auth.ts █ ⏺ Bash(bun test) │ ┃ 0.0s ⎿ 3 pass, 1 fail │ ┃ Write audit.ts █ │ ┃ 0.0s ● Done. refresh now rejects expired claims and logs an audit event. │ ┃ Write cache.ts █ │ ┃ 0.0s ✻ Worked… │ ┃ Bash bun test █ 🔧 9 tool · รวม 0.2s · 🐢 ช้าสุด Read auth.ts 0.0s · /timeline │ ┃ 0.0s │ ┃ Bash git status --porcelain █ › /timeline │ ┃ 0.0s ⎿ tool-timeline-th: Read auth.ts ██ │ ┃ Bash rm -rf build && git push --fo █ ⎿ tool-timeline-th: Grep src ██ │ ┃ 0.0s ⎿ tool-timeline-th: Edit auth.ts ██ │ ┃ Bash cat .env ⎿ tool-timeline-th: Write audit.ts │ ┃ █ 0.0s ⎿ tool-timeline-th: Write cache.ts │ ⎿ tool-timeline-th: Bash bun test │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · ⏱️ Tool Timeline
⏹️ รอบล่าสุด Read auth.ts █ 0.0s Grep src █ 0.0s Edit auth.ts █ 0.0s Write audit.ts █ 0.0s Write cache.ts █ 0.0s Bash bun test █ 0.0s Bash git status --porcelain █ 0.0s Bash rm -rf build && git push --fo █ 0.0s Bash cat .env █ 0.0s
Turn line
✻ Worked… 🔧 9 tool · รวม 0.2s · 🐢 ช้าสุด Read auth.ts 0.0s · /timeline
README

Mod 50: Tool Timeline TH

ไทม์ไลน์การทำงานของ 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 แล้วอ่านตามบทที่เขียนไว้ในคอมเมนต์:

  1. ป้ายชื่อสั้นของแต่ละ tool
  2. กราฟแกนต์บนแกนเวลาเดียวกัน
  3. จับเวลาด้วย try/finally รอบ next(e)
  4. แต่งแถว ToolUse โดยห่อของเดิม {await next(e)}
  5. แต่งบรรทัด TurnDuration และแผง

หมายเหตุ

เติมเวลาเฉพาะ tool ที่ใช้ตั้งแต่ 1 วินาที เพื่อไม่ให้แชตรก

Source 1 files
hooks/register.tsx 241 lines
1/**
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