A fluid side conversation over the main session, without interrupting it

A Claude Code mod for a fluid, multi-turn side conversation ("btw") that never interrupts the main session.
/btw-chat opens a pane with its own thread and a focused input; Escape closes it./btw-chat <question> opens the pane and asks right away./btw-chat clear resets the thread.Each message calls $.model.fork: it resends the main thread's last API request (model, system prompt, tools, messages) with one user message after it, so the API serves that prefix from its cache. That message carries the side thread so far plus the new question. Every tool is denied in the fork, and nothing goes back to the main thread.
/clear, there is nothing to fork: the pane shows no reply: nothing-to-fork.$.state for the session, not across sessions.At the prompt of a terminal session:
/plugin install btw-chat --marketplace Voltaire405/claude-mods
Answer y to add the marketplace, then pick a scope (user scope loads it in every session and repository). From a shell: claude plugin install btw-chat --marketplace Voltaire405/claude-mods.
claude plugin validate ./btw-chat
claude plugin test ./btw-chathooks/register.tsx 84 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { BtwMessage } from '../types'
5
6const PANE = 'btw'
7const thread = atom({ plugin: 'btw-chat', key: 'thread' } as const, [])
8const isThinking = atom({ plugin: 'btw-chat', key: 'isThinking' } as const, false)
9
10const openPane = ($: EngineInterface) =>
11 $.ui.open({ id: PANE, title: 'btw', focus: true, closeOnEscape: true })
12
13export const composeForkPrompt = (history: readonly BtwMessage[], question: string) =>
14 [
15 'This is a side conversation ("btw") the person is having with you while the main task goes on.',
16 'Answer from what you already know of this session. You cannot use tools here, and this exchange never enters the main thread.',
17 'Be brief and conversational.',
18 ...(history.length > 0
19 ? ['', 'The side conversation so far:', ...history.map(m => `${m.role === 'you' ? 'Person' : 'You'}: ${m.text}`)]
20 : []),
21 '',
22 `Person: ${question}`,
23 ].join('\n')
24
25const ask = async ($: EngineInterface, question: string) => {
26 const text = question.trim()
27 if (text === '' || (await read($, isThinking))) return
28
29 const history = await read($, thread)
30 await update($, thread, list => [...list, { role: 'you' as const, text }])
31 await update($, isThinking, () => true)
32
33 const reply = await $.model.fork({ prompt: composeForkPrompt(history, text) })
34 const answer = reply.isAnswered ? reply.text : `_(no reply: ${reply.reason})_`
35
36 await update($, thread, list => [...list, { role: 'claude' as const, text: answer }])
37 await update($, isThinking, () => false)
38}
39
40export const register: Register = on => {
41 on('session.start', async ($, e, next) => {
42 await $.command.register({
43 name: 'btw-chat',
44 description: 'Side conversation that never interrupts the main session (`clear` resets the thread)',
45 })
46 return next(e)
47 })
48
49 on('command.run', { command: 'btw-chat' }, async ($, e) => {
50 if (e.args.trim() === 'clear') {
51 await update($, thread, () => [])
52 return { text: 'btw thread cleared.' }
53 }
54 await openPane($)
55 if (e.args.trim() !== '') void ask($, e.args)
56 return { text: 'btw pane opened.' }
57 })
58
59 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e, next) => {
60 if (e.surface === 'mobile') return next(e)
61 const { Box, Text, Markdown, Input } = $.ui.resolve(e)
62 const list = await read($, thread)
63 const thinking = await read($, isThinking)
64
65 return (
66 <Box flexDirection="column">
67 {list.length === 0 && <Text dimColor>Ask anything; the main session keeps working.</Text>}
68 {list.map(m =>
69 m.role === 'you' ? <Text bold>› {m.text}</Text> : <Markdown text={m.text} />,
70 )}
71 {thinking && <Text dimColor>thinking…</Text>}
72 <Input
73 key="btw-input"
74 placeholder={thinking ? 'waiting for the reply…' : 'btw…'}
75 submitLabel="send"
76 value=""
77 autoFocus
78 onSubmit={(value: string) => void ask($, value)}
79 />
80 </Box>
81 )
82 })
83}
84types/index.d.ts 8 lines1export type BtwMessage = { role: 'you' | 'claude'; text: string }
2
3declare module 'claude-code' {
4 interface PluginState {
5 'btw-chat': { thread: BtwMessage[]; isThinking: boolean }
6 }
7}
8