soothe-cli 0.4.6__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 (127) hide show
  1. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/PKG-INFO +1 -1
  2. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/cli/execution/daemon.py +2 -4
  3. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/cli/execution/headless_renderer.py +1 -1
  4. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/cli/stream/pipeline.py +2 -2
  5. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/config/cli_config.py +0 -13
  6. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/shared/core/event_processor.py +2 -8
  7. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/shared/core/processor_state.py +2 -4
  8. soothe_cli-0.4.8/src/soothe_cli/shared/tools/__init__.py +96 -0
  9. soothe_cli-0.4.8/src/soothe_cli/shared/tools/_utils.py +30 -0
  10. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/shared/tools/message_processing.py +1 -1
  11. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/shared/tools/tool_card_payload.py +2 -2
  12. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/shared/tools/tool_formatters/execution.py +8 -14
  13. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/shared/tools/tool_formatters/fallback.py +2 -2
  14. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/shared/tools/tool_formatters/file_ops.py +57 -27
  15. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/shared/tools/tool_formatters/goal_formatter.py +6 -5
  16. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/shared/tools/tool_formatters/media.py +5 -4
  17. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/shared/tools/tool_formatters/subagent.py +2 -2
  18. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/shared/tools/tool_formatters/web.py +4 -3
  19. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/shared/tools/tool_output_formatter.py +3 -2
  20. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/tui/_session_stats.py +6 -3
  21. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/tui/app.py +20 -16
  22. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/tui/app.tcss +15 -5
  23. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/tui/textual_adapter.py +751 -253
  24. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/tui/tool_display.py +97 -1
  25. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/tui/widgets/loading.py +51 -18
  26. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/tui/widgets/message_store.py +31 -1
  27. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/tui/widgets/messages.py +1000 -98
  28. soothe_cli-0.4.6/src/soothe_cli/shared/presentation_engine.py +0 -5
  29. soothe_cli-0.4.6/src/soothe_cli/shared/renderer_base.py +0 -5
  30. soothe_cli-0.4.6/src/soothe_cli/shared/stream_accumulator.py +0 -13
  31. soothe_cli-0.4.6/src/soothe_cli/shared/subagent_routing.py +0 -15
  32. soothe_cli-0.4.6/src/soothe_cli/shared/tools/__init__.py +0 -3
  33. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/.gitignore +0 -0
  34. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/README.md +0 -0
  35. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/pyproject.toml +0 -0
  36. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/__init__.py +0 -0
  37. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/cli/__init__.py +0 -0
  38. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/cli/commands/__init__.py +0 -0
  39. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/cli/commands/autopilot_cmd.py +0 -0
  40. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/cli/commands/loop_cmd.py +0 -0
  41. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/cli/commands/run_cmd.py +0 -0
  42. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/cli/commands/thread_cmd.py +0 -0
  43. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/cli/execution/__init__.py +0 -0
  44. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/cli/execution/headless.py +0 -0
  45. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/cli/execution/launcher.py +0 -0
  46. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/cli/main.py +0 -0
  47. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/cli/stream/__init__.py +0 -0
  48. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/cli/stream/context.py +0 -0
  49. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/cli/stream/display_line.py +0 -0
  50. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/cli/stream/formatter.py +0 -0
  51. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/cli/stream/task_scope.py +0 -0
  52. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/config/__init__.py +0 -0
  53. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/plan/__init__.py +0 -0
  54. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/plan/rich_tree.py +0 -0
  55. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/shared/__init__.py +0 -0
  56. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/shared/commands/__init__.py +0 -0
  57. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/shared/commands/command_router.py +0 -0
  58. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/shared/commands/slash_commands.py +0 -0
  59. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/shared/commands/subagent_routing.py +0 -0
  60. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/shared/config_loader.py +0 -0
  61. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/shared/core/__init__.py +0 -0
  62. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/shared/core/presentation_engine.py +0 -0
  63. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/shared/core/renderer_protocol.py +0 -0
  64. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/shared/events/__init__.py +0 -0
  65. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/shared/events/display_policy.py +0 -0
  66. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/shared/events/essential_events.py +0 -0
  67. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/shared/events/explore_task_display.py +0 -0
  68. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/shared/events/stream_accumulator.py +0 -0
  69. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/shared/events/tui_trace_log.py +0 -0
  70. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/shared/rendering/__init__.py +0 -0
  71. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/shared/rendering/async_renderer_protocol.py +0 -0
  72. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/shared/rendering/renderer_base.py +0 -0
  73. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/shared/tools/rendering.py +0 -0
  74. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/shared/tools/tool_call_resolution.py +0 -0
  75. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/shared/tools/tool_card_visibility.py +0 -0
  76. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/shared/tools/tool_formatters/__init__.py +0 -0
  77. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/shared/tools/tool_formatters/base.py +0 -0
  78. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/shared/tools/tool_formatters/structured.py +0 -0
  79. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/shared/tools/tool_message_format.py +0 -0
  80. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/tui/__init__.py +0 -0
  81. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/tui/_ask_user_types.py +0 -0
  82. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/tui/_cli_context.py +0 -0
  83. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/tui/_env_vars.py +0 -0
  84. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/tui/_version.py +0 -0
  85. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/tui/command_registry.py +0 -0
  86. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/tui/config.py +0 -0
  87. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/tui/daemon_session.py +0 -0
  88. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/tui/file_ops.py +0 -0
  89. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/tui/formatting.py +0 -0
  90. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/tui/hooks.py +0 -0
  91. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/tui/input.py +0 -0
  92. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/tui/media_utils.py +0 -0
  93. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/tui/message_display_filter.py +0 -0
  94. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/tui/model_config.py +0 -0
  95. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/tui/output.py +0 -0
  96. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/tui/preview_limits.py +0 -0
  97. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/tui/project_utils.py +0 -0
  98. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/tui/sessions.py +0 -0
  99. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/tui/skills/__init__.py +0 -0
  100. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/tui/skills/invocation.py +0 -0
  101. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/tui/skills/load.py +0 -0
  102. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/tui/theme.py +0 -0
  103. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/tui/unicode_security.py +0 -0
  104. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/tui/update_check.py +0 -0
  105. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/tui/widgets/__init__.py +0 -0
  106. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/tui/widgets/_links.py +0 -0
  107. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/tui/widgets/approval.py +0 -0
  108. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/tui/widgets/ask_user.py +0 -0
  109. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/tui/widgets/autocomplete.py +0 -0
  110. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/tui/widgets/autopilot_dashboard.py +0 -0
  111. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/tui/widgets/autopilot_screen.py +0 -0
  112. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/tui/widgets/chat_input.py +0 -0
  113. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/tui/widgets/clipboard.py +0 -0
  114. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/tui/widgets/diff.py +0 -0
  115. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/tui/widgets/editor.py +0 -0
  116. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/tui/widgets/history.py +0 -0
  117. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/tui/widgets/loop_selector.py +0 -0
  118. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/tui/widgets/mcp_viewer.py +0 -0
  119. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/tui/widgets/model_selector.py +0 -0
  120. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/tui/widgets/notification_settings.py +0 -0
  121. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/tui/widgets/status.py +0 -0
  122. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/tui/widgets/theme_selector.py +0 -0
  123. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/tui/widgets/thread_selector.py +0 -0
  124. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/tui/widgets/tool_renderers.py +0 -0
  125. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/tui/widgets/tool_widgets.py +0 -0
  126. {soothe_cli-0.4.6 → soothe_cli-0.4.8}/src/soothe_cli/tui/widgets/tools.py +0 -0
  127. {soothe_cli-0.4.6 → 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.6
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
@@ -20,8 +20,8 @@ from soothe_sdk.client import (
20
20
 
21
21
  from soothe_cli.cli.execution.headless_renderer import HeadlessCliRenderer
22
22
  from soothe_cli.shared import EventProcessor
23
- from soothe_cli.shared.presentation_engine import PresentationEngine
24
- from soothe_cli.shared.subagent_routing import parse_subagent_from_input
23
+ from soothe_cli.shared.commands.subagent_routing import parse_subagent_from_input
24
+ from soothe_cli.shared.core.presentation_engine import PresentationEngine
25
25
 
26
26
  logger = logging.getLogger(__name__)
27
27
 
@@ -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
  )
@@ -12,7 +12,7 @@ from typing import Any
12
12
 
13
13
  from rich.console import Console
14
14
 
15
- from soothe_cli.shared.renderer_base import RendererBase
15
+ from soothe_cli.shared.rendering.renderer_base import RendererBase
16
16
 
17
17
 
18
18
  class HeadlessCliRenderer(RendererBase):
@@ -30,13 +30,13 @@ from soothe_cli.cli.stream.formatter import (
30
30
  format_subagent_done,
31
31
  format_subagent_milestone,
32
32
  )
33
- from soothe_cli.shared.essential_events import (
33
+ from soothe_cli.shared.core.presentation_engine import PresentationEngine
34
+ from soothe_cli.shared.events.essential_events import (
34
35
  LOOP_REASON_EVENT_TYPE,
35
36
  is_goal_start_event_type,
36
37
  is_step_complete_event_type,
37
38
  is_step_start_event_type,
38
39
  )
39
- from soothe_cli.shared.presentation_engine import PresentationEngine
40
40
 
41
41
  logger = logging.getLogger(__name__)
42
42
 
@@ -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
 
@@ -13,7 +13,7 @@ from typing import TYPE_CHECKING, Any
13
13
  if TYPE_CHECKING:
14
14
  from soothe_sdk.client.schemas import Plan
15
15
 
16
- from soothe_cli.shared.events.stream_accumulator import StreamingTextAccumulator
16
+ from soothe_cli.shared.events.stream_accumulator import StreamingTextAccumulator
17
17
 
18
18
 
19
19
  @dataclass
@@ -56,9 +56,7 @@ class ProcessorState:
56
56
 
57
57
  # Unified streaming text accumulator (RFC-614)
58
58
  streaming_accumulator: StreamingTextAccumulator = field(
59
- default_factory=lambda: __import__(
60
- "soothe_cli.shared.stream_accumulator", fromlist=["StreamingTextAccumulator"]
61
- ).StreamingTextAccumulator()
59
+ default_factory=StreamingTextAccumulator
62
60
  )
63
61
  """Unified streaming text accumulator with namespace isolation."""
64
62
 
@@ -0,0 +1,96 @@
1
+ """Tool call/result handling utilities."""
2
+
3
+ from soothe_cli.shared.tools.message_processing import (
4
+ accumulate_tool_call_chunks,
5
+ coerce_tool_call_args_to_dict,
6
+ extract_tool_args_dict,
7
+ extract_tool_brief,
8
+ finalize_pending_tool_call,
9
+ format_tool_call_args,
10
+ normalize_tool_calls_list,
11
+ strip_internal_tags,
12
+ tool_calls_have_any_arg_dict,
13
+ try_parse_pending_tool_call_args,
14
+ )
15
+ from soothe_cli.shared.tools.rendering import update_name_map_from_tool_calls
16
+ from soothe_cli.shared.tools.tool_call_resolution import (
17
+ build_streaming_args_overlay,
18
+ infer_tool_name_from_call_id,
19
+ materialize_ai_blocks_with_resolved_tools,
20
+ tool_args_meaningful,
21
+ )
22
+ from soothe_cli.shared.tools.tool_card_payload import (
23
+ ToolResultCardPayload,
24
+ extract_tool_result_card_payload,
25
+ infer_tool_output_suggests_error,
26
+ )
27
+ from soothe_cli.shared.tools.tool_card_visibility import (
28
+ should_elide_completed_tool_call_message,
29
+ should_elide_stream_tool_card_mount,
30
+ should_elide_tool_card_no_info,
31
+ )
32
+ from soothe_cli.shared.tools.tool_formatters import (
33
+ BaseFormatter,
34
+ ExecutionFormatter,
35
+ FallbackFormatter,
36
+ FileOpsFormatter,
37
+ GoalFormatter,
38
+ MediaFormatter,
39
+ StructuredFormatter,
40
+ SubagentFormatter,
41
+ WebFormatter,
42
+ )
43
+ from soothe_cli.shared.tools.tool_message_format import (
44
+ format_content_block_for_tool_display,
45
+ format_tool_message_content,
46
+ run_python_envelope_indicates_failure,
47
+ try_parse_run_python_result_envelope,
48
+ )
49
+ from soothe_cli.shared.tools.tool_output_formatter import ToolBrief, ToolOutputFormatter
50
+
51
+ __all__ = [
52
+ # Message processing
53
+ "accumulate_tool_call_chunks",
54
+ "coerce_tool_call_args_to_dict",
55
+ "extract_tool_args_dict",
56
+ "extract_tool_brief",
57
+ "finalize_pending_tool_call",
58
+ "format_tool_call_args",
59
+ "normalize_tool_calls_list",
60
+ "strip_internal_tags",
61
+ "tool_calls_have_any_arg_dict",
62
+ "try_parse_pending_tool_call_args",
63
+ # Rendering
64
+ "update_name_map_from_tool_calls",
65
+ # Tool call resolution
66
+ "build_streaming_args_overlay",
67
+ "infer_tool_name_from_call_id",
68
+ "materialize_ai_blocks_with_resolved_tools",
69
+ "tool_args_meaningful",
70
+ # Tool card payload
71
+ "ToolResultCardPayload",
72
+ "extract_tool_result_card_payload",
73
+ "infer_tool_output_suggests_error",
74
+ # Tool card visibility
75
+ "should_elide_completed_tool_call_message",
76
+ "should_elide_stream_tool_card_mount",
77
+ "should_elide_tool_card_no_info",
78
+ # Tool message format
79
+ "format_content_block_for_tool_display",
80
+ "format_tool_message_content",
81
+ "run_python_envelope_indicates_failure",
82
+ "try_parse_run_python_result_envelope",
83
+ # Formatter
84
+ "ToolBrief",
85
+ "ToolOutputFormatter",
86
+ # Formatters
87
+ "BaseFormatter",
88
+ "ExecutionFormatter",
89
+ "FallbackFormatter",
90
+ "FileOpsFormatter",
91
+ "GoalFormatter",
92
+ "MediaFormatter",
93
+ "SubagentFormatter",
94
+ "StructuredFormatter",
95
+ "WebFormatter",
96
+ ]
@@ -0,0 +1,30 @@
1
+ """Internal utilities for tool formatting (not part of public API)."""
2
+
3
+
4
+ def normalize_tool_name(tool_name: str) -> str:
5
+ """Normalize tool name to snake_case for comparison and lookup.
6
+
7
+ Args:
8
+ tool_name: Raw tool name (may contain dashes or spaces).
9
+
10
+ Returns:
11
+ Lowercase snake_case name.
12
+ """
13
+ return tool_name.lower().replace("-", "_").replace(" ", "_")
14
+
15
+
16
+ def text_looks_like_error(text: str) -> bool:
17
+ """Return True if text content suggests a tool failure.
18
+
19
+ Checks for common error indicator substrings.
20
+
21
+ Args:
22
+ text: Tool output text to inspect.
23
+
24
+ Returns:
25
+ True if any error indicator is found.
26
+ """
27
+ if not text:
28
+ return False
29
+ lowered = text.lower()
30
+ return any(indicator in lowered for indicator in ("error", "failed", "exception", "traceback"))
@@ -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,6 +13,7 @@ from collections.abc import Mapping
13
13
  from dataclasses import dataclass
14
14
  from typing import Any
15
15
 
16
+ from soothe_cli.shared.tools._utils import text_looks_like_error
16
17
  from soothe_cli.shared.tools.tool_call_resolution import infer_tool_name_from_call_id
17
18
  from soothe_cli.shared.tools.tool_message_format import (
18
19
  format_tool_message_content,
@@ -38,8 +39,7 @@ def infer_tool_output_suggests_error(output_display: str, _tool_name: str = "")
38
39
 
39
40
  if not output_display:
40
41
  return False
41
- lowered = output_display.lower()
42
- return any(indicator in lowered for indicator in ("error", "failed", "exception", "traceback"))
42
+ return text_looks_like_error(output_display)
43
43
 
44
44
 
45
45
  @dataclass(frozen=True, slots=True)
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  from typing import Any
6
6
 
7
+ from soothe_cli.shared.tools._utils import normalize_tool_name, text_looks_like_error
7
8
  from soothe_cli.shared.tools.tool_formatters.base import BaseFormatter
8
9
  from soothe_cli.shared.tools.tool_message_format import try_parse_run_python_result_envelope
9
10
  from soothe_cli.shared.tools.tool_output_formatter import ToolBrief
@@ -37,10 +38,11 @@ class ExecutionFormatter(BaseFormatter):
37
38
  '✓ Done'
38
39
  """
39
40
  # Normalize tool name
40
- normalized = tool_name.lower().replace("-", "_").replace(" ", "_")
41
+ normalized = normalize_tool_name(tool_name)
41
42
 
42
43
  # Route to specific formatter
43
- if normalized == "run_command":
44
+ # execute/shell/bash are aliases of run_command (per TOOL_REGISTRY)
45
+ if normalized in ("run_command", "execute", "shell", "bash"):
44
46
  return self._format_run_command(result)
45
47
  if normalized == "run_python":
46
48
  return self._format_run_python(result)
@@ -83,9 +85,7 @@ class ExecutionFormatter(BaseFormatter):
83
85
  )
84
86
 
85
87
  # Check for other error patterns
86
- error_indicators = ["failed:", "exception:", "traceback", "command not found"]
87
- result_lower = result.lower()
88
- if any(indicator in result_lower for indicator in error_indicators):
88
+ if text_looks_like_error(result):
89
89
  # Extract first line as error message
90
90
  first_line = result.partition("\n")[0].strip()
91
91
  return ToolBrief(
@@ -170,13 +170,7 @@ class ExecutionFormatter(BaseFormatter):
170
170
 
171
171
  # Handle string result (fallback — not the standard run_python JSON envelope)
172
172
  if isinstance(result, str):
173
- lowered = result.lower()
174
- if (
175
- "traceback" in lowered
176
- or "exception:" in lowered
177
- or "failed:" in lowered
178
- or result.startswith("Error:")
179
- ):
173
+ if text_looks_like_error(result) or result.startswith("Error:"):
180
174
  return ToolBrief(
181
175
  icon="✗",
182
176
  summary="Execution failed",
@@ -240,7 +234,7 @@ class ExecutionFormatter(BaseFormatter):
240
234
 
241
235
  # Handle string result (fallback)
242
236
  if isinstance(result, str):
243
- if "error" in result.lower() or "failed" in result.lower():
237
+ if text_looks_like_error(result):
244
238
  return ToolBrief(
245
239
  icon="✗",
246
240
  summary="Start failed",
@@ -280,7 +274,7 @@ class ExecutionFormatter(BaseFormatter):
280
274
  'Terminated PID 12345'
281
275
  """
282
276
  # Check for error
283
- if "error" in result.lower() or "failed" in result.lower() or "not found" in result.lower():
277
+ if text_looks_like_error(result) or "not found" in result.lower():
284
278
  return ToolBrief(
285
279
  icon="✗",
286
280
  summary="Termination failed",
@@ -6,6 +6,7 @@ from typing import Any
6
6
 
7
7
  from soothe_sdk.client.protocol import preview_first
8
8
 
9
+ from soothe_cli.shared.tools._utils import text_looks_like_error
9
10
  from soothe_cli.shared.tools.tool_formatters.base import BaseFormatter
10
11
  from soothe_cli.shared.tools.tool_output_formatter import ToolBrief
11
12
 
@@ -40,8 +41,7 @@ class FallbackFormatter(BaseFormatter):
40
41
  # Handle string results
41
42
  if isinstance(result, str):
42
43
  # Check for error indicators
43
- error_indicators = ["error:", "failed:", "exception:", "traceback"]
44
- is_error = any(indicator in result.lower() for indicator in error_indicators)
44
+ is_error = text_looks_like_error(result)
45
45
 
46
46
  if is_error:
47
47
  # Extract error message (first line or first 80 chars)
@@ -5,6 +5,7 @@ from __future__ import annotations
5
5
  import re
6
6
  from typing import Any
7
7
 
8
+ from soothe_cli.shared.tools._utils import normalize_tool_name, text_looks_like_error
8
9
  from soothe_cli.shared.tools.tool_formatters.base import BaseFormatter
9
10
  from soothe_cli.shared.tools.tool_output_formatter import ToolBrief
10
11
 
@@ -12,8 +13,8 @@ from soothe_cli.shared.tools.tool_output_formatter import ToolBrief
12
13
  class FileOpsFormatter(BaseFormatter):
13
14
  """Formatter for file operation tools.
14
15
 
15
- Handles: read_file, write_file, delete_file, list_files, search_files, glob,
16
- 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
17
18
 
18
19
  Provides semantic summaries with size, line count, and item count metrics.
19
20
  """
@@ -38,13 +39,33 @@ class FileOpsFormatter(BaseFormatter):
38
39
  '✓ Read 12 B (2 lines)'
39
40
  """
40
41
  # Normalize tool name
41
- normalized = tool_name.lower().replace("-", "_").replace(" ", "_")
42
+ normalized = normalize_tool_name(tool_name)
42
43
 
43
44
  # Route to specific formatter
44
45
  if normalized == "read_file":
45
46
  return self._format_read_file(result)
46
47
  if normalized == "write_file":
47
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
+ )
48
69
  if normalized == "delete_file":
49
70
  return self._format_delete_file(result)
50
71
  if normalized in ("list_files", "ls"):
@@ -142,6 +163,28 @@ class FileOpsFormatter(BaseFormatter):
142
163
  metrics={"size_bytes": size_bytes, "lines": lines},
143
164
  )
144
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
+
145
188
  def _format_write_file(self, result: str) -> ToolBrief:
146
189
  """Format write_file result.
147
190
 
@@ -156,25 +199,10 @@ class FileOpsFormatter(BaseFormatter):
156
199
  Example:
157
200
  >>> brief = formatter._format_write_file("Successfully wrote to file")
158
201
  >>> brief.summary
159
- 'Wrote 0 B'
202
+ 'Wrote file'
160
203
  """
161
- # Check for error
162
- if "error" in result.lower() or "failed" in result.lower():
163
- return ToolBrief(
164
- icon="✗",
165
- summary="Write failed",
166
- detail=self._truncate_text(result, 80),
167
- metrics={"error": True},
168
- )
169
-
170
- # Try to extract size from result (if available)
171
- # Common patterns: "Wrote X bytes", "Successfully wrote X"
172
- # For now, show simple success
173
- return ToolBrief(
174
- icon="✓",
175
- summary="Wrote file",
176
- detail=None,
177
- metrics={},
204
+ return self._format_file_mutation(
205
+ result, success_summary="Wrote file", failure_summary="Write failed"
178
206
  )
179
207
 
180
208
  def _format_delete_file(self, result: str) -> ToolBrief:
@@ -194,7 +222,7 @@ class FileOpsFormatter(BaseFormatter):
194
222
  'Deleted'
195
223
  """
196
224
  # Check for error
197
- if "error" in result.lower() or "failed" in result.lower():
225
+ if text_looks_like_error(result):
198
226
  return ToolBrief(
199
227
  icon="✗",
200
228
  summary="Delete failed",
@@ -226,7 +254,7 @@ class FileOpsFormatter(BaseFormatter):
226
254
  'Found 3 items'
227
255
  """
228
256
  # Check for error
229
- if "error" in result.lower() or "failed" in result.lower():
257
+ if text_looks_like_error(result):
230
258
  return ToolBrief(
231
259
  icon="✗",
232
260
  summary="List failed",
@@ -265,7 +293,7 @@ class FileOpsFormatter(BaseFormatter):
265
293
  'Found 2 matches'
266
294
  """
267
295
  # Check for error
268
- if "error" in result.lower() or "failed" in result.lower():
296
+ if text_looks_like_error(result):
269
297
  return ToolBrief(
270
298
  icon="✗",
271
299
  summary="Search failed",
@@ -311,12 +339,14 @@ class FileOpsFormatter(BaseFormatter):
311
339
  >>> brief.summary
312
340
  'Found 2 files'
313
341
  """
314
- # Check for error
315
- if "error" in result.lower() or "failed" in result.lower():
342
+ # Check for actual error message (not file paths containing error-like words)
343
+ # Glob results are newline-separated paths; errors start with "Error:"
344
+ if result.startswith("Error:"):
345
+ error_msg = result[6:].strip()
316
346
  return ToolBrief(
317
347
  icon="✗",
318
348
  summary="Glob failed",
319
- detail=self._truncate_text(result, 80),
349
+ detail=self._truncate_text(error_msg, 80),
320
350
  metrics={"error": True},
321
351
  )
322
352
 
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  from typing import Any
6
6
 
7
+ from soothe_cli.shared.tools._utils import normalize_tool_name, text_looks_like_error
7
8
  from soothe_cli.shared.tools.tool_formatters.base import BaseFormatter
8
9
  from soothe_cli.shared.tools.tool_output_formatter import ToolBrief
9
10
 
@@ -36,7 +37,7 @@ class GoalFormatter(BaseFormatter):
36
37
  '✓ Created goal g1'
37
38
  """
38
39
  # Normalize tool name
39
- normalized = tool_name.lower().replace("-", "_").replace(" ", "_")
40
+ normalized = normalize_tool_name(tool_name)
40
41
 
41
42
  # Route to specific formatter
42
43
  if normalized == "create_goal":
@@ -100,7 +101,7 @@ class GoalFormatter(BaseFormatter):
100
101
 
101
102
  # Handle string result (fallback)
102
103
  if isinstance(result, str):
103
- if "error" in result.lower() or "failed" in result.lower():
104
+ if text_looks_like_error(result):
104
105
  return ToolBrief(
105
106
  icon="✗",
106
107
  summary="Create failed",
@@ -167,7 +168,7 @@ class GoalFormatter(BaseFormatter):
167
168
 
168
169
  # Handle string result (fallback)
169
170
  if isinstance(result, str):
170
- if "error" in result.lower() or "failed" in result.lower():
171
+ if text_looks_like_error(result):
171
172
  return ToolBrief(
172
173
  icon="✗",
173
174
  summary="List failed",
@@ -234,7 +235,7 @@ class GoalFormatter(BaseFormatter):
234
235
 
235
236
  # Handle string result (fallback)
236
237
  if isinstance(result, str):
237
- if "error" in result.lower() or "failed" in result.lower():
238
+ if text_looks_like_error(result):
238
239
  return ToolBrief(
239
240
  icon="✗",
240
241
  summary="Complete failed",
@@ -307,7 +308,7 @@ class GoalFormatter(BaseFormatter):
307
308
 
308
309
  # Handle string result (fallback)
309
310
  if isinstance(result, str):
310
- if "error" in result.lower() or "failed" in result.lower():
311
+ if text_looks_like_error(result):
311
312
  return ToolBrief(
312
313
  icon="✗",
313
314
  summary="Fail operation failed",
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  from typing import Any
6
6
 
7
+ from soothe_cli.shared.tools._utils import normalize_tool_name, text_looks_like_error
7
8
  from soothe_cli.shared.tools.tool_formatters.base import BaseFormatter
8
9
  from soothe_cli.shared.tools.tool_output_formatter import ToolBrief
9
10
 
@@ -36,7 +37,7 @@ class MediaFormatter(BaseFormatter):
36
37
  '✓ Transcribed 45.2s (en)'
37
38
  """
38
39
  # Normalize tool name
39
- normalized = tool_name.lower().replace("-", "_").replace(" ", "_")
40
+ normalized = normalize_tool_name(tool_name)
40
41
 
41
42
  # Route to specific formatter
42
43
  if normalized == "transcribe_audio":
@@ -104,7 +105,7 @@ class MediaFormatter(BaseFormatter):
104
105
 
105
106
  # Handle string result (fallback)
106
107
  if isinstance(result, str):
107
- if "error" in result.lower() or "failed" in result.lower():
108
+ if text_looks_like_error(result):
108
109
  return ToolBrief(
109
110
  icon="✗",
110
111
  summary="Transcription failed",
@@ -182,7 +183,7 @@ class MediaFormatter(BaseFormatter):
182
183
 
183
184
  # Handle string result (fallback)
184
185
  if isinstance(result, str):
185
- if "error" in result.lower() or "failed" in result.lower():
186
+ if text_looks_like_error(result):
186
187
  return ToolBrief(
187
188
  icon="✗",
188
189
  summary="Video info failed",
@@ -267,7 +268,7 @@ class MediaFormatter(BaseFormatter):
267
268
 
268
269
  # Handle string result (fallback)
269
270
  if isinstance(result, str):
270
- if "error" in result.lower() or "failed" in result.lower():
271
+ if text_looks_like_error(result):
271
272
  return ToolBrief(
272
273
  icon="✗",
273
274
  summary="Image analysis failed",
@@ -7,6 +7,7 @@ from __future__ import annotations
7
7
 
8
8
  from typing import Any
9
9
 
10
+ from soothe_cli.shared.tools._utils import text_looks_like_error
10
11
  from soothe_cli.shared.tools.tool_formatters.base import BaseFormatter
11
12
  from soothe_cli.shared.tools.tool_output_formatter import ToolBrief
12
13
 
@@ -36,8 +37,7 @@ class SubagentFormatter(BaseFormatter):
36
37
  # Handle string results
37
38
  if isinstance(result, str):
38
39
  # Check for error indicators
39
- error_indicators = ["error:", "failed:", "exception:", "traceback"]
40
- is_error = any(indicator in result.lower() for indicator in error_indicators)
40
+ is_error = text_looks_like_error(result)
41
41
 
42
42
  if is_error:
43
43
  # Extract first line of error