Skip to content

feat(chat): add Claude /btw side-question#138

Merged
Zhang-Henry merged 2 commits intomainfrom
feat/claude-btw-side-question
Apr 11, 2026
Merged

feat(chat): add Claude /btw side-question#138
Zhang-Henry merged 2 commits intomainfrom
feat/claude-btw-side-question

Conversation

@davidliuk
Copy link
Copy Markdown
Collaborator

Summary

Adds a Claude Code–style /btw side question: one-shot, tool-free answer in an overlay from current chat context, without appending to the main transcript. Only when the chat provider is Claude.

Changes

  • runClaudeBtw + POST /api/claude/btw
  • Built-in /btw in commands API
  • BtwOverlay UI; slash commands can run while the model is streaming (textarea not disabled for /…)

Notes

  • Other harnesses (Cursor, Codex, Gemini, etc.) still show the in-chat hint to switch to Claude.

Copy link
Copy Markdown
Contributor

@bbsngg bbsngg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Review: /btw side-question feature

Overall the idea is solid and the UX is well thought out. A few issues worth addressing before merge:

🔴 Critical

1. Race condition with process.env mutation (server/claude-sdk.js:818-827)

const prevStreamTimeout = process.env.CLAUDE_CODE_STREAM_CLOSE_TIMEOUT;
process.env.CLAUDE_CODE_STREAM_CLOSE_TIMEOUT = '120000';
const queryInstance = query({ ... });
// immediately restores env var

This is not safe under concurrent requests — two simultaneous /btw calls will stomp on each other's env var. The restore also happens before the stream is consumed (for await), so the env var may already be reset by the time it's actually needed. Consider passing this as a config option instead of mutating process.env.

2. Textarea disabled removed entirely (ChatComposer.tsx:381)

disabled={isLoading} was removed from the textarea entirely (along with disabled:opacity-50 in the className). This means during normal streaming the user can type freely and the textarea is no longer visually dimmed. The intent was to allow / commands while streaming, but the fix is too broad — consider keeping disabled and only conditionally allowing input when the text starts with /.

🟡 Should fix

3. Submit button allows any /-prefixed text during streaming (ChatComposer.tsx:421-424)

(isLoading && !input.trim().startsWith('/'))

This lets the user submit any slash command while streaming (e.g. /clear, /rewind), not just /btw. Other commands probably shouldn't be executable mid-stream.

4. handleSubmit guard reordering (useChatComposerState.ts:922-952)

The isLoading check was moved after slash command processing. If a non-/btw slash command is entered during streaming, it won't be caught by the /btw early-return and will fall through past the isLoading guard, potentially triggering a normal message send.

5. Transcript truncation cuts mid-message (useChatComposerState.ts:197-200)

out.slice(out.length - BTW_TRANSCRIPT_MAX_CHARS) can cut a message in half. Should find the next \n\n boundary after the cut point to preserve message integrity.

6. No request abort on overlay close

If the user closes the overlay while the API call is in flight, the fetch continues and the response is silently discarded. Should use AbortController to cancel the request on close.

💡 Minor

  • No server-side size limit on question field — a very long question + 120K transcript could create an oversized prompt.
  • BtwOverlay is rendered inside ChatComposer but is semantically unrelated — consider rendering at ChatInterface level.
  • CLOSED_BTW_OVERLAY default shape is duplicated in closeBtwOverlay callback and useState initializer — could reuse the constant.

✅ What looks good

  • Clean overlay UI with Escape + backdrop click-to-close
  • Proper aria-modal and role="dialog" accessibility
  • Non-Claude providers get a clear fallback message
  • Transcript builder correctly filters to user/assistant messages
  • Server-side input validation on the route

@bbsngg
Copy link
Copy Markdown
Contributor

bbsngg commented Apr 8, 2026

@davidliuk

Critical fixes:
- Move process.env restore into finally block so stream timeout env
  var persists through full stream consumption, eliminating the race
  condition under concurrent requests.
- Fix textarea disabled catch-22: remove disabled attr (which blocked
  typing /btw entirely), use conditional opacity-50 class instead.
- Fix premature server-side abort: switch from req.on('close') (fires
  when json middleware drains body) to res.on('close') + writableFinished
  guard so abort only fires on real client disconnect.

Guard logic:
- Only allow /btw during streaming in submit button, handleSubmit guard,
  and textarea; all other slash commands blocked when isLoading.
- Add isLoading && commandName !== '/btw' check before executeCommand.

Abort & cleanup:
- Add AbortController to btw fetch; abort on overlay close; catch block
  silently returns when aborted.
- Server-side: pass signal to runClaudeBtw, check signal.aborted in
  stream loop, catch, and post-loop; res.headersSent guards on response.

Minor:
- Server-side size limits: question 2K chars, transcript 150K chars.
- Move BtwOverlay from ChatComposer to ChatInterface level.
- Extract shared CLOSED_BTW_OVERLAY constant.
- Stabilize getChatMessagesForBtw with useRef + useCallback to avoid
  unnecessary executeCommand re-creation. Consolidate duplicate ref.
- Transcript truncation snaps to next \n\n boundary.
- Remove stray blank lines in ChatComposer.

Made-with: Cursor
@davidliuk davidliuk force-pushed the feat/claude-btw-side-question branch from 3922a67 to ce85e72 Compare April 11, 2026 02:01
@davidliuk
Copy link
Copy Markdown
Collaborator Author

All review items addressed in ce85e72. Here's the point-by-point resolution:

🔴 Critical

1. Race condition with process.env mutation — Moved env var restore into a finally block so the stream timeout persists through the entire for await loop, not just the query() constructor call.

2. Textarea disabled removed entirely — This turned out to be trickier than just restoring disabled. Adding disabled={isLoading && !input.startsWith('/btw')} creates a catch-22: when streaming starts the input is empty → textarea disabled → user can't type /btw at all. Fix: removed the disabled attribute, applied conditional opacity-50 CSS class for visual dimming during streaming. The submit button + handleSubmit guard enforce that only /btw can be submitted while streaming.

🟡 Should fix

3. Submit button allows any /-prefixed text — Changed from startsWith('/') to startsWith('/btw ') on the submit button disabled logic.

4. handleSubmit guard reordering — Added if (isLoading && commandName !== '/btw') return; inside the matched-command block, before executeCommand. Non-btw slash commands are now blocked during streaming.

5. Transcript truncation cuts mid-message — Now finds the next \n\n boundary (within 2000 chars) after the cut point before slicing.

6. No request abort on overlay close — Added AbortController ref; signal passed to authenticatedFetch; closeBtwOverlay aborts in-flight request; catch block silently returns on abort. Server-side: runClaudeBtw accepts a signal param, checks signal.aborted in the stream loop and returns early. Route uses res.on('close') + writableFinished guard to detect real client disconnect (not req.on('close') which fires prematurely when Express json middleware drains the POST body).

💡 Minor

  • Server-side size limit — Added MAX_QUESTION_CHARS = 2000 and MAX_TRANSCRIPT_CHARS = 150_000 with validation in the route.
  • BtwOverlay placement — Moved from ChatComposer to ChatInterface level.
  • CLOSED_BTW_OVERLAY dedup — Extracted shared constant in useChatComposerState.ts, used by both useState init and closeBtwOverlay.
  • Bonus: Stabilized getChatMessagesForBtw with useRef + useCallback to avoid unnecessary executeCommand re-creation on every chatMessages change. Consolidated duplicate chatMessagesRef.

@Zhang-Henry Zhang-Henry merged commit 104f19e into main Apr 11, 2026
3 checks passed
@Zhang-Henry Zhang-Henry deleted the feat/claude-btw-side-question branch April 11, 2026 04:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants