soothe-cli 0.6.8__tar.gz → 0.6.9__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.9}/PKG-INFO +1 -1
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/cli/commands/autopilot_cmd.py +74 -13
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/runtime/transport/session.py +8 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/tui/app/_app.py +1 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/tui/app/_execution.py +3 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/tui/app/_model.py +57 -1
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/tui/app/_startup.py +7 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/tui/commands/slash_commands.py +7 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/tui/input.py +37 -26
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/tui/preview_limits.py +2 -2
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/tui/textual_adapter.py +24 -1
- soothe_cli-0.6.9/src/soothe_cli/tui/widgets/autopilot_dashboard.py +614 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/tui/widgets/autopilot_screen.py +4 -3
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/tui/widgets/chat_input.py +37 -5
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/tui/widgets/messages.py +75 -7
- soothe_cli-0.6.8/src/soothe_cli/tui/widgets/autopilot_dashboard.py +0 -318
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/.gitignore +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/README.md +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/pyproject.toml +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/__init__.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/cli/__init__.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/cli/commands/__init__.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/cli/commands/loop_cmd.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/cli/commands/run_cmd.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/cli/commands/status_cmd.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/cli/execution/__init__.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/cli/execution/daemon.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/cli/execution/daemon_errors.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/cli/execution/headless.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/cli/execution/headless_renderer.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/cli/execution/launcher.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/cli/main.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/config/__init__.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/config/cli_config.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/config/loader.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/config/logging_setup.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/runtime/__init__.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/runtime/headless/processor.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/runtime/headless/processor_state.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/runtime/parse/_utils.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/runtime/parse/message_processing.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/runtime/parse/tool_call_resolution.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/runtime/parse/tool_message_format.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/runtime/parse/tool_result.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/runtime/policy/display_policy.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/runtime/policy/essential_events.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/runtime/policy/tui_trace_log.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/runtime/presentation/async_renderer_protocol.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/runtime/presentation/duration_format.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/runtime/presentation/engine.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/runtime/presentation/explore_task_display.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/runtime/presentation/renderer_base.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/runtime/presentation/renderer_protocol.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/runtime/state/file_tracker.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/runtime/state/session_stats.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/runtime/state/step_router.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/runtime/state/stream_accumulator.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/runtime/state/transcript.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/runtime/task_scope.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/runtime/turn/pipeline.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/runtime/turn/prepare.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/runtime/wire/chunk_filter.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/runtime/wire/display_text.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/runtime/wire/message_text.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/runtime/wire/messages.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/tui/__init__.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/tui/_cli_context.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/tui/_env_vars.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/tui/_version.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/tui/app/__init__.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/tui/app/_commands.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/tui/app/_history.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/tui/app/_messages_mixin.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/tui/app/_module_init.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/tui/app/_ui.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/tui/app/app.tcss +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/tui/binding.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/tui/command_registry.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/tui/commands/__init__.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/tui/commands/command_router.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/tui/commands/subagent_routing.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/tui/config.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/tui/file_change_notify.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/tui/file_change_renderers.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/tui/hooks.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/tui/media_utils.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/tui/model_config.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/tui/path_utils.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/tui/project_utils.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/tui/sessions.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/tui/skills/__init__.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/tui/skills/invocation.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/tui/skills/load.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/tui/theme.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/tui/tips.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/tui/tool_display.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/tui/unicode_security.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/tui/update_check.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/tui/widgets/__init__.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/tui/widgets/_links.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/tui/widgets/autocomplete.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/tui/widgets/clipboard.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/tui/widgets/diff.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/tui/widgets/editor.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/tui/widgets/file_change_preview.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/tui/widgets/history.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/tui/widgets/loading.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/tui/widgets/loop_selector.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/tui/widgets/mcp_viewer.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/tui/widgets/message_store.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/tui/widgets/model_selector.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/tui/widgets/notification_settings.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/tui/widgets/status.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/src/soothe_cli/tui/widgets/theme_selector.py +0 -0
- {soothe_cli-0.6.8 → soothe_cli-0.6.9}/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
|
|
|
@@ -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)
|
|
@@ -272,6 +272,13 @@ COMMANDS: dict[str, dict[str, Any]] = {
|
|
|
272
272
|
"requires_loop": True,
|
|
273
273
|
"handler": show_autopilot_dashboard,
|
|
274
274
|
},
|
|
275
|
+
"/autopilot-toggle": {
|
|
276
|
+
"location": "daemon",
|
|
277
|
+
"type": "rpc",
|
|
278
|
+
"daemon_command": "autopilot_toggle",
|
|
279
|
+
"description": "Toggle autopilot mode (solo ↔ autopilot)",
|
|
280
|
+
"requires_loop": True,
|
|
281
|
+
},
|
|
275
282
|
# Daemon routing commands (3)
|
|
276
283
|
"/plan": {"location": "daemon", "type": "routing", "description": "Trigger plan mode"},
|
|
277
284
|
"/tacitus": {
|
|
@@ -16,9 +16,6 @@ from soothe_cli.tui.path_utils import path_exists, path_is_dir, path_is_file
|
|
|
16
16
|
from soothe_cli.tui.preview_limits import (
|
|
17
17
|
CHAT_INPUT_PASTE_ABBREVIATE_CHAR_COUNT,
|
|
18
18
|
CHAT_INPUT_PASTE_ABBREVIATE_LINE_COUNT,
|
|
19
|
-
CHAT_INPUT_PASTE_PREVIEW_HEAD_LINES,
|
|
20
|
-
CHAT_INPUT_PASTE_PREVIEW_LINE_MAX_CHARS,
|
|
21
|
-
CHAT_INPUT_PASTE_PREVIEW_TAIL_LINES,
|
|
22
19
|
)
|
|
23
20
|
|
|
24
21
|
logger = logging.getLogger(__name__)
|
|
@@ -806,16 +803,44 @@ def should_abbreviate_pasted_input(text: str) -> bool:
|
|
|
806
803
|
return len(text) > CHAT_INPUT_PASTE_ABBREVIATE_CHAR_COUNT
|
|
807
804
|
|
|
808
805
|
|
|
809
|
-
def
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
806
|
+
def compose_paste_into_input(
|
|
807
|
+
existing_text: str,
|
|
808
|
+
pasted_text: str,
|
|
809
|
+
*,
|
|
810
|
+
replace_start: int | None = None,
|
|
811
|
+
replace_end: int | None = None,
|
|
812
|
+
) -> str:
|
|
813
|
+
"""Return input text after applying a paste at the given replacement range.
|
|
814
|
+
|
|
815
|
+
Args:
|
|
816
|
+
existing_text: Current input text before paste.
|
|
817
|
+
pasted_text: Raw pasted payload.
|
|
818
|
+
replace_start: Inclusive replacement start offset. Defaults to the end.
|
|
819
|
+
replace_end: Exclusive replacement end offset. Defaults to start.
|
|
820
|
+
|
|
821
|
+
Returns:
|
|
822
|
+
Full input text after replacing ``[replace_start:replace_end]`` with
|
|
823
|
+
``pasted_text``.
|
|
824
|
+
"""
|
|
825
|
+
text_len = len(existing_text)
|
|
826
|
+
if replace_start is None:
|
|
827
|
+
start = text_len
|
|
828
|
+
else:
|
|
829
|
+
start = max(0, min(replace_start, text_len))
|
|
830
|
+
|
|
831
|
+
if replace_end is None:
|
|
832
|
+
end = start
|
|
833
|
+
else:
|
|
834
|
+
end = max(0, min(replace_end, text_len))
|
|
835
|
+
|
|
836
|
+
if end < start:
|
|
837
|
+
start, end = end, start
|
|
838
|
+
|
|
839
|
+
return f"{existing_text[:start]}{pasted_text}{existing_text[end:]}"
|
|
815
840
|
|
|
816
841
|
|
|
817
842
|
def abbreviate_pasted_input_display(text: str) -> str:
|
|
818
|
-
"""Build a
|
|
843
|
+
"""Build a compact one-line preview for a large pasted payload.
|
|
819
844
|
|
|
820
845
|
The full ``text`` is retained separately for submission; this string is
|
|
821
846
|
only for on-screen display in the input widget.
|
|
@@ -824,25 +849,11 @@ def abbreviate_pasted_input_display(text: str) -> str:
|
|
|
824
849
|
text: Full pasted content.
|
|
825
850
|
|
|
826
851
|
Returns:
|
|
827
|
-
Abbreviated display text
|
|
852
|
+
Abbreviated display text containing only a summary header.
|
|
828
853
|
"""
|
|
829
854
|
lines = text.splitlines()
|
|
830
855
|
if not lines and text:
|
|
831
856
|
lines = [text]
|
|
832
857
|
n_lines = len(lines) if lines else 1
|
|
833
858
|
n_chars = len(text)
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
head_n = min(CHAT_INPUT_PASTE_PREVIEW_HEAD_LINES, n_lines)
|
|
837
|
-
tail_n = min(CHAT_INPUT_PASTE_PREVIEW_TAIL_LINES, max(0, n_lines - head_n))
|
|
838
|
-
omitted = n_lines - head_n - tail_n
|
|
839
|
-
|
|
840
|
-
parts: list[str] = [header]
|
|
841
|
-
parts.extend(_truncate_paste_preview_line(line) for line in lines[:head_n])
|
|
842
|
-
if omitted > 0:
|
|
843
|
-
parts.append(f"… ({omitted} more lines) …")
|
|
844
|
-
if tail_n > 0:
|
|
845
|
-
tail_start = n_lines - tail_n
|
|
846
|
-
if tail_start > head_n:
|
|
847
|
-
parts.extend(_truncate_paste_preview_line(line) for line in lines[tail_start:])
|
|
848
|
-
return "\n".join(parts)
|
|
859
|
+
return f"[pasted {n_lines} lines, {n_chars} characters]"
|
|
@@ -15,10 +15,10 @@ from typing import Final
|
|
|
15
15
|
STEP_CARD_SHOW_TOOL_ROW_DETAILS: Final[bool] = False
|
|
16
16
|
|
|
17
17
|
# Latest per-tool invocation lines shown per scope (task branch vs main-agent branch).
|
|
18
|
-
STEP_CARD_TOOL_ACTIVITY_PREVIEW_COUNT: Final[int] =
|
|
18
|
+
STEP_CARD_TOOL_ACTIVITY_PREVIEW_COUNT: Final[int] = 3
|
|
19
19
|
|
|
20
20
|
# When estimated body lines exceed this count, the card auto-collapses (strict `>`).
|
|
21
|
-
STEP_TASK_CARD_COLLAPSE_LINE_THRESHOLD: Final[int] =
|
|
21
|
+
STEP_TASK_CARD_COLLAPSE_LINE_THRESHOLD: Final[int] = 3
|
|
22
22
|
|
|
23
23
|
# --- Skill invocation cards (`SkillMessage` collapsed SKILL.md body) ---
|
|
24
24
|
SKILL_CARD_PREVIEW_LINES: Final[int] = 4
|
|
@@ -1482,6 +1482,26 @@ def _log_turn_event_stats(
|
|
|
1482
1482
|
)
|
|
1483
1483
|
|
|
1484
1484
|
|
|
1485
|
+
def _should_show_clarification_prompt(
|
|
1486
|
+
*, event_data: dict[str, Any], fallback_mode: str | None
|
|
1487
|
+
) -> bool:
|
|
1488
|
+
"""Return True when TUI should show the interactive clarification card.
|
|
1489
|
+
|
|
1490
|
+
In auto mode, clarifications are resolved/deferred by policy and should not
|
|
1491
|
+
render "Awaiting your answer" UI prompts in the TUI message stream.
|
|
1492
|
+
"""
|
|
1493
|
+
mode = event_data.get("mode")
|
|
1494
|
+
if isinstance(mode, str) and mode.strip():
|
|
1495
|
+
normalized = mode.strip().lower()
|
|
1496
|
+
elif isinstance(fallback_mode, str) and fallback_mode.strip():
|
|
1497
|
+
normalized = fallback_mode.strip().lower()
|
|
1498
|
+
else:
|
|
1499
|
+
normalized = "auto"
|
|
1500
|
+
if normalized not in {"auto", "manual"}:
|
|
1501
|
+
normalized = "auto"
|
|
1502
|
+
return normalized == "manual"
|
|
1503
|
+
|
|
1504
|
+
|
|
1485
1505
|
async def execute_task_textual(
|
|
1486
1506
|
user_input: str,
|
|
1487
1507
|
assistant_id: str | None,
|
|
@@ -2511,7 +2531,10 @@ async def execute_task_textual(
|
|
|
2511
2531
|
questions_list = [
|
|
2512
2532
|
str(q) for q in raw_questions if str(q).strip()
|
|
2513
2533
|
]
|
|
2514
|
-
if questions_list
|
|
2534
|
+
if questions_list and _should_show_clarification_prompt(
|
|
2535
|
+
event_data=data,
|
|
2536
|
+
fallback_mode=clarification_mode,
|
|
2537
|
+
):
|
|
2515
2538
|
# The ask_user step card was put into "running" by
|
|
2516
2539
|
# ``step_started`` just before await_clarification.
|
|
2517
2540
|
# Surface the pending questions on it so the user
|