SLOPSHOPPER

quenching-hud

Band above the prompt: the worktree's active spec, its stage and its task progress, read through cq specs status --no-fetch. Writes no spec or branch and never…

newbandprocess
★ 1v6.4.0MITupdated 2026-10-16holetz/quenching/plugins/quenching-hud
A shopper browsing a rack in a slop shop
README

quenching-hud

Faixa acima do prompt do Claude Code: a spec ativa da worktree, o estágio dela e o progresso das tasks, sem gastar uma tool call por consulta.

spec 1521 · approved · 3/7 · claude-quenching-1521-plugin-quenching-hud-minimo

O que aparece

A faixa aparece quando o branch atual é plan/<id>-<handle>; em qualquer outro branch ela some. Ela se atualiza em session.start e a cada turn.complete.

O que executa

Só dois comandos, por argv fixo e sem shell, e o id vem de dígitos do nome do branch:

  • git branch --show-current
  • cq specs status --spec <id> --no-fetch --json

O plugin não escreve em spec nem em branch e não registra comando, botão nem hook de ferramenta. --no-fetch faz o cq specs status ler só o ref local refs/remotes/origin/quenching que já existe: sem fetch, sem rede, sem ssh, sem escrita de ref e sem cache. A faixa mostra o estado da última vez que outro comando cq buscou origin, então pode estar defasada. Sem esse ref, ou num backend que não é o git (github, azure), o comando recusa e a faixa some, sem chamar a API do GitHub.

Pré-requisito

cq no PATH, o que o plugin quenching garante quando está ativo. Sem cq a faixa some, sem erro.

Instalação

/plugin install quenching-hud --marketplace holetz/claude-quenching

Verificação

claude plugin validate plugins/quenching-hud
claude plugin test plugins/quenching-hud
Source 3 files
hooks/register.tsx 58 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { HudLine } from '../types'
5import { basename, formatLine, lineFromStatus, specIdFromBranch } from './hud'
6
7const line = atom({ plugin: 'quenching-hud', key: 'line' } as const, null as HudLine | null)
8
9async function refresh($: EngineInterface) {
10  try {
11    const cwd = await $.session.cwd()
12    const branch = await $.process.run(['git', 'branch', '--show-current'], { cwd })
13    const id = branch.exitCode === 0 ? specIdFromBranch(branch.stdout) : null
14
15    if (id === null) {
16      await update($, line, () => null)
17
18      return
19    }
20
21    const status = await $.process.run(['cq', 'specs', 'status', '--spec', String(id), '--no-fetch', '--json'], { cwd })
22    const next = status.exitCode === 0 ? lineFromStatus(id, status.stdout, basename(cwd)) : null
23    await update($, line, () => next)
24  } catch {
25    await update($, line, () => null)
26  }
27}
28
29export const register: Register = on => {
30  on('session.start', async ($, e, next) => {
31    await refresh($)
32
33    return next(e)
34  })
35
36  on('turn.complete', async ($, e, next) => {
37    await refresh($)
38
39    return next(e)
40  })
41
42  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
43    const current = await read($, line)
44
45    if (current === null) {
46      return next(e)
47    }
48
49    const { Box, Text } = $.ui.resolve(e)
50
51    return (
52      <Box>
53        <Text dimColor>{formatLine(current)}</Text>
54      </Box>
55    )
56  })
57}
58
hooks/hud.ts 36 lines
1import type { HudLine } from '../types'
2
3const BRANCH = /^plan\/(\d+)-/
4
5/** The spec id a `plan/<id>-<handle>` branch carries; digits only, so it is safe in an argv. */
6export const specIdFromBranch = (branch: string): number | null => {
7  const match = BRANCH.exec(branch.trim())
8
9  return match ? Number(match[1]) : null
10}
11
12export const formatLine = (line: HudLine): string =>
13  `spec ${line.id} · ${line.stage} · ${line.checked}/${line.total} · ${line.worktree}`
14
15/** Builds a line from `cq specs status --spec <id> --json` stdout; null when it does not parse. */
16export const lineFromStatus = (
17  id: number,
18  stdout: string,
19  worktree: string
20): HudLine | null => {
21  try {
22    const status = JSON.parse(stdout)
23    const tasks = status?.tasks
24
25    if (typeof status?.stage !== 'string' || typeof tasks?.checked !== 'number' || typeof tasks?.total !== 'number') {
26      return null
27    }
28
29    return { id, stage: status.stage, checked: tasks.checked, total: tasks.total, worktree }
30  } catch {
31    return null
32  }
33}
34
35export const basename = (path: string): string => path.replace(/\/+$/, '').split('/').pop() ?? path
36
types/index.d.ts 14 lines
1export type HudLine = {
2  id: number
3  stage: string
4  checked: number
5  total: number
6  worktree: string
7}
8
9declare module 'claude-code' {
10  interface PluginState {
11    'quenching-hud': { line: HudLine | null }
12  }
13}
14