soothe-cli 0.5.29__tar.gz → 0.5.30__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.5.29 → soothe_cli-0.5.30}/.gitignore +1 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/PKG-INFO +1 -1
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/cli/commands/autopilot_cmd.py +126 -12
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/runtime/transport/session.py +11 -2
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/tui/app/_execution.py +24 -7
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/tui/app/_model.py +51 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/tui/app/_startup.py +5 -1
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/tui/command_registry.py +7 -1
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/tui/commands/slash_commands.py +6 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/tui/widgets/chat_input.py +29 -2
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/README.md +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/pyproject.toml +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/__init__.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/cli/__init__.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/cli/commands/__init__.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/cli/commands/loop_cmd.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/cli/commands/run_cmd.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/cli/execution/__init__.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/cli/execution/daemon.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/cli/execution/daemon_errors.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/cli/execution/headless.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/cli/execution/headless_renderer.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/cli/execution/launcher.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/cli/main.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/config/__init__.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/config/cli_config.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/config/loader.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/config/logging_setup.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/runtime/__init__.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/runtime/headless/processor.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/runtime/headless/processor_state.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/runtime/parse/_utils.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/runtime/parse/message_processing.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/runtime/parse/tool_call_resolution.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/runtime/parse/tool_message_format.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/runtime/parse/tool_result.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/runtime/policy/display_policy.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/runtime/policy/essential_events.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/runtime/policy/tui_trace_log.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/runtime/presentation/async_renderer_protocol.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/runtime/presentation/duration_format.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/runtime/presentation/engine.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/runtime/presentation/explore_task_display.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/runtime/presentation/renderer_base.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/runtime/presentation/renderer_protocol.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/runtime/state/file_tracker.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/runtime/state/session_stats.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/runtime/state/step_router.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/runtime/state/stream_accumulator.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/runtime/state/transcript.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/runtime/task_scope.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/runtime/turn/pipeline.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/runtime/turn/prepare.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/runtime/wire/chunk_filter.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/runtime/wire/display_text.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/runtime/wire/message_text.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/runtime/wire/messages.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/tui/__init__.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/tui/_cli_context.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/tui/_env_vars.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/tui/_version.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/tui/app/__init__.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/tui/app/_app.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/tui/app/_commands.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/tui/app/_history.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/tui/app/_messages_mixin.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/tui/app/_module_init.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/tui/app/_ui.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/tui/app/app.tcss +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/tui/binding.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/tui/commands/__init__.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/tui/commands/command_router.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/tui/commands/subagent_routing.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/tui/config.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/tui/file_change_notify.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/tui/file_change_renderers.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/tui/hooks.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/tui/input.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/tui/media_utils.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/tui/model_config.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/tui/path_utils.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/tui/preview_limits.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/tui/project_utils.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/tui/sessions.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/tui/skills/__init__.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/tui/skills/invocation.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/tui/skills/load.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/tui/textual_adapter.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/tui/theme.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/tui/tips.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/tui/tool_display.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/tui/unicode_security.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/tui/update_check.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/tui/widgets/__init__.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/tui/widgets/_links.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/tui/widgets/autocomplete.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/tui/widgets/autopilot_dashboard.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/tui/widgets/autopilot_screen.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/tui/widgets/clipboard.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/tui/widgets/diff.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/tui/widgets/editor.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/tui/widgets/file_change_preview.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/tui/widgets/history.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/tui/widgets/loading.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/tui/widgets/loop_selector.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/tui/widgets/mcp_viewer.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/tui/widgets/message_store.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/tui/widgets/messages.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/tui/widgets/model_selector.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/tui/widgets/notification_settings.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/tui/widgets/status.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/tui/widgets/theme_selector.py +0 -0
- {soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/tui/widgets/welcome.py +0 -0
|
@@ -133,27 +133,121 @@ def status() -> None:
|
|
|
133
133
|
|
|
134
134
|
|
|
135
135
|
@app.command("list")
|
|
136
|
-
def
|
|
136
|
+
def list_jobs(
|
|
137
137
|
status_filter: str = typer.Option("", "--status", "-s", help="Filter by status."),
|
|
138
138
|
) -> None:
|
|
139
|
-
"""List goals from the
|
|
139
|
+
"""List jobs (root goals) from the daemon autopilot.
|
|
140
|
+
|
|
141
|
+
Jobs are user-submitted tasks. Subgoals created during autonomous
|
|
142
|
+
execution are not shown here; use 'goal <id>' to inspect subgoals.
|
|
143
|
+
"""
|
|
140
144
|
client = _require_daemon_http()
|
|
141
|
-
payload = client.
|
|
142
|
-
|
|
143
|
-
if not
|
|
144
|
-
typer.echo("No
|
|
145
|
+
payload = client.list_jobs()
|
|
146
|
+
jobs = payload.get("jobs") or []
|
|
147
|
+
if not jobs:
|
|
148
|
+
typer.echo("No jobs found.")
|
|
145
149
|
return
|
|
146
150
|
|
|
147
|
-
for
|
|
148
|
-
if status_filter and
|
|
151
|
+
for j in jobs:
|
|
152
|
+
if status_filter and j.get("status", "") != status_filter:
|
|
149
153
|
continue
|
|
150
|
-
sid =
|
|
151
|
-
sdesc = preview_first(
|
|
152
|
-
sstat =
|
|
153
|
-
spri =
|
|
154
|
+
sid = j.get("id", "?")[:8]
|
|
155
|
+
sdesc = preview_first(j.get("description", ""), 60)
|
|
156
|
+
sstat = j.get("status", "pending")
|
|
157
|
+
spri = j.get("priority", 50)
|
|
154
158
|
typer.echo(f" [{sid}] {sstat:10s} pri={spri:3d} {sdesc}")
|
|
155
159
|
|
|
156
160
|
|
|
161
|
+
def _render_dag_tree(dag: dict, root_id: str) -> None:
|
|
162
|
+
"""Render DAG as ASCII tree for job visualization."""
|
|
163
|
+
nodes = {n["id"]: n for n in dag.get("nodes", [])}
|
|
164
|
+
edges = dag.get("edges", [])
|
|
165
|
+
|
|
166
|
+
# Build children map from edges
|
|
167
|
+
children: dict[str, list[str]] = {}
|
|
168
|
+
for edge in edges:
|
|
169
|
+
src = edge.get("source")
|
|
170
|
+
tgt = edge.get("target")
|
|
171
|
+
if src and tgt:
|
|
172
|
+
if src not in children:
|
|
173
|
+
children[src] = []
|
|
174
|
+
children[src].append(tgt)
|
|
175
|
+
|
|
176
|
+
def render_node(goal_id: str, indent: str = "", is_last: bool = True) -> None:
|
|
177
|
+
node = nodes.get(goal_id)
|
|
178
|
+
if not node:
|
|
179
|
+
return
|
|
180
|
+
|
|
181
|
+
# Prefix for this level
|
|
182
|
+
if indent:
|
|
183
|
+
prefix = indent + ("└─ " if is_last else "├─ ")
|
|
184
|
+
else:
|
|
185
|
+
prefix = ""
|
|
186
|
+
|
|
187
|
+
status = node.get("status", "pending")
|
|
188
|
+
desc = preview_first(node.get("description", ""), 50)
|
|
189
|
+
typer.echo(f'{prefix}{goal_id[:8]} ({status}) "{desc}"')
|
|
190
|
+
|
|
191
|
+
# Render children
|
|
192
|
+
child_ids = children.get(goal_id, [])
|
|
193
|
+
for i, child_id in enumerate(child_ids):
|
|
194
|
+
child_indent = indent + (" " if is_last else "│ ")
|
|
195
|
+
render_node(child_id, child_indent, i == len(child_ids) - 1)
|
|
196
|
+
|
|
197
|
+
render_node(root_id)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
@app.command("job")
|
|
201
|
+
def show_job(
|
|
202
|
+
job_id: str = typer.Argument(..., help="Job ID to show details and DAG."),
|
|
203
|
+
) -> None:
|
|
204
|
+
"""Show job status and DAG tree visualization.
|
|
205
|
+
|
|
206
|
+
A job is a root goal submitted by the user. This command shows
|
|
207
|
+
the job's details and the complete goal DAG under it.
|
|
208
|
+
"""
|
|
209
|
+
client = _require_daemon_http()
|
|
210
|
+
try:
|
|
211
|
+
payload = client.get_job(job_id)
|
|
212
|
+
except RuntimeError as exc:
|
|
213
|
+
typer.echo(str(exc), err=True)
|
|
214
|
+
raise typer.Exit(1)
|
|
215
|
+
|
|
216
|
+
job = payload.get("job")
|
|
217
|
+
dag = payload.get("dag")
|
|
218
|
+
|
|
219
|
+
if not job:
|
|
220
|
+
typer.echo(f"Job '{job_id}' not found.", err=True)
|
|
221
|
+
raise typer.Exit(1)
|
|
222
|
+
|
|
223
|
+
# Job header
|
|
224
|
+
typer.echo(f"Job ID: {job.get('id')}")
|
|
225
|
+
typer.echo(f"Status: {job.get('status', 'pending')}")
|
|
226
|
+
typer.echo(f"Priority: {job.get('priority', 50)}")
|
|
227
|
+
if job.get("workspace"):
|
|
228
|
+
typer.echo(f"Workspace: {job['workspace']}")
|
|
229
|
+
created = job.get("created_at", "")
|
|
230
|
+
if created:
|
|
231
|
+
# Truncate timestamp for readability
|
|
232
|
+
created_short = created[:19] if len(created) > 19 else created
|
|
233
|
+
typer.echo(f"Created: {created_short}")
|
|
234
|
+
|
|
235
|
+
# DAG stats from response
|
|
236
|
+
active = payload.get("active_goals", 0)
|
|
237
|
+
completed = payload.get("completed_goals", 0)
|
|
238
|
+
total = payload.get("total_goals", 0)
|
|
239
|
+
typer.echo(f"Active goals: {active}")
|
|
240
|
+
typer.echo(f"Completed goals: {completed}")
|
|
241
|
+
typer.echo(f"Total goals: {total}")
|
|
242
|
+
|
|
243
|
+
# DAG tree
|
|
244
|
+
typer.echo("\nDAG:")
|
|
245
|
+
if dag:
|
|
246
|
+
_render_dag_tree(dag, job_id)
|
|
247
|
+
else:
|
|
248
|
+
typer.echo(" (no subgoals)")
|
|
249
|
+
|
|
250
|
+
|
|
157
251
|
@app.command("goal")
|
|
158
252
|
def show_goal(
|
|
159
253
|
goal_id: str = typer.Argument(..., help="Goal ID to show details for."),
|
|
@@ -216,6 +310,26 @@ def reject_goal(
|
|
|
216
310
|
typer.echo(f"Confirmation rejected: {result.get('goal_id', goal_id)}")
|
|
217
311
|
|
|
218
312
|
|
|
313
|
+
@app.command("resume")
|
|
314
|
+
def resume_goal(
|
|
315
|
+
goal_id: str = typer.Argument(..., help="Goal ID to resume."),
|
|
316
|
+
) -> None:
|
|
317
|
+
"""Resume a suspended or blocked goal.
|
|
318
|
+
|
|
319
|
+
Reactivates a paused goal back to pending status so the scheduler
|
|
320
|
+
can pick it up for execution. Use 'jobs' to list goals and their status.
|
|
321
|
+
"""
|
|
322
|
+
client = _require_daemon_http()
|
|
323
|
+
try:
|
|
324
|
+
result = client.resume(goal_id)
|
|
325
|
+
except RuntimeError as exc:
|
|
326
|
+
typer.echo(str(exc), err=True)
|
|
327
|
+
raise typer.Exit(1) from exc
|
|
328
|
+
typer.echo(
|
|
329
|
+
f"Goal resumed: {result.get('goal_id', goal_id)} → {result.get('new_status', 'pending')}"
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
|
|
219
333
|
@app.command("wake")
|
|
220
334
|
def wake() -> None:
|
|
221
335
|
"""Exit dreaming mode — resume active execution."""
|
|
@@ -32,7 +32,13 @@ _POST_IDLE_DRAIN_DEADLINE_S = 2.5
|
|
|
32
32
|
class TuiDaemonSession:
|
|
33
33
|
"""Own the daemon websocket session used by the TUI."""
|
|
34
34
|
|
|
35
|
-
def __init__(
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
cfg: Any,
|
|
38
|
+
*,
|
|
39
|
+
workspace: str | None = None,
|
|
40
|
+
post_idle_drain_deadline: float = _POST_IDLE_DRAIN_DEADLINE_S,
|
|
41
|
+
) -> None:
|
|
36
42
|
self._cfg = cfg
|
|
37
43
|
self._workspace = workspace
|
|
38
44
|
ws_url = websocket_url_from_config(cfg)
|
|
@@ -43,6 +49,7 @@ class TuiDaemonSession:
|
|
|
43
49
|
self._rpc_lock = asyncio.Lock()
|
|
44
50
|
self._rpc_connected = False
|
|
45
51
|
self._streaming = False
|
|
52
|
+
self._post_idle_drain_deadline = post_idle_drain_deadline
|
|
46
53
|
self.turn_event_stats = TurnEventStats()
|
|
47
54
|
|
|
48
55
|
@property
|
|
@@ -178,7 +185,9 @@ class TuiDaemonSession:
|
|
|
178
185
|
) -> Any:
|
|
179
186
|
"""Yield stream chunks that arrive just after ``idle`` (headless client parity)."""
|
|
180
187
|
loop = asyncio.get_running_loop()
|
|
181
|
-
deadline = loop.time() +
|
|
188
|
+
deadline = loop.time() + getattr(
|
|
189
|
+
self, "_post_idle_drain_deadline", _POST_IDLE_DRAIN_DEADLINE_S
|
|
190
|
+
)
|
|
182
191
|
exp = expected_loop_id
|
|
183
192
|
while loop.time() < deadline:
|
|
184
193
|
try:
|
|
@@ -340,10 +340,13 @@ class _ExecutionMixin:
|
|
|
340
340
|
self._shell_process = None
|
|
341
341
|
self._shell_running = False
|
|
342
342
|
self._shell_worker = None
|
|
343
|
-
|
|
344
|
-
|
|
343
|
+
|
|
344
|
+
# Restore input focus first so the user can type immediately.
|
|
345
345
|
if self._chat_input:
|
|
346
346
|
self._chat_input.set_cursor_active(active=True)
|
|
347
|
+
|
|
348
|
+
if was_interrupted:
|
|
349
|
+
await self._mount_message(AppMessage("Command interrupted"))
|
|
347
350
|
try:
|
|
348
351
|
await self._maybe_drain_deferred()
|
|
349
352
|
except Exception:
|
|
@@ -490,7 +493,7 @@ class _ExecutionMixin:
|
|
|
490
493
|
elif cmd == "/help":
|
|
491
494
|
await self._mount_message(UserMessage(command))
|
|
492
495
|
help_body = (
|
|
493
|
-
"Commands: /quit, /clear, /editor, /autopilot, /mcp, "
|
|
496
|
+
"Commands: /quit, /clear, /editor, /autopilot <task>, /autopilot-dashboard, /mcp, "
|
|
494
497
|
"/model [--model-params JSON] [--default], /notifications, "
|
|
495
498
|
"/reload, /skill:<name>, /theme, "
|
|
496
499
|
"/tokens, /resume, "
|
|
@@ -633,7 +636,19 @@ class _ExecutionMixin:
|
|
|
633
636
|
args = command.strip()[len("/skill-creator") :].strip()
|
|
634
637
|
rewritten = f"/skill:skill-creator {args}" if args else "/skill:skill-creator"
|
|
635
638
|
await self._handle_skill_command(rewritten)
|
|
636
|
-
elif cmd == "/autopilot":
|
|
639
|
+
elif cmd == "/autopilot" or cmd.startswith("/autopilot "):
|
|
640
|
+
# Submit autopilot job via HTTP REST (like CLI `soothe autopilot run`)
|
|
641
|
+
args = command.strip()[len("/autopilot") :].strip()
|
|
642
|
+
if not args:
|
|
643
|
+
await self._mount_message(UserMessage(command))
|
|
644
|
+
await self._mount_message(
|
|
645
|
+
AppMessage(
|
|
646
|
+
"Usage: /autopilot <task description>\nExample: /autopilot refactor the auth module"
|
|
647
|
+
)
|
|
648
|
+
)
|
|
649
|
+
return
|
|
650
|
+
await self._submit_autopilot_job(args)
|
|
651
|
+
elif cmd == "/autopilot-dashboard":
|
|
637
652
|
await self._show_autopilot_dashboard()
|
|
638
653
|
elif cmd == "/mcp":
|
|
639
654
|
await self._show_mcp_viewer()
|
|
@@ -961,12 +976,14 @@ class _ExecutionMixin:
|
|
|
961
976
|
self._agent_running = False
|
|
962
977
|
self._agent_worker = None
|
|
963
978
|
|
|
964
|
-
#
|
|
965
|
-
|
|
966
|
-
|
|
979
|
+
# Restore input focus FIRST so the user can start typing immediately
|
|
980
|
+
# while remaining cleanup (spinner, tokens, deferred) runs.
|
|
967
981
|
if self._chat_input:
|
|
968
982
|
self._chat_input.set_cursor_active(active=True)
|
|
969
983
|
|
|
984
|
+
# Remove spinner if present
|
|
985
|
+
await self._set_spinner(None)
|
|
986
|
+
|
|
970
987
|
# Ensure token display is restored (in case of early cancellation).
|
|
971
988
|
# Pass the cached approximate flag so an interrupted "+" isn't clobbered.
|
|
972
989
|
self._show_tokens(approximate=self._tokens_approximate)
|
|
@@ -278,6 +278,57 @@ class _ModelMixin:
|
|
|
278
278
|
|
|
279
279
|
self.push_screen(screen, handle_result)
|
|
280
280
|
|
|
281
|
+
async def _submit_autopilot_job(self, task: str) -> None:
|
|
282
|
+
"""Submit an autopilot job via HTTP REST (like CLI `soothe autopilot run`).
|
|
283
|
+
|
|
284
|
+
Args:
|
|
285
|
+
task: Task description for autonomous execution.
|
|
286
|
+
"""
|
|
287
|
+
import os
|
|
288
|
+
|
|
289
|
+
from soothe_sdk.client import (
|
|
290
|
+
AutopilotHttpClient,
|
|
291
|
+
http_rest_url_from_config,
|
|
292
|
+
is_daemon_live,
|
|
293
|
+
websocket_url_from_config,
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
from soothe_cli.runtime import load_config
|
|
297
|
+
from soothe_cli.tui.widgets.messages import ErrorMessage, UserMessage
|
|
298
|
+
|
|
299
|
+
await self._mount_message(UserMessage(f"/autopilot {task}"))
|
|
300
|
+
|
|
301
|
+
cfg = load_config()
|
|
302
|
+
ws_url = websocket_url_from_config(cfg)
|
|
303
|
+
|
|
304
|
+
# Check daemon is running
|
|
305
|
+
if not await is_daemon_live(ws_url, timeout=5.0):
|
|
306
|
+
await self._mount_message(
|
|
307
|
+
ErrorMessage("Daemon not running. Start with 'soothed start'.")
|
|
308
|
+
)
|
|
309
|
+
return
|
|
310
|
+
|
|
311
|
+
base_url = http_rest_url_from_config(cfg)
|
|
312
|
+
workspace = self._cwd if hasattr(self, "_cwd") else os.getcwd()
|
|
313
|
+
|
|
314
|
+
try:
|
|
315
|
+
client = AutopilotHttpClient(base_url)
|
|
316
|
+
result = client.submit(task, workspace=workspace)
|
|
317
|
+
except RuntimeError as exc:
|
|
318
|
+
await self._mount_message(ErrorMessage(str(exc)))
|
|
319
|
+
return
|
|
320
|
+
except Exception as exc: # noqa: BLE001
|
|
321
|
+
logger.exception("Autopilot submit failed")
|
|
322
|
+
await self._mount_message(ErrorMessage(f"Failed to submit autopilot job: {exc}"))
|
|
323
|
+
return
|
|
324
|
+
|
|
325
|
+
goal_id = result.get("goal_id", "")
|
|
326
|
+
if goal_id:
|
|
327
|
+
self.notify(f"Autopilot job submitted: {goal_id[:8]}", timeout=5)
|
|
328
|
+
logger.info("Submitted autopilot job %s: %s", goal_id, task[:50])
|
|
329
|
+
else:
|
|
330
|
+
await self._mount_message(ErrorMessage("No goal_id returned from daemon"))
|
|
331
|
+
|
|
281
332
|
async def _show_loop_selector(self) -> None:
|
|
282
333
|
"""Show interactive loop selector as a modal screen."""
|
|
283
334
|
from functools import partial
|
|
@@ -452,7 +452,11 @@ class _StartupMixin:
|
|
|
452
452
|
f"Please start the daemon with: soothed start"
|
|
453
453
|
)
|
|
454
454
|
|
|
455
|
-
session = TuiDaemonSession(
|
|
455
|
+
session = TuiDaemonSession(
|
|
456
|
+
self._daemon_config,
|
|
457
|
+
workspace=self._cwd,
|
|
458
|
+
post_idle_drain_deadline=0.3,
|
|
459
|
+
)
|
|
456
460
|
status_event = await session.connect(resume_loop_id=self._lc_loop_id)
|
|
457
461
|
except Exception as exc:
|
|
458
462
|
self.post_message(self.ServerStartFailed(error=exc))
|
|
@@ -57,9 +57,15 @@ class SlashCommand:
|
|
|
57
57
|
COMMANDS: tuple[SlashCommand, ...] = (
|
|
58
58
|
SlashCommand(
|
|
59
59
|
name="/autopilot",
|
|
60
|
+
description="Submit autopilot job (usage: /autopilot <task>)",
|
|
61
|
+
bypass_tier=BypassTier.QUEUED,
|
|
62
|
+
hidden_keywords="goals autonomous job submit",
|
|
63
|
+
),
|
|
64
|
+
SlashCommand(
|
|
65
|
+
name="/autopilot-dashboard",
|
|
60
66
|
description="Open autopilot dashboard",
|
|
61
67
|
bypass_tier=BypassTier.IMMEDIATE_UI,
|
|
62
|
-
hidden_keywords="goals
|
|
68
|
+
hidden_keywords="goals status workers",
|
|
63
69
|
),
|
|
64
70
|
SlashCommand(
|
|
65
71
|
name="/clear",
|
|
@@ -259,6 +259,12 @@ COMMANDS: dict[str, dict[str, Any]] = {
|
|
|
259
259
|
"params_schema": {"loop_id": {"type": "string", "required": True}},
|
|
260
260
|
},
|
|
261
261
|
"/autopilot": {
|
|
262
|
+
"location": "daemon",
|
|
263
|
+
"type": "routing",
|
|
264
|
+
"description": "Submit autopilot job (usage: /autopilot <task>)",
|
|
265
|
+
"requires_query": True,
|
|
266
|
+
},
|
|
267
|
+
"/autopilot-dashboard": {
|
|
262
268
|
"location": "daemon",
|
|
263
269
|
"type": "rpc",
|
|
264
270
|
"daemon_command": "autopilot_dashboard",
|
|
@@ -841,6 +841,13 @@ class ChatInput(Vertical):
|
|
|
841
841
|
- Autocomplete for @ (files) and / (commands)
|
|
842
842
|
"""
|
|
843
843
|
|
|
844
|
+
_PATH_INDICATOR_CHARS: ClassVar[frozenset[str]] = frozenset("/\\~")
|
|
845
|
+
"""Characters that indicate text might be a file path.
|
|
846
|
+
|
|
847
|
+
Used by `_is_dropped_path_payload` as a fast-path guard to skip
|
|
848
|
+
filesystem I/O when text cannot possibly be a path.
|
|
849
|
+
"""
|
|
850
|
+
|
|
844
851
|
DEFAULT_CSS = """
|
|
845
852
|
ChatInput {
|
|
846
853
|
height: auto;
|
|
@@ -1038,7 +1045,7 @@ class ChatInput(Vertical):
|
|
|
1038
1045
|
"Cannot update slash commands: controller not initialized (widget not yet mounted)"
|
|
1039
1046
|
)
|
|
1040
1047
|
|
|
1041
|
-
def on_text_area_changed(self, event: TextArea.Changed) -> None:
|
|
1048
|
+
async def on_text_area_changed(self, event: TextArea.Changed) -> None:
|
|
1042
1049
|
"""Detect input mode and update completions."""
|
|
1043
1050
|
text = event.text_area.text
|
|
1044
1051
|
self._sync_media_tracker_to_text(text)
|
|
@@ -1076,6 +1083,11 @@ class ChatInput(Vertical):
|
|
|
1076
1083
|
# or prefix stripping, which never need path detection.
|
|
1077
1084
|
is_path_payload = self._is_dropped_path_payload(text)
|
|
1078
1085
|
|
|
1086
|
+
# Yield to the event loop so pending key events are not starved
|
|
1087
|
+
# when the path check above did filesystem I/O.
|
|
1088
|
+
if is_path_payload:
|
|
1089
|
+
await asyncio.sleep(0)
|
|
1090
|
+
|
|
1079
1091
|
# Guard: skip mode re-detection after we programmatically stripped
|
|
1080
1092
|
# a prefix character.
|
|
1081
1093
|
if self._stripping_prefix:
|
|
@@ -1193,13 +1205,28 @@ class ChatInput(Vertical):
|
|
|
1193
1205
|
"""Return whether text is a dropped-path payload for existing files."""
|
|
1194
1206
|
if len(text) < 2: # noqa: PLR2004 # Need at least '/' + one char
|
|
1195
1207
|
return False
|
|
1208
|
+
# Short texts without path-like prefixes are never valid payloads.
|
|
1209
|
+
# Avoids stat(2) calls for slash-command fragments like "/h" or "/he".
|
|
1210
|
+
if len(text) < 3 and not text.startswith(("'", '"', "~/")): # noqa: PLR2004
|
|
1211
|
+
return False
|
|
1196
1212
|
from soothe_cli.tui.input import parse_pasted_path_payload
|
|
1197
1213
|
|
|
1198
1214
|
return parse_pasted_path_payload(text, allow_leading_path=True) is not None
|
|
1199
1215
|
|
|
1200
1216
|
def _is_dropped_path_payload(self, text: str) -> bool:
|
|
1201
1217
|
"""Return whether current text looks like a dropped file-path payload."""
|
|
1202
|
-
if not text:
|
|
1218
|
+
if not text or len(text) < 2:
|
|
1219
|
+
return False
|
|
1220
|
+
# Fast path: skip filesystem I/O when text has no path indicators.
|
|
1221
|
+
# Dropped paths always contain at least one of: / (POSIX), \ (Windows),
|
|
1222
|
+
# ~ (home), or a quote prefix ('/path' or "/path"). Plain prose and
|
|
1223
|
+
# slash-command text (e.g. "/help") without path separators never
|
|
1224
|
+
# triggers dropped-path parsing.
|
|
1225
|
+
if (
|
|
1226
|
+
not text.startswith(("'", '"'))
|
|
1227
|
+
and not text.startswith("file://")
|
|
1228
|
+
and not any(c in self._PATH_INDICATOR_CHARS for c in text)
|
|
1229
|
+
):
|
|
1203
1230
|
return False
|
|
1204
1231
|
if self._is_existing_path_payload(text):
|
|
1205
1232
|
return True
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/runtime/parse/tool_call_resolution.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/runtime/presentation/duration_format.py
RENAMED
|
File without changes
|
|
File without changes
|
{soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/runtime/presentation/explore_task_display.py
RENAMED
|
File without changes
|
{soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/runtime/presentation/renderer_base.py
RENAMED
|
File without changes
|
{soothe_cli-0.5.29 → soothe_cli-0.5.30}/src/soothe_cli/runtime/presentation/renderer_protocol.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|