soothe-cli 1.0.3__tar.gz → 1.0.5__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 (146) hide show
  1. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/PKG-INFO +2 -2
  2. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/pyproject.toml +1 -1
  3. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/cli/main.py +2 -2
  4. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/commands/slash_commands.py +1 -1
  5. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/config/cli_config.py +2 -2
  6. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/display/tips.py +1 -1
  7. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/app/_app.py +32 -15
  8. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/app/_execution.py +12 -16
  9. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/app/_messages_mixin.py +196 -38
  10. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/app/_model.py +1 -0
  11. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/app/_startup.py +7 -3
  12. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/app/_ui.py +30 -0
  13. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/app/app.tcss +12 -0
  14. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/composer_mode.py +22 -16
  15. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/textual_adapter.py +9 -6
  16. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/widgets/help_screen.py +1 -1
  17. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/widgets/loading.py +71 -1
  18. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/widgets/messages/cognition_goal_tree.py +13 -2
  19. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/widgets/messages/cognition_step.py +20 -2
  20. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/widgets/plan_quick_view_overlay.py +2 -2
  21. soothe_cli-1.0.5/src/soothe_cli/tui/widgets/queued_goals_bar.py +384 -0
  22. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/widgets/status.py +22 -65
  23. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/.gitignore +0 -0
  24. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/README.md +0 -0
  25. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/__init__.py +0 -0
  26. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/_cli_context.py +0 -0
  27. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/_env_vars.py +0 -0
  28. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/_version.py +0 -0
  29. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/card_wire.py +0 -0
  30. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/cli/__init__.py +0 -0
  31. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/cli/commands/__init__.py +0 -0
  32. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/cli/commands/autopilot_cmd.py +0 -0
  33. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/cli/commands/config_cmd.py +0 -0
  34. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/cli/commands/cron_cmd.py +0 -0
  35. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/cli/commands/loop_cmd.py +0 -0
  36. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/cli/commands/run_cmd.py +0 -0
  37. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/cli/commands/status_cmd.py +0 -0
  38. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/cli/execution/__init__.py +0 -0
  39. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/cli/execution/daemon.py +0 -0
  40. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/cli/execution/daemon_errors.py +0 -0
  41. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/cli/execution/headless.py +0 -0
  42. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/cli/execution/headless_renderer.py +0 -0
  43. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/cli/execution/launcher.py +0 -0
  44. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/cli/soothecli.py +0 -0
  45. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/commands/__init__.py +0 -0
  46. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/commands/binding.py +0 -0
  47. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/commands/command_registry.py +0 -0
  48. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/commands/command_router.py +0 -0
  49. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/commands/subagent_routing.py +0 -0
  50. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/config/__init__.py +0 -0
  51. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/config/loader.py +0 -0
  52. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/config/logging_setup.py +0 -0
  53. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/display/__init__.py +0 -0
  54. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/display/markdown_theme.py +0 -0
  55. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/display/media_utils.py +0 -0
  56. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/display/mermaid_render.py +0 -0
  57. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/display/path_utils.py +0 -0
  58. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/display/preview_limits.py +0 -0
  59. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/display/shell_color.py +0 -0
  60. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/display/spinner_labels.py +0 -0
  61. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/display/theme.py +0 -0
  62. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/display/tool_display.py +0 -0
  63. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/display/unicode_security.py +0 -0
  64. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/loops/__init__.py +0 -0
  65. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/loops/sessions.py +0 -0
  66. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/model_config.py +0 -0
  67. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/project_utils.py +0 -0
  68. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/runtime/__init__.py +0 -0
  69. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/runtime/headless/processor.py +0 -0
  70. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/runtime/headless/processor_state.py +0 -0
  71. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/runtime/parse/tool_call_resolution.py +0 -0
  72. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/runtime/policy/tui_trace_log.py +0 -0
  73. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/runtime/presentation/duration_format.py +0 -0
  74. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/runtime/presentation/engine.py +0 -0
  75. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/runtime/presentation/id_format.py +0 -0
  76. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/runtime/presentation/renderer_base.py +0 -0
  77. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/runtime/presentation/renderer_protocol.py +0 -0
  78. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/runtime/presentation/step_id_format.py +0 -0
  79. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/runtime/presentation/subagent_task_display.py +0 -0
  80. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/runtime/state/file_tracker.py +0 -0
  81. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/runtime/state/session_stats.py +0 -0
  82. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/runtime/state/step_router.py +0 -0
  83. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/runtime/state/stream_accumulator.py +0 -0
  84. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/runtime/token_events_debug.py +0 -0
  85. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/runtime/token_usage.py +0 -0
  86. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/runtime/transport/session.py +0 -0
  87. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/runtime/turn/prepare.py +0 -0
  88. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/runtime/wire/chunk_filter.py +0 -0
  89. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/runtime/wire/message_text.py +0 -0
  90. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/runtime/wire/messages.py +0 -0
  91. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/settings/__init__.py +0 -0
  92. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/settings/_console_impl.py +0 -0
  93. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/settings/bootstrap.py +0 -0
  94. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/settings/core.py +0 -0
  95. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/settings/glyphs.py +0 -0
  96. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/settings/provider.py +0 -0
  97. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/settings/shell_allow.py +0 -0
  98. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/settings/skills_dirs.py +0 -0
  99. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/settings/stream_config.py +0 -0
  100. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/__init__.py +0 -0
  101. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/app/__init__.py +0 -0
  102. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/app/_clarification.py +0 -0
  103. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/app/_entrypoints.py +0 -0
  104. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/app/_history.py +0 -0
  105. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/app/_model_params.py +0 -0
  106. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/app/_terminal.py +0 -0
  107. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/app/_theme_prefs.py +0 -0
  108. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/app/_types.py +0 -0
  109. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/file_change_notify.py +0 -0
  110. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/file_change_renderers.py +0 -0
  111. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/input.py +0 -0
  112. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/skills/__init__.py +0 -0
  113. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/skills/invocation.py +0 -0
  114. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/skills/load.py +0 -0
  115. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/widgets/__init__.py +0 -0
  116. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/widgets/_links.py +0 -0
  117. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/widgets/autocomplete.py +0 -0
  118. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/widgets/chat_input.py +0 -0
  119. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/widgets/clipboard.py +0 -0
  120. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/widgets/context_data.py +0 -0
  121. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/widgets/context_viewer.py +0 -0
  122. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/widgets/diff.py +0 -0
  123. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/widgets/editor.py +0 -0
  124. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/widgets/file_change_preview.py +0 -0
  125. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/widgets/history.py +0 -0
  126. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/widgets/loop_selector.py +0 -0
  127. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/widgets/mcp_viewer.py +0 -0
  128. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/widgets/message_store.py +0 -0
  129. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/widgets/messages/__init__.py +0 -0
  130. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/widgets/messages/_helpers.py +0 -0
  131. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/widgets/messages/app.py +0 -0
  132. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/widgets/messages/assistant.py +0 -0
  133. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/widgets/messages/cognition_reason.py +0 -0
  134. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/widgets/messages/cognition_step_activity.py +0 -0
  135. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/widgets/messages/diff_message.py +0 -0
  136. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/widgets/messages/error.py +0 -0
  137. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/widgets/messages/skill.py +0 -0
  138. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/widgets/messages/structured_ask_user.py +0 -0
  139. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/widgets/messages/user.py +0 -0
  140. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/widgets/model_selector.py +0 -0
  141. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/widgets/notification_settings.py +0 -0
  142. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/widgets/pinned_goal_bar.py +0 -0
  143. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/widgets/router_profile_selector.py +0 -0
  144. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/widgets/theme_selector.py +0 -0
  145. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/widgets/welcome.py +0 -0
  146. {soothe_cli-1.0.3 → soothe_cli-1.0.5}/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.3
3
+ Version: 1.0.5
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
@@ -21,7 +21,7 @@ Requires-Dist: pillow<12.0.0,>=10.0.0
21
21
  Requires-Dist: python-dotenv<2.0.0,>=1.0.0
22
22
  Requires-Dist: pyyaml<7.0.0,>=6.0.0
23
23
  Requires-Dist: rich<15.0.0,>=13.0.0
24
- Requires-Dist: soothe-client-python<2.0.0,>=1.0.18
24
+ Requires-Dist: soothe-client-python<2.0.0,>=1.0.21
25
25
  Requires-Dist: soothe-sdk<2.0.0,>=1.0.12
26
26
  Requires-Dist: termaid<1.0.0,>=0.7.1
27
27
  Requires-Dist: textual<10.0.0,>=8.0.0
@@ -23,7 +23,7 @@ classifiers = [
23
23
  ]
24
24
  dependencies = [
25
25
  "soothe-sdk>=1.0.12,<2.0.0", # events/display/wire contracts
26
- "soothe-client-python>=1.0.18,<2.0.0", # WebSocket transport client; matched to latest release v1.0.18
26
+ "soothe-client-python>=1.0.21,<2.0.0", # WebSocket transport client; matched to latest release v1.0.21
27
27
  "typer>=0.9.0,<2.0.0", # CLI framework
28
28
  "textual>=8.0.0,<10.0.0", # TUI framework
29
29
  "rich>=13.0.0,<15.0.0", # Console output
@@ -198,10 +198,10 @@ def main(
198
198
  help=(
199
199
  "Auto-show the in-flow plan panel while a goal is executing "
200
200
  "and hide it when the goal completes. Ctrl+t toggles "
201
- "thereafter. Default: enabled."
201
+ "thereafter. Default: disabled (hidden)."
202
202
  ),
203
203
  ),
204
- ] = True,
204
+ ] = False,
205
205
  show_version: Annotated[ # noqa: FBT002
206
206
  bool,
207
207
  typer.Option("--version", help="Show version and exit."),
@@ -154,7 +154,7 @@ KEYBOARD_SHORTCUTS: dict[str, str] = {
154
154
  "Ctrl+V": "Paste image from clipboard as [image N] attachment",
155
155
  "Ctrl+T": "Toggle plan panel above thinking row",
156
156
  "Ctrl+O": "Toggle expand/collapse of the most recent skill or tool card",
157
- "Shift+Tab": "Cycle composer mode (Auto → Manual → Plan → Ask)",
157
+ "Shift+Tab": "Cycle composer mode (Auto → Bypass → Manual → Plan → Ask)",
158
158
  }
159
159
 
160
160
 
@@ -33,9 +33,9 @@ class CLIConfig:
33
33
  """Markdown appearance preset (``match-app``, ``langchain``, ``standard``, …)."""
34
34
 
35
35
  # Plan panel visibility preference
36
- plan_panel_default_visible: bool = True
36
+ plan_panel_default_visible: bool = False
37
37
  """When True, auto-show the in-flow plan panel while a goal is executing and
38
- hide it when the goal completes. Default True (Ctrl+t to close)."""
38
+ hide it when the goal completes. Default False (hidden; Ctrl+t to open)."""
39
39
 
40
40
  # Output streaming overrides (RFC-614)
41
41
  output_streaming_enabled: bool | None = None
@@ -1,4 +1,4 @@
1
- """Rotating session tips for the status bar below the chat input."""
1
+ """Rotating session tips for the thinking row shown when the loop is idle."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
@@ -40,7 +40,7 @@ from soothe_cli.tui.app._types import (
40
40
  from soothe_cli.tui.app._ui import _UIMixin
41
41
  from soothe_cli.tui.composer_mode import normalize_composer_mode
42
42
  from soothe_cli.tui.widgets.chat_input import ChatInput
43
- from soothe_cli.tui.widgets.loading import LoadingWidget
43
+ from soothe_cli.tui.widgets.loading import LoadingWidget, TipRow
44
44
  from soothe_cli.tui.widgets.message_store import MessageStore
45
45
  from soothe_cli.tui.widgets.messages import (
46
46
  AssistantMessage,
@@ -48,6 +48,7 @@ from soothe_cli.tui.widgets.messages import (
48
48
  )
49
49
  from soothe_cli.tui.widgets.pinned_goal_bar import PinnedGoalBar
50
50
  from soothe_cli.tui.widgets.plan_quick_view_overlay import PlanQuickViewOverlay
51
+ from soothe_cli.tui.widgets.queued_goals_bar import QueuedGoalsBar
51
52
  from soothe_cli.tui.widgets.status import StatusBar
52
53
  from soothe_cli.tui.widgets.welcome import WelcomeBanner
53
54
 
@@ -133,6 +134,13 @@ class SootheApp(
133
134
  show=False,
134
135
  priority=True,
135
136
  ),
137
+ Binding(
138
+ "ctrl+q",
139
+ "focus_queued_goals",
140
+ "Focus Queued Goals",
141
+ show=False,
142
+ priority=True,
143
+ ),
136
144
  ]
137
145
  """App-level keybindings for dismiss, quit, and navigation."""
138
146
 
@@ -226,10 +234,11 @@ class SootheApp(
226
234
  self._mcp_tool_count = sum(len(s.tools) for s in (mcp_server_info or []))
227
235
 
228
236
  self._status_bar: StatusBar | None = None
237
+ self._tip_row: TipRow | None = None
229
238
  self._default_session_tip: str = ""
230
239
  self._status_notification_timer: Timer | None = None
231
240
  self._status_notification_active = False
232
- # Rotating tip source + interval timer for the status footer.
241
+ # Rotating tip source + interval timer for the thinking row.
233
242
  self._tip_rotator: TipRotator = TipRotator()
234
243
  self._tip_rotation_timer: Timer | None = None
235
244
 
@@ -381,11 +390,11 @@ class SootheApp(
381
390
  """Persist and render the fallback tip shown when no notification is active."""
382
391
  cleaned = (tip or "").strip()
383
392
  self._default_session_tip = cleaned
384
- if self._status_bar is not None and not self._status_notification_active:
385
- self._status_bar.set_session_tip(cleaned)
393
+ if self._tip_row is not None and not self._status_notification_active:
394
+ self._tip_row.set_tip(cleaned)
386
395
 
387
396
  def start_tip_rotation(self, interval: float = 12.0) -> None:
388
- """Cycle through rotating tips in the status footer at a fixed interval.
397
+ """Cycle through rotating tips in the thinking row at a fixed interval.
389
398
 
390
399
  Args:
391
400
  interval: Seconds between tip rotations.
@@ -401,7 +410,7 @@ class SootheApp(
401
410
  self._tip_rotation_timer = None
402
411
 
403
412
  def _rotate_session_tip(self) -> None:
404
- """Advance to the next tip and push it to the status footer.
413
+ """Advance to the next tip and push it to the thinking row.
405
414
 
406
415
  Transient notifications take precedence: rotation is skipped while a
407
416
  notification is active so it can run its timeout uninterrupted.
@@ -411,7 +420,7 @@ class SootheApp(
411
420
  self.set_default_session_tip(self._tip_rotator.next_tip())
412
421
 
413
422
  def _set_status_notification(self, message: str, *, timeout: float | None = None) -> None:
414
- """Render a transient notification in the status-tip area."""
423
+ """Render a transient notification in the thinking-row tip area."""
415
424
  text = (message or "").strip()
416
425
  if not text:
417
426
  return
@@ -420,8 +429,8 @@ class SootheApp(
420
429
  self._status_notification_timer.stop()
421
430
  self._status_notification_timer = None
422
431
  self._status_notification_active = True
423
- if self._status_bar is not None:
424
- self._status_bar.set_notification_message(text)
432
+ if self._tip_row is not None:
433
+ self._tip_row.set_notification(text)
425
434
 
426
435
  duration = timeout if timeout and timeout > 0 else 3.0
427
436
  self._status_notification_timer = self.set_timer(duration, self._clear_status_notification)
@@ -430,8 +439,8 @@ class SootheApp(
430
439
  """Restore the default tip after transient notification timeout."""
431
440
  self._status_notification_timer = None
432
441
  self._status_notification_active = False
433
- if self._status_bar is not None:
434
- self._status_bar.set_session_tip(self._default_session_tip)
442
+ if self._tip_row is not None:
443
+ self._tip_row.set_tip(self._default_session_tip)
435
444
 
436
445
  def notify( # type: ignore[override]
437
446
  self,
@@ -442,7 +451,7 @@ class SootheApp(
442
451
  timeout: float | None = None,
443
452
  markup: bool = True, # noqa: ARG002
444
453
  ) -> None:
445
- """Display notifications in the status-tip area instead of toast bubbles."""
454
+ """Display notifications in the thinking-row tip area instead of toast bubbles."""
446
455
  text = str(message or "").strip()
447
456
  if not text:
448
457
  return
@@ -488,14 +497,22 @@ class SootheApp(
488
497
  # In-flow plan panel: sits above the sticky bottom chrome so Ctrl+t
489
498
  # never covers the thinking row or chat input. Auto-shows while a goal
490
499
  # is executing when CLIConfig.plan_panel_default_visible is True
491
- # (default); --no-plan-panel disables it.
492
- plan_visible = getattr(self._daemon_config, "plan_panel_default_visible", True)
500
+ # (--plan-panel); default is False (hidden, Ctrl+t to open).
501
+ plan_visible = getattr(self._daemon_config, "plan_panel_default_visible", False)
493
502
  yield PlanQuickViewOverlay(
494
503
  id="plan-quick-view-overlay",
495
504
  default_visible=plan_visible,
496
505
  )
497
506
  with Container(id="bottom-app-container"):
498
- yield Container(id="thinking-status")
507
+ # Queued goals bar — inside the docked bottom container, above
508
+ # the thinking row and chat input. Hidden when the queue is
509
+ # empty; shows all pending queued goals as selectable rows.
510
+ # Placed inside #bottom-app-container (rather than as a separate
511
+ # docked sibling) because Textual does not stack multiple
512
+ # docked-to-same-edge widgets — they overlap instead.
513
+ yield QueuedGoalsBar(id="queued-goals")
514
+ with Container(id="thinking-status"):
515
+ yield TipRow(id="tip-row")
499
516
  yield ChatInput(
500
517
  cwd=self._cwd,
501
518
  image_tracker=self._image_tracker,
@@ -189,10 +189,7 @@ class _ExecutionMixin:
189
189
  await self._process_message(value, mode)
190
190
  return
191
191
  self._pending_messages.append(QueuedMessage(text=value, mode=mode))
192
- queued_widget = QueuedUserMessage(value)
193
- self._queued_widgets.append(queued_widget)
194
192
  self._refresh_queued_goal_tips()
195
- await self._mount_message(queued_widget)
196
193
  return
197
194
 
198
195
  await self._process_message(value, mode)
@@ -576,14 +573,12 @@ class _ExecutionMixin:
576
573
  if self._agent_running or self._shell_running:
577
574
  queued_widget = QueuedUserMessage(command)
578
575
  self._queued_widgets.append(queued_widget)
579
- self._refresh_queued_goal_tips()
580
576
  await self._mount_message(queued_widget)
581
577
 
582
578
  async def _mount_output() -> None:
583
579
  # Remove the ephemeral queued widget, then mount real output.
584
580
  if queued_widget in self._queued_widgets:
585
581
  self._queued_widgets.remove(queued_widget)
586
- self._refresh_queued_goal_tips()
587
582
  with suppress(Exception):
588
583
  await queued_widget.remove()
589
584
  await self._mount_message(UserMessage(command))
@@ -677,6 +672,7 @@ class _ExecutionMixin:
677
672
  elif cmd == "/clear":
678
673
  self._pending_messages.clear()
679
674
  self._queued_widgets.clear()
675
+ self._refresh_queued_goal_tips()
680
676
  await self._clear_messages()
681
677
  self._reset_loop_token_usage(None)
682
678
  # Clear status message (e.g., "Interrupted" from previous session)
@@ -941,6 +937,10 @@ class _ExecutionMixin:
941
937
  )
942
938
  )
943
939
  return
940
+ except Exception as exc:
941
+ logger.warning("Unexpected error connecting to daemon", exc_info=True)
942
+ await self._mount_message(ErrorMessage(f"Unexpected connection error. {exc}"))
943
+ return
944
944
  self._agent_running = True
945
945
 
946
946
  # Stop passive reads so active turn streaming has sole websocket access.
@@ -1093,6 +1093,11 @@ class _ExecutionMixin:
1093
1093
  continue
1094
1094
  except (ConnectionError, OSError, TimeoutError):
1095
1095
  pass
1096
+ except Exception:
1097
+ logger.debug(
1098
+ "Unexpected error during daemon reconnect retry",
1099
+ exc_info=True,
1100
+ )
1096
1101
  raise
1097
1102
  except Exception as e: # Resilient tool rendering
1098
1103
  # Attach-only idle timeout is benign: the prior turn had already
@@ -1156,12 +1161,7 @@ class _ExecutionMixin:
1156
1161
  self._processing_pending = True
1157
1162
  try:
1158
1163
  msg = self._pending_messages.popleft()
1159
-
1160
- # Remove the ephemeral queued-message widget
1161
- if self._queued_widgets:
1162
- widget = self._queued_widgets.popleft()
1163
- await widget.remove()
1164
- self._refresh_queued_goal_tips()
1164
+ self._refresh_queued_goal_tips()
1165
1165
 
1166
1166
  await self._process_message(msg.text, msg.mode)
1167
1167
  except Exception:
@@ -1292,11 +1292,7 @@ class _ExecutionMixin:
1292
1292
  if self._pending_messages:
1293
1293
  msg = self._pending_messages.popleft()
1294
1294
  prompt = msg.text
1295
- if self._queued_widgets:
1296
- widget = self._queued_widgets.popleft()
1297
- with suppress(Exception):
1298
- await widget.remove()
1299
- self._refresh_queued_goal_tips()
1295
+ self._refresh_queued_goal_tips()
1300
1296
  if msg.mode != "normal":
1301
1297
  # Shell/command still need their normal handlers.
1302
1298
  await self._process_message(msg.text, msg.mode)
@@ -38,6 +38,7 @@ from soothe_cli.tui.widgets.messages import (
38
38
  SkillMessage,
39
39
  SummarizationMessage,
40
40
  )
41
+ from soothe_cli.tui.widgets.queued_goals_bar import QueuedGoalsBar
41
42
 
42
43
  logger = logging.getLogger(__name__)
43
44
 
@@ -66,17 +67,40 @@ class _MessagesMixin:
66
67
  """Message widget lifecycle, store management, queue, interrupt/quit, toggles, editor, and events."""
67
68
 
68
69
  def _refresh_queued_goal_tips(self) -> None:
69
- """Show queue interaction tips on the most recently queued goal only."""
70
- if not self._queued_widgets:
70
+ """Sync the queued goals bar with the current queue state.
71
+
72
+ Feeds the current ``_pending_messages`` snapshot to the
73
+ ``QueuedGoalsBar`` so it re-renders all rows. Shows queue
74
+ interaction tips on the bar when the tail goal is a normal-mode
75
+ goal and the agent is running.
76
+ """
77
+ self._sync_queued_goals_bar()
78
+ if not self._pending_messages:
71
79
  return
72
- tip_index: int | None = None
80
+ show_tips = False
73
81
  if self._pending_messages and self._pending_messages[-1].mode == "normal":
74
- pending_tail_index = len(self._pending_messages) - 1
75
- if pending_tail_index < len(self._queued_widgets):
76
- tip_index = pending_tail_index
77
- for index, widget in enumerate(self._queued_widgets):
78
- with suppress(Exception):
79
- widget.set_show_queue_tips(tip_index is not None and index == tip_index)
82
+ show_tips = True
83
+ try:
84
+ bar = self.query_one("#queued-goals", QueuedGoalsBar)
85
+ except Exception:
86
+ # NoMatches, ScreenStackError, or no app/screen in test stubs.
87
+ return
88
+ with suppress(Exception):
89
+ bar.set_show_tips(show_tips)
90
+
91
+ def _sync_queued_goals_bar(self) -> None:
92
+ """Push the current ``_pending_messages`` snapshot to the bar widget.
93
+
94
+ Safe to call when the bar is not yet mounted (e.g. during startup
95
+ or in unit tests using stubs) — the call is silently skipped.
96
+ """
97
+ try:
98
+ bar = self.query_one("#queued-goals", QueuedGoalsBar)
99
+ except Exception:
100
+ # NoMatches, ScreenStackError, or no app/screen in test stubs.
101
+ return
102
+ goals = list(self._pending_messages)
103
+ bar.set_goals(goals)
80
104
 
81
105
  def _has_pending_chat_input(self) -> bool:
82
106
  """Return whether chat input has draft content that should be preserved."""
@@ -369,7 +393,11 @@ class _MessagesMixin:
369
393
  return False
370
394
 
371
395
  def _assistant_card_already_visible(self, card: Any) -> bool:
372
- """True when stream already mounted this assistant body (avoid double report)."""
396
+ """True when the stream already mounted this assistant body.
397
+
398
+ Also suppresses the raw plan ASSISTANT card when a plan-review
399
+ `StructuredAskUserWidget` with overlapping body is mounted.
400
+ """
373
401
  from soothe_sdk.display.transcript_types import MessageType
374
402
 
375
403
  adapter = getattr(self, "_ui_adapter", None)
@@ -388,6 +416,54 @@ class _MessagesMixin:
388
416
  return True
389
417
  # Only inspect the latest assistant card.
390
418
  break
419
+ # Plan-mode: suppress ASSISTANT card when a plan-review widget already
420
+ # shows the same plan body.
421
+ if self._plan_review_widget_shows_text(text):
422
+ return True
423
+ return False
424
+
425
+ @staticmethod
426
+ def _text_overlaps_plan_body(text: str, body: str) -> bool:
427
+ """True when `text` and `body` overlap (exact, prefix, substring, or 200-char prefix).
428
+
429
+ Substring checks require the shorter string to be at least 50 chars
430
+ to avoid false positives.
431
+ """
432
+ if not text or not body:
433
+ return False
434
+ if text == body or text.startswith(body) or body.startswith(text):
435
+ return True
436
+ # Substring containment: handles preface trimming (e.g. "Here is the
437
+ # plan.\n\n## Plan:..." card vs "## Plan:..." widget body). Require the
438
+ # shorter string to be at least 50 chars to avoid false positives.
439
+ shorter = body if len(body) <= len(text) else text
440
+ longer = text if shorter is body else body
441
+ if len(shorter) >= 50 and shorter in longer:
442
+ return True
443
+ # Handle truncation: compare a significant prefix of the shorter text.
444
+ prefix_len = min(200, len(text), len(body))
445
+ if prefix_len >= 50 and text[:prefix_len] == body[:prefix_len]:
446
+ return True
447
+ return False
448
+
449
+ def _plan_review_widget_shows_text(self, text: str) -> bool:
450
+ """True when a mounted plan-review widget's body overlaps `text`.
451
+
452
+ Only plan-review widgets (`_is_plan_review`) are considered, so
453
+ non-plan ASSISTANT cards are never suppressed.
454
+ """
455
+ adapter = getattr(self, "_ui_adapter", None)
456
+ if adapter is None:
457
+ return False
458
+ by_step = getattr(adapter, "_clarification_input_by_step", None) or {}
459
+ for widget in by_step.values():
460
+ if not getattr(widget, "_is_plan_review", False):
461
+ continue
462
+ body = str(getattr(widget, "_body_markdown", "") or "").strip()
463
+ if not body:
464
+ continue
465
+ if self._text_overlaps_plan_body(text, body):
466
+ return True
391
467
  return False
392
468
 
393
469
  def _cognition_reason_already_visible(self, card: Any) -> bool:
@@ -624,18 +700,15 @@ class _MessagesMixin:
624
700
  await self._set_spinner(None)
625
701
 
626
702
  def _pop_last_queued_entry(self) -> Any | None:
627
- """Pop the latest queued message and paired widget, if available."""
703
+ """Pop the latest queued message, if available.
704
+
705
+ Removes the tail entry from ``_pending_messages`` and refreshes
706
+ the ``QueuedGoalsBar`` so the row disappears.
707
+ """
628
708
  if not self._pending_messages:
629
709
  return None
630
710
  msg = self._pending_messages.pop()
631
- if self._queued_widgets:
632
- widget = self._queued_widgets.pop()
633
- widget.remove()
634
- self._refresh_queued_goal_tips()
635
- else:
636
- logger.warning(
637
- "Queued-widget deque empty while pending-messages was not; widget/message tracking may be out of sync"
638
- )
711
+ self._refresh_queued_goal_tips()
639
712
  return msg
640
713
 
641
714
  def _restore_last_queued_goal_to_input(self) -> bool:
@@ -661,11 +734,10 @@ class _MessagesMixin:
661
734
  return True
662
735
 
663
736
  def _discard_queue(self) -> None:
664
- """Clear pending messages, deferred actions, and queued widgets."""
737
+ """Clear pending messages, deferred actions, and queued goals bar."""
665
738
  self._pending_messages.clear()
666
- for w in self._queued_widgets:
667
- w.remove()
668
739
  self._queued_widgets.clear()
740
+ self._refresh_queued_goal_tips()
669
741
  self._deferred_actions.clear()
670
742
 
671
743
  def _cancel_last_queued_message(self) -> bool:
@@ -687,6 +759,86 @@ class _MessagesMixin:
687
759
  return False
688
760
  return self._restore_last_queued_goal_to_input()
689
761
 
762
+ def cancel_queued_goal_at_index(self, index: int) -> bool:
763
+ """Cancel the queued goal at ``index`` without restoring it to input.
764
+
765
+ Args:
766
+ index: Zero-based index into ``_pending_messages`` (0 = head/oldest).
767
+
768
+ Returns:
769
+ ``True`` when a goal was cancelled, ``False`` when the index is
770
+ out of range.
771
+ """
772
+ if index < 0 or index >= len(self._pending_messages):
773
+ return False
774
+ # deque doesn't support del[] — convert to list, remove, rebuild.
775
+ items = list(self._pending_messages)
776
+ msg = items.pop(index)
777
+ self._pending_messages.clear()
778
+ self._pending_messages.extend(items)
779
+ self._refresh_queued_goal_tips()
780
+ self.notify(f"Cancelled queued goal: {msg.text[:60]}", timeout=2)
781
+ return True
782
+
783
+ def edit_queued_goal_at_index(self, index: int) -> bool:
784
+ """Move the queued goal at ``index`` into the chat input for editing.
785
+
786
+ Only normal-mode goals can be edited; shell/command goals are skipped
787
+ to avoid clobbering the input with a mode prefix.
788
+
789
+ Args:
790
+ index: Zero-based index into ``_pending_messages`` (0 = head/oldest).
791
+
792
+ Returns:
793
+ ``True`` when the goal was moved to input, ``False`` otherwise.
794
+ """
795
+ if index < 0 or index >= len(self._pending_messages):
796
+ return False
797
+ if self._has_pending_chat_input():
798
+ return False
799
+ items = list(self._pending_messages)
800
+ msg = items[index]
801
+ if msg.mode != "normal":
802
+ return False
803
+ items.pop(index)
804
+ self._pending_messages.clear()
805
+ self._pending_messages.extend(items)
806
+ if self._chat_input:
807
+ self._chat_input.value = msg.text
808
+ self._refresh_queued_goal_tips()
809
+ self.notify("Queued goal moved to input", timeout=2)
810
+ return True
811
+
812
+ def submit_queued_goal_at_index(self, index: int) -> bool:
813
+ """Submit the queued goal at ``index`` for immediate execution.
814
+
815
+ Pops the goal from the queue and processes it right away, bypassing
816
+ the normal FIFO order. Only valid when no agent/worker is already
817
+ running, since submitting while busy would just re-queue the goal.
818
+
819
+ Args:
820
+ index: Zero-based index into ``_pending_messages`` (0 = head/oldest).
821
+
822
+ Returns:
823
+ ``True`` when the goal was submitted, ``False`` when the index is
824
+ out of range or an agent is already running.
825
+ """
826
+ if index < 0 or index >= len(self._pending_messages):
827
+ return False
828
+ if self._agent_running or self._shell_running or self._processing_pending:
829
+ self.notify("Agent is busy — goal stays queued", timeout=2)
830
+ return False
831
+ items = list(self._pending_messages)
832
+ msg = items.pop(index)
833
+ self._pending_messages.clear()
834
+ self._pending_messages.extend(items)
835
+ self._refresh_queued_goal_tips()
836
+ import asyncio
837
+
838
+ asyncio.ensure_future(self._process_message(msg.text, msg.mode))
839
+ self.notify(f"Submitting: {msg.text[:60]}", timeout=2)
840
+ return True
841
+
690
842
  def _defer_action(self, action: DeferredAction) -> None:
691
843
  """Queue a deferred action, replacing any existing action of the same kind.
692
844
 
@@ -891,13 +1043,25 @@ class _MessagesMixin:
891
1043
 
892
1044
  # Close completion popup or exit slash/shell command mode
893
1045
  if self._chat_input:
894
- # When queue has pending goals and input is idle, Esc cancels the
895
- # latest queued normal goal (without interrupting current work).
1046
+ # When queue has pending goals and input is idle, Esc cancels a
1047
+ # queued normal goal (without interrupting current work). If the
1048
+ # QueuedGoalsBar is focused, cancel the selected goal; otherwise
1049
+ # cancel the latest (tail) goal.
896
1050
  if not self._has_pending_chat_input():
897
- queue_tail = self._pending_messages[-1] if self._pending_messages else None
898
- if queue_tail is not None and queue_tail.mode == "normal":
899
- if self._cancel_last_queued_message():
900
- return
1051
+ cancelled = False
1052
+ try:
1053
+ bar = self.query_one("#queued-goals", QueuedGoalsBar)
1054
+ except Exception:
1055
+ # NoMatches, ScreenStackError, or no app/screen in test stubs.
1056
+ bar = None
1057
+ if bar is not None and self.focused is bar:
1058
+ cancelled = bar.cancel_selected()
1059
+ if not cancelled:
1060
+ queue_tail = self._pending_messages[-1] if self._pending_messages else None
1061
+ if queue_tail is not None and queue_tail.mode == "normal":
1062
+ cancelled = self._cancel_last_queued_message()
1063
+ if cancelled:
1064
+ return
901
1065
  if self._chat_input.dismiss_completion():
902
1066
  return
903
1067
  if self._chat_input.exit_mode():
@@ -983,7 +1147,7 @@ class _MessagesMixin:
983
1147
  """Shift+Tab: navigate loop selector when active, otherwise cycle mode.
984
1148
 
985
1149
  - In the LoopSelectorScreen, defer to its filter navigation.
986
- - On the main screen, cycle composer mode Auto → Manual → Plan → Ask.
1150
+ - On the main screen, cycle composer mode Auto → Bypass → Manual → Plan → Ask.
987
1151
  """
988
1152
  from soothe_cli.tui.widgets.loop_selector import LoopSelectorScreen
989
1153
 
@@ -995,15 +1159,9 @@ class _MessagesMixin:
995
1159
  def cycle_composer_mode(self) -> None:
996
1160
  """Advance composer mode and refresh the status-bar badge.
997
1161
 
998
- The mode is held on the app (`self._composer_mode`). Auto/Manual map
999
- to the `clarification_mode` wire field; Plan sets
1000
- `interaction_mode=plan` on subsequent turns. The badge updates
1001
- immediately; no toast is emitted because the badge is the feedback.
1002
-
1003
- When a goal is running and the new mode is Auto or Manual, the mode is
1004
- also hot-swapped on the daemon's live `LoopRuntimeContext` via the
1005
- `loop_set_clarification_mode` RPC so the next clarification uses the
1006
- new policy without waiting for the next turn.
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`.
1007
1165
  """
1008
1166
  from soothe_cli.tui.composer_mode import next_composer_mode
1009
1167
 
@@ -486,6 +486,7 @@ class _ModelMixin:
486
486
  # Clear conversation (similar to /clear, without creating a new loop)
487
487
  self._pending_messages.clear()
488
488
  self._queued_widgets.clear()
489
+ self._refresh_queued_goal_tips()
489
490
  await self._clear_messages()
490
491
  self._reset_loop_token_usage(None)
491
492
  self._update_status("")
@@ -24,6 +24,7 @@ from soothe_cli.tui.app._types import (
24
24
  TextualSessionState,
25
25
  )
26
26
  from soothe_cli.tui.widgets.chat_input import ChatInput
27
+ from soothe_cli.tui.widgets.loading import TipRow
27
28
  from soothe_cli.tui.widgets.messages import (
28
29
  AppMessage,
29
30
  ErrorMessage,
@@ -129,6 +130,7 @@ class _StartupMixin:
129
130
  self.watch(chat, "scroll_y", self._on_chat_scroll_y_changed)
130
131
 
131
132
  self._status_bar = self.query_one("#status-bar", StatusBar)
133
+ self._tip_row = self.query_one("#tip-row", TipRow)
132
134
  self._chat_input = self.query_one("#input-area", ChatInput)
133
135
  with suppress(NoMatches):
134
136
  from soothe_cli.tui.widgets.plan_quick_view_overlay import PlanQuickViewOverlay
@@ -142,8 +144,11 @@ class _StartupMixin:
142
144
  # hard-coded "auto" fallback before the daemon-default seed runs).
143
145
  self._status_bar.set_clarification_mode(self._composer_mode)
144
146
 
145
- # Seed the status footer with the first rotating tip and start rotation.
147
+ # Seed the thinking-row tip with the first rotating tip and start rotation.
148
+ # The tip row is visible while the loop is idle; `_set_spinner` hides it
149
+ # when the agent starts working and restores it when the loop goes idle.
146
150
  self.set_default_session_tip(self._tip_rotator.next_tip())
151
+ self._show_tip_row()
147
152
  self.start_tip_rotation()
148
153
 
149
154
  # Focus the input immediately so the cursor is visible on first paint
@@ -641,9 +646,8 @@ class _StartupMixin:
641
646
  # Discard any messages queued while connecting
642
647
  if self._pending_messages:
643
648
  self._pending_messages.clear()
644
- for w in self._queued_widgets:
645
- w.remove()
646
649
  self._queued_widgets.clear()
650
+ self._refresh_queued_goal_tips()
647
651
  self._deferred_actions.clear()
648
652
 
649
653
  @staticmethod