mirror of
https://github.com/Alishahryar1/free-claude-code.git
synced 2026-07-03 14:05:26 +02:00
179 lines
5.3 KiB
Python
179 lines
5.3 KiB
Python
"""Transcript segment types for messaging UI output."""
|
|
|
|
from __future__ import annotations
|
|
|
|
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)}"
|