Skip to content

Instantly share code, notes, and snippets.

@sorrycc
Created March 12, 2026 12:14
Show Gist options
  • Select an option

  • Save sorrycc/cca0b238a15027c3572f19b353a00853 to your computer and use it in GitHub Desktop.

Select an option

Save sorrycc/cca0b238a15027c3572f19b353a00853 to your computer and use it in GitHub Desktop.
Claude Code /btw command - Quick Side Question implementation details

/btw — Quick Side Question Command Implementation

Overview

/btw is a slash command that lets users ask a quick side question without interrupting the main conversation flow. It forks a lightweight, single-turn API call using the existing conversation context but with no tools available.

The response is displayed inline and can be dismissed with Space/Enter/Escape to resume the main conversation.


Architecture

User types "/btw how do I format a date?"
        |
        v
  [Command Registration] -- checks feature flag H96()
        |
        v
  [Entry Point: ubY()] -- validates input, increments btwUseCount
        |
        v
  [React Component: IbY] -- renders UI (spinner, question, response)
        |
        v
  [Context Gathering: xbY()] -- collects system prompt, user context, conversation messages
        |
        v
  [API Call: C04()] -- single-turn, no-tools, forked API call
        |
        v
  [Display Response] -- markdown-rendered answer, dismissible with Space/Enter/Escape

1. Feature Gate

The entire feature is behind a feature flag. All UI elements and the command registration check this before enabling.

function isBtwEnabled() {
  return getFeatureFlag("tengu_marble_whisper2", false);
}

Source (minified):

// cli.js:321710
function H96() {
  return W8("tengu_marble_whisper2", !1);
}

2. Regex Pattern

Used to detect and match the /btw command at the start of user input.

const BTW_REGEX = /^\/btw\b/gi;

Source (minified):

// cli.js:321761
et9 = /^\/btw\b/gi;

3. Command Registration

const btwCommand = {
  type: "local-jsx",
  name: "btw",
  description: "Ask a quick side question without interrupting the main conversation",
  isEnabled: () => isBtwEnabled(),
  isHidden: false,
  immediate: true,           // executes immediately, no confirmation step
  argumentHint: "<question>",
  load: () => Promise.resolve().then(() => loadBtwModule()),
  userFacingName() {
    return "btw";
  },
};

Source (minified):

// cli.js:443857-443871
mbY = {
  type: "local-jsx",
  name: "btw",
  description:
    "Ask a quick side question without interrupting the main conversation",
  isEnabled: () => H96(),
  isHidden: !1,
  immediate: !0,
  argumentHint: "<question>",
  load: () => Promise.resolve().then(() => (I4q(), C4q)),
  userFacingName() {
    return "btw";
  },
};

4. Entry Point

The command handler validates input, increments usage counter, and renders the component.

async function handleBtwCommand(emit, context, rawArgs) {
  const question = rawArgs?.trim();

  // Show usage if no question provided
  if (!question) {
    emit("Usage: /btw <your question>", { display: "system" });
    return null;
  }

  // Increment usage count (used to suppress tooltip after first use)
  updateState((state) => ({ ...state, btwUseCount: state.btwUseCount + 1 }));

  // Render the side question component
  return React.createElement(BtwComponent, {
    question,
    context,
    onDone: emit,
  });
}

Source (minified):

// cli.js:443830-443838
async function ubY(A, q, K) {
  let Y = K?.trim();
  if (!Y)
    return (A("Usage: /btw <your question>", { display: "system" }), null);
  return (
    i1((z) => ({ ...z, btwUseCount: z.btwUseCount + 1 })),
    B2.createElement(IbY, { question: Y, context: q, onDone: A })
  );
}

5. React UI Component

An Ink (React for CLI) component that manages the full lifecycle: spinner -> fetch -> display -> dismiss.

function BtwComponent({ question, context, onDone }) {
  const [response, setResponse] = useState(null);
  const [error, setError] = useState(null);
  const [frame, setFrame] = useState(0);

  // Animate spinner at 80ms intervals while waiting
  useInterval(() => setFrame((f) => f + 1), response || error ? null : 80);

  // Dismiss on Space, Enter, or Escape
  useInput((input, key) => {
    if (key.escape || key.return || input === " ") {
      onDone(undefined, { display: "skip" });
    }
  });

  // Fetch answer on mount
  useEffect(() => {
    const controller = new AbortController();
    (async () => {
      try {
        const cacheSafeParams = await gatherContext(context);
        const result = await callSideQuestion({ question, cacheSafeParams });
        if (!controller.signal.aborted) {
          if (result.response) setResponse(result.response);
          else setError("No response received");
        }
      } catch (err) {
        if (!controller.signal.aborted) setError(err.message || "Failed to get response");
      }
    })();
    return () => controller.abort();
  }, [question, context]);

  return (
    <Box flexDirection="column" paddingLeft={2} marginTop={1}>
      {/* Question header */}
      <Box>
        <Text color="warning" bold>/btw </Text>
        <Text dimColor>{question}</Text>
      </Box>

      {/* Response area */}
      <Box marginTop={1} marginLeft={2}>
        {error ? (
          <Text color="error">{error}</Text>
        ) : response ? (
          <MarkdownRenderer>{response}</MarkdownRenderer>
        ) : (
          <Box>
            <Spinner frame={frame} messageColor="warning" />
            <Text color="warning">Answering...</Text>
          </Box>
        )}
      </Box>

      {/* Dismiss hint */}
      {(response || error) && (
        <Box marginTop={1}>
          <Text dimColor>Press Space, Enter, or Escape to dismiss</Text>
        </Box>
      )}
    </Box>
  );
}

Source (minified):

// cli.js:443685-443803
function IbY(A) {
  let q = e(21),
    { question: K, context: Y, onDone: z } = A,
    [_, w] = vT6.useState(null),
    [O, $] = vT6.useState(null),
    [H, j] = vT6.useState(0),
    J;
  if (q[0] === Symbol.for("react.memo_cache_sentinel"))
    ((J = () => j(bbY)), (q[0] = J));
  else J = q[0];
  aD(J, _ || O ? null : 80);
  let M;
  if (q[1] !== z)
    ((M = (v, N) => {
      if (N.escape || N.return || v === " ") z(void 0, { display: "skip" });
    }),
      (q[1] = z),
      (q[2] = M));
  else M = q[2];
  KA(M);
  let D, X;
  if (q[3] !== Y || q[4] !== K)
    ((D = () => {
      let v = O3();
      return (
        (async function () {
          try {
            let L = await xbY(Y),
              h = await C04({ question: K, cacheSafeParams: L });
            if (!v.signal.aborted)
              if (h.response) w(h.response);
              else $("No response received");
          } catch (L) {
            let h = L;
            if (!v.signal.aborted) $(h.message || "Failed to get response");
          }
        })(),
        () => {
          v.abort();
        }
      );
    }),
      (X = [K, Y]),
      (q[3] = Y),
      (q[4] = K),
      (q[5] = D),
      (q[6] = X));
  else ((D = q[5]), (X = q[6]));
  vT6.useEffect(D, X);
  let P;
  if (q[7] === Symbol.for("react.memo_cache_sentinel"))
    ((P = B2.createElement(T, { color: "warning", bold: !0 }, "/btw", " ")),
      (q[7] = P));
  else P = q[7];
  let W;
  if (q[8] !== K)
    ((W = B2.createElement(
      m,
      null,
      P,
      B2.createElement(T, { dimColor: !0 }, K),
    )),
      (q[8] = K),
      (q[9] = W));
  else W = q[9];
  let Z;
  if (q[10] !== O || q[11] !== H || q[12] !== _)
    ((Z = B2.createElement(
      m,
      { marginTop: 1, marginLeft: 2 },
      O
        ? B2.createElement(T, { color: "error" }, O)
        : _
          ? B2.createElement(S_, null, _)
          : B2.createElement(
              m,
              null,
              B2.createElement(kQ6, { frame: H, messageColor: "warning" }),
              B2.createElement(T, { color: "warning" }, "Answering..."),
            ),
    )),
      (q[10] = O),
      (q[11] = H),
      (q[12] = _),
      (q[13] = Z));
  else Z = q[13];
  let f;
  if (q[14] !== O || q[15] !== _)
    ((f =
      (_ || O) &&
      B2.createElement(
        m,
        { marginTop: 1 },
        B2.createElement(
          T,
          { dimColor: !0 },
          "Press Space, Enter, or Escape to dismiss",
        ),
      )),
      (q[14] = O),
      (q[15] = _),
      (q[16] = f));
  else f = q[16];
  let G;
  if (q[17] !== W || q[18] !== Z || q[19] !== f)
    ((G = B2.createElement(
      m,
      { flexDirection: "column", paddingLeft: 2, marginTop: 1 },
      W,
      Z,
      f,
    )),
      (q[17] = W),
      (q[18] = Z),
      (q[19] = f),
      (q[20] = G));
  else G = q[20];
  return G;
}

6. Context Gathering

Collects system prompt, user context (CLAUDE.md etc.), and conversation messages so the side question has full awareness of the current session.

async function gatherContext(toolUseContext) {
  // Try cached context first
  const cached = getCachedContext();
  if (cached) {
    return {
      systemPrompt: cached.systemPrompt,
      userContext: cached.userContext,
      systemContext: cached.systemContext,
      toolUseContext,
      forkContextMessages: toolUseContext.messages,
    };
  }

  // Build fresh context in parallel
  const [systemPromptParts, userContext, systemContext] = await Promise.all([
    buildSystemPrompt(tools, model, [], mcpClients),
    getUserContext(),      // CLAUDE.md, user instructions
    getSystemContext(),    // environment info
  ]);

  return {
    systemPrompt: formatSystemPrompt(systemPromptParts),
    userContext,
    systemContext,
    toolUseContext,
    forkContextMessages: toolUseContext.messages,
  };
}

Source (minified):

// cli.js:443807-443828
async function xbY(A) {
  let q = hk1();
  if (q)
    return {
      systemPrompt: q.systemPrompt,
      userContext: q.userContext,
      systemContext: q.systemContext,
      toolUseContext: A,
      forkContextMessages: A.messages,
    };
  let [K, Y, z] = await Promise.all([
    R0(A.options.tools, A.options.mainLoopModel, [], A.options.mcpClients),
    y2(),
    rO(),
  ]);
  return {
    systemPrompt: Bq(K),
    userContext: Y,
    systemContext: z,
    toolUseContext: A,
    forkContextMessages: A.messages,
  };
}

7. Core API Call

The heart of the feature — a single-turn, tool-less, forked API call.

async function callSideQuestion({ question, cacheSafeParams }) {
  const systemReminder = `<system-reminder>This is a side question from the user. You must answer this question directly in a single response.

CRITICAL CONSTRAINTS:
- You have NO tools available - you cannot read files, run commands, search, or take any actions
- This is a one-off response - there will be no follow-up turns
- You can ONLY provide information based on what you already know from the conversation context
- NEVER say things like "Let me try...", "I'll now...", "Let me check...", or promise to take any action
- If you don't know the answer, say so - do not offer to look it up or investigate

Simply answer the question with the information you have.</system-reminder>

${question}`;

  const result = await queryAPI({
    promptMessages: [createUserMessage({ content: systemReminder })],
    cacheSafeParams,
    canUseTool: async () => ({
      behavior: "deny",
      message: "Side questions cannot use tools",
      decisionReason: { type: "other", reason: "side_question" },
    }),
    querySource: "side_question",
    forkLabel: "side_question",
    maxTurns: 1,              // strictly one API round-trip
    skipCacheWrite: true,     // don't pollute the main conversation cache
  });

  // Extract text response from assistant message
  const textBlock = result.messages
    .find((m) => m.type === "assistant")
    ?.message?.content?.find((c) => c.type === "text");

  return {
    response: textBlock && textBlock.type === "text" ? textBlock.text.trim() : null,
    usage: result.totalUsage,
  };
}

Source (minified):

// cli.js:321722-321755
async function C04({ question: A, cacheSafeParams: q }) {
  let K = `<system-reminder>This is a side question from the user. You must answer this question directly in a single response.

CRITICAL CONSTRAINTS:
- You have NO tools available - you cannot read files, run commands, search, or take any actions
- This is a one-off response - there will be no follow-up turns
- You can ONLY provide information based on what you already know from the conversation context
- NEVER say things like "Let me try...", "I'll now...", "Let me check...", or promise to take any action
- If you don't know the answer, say so - do not offer to look it up or investigate

Simply answer the question with the information you have.</system-reminder>

${A}`,
    Y = await WR({
      promptMessages: [d1({ content: K })],
      cacheSafeParams: q,
      canUseTool: async () => ({
        behavior: "deny",
        message: "Side questions cannot use tools",
        decisionReason: { type: "other", reason: "side_question" },
      }),
      querySource: "side_question",
      forkLabel: "side_question",
      maxTurns: 1,
      skipCacheWrite: !0,
    }),
    _ = Y.messages
      .find((O) => O.type === "assistant")
      ?.message?.content?.find((O) => O.type === "text");
  return {
    response: _ && _.type === "text" ? _.text.trim() : null,
    usage: Y.totalUsage,
  };
}

8. Spinner Tip Integration

When the spinner has been active for >30 seconds and the user has never used /btw, a tip is shown.

const showBtwTip = tipsEnabled && elapsed > 30000 && isBtwEnabled() && !getState().btwUseCount;

const tip = showClearTip && !hasTip
  ? "Use /clear to start fresh when switching topics and free up context"
  : showBtwTip && !hasTip
    ? "Use /btw to ask a quick side question without interrupting Claude's current work"
    : defaultTip;

Source (minified):

// cli.js:321906-321914
let a = M.spinnerTipsEnabled !== !1,
  s = a && t > 1800000,
  n = a && t > 30000 && H96() && !D1().btwUseCount,
  l =
    s && !g
      ? "Use /clear to start fresh when switching topics and free up context"
      : n && !g
        ? "Use /btw to ask a quick side question without interrupting Claude's current work"
        : z,

9. Help Bar Hint

A dimmed hint shown in the bottom help bar when the feature is enabled.

isBtwEnabled() && <Text dimColor>/btw for side question</Text>

Source (minified):

// cli.js:456982-456988
V6 =
  H96() &&
  L7.createElement(
    m,
    null,
    L7.createElement(T, { dimColor: K }, "/btw for side question"),
  );

10. Global State

The btwUseCount is initialized in the global state store at 0:

// cli.js:533767-533769
{
  memoryUsageCount: 0,
  promptQueueUseCount: 0,
  btwUseCount: 0,
  todoFeatureEnabled: true,
  showExpandedTodos: false,
}

Summary Table

Aspect Detail
Purpose Ask a quick question without disrupting main conversation
Feature flag tengu_marble_whisper2
Tools All denied — text-only response
Turns Exactly 1 (single round-trip)
Context Full conversation context is forked/passed
Cache Skips cache write to avoid side effects
State tracking btwUseCount — suppresses tooltip after first use
Dismissal Space / Enter / Escape
System prompt Explicitly tells model: no tools, no actions, answer with what you know

Implementation Checklist

When porting to another project:

  1. Feature flag — Add a toggle to enable/disable the feature
  2. Command registration — Register /btw as an immediate slash command with <question> argument
  3. Input validation — Show usage hint if no question text provided
  4. Context gathering — Collect system prompt + user context + conversation messages
  5. API call — Fork a single-turn call with:
    • maxTurns: 1
    • All tool use denied
    • skipCacheWrite: true
    • System reminder constraining the model to text-only answers
  6. UI component — Spinner while loading, markdown-rendered response, dismissible overlay
  7. Usage tracking — Count uses to suppress the onboarding tooltip
  8. Spinner tip — Show tip after 30s of spinner if user hasn't tried /btw yet
  9. Help bar — Add /btw for side question hint
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment