สมุดโน้ตงานที่รอดทุกการ compact: Claude จดเป้าหมาย การตัดสินใจ กับดัก งานค้างเองผ่าน tool เก็บเป็นไฟล์ แนบกลับหลัง compact และข้าม session

สมุดโน้ตงานที่รอดทุกการ compact: งานยาวๆ พอ compact ทีไร เป้าหมาย สิ่งที่ตกลงกันไว้ และกับดักที่เคยเจอมักหาย Mod นี้ให้ Claude มี tool note จดโน้ตเองระหว่างทำงาน (🎯 เป้าหมาย · ✅ ตัดสินใจแล้ว · ⚠️ กับดัก · ☐ ต้องทำ · 📍 ความคืบหน้า) ตอน compact จะสั่งตัวสรุปให้เก็บโน้ตไว้ครบ แล้วแนบโน้ตกลับไปกับ prompt ถัดไป เก็บเป็นไฟล์ .claude/workface.md เปิด session ใหม่ Claude ก็รู้งานเดิม
| คำสั่ง | ทำอะไร |
|---|---|
/note <ข้อความ> | จดเป็น "ต้องทำ" |
/note กับดัก: <ข้อความ> | กำหนดประเภท: เป้าหมาย / ตัดสินใจ / กับดัก / ต้องทำ / คืบหน้า |
/note done <id> | ทำเสร็จแล้ว |
/note rm <id> | ลบ |
/note clear | ล้างทั้งหมด |
/workface | เปิดแผงสมุดโน้ต |
/plugin install workface-th --marketplace tksoft11/claude-code-mods-th
ต้องตั้ง CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 ก่อนเปิด claude (ดู README หลัก)
เปิด hooks/register.tsx แล้วอ่านตามบทที่เขียนไว้ในคอมเมนต์:
$.tool.register และตอบด้วย tool.callinstructions ให้ตัวสรุป แล้วแนบโน้ตกลับด้วย context ของ prompt.submitทดสอบจริงแล้ว: หลัง /compact และใน session ใหม่ Claude ยังอ้างกับดักที่จดไว้ได้ตรงคำ แก้ไฟล์ .claude/workface.md ด้วยมือได้
hooks/register.tsx 217 lines1/**
2 * ============================================================================
3 * MOD 104: Workface TH - สมุดโน้ตงานที่รอดทุกการ compact (ฉบับสอนทำ)
4 * ============================================================================
5 * แรงบันดาลใจจาก: workface ใน awesome-claude-code-mods
6 *
7 * ปัญหา: งานยาวๆ พอ compact ทีไร รายละเอียดสำคัญหาย ทั้งเป้าหมาย สิ่งที่ตกลงกันไว้
8 * และกับดักที่เคยเจอ Claude เลยกลับไปทำผิดซ้ำ
9 *
10 * หน้าที่ของ Mod นี้:
11 * 1. ให้ Claude มี tool "note" จดโน้ตงานเองระหว่างทำงาน แบ่ง 5 ประเภท:
12 * 🎯 เป้าหมาย · ✅ ตัดสินใจแล้ว · ⚠️ กับดัก · ☐ ต้องทำ · 📍 ความคืบหน้า
13 * 2. ตอน compact: สั่งตัวสรุปให้เก็บโน้ตไว้ครบ แล้วแนบโน้ตกลับไปกับ prompt ถัดไป
14 * 3. เก็บเป็นไฟล์ .claude/workface.md ในโปรเจกต์ เปิด session ใหม่ Claude ก็รู้งานเดิม
15 * 4. เราจดเองได้ด้วย /note และเปิดแผงดูได้ด้วย /workface
16 */
17
18import type { EngineInterface, Register } from 'claude-code'
19
20const FILE = '.claude/workface.md'
21const PANE = 'workface-th'
22const TOOL = 'note'
23
24type Kind = 'goal' | 'decision' | 'gotcha' | 'todo' | 'progress'
25
26interface Note {
27 id: number
28 kind: Kind
29 text: string
30 done: boolean
31}
32
33const KINDS: Record<Kind, { icon: string; th: string; color: string }> = {
34 goal: { icon: '🎯', th: 'เป้าหมาย', color: '#4fc3f7' },
35 decision: { icon: '✅', th: 'ตัดสินใจแล้ว', color: '#81c784' },
36 gotcha: { icon: '⚠️', th: 'กับดัก', color: '#ffb74d' },
37 todo: { icon: '☐', th: 'ต้องทำ', color: '#e0e0e0' },
38 progress: { icon: '📍', th: 'ความคืบหน้า', color: '#ce93d8' },
39}
40const ORDER: Kind[] = ['goal', 'decision', 'gotcha', 'todo', 'progress']
41
42let notes: Note[] = []
43let root = ''
44// ต้องแนบโน้ตไปกับ prompt ถัดไปไหม (หลัง compact หรือเปิด session ใหม่)
45let reattach = false
46
47// ==========================================================================
48// บทที่ 1: อ่าน/เขียนไฟล์โน้ต (Markdown ที่คนอ่านรู้เรื่อง)
49// ==========================================================================
50function toMarkdown(): string {
51 const lines = ['# Workface notes', '', '<!-- สมุดโน้ตงานของ Claude (Mod 104) แก้ด้วยมือได้ -->', '']
52 for (const n of notes) lines.push(`- [${n.kind}]${n.done ? ' ✓' : ''} ${n.text} <!-- id:${n.id} -->`)
53 return `${lines.join('\n')}\n`
54}
55
56function fromMarkdown(md: string): Note[] {
57 const out: Note[] = []
58 for (const m of md.matchAll(/^- \[(goal|decision|gotcha|todo|progress)\]( ✓)? (.*?)(?: <!-- id:(\d+) -->)?$/gm)) {
59 out.push({ id: Number(m[4]) || out.length + 1, kind: m[1] as Kind, done: Boolean(m[2]), text: m[3]!.trim() })
60 }
61 return out
62}
63
64async function save($: EngineInterface): Promise<void> {
65 await $.fs.write(`${root}/${FILE}`, toMarkdown())
66 $.ui.invalidate('ui.render')
67}
68
69function brief(): string {
70 if (notes.length === 0) return ''
71 const lines = ['Workface notes (the working notebook for this task; keep using the note tool to update it):']
72 for (const k of ORDER) {
73 const group = notes.filter(n => n.kind === k)
74 if (group.length) lines.push(`${k}:`, ...group.map(n => ` #${n.id}${n.done ? ' [done]' : ''} ${n.text}`))
75 }
76 return lines.join('\n')
77}
78
79function add(kind: Kind, text: string): Note {
80 const n: Note = { id: Math.max(0, ...notes.map(x => x.id)) + 1, kind, text: text.replace(/\s+/g, ' ').trim(), done: false }
81 notes = [...notes, n]
82 return n
83}
84
85// ==========================================================================
86// บทที่ 2: tool ให้ Claude จดเอง
87// ==========================================================================
88const TOOL_SPEC = {
89 name: TOOL,
90 description:
91 'Workface: your working notebook for long tasks. It survives context compaction and new sessions, so record what you must not forget: the goal, decisions the user agreed to, gotchas you hit, todos, and progress. Keep notes short (one line). Mark todos done instead of deleting them.',
92 inputSchema: {
93 type: 'object',
94 properties: {
95 action: { type: 'string', enum: ['add', 'done', 'remove', 'list'] },
96 kind: { type: 'string', enum: ORDER, description: 'for add' },
97 text: { type: 'string', description: 'for add: one short line' },
98 id: { type: 'number', description: 'for done/remove' },
99 },
100 required: ['action'],
101 },
102 isDeferred: false as const,
103}
104
105async function runTool($: EngineInterface, input: Record<string, unknown>): Promise<string> {
106 const action = String(input.action ?? '')
107 if (action === 'list') return brief() || 'No notes yet.'
108 if (action === 'add') {
109 const kind = ORDER.includes(input.kind as Kind) ? (input.kind as Kind) : 'progress'
110 const text = String(input.text ?? '').trim()
111 if (!text) return 'text is required for add'
112 const n = add(kind, text)
113 await save($)
114 return `Saved note #${n.id} (${kind}).`
115 }
116 const id = Number(input.id)
117 const target = notes.find(n => n.id === id)
118 if (!target) return `No note #${input.id}. ${brief()}`
119 if (action === 'done') notes = notes.map(n => (n.id === id ? { ...n, done: true } : n))
120 else if (action === 'remove') notes = notes.filter(n => n.id !== id)
121 else return 'action must be add, done, remove or list'
122 await save($)
123 return `Note #${id} ${action === 'done' ? 'marked done' : 'removed'}.`
124}
125
126export const register: Register = on => {
127 on('session.start', async ($, e, next) => {
128 root = (await $.session.cwd()).replace(/[\\/]$/, '')
129 const path = `${root}/${FILE}`
130 notes = (await $.fs.exists(path)) ? fromMarkdown((await $.fs.read(path)) as string) : []
131 reattach = notes.length > 0
132 await $.tool.register(TOOL_SPEC)
133 await $.command.register({ name: 'note', description: 'จดโน้ตงานที่รอดทุกการ compact: /note กับดัก: ... (/note help)', argumentHint: '[ประเภท:] ข้อความ | done <id> | rm <id> | clear' })
134 await $.command.register({ name: 'workface', description: 'เปิดแผงสมุดโน้ตงาน (Workface)' })
135 if (notes.length) $.ui.toast(`📓 Workface: มีโน้ตงาน ${notes.length} ข้อจากรอบก่อน จะแนบให้ Claude กับ prompt แรก`, { timeoutMs: 8_000 })
136 return next(e)
137 })
138
139 on('tool.call', { tool: `mcp__workface-th__${TOOL}` }, async ($, e) => ({ result: await runTool($, e as unknown as Record<string, unknown>) }))
140
141 // ==========================================================================
142 // บทที่ 3: ตอน compact - บอกตัวสรุปให้เก็บโน้ต แล้วแนบกลับหลังสรุป
143 // ==========================================================================
144 on('session.compact', async ($, e, next) => {
145 if (e.agentId || notes.length === 0) return next(e)
146 const keep = `Keep these working notes verbatim in the summary:\n${brief()}`
147 const r = await next({ ...e, instructions: e.instructions ? `${e.instructions}\n\n${keep}` : keep })
148 if (r.skip === undefined) reattach = true
149 return r
150 })
151
152 on('prompt.submit', async ($, e, next) => {
153 if (!reattach || notes.length === 0) return next(e)
154 reattach = false
155 return next({ ...e, context: [...(e.context ?? []), brief()] })
156 })
157
158 // ==========================================================================
159 // บทที่ 4: แผงสมุดโน้ต
160 // ==========================================================================
161 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
162 const { Box, Text } = $.ui.resolve(e)
163 if (notes.length === 0) return <Text dimColor>ยังไม่มีโน้ต · Claude จะจดเองระหว่างทำงาน หรือจดเองด้วย /note</Text>
164 return (
165 <Box flexDirection="column">
166 {ORDER.filter(k => notes.some(n => n.kind === k)).map(k => (
167 <Box flexDirection="column" marginBottom={1}>
168 <Text color={KINDS[k].color}>{`${KINDS[k].icon} ${KINDS[k].th}`}</Text>
169 {notes
170 .filter(n => n.kind === k)
171 .map(n => (
172 <Text dimColor={n.done}>{` #${n.id} ${n.done ? '✓ ' : ''}${n.text}`}</Text>
173 ))}
174 </Box>
175 ))}
176 </Box>
177 )
178 })
179
180 on('command.run', { command: 'workface' }, async $ => {
181 await $.ui.open({ id: PANE, title: '📓 Workface' })
182 return { text: `📓 โน้ตงาน ${notes.length} ข้อ (${FILE})` }
183 })
184
185 on('command.run', { command: 'note' }, async ($, e) => {
186 const args = e.args.trim()
187 const [sub = '', arg] = args.split(/\s+/)
188 if (!args) return { text: brief() || '📓 ยังไม่มีโน้ต' }
189 if (sub === 'help') return { text: `📓 Workface TH (Mod 104)\n/note <ข้อความ> จดเป็น "ต้องทำ"\n/note กับดัก: <ข้อความ> ประเภท: เป้าหมาย / ตัดสินใจ / กับดัก / ต้องทำ / คืบหน้า\n/note done <id> ทำเสร็จแล้ว\n/note rm <id> ลบ\n/note clear ล้างทั้งหมด\n/workface เปิดแผง\nClaude จดเองได้ด้วย tool "note" · ไฟล์: ${FILE}` }
190 if (sub === 'clear') {
191 notes = []
192 await save($)
193 return { text: '🧹 ล้างโน้ตแล้ว' }
194 }
195 if ((sub === 'done' || sub === 'rm') && arg) {
196 const text = await runTool($, { action: sub === 'rm' ? 'remove' : 'done', id: Number(arg) })
197 return { text: `📓 ${text}` }
198 }
199 // "กับดัก: ..." หรือ "gotcha: ..." กำหนดประเภท
200 const typed = /^(เป้าหมาย|goal|ตัดสินใจ|decision|กับดัก|gotcha|ต้องทำ|todo|คืบหน้า|progress)\s*:\s*(.+)$/i.exec(args)
201 const map: Record<string, Kind> = { เป้าหมาย: 'goal', goal: 'goal', ตัดสินใจ: 'decision', decision: 'decision', กับดัก: 'gotcha', gotcha: 'gotcha', ต้องทำ: 'todo', todo: 'todo', คืบหน้า: 'progress', progress: 'progress' }
202 const kind = typed ? (map[typed[1]!.toLowerCase()] ?? 'todo') : 'todo'
203 const n = add(kind, typed ? typed[2]! : args)
204 await save($)
205 return { text: `📓 จด #${n.id} ${KINDS[kind].icon} ${KINDS[kind].th}: ${n.text}` }
206 })
207}
208
209/**
210 * ========================================================================
211 * การบ้าน / ต่อยอด
212 * ========================================================================
213 * 1. เตือน Claude ให้จดโน้ตเมื่อทำงานไปนานแล้วยังไม่จดเลย
214 * 2. ส่งโน้ต "ต้องทำ" ที่ยังไม่เสร็จเข้า Mod 82 Plan Progress
215 * 3. ให้ Haiku ยุบโน้ตที่ซ้ำกันเมื่อมีเกิน 30 ข้อ
216 */
217