Tooling only, never installed: draws what sidepad avoids, so the limit scenarios of check:live can watch Claude Code do it.

A Claude Code mod that adds a read-only side pane to the terminal: a file tree, a viewer for code and rendered Markdown, and a way to send a selected passage back to Claude.
The plugin is built and its tests pass. No version is tagged yet, so what the marketplace serves is what sits on the default branch.
| Feature | In one line |
|---|---|
| Opening | /sidepad opens the pane and closes it again; /sidepad auto governs its opening on Claude's edits |
| Themes | /sidepad theme picks auto, classic or contrast for the pane's colours |
| Following Claude | At the end of Claude's turn the pane shows the last file the turn edited, at its first changed line |
| Frame | The top row names the page shown; a status line says where in it you are |
| Navigating | One page at a time: a file, a directory's listing, or the files Claude edited this session |
| Reading | Code with the engine's highlighting, Markdown formatted or line by line, a .diff with its hunk headers marked, a .csv or .tsv as a table, a PNG as a picture |
| Selecting | A drag takes lines, a click takes the block under it |
| Asking | A command bar sends the selection to Claude, or the next prompt you type carries it |
The pane is read-only: every change to a file is made by Claude from a selection.
Each feature with its rules and its limits: docs/features.md. Every limit in one list: docs/limits.md. What the plugin hooks and calls on the engine: plugins/sidepad/README.md.
sidepad is a mod: a Claude Code plugin whose behaviour lives in a hooks module. Mods are early access in Claude Code, load only with CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1, and their API may change between Claude Code releases without notice. sidepad is verified against one version at a time, currently Claude Code 2.1.280, and carries no code for an earlier one until mods are released.
The pane draws where the layout docks it beside the transcript, which is the fullscreen layout, and from 110 terminal columns. On the main screen nothing opens by itself.
From the marketplace this repository serves:
claude plugin marketplace add duqaXxX/sidepad
claude plugin install sidepad@sidepad
Then /sidepad in any session started with CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1. An installed copy is the version it was installed at: claude plugin update compares the version in the plugin's manifest, so a change on the default branch reaches it only once that version changes.
git clone https://github.com/duqaXxX/sidepad.git
cd sidepad
CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 claude --plugin-dir plugins/sidepad
This is the copy to use while changing it: Claude Code reloads the plugin as a file under plugins/sidepad/ is saved, and an open pane takes one /sidepad to fill again. bun run playground writes a synthetic project to try it on.
See CONTRIBUTING.md. Security reports go through SECURITY.md. Changes are recorded in docs/CHANGELOG.md.
hooks/register.tsx 77 lines1import type { On } from 'claude-code';
2import {
3 CODE_WITH_EMPTY_LINE,
4 COMMAND,
5 PANE_ID,
6 UNASKED_OPEN_DELAY_MS,
7 UNASKED_PANE_ID,
8 UNASKED_STATUS,
9} from './cases';
10
11/**
12 * Registers `/engine-fixture code-no-gutter`, which opens a pane drawing `CODE_WITH_EMPTY_LINE` in a
13 * `Code` with no `startLine`, and `/engine-fixture panes`, which answers `<id> isShown=<bool>
14 * isPlaced=<bool>` for each pane `$.ui.panes()` reports. `/engine-fixture unasked` opens
15 * `UNASKED_PANE_ID` on its own a moment after answering, and sets the status line to what that open
16 * resolved, under the tag given after the word; `/engine-fixture asked` opens the same pane as the person's command. The scenarios read
17 * all of it off the screen.
18 *
19 * @param on the engine's registrar
20 */
21export function register(on: On) {
22 on('session.start', async ($, e, next) => {
23 await $.command.register({
24 name: COMMAND,
25 description: 'Tooling for check:live: draw a gutterless Code, report the open panes, or open one unasked',
26 argumentHint: 'code-no-gutter | panes | unasked | asked',
27 });
28
29 return next(e);
30 });
31
32 on('command.run', { command: COMMAND }, async ($, e) => {
33 const [word, tag = ''] = e.args.trim().split(/\s+/);
34
35 if (word === 'code-no-gutter') {
36 await $.ui.open({ id: PANE_ID, title: 'engine fixture' });
37
38 return { text: 'engine fixture: pane open' };
39 }
40
41 if (word === 'unasked') {
42 // Opened once the command has answered, so the engine counts the open as the plugin's own.
43 void $.clock
44 .sleep(UNASKED_OPEN_DELAY_MS)
45 .then(() => $.ui.open({ id: UNASKED_PANE_ID, title: 'engine fixture unasked' }))
46 .then((opened) => $.ui.status(`${UNASKED_STATUS(tag)}${opened.isPlaced}`));
47
48 return { text: 'engine fixture: unasked open scheduled' };
49 }
50
51 if (word === 'asked') {
52 await $.ui.open({ id: UNASKED_PANE_ID, title: 'engine fixture unasked' });
53
54 return { text: 'engine fixture: asked open' };
55 }
56
57 if (word === 'panes') {
58 const panes = await $.ui.panes();
59 const listed = panes.map((pane) => `${pane.id} isShown=${pane.isShown} isPlaced=${pane.isPlaced}`);
60
61 return { text: `engine fixture: ${listed.join(', ') || 'no pane'}` };
62 }
63
64 return { text: 'engine fixture: code-no-gutter | panes | unasked | asked' };
65 });
66
67 on('ui.render', { component: 'Pane' }, ($, e, next) => {
68 if ((e.requestId !== PANE_ID && e.requestId !== UNASKED_PANE_ID) || e.surface !== 'terminal') {
69 return next(e);
70 }
71
72 const { Code } = $.ui.resolve(e);
73
74 return <Code source={CODE_WITH_EMPTY_LINE} />;
75 });
76}
77hooks/cases.ts 25 lines1// What the fixture draws and answers, read by the limit scenarios as well: a plain module, so the
2// tooling can import it without the fixture's JSX.
3
4/** The fixture's pane, and the command that drives it. */
5export const PANE_ID = 'engine-fixture';
6export const COMMAND = 'engine-fixture';
7
8/**
9 * The pane the fixture opens on its own, apart from `PANE_ID`: the engine remembers across sessions
10 * which ids a person asked for, and that memory moves the floor an unasked pane is drawn from.
11 */
12export const UNASKED_PANE_ID = 'engine-fixture-unasked';
13
14/** How long after its command the fixture opens `UNASKED_PANE_ID`, so the open answers no input. */
15export const UNASKED_OPEN_DELAY_MS = 1_500;
16
17/**
18 * The status line the fixture sets once the unasked open `tag` names resolved, `isPlaced` appended:
19 * the tag tells one open's line from an earlier one's still drawn.
20 */
21export const UNASKED_STATUS = (tag: string) => `engine fixture: unasked ${tag} isPlaced=`;
22
23/** A gutterless `Code` source with an empty line between two others. */
24export const CODE_WITH_EMPTY_LINE = 'first line\n\nthird line';
25