SLOPSHOPPER

aikya-boxbot

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

newpanecommandtimer
v0.3.2MITupdated 2026-10-26jlema0590/aikya-mods/mods/aikya-boxbot
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · aikya-boxbot
│ ┃ Aikya BoxBot ✕ › fix the failing auth test and add an audit log call │ ┃ AIKYA BOXBOT │ ┃ Conveyor running ⏺ Read(src/auth.ts) │ ┃ +--+ +--+ ⎿ Read 6 lines │ ┃ |::| |::| ⏺ Update(src/auth.ts) │ ┃ +--+ +--+ ⎿ Added 2 lines, removed 1 line │ ┃ >=====>=====>=====>=====>=====>=====>=====>= ⏺ Bash(bun test) │ ┃ o o o o o o o o o o o ⎿ 3 pass, 1 fail │ ┃ | │ ┃ IN ● Done. refresh now rejects expired claims and logs an audit event. │ ┃ │ ┃ ✻ Worked for 42s · done 4:20 PM │ ┃ │ ┃ › /aikya-boxbot │ ┃ │ ┃ │ ┃ │ ┃ │ ┃ │ ┃ │ ┃ │ ┃ │ ┃ │ ┃ │ ┃ │ ┃ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · Aikya BoxBot
AIKYA BOXBOT Conveyor running +--+ +--+ |::| |::| +--+ +--+ >=====>=====>=====>=====>=====>=====>=====>=====>=====>= o o o o o o o o o o o o o o | | IN OUT
README

Aikya BoxBot

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.

Install

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.

Try without installation

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.

Terminal layout requirement

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.

Rendering and access

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.

Verify

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

Source 2 files
hooks/register.js 114 lines
1import { 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}
114
lib/scene.js 68 lines
1/** 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