SLOPSHOPPER

side-question

/btw で、作業を止めずに会話の続きとして脇道の質問をし、答えをペインに出す。答えは会話に残らない

newpanecommandmodel
v0.1.0MITupdated 2026-10-09yut0takagi/mods/side-question
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · side-question
│ ┃ BTW ✕ › fix the failing auth test and add an audit log call │ ┃ 脇道の質問を入力して Enter ⏎ 聞く │ ┃ 答えはここに出ます。会話には残りません。 ⏺ Read(src/auth.ts) │ ⎿ 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 │ │ › /btw │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · BTW
脇道の質問を入力して Enter ⏎ 聞く 答えはここに出ます。会話には残りません。
README

mods

English | 日本語

A marketplace of mods for Claude Code. A mod is a plugin that adds features to Claude Code through function hooks.

ModWhat it does
branch-watchStops commits on a branch another session switched to, and lists each session's worktree and branch
push-gateHolds git push and similar commands until a person approves
auto-worktreeCreates a worktree on the first prompt and starts the work there
session-diffShows the files this session edited and their diffs
pr-paneShows open pull requests and their CI results
side-question/btw asks a side question without stopping the work
done-chimePlays a sound when a long turn ends and when Claude waits for permission
ja-checkTells you when a reply starts in English
usage-meterShows each turn's token counts and prompt cache rate

Install

claude plugin marketplace add yut0takagi/mods
claude plugin install branch-watch@yut0takagi-mods   # one line per mod you want

Developed with Claude Code 2.1.289. Mods are an early access feature, so a Claude Code update may break them. The VS Code and Cursor extension draws no mod UI, so some mods are used differently there.

Docs

To change a mod, read CONTRIBUTING. Changes are listed in the CHANGELOG. Report security problems as described in SECURITY.

License

MIT

Source 3 files
hooks/register.tsx 94 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import { KEEP, outcomeOf, promptOf, replyOf, USAGE_TEXT } from './fork'
5import type { Outcome } from './fork'
6
7const PANE = 'side-question'
8const entries = atom({ plugin: 'side-question', key: 'entries' } as const, [])
9
10/** 会話の続きとして 1 問だけ聞き、答えをペインに出す。答えは会話には足さない。答えか理由を返す(空の質問なら undefined)。 */
11async function ask($: EngineInterface, question: string): Promise<Outcome | undefined> {
12  const text = question.trim()
13
14  if (text === '') {
15    return undefined
16  }
17
18  const id = await $.clock.now()
19  await update($, entries, list => [{ id, question: text }, ...list].slice(0, KEEP))
20
21  const result = await $.model.fork({ prompt: promptOf(text) })
22  const outcome = outcomeOf(result)
23  await update($, entries, list => list.map(entry => (entry.id === id ? { ...entry, ...outcome } : entry)))
24
25  return outcome
26}
27
28export const register: Register = on => {
29  on('session.start', async ($, e, next) => {
30    // 画面がない(VS Code の拡張機能など)と、答えは返答に出て会話に残る。説明もそれに合わせる
31    const isShown = (await $.session.surfaces().catch(() => undefined))?.length !== 0
32    await $.command.register({
33      name: 'btw',
34      description: isShown
35        ? '作業を止めずに脇道の質問をする。答えはペインに出て、会話には残らない'
36        : '作業を止めずに脇道の質問をする。答えは返答に出て、会話に残る',
37      argumentHint: '[質問]',
38      immediate: true,
39    })
40
41    return next(e)
42  })
43
44  on('command.run', { command: 'btw' }, async ($, e) => {
45    const question = e.args.trim()
46
47    // 画面がないセッション(VS Code・Cursor の拡張機能、-p)では、ペインが描かれない
48    // (ui.open は isPlaced: true を返すので、それでは見分けられない)。答えを待って返答の文で返す。
49    // 返答の文はモデルも読む行として会話に残る。人にだけ見せる道(ui.log、session.append の system)は
50    // 拡張機能では描かれないので使えない(2.1.289 の webview は ui_log を扱わず、プラグインの notice が
51    // なる level: info の informational 行も描かない。コードを読んだ結果で、実機では確かめていない)
52    if ((await $.session.surfaces()).length === 0) {
53      const outcome = await ask($, question)
54
55      return { text: outcome === undefined ? USAGE_TEXT : replyOf(question, outcome) }
56    }
57
58    await $.ui.open({ id: PANE, title: 'BTW', ...(question === '' && { focus: true as const }) })
59
60    if (question !== '') {
61      await ask($, question)
62    }
63
64    // 会話に何も残さないよう、出力の文は返さない
65    return {}
66  })
67
68  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
69    const table = $.ui.resolve(e)
70    const { Box, Markdown, Text } = table
71    const list = await read($, entries)
72
73    return (
74      <Box flexDirection="column">
75        {'Input' in table && (
76          <table.Input key="ask" placeholder="脇道の質問を入力して Enter" submitLabel="聞く" value="" autoFocus onSubmit={value => ask($, value)} />
77        )}
78        {list.length === 0 && <Text dimColor>答えはここに出ます。会話には残りません。</Text>}
79        {list.map(entry => (
80          <Box key={`q-${entry.id}`} flexDirection="column" marginTop={1}>
81            <Text bold>Q. {entry.question}</Text>
82            {entry.answer !== undefined && <Markdown text={entry.answer} />}
83            {entry.error !== undefined && <Text color="red">{entry.error}</Text>}
84            {entry.answer === undefined && entry.error === undefined && <Text dimColor>考え中…</Text>}
85            {entry.cacheRate !== undefined && (
86              <Text dimColor>キャッシュから読んだ入力 {Math.round(entry.cacheRate * 100)}%</Text>
87            )}
88          </Box>
89        ))}
90      </Box>
91    )
92  })
93}
94
hooks/fork.ts 60 lines
1/** 脇道の質問の組み立てと、fork の結果の読み方。$ に触れない部分。 */
2
3import type { ModelForkResult } from 'claude-code'
4
5import type { Entry } from '../types'
6
7// 質問の言語で答えてもらうため、前置きは英語にしておく
8const PREFIX = 'This is a side question, separate from the current task. Do not continue the task; answer only this question, briefly.'
9
10export const promptOf = (question: string) => `${PREFIX}\n\n${question}`
11
12/** ペインに残す数。古いものから消す。 */
13export const KEEP = 10
14
15/** Markdown に渡せる長さ。 */
16const MAX_TEXT = 10_000
17
18/** 1 問の結末。答えか、答えられなかった理由のどちらか。 */
19export type Outcome = Pick<Entry, 'answer' | 'error' | 'cacheRate'>
20
21/** fork の結果を、ペインに出す答えか理由にする。 */
22export function outcomeOf(result: ModelForkResult): Outcome {
23  if (result.isAnswered) {
24    const { input_tokens, cache_read_input_tokens, cache_creation_input_tokens } = result.usage
25    const total = input_tokens + cache_read_input_tokens + cache_creation_input_tokens
26
27    return {
28      answer: result.text.slice(0, MAX_TEXT),
29      ...(total > 0 && { cacheRate: cache_read_input_tokens / total }),
30    }
31  }
32
33  switch (result.reason) {
34    case 'nothing-to-fork':
35      return { error: 'まだ会話がありません。最初の返答のあとで聞けます。' }
36    case 'api-error':
37      return { error: `API エラーで答えられませんでした(${result.status ?? '?'})。` }
38    case 'empty-reply':
39      return { error: '答えが空でした。' }
40    default:
41      return { error: '中断されました。' }
42  }
43}
44
45/** 画面がないセッションで /btw に質問がなかったときの返答。入力欄のあるペインが出ないので、書き方を返す。 */
46export const USAGE_TEXT = '質問は /btw のあとに続けて書いてください。例: /btw なんでこの設計?'
47
48/** 画面がないセッションで、ペインの代わりにコマンドの返答として出す文。この文は会話に残り、モデルも読む。 */
49export function replyOf(question: string, outcome: Outcome): string {
50  const lines = [`Q. ${question}`, '', outcome.answer ?? outcome.error ?? '']
51
52  if (outcome.cacheRate !== undefined) {
53    lines.push('', `キャッシュから読んだ入力 ${Math.round(outcome.cacheRate * 100)}%`)
54  }
55
56  lines.push('', '(画面のないセッションなのでペインの代わりにここへ出しました。この答えは会話に残ります)')
57
58  return lines.join('\n')
59}
60
types/index.d.ts 17 lines
1/** 脇道の質問 1 つと、その答え。答えも理由もないあいだは考え中。 */
2export type Entry = {
3  id: number
4  question: string
5  answer?: string
6  /** 答えられなかった理由。 */
7  error?: string
8  /** プロンプトキャッシュから読んだ入力の割合(0〜1)。 */
9  cacheRate?: number
10}
11
12declare module 'claude-code' {
13  interface PluginState {
14    'side-question': { entries: Entry[] }
15  }
16}
17