soothe-cli 0.5.5__tar.gz → 0.5.7__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/PKG-INFO +1 -1
  2. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/cli/execution/headless.py +1 -1
  3. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/shared/core/event_processor.py +1 -1
  4. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/shared/core/presentation_engine.py +1 -1
  5. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/tui/app/_execution.py +17 -3
  6. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/tui/textual_adapter/_turn.py +1 -1
  7. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/tui/widgets/loading.py +11 -3
  8. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/tui/widgets/messages.py +66 -9
  9. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/.gitignore +0 -0
  10. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/README.md +0 -0
  11. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/pyproject.toml +0 -0
  12. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/__init__.py +0 -0
  13. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/cli/__init__.py +0 -0
  14. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/cli/commands/__init__.py +0 -0
  15. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/cli/commands/autopilot_cmd.py +0 -0
  16. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/cli/commands/loop_cmd.py +0 -0
  17. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/cli/commands/run_cmd.py +0 -0
  18. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/cli/execution/__init__.py +0 -0
  19. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/cli/execution/daemon.py +0 -0
  20. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/cli/execution/headless_renderer.py +0 -0
  21. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/cli/execution/launcher.py +0 -0
  22. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/cli/main.py +0 -0
  23. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/cli/stream/__init__.py +0 -0
  24. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/cli/stream/context.py +0 -0
  25. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/cli/stream/display_line.py +0 -0
  26. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/cli/stream/formatter.py +0 -0
  27. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/cli/stream/pipeline.py +0 -0
  28. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/cli/stream/task_scope.py +0 -0
  29. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/config/__init__.py +0 -0
  30. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/config/cli_config.py +0 -0
  31. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/plan/__init__.py +0 -0
  32. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/plan/rich_tree.py +0 -0
  33. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/shared/__init__.py +0 -0
  34. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/shared/commands/__init__.py +0 -0
  35. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/shared/commands/command_router.py +0 -0
  36. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/shared/commands/slash_commands.py +0 -0
  37. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/shared/commands/subagent_routing.py +0 -0
  38. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/shared/config_loader.py +0 -0
  39. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/shared/core/__init__.py +0 -0
  40. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/shared/core/processor_state.py +0 -0
  41. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/shared/core/renderer_protocol.py +0 -0
  42. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/shared/duration_format.py +0 -0
  43. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/shared/events/__init__.py +0 -0
  44. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/shared/events/display_policy.py +0 -0
  45. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/shared/events/essential_events.py +0 -0
  46. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/shared/events/explore_task_display.py +0 -0
  47. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/shared/events/stream_accumulator.py +0 -0
  48. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/shared/events/tui_trace_log.py +0 -0
  49. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/shared/rendering/__init__.py +0 -0
  50. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/shared/rendering/async_renderer_protocol.py +0 -0
  51. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/shared/rendering/renderer_base.py +0 -0
  52. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/shared/tools/__init__.py +0 -0
  53. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/shared/tools/_utils.py +0 -0
  54. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/shared/tools/message_processing.py +0 -0
  55. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/shared/tools/rendering.py +0 -0
  56. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/shared/tools/tool_call_resolution.py +0 -0
  57. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/shared/tools/tool_card_payload.py +0 -0
  58. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/shared/tools/tool_card_visibility.py +0 -0
  59. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/shared/tools/tool_formatters/__init__.py +0 -0
  60. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/shared/tools/tool_formatters/base.py +0 -0
  61. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/shared/tools/tool_formatters/execution.py +0 -0
  62. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/shared/tools/tool_formatters/fallback.py +0 -0
  63. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/shared/tools/tool_formatters/file_ops.py +0 -0
  64. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/shared/tools/tool_formatters/goal_formatter.py +0 -0
  65. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/shared/tools/tool_formatters/media.py +0 -0
  66. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/shared/tools/tool_formatters/structured.py +0 -0
  67. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/shared/tools/tool_formatters/subagent.py +0 -0
  68. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/shared/tools/tool_formatters/web.py +0 -0
  69. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/shared/tools/tool_message_format.py +0 -0
  70. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/shared/tools/tool_output_formatter.py +0 -0
  71. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/tui/__init__.py +0 -0
  72. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/tui/_ask_user_types.py +0 -0
  73. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/tui/_cli_context.py +0 -0
  74. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/tui/_env_vars.py +0 -0
  75. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/tui/_session_stats.py +0 -0
  76. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/tui/_version.py +0 -0
  77. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/tui/app/__init__.py +0 -0
  78. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/tui/app/_app.py +0 -0
  79. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/tui/app/_commands.py +0 -0
  80. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/tui/app/_history.py +0 -0
  81. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/tui/app/_messages_mixin.py +0 -0
  82. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/tui/app/_model.py +0 -0
  83. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/tui/app/_module_init.py +0 -0
  84. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/tui/app/_startup.py +0 -0
  85. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/tui/app/_ui.py +0 -0
  86. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/tui/app/app.tcss +0 -0
  87. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/tui/command_registry.py +0 -0
  88. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/tui/config.py +0 -0
  89. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/tui/daemon_session.py +0 -0
  90. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/tui/file_ops.py +0 -0
  91. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/tui/formatting.py +0 -0
  92. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/tui/hooks.py +0 -0
  93. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/tui/input.py +0 -0
  94. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/tui/media_utils.py +0 -0
  95. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/tui/message_display_filter.py +0 -0
  96. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/tui/model_config.py +0 -0
  97. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/tui/output.py +0 -0
  98. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/tui/preview_limits.py +0 -0
  99. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/tui/project_utils.py +0 -0
  100. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/tui/sessions.py +0 -0
  101. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/tui/skills/__init__.py +0 -0
  102. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/tui/skills/invocation.py +0 -0
  103. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/tui/skills/load.py +0 -0
  104. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/tui/textual_adapter/__init__.py +0 -0
  105. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/tui/textual_adapter/_adapter.py +0 -0
  106. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/tui/textual_adapter/_stream_formatting.py +0 -0
  107. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/tui/textual_adapter/_stream_messages.py +0 -0
  108. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/tui/textual_adapter/_turn_helpers.py +0 -0
  109. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/tui/theme.py +0 -0
  110. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/tui/tool_display.py +0 -0
  111. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/tui/unicode_security.py +0 -0
  112. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/tui/update_check.py +0 -0
  113. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/tui/widgets/__init__.py +0 -0
  114. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/tui/widgets/_links.py +0 -0
  115. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/tui/widgets/approval.py +0 -0
  116. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/tui/widgets/ask_user.py +0 -0
  117. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/tui/widgets/autocomplete.py +0 -0
  118. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/tui/widgets/autopilot_dashboard.py +0 -0
  119. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/tui/widgets/autopilot_screen.py +0 -0
  120. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/tui/widgets/chat_input.py +0 -0
  121. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/tui/widgets/clipboard.py +0 -0
  122. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/tui/widgets/diff.py +0 -0
  123. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/tui/widgets/editor.py +0 -0
  124. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/tui/widgets/history.py +0 -0
  125. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/tui/widgets/loop_selector.py +0 -0
  126. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/tui/widgets/mcp_viewer.py +0 -0
  127. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/tui/widgets/message_store.py +0 -0
  128. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/tui/widgets/model_selector.py +0 -0
  129. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/tui/widgets/notification_settings.py +0 -0
  130. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/tui/widgets/status.py +0 -0
  131. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/tui/widgets/theme_selector.py +0 -0
  132. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/tui/widgets/tool_renderers.py +0 -0
  133. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/tui/widgets/tool_widgets.py +0 -0
  134. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/tui/widgets/tools.py +0 -0
  135. {soothe_cli-0.5.5 → soothe_cli-0.5.7}/src/soothe_cli/tui/widgets/welcome.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: soothe-cli
3
- Version: 0.5.5
3
+ Version: 0.5.7
4
4
  Summary: Soothe CLI client - communicates with daemon via WebSocket
5
5
  Project-URL: Homepage, https://github.com/OpenSoothe/soothe
6
6
  Project-URL: Documentation, https://soothe.readthedocs.io
@@ -59,7 +59,7 @@ def run_headless(
59
59
  import subprocess
60
60
 
61
61
  subprocess.Popen(
62
- [sys.executable, "-m", "soothe.daemon", "--detached"],
62
+ [sys.executable, "-m", "soothe_daemon", "--detached"],
63
63
  stdout=subprocess.DEVNULL,
64
64
  stderr=subprocess.DEVNULL,
65
65
  )
@@ -229,7 +229,7 @@ class EventProcessor:
229
229
  """Headless stdout: only RFC-614 loop-tagged finals (IG-343 / IG-345).
230
230
 
231
231
  Suppresses unphased execute-wave narration on the main graph; ``goal_completion``,
232
- ``chitchat``, etc. are routed via ``assistant_output_phase`` before this path.
232
+ ``quiz``, etc. are routed via ``assistant_output_phase`` before this path.
233
233
 
234
234
  Subgraph streams without RFC-614 phases are skipped in ``_handle_ai_message`` before
235
235
  this runs; loop-tagged subgraph finals use ``_dispatch_loop_tagged_assistant_text``.
@@ -49,7 +49,7 @@ class PresentationEngine:
49
49
 
50
50
  @property
51
51
  def final_answer_locked(self) -> bool:
52
- """True after a custom final/chitchat response was emitted for this turn."""
52
+ """True after a custom final or quiz-phase response was emitted for this turn."""
53
53
  return self._state.final_answer_locked
54
54
 
55
55
  def mark_final_answer_locked(self) -> None:
@@ -46,6 +46,19 @@ InputMode = Literal["normal", "shell", "command"]
46
46
 
47
47
  logger = logging.getLogger(__name__)
48
48
 
49
+ # Partial match for daemon pool_runner RuntimeError when an OS worker exits mid-turn.
50
+ _DAEMON_WORKER_SUBPROCESS_LOST = "Worker subprocess exited unexpectedly during query execution"
51
+
52
+
53
+ def _friendly_agent_execution_error(exc: BaseException) -> str:
54
+ """Map known daemon failures to concise TUI copy."""
55
+ if isinstance(exc, RuntimeError) and _DAEMON_WORKER_SUBPROCESS_LOST in str(exc):
56
+ return (
57
+ "The daemon execution worker stopped unexpectedly (for example after the pool "
58
+ "recycled an idle subprocess). Send your message again."
59
+ )
60
+ return str(exc)
61
+
49
62
 
50
63
  class _ExecutionMixin:
51
64
  """Agent execution, message routing, queue, shell commands, and daemon events."""
@@ -859,13 +872,14 @@ class _ExecutionMixin:
859
872
  )
860
873
  except Exception as e: # Resilient tool rendering
861
874
  logger.exception("Agent execution failed")
875
+ display_err = _friendly_agent_execution_error(e)
862
876
  # Ensure any in-flight tool calls don't remain stuck in "Running..."
863
877
  # when streaming aborts before tool results arrive.
864
878
  if self._ui_adapter:
865
- self._ui_adapter.finalize_pending_tools_with_error(f"Agent error: {e}")
866
- self._ui_adapter.finalize_pending_steps_with_error(f"Agent error: {e}")
879
+ self._ui_adapter.finalize_pending_tools_with_error(f"Agent error: {display_err}")
880
+ self._ui_adapter.finalize_pending_steps_with_error(f"Agent error: {display_err}")
867
881
  try:
868
- await self._mount_message(ErrorMessage(f"Agent error: {e}"))
882
+ await self._mount_message(ErrorMessage(f"Agent error: {display_err}"))
869
883
  except Exception:
870
884
  logger.debug("Could not mount error message (app closing?)", exc_info=True)
871
885
  finally:
@@ -841,7 +841,7 @@ async def execute_task_textual(
841
841
  # Main graph: skip standalone AssistantMessage cards for
842
842
  # intermediate AIMessage streams (execute_wave, unphased, etc.).
843
843
  # ``goal_completion`` is handled above. Other RFC-614 user-output
844
- # phases (chitchat, quiz, autonomous_goal) still use cards.
844
+ # phases (quiz, autonomous_goal) still use cards.
845
845
  if (
846
846
  is_main_agent
847
847
  and assistant_output_phase(message)
@@ -137,7 +137,8 @@ class LoadingWidget(Static):
137
137
  now = monotonic()
138
138
  if self._turn_start_mono is None:
139
139
  self._turn_start_mono = now
140
- self._animation_timer = self.set_interval(0.1, self._update_animation)
140
+ # Reduced from 0.1s (10fps) to 0.2s (5fps) to reduce UI thread contention
141
+ self._animation_timer = self.set_interval(0.2, self._update_animation)
141
142
 
142
143
  def on_unmount(self) -> None:
143
144
  """Stop the animation timer when the widget leaves the DOM."""
@@ -159,13 +160,20 @@ class LoadingWidget(Static):
159
160
  self._animation_timer = None
160
161
 
161
162
  def _update_animation(self) -> None:
162
- """Update spinner and elapsed time."""
163
+ """Update spinner and elapsed time (optimized to reduce render overhead)."""
163
164
  if self._paused:
164
165
  return
165
166
 
167
+ # Skip update if widget is not visible on screen
168
+ if not self.is_on_screen:
169
+ return
170
+
166
171
  if self._spinner_widget:
167
172
  frame = self._spinner.next_frame()
168
- self._spinner_widget.update(frame)
173
+ # Use refresh with layout=False to avoid expensive layout recalculation
174
+ self._spinner_widget._content = frame
175
+ self._spinner_widget._render_cache = None
176
+ self._spinner_widget.refresh(repaint=True, layout=False)
169
177
 
170
178
  if self._hint_widget and self._turn_start_mono is not None:
171
179
  now = monotonic()
@@ -59,7 +59,7 @@ _STEP_TOOL_PREVIEW_ROWS = STEP_TASK_CARD_COLLAPSE_LINE_THRESHOLD
59
59
  """Collapsed step/task activity preview shows this many rows (IG-402)."""
60
60
 
61
61
  _MAX_STEP_STAT_TOOL_KINDS = 4
62
- """Max distinct tool display names in the step header before ``+N more``."""
62
+ """Max distinct tool display names in the running-line stats suffix before ``+N more``."""
63
63
 
64
64
  _RUNNING_SPINNER_INTERVAL_SECONDS = 0.2
65
65
  """Spinner/status animation cadence for running cards."""
@@ -711,6 +711,10 @@ class AssistantMessage(Vertical):
711
711
  }
712
712
  """
713
713
 
714
+ # Performance optimization: batch streaming updates to reduce render frequency
715
+ _STREAM_FLUSH_INTERVAL: float = 0.05 # 50ms batching for streaming
716
+ _VISIBILITY_REFRESH_INTERVAL: float = 0.1 # 100ms throttle for visibility refresh
717
+
714
718
  def __init__(self, content: str = "", **kwargs: Any) -> None:
715
719
  """Initialize an assistant message.
716
720
 
@@ -726,6 +730,11 @@ class AssistantMessage(Vertical):
726
730
  self._preview_widget: Static | None = None
727
731
  self._hint_widget: Static | None = None
728
732
 
733
+ # Batching buffer for streaming content
734
+ self._pending_buffer: str = ""
735
+ self._flush_timer: Timer | None = None
736
+ self._last_visibility_refresh: float = 0.0
737
+
729
738
  def compose(self) -> ComposeResult: # noqa: PLR6301 # Textual widget method convention
730
739
  """Compose markdown body, plain preview, and expand hint."""
731
740
  from textual.widgets import Markdown
@@ -851,14 +860,50 @@ class AssistantMessage(Vertical):
851
860
  else:
852
861
  _show_timestamp_toast(self)
853
862
 
863
+ def _should_refresh_visibility(self) -> bool:
864
+ """Check if visibility refresh should run (throttled)."""
865
+ from time import monotonic
866
+
867
+ now = monotonic()
868
+ if now - self._last_visibility_refresh > self._VISIBILITY_REFRESH_INTERVAL:
869
+ self._last_visibility_refresh = now
870
+ return True
871
+ return False
872
+
873
+ async def _flush_pending_content(self) -> None:
874
+ """Flush buffered content to stream (batched update)."""
875
+ self._flush_timer = None
876
+ if not self._pending_buffer:
877
+ return
878
+
879
+ text = self._pending_buffer
880
+ self._pending_buffer = ""
881
+
882
+ stream = self._ensure_stream()
883
+ await stream.write(text)
884
+
885
+ # Throttle visibility refresh
886
+ if self._should_refresh_visibility():
887
+ self._refresh_body_visibility()
888
+
854
889
  async def append_content(self, text: str) -> None:
855
- """Append content to the message (for streaming)."""
890
+ """Append content to the message (for streaming with batching).
891
+
892
+ Uses internal buffering to batch writes and reduce render frequency.
893
+ """
856
894
  if not text:
857
895
  return
896
+
897
+ # Accumulate content
858
898
  self._content += text
859
- stream = self._ensure_stream()
860
- self._refresh_body_visibility()
861
- await stream.write(text)
899
+ self._pending_buffer += text
900
+
901
+ # Schedule batched flush if not already scheduled
902
+ if self._flush_timer is None:
903
+ self._flush_timer = self.set_timer(
904
+ self._STREAM_FLUSH_INTERVAL,
905
+ self._flush_pending_content,
906
+ )
862
907
 
863
908
  async def write_initial_content(self) -> None:
864
909
  """Write initial content from constructor and finalize the stream."""
@@ -869,6 +914,15 @@ class AssistantMessage(Vertical):
869
914
 
870
915
  async def stop_stream(self) -> None:
871
916
  """Stop the streaming and apply collapsed layout when appropriate."""
917
+ # Cancel any pending flush timer
918
+ if self._flush_timer is not None:
919
+ self._flush_timer.stop()
920
+ self._flush_timer = None
921
+
922
+ # Flush any remaining buffered content
923
+ if self._pending_buffer:
924
+ await self._flush_pending_content()
925
+
872
926
  if self._stream is not None:
873
927
  await self._stream.stop()
874
928
  self._stream = None
@@ -878,6 +932,7 @@ class AssistantMessage(Vertical):
878
932
  """Set the full message content (stops any active stream)."""
879
933
  await self.stop_stream()
880
934
  self._content = content
935
+ self._pending_buffer = "" # Clear any pending buffer
881
936
  if self._markdown:
882
937
  await self._markdown.update(content)
883
938
  self._refresh_body_visibility()
@@ -2461,8 +2516,9 @@ class _StepToolRow:
2461
2516
  class CognitionStepMessage(Vertical):
2462
2517
  """Agent-loop act step card: aggregates main-agent tool calls (IG-402).
2463
2518
 
2464
- Header shows per-tool counts; body lists one CLI-style row per call. When
2465
- there are more than ``_STEP_TOOL_PREVIEW_ROWS`` rows, click first folds or
2519
+ Header is the step description only; per-tool counts appear on the running
2520
+ status line (and match the prior header format). Body lists one CLI-style row
2521
+ per call. When there are more than ``_STEP_TOOL_PREVIEW_ROWS`` rows, click first folds or
2466
2522
  unfolds the tool list; otherwise click toggles whole-card collapse. When tool
2467
2523
  rows, subagent notes, and execute prose together exceed that same threshold,
2468
2524
  the card body auto-collapses until the user expands it (a new ``set_running``
@@ -2630,7 +2686,7 @@ class CognitionStepMessage(Vertical):
2630
2686
  return _assemble_card_header(
2631
2687
  self,
2632
2688
  "🚀 ",
2633
- f"{self._description}{self._stats_title_suffix()}",
2689
+ self._description,
2634
2690
  )
2635
2691
 
2636
2692
  def compose(self) -> ComposeResult:
@@ -3169,7 +3225,8 @@ class CognitionStepMessage(Vertical):
3169
3225
  toggle_icon = ""
3170
3226
  if has_collapsible:
3171
3227
  toggle_icon = f" {g.expand if self._card_collapsed else g.collapse}"
3172
- line = f"{gutter}{frame} Running...{elapsed}{toggle_icon}"
3228
+ stats_suffix = self._stats_title_suffix()
3229
+ line = f"{gutter}{frame} Running...{elapsed}{stats_suffix}{toggle_icon}"
3173
3230
  self._status_widget.update(Content.styled(line, colors.cognition))
3174
3231
  now = monotonic()
3175
3232
  if any(r.phase == "running" for r in self._rows) and (
File without changes
File without changes
File without changes