เพื่อนน่ารักเหนือช่องพิมพ์ที่เปลี่ยนสีหน้าตาม Claude: คิดอยู่ ทำงาน เจอ error ทำเสร็จ พร้อม kaomoji 50 แบบ

มาสคอตน่ารักเหนือช่องพิมพ์ที่เปลี่ยนสีหน้าตามสถานะจริงของ Claude: กำลังคิด / ใช้เครื่องมืออะไรอยู่ / เจอ error / เสร็จแล้ว / รอคุณตอบ / ว่าง เลือกได้ 6 ตัว (แมว หมี กระต่าย เพนกวิน ผี ก้อนวุ้น) และคลัง kaomoji 50 แบบ 5 หมวดที่ส่งลงช่องพิมพ์ได้
| คำสั่ง | ทำอะไร | |
|---|---|---|
/kawaii | ดูมาสคอตและสถานะ | |
/kawaii pick <ตัว> | เปลี่ยนตัว: cat bear bunny penguin ghost blob | |
/kawaii preview | ทุกอารมณ์ของตัวปัจจุบัน | |
/kawaii gallery [หมวด] | คลัง kaomoji 50 แบบ | |
/kawaii use <หมวด> <เลข> | ส่ง kaomoji ลงช่องพิมพ์ | |
| `/kawaii on\ | off` | แสดง/ซ่อนมาสคอต |
/plugin install kawaii-th --marketplace tksoft11/claude-code-mods-th
ต้องตั้ง CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 ก่อนเปิด claude (ดู README หลัก)
เปิด hooks/register.tsx แล้วอ่านตามบทที่เขียนไว้ในคอมเมนต์:
spinner น่ารักอยู่ใน Mod 35 แล้ว ตัวนี้เน้นมาสคอตตามสถานะ ถ้าเปิดหลาย Mod ที่วาดเหนือช่องพิมพ์พร้อมกัน (เช่น Mod 01) อาจเห็นแค่ตัวเดียว
hooks/register.tsx 286 lines1/**
2 * ============================================================================
3 * MOD 44: Kawaii TH - มาสคอตน่ารักที่เปลี่ยนหน้าตามอารมณ์ของ Claude (ฉบับสอนทำ)
4 * ============================================================================
5 * แรงบันดาลใจจาก: Tamagotchi และ kaomoji ญี่ปุ่น (◕‿◕)
6 *
7 * หน้าที่ของ Mod นี้:
8 * 1. มาสคอตเหนือช่องพิมพ์ เปลี่ยนสีหน้าตามสถานะจริงของ Claude:
9 * กำลังคิด / ใช้เครื่องมือ / เจอ error / เสร็จแล้ว / รอคุณตอบ / ว่าง
10 * 2. เลือกตัวละครได้ 6 ตัว (แมว หมี กระต่าย เพนกวิน ผี ก้อนวุ้น)
11 * 3. คลัง kaomoji 50 แบบ แบ่งหมวด ส่งเข้าช่องพิมพ์ได้
12 *
13 * หมายเหตุ: spinner น่ารักอยู่ใน Mod 35 แล้ว ตัวนี้เน้นมาสคอตตามสถานะ
14 */
15
16import type { EngineInterface, Register, Timer } from 'claude-code'
17
18type Mood = 'idle' | 'thinking' | 'tool' | 'error' | 'done' | 'waiting'
19
20// ==========================================================================
21// บทที่ 1: ตัวละคร - แต่ละตัวมีหน้า 2 เฟรมต่ออารมณ์ (สลับเฟรม = กระพริบ/ขยับ)
22// ==========================================================================
23interface Mascot {
24 name: string
25 color: string
26 faces: Record<Mood, [string, string]>
27}
28
29const MASCOTS = {
30 cat: {
31 name: 'แมวเหมียว',
32 color: '#ff8fab',
33 faces: {
34 idle: ['ฅ^•ﻌ•^ฅ', 'ฅ^-ﻌ-^ฅ'],
35 thinking: ['(=ↀωↀ=)?', '(=ↀωↀ=)…'],
36 tool: ['ฅ(=•ω•=)ฅ🔧', 'ฅ(=•ω•=)ฅ🔨'],
37 error: ['(=TωT=)', '(=;ω;=)'],
38 done: ['(=^▽^=)✨', '(=^ω^=)🎉'],
39 waiting: ['(=・ω・=)ノ?', '(=・ω・=)ノ!'],
40 },
41 },
42 bear: {
43 name: 'หมีน้อย',
44 color: '#c08552',
45 faces: {
46 idle: ['ʕ•ᴥ•ʔ', 'ʕ-ᴥ-ʔ'],
47 thinking: ['ʕ•ᴥ•ʔ💭', 'ʕ·ᴥ·ʔ💭'],
48 tool: ['ʕ•̀ᴥ•́ʔ✧🔧', 'ʕ•̀ᴥ•́ʔ✧⚙️'],
49 error: ['ʕ´•ᴥ•`ʔ', 'ʕ;ᴥ;ʔ'],
50 done: ['ʕ•ᴥ•ʔ🍯', 'ʕ^ᴥ^ʔ🍯'],
51 waiting: ['ʕ•ᴥ•ʔノ', 'ʕ•ᴥ•ʔノ゛'],
52 },
53 },
54 bunny: {
55 name: 'กระต่าย',
56 color: '#b39ddb',
57 faces: {
58 idle: ['(\\_/)\n( •.•)', '(\\_/)\n( -.-)'],
59 thinking: ['(\\_/)\n( •.•)?', '(\\_/)\n( °.°)?'],
60 tool: ['(\\_/)\n( •.•)>🥕', '(\\_/)\n( •.•)>🔧'],
61 error: ['(\\_/)\n( T.T)', '(\\_/)\n( ;.;)'],
62 done: ['(\\_/)\n( ^.^)✨', '(\\_/)\n(≧.≦)🎉'],
63 waiting: ['(\\_/)\n( •.•)ノ', '(\\_/)\n( •.•)/'],
64 },
65 },
66 penguin: {
67 name: 'เพนกวิน',
68 color: '#4fc3f7',
69 faces: {
70 idle: ['<(\'v\')>', '<(-v-)>'],
71 thinking: ['<(\'v\')>?', '<(°v°)>?'],
72 tool: ['<(\'v\')>🔧', '<(\'v\')>💻'],
73 error: ['<(;v;)>', '<(TvT)>'],
74 done: ['\\(^v^)/', '<(^v^)>🐟'],
75 waiting: ['<(\'v\')>ノ', '<(\'v\')>/'],
76 },
77 },
78 ghost: {
79 name: 'ผีน้อย',
80 color: '#b0bec5',
81 faces: {
82 idle: ['👻 (ᵔᴥᵔ)', '👻 (ᵕᴥᵕ)'],
83 thinking: ['👻 (・_・ヾ', '👻 (・_・;)'],
84 tool: ['👻 ᕦ(ò_óˇ)ᕤ', '👻 ᕙ(ò_óˇ)ᕗ'],
85 error: ['👻 (×_×)', '👻 (x_x)'],
86 done: ['👻 ヽ(・∀・)ノ', '👻 \\(•◡•)/'],
87 waiting: ['👻 (・ω・)ノ', '👻 (・ω・)/'],
88 },
89 },
90 blob: {
91 name: 'ก้อนวุ้น',
92 color: '#81c784',
93 faces: {
94 idle: ['(。•ᴗ•。)', '(。-ᴗ-。)'],
95 thinking: ['(。•ˇ‸ˇ•。)', '(。•́︿•̀。)?'],
96 tool: ['(ง •̀_•́)ง', '(ว •̀_•́)ว'],
97 error: ['(。•́︿•̀。)', '(。╯︵╰。)'],
98 done: ['(ノ◕ヮ◕)ノ*:・゚✧', '(ノ´ヮ`)ノ*: ・゚'],
99 waiting: ['(。•ᴗ•。)ノ', '(。•ᴗ•。)/'],
100 },
101 },
102} satisfies Record<string, Mascot>
103
104function findMascot(key: string): Mascot | undefined {
105 return (MASCOTS as Record<string, Mascot>)[key]
106}
107
108const MOOD_TEXT: Record<Mood, string> = {
109 idle: 'ว่างอยู่ คุยกันได้นะ',
110 thinking: 'กำลังคิด…',
111 tool: 'กำลังทำงาน',
112 error: 'อุ๊ย มีอะไรผิดพลาด',
113 done: 'เสร็จแล้ว!',
114 waiting: 'รอคุณอยู่นะ',
115}
116
117// ==========================================================================
118// บทที่ 2: คลัง kaomoji 50 แบบ
119// ==========================================================================
120const KAOMOJI: Record<string, string[]> = {
121 ดีใจ: ['(◕‿◕)', '(^▽^)', '(≧▽≦)', 'ヽ(・∀・)ノ', '(ノ◕ヮ◕)ノ*:・゚✧', '٩(◕‿◕。)۶', '(*^‿^*)', '\\(^o^)/', '(✿◠‿◠)', '(っ˘ω˘ς)'],
122 รัก: ['(♡˙︶˙♡)', '(´。• ᵕ •。`) ♡', '(◍•ᴗ•◍)❤', '♡(ᐢ ᴥ ᐢし)', '(っ´▽`)っ♡', '(˘³˘)♥', '(*˘︶˘*).。.:*♡', '(ɔˆ ³(ˆ⌣ˆc)', '♡^▽^♡', '(灬º‿º灬)♡'],
123 เศร้า: ['(╥﹏╥)', '(。•́︿•̀。)', '(ಥ﹏ಥ)', '(T_T)', '(´;ω;`)', '(っ˘̩╭╮˘̩)っ', '(。╯︵╰。)', '(ノ_<。)', '(-_-;)', '(;へ:)'],
124 สู้ๆ: ['(ง •̀_•́)ง', 'ᕦ(ò_óˇ)ᕤ', '(๑•̀ㅂ•́)و✧', '(ง°ل͜°)ง', '٩(ˊᗜˋ*)و', 'o(`ω´ )o', '(•̀ᴗ•́)و ̑̑', '(ᗒᗣᗕ)՞', '୧(๑•̀ᗝ•́)૭', '(メ` ロ ´)'],
125 สัตว์: ['ฅ^•ﻌ•^ฅ', 'ʕ•ᴥ•ʔ', '(=^・ω・^=)', 'U・ᴥ・U', "<('v')>", '(・θ・)', '(•ㅅ•)', '∪・ω・∪', '(◕ᴥ◕)', '/ᐠ。ꞈ。ᐟ\\'],
126}
127
128let mood: Mood = 'idle'
129let mascot = 'cat'
130let enabled = true
131let toolName = ''
132let frame = 0
133let animator: Timer | undefined
134let settle: Timer | undefined
135
136// ==========================================================================
137// บทที่ 3: เปลี่ยนอารมณ์ - ขยับเฟรมเฉพาะตอนกำลังทำงาน ว่างแล้วหยุด timer
138// ==========================================================================
139function setMood($: EngineInterface, next: Mood): void {
140 mood = next
141 settle?.cancel()
142 settle = undefined
143 const busy = next === 'thinking' || next === 'tool'
144 if (busy && !animator) animator = $.clock.every(700, () => {
145 frame++
146 $.ui.invalidate('ui.render')
147 })
148 if (!busy) {
149 animator?.cancel()
150 animator = undefined
151 }
152 // done/error ค้างไว้สักพัก แล้วกลับเป็นว่าง
153 if (next === 'done' || next === 'error') settle = $.clock.after(8_000, () => setMood($, 'idle'))
154 $.ui.invalidate('ui.render')
155}
156
157async function save($: EngineInterface): Promise<void> {
158 await $.store.set('kawaii', { mascot, enabled })
159}
160
161const HELP = `🌸 Kawaii TH (Mod 44)
162========================================
163/kawaii ดูมาสคอตและสถานะ
164/kawaii pick <ตัว> เปลี่ยนตัว: ${Object.entries(MASCOTS).map(([k, m]) => `${k}=${m.name}`).join(' ')}
165/kawaii preview ดูทุกอารมณ์ของตัวปัจจุบัน
166/kawaii gallery [หมวด] คลัง kaomoji 50 แบบ (${Object.keys(KAOMOJI).join(' ')})
167/kawaii use <หมวด> <เลข> ส่ง kaomoji ลงช่องพิมพ์
168/kawaii on|off แสดง/ซ่อนมาสคอต`
169
170export const register: Register = on => {
171 on('session.start', async ($, e, next) => {
172 const saved = (await $.store.get('kawaii')) as { mascot?: string; enabled?: boolean } | undefined
173 mascot = saved?.mascot && findMascot(saved.mascot) ? saved.mascot : 'cat'
174 enabled = saved?.enabled ?? true
175 mood = 'idle'
176 await $.command.register({ name: 'kawaii', description: 'มาสคอตน่ารักที่เปลี่ยนหน้าตามสถานะ Claude + คลัง kaomoji (/kawaii help)', argumentHint: 'pick|preview|gallery|use|on|off' })
177 return next(e)
178 })
179
180 // ==========================================================================
181 // บทที่ 4: ฟังสถานะจริงของ Claude (เฉพาะงานหลัก ไม่นับ subagent)
182 // ==========================================================================
183 on('turn.start', async ($, e, next) => {
184 if (!e.agentId) setMood($, 'thinking')
185 return next(e)
186 })
187
188 on('tool.call', async ($, e, next) => {
189 toolName = e.tool
190 if (mood !== 'error') setMood($, 'tool')
191 const r = await next(e)
192 if (r.isError) setMood($, 'error')
193 else if (mood === 'tool') setMood($, 'thinking')
194 return r
195 })
196
197 on('turn.complete', async ($, e, next) => {
198 const r = await next(e)
199 if (!e.agentId) setMood($, e.reason === 'error' ? 'error' : e.reason === 'aborted' ? 'idle' : 'done')
200 return r
201 })
202
203 // Claude Code แจ้งว่ารออนุญาต/รอคำตอบ
204 on('classic.Notification', async ($, e, next) => {
205 setMood($, 'waiting')
206 return next(e)
207 })
208
209 on('prompt.submit', async ($, e, next) => {
210 if (mood === 'waiting') setMood($, 'thinking')
211 return next(e)
212 })
213
214 // ==========================================================================
215 // บทที่ 5: วาดมาสคอตเหนือช่องพิมพ์
216 // ==========================================================================
217 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
218 if (!enabled || e.props.hasSurvey) return next(e)
219 const { Box, Text } = $.ui.resolve(e)
220 const m = findMascot(mascot) ?? MASCOTS.cat
221 const face = m.faces[mood][frame % 2 === 0 ? 0 : 1].split('\n')
222 const label = mood === 'tool' && toolName ? `${MOOD_TEXT.tool}: ${toolName}` : MOOD_TEXT[mood]
223 return (
224 <Box flexDirection="row" gap={1}>
225 <Box flexDirection="column">
226 {face.map(l => (
227 <Text color={m.color}>{l}</Text>
228 ))}
229 </Box>
230 <Text color={mood === 'error' ? '#e57373' : undefined} dimColor={mood === 'idle'}>
231 {label}
232 </Text>
233 </Box>
234 )
235 })
236
237 on('command.run', { command: 'kawaii' }, async ($, e) => {
238 const [sub = '', ...rest] = e.args.trim().split(/\s+/).filter(Boolean)
239 const m = findMascot(mascot) ?? MASCOTS.cat
240 switch (sub) {
241 case '':
242 return { text: `${m.faces[mood][0]} ${m.name} · ${MOOD_TEXT[mood]}${enabled ? '' : ' (ซ่อนอยู่)'}\n\n${HELP}` }
243 case 'pick': {
244 const key = rest[0] ?? ''
245 const picked = findMascot(key)
246 if (!picked) return { text: `ไม่มีตัว "${key}"\n${HELP}` }
247 mascot = key
248 enabled = true
249 await save($)
250 $.ui.invalidate('ui.render')
251 return { text: `${picked.faces.done[0]} สวัสดี! ฉันคือ${picked.name}` }
252 }
253 case 'preview':
254 return { text: [`🌸 ${m.name}`, ...(Object.keys(MOOD_TEXT) as Mood[]).map(k => `${MOOD_TEXT[k].padEnd(22)} ${m.faces[k][0].replace('\n', ' ')}`)].join('\n') }
255 case 'gallery': {
256 const cats = rest[0] ? [rest[0]] : Object.keys(KAOMOJI)
257 const out = cats.filter(c => KAOMOJI[c]).map(c => `【${c}】\n${(KAOMOJI[c] ?? []).map((k, i) => `${String(i + 1).padStart(2)}. ${k}`).join('\n')}`)
258 return { text: out.length ? `🌸 kaomoji ${cats.length === 1 ? '' : '50 แบบ'}\n\n${out.join('\n\n')}\n\nส่งลงช่องพิมพ์: /kawaii use ดีใจ 3` : `ไม่มีหมวด "${rest[0]}" (${Object.keys(KAOMOJI).join(' ')})` }
259 }
260 case 'use': {
261 const k = KAOMOJI[rest[0] ?? '']?.[Number(rest[1]) - 1]
262 if (!k) return { text: 'วิธีใช้: /kawaii use ดีใจ 3 (ดูเลขจาก /kawaii gallery)' }
263 await $.prompt.fill({ text: k })
264 return { text: `ใส่ ${k} ลงช่องพิมพ์แล้ว` }
265 }
266 case 'on':
267 case 'off':
268 enabled = sub === 'on'
269 await save($)
270 $.ui.invalidate('ui.render')
271 return { text: enabled ? `${m.faces.idle[0]} กลับมาแล้ว!` : 'ซ่อนมาสคอตแล้ว (/kawaii on เพื่อเรียกกลับ)' }
272 default:
273 return { text: HELP }
274 }
275 })
276}
277
278/**
279 * ========================================================================
280 * การบ้าน / ต่อยอด
281 * ========================================================================
282 * 1. ให้มาสคอต "หิว" ถ้าไม่ได้คุยกันนาน และ "อิ่ม" ทุกครั้งที่งานสำเร็จ แบบ Tamagotchi
283 * 2. ใช้หน้าตาต่างกันตามเครื่องมือ: Read = อ่านหนังสือ, Bash = พิมพ์คีย์บอร์ด
284 * 3. ออกแบบตัวละครของคุณเองแล้วเพิ่มลงใน MASCOTS
285 */
286