"""Transcript event application and open-block tracking.""" from __future__ import annotations from typing import Any from .context import RenderCtx from .renderer import render_segments from .segments import ( ErrorSegment, Segment, SubagentSegment, TextSegment, ThinkingSegment, ToolCallSegment, ToolResultSegment, ) from .subagents import SubagentState, task_heading_from_input _SUBAGENT_SUPPRESSED_EVENTS = frozenset( { "thinking_start", "thinking_delta", "thinking_chunk", "text_start", "text_delta", "text_chunk", } ) class TranscriptBuffer: """Maintains an ordered, truncatable transcript of parsed CLI events.""" def __init__( self, *, show_tool_results: bool = True, debug_subagent_stack: bool = False, ) -> None: self._segments: list[Segment] = [] self._open_thinking_by_index: dict[int, ThinkingSegment] = {} self._open_text_by_index: dict[int, TextSegment] = {} self._open_tools_by_index: dict[int, ToolCallSegment] = {} self._tool_name_by_id: dict[str, str] = {} self._show_tool_results = bool(show_tool_results) self._subagents = SubagentState(debug=debug_subagent_stack) def apply(self, event: dict[str, Any]) -> None: """Apply a parsed CLI transcript event.""" event_type = event.get("type") if self._subagents.in_subagent() and event_type in _SUBAGENT_SUPPRESSED_EVENTS: return if event_type == "thinking_start": self._start_thinking(_event_index(event)) return if event_type in ("thinking_delta", "thinking_chunk"): self._append_thinking(_event_index(event), str(event.get("text", ""))) return if event_type == "thinking_stop": self._open_thinking_by_index.pop(_event_index(event), None) return if event_type == "text_start": self._start_text(_event_index(event)) return if event_type in ("text_delta", "text_chunk"): self._append_text(_event_index(event), str(event.get("text", ""))) return if event_type == "text_stop": self._open_text_by_index.pop(_event_index(event), None) return if event_type == "tool_use_start": self._start_tool_use(event) return if event_type == "tool_use_delta": return if event_type == "tool_use_stop": segment = self._open_tools_by_index.pop(_event_index(event), None) if segment is not None: segment.closed = True return if event_type == "block_stop": self._close_block(_event_index(event)) return if event_type == "tool_use": self._append_complete_tool_use(event) return if event_type == "tool_result": self._append_tool_result(event) return if event_type == "error": self._segments.append(ErrorSegment(str(event.get("message", "")))) def render(self, ctx: RenderCtx, *, limit_chars: int, status: str | None) -> str: return render_segments( self._segments, ctx, limit_chars=limit_chars, status=status, ) def _start_thinking(self, index: int) -> None: if index >= 0: self._close_block(index) segment = ThinkingSegment() self._segments.append(segment) if index >= 0: self._open_thinking_by_index[index] = segment def _append_thinking(self, index: int, text: str) -> None: segment = self._open_thinking_by_index.get(index) if segment is None: segment = ThinkingSegment() self._segments.append(segment) if index >= 0: self._open_thinking_by_index[index] = segment segment.append(text) def _start_text(self, index: int) -> None: if index >= 0: self._close_block(index) segment = TextSegment() self._segments.append(segment) if index >= 0: self._open_text_by_index[index] = segment def _append_text(self, index: int, text: str) -> None: segment = self._open_text_by_index.get(index) if segment is None: segment = TextSegment() self._segments.append(segment) if index >= 0: self._open_text_by_index[index] = segment segment.append(text) def _start_tool_use(self, event: dict[str, Any]) -> None: index = _event_index(event) if index >= 0: self._close_block(index) tool_id = _event_tool_id(event, "id") name = str(event.get("name", "") or "tool") if tool_id: self._tool_name_by_id[tool_id] = name if name == "Task": segment = SubagentSegment(task_heading_from_input(event.get("input"))) self._segments.append(segment) self._subagents.push(tool_id, segment) return segment = self._append_tool_call(tool_id, name) if index >= 0: self._open_tools_by_index[index] = segment def _append_complete_tool_use(self, event: dict[str, Any]) -> None: tool_id = _event_tool_id(event, "id") name = str(event.get("name", "") or "tool") if tool_id: self._tool_name_by_id[tool_id] = name if name == "Task": segment = SubagentSegment(task_heading_from_input(event.get("input"))) self._segments.append(segment) self._subagents.push(tool_id, segment) return segment = self._append_tool_call(tool_id, name) segment.closed = True def _append_tool_call(self, tool_id: str, name: str) -> ToolCallSegment: if self._subagents.in_subagent(): parent = self._subagents.current_segment() if parent is not None: return parent.set_current_tool_call(tool_id, name) segment = ToolCallSegment(tool_id, name) self._segments.append(segment) return segment def _append_tool_result(self, event: dict[str, Any]) -> None: tool_id = _event_tool_id(event, "tool_use_id") name = self._tool_name_by_id.get(tool_id) if self._subagents.in_subagent(): self._subagents.close_for_tool_result(tool_id, tool_name=name) if not self._show_tool_results: return self._segments.append( ToolResultSegment( tool_id, event.get("content"), name=name, is_error=bool(event.get("is_error", False)), ) ) def _close_block(self, index: int) -> None: if index in self._open_tools_by_index: segment = self._open_tools_by_index.pop(index, None) if segment is not None: segment.closed = True return if index in self._open_thinking_by_index: self._open_thinking_by_index.pop(index, None) return if index in self._open_text_by_index: self._open_text_by_index.pop(index, None) def _event_index(event: dict[str, Any]) -> int: return int(event.get("index", -1)) def _event_tool_id(event: dict[str, Any], key: str) -> str: return str(event.get(key, "") or "").strip()