SLOPSHOPPER

bead-band

Shows the in-progress Beads task, its src/<branch> merge target and the current worktree/branch at the top of the flightdeck pane

newpaneprocess
v0.2.0no licenseupdated 2026-10-06Ascurse/bead-band
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · bead-band
│ ┃ flightdeck ✕ › fix the failing auth test and add an audit log call │ ┃ нет задачи в работе app · feat/auth-refresh │ ┃ ⟨Claude Code's own drawing⟩ ⏺ Read(src/auth.ts) │ ⎿ Read 6 lines │ ⏺ 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 │ │ │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · flightdeck
нет задачи в работе app · feat/auth-refresh ⟨Claude Code's own drawing⟩
Source 2 files
hooks/register.tsx 133 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Bead, BeadBandInfo } from '../types'
5
6const info = atom({ plugin: 'bead-band', key: 'info' } as const, null)
7
8// Хост может не передать PATH из zsh — тогда bd ищется по пути Homebrew
9const BD_CANDIDATES = ['bd', '/opt/homebrew/bin/bd']
10const SOURCE_LABEL_PREFIX = 'src/'
11const WORKTREE_BRANCH_PREFIX = 'bd-'
12const TITLE_CHARS = 48
13const RUN_TIMEOUT_MS = 10_000
14
15type RawBead = { id?: unknown; title?: unknown; labels?: unknown }
16
17const clip = (s: string, max: number) => (s.length > max ? s.slice(0, max - 1) + '…' : s)
18
19async function runText($: EngineInterface, argv: string[]): Promise<string | null> {
20  try {
21    const r = await $.process.run(argv, { timeoutMs: RUN_TIMEOUT_MS })
22    return r.exitCode === 0 ? r.stdout.trim() : null
23  } catch {
24    return null
25  }
26}
27
28async function runBd($: EngineInterface, args: string[]): Promise<string | null> {
29  for (const bd of BD_CANDIDATES) {
30    const out = await runText($, [bd, ...args])
31    if (out !== null) return out
32  }
33  return null
34}
35
36function toBead(raw: RawBead): Bead | null {
37  if (typeof raw.id !== 'string' || typeof raw.title !== 'string') return null
38  const labels = Array.isArray(raw.labels) ? raw.labels.filter((l): l is string => typeof l === 'string') : []
39  const source = labels.find(l => l.startsWith(SOURCE_LABEL_PREFIX))
40  return { id: raw.id, title: raw.title, source: source ? source.slice(SOURCE_LABEL_PREFIX.length) : null }
41}
42
43function parseBeads(json: string | null): Bead[] {
44  if (!json) return []
45  try {
46    const parsed: unknown = JSON.parse(json)
47    const list = Array.isArray(parsed) ? parsed : [parsed]
48    return list.map(x => toBead((x ?? {}) as RawBead)).filter((b): b is Bead => b !== null)
49  } catch {
50    return []
51  }
52}
53
54async function collect($: EngineInterface): Promise<BeadBandInfo> {
55  const [toplevel, branch, inProgressJson] = await Promise.all([
56    runText($, ['git', 'rev-parse', '--show-toplevel']),
57    runText($, ['git', 'rev-parse', '--abbrev-ref', 'HEAD']),
58    runBd($, ['list', '--status', 'in_progress', '--json', '--brief', '--readonly']),
59  ])
60  let beads = parseBeads(inProgressJson)
61
62  // В воркчтри bd-<ID> его бид — главный, даже если он ещё не in_progress
63  if (branch?.startsWith(WORKTREE_BRANCH_PREFIX)) {
64    const ownId = branch.slice(WORKTREE_BRANCH_PREFIX.length)
65    const own = beads.find(b => b.id === ownId) ?? parseBeads(await runBd($, ['show', ownId, '--json', '--readonly']))[0]
66    if (own) beads = [own, ...beads.filter(b => b.id !== own.id)]
67  }
68
69  return { worktree: toplevel ? toplevel.split('/').pop() ?? null : null, branch, beads }
70}
71
72async function refresh($: EngineInterface) {
73  const next = await collect($)
74  await update($, info, () => next)
75}
76
77const FLIGHTDECK_PANE = 'flightdeck'
78
79export const register: Register = on => {
80  on('session.start', async ($, e, next) => {
81    const result = await next(e)
82    void refresh($)
83    return result
84  })
85
86  // Бид или ветка могли смениться за ход — перечитываем в фоне
87  on('turn.complete', async ($, e, next) => {
88    const result = await next(e)
89    void refresh($)
90    return result
91  })
92
93  // Строка задачи — первой строкой панели flightdeck: над полем ввода она теснила подсказки next-steps
94  on('ui.render', { component: 'Pane', requestId: FLIGHTDECK_PANE }, async ($, e, next) => {
95    const inner = await next(e)
96    const data = await read($, info)
97    if (!data || (!data.branch && data.beads.length === 0)) return inner
98
99    const { Box, Text } = $.ui.resolve(e)
100    const [main, ...rest] = data.beads
101    const where = [data.worktree, data.branch].filter(Boolean).join(' · ')
102
103    return (
104      <Box flexDirection="column" alignItems="stretch" flexGrow={1}>
105        <Box flexDirection="row" gap={1} minWidth={0}>
106          {main ? (
107            <>
108              <Text color="claude">●</Text>
109              <Text bold>{main.id}</Text>
110              <Text wrap="truncate-end">{clip(main.title, TITLE_CHARS)}</Text>
111            </>
112          ) : (
113            <Text dimColor>нет задачи в работе</Text>
114          )}
115          {main && rest.length > 0 ? <Text dimColor>+{rest.length}</Text> : null}
116          {where ? <Text dimColor>{where}</Text> : null}
117          {main ? (
118            main.source ? (
119              <Text dimColor>→ {main.source}</Text>
120            ) : (
121              <>
122                <Text dimColor>→</Text>
123                <Text color="warning">нет src/</Text>
124              </>
125            )
126          ) : null}
127        </Box>
128        <Box>{inner}</Box>
129      </Box>
130    )
131  })
132}
133
types/index.d.ts 15 lines
1export type Bead = { id: string; title: string; source: string | null }
2
3export type BeadBandInfo = {
4  /** Имя рабочего дерева (папка корня репозитория) */
5  worktree: string | null
6  branch: string | null
7  beads: Bead[]
8}
9
10declare module 'claude-code' {
11  interface PluginState {
12    'bead-band': { info: BeadBandInfo | null }
13  }
14}
15