Boxes moving along a conveyor belt in an optional Claude Code pane.

Boxes move from left to right along a conveyor belt with turning rollers. BoxBot is an optional, decorative Claude Code mod. Its movement does not measure task completion, tokens, or tool success. All interface text and commands are in English.
From your terminal, install directly from the hosted marketplace:
claude plugin install aikya-boxbot --marketplace jlema0590/aikya-mods
This installs only aikya-boxbot. To install the other mods, run their matching commands in the installation guide.
From the repository root:
bash mods/aikya-boxbot/install.sh
For direct installation from the hosted marketplace in a Claude Code session:
/plugin install aikya-boxbot --marketplace jlema0590/aikya-mods
Restart Claude Code after installation, then enter /aikya-boxbot. This repository is public. See the online installers for Linux, macOS and Windows.
Requires Claude Code with mods support. Tested with Claude Code 2.1.308.
From the directory containing this plugin:
claude --plugin-dir ./aikya-boxbot
Then enter:
/aikya-boxbot
The panel displays the animation without buttons or on-screen controls. Escape closes it when Claude gives the pane keyboard focus. The loop runs when its independent pane or its shared Flow section is visible and stops when that pane closes or the session ends. Reopening preserves its position and playback choices. Playback remains available through the commands below.
The same Claude terminal surface applies when claude is started in Cursor, Trae or a JetBrains IDE's integrated terminal. User-scoped installation is shared only when those terminals use the same user, runtime and Claude configuration. See the integrated-terminal validation matrix for actual validation status and the manual protocol.
BoxBot 0.3.1 anchors the conveyor at the bottom of its docked pane. With Aikya Flow 0.1.1 or later enabled, Flow stays at the top and BoxBot fills the bottom section of that same native pane. Flow opens that pane at interactive startup, keeping the prompt's keyboard focus. No on-screen controls are added. Closing the Flow pane stops the conveyor; hiding BoxBot removes only its section.
Without Flow, use /aikya-boxbot to open its independent pane, with the conveyor at the bottom. The layout uses the measured pane body height and adjusts on resize. Inline panes remain compact. If the combined content is taller than the available pane, Claude scrolls the whole content; the footer is not a separate scrolling region. Native panes require a sufficiently wide fullscreen terminal for side docking; the host controls placement. The animation stops when another pane is drawn. Pausing for every possible platform visibility change is not guaranteed by this prototype.
You can also use:
/aikya-boxbot pause
/aikya-boxbot resume
/aikya-boxbot still
/aikya-boxbot motion
/aikya-boxbot hide
still displays stationary boxes on the conveyor belt. It is the native mod's manual reduced-motion option. The browser preview additionally honors prefers-reduced-motion; the mod does not claim to detect an operating-system preference that its API does not expose.
BoxBot 0.3.2 keeps only its title, playback state and conveyor in the pane; the decorative-animation disclaimer is documented here instead of repeated on screen. Its motion still does not measure task progress. Update from /plugin → Marketplaces → aikya-mods → Update marketplace, or refresh the marketplace and run claude plugin update aikya-boxbot@aikya-mods in your terminal, then restart. See the update guide.
Desktop and VS Code use the native Svg element. The terminal uses a seven-row monospace drawing that fits the pane width. Three boxes travel at a constant speed in an eight-second loop sampled at eight frames per second. Boxes enter at the left edge and leave at the right edge; the belt and rollers move with them. The animation timer is cancelled while paused, in still mode, and when hidden. No model request, tool call, subprocess, network request, credential lookup, file read/write, or telemetry call is made by the mod.
The plugin registers its own command and observes its own pane closing plus session lifecycle. It does not modify prompts, permissions, other panes, or Aikya's execution state. --plugin-dir loads it for the session; the installer registers it through the Aikya Mods marketplace.
claude plugin validate ./aikya-boxbot --strict
claude plugin test ./aikya-boxbot
The native tests exercise opening, timer-driven updates, pause/resume, still mode, Desktop/VS Code element validation, closing/reopening, narrow layouts, conveyor movement, loop continuity, and bottom anchoring at different pane heights, compact inline layouts, and a single shared frame in either hook order. Native surface tests validate element trees; they do not replace a manual session check of the final application paint.
API references: official mods reference, official testing guide.
GENERATED BY AIKYA HARNESS
hooks/register.js 114 lines1import { FRAME_MS, FRAME_COUNT, sampleScene, asciiFrame, svgFrame } from '../lib/scene.js';
2
3const PANE = 'aikya-boxbot';
4
5function stop(state) {
6 if (state.timer) state.timer.cancel();
7 state.timer = null;
8}
9
10function sync($, state) {
11 stop(state);
12 if (state.visible && !state.paused && !state.still) {
13 state.timer = $.clock.every(FRAME_MS, () => {
14 state.frame = (state.frame + 1) % FRAME_COUNT;
15 $.ui.invalidate('ui.render');
16 });
17 }
18 $.ui.invalidate('ui.render');
19}
20
21export function register(on) {
22 const state = { frame: 0, visible: false, paused: false, still: false, timer: null, shared: false, hidden: false };
23
24 on('session.start', async ($, e, next) => {
25 await $.command.register({
26 name: 'aikya-boxbot',
27 description: 'Show boxes moving along a conveyor belt',
28 argumentHint: '[show|pause|resume|still|motion|hide]',
29 immediate: true,
30 });
31 return next(e);
32 });
33
34 on('command.run', { command: 'aikya-boxbot' }, async ($, e) => {
35 const action = e.args.trim() || 'show';
36 if (action === 'hide') {
37 state.hidden = true;
38 state.visible = false;
39 stop(state);
40 await $.ui.close({ id: PANE });
41 $.ui.invalidate('ui.render');
42 return {};
43 }
44 if (!['show', 'pause', 'resume', 'still', 'motion'].includes(action)) {
45 return { text: 'Usage: /aikya-boxbot [show|pause|resume|still|motion|hide]' };
46 }
47 if (action === 'pause') state.paused = true;
48 if (action === 'resume') state.paused = false;
49 if (action === 'still') { state.still = true; state.frame = 20; }
50 if (action === 'motion') state.still = false;
51 state.hidden = false;
52 if (state.shared) { state.visible = true; sync($, state); return {}; }
53 const placement = await $.ui.open({ id: PANE, title: 'Aikya BoxBot', focus: true, closeOnEscape: true });
54 state.visible = placement.isPlaced;
55 sync($, state);
56 return {};
57 });
58
59 on('ui.close', { id: PANE }, async ($, e, next) => {
60 if (!state.shared) { state.visible = false; stop(state); }
61 return next(e);
62 });
63
64 on('ui.close', { id: 'aikya-flow' }, async ($, e, next) => {
65 if (state.shared) { state.visible = false; state.shared = false; stop(state); }
66 return next(e);
67 });
68
69 on('session.end', async ($, e, next) => {
70 state.visible = false;
71 stop(state);
72 return next(e);
73 });
74
75 on('ui.render', { component: 'Pane' }, async ($, e, next) => {
76 const shared = e.requestId === 'aikya-flow';
77 if (e.requestId !== PANE && !shared) {
78 state.visible = false; state.shared = false; stop(state);
79 return next(e);
80 }
81 const theirs = shared ? await next(e) : null;
82 if (shared && state.hidden) return theirs;
83 if (shared && (!state.shared || !state.visible)) {
84 state.shared = true; state.visible = true; sync($, state);
85 }
86 if (!shared) state.shared = false;
87 const { Box, Text, Code, Svg } = $.ui.resolve(e);
88 const status = state.still ? 'Still mode' : state.paused ? 'Paused' : sampleScene(state.frame).phase;
89 const columns = e.props.bodyColumns || Math.max(12, (e.viewport?.columns || 48) - 4);
90 const artwork = e.surface === 'desktop' || e.surface === 'vscode'
91 ? Svg({ source: svgFrame(state.frame), alt: 'Boxes moving from left to right on a conveyor belt' })
92 : Code({ source: asciiFrame(state.frame, columns) });
93 const content = Box({ flexDirection: 'column', flexShrink: 0, children: [
94 Text({ key: 'title', bold: true, children: ['AIKYA BOXBOT'] }),
95 Text({ key: 'activity', children: [status] }),
96 artwork,
97 ] });
98 if (shared) {
99 // Flow owns the full-height frame. Fill its footer when it rendered first;
100 // otherwise return compact content for Flow to place there after next().
101 if (theirs?.props?.key === 'aikya-mods-pane' && theirs.children?.some(child => child?.props?.key === 'aikya-mods-footer')) {
102 return { ...theirs, children: theirs.children.map(child => child?.props?.key === 'aikya-mods-footer'
103 ? { ...child, children: [...(child.children || []), content] } : child) };
104 }
105 if (theirs?.type === 'engine') return content;
106 return Box({ flexDirection: 'column', children: [theirs, content] });
107 }
108 const rows = e.props.scroll?.bodyRows;
109 const minHeight = e.props.placement === 'dock' && Number.isFinite(rows) && rows > 0 ? rows : undefined;
110 return Box({ key: 'aikya-boxbot-pane', flexDirection: 'column', minHeight,
111 justifyContent: 'flex-end', children: [content] });
112 });
113}
114lib/scene.js 68 lines1/** Decorative conveyor: an eight-second linear loop at eight frames/second. */
2export const FRAME_MS = 125;
3export const FRAME_COUNT = 64;
4
5export function sampleScene(frame) {
6 const f = ((Math.floor(frame) % FRAME_COUNT) + FRAME_COUNT) % FRAME_COUNT;
7 const distance = f * 660 / FRAME_COUNT;
8 return {
9 frame: f,
10 phase: 'Conveyor running',
11 distance,
12 beltOffset: distance % 16,
13 boxes: [0, 220, 440].map(offset => ({ x: (distance + offset) % 660 - 60, y: 84 })),
14 };
15}
16
17function crate(x, y) {
18 return `<g transform="translate(${x} ${y})"><rect width="44" height="40" rx="3" class="crate"/><path d="M16 0v13l6-3 6 3V0M0 15h44" class="tape"/><path d="M8 31h10" class="ink"/></g>`;
19}
20
21export function svgFrame(frame) {
22 const s = sampleScene(frame);
23 const rollers = Array.from({ length: 17 }, (_, i) => {
24 const x = 64 + i * 32;
25 return `<g transform="translate(${x} 138)"><circle r="8" fill="var(--roller)" class="edge"/><g transform="rotate(${s.distance * 3})"><path d="M-5 0h10M0-5v10" class="ink"/></g></g>`;
26 }).join('');
27 const tread = Array.from({ length: 37 }, (_, i) => `<path d="M${32 + i * 16 + s.beltOffset} 124v4" stroke="var(--tread)" stroke-width="4"/>`).join('');
28 return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 210" width="640" height="210" role="img" aria-label="Boxes moving from left to right on a conveyor belt">
29<style>
30:root{color-scheme:light dark;--bg:#f6f3ed;--floor:#d9d4c9;--line:#47483f;--belt:#b8bba9;--roller:#e4e5da;--tread:#777f6b;--box:#c88d55;--tape:#f1d4a4;--muted:#77786c}
31@media(prefers-color-scheme:dark){:root{--bg:#242622;--floor:#42483e;--line:#a5b09e;--belt:#65745b;--roller:#33402f;--tread:#c1cfb6;--box:#c49462;--tape:#e8c994;--muted:#a9b19e}}
32.edge{stroke:var(--line);stroke-width:2;stroke-linejoin:round}.crate{fill:var(--box);stroke:var(--line);stroke-width:2}.tape{fill:none;stroke:var(--tape);stroke-width:3}.ink{fill:none;stroke:var(--line);stroke-width:2}.label{fill:var(--muted);font:11px ui-monospace,monospace;letter-spacing:2px}
33</style>
34<defs><clipPath id="boxbot-belt-clip"><rect x="48" y="38" width="544" height="87"/></clipPath><clipPath id="boxbot-tread-clip"><rect x="48" y="123" width="544" height="6" rx="3"/></clipPath></defs>
35<rect width="640" height="210" rx="12" fill="var(--bg)"/>
36<path d="M32 186H608" stroke="var(--floor)" stroke-width="2"/>
37<path d="M96 152v29M544 152v29" stroke="var(--line)" stroke-width="6"/>
38<path d="M78 182h36M526 182h36" stroke="var(--belt)" stroke-width="5" stroke-linecap="round"/>
39<rect x="48" y="124" width="544" height="28" rx="14" fill="var(--belt)" class="edge"/>
40${rollers}
41<g clip-path="url(#boxbot-tread-clip)">${tread}</g>
42<g clip-path="url(#boxbot-belt-clip)">${s.boxes.map(box => crate(box.x, box.y)).join('')}</g>
43<text x="48" y="205" class="label">IN</text><text x="560" y="205" class="label">OUT</text>
44</svg>`;
45}
46
47export function asciiFrame(frame, width = 44) {
48 const s = sampleScene(frame);
49 const columns = Math.max(1, Math.min(60, Math.floor(width) || 44));
50 const grid = Array.from({ length: 7 }, () => Array(columns).fill(' '));
51 const put = (x, y, value) => {
52 for (let i = 0; i < value.length; i++) {
53 if (x + i >= 0 && x + i < columns) grid[y][x + i] = value[i];
54 }
55 };
56 for (const box of s.boxes) {
57 const x = Math.round((box.x - 48) / 544 * columns);
58 put(x, 0, '+--+'); put(x, 1, '|::|'); put(x, 2, '+--+');
59 }
60 put(0, 3, '='.repeat(columns));
61 const shift = Math.floor(s.distance / 544 * columns) % 6;
62 for (let x = shift; x < columns; x += 6) put(x, 3, '>');
63 for (let x = 1; x < columns; x += 4) put(x, 4, 'o');
64 put(Math.floor(columns / 6), 5, '|'); put(Math.floor(columns * 5 / 6), 5, '|');
65 put(0, 6, 'IN'); put(Math.max(0, columns - 3), 6, 'OUT');
66 return grid.map(row => row.join('')).join('\n');
67}
68