Task Manager ของ subagent: ดูสถานะ จำนวน tool และ token ของแต่ละ agent สด ๆ ในแผง และหยุด agent ที่ทำงานนานเกินได้

Task Manager ของ subagent เวลา Claude แตกงานให้ agent หลายตัวพร้อมกัน แผงด้านข้างจะแสดงทุกตัว (รวมงานหลัก) แบบสดๆ ว่ากำลังทำอะไร ใช้ tool ไปกี่ครั้ง ล้มเหลวกี่ครั้ง กิน token เท่าไหร่ และใช้เวลานานแค่ไหน ดู log ของแต่ละตัวและหยุดตัวที่ทำงานนานเกินได้
| คำสั่ง | ทำอะไร |
|---|---|
/agentmon | รายการ agent + เปิดแผง |
/agentmon log <id> | tool ที่ agent นั้นเรียกล่าสุด (ใช้ id 8 ตัวแรกได้) |
/agentmon stop <id> | หยุดงานรอบปัจจุบันของ agent |
/agentmon stats | สรุปรวมทั้ง session |
/agentmon clear | ล้างสถิติ |
/plugin install agent-monitor-th --marketplace tksoft11/claude-code-mods-th
ต้องตั้ง CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 ก่อนเปิด claude (ดู README หลัก)
เปิด hooks/register.tsx แล้วอ่านตามบทที่เขียนไว้ในคอมเมนต์:
ใช้ชื่อ /agentmon เพราะ /agents เป็นคำสั่งเดิมของ Claude Code สถิติเริ่มนับตั้งแต่ Mod โหลด
hooks/register.tsx 228 lines1/**
2 * ============================================================================
3 * MOD 23: Agent Monitor TH - Task Manager ของ subagent (ฉบับสอนทำ)
4 * ============================================================================
5 * แรงบันดาลใจจาก: Task Manager ของ Windows และหน้าจอสถานะแบบ J.A.R.V.I.S.
6 *
7 * เวลา Claude แตกงานให้ subagent (เช่น Explore, general-purpose) หลายตัวพร้อมกัน
8 * เราไม่รู้ว่าตัวไหนทำอะไร ใช้ tool ไปกี่ครั้ง กิน token เท่าไหร่ Mod นี้แสดงให้ดูสดๆ
9 *
10 * หน้าที่ของ Mod นี้:
11 * 1. แผงแสดง agent ทุกตัว (รวมงานหลัก): สถานะ, เวลาที่ใช้, จำนวน tool, tool ล่าสุด, token
12 * 2. /agentmon log <id> ดู tool ที่ agent นั้นเรียกล่าสุด
13 * 3. /agentmon stop <id> หยุดงานรอบปัจจุบันของ agent ที่ทำนานเกิน
14 * 4. /agentmon stats สรุปรวมทั้ง session
15 *
16 * หมายเหตุ: ใช้ชื่อ /agentmon เพราะ /agents เป็นคำสั่งเดิมของ Claude Code
17 */
18
19import type { AgentInfo, AgentStatus, EngineInterface, Register } from 'claude-code'
20
21interface Track {
22 id: string // 'main' = งานหลัก
23 firstSeen: number
24 tools: number
25 errors: number
26 lastTool: string
27 log: { at: number; tool: string; ok: boolean }[]
28 tokens: number
29 turnId?: string // turn ที่กำลังทำ (ใช้หยุดงาน)
30 label?: string // ชื่อ/งานของ agent (จำไว้ เพราะ agent ที่จบแล้วจะหายจาก $.agent.list())
31}
32
33const PANE = 'agent-monitor-th'
34const STATUS_ICON: Record<AgentStatus | 'main', string> = { pending: '⏳', running: '🟢', waiting: '🟡', idle: '⚪', completed: '✅', failed: '❌', killed: '⛔', main: '🧠' }
35let tracks = new Map<string, Track>()
36// คำอธิบายงานจากตอนที่งานหลักเรียก tool Agent รอผูกกับ subagent ตัวใหม่ที่โผล่มา
37let pendingLabels: string[] = []
38
39// ==========================================================================
40// บทที่ 1: เก็บสถิติแยกตาม agent - event มี agentId บอกว่ามาจาก agent ไหน
41// ==========================================================================
42async function track($: EngineInterface, agentId: string | undefined): Promise<Track> {
43 const id = agentId ?? 'main'
44 let t = tracks.get(id)
45 if (!t) {
46 t = { id, firstSeen: await $.clock.now(), tools: 0, errors: 0, lastTool: '-', log: [], tokens: 0, label: id === 'main' ? undefined : pendingLabels.shift() }
47 tracks.set(id, t)
48 }
49 return t
50}
51
52function elapsed(ms: number): string {
53 const s = Math.floor(ms / 1000)
54 return s < 60 ? `${s}วิ` : `${Math.floor(s / 60)}น ${s % 60}วิ`
55}
56
57// ==========================================================================
58// บทที่ 2: รวมข้อมูลจาก $.agent.list() (สถานะจริงจาก engine) กับสถิติที่เราเก็บ
59// ==========================================================================
60interface Row {
61 id: string
62 icon: string
63 label: string
64 track?: Track
65 status: AgentStatus | 'main'
66}
67
68async function rows($: EngineInterface): Promise<Row[]> {
69 const agents: AgentInfo[] = await $.agent.list().catch(() => [])
70 const main = tracks.get('main')
71 const result: Row[] = [{ id: 'main', icon: STATUS_ICON.main, label: 'งานหลัก', track: main, status: 'main' }]
72 for (const a of agents) {
73 const label = `${a.name ?? a.type}: ${a.description}`.slice(0, 60)
74 const t = tracks.get(a.id)
75 if (t) t.label = label
76 result.push({ id: a.id, icon: STATUS_ICON[a.status], label, track: t, status: a.status })
77 }
78 // agent ที่จบแล้วไม่อยู่ใน list แต่เรายังมีสถิติ -> แสดงเป็น "จบแล้ว"
79 for (const t of tracks.values()) {
80 if (t.id !== 'main' && !agents.some(a => a.id === t.id)) result.push({ id: t.id, icon: STATUS_ICON.completed, label: `${t.label ?? 'subagent'} (จบแล้ว)`, track: t, status: 'completed' })
81 }
82 return result
83}
84
85function rowText(r: Row, now: number): string {
86 const t = r.track
87 const stats = t ? `🔧${t.tools}${t.errors ? ` ❌${t.errors}` : ''} 🪙${t.tokens.toLocaleString('en-US')} ⏱️${elapsed(now - t.firstSeen)} · ${t.lastTool}` : 'ยังไม่มีข้อมูล'
88 return `${r.icon} ${r.id === 'main' ? 'main' : r.id.slice(0, 8)} ${r.label}\n ${stats}`
89}
90
91const HELP = `🤖 Agent Monitor TH (Mod 23)
92========================================
93/agentmon รายการ agent + เปิดแผง
94/agentmon log <id> tool ที่ agent นั้นเรียกล่าสุด (ใช้ id 8 ตัวแรกได้)
95/agentmon stop <id> หยุดงานรอบปัจจุบันของ agent
96/agentmon stats สรุปรวม
97/agentmon clear ล้างสถิติ`
98
99function findTrack(prefix: string | undefined): Track | undefined {
100 if (!prefix) return undefined
101 return tracks.get(prefix) ?? [...tracks.values()].find(t => t.id.startsWith(prefix))
102}
103
104export const register: Register = on => {
105 on('session.start', async ($, e, next) => {
106 tracks = new Map()
107 pendingLabels = []
108 await $.command.register({ name: 'agentmon', description: 'Task Manager ของ subagent: สถานะ tool และ token (/agentmon help)', argumentHint: 'log|stop|stats' })
109 return next(e)
110 })
111
112 // ==========================================================================
113 // บทที่ 3: ฟัง event ของทุก agent
114 // ==========================================================================
115 on('turn.start', async ($, e, next) => {
116 const t = await track($, e.agentId)
117 t.turnId = e.turnId
118 if (e.agentId && !t.label) {
119 const info = (await $.agent.list().catch(() => [])).find(a => a.id === e.agentId)
120 if (info) t.label = `${info.name ?? info.type}: ${info.description}`.slice(0, 60)
121 }
122 $.ui.invalidate('ui.render')
123 return next(e)
124 })
125
126 on('tool.call', async ($, e, next) => {
127 if (e.tool === 'Agent') pendingLabels.push(`${e.subagent_type ?? 'agent'}: ${e.description}`.slice(0, 60))
128 const t = await track($, e.agentId)
129 t.lastTool = `${e.tool}…`
130 $.ui.invalidate('ui.render')
131 const ran = await next(e)
132 const ok = ran.deny === undefined && !ran.isError
133 t.tools++
134 if (!ok) t.errors++
135 t.lastTool = e.tool
136 t.log = [...t.log, { at: await $.clock.now(), tool: e.tool, ok }].slice(-30)
137 $.ui.invalidate('ui.render')
138 return ran
139 })
140
141 on('turn.complete', async ($, e, next) => {
142 const t = await track($, e.agentId)
143 const u = e.usage
144 if (u) t.tokens += u.input_tokens + u.output_tokens + u.cache_read_input_tokens + u.cache_creation_input_tokens
145 t.turnId = undefined
146 $.ui.invalidate('ui.render')
147 return next(e)
148 })
149
150 // ==========================================================================
151 // บทที่ 4: แผง Task Manager
152 // ==========================================================================
153 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
154 const { Box, Text } = $.ui.resolve(e)
155 const now = await $.clock.now()
156 const list = await rows($)
157 const running = list.filter(r => r.status === 'running' || r.status === 'pending').length
158 return (
159 <Box flexDirection="column">
160 <Text bold>
161 🤖 {list.length - 1} subagent · ทำงานอยู่ {running}
162 </Text>
163 {list.map(r => (
164 <Box flexDirection="column" marginTop={1}>
165 <Text wrap="truncate-end">
166 {r.icon} <Text bold>{r.id === 'main' ? 'main' : r.id.slice(0, 8)}</Text> {r.label}
167 </Text>
168 <Text dimColor wrap="truncate-end">
169 {r.track ? `🔧${r.track.tools} 🪙${r.track.tokens.toLocaleString('en-US')} ⏱️${elapsed(now - r.track.firstSeen)} · ${r.track.lastTool}` : 'รอข้อมูล'}
170 </Text>
171 </Box>
172 ))}
173 </Box>
174 )
175 })
176
177 // ==========================================================================
178 // บทที่ 5: คำสั่ง /agentmon
179 // ==========================================================================
180 on('command.run', { command: 'agentmon' }, async ($, e) => {
181 const [sub = '', arg] = e.args.trim().split(/\s+/).filter(Boolean)
182 const now = await $.clock.now()
183
184 switch (sub) {
185 case 'help':
186 return { text: HELP }
187 case 'log': {
188 const t = findTrack(arg ?? 'main')
189 if (!t) return { text: `❌ ไม่พบ agent ${arg ?? ''}` }
190 return { text: [`📜 ${t.id}: ${t.tools} tools`, ...t.log.slice(-15).map(l => ` ${l.ok ? '✅' : '❌'} ${new Date(l.at).toLocaleTimeString('th-TH')} ${l.tool}`)].join('\n') }
191 }
192 case 'stop': {
193 const t = findTrack(arg)
194 if (!t) return { text: `❌ ไม่พบ agent ${arg ?? ''} (ดู id ด้วย /agentmon)` }
195 if (!t.turnId) return { text: `ℹ️ ${t.id} ไม่ได้ทำงานอยู่` }
196 await $.turn.abort({ turnId: t.turnId })
197 return { text: `⛔ หยุดงานรอบปัจจุบันของ ${t.id} แล้ว` }
198 }
199 case 'stats': {
200 const all = [...tracks.values()]
201 const tools = all.reduce((s, t) => s + t.tools, 0)
202 const errors = all.reduce((s, t) => s + t.errors, 0)
203 const tokens = all.reduce((s, t) => s + t.tokens, 0)
204 const busiest = [...all].sort((a, b) => b.tokens - a.tokens)[0]
205 return { text: `📊 agent ${all.length} ตัว (รวมงานหลัก) | tool ${tools} ครั้ง (ล้มเหลว ${errors}) | 🪙 ${tokens.toLocaleString('en-US')} tokens${busiest ? `\nกิน token มากสุด: ${busiest.id} (${busiest.tokens.toLocaleString('en-US')})` : ''}` }
206 }
207 case 'clear':
208 tracks = new Map()
209 $.ui.invalidate('ui.render')
210 return { text: '🧹 ล้างสถิติแล้ว' }
211 default: {
212 await $.ui.open({ id: PANE, title: '🤖 Agents' })
213 const list = await rows($)
214 return { text: ['🤖 Agent Monitor', '----------------------------------------', ...list.map(r => rowText(r, now))].join('\n') }
215 }
216 }
217 })
218}
219
220/**
221 * ========================================================================
222 * การบ้าน / ต่อยอด
223 * ========================================================================
224 * 1. หยุด agent อัตโนมัติเมื่อกิน token เกินงบที่ตั้งไว้ (เช็คใน turn.complete แล้ว $.turn.abort)
225 * 2. คิดเป็นเงินบาทแยกตาม agent (ใช้สูตรจาก Mod 05)
226 * 3. ส่งแจ้งเตือนเข้ามือถือเมื่อ agent ล้มเหลว (ต่อกับ Mod 22)
227 */
228