soothe-cli 0.6.5__tar.gz → 0.6.7__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.6.5 → soothe_cli-0.6.7}/.gitignore +0 -1
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/PKG-INFO +1 -1
- soothe_cli-0.6.7/src/soothe_cli/cli/commands/status_cmd.py +367 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/cli/main.py +5 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/textual_adapter.py +65 -2
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/widgets/messages.py +52 -1
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/README.md +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/pyproject.toml +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/__init__.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/cli/__init__.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/cli/commands/__init__.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/cli/commands/autopilot_cmd.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/cli/commands/loop_cmd.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/cli/commands/run_cmd.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/cli/execution/__init__.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/cli/execution/daemon.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/cli/execution/daemon_errors.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/cli/execution/headless.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/cli/execution/headless_renderer.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/cli/execution/launcher.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/config/__init__.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/config/cli_config.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/config/loader.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/config/logging_setup.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/runtime/__init__.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/runtime/headless/processor.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/runtime/headless/processor_state.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/runtime/parse/_utils.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/runtime/parse/message_processing.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/runtime/parse/tool_call_resolution.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/runtime/parse/tool_message_format.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/runtime/parse/tool_result.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/runtime/policy/display_policy.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/runtime/policy/essential_events.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/runtime/policy/tui_trace_log.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/runtime/presentation/async_renderer_protocol.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/runtime/presentation/duration_format.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/runtime/presentation/engine.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/runtime/presentation/explore_task_display.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/runtime/presentation/renderer_base.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/runtime/presentation/renderer_protocol.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/runtime/state/file_tracker.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/runtime/state/session_stats.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/runtime/state/step_router.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/runtime/state/stream_accumulator.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/runtime/state/transcript.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/runtime/task_scope.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/runtime/transport/session.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/runtime/turn/pipeline.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/runtime/turn/prepare.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/runtime/wire/chunk_filter.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/runtime/wire/display_text.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/runtime/wire/message_text.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/runtime/wire/messages.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/__init__.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/_cli_context.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/_env_vars.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/_version.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/app/__init__.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/app/_app.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/app/_commands.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/app/_execution.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/app/_history.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/app/_messages_mixin.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/app/_model.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/app/_module_init.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/app/_startup.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/app/_ui.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/app/app.tcss +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/binding.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/command_registry.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/commands/__init__.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/commands/command_router.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/commands/slash_commands.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/commands/subagent_routing.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/config.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/file_change_notify.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/file_change_renderers.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/hooks.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/input.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/media_utils.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/model_config.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/path_utils.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/preview_limits.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/project_utils.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/sessions.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/skills/__init__.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/skills/invocation.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/skills/load.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/theme.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/tips.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/tool_display.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/unicode_security.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/update_check.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/widgets/__init__.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/widgets/_links.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/widgets/autocomplete.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/widgets/autopilot_dashboard.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/widgets/autopilot_screen.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/widgets/chat_input.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/widgets/clipboard.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/widgets/diff.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/widgets/editor.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/widgets/file_change_preview.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/widgets/history.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/widgets/loading.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/widgets/loop_selector.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/widgets/mcp_viewer.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/widgets/message_store.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/widgets/model_selector.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/widgets/notification_settings.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/widgets/status.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/widgets/theme_selector.py +0 -0
- {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/widgets/welcome.py +0 -0
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
"""Daemon status CLI command for client-side validation.
|
|
2
|
+
|
|
3
|
+
Provides lightweight status checks for the soothe daemon from the client side,
|
|
4
|
+
useful for validating daemon connectivity before running commands.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import json
|
|
11
|
+
import sys
|
|
12
|
+
from typing import Annotated, Any
|
|
13
|
+
|
|
14
|
+
import typer
|
|
15
|
+
from rich.console import Console
|
|
16
|
+
from rich.panel import Panel
|
|
17
|
+
from rich.table import Table
|
|
18
|
+
from soothe_sdk.client import WebSocketClient, is_daemon_live, websocket_url_from_config
|
|
19
|
+
|
|
20
|
+
from soothe_cli.config.loader import load_config
|
|
21
|
+
|
|
22
|
+
console = Console()
|
|
23
|
+
|
|
24
|
+
# Create status command group
|
|
25
|
+
status_app = typer.Typer(help="Check daemon and client status", no_args_is_help=False)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
async def _fetch_status(ws_url: str, timeout: float = 5.0) -> dict[str, Any]:
|
|
29
|
+
"""Fetch daemon status via WebSocket RPC.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
ws_url: WebSocket URL to connect to.
|
|
33
|
+
timeout: Request timeout in seconds.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
Status dict from daemon, or error dict on failure.
|
|
37
|
+
"""
|
|
38
|
+
client = WebSocketClient(url=ws_url)
|
|
39
|
+
try:
|
|
40
|
+
await client.connect()
|
|
41
|
+
status = await client.fetch_daemon_status(timeout=timeout)
|
|
42
|
+
return status
|
|
43
|
+
except Exception as e:
|
|
44
|
+
return {"error": str(e)}
|
|
45
|
+
finally:
|
|
46
|
+
await client.close()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
async def _fetch_ready_state(ws_url: str, timeout: float = 5.0) -> dict[str, Any] | None:
|
|
50
|
+
"""Fetch daemon readiness state via WebSocket handshake.
|
|
51
|
+
|
|
52
|
+
The daemon sends a daemon_ready message on connect with its state.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
ws_url: WebSocket URL.
|
|
56
|
+
timeout: Timeout for handshake.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
daemon_ready message dict or None.
|
|
60
|
+
"""
|
|
61
|
+
import websockets
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
async with asyncio.timeout(timeout):
|
|
65
|
+
async with websockets.connect(ws_url) as ws:
|
|
66
|
+
# Read initial messages - daemon sends status then daemon_ready
|
|
67
|
+
for _ in range(3):
|
|
68
|
+
msg = await ws.recv()
|
|
69
|
+
data = json.loads(msg)
|
|
70
|
+
if data.get("type") == "daemon_ready":
|
|
71
|
+
return data
|
|
72
|
+
except Exception:
|
|
73
|
+
pass
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _render_connection_table(config: Any, ws_url: str) -> Table:
|
|
78
|
+
"""Render connection settings table."""
|
|
79
|
+
table = Table(title="Connection Settings")
|
|
80
|
+
table.add_column("Setting", style="cyan")
|
|
81
|
+
table.add_column("Value", style="green")
|
|
82
|
+
|
|
83
|
+
table.add_row("WebSocket URL", ws_url)
|
|
84
|
+
table.add_row("Daemon Host", config.daemon_host)
|
|
85
|
+
table.add_row("Daemon Port", str(config.daemon_port))
|
|
86
|
+
table.add_row("Soothe Home", str(config.soothe_home))
|
|
87
|
+
|
|
88
|
+
return table
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _render_daemon_table(
|
|
92
|
+
ws_url: str,
|
|
93
|
+
running: bool,
|
|
94
|
+
port_live: bool,
|
|
95
|
+
active_threads: int,
|
|
96
|
+
daemon_pid: int | None,
|
|
97
|
+
ready_state: dict[str, Any] | None = None,
|
|
98
|
+
) -> Table:
|
|
99
|
+
"""Render daemon status table."""
|
|
100
|
+
table = Table(title="Daemon Status")
|
|
101
|
+
table.add_column("Setting", style="cyan")
|
|
102
|
+
table.add_column("Value", style="green")
|
|
103
|
+
|
|
104
|
+
table.add_row("WebSocket URL", ws_url)
|
|
105
|
+
table.add_row("Running", "[green]Yes[/green]" if running else "[red]No[/red]")
|
|
106
|
+
table.add_row("Port Live", "[green]Yes[/green]" if port_live else "[red]No[/red]")
|
|
107
|
+
table.add_row("Active Threads", str(active_threads))
|
|
108
|
+
if daemon_pid:
|
|
109
|
+
table.add_row("Daemon PID", str(daemon_pid))
|
|
110
|
+
|
|
111
|
+
if ready_state:
|
|
112
|
+
state = ready_state.get("state", "unknown")
|
|
113
|
+
state_color = {
|
|
114
|
+
"ready": "green",
|
|
115
|
+
"degraded": "yellow",
|
|
116
|
+
"error": "red",
|
|
117
|
+
"starting": "blue",
|
|
118
|
+
"warming": "blue",
|
|
119
|
+
"stopped": "dim",
|
|
120
|
+
}.get(state, "white")
|
|
121
|
+
table.add_row("Readiness", f"[{state_color}]{state}[/{state_color}]")
|
|
122
|
+
if ready_state.get("message"):
|
|
123
|
+
table.add_row("Message", ready_state["message"])
|
|
124
|
+
|
|
125
|
+
return table
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@status_app.command("daemon")
|
|
129
|
+
def daemon_status(
|
|
130
|
+
json_output: Annotated[
|
|
131
|
+
bool,
|
|
132
|
+
typer.Option("--json", help="Output as JSON."),
|
|
133
|
+
] = False,
|
|
134
|
+
verbose: Annotated[
|
|
135
|
+
bool,
|
|
136
|
+
typer.Option("--verbose", "-v", help="Show detailed status."),
|
|
137
|
+
] = False,
|
|
138
|
+
) -> None:
|
|
139
|
+
"""Check daemon status from client side.
|
|
140
|
+
|
|
141
|
+
Validates that the soothe daemon is running and responsive.
|
|
142
|
+
|
|
143
|
+
Examples:
|
|
144
|
+
soothe status daemon
|
|
145
|
+
soothe status daemon --json
|
|
146
|
+
soothe status daemon -v
|
|
147
|
+
"""
|
|
148
|
+
config = load_config()
|
|
149
|
+
ws_url = websocket_url_from_config(config)
|
|
150
|
+
|
|
151
|
+
# Quick liveness check
|
|
152
|
+
live = asyncio.run(is_daemon_live(ws_url, timeout=5.0))
|
|
153
|
+
|
|
154
|
+
if not live:
|
|
155
|
+
if json_output:
|
|
156
|
+
console.print_json(
|
|
157
|
+
json.dumps(
|
|
158
|
+
{
|
|
159
|
+
"status": "not_running",
|
|
160
|
+
"websocket_url": ws_url,
|
|
161
|
+
"message": "Daemon not reachable",
|
|
162
|
+
}
|
|
163
|
+
)
|
|
164
|
+
)
|
|
165
|
+
else:
|
|
166
|
+
console.print(
|
|
167
|
+
Panel(
|
|
168
|
+
f"WebSocket URL: {ws_url}\n"
|
|
169
|
+
"Status: [red]Not running[/red]\n"
|
|
170
|
+
"Hint: Start with 'soothed start'",
|
|
171
|
+
title="Daemon Status",
|
|
172
|
+
border_style="red",
|
|
173
|
+
)
|
|
174
|
+
)
|
|
175
|
+
sys.exit(1)
|
|
176
|
+
|
|
177
|
+
# Fetch detailed status
|
|
178
|
+
status = asyncio.run(_fetch_status(ws_url, timeout=5.0))
|
|
179
|
+
|
|
180
|
+
if "error" in status:
|
|
181
|
+
if json_output:
|
|
182
|
+
console.print_json(
|
|
183
|
+
json.dumps(
|
|
184
|
+
{
|
|
185
|
+
"status": "error",
|
|
186
|
+
"websocket_url": ws_url,
|
|
187
|
+
"error": status["error"],
|
|
188
|
+
}
|
|
189
|
+
)
|
|
190
|
+
)
|
|
191
|
+
else:
|
|
192
|
+
console.print(
|
|
193
|
+
Panel(
|
|
194
|
+
f"WebSocket URL: {ws_url}\nError: [red]{status['error']}[/red]",
|
|
195
|
+
title="Daemon Status",
|
|
196
|
+
border_style="red",
|
|
197
|
+
)
|
|
198
|
+
)
|
|
199
|
+
sys.exit(1)
|
|
200
|
+
|
|
201
|
+
# Get readiness state for verbose mode
|
|
202
|
+
ready_state = None
|
|
203
|
+
if verbose:
|
|
204
|
+
ready_state = asyncio.run(_fetch_ready_state(ws_url, timeout=5.0))
|
|
205
|
+
|
|
206
|
+
if json_output:
|
|
207
|
+
output = {
|
|
208
|
+
"status": "running",
|
|
209
|
+
"websocket_url": ws_url,
|
|
210
|
+
"running": status.get("running", True),
|
|
211
|
+
"port_live": status.get("port_live", True),
|
|
212
|
+
"active_threads": status.get("active_threads", 0),
|
|
213
|
+
"daemon_pid": status.get("daemon_pid"),
|
|
214
|
+
}
|
|
215
|
+
if ready_state:
|
|
216
|
+
output["readiness_state"] = ready_state.get("state", "unknown")
|
|
217
|
+
output["readiness_message"] = ready_state.get("message")
|
|
218
|
+
console.print_json(json.dumps(output))
|
|
219
|
+
return
|
|
220
|
+
|
|
221
|
+
# Render daemon status table
|
|
222
|
+
running = status.get("running", True)
|
|
223
|
+
port_live = status.get("port_live", True)
|
|
224
|
+
active_threads = status.get("active_threads", 0)
|
|
225
|
+
daemon_pid = status.get("daemon_pid")
|
|
226
|
+
|
|
227
|
+
table = _render_daemon_table(
|
|
228
|
+
ws_url, running, port_live, active_threads, daemon_pid, ready_state
|
|
229
|
+
)
|
|
230
|
+
console.print(table)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
@status_app.command("connection")
|
|
234
|
+
def connection_status(
|
|
235
|
+
json_output: Annotated[
|
|
236
|
+
bool,
|
|
237
|
+
typer.Option("--json", help="Output as JSON."),
|
|
238
|
+
] = False,
|
|
239
|
+
) -> None:
|
|
240
|
+
"""Check client-daemon connection settings.
|
|
241
|
+
|
|
242
|
+
Shows the WebSocket URL and connection parameters the CLI will use.
|
|
243
|
+
|
|
244
|
+
Examples:
|
|
245
|
+
soothe status connection
|
|
246
|
+
soothe status connection --json
|
|
247
|
+
"""
|
|
248
|
+
config = load_config()
|
|
249
|
+
ws_url = websocket_url_from_config(config)
|
|
250
|
+
|
|
251
|
+
if json_output:
|
|
252
|
+
console.print_json(
|
|
253
|
+
json.dumps(
|
|
254
|
+
{
|
|
255
|
+
"websocket_url": ws_url,
|
|
256
|
+
"daemon_host": config.daemon_host,
|
|
257
|
+
"daemon_port": config.daemon_port,
|
|
258
|
+
"soothe_home": str(config.soothe_home),
|
|
259
|
+
}
|
|
260
|
+
)
|
|
261
|
+
)
|
|
262
|
+
return
|
|
263
|
+
|
|
264
|
+
table = _render_connection_table(config, ws_url)
|
|
265
|
+
console.print(table)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
@status_app.callback(invoke_without_command=True)
|
|
269
|
+
def status_main(
|
|
270
|
+
ctx: typer.Context,
|
|
271
|
+
show_help: Annotated[
|
|
272
|
+
bool,
|
|
273
|
+
typer.Option("-h", "--help", is_flag=True, help="Show this message and exit."),
|
|
274
|
+
] = False,
|
|
275
|
+
json_output: Annotated[
|
|
276
|
+
bool,
|
|
277
|
+
typer.Option("--json", help="Output as JSON."),
|
|
278
|
+
] = False,
|
|
279
|
+
) -> None:
|
|
280
|
+
"""Show overall daemon and connection status (default when no subcommand)."""
|
|
281
|
+
if show_help:
|
|
282
|
+
typer.echo(ctx.get_help())
|
|
283
|
+
raise typer.Exit(code=0)
|
|
284
|
+
|
|
285
|
+
if ctx.invoked_subcommand is not None:
|
|
286
|
+
return
|
|
287
|
+
|
|
288
|
+
config = load_config()
|
|
289
|
+
ws_url = websocket_url_from_config(config)
|
|
290
|
+
|
|
291
|
+
# Check daemon liveness
|
|
292
|
+
live = asyncio.run(is_daemon_live(ws_url, timeout=5.0))
|
|
293
|
+
|
|
294
|
+
if json_output:
|
|
295
|
+
output: dict[str, Any] = {
|
|
296
|
+
"daemon": {
|
|
297
|
+
"status": "running" if live else "not_running",
|
|
298
|
+
"websocket_url": ws_url,
|
|
299
|
+
},
|
|
300
|
+
"connection": {
|
|
301
|
+
"daemon_host": config.daemon_host,
|
|
302
|
+
"daemon_port": config.daemon_port,
|
|
303
|
+
"soothe_home": str(config.soothe_home),
|
|
304
|
+
},
|
|
305
|
+
}
|
|
306
|
+
if live:
|
|
307
|
+
status = asyncio.run(_fetch_status(ws_url, timeout=5.0))
|
|
308
|
+
if "error" not in status:
|
|
309
|
+
output["daemon"]["running"] = status.get("running", True)
|
|
310
|
+
output["daemon"]["port_live"] = status.get("port_live", True)
|
|
311
|
+
output["daemon"]["active_threads"] = status.get("active_threads", 0)
|
|
312
|
+
output["daemon"]["daemon_pid"] = status.get("daemon_pid")
|
|
313
|
+
console.print_json(json.dumps(output))
|
|
314
|
+
return
|
|
315
|
+
|
|
316
|
+
# Render combined status with tables
|
|
317
|
+
if not live:
|
|
318
|
+
console.print(
|
|
319
|
+
Panel(
|
|
320
|
+
f"WebSocket URL: {ws_url}\n"
|
|
321
|
+
f"Daemon Host: {config.daemon_host}\n"
|
|
322
|
+
f"Daemon Port: {config.daemon_port}\n"
|
|
323
|
+
f"Soothe Home: {config.soothe_home}\n\n"
|
|
324
|
+
"Daemon Status: [red]Not running[/red]\n"
|
|
325
|
+
"Hint: Start with 'soothed start'",
|
|
326
|
+
title="Soothe Status",
|
|
327
|
+
border_style="red",
|
|
328
|
+
)
|
|
329
|
+
)
|
|
330
|
+
sys.exit(1)
|
|
331
|
+
|
|
332
|
+
# Fetch detailed daemon status
|
|
333
|
+
status = asyncio.run(_fetch_status(ws_url, timeout=5.0))
|
|
334
|
+
|
|
335
|
+
if "error" in status:
|
|
336
|
+
console.print(
|
|
337
|
+
Panel(
|
|
338
|
+
f"WebSocket URL: {ws_url}\n"
|
|
339
|
+
f"Daemon Host: {config.daemon_host}\n"
|
|
340
|
+
f"Daemon Port: {config.daemon_port}\n"
|
|
341
|
+
f"Soothe Home: {config.soothe_home}\n\n"
|
|
342
|
+
f"Daemon Status: [red]Error[/red]\n"
|
|
343
|
+
f"Error: {status['error']}",
|
|
344
|
+
title="Soothe Status",
|
|
345
|
+
border_style="red",
|
|
346
|
+
)
|
|
347
|
+
)
|
|
348
|
+
sys.exit(1)
|
|
349
|
+
|
|
350
|
+
# Render both tables
|
|
351
|
+
connection_table = _render_connection_table(config, ws_url)
|
|
352
|
+
console.print(connection_table)
|
|
353
|
+
|
|
354
|
+
running = status.get("running", True)
|
|
355
|
+
port_live = status.get("port_live", True)
|
|
356
|
+
active_threads = status.get("active_threads", 0)
|
|
357
|
+
daemon_pid = status.get("daemon_pid")
|
|
358
|
+
|
|
359
|
+
daemon_table = _render_daemon_table(ws_url, running, port_live, active_threads, daemon_pid)
|
|
360
|
+
console.print(daemon_table)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
__all__ = [
|
|
364
|
+
"status_app",
|
|
365
|
+
"daemon_status",
|
|
366
|
+
"connection_status",
|
|
367
|
+
]
|
|
@@ -202,7 +202,9 @@ def main(
|
|
|
202
202
|
|
|
203
203
|
from soothe_cli.cli.commands.autopilot_cmd import app as _autopilot_app # noqa: E402
|
|
204
204
|
from soothe_cli.cli.commands.loop_cmd import loop_app as _loop_app # noqa: E402
|
|
205
|
+
from soothe_cli.cli.commands.status_cmd import status_app as _status_app # noqa: E402
|
|
205
206
|
|
|
207
|
+
# status_app has custom default behavior (shows combined status), skip add_help_alias
|
|
206
208
|
for _sub_app, _name in (
|
|
207
209
|
(_loop_app, "loop"),
|
|
208
210
|
(_autopilot_app, "autopilot"),
|
|
@@ -210,6 +212,9 @@ for _sub_app, _name in (
|
|
|
210
212
|
add_help_alias(_sub_app)
|
|
211
213
|
app.add_typer(_sub_app, name=_name)
|
|
212
214
|
|
|
215
|
+
# status_app has its own callback for default behavior
|
|
216
|
+
app.add_typer(_status_app, name="status")
|
|
217
|
+
|
|
213
218
|
|
|
214
219
|
# ---------------------------------------------------------------------------
|
|
215
220
|
# Help Command
|
|
@@ -564,6 +564,48 @@ def _resolve_step_widget_for_tool(
|
|
|
564
564
|
return None
|
|
565
565
|
|
|
566
566
|
|
|
567
|
+
def _fallback_ingest_subgraph_tool_on_step_card(
|
|
568
|
+
adapter: TextualUIAdapter,
|
|
569
|
+
router: StepTaskRouter,
|
|
570
|
+
*,
|
|
571
|
+
lookup_id: str,
|
|
572
|
+
display_key: str,
|
|
573
|
+
tool_name: str,
|
|
574
|
+
args: dict[str, Any],
|
|
575
|
+
raw_args: str = "",
|
|
576
|
+
ns_key: tuple[str, ...],
|
|
577
|
+
) -> bool:
|
|
578
|
+
"""Best-effort fallback when namespace routing cannot resolve a parent task.
|
|
579
|
+
|
|
580
|
+
This keeps subgraph tool activity visible on the step card (as orphan rows)
|
|
581
|
+
instead of dropping it entirely when task binding arrives late or never.
|
|
582
|
+
"""
|
|
583
|
+
lookup = str(lookup_id or "").strip()
|
|
584
|
+
display = str(display_key or "").strip()
|
|
585
|
+
if not lookup:
|
|
586
|
+
return False
|
|
587
|
+
if is_inner_subgraph_task_tool_id(lookup):
|
|
588
|
+
return False
|
|
589
|
+
parsed_sid, type_code, _, _ = parse_unified_tool_call_id(lookup)
|
|
590
|
+
bound_step_id = parsed_sid or router.step_id_for_tool(lookup)
|
|
591
|
+
step_w = _resolve_step_widget_for_tool(
|
|
592
|
+
adapter,
|
|
593
|
+
router,
|
|
594
|
+
bound_step_id=bound_step_id,
|
|
595
|
+
ns_key=ns_key,
|
|
596
|
+
)
|
|
597
|
+
if step_w is None:
|
|
598
|
+
return False
|
|
599
|
+
row_id = lookup if type_code == "t" else (display or lookup)
|
|
600
|
+
if step_w.has_tool_call_row(row_id):
|
|
601
|
+
step_w.update_tool_args(row_id, args)
|
|
602
|
+
else:
|
|
603
|
+
step_w.add_tool_call(row_id, tool_name, args, raw_args=raw_args)
|
|
604
|
+
adapter._tool_to_step[row_id] = step_w
|
|
605
|
+
adapter._tool_display_by_call_id[row_id] = step_w
|
|
606
|
+
return True
|
|
607
|
+
|
|
608
|
+
|
|
567
609
|
async def sync_pending_step_cards_from_plan(
|
|
568
610
|
adapter: TextualUIAdapter,
|
|
569
611
|
*,
|
|
@@ -912,7 +954,7 @@ async def apply_tool_call_wire_update(
|
|
|
912
954
|
|
|
913
955
|
_merge_buf, display_key = canonical_subgraph_tool_ids(ns_key, tcid, task_scope=ts)
|
|
914
956
|
if display_key:
|
|
915
|
-
router.try_route_subgraph_tool(
|
|
957
|
+
routed = router.try_route_subgraph_tool(
|
|
916
958
|
ns_key=ns_key,
|
|
917
959
|
lookup_id=tcid,
|
|
918
960
|
display_key=display_key,
|
|
@@ -922,6 +964,16 @@ async def apply_tool_call_wire_update(
|
|
|
922
964
|
tool_to_step=adapter._tool_to_step,
|
|
923
965
|
tool_display_by_call_id=adapter._tool_display_by_call_id,
|
|
924
966
|
)
|
|
967
|
+
if not routed:
|
|
968
|
+
_fallback_ingest_subgraph_tool_on_step_card(
|
|
969
|
+
adapter,
|
|
970
|
+
router,
|
|
971
|
+
lookup_id=tcid,
|
|
972
|
+
display_key=display_key,
|
|
973
|
+
tool_name=name,
|
|
974
|
+
args=display_args,
|
|
975
|
+
ns_key=ns_key,
|
|
976
|
+
)
|
|
925
977
|
return True
|
|
926
978
|
|
|
927
979
|
|
|
@@ -2344,7 +2396,7 @@ async def execute_task_textual(
|
|
|
2344
2396
|
ns_key, str(lookup_id), task_scope=ts_disp
|
|
2345
2397
|
)
|
|
2346
2398
|
display_key = display_key or str(lookup_id)
|
|
2347
|
-
router.try_route_subgraph_tool(
|
|
2399
|
+
routed = router.try_route_subgraph_tool(
|
|
2348
2400
|
ns_key=ns_key,
|
|
2349
2401
|
lookup_id=str(lookup_id),
|
|
2350
2402
|
display_key=display_key,
|
|
@@ -2355,6 +2407,17 @@ async def execute_task_textual(
|
|
|
2355
2407
|
tool_to_step=adapter._tool_to_step,
|
|
2356
2408
|
tool_display_by_call_id=adapter._tool_display_by_call_id,
|
|
2357
2409
|
)
|
|
2410
|
+
if not routed:
|
|
2411
|
+
_fallback_ingest_subgraph_tool_on_step_card(
|
|
2412
|
+
adapter,
|
|
2413
|
+
router,
|
|
2414
|
+
lookup_id=str(lookup_id),
|
|
2415
|
+
display_key=display_key,
|
|
2416
|
+
tool_name=buffer_name,
|
|
2417
|
+
args=parsed_args,
|
|
2418
|
+
raw_args=raw_args_stream,
|
|
2419
|
+
ns_key=ns_key,
|
|
2420
|
+
)
|
|
2358
2421
|
|
|
2359
2422
|
tool_call_buffers.pop(buffer_key, None)
|
|
2360
2423
|
|
|
@@ -1511,6 +1511,8 @@ class CognitionStepMessage(Vertical):
|
|
|
1511
1511
|
return True
|
|
1512
1512
|
if self._iter_task_delegation_rows():
|
|
1513
1513
|
return True
|
|
1514
|
+
if self._orphan_subgraph_tool_rows_for_preview():
|
|
1515
|
+
return True
|
|
1514
1516
|
return bool(self._main_agent_tool_rows_for_preview())
|
|
1515
1517
|
|
|
1516
1518
|
@staticmethod
|
|
@@ -1529,6 +1531,43 @@ class CognitionStepMessage(Vertical):
|
|
|
1529
1531
|
"""Direct main-agent tool rows (excludes task delegations and subgraph tools)."""
|
|
1530
1532
|
return [r for r in self._rows if self._row_counts_for_step_status_line(r)]
|
|
1531
1533
|
|
|
1534
|
+
def _orphan_subgraph_tool_rows_for_preview(self) -> list[_StepToolRow]:
|
|
1535
|
+
"""Subgraph tool rows whose parent task delegation row is missing.
|
|
1536
|
+
|
|
1537
|
+
Some streams deliver ``t`` tool rows before/without a visible ``s:task`` row.
|
|
1538
|
+
Keep these rows visible on the step card so users still see tool activity.
|
|
1539
|
+
"""
|
|
1540
|
+
task_parent_ids: set[str] = set()
|
|
1541
|
+
for task_row in self._iter_task_delegation_rows():
|
|
1542
|
+
key = self._task_delegation_dedupe_key(task_row)
|
|
1543
|
+
if key:
|
|
1544
|
+
task_parent_ids.add(key)
|
|
1545
|
+
|
|
1546
|
+
out: list[_StepToolRow] = []
|
|
1547
|
+
for row in self._rows:
|
|
1548
|
+
if row.is_task_row:
|
|
1549
|
+
continue
|
|
1550
|
+
if not self._row_belongs_to_step(row):
|
|
1551
|
+
continue
|
|
1552
|
+
tcid = str(row.tool_call_id or "").strip()
|
|
1553
|
+
if not tcid:
|
|
1554
|
+
continue
|
|
1555
|
+
if is_step_level_task_tool_id(tcid) or is_inner_subgraph_task_tool_id(tcid):
|
|
1556
|
+
continue
|
|
1557
|
+
parsed_sid, type_code, _, _ = parse_unified_tool_call_id(tcid)
|
|
1558
|
+
if parsed_sid and parsed_sid != self._step_id:
|
|
1559
|
+
continue
|
|
1560
|
+
|
|
1561
|
+
parent_id = str(row.parent_tool_call_id or "").strip()
|
|
1562
|
+
if parent_id and is_step_level_task_tool_id(parent_id):
|
|
1563
|
+
parent_id = normalize_step_task_tool_call_id(self._step_id, parent_id)
|
|
1564
|
+
has_visible_parent = bool(parent_id and parent_id in task_parent_ids)
|
|
1565
|
+
|
|
1566
|
+
# Keep unresolved/unparented task-subgraph tool rows visible.
|
|
1567
|
+
if type_code == "t" and not has_visible_parent:
|
|
1568
|
+
out.append(row)
|
|
1569
|
+
return out
|
|
1570
|
+
|
|
1532
1571
|
def _append_tool_activity_lines(
|
|
1533
1572
|
self,
|
|
1534
1573
|
parts: list[object],
|
|
@@ -1927,7 +1966,8 @@ class CognitionStepMessage(Vertical):
|
|
|
1927
1966
|
|
|
1928
1967
|
task_rows = self._iter_task_delegation_rows()
|
|
1929
1968
|
main_preview = self._latest_preview_rows(self._main_agent_tool_rows_for_preview())
|
|
1930
|
-
|
|
1969
|
+
orphan_preview = self._latest_preview_rows(self._orphan_subgraph_tool_rows_for_preview())
|
|
1970
|
+
if not task_rows and not main_preview and not orphan_preview and not self._subagent_notes:
|
|
1931
1971
|
if not self._subagent_notes_by_task:
|
|
1932
1972
|
return Content("")
|
|
1933
1973
|
|
|
@@ -2009,6 +2049,17 @@ class CognitionStepMessage(Vertical):
|
|
|
2009
2049
|
animate_running=self._status == "running",
|
|
2010
2050
|
)
|
|
2011
2051
|
|
|
2052
|
+
if orphan_preview:
|
|
2053
|
+
first_block = False
|
|
2054
|
+
self._append_tool_activity_lines(
|
|
2055
|
+
parts,
|
|
2056
|
+
orphan_preview,
|
|
2057
|
+
gutter=branch_gutter,
|
|
2058
|
+
g=g,
|
|
2059
|
+
colors=colors,
|
|
2060
|
+
animate_running=self._status == "running",
|
|
2061
|
+
)
|
|
2062
|
+
|
|
2012
2063
|
for note in self._subagent_notes:
|
|
2013
2064
|
t = (note or "").strip()
|
|
2014
2065
|
if not t:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/runtime/presentation/async_renderer_protocol.py
RENAMED
|
File without changes
|
{soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/runtime/presentation/duration_format.py
RENAMED
|
File without changes
|
|
File without changes
|
{soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/runtime/presentation/explore_task_display.py
RENAMED
|
File without changes
|
|
File without changes
|
{soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/runtime/presentation/renderer_protocol.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|