soothe-cli 1.0.3__tar.gz → 1.0.4__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 (145) hide show
  1. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/.gitignore +1 -0
  2. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/PKG-INFO +2 -2
  3. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/pyproject.toml +1 -1
  4. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/cli/main.py +2 -2
  5. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/commands/slash_commands.py +1 -1
  6. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/config/cli_config.py +2 -2
  7. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/display/tips.py +1 -1
  8. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/tui/app/_app.py +17 -15
  9. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/tui/app/_execution.py +9 -0
  10. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/tui/app/_messages_mixin.py +4 -10
  11. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/tui/app/_startup.py +6 -1
  12. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/tui/app/_ui.py +12 -0
  13. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/tui/composer_mode.py +22 -16
  14. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/tui/textual_adapter.py +9 -6
  15. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/tui/widgets/help_screen.py +1 -1
  16. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/tui/widgets/loading.py +71 -1
  17. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/tui/widgets/messages/cognition_goal_tree.py +13 -2
  18. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/tui/widgets/messages/cognition_step.py +20 -2
  19. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/tui/widgets/plan_quick_view_overlay.py +2 -2
  20. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/tui/widgets/status.py +22 -65
  21. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/README.md +0 -0
  22. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/__init__.py +0 -0
  23. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/_cli_context.py +0 -0
  24. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/_env_vars.py +0 -0
  25. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/_version.py +0 -0
  26. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/card_wire.py +0 -0
  27. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/cli/__init__.py +0 -0
  28. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/cli/commands/__init__.py +0 -0
  29. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/cli/commands/autopilot_cmd.py +0 -0
  30. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/cli/commands/config_cmd.py +0 -0
  31. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/cli/commands/cron_cmd.py +0 -0
  32. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/cli/commands/loop_cmd.py +0 -0
  33. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/cli/commands/run_cmd.py +0 -0
  34. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/cli/commands/status_cmd.py +0 -0
  35. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/cli/execution/__init__.py +0 -0
  36. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/cli/execution/daemon.py +0 -0
  37. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/cli/execution/daemon_errors.py +0 -0
  38. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/cli/execution/headless.py +0 -0
  39. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/cli/execution/headless_renderer.py +0 -0
  40. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/cli/execution/launcher.py +0 -0
  41. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/cli/soothecli.py +0 -0
  42. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/commands/__init__.py +0 -0
  43. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/commands/binding.py +0 -0
  44. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/commands/command_registry.py +0 -0
  45. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/commands/command_router.py +0 -0
  46. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/commands/subagent_routing.py +0 -0
  47. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/config/__init__.py +0 -0
  48. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/config/loader.py +0 -0
  49. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/config/logging_setup.py +0 -0
  50. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/display/__init__.py +0 -0
  51. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/display/markdown_theme.py +0 -0
  52. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/display/media_utils.py +0 -0
  53. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/display/mermaid_render.py +0 -0
  54. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/display/path_utils.py +0 -0
  55. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/display/preview_limits.py +0 -0
  56. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/display/shell_color.py +0 -0
  57. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/display/spinner_labels.py +0 -0
  58. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/display/theme.py +0 -0
  59. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/display/tool_display.py +0 -0
  60. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/display/unicode_security.py +0 -0
  61. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/loops/__init__.py +0 -0
  62. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/loops/sessions.py +0 -0
  63. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/model_config.py +0 -0
  64. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/project_utils.py +0 -0
  65. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/runtime/__init__.py +0 -0
  66. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/runtime/headless/processor.py +0 -0
  67. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/runtime/headless/processor_state.py +0 -0
  68. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/runtime/parse/tool_call_resolution.py +0 -0
  69. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/runtime/policy/tui_trace_log.py +0 -0
  70. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/runtime/presentation/duration_format.py +0 -0
  71. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/runtime/presentation/engine.py +0 -0
  72. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/runtime/presentation/id_format.py +0 -0
  73. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/runtime/presentation/renderer_base.py +0 -0
  74. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/runtime/presentation/renderer_protocol.py +0 -0
  75. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/runtime/presentation/step_id_format.py +0 -0
  76. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/runtime/presentation/subagent_task_display.py +0 -0
  77. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/runtime/state/file_tracker.py +0 -0
  78. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/runtime/state/session_stats.py +0 -0
  79. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/runtime/state/step_router.py +0 -0
  80. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/runtime/state/stream_accumulator.py +0 -0
  81. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/runtime/token_events_debug.py +0 -0
  82. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/runtime/token_usage.py +0 -0
  83. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/runtime/transport/session.py +0 -0
  84. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/runtime/turn/prepare.py +0 -0
  85. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/runtime/wire/chunk_filter.py +0 -0
  86. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/runtime/wire/message_text.py +0 -0
  87. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/runtime/wire/messages.py +0 -0
  88. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/settings/__init__.py +0 -0
  89. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/settings/_console_impl.py +0 -0
  90. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/settings/bootstrap.py +0 -0
  91. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/settings/core.py +0 -0
  92. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/settings/glyphs.py +0 -0
  93. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/settings/provider.py +0 -0
  94. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/settings/shell_allow.py +0 -0
  95. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/settings/skills_dirs.py +0 -0
  96. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/settings/stream_config.py +0 -0
  97. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/tui/__init__.py +0 -0
  98. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/tui/app/__init__.py +0 -0
  99. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/tui/app/_clarification.py +0 -0
  100. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/tui/app/_entrypoints.py +0 -0
  101. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/tui/app/_history.py +0 -0
  102. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/tui/app/_model.py +0 -0
  103. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/tui/app/_model_params.py +0 -0
  104. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/tui/app/_terminal.py +0 -0
  105. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/tui/app/_theme_prefs.py +0 -0
  106. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/tui/app/_types.py +0 -0
  107. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/tui/app/app.tcss +0 -0
  108. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/tui/file_change_notify.py +0 -0
  109. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/tui/file_change_renderers.py +0 -0
  110. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/tui/input.py +0 -0
  111. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/tui/skills/__init__.py +0 -0
  112. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/tui/skills/invocation.py +0 -0
  113. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/tui/skills/load.py +0 -0
  114. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/tui/widgets/__init__.py +0 -0
  115. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/tui/widgets/_links.py +0 -0
  116. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/tui/widgets/autocomplete.py +0 -0
  117. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/tui/widgets/chat_input.py +0 -0
  118. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/tui/widgets/clipboard.py +0 -0
  119. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/tui/widgets/context_data.py +0 -0
  120. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/tui/widgets/context_viewer.py +0 -0
  121. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/tui/widgets/diff.py +0 -0
  122. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/tui/widgets/editor.py +0 -0
  123. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/tui/widgets/file_change_preview.py +0 -0
  124. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/tui/widgets/history.py +0 -0
  125. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/tui/widgets/loop_selector.py +0 -0
  126. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/tui/widgets/mcp_viewer.py +0 -0
  127. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/tui/widgets/message_store.py +0 -0
  128. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/tui/widgets/messages/__init__.py +0 -0
  129. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/tui/widgets/messages/_helpers.py +0 -0
  130. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/tui/widgets/messages/app.py +0 -0
  131. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/tui/widgets/messages/assistant.py +0 -0
  132. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/tui/widgets/messages/cognition_reason.py +0 -0
  133. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/tui/widgets/messages/cognition_step_activity.py +0 -0
  134. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/tui/widgets/messages/diff_message.py +0 -0
  135. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/tui/widgets/messages/error.py +0 -0
  136. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/tui/widgets/messages/skill.py +0 -0
  137. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/tui/widgets/messages/structured_ask_user.py +0 -0
  138. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/tui/widgets/messages/user.py +0 -0
  139. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/tui/widgets/model_selector.py +0 -0
  140. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/tui/widgets/notification_settings.py +0 -0
  141. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/tui/widgets/pinned_goal_bar.py +0 -0
  142. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/tui/widgets/router_profile_selector.py +0 -0
  143. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/tui/widgets/theme_selector.py +0 -0
  144. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/tui/widgets/welcome.py +0 -0
  145. {soothe_cli-1.0.3 → soothe_cli-1.0.4}/src/soothe_cli/update_check.py +0 -0
@@ -235,3 +235,4 @@ __MACOSX
235
235
 
236
236
  # Accidental MemU test persist_dir artifacts
237
237
  packages/soothe/MagicMock/
238
+ uv.lock
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: soothe-cli
3
- Version: 1.0.3
3
+ Version: 1.0.4
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,
@@ -226,10 +226,11 @@ class SootheApp(
226
226
  self._mcp_tool_count = sum(len(s.tools) for s in (mcp_server_info or []))
227
227
 
228
228
  self._status_bar: StatusBar | None = None
229
+ self._tip_row: TipRow | None = None
229
230
  self._default_session_tip: str = ""
230
231
  self._status_notification_timer: Timer | None = None
231
232
  self._status_notification_active = False
232
- # Rotating tip source + interval timer for the status footer.
233
+ # Rotating tip source + interval timer for the thinking row.
233
234
  self._tip_rotator: TipRotator = TipRotator()
234
235
  self._tip_rotation_timer: Timer | None = None
235
236
 
@@ -381,11 +382,11 @@ class SootheApp(
381
382
  """Persist and render the fallback tip shown when no notification is active."""
382
383
  cleaned = (tip or "").strip()
383
384
  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)
385
+ if self._tip_row is not None and not self._status_notification_active:
386
+ self._tip_row.set_tip(cleaned)
386
387
 
387
388
  def start_tip_rotation(self, interval: float = 12.0) -> None:
388
- """Cycle through rotating tips in the status footer at a fixed interval.
389
+ """Cycle through rotating tips in the thinking row at a fixed interval.
389
390
 
390
391
  Args:
391
392
  interval: Seconds between tip rotations.
@@ -401,7 +402,7 @@ class SootheApp(
401
402
  self._tip_rotation_timer = None
402
403
 
403
404
  def _rotate_session_tip(self) -> None:
404
- """Advance to the next tip and push it to the status footer.
405
+ """Advance to the next tip and push it to the thinking row.
405
406
 
406
407
  Transient notifications take precedence: rotation is skipped while a
407
408
  notification is active so it can run its timeout uninterrupted.
@@ -411,7 +412,7 @@ class SootheApp(
411
412
  self.set_default_session_tip(self._tip_rotator.next_tip())
412
413
 
413
414
  def _set_status_notification(self, message: str, *, timeout: float | None = None) -> None:
414
- """Render a transient notification in the status-tip area."""
415
+ """Render a transient notification in the thinking-row tip area."""
415
416
  text = (message or "").strip()
416
417
  if not text:
417
418
  return
@@ -420,8 +421,8 @@ class SootheApp(
420
421
  self._status_notification_timer.stop()
421
422
  self._status_notification_timer = None
422
423
  self._status_notification_active = True
423
- if self._status_bar is not None:
424
- self._status_bar.set_notification_message(text)
424
+ if self._tip_row is not None:
425
+ self._tip_row.set_notification(text)
425
426
 
426
427
  duration = timeout if timeout and timeout > 0 else 3.0
427
428
  self._status_notification_timer = self.set_timer(duration, self._clear_status_notification)
@@ -430,8 +431,8 @@ class SootheApp(
430
431
  """Restore the default tip after transient notification timeout."""
431
432
  self._status_notification_timer = None
432
433
  self._status_notification_active = False
433
- if self._status_bar is not None:
434
- self._status_bar.set_session_tip(self._default_session_tip)
434
+ if self._tip_row is not None:
435
+ self._tip_row.set_tip(self._default_session_tip)
435
436
 
436
437
  def notify( # type: ignore[override]
437
438
  self,
@@ -442,7 +443,7 @@ class SootheApp(
442
443
  timeout: float | None = None,
443
444
  markup: bool = True, # noqa: ARG002
444
445
  ) -> None:
445
- """Display notifications in the status-tip area instead of toast bubbles."""
446
+ """Display notifications in the thinking-row tip area instead of toast bubbles."""
446
447
  text = str(message or "").strip()
447
448
  if not text:
448
449
  return
@@ -488,14 +489,15 @@ class SootheApp(
488
489
  # In-flow plan panel: sits above the sticky bottom chrome so Ctrl+t
489
490
  # never covers the thinking row or chat input. Auto-shows while a goal
490
491
  # 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)
492
+ # (--plan-panel); default is False (hidden, Ctrl+t to open).
493
+ plan_visible = getattr(self._daemon_config, "plan_panel_default_visible", False)
493
494
  yield PlanQuickViewOverlay(
494
495
  id="plan-quick-view-overlay",
495
496
  default_visible=plan_visible,
496
497
  )
497
498
  with Container(id="bottom-app-container"):
498
- yield Container(id="thinking-status")
499
+ with Container(id="thinking-status"):
500
+ yield TipRow(id="tip-row")
499
501
  yield ChatInput(
500
502
  cwd=self._cwd,
501
503
  image_tracker=self._image_tracker,
@@ -941,6 +941,10 @@ class _ExecutionMixin:
941
941
  )
942
942
  )
943
943
  return
944
+ except Exception as exc:
945
+ logger.warning("Unexpected error connecting to daemon", exc_info=True)
946
+ await self._mount_message(ErrorMessage(f"Unexpected connection error. {exc}"))
947
+ return
944
948
  self._agent_running = True
945
949
 
946
950
  # Stop passive reads so active turn streaming has sole websocket access.
@@ -1093,6 +1097,11 @@ class _ExecutionMixin:
1093
1097
  continue
1094
1098
  except (ConnectionError, OSError, TimeoutError):
1095
1099
  pass
1100
+ except Exception:
1101
+ logger.debug(
1102
+ "Unexpected error during daemon reconnect retry",
1103
+ exc_info=True,
1104
+ )
1096
1105
  raise
1097
1106
  except Exception as e: # Resilient tool rendering
1098
1107
  # Attach-only idle timeout is benign: the prior turn had already
@@ -983,7 +983,7 @@ class _MessagesMixin:
983
983
  """Shift+Tab: navigate loop selector when active, otherwise cycle mode.
984
984
 
985
985
  - In the LoopSelectorScreen, defer to its filter navigation.
986
- - On the main screen, cycle composer mode Auto → Manual → Plan → Ask.
986
+ - On the main screen, cycle composer mode Auto → Bypass → Manual → Plan → Ask.
987
987
  """
988
988
  from soothe_cli.tui.widgets.loop_selector import LoopSelectorScreen
989
989
 
@@ -995,15 +995,9 @@ class _MessagesMixin:
995
995
  def cycle_composer_mode(self) -> None:
996
996
  """Advance composer mode and refresh the status-bar badge.
997
997
 
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.
998
+ Cycle: Auto → Bypass → Manual → Plan → Ask → Auto. When a goal is
999
+ running and the new mode is Auto or Manual, hot-swaps the daemon's
1000
+ live clarification policy via `loop_set_clarification_mode`.
1007
1001
  """
1008
1002
  from soothe_cli.tui.composer_mode import next_composer_mode
1009
1003
 
@@ -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
@@ -421,8 +421,10 @@ class _UIMixin:
421
421
  await self._loading_widget.remove()
422
422
  self._loading_widget = None
423
423
  self._connect_spinner_start_mono = None
424
+ self._show_tip_row()
424
425
  return
425
426
 
427
+ self._hide_tip_row()
426
428
  thinking_status = self.query_one("#thinking-status", Container)
427
429
 
428
430
  connect_start_mono: float | None = None
@@ -481,6 +483,16 @@ class _UIMixin:
481
483
  if self._loading_widget is not None:
482
484
  self._loading_widget.resume()
483
485
 
486
+ def _show_tip_row(self) -> None:
487
+ """Show the rotating tip in the thinking row when the loop is idle."""
488
+ if self._tip_row is not None:
489
+ self._tip_row.display = True
490
+
491
+ def _hide_tip_row(self) -> None:
492
+ """Hide the tip row so the thinking spinner gets the full row."""
493
+ if self._tip_row is not None:
494
+ self._tip_row.display = False
495
+
484
496
  def action_toggle_plan_quick_view(self) -> None:
485
497
  """Expand or collapse the in-flow plan panel above the thinking row."""
486
498
  if self.screen.is_modal:
@@ -1,4 +1,8 @@
1
- """Sticky TUI composer modes: Auto / Manual / Plan / Ask."""
1
+ """TUI composer modes — working-mode hierarchy.
2
+
3
+ Agent sub-modes (auto, bypass, manual) grouped first, then plan and ask.
4
+ Shift+Tab cycle: Auto → Bypass → Manual → Plan → Ask → Auto.
5
+ """
2
6
 
3
7
  from __future__ import annotations
4
8
 
@@ -8,15 +12,25 @@ COMPOSER_MODE_AUTO = "auto"
8
12
  COMPOSER_MODE_MANUAL = "manual"
9
13
  COMPOSER_MODE_PLAN = "plan"
10
14
  COMPOSER_MODE_ASK = "ask"
15
+ COMPOSER_MODE_BYPASS = "bypass"
11
16
 
17
+ # Agent sub-modes grouped first, then standalone working modes.
12
18
  COMPOSER_MODE_ORDER: tuple[str, ...] = (
13
19
  COMPOSER_MODE_AUTO,
20
+ COMPOSER_MODE_BYPASS,
14
21
  COMPOSER_MODE_MANUAL,
15
22
  COMPOSER_MODE_PLAN,
16
23
  COMPOSER_MODE_ASK,
17
24
  )
18
25
  VALID_COMPOSER_MODES: frozenset[str] = frozenset(COMPOSER_MODE_ORDER)
19
26
 
27
+ # Agent working mode sub-modes (full mutating tool surface).
28
+ AGENT_SUB_MODES: frozenset[str] = frozenset(
29
+ {COMPOSER_MODE_AUTO, COMPOSER_MODE_MANUAL, COMPOSER_MODE_BYPASS}
30
+ )
31
+ # Standalone working modes (not agent sub-modes).
32
+ STANDALONE_WORKING_MODES: frozenset[str] = frozenset({COMPOSER_MODE_PLAN, COMPOSER_MODE_ASK})
33
+
20
34
 
21
35
  @dataclass(frozen=True)
22
36
  class ComposerWireFields:
@@ -35,11 +49,7 @@ def normalize_composer_mode(mode: str | None) -> str:
35
49
 
36
50
 
37
51
  def next_composer_mode(current: str) -> str:
38
- """Advance Auto → Manual → Plan → Ask → Auto.
39
-
40
- Unknown values normalize to `auto` (the default, same as a first
41
- Shift+Tab from a garbage seed), without advancing past Auto in that step.
42
- """
52
+ """Advance to the next mode in the cycle. Unknown values reset to `auto`."""
43
53
  if current not in VALID_COMPOSER_MODES:
44
54
  return COMPOSER_MODE_AUTO
45
55
  idx = COMPOSER_MODE_ORDER.index(current)
@@ -47,23 +57,19 @@ def next_composer_mode(current: str) -> str:
47
57
 
48
58
 
49
59
  def resolve_composer_wire_fields(mode: str) -> ComposerWireFields:
50
- """Map composer mode to wire `clarification_mode`, sticky subagent, and
51
- `interaction_mode`.
52
-
53
- Args:
54
- mode: Composer mode (`auto`, `manual`, `plan`, or `ask`).
60
+ """Map composer mode to wire fields.
55
61
 
56
- Returns:
57
- A :class:`ComposerWireFields`. Plan mode sends `clarification_mode=auto`
58
- plus `interaction_mode=plan` (read-only plan graph); ask mode sends
59
- `clarification_mode=auto` plus `interaction_mode=ask`. Slash routing
60
- in the message still wins over the sticky hint at send time.
62
+ auto → clarification=auto, interaction=None; manual → clarification=manual;
63
+ bypass → clarification=auto, interaction=bypass; plan → interaction=plan;
64
+ ask → interaction=ask. Slash routing in the message wins over the sticky hint.
61
65
  """
62
66
  normalized = normalize_composer_mode(mode)
63
67
  if normalized == COMPOSER_MODE_PLAN:
64
68
  return ComposerWireFields(COMPOSER_MODE_AUTO, None, "plan")
65
69
  if normalized == COMPOSER_MODE_ASK:
66
70
  return ComposerWireFields(COMPOSER_MODE_AUTO, None, COMPOSER_MODE_ASK)
71
+ if normalized == COMPOSER_MODE_BYPASS:
72
+ return ComposerWireFields(COMPOSER_MODE_AUTO, None, COMPOSER_MODE_BYPASS)
67
73
  if normalized == COMPOSER_MODE_MANUAL:
68
74
  return ComposerWireFields(COMPOSER_MODE_MANUAL, None, None)
69
75
  return ComposerWireFields(COMPOSER_MODE_AUTO, None, None)
@@ -4121,7 +4121,15 @@ async def execute_task_textual(
4121
4121
  goal_loop_start_monotonic=goal_loop_start_monotonic,
4122
4122
  turn_start_monotonic=start_time,
4123
4123
  )
4124
- if adapter._goal_tree_message is not None:
4124
+ follow_on = (
4125
+ data.get("follow_on_exec")
4126
+ if isinstance(data, dict)
4127
+ else None
4128
+ )
4129
+ # Skip the terminal footer when a follow-on exec
4130
+ # goal is pending: the daemon immediately enqueues
4131
+ # it, so showing "Done" here is misleading.
4132
+ if adapter._goal_tree_message is not None and not follow_on:
4125
4133
  goal_elapsed_start = _goal_loop_elapsed_start(
4126
4134
  goal_loop_start_monotonic=goal_loop_start_monotonic,
4127
4135
  turn_start_monotonic=start_time,
@@ -4164,11 +4172,6 @@ async def execute_task_textual(
4164
4172
  # row stays active through the plan→exec transition;
4165
4173
  # the exec goal's ``STRANGE_LOOP_STARTED`` will
4166
4174
  # re-anchor and replace it.
4167
- follow_on = (
4168
- data.get("follow_on_exec")
4169
- if isinstance(data, dict)
4170
- else None
4171
- )
4172
4175
  if adapter._set_spinner and not clarification_pending:
4173
4176
  if follow_on:
4174
4177
  adapter._plan_approve_follow_on_pending = True
@@ -69,7 +69,7 @@ def build_keyboard_shortcut_rows() -> list[tuple[str, str]]:
69
69
  "Toggle plan panel (shown by default; --no-plan-panel disables auto-show)",
70
70
  ),
71
71
  ("Ctrl+O", "Toggle expand/collapse of the most recent skill or tool card"),
72
- ("Shift+Tab", "Cycle composer mode (Auto → Manual → Plan → Ask)"),
72
+ ("Shift+Tab", "Cycle composer mode (Auto → Bypass → Manual → Plan → Ask)"),
73
73
  ("@filename", "Autocomplete files and inject content"),
74
74
  ("/command", "Slash commands (e.g. /help, /clear, /quit)"),
75
75
  ("!command", "Run shell commands directly"),
@@ -4,7 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  from collections.abc import Callable
6
6
  from time import monotonic
7
- from typing import TYPE_CHECKING
7
+ from typing import TYPE_CHECKING, Any
8
8
 
9
9
  from textual.content import Content
10
10
  from textual.widgets import Static
@@ -305,3 +305,73 @@ class LoadingWidget(Static):
305
305
  def stop(self) -> None:
306
306
  """Stop the animation (widget will be removed by caller)."""
307
307
  self._stop_timer()
308
+
309
+
310
+ class TipRow(Static):
311
+ """Rotating tip / transient notice shown in the thinking row when idle.
312
+
313
+ Sits in the same ``#thinking-status`` container as :class:`LoadingWidget`.
314
+ When the loop is inactive (no spinner) the tip is visible; when the loop
315
+ is active the spinner replaces it and this row is hidden by the caller.
316
+ """
317
+
318
+ DEFAULT_CSS = """
319
+ TipRow {
320
+ height: 1;
321
+ padding: 0 1;
322
+ margin-top: 1;
323
+ }
324
+
325
+ TipRow.notification {
326
+ color: $warning;
327
+ text-style: bold;
328
+ }
329
+ """
330
+
331
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
332
+ """Initialize the tip row with no content."""
333
+ super().__init__(*args, **kwargs)
334
+ self._is_notification = False
335
+ self._tip_text: str = ""
336
+
337
+ def _render_tip(self, text: str) -> Content:
338
+ colors = theme.get_theme_colors(self)
339
+ prefix = Content.styled("💡 ", colors.muted)
340
+ body = Content.styled(text, colors.muted)
341
+ return Content.assemble(prefix, body)
342
+
343
+ def _render_notice(self, text: str) -> Content:
344
+ colors = theme.get_theme_colors(self)
345
+ prefix = Content.styled("⚠ ", colors.warning)
346
+ body = Content.styled(text, colors.warning)
347
+ return Content.assemble(prefix, body)
348
+
349
+ def _refresh_content(self) -> None:
350
+ """Re-render the stored tip/notice text, styled only when mounted."""
351
+ if not self.is_mounted:
352
+ return
353
+ text = self._tip_text
354
+ if not text:
355
+ self.update("")
356
+ elif self._is_notification:
357
+ self.update(self._render_notice(text))
358
+ else:
359
+ self.update(self._render_tip(text))
360
+
361
+ def on_mount(self) -> None:
362
+ """Render the initial tip content once the widget is in the DOM."""
363
+ self._refresh_content()
364
+
365
+ def set_tip(self, text: str) -> None:
366
+ """Show a default rotating tip (muted, non-bold)."""
367
+ self._is_notification = False
368
+ self.remove_class("notification")
369
+ self._tip_text = (text or "").strip()
370
+ self._refresh_content()
371
+
372
+ def set_notification(self, text: str) -> None:
373
+ """Show a transient notice (warning-colored, bold)."""
374
+ self._is_notification = True
375
+ self.add_class("notification")
376
+ self._tip_text = (text or "").strip()
377
+ self._refresh_content()
@@ -343,9 +343,20 @@ class CognitionGoalTreeMessage(Vertical):
343
343
  return f"↑{format_token_count(total_in)} ↓{format_token_count(total_out)}"
344
344
 
345
345
  def mark_loop_started(self, started_at: float | None = None) -> None:
346
- """Anchor plan-level elapsed time (matches thinking-row turn start)."""
347
- if self._loop_started_at is None:
346
+ """Anchor plan-level elapsed time (matches thinking-row turn start).
347
+
348
+ On reuse after a terminal footer (plan-approve → exec follow-on),
349
+ clear the stale footer + step rows so the panel returns to live state.
350
+ """
351
+ if self._loop_started_at is None or self._footer_visible:
348
352
  self._loop_started_at = started_at if started_at is not None else time()
353
+ if self._footer_visible:
354
+ self._footer_visible = False
355
+ self._footer_plain = ""
356
+ self._footer_tone = "muted"
357
+ self._step_order.clear()
358
+ self._steps.clear()
359
+ self._sync_goal_tree_widgets()
349
360
 
350
361
  def _indent_prefix(self) -> str:
351
362
  """Body gutter aligned to the right of the goal header prefix dot.
@@ -233,6 +233,20 @@ class CognitionStepMessage(Vertical):
233
233
  return "[Ask] "
234
234
  return ""
235
235
 
236
+ @property
237
+ def _suppress_execute_prose(self) -> bool:
238
+ """True when the step's execute prose should not render in the detail panel.
239
+
240
+ In plan mode, the agent's final message IS the plan document. The plan
241
+ review card (StructuredAskUserWidget with origin ``plan_mode_review``)
242
+ already renders the full plan body (formatted, collapsible). Showing the
243
+ raw plan markdown again on the step card is redundant — especially after a
244
+ refinement, where the old step card would display the *original* plan
245
+ alongside the *refined* plan review card. Suppress the prose so only the
246
+ review card shows the plan content.
247
+ """
248
+ return self._interaction_mode == "plan"
249
+
236
250
  def set_description(self, description: str) -> None:
237
251
  """Update the step title (full plan/execute brief, no abbreviation)."""
238
252
  text = (description or "").strip() or "(step)"
@@ -487,7 +501,7 @@ class CognitionStepMessage(Vertical):
487
501
  suffix=tool_part,
488
502
  )
489
503
  prose = (self._last_completed_execute_prose or "").strip()
490
- if prose:
504
+ if prose and not self._suppress_execute_prose:
491
505
  self._detail_widget.update(self._step_branched_execute_body(prose, muted=True))
492
506
  self._detail_widget.display = True
493
507
  elif self._has_clarification_details:
@@ -1445,8 +1459,12 @@ class CognitionStepMessage(Vertical):
1445
1459
  suffix=f"{tool_part}{token_suffix}",
1446
1460
  )
1447
1461
  self._sync_step_card_surface()
1448
- if prose:
1462
+ if prose and not self._suppress_execute_prose:
1449
1463
  self._detail_widget.update(self._step_branched_execute_body(prose, muted=True))
1464
+ elif self._detail_widget is not None:
1465
+ # Clear stale running-display content (e.g. plan markdown in
1466
+ # plan mode) so the detail panel is empty when collapsed.
1467
+ self._detail_widget.update("")
1450
1468
  self._auto_collapse_on_terminal()
1451
1469
  return
1452
1470
 
@@ -135,7 +135,7 @@ class PlanQuickViewOverlay(Vertical):
135
135
 
136
136
  Auto-shows while a goal is executing (`_loop_executing()`) when the
137
137
  preferred visibility is on (`CLIConfig.plan_panel_default_visible`,
138
- default True). Auto-hides once the loop reaches a terminal footer
138
+ default False). Auto-hides once the loop reaches a terminal footer
139
139
  (`set_loop_finished` / `set_interrupted`). Toggle with `Ctrl+t`.
140
140
  Mounted as a Screen sibling between `#chat` and
141
141
  `#bottom-app-container` so expanding it shrinks the transcript
@@ -191,7 +191,7 @@ class PlanQuickViewOverlay(Vertical):
191
191
  }
192
192
  """
193
193
 
194
- def __init__(self, *, default_visible: bool = True, **kwargs: Any) -> None:
194
+ def __init__(self, *, default_visible: bool = False, **kwargs: Any) -> None:
195
195
  super().__init__(**kwargs)
196
196
  self._preferred_visible: bool = default_visible
197
197
  # True when the user explicitly opened the panel (Ctrl+t). Suppresses