soothe-cli 1.0.5__tar.gz → 1.0.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 (148) hide show
  1. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/PKG-INFO +1 -1
  2. soothe_cli-1.0.7/src/soothe_cli/display/card.py +158 -0
  3. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/tui/app/_clarification.py +5 -8
  4. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/tui/app/_messages_mixin.py +29 -12
  5. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/tui/widgets/file_change_preview.py +1 -1
  6. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/tui/widgets/messages/__init__.py +0 -10
  7. soothe_cli-1.0.7/src/soothe_cli/tui/widgets/messages/_helpers.py +155 -0
  8. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/tui/widgets/messages/app.py +1 -1
  9. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/tui/widgets/messages/assistant.py +2 -4
  10. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/tui/widgets/messages/cognition_goal_tree.py +4 -4
  11. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/tui/widgets/messages/cognition_reason.py +1 -1
  12. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/tui/widgets/messages/cognition_step.py +7 -5
  13. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/tui/widgets/messages/cognition_step_activity.py +29 -29
  14. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/tui/widgets/messages/error.py +1 -1
  15. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/tui/widgets/messages/skill.py +1 -1
  16. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/tui/widgets/messages/structured_ask_user.py +33 -21
  17. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/tui/widgets/plan_quick_view_overlay.py +5 -5
  18. soothe_cli-1.0.5/src/soothe_cli/tui/widgets/messages/_helpers.py +0 -318
  19. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/.gitignore +0 -0
  20. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/README.md +0 -0
  21. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/pyproject.toml +0 -0
  22. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/__init__.py +0 -0
  23. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/_cli_context.py +0 -0
  24. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/_env_vars.py +0 -0
  25. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/_version.py +0 -0
  26. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/card_wire.py +0 -0
  27. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/cli/__init__.py +0 -0
  28. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/cli/commands/__init__.py +0 -0
  29. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/cli/commands/autopilot_cmd.py +0 -0
  30. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/cli/commands/config_cmd.py +0 -0
  31. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/cli/commands/cron_cmd.py +0 -0
  32. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/cli/commands/loop_cmd.py +0 -0
  33. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/cli/commands/run_cmd.py +0 -0
  34. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/cli/commands/status_cmd.py +0 -0
  35. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/cli/execution/__init__.py +0 -0
  36. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/cli/execution/daemon.py +0 -0
  37. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/cli/execution/daemon_errors.py +0 -0
  38. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/cli/execution/headless.py +0 -0
  39. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/cli/execution/headless_renderer.py +0 -0
  40. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/cli/execution/launcher.py +0 -0
  41. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/cli/main.py +0 -0
  42. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/cli/soothecli.py +0 -0
  43. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/commands/__init__.py +0 -0
  44. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/commands/binding.py +0 -0
  45. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/commands/command_registry.py +0 -0
  46. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/commands/command_router.py +0 -0
  47. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/commands/slash_commands.py +0 -0
  48. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/commands/subagent_routing.py +0 -0
  49. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/config/__init__.py +0 -0
  50. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/config/cli_config.py +0 -0
  51. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/config/loader.py +0 -0
  52. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/config/logging_setup.py +0 -0
  53. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/display/__init__.py +0 -0
  54. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/display/markdown_theme.py +0 -0
  55. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/display/media_utils.py +0 -0
  56. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/display/mermaid_render.py +0 -0
  57. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/display/path_utils.py +0 -0
  58. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/display/preview_limits.py +0 -0
  59. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/display/shell_color.py +0 -0
  60. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/display/spinner_labels.py +0 -0
  61. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/display/theme.py +0 -0
  62. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/display/tips.py +0 -0
  63. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/display/tool_display.py +0 -0
  64. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/display/unicode_security.py +0 -0
  65. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/loops/__init__.py +0 -0
  66. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/loops/sessions.py +0 -0
  67. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/model_config.py +0 -0
  68. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/project_utils.py +0 -0
  69. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/runtime/__init__.py +0 -0
  70. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/runtime/headless/processor.py +0 -0
  71. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/runtime/headless/processor_state.py +0 -0
  72. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/runtime/parse/tool_call_resolution.py +0 -0
  73. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/runtime/policy/tui_trace_log.py +0 -0
  74. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/runtime/presentation/duration_format.py +0 -0
  75. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/runtime/presentation/engine.py +0 -0
  76. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/runtime/presentation/id_format.py +0 -0
  77. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/runtime/presentation/renderer_base.py +0 -0
  78. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/runtime/presentation/renderer_protocol.py +0 -0
  79. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/runtime/presentation/step_id_format.py +0 -0
  80. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/runtime/presentation/subagent_task_display.py +0 -0
  81. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/runtime/state/file_tracker.py +0 -0
  82. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/runtime/state/session_stats.py +0 -0
  83. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/runtime/state/step_router.py +0 -0
  84. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/runtime/state/stream_accumulator.py +0 -0
  85. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/runtime/token_events_debug.py +0 -0
  86. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/runtime/token_usage.py +0 -0
  87. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/runtime/transport/session.py +0 -0
  88. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/runtime/turn/prepare.py +0 -0
  89. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/runtime/wire/chunk_filter.py +0 -0
  90. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/runtime/wire/message_text.py +0 -0
  91. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/runtime/wire/messages.py +0 -0
  92. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/settings/__init__.py +0 -0
  93. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/settings/_console_impl.py +0 -0
  94. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/settings/bootstrap.py +0 -0
  95. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/settings/core.py +0 -0
  96. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/settings/glyphs.py +0 -0
  97. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/settings/provider.py +0 -0
  98. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/settings/shell_allow.py +0 -0
  99. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/settings/skills_dirs.py +0 -0
  100. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/settings/stream_config.py +0 -0
  101. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/tui/__init__.py +0 -0
  102. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/tui/app/__init__.py +0 -0
  103. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/tui/app/_app.py +0 -0
  104. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/tui/app/_entrypoints.py +0 -0
  105. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/tui/app/_execution.py +0 -0
  106. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/tui/app/_history.py +0 -0
  107. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/tui/app/_model.py +0 -0
  108. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/tui/app/_model_params.py +0 -0
  109. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/tui/app/_startup.py +0 -0
  110. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/tui/app/_terminal.py +0 -0
  111. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/tui/app/_theme_prefs.py +0 -0
  112. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/tui/app/_types.py +0 -0
  113. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/tui/app/_ui.py +0 -0
  114. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/tui/app/app.tcss +0 -0
  115. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/tui/composer_mode.py +0 -0
  116. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/tui/file_change_notify.py +0 -0
  117. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/tui/file_change_renderers.py +0 -0
  118. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/tui/input.py +0 -0
  119. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/tui/skills/__init__.py +0 -0
  120. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/tui/skills/invocation.py +0 -0
  121. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/tui/skills/load.py +0 -0
  122. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/tui/textual_adapter.py +0 -0
  123. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/tui/widgets/__init__.py +0 -0
  124. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/tui/widgets/_links.py +0 -0
  125. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/tui/widgets/autocomplete.py +0 -0
  126. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/tui/widgets/chat_input.py +0 -0
  127. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/tui/widgets/clipboard.py +0 -0
  128. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/tui/widgets/context_data.py +0 -0
  129. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/tui/widgets/context_viewer.py +0 -0
  130. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/tui/widgets/diff.py +0 -0
  131. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/tui/widgets/editor.py +0 -0
  132. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/tui/widgets/help_screen.py +0 -0
  133. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/tui/widgets/history.py +0 -0
  134. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/tui/widgets/loading.py +0 -0
  135. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/tui/widgets/loop_selector.py +0 -0
  136. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/tui/widgets/mcp_viewer.py +0 -0
  137. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/tui/widgets/message_store.py +0 -0
  138. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/tui/widgets/messages/diff_message.py +0 -0
  139. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/tui/widgets/messages/user.py +0 -0
  140. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/tui/widgets/model_selector.py +0 -0
  141. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/tui/widgets/notification_settings.py +0 -0
  142. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/tui/widgets/pinned_goal_bar.py +0 -0
  143. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/tui/widgets/queued_goals_bar.py +0 -0
  144. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/tui/widgets/router_profile_selector.py +0 -0
  145. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/tui/widgets/status.py +0 -0
  146. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/tui/widgets/theme_selector.py +0 -0
  147. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/tui/widgets/welcome.py +0 -0
  148. {soothe_cli-1.0.5 → soothe_cli-1.0.7}/src/soothe_cli/update_check.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: soothe-cli
3
- Version: 1.0.5
3
+ Version: 1.0.7
4
4
  Summary: Soothe CLI client - communicates with daemon via WebSocket
5
5
  Project-URL: Homepage, https://github.com/mirasoth/soothe
6
6
  Project-URL: Documentation, https://soothe.readthedocs.io
@@ -0,0 +1,158 @@
1
+ """Card prefix glyph, header assembly, and body gutter alignment shared by TUI card widgets.
2
+
3
+ These helpers encode card *appearance* (prefix tone, glyph prefix, gutter
4
+ alignment, header assembly). They are intentionally free of Textual widget
5
+ lifecycle concerns; refresh throttling and animation visibility stay in the
6
+ TUI layer (``tui/widgets/messages/_helpers.py``).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from textual.content import Content
12
+
13
+ from soothe_cli.display import theme
14
+ from soothe_cli.display.preview_limits import STEP_TASK_CARD_COLLAPSE_LINE_THRESHOLD
15
+ from soothe_cli.display.tool_display import display_width
16
+ from soothe_cli.settings import get_glyphs
17
+
18
+ # Preview limits imported from preview_limits module
19
+ _STEP_TOOL_PREVIEW_ROWS = STEP_TASK_CARD_COLLAPSE_LINE_THRESHOLD
20
+ """Collapsed step/task activity preview shows this many rows."""
21
+
22
+
23
+ def _card_dot_tone(
24
+ status: str,
25
+ colors: theme.ThemeColors,
26
+ *,
27
+ accent: str | None = None,
28
+ spinner_position: int = 0,
29
+ animate_running: bool = False,
30
+ ) -> str:
31
+ """Return a Textual style string for a card prefix dot from lifecycle status."""
32
+ phase = (status or "pending").strip().lower()
33
+ if phase in ("success", "done", "completed"):
34
+ return colors.card_success
35
+ if phase in ("error", "failed", "interrupted"):
36
+ return colors.card_error
37
+ if phase == "running":
38
+ # Yellow flash on every other spinner tick; dim amber in between.
39
+ if animate_running and spinner_position % 2:
40
+ return colors.card_running_muted
41
+ return colors.card_running
42
+ if phase in ("queued", "pending", "continue", "replan"):
43
+ return colors.muted
44
+ if accent:
45
+ return accent
46
+ return colors.muted
47
+
48
+
49
+ def _card_dot_prefix_content(
50
+ widget: object,
51
+ status: str,
52
+ *,
53
+ accent: str | None = None,
54
+ glyph_override: str | None = None,
55
+ spinner_position: int = 0,
56
+ animate_running: bool = False,
57
+ ) -> Content:
58
+ """Build a stateful colored card-prefix glyph."""
59
+ try:
60
+ colors = theme.get_theme_colors(widget)
61
+ except Exception: # noqa: BLE001
62
+ colors = theme.DARK_COLORS
63
+ glyph = glyph_override or get_glyphs().tool_prefix
64
+ tone = _card_dot_tone(
65
+ status,
66
+ colors,
67
+ accent=accent,
68
+ spinner_position=spinner_position,
69
+ animate_running=animate_running,
70
+ )
71
+ return Content.styled(f"{glyph} ", tone)
72
+
73
+
74
+ def _card_prefix_width(glyph_override: str | None = None) -> int:
75
+ """Display width of the card header prefix (the dot/glyph plus one space).
76
+
77
+ Every body line and tree-branch line must left-align at the right side of
78
+ this prefix, so card content never extends left of the header text. In
79
+ ASCII mode the prefix is ``"[*] "`` (4 cols) while the tree gutter glyph
80
+ ``"L"`` is only 1 col; the body gutter is padded to close that gap.
81
+ """
82
+ glyph = glyph_override or get_glyphs().tool_prefix
83
+ return display_width(glyph) + 1 # +1 for the trailing space
84
+
85
+
86
+ def _card_body_gutter(glyph_override: str | None = None) -> str:
87
+ """Body/tree-branch gutter aligned to the right of the card prefix dot.
88
+
89
+ The tree glyph (``⎿`` / ``L``) stays at the left edge of the prefix column;
90
+ trailing spaces pad the gutter so its display width equals the header
91
+ prefix width (``glyph + " "``). That makes every card line and tree branch
92
+ start at the right side of the dot space.
93
+ """
94
+ g = get_glyphs()
95
+ prefix_width = _card_prefix_width(glyph_override)
96
+ glyph_w = display_width(g.output_prefix)
97
+ pad = max(0, prefix_width - glyph_w)
98
+ return f"{g.output_prefix}{' ' * pad}"
99
+
100
+
101
+ def _card_item_indent(
102
+ extra_indent: int = 0,
103
+ *,
104
+ glyph_override: str | None = None,
105
+ ) -> str:
106
+ """Plain-space indent for activity item rows (no tree glyph).
107
+
108
+ Item rows under a section (tool rows, todo items, notes, "+N more") sit at
109
+ the card header prefix width plus ``extra_indent`` columns — aligned with
110
+ the right side of the dot space so they never extend left of the title
111
+ text, and indented under the section's ``⎿`` label.
112
+ """
113
+ prefix_width = _card_prefix_width(glyph_override)
114
+ return f"{' ' * max(0, prefix_width + extra_indent)}"
115
+
116
+
117
+ def _assemble_card_header(
118
+ widget: object,
119
+ body_part: str,
120
+ *,
121
+ status: str = "running",
122
+ accent: str | None = None,
123
+ glyph_override: str | None = None,
124
+ spinner_position: int = 0,
125
+ animate_running: bool = False,
126
+ ) -> Content:
127
+ """Build a card title: stateful prefix glyph plus foreground body (no bold).
128
+
129
+ Used for Goal, Plan, Step, and tool (including Task) headers. The glyph color
130
+ reflects lifecycle status; body lines below use the tree gutter returned by
131
+ :func:`_card_body_gutter`, which aligns to the right of the card prefix dot.
132
+
133
+ Args:
134
+ widget: Mounted widget (or any object accepted by ``get_theme_colors``).
135
+ body_part: Header text (goal, step description, etc.).
136
+ status: Card lifecycle phase for glyph color/shape.
137
+ accent: Optional override tone for the glyph (e.g. skill/error accent).
138
+ spinner_position: Toggles gray flash while ``animate_running`` is true.
139
+ animate_running: Flash the glyph gray while running (step/task/assistant cards).
140
+
141
+ Returns:
142
+ Assembled ``Content`` for a ``Static`` header.
143
+ """
144
+ try:
145
+ colors = theme.get_theme_colors(widget)
146
+ except Exception: # noqa: BLE001
147
+ colors = theme.DARK_COLORS
148
+ return Content.assemble(
149
+ _card_dot_prefix_content(
150
+ widget,
151
+ status,
152
+ accent=accent,
153
+ glyph_override=glyph_override,
154
+ spinner_position=spinner_position,
155
+ animate_running=animate_running,
156
+ ),
157
+ Content.styled(body_part, colors.foreground),
158
+ )
@@ -7,19 +7,16 @@ keep the execution mixin focused on agent execution and message routing.
7
7
  from __future__ import annotations
8
8
 
9
9
  _PLAN_REVIEW_ACTIONS = frozenset({"Approve", "Reject", "Refine"})
10
- _TOOL_APPROVAL_ACTIONS = frozenset({"Approve", "Reject", "Edit"})
10
+ _TOOL_APPROVAL_ACTIONS = frozenset({"Approve", "Reject"})
11
11
 
12
12
 
13
13
  def clarification_wire_content(answers: list[str], *, origin_node: str = "") -> str:
14
14
  """Human-readable turn content for a clarification submit (not a new goal).
15
15
 
16
- Option-selector actions use a stable `<prefix>: …` header so a dropped
17
- `clarification_answer` flag cannot turn a bare action into Pass1 TASK.
18
- Plan review → `Plan review:` (Refine carries refinement text in
19
- `answers[1]`). Tool approval → `Tool approval:` (Edit carries revised
20
- instructions in `answers[1]`). When `origin_node` is empty (legacy
21
- callers) the action label alone selects the prefix, preserving the safety
22
- net for any selector-origin submit.
16
+ Option-selector actions use a stable ``<prefix>: …`` header so a dropped
17
+ ``clarification_answer`` flag cannot turn a bare action into Pass1 TASK.
18
+ Plan review → ``Plan review:`` (Refine carries refinement text in
19
+ ``answers[1]``). Tool approval → ``Tool approval:``.
23
20
  """
24
21
  non_empty = [a for a in answers if str(a).strip()]
25
22
  if not non_empty:
@@ -1159,11 +1159,15 @@ class _MessagesMixin:
1159
1159
  def cycle_composer_mode(self) -> None:
1160
1160
  """Advance composer mode and refresh the status-bar badge.
1161
1161
 
1162
- Cycle: Auto → Bypass → Manual → Plan → Ask → Auto. When a goal is
1163
- running and the new mode is Auto or Manual, hot-swaps the daemon's
1164
- live clarification policy via `loop_set_clarification_mode`.
1162
+ Cycle: Auto → Bypass → Manual → Plan → Ask → Auto. For agent sub-modes
1163
+ (auto/bypass/manual) hot-swap the running goal's agent mode via
1164
+ `loop_set_clarification_mode`; Plan/Ask take effect on the next turn.
1165
1165
  """
1166
- from soothe_cli.tui.composer_mode import next_composer_mode
1166
+ from soothe_cli.tui.composer_mode import (
1167
+ AGENT_SUB_MODES,
1168
+ next_composer_mode,
1169
+ resolve_composer_wire_fields,
1170
+ )
1167
1171
 
1168
1172
  current = getattr(self, "_composer_mode", "auto")
1169
1173
  new_mode = next_composer_mode(current)
@@ -1171,13 +1175,21 @@ class _MessagesMixin:
1171
1175
  if self._status_bar is not None:
1172
1176
  self._status_bar.set_clarification_mode(new_mode)
1173
1177
 
1174
- # Hot-swap the running goal's clarification policy when applicable.
1175
- # Plan/Ask modes don't carry a clarification_mode wire field, so skip
1176
- # them — they only take effect on the next turn dispatch.
1177
- if new_mode in ("auto", "manual"):
1178
- self._push_clarification_mode_to_running_loop(new_mode)
1178
+ # Bypass carries an `interaction_mode` wire field; Plan/Ask stay
1179
+ # next-turn-only.
1180
+ if new_mode in AGENT_SUB_MODES:
1181
+ wire = resolve_composer_wire_fields(new_mode)
1182
+ self._push_clarification_mode_to_running_loop(
1183
+ wire.clarification_mode,
1184
+ interaction_mode=wire.interaction_mode,
1185
+ )
1179
1186
 
1180
- def _push_clarification_mode_to_running_loop(self, mode: str) -> None:
1187
+ def _push_clarification_mode_to_running_loop(
1188
+ self,
1189
+ mode: str,
1190
+ *,
1191
+ interaction_mode: str | None = None,
1192
+ ) -> None:
1181
1193
  """Best-effort hot-swap RPC; failures are silent (next turn still works)."""
1182
1194
  import asyncio
1183
1195
 
@@ -1190,11 +1202,16 @@ class _MessagesMixin:
1190
1202
 
1191
1203
  async def _push() -> None:
1192
1204
  try:
1193
- await session.set_clarification_mode(mode)
1205
+ await session.set_clarification_mode(
1206
+ mode,
1207
+ interaction_mode=interaction_mode,
1208
+ )
1194
1209
  except Exception:
1195
1210
  logger.debug(
1196
- "set_clarification_mode RPC failed (mode=%s); will apply on next turn",
1211
+ "set_clarification_mode RPC failed (mode=%s, interaction_mode=%s); "
1212
+ "will apply on next turn",
1197
1213
  mode,
1214
+ interaction_mode,
1198
1215
  exc_info=True,
1199
1216
  )
1200
1217
 
@@ -11,6 +11,7 @@ from textual.events import Click
11
11
  from textual.widgets import Static
12
12
 
13
13
  from soothe_cli.display import theme
14
+ from soothe_cli.display.card import _assemble_card_header
14
15
  from soothe_cli.display.preview_limits import (
15
16
  TOOL_APPROVAL_BODY_MAX_LINES,
16
17
  TOOL_APPROVAL_DIFF_WIDGET_MAX_LINES,
@@ -19,7 +20,6 @@ from soothe_cli.display.preview_limits import (
19
20
  from soothe_cli.settings import get_glyphs
20
21
  from soothe_cli.tui.widgets.clipboard import screen_has_text_selection
21
22
  from soothe_cli.tui.widgets.diff import DIFF_CODE_GAP, compose_diff_line_list
22
- from soothe_cli.tui.widgets.messages._helpers import _assemble_card_header
23
23
 
24
24
  if TYPE_CHECKING:
25
25
  from textual.app import ComposeResult
@@ -1,11 +1,6 @@
1
1
  """Message widgets for Soothe TUI."""
2
2
 
3
3
  from soothe_cli.tui.widgets.messages._helpers import (
4
- _assemble_card_header,
5
- _is_widget_animation_visible,
6
- _mode_color,
7
- _should_refresh_now,
8
- _strip_success_exit_line,
9
4
  flush_deferred_tools_refreshes,
10
5
  request_deferred_tools_refresh,
11
6
  reset_turn_tool_refresh_state,
@@ -39,11 +34,6 @@ from soothe_cli.tui.widgets.messages.user import (
39
34
 
40
35
  __all__ = [
41
36
  # Helpers
42
- "_assemble_card_header",
43
- "_is_widget_animation_visible",
44
- "_mode_color",
45
- "_should_refresh_now",
46
- "_strip_success_exit_line",
47
37
  "flush_deferred_tools_refreshes",
48
38
  "request_deferred_tools_refresh",
49
39
  "reset_turn_tool_refresh_state",
@@ -0,0 +1,155 @@
1
+ """Shared helpers and constants for message widgets."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import os
7
+ import re
8
+ import weakref
9
+ from time import monotonic
10
+ from typing import Any
11
+
12
+ from soothe_cli.display import theme
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ # TUI refresh throttling - minimum interval between widget refreshes
17
+ _DEFAULT_TUI_REFRESH_INTERVAL_MS = 800
18
+ """Default minimum interval between TUI refreshes in milliseconds."""
19
+
20
+ _global_refresh_interval_ms: int | None = None
21
+
22
+
23
+ def _get_tui_refresh_interval_ms() -> int:
24
+ """Get the TUI refresh interval from environment or default.
25
+
26
+ Returns:
27
+ Minimum interval between refreshes in milliseconds.
28
+ """
29
+ global _global_refresh_interval_ms
30
+ if _global_refresh_interval_ms is not None:
31
+ return _global_refresh_interval_ms
32
+ from soothe_cli._env_vars import TUI_REFRESH_INTERVAL_MS
33
+
34
+ env_val = os.environ.get(TUI_REFRESH_INTERVAL_MS)
35
+ if env_val:
36
+ try:
37
+ parsed = int(env_val.strip())
38
+ if parsed >= 50: # Minimum 50ms to prevent UI lockup
39
+ _global_refresh_interval_ms = parsed
40
+ return parsed
41
+ except ValueError:
42
+ pass
43
+ _global_refresh_interval_ms = _DEFAULT_TUI_REFRESH_INTERVAL_MS
44
+ return _DEFAULT_TUI_REFRESH_INTERVAL_MS
45
+
46
+
47
+ def _should_refresh_now(last_refresh_time: float | None) -> bool:
48
+ """Check if enough time has passed since last refresh for throttling.
49
+
50
+ Args:
51
+ last_refresh_time: Monotonic time of last refresh, or None if never refreshed.
52
+
53
+ Returns:
54
+ True if refresh should proceed, False if throttled.
55
+ """
56
+ if last_refresh_time is None:
57
+ return True
58
+ interval_secs = _get_tui_refresh_interval_ms() / 1000.0
59
+ return (monotonic() - last_refresh_time) >= interval_secs
60
+
61
+
62
+ _RUNNING_SPINNER_INTERVAL_SECONDS = 0.2
63
+ """Spinner/status animation cadence for running cards."""
64
+
65
+ # Deferred tool-list refresh (turn-level coalescing + global repaint budget).
66
+ _DEFERRED_TOOL_REFRESH_WIDGETS: weakref.WeakSet[Any] = weakref.WeakSet()
67
+ _global_tools_list_refresh_at: float = 0.0
68
+
69
+
70
+ def reset_turn_tool_refresh_state() -> None:
71
+ """Clear deferred refresh registry at the start of a new agent turn."""
72
+ global _global_tools_list_refresh_at
73
+ _global_tools_list_refresh_at = 0.0
74
+ _DEFERRED_TOOL_REFRESH_WIDGETS.clear()
75
+
76
+
77
+ def request_deferred_tools_refresh(widget: Any) -> None:
78
+ """Queue a card for batched tool-list repaint."""
79
+ _DEFERRED_TOOL_REFRESH_WIDGETS.add(widget)
80
+
81
+
82
+ def flush_deferred_tools_refreshes(*, force: bool = False) -> None:
83
+ """Repaint queued tool cards (global budget unless `force`)."""
84
+ global _global_tools_list_refresh_at
85
+ pending = list(_DEFERRED_TOOL_REFRESH_WIDGETS)
86
+ if not pending:
87
+ return
88
+ now = monotonic()
89
+ if not force:
90
+ interval = _get_tui_refresh_interval_ms() / 1000.0
91
+ if now - _global_tools_list_refresh_at < interval:
92
+ return
93
+ _global_tools_list_refresh_at = now
94
+ _DEFERRED_TOOL_REFRESH_WIDGETS.clear()
95
+ for widget in pending:
96
+ flush_fn = getattr(widget, "_flush_deferred_tools_refresh", None)
97
+ if callable(flush_fn):
98
+ flush_fn()
99
+
100
+
101
+ def _is_widget_animation_visible(widget: object) -> bool:
102
+ """Return whether a widget is currently visible on screen.
103
+
104
+ This is used to skip animation work for off-screen cards.
105
+ """
106
+ try:
107
+ if not getattr(widget, "is_attached", False):
108
+ return False
109
+ if not getattr(widget, "visible", True):
110
+ return False
111
+ is_on_screen = getattr(widget, "is_on_screen", True)
112
+ if callable(is_on_screen):
113
+ return bool(is_on_screen())
114
+ return bool(is_on_screen)
115
+ except Exception:
116
+ return False
117
+
118
+
119
+ def _mode_color(mode: str | None, widget_or_app: object | None = None) -> str:
120
+ """Return the hex color string for a mode, falling back to primary.
121
+
122
+ Args:
123
+ mode: Mode name (e.g. `'shell'`, `'command'`) or `None`.
124
+ widget_or_app: Textual widget or `App` for theme-aware lookup.
125
+
126
+ Returns:
127
+ Color string from the active theme's `ThemeColors`.
128
+ """
129
+ colors = theme.get_theme_colors(widget_or_app)
130
+ if not mode:
131
+ return colors.primary
132
+ if mode == "shell":
133
+ return colors.mode_bash
134
+ if mode == "command":
135
+ return colors.mode_command
136
+ logger.warning("Missing color for mode '%s'; falling back to primary.", mode)
137
+ return colors.primary
138
+
139
+
140
+ _SUCCESS_EXIT_RE = re.compile(r"\n?\[Command succeeded with exit code 0\]\s*$")
141
+ """Strip the SDK's `[Command succeeded with exit code 0]` trailer from tool output."""
142
+
143
+
144
+ def _strip_success_exit_line(text: str) -> str:
145
+ """Remove the `[Command succeeded with exit code 0]` trailer.
146
+
147
+ Non-zero exit codes are left intact (they come through `set_error`).
148
+
149
+ Args:
150
+ text: Raw tool output string.
151
+
152
+ Returns:
153
+ Text with the success exit-code trailer removed, if present.
154
+ """
155
+ return _SUCCESS_EXIT_RE.sub("", text)
@@ -9,9 +9,9 @@ from textual.events import Click
9
9
  from textual.widgets import Static
10
10
 
11
11
  from soothe_cli.display import theme
12
+ from soothe_cli.display.card import _assemble_card_header
12
13
  from soothe_cli.tui.widgets._links import open_style_link
13
14
  from soothe_cli.tui.widgets.clipboard import screen_has_text_selection
14
- from soothe_cli.tui.widgets.messages._helpers import _assemble_card_header
15
15
 
16
16
 
17
17
  class AppMessage(Static):
@@ -10,11 +10,9 @@ from textual.selection import Selection
10
10
  from textual.strip import Strip
11
11
  from textual.widgets import Static
12
12
 
13
+ from soothe_cli.display.card import _card_dot_prefix_content
13
14
  from soothe_cli.display.markdown_theme import ThemedMarkdownRenderer, resolve_markdown_theme_parts
14
- from soothe_cli.tui.widgets.messages._helpers import (
15
- _RUNNING_SPINNER_INTERVAL_SECONDS,
16
- _card_dot_prefix_content,
17
- )
15
+ from soothe_cli.tui.widgets.messages._helpers import _RUNNING_SPINNER_INTERVAL_SECONDS
18
16
 
19
17
  if TYPE_CHECKING:
20
18
  from textual.app import ComposeResult
@@ -12,6 +12,10 @@ from textual.content import Content
12
12
  from textual.widgets import Static
13
13
 
14
14
  from soothe_cli.display import theme
15
+ from soothe_cli.display.card import (
16
+ _card_body_gutter,
17
+ _card_dot_prefix_content,
18
+ )
15
19
  from soothe_cli.display.preview_limits import PLAN_QUICK_VIEW_STEP_LINE_MAX_CHARS
16
20
  from soothe_cli.runtime.presentation.duration_format import (
17
21
  format_duration,
@@ -23,10 +27,6 @@ from soothe_cli.runtime.presentation.step_id_format import (
23
27
  numeric_step_prefix,
24
28
  )
25
29
  from soothe_cli.settings import get_glyphs
26
- from soothe_cli.tui.widgets.messages._helpers import (
27
- _card_body_gutter,
28
- _card_dot_prefix_content,
29
- )
30
30
 
31
31
  if TYPE_CHECKING:
32
32
  from textual.app import ComposeResult
@@ -8,7 +8,7 @@ from textual.containers import Vertical
8
8
  from textual.content import Content
9
9
  from textual.widgets import Static
10
10
 
11
- from soothe_cli.tui.widgets.messages._helpers import _assemble_card_header
11
+ from soothe_cli.display.card import _assemble_card_header
12
12
 
13
13
  if TYPE_CHECKING:
14
14
  from textual.app import ComposeResult
@@ -19,6 +19,11 @@ from textual.events import Click
19
19
  from textual.widgets import Static
20
20
 
21
21
  from soothe_cli.display import theme
22
+ from soothe_cli.display.card import (
23
+ _STEP_TOOL_PREVIEW_ROWS,
24
+ _assemble_card_header,
25
+ _card_body_gutter,
26
+ )
22
27
  from soothe_cli.display.preview_limits import STEP_CARD_SHOW_TOOL_ROW_DETAILS
23
28
  from soothe_cli.display.tool_display import (
24
29
  display_width,
@@ -31,9 +36,6 @@ from soothe_cli.tui.widgets.clipboard import (
31
36
  )
32
37
  from soothe_cli.tui.widgets.messages._helpers import (
33
38
  _RUNNING_SPINNER_INTERVAL_SECONDS,
34
- _STEP_TOOL_PREVIEW_ROWS,
35
- _assemble_card_header,
36
- _card_body_gutter,
37
39
  _is_widget_animation_visible,
38
40
  _should_refresh_now,
39
41
  _strip_success_exit_line,
@@ -645,7 +647,7 @@ class CognitionStepMessage(Vertical):
645
647
  return max(0, width - 2)
646
648
 
647
649
  def _step_task_activity_content(self) -> Content:
648
- """To-do then Tool-use (task markers, tool preview, notes) under the step title."""
650
+ """Tool-use then To-do (task markers, tool preview, notes) under the step title."""
649
651
  g = get_glyphs()
650
652
  try:
651
653
  colors = theme.get_theme_colors(self)
@@ -1643,7 +1645,7 @@ def create_subagent_card(
1643
1645
  """Create an intake-only orphan SubAgent card (shared step-card style path).
1644
1646
 
1645
1647
  In-step `task` delegations stay on the parent step card. This factory only
1646
- sets orphan fields (`_subagent_type`, etc.); header meta, To-do / Tool-use
1648
+ sets orphan fields (`_subagent_type`, etc.); header meta, Tool-use / To-do
1647
1649
  activity, and footers use the same `CognitionStepMessage` methods.
1648
1650
 
1649
1651
  Args:
@@ -17,6 +17,10 @@ from textual.content import Content
17
17
 
18
18
  from soothe_cli.commands.subagent_routing import get_subagent_display_name
19
19
  from soothe_cli.display import theme
20
+ from soothe_cli.display.card import (
21
+ _card_body_gutter,
22
+ _card_item_indent,
23
+ )
20
24
  from soothe_cli.display.preview_limits import (
21
25
  STEP_CARD_TOOL_ACTIVITY_PREVIEW_COUNT,
22
26
  TASK_DELEGATION_DESC_MAX_CHARS,
@@ -27,10 +31,6 @@ from soothe_cli.display.tool_display import (
27
31
  format_step_tool_activity_line,
28
32
  )
29
33
  from soothe_cli.runtime.presentation.duration_format import format_running_elapsed_compact
30
- from soothe_cli.tui.widgets.messages._helpers import (
31
- _card_body_gutter,
32
- _card_item_indent,
33
- )
34
34
 
35
35
  # Todo item status → phase_icon input.
36
36
  _TODO_STATUS_TO_PHASE: dict[str, str] = {
@@ -663,7 +663,7 @@ class StepCardStatusLine:
663
663
 
664
664
 
665
665
  class StepActivityTree:
666
- """Pure render: To-do + Tool-use sections under the step title.
666
+ """Pure render: Tool-use + To-do sections under the step title.
667
667
 
668
668
  Task rows are flat markers under Tool-use. While a task is running, the marker
669
669
  line shows that task's subgraph tool count. Nested child tool lines are not
@@ -686,14 +686,14 @@ class StepActivityTree:
686
686
  max_cols: int | None = None,
687
687
  glyph_override: str | None = None,
688
688
  ) -> Content:
689
- """To-do section then Tool-use section (task markers + main tool preview).
689
+ """Tool-use section then To-do section (task markers + main tool preview).
690
690
 
691
691
  `max_cols` bounds each rendered line to the terminal width so tool
692
692
  rows never wrap; `None` preserves the prior fixed-cap behavior.
693
693
  `glyph_override` is the card header glyph (subagent glyph for orphan
694
694
  SubAgent cards) so the activity gutters align to the right of that dot.
695
695
 
696
- Layout: the section header (`To-do` / `Tool-use`) sits on a `⎿`
696
+ Layout: the section header (`Tool-use` / `To-do`) sits on a `⎿`
697
697
  body-gutter line; its items (tool rows, todo markers, notes, `+N
698
698
  more`) are indented one column beyond the section label so they nest
699
699
  under the header without a tree glyph.
@@ -715,29 +715,7 @@ class StepActivityTree:
715
715
  return Content("")
716
716
 
717
717
  first_block = True
718
- if todo_items:
719
- first_block = False
720
- parts.append(Content.styled(f"{section_gutter}To-do", theme.SECONDARY_TEXT_STYLE))
721
- for item in todo_items:
722
- content = str(item.get("content") or "").strip()
723
- if not content:
724
- continue
725
- status = str(item.get("status") or "pending")
726
- phase = todo_status_phase(status)
727
- animate = phase == "running" and step_status == "running"
728
- icon = phase_icon(
729
- phase,
730
- g,
731
- spinner_position=spinner_position,
732
- animate_running=animate,
733
- )
734
- tone = task_tool_row_tone_for_phase(phase, colors)
735
- parts.append("\n")
736
- parts.append(Content.styled(f"{item_gutter}{icon} {content}", tone))
737
-
738
718
  if has_tools:
739
- if not first_block:
740
- parts.append("\n")
741
719
  first_block = False
742
720
  parts.append(Content.styled(f"{section_gutter}Tool-use", theme.SECONDARY_TEXT_STYLE))
743
721
 
@@ -806,4 +784,26 @@ class StepActivityTree:
806
784
  parts.append("\n")
807
785
  parts.append(Content.styled(f"{item_gutter}· {t}", theme.SECONDARY_TEXT_STYLE))
808
786
 
787
+ if todo_items:
788
+ if not first_block:
789
+ parts.append("\n")
790
+ first_block = False
791
+ parts.append(Content.styled(f"{section_gutter}To-do", theme.SECONDARY_TEXT_STYLE))
792
+ for item in todo_items:
793
+ content = str(item.get("content") or "").strip()
794
+ if not content:
795
+ continue
796
+ status = str(item.get("status") or "pending")
797
+ phase = todo_status_phase(status)
798
+ animate = phase == "running" and step_status == "running"
799
+ icon = phase_icon(
800
+ phase,
801
+ g,
802
+ spinner_position=spinner_position,
803
+ animate_running=animate,
804
+ )
805
+ tone = task_tool_row_tone_for_phase(phase, colors)
806
+ parts.append("\n")
807
+ parts.append(Content.styled(f"{item_gutter}{icon} {content}", tone))
808
+
809
809
  return Content.assemble(*parts) if parts else Content("")
@@ -8,7 +8,7 @@ from textual.content import Content
8
8
  from textual.widgets import Static
9
9
 
10
10
  from soothe_cli.display import theme
11
- from soothe_cli.tui.widgets.messages._helpers import _assemble_card_header
11
+ from soothe_cli.display.card import _assemble_card_header
12
12
 
13
13
 
14
14
  class ErrorMessage(Static):