ช่วย Dev ที่คิดไม่ออก: ถามคำถามนำ แตกงานเป็นขั้นเล็กๆ ด้วย Claude อิงจาก git diff ล่าสุด และจดว่าติดตรงไหนบ่อย

ช่วย Dev ที่ตัน: บอกอาการ (ติดบั๊ก งง งานท่วม เหนื่อย เบื่อ) แล้ว Claude จะอ่าน git status และ diff ล่าสุดของโปรเจกต์ ถามคำถามนำไม่เกิน 2 ข้อ และเสนอขั้นต่อไปที่เล็กที่สุด สั่ง /unblock go ให้ Claude ลงมือทำขั้นนั้นได้ทันที มีคำถามแบบ rubber duck และบันทึกว่าตันตอนไหนบ่อย
| คำสั่ง | ทำอะไร |
|---|---|
/unblock [อาการ] [เล่าเพิ่ม] | อาการ: stuck confused overwhelm tired bored |
/unblock go | ให้ Claude ทำขั้นต่อไปที่ได้ |
/unblock duck | คำถามแบบ rubber duck |
/unblock duck <เล่าปัญหา> | คุยกับเป็ดยาง: ถามทีละข้อ ไม่เฉลย ให้คุณเจอคำตอบเอง |
/unblock duck bye | จบบทสนทนาและสรุปสิ่งที่ค้นพบ |
/unblock log [clear] | ดู / ล้างบันทึก |
/plugin install dev-unblock-th --marketplace tksoft11/claude-code-mods-th
ต้องตั้ง CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 ก่อนเปิด claude (ดู README หลัก)
เปิด hooks/register.tsx แล้วอ่านตามบทที่เขียนไว้ในคอมเมนต์:
ส่ง git diff ล่าสุด (ตัดเหลือประมาณ 6,000 ตัวอักษร) ให้ model ใช้ token ตามจริง
hooks/register.tsx 195 lines1/**
2 * ============================================================================
3 * MOD 37: Dev Unblock TH - ช่วย Dev ที่ตัน คิดไม่ออก (ฉบับสอนทำ)
4 * ============================================================================
5 * แรงบันดาลใจจาก: Rubber duck debugging และเทคนิค "ขั้นต่อไปที่เล็กที่สุด"
6 *
7 * หน้าที่ของ Mod นี้:
8 * 1. บอกอาการ (ติดบั๊ก / งง / งานเยอะจนท่วม / เหนื่อย / เบื่อ) แล้วได้คำแนะนำที่ตรงอาการ
9 * 2. Claude อ่าน git status/diff ล่าสุดของโปรเจกต์ แล้วเสนอ "ขั้นต่อไปที่เล็กที่สุด" + คำถามนำ
10 * 3. /unblock go ส่งขั้นต่อไปนั้นให้ Claude ลงมือทำต่อได้ทันที
11 * 4. Rubber duck: คำถามนำให้อธิบายปัญหาเอง หรือคุยกับเป็ดยาง (Haiku) ที่ถามทีละข้อแต่ไม่เฉลย
12 * 5. จดว่าตันตอนไหนบ่อย จะได้รู้รูปแบบของตัวเอง
13 */
14
15import type { EngineInterface, Register } from 'claude-code'
16
17type Mood = 'stuck' | 'confused' | 'overwhelm' | 'tired' | 'bored'
18
19const MOODS: Record<Mood, { th: string; icon: string; ask: string }> = {
20 stuck: { th: 'ติดบั๊ก/แก้ไม่ผ่าน', icon: '🧱', ask: 'The developer is stuck on a problem. Suggest how to narrow it down: what to check first, a quick experiment to split the problem in half, and the single smallest next step.' },
21 confused: { th: 'งง ไม่เข้าใจ', icon: '🌀', ask: 'The developer is confused by something. Explain the likely concept in simple Thai with one tiny example, then give the smallest next step to confirm understanding.' },
22 overwhelm: { th: 'งานเยอะจนท่วม', icon: '🌊', ask: 'The developer feels overwhelmed. Break the work into 3-6 small, ordered steps that each take under 30 minutes, and say which one to start right now.' },
23 tired: { th: 'เหนื่อย หมดแรง', icon: '🔋', ask: 'The developer is tired. Suggest a short reset (break, water, walk), then one very small, low-risk task to regain momentum, and what to write down so tomorrow starts easily.' },
24 bored: { th: 'เบื่อ ไม่มีไฟ', icon: '😐', ask: 'The developer is bored. Suggest a small, satisfying win from the current work (a quick cleanup, a test, a tiny feature) and a way to make the task more interesting.' },
25}
26
27// คำถามแบบ rubber duck: อธิบายให้เป็ดฟังทีละข้อ
28const DUCK = [
29 'โค้ดส่วนนี้ "ควร" ทำอะไร? อธิบายเป็นประโยคเดียว',
30 'ตอนนี้มัน "ทำ" อะไรจริงๆ? ต่างจากที่ควรเป็นตรงไหน',
31 'ครั้งสุดท้ายที่มันยังทำงานถูกคือเมื่อไหร่? หลังจากนั้นเปลี่ยนอะไรไปบ้าง',
32 'ข้อมูลที่เข้าฟังก์ชันเป็นค่าที่คิดไว้จริงไหม? ลอง print/log ดูหรือยัง',
33 'ทำให้ปัญหาเล็กลงครึ่งหนึ่งได้ไหม? ตัดส่วนไหนออกแล้วยังเกิดอยู่',
34 'ถ้าต้องอธิบายให้เพื่อนใหม่ฟัง จะเริ่มที่บรรทัดไหน',
35]
36
37let lastStep = ''
38let log: { at: number; mood: Mood }[] = []
39
40// ==========================================================================
41// บทที่ 1: เก็บบริบทจาก git (ไม่ส่งโค้ดทั้งไฟล์ ส่งแค่สรุปการเปลี่ยนแปลง)
42// ==========================================================================
43async function gitContext($: EngineInterface): Promise<string> {
44 const run = async (args: string[]) => {
45 const r = await $.process.run(['git', ...args], { timeoutMs: 10_000 }).catch(() => undefined)
46 return r && r.exitCode === 0 ? r.stdout.trim() : ''
47 }
48 const status = await run(['status', '--short'])
49 if (!status && !(await run(['rev-parse', '--git-dir']))) return '(not a git repository)'
50 const stat = await run(['diff', '--stat'])
51 const diff = (await run(['diff', '--unified=2'])).slice(0, 6000) // จำกัดขนาด ประหยัด token
52 const recent = await run(['log', '--oneline', '-5'])
53 return `git status:\n${status || '(clean)'}\n\nrecent commits:\n${recent}\n\ndiff stat:\n${stat}\n\ndiff (truncated):\n${diff}`
54}
55
56// ==========================================================================
57// บทที่ 2: ให้ Claude โค้ช - ตอบเป็นไทย สั้น และจบด้วยขั้นต่อไปบรรทัดเดียว
58// ==========================================================================
59async function coach($: EngineInterface, mood: Mood, note: string): Promise<{ text: string; next: string }> {
60 const system = `You are a calm senior developer coaching a Thai developer. Answer in Thai, short and kind, no lectures. ${MOODS[mood].ask} Ask at most 2 clarifying questions. End with exactly one line that starts with "NEXT:" followed by the next step written as an instruction to an AI coding assistant.`
61 const prompt = `What the developer says: ${note || '(nothing more)'}\n\nProject context:\n${await gitContext($)}`
62 for (const model of ['haiku', await $.session.model()]) {
63 const r = await $.model.complete({ model, system, prompt, maxTokens: 900 }).catch(() => undefined)
64 if (r?.isAnswered) {
65 const next = /^NEXT:\s*(.+)$/m.exec(r.text)?.[1]?.trim() ?? ''
66 return { text: r.text.replace(/^NEXT:.*$/m, '').trim(), next }
67 }
68 }
69 throw new Error('เรียก model ไม่สำเร็จ')
70}
71
72function patterns(): string {
73 if (log.length === 0) return 'ยังไม่มีบันทึก'
74 const byMood = new Map<Mood, number>()
75 const byHour = new Map<number, number>()
76 for (const l of log) {
77 byMood.set(l.mood, (byMood.get(l.mood) ?? 0) + 1)
78 const h = new Date(l.at).getHours()
79 byHour.set(h, (byHour.get(h) ?? 0) + 1)
80 }
81 const topHour = [...byHour.entries()].sort((a, b) => b[1] - a[1])[0]
82 return [
83 `📒 ตันไปแล้ว ${log.length} ครั้ง`,
84 ...[...byMood.entries()].sort((a, b) => b[1] - a[1]).map(([m, c]) => ` ${MOODS[m].icon} ${MOODS[m].th}: ${c}`),
85 topHour ? `⏰ ตันบ่อยสุดช่วง ${String(topHour[0]).padStart(2, '0')}:00 (ลองพักก่อนถึงช่วงนี้)` : '',
86 ].join('\n')
87}
88
89// ==========================================================================
90// บทที่ 2.5: เป็ดยางที่คุยได้ - ถามทีละคำถาม ห้ามเฉลย ให้คุณเจอคำตอบเอง
91// ==========================================================================
92let duckChat: { who: 'you' | 'duck'; text: string }[] = []
93
94const DUCK_SYSTEM = `คุณคือ "เป็ดยาง" สำหรับ rubber duck debugging พูดภาษาไทยเป็นกันเอง น่ารัก ขึ้นต้นด้วย 🦆
95กฎ: ถามคำถามนำทีละ 1 ข้อเท่านั้น สั้นไม่เกิน 2 ประโยค ห้ามเฉลยหรือเสนอวิธีแก้เอง
96ถามเพื่อให้ผู้ใช้อธิบายสิ่งที่คาดหวัง สิ่งที่เกิดจริง ข้อสันนิษฐานที่ยังไม่ได้พิสูจน์ และการทดลองเล็กๆ ที่แยกปัญหาออกเป็นครึ่ง
97ถ้าผู้ใช้ดูเหมือนเจอคำตอบแล้ว ให้ชมสั้นๆ แล้วถามว่าจะลองทำอะไรต่อ
98ใช้ภาษาไทยที่คนพูดกันจริงและถูกต้อง เรียกตัวเองว่า "เป็ด" เท่านั้น ห้ามแต่งคำแปลกหรือคำที่ไม่มีความหมาย`
99
100async function duckAsk($: EngineInterface, system: string, prompt: string): Promise<string | undefined> {
101 for (const model of ['haiku', await $.session.model()]) {
102 const r = await $.model.complete({ model, system, prompt, maxTokens: 300 }).catch(() => undefined)
103 if (r?.isAnswered) return r.text.trim()
104 }
105 return undefined
106}
107
108async function duckTalk($: EngineInterface, said: string): Promise<string> {
109 duckChat = [...duckChat, { who: 'you' as const, text: said.slice(0, 1500) }].slice(-20)
110 const transcript = duckChat.map(m => `${m.who === 'you' ? 'ผู้ใช้' : 'เป็ด'}: ${m.text}`).join('\n')
111 const reply = (await duckAsk($, DUCK_SYSTEM, transcript)) ?? '🦆 ก๊าบ? เล่าให้ฟังอีกนิดสิ ว่าคาดว่าจะเกิดอะไร แล้วเกิดอะไรจริง'
112 duckChat = [...duckChat, { who: 'duck' as const, text: reply }]
113 return `${reply}\n\n(ตอบ: /unblock duck <คำตอบ> · จบ: /unblock duck bye)`
114}
115
116async function duckBye($: EngineInterface): Promise<string> {
117 if (duckChat.length === 0) return '🦆 ก๊าบ ไว้คุยกันใหม่นะ'
118 const transcript = duckChat.map(m => `${m.who === 'you' ? 'ผู้ใช้' : 'เป็ด'}: ${m.text}`).join('\n')
119 duckChat = []
120 const summary = await duckAsk($, 'สรุปบทสนทนา rubber duck เป็นภาษาไทย 3 ข้อสั้นๆ: ปัญหาคืออะไร, ผู้ใช้ค้นพบอะไรระหว่างอธิบาย, ขั้นต่อไปที่ผู้ใช้บอกว่าจะทำ (ถ้ามี) ห้ามเพิ่มวิธีแก้ที่ผู้ใช้ไม่ได้พูด', transcript)
121 return `🦆 บ๊ายบาย! สรุปที่เราคุยกัน\n\n${summary ?? '(สรุปไม่สำเร็จ)'}`
122}
123
124const HELP = `🧭 Dev Unblock TH (Mod 37)
125========================================
126/unblock [อาการ] [เล่าเพิ่ม...]
127 อาการ: ${Object.entries(MOODS).map(([k, v]) => `${k}=${v.th}`).join(' | ')}
128 เช่น /unblock stuck เทสต์ login fail ตลอด ไม่รู้ทำไม
129/unblock go ให้ Claude ลงมือทำ "ขั้นต่อไป" ที่เพิ่งได้
130/unblock duck คำถามนำแบบ rubber duck
131/unblock duck <เล่า...> คุยกับเป็ดยางทีละคำถาม (เป็ดไม่บอกคำตอบ ช่วยให้คิดเอง) | duck bye จบและสรุป
132/unblock log [clear] ดูว่าตันตอนไหนบ่อย / ล้างบันทึก`
133
134export const register: Register = on => {
135 on('session.start', async ($, e, next) => {
136 const saved = await $.store.get('log')
137 log = Array.isArray(saved) ? (saved as typeof log) : []
138 await $.command.register({ name: 'unblock', description: 'ตัน คิดไม่ออก? ให้ช่วยหาขั้นต่อไปที่เล็กที่สุด (/unblock help)', argumentHint: '[stuck|confused|overwhelm|tired|bored] [เล่าเพิ่ม]' })
139 return next(e)
140 })
141
142 // ==========================================================================
143 // บทที่ 3: คำสั่ง /unblock
144 // ==========================================================================
145 on('command.run', { command: 'unblock' }, async ($, e) => {
146 const [first = '', ...rest] = e.args.trim().split(/\s+/).filter(Boolean)
147 switch (first) {
148 case 'help':
149 return { text: HELP }
150 case 'duck': {
151 const said = rest.join(' ')
152 if (!said && duckChat.length === 0) {
153 return { text: ['🦆 อธิบายให้เป็ดฟังทีละข้อ (พิมพ์ตอบในใจหรือในกระดาษก็ได้)', ...DUCK.map((q, i) => ` ${i + 1}. ${q}`), '', 'อยากคุยกับเป็ดจริงๆ: /unblock duck <เล่าปัญหา>', 'ยังไม่เจอ? /unblock stuck <เล่าสิ่งที่ตอบ>'].join('\n') }
154 }
155 if (said === 'bye' || said === 'stop') return { text: await duckBye($) }
156 return { text: await duckTalk($, said || 'ยังคิดไม่ออก') }
157 }
158 case 'log':
159 if (rest[0] === 'clear') {
160 log = []
161 await $.store.delete('log')
162 return { text: '🧹 ล้างบันทึกแล้ว' }
163 }
164 return { text: patterns() }
165 case 'go':
166 if (!lastStep) return { text: 'ยังไม่มีขั้นต่อไป ลอง /unblock stuck <เล่าปัญหา> ก่อน' }
167 $.clock.after(50, () => void $.prompt.submit({ text: lastStep }).catch(() => undefined)) // ส่งหลังคำสั่งตอบกลับแล้ว (ส่งระหว่าง command.run ไม่ได้)
168 return { text: `🚀 ส่งให้ Claude ทำแล้ว: ${lastStep}` }
169 default: {
170 const mood: Mood = first in MOODS ? (first as Mood) : 'stuck'
171 const note = (first in MOODS ? rest : [first, ...rest]).join(' ')
172 log = [...log, { at: await $.clock.now(), mood }].slice(-200)
173 await $.store.set('log', log)
174 if (mood === 'tired') $.ui.toast('🔋 ลุกไปเติมน้ำก่อนสัก 5 นาที แล้วค่อยกลับมาอ่านคำแนะนำ')
175 try {
176 const { text, next } = await coach($, mood, note)
177 lastStep = next
178 return { text: `${MOODS[mood].icon} ${MOODS[mood].th}\n----------------------------------------\n${text}${next ? `\n\n👉 ขั้นต่อไป: ${next}\n พิมพ์ /unblock go ให้ Claude ลงมือทำ` : ''}` }
179 } catch (err) {
180 return { text: `❌ ${err instanceof Error ? err.message : String(err)}\nลอง /unblock duck ระหว่างนี้` }
181 }
182 }
183 }
184 })
185}
186
187/**
188 * ========================================================================
189 * การบ้าน / ต่อยอด
190 * ========================================================================
191 * 1. ตรวจจับอัตโนมัติ: tool ล้มเหลวติดกันหลายครั้ง (ดู Mod 07) แล้วเสนอ /unblock
192 * 2. ใส่ error ล่าสุดจาก terminal ลงในบริบทด้วย
193 * 3. สรุปรายสัปดาห์ว่าตันเรื่องอะไรบ่อย แล้วแนะนำหัวข้อที่ควรศึกษาเพิ่ม
194 */
195