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.
Files changed (114) hide show
  1. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/.gitignore +0 -1
  2. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/PKG-INFO +1 -1
  3. soothe_cli-0.6.7/src/soothe_cli/cli/commands/status_cmd.py +367 -0
  4. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/cli/main.py +5 -0
  5. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/textual_adapter.py +65 -2
  6. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/widgets/messages.py +52 -1
  7. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/README.md +0 -0
  8. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/pyproject.toml +0 -0
  9. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/__init__.py +0 -0
  10. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/cli/__init__.py +0 -0
  11. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/cli/commands/__init__.py +0 -0
  12. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/cli/commands/autopilot_cmd.py +0 -0
  13. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/cli/commands/loop_cmd.py +0 -0
  14. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/cli/commands/run_cmd.py +0 -0
  15. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/cli/execution/__init__.py +0 -0
  16. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/cli/execution/daemon.py +0 -0
  17. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/cli/execution/daemon_errors.py +0 -0
  18. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/cli/execution/headless.py +0 -0
  19. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/cli/execution/headless_renderer.py +0 -0
  20. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/cli/execution/launcher.py +0 -0
  21. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/config/__init__.py +0 -0
  22. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/config/cli_config.py +0 -0
  23. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/config/loader.py +0 -0
  24. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/config/logging_setup.py +0 -0
  25. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/runtime/__init__.py +0 -0
  26. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/runtime/headless/processor.py +0 -0
  27. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/runtime/headless/processor_state.py +0 -0
  28. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/runtime/parse/_utils.py +0 -0
  29. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/runtime/parse/message_processing.py +0 -0
  30. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/runtime/parse/tool_call_resolution.py +0 -0
  31. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/runtime/parse/tool_message_format.py +0 -0
  32. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/runtime/parse/tool_result.py +0 -0
  33. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/runtime/policy/display_policy.py +0 -0
  34. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/runtime/policy/essential_events.py +0 -0
  35. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/runtime/policy/tui_trace_log.py +0 -0
  36. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/runtime/presentation/async_renderer_protocol.py +0 -0
  37. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/runtime/presentation/duration_format.py +0 -0
  38. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/runtime/presentation/engine.py +0 -0
  39. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/runtime/presentation/explore_task_display.py +0 -0
  40. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/runtime/presentation/renderer_base.py +0 -0
  41. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/runtime/presentation/renderer_protocol.py +0 -0
  42. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/runtime/state/file_tracker.py +0 -0
  43. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/runtime/state/session_stats.py +0 -0
  44. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/runtime/state/step_router.py +0 -0
  45. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/runtime/state/stream_accumulator.py +0 -0
  46. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/runtime/state/transcript.py +0 -0
  47. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/runtime/task_scope.py +0 -0
  48. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/runtime/transport/session.py +0 -0
  49. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/runtime/turn/pipeline.py +0 -0
  50. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/runtime/turn/prepare.py +0 -0
  51. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/runtime/wire/chunk_filter.py +0 -0
  52. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/runtime/wire/display_text.py +0 -0
  53. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/runtime/wire/message_text.py +0 -0
  54. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/runtime/wire/messages.py +0 -0
  55. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/__init__.py +0 -0
  56. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/_cli_context.py +0 -0
  57. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/_env_vars.py +0 -0
  58. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/_version.py +0 -0
  59. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/app/__init__.py +0 -0
  60. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/app/_app.py +0 -0
  61. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/app/_commands.py +0 -0
  62. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/app/_execution.py +0 -0
  63. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/app/_history.py +0 -0
  64. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/app/_messages_mixin.py +0 -0
  65. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/app/_model.py +0 -0
  66. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/app/_module_init.py +0 -0
  67. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/app/_startup.py +0 -0
  68. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/app/_ui.py +0 -0
  69. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/app/app.tcss +0 -0
  70. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/binding.py +0 -0
  71. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/command_registry.py +0 -0
  72. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/commands/__init__.py +0 -0
  73. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/commands/command_router.py +0 -0
  74. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/commands/slash_commands.py +0 -0
  75. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/commands/subagent_routing.py +0 -0
  76. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/config.py +0 -0
  77. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/file_change_notify.py +0 -0
  78. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/file_change_renderers.py +0 -0
  79. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/hooks.py +0 -0
  80. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/input.py +0 -0
  81. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/media_utils.py +0 -0
  82. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/model_config.py +0 -0
  83. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/path_utils.py +0 -0
  84. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/preview_limits.py +0 -0
  85. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/project_utils.py +0 -0
  86. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/sessions.py +0 -0
  87. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/skills/__init__.py +0 -0
  88. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/skills/invocation.py +0 -0
  89. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/skills/load.py +0 -0
  90. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/theme.py +0 -0
  91. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/tips.py +0 -0
  92. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/tool_display.py +0 -0
  93. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/unicode_security.py +0 -0
  94. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/update_check.py +0 -0
  95. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/widgets/__init__.py +0 -0
  96. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/widgets/_links.py +0 -0
  97. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/widgets/autocomplete.py +0 -0
  98. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/widgets/autopilot_dashboard.py +0 -0
  99. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/widgets/autopilot_screen.py +0 -0
  100. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/widgets/chat_input.py +0 -0
  101. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/widgets/clipboard.py +0 -0
  102. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/widgets/diff.py +0 -0
  103. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/widgets/editor.py +0 -0
  104. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/widgets/file_change_preview.py +0 -0
  105. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/widgets/history.py +0 -0
  106. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/widgets/loading.py +0 -0
  107. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/widgets/loop_selector.py +0 -0
  108. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/widgets/mcp_viewer.py +0 -0
  109. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/widgets/message_store.py +0 -0
  110. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/widgets/model_selector.py +0 -0
  111. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/widgets/notification_settings.py +0 -0
  112. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/widgets/status.py +0 -0
  113. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/widgets/theme_selector.py +0 -0
  114. {soothe_cli-0.6.5 → soothe_cli-0.6.7}/src/soothe_cli/tui/widgets/welcome.py +0 -0
@@ -217,5 +217,4 @@ _bmad
217
217
  __MACOSX
218
218
  .qoder
219
219
  .soothe
220
- deploy/config.yml
221
220
  .backups/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: soothe-cli
3
- Version: 0.6.5
3
+ Version: 0.6.7
4
4
  Summary: Soothe CLI client - communicates with daemon via WebSocket
5
5
  Project-URL: Homepage, https://github.com/mirasoth/soothe
6
6
  Project-URL: Documentation, https://soothe.readthedocs.io
@@ -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
- if not task_rows and not main_preview and not self._subagent_notes:
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