soothe-cli 0.11.0__tar.gz → 1.0.0__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-0.11.0 → soothe_cli-1.0.0}/PKG-INFO +2 -2
  2. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/pyproject.toml +1 -1
  3. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/cli/main.py +2 -2
  4. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/config/cli_config.py +2 -2
  5. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/app/__init__.py +3 -7
  6. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/app/_app.py +7 -7
  7. soothe_cli-1.0.0/src/soothe_cli/tui/app/_clarification.py +41 -0
  8. soothe_cli-1.0.0/src/soothe_cli/tui/app/_entrypoints.py +95 -0
  9. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/app/_execution.py +6 -40
  10. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/app/_history.py +1 -1
  11. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/app/_messages_mixin.py +26 -17
  12. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/app/_model.py +2 -2
  13. soothe_cli-1.0.0/src/soothe_cli/tui/app/_model_params.py +97 -0
  14. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/app/_startup.py +1 -1
  15. soothe_cli-1.0.0/src/soothe_cli/tui/app/_terminal.py +75 -0
  16. soothe_cli-1.0.0/src/soothe_cli/tui/app/_theme_prefs.py +91 -0
  17. soothe_cli-1.0.0/src/soothe_cli/tui/app/_types.py +112 -0
  18. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/widgets/help_screen.py +1 -1
  19. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/widgets/messages/structured_ask_user.py +209 -168
  20. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/widgets/plan_quick_view_overlay.py +2 -2
  21. soothe_cli-0.11.0/src/soothe_cli/tui/app/_module_init.py +0 -435
  22. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/.gitignore +0 -0
  23. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/README.md +0 -0
  24. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/__init__.py +0 -0
  25. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/_cli_context.py +0 -0
  26. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/_env_vars.py +0 -0
  27. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/_version.py +0 -0
  28. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/card_wire.py +0 -0
  29. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/cli/__init__.py +0 -0
  30. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/cli/commands/__init__.py +0 -0
  31. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/cli/commands/autopilot_cmd.py +0 -0
  32. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/cli/commands/config_cmd.py +0 -0
  33. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/cli/commands/cron_cmd.py +0 -0
  34. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/cli/commands/loop_cmd.py +0 -0
  35. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/cli/commands/run_cmd.py +0 -0
  36. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/cli/commands/status_cmd.py +0 -0
  37. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/cli/execution/__init__.py +0 -0
  38. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/cli/execution/daemon.py +0 -0
  39. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/cli/execution/daemon_errors.py +0 -0
  40. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/cli/execution/headless.py +0 -0
  41. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/cli/execution/headless_renderer.py +0 -0
  42. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/cli/execution/launcher.py +0 -0
  43. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/cli/soothecli.py +0 -0
  44. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/commands/__init__.py +0 -0
  45. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/commands/binding.py +0 -0
  46. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/commands/command_registry.py +0 -0
  47. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/commands/command_router.py +0 -0
  48. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/commands/slash_commands.py +0 -0
  49. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/commands/subagent_routing.py +0 -0
  50. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/config/__init__.py +0 -0
  51. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/config/loader.py +0 -0
  52. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/config/logging_setup.py +0 -0
  53. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/display/__init__.py +0 -0
  54. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/display/markdown_theme.py +0 -0
  55. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/display/media_utils.py +0 -0
  56. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/display/mermaid_render.py +0 -0
  57. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/display/path_utils.py +0 -0
  58. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/display/preview_limits.py +0 -0
  59. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/display/shell_color.py +0 -0
  60. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/display/spinner_labels.py +0 -0
  61. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/display/theme.py +0 -0
  62. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/display/tips.py +0 -0
  63. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/display/tool_display.py +0 -0
  64. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/display/unicode_security.py +0 -0
  65. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/loops/__init__.py +0 -0
  66. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/loops/sessions.py +0 -0
  67. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/model_config.py +0 -0
  68. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/project_utils.py +0 -0
  69. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/runtime/__init__.py +0 -0
  70. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/runtime/headless/processor.py +0 -0
  71. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/runtime/headless/processor_state.py +0 -0
  72. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/runtime/parse/tool_call_resolution.py +0 -0
  73. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/runtime/policy/tui_trace_log.py +0 -0
  74. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/runtime/presentation/duration_format.py +0 -0
  75. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/runtime/presentation/engine.py +0 -0
  76. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/runtime/presentation/id_format.py +0 -0
  77. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/runtime/presentation/renderer_base.py +0 -0
  78. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/runtime/presentation/renderer_protocol.py +0 -0
  79. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/runtime/presentation/step_id_format.py +0 -0
  80. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/runtime/presentation/subagent_task_display.py +0 -0
  81. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/runtime/state/file_tracker.py +0 -0
  82. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/runtime/state/session_stats.py +0 -0
  83. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/runtime/state/step_router.py +0 -0
  84. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/runtime/state/stream_accumulator.py +0 -0
  85. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/runtime/token_events_debug.py +0 -0
  86. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/runtime/token_usage.py +0 -0
  87. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/runtime/transport/session.py +0 -0
  88. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/runtime/turn/prepare.py +0 -0
  89. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/runtime/wire/chunk_filter.py +0 -0
  90. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/runtime/wire/message_text.py +0 -0
  91. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/runtime/wire/messages.py +0 -0
  92. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/settings/__init__.py +0 -0
  93. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/settings/_console_impl.py +0 -0
  94. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/settings/bootstrap.py +0 -0
  95. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/settings/core.py +0 -0
  96. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/settings/glyphs.py +0 -0
  97. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/settings/provider.py +0 -0
  98. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/settings/shell_allow.py +0 -0
  99. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/settings/skills_dirs.py +0 -0
  100. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/settings/stream_config.py +0 -0
  101. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/__init__.py +0 -0
  102. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/app/_ui.py +0 -0
  103. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/app/app.tcss +0 -0
  104. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/composer_mode.py +0 -0
  105. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/file_change_notify.py +0 -0
  106. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/file_change_renderers.py +0 -0
  107. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/input.py +0 -0
  108. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/skills/__init__.py +0 -0
  109. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/skills/invocation.py +0 -0
  110. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/skills/load.py +0 -0
  111. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/textual_adapter.py +0 -0
  112. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/widgets/__init__.py +0 -0
  113. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/widgets/_links.py +0 -0
  114. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/widgets/autocomplete.py +0 -0
  115. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/widgets/chat_input.py +0 -0
  116. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/widgets/clipboard.py +0 -0
  117. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/widgets/context_data.py +0 -0
  118. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/widgets/context_viewer.py +0 -0
  119. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/widgets/diff.py +0 -0
  120. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/widgets/editor.py +0 -0
  121. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/widgets/file_change_preview.py +0 -0
  122. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/widgets/history.py +0 -0
  123. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/widgets/loading.py +0 -0
  124. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/widgets/loop_selector.py +0 -0
  125. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/widgets/mcp_viewer.py +0 -0
  126. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/widgets/message_store.py +0 -0
  127. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/widgets/messages/__init__.py +0 -0
  128. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/widgets/messages/_helpers.py +0 -0
  129. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/widgets/messages/app.py +0 -0
  130. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/widgets/messages/assistant.py +0 -0
  131. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/widgets/messages/cognition_goal_tree.py +0 -0
  132. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/widgets/messages/cognition_reason.py +0 -0
  133. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/widgets/messages/cognition_step.py +0 -0
  134. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/widgets/messages/cognition_step_activity.py +0 -0
  135. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/widgets/messages/diff_message.py +0 -0
  136. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/widgets/messages/error.py +0 -0
  137. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/widgets/messages/skill.py +0 -0
  138. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/widgets/messages/user.py +0 -0
  139. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/widgets/model_selector.py +0 -0
  140. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/widgets/notification_settings.py +0 -0
  141. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/widgets/pinned_goal_bar.py +0 -0
  142. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/widgets/router_profile_selector.py +0 -0
  143. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/widgets/status.py +0 -0
  144. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/widgets/theme_selector.py +0 -0
  145. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/widgets/welcome.py +0 -0
  146. {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/update_check.py +0 -0
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: soothe-cli
3
- Version: 0.11.0
3
+ Version: 1.0.0
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
7
7
  Project-URL: Repository, https://github.com/mirasoth/soothe
8
8
  License: MIT
9
9
  Keywords: cli,client,soothe,tui,websocket
10
- Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Development Status :: 5 - Production/Stable
11
11
  Classifier: Intended Audience :: Developers
12
12
  Classifier: License :: OSI Approved :: MIT License
13
13
  Classifier: Programming Language :: Python :: 3
@@ -11,7 +11,7 @@ license = { text = "MIT" }
11
11
  requires-python = ">=3.11,<3.14"
12
12
  keywords = ["soothe", "cli", "client", "tui", "websocket"]
13
13
  classifiers = [
14
- "Development Status :: 3 - Alpha",
14
+ "Development Status :: 5 - Production/Stable",
15
15
  "Intended Audience :: Developers",
16
16
  "License :: OSI Approved :: MIT License",
17
17
  "Programming Language :: Python :: 3",
@@ -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: disabled."
201
+ "thereafter. Default: enabled."
202
202
  ),
203
203
  ),
204
- ] = False,
204
+ ] = True,
205
205
  show_version: Annotated[ # noqa: FBT002
206
206
  bool,
207
207
  typer.Option("--version", help="Show version and exit."),
@@ -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 = False
36
+ plan_panel_default_visible: bool = True
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 False (Ctrl+t to open)."""
38
+ hide it when the goal completes. Default True (Ctrl+t to close)."""
39
39
 
40
40
  # Output streaming overrides (RFC-614)
41
41
  output_streaming_enabled: bool | None = None
@@ -1,13 +1,9 @@
1
1
  """Public API for the app sub-package."""
2
2
 
3
3
  from soothe_cli.tui.app._app import SootheApp
4
- from soothe_cli.tui.app._module_init import (
5
- AppResult,
6
- TextualSessionState,
7
- run_textual_app,
8
- run_textual_tui,
9
- save_theme_preference,
10
- )
4
+ from soothe_cli.tui.app._entrypoints import run_textual_app, run_textual_tui
5
+ from soothe_cli.tui.app._theme_prefs import save_theme_preference
6
+ from soothe_cli.tui.app._types import AppResult, TextualSessionState
11
7
 
12
8
  __all__ = [
13
9
  "SootheApp",
@@ -30,13 +30,13 @@ from soothe_cli.tui.app._execution import _ExecutionMixin
30
30
  from soothe_cli.tui.app._history import _HistoryMixin
31
31
  from soothe_cli.tui.app._messages_mixin import _MessagesMixin
32
32
  from soothe_cli.tui.app._model import _ModelMixin
33
- from soothe_cli.tui.app._module_init import (
33
+ from soothe_cli.tui.app._startup import _StartupMixin
34
+ from soothe_cli.tui.app._theme_prefs import _load_theme_preference
35
+ from soothe_cli.tui.app._types import (
34
36
  DeferredAction,
35
37
  QueuedMessage,
36
38
  TextualSessionState,
37
- _load_theme_preference,
38
39
  )
39
- from soothe_cli.tui.app._startup import _StartupMixin
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
@@ -486,10 +486,10 @@ class SootheApp(
486
486
  )
487
487
  yield Container(id="messages")
488
488
  # In-flow plan panel: sits above the sticky bottom chrome so Ctrl+t
489
- # never covers the thinking row or chat input. Stays collapsed by
490
- # default; auto-shows while a goal is executing only when
491
- # CLIConfig.plan_panel_default_visible is True (--plan-panel).
492
- plan_visible = getattr(self._daemon_config, "plan_panel_default_visible", False)
489
+ # never covers the thinking row or chat input. Auto-shows while a goal
490
+ # 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)
493
493
  yield PlanQuickViewOverlay(
494
494
  id="plan-quick-view-overlay",
495
495
  default_visible=plan_visible,
@@ -0,0 +1,41 @@
1
+ """Clarification wire-content formatting for plan-review and tool-approval submits.
2
+
3
+ Pure utility — no mixin state dependency. Extracted from `_execution.py` to
4
+ keep the execution mixin focused on agent execution and message routing.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ _PLAN_REVIEW_ACTIONS = frozenset({"Approve", "Reject", "Refine"})
10
+ _TOOL_APPROVAL_ACTIONS = frozenset({"Approve", "Reject", "Edit"})
11
+
12
+
13
+ def clarification_wire_content(answers: list[str], *, origin_node: str = "") -> str:
14
+ """Human-readable turn content for a clarification submit (not a new goal).
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.
23
+ """
24
+ non_empty = [a for a in answers if str(a).strip()]
25
+ if not non_empty:
26
+ return ""
27
+ first = str(answers[0]).strip() if answers else ""
28
+ is_tool_approval = origin_node == "tool_approval"
29
+ is_selector_action = first in _PLAN_REVIEW_ACTIONS or first in _TOOL_APPROVAL_ACTIONS
30
+ if is_selector_action:
31
+ refinement = str(answers[1]).strip() if len(answers) > 1 else ""
32
+ # Origin-aware prefix; when origin is unknown (legacy caller), default
33
+ # to "Plan review" to preserve the pre-RFC behavior where the action
34
+ # label alone selected the plan-review prefix.
35
+ prefix = "Tool approval" if is_tool_approval else "Plan review"
36
+ if refinement:
37
+ return f"{prefix}: {first} — {refinement}"
38
+ return f"{prefix}: {first}"
39
+ if len(non_empty) == 1:
40
+ return non_empty[0]
41
+ return " | ".join(f"A{i + 1}: {a}" for i, a in enumerate(answers) if str(a).strip())
@@ -0,0 +1,95 @@
1
+ """App entry points and slash-command URL constants.
2
+
3
+ `run_textual_app` and `run_textual_tui` are the public async and sync entry
4
+ points for launching the Textual TUI. `_COMMAND_URLS` maps slash-commands
5
+ that just open a browser to their target URLs.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import asyncio
11
+ import os
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ from soothe_cli._version import CHANGELOG_URL, DOCS_URL
16
+ from soothe_cli.tui.app._terminal import init_terminal_integration
17
+ from soothe_cli.tui.app._types import AppResult
18
+
19
+ _COMMAND_URLS: dict[str, str] = {
20
+ "/changelog": CHANGELOG_URL,
21
+ "/docs": DOCS_URL,
22
+ "/feedback": "https://github.com/mirasoth/soothe/issues/new/choose",
23
+ }
24
+ """Slash-command to URL mapping for commands that just open a browser."""
25
+
26
+
27
+ async def run_textual_app(
28
+ *,
29
+ daemon_config: Any, # noqa: ANN401
30
+ assistant_id: str | None = None,
31
+ cwd: str | Path | None = None,
32
+ resume_loop_id: str | None = None,
33
+ initial_prompt: str | None = None,
34
+ initial_skill: str | None = None,
35
+ mcp_server_info: list[dict[str, Any]] | None = None,
36
+ profile_override: dict[str, Any] | None = None,
37
+ ) -> AppResult:
38
+ """Run the Textual TUI (daemon execution only)."""
39
+ from soothe_cli.tui.app._app import SootheApp # deferred to avoid circular import
40
+
41
+ # Disable iTerm2 cursor guide before Textual takes over the terminal.
42
+ # Previously this was an import-time side effect; now explicit.
43
+ init_terminal_integration()
44
+
45
+ app = SootheApp(
46
+ daemon_config=daemon_config,
47
+ assistant_id=assistant_id,
48
+ cwd=cwd,
49
+ resume_loop_id=resume_loop_id,
50
+ initial_prompt=initial_prompt,
51
+ initial_skill=initial_skill,
52
+ mcp_server_info=mcp_server_info,
53
+ profile_override=profile_override,
54
+ )
55
+ try:
56
+ await app.run_async()
57
+ finally:
58
+ if app._daemon_session is not None:
59
+ from soothe_cli.runtime.transport.session import TUI_EXIT_HANDSHAKE_TIMEOUT_S
60
+
61
+ await app._daemon_session.close(handshake_timeout=TUI_EXIT_HANDSHAKE_TIMEOUT_S)
62
+
63
+ return AppResult(
64
+ return_code=app.return_code or 0,
65
+ loop_id=app._lc_loop_id,
66
+ session_stats=app._session_stats,
67
+ update_available=app._update_available,
68
+ )
69
+
70
+
71
+ def run_textual_tui(
72
+ config: Any, # noqa: ANN401
73
+ resume_loop_id: str | None = None,
74
+ initial_prompt: str | None = None,
75
+ ) -> AppResult:
76
+ """Launch the Textual TUI with optional loop attachment and initial prompt.
77
+
78
+ Args:
79
+ config: Soothe configuration used for daemon-backed startup.
80
+ resume_loop_id: Loop id to attach to when starting the TUI
81
+ initial_prompt: Auto-submit prompt on launch
82
+ """
83
+
84
+ # Caller cwd is forwarded as the loop workspace hint.
85
+ # Daemon workspace is ephemeral TEMP unless SOOTHE_WORKSPACE env set.
86
+ cwd = os.getcwd()
87
+
88
+ return asyncio.run(
89
+ run_textual_app(
90
+ resume_loop_id=resume_loop_id,
91
+ initial_prompt=initial_prompt,
92
+ daemon_config=config,
93
+ cwd=cwd,
94
+ )
95
+ )
@@ -10,7 +10,7 @@ import sys
10
10
  import time
11
11
  import webbrowser
12
12
  from contextlib import suppress
13
- from typing import Any, Literal
13
+ from typing import Any
14
14
 
15
15
  from textual.app import ScreenStackError
16
16
  from textual.containers import VerticalScroll
@@ -30,11 +30,13 @@ from soothe_cli.cli.execution.daemon_errors import (
30
30
  from soothe_cli.display import theme
31
31
  from soothe_cli.display.shell_color import shell_subprocess_env, wrap_shell_command_for_color
32
32
  from soothe_cli.runtime.state.session_stats import SessionStats
33
- from soothe_cli.tui.app._module_init import (
34
- _COMMAND_URLS,
33
+ from soothe_cli.tui.app._clarification import clarification_wire_content
34
+ from soothe_cli.tui.app._entrypoints import _COMMAND_URLS
35
+ from soothe_cli.tui.app._model_params import _extract_model_params_flag
36
+ from soothe_cli.tui.app._types import (
35
37
  DeferredAction,
38
+ InputMode,
36
39
  QueuedMessage,
37
- _extract_model_params_flag,
38
40
  )
39
41
  from soothe_cli.tui.widgets.chat_input import ChatInput
40
42
  from soothe_cli.tui.widgets.messages import (
@@ -49,44 +51,8 @@ from soothe_cli.tui.widgets.welcome import WelcomeBanner
49
51
 
50
52
  _monotonic = time.monotonic
51
53
 
52
- InputMode = Literal["normal", "shell", "command"]
53
-
54
54
  logger = logging.getLogger(__name__)
55
55
 
56
- _PLAN_REVIEW_ACTIONS = frozenset({"Approve", "Reject", "Refine"})
57
- _TOOL_APPROVAL_ACTIONS = frozenset({"Approve", "Reject", "Edit"})
58
-
59
-
60
- def clarification_wire_content(answers: list[str], *, origin_node: str = "") -> str:
61
- """Human-readable turn content for a clarification submit (not a new goal).
62
-
63
- Option-selector actions use a stable `<prefix>: …` header so a dropped
64
- `clarification_answer` flag cannot turn a bare action into Pass1 TASK.
65
- Plan review → `Plan review:` (Refine carries refinement text in
66
- `answers[1]`). Tool approval → `Tool approval:` (Edit carries revised
67
- instructions in `answers[1]`). When `origin_node` is empty (legacy
68
- callers) the action label alone selects the prefix, preserving the safety
69
- net for any selector-origin submit.
70
- """
71
- non_empty = [a for a in answers if str(a).strip()]
72
- if not non_empty:
73
- return ""
74
- first = str(answers[0]).strip() if answers else ""
75
- is_tool_approval = origin_node == "tool_approval"
76
- is_selector_action = first in _PLAN_REVIEW_ACTIONS or first in _TOOL_APPROVAL_ACTIONS
77
- if is_selector_action:
78
- refinement = str(answers[1]).strip() if len(answers) > 1 else ""
79
- # Origin-aware prefix; when origin is unknown (legacy caller), default
80
- # to "Plan review" to preserve the pre-RFC behavior where the action
81
- # label alone selected the plan-review prefix.
82
- prefix = "Tool approval" if is_tool_approval else "Plan review"
83
- if refinement:
84
- return f"{prefix}: {first} — {refinement}"
85
- return f"{prefix}: {first}"
86
- if len(non_empty) == 1:
87
- return non_empty[0]
88
- return " | ".join(f"A{i + 1}: {a}" for i, a in enumerate(answers) if str(a).strip())
89
-
90
56
 
91
57
  class _ExecutionMixin:
92
58
  """Agent execution, message routing, queue, shell commands, and daemon events."""
@@ -11,7 +11,7 @@ from soothe_sdk.display import card_binder as _binder
11
11
  from soothe_sdk.display.transcript_types import MessageData
12
12
  from textual.content import Content
13
13
 
14
- from soothe_cli.tui.app._module_init import _LoopHistoryPayload
14
+ from soothe_cli.tui.app._types import _LoopHistoryPayload
15
15
  from soothe_cli.tui.widgets.messages import AppMessage
16
16
 
17
17
  logger = logging.getLogger(__name__)
@@ -17,11 +17,13 @@ from textual.app import App
17
17
  from textual.containers import Container, VerticalScroll
18
18
  from textual.css.query import NoMatches
19
19
 
20
- from soothe_cli.tui.app._module_init import (
20
+ from soothe_cli.tui.app._terminal import (
21
21
  _ITERM_CURSOR_GUIDE_ON,
22
+ _write_iterm_escape,
23
+ )
24
+ from soothe_cli.tui.app._types import (
22
25
  DeferredAction,
23
26
  _LoopHistoryPayload,
24
- _write_iterm_escape,
25
27
  )
26
28
  from soothe_cli.tui.widgets.file_change_preview import FileChangePreviewWidget
27
29
  from soothe_cli.tui.widgets.messages import (
@@ -1212,15 +1214,15 @@ class _MessagesMixin:
1212
1214
  def on_app_focus(self) -> None:
1213
1215
  """Restore primary input focus when the terminal regains OS focus.
1214
1216
 
1215
- When the user opens a link via `webbrowser.open`, OS focus shifts to
1216
- the browser. On returning to the terminal, Textual fires `AppFocus`
1217
- (requires a terminal that supports FocusIn events). Re-focusing the
1218
- primary input keeps it ready for typing — but only when no other
1219
- focusable widget (e.g., a step card control) currently owns focus, so
1220
- the user does not lose an in-progress answer to a tab-out and back.
1217
+ Fires when focus returns from an external program (e.g. a browser
1218
+ link). Re-focuses the primary input unless a modal is open, another
1219
+ focusable widget owns focus, or an unsubmitted QA card is active
1220
+ (its own focus guard handles recapture).
1221
1221
  """
1222
1222
  if self.screen.is_modal:
1223
1223
  return
1224
+ if self._has_active_qa_widget():
1225
+ return
1224
1226
  focused = self.focused
1225
1227
  primary = self._primary_text_input()
1226
1228
  if primary is not None:
@@ -1235,15 +1237,12 @@ class _MessagesMixin:
1235
1237
  self._chat_input.focus_input()
1236
1238
 
1237
1239
  def on_click(self, _event: Click) -> None:
1238
- """Focus the chat input when the click landed on non-focusable chrome.
1239
-
1240
- Original intent: clicking the dead transcript area should drop the
1241
- caret back in the prompt. But this handler bubbles for *every* click,
1242
- so an unconditional refocus also steals focus from inline focusable
1243
- widgets (e.g., the StructuredAskUserWidget answer field) on the same
1244
- click that Textual just used to focus them. Skip the refocus whenever
1245
- the click landed on a focusable widget — Textual's default focus
1246
- handling already does the right thing there.
1240
+ """Focus the chat input when a click lands on non-focusable chrome.
1241
+
1242
+ Bubbles for every click, so an unconditional refocus would steal
1243
+ focus from inline focusable widgets. Skip when the click target is
1244
+ focusable, a text selection is active, or an unsubmitted QA card is
1245
+ on screen (scrolling to review a plan must not yank focus).
1247
1246
  """
1248
1247
  if not self._chat_input:
1249
1248
  return
@@ -1254,6 +1253,8 @@ class _MessagesMixin:
1254
1253
  return
1255
1254
  if self._click_landed_on_focusable(_event):
1256
1255
  return
1256
+ if self._has_active_qa_widget():
1257
+ return
1257
1258
  self.call_after_refresh(self.focus_primary_input)
1258
1259
 
1259
1260
  def _click_landed_on_focusable(self, event: Click) -> bool:
@@ -1277,6 +1278,14 @@ class _MessagesMixin:
1277
1278
  node = getattr(node, "parent", None)
1278
1279
  return False
1279
1280
 
1281
+ def _has_active_qa_widget(self) -> bool:
1282
+ """Return True when an unsubmitted StructuredAskUserWidget is mounted."""
1283
+ adapter = getattr(self, "_ui_adapter", None)
1284
+ if adapter is None:
1285
+ return False
1286
+ by_step = getattr(adapter, "_clarification_input_by_step", None) or {}
1287
+ return any(not getattr(w, "_submitted", False) for w in by_step.values())
1288
+
1280
1289
  def on_text_selected(self, _event: TextSelected) -> None:
1281
1290
  """Copy selected transcript text on mouse release.
1282
1291
 
@@ -17,9 +17,9 @@ from textual.css.query import NoMatches
17
17
  from textual.theme import Theme
18
18
 
19
19
  from soothe_cli.display import theme
20
- from soothe_cli.tui.app._module_init import (
20
+ from soothe_cli.tui.app._theme_prefs import save_theme_preference
21
+ from soothe_cli.tui.app._types import (
21
22
  DeferredAction,
22
- save_theme_preference,
23
23
  )
24
24
  from soothe_cli.tui.widgets.messages import AppMessage, ErrorMessage
25
25
  from soothe_cli.tui.widgets.welcome import WelcomeBanner
@@ -0,0 +1,97 @@
1
+ """Parser for the `--model-params` flag in `/model` slash-command arguments."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import shlex
7
+ from typing import Any
8
+
9
+
10
+ def _extract_model_params_flag(raw_arg: str) -> tuple[str, dict[str, Any] | None]:
11
+ """Extract `--model-params` and its JSON value from a `/model` arg string.
12
+
13
+ Handles quoted (`'...'` / `"..."`) and bare `{...}` values with balanced
14
+ braces so that JSON containing spaces works without quoting.
15
+
16
+ Note:
17
+ The bare-brace mode counts `{` / `}` characters without awareness of
18
+ JSON string contents. Values that contain literal braces inside strings
19
+ (e.g., `{"stop": "end}here"}`) will mis-parse. Users should quote the
20
+ value in that case.
21
+
22
+ Args:
23
+ raw_arg: The argument string after `/model `.
24
+
25
+ Returns:
26
+ Tuple of `(remaining_args, parsed_dict | None)`. Returns `None` for the
27
+ dict when the flag is absent.
28
+
29
+ Raises:
30
+ ValueError: If the value is missing, has unclosed quotes,
31
+ unbalanced braces, or is not valid JSON.
32
+ TypeError: If the parsed JSON is not a dict.
33
+ """
34
+ flag = "--model-params"
35
+ idx = raw_arg.find(flag)
36
+ if idx == -1:
37
+ return raw_arg, None
38
+
39
+ before = raw_arg[:idx].rstrip()
40
+ after = raw_arg[idx + len(flag) :].lstrip()
41
+
42
+ if not after:
43
+ msg = "--model-params requires a JSON object value"
44
+ raise ValueError(msg)
45
+
46
+ # Determine the JSON string boundaries.
47
+ if after[0] in {"'", '"'}:
48
+ quote = after[0]
49
+ end = -1
50
+ backslash_count = 0
51
+ for i, ch in enumerate(after[1:], start=1):
52
+ if ch == "\\":
53
+ backslash_count += 1
54
+ continue
55
+ if ch == quote and backslash_count % 2 == 0:
56
+ end = i
57
+ break
58
+ backslash_count = 0
59
+ if end == -1:
60
+ msg = f"Unclosed {quote} in --model-params value"
61
+ raise ValueError(msg)
62
+ # Parse the quoted token with shlex so escaped quotes are unescaped.
63
+ json_str = shlex.split(after[: end + 1], posix=True)[0]
64
+ rest = after[end + 1 :].lstrip()
65
+ elif after[0] == "{":
66
+ # Walk forward to find the matching closing brace.
67
+ depth = 0
68
+ end = -1
69
+ for i, ch in enumerate(after):
70
+ if ch == "{":
71
+ depth += 1
72
+ elif ch == "}":
73
+ depth -= 1
74
+ if depth == 0:
75
+ end = i
76
+ break
77
+ if end == -1:
78
+ msg = "Unbalanced braces in --model-params value"
79
+ raise ValueError(msg)
80
+ json_str = after[: end + 1]
81
+ rest = after[end + 1 :].lstrip()
82
+ else:
83
+ # Non-brace, non-quoted — take the next whitespace-delimited token.
84
+ parts = after.split(None, 1)
85
+ json_str = parts[0]
86
+ rest = parts[1] if len(parts) > 1 else ""
87
+
88
+ remaining = f"{before} {rest}".strip()
89
+ try:
90
+ params = json.loads(json_str)
91
+ except json.JSONDecodeError:
92
+ msg = f'Invalid JSON in --model-params: {json_str!r}. Expected format: --model-params \'{{"key": "value"}}\''
93
+ raise ValueError(msg) from None
94
+ if not isinstance(params, dict):
95
+ msg = "--model-params must be a JSON object, got " + type(params).__name__
96
+ raise TypeError(msg)
97
+ return remaining, params
@@ -20,7 +20,7 @@ from textual.containers import VerticalScroll
20
20
  from textual.css.query import NoMatches
21
21
 
22
22
  from soothe_cli._version import CHANGELOG_URL
23
- from soothe_cli.tui.app._module_init import (
23
+ from soothe_cli.tui.app._types import (
24
24
  TextualSessionState,
25
25
  )
26
26
  from soothe_cli.tui.widgets.chat_input import ChatInput
@@ -0,0 +1,75 @@
1
+ """iTerm2 terminal integration: cursor guide escape sequences and lifecycle.
2
+
3
+ The cursor guide (highlight cursor line) causes visual artifacts when Textual
4
+ takes over the terminal in alternate screen mode. We disable it before the app
5
+ launches and restore on exit. Both atexit and exit() override are used for
6
+ defense-in-depth: atexit catches abnormal termination (SIGTERM, unhandled
7
+ exceptions), while exit() ensures restoration before Textual's cleanup.
8
+
9
+ Unlike the previous catch-all module, this module performs **no** side effects
10
+ at import time. Call `init_terminal_integration()` explicitly before starting
11
+ the Textual app.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import os
17
+
18
+ # Detection: check env vars AND that stderr is a TTY (avoids false positives
19
+ # when env vars are inherited but running in non-TTY context like CI)
20
+ _IS_ITERM = (
21
+ (
22
+ os.environ.get("LC_TERMINAL", "") == "iTerm2"
23
+ or os.environ.get("TERM_PROGRAM", "") == "iTerm.app"
24
+ )
25
+ and hasattr(os, "isatty")
26
+ and os.isatty(2)
27
+ )
28
+
29
+ # iTerm2 cursor guide escape sequences (OSC 1337)
30
+ # Format: OSC 1337 ; HighlightCursorLine=<yes|no> ST
31
+ # Where OSC = ESC ] (0x1b 0x5d) and ST = ESC \ (0x1b 0x5c)
32
+ _ITERM_CURSOR_GUIDE_OFF = "\x1b]1337;HighlightCursorLine=no\x1b\\"
33
+ _ITERM_CURSOR_GUIDE_ON = "\x1b]1337;HighlightCursorLine=yes\x1b\\"
34
+
35
+
36
+ def _write_iterm_escape(sequence: str) -> None:
37
+ """Write an iTerm2 escape sequence to stderr.
38
+
39
+ Silently fails if the terminal is unavailable (redirected, closed, broken
40
+ pipe). This is a cosmetic feature, so failures should never crash the app.
41
+ """
42
+ if not _IS_ITERM:
43
+ return
44
+ try:
45
+ import sys
46
+
47
+ if sys.__stderr__ is not None:
48
+ sys.__stderr__.write(sequence)
49
+ sys.__stderr__.flush()
50
+ except OSError:
51
+ # Terminal may be unavailable (redirected, closed, broken pipe)
52
+ pass
53
+
54
+
55
+ def _restore_cursor_guide() -> None:
56
+ """Restore iTerm2 cursor guide on exit.
57
+
58
+ Registered with atexit to ensure the cursor guide is re-enabled
59
+ when the CLI exits, regardless of how the exit occurs.
60
+ """
61
+ _write_iterm_escape(_ITERM_CURSOR_GUIDE_ON)
62
+
63
+
64
+ def init_terminal_integration() -> None:
65
+ """Disable the iTerm2 cursor guide and register restore-on-exit.
66
+
67
+ Call this explicitly before starting the Textual app. This replaces the
68
+ previous import-time side effect so that merely importing the module does
69
+ not write to the terminal.
70
+ """
71
+ _write_iterm_escape(_ITERM_CURSOR_GUIDE_OFF)
72
+ if _IS_ITERM:
73
+ import atexit
74
+
75
+ atexit.register(_restore_cursor_guide)