Remote Control row in the statuspane card and a caveman mode picker under it

Two small additions to the statuspane card above the Claude Code prompt:
on, or N attached while a phone or claude.ai has the session open. Gone outside Remote Control.defaultMode, so new sessions start in it. /plugin marketplace add xuanji86/claude-statuspane
off and picking a mode says caveman was not found): /plugin marketplace add JuliusBrussee/caveman
/plugin install caveman@caveman
/plugin install session-extras --marketplace marcinlukasik/mods
~/.config/caveman/config.json (or $XDG_CONFIG_HOME/caveman/config.json), keeping any other keys in it. /caveman <mode> typed at the prompt changes the current session only.claude plugin validate ./session-extras
claude plugin test ./session-extrashooks/register.tsx 141 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register, RenderSurface } from 'claude-code'
3
4export const ROW = 'remote-control'
5// Long ttl: the row is refreshed on every turn and attach, and cleared outside RC
6const TTL = 24 * 60 * 60
7
8const cavemanMode = atom({ plugin: 'session-extras', key: 'cavemanMode' } as const, null)
9const isCavemanMenuOpen = atom({ plugin: 'session-extras', key: 'isCavemanMenuOpen' } as const, false)
10
11// What the menu offers, in order; off removes the flag
12export const MENU_MODES = ['lite', 'full', 'ultra', 'wenyan-lite', 'wenyan', 'wenyan-ultra', 'off'] as const
13const KNOWN_MODES = new Set<string>([...MENU_MODES, 'wenyan-full', 'commit', 'review', 'compress'])
14
15// The statuspane row for this session, or null outside Remote Control
16export function rcRow(bridge: string | undefined, surfaces: readonly RenderSurface[]) {
17 if (!bridge) return null
18 const remotes = surfaces.filter(s => s !== 'terminal').length
19 return {
20 id: ROW,
21 label: '📡 Remote Control',
22 text: remotes > 0 ? `${remotes} attached` : 'on',
23 ttl: TTL,
24 state: 'ok' as const,
25 }
26}
27
28// Caveman's flag file holds the mode; same sanitizing and whitelist as its own statusline script
29export function parseCavemanMode(flag: string | undefined) {
30 const mode = (flag ?? '').slice(0, 64).toLowerCase().replace(/[^a-z0-9-]/g, '')
31 return KNOWN_MODES.has(mode) && mode !== 'off' ? mode : null
32}
33
34async function cavemanFlagPath($: EngineInterface) {
35 const dir = (await $.env.get('CLAUDE_CONFIG_DIR')) ?? `${await $.env.get('HOME')}/.claude`
36 return `${dir}/.caveman-active`
37}
38
39async function readCavemanMode($: EngineInterface) {
40 const path = await cavemanFlagPath($)
41 // Refuse symlinks, as caveman does: stat without resolving reports a link as 'other'
42 const stat = await $.fs.stat(path).catch(() => undefined)
43 if (stat?.kind !== 'file') return null
44 return parseCavemanMode(await $.fs.read(path).catch(() => undefined))
45}
46
47// Bridge session id is set while the session is reachable from claude.ai / mobile
48async function refresh($: EngineInterface) {
49 const rc = rcRow(await $.env.get('CLAUDE_CODE_BRIDGE_SESSION_ID'), await $.session.surfaces())
50 await (rc ? $.statuspane.progress(rc) : $.statuspane.clear(ROW))
51 const mode = await readCavemanMode($)
52 await update($, cavemanMode, () => mode)
53}
54
55async function pickCavemanMode($: EngineInterface, mode: string) {
56 const { exitCode, stderr } = await $.process.run(['node', `${$.plugin.root}/bin/set-caveman-mode.js`, mode])
57 if (exitCode !== 0) $.ui.toast(`caveman: ${stderr.trim() || 'mode not changed'}`)
58 const now = await readCavemanMode($)
59 await update($, cavemanMode, () => now)
60 await update($, isCavemanMenuOpen, () => false)
61}
62
63export const register: Register = on => {
64 on('session.start', async ($, e, next) => {
65 await refresh($)
66 // Remote Control connects a moment after start, so poll rather than wait for the first turn
67 $.clock.every(15_000, () => void refresh($))
68 return next(e)
69 })
70 on('turn.start', async ($, e, next) => {
71 await refresh($)
72 return next(e)
73 })
74 on('session.attach', async ($, e, next) => {
75 const result = await next(e)
76 await refresh($)
77 return result
78 })
79 on('session.detach', async ($, e, next) => {
80 const result = await next(e)
81 await refresh($)
82 return result
83 })
84
85 // Caveman button under the statuspane card; pressed, it opens a mode list framed like statuspane's settings
86 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
87 if (e.props.hasSurvey || e.surface === 'desktop') return next(e)
88 const { Box, Button, Text } = $.ui.resolve(e)
89 // Keep what the plugins beneath drew; never hand the band itself back, or it stops redrawing
90 const below = await next(e).catch(() => null)
91 const mode = await read($, cavemanMode)
92 const setOpen = (open: boolean) => update($, isCavemanMenuOpen, () => open)
93
94 const ours = (await read($, isCavemanMenuOpen)) ? (
95 <Box justifyContent="flex-end" paddingRight={1}>
96 <Box flexDirection="column" borderStyle="round" borderColor="claude" paddingX={1}>
97 <Text color="claude" bold>Caveman mode</Text>
98 {MENU_MODES.map(m => (
99 // hover needs a keyed Box around the Button, or the whole band is refused
100 <Box key={`caveman-row-${m}`}>
101 <Button
102 key={`caveman-${m}`}
103 label={`${(mode ?? 'off') === m ? '●' : '○'} ${m}`}
104 plain
105 hover={{ color: 'claude' }}
106 onPress={() => pickCavemanMode($, m)}
107 />
108 </Box>
109 ))}
110 <Box justifyContent="flex-end">
111 <Button key="caveman-close" label="Done" variant="primary" onPress={() => setOpen(false)} />
112 </Box>
113 </Box>
114 </Box>
115 ) : (
116 // Own box, framed like the statuspane card above it; keyed so the Button's hover is allowed
117 <Box justifyContent="flex-end" paddingRight={1}>
118 <Box key="caveman-open-row" borderStyle="round" borderColor="claude" paddingX={1}>
119 <Button
120 key="caveman-open"
121 label={`🪨 caveman: ${mode ?? 'off'} ▾`}
122 plain
123 dimColor
124 hover={{ color: 'claude' }}
125 onPress={() => setOpen(true)}
126 />
127 </Box>
128 </Box>
129 )
130
131 return below ? (
132 <Box flexDirection="column">
133 {below}
134 {ours}
135 </Box>
136 ) : (
137 ours
138 )
139 })
140}
141types/index.d.ts 13 lines1/** Caveman's active mode as its flag file says, null when off. */
2export type SessionExtrasCavemanMode = string | null
3
4declare module 'claude-code' {
5 interface PluginState {
6 'session-extras': {
7 cavemanMode: SessionExtrasCavemanMode
8 /** Whether the caveman mode list is open. */
9 isCavemanMenuOpen: boolean
10 }
11 }
12}
13