soothe-cli 0.5.4__tar.gz → 0.5.6__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.4 → soothe_cli-0.5.6}/PKG-INFO +1 -1
  2. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/cli/execution/headless.py +1 -1
  3. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/tui/widgets/loading.py +11 -3
  4. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/tui/widgets/messages.py +66 -9
  5. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/.gitignore +0 -0
  6. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/README.md +0 -0
  7. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/pyproject.toml +0 -0
  8. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/__init__.py +0 -0
  9. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/cli/__init__.py +0 -0
  10. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/cli/commands/__init__.py +0 -0
  11. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/cli/commands/autopilot_cmd.py +0 -0
  12. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/cli/commands/loop_cmd.py +0 -0
  13. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/cli/commands/run_cmd.py +0 -0
  14. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/cli/execution/__init__.py +0 -0
  15. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/cli/execution/daemon.py +0 -0
  16. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/cli/execution/headless_renderer.py +0 -0
  17. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/cli/execution/launcher.py +0 -0
  18. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/cli/main.py +0 -0
  19. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/cli/stream/__init__.py +0 -0
  20. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/cli/stream/context.py +0 -0
  21. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/cli/stream/display_line.py +0 -0
  22. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/cli/stream/formatter.py +0 -0
  23. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/cli/stream/pipeline.py +0 -0
  24. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/cli/stream/task_scope.py +0 -0
  25. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/config/__init__.py +0 -0
  26. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/config/cli_config.py +0 -0
  27. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/plan/__init__.py +0 -0
  28. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/plan/rich_tree.py +0 -0
  29. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/shared/__init__.py +0 -0
  30. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/shared/commands/__init__.py +0 -0
  31. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/shared/commands/command_router.py +0 -0
  32. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/shared/commands/slash_commands.py +0 -0
  33. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/shared/commands/subagent_routing.py +0 -0
  34. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/shared/config_loader.py +0 -0
  35. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/shared/core/__init__.py +0 -0
  36. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/shared/core/event_processor.py +0 -0
  37. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/shared/core/presentation_engine.py +0 -0
  38. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/shared/core/processor_state.py +0 -0
  39. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/shared/core/renderer_protocol.py +0 -0
  40. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/shared/duration_format.py +0 -0
  41. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/shared/events/__init__.py +0 -0
  42. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/shared/events/display_policy.py +0 -0
  43. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/shared/events/essential_events.py +0 -0
  44. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/shared/events/explore_task_display.py +0 -0
  45. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/shared/events/stream_accumulator.py +0 -0
  46. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/shared/events/tui_trace_log.py +0 -0
  47. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/shared/rendering/__init__.py +0 -0
  48. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/shared/rendering/async_renderer_protocol.py +0 -0
  49. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/shared/rendering/renderer_base.py +0 -0
  50. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/shared/tools/__init__.py +0 -0
  51. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/shared/tools/_utils.py +0 -0
  52. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/shared/tools/message_processing.py +0 -0
  53. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/shared/tools/rendering.py +0 -0
  54. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/shared/tools/tool_call_resolution.py +0 -0
  55. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/shared/tools/tool_card_payload.py +0 -0
  56. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/shared/tools/tool_card_visibility.py +0 -0
  57. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/shared/tools/tool_formatters/__init__.py +0 -0
  58. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/shared/tools/tool_formatters/base.py +0 -0
  59. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/shared/tools/tool_formatters/execution.py +0 -0
  60. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/shared/tools/tool_formatters/fallback.py +0 -0
  61. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/shared/tools/tool_formatters/file_ops.py +0 -0
  62. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/shared/tools/tool_formatters/goal_formatter.py +0 -0
  63. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/shared/tools/tool_formatters/media.py +0 -0
  64. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/shared/tools/tool_formatters/structured.py +0 -0
  65. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/shared/tools/tool_formatters/subagent.py +0 -0
  66. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/shared/tools/tool_formatters/web.py +0 -0
  67. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/shared/tools/tool_message_format.py +0 -0
  68. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/shared/tools/tool_output_formatter.py +0 -0
  69. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/tui/__init__.py +0 -0
  70. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/tui/_ask_user_types.py +0 -0
  71. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/tui/_cli_context.py +0 -0
  72. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/tui/_env_vars.py +0 -0
  73. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/tui/_session_stats.py +0 -0
  74. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/tui/_version.py +0 -0
  75. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/tui/app/__init__.py +0 -0
  76. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/tui/app/_app.py +0 -0
  77. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/tui/app/_commands.py +0 -0
  78. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/tui/app/_execution.py +0 -0
  79. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/tui/app/_history.py +0 -0
  80. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/tui/app/_messages_mixin.py +0 -0
  81. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/tui/app/_model.py +0 -0
  82. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/tui/app/_module_init.py +0 -0
  83. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/tui/app/_startup.py +0 -0
  84. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/tui/app/_ui.py +0 -0
  85. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/tui/app/app.tcss +0 -0
  86. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/tui/command_registry.py +0 -0
  87. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/tui/config.py +0 -0
  88. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/tui/daemon_session.py +0 -0
  89. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/tui/file_ops.py +0 -0
  90. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/tui/formatting.py +0 -0
  91. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/tui/hooks.py +0 -0
  92. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/tui/input.py +0 -0
  93. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/tui/media_utils.py +0 -0
  94. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/tui/message_display_filter.py +0 -0
  95. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/tui/model_config.py +0 -0
  96. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/tui/output.py +0 -0
  97. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/tui/preview_limits.py +0 -0
  98. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/tui/project_utils.py +0 -0
  99. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/tui/sessions.py +0 -0
  100. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/tui/skills/__init__.py +0 -0
  101. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/tui/skills/invocation.py +0 -0
  102. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/tui/skills/load.py +0 -0
  103. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/tui/textual_adapter/__init__.py +0 -0
  104. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/tui/textual_adapter/_adapter.py +0 -0
  105. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/tui/textual_adapter/_stream_formatting.py +0 -0
  106. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/tui/textual_adapter/_stream_messages.py +0 -0
  107. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/tui/textual_adapter/_turn.py +0 -0
  108. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/tui/textual_adapter/_turn_helpers.py +0 -0
  109. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/tui/theme.py +0 -0
  110. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/tui/tool_display.py +0 -0
  111. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/tui/unicode_security.py +0 -0
  112. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/tui/update_check.py +0 -0
  113. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/tui/widgets/__init__.py +0 -0
  114. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/tui/widgets/_links.py +0 -0
  115. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/tui/widgets/approval.py +0 -0
  116. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/tui/widgets/ask_user.py +0 -0
  117. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/tui/widgets/autocomplete.py +0 -0
  118. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/tui/widgets/autopilot_dashboard.py +0 -0
  119. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/tui/widgets/autopilot_screen.py +0 -0
  120. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/tui/widgets/chat_input.py +0 -0
  121. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/tui/widgets/clipboard.py +0 -0
  122. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/tui/widgets/diff.py +0 -0
  123. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/tui/widgets/editor.py +0 -0
  124. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/tui/widgets/history.py +0 -0
  125. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/tui/widgets/loop_selector.py +0 -0
  126. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/tui/widgets/mcp_viewer.py +0 -0
  127. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/tui/widgets/message_store.py +0 -0
  128. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/tui/widgets/model_selector.py +0 -0
  129. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/tui/widgets/notification_settings.py +0 -0
  130. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/tui/widgets/status.py +0 -0
  131. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/tui/widgets/theme_selector.py +0 -0
  132. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/tui/widgets/tool_renderers.py +0 -0
  133. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/tui/widgets/tool_widgets.py +0 -0
  134. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/src/soothe_cli/tui/widgets/tools.py +0 -0
  135. {soothe_cli-0.5.4 → soothe_cli-0.5.6}/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.4
3
+ Version: 0.5.6
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
  )
@@ -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