soothe-cli 0.6.8__tar.gz → 0.6.10__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.8 → soothe_cli-0.6.10}/PKG-INFO +1 -1
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/cli/commands/autopilot_cmd.py +74 -13
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/cli/commands/status_cmd.py +52 -67
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/runtime/parse/tool_call_resolution.py +34 -2
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/runtime/state/step_router.py +48 -9
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/runtime/transport/session.py +8 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/tui/app/_app.py +1 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/tui/app/_execution.py +3 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/tui/app/_model.py +57 -1
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/tui/app/_startup.py +7 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/tui/commands/slash_commands.py +7 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/tui/input.py +37 -26
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/tui/preview_limits.py +2 -2
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/tui/textual_adapter.py +89 -33
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/tui/tool_display.py +17 -3
- soothe_cli-0.6.10/src/soothe_cli/tui/widgets/autopilot_dashboard.py +614 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/tui/widgets/autopilot_screen.py +4 -3
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/tui/widgets/chat_input.py +37 -5
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/tui/widgets/loading.py +39 -78
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/tui/widgets/messages.py +118 -8
- soothe_cli-0.6.8/src/soothe_cli/tui/widgets/autopilot_dashboard.py +0 -318
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/.gitignore +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/README.md +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/pyproject.toml +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/__init__.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/cli/__init__.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/cli/commands/__init__.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/cli/commands/loop_cmd.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/cli/commands/run_cmd.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/cli/execution/__init__.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/cli/execution/daemon.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/cli/execution/daemon_errors.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/cli/execution/headless.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/cli/execution/headless_renderer.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/cli/execution/launcher.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/cli/main.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/config/__init__.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/config/cli_config.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/config/loader.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/config/logging_setup.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/runtime/__init__.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/runtime/headless/processor.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/runtime/headless/processor_state.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/runtime/parse/_utils.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/runtime/parse/message_processing.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/runtime/parse/tool_message_format.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/runtime/parse/tool_result.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/runtime/policy/display_policy.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/runtime/policy/essential_events.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/runtime/policy/tui_trace_log.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/runtime/presentation/async_renderer_protocol.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/runtime/presentation/duration_format.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/runtime/presentation/engine.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/runtime/presentation/explore_task_display.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/runtime/presentation/renderer_base.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/runtime/presentation/renderer_protocol.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/runtime/state/file_tracker.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/runtime/state/session_stats.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/runtime/state/stream_accumulator.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/runtime/state/transcript.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/runtime/task_scope.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/runtime/turn/pipeline.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/runtime/turn/prepare.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/runtime/wire/chunk_filter.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/runtime/wire/display_text.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/runtime/wire/message_text.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/runtime/wire/messages.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/tui/__init__.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/tui/_cli_context.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/tui/_env_vars.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/tui/_version.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/tui/app/__init__.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/tui/app/_commands.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/tui/app/_history.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/tui/app/_messages_mixin.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/tui/app/_module_init.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/tui/app/_ui.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/tui/app/app.tcss +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/tui/binding.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/tui/command_registry.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/tui/commands/__init__.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/tui/commands/command_router.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/tui/commands/subagent_routing.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/tui/config.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/tui/file_change_notify.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/tui/file_change_renderers.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/tui/hooks.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/tui/media_utils.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/tui/model_config.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/tui/path_utils.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/tui/project_utils.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/tui/sessions.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/tui/skills/__init__.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/tui/skills/invocation.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/tui/skills/load.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/tui/theme.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/tui/tips.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/tui/unicode_security.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/tui/update_check.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/tui/widgets/__init__.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/tui/widgets/_links.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/tui/widgets/autocomplete.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/tui/widgets/clipboard.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/tui/widgets/diff.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/tui/widgets/editor.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/tui/widgets/file_change_preview.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/tui/widgets/history.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/tui/widgets/loop_selector.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/tui/widgets/mcp_viewer.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/tui/widgets/message_store.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/tui/widgets/model_selector.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/tui/widgets/notification_settings.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/tui/widgets/status.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/tui/widgets/theme_selector.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.10}/src/soothe_cli/tui/widgets/welcome.py +0 -0
|
@@ -10,6 +10,7 @@ import asyncio
|
|
|
10
10
|
import os
|
|
11
11
|
import sys
|
|
12
12
|
import time
|
|
13
|
+
from collections import Counter
|
|
13
14
|
from pathlib import Path
|
|
14
15
|
|
|
15
16
|
import typer
|
|
@@ -124,12 +125,40 @@ def submit(
|
|
|
124
125
|
|
|
125
126
|
@app.command("status")
|
|
126
127
|
def status() -> None:
|
|
127
|
-
"""Show overall autopilot state
|
|
128
|
+
"""Show overall autopilot state and goal DAG summary."""
|
|
128
129
|
client = _require_daemon_http()
|
|
129
130
|
data = client.status()
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
131
|
+
state = data.get("state", data.get("status", "unknown"))
|
|
132
|
+
running = data.get("running", False)
|
|
133
|
+
dreaming = data.get("dreaming", False)
|
|
134
|
+
typer.echo(f"Autopilot state: {state}")
|
|
135
|
+
typer.echo(f"Scheduling loop: {'running' if running else 'stopped'}")
|
|
136
|
+
if dreaming:
|
|
137
|
+
typer.echo("Dreaming: yes")
|
|
138
|
+
loop_pool = data.get("loop_pool")
|
|
139
|
+
if isinstance(loop_pool, dict) and loop_pool:
|
|
140
|
+
typer.echo(f"Worker pool: {loop_pool}")
|
|
141
|
+
|
|
142
|
+
jobs = client.list_jobs().get("jobs") or []
|
|
143
|
+
goals = client.list_goals().get("goals") or []
|
|
144
|
+
typer.echo(f"\nJobs (root goals): {len(jobs)}")
|
|
145
|
+
if goals:
|
|
146
|
+
counts = Counter(str(g.get("status", "pending")) for g in goals)
|
|
147
|
+
typer.echo(f"Goals in DAG: {len(goals)}")
|
|
148
|
+
for stat, count in sorted(counts.items()):
|
|
149
|
+
typer.echo(f" {stat}: {count}")
|
|
150
|
+
elif not jobs:
|
|
151
|
+
typer.echo("Goals in DAG: 0")
|
|
152
|
+
|
|
153
|
+
if jobs:
|
|
154
|
+
typer.echo("\nJobs:")
|
|
155
|
+
for j in jobs:
|
|
156
|
+
jid = str(j.get("id", "?"))
|
|
157
|
+
sid = jid[:8]
|
|
158
|
+
sstat = j.get("status", "pending")
|
|
159
|
+
sdesc = preview_first(j.get("description", ""), 50)
|
|
160
|
+
typer.echo(f" [{sid}] {sstat:10s} {sdesc}")
|
|
161
|
+
typer.echo("\nFull DAG for a job: soothe autopilot job <job_id>")
|
|
133
162
|
|
|
134
163
|
|
|
135
164
|
@app.command("list")
|
|
@@ -139,8 +168,20 @@ def list_jobs(
|
|
|
139
168
|
"""List jobs (root goals) from the daemon autopilot.
|
|
140
169
|
|
|
141
170
|
Jobs are user-submitted tasks. Subgoals created during autonomous
|
|
142
|
-
execution are not shown here; use
|
|
171
|
+
execution are not shown here; use ``goals`` or ``goal <id>`` for details.
|
|
143
172
|
"""
|
|
173
|
+
_list_jobs_impl(status_filter)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@app.command("jobs")
|
|
177
|
+
def list_jobs_alias(
|
|
178
|
+
status_filter: str = typer.Option("", "--status", "-s", help="Filter by status."),
|
|
179
|
+
) -> None:
|
|
180
|
+
"""Alias for ``list`` — list root autopilot jobs."""
|
|
181
|
+
_list_jobs_impl(status_filter)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _list_jobs_impl(status_filter: str) -> None:
|
|
144
185
|
client = _require_daemon_http()
|
|
145
186
|
payload = client.list_jobs()
|
|
146
187
|
jobs = payload.get("jobs") or []
|
|
@@ -158,6 +199,29 @@ def list_jobs(
|
|
|
158
199
|
typer.echo(f" [{sid}] {sstat:10s} pri={spri:3d} {sdesc}")
|
|
159
200
|
|
|
160
201
|
|
|
202
|
+
@app.command("goals")
|
|
203
|
+
def list_goals(
|
|
204
|
+
status_filter: str = typer.Option("", "--status", "-s", help="Filter by status."),
|
|
205
|
+
) -> None:
|
|
206
|
+
"""List all goals in the daemon autopilot DAG (including subgoals)."""
|
|
207
|
+
client = _require_daemon_http()
|
|
208
|
+
payload = client.list_goals()
|
|
209
|
+
goals = payload.get("goals") or []
|
|
210
|
+
if not goals:
|
|
211
|
+
typer.echo("No goals found.")
|
|
212
|
+
return
|
|
213
|
+
|
|
214
|
+
for g in goals:
|
|
215
|
+
if status_filter and g.get("status", "") != status_filter:
|
|
216
|
+
continue
|
|
217
|
+
gid = str(g.get("id", "?"))[:8]
|
|
218
|
+
parent = g.get("parent_id")
|
|
219
|
+
parent_s = f" parent={str(parent)[:8]}" if parent else ""
|
|
220
|
+
desc = preview_first(g.get("description", ""), 50)
|
|
221
|
+
stat = g.get("status", "pending")
|
|
222
|
+
typer.echo(f" [{gid}] {stat:10s}{parent_s} {desc}")
|
|
223
|
+
|
|
224
|
+
|
|
161
225
|
def _render_dag_tree(dag: dict, root_id: str) -> None:
|
|
162
226
|
"""Render DAG as ASCII tree for job visualization."""
|
|
163
227
|
nodes = {n["id"]: n for n in dag.get("nodes", [])}
|
|
@@ -199,9 +263,9 @@ def _render_dag_tree(dag: dict, root_id: str) -> None:
|
|
|
199
263
|
|
|
200
264
|
@app.command("job")
|
|
201
265
|
def show_job(
|
|
202
|
-
job_id: str = typer.Argument(..., help="Job ID to show details and DAG."),
|
|
266
|
+
job_id: str = typer.Argument(..., help="Job ID to show details and goal DAG."),
|
|
203
267
|
) -> None:
|
|
204
|
-
"""Show job status and DAG tree visualization.
|
|
268
|
+
"""Show job status and goal DAG tree visualization.
|
|
205
269
|
|
|
206
270
|
A job is a root goal submitted by the user. This command shows
|
|
207
271
|
the job's details and the complete goal DAG under it.
|
|
@@ -211,7 +275,7 @@ def show_job(
|
|
|
211
275
|
payload = client.get_job(job_id)
|
|
212
276
|
except RuntimeError as exc:
|
|
213
277
|
typer.echo(str(exc), err=True)
|
|
214
|
-
raise typer.Exit(1)
|
|
278
|
+
raise typer.Exit(1) from exc
|
|
215
279
|
|
|
216
280
|
job = payload.get("job")
|
|
217
281
|
dag = payload.get("dag")
|
|
@@ -228,11 +292,9 @@ def show_job(
|
|
|
228
292
|
typer.echo(f"Workspace: {job['workspace']}")
|
|
229
293
|
created = job.get("created_at", "")
|
|
230
294
|
if created:
|
|
231
|
-
# Truncate timestamp for readability
|
|
232
295
|
created_short = created[:19] if len(created) > 19 else created
|
|
233
296
|
typer.echo(f"Created: {created_short}")
|
|
234
297
|
|
|
235
|
-
# DAG stats from response
|
|
236
298
|
active = payload.get("active_goals", 0)
|
|
237
299
|
completed = payload.get("completed_goals", 0)
|
|
238
300
|
total = payload.get("total_goals", 0)
|
|
@@ -240,10 +302,9 @@ def show_job(
|
|
|
240
302
|
typer.echo(f"Completed goals: {completed}")
|
|
241
303
|
typer.echo(f"Total goals: {total}")
|
|
242
304
|
|
|
243
|
-
|
|
244
|
-
typer.echo("\nDAG:")
|
|
305
|
+
typer.echo("\nGoal DAG:")
|
|
245
306
|
if dag:
|
|
246
|
-
_render_dag_tree(dag, job_id)
|
|
307
|
+
_render_dag_tree(dag, str(job.get("id") or job_id))
|
|
247
308
|
else:
|
|
248
309
|
typer.echo(" (no subgoals)")
|
|
249
310
|
|
|
@@ -74,39 +74,41 @@ async def _fetch_ready_state(ws_url: str, timeout: float = 5.0) -> dict[str, Any
|
|
|
74
74
|
return None
|
|
75
75
|
|
|
76
76
|
|
|
77
|
-
def
|
|
78
|
-
|
|
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(
|
|
77
|
+
def _render_unified_status_table(
|
|
78
|
+
config: Any,
|
|
92
79
|
ws_url: str,
|
|
93
|
-
running: bool,
|
|
94
|
-
port_live: bool,
|
|
95
|
-
active_threads: int,
|
|
96
|
-
daemon_pid: int | None,
|
|
80
|
+
running: bool | None = None,
|
|
81
|
+
port_live: bool | None = None,
|
|
82
|
+
active_threads: int | None = None,
|
|
83
|
+
daemon_pid: int | None = None,
|
|
97
84
|
ready_state: dict[str, Any] | None = None,
|
|
85
|
+
daemon_live: bool = True,
|
|
98
86
|
) -> Table:
|
|
99
|
-
"""Render
|
|
100
|
-
|
|
87
|
+
"""Render unified status table without duplicated info.
|
|
88
|
+
|
|
89
|
+
Sections:
|
|
90
|
+
- Connection: WebSocket URL, Soothe Home
|
|
91
|
+
- Daemon: Running, Threads, PID (only when daemon is live)
|
|
92
|
+
"""
|
|
93
|
+
table = Table(title="Soothe Status")
|
|
94
|
+
table.add_column("Section", style="dim", width=12)
|
|
101
95
|
table.add_column("Setting", style="cyan")
|
|
102
96
|
table.add_column("Value", style="green")
|
|
103
97
|
|
|
104
|
-
|
|
105
|
-
table.add_row("
|
|
106
|
-
table.add_row("
|
|
107
|
-
|
|
98
|
+
# Connection section
|
|
99
|
+
table.add_row("Connection", "WebSocket URL", ws_url)
|
|
100
|
+
table.add_row("", "Soothe Home", str(config.soothe_home))
|
|
101
|
+
|
|
102
|
+
# Daemon section
|
|
103
|
+
if not daemon_live:
|
|
104
|
+
table.add_row("Daemon", "Status", "[red]Not running[/red]")
|
|
105
|
+
return table
|
|
106
|
+
|
|
107
|
+
table.add_row("Daemon", "Status", "[green]Running[/green]")
|
|
108
108
|
if daemon_pid:
|
|
109
|
-
table.add_row("
|
|
109
|
+
table.add_row("", "PID", str(daemon_pid))
|
|
110
|
+
if active_threads is not None:
|
|
111
|
+
table.add_row("", "Active Threads", str(active_threads))
|
|
110
112
|
|
|
111
113
|
if ready_state:
|
|
112
114
|
state = ready_state.get("state", "unknown")
|
|
@@ -118,9 +120,9 @@ def _render_daemon_table(
|
|
|
118
120
|
"warming": "blue",
|
|
119
121
|
"stopped": "dim",
|
|
120
122
|
}.get(state, "white")
|
|
121
|
-
table.add_row("Readiness", f"[{state_color}]{state}[/{state_color}]")
|
|
123
|
+
table.add_row("", "Readiness", f"[{state_color}]{state}[/{state_color}]")
|
|
122
124
|
if ready_state.get("message"):
|
|
123
|
-
table.add_row("Message", ready_state["message"])
|
|
125
|
+
table.add_row("", "Message", ready_state["message"])
|
|
124
126
|
|
|
125
127
|
return table
|
|
126
128
|
|
|
@@ -163,15 +165,9 @@ def daemon_status(
|
|
|
163
165
|
)
|
|
164
166
|
)
|
|
165
167
|
else:
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
"Status: [red]Not running[/red]\n"
|
|
170
|
-
"Hint: Start with 'soothed start'",
|
|
171
|
-
title="Daemon Status",
|
|
172
|
-
border_style="red",
|
|
173
|
-
)
|
|
174
|
-
)
|
|
168
|
+
table = _render_unified_status_table(config, ws_url, daemon_live=False)
|
|
169
|
+
console.print(table)
|
|
170
|
+
console.print("\n[dim]Hint: Start with 'soothed start'[/dim]")
|
|
175
171
|
sys.exit(1)
|
|
176
172
|
|
|
177
173
|
# Fetch detailed status
|
|
@@ -218,14 +214,14 @@ def daemon_status(
|
|
|
218
214
|
console.print_json(json.dumps(output))
|
|
219
215
|
return
|
|
220
216
|
|
|
221
|
-
# Render daemon status table
|
|
217
|
+
# Render unified daemon status table
|
|
222
218
|
running = status.get("running", True)
|
|
223
219
|
port_live = status.get("port_live", True)
|
|
224
220
|
active_threads = status.get("active_threads", 0)
|
|
225
221
|
daemon_pid = status.get("daemon_pid")
|
|
226
222
|
|
|
227
|
-
table =
|
|
228
|
-
ws_url, running, port_live, active_threads, daemon_pid, ready_state
|
|
223
|
+
table = _render_unified_status_table(
|
|
224
|
+
config, ws_url, running, port_live, active_threads, daemon_pid, ready_state
|
|
229
225
|
)
|
|
230
226
|
console.print(table)
|
|
231
227
|
|
|
@@ -261,7 +257,12 @@ def connection_status(
|
|
|
261
257
|
)
|
|
262
258
|
return
|
|
263
259
|
|
|
264
|
-
|
|
260
|
+
# Simple connection table
|
|
261
|
+
table = Table(title="Connection Settings")
|
|
262
|
+
table.add_column("Setting", style="cyan")
|
|
263
|
+
table.add_column("Value", style="green")
|
|
264
|
+
table.add_row("WebSocket URL", ws_url)
|
|
265
|
+
table.add_row("Soothe Home", str(config.soothe_home))
|
|
265
266
|
console.print(table)
|
|
266
267
|
|
|
267
268
|
|
|
@@ -313,20 +314,11 @@ def status_main(
|
|
|
313
314
|
console.print_json(json.dumps(output))
|
|
314
315
|
return
|
|
315
316
|
|
|
316
|
-
# Render
|
|
317
|
+
# Render unified status table
|
|
317
318
|
if not live:
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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
|
-
)
|
|
319
|
+
table = _render_unified_status_table(config, ws_url, daemon_live=False)
|
|
320
|
+
console.print(table)
|
|
321
|
+
console.print("\n[dim]Hint: Start with 'soothed start'[/dim]")
|
|
330
322
|
sys.exit(1)
|
|
331
323
|
|
|
332
324
|
# Fetch detailed daemon status
|
|
@@ -335,29 +327,22 @@ def status_main(
|
|
|
335
327
|
if "error" in status:
|
|
336
328
|
console.print(
|
|
337
329
|
Panel(
|
|
338
|
-
f"WebSocket URL: {ws_url}\
|
|
339
|
-
|
|
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",
|
|
330
|
+
f"WebSocket URL: {ws_url}\nError: [red]{status['error']}[/red]",
|
|
331
|
+
title="Daemon Status",
|
|
345
332
|
border_style="red",
|
|
346
333
|
)
|
|
347
334
|
)
|
|
348
335
|
sys.exit(1)
|
|
349
336
|
|
|
350
|
-
# Render both tables
|
|
351
|
-
connection_table = _render_connection_table(config, ws_url)
|
|
352
|
-
console.print(connection_table)
|
|
353
|
-
|
|
354
337
|
running = status.get("running", True)
|
|
355
338
|
port_live = status.get("port_live", True)
|
|
356
339
|
active_threads = status.get("active_threads", 0)
|
|
357
340
|
daemon_pid = status.get("daemon_pid")
|
|
358
341
|
|
|
359
|
-
|
|
360
|
-
|
|
342
|
+
table = _render_unified_status_table(
|
|
343
|
+
config, ws_url, running, port_live, active_threads, daemon_pid
|
|
344
|
+
)
|
|
345
|
+
console.print(table)
|
|
361
346
|
|
|
362
347
|
|
|
363
348
|
__all__ = [
|
|
@@ -39,6 +39,18 @@ def tool_args_meaningful(raw: Any) -> bool:
|
|
|
39
39
|
return True
|
|
40
40
|
|
|
41
41
|
|
|
42
|
+
def is_execute_step_namespace(ns_key: tuple[str, ...]) -> bool:
|
|
43
|
+
"""True for CoreAgent execute root namespace (``execute:{run_id}``), not nested ``tools:`` subgraphs."""
|
|
44
|
+
if len(ns_key) != 1:
|
|
45
|
+
return False
|
|
46
|
+
return str(ns_key[0] or "").startswith("execute:")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def is_step_card_tool_scope(*, ns_key: tuple[str, ...]) -> bool:
|
|
50
|
+
"""True when tool activity belongs on the step card as main execute-graph tools."""
|
|
51
|
+
return ns_key == () or is_execute_step_namespace(ns_key)
|
|
52
|
+
|
|
53
|
+
|
|
42
54
|
def is_main_step_level_tool_call_id(tool_call_id: str) -> bool:
|
|
43
55
|
"""True for unified main-graph step tools (``{step}:s:{tool}:{n}``), not ``task`` rows."""
|
|
44
56
|
from soothe_sdk.ux.task_namespace import is_step_level_task_tool_id, parse_unified_tool_call_id
|
|
@@ -52,9 +64,23 @@ def is_main_step_level_tool_call_id(tool_call_id: str) -> bool:
|
|
|
52
64
|
return not is_step_level_task_tool_id(tcid)
|
|
53
65
|
|
|
54
66
|
|
|
67
|
+
def is_task_level_subgraph_tool_call_id(tool_call_id: str) -> bool:
|
|
68
|
+
"""True for unified subgraph tools (``{step}:t{n}:{tool}:{seq}``), not nested ``task`` rows."""
|
|
69
|
+
from soothe_sdk.ux.task_namespace import (
|
|
70
|
+
is_inner_subgraph_task_tool_id,
|
|
71
|
+
parse_unified_tool_call_id,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
tcid = str(tool_call_id or "").strip()
|
|
75
|
+
if not tcid or is_inner_subgraph_task_tool_id(tcid):
|
|
76
|
+
return False
|
|
77
|
+
_, type_code, _, _ = parse_unified_tool_call_id(tcid)
|
|
78
|
+
return type_code == "t"
|
|
79
|
+
|
|
80
|
+
|
|
55
81
|
def should_ingest_tool_for_step_stats(
|
|
56
82
|
*,
|
|
57
|
-
|
|
83
|
+
is_step_card_scope: bool,
|
|
58
84
|
tool_name: str,
|
|
59
85
|
tool_call_id: str,
|
|
60
86
|
args_meaningful: bool,
|
|
@@ -70,7 +96,10 @@ def should_ingest_tool_for_step_stats(
|
|
|
70
96
|
return False
|
|
71
97
|
if args_meaningful:
|
|
72
98
|
return True
|
|
73
|
-
|
|
99
|
+
if is_step_card_scope and is_main_step_level_tool_call_id(tcid):
|
|
100
|
+
return True
|
|
101
|
+
# Subgraph explore tools often arrive with ``{"_subgraph_tool": true}`` before real args.
|
|
102
|
+
return not is_step_card_scope and is_task_level_subgraph_tool_call_id(tcid)
|
|
74
103
|
|
|
75
104
|
|
|
76
105
|
def _args_from_toolish_block(block: dict[str, Any]) -> dict[str, Any]:
|
|
@@ -465,7 +494,10 @@ def build_streaming_args_overlay(
|
|
|
465
494
|
__all__ = [
|
|
466
495
|
"ResolvedToolInvocation",
|
|
467
496
|
"build_streaming_args_overlay",
|
|
497
|
+
"is_execute_step_namespace",
|
|
468
498
|
"is_main_step_level_tool_call_id",
|
|
499
|
+
"is_step_card_tool_scope",
|
|
500
|
+
"is_task_level_subgraph_tool_call_id",
|
|
469
501
|
"is_toolish_display_block",
|
|
470
502
|
"materialize_ai_blocks_with_resolved_tools",
|
|
471
503
|
"merge_tool_display_args",
|
|
@@ -17,7 +17,7 @@ from typing import Any, TypeAlias
|
|
|
17
17
|
from soothe_sdk.ux.task_namespace import (
|
|
18
18
|
TaskScope,
|
|
19
19
|
is_inner_subgraph_task_tool_id,
|
|
20
|
-
|
|
20
|
+
normalize_main_task_delegation_id,
|
|
21
21
|
parse_unified_tool_call_id,
|
|
22
22
|
prune_bound_pending_namespaces,
|
|
23
23
|
register_task_spawn_for_step,
|
|
@@ -62,6 +62,21 @@ def _subgraph_pending_key(ns_key: tuple[str, ...], lookup_id: str) -> tuple[tupl
|
|
|
62
62
|
return (ns_key, str(lookup_id).strip())
|
|
63
63
|
|
|
64
64
|
|
|
65
|
+
def _is_task_metadata_subgraph_tool(item: PendingSubgraphTool) -> bool:
|
|
66
|
+
"""True when a buffered subgraph item is task metadata, not a user-facing tool row."""
|
|
67
|
+
if (item.tool_name or "").strip() == "task":
|
|
68
|
+
return True
|
|
69
|
+
for candidate in (item.lookup_id, item.display_key):
|
|
70
|
+
cid = str(candidate or "").strip()
|
|
71
|
+
if cid and is_inner_subgraph_task_tool_id(cid):
|
|
72
|
+
return True
|
|
73
|
+
args = item.args if isinstance(item.args, dict) else {}
|
|
74
|
+
subagent_type = str(args.get("subagent_type") or "").strip()
|
|
75
|
+
prompt = str(args.get("description") or args.get("prompt") or "").strip()
|
|
76
|
+
# Some providers emit opaque names (e.g. "tool-<id>") for task chunks.
|
|
77
|
+
return bool(subagent_type and prompt)
|
|
78
|
+
|
|
79
|
+
|
|
65
80
|
@dataclass
|
|
66
81
|
class StepTaskRouter:
|
|
67
82
|
"""High-performance per-turn router for steps, tools, and task namespaces.
|
|
@@ -123,7 +138,15 @@ class StepTaskRouter:
|
|
|
123
138
|
if existing is None:
|
|
124
139
|
self._pending_subgraph_tools[key] = item
|
|
125
140
|
return
|
|
126
|
-
|
|
141
|
+
# Prefer meaningful args over placeholder metadata like {"_subgraph_tool": true}.
|
|
142
|
+
from soothe_cli.runtime.parse.message_processing import extract_tool_args_dict
|
|
143
|
+
|
|
144
|
+
item_meaningful = extract_tool_args_dict(item.args or {})
|
|
145
|
+
existing_meaningful = extract_tool_args_dict(existing.args or {})
|
|
146
|
+
if len(item_meaningful) >= len(existing_meaningful):
|
|
147
|
+
args = item.args
|
|
148
|
+
else:
|
|
149
|
+
args = existing.args
|
|
127
150
|
raw = item.raw_args if len(item.raw_args) >= len(existing.raw_args) else existing.raw_args
|
|
128
151
|
self._pending_subgraph_tools[key] = PendingSubgraphTool(
|
|
129
152
|
ns_key=item.ns_key,
|
|
@@ -219,14 +242,12 @@ class StepTaskRouter:
|
|
|
219
242
|
if not tcid or is_inner_subgraph_task_tool_id(tcid):
|
|
220
243
|
return False
|
|
221
244
|
parsed_sid, type_code, _, _ = parse_unified_tool_call_id(tcid)
|
|
222
|
-
if type_code
|
|
223
|
-
return False
|
|
224
|
-
sid = parsed_sid if (parsed_sid and type_code == "s") else ""
|
|
245
|
+
sid = parsed_sid if (parsed_sid and type_code in ("s", "t")) else ""
|
|
225
246
|
if not sid:
|
|
226
247
|
sid = str(step_id).strip()
|
|
227
248
|
if not sid:
|
|
228
249
|
return False
|
|
229
|
-
normalized_tcid =
|
|
250
|
+
normalized_tcid = normalize_main_task_delegation_id(sid, tcid, tool_name="task")
|
|
230
251
|
spawn_key = (sid, normalized_tcid)
|
|
231
252
|
if spawn_key in self._spawn_recorded:
|
|
232
253
|
return False
|
|
@@ -375,7 +396,7 @@ class StepTaskRouter:
|
|
|
375
396
|
tool_to_step: dict[str, ParentWidget],
|
|
376
397
|
) -> bool:
|
|
377
398
|
"""Register one subgraph tool row on an already-resolved parent step card."""
|
|
378
|
-
if (item
|
|
399
|
+
if _is_task_metadata_subgraph_tool(item):
|
|
379
400
|
# Inner explore ``task`` chunks are not user-facing tool stats; ingesting
|
|
380
401
|
# them used to rewrite the main ``{step}:s:task:…`` delegation row args.
|
|
381
402
|
return True
|
|
@@ -399,13 +420,31 @@ class StepTaskRouter:
|
|
|
399
420
|
if has_row(row_id):
|
|
400
421
|
update = getattr(parent, "update_tool_args", None)
|
|
401
422
|
if callable(update):
|
|
402
|
-
|
|
423
|
+
resolved_args = dict(item.args or {})
|
|
424
|
+
# Placeholder args like {"_subgraph_tool": true} are not meaningful.
|
|
425
|
+
# Parse raw_args when resolved_args lacks real invocation kwargs.
|
|
426
|
+
from soothe_cli.runtime.parse.message_processing import extract_tool_args_dict
|
|
427
|
+
|
|
428
|
+
meaningful_args = extract_tool_args_dict(resolved_args)
|
|
429
|
+
if item.raw_args and not meaningful_args:
|
|
430
|
+
parsed = extract_tool_args_dict({"_raw": item.raw_args})
|
|
431
|
+
if parsed:
|
|
432
|
+
resolved_args = parsed
|
|
433
|
+
update(row_id, resolved_args)
|
|
403
434
|
else:
|
|
404
435
|
parent_task_id = str(scope[0]).strip()
|
|
436
|
+
resolved_args = dict(item.args or {})
|
|
437
|
+
from soothe_cli.runtime.parse.message_processing import extract_tool_args_dict
|
|
438
|
+
|
|
439
|
+
meaningful_args = extract_tool_args_dict(resolved_args)
|
|
440
|
+
if item.raw_args and not meaningful_args:
|
|
441
|
+
parsed = extract_tool_args_dict({"_raw": item.raw_args})
|
|
442
|
+
if parsed:
|
|
443
|
+
resolved_args = parsed
|
|
405
444
|
ingest(
|
|
406
445
|
row_id,
|
|
407
446
|
item.tool_name,
|
|
408
|
-
|
|
447
|
+
resolved_args,
|
|
409
448
|
raw_args=item.raw_args,
|
|
410
449
|
parent_tool_call_id=parent_task_id or None,
|
|
411
450
|
)
|
|
@@ -45,6 +45,7 @@ class TuiDaemonSession:
|
|
|
45
45
|
self._client = WebSocketClient(url=ws_url)
|
|
46
46
|
self._rpc_client = WebSocketClient(url=ws_url)
|
|
47
47
|
self._loop_id: str | None = None
|
|
48
|
+
self._autopilot_mode: str | None = None
|
|
48
49
|
self._read_lock = asyncio.Lock()
|
|
49
50
|
self._rpc_lock = asyncio.Lock()
|
|
50
51
|
self._rpc_connected = False
|
|
@@ -57,6 +58,11 @@ class TuiDaemonSession:
|
|
|
57
58
|
"""Active StrangeLoop id for this WebSocket session."""
|
|
58
59
|
return self._loop_id
|
|
59
60
|
|
|
61
|
+
@property
|
|
62
|
+
def autopilot_mode(self) -> str | None:
|
|
63
|
+
"""Active loop Solo/Autopilot mode when known."""
|
|
64
|
+
return self._autopilot_mode
|
|
65
|
+
|
|
60
66
|
async def connect(self, *, resume_loop_id: str | None = None) -> dict[str, Any]:
|
|
61
67
|
"""Connect and bootstrap a daemon loop session."""
|
|
62
68
|
await connect_websocket_with_retries(self._client)
|
|
@@ -77,6 +83,8 @@ class TuiDaemonSession:
|
|
|
77
83
|
if status_event.get("type") == "error":
|
|
78
84
|
raise RuntimeError(str(status_event.get("message", "daemon bootstrap failed")))
|
|
79
85
|
self._loop_id = status_event.get("loop_id")
|
|
86
|
+
mode = status_event.get("autopilot_mode")
|
|
87
|
+
self._autopilot_mode = str(mode) if mode in ("solo", "autopilot") else None
|
|
80
88
|
return status_event
|
|
81
89
|
|
|
82
90
|
def _resolve_stream_delivery_mode(self) -> str:
|
|
@@ -177,6 +177,7 @@ class SootheApp(
|
|
|
177
177
|
# Active StrangeLoop id; LangGraph stores it as configurable.thread_id.
|
|
178
178
|
# Named `_lc_loop_id` to avoid colliding with Textual's App._thread_id.
|
|
179
179
|
self._lc_loop_id = resume_loop_id
|
|
180
|
+
self._loop_autopilot_mode: str = "solo"
|
|
180
181
|
|
|
181
182
|
self._initial_prompt = initial_prompt
|
|
182
183
|
|
|
@@ -570,6 +570,7 @@ class _ExecutionMixin:
|
|
|
570
570
|
)
|
|
571
571
|
self._session_state.loop_id = new_loop_id
|
|
572
572
|
self._lc_loop_id = new_loop_id
|
|
573
|
+
self._apply_loop_autopilot_mode(status_event.get("autopilot_mode"))
|
|
573
574
|
try:
|
|
574
575
|
banner = self.query_one("#welcome-banner", WelcomeBanner)
|
|
575
576
|
banner.update_loop_id(new_loop_id)
|
|
@@ -650,6 +651,8 @@ class _ExecutionMixin:
|
|
|
650
651
|
await self._submit_autopilot_job(args)
|
|
651
652
|
elif cmd == "/autopilot-dashboard":
|
|
652
653
|
await self._show_autopilot_dashboard()
|
|
654
|
+
elif cmd == "/autopilot-toggle":
|
|
655
|
+
await self._toggle_autopilot_mode()
|
|
653
656
|
elif cmd == "/mcp":
|
|
654
657
|
await self._show_mcp_viewer()
|
|
655
658
|
elif cmd == "/theme":
|
|
@@ -265,12 +265,26 @@ class _ModelMixin:
|
|
|
265
265
|
return None
|
|
266
266
|
return resp.get("servers")
|
|
267
267
|
|
|
268
|
+
def _apply_loop_autopilot_mode(self, mode: str | None) -> None:
|
|
269
|
+
"""Sync local Solo/Autopilot mode from daemon bootstrap or toggle events."""
|
|
270
|
+
if mode not in ("solo", "autopilot"):
|
|
271
|
+
return
|
|
272
|
+
self._loop_autopilot_mode = mode
|
|
273
|
+
if self._status_bar is not None:
|
|
274
|
+
label = "Autopilot" if mode == "autopilot" else "Solo"
|
|
275
|
+
self._status_bar.set_session_tip(f"Mode: {label}")
|
|
276
|
+
|
|
268
277
|
async def _show_autopilot_dashboard(self) -> None:
|
|
269
278
|
"""Show autopilot dashboard as a screen overlay."""
|
|
270
279
|
from soothe_cli.tui.widgets.autopilot_screen import AutopilotScreen
|
|
271
280
|
|
|
272
281
|
is_narrow = self.size.width < 100
|
|
273
|
-
|
|
282
|
+
mode = (
|
|
283
|
+
self._loop_autopilot_mode
|
|
284
|
+
if self._loop_autopilot_mode in ("solo", "autopilot")
|
|
285
|
+
else "solo"
|
|
286
|
+
)
|
|
287
|
+
screen = AutopilotScreen(is_narrow=is_narrow, mode=mode)
|
|
274
288
|
|
|
275
289
|
def handle_result(result: None) -> None: # noqa: ARG001
|
|
276
290
|
if self._chat_input:
|
|
@@ -278,6 +292,48 @@ class _ModelMixin:
|
|
|
278
292
|
|
|
279
293
|
self.push_screen(screen, handle_result)
|
|
280
294
|
|
|
295
|
+
async def _toggle_autopilot_mode(self) -> None:
|
|
296
|
+
"""Toggle autopilot mode (solo ↔ autopilot) via daemon RPC.
|
|
297
|
+
|
|
298
|
+
Sends command_request to daemon and updates local dashboard mode.
|
|
299
|
+
"""
|
|
300
|
+
from soothe_cli.tui.widgets.messages import ErrorMessage, UserMessage
|
|
301
|
+
|
|
302
|
+
await self._mount_message(UserMessage("/autopilot-toggle"))
|
|
303
|
+
|
|
304
|
+
lid = self._loop_id
|
|
305
|
+
if not lid:
|
|
306
|
+
await self._mount_message(ErrorMessage("No active loop for autopilot toggle"))
|
|
307
|
+
return
|
|
308
|
+
|
|
309
|
+
if not self._client:
|
|
310
|
+
await self._mount_message(ErrorMessage("Not connected to daemon"))
|
|
311
|
+
return
|
|
312
|
+
|
|
313
|
+
try:
|
|
314
|
+
from soothe_cli.tui.commands.command_router import handle_rpc_command
|
|
315
|
+
from soothe_cli.tui.commands.slash_commands import COMMANDS
|
|
316
|
+
|
|
317
|
+
entry = COMMANDS.get("/autopilot-toggle")
|
|
318
|
+
if not entry:
|
|
319
|
+
await self._mount_message(ErrorMessage("Command /autopilot-toggle not found"))
|
|
320
|
+
return
|
|
321
|
+
|
|
322
|
+
await handle_rpc_command(
|
|
323
|
+
entry,
|
|
324
|
+
"autopilot-toggle",
|
|
325
|
+
None,
|
|
326
|
+
self._console,
|
|
327
|
+
self._client,
|
|
328
|
+
loop_id=lid,
|
|
329
|
+
)
|
|
330
|
+
new_mode = "solo" if self._loop_autopilot_mode == "autopilot" else "autopilot"
|
|
331
|
+
self._apply_loop_autopilot_mode(new_mode)
|
|
332
|
+
|
|
333
|
+
except Exception as exc:
|
|
334
|
+
logger.exception("Autopilot toggle failed")
|
|
335
|
+
await self._mount_message(ErrorMessage(f"Failed to toggle autopilot: {exc}"))
|
|
336
|
+
|
|
281
337
|
async def _submit_autopilot_job(self, task: str) -> None:
|
|
282
338
|
"""Submit an autopilot job via HTTP REST (like CLI `soothe autopilot run`).
|
|
283
339
|
|
|
@@ -478,6 +478,13 @@ class _StartupMixin:
|
|
|
478
478
|
if self._session_state is not None:
|
|
479
479
|
self._session_state.loop_id = status_loop_id
|
|
480
480
|
|
|
481
|
+
autopilot_mode = event.status_event.get("autopilot_mode")
|
|
482
|
+
if autopilot_mode not in ("solo", "autopilot") and event.session is not None:
|
|
483
|
+
autopilot_mode = getattr(event.session, "autopilot_mode", None)
|
|
484
|
+
self._apply_loop_autopilot_mode(
|
|
485
|
+
str(autopilot_mode) if autopilot_mode in ("solo", "autopilot") else None
|
|
486
|
+
)
|
|
487
|
+
|
|
481
488
|
try:
|
|
482
489
|
banner = self.query_one("#welcome-banner", WelcomeBanner)
|
|
483
490
|
banner.set_connected(self._mcp_tool_count)
|