แถบ progress ภาษาไทย: เวลาที่ใช้, จำนวน tool, token และค่าใช้จ่ายเป็นบาท

แถบเหนือช่องพิมพ์ที่บอกว่า session นี้ใช้เวลาไปเท่าไหร่ เรียก tool กี่ครั้ง ใช้ token เท่าไหร่ และคิดเป็นเงินบาทประมาณเท่าไหร่ ระหว่างที่ tool ทำงานจะขึ้นชื่อ tool ใน status line ด้วย
| คำสั่ง | ทำอะไร |
|---|---|
/progress | ดูรายงานเต็ม |
/progress hide | ซ่อนแถบ |
/progress show | แสดงแถบ |
/plugin install savvy-progress-th --marketplace tksoft11/claude-code-mods-th
ต้องตั้ง CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 ก่อนเปิด claude (ดู README หลัก)
เปิด hooks/register.tsx แล้วอ่านตามบทที่เขียนไว้ในคอมเมนต์:
register()tool.call ทั้งก่อนและหลัง next(e)usage จาก turn.completeAbovePrompt และสั่งวาดใหม่ด้วย $.ui.invalidate$.command.register + command.runราคาเป็นค่าประมาณ แก้ได้ที่ PRICE และ THB_PER_USD ด้านบนของไฟล์
hooks/register.tsx 203 lines1/**
2 * ============================================================================
3 * MOD 01: Savvy Progress TH - แถบ Progress แบบละเอียด (ฉบับสอนทำ)
4 * ============================================================================
5 * แรงบันดาลใจจาก: https://github.com/JohnnyVizz/claude-savvy
6 *
7 * หน้าที่ของ Mod นี้:
8 * 1. แสดงเวลาที่ใช้ไปตั้งแต่เริ่ม session
9 * 2. นับจำนวนครั้งที่เรียก tool และบอกว่ากำลังรัน tool อะไร
10 * 3. รวม token ที่ใช้ และคิดเป็นเงินบาทโดยประมาณ
11 * 4. วาดแถบเหนือช่องพิมพ์ (AbovePrompt) + มีคำสั่ง /progress
12 *
13 * แนวคิดสำคัญของ Claude Code Mod:
14 * - on("ชื่อ event", hook) ใช้ดัก event; hook ได้ ($, e, next)
15 * - $ คือ engine ของ Claude Code (เรียกแบบ $.noun.method เช่น $.ui.toast)
16 * - e คือข้อมูลของ event, next(e) คือ "ทำงานต่อตามปกติ"
17 * - ตัวแปรนอก register() จำค่าข้าม event ได้ (แต่จะเริ่มใหม่เมื่อ reload mod)
18 * - เปลี่ยนค่าที่วาดบนจอแล้ว ต้องเรียก $.ui.invalidate("ui.render") ให้จอวาดใหม่
19 */
20
21import type { EngineInterface, Register, TurnUsage } from 'claude-code'
22
23type ProgressStats = {
24 startedAt: number // เวลาเริ่ม session (ms)
25 toolCalls: number // เรียก tool ไปกี่ครั้ง
26 currentTool: string // tool ที่กำลังรันอยู่ ("" = ว่าง)
27 inputTokens: number
28 outputTokens: number
29 cacheReadTokens: number
30 cacheWriteTokens: number
31 costUSD: number
32}
33
34// ราคาโดยประมาณ (USD ต่อ 1 ล้าน token) - ตรวจราคาล่าสุดที่ anthropic.com/pricing แล้วแก้ได้เลย
35const PRICE = { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.3 }
36const THB_PER_USD = 36.5 // อัตราแลกเปลี่ยน ปรับตามจริง
37
38const EMPTY: ProgressStats = {
39 startedAt: 0,
40 toolCalls: 0,
41 currentTool: '',
42 inputTokens: 0,
43 outputTokens: 0,
44 cacheReadTokens: 0,
45 cacheWriteTokens: 0,
46 costUSD: 0,
47}
48
49// state ของ Mod: ตัวแปรนอก register() จำค่าข้าม event ได้
50let stats: ProgressStats = { ...EMPTY }
51let isHidden = false
52
53// แก้ค่าแล้วบอก engine ให้วาดแถบใหม่ (วาดได้สูงสุด ~10 ครั้ง/วินาที engine รวมให้เอง)
54function patchStats($: EngineInterface, fn: (s: ProgressStats) => ProgressStats): void {
55 stats = fn(stats)
56 $.ui.invalidate('ui.render')
57}
58
59// ฟังก์ชันช่วย: แปลง ms เป็น 1:05:23 หรือ 5:23
60function formatElapsed(ms: number): string {
61 const sec = Math.max(0, Math.floor(ms / 1000))
62 const h = Math.floor(sec / 3600)
63 const m = Math.floor((sec % 3600) / 60)
64 const s = sec % 60
65 const pad = (n: number) => String(n).padStart(2, '0')
66 return h > 0 ? `${h}:${pad(m)}:${pad(s)}` : `${m}:${pad(s)}`
67}
68
69// ฟังก์ชันช่วย: คิดเงินจาก usage ของหนึ่ง turn
70function costOf(usage: TurnUsage): number {
71 return (
72 (usage.input_tokens / 1e6) * PRICE.input +
73 (usage.output_tokens / 1e6) * PRICE.output +
74 (usage.cache_creation_input_tokens / 1e6) * PRICE.cacheWrite +
75 (usage.cache_read_input_tokens / 1e6) * PRICE.cacheRead
76 )
77}
78
79function totalTokens(s: ProgressStats): number {
80 return s.inputTokens + s.outputTokens + s.cacheReadTokens + s.cacheWriteTokens
81}
82
83function buildReport(s: ProgressStats, now: number): string {
84 const thb = s.costUSD * THB_PER_USD
85 return [
86 '📊 Savvy Progress TH',
87 '----------------------------------------',
88 `⏱️ เวลาที่ใช้: ${formatElapsed(now - s.startedAt)}`,
89 `🔧 เรียก tool: ${s.toolCalls} ครั้ง`,
90 `🛠️ ตอนนี้: ${s.currentTool || 'ว่าง'}`,
91 `🪙 token รวม: ${totalTokens(s).toLocaleString('en-US')}`,
92 ` ├─ input ${s.inputTokens.toLocaleString('en-US')} | output ${s.outputTokens.toLocaleString('en-US')}`,
93 ` └─ cache อ่าน ${s.cacheReadTokens.toLocaleString('en-US')} | cache เขียน ${s.cacheWriteTokens.toLocaleString('en-US')}`,
94 `💰 ค่าใช้จ่ายโดยประมาณ: $${s.costUSD.toFixed(4)} (~${thb.toFixed(2)}฿)`,
95 ].join('\n')
96}
97
98export const register: Register = on => {
99 // ==========================================================================
100 // บทที่ 1: session.start - เริ่มนับใหม่ + ลงทะเบียนคำสั่ง /progress
101 // ==========================================================================
102 on('session.start', async ($, e, next) => {
103 const now = await $.clock.now()
104 patchStats($, () => ({ ...EMPTY, startedAt: now }))
105
106 // คำสั่ง slash ต้องลงทะเบียนก่อน แล้วค่อยตอบใน command.run (บทที่ 5)
107 await $.command.register({
108 name: 'progress',
109 description: 'ดูความคืบหน้า: เวลา, จำนวน tool, token และค่าใช้จ่ายเป็นบาท',
110 argumentHint: '[show|hide]',
111 })
112
113 return next(e) // สำคัญ: ต้อง next ไม่งั้น session จะไม่เริ่มต่อ
114 })
115
116 // ==========================================================================
117 // บทที่ 2: tool.call - รู้ว่ากำลังรัน tool อะไร
118 // ==========================================================================
119 // ก่อน next = tool ยังไม่รัน, หลัง await next = tool รันเสร็จแล้ว
120 on('tool.call', async ($, e, next) => {
121 patchStats($, s => ({ ...s, toolCalls: s.toolCalls + 1, currentTool: e.tool }))
122 $.ui.status(`⏳ กำลังรัน: ${e.tool}`)
123
124 const ran = await next(e)
125
126 patchStats($, s => ({ ...s, currentTool: '' }))
127 $.ui.status(undefined) // undefined = ลบข้อความใน status line
128
129 return ran // ส่งผลลัพธ์ของ tool ต่อไปตามเดิม
130 })
131
132 // ==========================================================================
133 // บทที่ 3: turn.complete - Claude ตอบจบหนึ่งรอบ ได้ usage มาบวกเพิ่ม
134 // ==========================================================================
135 on('turn.complete', async ($, e, next) => {
136 const usage = e.usage
137 if (usage) {
138 patchStats($, s => ({
139 ...s,
140 inputTokens: s.inputTokens + usage.input_tokens,
141 outputTokens: s.outputTokens + usage.output_tokens,
142 cacheReadTokens: s.cacheReadTokens + usage.cache_read_input_tokens,
143 cacheWriteTokens: s.cacheWriteTokens + usage.cache_creation_input_tokens,
144 costUSD: s.costUSD + costOf(usage),
145 }))
146 }
147 return next(e)
148 })
149
150 // ==========================================================================
151 // บทที่ 4: ui.render - วาดแถบเหนือช่องพิมพ์ (AbovePrompt)
152 // ==========================================================================
153 // engine เรียก hook นี้ทุกครั้งที่ต้องวาดแถบ (รวมถึงหลัง $.ui.invalidate)
154 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
155 const s = stats
156 if (e.props.hasSurvey || s.startedAt === 0 || isHidden) {
157 return next(e) // ไม่วาดอะไร ให้ engine วาดของมันเอง
158 }
159
160 const { Box, Text } = $.ui.resolve(e) // element ของจอนี้ (terminal/desktop/...)
161 const now = await $.clock.now()
162 const thb = s.costUSD * THB_PER_USD
163
164 return (
165 <Box>
166 <Text dimColor>
167 ⏱️ {formatElapsed(now - s.startedAt)} | 🔧 {s.toolCalls} | 🪙{' '}
168 {totalTokens(s).toLocaleString('en-US')} | 💰 {thb.toFixed(2)}฿
169 {s.currentTool ? ` | 🛠️ ${s.currentTool}` : ''}
170 </Text>
171 </Box>
172 )
173 })
174
175 // ==========================================================================
176 // บทที่ 5: command.run - ตอบคำสั่ง /progress ของเราเอง
177 // ==========================================================================
178 // ไม่เรียก next เพราะเราตอบเองทั้งหมด; { text } จะแสดงเป็นผลลัพธ์ของคำสั่ง
179 on('command.run', { command: 'progress' }, async ($, e) => {
180 const arg = e.args.trim()
181 if (arg === 'hide') {
182 isHidden = true
183 $.ui.invalidate('ui.render')
184 return { text: '🙈 ซ่อนแถบ progress แล้ว (/progress show เพื่อแสดงอีกครั้ง)' }
185 }
186 if (arg === 'show') {
187 isHidden = false
188 $.ui.invalidate('ui.render')
189 return { text: '👀 แสดงแถบ progress แล้ว' }
190 }
191 return { text: buildReport(stats, await $.clock.now()) }
192 })
193}
194
195/**
196 * ========================================================================
197 * การบ้าน / ต่อยอด
198 * ========================================================================
199 * 1. เก็บยอดรวมข้าม session ด้วย $.store.set("total", ...) แล้วโชว์ "เดือนนี้ใช้ไปกี่บาท"
200 * 2. ถ้า turn ไหนนานเกิน 2 นาที ให้ $.ui.toast("งานนี้ใช้เวลานานนะ")
201 * 3. เปลี่ยน PRICE ให้ตรงกับ model ที่ใช้ (usage.model บอกชื่อ model ของ turn นั้น)
202 */
203