Read-only live pane: services, launchd jobs, tmux. Open with /fleet

hooks/register.tsx 63 lines1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { Fleet } from '../types'
5
6const PANE = 'fleet'
7const fleet = atom({ plugin: 'fleet-pane', key: 'fleet' } as const, null)
8const SCRIPT = '/Users/dillonmohr/code/claude-skills-repo/mods/fleet-pane/fleet.py'
9// Claude Code's ultracode rainbow, red to violet
10const RB = ['#EB5F57', '#F58B57', '#FAC35F', '#91C882', '#82AADC', '#9B82C8', '#C882B4']
11const dotColor = (s: string) =>
12 s === 'up' || s === 'running' ? '#91C882' : s === 'down' || s === 'failed' ? '#EB5F57' : '#9AA0A8'
13
14export const register: Register = on => {
15 on('session.start', async ($, e, next) => {
16 await $.command.register({ name: 'fleet', description: 'Live pane: services, launchd jobs, tmux' })
17 // read-only snapshot every 15s; fleet.py caches and uses 1s timeouts, never blocks the prompt
18 $.clock.every(15000, async () => {
19 const r = await $.process.run(['python3', SCRIPT, '--json'])
20 if (r.exitCode === 0) await update($, fleet, () => JSON.parse(r.stdout) as Fleet)
21 })
22 return next(e)
23 })
24
25 on('command.run', { command: 'fleet' }, async $ => {
26 const r = await $.process.run(['python3', SCRIPT, '--json'])
27 if (r.exitCode === 0) await update($, fleet, () => JSON.parse(r.stdout) as Fleet)
28 await $.ui.open({ id: PANE, title: 'Fleet' })
29 return { text: 'Fleet pane opened.' }
30 })
31
32 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
33 const { Box, Text } = $.ui.resolve(e)
34 const f = await read($, fleet)
35 if (!f) return <Text dimColor>Loading...</Text>
36 const room = Math.max(4, (e.viewport?.rows ?? 24) - 8)
37
38 return (
39 <Box flexDirection="column">
40 <Text bold color={RB[0]}>SERVICES</Text>
41 {f.services.map(s => (
42 <Text>
43 <Text color={dotColor(s.state)}>●</Text> <Text color={RB[1]}>{s.port}</Text> {s.name}
44 </Text>
45 ))}
46 <Text bold color={RB[2]}>LAUNCHD</Text>
47 {f.jobs.slice(0, room).map(j => (
48 <Text>
49 <Text color={dotColor(j.state)}>●</Text> <Text color={RB[3]}>{j.name}</Text>{' '}
50 <Text dimColor>{j.state}</Text>
51 </Text>
52 ))}
53 <Text bold color={RB[4]}>TMUX</Text>
54 {f.tmux.map((t, i) => (
55 <Text>
56 <Text color={RB[5]}>●</Text> {t}
57 </Text>
58 ))}
59 </Box>
60 )
61 })
62}
63types/index.d.ts 12 lines1export type Fleet = {
2 jobs: { name: string; state: string }[]
3 tmux: string[]
4 services: { port: number; name: string; state: string }[]
5}
6
7declare module 'claude-code' {
8 interface PluginState {
9 'fleet-pane': { fleet: Fleet | null }
10 }
11}
12