soothe-cli 0.4.0__tar.gz → 0.4.2__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 (118) hide show
  1. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/.gitignore +1 -0
  2. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/PKG-INFO +1 -1
  3. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/cli/commands/run_cmd.py +10 -0
  4. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/cli/execution/daemon.py +7 -1
  5. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/cli/execution/headless.py +1 -1
  6. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/cli/main.py +10 -0
  7. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/cli/renderer.py +58 -86
  8. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/cli/stream/formatter.py +10 -53
  9. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/cli/stream/pipeline.py +5 -18
  10. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/config/cli_config.py +25 -4
  11. soothe_cli-0.4.2/src/soothe_cli/shared/async_renderer_protocol.py +184 -0
  12. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/shared/config_loader.py +1 -1
  13. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/shared/display_policy.py +0 -35
  14. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/shared/event_processor.py +161 -30
  15. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/shared/message_processing.py +11 -1
  16. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/shared/presentation_engine.py +3 -3
  17. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/shared/processor_state.py +19 -4
  18. soothe_cli-0.4.2/src/soothe_cli/shared/renderer_base.py +72 -0
  19. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/shared/renderer_protocol.py +25 -0
  20. soothe_cli-0.4.2/src/soothe_cli/shared/stream_accumulator.py +141 -0
  21. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/shared/tool_formatters/__init__.py +2 -0
  22. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/shared/tool_formatters/fallback.py +1 -1
  23. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/shared/tool_formatters/file_ops.py +4 -1
  24. soothe_cli-0.4.2/src/soothe_cli/shared/tool_formatters/subagent.py +122 -0
  25. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/shared/tool_output_formatter.py +4 -0
  26. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/app.py +12 -2
  27. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/daemon_session.py +24 -1
  28. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/textual_adapter.py +132 -74
  29. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/tool_display.py +2 -2
  30. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/widgets/message_store.py +0 -5
  31. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/widgets/messages.py +0 -3
  32. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/widgets/welcome.py +1 -2
  33. soothe_cli-0.4.0/src/soothe_cli/shared/suppression_state.py +0 -189
  34. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/README.md +0 -0
  35. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/pyproject.toml +0 -0
  36. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/__init__.py +0 -0
  37. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/cli/__init__.py +0 -0
  38. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/cli/commands/__init__.py +0 -0
  39. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/cli/commands/autopilot_cmd.py +0 -0
  40. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/cli/commands/config_cmd.py +0 -0
  41. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/cli/commands/status_cmd.py +0 -0
  42. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/cli/commands/subagent_names.py +0 -0
  43. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/cli/commands/thread_cmd.py +0 -0
  44. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/cli/execution/__init__.py +0 -0
  45. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/cli/execution/launcher.py +0 -0
  46. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/cli/stream/__init__.py +0 -0
  47. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/cli/stream/context.py +0 -0
  48. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/cli/stream/display_line.py +0 -0
  49. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/cli/utils.py +0 -0
  50. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/config/__init__.py +0 -0
  51. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/loop_commands.py +0 -0
  52. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/plan/__init__.py +0 -0
  53. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/plan/rich_tree.py +0 -0
  54. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/shared/__init__.py +0 -0
  55. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/shared/command_router.py +0 -0
  56. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/shared/essential_events.py +0 -0
  57. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/shared/rendering.py +0 -0
  58. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/shared/slash_commands.py +0 -0
  59. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/shared/subagent_routing.py +0 -0
  60. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/shared/tool_call_resolution.py +0 -0
  61. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/shared/tool_card_payload.py +0 -0
  62. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/shared/tool_formatters/base.py +0 -0
  63. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/shared/tool_formatters/execution.py +0 -0
  64. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/shared/tool_formatters/goal_formatter.py +0 -0
  65. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/shared/tool_formatters/media.py +0 -0
  66. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/shared/tool_formatters/structured.py +0 -0
  67. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/shared/tool_formatters/web.py +0 -0
  68. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/shared/tool_message_format.py +0 -0
  69. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/shared/tui_trace_log.py +0 -0
  70. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/__init__.py +0 -0
  71. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/_ask_user_types.py +0 -0
  72. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/_cli_context.py +0 -0
  73. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/_env_vars.py +0 -0
  74. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/_session_stats.py +0 -0
  75. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/_version.py +0 -0
  76. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/app.tcss +0 -0
  77. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/command_registry.py +0 -0
  78. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/config.py +0 -0
  79. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/file_ops.py +0 -0
  80. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/formatting.py +0 -0
  81. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/hooks.py +0 -0
  82. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/input.py +0 -0
  83. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/media_utils.py +0 -0
  84. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/message_display_filter.py +0 -0
  85. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/model_config.py +0 -0
  86. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/output.py +0 -0
  87. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/preview_limits.py +0 -0
  88. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/project_utils.py +0 -0
  89. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/sessions.py +0 -0
  90. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/skills/__init__.py +0 -0
  91. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/skills/invocation.py +0 -0
  92. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/skills/load.py +0 -0
  93. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/theme.py +0 -0
  94. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/unicode_security.py +0 -0
  95. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/update_check.py +0 -0
  96. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/widgets/__init__.py +0 -0
  97. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/widgets/_links.py +0 -0
  98. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/widgets/approval.py +0 -0
  99. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/widgets/ask_user.py +0 -0
  100. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/widgets/autocomplete.py +0 -0
  101. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/widgets/autopilot_dashboard.py +0 -0
  102. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/widgets/autopilot_screen.py +0 -0
  103. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/widgets/chat_input.py +0 -0
  104. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/widgets/clipboard.py +0 -0
  105. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/widgets/diff.py +0 -0
  106. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/widgets/editor.py +0 -0
  107. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/widgets/history.py +0 -0
  108. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/widgets/loading.py +0 -0
  109. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/widgets/loop_selector.py +0 -0
  110. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/widgets/mcp_viewer.py +0 -0
  111. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/widgets/model_selector.py +0 -0
  112. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/widgets/notification_settings.py +0 -0
  113. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/widgets/status.py +0 -0
  114. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/widgets/theme_selector.py +0 -0
  115. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/widgets/thread_selector.py +0 -0
  116. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/widgets/tool_renderers.py +0 -0
  117. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/widgets/tool_widgets.py +0 -0
  118. {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/widgets/tools.py +0 -0
@@ -218,3 +218,4 @@ plot_*
218
218
  checkpoint.json
219
219
  manifest.json
220
220
  _bmad
221
+ __MACOSX
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: soothe-cli
3
- Version: 0.4.0
3
+ Version: 0.4.2
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
@@ -23,6 +23,8 @@ def run_impl(
23
23
  autonomous: bool, # noqa: FBT001
24
24
  max_iterations: int | None,
25
25
  output_format: str,
26
+ streaming_enabled: bool | None = None,
27
+ streaming_mode: str | None = None,
26
28
  ) -> None:
27
29
  """Core implementation for running Soothe agent.
28
30
 
@@ -35,6 +37,8 @@ def run_impl(
35
37
  autonomous: Enable autonomous iteration mode
36
38
  max_iterations: Max iterations for autonomous mode
37
39
  output_format: Output format (text or jsonl)
40
+ streaming_enabled: Override daemon streaming enabled setting (RFC-614)
41
+ streaming_mode: Override daemon streaming mode ('streaming' or 'batch')
38
42
  """
39
43
  startup_start = time.perf_counter()
40
44
 
@@ -50,6 +54,12 @@ def run_impl(
50
54
  if checkpointer == "postgresql":
51
55
  logger.info("PostgreSQL checkpointer configured; ensure server is running.")
52
56
 
57
+ # Apply CLI streaming overrides (RFC-614)
58
+ if streaming_enabled is not None:
59
+ cfg.output_streaming_enabled = streaming_enabled
60
+ if streaming_mode is not None:
61
+ cfg.output_streaming_mode = streaming_mode
62
+
53
63
  startup_elapsed_ms = (time.perf_counter() - startup_start) * 1000
54
64
  logger.info("[Startup] ✓ Ready (%.1fms)", startup_elapsed_ms)
55
65
 
@@ -48,6 +48,7 @@ async def run_headless_via_daemon(
48
48
  ws_url = websocket_url_from_config(cfg)
49
49
  client = WebSocketClient(url=ws_url)
50
50
  verbosity = cfg.logging.verbosity
51
+ final_output_mode = getattr(cfg, "final_output_mode", "streaming")
51
52
 
52
53
  try:
53
54
  await connect_websocket_with_retries(client)
@@ -84,7 +85,12 @@ async def run_headless_via_daemon(
84
85
  # for pipeline + message gating (RFC-502).
85
86
  presentation = PresentationEngine()
86
87
  renderer = CliRenderer(verbosity=verbosity, presentation_engine=presentation)
87
- processor = EventProcessor(renderer, verbosity=verbosity, presentation_engine=presentation)
88
+ processor = EventProcessor(
89
+ renderer,
90
+ verbosity=verbosity,
91
+ final_output_mode=final_output_mode,
92
+ presentation_engine=presentation,
93
+ )
88
94
 
89
95
  has_error = False
90
96
  query_started = False # Track if we've seen the query start running
@@ -29,7 +29,7 @@ def run_headless(
29
29
  ) -> None:
30
30
  """Run a single prompt with streaming output and progress events.
31
31
 
32
- Connects to running daemon via WebSocket if available to avoid RocksDB lock conflicts.
32
+ Connects to running daemon via WebSocket if available to avoid database lock conflicts.
33
33
  Auto-starts daemon if not running (RFC-0013 daemon lifecycle).
34
34
 
35
35
  Note (RFC-0013): Daemon persists after request completion. Use 'soothed stop'
@@ -74,6 +74,14 @@ def main(
74
74
  str,
75
75
  typer.Option("--format", "-f", help="Output format for headless mode: text or jsonl."),
76
76
  ] = "text",
77
+ streaming: Annotated[
78
+ bool | None,
79
+ typer.Option("--streaming/--no-streaming", help="Enable/disable output streaming."),
80
+ ] = None,
81
+ streaming_mode: Annotated[
82
+ str | None,
83
+ typer.Option("--streaming-mode", help="Streaming mode: 'streaming' or 'batch'"),
84
+ ] = None,
77
85
  show_help: Annotated[ # noqa: FBT002
78
86
  bool,
79
87
  typer.Option("--help", "-h", is_flag=True, help="Show this message and exit."),
@@ -117,6 +125,8 @@ def main(
117
125
  autonomous=False,
118
126
  max_iterations=None,
119
127
  output_format=output_format,
128
+ streaming_enabled=streaming,
129
+ streaming_mode=streaming_mode,
120
130
  )
121
131
 
122
132
 
@@ -13,14 +13,14 @@ from dataclasses import dataclass, field
13
13
  from typing import TYPE_CHECKING, Any
14
14
 
15
15
  from soothe_sdk.core.verbosity import VerbosityTier
16
- from soothe_sdk.utils import get_tool_pascal_name
16
+ from soothe_sdk.utils import get_tool_display_name
17
17
 
18
18
  from soothe_cli.cli.stream import DisplayLine, StreamDisplayPipeline
19
19
  from soothe_cli.cli.utils import make_tool_block
20
20
  from soothe_cli.shared.display_policy import VerbosityLevel, normalize_verbosity
21
21
  from soothe_cli.shared.message_processing import format_tool_call_args
22
22
  from soothe_cli.shared.presentation_engine import PresentationEngine
23
- from soothe_cli.shared.suppression_state import SuppressionState
23
+ from soothe_cli.shared.renderer_base import RendererBase
24
24
 
25
25
  if TYPE_CHECKING:
26
26
  from soothe_sdk.client.schemas import Plan
@@ -36,8 +36,8 @@ class CliRendererState:
36
36
  # Track if stderr was just written (to add spacing before next stdout)
37
37
  stderr_just_written: bool = False
38
38
 
39
- # Multi-step/agentic suppression state (IG-143)
40
- suppression: SuppressionState = field(default_factory=SuppressionState)
39
+ # Per-turn assistant output accumulation for diagnostics/tests.
40
+ full_response: list[str] = field(default_factory=list)
41
41
 
42
42
  # Track current plan for status display
43
43
  current_plan: Plan | None = None
@@ -45,11 +45,14 @@ class CliRendererState:
45
45
  # Track tool call start times for duration display (RFC-0020)
46
46
  tool_call_start_times: dict[str, float] = field(default_factory=dict)
47
47
 
48
+ # Buffer tool-call line text until result arrives for single-line rendering.
49
+ pending_tool_call_lines: dict[str, str] = field(default_factory=dict)
50
+
48
51
  # After LLM text on stdout, next stderr icon block gets one leading blank line
49
52
  stderr_blank_before_next_icon_block: bool = False
50
53
 
51
54
 
52
- class CliRenderer:
55
+ class CliRenderer(RendererBase):
53
56
  """CLI renderer for headless stdout/stderr output.
54
57
 
55
58
  Implements RendererProtocol callbacks for CLI mode:
@@ -58,6 +61,8 @@ class CliRenderer:
58
61
  - Progress events -> stderr via StreamDisplayPipeline
59
62
  - Errors -> stderr
60
63
 
64
+ Inherits from RendererBase for unified text repair logic.
65
+
61
66
  Spacing: Soothe-originated stderr lines (icons from the pipeline, tools, results,
62
67
  errors) call `_stderr_begin_icon_block()`, which inserts one blank stderr line only
63
68
  after LLM text was written to stdout, so icon blocks separate from answers without
@@ -80,6 +85,7 @@ class CliRenderer:
80
85
  verbosity: Progress visibility level.
81
86
  presentation_engine: Shared presentation engine (optional).
82
87
  """
88
+ super().__init__()
83
89
  self._verbosity = normalize_verbosity(verbosity)
84
90
  self._state = CliRendererState()
85
91
  self._presentation = presentation_engine or PresentationEngine()
@@ -99,12 +105,7 @@ class CliRenderer:
99
105
  @property
100
106
  def full_response(self) -> list[str]:
101
107
  """Get accumulated response text."""
102
- return self._state.suppression.full_response
103
-
104
- @property
105
- def multi_step_active(self) -> bool:
106
- """Whether multi-step plan is active."""
107
- return self._state.suppression.multi_step_active
108
+ return self._state.full_response
108
109
 
109
110
  @property
110
111
  def presentation_engine(self) -> PresentationEngine:
@@ -138,38 +139,17 @@ class CliRenderer:
138
139
  sys.stderr.flush()
139
140
  self._state.stderr_just_written = True
140
141
 
141
- def _write_stdout_final_report(self, text: str) -> None:
142
- """Write aggregated final answer to stdout (multi-step headless mode only)."""
143
- stripped = text.strip()
144
- if not stripped:
145
- return
146
-
147
- self._state.suppression.full_response.append(stripped)
148
-
149
- # Add newline before final report if stderr was just written (goal completion)
150
- if self._state.stderr_just_written:
151
- sys.stdout.write("\n")
152
- self._state.stderr_just_written = False
153
-
154
- sys.stdout.write(stripped)
155
- if not stripped.endswith("\n"):
156
- sys.stdout.write("\n")
157
- sys.stdout.flush()
158
- self._state.needs_stdout_newline = True
159
- self._state.stderr_blank_before_next_icon_block = True
160
- self._presentation.mark_final_answer_locked()
161
-
162
142
  def on_assistant_text(
163
143
  self,
164
144
  text: str,
165
145
  *,
166
146
  is_main: bool,
167
- is_streaming: bool, # noqa: ARG002
147
+ is_streaming: bool,
168
148
  ) -> None:
169
149
  """Write assistant text to stdout.
170
150
 
171
- HARD SUPPRESS during multi-step execution to prevent intermediate
172
- LLM response text from flooding output (IG-143).
151
+ Write assistant text directly. Daemon-side output contract decides
152
+ which assistant text reaches clients.
173
153
 
174
154
  Args:
175
155
  text: Text content to display.
@@ -179,25 +159,41 @@ class CliRenderer:
179
159
  if not is_main:
180
160
  return # Subagent text not shown in CLI headless mode
181
161
 
182
- # HARD BLOCK: No text during multi-step execution (IG-143)
183
- if self._state.suppression.should_suppress_output():
184
- # Accumulate for final report instead
185
- self._state.suppression.accumulate_text(text)
186
- return
187
-
188
- # Emit only on final iteration (after flags cleared)
189
- self._state.suppression.full_response.append(text)
162
+ payload = text if is_streaming else self.repair_concatenated_output(text)
163
+ self._state.full_response.append(payload)
190
164
 
191
165
  if self._state.stderr_just_written:
192
166
  self._state.stderr_just_written = False
193
167
 
194
168
  # LLM stream: do not inject extra blank lines (spacing before icon stderr
195
169
  # is handled in _stderr_begin_icon_block when progress resumes).
196
- sys.stdout.write(text)
170
+ sys.stdout.write(payload)
197
171
  sys.stdout.flush()
198
172
  self._state.needs_stdout_newline = True
199
173
  self._state.stderr_blank_before_next_icon_block = True
200
174
 
175
+ def on_streaming_output(
176
+ self,
177
+ event_type: str,
178
+ text: str,
179
+ *,
180
+ is_chunk: bool,
181
+ namespace: tuple[str, ...],
182
+ ) -> None:
183
+ """Handle streaming output from unified framework (RFC-614).
184
+
185
+ Default implementation: delegate to on_assistant_text.
186
+ CLI renderer treats all streaming output as assistant text.
187
+
188
+ Args:
189
+ event_type: Event type string.
190
+ text: Text content (may be chunk or final).
191
+ is_chunk: True if partial chunk, False if final.
192
+ namespace: Namespace tuple for stream context (ignored in CLI headless mode).
193
+ """
194
+ # Delegate to on_assistant_text for unified display
195
+ self.on_assistant_text(text, is_main=True, is_streaming=is_chunk)
196
+
201
197
  def on_tool_call(
202
198
  self,
203
199
  name: str,
@@ -217,13 +213,9 @@ class CliRenderer:
217
213
  if not self._presentation.tier_visible(VerbosityTier.NORMAL, self._verbosity):
218
214
  return
219
215
 
220
- # Multi-step / agentic suppression applies to assistant stdout only (IG-143).
221
- # Tool calls and results still stream to stderr at normal+ verbosity so headless
222
- # runs show the same tool activity as the TUI.
223
-
224
216
  self._stderr_begin_icon_block()
225
217
 
226
- display_name = get_tool_pascal_name(name)
218
+ display_name = get_tool_display_name(name)
227
219
 
228
220
  # Pass args directly, including any _raw fallback
229
221
  args_str = format_tool_call_args(name, {"args": args, "_raw": args.get("_raw", "")})
@@ -239,10 +231,12 @@ class CliRenderer:
239
231
  # Track start time for duration display (RFC-0020)
240
232
  if tool_call_id:
241
233
  self._state.tool_call_start_times[tool_call_id] = time.time()
234
+ self._state.pending_tool_call_lines[tool_call_id] = tool_block
235
+ return
242
236
 
237
+ # No stable ID means we cannot join with result later - keep old behavior.
243
238
  sys.stderr.write(f"{tool_block}\n")
244
239
  sys.stderr.flush()
245
- # Mark that stderr was just written
246
240
  self._state.stderr_just_written = True
247
241
 
248
242
  def on_tool_result(
@@ -266,8 +260,6 @@ class CliRenderer:
266
260
  if not self._presentation.tier_visible(VerbosityTier.NORMAL, self._verbosity):
267
261
  return
268
262
 
269
- # See on_tool_call: do not suppress stderr tool results during multi-step runs.
270
-
271
263
  self._stderr_begin_icon_block()
272
264
 
273
265
  # Calculate duration (RFC-0020)
@@ -287,9 +279,15 @@ class CliRenderer:
287
279
  if duration_ms > 0:
288
280
  result_line += f" ({duration_ms}ms)"
289
281
 
290
- # IG-257: Add indentation when inside step context
291
- # Unicode U+2514 "└─" (Box Drawings Light Up and Right) for tree branch
292
- if self._is_inside_step_context():
282
+ combined_call_line: str | None = None
283
+ if tool_call_id:
284
+ combined_call_line = self._state.pending_tool_call_lines.pop(tool_call_id, None)
285
+
286
+ if combined_call_line:
287
+ result_line = f"{combined_call_line} -> {result_line}"
288
+ elif self._is_inside_step_context():
289
+ # IG-257: Add indentation when inside step context
290
+ # Unicode U+2514 "└─" (Box Drawings Light Up and Right) for tree branch
293
291
  result_line = f" └─ {result_line}"
294
292
 
295
293
  sys.stderr.write(result_line + "\n")
@@ -323,7 +321,7 @@ class CliRenderer:
323
321
  event_type: str,
324
322
  data: dict[str, Any],
325
323
  *,
326
- namespace: tuple[str, ...], # noqa: ARG002
324
+ namespace: tuple[str, ...],
327
325
  ) -> None:
328
326
  """Write progress event to stderr using StreamDisplayPipeline.
329
327
 
@@ -332,22 +330,11 @@ class CliRenderer:
332
330
  data: Event payload.
333
331
  namespace: Subagent namespace.
334
332
  """
335
- # Track suppression state from event (IG-143)
336
- final_stdout = self._state.suppression.track_from_event(event_type, data)
337
-
338
- payload = dict(data)
339
- payload.pop("final_stdout_message", None)
340
-
341
333
  # Build event dict for pipeline
342
- event = {"type": event_type, **payload}
334
+ event = {"type": event_type, **data}
343
335
  lines = self._pipeline.process(event)
344
336
  self.write_lines(lines)
345
337
 
346
- # Emit final report on loop completion (IG-143)
347
- if self._state.suppression.should_emit_final_report(event_type, final_stdout):
348
- response = self._state.suppression.get_final_response(final_stdout)
349
- self._write_stdout_final_report(response)
350
-
351
338
  def on_plan_created(self, plan: Plan) -> None:
352
339
  """Write plan creation to stderr.
353
340
 
@@ -355,7 +342,6 @@ class CliRenderer:
355
342
  plan: Created plan object.
356
343
  """
357
344
  self._state.current_plan = plan
358
- self._state.suppression.track_from_plan(len(plan.steps))
359
345
 
360
346
  # Use pipeline for consistent formatting
361
347
  event = {
@@ -420,24 +406,10 @@ class CliRenderer:
420
406
  self.write_lines(lines)
421
407
 
422
408
  def on_turn_end(self) -> None:
423
- """Finalize output on turn end.
424
-
425
- If multi_step_active was suppressing output, flush the accumulated
426
- response to stdout now that the plan is complete.
427
- """
428
- # Capture state BEFORE resetting
429
- was_multi_step = self._state.suppression.multi_step_active
430
- accumulated_response = self._state.suppression.full_response
431
-
432
- # Reset state for next turn FIRST (before output logic)
409
+ """Finalize turn-local renderer state."""
433
410
  self._state.needs_stdout_newline = False
434
- self._state.suppression.reset_turn()
435
-
436
- # Multi-step mode intentionally suppresses step body output in headless CLI.
437
- # For single-step mode, keep existing newline flush behavior.
438
- if (not was_multi_step) and accumulated_response:
439
- sys.stdout.write("\n")
440
- sys.stdout.flush()
411
+ self._state.full_response.clear()
412
+ self._state.pending_tool_call_lines.clear()
441
413
 
442
414
  def _stderr_begin_icon_block(self) -> None:
443
415
  """Prepare stderr for Soothe icon lines (progress, tools, tool results).
@@ -3,7 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from soothe_sdk.core.verbosity import VerbosityTier
6
- from soothe_sdk.utils import get_tool_pascal_name
6
+ from soothe_sdk.utils import get_tool_display_name
7
7
 
8
8
  from soothe_cli.cli.stream.display_line import DisplayLine, indent_for_level
9
9
 
@@ -96,7 +96,7 @@ def format_goal_header(
96
96
  DisplayLine for goal header.
97
97
  """
98
98
  # Add inline symbol for goal marker
99
- content = f"⌯⌲ {goal}"
99
+ content = f"📍 {goal}"
100
100
  return DisplayLine(
101
101
  level=1,
102
102
  content=content,
@@ -126,7 +126,7 @@ def format_step_header(
126
126
  """
127
127
  suffix = " (parallel)" if parallel else ""
128
128
  # Add inline symbol for step progression
129
- content = f" {description}{suffix}"
129
+ content = f"❇️ {description}{suffix}"
130
130
  return DisplayLine(
131
131
  level=2,
132
132
  content=content,
@@ -160,7 +160,7 @@ def format_tool_call(
160
160
  DisplayLine for tool/subagent call with uniform wrench icon.
161
161
  """
162
162
  # Transform to PascalCase for display
163
- display_name = get_tool_pascal_name(name)
163
+ display_name = get_tool_display_name(name)
164
164
 
165
165
  # IG-256: No differentiation - use wrench for all tools/subagents
166
166
  icon_emoji = "🔧"
@@ -242,7 +242,6 @@ def format_subagent_milestone(
242
242
  def format_subagent_done(
243
243
  summary: str,
244
244
  duration_s: float,
245
- result_preview: str = "",
246
245
  *,
247
246
  namespace: tuple[str, ...] = (),
248
247
  verbosity_tier: VerbosityTier = VerbosityTier.NORMAL,
@@ -250,12 +249,11 @@ def format_subagent_done(
250
249
  """Format a subagent completion line with metrics.
251
250
 
252
251
  IG-256: Restored verbose format with triple success markers and separate result display.
253
- Result preview parameter is ignored - results show via separate tool events.
252
+ Results show via separate tool events.
254
253
 
255
254
  Args:
256
255
  summary: Completion summary with subagent-specific metrics (e.g., "success", "$1.23").
257
256
  duration_s: Duration in seconds.
258
- result_preview: Ignored (kept for backward compatibility).
259
257
  namespace: Event namespace.
260
258
  verbosity_tier: Current verbosity tier.
261
259
 
@@ -264,9 +262,7 @@ def format_subagent_done(
264
262
  """
265
263
  duration_ms = int(duration_s * 1000)
266
264
 
267
- # IG-256: Verbose format restored - triple success markers, result shows separately
268
- # Format: "✓ ✅ ✓ {summary}"
269
- # result_preview is ignored - let result show via separate tool execution events
265
+ # IG-256: Verbose format restored - triple success markers
270
266
  content = f"✓ ✅ ✓ {summary}"
271
267
 
272
268
  return DisplayLine(
@@ -307,54 +303,21 @@ def format_plan_phase_reasoning(
307
303
  )
308
304
 
309
305
 
310
- def format_reasoning(
311
- reasoning: str,
312
- *,
313
- namespace: tuple[str, ...] = (),
314
- verbosity_tier: VerbosityTier = VerbosityTier.NORMAL,
315
- ) -> DisplayLine:
316
- """Format a reasoning line for LLM decision internal analysis.
317
-
318
- IG-XXX: Shows technical reasoning with "Reasoning:" prefix for clarity.
319
- Uses solid bullet ● (matching goal) to indicate reasoning is active phase.
320
-
321
- Args:
322
- reasoning: Internal technical analysis text.
323
- namespace: Event namespace.
324
- verbosity_tier: Current verbosity tier.
325
-
326
- Returns:
327
- DisplayLine for reasoning.
328
- """
329
- # Polish: Add "Reasoning:" prefix to make internal analysis visible
330
- content = f"💭 {reasoning}"
331
-
332
- return DisplayLine(
333
- level=3, # Use level 3 for less prominence (subordinate to next_action)
334
- content=content,
335
- icon="●", # Solid bullet matching goal icon (polish)
336
- indent=indent_for_level(3),
337
- source_prefix=_derive_source_prefix(namespace, verbosity_tier),
338
- )
339
-
340
-
341
306
  def format_judgement(
342
307
  judgement: str,
343
308
  action: str,
344
309
  *,
345
- plan_action: str | None = None,
346
310
  namespace: tuple[str, ...] = (),
347
311
  verbosity_tier: VerbosityTier = VerbosityTier.NORMAL,
348
312
  ) -> DisplayLine:
349
313
  """Format a judgement line for LLM decision reasoning.
350
314
 
351
315
  IG-089: Shows meaningful judgement info without raw intermediate data.
352
- IG-XXX: Prominent reasoning display with "Reason:" prefix for clarity.
316
+ IG-265: Removed [new]/[keep] badge from CLI display (kept in event data for logs).
353
317
 
354
318
  Args:
355
319
  judgement: Human-readable summary of the decision.
356
320
  action: Action taken ("continue" or "complete").
357
- plan_action: When set, show ``[keep]`` or ``[new]`` before the judgement text.
358
321
  namespace: Event namespace.
359
322
  verbosity_tier: Current verbosity tier.
360
323
 
@@ -363,12 +326,7 @@ def format_judgement(
363
326
  """
364
327
  action_icon = "○" if action == "continue" else "●" # Polish: ○ for continue, ● for complete
365
328
 
366
- badge = ""
367
- if plan_action in ("keep", "new"):
368
- badge = f"[{plan_action}] "
369
-
370
- # Polish: Add "Reason:" prefix to make LLM reasoning prominent
371
- content = f"🌀 {badge}{judgement}"
329
+ content = f"🌟 {judgement}"
372
330
 
373
331
  return DisplayLine(
374
332
  level=2, # Use level 2 for more prominence (like step headers)
@@ -411,7 +369,7 @@ def format_step_done(
411
369
 
412
370
  # Success case: single line
413
371
  if success:
414
- content = f"Done{tool_info}"
372
+ content = f"Done{tool_info}"
415
373
  return [
416
374
  DisplayLine(
417
375
  level=3, # Child node of step header (level 2)
@@ -427,7 +385,7 @@ def format_step_done(
427
385
  lines = [
428
386
  DisplayLine(
429
387
  level=3,
430
- content=f"Failed{tool_info}",
388
+ content=f"Failed{tool_info}",
431
389
  icon="└─", # IG-257: Unicode tree branch (U+2514)
432
390
  indent=indent_for_level(3),
433
391
  duration_ms=duration_ms,
@@ -489,7 +447,6 @@ __all__ = [
489
447
  "format_goal_header",
490
448
  "format_judgement",
491
449
  "format_plan_phase_reasoning",
492
- "format_reasoning",
493
450
  "format_step_done",
494
451
  "format_step_header",
495
452
  "format_subagent_done",
@@ -16,7 +16,6 @@ from soothe_cli.cli.stream.formatter import (
16
16
  format_goal_header,
17
17
  format_judgement,
18
18
  format_plan_phase_reasoning,
19
- format_reasoning,
20
19
  format_step_done,
21
20
  format_step_header,
22
21
  format_subagent_done,
@@ -197,13 +196,16 @@ class StreamDisplayPipeline:
197
196
  Returns:
198
197
  Display lines for goal header.
199
198
  """
200
- goal = event.get("goal", event.get("goal_description", ""))
199
+ # IG-287: Prefer friendly_message over goal/goal_description
200
+ friendly_message = event.get("friendly_message")
201
+ goal = friendly_message or event.get("goal", event.get("goal_description", ""))
201
202
  if not goal:
202
203
  return []
203
204
 
204
205
  # Reset context for new goal
205
206
  self._context.reset_goal()
206
- self._context.current_goal = goal
207
+ # Store the actual goal description (not friendly message) for context tracking
208
+ self._context.current_goal = event.get("goal", event.get("goal_description", goal))
207
209
  self._context.goal_start_time = time.time()
208
210
 
209
211
  # Get steps count if available
@@ -394,7 +396,6 @@ class StreamDisplayPipeline:
394
396
  format_subagent_done(
395
397
  preview_first(summary, 70), # Increased from 50 for richer metrics
396
398
  duration_s,
397
- result_preview=result_preview,
398
399
  namespace=self._current_namespace,
399
400
  verbosity_tier=self._verbosity_tier,
400
401
  )
@@ -609,14 +610,10 @@ class StreamDisplayPipeline:
609
610
  # Determine action type
610
611
  action = "complete" if status == "done" else "continue"
611
612
 
612
- raw_plan_action = event.get("plan_action")
613
- plan_action_kw: str | None = raw_plan_action if raw_plan_action in ("keep", "new") else None
614
-
615
613
  lines = [
616
614
  format_judgement(
617
615
  action_text,
618
616
  action,
619
- plan_action=plan_action_kw,
620
617
  namespace=self._current_namespace,
621
618
  verbosity_tier=self._verbosity_tier,
622
619
  )
@@ -634,16 +631,6 @@ class StreamDisplayPipeline:
634
631
  verbosity_tier=self._verbosity_tier,
635
632
  )
636
633
  )
637
- else:
638
- reasoning = event.get("reasoning", "").strip()
639
- if reasoning:
640
- lines.append(
641
- format_reasoning(
642
- reasoning,
643
- namespace=self._current_namespace,
644
- verbosity_tier=self._verbosity_tier,
645
- )
646
- )
647
634
 
648
635
  return lines
649
636
 
@@ -30,6 +30,14 @@ class CLIConfig:
30
30
  logging_level: str | None = None
31
31
 
32
32
  output_format: str = "text"
33
+ final_output_mode: str = "streaming"
34
+
35
+ # Output streaming overrides (RFC-614)
36
+ output_streaming_enabled: bool | None = None
37
+ """Override daemon streaming enabled setting."""
38
+
39
+ output_streaming_mode: str | None = None
40
+ """Override daemon streaming mode: 'streaming' or 'batch'."""
33
41
 
34
42
  # Paths
35
43
  soothe_home: Path = field(default_factory=lambda: Path.home() / ".soothe")
@@ -101,16 +109,28 @@ class CLIConfig:
101
109
  daemon_section = data.get("daemon", {})
102
110
  transports = daemon_section.get("transports", {})
103
111
  websocket = transports.get("websocket", {})
112
+ websocket_legacy = data.get("websocket", {})
113
+ ui_section = data.get("ui", {})
104
114
 
105
115
  raw_level = data.get("logging_level")
106
116
  if raw_level is not None and not isinstance(raw_level, str):
107
117
  raw_level = None
108
118
 
119
+ raw_final_output_mode = data.get("final_output_mode")
120
+ if raw_final_output_mode is None and isinstance(ui_section, dict):
121
+ raw_final_output_mode = ui_section.get("final_output_mode")
122
+ if not isinstance(raw_final_output_mode, str):
123
+ raw_final_output_mode = "streaming"
124
+ final_output_mode = raw_final_output_mode.strip().lower()
125
+ if final_output_mode not in {"streaming", "batch"}:
126
+ final_output_mode = "streaming"
127
+
109
128
  return cls(
110
- daemon_host=websocket.get("host", "127.0.0.1"),
111
- daemon_port=websocket.get("port", 8765),
112
- verbosity=data.get("verbosity", "normal"),
129
+ daemon_host=websocket.get("host", websocket_legacy.get("host", "127.0.0.1")),
130
+ daemon_port=websocket.get("port", websocket_legacy.get("port", 8765)),
131
+ verbosity=data.get("verbosity", ui_section.get("verbosity", "normal")),
113
132
  logging_level=raw_level,
133
+ final_output_mode=final_output_mode,
114
134
  soothe_home=Path(data.get("home", str(Path.home() / ".soothe"))),
115
135
  )
116
136
 
@@ -135,8 +155,9 @@ class CLIConfig:
135
155
  return cls(
136
156
  daemon_host=soothe_config.daemon.transports.websocket.host,
137
157
  daemon_port=soothe_config.daemon.transports.websocket.port,
138
- verbosity=soothe_config.logging.verbosity,
158
+ verbosity=soothe_config.observability.verbosity,
139
159
  logging_level=logging_level,
160
+ final_output_mode="streaming",
140
161
  soothe_home=Path(soothe_config.home),
141
162
  )
142
163