SLOPSHOPPER

homedot-panel

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.

newpanecommandtoaststatusprocess
v0.1.7MITupdated 2026-10-03tsurutanmen/homedot-panel
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · homedot-panel
│ ┃ ドット ✕ › fix the failing auth test and add an audit log call │ ┃ 自分用のドット │ ┃ 止まっている(画面の口 に届かない) ⏺ Read(src/auth.ts) │ ┃ ⎿ Read 6 lines │ ┃ グループ用のドット ⏺ Update(src/auth.ts) │ ┃ 止まっている(画面の口 に届かない) ⎿ Added 2 lines, removed 1 line │ ┃ ⏺ Bash(bun test) │ ┃ 15秒ごとに更新・22:30:38 ⎿ 3 pass, 1 fail │ │ ● Done. refresh now rejects expired claims and logs an audit event. │ │ ✻ Worked for 42s · done 4:20 PM │ │ › /dots │ ⎿ homedot-panel: ドットのパネルを開いた。15秒ごとに新しくなる。 │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts ⚠ homedot-panel: 自分🔴 グループ🔴 5h 31% あと0分

Draws

Pane · ドット
自分用のドット 止まっている(画面の口 に届かない) グループ用のドット 止まっている(画面の口 に届かない) 15秒ごとに更新・22:30:38
README

homedot-panel

A Claude Code mod for homedot. It puts your agents and your 5-hour limit in the status line:

自分🟢 グループ🟢 0% 5h 32% あと206分
  • 🟢 running, 🟡 resting (a game is on, or you told it to rest), 🔴 not reachable
  • 実行1 / 許可1 appear when a task is running or waiting for your approval, and a toast pops up for each new approval
  • 0% after the group agent is how much of its share of the 5-hour window it has used
  • 5h 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 activity

It 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.

Install

/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.

What it does on your PC

  • Runs 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.
  • Reads this session's usage figures from Claude Code itself ($.session.usage()).
  • Writes nothing to disk. It keeps what it shows in the session's own plugin state.
  • Opens no port and submits no prompts.

Related

Five tools that work together, all MIT:

homedotA Dots-style personal agent on Claude Code, in a WSL2 VM on your own PC
session-bridgeLet 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-panelhomedot and the 5-hour limit in Claude Code's status line
session-dashUsage limits above the prompt, and every open session in one pane

More Claude Code plugins: tsurutanmen/claude-plugins

License

MIT


日本語

homedot の状態と5時間枠を、Claude Code の下の行に出す mod。🟢 動いている・🟡 休み・🔴 止まっている。5h 32% あと206分 は5時間枠と、戻るまでの分。/dots で詳しいパネルが開く。

Source 2 files
hooks/register.tsx 144 lines
1import 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}
144
types/index.d.ts 17 lines
1export 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