SLOPSHOPPER

chat-inventory

Zero-token side panel, In This Chat: goal, to-dos, connectors, skills and files.

newpaneguardcommandprompt
v0.1.0no licenseupdated 2026-10-25ikentrock/starwars-claude-mods/chat-inventory
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · chat-inventory
│ ┃ In This Chat ✕ › fix the failing auth test and add an audit log call │ ┃ Goal │ ┃ fix the failing auth test and add an audit ⏺ Read(src/auth.ts) │ ┃ log call ⎿ Read 6 lines │ ┃ To-do ⏺ Update(src/auth.ts) │ ┃ None ⎿ Added 2 lines, removed 1 line │ ┃ Connectors ⏺ Bash(bun test) │ ┃ None called ⎿ 3 pass, 1 fail │ ┃ Skills │ ┃ None active ● Done. refresh now rejects expired claims and logs an audit event. │ ┃ Files │ ┃ read app/src/auth.ts ✻ Worked for 42s · done 4:20 PM │ ┃ edited app/src/auth.ts │ ┃ created app/src/audit.ts › /inventory │ ┃ created app/src/cache.ts ⎿ chat-inventory: In This Chat opened. │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · In This Chat
Goal fix the failing auth test and add an audit log call To-do None Connectors None called Skills None active Files read app/src/auth.ts edited app/src/auth.ts created app/src/audit.ts created app/src/cache.ts
README

starwars-claude-mods

Nine local Claude Code mods (UI and hook plugins) with a light Star Wars flavor.

ModWhat it does
brain-dumpIntercepts long, unstructured prompts, asks clarifying questions, rewrites them into a structured prompt. !raw prefix skips it.
helper-deskProgress bar from the to-do list, plus a side panel for sub-agents (/helper-desk).
cache-clockPrompt-cache countdown, Keep Warm button, /keep-warm.
handoffGenerates a handoff prompt at 250k tokens, on cold cache, or on /handoff.
reflectOffers to save rules/preferences to memory, CLAUDE.md, or a second brain.
chat-inventoryZero-token "In This Chat" panel (/inventory).
switchboard/mods panel to toggle mods and hide panels.
power-cellsGauges above the prompt for context, the 5-hour limit, and a weekly pacing plan: a daily ration, work-day weighting, over/under-plan warnings. /power plan shows the week.
droid-consoleRestyles the whole UI as a droid console: personality palettes, transmission headers, droid spinner words, status band. /droid switches personality.

Status: experimental. They pass claude plugin validate and tsc, but have not been tested at runtime. Expect rough edges.

Install

Add the marketplace once, then install the mods you want (user scope makes them load in every session):

claude plugin marketplace add ikentrock/starwars-claude-mods
claude plugin install brain-dump@starwars-claude-mods --scope user
claude plugin install cache-clock@starwars-claude-mods --scope user
claude plugin install chat-inventory@starwars-claude-mods --scope user
claude plugin install droid-console@starwars-claude-mods --scope user
claude plugin install handoff@starwars-claude-mods --scope user
claude plugin install helper-desk@starwars-claude-mods --scope user
claude plugin install power-cells@starwars-claude-mods --scope user
claude plugin install reflect@starwars-claude-mods --scope user
claude plugin install switchboard@starwars-claude-mods --scope user

Or one at a time from a session prompt:

/plugin install <mod> --marketplace ikentrock/starwars-claude-mods

Update later with claude plugin marketplace update starwars-claude-mods.

Source 2 files
hooks/register.tsx 137 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { InventoryFile } from '../types'
5
6const PANE = 'in-this-chat'
7const goal = atom({ plugin: 'chat-inventory', key: 'goal' } as const, '', { shape: 'goal-1' })
8const todos = atom({ plugin: 'chat-inventory', key: 'todos' } as const, [], { shape: 'todos-1' })
9const connectors = atom({ plugin: 'chat-inventory', key: 'connectors' } as const, {}, { shape: 'connectors-1' })
10const skills = atom({ plugin: 'chat-inventory', key: 'skills' } as const, {}, { shape: 'skills-1' })
11const files = atom({ plugin: 'chat-inventory', key: 'files' } as const, [], { shape: 'files-1' })
12
13const bump = (counts: Record<string, number>, name: string) => ({
14  ...counts,
15  [name]: (counts[name] ?? 0) + 1,
16})
17
18async function note($: EngineInterface, path: string, kind: InventoryFile['kind']) {
19  await update($, files, list =>
20    list.some(f => f.path === path && f.kind === kind)
21      ? list
22      : [...list, { path, kind }].slice(-300),
23  )
24}
25
26export const register: Register = on => {
27  on('session.start', async ($, e, next) => {
28    await $.command.register({
29      name: 'inventory',
30      description: 'Open the In This Chat panel',
31    })
32    void $.ui.open({ id: PANE, title: 'In This Chat' })
33
34    return next(e)
35  })
36
37  on('command.run', { command: 'inventory' }, async $ => {
38    await $.ui.open({ id: PANE, title: 'In This Chat' })
39
40    return { text: 'In This Chat opened.' }
41  })
42
43  on('session.end', async ($, e, next) => {
44    if (e.reason === 'clear') {
45      await update($, goal, () => '')
46      await update($, todos, () => [])
47      await update($, connectors, () => ({}))
48      await update($, skills, () => ({}))
49      await update($, files, () => [])
50    }
51
52    return next(e)
53  })
54
55  on('prompt.submit', async ($, e, next) => {
56    if (!e.command && !e.isMeta) {
57      await update($, goal, current => (current === '' ? e.text.trim().slice(0, 160) : current))
58    }
59
60    return next(e)
61  })
62
63  // Pure bookkeeping: no model call, so the panel costs no tokens.
64  on('tool.call', async ($, e, next) => {
65    const tool = String(e.tool)
66    const input = e as unknown as Record<string, unknown>
67
68    if (tool === 'TodoWrite') {
69      const list = (input.todos ?? []) as { content: string; status: string; activeForm: string }[]
70      await update($, todos, () => list.map(t => ({ ...t })))
71    } else if (tool.startsWith('mcp__')) {
72      const server = tool.split('__')[1] ?? tool
73      await update($, connectors, counts => bump(counts, server))
74    } else if (tool === 'Skill') {
75      await update($, skills, counts => bump(counts, String(input.skill ?? 'unknown')))
76    } else if (tool === 'Read' && typeof input.file_path === 'string') {
77      await note($, input.file_path, 'read')
78    } else if (tool === 'Write' && typeof input.file_path === 'string') {
79      await note($, input.file_path, 'created')
80    } else if (tool === 'Edit' && typeof input.file_path === 'string') {
81      await note($, input.file_path, 'edited')
82    }
83
84    return next(e)
85  })
86
87  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
88    const { Box, Text } = $.ui.resolve(e)
89    const [g, list, conn, sk, fl] = [
90      await read($, goal),
91      await read($, todos),
92      await read($, connectors),
93      await read($, skills),
94      await read($, files),
95    ]
96    const width = Math.max(16, (e.props.bodyColumns ?? 40) - 4)
97    const cut = (text: string) => (text.length > width ? `${text.slice(0, width - 1)}…` : text)
98    const mark = { completed: '✔', in_progress: '▸', pending: '·' } as Record<string, string>
99
100    return (
101      <Box flexDirection="column">
102        <Text bold>Goal</Text>
103        <Text dimColor={g === ''}>{g === '' ? 'Not set yet' : cut(g)}</Text>
104        <Text bold>To-do</Text>
105        {list.length === 0 && <Text dimColor>None</Text>}
106        {list.map(t => (
107          <Text dimColor={t.status === 'completed'}>
108            {mark[t.status] ?? '·'} {cut(t.status === 'in_progress' ? t.activeForm : t.content)}
109          </Text>
110        ))}
111        <Text bold>Connectors</Text>
112        {Object.keys(conn).length === 0 && <Text dimColor>None called</Text>}
113        {Object.entries(conn).map(([name, n]) => (
114          <Text>
115            {cut(name)} <Text dimColor>×{n}</Text>
116          </Text>
117        ))}
118        <Text bold>Skills</Text>
119        {Object.keys(sk).length === 0 && <Text dimColor>None active</Text>}
120        {Object.entries(sk).map(([name, n]) => (
121          <Text>
122            {cut(name)} <Text dimColor>×{n}</Text>
123          </Text>
124        ))}
125        <Text bold>Files</Text>
126        {fl.length === 0 && <Text dimColor>None touched</Text>}
127        {fl.slice(-40).map(f => (
128          <Text>
129            <Text dimColor>{f.kind} </Text>
130            {cut(f.path.split('/').slice(-3).join('/'))}
131          </Text>
132        ))}
133      </Box>
134    )
135  })
136}
137
types/index.d.ts 15 lines
1export type InventoryTodo = { content: string; status: string; activeForm: string }
2export type InventoryFile = { path: string; kind: 'read' | 'created' | 'edited' }
3
4declare module 'claude-code' {
5  interface PluginState {
6    'chat-inventory': {
7      goal: Shaped<string>
8      todos: Shaped<InventoryTodo[]>
9      connectors: Shaped<Record<string, number>>
10      skills: Shaped<Record<string, number>>
11      files: Shaped<InventoryFile[]>
12    }
13  }
14}
15