SLOPSHOPPER

agent-monitor-th

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

newpaneguardcommand
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · agent-monitor-th
│ ┃ 🤖 Agents ✕ › fix the failing auth test and add an audit log call │ ┃ 🤖 0 subagent · ทำงานอยู่ 0 │ ┃ ⏺ Read(src/auth.ts) │ ┃ 🧠 main งานหลัก ⎿ Read 6 lines │ ┃ 🔧9 🪙98,880 ⏱️0วิ · Bash ⏺ 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 │ │ › /agentmon │ ⎿ agent-monitor-th: 🤖 Agent Monitor │ ⎿ agent-monitor-th: ---------------------------------------- │ ⎿ agent-monitor-th: 🧠 main งานหลัก │ ⎿ agent-monitor-th: 🔧9 ❌1 🪙98,880 ⏱️0วิ · Bash │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · 🤖 Agents
🤖 0 subagent · ทำงานอยู่ 0 🧠 main งานหลัก 🔧9 🪙98,880 ⏱️0วิ · Bash
README

Mod 23: Agent Monitor TH

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

  1. เก็บสถิติแยกตาม agent ด้วย e.agentId ที่มากับทุก event
  2. รวมสถานะจริงจาก $.agent.list() กับสถิติที่เก็บเอง และจำ agent ที่จบไปแล้ว
  3. ฟัง turn.start, tool.call และ turn.complete ของทุก agent
  4. แผง Task Manager
  5. หยุด agent ด้วย $.turn.abort

หมายเหตุ

ใช้ชื่อ /agentmon เพราะ /agents เป็นคำสั่งเดิมของ Claude Code สถิติเริ่มนับตั้งแต่ Mod โหลด

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