From 37614a3362824bf2ff101176543ef560f029066c Mon Sep 17 00:00:00 2001 From: catlog22 Date: Sun, 11 Jan 2026 22:37:44 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=20SmartContentFormat?= =?UTF-8?q?ter=EF=BC=8C=E7=A1=AE=E4=BF=9D=E6=A0=BC=E5=BC=8F=E5=8C=96?= =?UTF-8?q?=E5=86=85=E5=AE=B9=E5=A7=8B=E7=BB=88=E8=BF=94=E5=9B=9E=E5=8F=AF?= =?UTF-8?q?=E6=98=BE=E7=A4=BA=E7=9A=84=E5=AD=97=E7=AC=A6=E4=B8=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ccw/src/commands/cli.ts | 4 +- ccw/src/core/routes/claude-routes.ts | 4 +- ccw/src/core/routes/cli-routes.ts | 4 +- ccw/src/core/routes/memory-routes.ts | 4 +- ccw/src/core/routes/rules-routes.ts | 8 +- ccw/src/core/routes/skills-routes.ts | 4 +- ccw/src/tools/cli-output-converter.ts | 111 ++++++++++++++++++++------ 7 files changed, 100 insertions(+), 39 deletions(-) diff --git a/ccw/src/commands/cli.ts b/ccw/src/commands/cli.ts index 112d1549..a1c6465b 100644 --- a/ccw/src/commands/cli.ts +++ b/ccw/src/commands/cli.ts @@ -794,8 +794,8 @@ async function execAction(positionalPrompt: string | undefined, options: CliExec // Always broadcast to dashboard for real-time viewing // Note: /api/hook wraps extraData into payload, so send fields directly // Maintain backward compatibility with frontend expecting { chunkType, data } - // Use SmartContentFormatter for intelligent content formatting - const content = SmartContentFormatter.format(unit.content, unit.type) || JSON.stringify(unit.content); + // Use SmartContentFormatter for intelligent content formatting (never returns null) + const content = SmartContentFormatter.format(unit.content, unit.type); broadcastStreamEvent('CLI_OUTPUT', { executionId, chunkType: unit.type, // For backward compatibility diff --git a/ccw/src/core/routes/claude-routes.ts b/ccw/src/core/routes/claude-routes.ts index 668c7d81..9f17a83d 100644 --- a/ccw/src/core/routes/claude-routes.ts +++ b/ccw/src/core/routes/claude-routes.ts @@ -629,8 +629,8 @@ export async function handleClaudeRoutes(ctx: RouteContext): Promise { category: 'internal', id: syncId }, (unit) => { - // CliOutputUnit handler: use SmartContentFormatter for intelligent formatting - const content = SmartContentFormatter.format(unit.content, unit.type) || JSON.stringify(unit.content); + // CliOutputUnit handler: use SmartContentFormatter for intelligent formatting (never returns null) + const content = SmartContentFormatter.format(unit.content, unit.type); broadcastToClients({ type: 'CLI_OUTPUT', payload: { diff --git a/ccw/src/core/routes/cli-routes.ts b/ccw/src/core/routes/cli-routes.ts index 4b3e8b20..5f0731f0 100644 --- a/ccw/src/core/routes/cli-routes.ts +++ b/ccw/src/core/routes/cli-routes.ts @@ -565,8 +565,8 @@ export async function handleCliRoutes(ctx: RouteContext): Promise { parentExecutionId, stream: true }, (unit) => { - // CliOutputUnit handler: use SmartContentFormatter for intelligent formatting - const content = SmartContentFormatter.format(unit.content, unit.type) || JSON.stringify(unit.content); + // CliOutputUnit handler: use SmartContentFormatter for intelligent formatting (never returns null) + const content = SmartContentFormatter.format(unit.content, unit.type); // Append to active execution buffer const activeExec = activeExecutions.get(executionId); diff --git a/ccw/src/core/routes/memory-routes.ts b/ccw/src/core/routes/memory-routes.ts index f8a5e54b..c273cf7d 100644 --- a/ccw/src/core/routes/memory-routes.ts +++ b/ccw/src/core/routes/memory-routes.ts @@ -1009,8 +1009,8 @@ RULES: Be concise. Focus on practical understanding. Include function signatures category: 'internal', id: syncId }, (unit) => { - // CliOutputUnit handler: use SmartContentFormatter for intelligent formatting - const content = SmartContentFormatter.format(unit.content, unit.type) || JSON.stringify(unit.content); + // CliOutputUnit handler: use SmartContentFormatter for intelligent formatting (never returns null) + const content = SmartContentFormatter.format(unit.content, unit.type); broadcastToClients({ type: 'CLI_OUTPUT', payload: { diff --git a/ccw/src/core/routes/rules-routes.ts b/ccw/src/core/routes/rules-routes.ts index c3dc2d16..95b4bdbb 100644 --- a/ccw/src/core/routes/rules-routes.ts +++ b/ccw/src/core/routes/rules-routes.ts @@ -663,8 +663,8 @@ FILE NAME: ${fileName}`; // Create onOutput callback for real-time streaming const onOutput = broadcastToClients ? (unit: import('../../tools/cli-output-converter.js').CliOutputUnit) => { - // CliOutputUnit handler: use SmartContentFormatter for intelligent formatting - const content = SmartContentFormatter.format(unit.content, unit.type) || JSON.stringify(unit.content); + // CliOutputUnit handler: use SmartContentFormatter for intelligent formatting (never returns null) + const content = SmartContentFormatter.format(unit.content, unit.type); broadcastToClients({ type: 'CLI_OUTPUT', payload: { @@ -750,8 +750,8 @@ FILE NAME: ${fileName}`; // Create onOutput callback for review step const reviewOnOutput = broadcastToClients ? (unit: import('../../tools/cli-output-converter.js').CliOutputUnit) => { - // CliOutputUnit handler: use SmartContentFormatter for intelligent formatting - const content = SmartContentFormatter.format(unit.content, unit.type) || JSON.stringify(unit.content); + // CliOutputUnit handler: use SmartContentFormatter for intelligent formatting (never returns null) + const content = SmartContentFormatter.format(unit.content, unit.type); broadcastToClients({ type: 'CLI_OUTPUT', payload: { diff --git a/ccw/src/core/routes/skills-routes.ts b/ccw/src/core/routes/skills-routes.ts index c34b62c1..087df59c 100644 --- a/ccw/src/core/routes/skills-routes.ts +++ b/ccw/src/core/routes/skills-routes.ts @@ -581,8 +581,8 @@ Create a new Claude Code skill with the following specifications: // Create onOutput callback for real-time streaming const onOutput = broadcastToClients ? (unit: import('../../tools/cli-output-converter.js').CliOutputUnit) => { - // CliOutputUnit handler: use SmartContentFormatter for intelligent formatting - const content = SmartContentFormatter.format(unit.content, unit.type) || JSON.stringify(unit.content); + // CliOutputUnit handler: use SmartContentFormatter for intelligent formatting (never returns null) + const content = SmartContentFormatter.format(unit.content, unit.type); broadcastToClients({ type: 'CLI_OUTPUT', payload: { diff --git a/ccw/src/tools/cli-output-converter.ts b/ccw/src/tools/cli-output-converter.ts index 6c2d7309..01874a4d 100644 --- a/ccw/src/tools/cli-output-converter.ts +++ b/ccw/src/tools/cli-output-converter.ts @@ -712,43 +712,94 @@ export class JsonLinesParser implements IOutputParser { export class SmartContentFormatter { /** * Format structured content into human-readable text - * Returns formatted string or null if should use original content + * NEVER returns null - always returns displayable content to prevent data loss */ - static format(content: any, type: CliOutputUnitType): string | null { + static format(content: any, type: CliOutputUnitType): string { + // Handle null/undefined + if (content === null || content === undefined) { + return ''; + } + + // String content - return as-is if (typeof content === 'string') { return content; } - if (typeof content !== 'object' || content === null) { + // Primitive types - convert to string + if (typeof content !== 'object') { return String(content); } - // Type-specific formatting + // Type-specific formatting with fallback chain + let result: string | null = null; + switch (type) { case 'metadata': - return this.formatMetadata(content); + result = this.formatMetadata(content); + break; case 'progress': - return this.formatProgress(content); + result = this.formatProgress(content); + break; case 'tool_call': - return this.formatToolCall(content); + result = this.formatToolCall(content); + break; case 'code': - return this.formatCode(content); + result = this.formatCode(content); + break; case 'file_diff': - return this.formatFileDiff(content); + result = this.formatFileDiff(content); + break; case 'thought': - return this.formatThought(content); + result = this.formatThought(content); + break; case 'system': - return this.formatSystem(content); + result = this.formatSystem(content); + break; default: // Try to extract text content from common fields - return this.extractTextContent(content); + result = this.extractTextContent(content); + } + + // If type-specific formatting succeeded, return it + if (result && result.trim()) { + return result; + } + + // Fallback: try to extract any text content regardless of type + const textContent = this.extractTextContent(content); + if (textContent && textContent.trim()) { + return textContent; + } + + // Last resort: format as readable JSON with type hint + return this.formatAsReadableJson(content, type); + } + + /** + * Format object as readable JSON with type hint (fallback for unknown content) + * Ensures content is never lost + */ + private static formatAsReadableJson(content: any, type: CliOutputUnitType): string { + try { + const jsonStr = JSON.stringify(content, null, 0); + // For short content, show inline; for long content, indicate it's data + if (jsonStr.length <= 200) { + return `[${type}] ${jsonStr}`; + } + // For long content, show truncated with type indicator + return `[${type}] ${jsonStr.substring(0, 200)}...`; + } catch { + // If JSON.stringify fails, try to extract keys + const keys = Object.keys(content).slice(0, 5).join(', '); + return `[${type}] {${keys}${Object.keys(content).length > 5 ? ', ...' : ''}}`; } } /** * Format metadata (session info, stats, etc.) + * Returns null if no meaningful metadata could be extracted */ - private static formatMetadata(content: any): string { + private static formatMetadata(content: any): string | null { const parts: string[] = []; // Tool identifier @@ -772,6 +823,11 @@ export class SmartContentFormatter { parts.push(`Status: ${content.status}`); } + // Reason (for step_finish events) + if (content.reason) { + parts.push(`Reason: ${content.reason}`); + } + // Duration if (content.durationMs || content.duration_ms) { const ms = content.durationMs || content.duration_ms; @@ -785,8 +841,8 @@ export class SmartContentFormatter { } // Cost - if (content.totalCostUsd || content.total_cost_usd || content.cost) { - const cost = content.totalCostUsd || content.total_cost_usd || content.cost; + if (content.totalCostUsd !== undefined || content.total_cost_usd !== undefined || content.cost !== undefined) { + const cost = content.totalCostUsd ?? content.total_cost_usd ?? content.cost; parts.push(`Cost: $${typeof cost === 'number' ? cost.toFixed(6) : cost}`); } @@ -795,13 +851,15 @@ export class SmartContentFormatter { parts.push(`Result: ${this.truncate(content.result, 100)}`); } - return parts.length > 0 ? parts.join(' | ') : JSON.stringify(content); + // Return null if no meaningful parts extracted (let fallback handle it) + return parts.length > 0 ? parts.join(' | ') : null; } /** * Format progress updates + * Returns null if no meaningful progress info could be extracted */ - private static formatProgress(content: any): string { + private static formatProgress(content: any): string | null { const parts: string[] = []; // Tool identifier @@ -824,13 +882,14 @@ export class SmartContentFormatter { parts.push(`[${content.progress}/${content.total}]`); } - // Session ID (brief) + // Session ID (brief) - only show if no message (avoid duplication) const sessionId = content.sessionId || content.session_id; if (sessionId && !content.message) { parts.push(`Session: ${this.truncate(sessionId, 12)}`); } - return parts.length > 0 ? parts.join(' ') : JSON.stringify(content); + // Return null if no meaningful parts extracted (let fallback handle it) + return parts.length > 0 ? parts.join(' ') : null; } /** @@ -900,24 +959,26 @@ export class SmartContentFormatter { /** * Format thought/reasoning + * Returns null if no text content could be extracted */ - private static formatThought(content: any): string { + private static formatThought(content: any): string | null { if (typeof content === 'string') { return `💭 ${content}`; } - const text = content.text || content.summary || content.content; - return text ? `💭 ${text}` : JSON.stringify(content); + const text = content.text || content.summary || content.content || content.thinking; + return text ? `💭 ${text}` : null; } /** * Format system message + * Returns null if no message content could be extracted */ - private static formatSystem(content: any): string { + private static formatSystem(content: any): string | null { if (typeof content === 'string') { return `⚙️ ${content}`; } - const message = content.message || content.content || content.event; - return message ? `⚙️ ${message}` : JSON.stringify(content); + const message = content.message || content.content || content.event || content.info; + return message ? `⚙️ ${message}` : null; } /**