Skip to content

Commit 62677ca

Browse files
committed
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. Previously agents showed only 'Initializing' or a generic sweep animation. Status messages now 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 fixes: - Text span out-of-bounds crash when merging Rich Text renderables - Thinking blocks now rendered in chat history from metadata - Indented thought display in ThinkRenderer
1 parent 5d91500 commit 62677ca

5 files changed

Lines changed: 114 additions & 13 deletions

File tree

strix/agents/base_agent.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,13 @@ async def _initialize_sandbox_and_state(self, task: str) -> None:
321321
sandbox_mode = os.getenv("STRIX_SANDBOX_MODE", "false").lower() == "true"
322322
if not sandbox_mode and self.state.sandbox_id is None:
323323
from strix.runtime import get_runtime
324+
from strix.telemetry.tracer import get_global_tracer
325+
326+
tracer = get_global_tracer()
327+
if tracer:
328+
tracer.update_agent_system_message(
329+
self.state.agent_id, "Setting up sandbox environment..."
330+
)
324331

325332
try:
326333
runtime = get_runtime()
@@ -355,6 +362,9 @@ async def _initialize_sandbox_and_state(self, task: str) -> None:
355362
async def _process_iteration(self, tracer: Optional["Tracer"]) -> bool:
356363
final_response = None
357364

365+
if tracer:
366+
tracer.update_agent_system_message(self.state.agent_id, "Thinking...")
367+
358368
async for response in self.llm.generate(self.state.get_conversation_history()):
359369
final_response = response
360370
if tracer and response.content:
@@ -396,8 +406,19 @@ async def _process_iteration(self, tracer: Optional["Tracer"]) -> bool:
396406
)
397407

398408
if actions:
409+
if tracer:
410+
tool_names = [a.get("toolName") or a.get("tool_name") or "tool" for a in actions]
411+
display_names = tool_names[:2]
412+
overflow = len(tool_names) - 2
413+
suffix = f" +{overflow} more" if overflow > 0 else ""
414+
tracer.update_agent_system_message(
415+
self.state.agent_id, f"Executing {', '.join(display_names)}{suffix}..."
416+
)
399417
return await self._execute_actions(actions, tracer)
400418

419+
if tracer:
420+
tracer.update_agent_system_message(self.state.agent_id, "Processing response...")
421+
401422
return False
402423

403424
async def _execute_actions(self, actions: list[Any], tracer: Optional["Tracer"]) -> bool:

strix/interface/tool_components/thinking_renderer.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ def render(cls, tool_data: dict[str, Any]) -> Static:
2323
text.append("\n ")
2424

2525
if thought:
26-
text.append(thought, style="italic dim")
26+
indented_thought = "\n ".join(thought.split("\n"))
27+
text.append(indented_thought, style="italic dim")
2728
else:
2829
text.append("Thinking...", style="italic dim")
2930

strix/interface/tui.py

Lines changed: 66 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1036,13 +1036,39 @@ def _merge_renderables(renderables: list[Any]) -> Text:
10361036
if i > 0:
10371037
combined.append("\n")
10381038
StrixTUIApp._append_renderable(combined, item)
1039-
return combined
1039+
return StrixTUIApp._sanitize_text_spans(combined)
1040+
1041+
@staticmethod
1042+
def _sanitize_text_spans(text: Text) -> Text:
1043+
plain = text.plain
1044+
plain_len = len(plain)
1045+
1046+
if plain_len == 0 or not text.spans:
1047+
return text
1048+
1049+
sanitized = Text(
1050+
plain,
1051+
style=text.style,
1052+
justify=text.justify,
1053+
overflow=text.overflow,
1054+
no_wrap=text.no_wrap,
1055+
end=text.end,
1056+
tab_size=text.tab_size,
1057+
)
1058+
1059+
for span in text.spans:
1060+
start = max(0, min(span.start, plain_len))
1061+
end = max(0, min(span.end, plain_len))
1062+
if end > start:
1063+
sanitized.stylize(span.style, start, end)
1064+
1065+
return sanitized
10401066

10411067
@staticmethod
10421068
def _append_renderable(combined: Text, item: Any) -> None:
10431069
"""Recursively append a renderable's text content to a combined Text."""
10441070
if isinstance(item, Text):
1045-
combined.append_text(item)
1071+
combined.append_text(StrixTUIApp._sanitize_text_spans(item))
10461072
elif isinstance(item, Group):
10471073
for j, sub in enumerate(item.renderables):
10481074
if j > 0:
@@ -1087,7 +1113,7 @@ def _get_rendered_events_content(self, events: list[dict[str, Any]]) -> Any:
10871113
return Text()
10881114

10891115
if len(renderables) == 1 and isinstance(renderables[0], Text):
1090-
return renderables[0]
1116+
return self._sanitize_text_spans(renderables[0])
10911117

10921118
return self._merge_renderables(renderables)
10931119

@@ -1123,7 +1149,7 @@ def _render_streaming_content(self, content: str, agent_id: str | None = None) -
11231149
if not renderables:
11241150
result = Text()
11251151
elif len(renderables) == 1 and isinstance(renderables[0], Text):
1126-
result = renderables[0]
1152+
result = self._sanitize_text_spans(renderables[0])
11271153
else:
11281154
result = self._merge_renderables(renderables)
11291155

@@ -1215,14 +1241,19 @@ def keymap_styled(keys: list[tuple[str, str]]) -> Text:
12151241
return (Text(" "), keymap, False)
12161242

12171243
if status == "running":
1244+
sys_msg = agent_data.get("system_message", "")
12181245
if self._agent_has_real_activity(agent_id):
12191246
animated_text = Text()
12201247
animated_text.append_text(self._get_sweep_animation(self._sweep_colors))
1248+
if sys_msg:
1249+
animated_text.append(sys_msg, style="dim italic")
1250+
animated_text.append(" ", style="dim")
12211251
animated_text.append("esc", style="white")
12221252
animated_text.append(" ", style="dim")
12231253
animated_text.append("stop", style="dim")
12241254
return (animated_text, keymap_styled([("ctrl-q", "quit")]), True)
1225-
animated_text = self._get_animated_verb_text(agent_id, "Initializing")
1255+
msg = sys_msg or "Initializing..."
1256+
animated_text = self._get_animated_verb_text(agent_id, msg)
12261257
return (animated_text, keymap_styled([("ctrl-q", "quit")]), True)
12271258

12281259
return (None, Text(), False)
@@ -1394,7 +1425,7 @@ def _animate_dots(self) -> None:
13941425
if not has_active_agents:
13951426
has_active_agents = any(
13961427
agent_data.get("status", "running") in ["running", "waiting"]
1397-
for agent_data in self.tracer.agents.values()
1428+
for agent_data in list(self.tracer.agents.values())
13981429
)
13991430

14001431
if not has_active_agents:
@@ -1655,12 +1686,26 @@ def _render_chat_content(self, msg_data: dict[str, Any]) -> Any:
16551686
content = msg_data.get("content", "")
16561687
metadata = msg_data.get("metadata", {})
16571688

1658-
if not content:
1659-
return None
1660-
16611689
if role == "user":
1690+
if not content:
1691+
return None
16621692
return UserMessageRenderer.render_simple(content)
16631693

1694+
renderables = []
1695+
1696+
if "thinking_blocks" in metadata and metadata["thinking_blocks"]:
1697+
from strix.interface.tool_components.thinking_renderer import ThinkRenderer
1698+
1699+
for block in metadata["thinking_blocks"]:
1700+
thought = block.get("thinking", "")
1701+
if thought:
1702+
renderables.append(
1703+
ThinkRenderer.render({"args": {"thought": thought}}).renderable
1704+
)
1705+
1706+
if not content and not renderables:
1707+
return None
1708+
16641709
if metadata.get("interrupted"):
16651710
streaming_result = self._render_streaming_content(content)
16661711
interrupted_text = Text()
@@ -1669,7 +1714,18 @@ def _render_chat_content(self, msg_data: dict[str, Any]) -> Any:
16691714
interrupted_text.append("Interrupted by user", style="yellow dim")
16701715
return self._merge_renderables([streaming_result, interrupted_text])
16711716

1672-
return AgentMessageRenderer.render_simple(content)
1717+
if content:
1718+
msg_renderable = AgentMessageRenderer.render_simple(content)
1719+
renderables.append(msg_renderable)
1720+
1721+
if not renderables:
1722+
return None
1723+
1724+
if len(renderables) == 1:
1725+
r = renderables[0]
1726+
return self._sanitize_text_spans(r) if isinstance(r, Text) else r
1727+
1728+
return self._merge_renderables(renderables)
16731729

16741730
def _render_tool_content_simple(self, tool_data: dict[str, Any]) -> Any:
16751731
tool_name = tool_data.get("tool_name", "Unknown Tool")

strix/llm/llm.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,12 +112,21 @@ def set_agent_identity(self, agent_name: str | None, agent_id: str | None) -> No
112112
async def generate(
113113
self, conversation_history: list[dict[str, Any]]
114114
) -> AsyncIterator[LLMResponse]:
115+
from strix.telemetry.tracer import get_global_tracer
116+
117+
tracer = get_global_tracer()
118+
if tracer and self.agent_id:
119+
tracer.update_agent_system_message(self.agent_id, "Compressing memory...")
120+
115121
messages = self._prepare_messages(conversation_history)
116122
max_retries = int(Config.get("strix_llm_max_retries") or "5")
117123

118124
for attempt in range(max_retries + 1):
119125
try:
120-
async for response in self._stream(messages):
126+
if tracer and self.agent_id:
127+
tracer.update_agent_system_message(self.agent_id, "Waiting for LLM provider...")
128+
129+
async for response in self._stream(messages, tracer):
121130
yield response
122131
return # noqa: TRY300
123132
except Exception as e: # noqa: BLE001
@@ -126,15 +135,23 @@ async def generate(
126135
wait = min(10, 2 * (2**attempt))
127136
await asyncio.sleep(wait)
128137

129-
async def _stream(self, messages: list[dict[str, Any]]) -> AsyncIterator[LLMResponse]:
138+
async def _stream(
139+
self, messages: list[dict[str, Any]], tracer: Any = None
140+
) -> AsyncIterator[LLMResponse]:
130141
accumulated = ""
131142
chunks: list[Any] = []
132143
done_streaming = 0
144+
first_chunk_received = False
133145

134146
self._total_stats.requests += 1
135147
response = await acompletion(**self._build_completion_args(messages), stream=True)
136148

137149
async for chunk in response:
150+
if not first_chunk_received:
151+
first_chunk_received = True
152+
if tracer and self.agent_id:
153+
tracer.update_agent_system_message(self.agent_id, "Generating response...")
154+
138155
chunks.append(chunk)
139156
if done_streaming:
140157
done_streaming += 1

strix/telemetry/tracer.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@ def log_agent_creation(
189189
"name": name,
190190
"task": task,
191191
"status": "running",
192+
"system_message": "",
192193
"parent_id": parent_id,
193194
"created_at": datetime.now(UTC).isoformat(),
194195
"updated_at": datetime.now(UTC).isoformat(),
@@ -260,6 +261,11 @@ def update_agent_status(
260261
if error_message:
261262
self.agents[agent_id]["error_message"] = error_message
262263

264+
def update_agent_system_message(self, agent_id: str, message: str) -> None:
265+
if agent_id in self.agents:
266+
self.agents[agent_id]["system_message"] = message
267+
self.agents[agent_id]["updated_at"] = datetime.now(UTC).isoformat()
268+
263269
def set_scan_config(self, config: dict[str, Any]) -> None:
264270
self.scan_config = config
265271
self.run_metadata.update(

0 commit comments

Comments
 (0)