@@ -275,6 +275,10 @@ export class OpenCodeProcess extends EventEmitter<ClaudeProcessEvents> implement
275275 private reasoningBuffer = ''
276276 /** Whether we've already emitted a thinking summary from reasoning deltas. */
277277 private emittedReasoningSummary = false
278+ /** Whether we're currently inside a <think> block in streamed text deltas. */
279+ private insideThinkTag = false
280+ /** Buffer for accumulating text inside <think> tags during streaming. */
281+ private thinkTagBuffer = ''
278282
279283 constructor ( workingDir : string , opts ?: Partial < OpenCodeProcessOptions > ) {
280284 super ( )
@@ -467,9 +471,10 @@ export class OpenCodeProcess extends EventEmitter<ClaudeProcessEvents> implement
467471 this . deltaBufferFlushed = true
468472 if ( this . lastUserInput && this . deltaBuffer . startsWith ( this . lastUserInput ) ) {
469473 const remainder = this . deltaBuffer . slice ( this . lastUserInput . length )
470- if ( remainder ) this . emit ( 'text' , remainder )
474+ if ( remainder ) { const clean = this . stripThinkTags ( remainder ) ; if ( clean ) this . emit ( 'text' , clean ) }
471475 } else {
472- this . emit ( 'text' , this . deltaBuffer )
476+ const clean = this . stripThinkTags ( this . deltaBuffer )
477+ if ( clean ) this . emit ( 'text' , clean )
473478 }
474479 this . deltaBuffer = ''
475480 }
@@ -496,15 +501,19 @@ export class OpenCodeProcess extends EventEmitter<ClaudeProcessEvents> implement
496501 this . deltaBufferFlushed = true
497502 if ( this . deltaBuffer . startsWith ( this . lastUserInput ) ) {
498503 const remainder = this . deltaBuffer . slice ( this . lastUserInput . length )
499- if ( remainder ) this . emit ( 'text' , remainder )
504+ if ( remainder ) { const clean = this . stripThinkTags ( remainder ) ; if ( clean ) this . emit ( 'text' , clean ) }
500505 } else {
501- this . emit ( 'text' , this . deltaBuffer )
506+ const clean = this . stripThinkTags ( this . deltaBuffer )
507+ if ( clean ) this . emit ( 'text' , clean )
502508 }
503509 this . deltaBuffer = ''
504510 }
505511 // Still buffering — don't emit yet
506512 } else {
507- this . emit ( 'text' , delta )
513+ // Strip <think>...</think> tags — some models (e.g. Kimi) embed
514+ // chain-of-thought reasoning in the text output using these tags.
515+ const clean = this . stripThinkTags ( delta )
516+ if ( clean ) this . emit ( 'text' , clean )
508517 }
509518 } else if ( field === 'reasoning' && delta ) {
510519 // Accumulate reasoning deltas and emit a thinking summary once we
@@ -543,6 +552,8 @@ export class OpenCodeProcess extends EventEmitter<ClaudeProcessEvents> implement
543552 if ( this . lastUserInput && text . startsWith ( this . lastUserInput ) ) {
544553 text = text . slice ( this . lastUserInput . length )
545554 }
555+ // Strip <think>...</think> blocks from full text
556+ text = OpenCodeProcess . stripThinkTagsFull ( text )
546557 if ( text ) this . emit ( 'text' , text )
547558 }
548559 break
@@ -736,6 +747,67 @@ export class OpenCodeProcess extends EventEmitter<ClaudeProcessEvents> implement
736747 }
737748 }
738749
750+ /**
751+ * Strip `<think>...</think>` tags from text content. Some models (e.g. Kimi)
752+ * embed chain-of-thought reasoning in the text output using these tags rather
753+ * than using a separate reasoning/thinking field.
754+ * Returns the text with think blocks removed; thinking content is routed to
755+ * the thinking summary emitter.
756+ */
757+ private stripThinkTags ( text : string ) : string {
758+ // Fast path: no think tags at all
759+ if ( ! text . includes ( '<think' ) && ! text . includes ( '</think' ) && ! this . insideThinkTag ) {
760+ return text
761+ }
762+
763+ let result = ''
764+ let i = 0
765+ while ( i < text . length ) {
766+ if ( this . insideThinkTag ) {
767+ const closeIdx = text . indexOf ( '</think>' , i )
768+ if ( closeIdx === - 1 ) {
769+ // Still inside think block, buffer the rest
770+ this . thinkTagBuffer += text . slice ( i )
771+ i = text . length
772+ } else {
773+ this . thinkTagBuffer += text . slice ( i , closeIdx )
774+ this . insideThinkTag = false
775+ // Emit thinking summary from accumulated think content
776+ if ( this . thinkTagBuffer . length > 20 && ! this . emittedReasoningSummary ) {
777+ this . emittedReasoningSummary = true
778+ const match = this . thinkTagBuffer . match ( / ^ ( .+ ?[ . ! ? \n ] ) / )
779+ const summary = match && match [ 1 ] . length <= 120
780+ ? match [ 1 ] . replace ( / \n / g, ' ' ) . trim ( )
781+ : this . thinkTagBuffer . slice ( 0 , 80 ) . trim ( )
782+ this . emit ( 'thinking' , summary )
783+ }
784+ this . thinkTagBuffer = ''
785+ i = closeIdx + '</think>' . length
786+ }
787+ } else {
788+ const openIdx = text . indexOf ( '<think>' , i )
789+ if ( openIdx === - 1 ) {
790+ result += text . slice ( i )
791+ i = text . length
792+ } else {
793+ result += text . slice ( i , openIdx )
794+ this . insideThinkTag = true
795+ i = openIdx + '<think>' . length
796+ }
797+ }
798+ }
799+ return result
800+ }
801+
802+ /**
803+ * Strip `<think>...</think>` blocks from a complete text string (non-streaming).
804+ * Returns the text with all think blocks removed.
805+ */
806+ private static stripThinkTagsFull ( text : string ) : string {
807+ // Remove complete <think>...</think> blocks (including multiline)
808+ return text . replace ( / < t h i n k > [ \s \S ] * ?< \/ t h i n k > \s * / g, '' ) . trimStart ( )
809+ }
810+
739811 /**
740812 * Detect TodoWrite/TaskCreate/TaskUpdate tool calls and emit todo_update events.
741813 * Mirrors the task-tracking logic in ClaudeProcess.handleTaskTool().
@@ -804,6 +876,8 @@ export class OpenCodeProcess extends EventEmitter<ClaudeProcessEvents> implement
804876 this . deltaBufferFlushed = false
805877 this . reasoningBuffer = ''
806878 this . emittedReasoningSummary = false
879+ this . insideThinkTag = false
880+ this . thinkTagBuffer = ''
807881
808882 const baseUrl = `http://localhost:${ serverState . port } `
809883 // Parse [Attached files: ...] prefix and convert image paths to proper parts.
0 commit comments