SLOPSHOPPER

workface-th

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

newpaneguardcommandtoastprompt
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · workface-th
│ ┃ 📓 Workface ✕ › fix the failing auth test and add an audit log call │ ┃ ยังไม่มีโน้ต · Claude จะจดเองระหว่างทำงาน │ ┃ หรือจดเองด้วย /note ⏺ 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 │ │ › /note │ ⎿ workface-th: 📓 ยังไม่มีโน้ต │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · 📓 Workface
ยังไม่มีโน้ต · Claude จะจดเองระหว่างทำงาน หรือจดเองด้วย /note
README

Mod 104: Workface TH

สมุดโน้ตงานที่รอดทุกการ 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 แล้วอ่านตามบทที่เขียนไว้ในคอมเมนต์:

  1. อ่าน/เขียนไฟล์โน้ตเป็น Markdown ที่คนอ่านรู้เรื่อง
  2. ลงทะเบียน tool ให้ Claude จดเองด้วย $.tool.register และตอบด้วย tool.call
  3. ตอน compact: เติม instructions ให้ตัวสรุป แล้วแนบโน้ตกลับด้วย context ของ prompt.submit
  4. แผงสมุดโน้ต

หมายเหตุ

ทดสอบจริงแล้ว: หลัง /compact และใน session ใหม่ Claude ยังอ้างกับดักที่จดไว้ได้ตรงคำ แก้ไฟล์ .claude/workface.md ด้วยมือได้

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