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

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.

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.
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.
claude --plugin-dir ./band-boxes
claude plugin validate ./band-boxes
claude plugin test ./band-boxeshooks/register.tsx 18 lines1import 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}
18hooks/layers.ts 79 lines1import 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