soothe-cli 0.5.2__tar.gz → 0.5.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/PKG-INFO +1 -1
  2. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/cli/commands/run_cmd.py +25 -7
  3. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/cli/main.py +19 -4
  4. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/config/cli_config.py +1 -1
  5. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/tui/_env_vars.py +4 -1
  6. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/tui/app/_history.py +4 -4
  7. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/tui/app/_messages_mixin.py +2 -2
  8. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/tui/app/_startup.py +19 -15
  9. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/tui/config.py +3 -3
  10. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/tui/textual_adapter/_turn.py +13 -4
  11. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/tui/update_check.py +46 -15
  12. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/tui/widgets/message_store.py +14 -14
  13. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/tui/widgets/messages.py +35 -17
  14. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/tui/widgets/welcome.py +23 -0
  15. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/.gitignore +0 -0
  16. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/README.md +0 -0
  17. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/pyproject.toml +0 -0
  18. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/__init__.py +0 -0
  19. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/cli/__init__.py +0 -0
  20. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/cli/commands/__init__.py +0 -0
  21. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/cli/commands/autopilot_cmd.py +0 -0
  22. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/cli/commands/loop_cmd.py +0 -0
  23. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/cli/execution/__init__.py +0 -0
  24. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/cli/execution/daemon.py +0 -0
  25. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/cli/execution/headless.py +0 -0
  26. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/cli/execution/headless_renderer.py +0 -0
  27. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/cli/execution/launcher.py +0 -0
  28. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/cli/stream/__init__.py +0 -0
  29. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/cli/stream/context.py +0 -0
  30. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/cli/stream/display_line.py +0 -0
  31. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/cli/stream/formatter.py +0 -0
  32. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/cli/stream/pipeline.py +0 -0
  33. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/cli/stream/task_scope.py +0 -0
  34. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/config/__init__.py +0 -0
  35. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/plan/__init__.py +0 -0
  36. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/plan/rich_tree.py +0 -0
  37. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/shared/__init__.py +0 -0
  38. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/shared/commands/__init__.py +0 -0
  39. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/shared/commands/command_router.py +0 -0
  40. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/shared/commands/slash_commands.py +0 -0
  41. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/shared/commands/subagent_routing.py +0 -0
  42. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/shared/config_loader.py +0 -0
  43. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/shared/core/__init__.py +0 -0
  44. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/shared/core/event_processor.py +0 -0
  45. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/shared/core/presentation_engine.py +0 -0
  46. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/shared/core/processor_state.py +0 -0
  47. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/shared/core/renderer_protocol.py +0 -0
  48. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/shared/duration_format.py +0 -0
  49. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/shared/events/__init__.py +0 -0
  50. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/shared/events/display_policy.py +0 -0
  51. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/shared/events/essential_events.py +0 -0
  52. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/shared/events/explore_task_display.py +0 -0
  53. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/shared/events/stream_accumulator.py +0 -0
  54. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/shared/events/tui_trace_log.py +0 -0
  55. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/shared/rendering/__init__.py +0 -0
  56. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/shared/rendering/async_renderer_protocol.py +0 -0
  57. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/shared/rendering/renderer_base.py +0 -0
  58. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/shared/tools/__init__.py +0 -0
  59. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/shared/tools/_utils.py +0 -0
  60. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/shared/tools/message_processing.py +0 -0
  61. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/shared/tools/rendering.py +0 -0
  62. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/shared/tools/tool_call_resolution.py +0 -0
  63. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/shared/tools/tool_card_payload.py +0 -0
  64. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/shared/tools/tool_card_visibility.py +0 -0
  65. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/shared/tools/tool_formatters/__init__.py +0 -0
  66. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/shared/tools/tool_formatters/base.py +0 -0
  67. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/shared/tools/tool_formatters/execution.py +0 -0
  68. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/shared/tools/tool_formatters/fallback.py +0 -0
  69. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/shared/tools/tool_formatters/file_ops.py +0 -0
  70. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/shared/tools/tool_formatters/goal_formatter.py +0 -0
  71. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/shared/tools/tool_formatters/media.py +0 -0
  72. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/shared/tools/tool_formatters/structured.py +0 -0
  73. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/shared/tools/tool_formatters/subagent.py +0 -0
  74. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/shared/tools/tool_formatters/web.py +0 -0
  75. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/shared/tools/tool_message_format.py +0 -0
  76. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/shared/tools/tool_output_formatter.py +0 -0
  77. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/tui/__init__.py +0 -0
  78. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/tui/_ask_user_types.py +0 -0
  79. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/tui/_cli_context.py +0 -0
  80. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/tui/_session_stats.py +0 -0
  81. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/tui/_version.py +0 -0
  82. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/tui/app/__init__.py +0 -0
  83. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/tui/app/_app.py +0 -0
  84. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/tui/app/_commands.py +0 -0
  85. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/tui/app/_execution.py +0 -0
  86. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/tui/app/_model.py +0 -0
  87. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/tui/app/_module_init.py +0 -0
  88. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/tui/app/_ui.py +0 -0
  89. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/tui/app/app.tcss +0 -0
  90. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/tui/command_registry.py +0 -0
  91. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/tui/daemon_session.py +0 -0
  92. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/tui/file_ops.py +0 -0
  93. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/tui/formatting.py +0 -0
  94. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/tui/hooks.py +0 -0
  95. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/tui/input.py +0 -0
  96. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/tui/media_utils.py +0 -0
  97. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/tui/message_display_filter.py +0 -0
  98. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/tui/model_config.py +0 -0
  99. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/tui/output.py +0 -0
  100. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/tui/preview_limits.py +0 -0
  101. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/tui/project_utils.py +0 -0
  102. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/tui/sessions.py +0 -0
  103. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/tui/skills/__init__.py +0 -0
  104. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/tui/skills/invocation.py +0 -0
  105. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/tui/skills/load.py +0 -0
  106. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/tui/textual_adapter/__init__.py +0 -0
  107. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/tui/textual_adapter/_adapter.py +0 -0
  108. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/tui/textual_adapter/_stream_formatting.py +0 -0
  109. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/tui/textual_adapter/_stream_messages.py +0 -0
  110. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/tui/textual_adapter/_turn_helpers.py +0 -0
  111. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/tui/theme.py +0 -0
  112. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/tui/tool_display.py +0 -0
  113. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/tui/unicode_security.py +0 -0
  114. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/tui/widgets/__init__.py +0 -0
  115. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/tui/widgets/_links.py +0 -0
  116. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/tui/widgets/approval.py +0 -0
  117. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/tui/widgets/ask_user.py +0 -0
  118. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/tui/widgets/autocomplete.py +0 -0
  119. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/tui/widgets/autopilot_dashboard.py +0 -0
  120. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/tui/widgets/autopilot_screen.py +0 -0
  121. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/tui/widgets/chat_input.py +0 -0
  122. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/tui/widgets/clipboard.py +0 -0
  123. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/tui/widgets/diff.py +0 -0
  124. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/tui/widgets/editor.py +0 -0
  125. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/tui/widgets/history.py +0 -0
  126. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/tui/widgets/loading.py +0 -0
  127. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/tui/widgets/loop_selector.py +0 -0
  128. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/tui/widgets/mcp_viewer.py +0 -0
  129. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/tui/widgets/model_selector.py +0 -0
  130. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/tui/widgets/notification_settings.py +0 -0
  131. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/tui/widgets/status.py +0 -0
  132. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/tui/widgets/theme_selector.py +0 -0
  133. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/tui/widgets/tool_renderers.py +0 -0
  134. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/tui/widgets/tool_widgets.py +0 -0
  135. {soothe_cli-0.5.2 → soothe_cli-0.5.3}/src/soothe_cli/tui/widgets/tools.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: soothe-cli
3
- Version: 0.5.2
3
+ Version: 0.5.3
4
4
  Summary: Soothe CLI client - communicates with daemon via WebSocket
5
5
  Project-URL: Homepage, https://github.com/OpenSoothe/soothe
6
6
  Project-URL: Documentation, https://soothe.readthedocs.io
@@ -24,25 +24,29 @@ def run_impl(
24
24
  streaming_enabled: bool | None = None,
25
25
  streaming_mode: str | None = None,
26
26
  *,
27
+ tui_with_prompt: bool = False,
27
28
  config_path: str | None = None,
28
29
  ) -> None:
29
30
  """Core implementation for running Soothe agent.
30
31
 
31
32
  Args:
32
- prompt: Optional prompt for headless mode
33
+ prompt: Optional user message; non-empty prompt defaults to a headless
34
+ one-shot run unless ``tui_with_prompt`` is set or a loop is being
35
+ resumed (``resume_loop_id``).
33
36
  resume_loop_id: Existing loop id to attach to (optional)
34
- no_tui: Force headless mode
37
+ no_tui: Require headless mode (must include a non-empty prompt)
35
38
  autonomous: Enable autonomous iteration mode
36
39
  max_iterations: Max iterations for autonomous mode
37
40
  streaming_enabled: Override daemon streaming enabled setting (RFC-614)
38
41
  streaming_mode: Override daemon streaming mode ('streaming' or 'batch')
42
+ tui_with_prompt: When True with a prompt, open the TUI instead of headless.
39
43
  """
40
44
  startup_start = time.perf_counter()
41
45
 
42
46
  try:
43
47
  cfg = load_config(config_path)
44
48
  log_level = resolve_cli_log_level(logging_level=cfg.logging_level)
45
- log_file = Path(SOOTHE_HOME) / "logs" / "soothe-cli.log"
49
+ log_file = Path(SOOTHE_HOME) / "logs" / "cli.log"
46
50
  setup_logging(log_level, log_file=log_file)
47
51
 
48
52
  # PostgreSQL availability check (requires daemon-side config)
@@ -62,17 +66,31 @@ def run_impl(
62
66
 
63
67
  run_start = time.perf_counter()
64
68
 
65
- if no_tui:
66
- # Headless mode (force no TUI)
69
+ has_prompt = bool(prompt and str(prompt).strip())
70
+ attaching_loop = bool(resume_loop_id and str(resume_loop_id).strip())
71
+
72
+ if tui_with_prompt and has_prompt:
73
+ use_headless = False
74
+ elif no_tui and not has_prompt:
75
+ typer.echo(
76
+ "Error: --no-tui requires a non-empty --prompt (-p).",
77
+ err=True,
78
+ )
79
+ sys.exit(1)
80
+ elif no_tui:
81
+ use_headless = True
82
+ else:
83
+ use_headless = has_prompt and not attaching_loop
84
+
85
+ if use_headless:
67
86
  run_headless(
68
87
  cfg,
69
- prompt or "",
88
+ str(prompt).strip(),
70
89
  resume_loop_id=resume_loop_id,
71
90
  autonomous=autonomous,
72
91
  max_iterations=max_iterations,
73
92
  )
74
93
  else:
75
- # TUI mode (with optional initial prompt)
76
94
  run_tui(cfg, resume_loop_id=resume_loop_id, initial_prompt=prompt)
77
95
 
78
96
  run_elapsed_s = time.perf_counter() - run_start
@@ -55,12 +55,24 @@ def main(
55
55
  prompt: Annotated[
56
56
  str | None,
57
57
  typer.Option(
58
- "--prompt", "-p", help="Prompt to send as user message (headless single-shot mode)."
58
+ "--prompt",
59
+ "-p",
60
+ help="User message; runs a one-shot headless query by default (use --tui for TUI).",
59
61
  ),
60
62
  ] = None,
61
63
  no_tui: Annotated[ # noqa: FBT002
62
64
  bool,
63
- typer.Option("--no-tui", help="Disable TUI; run single prompt and exit."),
65
+ typer.Option(
66
+ "--no-tui",
67
+ help="Headless mode (requires --prompt). Same as default when -p is set.",
68
+ ),
69
+ ] = False,
70
+ tui_with_prompt: Annotated[ # noqa: FBT002
71
+ bool,
72
+ typer.Option(
73
+ "--tui",
74
+ help="With --prompt/-p, open the interactive TUI and auto-submit the prompt.",
75
+ ),
64
76
  ] = False,
65
77
  streaming: Annotated[
66
78
  bool | None,
@@ -81,13 +93,15 @@ def main(
81
93
  ) -> None:
82
94
  """Soothe CLI - Intelligent AI assistant client.
83
95
 
84
- Run without arguments for interactive TUI mode, or provide a prompt via --prompt/-p option.
96
+ Run without arguments for interactive TUI mode, or pass --prompt for a one-shot
97
+ headless query (stdout, then exit).
85
98
 
86
99
  Note: This is the CLI client. Use 'soothed' command to manage the daemon server.
87
100
 
88
101
  Examples:
89
102
  soothe # Interactive TUI mode
90
- soothe -p "Research AI advances" # Headless single-prompt mode
103
+ soothe -p "Research AI advances" # One-shot headless (non-TUI) query
104
+ soothe -p "Hello" --tui # TUI with an auto-submitted prompt
91
105
  soothe loop list # List AgentLoop instances
92
106
  """
93
107
  # Handle -h/--help flag
@@ -112,6 +126,7 @@ def main(
112
126
  max_iterations=None,
113
127
  streaming_enabled=streaming,
114
128
  streaming_mode=streaming_mode,
129
+ tui_with_prompt=tui_with_prompt,
115
130
  )
116
131
 
117
132
 
@@ -24,7 +24,7 @@ class CLIConfig:
24
24
  daemon_host: str = "127.0.0.1"
25
25
  daemon_port: int = 8765
26
26
 
27
- # logging_level: DEBUG/INFO/… for ~/.soothe/logs/soothe-cli.log; None = default INFO.
27
+ # logging_level: DEBUG/INFO/… for ~/.soothe/logs/cli.log; None = default INFO.
28
28
  logging_level: str | None = None
29
29
 
30
30
  # Output streaming overrides (RFC-614)
@@ -29,7 +29,7 @@ from __future__ import annotations
29
29
  # ---------------------------------------------------------------------------
30
30
 
31
31
  AUTO_UPDATE = "SOOTHE_CLI_AUTO_UPDATE"
32
- """Enable automatic CLI updates ('1', 'true', or 'yes')."""
32
+ """Override automatic CLI updates: '1'/'true'/'yes' on, '0'/'false'/'no' off. On by default when unset."""
33
33
 
34
34
  DEBUG = "SOOTHE_CLI_DEBUG"
35
35
  """Enable verbose debug logging to a file."""
@@ -43,6 +43,9 @@ EXTRA_SKILLS_DIRS = "SOOTHE_CLI_EXTRA_SKILLS_DIRS"
43
43
  NO_UPDATE_CHECK = "SOOTHE_CLI_NO_UPDATE_CHECK"
44
44
  """Disable automatic update checking when set."""
45
45
 
46
+ UPDATE_CHECK = "SOOTHE_CLI_UPDATE_CHECK"
47
+ """Force-enable startup PyPI update checks ('1', 'true', or 'yes'). On by default."""
48
+
46
49
  SERVER_ENV_PREFIX = "SOOTHE_CLI_SERVER_"
47
50
  """Environment variable prefix used to pass CLI config to the server subprocess."""
48
51
 
@@ -438,10 +438,10 @@ class _HistoryMixin:
438
438
  # (``phase=goal_completion``); avoid duplicate app-line noise.
439
439
  return None
440
440
  if event_type == "soothe.cognition.agent_loop.reasoned":
441
- plan_action_raw = str(event_data.get("plan_action") or "new").strip()
442
- plan_action = plan_action_raw if plan_action_raw in {"keep", "new"} else "new"
441
+ plan_action_raw = str(event_data.get("plan_action") or "").strip()
442
+ plan_action = plan_action_raw if plan_action_raw in {"keep", "new"} else ""
443
443
  return MessageData(
444
- type=MessageType.COGNITION_PLAN,
444
+ type=MessageType.COGNITION_REASON,
445
445
  content="",
446
446
  timestamp=event_timestamp,
447
447
  cognition_plan_next_action=str(event_data.get("next_action") or ""),
@@ -541,7 +541,7 @@ class _HistoryMixin:
541
541
  if msg_data is None:
542
542
  continue
543
543
  if msg_data.type not in (
544
- MessageType.COGNITION_PLAN,
544
+ MessageType.COGNITION_REASON,
545
545
  MessageType.COGNITION_GOAL_TREE,
546
546
  MessageType.STEP_PROGRESS,
547
547
  ):
@@ -28,7 +28,7 @@ from soothe_cli.tui.widgets.messages import (
28
28
  AppMessage,
29
29
  AssistantMessage,
30
30
  CognitionGoalTreeMessage,
31
- CognitionPlanReasonMessage,
31
+ CognitionReasonMessage,
32
32
  CognitionStepMessage,
33
33
  ErrorMessage,
34
34
  QueuedUserMessage,
@@ -153,7 +153,7 @@ class _MessagesMixin:
153
153
  | ToolCallMessage
154
154
  | SkillMessage
155
155
  | CognitionStepMessage
156
- | CognitionPlanReasonMessage
156
+ | CognitionReasonMessage
157
157
  | CognitionGoalTreeMessage,
158
158
  ) -> None:
159
159
  """Mount a message widget to the messages area.
@@ -153,8 +153,9 @@ class _StartupMixin:
153
153
  group="daemon-connect",
154
154
  )
155
155
 
156
- # Background update check and what's-new banner
157
- # (opt-out via env var or config.yml [update].check)
156
+ # Background update check and what's-new banner (on by default; opt-out via
157
+ # SOOTHE_CLI_NO_UPDATE_CHECK or [update].check: false; SOOTHE_CLI_UPDATE_CHECK
158
+ # forces on if config disables)
158
159
  from soothe_cli.tui.update_check import is_update_check_enabled
159
160
 
160
161
  if is_update_check_enabled():
@@ -603,14 +604,13 @@ class _StartupMixin:
603
604
  return
604
605
 
605
606
  self._update_available = (True, latest)
607
+ self.call_after_refresh(lambda v=latest: self._apply_welcome_update_notice(v))
606
608
  except Exception:
607
609
  logger.debug("Background update check failed", exc_info=True)
608
610
  return
609
611
 
610
- # Phase 2: auto-update or notify (failures surfaced to user)
612
+ # Phase 2: optional auto-update (version notice lives on the welcome banner only)
611
613
  try:
612
- from soothe_cli.tui._version import __version__ as cli_version
613
-
614
614
  if is_auto_update_enabled():
615
615
  from soothe_cli.tui.update_check import perform_upgrade
616
616
 
@@ -626,6 +626,7 @@ class _StartupMixin:
626
626
  severity="information",
627
627
  timeout=10,
628
628
  )
629
+ self.call_after_refresh(lambda: self._apply_welcome_update_notice(None))
629
630
  else:
630
631
  cmd = upgrade_command()
631
632
  self.notify(
@@ -634,16 +635,6 @@ class _StartupMixin:
634
635
  timeout=15,
635
636
  markup=False,
636
637
  )
637
- else:
638
- cmd = upgrade_command()
639
- self.notify(
640
- f"Update available: v{latest} (current: v{cli_version}). "
641
- f"Run: {cmd}\n\n"
642
- f"Enable auto-updates: /auto-update",
643
- severity="information",
644
- timeout=15,
645
- markup=False,
646
- )
647
638
  except Exception:
648
639
  logger.warning("Auto-update failed unexpectedly", exc_info=True)
649
640
  self.notify(
@@ -652,6 +643,14 @@ class _StartupMixin:
652
643
  timeout=10,
653
644
  )
654
645
 
646
+ def _apply_welcome_update_notice(self, latest: str | None) -> None:
647
+ """Show or hide the welcome-banner update line (must run on the UI thread)."""
648
+ try:
649
+ banner = self.query_one("#welcome-banner", WelcomeBanner)
650
+ banner.set_update_notice(latest)
651
+ except NoMatches:
652
+ logger.debug("Welcome banner not found while applying update notice")
653
+
655
654
  async def _show_whats_new(self) -> None:
656
655
  """Show a 'what's new' banner on the first launch after an upgrade."""
657
656
  try:
@@ -698,17 +697,22 @@ class _StartupMixin:
698
697
  await self._mount_message(AppMessage("Checking for updates..."))
699
698
  available, latest = await asyncio.to_thread(is_update_available, bypass_cache=True)
700
699
  if not available:
700
+ self._update_available = (False, None)
701
+ self._apply_welcome_update_notice(None)
701
702
  await self._mount_message(AppMessage("Already on the latest version."))
702
703
  return
703
704
 
704
705
  from soothe_cli.tui._version import __version__ as cli_version
705
706
 
707
+ self._update_available = (True, latest)
708
+ self._apply_welcome_update_notice(latest)
706
709
  await self._mount_message(
707
710
  AppMessage(f"Update available: v{latest} (current: v{cli_version}). Upgrading...")
708
711
  )
709
712
  success, output = await perform_upgrade()
710
713
  if success:
711
714
  self._update_available = (False, None)
715
+ self._apply_welcome_update_notice(None)
712
716
  await self._mount_message(
713
717
  AppMessage(f"Updated to v{latest}. Restart to use the new version.")
714
718
  )
@@ -252,7 +252,7 @@ class Glyphs:
252
252
 
253
253
  # Expand/collapse icons
254
254
  expand: str # ▶ vs [+] - shown when collapsed (click to expand)
255
- collapse: str # vs [^] - shown when expanded (click to collapse)
255
+ collapse: str # vs [v] - shown when expanded (click to collapse)
256
256
 
257
257
  # Box-drawing characters
258
258
  box_vertical: str # │ vs |
@@ -287,7 +287,7 @@ UNICODE_GLYPHS = Glyphs(
287
287
  assistant="🤖", # AI/assistant icon
288
288
  # Expand/collapse icons
289
289
  expand="▶",
290
- collapse="",
290
+ collapse="",
291
291
  # Box-drawing characters
292
292
  box_vertical="│",
293
293
  box_horizontal="─",
@@ -318,7 +318,7 @@ ASCII_GLYPHS = Glyphs(
318
318
  assistant="[A]", # AI/assistant icon (ASCII)
319
319
  # Expand/collapse icons
320
320
  expand="[+]",
321
- collapse="[^]",
321
+ collapse="[v]",
322
322
  # Box-drawing characters
323
323
  box_vertical="|",
324
324
  box_horizontal="-",
@@ -91,7 +91,7 @@ from soothe_cli.tui.textual_adapter._turn_helpers import (
91
91
  from soothe_cli.tui.widgets.messages import (
92
92
  AppMessage,
93
93
  AssistantMessage,
94
- CognitionPlanReasonMessage,
94
+ CognitionReasonMessage,
95
95
  CognitionStepMessage,
96
96
  DiffMessage,
97
97
  SummarizationMessage,
@@ -1249,6 +1249,8 @@ async def execute_task_textual(
1249
1249
  error_text = str(
1250
1250
  data.get("error") or data.get("message") or "Agent error"
1251
1251
  )
1252
+ adapter.finalize_pending_tools_with_error(error_text)
1253
+ adapter.finalize_pending_steps_with_error(error_text)
1252
1254
  await adapter._mount_message(AppMessage(error_text))
1253
1255
  if adapter._set_spinner:
1254
1256
  await adapter._set_spinner(None)
@@ -1395,9 +1397,9 @@ async def execute_task_textual(
1395
1397
  )
1396
1398
  pending_text_by_namespace[ns_key] = ""
1397
1399
  assistant_message_by_namespace.pop(ns_key, None)
1398
- pa_raw = data.get("plan_action", "new")
1399
- plan_action = pa_raw if pa_raw in ("keep", "new") else "new"
1400
- plan_widget = CognitionPlanReasonMessage(
1400
+ pa_raw = data.get("plan_action", "")
1401
+ plan_action = pa_raw if pa_raw in ("keep", "new") else ""
1402
+ plan_widget = CognitionReasonMessage(
1401
1403
  next_action=str(data.get("next_action", "")),
1402
1404
  status=str(data.get("status", "")),
1403
1405
  iteration=int(data.get("iteration", 0)),
@@ -1509,6 +1511,13 @@ async def execute_task_textual(
1509
1511
  )
1510
1512
  adapter._pending_main_tools.clear()
1511
1513
 
1514
+ # Safety net: finalize any steps/tools still in-flight (e.g. worker
1515
+ # crash sent a soothe.error.* event but step_completed was never
1516
+ # emitted, or stream ended before matching results arrived).
1517
+ if adapter._current_step_messages or adapter._current_tool_messages:
1518
+ adapter.finalize_pending_tools_with_error("Stream ended unexpectedly")
1519
+ adapter.finalize_pending_steps_with_error("Stream ended unexpectedly")
1520
+
1512
1521
  # Handle HITL after stream completes
1513
1522
  if interrupt_occurred:
1514
1523
  any_rejected = False
@@ -316,35 +316,66 @@ async def perform_upgrade() -> tuple[bool, str]:
316
316
 
317
317
 
318
318
  def is_update_check_enabled() -> bool:
319
- """Return whether update checks are enabled.
319
+ """Return whether startup update checks are enabled.
320
320
 
321
- Checks `SOOTHE_NO_UPDATE_CHECK` env var and the `[update].check` key
322
- in `config.yml`.
323
-
324
- Defaults to enabled.
321
+ Disabled when `SOOTHE_CLI_NO_UPDATE_CHECK` or legacy `SOOTHE_NO_UPDATE_CHECK`
322
+ is set. When `SOOTHE_CLI_UPDATE_CHECK` is ``1``/``true``/``yes``, checks are
323
+ enabled (including when ``[update].check: false`` would otherwise turn them
324
+ off). Otherwise, respects ``[update].check`` in ``config.yml`` when
325
+ present; defaults to on. Use ``/update`` to check manually any time.
325
326
  """
326
- if os.environ.get("SOOTHE_NO_UPDATE_CHECK"):
327
+ from soothe_cli.tui._env_vars import NO_UPDATE_CHECK, UPDATE_CHECK
328
+
329
+ if os.environ.get("SOOTHE_NO_UPDATE_CHECK") or os.environ.get(NO_UPDATE_CHECK):
327
330
  return False
328
- return _read_update_config().get("check", True)
331
+ if os.environ.get(UPDATE_CHECK, "").lower() in {"1", "true", "yes"}:
332
+ return True
333
+ cfg = _read_update_config()
334
+ if "check" in cfg:
335
+ return bool(cfg["check"])
336
+ return True
329
337
 
330
338
 
331
- def is_auto_update_enabled() -> bool:
332
- """Return whether auto-update is enabled.
339
+ def _auto_update_env_override() -> bool | None:
340
+ """Return env-forced auto-update flag, or ``None`` if unset.
341
+
342
+ ``SOOTHE_CLI_AUTO_UPDATE`` (and legacy ``SOOTHE_AUTO_UPDATE``) may be
343
+ ``1``/``true``/``yes`` to force on or ``0``/``false``/``no`` to force off.
344
+ """
345
+ from soothe_cli.tui._env_vars import AUTO_UPDATE
333
346
 
334
- Opt-in via `SOOTHE_AUTO_UPDATE=1` env var or
335
- `[update].auto_update = true` in `config.yml`.
347
+ for key in (AUTO_UPDATE, "SOOTHE_AUTO_UPDATE"):
348
+ raw = os.environ.get(key)
349
+ if raw is None or not str(raw).strip():
350
+ continue
351
+ lv = str(raw).strip().lower()
352
+ if lv in {"1", "true", "yes"}:
353
+ return True
354
+ if lv in {"0", "false", "no"}:
355
+ return False
356
+ return None
336
357
 
337
- Defaults to `False`.
358
+
359
+ def is_auto_update_enabled() -> bool:
360
+ """Return whether auto-update is enabled.
338
361
 
339
362
  Always disabled for editable installs.
363
+
364
+ Otherwise, ``SOOTHE_CLI_AUTO_UPDATE`` (or legacy ``SOOTHE_AUTO_UPDATE``)
365
+ forces on or off when set. When unset, ``[update].auto_update`` in
366
+ ``config.yml`` is used if present; defaults to on.
340
367
  """
341
368
  from soothe_cli.tui.config import _is_editable_install
342
369
 
343
370
  if _is_editable_install():
344
371
  return False
345
- if os.environ.get("SOOTHE_AUTO_UPDATE", "").lower() in {"1", "true", "yes"}:
346
- return True
347
- return _read_update_config().get("auto_update", False)
372
+ env_val = _auto_update_env_override()
373
+ if env_val is not None:
374
+ return env_val
375
+ cfg = _read_update_config()
376
+ if "auto_update" in cfg:
377
+ return bool(cfg["auto_update"])
378
+ return True
348
379
 
349
380
 
350
381
  def set_auto_update(enabled: bool) -> None:
@@ -53,7 +53,7 @@ class MessageType(StrEnum):
53
53
  APP = "app"
54
54
  SUMMARIZATION = "summarization"
55
55
  STEP_PROGRESS = "step_progress"
56
- COGNITION_PLAN = "cognition_plan"
56
+ COGNITION_REASON = "cognition_reason"
57
57
  COGNITION_GOAL_TREE = "cognition_goal_tree"
58
58
  DIFF = "diff"
59
59
 
@@ -163,22 +163,22 @@ class MessageData:
163
163
  """JSON list of tool rows from ``CognitionStepMessage.snapshot_tool_rows()`` (IG-402)."""
164
164
 
165
165
  cognition_plan_next_action: str | None = None
166
- """User-facing next step (COGNITION_PLAN only)."""
166
+ """User-facing next step (COGNITION_REASON only)."""
167
167
 
168
168
  cognition_plan_status: str | None = None
169
- """Plan status: continue, replan, done (COGNITION_PLAN only)."""
169
+ """Plan status: continue, replan, done (COGNITION_REASON only)."""
170
170
 
171
171
  cognition_plan_iteration: int | None = None
172
- """Agent-loop iteration (COGNITION_PLAN only)."""
172
+ """Agent-loop iteration (COGNITION_REASON only)."""
173
173
 
174
174
  cognition_plan_action: str | None = None
175
- """``keep`` or ``new`` (COGNITION_PLAN only)."""
175
+ """``keep`` or ``new`` (COGNITION_REASON only)."""
176
176
 
177
177
  cognition_plan_assessment: str | None = None
178
- """Phase-1 assessment text (COGNITION_PLAN only)."""
178
+ """Phase-1 assessment text (COGNITION_REASON only)."""
179
179
 
180
180
  cognition_plan_strategy: str | None = None
181
- """Phase-2 plan reasoning (COGNITION_PLAN only)."""
181
+ """Phase-2 plan reasoning (COGNITION_REASON only)."""
182
182
 
183
183
  cognition_goal_snapshot_json: str | None = None
184
184
  """JSON blob from ``CognitionGoalTreeMessage.snapshot_dict()`` (COGNITION_GOAL_TREE only)."""
@@ -227,7 +227,7 @@ class MessageData:
227
227
  AppMessage,
228
228
  AssistantMessage,
229
229
  CognitionGoalTreeMessage,
230
- CognitionPlanReasonMessage,
230
+ CognitionReasonMessage,
231
231
  CognitionStepMessage,
232
232
  DiffMessage,
233
233
  ErrorMessage,
@@ -329,12 +329,12 @@ class MessageData:
329
329
  )
330
330
  return w
331
331
 
332
- case MessageType.COGNITION_PLAN:
333
- return CognitionPlanReasonMessage(
332
+ case MessageType.COGNITION_REASON:
333
+ return CognitionReasonMessage(
334
334
  next_action=self.cognition_plan_next_action or "",
335
335
  status=self.cognition_plan_status or "",
336
336
  iteration=int(self.cognition_plan_iteration or 0),
337
- plan_action=self.cognition_plan_action or "new",
337
+ plan_action=self.cognition_plan_action or "",
338
338
  assessment_reasoning=self.cognition_plan_assessment or "",
339
339
  plan_reasoning=self.cognition_plan_strategy or "",
340
340
  id=self.id,
@@ -386,7 +386,7 @@ class MessageData:
386
386
  AppMessage,
387
387
  AssistantMessage,
388
388
  CognitionGoalTreeMessage,
389
- CognitionPlanReasonMessage,
389
+ CognitionReasonMessage,
390
390
  CognitionStepMessage,
391
391
  DiffMessage,
392
392
  ErrorMessage,
@@ -414,9 +414,9 @@ class MessageData:
414
414
  cognition_goal_snapshot_json=json.dumps(widget.snapshot_dict()),
415
415
  )
416
416
 
417
- if isinstance(widget, CognitionPlanReasonMessage):
417
+ if isinstance(widget, CognitionReasonMessage):
418
418
  return cls(
419
- type=MessageType.COGNITION_PLAN,
419
+ type=MessageType.COGNITION_REASON,
420
420
  content="",
421
421
  id=widget_id,
422
422
  cognition_plan_next_action=widget._next_action,
@@ -1074,6 +1074,8 @@ class ToolCallMessage(Vertical):
1074
1074
  """Whether the entire card body is collapsed (header remains visible)."""
1075
1075
  self._collapse_hint_widget: Static | None = None
1076
1076
  """Widget showing expand/collapse hint text."""
1077
+ self._final_duration_ms: int | None = None
1078
+ """Frozen duration at completion; prevents drift when re-rendering."""
1077
1079
  self._task_card_user_expanded: bool = False
1078
1080
  """If True, do not auto-collapse the task card (user expanded the body)."""
1079
1081
  self._task_activity_list_user_expanded: bool = False
@@ -1082,6 +1084,17 @@ class ToolCallMessage(Vertical):
1082
1084
  def _is_task_tool_card(self) -> bool:
1083
1085
  return _normalize_tool_name_for_arg_map(self._tool_name) == "task"
1084
1086
 
1087
+ def _has_whole_card_collapse_affordance(self) -> bool:
1088
+ """True when the card body can be collapsed/expanded.
1089
+
1090
+ Must stay aligned with :meth:`on_click` (whole-card toggle branch).
1091
+ """
1092
+ return bool(
1093
+ self._activity
1094
+ or (self._output or "").strip()
1095
+ or self._status in ("success", "error")
1096
+ )
1097
+
1085
1098
  def _maybe_auto_collapse_task_card(self) -> None:
1086
1099
  """Collapse task cards when activity rows exceed the shared threshold."""
1087
1100
  if not self._is_task_tool_card():
@@ -1584,10 +1597,9 @@ class ToolCallMessage(Vertical):
1584
1597
  colors = theme.get_theme_colors(self)
1585
1598
  gutter = f"{get_glyphs().output_prefix} "
1586
1599
  # Expand/collapse affordance: collapsed → show right arrow (can expand); expanded → show down arrow (can collapse).
1587
- has_collapsible = self._activity or (self._output or "").strip()
1588
1600
  g = get_glyphs()
1589
1601
  toggle_icon = ""
1590
- if has_collapsible:
1602
+ if self._has_whole_card_collapse_affordance():
1591
1603
  toggle_icon = f" {g.expand if self._card_collapsed else g.collapse}"
1592
1604
  line = f"{gutter}{frame} Running...{elapsed}{toggle_icon}"
1593
1605
  self._status_widget.update(Content.styled(line, colors.cognition))
@@ -1622,8 +1634,7 @@ class ToolCallMessage(Vertical):
1622
1634
  gutter = f"{get_glyphs().output_prefix} "
1623
1635
  line = f"{gutter}{line}"
1624
1636
  # Add expand/collapse icon at the end of status line
1625
- has_collapsible = self._activity or (self._output or "").strip()
1626
- if has_collapsible:
1637
+ if self._has_whole_card_collapse_affordance():
1627
1638
  icon = get_glyphs().expand if self._card_collapsed else get_glyphs().collapse
1628
1639
  line = f"{line} {icon}"
1629
1640
  w.update(Content(line))
@@ -1641,6 +1652,7 @@ class ToolCallMessage(Vertical):
1641
1652
  self._invalidate_output_render_cache()
1642
1653
  self._expanded = False
1643
1654
  duration_ms = self._duration_ms_since_start()
1655
+ self._final_duration_ms = duration_ms
1644
1656
  line = _TOOL_CARD_PRESENTATION.format_tool_result_status_line(
1645
1657
  self._tool_name,
1646
1658
  self._output,
@@ -1671,6 +1683,7 @@ class ToolCallMessage(Vertical):
1671
1683
  self._invalidate_output_render_cache()
1672
1684
  self._expanded = False
1673
1685
  duration_ms = self._duration_ms_since_start()
1686
+ self._final_duration_ms = duration_ms
1674
1687
  line = _TOOL_CARD_PRESENTATION.format_tool_result_status_line(
1675
1688
  self._tool_name,
1676
1689
  error,
@@ -1760,10 +1773,7 @@ class ToolCallMessage(Vertical):
1760
1773
  self._refresh_activity_display()
1761
1774
  return
1762
1775
  # Priority 2: Card-level collapse when there's content to collapse
1763
- has_collapsible_content = (
1764
- self._activity or (self._output or "").strip() or self._status in ("success", "error")
1765
- )
1766
- if has_collapsible_content:
1776
+ if self._has_whole_card_collapse_affordance():
1767
1777
  self.toggle_collapse()
1768
1778
  return
1769
1779
  # Priority 3: Toggle output expansion
@@ -1804,8 +1814,12 @@ class ToolCallMessage(Vertical):
1804
1814
  if self._status == "running":
1805
1815
  self._update_running_animation()
1806
1816
  elif self._status in ("success", "error") and self._result_summary_widget:
1807
- # Re-apply result summary with updated icon
1808
- duration_ms = self._duration_ms_since_start()
1817
+ # Re-apply result summary with updated icon using frozen duration
1818
+ duration_ms = (
1819
+ self._final_duration_ms
1820
+ if self._final_duration_ms is not None
1821
+ else self._duration_ms_since_start()
1822
+ )
1809
1823
  line = _TOOL_CARD_PRESENTATION.format_tool_result_status_line(
1810
1824
  self._tool_name,
1811
1825
  self._output,
@@ -3301,7 +3315,7 @@ class CognitionStepMessage(Vertical):
3301
3315
  self._detail_widget.display = False
3302
3316
 
3303
3317
 
3304
- class CognitionPlanReasonMessage(_TimestampClickMixin, Vertical):
3318
+ class CognitionReasonMessage(_TimestampClickMixin, Vertical):
3305
3319
  """Single card for plan assessment, plan reasoning, and next action (keep/new).
3306
3320
 
3307
3321
  Header uses the same cognition-colored label plus foreground body as ``CognitionStepMessage``.
@@ -3310,7 +3324,7 @@ class CognitionPlanReasonMessage(_TimestampClickMixin, Vertical):
3310
3324
  can_select = True
3311
3325
 
3312
3326
  DEFAULT_CSS = """
3313
- CognitionPlanReasonMessage {
3327
+ CognitionReasonMessage {
3314
3328
  height: auto;
3315
3329
  padding: 0 1;
3316
3330
  margin: 0 0 1 0;
@@ -3318,19 +3332,19 @@ class CognitionPlanReasonMessage(_TimestampClickMixin, Vertical):
3318
3332
  border-left: wide $cognition;
3319
3333
  }
3320
3334
 
3321
- CognitionPlanReasonMessage .cognition-plan-header {
3335
+ CognitionReasonMessage .cognition-plan-header {
3322
3336
  height: auto;
3323
3337
  margin: 0;
3324
3338
  color: $foreground;
3325
3339
  }
3326
3340
 
3327
- CognitionPlanReasonMessage .plan-section-line {
3341
+ CognitionReasonMessage .plan-section-line {
3328
3342
  height: auto;
3329
3343
  margin-left: 3;
3330
3344
  color: $text-muted;
3331
3345
  }
3332
3346
 
3333
- CognitionPlanReasonMessage:hover {
3347
+ CognitionReasonMessage:hover {
3334
3348
  border-left: wide $cognition-hover;
3335
3349
  }
3336
3350
  """
@@ -3361,11 +3375,15 @@ class CognitionPlanReasonMessage(_TimestampClickMixin, Vertical):
3361
3375
  self._next_action = next_action.strip()
3362
3376
  self._status = status
3363
3377
  self._iteration = iteration
3364
- self._plan_action = plan_action if plan_action in ("keep", "new") else "new"
3378
+ self._plan_action = plan_action if plan_action in ("keep", "new") else ""
3365
3379
  self._assessment_reasoning = assessment_reasoning.strip()
3366
3380
  self._plan_reasoning = plan_reasoning.strip()
3367
3381
 
3368
3382
  def _plan_header_content(self) -> Content:
3383
+ # Assess-only card: only assessment_reasoning populated
3384
+ if self._assessment_reasoning and not self._plan_reasoning and not self._next_action:
3385
+ return _assemble_card_header(self, "💭 ", self._assessment_reasoning)
3386
+
3369
3387
  # Concatenate plan_reasoning and next_action with proper separation
3370
3388
  parts: list[str] = []
3371
3389
  if self._plan_reasoning:
@@ -3437,7 +3455,7 @@ class _StepLineState:
3437
3455
  class CognitionGoalTreeMessage(_TimestampClickMixin, Vertical):
3438
3456
  """Two-level Goal → steps tree; one aggregate block updates in place.
3439
3457
 
3440
- Title line matches ``CognitionStepMessage`` / ``CognitionPlanReasonMessage``:
3458
+ Title line matches ``CognitionStepMessage`` / ``CognitionReasonMessage``:
3441
3459
  ``{prefix} 📍 …`` with optional ``· iter<=N`` when ``max_iterations`` is set.
3442
3460
  """
3443
3461
 
@@ -90,6 +90,8 @@ class WelcomeBanner(Static):
90
90
  self._failed = False
91
91
  self._failure_error: str = ""
92
92
  self._tip: str = random.choice(_TIPS) # noqa: S311
93
+ self._update_latest: str | None = None
94
+ """PyPI version string when an update is available; drives banner line only."""
93
95
 
94
96
  super().__init__(self._build_banner(), **kwargs)
95
97
 
@@ -124,6 +126,16 @@ class WelcomeBanner(Static):
124
126
  self._failure_error = error
125
127
  self.update(self._build_banner())
126
128
 
129
+ def set_update_notice(self, latest: str | None) -> None:
130
+ """Show or hide the \"update available\" line in the welcome area.
131
+
132
+ Args:
133
+ latest: Newer version from PyPI, or ``None`` to remove the line.
134
+ """
135
+ cleaned = str(latest).strip() if latest else ""
136
+ self._update_latest = cleaned or None
137
+ self.update(self._build_banner())
138
+
127
139
  def on_click(self, event: Click) -> None: # noqa: PLR6301 # Textual event handler
128
140
  """Open style-embedded hyperlinks on single click."""
129
141
  open_style_link(event)
@@ -186,6 +198,17 @@ class WelcomeBanner(Static):
186
198
  label = "MCP tool" if self._mcp_tool_count == 1 else "MCP tools"
187
199
  parts.append(f"Loaded {self._mcp_tool_count} {label}\n")
188
200
 
201
+ if self._update_latest and not self._failed:
202
+ from soothe_cli.tui.update_check import upgrade_command
203
+
204
+ cmd = upgrade_command()
205
+ update_line = (
206
+ f"Update available: v{self._update_latest} (current: v{__version__}). "
207
+ f"Run: {cmd} — or /auto-update\n"
208
+ )
209
+ update_style = "yellow" if ansi else colors.warning
210
+ parts.append((update_line, update_style))
211
+
189
212
  if tip_line is not None:
190
213
  parts.append((tip_line, "dim"))
191
214
 
File without changes
File without changes
File without changes