From b9474d5e3384a51ed4cd90fea13a950a81affc78 Mon Sep 17 00:00:00 2001 From: 0xhis <0xhis@users.noreply.github.com> Date: Sat, 21 Mar 2026 01:14:49 -0700 Subject: [PATCH 1/2] feat(ui): add live status updates during agent execution Add real-time status messages to the TUI showing what each agent is doing at any given moment. Status messages shown: - 'Compressing memory...' during conversation history preparation - 'Waiting for LLM provider...' during API call setup - 'Generating response...' after first chunk received - 'Executing {tool1}, {tool2} +N more...' during tool execution - 'Setting up sandbox environment...' during sandbox init Also renders thinking blocks in chat history from metadata and fixes indented thought display for multi-line thoughts in ThinkRenderer. --- strix/agents/base_agent.py | 21 ++++++++++ .../tool_components/thinking_renderer.py | 3 +- strix/interface/tui.py | 40 ++++++++++++++++--- strix/llm/llm.py | 21 +++++++++- strix/telemetry/tracer.py | 7 ++++ 5 files changed, 84 insertions(+), 8 deletions(-) diff --git a/strix/agents/base_agent.py b/strix/agents/base_agent.py index 74fe21ef..c963d803 100644 --- a/strix/agents/base_agent.py +++ b/strix/agents/base_agent.py @@ -333,6 +333,13 @@ class BaseAgent(metaclass=AgentMeta): sandbox_mode = os.getenv("STRIX_SANDBOX_MODE", "false").lower() == "true" if not sandbox_mode and self.state.sandbox_id is None: from strix.runtime import get_runtime + from strix.telemetry.tracer import get_global_tracer + + tracer = get_global_tracer() + if tracer: + tracer.update_agent_system_message( + self.state.agent_id, "Setting up sandbox environment..." + ) try: runtime = get_runtime() @@ -367,6 +374,9 @@ class BaseAgent(metaclass=AgentMeta): async def _process_iteration(self, tracer: Optional["Tracer"]) -> bool | None: final_response = None + if tracer: + tracer.update_agent_system_message(self.state.agent_id, "Thinking...") + async for response in self.llm.generate(self.state.get_conversation_history()): final_response = response if tracer and response.content: @@ -408,8 +418,19 @@ class BaseAgent(metaclass=AgentMeta): ) if actions: + if tracer: + tool_names = [a.get("toolName") or a.get("tool_name") or "tool" for a in actions] + display_names = tool_names[:2] + overflow = len(tool_names) - 2 + suffix = f" +{overflow} more" if overflow > 0 else "" + tracer.update_agent_system_message( + self.state.agent_id, f"Executing {', '.join(display_names)}{suffix}..." + ) return await self._execute_actions(actions, tracer) + if tracer: + tracer.update_agent_system_message(self.state.agent_id, "Processing response...") + return None async def _execute_actions(self, actions: list[Any], tracer: Optional["Tracer"]) -> bool: diff --git a/strix/interface/tool_components/thinking_renderer.py b/strix/interface/tool_components/thinking_renderer.py index 598bdf33..bbcc64cc 100644 --- a/strix/interface/tool_components/thinking_renderer.py +++ b/strix/interface/tool_components/thinking_renderer.py @@ -23,7 +23,8 @@ class ThinkRenderer(BaseToolRenderer): text.append("\n ") if thought: - text.append(thought, style="italic dim") + indented_thought = "\n ".join(thought.split("\n")) + text.append(indented_thought, style="italic dim") else: text.append("Thinking...", style="italic dim") diff --git a/strix/interface/tui.py b/strix/interface/tui.py index f3665621..6273919a 100644 --- a/strix/interface/tui.py +++ b/strix/interface/tui.py @@ -1238,14 +1238,19 @@ class StrixTUIApp(App): # type: ignore[misc] return (Text(" "), keymap, False) if status == "running": + sys_msg = agent_data.get("system_message", "") if self._agent_has_real_activity(agent_id): animated_text = Text() animated_text.append_text(self._get_sweep_animation(self._sweep_colors)) + if sys_msg: + animated_text.append(sys_msg, style="dim italic") + animated_text.append(" ", style="dim") animated_text.append("esc", style="white") animated_text.append(" ", style="dim") animated_text.append("stop", style="dim") return (animated_text, keymap_styled([("ctrl-q", "quit")]), True) - animated_text = self._get_animated_verb_text(agent_id, "Initializing") + msg = sys_msg or "Initializing..." + animated_text = self._get_animated_verb_text(agent_id, msg) return (animated_text, keymap_styled([("ctrl-q", "quit")]), True) return (None, Text(), False) @@ -1678,12 +1683,26 @@ class StrixTUIApp(App): # type: ignore[misc] content = msg_data.get("content", "") metadata = msg_data.get("metadata", {}) - if not content: - return None - if role == "user": + if not content: + return None return UserMessageRenderer.render_simple(content) + renderables = [] + + if "thinking_blocks" in metadata and metadata["thinking_blocks"]: + from strix.interface.tool_components.thinking_renderer import ThinkRenderer + + for block in metadata["thinking_blocks"]: + thought = block.get("thinking", "") + if thought: + renderables.append( + ThinkRenderer.render({"args": {"thought": thought}}).renderable + ) + + if not content and not renderables: + return None + if metadata.get("interrupted"): streaming_result = self._render_streaming_content(content) interrupted_text = Text() @@ -1692,7 +1711,18 @@ class StrixTUIApp(App): # type: ignore[misc] interrupted_text.append("Interrupted by user", style="yellow dim") return self._merge_renderables([streaming_result, interrupted_text]) - return AgentMessageRenderer.render_simple(content) + if content: + msg_renderable = AgentMessageRenderer.render_simple(content) + renderables.append(msg_renderable) + + if not renderables: + return None + + if len(renderables) == 1: + r = renderables[0] + return self._sanitize_text(r) if isinstance(r, Text) else r + + return self._merge_renderables(renderables) def _render_tool_content_simple(self, tool_data: dict[str, Any]) -> Any: tool_name = tool_data.get("tool_name", "Unknown Tool") diff --git a/strix/llm/llm.py b/strix/llm/llm.py index fe1758f1..711f372f 100644 --- a/strix/llm/llm.py +++ b/strix/llm/llm.py @@ -141,12 +141,21 @@ class LLM: async def generate( self, conversation_history: list[dict[str, Any]] ) -> AsyncIterator[LLMResponse]: + from strix.telemetry.tracer import get_global_tracer + + tracer = get_global_tracer() + if tracer and self.agent_id: + tracer.update_agent_system_message(self.agent_id, "Compressing memory...") + messages = self._prepare_messages(conversation_history) max_retries = int(Config.get("strix_llm_max_retries") or "5") for attempt in range(max_retries + 1): try: - async for response in self._stream(messages): + if tracer and self.agent_id: + tracer.update_agent_system_message(self.agent_id, "Waiting for LLM provider...") + + async for response in self._stream(messages, tracer): yield response return # noqa: TRY300 except Exception as e: # noqa: BLE001 @@ -155,15 +164,23 @@ class LLM: wait = min(10, 2 * (2**attempt)) await asyncio.sleep(wait) - async def _stream(self, messages: list[dict[str, Any]]) -> AsyncIterator[LLMResponse]: + async def _stream( + self, messages: list[dict[str, Any]], tracer: Any = None + ) -> AsyncIterator[LLMResponse]: accumulated = "" chunks: list[Any] = [] done_streaming = 0 + first_chunk_received = False self._total_stats.requests += 1 response = await acompletion(**self._build_completion_args(messages), stream=True) async for chunk in response: + if not first_chunk_received: + first_chunk_received = True + if tracer and self.agent_id: + tracer.update_agent_system_message(self.agent_id, "Generating response...") + chunks.append(chunk) if done_streaming: done_streaming += 1 diff --git a/strix/telemetry/tracer.py b/strix/telemetry/tracer.py index bde9750a..42385cc3 100644 --- a/strix/telemetry/tracer.py +++ b/strix/telemetry/tracer.py @@ -36,6 +36,7 @@ _OTEL_BOOTSTRAP_LOCK = threading.Lock() _OTEL_BOOTSTRAPPED = False _OTEL_REMOTE_ENABLED = False + def get_global_tracer() -> Optional["Tracer"]: return _global_tracer @@ -437,6 +438,7 @@ class Tracer: "name": name, "task": task, "status": "running", + "system_message": "", "parent_id": parent_id, "created_at": datetime.now(UTC).isoformat(), "updated_at": datetime.now(UTC).isoformat(), @@ -585,6 +587,11 @@ class Tracer: source="strix.agents", ) + def update_agent_system_message(self, agent_id: str, message: str) -> None: + if agent_id in self.agents: + self.agents[agent_id]["system_message"] = message + self.agents[agent_id]["updated_at"] = datetime.now(UTC).isoformat() + def set_scan_config(self, config: dict[str, Any]) -> None: self.scan_config = config self.run_metadata.update( From 7a4c00872b18726a46442572785c9c6214c022cb Mon Sep 17 00:00:00 2001 From: 0xhis <0xhis@users.noreply.github.com> Date: Sat, 21 Mar 2026 01:31:16 -0700 Subject: [PATCH 2/2] fix(ui): include thinking blocks in interrupted message render --- strix/interface/tui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/strix/interface/tui.py b/strix/interface/tui.py index 6273919a..38284a89 100644 --- a/strix/interface/tui.py +++ b/strix/interface/tui.py @@ -1709,7 +1709,7 @@ class StrixTUIApp(App): # type: ignore[misc] interrupted_text.append("\n") interrupted_text.append("⚠ ", style="yellow") interrupted_text.append("Interrupted by user", style="yellow dim") - return self._merge_renderables([streaming_result, interrupted_text]) + return self._merge_renderables([*renderables, streaming_result, interrupted_text]) if content: msg_renderable = AgentMessageRenderer.render_simple(content)