While Claude works on your task, a sharp partner asks the questions that change the result. Answers reach Claude mid-turn. Brainstorm and side chat modes too.

While Claude works on your task, a sharp partner asks you the questions that would change the result. Your answers reach Claude in the middle of its turn.

claude plugin marketplace add theonly1me/claude-code-mods
claude plugin install grill@claude-code-mods
Register the marketplace once. Run /reload-plugins in an open session after installing. Requires Claude Code 2.1.289 or later; no build step or runtime dependencies.
When you send a task (a prompt of eight words or more), Grill reads it with your project's file list and writes three to five questions about requirements, edge cases, and trade-offs. They appear one at a time above the prompt:
▌ GRILL 1/4 Should an empty cart be deleted?
Decides whether empty carts exist in the database.
1: Delete it 2: Keep it 3: Type my own 4: Skip type the number, Enter
If Claude is still working, your answer goes straight into its turn, and Claude adjusts. If the turn already ended, Grill keeps your answers and offers to send them all as one prompt.
/grill grill: questions on each task (the default)./grill brainstorm: "what if" ideas on each task. Mark an idea "Worth doing" to send it to Claude./grill off: nothing starts by itself./grill ask or /grill ideas: run a round on your last task now./grill chat: a side chat with the helper model about anything. Claude does not see it until you press "Share with Claude", which puts short notes from the chat into your prompt for you to edit and send.While a question is open, a prompt that is only a number answers it instead of going to Claude. Any other prompt goes to Claude as usual.
In the chat pane, type in the box and press Enter to send. Tab moves to the "Share with Claude" button. Esc closes the chat, and /grill chat opens it again with the same messages.
Change these in /plugin:
startMode (grill): the mode for new sessions until you pick one with /grill.helperModel (claude-sonnet-5-5): the model that writes questions and chats with you.helperEffort (medium): how hard the helper model thinks (low, medium, or high).Each round and each chat reply is one helper-model call.
See the marketplace README for configuration, privacy, updates, and removal.
hooks/register.ts 136 lines1import type { EngineInterface, Register } from 'claude-code'
2
3import { CHAT_PANE, installChat } from './chat'
4import { helperFrom } from './grill/helper'
5import { CHAT_SYSTEM, chatPrompt, parseQuestions, ROUND_SYSTEM, roundPrompt, sharePrompt } from './grill/prompts'
6import { addChatMessage, configureGrill, grillView, setMode, setQuestions, startRound, takeJob } from './grill/state'
7import type { GrillMode } from './grill/types'
8import { installQuestions } from './questions'
9import { noop } from './shared/noop'
10
11const JOB_POLL_MS = 400
12const MODE_KEY = 'mode'
13const MODES: readonly GrillMode[] = ['grill', 'brainstorm', 'off']
14
15let isBusy = false
16let projectRoot = ''
17
18function modeFrom(value: unknown): GrillMode | undefined {
19 return MODES.find(mode => mode === value)
20}
21
22async function complete($: EngineInterface, options: { system: string; prompt: string; maxTokens: number }): Promise<string | undefined> {
23 const result = await $.model
24 .complete({
25 model: grillView().helper.helperModel,
26 effort: grillView().helper.helperEffort,
27 system: options.system,
28 prompt: options.prompt,
29 maxTokens: options.maxTokens,
30 timeoutMs: 40000,
31 })
32 .catch(() => undefined)
33 return result?.isAnswered ? result.text.trim() : undefined
34}
35
36async function runRound($: EngineInterface, options: { roundId: number }): Promise<void> {
37 const round = grillView().round
38 if (!round || round.id !== options.roundId) {
39 return
40 }
41 const listing = await $.process
42 .run(['git', 'ls-files'], { cwd: projectRoot, timeoutMs: 3000 })
43 .catch(() => undefined)
44 const files = (listing?.stdout ?? '').split('\n').slice(0, 200).join('\n')
45 const reply = await complete($, { system: ROUND_SYSTEM, prompt: roundPrompt({ mode: round.mode, task: round.prompt, files }), maxTokens: 1200 })
46 setQuestions({ roundId: round.id, questions: reply === undefined ? null : parseQuestions(reply) })
47 $.ui.invalidate('ui.render')
48}
49
50async function runChat($: EngineInterface): Promise<void> {
51 const { chat, lastTask } = grillView()
52 const reply = await complete($, { system: CHAT_SYSTEM, prompt: chatPrompt({ task: lastTask, chat }), maxTokens: 500 })
53 addChatMessage({ role: 'partner', text: reply ?? 'I could not answer just now. Say that again in a moment.' })
54 $.ui.invalidate('ui.render')
55}
56
57async function runShare($: EngineInterface): Promise<void> {
58 const { chat } = grillView()
59 if (chat.length === 0) {
60 $.ui.toast('The side chat is empty, so there is nothing to share')
61 return
62 }
63 const notes = await complete($, { system: CHAT_SYSTEM, prompt: sharePrompt(chat), maxTokens: 400 })
64 if (notes === undefined) {
65 $.ui.toast('The notes could not be written. Try Share again.')
66 return
67 }
68 await $.prompt.fill({ text: `Notes from my side chat:\n${notes}\n`, mode: 'insert' })
69 $.ui.toast('The notes are in your prompt. Edit them and press Enter to send.')
70}
71
72async function runJob($: EngineInterface): Promise<void> {
73 if (isBusy) {
74 return
75 }
76 const job = takeJob()
77 if (!job) {
78 return
79 }
80 isBusy = true
81 if (job.kind === 'round') {
82 await runRound($, { roundId: job.roundId }).catch(noop)
83 } else if (job.kind === 'chat') {
84 await runChat($).catch(noop)
85 } else {
86 await runShare($).catch(noop)
87 }
88 isBusy = false
89}
90
91export const register: Register = (on, options) => {
92 configureGrill(helperFrom(options))
93 installQuestions(on)
94 installChat(on)
95
96 on('session.start', async ($, e, next) => {
97 projectRoot = e.cwd
98 await $.command.register({
99 name: 'grill',
100 description: 'Grill: questions while Claude works. grill, brainstorm, or off sets the mode; ask or ideas runs a round now; chat opens a side chat.',
101 argumentHint: '[grill|brainstorm|off|ask|ideas|chat]',
102 immediate: true,
103 })
104 setMode(modeFrom(await $.store.get(MODE_KEY)) ?? modeFrom(options.startMode) ?? 'grill')
105 $.clock.every(JOB_POLL_MS, () => {
106 runJob($).catch(noop)
107 })
108 return next(e)
109 })
110
111 on('command.run', { command: 'grill' }, async ($, e) => {
112 const action = e.args.trim().toLowerCase()
113 const mode = modeFrom(action)
114 if (mode) {
115 setMode(mode)
116 await $.store.set(MODE_KEY, mode)
117 $.ui.invalidate('ui.render')
118 return { text: mode === 'off' ? 'Grill is off. /grill ask still runs a round on demand.' : `Grill mode: ${mode}. It starts on your next task prompt.` }
119 }
120 if (action === 'chat') {
121 await $.ui.open({ id: CHAT_PANE, title: 'Side chat', focus: true, closeOnEscape: true })
122 return { text: 'Side chat opened. Claude does not see it until you press Share with Claude.' }
123 }
124 if (action === 'ask' || action === 'ideas') {
125 const task = grillView().lastTask
126 if (task === '') {
127 return { text: 'There is no task yet. Send Claude a task first, then run /grill ask.' }
128 }
129 startRound({ prompt: task, mode: action === 'ask' ? 'grill' : 'brainstorm' })
130 $.ui.invalidate('ui.render')
131 return { text: 'Questions about your last task are on the way.' }
132 }
133 return { text: `Grill mode: ${grillView().mode}. Use /grill grill, brainstorm, off, ask, ideas, or chat.` }
134 })
135}
136hooks/chat.tsx 65 lines1import type { On } from 'claude-code'
2
3import { addChatMessage, grillView, queueShare } from './grill/state'
4
5export const CHAT_PANE = 'grill-chat'
6const PARTNER = '#facc15'
7const YOU = '#7dd3fc'
8
9export function installChat(on: On): void {
10 on('ui.render', { component: 'Pane', requestId: CHAT_PANE }, async ($, e) => {
11 if (e.surface === 'mobile') {
12 const { Text } = $.ui.resolve(e)
13 return <Text dimColor>The side chat needs the terminal or the desktop app.</Text>
14 }
15 const { Box, Text, Input, Button } = $.ui.resolve(e)
16 const { chat, isChatThinking, lastTask } = grillView()
17 const room = Math.max(4, (e.viewport?.rows ?? 30) - 10)
18 return (
19 <Box flexDirection="column">
20 <Text>
21 <Text bold>Side chat</Text>
22 <Text dimColor> Claude does not see this until you share it.</Text>
23 </Text>
24 {lastTask !== '' && <Text dimColor italic>{`Claude is on: ${lastTask.slice(0, 120)}`}</Text>}
25 <Box flexDirection="column" marginY={1}>
26 {chat.length === 0 && (
27 <Text dimColor>Think out loud about the task, a design, or anything else. Your partner answers here.</Text>
28 )}
29 {chat.slice(-room).map(message => (
30 <Text>
31 <Text bold color={message.role === 'you' ? YOU : PARTNER}>{message.role === 'you' ? 'you ' : 'partner '}</Text>
32 {message.text}
33 </Text>
34 ))}
35 {isChatThinking && <Text color={PARTNER}>partner thinking…</Text>}
36 </Box>
37 <Input
38 key="message"
39 autoFocus
40 placeholder="Say something and press Enter"
41 onSubmit={value => {
42 if (value.trim() === '') {
43 return
44 }
45 addChatMessage({ role: 'you', text: value.trim() })
46 $.ui.invalidate('ui.render')
47 }}
48 />
49 <Box flexDirection="row" columnGap={2} marginTop={1}>
50 <Button
51 key="share"
52 hotkey="s"
53 label="Share with Claude"
54 variant="primary"
55 onPress={() => {
56 queueShare()
57 $.ui.toast('Writing notes from this chat into your prompt')
58 }}
59 />
60 </Box>
61 </Box>
62 )
63 })
64}
65hooks/grill/helper.ts 16 lines1import type { ModelEffort, PluginOptions } from 'claude-code'
2
3export const DEFAULT_HELPER_MODEL = 'claude-sonnet-5-5'
4
5const EFFORTS: readonly ModelEffort[] = ['low', 'medium', 'high']
6
7export type Helper = { helperModel: string; helperEffort: ModelEffort }
8
9export function helperFrom(options: PluginOptions): Helper {
10 const model = options.helperModel
11 return {
12 helperModel: typeof model === 'string' && model.trim() !== '' ? model.trim() : DEFAULT_HELPER_MODEL,
13 helperEffort: EFFORTS.find(effort => effort === options.helperEffort) ?? 'medium',
14 }
15}
16hooks/grill/prompts.ts 106 lines1import type { Answer, ChatMessage, Question, RoundMode } from './types'
2
3export const ROUND_SYSTEM =
4 'You are a sharp senior engineer pairing with a developer. An AI agent has just started on their request. You ask the developer the few questions whose answers would change the result the most. Reply with one JSON object and nothing else.'
5
6export const CHAT_SYSTEM =
7 'You are a sharp, friendly thinking partner for a developer while their AI coding agent works. Keep replies under 120 words, plain and specific. Push back when an idea is weak, and end with a question when one would help.'
8
9export function roundPrompt(options: { mode: RoundMode; task: string; files: string }): string {
10 const context = [`The developer asked the agent: ${options.task.slice(0, 3000)}`, '', 'Files in the project (partial):', options.files || '(unknown)', '']
11 if (options.mode === 'brainstorm') {
12 return [
13 ...context,
14 'Suggest 3 to 5 ideas the developer may not have considered for this task: better approaches, risks, or extras worth doing now.',
15 'Each idea starts with "What if". Return JSON: {"questions": [{"text": "What if ...?", "why": "one sentence on the payoff", "options": []}]}',
16 ].join('\n')
17 }
18 return [
19 ...context,
20 'Ask 3 to 5 questions about requirements, edge cases, scope, and trade-offs that the request leaves open.',
21 'Never ask what the code can answer. Each question must be specific to this request, never generic.',
22 'Give 2 or 3 likely answers per question, at most 6 words each, that do not overlap.',
23 'Return JSON: {"questions": [{"text": "the question", "why": "one sentence on what the answer changes", "options": ["answer", "answer"]}]}',
24 ].join('\n')
25}
26
27function isRecord(value: unknown): value is Record<string, unknown> {
28 return typeof value === 'object' && value !== null && !Array.isArray(value)
29}
30
31function clean(options: { value: unknown; limit: number }): string {
32 return typeof options.value === 'string' ? options.value.replace(/\s+/g, ' ').trim().slice(0, options.limit) : ''
33}
34
35export function parseQuestions(reply: string): Question[] | null {
36 const start = reply.indexOf('{')
37 const end = reply.lastIndexOf('}')
38 if (start < 0 || end <= start) {
39 return null
40 }
41 let parsed: unknown
42 try {
43 parsed = JSON.parse(reply.slice(start, end + 1))
44 } catch {
45 return null
46 }
47 if (!isRecord(parsed) || !Array.isArray(parsed.questions)) {
48 return null
49 }
50 return parsed.questions
51 .filter(isRecord)
52 .map(question => ({
53 text: clean({ value: question.text, limit: 220 }),
54 why: clean({ value: question.why, limit: 200 }),
55 options: Array.isArray(question.options)
56 ? question.options.map(option => clean({ value: option, limit: 48 })).filter(Boolean).slice(0, 3)
57 : [],
58 }))
59 .filter(question => question.text !== '')
60 .slice(0, 5)
61}
62
63export function answerNote(answer: Answer): string {
64 if (answer.mode === 'brainstorm') {
65 return [
66 '[Grill] While you work, the user marked this idea as worth doing for the current task:',
67 answer.question,
68 'Fold it in if it fits the request. If it does not fit, say why in your final answer.',
69 ].join('\n')
70 }
71 return [
72 '[Grill] While you work, the user answered a clarifying question about the current task.',
73 `Q: ${answer.question}`,
74 `A: ${answer.answer}`,
75 'If this changes your plan, adjust your work now. Do not reply to this note on its own.',
76 ].join('\n')
77}
78
79export function answersMessage(answers: readonly Answer[]): string {
80 const lines = answers.map(answer =>
81 answer.mode === 'brainstorm' ? `- Idea to include: ${answer.question}` : `- ${answer.question} ${answer.answer}`,
82 )
83 return ['Here are my answers to the grill questions. Please apply them to the work you just did:', ...lines].join('\n')
84}
85
86export function chatPrompt(options: { task: string; chat: readonly ChatMessage[] }): string {
87 const transcript = options.chat.map(message => `${message.role === 'you' ? 'Developer' : 'You'}: ${message.text}`).join('\n')
88 return [
89 options.task ? `The developer's current task for their agent: ${options.task.slice(0, 1500)}` : '',
90 'Conversation so far:',
91 transcript,
92 'Reply as "You" with your next message only.',
93 ]
94 .filter(Boolean)
95 .join('\n')
96}
97
98export function sharePrompt(chat: readonly ChatMessage[]): string {
99 return [
100 'Summarize this side conversation into notes the developer can hand to their AI coding agent.',
101 'Write 2 to 5 short bullet points with the decisions and ideas worth acting on. No preamble.',
102 '',
103 chat.map(message => `${message.role === 'you' ? 'Developer' : 'Partner'}: ${message.text}`).join('\n'),
104 ].join('\n')
105}
106hooks/grill/state.ts 150 lines1import { DEFAULT_HELPER_MODEL } from './helper'
2import type { Helper } from './helper'
3import type { Answer, ChatMessage, GrillMode, Job, Question, Round, RoundMode } from './types'
4
5const MIN_WORDS = 8
6const MAX_CHAT = 40
7
8type Grill = {
9 mode: GrillMode
10 round: Round | undefined
11 isWorking: boolean
12 jobs: Job[]
13 chat: ChatMessage[]
14 isChatThinking: boolean
15 lastTask: string
16 helper: Helper
17 nextRoundId: number
18}
19
20const grill: Grill = {
21 mode: 'grill',
22 round: undefined,
23 isWorking: false,
24 jobs: [],
25 chat: [],
26 isChatThinking: false,
27 lastTask: '',
28 helper: { helperModel: DEFAULT_HELPER_MODEL, helperEffort: 'medium' },
29 nextRoundId: 1,
30}
31
32export function grillView(): Readonly<Grill> {
33 return grill
34}
35
36export function configureGrill(helper: Helper): void {
37 grill.helper = helper
38}
39
40export function setMode(mode: GrillMode): void {
41 grill.mode = mode
42}
43
44export function setWorking(isWorking: boolean): void {
45 grill.isWorking = isWorking
46}
47
48export function isTaskPrompt(text: string): boolean {
49 const trimmed = text.trim()
50 return !trimmed.startsWith('/') && trimmed.split(/\s+/).filter(Boolean).length >= MIN_WORDS
51}
52
53export function rememberTask(text: string): void {
54 grill.lastTask = text
55}
56
57export function startRound(options: { prompt: string; mode: RoundMode }): Round {
58 const round: Round = {
59 id: grill.nextRoundId,
60 mode: options.mode,
61 prompt: options.prompt,
62 status: 'thinking',
63 questions: [],
64 index: 0,
65 answers: [],
66 }
67 grill.nextRoundId += 1
68 grill.round = round
69 grill.lastTask = options.prompt
70 grill.jobs = [...grill.jobs.filter(job => job.kind !== 'round'), { kind: 'round', roundId: round.id }]
71 return round
72}
73
74export function setQuestions(options: { roundId: number; questions: Question[] | null }): void {
75 const round = grill.round
76 if (!round || round.id !== options.roundId) {
77 return
78 }
79 round.questions = options.questions ?? []
80 round.status = options.questions && options.questions.length > 0 ? 'ready' : 'failed'
81}
82
83export function currentQuestion(): Question | undefined {
84 const round = grill.round
85 return round && round.status === 'ready' ? round.questions[round.index] : undefined
86}
87
88function advance(round: Round): void {
89 round.index += 1
90 if (round.index >= round.questions.length) {
91 round.status = 'done'
92 }
93}
94
95export function answerCurrent(options: { answer: string; isDelivered: boolean }): Answer | undefined {
96 const round = grill.round
97 const question = currentQuestion()
98 if (!round || !question) {
99 return undefined
100 }
101 const answer: Answer = { question: question.text, answer: options.answer, mode: round.mode, isDelivered: options.isDelivered }
102 round.answers.push(answer)
103 advance(round)
104 return answer
105}
106
107export function skipCurrent(): void {
108 if (grill.round && currentQuestion()) {
109 advance(grill.round)
110 }
111}
112
113export function dismissRound(): void {
114 grill.round = undefined
115}
116
117export function undeliveredAnswers(): Answer[] {
118 return grill.round ? grill.round.answers.filter(answer => !answer.isDelivered) : []
119}
120
121export function markUndelivered(answer: Answer): void {
122 answer.isDelivered = false
123}
124
125export function markAllDelivered(): void {
126 grill.round?.answers.forEach(answer => {
127 answer.isDelivered = true
128 })
129}
130
131export function addChatMessage(message: ChatMessage): void {
132 grill.chat = [...grill.chat, message].slice(-MAX_CHAT)
133 if (message.role === 'you') {
134 grill.isChatThinking = true
135 grill.jobs = [...grill.jobs, { kind: 'chat' }]
136 } else {
137 grill.isChatThinking = false
138 }
139}
140
141export function queueShare(): void {
142 grill.jobs = [...grill.jobs, { kind: 'share' }]
143}
144
145export function takeJob(): Job | undefined {
146 const [job, ...rest] = grill.jobs
147 grill.jobs = rest
148 return job
149}
150hooks/grill/types.ts 24 lines1export type GrillMode = 'grill' | 'brainstorm' | 'off'
2
3export type RoundMode = 'grill' | 'brainstorm'
4
5export type Question = { text: string; why: string; options: string[] }
6
7export type Answer = { question: string; answer: string; mode: RoundMode; isDelivered: boolean }
8
9export type RoundStatus = 'thinking' | 'ready' | 'failed' | 'done'
10
11export type Round = {
12 id: number
13 mode: RoundMode
14 prompt: string
15 status: RoundStatus
16 questions: Question[]
17 index: number
18 answers: Answer[]
19}
20
21export type ChatMessage = { role: 'you' | 'partner'; text: string }
22
23export type Job = { kind: 'round'; roundId: number } | { kind: 'chat' } | { kind: 'share' }
24hooks/questions.tsx 198 lines1import type { EngineInterface, On } from 'claude-code'
2
3import { choiceForDigit, choicesFor } from './grill/choices'
4import type { Choice, LabeledChoice } from './grill/choices'
5import { answerNote, answersMessage } from './grill/prompts'
6import {
7 answerCurrent,
8 currentQuestion,
9 dismissRound,
10 grillView,
11 isTaskPrompt,
12 markAllDelivered,
13 markUndelivered,
14 rememberTask,
15 setWorking,
16 skipCurrent,
17 startRound,
18 undeliveredAnswers,
19} from './grill/state'
20
21export const ANSWER_PANE = 'grill-answer'
22const FLAME = '#ff7a45'
23const IDEA = '#facc15'
24
25function currentChoices(): LabeledChoice[] {
26 const { round, isWorking } = grillView()
27 return choicesFor({ round, question: currentQuestion(), lateCount: undeliveredAnswers().length, isWorking })
28}
29
30function keyFor(entry: LabeledChoice): string {
31 return entry.choice.kind === 'answer' ? `option-${entry.digit}` : entry.choice.kind
32}
33
34function takeAnswersToSend(): string | undefined {
35 const answers = undeliveredAnswers()
36 markAllDelivered()
37 dismissRound()
38 return answers.length > 0 ? answersMessage(answers) : undefined
39}
40
41async function deliver($: EngineInterface, options: { answer: string }): Promise<void> {
42 const answer = answerCurrent({ answer: options.answer, isDelivered: grillView().isWorking })
43 if (answer?.isDelivered) {
44 const appended = await $.session
45 .append({ message: { type: 'user', content: [{ type: 'text', text: answerNote(answer) }] } })
46 .catch(() => undefined)
47 if (appended === undefined || appended.deny !== undefined) {
48 markUndelivered(answer)
49 $.ui.toast('Claude could not take this mid-turn. Send it when the turn ends.')
50 } else {
51 $.ui.toast(answer.mode === 'brainstorm' ? 'Idea sent to Claude' : 'Answer sent to Claude')
52 }
53 }
54 $.ui.invalidate('ui.render')
55}
56
57async function perform($: EngineInterface, options: { choice: Choice }): Promise<void> {
58 const { choice } = options
59 if (choice.kind === 'answer') {
60 await deliver($, { answer: choice.text })
61 return
62 }
63 if (choice.kind === 'own') {
64 await $.ui.open({ id: ANSWER_PANE, title: 'Your answer', focus: true, closeOnEscape: true, rows: 5 })
65 return
66 }
67 if (choice.kind === 'send') {
68 const message = takeAnswersToSend()
69 $.ui.invalidate('ui.render')
70 if (message !== undefined) {
71 await $.prompt.submit({ text: message })
72 }
73 return
74 }
75 if (choice.kind === 'skip') {
76 skipCurrent()
77 } else {
78 dismissRound()
79 }
80 $.ui.invalidate('ui.render')
81}
82
83export function installQuestions(on: On): void {
84 on('prompt.submit', async ($, e, next) => {
85 const picked = e.origin.kind === 'composer' ? choiceForDigit({ choices: currentChoices(), text: e.text }) : undefined
86 if (picked?.choice.kind === 'send') {
87 const message = takeAnswersToSend()
88 $.ui.invalidate('ui.render')
89 return message === undefined ? { drop: `Grill: ${picked.label}` } : next({ ...e, text: message })
90 }
91 if (picked) {
92 await perform($, { choice: picked.choice })
93 return { drop: `Grill: ${picked.label}` }
94 }
95 const mode = grillView().mode
96 const isNewTask = e.origin.kind === 'composer' && e.turnId === undefined && isTaskPrompt(e.text)
97 if (isNewTask) {
98 rememberTask(e.text)
99 }
100 if (isNewTask && (mode === 'grill' || mode === 'brainstorm')) {
101 startRound({ prompt: e.text, mode })
102 $.ui.invalidate('ui.render')
103 }
104 return next(e)
105 })
106
107 on('turn.start', ($, e, next) => {
108 setWorking(true)
109 return next(e)
110 })
111
112 on('turn.complete', ($, e, next) => {
113 if (e.agentId === undefined) {
114 setWorking(false)
115 $.ui.invalidate('ui.render')
116 }
117 return next(e)
118 })
119
120 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
121 const below = await next(e)
122 const { round } = grillView()
123 if (!round || e.props.hasSurvey) {
124 return below
125 }
126 const { Box, Text, Button } = $.ui.resolve(e)
127 const question = currentQuestion()
128 const isIdea = round.mode === 'brainstorm'
129 const color = isIdea ? IDEA : FLAME
130 const choices = currentChoices()
131 const buttons = choices.map(entry => (
132 <Button key={keyFor(entry)} plain hotkey={entry.digit} label={entry.label} onPress={() => perform($, { choice: entry.choice })} />
133 ))
134 let header
135 if (round.status === 'thinking') {
136 header = <Text color={color}>{`▌ ${isIdea ? 'Brainstorm' : 'Grill'} is reading your request while Claude starts…`}</Text>
137 } else if (question) {
138 header = (
139 <Box flexDirection="column">
140 <Text>
141 <Text color={color} bold>{`▌ ${isIdea ? 'IDEA' : 'GRILL'} `}</Text>
142 <Text dimColor>{`${round.index + 1}/${round.questions.length} `}</Text>
143 <Text bold>{question.text}</Text>
144 </Text>
145 {question.why !== '' && <Text dimColor italic>{` ${question.why}`}</Text>}
146 </Box>
147 )
148 } else if (choices.length > 0) {
149 header = <Text color={color}>{`▌ Claude finished before reading ${undeliveredAnswers().length} of your answers.`}</Text>
150 } else {
151 return below
152 }
153 return (
154 <Box flexDirection="column">
155 {header}
156 {choices.length > 0 && (
157 <Box flexDirection="row" flexWrap="wrap" columnGap={2}>
158 {buttons}
159 <Text dimColor>type the number, Enter</Text>
160 </Box>
161 )}
162 {below}
163 </Box>
164 )
165 })
166
167 on('ui.render', { component: 'Pane', requestId: ANSWER_PANE }, async ($, e) => {
168 if (e.surface === 'mobile') {
169 const { Text } = $.ui.resolve(e)
170 return <Text dimColor>Answer from the terminal or the desktop app.</Text>
171 }
172 const { Box, Text, Input } = $.ui.resolve(e)
173 const question = currentQuestion()
174 if (!question) {
175 return <Text dimColor>There is no open question.</Text>
176 }
177 return (
178 <Box flexDirection="column">
179 <Text>
180 <Text color={FLAME} bold>GRILL </Text>
181 <Text bold>{question.text}</Text>
182 </Text>
183 <Input
184 key="answer"
185 autoFocus
186 placeholder="Type your answer and press Enter"
187 onSubmit={async value => {
188 if (value.trim() !== '') {
189 await deliver($, { answer: value.trim() })
190 }
191 await $.ui.close({ id: ANSWER_PANE })
192 }}
193 />
194 </Box>
195 )
196 })
197}
198hooks/shared/noop.ts 4 lines1export function noop(): undefined {
2 return undefined
3}
4hooks/grill/choices.ts 52 lines1import type { Question, Round } from './types'
2
3export type Choice =
4 | { kind: 'answer'; text: string }
5 | { kind: 'own' }
6 | { kind: 'skip' }
7 | { kind: 'send' }
8 | { kind: 'dismiss' }
9
10export type LabeledChoice = { digit: string; label: string; choice: Choice }
11
12export function choicesFor(options: {
13 round: Round | undefined
14 question: Question | undefined
15 lateCount: number
16 isWorking: boolean
17}): LabeledChoice[] {
18 const { round, question } = options
19 if (!round) {
20 return []
21 }
22 if (question) {
23 const isIdea = round.mode === 'brainstorm'
24 const answers: Choice[] = isIdea
25 ? [{ kind: 'answer', text: 'Worth doing' }, { kind: 'skip' }]
26 : question.options.map(text => ({ kind: 'answer', text }))
27 const labels = isIdea ? ['Worth doing', 'Not now'] : question.options
28 const extras: { label: string; choice: Choice }[] = isIdea
29 ? [{ label: 'Skip', choice: { kind: 'skip' } }]
30 : [
31 { label: 'Type my own', choice: { kind: 'own' } },
32 { label: 'Skip', choice: { kind: 'skip' } },
33 ]
34 return [
35 ...answers.map((choice, index) => ({ label: labels[index] ?? '', choice })),
36 ...extras,
37 ].map((entry, index) => ({ digit: String(index + 1), ...entry }))
38 }
39 if (round.status === 'done' && options.lateCount > 0 && !options.isWorking) {
40 return [
41 { digit: '1', label: 'Send them now', choice: { kind: 'send' } },
42 { digit: '2', label: 'Dismiss', choice: { kind: 'dismiss' } },
43 ]
44 }
45 return []
46}
47
48export function choiceForDigit(options: { choices: readonly LabeledChoice[]; text: string }): LabeledChoice | undefined {
49 const typed = options.text.trim()
50 return /^[1-9]$/.test(typed) ? options.choices.find(entry => entry.digit === typed) : undefined
51}
52