SLOPSHOPPER

session-peers

Always-on band above the prompt listing every other open Claude Code session: name, ref, kind, status and age.

newbandprocesstimer
A shopper browsing a rack in a slop shop
README

session-peers

Always-on band listing every other open Claude Code session.

Reads the session registry in ~/.claude/sessions/, drops entries whose process has exited, and shows one dim line per live session above the prompt, newest first, refreshed every 5 seconds:

robin-mods-50 (ref b79115, interactive, idle, started a minute ago)
demo-35 (ref 579855, interactive, busy, started 12 minutes ago) · this one

The band only appears when at least two sessions are open. ref is the first 6 characters of the session id; use the name with SendMessage to reach a session.

Install

/plugin install session-peers@mod-garage

Run /reload-plugins afterwards. Local try-out: claude --plugin-dir ./dev-mods/session-peers

Source 2 files
hooks/register.tsx 51 lines
1import type { Register } from 'claude-code'
2import { lines, parse, type Peer } from './lib'
3
4let rows: string[] = []
5
6async function refresh($: any) {
7  const home = (await $.process.run(['printenv', 'HOME']).catch(() => undefined))?.stdout.trim()
8  if (!home) return
9  const dir = `${home}/.claude/sessions`
10  const entries = await $.fs.list(dir).catch(() => [])
11  const peers: Peer[] = []
12  for (const f of entries) {
13    if (!f.name.endsWith('.json')) continue
14    const text = await $.fs.read(`${dir}/${f.name}`).catch(() => undefined)
15    const p = text && parse(text)
16    if (p) peers.push(p)
17  }
18  let live = peers
19  if (peers.length) {
20    const ps = await $.process.run(['ps', '-o', 'pid=', '-p', peers.map(p => p.pid).join(',')]).catch(() => undefined)
21    const alive = new Set((ps?.stdout ?? '').split(/\s+/).filter(Boolean).map(Number))
22    live = peers.filter(p => alive.has(p.pid))
23  }
24  const self = await $.session.id()
25  const next = live.length > 1 ? lines(live, Date.now(), self) : []
26  if (next.join('\n') !== rows.join('\n')) {
27    rows = next
28    $.ui.invalidate('ui.render')
29  }
30}
31
32export const register: Register = on => {
33  on('session.start', async ($, e, next) => {
34    await refresh($)
35    $.clock.every(5_000, () => refresh($))
36    return next(e)
37  })
38
39  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
40    const theirs = await next(e)
41    if (!rows.length || e.props.hasSurvey) return theirs
42    const { Box, Text } = $.ui.resolve(e)
43    return (
44      <Box flexDirection="column">
45        {theirs}
46        {rows.map(r => <Text dimColor>{r}</Text>)}
47      </Box>
48    )
49  })
50}
51
hooks/lib.ts 48 lines
1export type Peer = {
2  pid: number
3  sessionId: string
4  name: string
5  kind: string
6  status: string
7  startedAt: number
8}
9
10export function parse(text: string): Peer | undefined {
11  try {
12    const j = JSON.parse(text)
13    if (typeof j.pid !== 'number' || typeof j.sessionId !== 'string') return undefined
14    return {
15      pid: j.pid,
16      sessionId: j.sessionId,
17      name: typeof j.name === 'string' && j.name ? j.name : `pid-${j.pid}`,
18      kind: typeof j.kind === 'string' ? j.kind : 'unknown',
19      status: typeof j.status === 'string' ? j.status : 'unknown',
20      startedAt: typeof j.startedAt === 'number' ? j.startedAt : 0,
21    }
22  } catch {
23    return undefined
24  }
25}
26
27export function ago(ms: number): string {
28  const s = Math.max(0, Math.floor(ms / 1000))
29  if (s < 60) return 'just now'
30  const unit = (n: number, w: string) => `${n} ${w}${n === 1 ? '' : 's'} ago`
31  if (s < 3600) return unit(Math.floor(s / 60), 'minute')
32  if (s < 86400) return unit(Math.floor(s / 3600), 'hour')
33  return unit(Math.floor(s / 86400), 'day')
34}
35
36export function format(p: Peer, now: number, self?: string): string {
37  const mark = p.sessionId === self ? ' · this one' : ''
38  const age = p.startedAt ? `started ${ago(now - p.startedAt)}` : 'start unknown'
39  return `${p.name} (ref ${p.sessionId.replace(/-/g, '').slice(0, 6)}, ${p.kind}, ${p.status}, ${age})${mark}`
40}
41
42export function lines(peers: Peer[], now: number, self?: string, max = 6): string[] {
43  const sorted = [...peers].sort((a, b) => b.startedAt - a.startedAt)
44  const shown = sorted.slice(0, max).map(p => format(p, now, self))
45  if (sorted.length > max) shown.push(`+${sorted.length - max} more`)
46  return shown
47}
48