Skip to content

fix: render think tags as reasoning blocks + fix MCP MaxListeners overflow#15382

Closed
erwinh22 wants to merge 2 commits intoanomalyco:devfrom
erwinh22:fix/think-tag-rendering
Closed

fix: render think tags as reasoning blocks + fix MCP MaxListeners overflow#15382
erwinh22 wants to merge 2 commits intoanomalyco:devfrom
erwinh22:fix/think-tag-rendering

Conversation

@erwinh22
Copy link

@erwinh22 erwinh22 commented Feb 27, 2026

Summary

Two fixes in this PR:

1. Render <think> tags as reasoning blocks at display layer

Models like Kimi K2 Thinking embed reasoning in raw <think>...</think> tags within text parts. Previously these were displayed as raw HTML to the user.

Approach: Parse and render <think> / <thinking> tags at the display layer (TUI, Web UI, CLI) rather than stripping them from the message storage. This preserves the full model output for multi-turn context while giving users clean, styled reasoning blocks.

Changes:

  • packages/util/src/think.ts -- Shared utility: stripThinkTags(), splitThinkBlocks() with regex matching both <think> and <thinking> variants
  • packages/opencode/src/util/format.ts -- Same utilities for the core package (@/util/format path alias)
  • packages/opencode/src/cli/cmd/tui/routes/session/index.tsx -- TextPart renders think blocks with dim/italic/bordered styling matching ReasoningPart
  • packages/ui/src/components/message-part.tsx -- TextPartDisplay renders think blocks in <div data-component="reasoning-part">
  • packages/opencode/src/cli/cmd/run.ts -- CLI strips think tags from output, shows reasoning with --thinking flag

2. Fix MCP MaxListeners overflow with many servers

When 7+ stdio-based MCP servers are configured, the default Node.js EventEmitter limit of ~10 is exceeded. Each StdioClientTransport spawns a child process and adds listeners to its stdin/stdout/stderr pipes. The MCP protocol layer also sends many concurrent requests (getPrompt, listTools, etc.) that each add a drain listener.

Fix: Dynamically calculate the needed listener limit based on the number of configured local MCP servers and increase both EventEmitter.defaultMaxListeners and process stream limits before connecting.

Changes:

  • packages/opencode/src/mcp/index.ts -- Count local MCP servers, set EventEmitter.defaultMaxListeners and process stream limits proportionally

Related Issues

Testing

  • Type-check passes: bun turbo typecheck -- 0 errors in opencode and ui packages
  • Tested with Kimi K2 Thinking via NVIDIA NIM -- thinking content renders correctly in TUI
  • Tested with 7 MCP servers connected -- no MaxListeners overflow warnings

Models that emit <think>/<thinking> tags (e.g. kimi-k2-thinking via
NVIDIA NIM) currently display raw tag content in the UI instead of
rendering it as collapsible reasoning blocks.

The extractReasoningMiddleware was intentionally removed in PR anomalyco#11270
because stripping tags at the middleware layer prevents them from being
stored in messages, breaking multi-turn LLM context.

This fix takes the rendering-layer approach: tags are preserved in
storage for faithful multi-turn context, but parsed and rendered as
reasoning blocks at display time in all three rendering surfaces:

- TUI: TextPart in session/index.tsx uses splitThinkBlocks() to
  separate reasoning from display text, rendering reasoning with the
  same dim/italic styling as native ReasoningPart
- Web UI: TextPartDisplay in message-part.tsx renders extracted
  reasoning in a reasoning-part div above the text content
- CLI: run.ts strips think tags from text output and shows reasoning
  with dim/italic styling when --thinking flag is set

Shared utility functions (stripThinkTags, splitThinkBlocks) are added
to both @opencode-ai/util/think (for web UI) and the core
packages/opencode/src/util/format.ts (for TUI and CLI).

Fixes anomalyco#15380
Relates to anomalyco#11439
@github-actions github-actions bot added the needs:compliance This means the issue will auto-close after 2 hours. label Feb 27, 2026
@github-actions
Copy link
Contributor

This PR doesn't fully meet our contributing guidelines and PR template.

What needs to be fixed:

  • PR description is missing required template sections. Please use the PR template.

Please edit this PR description to address the above within 2 hours, or it will be automatically closed.

If you believe this was flagged incorrectly, please let a maintainer know.

Each StdioClientTransport spawns a child process and adds listeners to
its stdin/stdout/stderr pipes. The MCP protocol layer also sends many
concurrent requests (getPrompt, listTools, etc.) that each add a
'drain' listener on the child process's stdin. With 7+ MCP servers,
the default Node.js EventEmitter limit of ~10 is easily exceeded,
causing MaxListeners overflow warnings and potential crashes.

Dynamically calculate the needed limit based on the number of configured
local (stdio) MCP servers and increase both EventEmitter.defaultMaxListeners
and process stream limits before connecting.
@erwinh22 erwinh22 force-pushed the fix/think-tag-rendering branch from bb4e9f4 to c92846c Compare February 27, 2026 17:18
@erwinh22 erwinh22 changed the title fix: render <think> tags as reasoning blocks at display layer fix: render think tags as reasoning blocks + fix MCP MaxListeners overflow Feb 27, 2026
@github-actions
Copy link
Contributor

This pull request has been automatically closed because it was not updated to meet our contributing guidelines within the 2-hour window.

Feel free to open a new pull request that follows our guidelines.

@github-actions github-actions bot removed the needs:compliance This means the issue will auto-close after 2 hours. label Feb 27, 2026
@github-actions github-actions bot closed this Feb 27, 2026
@JDNdeveloper
Copy link

JDNdeveloper commented Mar 3, 2026

It would be great to get this landed, it's been a pretty annoying UX issue when using qwen-3.5.

EDIT: I found a workaround in LM Studio at least: Settings -> Developer -> Experimental Settings -> When applicable, separate `reasoning_content` and `content` in API responses.

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.

Support parsing <think> and <thinking> tags as reasoning blocks

2 participants