Skip to content

Commit e0fc374

Browse files
committed
refactor(renderer): extract BaseRenderer for shared logic
Refactor CliRenderer and ServerRenderer to extend a new BaseRenderer class, centralizing common rendering logic and buffer handling. This reduces code duplication and improves maintainability.
1 parent e820afc commit e0fc374

File tree

3 files changed

+74
-125
lines changed

3 files changed

+74
-125
lines changed

mpp-ui/src/jsMain/typescript/agents/render/BaseRenderer.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
/**
22
* Base TypeScript renderer implementing JsCodingAgentRenderer interface
33
* Provides common functionality for all TypeScript renderer implementations
4+
*
5+
* This mirrors the Kotlin BaseRenderer from mpp-core.
6+
* All TypeScript renderers (CliRenderer, ServerRenderer, TuiRenderer) should extend this class.
7+
*
8+
* @see mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/render/BaseRenderer.kt
9+
* @see mpp-core/src/jsMain/kotlin/cc/unitmesh/agent/RendererExports.kt - JsCodingAgentRenderer interface
410
*/
5-
611
export abstract class BaseRenderer {
712
// Required by Kotlin JS export interface
813
readonly __doNotUseOrImplementIt: any = {};
@@ -74,17 +79,22 @@ export abstract class BaseRenderer {
7479
return content.replace(/\n{3,}/g, '\n\n');
7580
}
7681

77-
// Abstract methods that must be implemented by subclasses
82+
// ============================================================================
83+
// JsCodingAgentRenderer Interface - Abstract methods
84+
// These must be implemented by subclasses (CliRenderer, ServerRenderer, TuiRenderer)
85+
// ============================================================================
86+
7887
abstract renderIterationHeader(current: number, max: number): void;
7988
abstract renderLLMResponseStart(): void;
8089
abstract renderLLMResponseChunk(chunk: string): void;
8190
abstract renderLLMResponseEnd(): void;
8291
abstract renderToolCall(toolName: string, paramsStr: string): void;
83-
abstract renderToolResult(toolName: string, success: boolean, output: string | null, fullOutput: string | null): void;
92+
abstract renderToolResult(toolName: string, success: boolean, output: string | null, fullOutput?: string | null): void;
8493
abstract renderTaskComplete(): void;
8594
abstract renderFinalResult(success: boolean, message: string, iterations: number): void;
8695
abstract renderError(message: string): void;
8796
abstract renderRepeatWarning(toolName: string, count: number): void;
97+
abstract renderRecoveryAdvice(recoveryAdvice: string): void;
8898

8999
/**
90100
* Common implementation for LLM response start

mpp-ui/src/jsMain/typescript/agents/render/CliRenderer.ts

Lines changed: 28 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -10,101 +10,76 @@
1010
import chalk from 'chalk';
1111
import hljs from 'highlight.js';
1212
import { semanticChalk, dividers } from '../../design-system/theme-helpers.js';
13+
import { BaseRenderer } from './BaseRenderer.js';
1314

1415
/**
15-
* CliRenderer implements the unified JsCodingAgentRenderer interface
16+
* CliRenderer extends BaseRenderer and implements the unified JsCodingAgentRenderer interface
1617
*/
17-
export class CliRenderer {
18-
readonly __doNotUseOrImplementIt: any = {};
18+
export class CliRenderer extends BaseRenderer {
19+
// BaseRenderer already has __doNotUseOrImplementIt
20+
// BaseRenderer already has reasoningBuffer, isInDevinBlock, lastIterationReasoning, consecutiveRepeats, lastOutputLength
1921

20-
private reasoningBuffer: string = '';
21-
private isInDevinBlock: boolean = false;
22-
private lastIterationReasoning: string = '';
23-
private consecutiveRepeats: number = 0;
22+
// ============================================================================
23+
// Platform-specific output methods (required by BaseRenderer)
24+
// ============================================================================
25+
26+
protected outputContent(content: string): void {
27+
process.stdout.write(chalk.white(content));
28+
}
29+
30+
protected outputNewline(): void {
31+
console.log(); // Single line break
32+
}
33+
34+
// ============================================================================
35+
// JsCodingAgentRenderer Interface Implementation
36+
// ============================================================================
2437

2538
renderIterationHeader(current: number, max: number): void {
2639
// Don't show iteration headers - they're not in the reference format
2740
// The reference format shows tools directly without iteration numbers
2841
}
2942

3043
renderLLMResponseStart(): void {
31-
this.reasoningBuffer = '';
32-
this.isInDevinBlock = false;
33-
this.lastOutputLength = 0;
44+
this.baseLLMResponseStart(); // Use BaseRenderer helper
3445
process.stdout.write(semanticChalk.muted('💭 '));
3546
}
3647

3748
renderLLMResponseChunk(chunk: string): void {
3849
// Add chunk to buffer
3950
this.reasoningBuffer += chunk;
4051

41-
// Wait for more content if we detect an incomplete devin block
52+
// Wait for more content if we detect an incomplete devin block (from BaseRenderer)
4253
if (this.hasIncompleteDevinBlock(this.reasoningBuffer)) {
4354
return; // Don't output anything yet, wait for more chunks
4455
}
4556

46-
// Process the buffer to filter out devin blocks
57+
// Process the buffer to filter out devin blocks (from BaseRenderer)
4758
let processedContent = this.filterDevinBlocks(this.reasoningBuffer);
4859

4960
// Only output new content that hasn't been printed yet
5061
if (processedContent.length > 0) {
5162
// Find what's new since last output
5263
const newContent = processedContent.slice(this.lastOutputLength || 0);
5364
if (newContent.length > 0) {
54-
// Clean up excessive newlines - replace multiple consecutive newlines with at most 2
55-
const cleanedContent = newContent.replace(/\n{3,}/g, '\n\n');
65+
// Clean up excessive newlines (from BaseRenderer)
66+
const cleanedContent = this.cleanNewlines(newContent);
5667
process.stdout.write(chalk.white(cleanedContent));
5768
this.lastOutputLength = processedContent.length;
5869
}
5970
}
6071
}
6172

62-
private lastOutputLength: number = 0;
63-
64-
private hasIncompleteDevinBlock(content: string): boolean {
65-
// Check if there's an incomplete devin block
66-
const lastOpenDevin = content.lastIndexOf('<devin');
67-
const lastCloseDevin = content.lastIndexOf('</devin>');
68-
69-
// If we have an opening tag without a closing tag after it, it's incomplete
70-
// Also check for partial opening tags like '<de' or '<dev' or just '<'
71-
const partialDevinPattern = /<de(?:v(?:i(?:n)?)?)?$|<$/;
72-
const hasPartialTag = partialDevinPattern.test(content);
73-
74-
return lastOpenDevin > lastCloseDevin || hasPartialTag;
75-
}
76-
77-
private filterDevinBlocks(content: string): string {
78-
// Remove all complete devin blocks
79-
let filtered = content.replace(/<devin[^>]*>[\s\S]*?<\/devin>/g, '');
80-
81-
// Handle incomplete devin blocks at the end - remove them completely
82-
const openDevinIndex = filtered.lastIndexOf('<devin');
83-
if (openDevinIndex !== -1) {
84-
const closeDevinIndex = filtered.indexOf('</devin>', openDevinIndex);
85-
if (closeDevinIndex === -1) {
86-
// Incomplete devin block, remove it
87-
filtered = filtered.substring(0, openDevinIndex);
88-
}
89-
}
90-
91-
// Also remove partial devin tags at the end and any standalone '<' that might be part of a devin tag
92-
const partialDevinPattern = /<de(?:v(?:i(?:n)?)?)?$|<$/;
93-
filtered = filtered.replace(partialDevinPattern, '');
94-
95-
return filtered;
96-
}
97-
9873
renderLLMResponseEnd(): void {
9974
// Force output any remaining content after filtering devin blocks
10075
const finalContent = this.filterDevinBlocks(this.reasoningBuffer);
10176
const remainingContent = finalContent.slice(this.lastOutputLength || 0);
10277

10378
if (remainingContent.length > 0) {
104-
process.stdout.write(chalk.white(remainingContent));
79+
this.outputContent(remainingContent);
10580
}
10681

107-
// Check if this reasoning is similar to the last one
82+
// Check if this reasoning is similar to the last one (from BaseRenderer)
10883
const currentReasoning = finalContent.trim();
10984
const similarity = this.calculateSimilarity(currentReasoning, this.lastIterationReasoning);
11085

@@ -122,23 +97,10 @@ export class CliRenderer {
12297
// Only add a line break if the content doesn't already end with one
12398
const trimmedContent = finalContent.trimEnd();
12499
if (trimmedContent.length > 0 && !trimmedContent.endsWith('\n')) {
125-
console.log(); // Single line break after reasoning only if needed
100+
this.outputNewline();
126101
}
127102
}
128103

129-
private calculateSimilarity(str1: string, str2: string): number {
130-
// Simple similarity calculation based on common words
131-
if (!str1 || !str2) return 0;
132-
133-
const words1 = str1.toLowerCase().split(/\s+/);
134-
const words2 = str2.toLowerCase().split(/\s+/);
135-
136-
const commonWords = words1.filter(word => words2.includes(word));
137-
const totalWords = Math.max(words1.length, words2.length);
138-
139-
return totalWords > 0 ? commonWords.length / totalWords : 0;
140-
}
141-
142104
renderToolCall(toolName: string, paramsStr: string): void {
143105
const toolInfo = this.formatToolCallDisplay(toolName, paramsStr);
144106
console.log(chalk.bold('● ') + chalk.bold(toolInfo.name) + semanticChalk.muted(' - ' + toolInfo.description));

mpp-ui/src/jsMain/typescript/agents/render/ServerRenderer.ts

Lines changed: 33 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,34 @@
77

88
import { semanticChalk } from '../../design-system/theme-helpers.js';
99
import type { AgentEvent, AgentStepInfo, AgentEditInfo } from '../ServerAgentClient.js';
10+
import { BaseRenderer } from './BaseRenderer.js';
1011

1112
/**
12-
* ServerRenderer implements the unified JsCodingAgentRenderer interface
13+
* ServerRenderer extends BaseRenderer and implements the unified JsCodingAgentRenderer interface
1314
* defined in mpp-core/src/jsMain/kotlin/cc/unitmesh/agent/RendererExports.kt
1415
*/
15-
export class ServerRenderer {
16-
// Required by Kotlin JS export interface
17-
readonly __doNotUseOrImplementIt: any = {};
16+
export class ServerRenderer extends BaseRenderer {
17+
// BaseRenderer already has __doNotUseOrImplementIt
1818

1919
private currentIteration: number = 0;
2020
private maxIterations: number = 20;
21-
private llmBuffer: string = '';
21+
// llmBuffer removed - use reasoningBuffer from BaseRenderer
2222
private toolCallsInProgress: Map<string, { toolName: string; params: string }> = new Map();
2323
private isCloning: boolean = false;
2424
private lastCloneProgress: number = 0;
2525
private hasStartedLLMOutput: boolean = false;
26-
private lastOutputLength: number = 0;
26+
27+
// ============================================================================
28+
// Platform-specific output methods (required by BaseRenderer)
29+
// ============================================================================
30+
31+
protected outputContent(content: string): void {
32+
process.stdout.write(content);
33+
}
34+
35+
protected outputNewline(): void {
36+
console.log();
37+
}
2738

2839
// ============================================================================
2940
// JsCodingAgentRenderer Interface Implementation
@@ -34,8 +45,8 @@ export class ServerRenderer {
3445
}
3546

3647
renderLLMResponseStart(): void {
48+
this.baseLLMResponseStart(); // Use BaseRenderer helper
3749
this.hasStartedLLMOutput = false;
38-
this.lastOutputLength = 0;
3950
}
4051

4152
renderLLMResponseChunk(chunk: string): void {
@@ -44,14 +55,14 @@ export class ServerRenderer {
4455

4556
renderLLMResponseEnd(): void {
4657
// Flush any buffered LLM output
47-
if (this.llmBuffer.trim()) {
48-
const finalContent = this.filterDevinBlocks(this.llmBuffer);
58+
if (this.reasoningBuffer.trim()) {
59+
const finalContent = this.filterDevinBlocks(this.reasoningBuffer);
4960
const remainingContent = finalContent.slice(this.lastOutputLength || 0);
5061
if (remainingContent.trim()) {
51-
process.stdout.write(remainingContent);
62+
this.outputContent(remainingContent);
5263
}
53-
console.log(''); // Ensure newline
54-
this.llmBuffer = '';
64+
this.outputNewline(); // Ensure newline
65+
this.reasoningBuffer = '';
5566
this.hasStartedLLMOutput = false;
5667
this.lastOutputLength = 0;
5768
}
@@ -182,15 +193,15 @@ export class ServerRenderer {
182193
}
183194

184195
// Add chunk to buffer
185-
this.llmBuffer += chunk;
196+
this.reasoningBuffer += chunk;
186197

187198
// Wait for more content if we detect an incomplete devin block
188-
if (this.hasIncompleteDevinBlock(this.llmBuffer)) {
199+
if (this.hasIncompleteDevinBlock(this.reasoningBuffer)) {
189200
return; // Don't output anything yet, wait for more chunks
190201
}
191202

192203
// Process the buffer to filter out devin blocks
193-
const processedContent = this.filterDevinBlocks(this.llmBuffer);
204+
const processedContent = this.filterDevinBlocks(this.reasoningBuffer);
194205

195206
// Only output new content that hasn't been printed yet
196207
if (processedContent.length > 0) {
@@ -204,49 +215,15 @@ export class ServerRenderer {
204215
}
205216
}
206217

207-
private hasIncompleteDevinBlock(content: string): boolean {
208-
// Check if there's an incomplete devin block
209-
const lastOpenDevin = content.lastIndexOf('<devin');
210-
const lastCloseDevin = content.lastIndexOf('</devin>');
211-
212-
// If we have an opening tag without a closing tag after it, it's incomplete
213-
// Also check for partial opening tags like '<de' or '<dev' or just '<'
214-
const partialDevinPattern = /<de(?:v(?:i(?:n)?)?)?$|<$/;
215-
const hasPartialTag = partialDevinPattern.test(content);
216-
217-
return lastOpenDevin > lastCloseDevin || hasPartialTag;
218-
}
219-
220-
private filterDevinBlocks(content: string): string {
221-
// Remove all complete devin blocks
222-
let filtered = content.replace(/<devin[^>]*>[\s\S]*?<\/devin>/g, '');
223-
224-
// Handle incomplete devin blocks at the end - remove them completely
225-
const openDevinIndex = filtered.lastIndexOf('<devin');
226-
if (openDevinIndex !== -1) {
227-
const closeDevinIndex = filtered.indexOf('</devin>', openDevinIndex);
228-
if (closeDevinIndex === -1) {
229-
// Incomplete devin block, remove it
230-
filtered = filtered.substring(0, openDevinIndex);
231-
}
232-
}
233-
234-
// Also remove partial devin tags at the end and any standalone '<' that might be part of a devin tag
235-
const partialDevinPattern = /<de(?:v(?:i(?:n)?)?)?$|<$/;
236-
filtered = filtered.replace(partialDevinPattern, '');
237-
238-
return filtered;
239-
}
240-
241218
// ============================================================================
242219
// Tool Execution Methods (JsCodingAgentRenderer)
243220
// ============================================================================
244221

245222
renderToolCall(toolName: string, params: string): void {
246223
// Flush any buffered LLM output first
247-
if (this.llmBuffer.trim()) {
224+
if (this.reasoningBuffer.trim()) {
248225
console.log(''); // New line before tool
249-
this.llmBuffer = '';
226+
this.reasoningBuffer = '';
250227
this.hasStartedLLMOutput = false;
251228
this.lastOutputLength = 0;
252229
}
@@ -480,9 +457,9 @@ export class ServerRenderer {
480457
this.maxIterations = max;
481458

482459
// Flush any buffered LLM output
483-
if (this.llmBuffer.trim()) {
460+
if (this.reasoningBuffer.trim()) {
484461
console.log(''); // Just a newline
485-
this.llmBuffer = '';
462+
this.reasoningBuffer = '';
486463
}
487464

488465
// Reset LLM output state for new iteration
@@ -501,14 +478,14 @@ export class ServerRenderer {
501478
edits: AgentEditInfo[]
502479
): void {
503480
// Flush any buffered LLM output
504-
if (this.llmBuffer.trim()) {
505-
const finalContent = this.filterDevinBlocks(this.llmBuffer);
481+
if (this.reasoningBuffer.trim()) {
482+
const finalContent = this.filterDevinBlocks(this.reasoningBuffer);
506483
const remainingContent = finalContent.slice(this.lastOutputLength || 0);
507484
if (remainingContent.trim()) {
508485
process.stdout.write(remainingContent);
509486
}
510487
console.log(''); // Ensure newline
511-
this.llmBuffer = '';
488+
this.reasoningBuffer = '';
512489
}
513490

514491
console.log('');

0 commit comments

Comments
 (0)