SLOPSHOPPER

savvy-progress-th

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

newbandguardcommandstatus
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · savvy-progress-th
› 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 › /progress ⎿ savvy-progress-th: 📊 Savvy Progress TH ⎿ savvy-progress-th: ---------------------------------------- ⎿ savvy-progress-th: ⏱️ เวลาที่ใช้: 0:00 ⎿ savvy-progress-th: 🔧 เรียก tool: 9 ครั้ง ⎿ savvy-progress-th: 🛠️ ตอนนี้: ว่าง ⎿ savvy-progress-th: 🪙 token รวม: 98,880 ⏱️ 0:00 | 🔧 9 | 🪙 98,880 | 💰 2.63฿ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
⏱️ 0:00 | 🔧 9 | 🪙 98,880 | 💰 2.63฿
README

Mod 01: Savvy Progress TH

แถบเหนือช่องพิมพ์ที่บอกว่า 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 แล้วอ่านตามบทที่เขียนไว้ในคอมเมนต์:

  1. จำค่าข้าม event ด้วยตัวแปรนอก register()
  2. ดัก tool.call ทั้งก่อนและหลัง next(e)
  3. อ่าน usage จาก turn.complete
  4. วาดแถบ AbovePrompt และสั่งวาดใหม่ด้วย $.ui.invalidate
  5. สร้างคำสั่งเองด้วย $.command.register + command.run

หมายเหตุ

ราคาเป็นค่าประมาณ แก้ได้ที่ PRICE และ THB_PER_USD ด้านบนของไฟล์

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