Skip to content

Commit f22a62a

Browse files
committed
feat(renderer): add metadata and user confirmation hooks #453
Extend renderToolResult to support metadata display and add optional hooks for user confirmation requests and live terminal sessions in BaseRenderer. Subclasses can override these for enhanced interactivity.
1 parent e0fc374 commit f22a62a

File tree

6 files changed

+119
-19
lines changed

6 files changed

+119
-19
lines changed

mpp-ui/src/jsMain/typescript/agents/ServerAgentClient.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export type AgentEvent =
4848
| { type: 'llm_chunk'; chunk: string }
4949
| { type: 'tool_call'; toolName: string; params: string }
5050
| { type: 'tool_result'; toolName: string; success: boolean; output?: string }
51+
| { type: 'user_confirmation'; toolName: string; params: Record<string, any> }
5152
| { type: 'error'; message: string }
5253
| { type: 'complete'; success: boolean; message: string; iterations: number; steps: AgentStepInfo[]; edits: AgentEditInfo[] };
5354

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

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
/**
22
* Base TypeScript renderer implementing JsCodingAgentRenderer interface
33
* Provides common functionality for all TypeScript renderer implementations
4-
*
4+
*
55
* This mirrors the Kotlin BaseRenderer from mpp-core.
66
* All TypeScript renderers (CliRenderer, ServerRenderer, TuiRenderer) should extend this class.
7-
*
7+
*
88
* @see mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/render/BaseRenderer.kt
99
* @see mpp-core/src/jsMain/kotlin/cc/unitmesh/agent/RendererExports.kt - JsCodingAgentRenderer interface
1010
*/
11-
export abstract class BaseRenderer {
11+
12+
import {cc} from "autodev-mpp-core/autodev-mpp-core";
13+
import JsCodingAgentRenderer = cc.unitmesh.agent.JsCodingAgentRenderer;
14+
15+
export abstract class BaseRenderer implements JsCodingAgentRenderer {
1216
// Required by Kotlin JS export interface
1317
readonly __doNotUseOrImplementIt: any = {};
1418

@@ -24,7 +28,7 @@ export abstract class BaseRenderer {
2428
protected filterDevinBlocks(content: string): string {
2529
// Remove all complete devin blocks
2630
let filtered = content.replace(/<devin[^>]*>[\s\S]*?<\/devin>/g, '');
27-
31+
2832
// Handle incomplete devin blocks at the end - remove them completely
2933
const openDevinIndex = filtered.lastIndexOf('<devin');
3034
if (openDevinIndex !== -1) {
@@ -34,11 +38,11 @@ export abstract class BaseRenderer {
3438
filtered = filtered.substring(0, openDevinIndex);
3539
}
3640
}
37-
41+
3842
// Also remove partial devin tags at the end and any standalone '<' that might be part of a devin tag
3943
const partialDevinPattern = /<de(?:v(?:i(?:n)?)?)?$|<$/;
4044
filtered = filtered.replace(partialDevinPattern, '');
41-
45+
4246
return filtered;
4347
}
4448

@@ -49,11 +53,11 @@ export abstract class BaseRenderer {
4953
// Check if there's an incomplete devin block
5054
const lastOpenDevin = content.lastIndexOf('<devin');
5155
const lastCloseDevin = content.lastIndexOf('</devin>');
52-
56+
5357
// Also check for partial opening tags like '<de' or '<dev' or just '<'
5458
const partialDevinPattern = /<de(?:v(?:i(?:n)?)?)?$|<$/;
5559
const hasPartialTag = partialDevinPattern.test(content);
56-
60+
5761
return lastOpenDevin > lastCloseDevin || hasPartialTag;
5862
}
5963

@@ -62,13 +66,13 @@ export abstract class BaseRenderer {
6266
*/
6367
protected calculateSimilarity(str1: string, str2: string): number {
6468
if (!str1 || !str2) return 0;
65-
69+
6670
const words1 = str1.toLowerCase().split(/\s+/);
6771
const words2 = str2.toLowerCase().split(/\s+/);
68-
72+
6973
const commonWords = words1.filter(word => words2.includes(word));
7074
const totalWords = Math.max(words1.length, words2.length);
71-
75+
7276
return totalWords > 0 ? commonWords.length / totalWords : 0;
7377
}
7478

@@ -83,18 +87,32 @@ export abstract class BaseRenderer {
8387
// JsCodingAgentRenderer Interface - Abstract methods
8488
// These must be implemented by subclasses (CliRenderer, ServerRenderer, TuiRenderer)
8589
// ============================================================================
86-
90+
8791
abstract renderIterationHeader(current: number, max: number): void;
8892
abstract renderLLMResponseStart(): void;
8993
abstract renderLLMResponseChunk(chunk: string): void;
9094
abstract renderLLMResponseEnd(): void;
9195
abstract renderToolCall(toolName: string, paramsStr: string): void;
92-
abstract renderToolResult(toolName: string, success: boolean, output: string | null, fullOutput?: string | null): void;
96+
abstract renderToolResult(toolName: string, success: boolean, output: string | null, fullOutput?: string | null, metadata?: Record<string, string>): void;
9397
abstract renderTaskComplete(): void;
9498
abstract renderFinalResult(success: boolean, message: string, iterations: number): void;
9599
abstract renderError(message: string): void;
96100
abstract renderRepeatWarning(toolName: string, count: number): void;
97101
abstract renderRecoveryAdvice(recoveryAdvice: string): void;
102+
/**
103+
* Optional policy/permission prompt. Default: no-op; subclasses can override.
104+
*/
105+
renderUserConfirmationRequest(toolName: string, params: Record<string, any>): void {
106+
// Default to an info/error line if desired; keeping no-op to avoid extra noise in CLI/Server outputs.
107+
}
108+
109+
/**
110+
* Live terminal sessions are optional; default no-op implementation.
111+
* Subclasses that support PTY streaming can override.
112+
*/
113+
addLiveTerminal(sessionId: string, command: string, workingDirectory?: string | null, ptyHandle?: any): void {
114+
// no-op by default
115+
}
98116

99117
/**
100118
* Common implementation for LLM response start
@@ -131,7 +149,7 @@ export abstract class BaseRenderer {
131149
}
132150

133151
this.lastIterationReasoning = currentReasoning;
134-
152+
135153
// Only add a line break if the content doesn't already end with one
136154
const trimmedContent = finalContent.trimEnd();
137155
if (trimmedContent.length > 0 && !trimmedContent.endsWith('\n')) {

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

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
/**
22
* CLI Renderer for CodingAgent
3-
*
3+
*
44
* Implements JsCodingAgentRenderer interface from Kotlin Multiplatform.
55
* Provides enhanced CLI output with colors, syntax highlighting, and formatting.
6-
*
6+
*
77
* @see mpp-core/src/jsMain/kotlin/cc/unitmesh/agent/RendererExports.kt - Interface definition
88
*/
99

@@ -169,7 +169,7 @@ export class CliRenderer extends BaseRenderer {
169169
return params;
170170
}
171171

172-
renderToolResult(toolName: string, success: boolean, output: string | null, fullOutput: string | null): void {
172+
renderToolResult(toolName: string, success: boolean, output: string | null, fullOutput: string | null, metadata?: Record<string, string>): void {
173173
if (success && output) {
174174
const summary = this.generateToolSummary(toolName, output);
175175
console.log(' ⎿ ' + semanticChalk.success(summary));
@@ -187,6 +187,42 @@ export class CliRenderer extends BaseRenderer {
187187
} else if (!success && output) {
188188
console.log(' ⎿ ' + semanticChalk.error(`Error: ${output.substring(0, 200)}`));
189189
}
190+
// Display metadata if provided
191+
if (metadata && Object.keys(metadata).length > 0) {
192+
this.displayMetadata(metadata);
193+
}
194+
}
195+
196+
private displayMetadata(metadata: Record<string, string>): void {
197+
const entries = Object.entries(metadata);
198+
if (entries.length === 0) return;
199+
200+
// Format metadata with icons based on common keys
201+
const formattedEntries = entries.map(([key, value]) => {
202+
switch (key) {
203+
case 'duration':
204+
case 'elapsed':
205+
case 'time':
206+
return `⏱ ${key}: ${semanticChalk.accent(value)}`;
207+
case 'size':
208+
case 'fileSize':
209+
case 'bytes':
210+
return `📦 ${key}: ${semanticChalk.accent(value)}`;
211+
case 'lines':
212+
case 'lineCount':
213+
return `📄 ${key}: ${semanticChalk.accent(value)}`;
214+
case 'status':
215+
case 'exitCode':
216+
return `📊 ${key}: ${semanticChalk.accent(value)}`;
217+
default:
218+
return ` ${key}: ${semanticChalk.muted(value)}`;
219+
}
220+
});
221+
222+
console.log(semanticChalk.muted(' ├─ metadata:'));
223+
formattedEntries.forEach(entry => {
224+
console.log(semanticChalk.muted(` │ ${entry}`));
225+
});
190226
}
191227

192228
private generateToolSummary(toolName: string, output: string): string {

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,25 @@ export class ServerRenderer extends BaseRenderer {
9191
console.log('');
9292
}
9393

94+
renderUserConfirmationRequest(toolName: string, params: Record<string, any>): void {
95+
console.log('');
96+
console.log(semanticChalk.warningBold('🔐 User Confirmation Required'));
97+
console.log(semanticChalk.accent('━'.repeat(50)));
98+
console.log(semanticChalk.warning(`Tool: ${toolName}`));
99+
100+
const paramEntries = Object.entries(params);
101+
if (paramEntries.length > 0) {
102+
console.log(semanticChalk.muted('Parameters:'));
103+
paramEntries.forEach(([key, value]) => {
104+
console.log(semanticChalk.muted(` • ${key}: ${JSON.stringify(value)}`));
105+
});
106+
}
107+
108+
console.log(semanticChalk.success('\n✓ Auto-approved for non-interactive mode'));
109+
console.log(semanticChalk.accent('━'.repeat(50)));
110+
console.log('');
111+
}
112+
94113
// ============================================================================
95114
// Server-Specific Event Handler
96115
// ============================================================================
@@ -119,6 +138,9 @@ export class ServerRenderer extends BaseRenderer {
119138
case 'tool_result':
120139
this.renderToolResult(event.toolName, event.success, event.output);
121140
break;
141+
case 'user_confirmation':
142+
this.renderUserConfirmationRequest(event.toolName, event.params || {});
143+
break;
122144
case 'error':
123145
this.renderError(event.message);
124146
break;

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

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,20 @@
1212

1313
import type { ModeContext } from '../../modes/Mode.js';
1414
import type { Message } from '../../ui/App.js';
15+
import {BaseRenderer} from "./BaseRenderer";
1516

1617
/**
1718
* TUI 渲染器
1819
* 实现 Kotlin CodingAgent 期望的 JsCodingAgentRenderer 接口
1920
*/
20-
export class TuiRenderer {
21+
export class TuiRenderer extends BaseRenderer {
22+
protected outputContent(content: string): void {
23+
24+
}
25+
protected outputNewline(): void {
26+
27+
}
28+
2129
// Required by Kotlin JS export interface
2230
readonly __doNotUseOrImplementIt: any = {};
2331

@@ -32,6 +40,7 @@ export class TuiRenderer {
3240
private readonly VERBOSE_MODE = process.env.AUTODEV_VERBOSE === 'true';
3341

3442
constructor(context: ModeContext) {
43+
super();
3544
this.context = context;
3645
}
3746

@@ -168,6 +177,18 @@ export class TuiRenderer {
168177
this.renderSystemMessage(message);
169178
}
170179

180+
/**
181+
* 渲染用户确认请求
182+
*/
183+
renderUserConfirmationRequest(toolName: string, params: Record<string, any>): void {
184+
const paramStr = Object.entries(params)
185+
.map(([k, v]) => `${k}=${JSON.stringify(v)}`)
186+
.join(', ');
187+
188+
const message = `🔐 Tool '${toolName}' needs approval: ${paramStr} (Auto-approved)`;
189+
this.renderSystemMessage(message);
190+
}
191+
171192
// Helper methods
172193

173194
/**

mpp-ui/tsconfig.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818
"sourceMap": true,
1919
"types": ["node"],
2020
"paths": {
21-
"mpp-core": ["../mpp-core/build/js/packages/mpp-core/kotlin/mpp-core.mjs"]
21+
"mpp-core": ["../mpp-core/build/js/packages/mpp-core/kotlin/mpp-core.mjs"],
22+
"autodev-mpp-core": ["../build/js/packages/autodev-mpp-core/kotlin/autodev-mpp-core.js"],
23+
"autodev-mpp-core/*": ["../build/js/packages/autodev-mpp-core/kotlin/*"]
2224
}
2325
},
2426
"include": [

0 commit comments

Comments
 (0)