Spaces:
Running
Running
| import type { MessageSegment } from "../models/chat-data"; | |
| import type { TodoListView } from "../models/segment-view"; | |
| import { parseTodoList } from "../models/segment-view"; | |
| export class SegmentFormatter { | |
| private static todoListCache: Map<string, TodoListView> = new Map(); | |
| static formatSegmentContent(segment: MessageSegment): string { | |
| switch (segment.type) { | |
| case "text": | |
| return segment.content; | |
| case "reasoning": | |
| return segment.content; | |
| case "tool-invocation": | |
| return this.formatToolInvocation(segment); | |
| case "tool-result": | |
| return this.formatToolResult(segment); | |
| default: | |
| return segment.content; | |
| } | |
| } | |
| static formatToolInvocation(segment: MessageSegment): string { | |
| const args = segment.toolArgs | |
| ? JSON.stringify(segment.toolArgs, null, 2) | |
| : "No arguments"; | |
| return `Tool: ${segment.toolName}\nArguments:\n${args}`; | |
| } | |
| static formatToolResult(segment: MessageSegment): string { | |
| if (segment.toolError) { | |
| return `β Error: ${segment.toolError}`; | |
| } | |
| if (segment.toolName?.includes("task")) { | |
| return this.formatTodoResult(segment); | |
| } | |
| if (segment.toolName === "observe_console") { | |
| return this.formatConsoleOutput(segment); | |
| } | |
| return segment.toolOutput || segment.content || "No output"; | |
| } | |
| static formatTodoResult(segment: MessageSegment): string { | |
| const content = segment.toolOutput || segment.content; | |
| const todoList = parseTodoList(content); | |
| if (todoList) { | |
| this.todoListCache.set(segment.id, todoList); | |
| return this.renderTodoList(todoList); | |
| } | |
| return content; | |
| } | |
| static formatConsoleOutput(segment: MessageSegment): string { | |
| const output = segment.toolOutput || segment.content; | |
| const lines = output.split("\n"); | |
| const formatted = lines | |
| .map((line) => { | |
| if (line.includes("[error]")) { | |
| return `π΄ ${line}`; | |
| } else if (line.includes("[warn]")) { | |
| return `π‘ ${line}`; | |
| } else if (line.includes("[info]")) { | |
| return `π΅ ${line}`; | |
| } else if (line.includes("[debug]")) { | |
| return `βͺ ${line}`; | |
| } | |
| return line; | |
| }) | |
| .join("\n"); | |
| return formatted; | |
| } | |
| static renderTodoList(todoList: TodoListView): string { | |
| const header = `π Tasks (${todoList.completedCount}/${todoList.totalCount} completed)\n`; | |
| const separator = "β".repeat(40) + "\n"; | |
| const tasks = todoList.tasks | |
| .map((task) => `${task.emoji} [${task.id}] ${task.description}`) | |
| .join("\n"); | |
| return header + separator + tasks; | |
| } | |
| static getLatestTodoList(): TodoListView | null { | |
| if (this.todoListCache.size === 0) { | |
| return null; | |
| } | |
| let latest: TodoListView | null = null; | |
| let latestTime = 0; | |
| for (const todoList of this.todoListCache.values()) { | |
| if (todoList.lastUpdated > latestTime) { | |
| latest = todoList; | |
| latestTime = todoList.lastUpdated; | |
| } | |
| } | |
| return latest; | |
| } | |
| static shouldCollapseByDefault(segment: MessageSegment): boolean { | |
| if (segment.type !== "tool-invocation" && segment.type !== "tool-result") { | |
| return false; | |
| } | |
| if (segment.toolError) { | |
| return false; | |
| } | |
| if (segment.toolName?.includes("task")) { | |
| return false; | |
| } | |
| const output = segment.toolOutput || segment.content || ""; | |
| const lineCount = output.split("\n").length; | |
| return lineCount > 10; | |
| } | |
| static getSegmentIcon(segment: MessageSegment): string { | |
| const iconMap: Record<string, string> = { | |
| text: "π¬", | |
| reasoning: "π€", | |
| "tool-invocation": "π§", | |
| "tool-result": "π", | |
| }; | |
| if (segment.toolName) { | |
| const toolIcons: Record<string, string> = { | |
| plan_tasks: "π", | |
| update_task: "βοΈ", | |
| view_tasks: "π", | |
| observe_console: "πΊ", | |
| }; | |
| return toolIcons[segment.toolName] || iconMap[segment.type] || "π"; | |
| } | |
| return iconMap[segment.type] || "π"; | |
| } | |
| static formatDuration(ms: number): string { | |
| if (ms < 1000) { | |
| return `${ms}ms`; | |
| } else if (ms < 60000) { | |
| return `${(ms / 1000).toFixed(1)}s`; | |
| } else { | |
| const minutes = Math.floor(ms / 60000); | |
| const seconds = Math.floor((ms % 60000) / 1000); | |
| return `${minutes}m ${seconds}s`; | |
| } | |
| } | |
| static truncateContent(content: string, maxLength: number = 100): string { | |
| if (content.length <= maxLength) { | |
| return content; | |
| } | |
| const truncated = content.substring(0, maxLength); | |
| const lastSpace = truncated.lastIndexOf(" "); | |
| if (lastSpace > maxLength * 0.8) { | |
| return truncated.substring(0, lastSpace) + "..."; | |
| } | |
| return truncated + "..."; | |
| } | |
| } | |
| export const segmentFormatter = new SegmentFormatter(); | |