SLOPSHOPPER

band-boxes

Draws each mod's part of the band above the prompt in its own rounded box

band
v0.2.0no licenseupdated 2026-10-20tsnAnh/band-boxes
A shopper browsing a rack in a slop shop
README

band-boxes

A Claude Code mod that gives every mod in the band above the prompt its own rounded box.

When several mods draw above the input (context meters, usage, goal recaps, next steps…), they stack into one undivided block and it's hard to tell where one ends and the next begins. band-boxes splits that block back into one layer per mod and draws each in a tight, dim, rounded border. Mods that already draw their own border are left alone.

Before and after band-boxes

Install

At the prompt of a Claude Code terminal session:

/plugin install band-boxes --marketplace tsnAnh/band-boxes

From a shell:

claude plugin install band-boxes --marketplace tsnAnh/band-boxes

In the desktop app: click + next to the prompt box, then Plugins, Add marketplace with tsnAnh/band-boxes, then Add plugin.

Answer y to add the marketplace, pick a scope, and it's active right away. Nothing to configure.

How it works

Band mods follow one convention: draw their own part and nest everything beneath them (next(e)) inside a column. band-boxes walks that nested tree down to Claude Code's own drawing, collects each mod's own part as a layer, strips its side padding, and wraps it in a round border.

It only sees the mods that sit beneath it in the render chain. If two mods still share a box, they were stacked above band-boxes.

Develop

claude --plugin-dir ./band-boxes
claude plugin validate ./band-boxes
claude plugin test ./band-boxes
Source 2 files
hooks/register.tsx 18 lines
1import type { Register } from 'claude-code'
2
3import { boxOwnParts, boxTree } from './layers'
4
5export const register: Register = on => {
6  on('ui.resolve', { component: 'AbovePrompt' }, async ($, e, next) => {
7    const table = await next(e)
8    const { Box } = table
9    return { ...table, Box: props => Box(boxOwnParts(props, Box)) }
10  })
11
12  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
13    const tree = await next(e)
14    if (e.props.hasSurvey) return tree
15    return boxTree(tree, $.ui.resolve(e).Box)
16  })
17}
18
hooks/layers.ts 79 lines
1import type { RenderElement, RenderNode } from 'claude-code'
2
3const isElement = (n: RenderNode | null | undefined): n is RenderElement => typeof n === 'object' && n !== null
4
5const holdsEngine = (n: RenderNode): boolean =>
6  isElement(n) && (n.type === 'engine' || ('children' in n && (n.children ?? []).some(holdsEngine)))
7
8const isColumnOnly = (props: Record<string, unknown>): boolean =>
9  props.flexDirection === 'column' && Object.keys(props).every(k => k === 'flexDirection')
10
11const isBare = (n: RenderNode): n is Extract<RenderElement, { type: 'Box' }> =>
12  isElement(n) && n.type === 'Box' && isColumnOnly(n.props ?? {})
13
14const isBlank = (n: RenderNode | undefined): boolean =>
15  n === undefined || (typeof n === 'string' && n.trim() === '') || (isBare(n) && (n.children ?? []).every(isBlank))
16
17export const isBoxed = (parts: RenderNode[]): boolean =>
18  parts.length === 1 && isElement(parts[0]) && parts[0].type === 'Box' && parts[0].props?.borderStyle !== undefined
19
20const SIDE_PADDING = new Set(['paddingX', 'paddingLeft', 'paddingRight', 'padding'])
21
22export const tighten = (n: RenderNode): RenderNode => {
23  if (!isElement(n) || n.type !== 'Box' || n.props?.borderStyle !== undefined) return n
24  const props = Object.fromEntries(Object.entries(n.props ?? {}).filter(([k]) => !SIDE_PADDING.has(k)))
25  return { ...n, props, children: (n.children ?? []).map(c => (isElement(c) && c.type === 'Box' ? tighten(c) : c)) }
26}
27
28type BoxProps = { children?: unknown } & Record<string, unknown>
29type BoxConstructor = (props: never) => RenderElement
30
31const flatten = (children: unknown): RenderNode[] =>
32  Array.isArray(children)
33    ? children.flatMap(flatten)
34    : typeof children === 'number'
35      ? [String(children)]
36      : typeof children === 'string' || isElement(children as RenderNode)
37        ? [children as RenderNode]
38        : []
39
40const ROUND_BOX = { flexDirection: 'column', borderStyle: 'round', borderColor: 'inactive' } as const
41
42const roundBox = (own: RenderNode[], Box: BoxConstructor): RenderElement =>
43  Box({ ...ROUND_BOX, children: own.map(tighten) } as never)
44
45const boxParts = (parts: RenderNode[], Box: BoxConstructor): RenderNode[] => {
46  const own = parts.filter(n => !isBlank(n))
47  if (own.length === 0) return parts
48  return isBoxed(own) ? own : [roundBox(own, Box)]
49}
50
51export function boxOwnParts<P extends BoxProps>(props: P, Box: BoxConstructor): P {
52  const { children, ...style } = props
53  if (!isColumnOnly(style)) return props
54  const nodes = flatten(children)
55  if (!nodes.some(holdsEngine)) return props
56
57  const out: RenderNode[] = []
58  let run: RenderNode[] = []
59  for (const n of nodes) {
60    if (holdsEngine(n)) {
61      out.push(...boxParts(run, Box), n)
62      run = []
63    } else run.push(n)
64  }
65  out.push(...boxParts(run, Box))
66  return { ...props, children: out }
67}
68
69const boxLayers = (node: RenderElement, Box: BoxConstructor): RenderElement => {
70  if (!isBare(node)) return node
71  const children = (node.children ?? []).map(c => (isElement(c) && holdsEngine(c) ? boxLayers(c, Box) : c))
72  return { ...node, children: boxOwnParts({ ...node.props, children }, Box).children as RenderNode[] }
73}
74
75export const boxTree = (tree: RenderElement, Box: BoxConstructor): RenderElement => {
76  if (holdsEngine(tree)) return boxLayers(tree, Box)
77  return isBlank(tree) || isBoxed([tree]) ? tree : roundBox([tree], Box)
78}
79