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.
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/.gitignore +1 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/PKG-INFO +1 -1
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/cli/commands/run_cmd.py +10 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/cli/execution/daemon.py +7 -1
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/cli/execution/headless.py +1 -1
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/cli/main.py +10 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/cli/renderer.py +58 -86
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/cli/stream/formatter.py +10 -53
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/cli/stream/pipeline.py +5 -18
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/config/cli_config.py +25 -4
- soothe_cli-0.4.2/src/soothe_cli/shared/async_renderer_protocol.py +184 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/shared/config_loader.py +1 -1
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/shared/display_policy.py +0 -35
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/shared/event_processor.py +161 -30
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/shared/message_processing.py +11 -1
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/shared/presentation_engine.py +3 -3
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/shared/processor_state.py +19 -4
- soothe_cli-0.4.2/src/soothe_cli/shared/renderer_base.py +72 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/shared/renderer_protocol.py +25 -0
- soothe_cli-0.4.2/src/soothe_cli/shared/stream_accumulator.py +141 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/shared/tool_formatters/__init__.py +2 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/shared/tool_formatters/fallback.py +1 -1
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/shared/tool_formatters/file_ops.py +4 -1
- soothe_cli-0.4.2/src/soothe_cli/shared/tool_formatters/subagent.py +122 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/shared/tool_output_formatter.py +4 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/app.py +12 -2
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/daemon_session.py +24 -1
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/textual_adapter.py +132 -74
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/tool_display.py +2 -2
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/widgets/message_store.py +0 -5
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/widgets/messages.py +0 -3
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/widgets/welcome.py +1 -2
- soothe_cli-0.4.0/src/soothe_cli/shared/suppression_state.py +0 -189
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/README.md +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/pyproject.toml +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/__init__.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/cli/__init__.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/cli/commands/__init__.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/cli/commands/autopilot_cmd.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/cli/commands/config_cmd.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/cli/commands/status_cmd.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/cli/commands/subagent_names.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/cli/commands/thread_cmd.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/cli/execution/__init__.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/cli/execution/launcher.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/cli/stream/__init__.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/cli/stream/context.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/cli/stream/display_line.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/cli/utils.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/config/__init__.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/loop_commands.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/plan/__init__.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/plan/rich_tree.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/shared/__init__.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/shared/command_router.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/shared/essential_events.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/shared/rendering.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/shared/slash_commands.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/shared/subagent_routing.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/shared/tool_call_resolution.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/shared/tool_card_payload.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/shared/tool_formatters/base.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/shared/tool_formatters/execution.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/shared/tool_formatters/goal_formatter.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/shared/tool_formatters/media.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/shared/tool_formatters/structured.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/shared/tool_formatters/web.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/shared/tool_message_format.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/shared/tui_trace_log.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/__init__.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/_ask_user_types.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/_cli_context.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/_env_vars.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/_session_stats.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/_version.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/app.tcss +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/command_registry.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/config.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/file_ops.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/formatting.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/hooks.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/input.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/media_utils.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/message_display_filter.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/model_config.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/output.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/preview_limits.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/project_utils.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/sessions.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/skills/__init__.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/skills/invocation.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/skills/load.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/theme.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/unicode_security.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/update_check.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/widgets/__init__.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/widgets/_links.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/widgets/approval.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/widgets/ask_user.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/widgets/autocomplete.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/widgets/autopilot_dashboard.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/widgets/autopilot_screen.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/widgets/chat_input.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/widgets/clipboard.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/widgets/diff.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/widgets/editor.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/widgets/history.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/widgets/loading.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/widgets/loop_selector.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/widgets/mcp_viewer.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/widgets/model_selector.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/widgets/notification_settings.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/widgets/status.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/widgets/theme_selector.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/widgets/thread_selector.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/widgets/tool_renderers.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/widgets/tool_widgets.py +0 -0
- {soothe_cli-0.4.0 → soothe_cli-0.4.2}/src/soothe_cli/tui/widgets/tools.py +0 -0
|
@@ -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(
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
#
|
|
40
|
-
|
|
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.
|
|
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,
|
|
147
|
+
is_streaming: bool,
|
|
168
148
|
) -> None:
|
|
169
149
|
"""Write assistant text to stdout.
|
|
170
150
|
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
183
|
-
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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, ...],
|
|
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, **
|
|
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
|
|
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.
|
|
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
|
|
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"
|
|
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"
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|