SLOPSHOPPER

aside

A side pane for questions about the session, answered from a fork of the transcript and kept out of it

newpanecommandmodel
★ 1v0.1.0no licenseupdated 2026-10-04turanmahmudov/dotfiles/.claude/skills/aside
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · aside
│ ┃ Aside ✕ › fix the failing auth test and add an audit log call │ ┃ Ask about the session. The main conversation │ ┃ does not see it. ⏺ Read(src/auth.ts) │ ┃ Ask about the session ⏎ ask ⎿ Read 6 lines │ ⏺ Update(src/auth.ts) │ ⎿ Added 2 lines, removed 1 line │ ⏺ Bash(bun test) │ ⎿ 3 pass, 1 fail │ │ ● Done. refresh now rejects expired claims and logs an audit event. │ │ ✻ Worked for 42s · done 4:20 PM │ │ › /aside │ ⎿ aside: Aside opened. │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · Aside
Ask about the session. The main conversation does not see it. Ask about the session ⏎ ask
README

Dotfiles

My personal dotfiles managed with YADM.

Tested on Ubuntu 25.10 and Ubuntu 25.04.

Installation

yadm clone <your-repo-url>
yadm config local.class work  # or personal
yadm config yadm.alt-copy true
yadm alt
yadm bootstrap

Important Keybindings

  • SUPER + Return - Terminal
  • SUPER + T - Terminal with tmux
  • SUPER + Space/D - Application launcher
Source 3 files
hooks/register.tsx 74 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { AsideEntry } from '../types'
5import { ASIDE_PROMPT, describeFailure } from '../core/prompt'
6
7const PANE = 'aside'
8const KEPT_ENTRIES = 20
9const entries = atom({ plugin: 'aside', key: 'entries' } as const, [])
10
11
12export const register: Register = on => {
13  on('session.start', async ($, e, next) => {
14    await $.command.register({
15      name: 'aside',
16      description: 'Ask a side question about this session without adding it to the conversation',
17      argumentHint: '[question]',
18      immediate: true,
19    })
20
21    return next(e)
22  })
23
24  on('command.run', { command: 'aside' }, async ($, e) => {
25    await $.ui.open({ id: PANE, title: 'Aside', focus: true, closeOnEscape: true })
26    if (e.args.trim() !== '') void askAside($, e.args.trim())
27
28    return { text: 'Aside opened.' }
29  })
30
31  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
32    const { Box, Markdown, Text } = $.ui.resolve(e)
33    const Input = e.surface === 'mobile' ? undefined : $.ui.resolve(e).Input
34    const list = await read($, entries)
35    const room = Math.max(1, Math.floor(((e.viewport?.rows ?? 30) - 4) / 6))
36
37    return (
38      <Box flexDirection="column">
39        {list.length === 0 && <Text dimColor>Ask about the session. The main conversation does not see it.</Text>}
40        {list.slice(-room).map(entry => (
41          <Box key={`entry-${entry.id}`} flexDirection="column" marginBottom={1}>
42            <Text bold>{'> '}{entry.question}</Text>
43            {entry.answer === null ? (
44              <Text dimColor>Thinking...</Text>
45            ) : entry.isError ? (
46              <Text color="red">{entry.answer}</Text>
47            ) : (
48              <Markdown key={`answer-${entry.id}`} text={entry.answer} />
49            )}
50          </Box>
51        ))}
52        {Input !== undefined && (
53          <Input key="question" placeholder="Ask about the session" submitLabel="ask" autoFocus onSubmit={(value: string) => void askAside($, value)} />
54        )}
55      </Box>
56    )
57  })
58}
59
60async function askAside($: EngineInterface, question: string): Promise<void> {
61  const text = question.trim()
62  if (text === '') return
63
64  const id = Math.max(0, ...(await read($, entries)).map(entry => entry.id)) + 1
65  await update($, entries, list => [...list, { id, question: text, answer: null, isError: false }].slice(-KEPT_ENTRIES))
66
67  const forked = await $.model.fork({ prompt: `${ASIDE_PROMPT}\n${text}` })
68  const answered: Pick<AsideEntry, 'answer' | 'isError'> = forked.isAnswered
69    ? { answer: forked.text, isError: false }
70    : { answer: describeFailure(forked.reason), isError: true }
71
72  await update($, entries, list => list.map(entry => (entry.id === id ? { ...entry, ...answered } : entry)))
73}
74
core/prompt.ts 14 lines
1export const ASIDE_PROMPT = [
2  'The user asks a side question about this session. The answer is shown in a side pane and does not enter the main conversation.',
3  'Answer from what the conversation already holds. Be short and direct. Do not plan new work or claim that you will do anything.',
4  '',
5  'Question:',
6].join('\n')
7
8export function describeFailure(reason: string): string {
9  if (reason === 'nothing-to-fork') return 'The session has no reply yet, so there is nothing to ask about.'
10  if (reason === 'aborted') return 'The question was stopped before it was answered.'
11
12  return `The question failed: ${reason}.`
13}
14
types/index.d.ts 8 lines
1export type AsideEntry = { id: number; question: string; answer: string | null; isError: boolean }
2
3declare module 'claude-code' {
4  interface PluginState {
5    'aside': { entries: AsideEntry[] }
6  }
7}
8