soothe-cli 0.4.7__tar.gz → 0.4.8__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 (122) hide show
  1. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/PKG-INFO +1 -1
  2. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/cli/execution/daemon.py +0 -2
  3. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/config/cli_config.py +0 -13
  4. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/shared/core/event_processor.py +2 -8
  5. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/shared/tools/message_processing.py +1 -1
  6. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/shared/tools/tool_formatters/file_ops.py +47 -20
  7. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/tui/_session_stats.py +6 -3
  8. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/tui/app.py +15 -11
  9. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/tui/app.tcss +15 -5
  10. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/tui/textual_adapter.py +736 -242
  11. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/tui/tool_display.py +96 -0
  12. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/tui/widgets/loading.py +51 -18
  13. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/tui/widgets/message_store.py +31 -1
  14. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/tui/widgets/messages.py +998 -96
  15. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/.gitignore +0 -0
  16. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/README.md +0 -0
  17. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/pyproject.toml +0 -0
  18. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/__init__.py +0 -0
  19. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/cli/__init__.py +0 -0
  20. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/cli/commands/__init__.py +0 -0
  21. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/cli/commands/autopilot_cmd.py +0 -0
  22. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/cli/commands/loop_cmd.py +0 -0
  23. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/cli/commands/run_cmd.py +0 -0
  24. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/cli/commands/thread_cmd.py +0 -0
  25. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/cli/execution/__init__.py +0 -0
  26. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/cli/execution/headless.py +0 -0
  27. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/cli/execution/headless_renderer.py +0 -0
  28. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/cli/execution/launcher.py +0 -0
  29. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/cli/main.py +0 -0
  30. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/cli/stream/__init__.py +0 -0
  31. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/cli/stream/context.py +0 -0
  32. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/cli/stream/display_line.py +0 -0
  33. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/cli/stream/formatter.py +0 -0
  34. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/cli/stream/pipeline.py +0 -0
  35. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/cli/stream/task_scope.py +0 -0
  36. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/config/__init__.py +0 -0
  37. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/plan/__init__.py +0 -0
  38. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/plan/rich_tree.py +0 -0
  39. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/shared/__init__.py +0 -0
  40. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/shared/commands/__init__.py +0 -0
  41. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/shared/commands/command_router.py +0 -0
  42. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/shared/commands/slash_commands.py +0 -0
  43. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/shared/commands/subagent_routing.py +0 -0
  44. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/shared/config_loader.py +0 -0
  45. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/shared/core/__init__.py +0 -0
  46. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/shared/core/presentation_engine.py +0 -0
  47. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/shared/core/processor_state.py +0 -0
  48. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/shared/core/renderer_protocol.py +0 -0
  49. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/shared/events/__init__.py +0 -0
  50. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/shared/events/display_policy.py +0 -0
  51. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/shared/events/essential_events.py +0 -0
  52. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/shared/events/explore_task_display.py +0 -0
  53. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/shared/events/stream_accumulator.py +0 -0
  54. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/shared/events/tui_trace_log.py +0 -0
  55. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/shared/rendering/__init__.py +0 -0
  56. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/shared/rendering/async_renderer_protocol.py +0 -0
  57. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/shared/rendering/renderer_base.py +0 -0
  58. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/shared/tools/__init__.py +0 -0
  59. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/shared/tools/_utils.py +0 -0
  60. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/shared/tools/rendering.py +0 -0
  61. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/shared/tools/tool_call_resolution.py +0 -0
  62. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/shared/tools/tool_card_payload.py +0 -0
  63. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/shared/tools/tool_card_visibility.py +0 -0
  64. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/shared/tools/tool_formatters/__init__.py +0 -0
  65. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/shared/tools/tool_formatters/base.py +0 -0
  66. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/shared/tools/tool_formatters/execution.py +0 -0
  67. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/shared/tools/tool_formatters/fallback.py +0 -0
  68. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/shared/tools/tool_formatters/goal_formatter.py +0 -0
  69. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/shared/tools/tool_formatters/media.py +0 -0
  70. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/shared/tools/tool_formatters/structured.py +0 -0
  71. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/shared/tools/tool_formatters/subagent.py +0 -0
  72. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/shared/tools/tool_formatters/web.py +0 -0
  73. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/shared/tools/tool_message_format.py +0 -0
  74. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/shared/tools/tool_output_formatter.py +0 -0
  75. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/tui/__init__.py +0 -0
  76. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/tui/_ask_user_types.py +0 -0
  77. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/tui/_cli_context.py +0 -0
  78. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/tui/_env_vars.py +0 -0
  79. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/tui/_version.py +0 -0
  80. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/tui/command_registry.py +0 -0
  81. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/tui/config.py +0 -0
  82. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/tui/daemon_session.py +0 -0
  83. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/tui/file_ops.py +0 -0
  84. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/tui/formatting.py +0 -0
  85. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/tui/hooks.py +0 -0
  86. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/tui/input.py +0 -0
  87. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/tui/media_utils.py +0 -0
  88. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/tui/message_display_filter.py +0 -0
  89. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/tui/model_config.py +0 -0
  90. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/tui/output.py +0 -0
  91. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/tui/preview_limits.py +0 -0
  92. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/tui/project_utils.py +0 -0
  93. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/tui/sessions.py +0 -0
  94. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/tui/skills/__init__.py +0 -0
  95. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/tui/skills/invocation.py +0 -0
  96. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/tui/skills/load.py +0 -0
  97. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/tui/theme.py +0 -0
  98. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/tui/unicode_security.py +0 -0
  99. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/tui/update_check.py +0 -0
  100. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/tui/widgets/__init__.py +0 -0
  101. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/tui/widgets/_links.py +0 -0
  102. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/tui/widgets/approval.py +0 -0
  103. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/tui/widgets/ask_user.py +0 -0
  104. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/tui/widgets/autocomplete.py +0 -0
  105. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/tui/widgets/autopilot_dashboard.py +0 -0
  106. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/tui/widgets/autopilot_screen.py +0 -0
  107. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/tui/widgets/chat_input.py +0 -0
  108. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/tui/widgets/clipboard.py +0 -0
  109. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/tui/widgets/diff.py +0 -0
  110. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/tui/widgets/editor.py +0 -0
  111. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/tui/widgets/history.py +0 -0
  112. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/tui/widgets/loop_selector.py +0 -0
  113. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/tui/widgets/mcp_viewer.py +0 -0
  114. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/tui/widgets/model_selector.py +0 -0
  115. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/tui/widgets/notification_settings.py +0 -0
  116. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/tui/widgets/status.py +0 -0
  117. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/tui/widgets/theme_selector.py +0 -0
  118. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/tui/widgets/thread_selector.py +0 -0
  119. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/tui/widgets/tool_renderers.py +0 -0
  120. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/tui/widgets/tool_widgets.py +0 -0
  121. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/tui/widgets/tools.py +0 -0
  122. {soothe_cli-0.4.7 → soothe_cli-0.4.8}/src/soothe_cli/tui/widgets/welcome.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: soothe-cli
3
- Version: 0.4.7
3
+ Version: 0.4.8
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
@@ -47,7 +47,6 @@ async def run_headless_via_daemon(
47
47
 
48
48
  ws_url = websocket_url_from_config(cfg)
49
49
  client = WebSocketClient(url=ws_url)
50
- final_output_mode = getattr(cfg, "final_output_mode", "streaming")
51
50
 
52
51
  try:
53
52
  await connect_websocket_with_retries(client)
@@ -85,7 +84,6 @@ async def run_headless_via_daemon(
85
84
  renderer = HeadlessCliRenderer()
86
85
  processor = EventProcessor(
87
86
  renderer,
88
- final_output_mode=final_output_mode,
89
87
  presentation_engine=presentation,
90
88
  headless_output=True,
91
89
  )
@@ -27,8 +27,6 @@ class CLIConfig:
27
27
  # logging_level: DEBUG/INFO/… for ~/.soothe/logs/soothe-cli.log; None = default INFO.
28
28
  logging_level: str | None = None
29
29
 
30
- final_output_mode: str = "streaming"
31
-
32
30
  # Output streaming overrides (RFC-614)
33
31
  output_streaming_enabled: bool | None = None
34
32
  """Override daemon streaming enabled setting."""
@@ -107,26 +105,15 @@ class CLIConfig:
107
105
  transports = daemon_section.get("transports", {})
108
106
  websocket = transports.get("websocket", {})
109
107
  websocket_legacy = data.get("websocket", {})
110
- ui_section = data.get("ui", {})
111
108
 
112
109
  raw_level = data.get("logging_level")
113
110
  if raw_level is not None and not isinstance(raw_level, str):
114
111
  raw_level = None
115
112
 
116
- raw_final_output_mode = data.get("final_output_mode")
117
- if raw_final_output_mode is None and isinstance(ui_section, dict):
118
- raw_final_output_mode = ui_section.get("final_output_mode")
119
- if not isinstance(raw_final_output_mode, str):
120
- raw_final_output_mode = "streaming"
121
- final_output_mode = raw_final_output_mode.strip().lower()
122
- if final_output_mode not in {"streaming", "batch"}:
123
- final_output_mode = "streaming"
124
-
125
113
  return cls(
126
114
  daemon_host=websocket.get("host", websocket_legacy.get("host", "127.0.0.1")),
127
115
  daemon_port=websocket.get("port", websocket_legacy.get("port", 8765)),
128
116
  logging_level=raw_level,
129
- final_output_mode=final_output_mode,
130
117
  soothe_home=Path(data.get("home", str(Path.home() / ".soothe"))),
131
118
  )
132
119
 
@@ -82,7 +82,6 @@ class EventProcessor:
82
82
  self,
83
83
  renderer: RendererProtocol,
84
84
  *,
85
- final_output_mode: str = "streaming",
86
85
  presentation_engine: PresentationEngine | None = None,
87
86
  tui_debug: bool = False,
88
87
  headless_output: bool = False,
@@ -98,9 +97,6 @@ class EventProcessor:
98
97
  """
99
98
  self._renderer = renderer
100
99
  self._headless_output = headless_output
101
- self._final_output_mode = (
102
- final_output_mode if final_output_mode in {"streaming", "batch"} else "streaming"
103
- )
104
100
  self._tui_debug = tui_debug
105
101
 
106
102
  rebind = getattr(renderer, "_rebind_presentation", None)
@@ -1103,12 +1099,10 @@ class EventProcessor:
1103
1099
  Dict with enabled, mode, and synthesis_streaming fields.
1104
1100
  """
1105
1101
  # Use defaults - streaming is enabled by default per RFC-614
1106
- # final_output_mode controls batch/streaming display mode
1102
+ # Always use streaming mode
1107
1103
  config = {
1108
1104
  "enabled": True,
1109
- "mode": self._final_output_mode
1110
- if self._final_output_mode in {"streaming", "batch"}
1111
- else "streaming",
1105
+ "mode": "streaming",
1112
1106
  "synthesis_streaming": True,
1113
1107
  }
1114
1108
 
@@ -392,7 +392,7 @@ def format_tool_call_args(tool_name: str, tool_call: dict[str, Any]) -> str:
392
392
  def _is_path_arg_name(key: str) -> bool:
393
393
  return _PATH_ARG_PATTERN.match(key) is not None
394
394
 
395
- max_value_length = 40 # Max length for displayed values
395
+ max_value_length = 50 # Max length for displayed values
396
396
 
397
397
  def _display_path_value(raw: str) -> str:
398
398
  out = convert_and_abbreviate_path(raw)
@@ -13,8 +13,8 @@ from soothe_cli.shared.tools.tool_output_formatter import ToolBrief
13
13
  class FileOpsFormatter(BaseFormatter):
14
14
  """Formatter for file operation tools.
15
15
 
16
- Handles: read_file, write_file, delete_file, list_files, search_files, glob,
17
- grep, ls, file_info
16
+ Handles: read_file, write_file, edit_file, delete_file, list_files, search_files,
17
+ glob, grep, ls, file_info, edit_file_lines, insert_lines, delete_lines, apply_diff
18
18
 
19
19
  Provides semantic summaries with size, line count, and item count metrics.
20
20
  """
@@ -46,6 +46,26 @@ class FileOpsFormatter(BaseFormatter):
46
46
  return self._format_read_file(result)
47
47
  if normalized == "write_file":
48
48
  return self._format_write_file(result)
49
+ if normalized == "edit_file":
50
+ return self._format_file_mutation(
51
+ result, success_summary="Edited file", failure_summary="Edit failed"
52
+ )
53
+ if normalized == "edit_file_lines":
54
+ return self._format_file_mutation(
55
+ result, success_summary="Updated file", failure_summary="Line edit failed"
56
+ )
57
+ if normalized == "insert_lines":
58
+ return self._format_file_mutation(
59
+ result, success_summary="Inserted lines", failure_summary="Insert failed"
60
+ )
61
+ if normalized == "delete_lines":
62
+ return self._format_file_mutation(
63
+ result, success_summary="Deleted lines", failure_summary="Delete lines failed"
64
+ )
65
+ if normalized == "apply_diff":
66
+ return self._format_file_mutation(
67
+ result, success_summary="Applied patch", failure_summary="Patch failed"
68
+ )
49
69
  if normalized == "delete_file":
50
70
  return self._format_delete_file(result)
51
71
  if normalized in ("list_files", "ls"):
@@ -143,6 +163,28 @@ class FileOpsFormatter(BaseFormatter):
143
163
  metrics={"size_bytes": size_bytes, "lines": lines},
144
164
  )
145
165
 
166
+ def _format_file_mutation(
167
+ self,
168
+ result: str,
169
+ *,
170
+ success_summary: str,
171
+ failure_summary: str,
172
+ ) -> ToolBrief:
173
+ """Format success/error for tools that mutate file content (write, edit, patch, …)."""
174
+ if text_looks_like_error(result):
175
+ return ToolBrief(
176
+ icon="✗",
177
+ summary=failure_summary,
178
+ detail=self._truncate_text(result, 80),
179
+ metrics={"error": True},
180
+ )
181
+ return ToolBrief(
182
+ icon="✓",
183
+ summary=success_summary,
184
+ detail=None,
185
+ metrics={},
186
+ )
187
+
146
188
  def _format_write_file(self, result: str) -> ToolBrief:
147
189
  """Format write_file result.
148
190
 
@@ -157,25 +199,10 @@ class FileOpsFormatter(BaseFormatter):
157
199
  Example:
158
200
  >>> brief = formatter._format_write_file("Successfully wrote to file")
159
201
  >>> brief.summary
160
- 'Wrote 0 B'
202
+ 'Wrote file'
161
203
  """
162
- # Check for error
163
- if text_looks_like_error(result):
164
- return ToolBrief(
165
- icon="✗",
166
- summary="Write failed",
167
- detail=self._truncate_text(result, 80),
168
- metrics={"error": True},
169
- )
170
-
171
- # Try to extract size from result (if available)
172
- # Common patterns: "Wrote X bytes", "Successfully wrote X"
173
- # For now, show simple success
174
- return ToolBrief(
175
- icon="✓",
176
- summary="Wrote file",
177
- detail=None,
178
- metrics={},
204
+ return self._format_file_mutation(
205
+ result, success_summary="Wrote file", failure_summary="Write failed"
179
206
  )
180
207
 
181
208
  def _format_delete_file(self, result: str) -> ToolBrief:
@@ -9,10 +9,13 @@ config, no widget imports) so that `app.py` can import `SessionStats` and
9
9
  from __future__ import annotations
10
10
 
11
11
  from dataclasses import dataclass, field
12
- from typing import Literal
13
12
 
14
- SpinnerStatus = Literal["Thinking", "Offloading"] | None
15
- """Valid spinner display states, or `None` to hide."""
13
+ SpinnerStatus = str | None
14
+ """Spinner line label, or `None` to hide.
15
+
16
+ Common values include ``Thinking``, ``Offloading``, ``Writing`` (assistant streaming),
17
+ ``Tools`` (tool execution), and ``Synthesizing`` (goal-completion stream).
18
+ """
16
19
 
17
20
 
18
21
  @dataclass
@@ -21,7 +21,7 @@ from typing import TYPE_CHECKING, Any, ClassVar, Literal
21
21
 
22
22
  from textual.app import App, ScreenStackError
23
23
  from textual.binding import Binding, BindingType
24
- from textual.containers import Container, VerticalScroll
24
+ from textual.containers import Container, Vertical, VerticalScroll
25
25
  from textual.content import Content
26
26
  from textual.css.query import NoMatches
27
27
  from textual.message import Message
@@ -780,15 +780,16 @@ class SootheApp(App):
780
780
  # Main chat area with scrollable messages
781
781
  # VerticalScroll tracks user scroll intent for better auto-scroll behavior
782
782
  with VerticalScroll(id="chat"):
783
- yield WelcomeBanner(
784
- thread_id=self._lc_loop_id,
785
- mcp_tool_count=self._mcp_tool_count,
786
- connecting=self._connecting,
787
- resuming=self._resume_thread_intent is not None,
788
- local_server=self._server_kwargs is not None,
789
- id="welcome-banner",
790
- )
791
- yield Container(id="messages")
783
+ with Vertical(id="chat-body"):
784
+ yield WelcomeBanner(
785
+ thread_id=self._lc_loop_id,
786
+ mcp_tool_count=self._mcp_tool_count,
787
+ connecting=self._connecting,
788
+ resuming=self._resume_thread_intent is not None,
789
+ local_server=self._server_kwargs is not None,
790
+ id="welcome-banner",
791
+ )
792
+ yield Container(id="messages")
792
793
  with Container(id="bottom-app-container"):
793
794
  yield Container(id="thinking-status")
794
795
  yield ChatInput(
@@ -1965,9 +1966,12 @@ class SootheApp(App):
1965
1966
 
1966
1967
  if self._loading_widget is None:
1967
1968
  # Create new
1968
- self._loading_widget = LoadingWidget(status)
1969
+ turn_mono = self._inflight_turn_start if self._agent_running else None
1970
+ self._loading_widget = LoadingWidget(status, turn_start_mono=turn_mono)
1969
1971
  await thinking_status.mount(self._loading_widget)
1970
1972
  else:
1973
+ if self._agent_running:
1974
+ self._loading_widget.set_turn_start_mono(self._inflight_turn_start)
1971
1975
  # Update existing
1972
1976
  self._loading_widget.set_status(status)
1973
1977
  # NOTE: Don't call anchor() here - it would re-anchor and drag user back
@@ -16,14 +16,24 @@ Screen {
16
16
  /* Chat area - main scrollable messages area */
17
17
  #chat {
18
18
  height: 1fr;
19
- padding: 1 2;
19
+ padding: 0 2;
20
20
  background: $background;
21
21
  }
22
22
 
23
+ /* Fills at least the scroll viewport; bottom-aligns welcome + messages when the
24
+ thread is short so dead space sits above the banner instead of above the input. */
25
+ #chat-body {
26
+ width: 1fr;
27
+ min-height: 100%;
28
+ height: auto;
29
+ layout: vertical;
30
+ align-vertical: bottom;
31
+ }
32
+
23
33
  /* Welcome banner */
24
34
  #welcome-banner {
25
35
  height: auto;
26
- margin-bottom: 1;
36
+ margin-bottom: 0;
27
37
  }
28
38
 
29
39
  /* Messages area — uses undocumented "stream" layout (Textual ≥5.2.0) for
@@ -36,14 +46,14 @@ Screen {
36
46
  /* Bottom app container - holds ChatInput (now inside scroll) */
37
47
  #bottom-app-container {
38
48
  height: auto;
39
- margin-top: 1;
49
+ margin-top: 0;
40
50
  padding: 0 1;
41
51
  }
42
52
 
43
53
  /* Sticky thinking row directly above input box */
44
54
  #thinking-status {
45
55
  height: auto;
46
- min-height: 1;
56
+ min-height: 0;
47
57
  margin-bottom: 0;
48
58
  }
49
59
 
@@ -175,7 +185,7 @@ QueuedUserMessage.-ascii {
175
185
  }
176
186
 
177
187
  ToolCallMessage.-ascii {
178
- border-left: ascii $panel;
188
+ border-left: ascii $primary;
179
189
  }
180
190
 
181
191
  ToolCallMessage.-ascii:hover {