Shows your homedot agents in Claude Code's status line (running, resting, stopped), with the 5-hour limit and minutes until it resets, plus a /dots pane.

A Claude Code mod for homedot. It puts your agents and your 5-hour limit in the status line:
自分🟢 グループ🟢 0% 5h 32% あと206分
実行1 / 許可1 appear when a task is running or waiting for your approval, and a toast pops up for each new approval0% after the group agent is how much of its share of the 5-hour window it has used5h 32% あと206分 is this account's 5-hour window and the minutes until it resets/dots opens a pane with each agent's status, running tasks, approvals and recent activityIt reads http://127.0.0.1:8790/api/state (personal) and 8792 (group) every 15 seconds with curl.exe, so it is Windows only as written. Change the DOTS list at the top of hooks/register.tsx for other ports.
/plugin marketplace add tsurutanmen/homedot-panel
/plugin install homedot-panel@homedot-panel
Tested with Claude Code 2.1.286 to 2.1.288. Messages are in Japanese.
C:\Windows\System32\curl.exe every 15 seconds against http://127.0.0.1:8790/api/state and http://127.0.0.1:8792/api/state. Nothing leaves the PC.$.session.usage()).Five tools that work together, all MIT:
| homedot | A Dots-style personal agent on Claude Code, in a WSL2 VM on your own PC |
| session-bridge | Let open Claude Code sessions talk to each other, hold meetings, and answer your voice |
| claude-desk | "Hey Claude" voice listener with VOICEVOX replies, and a desktop wallpaper of your sessions |
| homedot-panel | homedot and the 5-hour limit in Claude Code's status line |
| session-dash | Usage limits above the prompt, and every open session in one pane |
More Claude Code plugins: tsurutanmen/claude-plugins
MIT
homedot の状態と5時間枠を、Claude Code の下の行に出す mod。🟢 動いている・🟡 休み・🔴 止まっている。5h 32% あと206分 は5時間枠と、戻るまでの分。/dots で詳しいパネルが開く。
hooks/register.tsx 144 lines1import type { Register } from 'claude-code'
2
3import type { DotSnap } from '../types'
4
5// 自分用とグループ用のドットの画面の口(このPCの中だけで開いている)。読むだけ
6const DOTS = [
7 { name: '自分用', port: 8790 },
8 { name: 'グループ用', port: 8792 },
9]
10const PANE = 'dots'
11const EVERY_MS = 15000
12
13const SNAPS = { plugin: 'homedot-panel', key: 'snaps' } as const
14const SEEN = { plugin: 'homedot-panel', key: 'seen' } as const
15
16type Task = { id: number; title: string; status: string }
17type Approval = { id: number; summary: string; status: string }
18type Activity = { task_id: number; text: string }
19type State = { status: string; tasks: Task[]; approvals: Approval[]; activity: Activity[] }
20type Fetch = (port: number) => Promise<string | null>
21
22// $ は渡さない($ の呼び出しは各フックの中に直接書く。そうしないとエンジンがその機能を渡さない)
23async function look(fetch: Fetch, dot: { name: string; port: number }): Promise<DotSnap> {
24 const empty: DotSnap = {
25 name: dot.name, up: false, status: [], running: [], pending: 0, approvals: [], recent: [], checkedAt: Date.now(),
26 }
27 try {
28 const out = await fetch(dot.port)
29 if (!out) return empty
30 const s = JSON.parse(out) as State
31 return {
32 name: dot.name,
33 up: true,
34 status: String(s.status || '').split('\n').filter(Boolean),
35 running: s.tasks.filter(t => t.status === 'running').map(t => `#${t.id} ${t.title}`),
36 pending: s.tasks.filter(t => t.status === 'pending').length,
37 approvals: s.approvals.filter(a => a.status === 'pending').map(a => ({ id: a.id, summary: a.summary })),
38 recent: s.activity.slice(0, 4).map(a => `#${a.task_id} ${a.text}`.replace(/\s+/g, ' ')),
39 checkedAt: Date.now(),
40 }
41 } catch {
42 return empty
43 }
44}
45
46const seenId = (d: string, id: number) => id * 10 + (d === '自分用' ? 1 : 2)
47
48type Limit = { kind: string; percentUsed: number; resetsAt?: string }
49
50// この Claude Code の5時間枠(最後の返事が教えてくれた値。まだ無ければ出さない)
51function fiveHour(limits: Limit[]) {
52 const w = limits.find(l => l.kind === 'five_hour')
53 if (!w) return ''
54 const at = w.resetsAt ? new Date(w.resetsAt).getTime() : NaN
55 const left = isNaN(at) ? '' : ` あと${Math.max(0, Math.ceil((at - Date.now()) / 60000))}分`
56 return ` 5h ${Math.round(w.percentUsed)}%${left}`
57}
58
59function summary(list: DotSnap[]) {
60 return list
61 .map(d => {
62 const name = d.name === '自分用' ? '自分' : 'グループ'
63 if (!d.up) return `${name}🔴`
64 const quota = d.status.find(l => l.startsWith('5時間枠'))
65 const q = quota ? ' ' + (quota.match(/(\d+)%\s*使用/)?.[1] ?? '?') + '%' : ''
66 const ask = d.approvals.length ? ` 許可${d.approvals.length}` : ''
67 const run = d.running.length ? ` 実行${d.running.length}` : ''
68 return `${name}${d.status[0]?.includes('休み') ? '🟡' : '🟢'}${run}${ask}${q}`
69 })
70 .join(' ')
71}
72
73export const register: Register = on => {
74 on('session.start', async ($, e, next) => {
75 await $.command.register({ name: 'dots', description: 'ドット(自分用・グループ用)の状態をパネルで見る' })
76
77 const tick = async () => {
78 const fetch: Fetch = async port => {
79 const r = await $.process.run(
80 ['C:\\Windows\\System32\\curl.exe', '-s', '--max-time', '8', `http://127.0.0.1:${port}/api/state`],
81 { timeoutMs: 12000 },
82 )
83 return r.exitCode === 0 && r.stdout ? r.stdout : null
84 }
85 const list: DotSnap[] = []
86 for (const dot of DOTS) list.push(await look(fetch, dot))
87 await $.state.set(SNAPS, list)
88
89 // 許可待ちが新しく出たら知らせる(同じものは1回だけ)
90 const { value: known = [] } = await $.state.get(SEEN)
91 const fresh = list.flatMap(d => d.approvals.map(a => ({ d: d.name, ...a }))).filter(a => !known.includes(seenId(a.d, a.id)))
92 for (const a of fresh) $.ui.toast(`ドット(${a.d})が許可を待っている:#${a.id} ${a.summary.split('\n')[0].slice(0, 60)}`)
93 if (fresh.length) await $.state.set(SEEN, [...known, ...fresh.map(a => seenId(a.d, a.id))].slice(-200))
94
95 const { rateLimits } = await $.session.usage()
96 $.ui.status(`${summary(list)}${fiveHour(rateLimits)}`)
97 }
98
99 void tick()
100 $.clock.every(EVERY_MS, () => void tick())
101 return next(e)
102 })
103
104 // 5時間枠が1ポイント動いたら、15秒を待たずに書き直す
105 on('session.measure', async ($, e, next) => {
106 if (e.changed.includes('rateLimits')) {
107 const { value: list = [] } = await $.state.get(SNAPS)
108 if (list.length) $.ui.status(`${summary(list)}${fiveHour(e.rateLimits)}`)
109 }
110 return next(e)
111 })
112
113 on('command.run', { command: 'dots' }, async $ => {
114 await $.ui.open({ id: PANE, title: 'ドット' })
115 return { text: 'ドットのパネルを開いた。15秒ごとに新しくなる。' }
116 })
117
118 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
119 const { Box, Text } = $.ui.resolve(e)
120 const { value: list = [] } = await $.state.get(SNAPS)
121 if (list.length === 0) return <Text dimColor>読み込み中…</Text>
122
123 return (
124 <Box flexDirection="column" gap={1}>
125 {list.map(d => (
126 <Box flexDirection="column">
127 <Text bold>{d.name}のドット</Text>
128 {!d.up && <Text color="red">止まっている(画面の口 に届かない)</Text>}
129 {d.up && d.status.map(line => <Text wrap="truncate-end">{line}</Text>)}
130 {d.up && d.running.map(r => <Text color="cyan" wrap="truncate-end">実行中 {r}</Text>)}
131 {d.up && d.pending > 0 && <Text dimColor>待ち {d.pending} 件</Text>}
132 {d.up && d.approvals.map(a => (
133 <Text color="yellow" wrap="truncate-end">許可待ち #{a.id} {a.summary.split('\n')[0]}</Text>
134 ))}
135 {d.up && d.recent.length > 0 && <Text dimColor>最近の活動</Text>}
136 {d.up && d.recent.map(r => <Text dimColor wrap="truncate-end">{r}</Text>)}
137 </Box>
138 ))}
139 <Text dimColor>15秒ごとに更新・{new Date(list[0].checkedAt).toLocaleTimeString('ja-JP')}</Text>
140 </Box>
141 )
142 })
143}
144types/index.d.ts 17 lines1export type DotSnap = {
2 name: string
3 up: boolean
4 status: string[]
5 running: string[]
6 pending: number
7 approvals: { id: number; summary: string }[]
8 recent: string[]
9 checkedAt: number
10}
11
12declare module 'claude-code' {
13 interface PluginState {
14 'homedot-panel': { snaps: DotSnap[]; seen: number[] }
15 }
16}
17