"""Transcript segment types for messaging UI output.""" import json from abc import ABC, abstractmethod from dataclasses import dataclass, field from typing import Any from .context import RenderCtx def safe_json_dumps(obj: Any) -> str: try: return json.dumps(obj, indent=2, ensure_ascii=False, sort_keys=True) except Exception: return str(obj) @dataclass class Segment(ABC): kind: str @abstractmethod def render(self, ctx: RenderCtx) -> str: ... @dataclass class ThinkingSegment(Segment): def __init__(self) -> None: super().__init__(kind="thinking") self._parts: list[str] = [] def append(self, text: str) -> None: if text: self._parts.append(text) @property def text(self) -> str: return "".join(self._parts) def render(self, ctx: RenderCtx) -> str: raw = self.text or "" if ctx.thinking_tail_max is not None and len(raw) > ctx.thinking_tail_max: raw = "..." + raw[-(ctx.thinking_tail_max - 3) :] inner = ctx.escape_code(raw) return f"💭 {ctx.bold('Thinking')}\n```\n{inner}\n```" @dataclass class TextSegment(Segment): def __init__(self) -> None: super().__init__(kind="text") self._parts: list[str] = [] def append(self, text: str) -> None: if text: self._parts.append(text) @property def text(self) -> str: return "".join(self._parts) def render(self, ctx: RenderCtx) -> str: raw = self.text or "" if ctx.text_tail_max is not None and len(raw) > ctx.text_tail_max: raw = "..." + raw[-(ctx.text_tail_max - 3) :] return ctx.render_markdown(raw) @dataclass class ToolCallSegment(Segment): tool_use_id: str name: str closed: bool = False indent_level: int = 0 def __init__(self, tool_use_id: str, name: str, *, indent_level: int = 0) -> None: super().__init__(kind="tool_call") self.tool_use_id = str(tool_use_id or "") self.name = str(name or "tool") self.closed = False self.indent_level = max(0, int(indent_level)) def render(self, ctx: RenderCtx) -> str: name = ctx.code_inline(self.name) prefix = " " * self.indent_level return f"{prefix}🛠 {ctx.bold('Tool call:')} {name}" @dataclass class ToolResultSegment(Segment): tool_use_id: str name: str | None content_text: str is_error: bool = False def __init__( self, tool_use_id: str, content: Any, *, name: str | None = None, is_error: bool = False, ) -> None: super().__init__(kind="tool_result") self.tool_use_id = str(tool_use_id or "") self.name = str(name) if name is not None else None self.is_error = bool(is_error) self.content_text = ( content if isinstance(content, str) else safe_json_dumps(content) ) def render(self, ctx: RenderCtx) -> str: raw = self.content_text or "" if ctx.tool_output_tail_max is not None and len(raw) > ctx.tool_output_tail_max: raw = "..." + raw[-(ctx.tool_output_tail_max - 3) :] inner = ctx.escape_code(raw) label = "Tool error:" if self.is_error else "Tool result:" maybe_name = f" {ctx.code_inline(self.name)}" if self.name else "" return f"📤 {ctx.bold(label)}{maybe_name}\n```\n{inner}\n```" @dataclass class SubagentSegment(Segment): description: str tool_calls: int = 0 tools_used: set[str] = field(default_factory=set) current_tool: ToolCallSegment | None = None def __init__(self, description: str) -> None: super().__init__(kind="subagent") self.description = str(description or "Subagent") self.tool_calls = 0 self.tools_used = set() self.current_tool = None def set_current_tool_call(self, tool_use_id: str, name: str) -> ToolCallSegment: tool_use_id = str(tool_use_id or "") name = str(name or "tool") self.tools_used.add(name) self.tool_calls += 1 self.current_tool = ToolCallSegment(tool_use_id, name, indent_level=1) return self.current_tool def render(self, ctx: RenderCtx) -> str: inner_prefix = " " lines = [f"🤖 {ctx.bold('Subagent:')} {ctx.code_inline(self.description)}"] if self.current_tool is not None: try: rendered = self.current_tool.render(ctx) except Exception: rendered = "" if rendered: lines.append(rendered) tools_used = sorted(self.tools_used) tools_set_raw = "{{{}}}".format(", ".join(tools_used)) if tools_used else "{}" lines.append( f"{inner_prefix}{ctx.bold('Tools used:')} {ctx.code_inline(tools_set_raw)}" ) lines.append( f"{inner_prefix}{ctx.bold('Tool calls:')} {ctx.code_inline(str(self.tool_calls))}" ) return "\n".join(lines) @dataclass class ErrorSegment(Segment): message: str def __init__(self, message: str) -> None: super().__init__(kind="error") self.message = str(message or "Unknown error") def render(self, ctx: RenderCtx) -> str: return f"⚠️ {ctx.bold('Error:')} {ctx.code_inline(self.message)}"