soothe-cli 0.5.28__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.28 → soothe_cli-0.5.30}/.gitignore +1 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/PKG-INFO +1 -1
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/cli/commands/autopilot_cmd.py +127 -13
- soothe_cli-0.5.30/src/soothe_cli/runtime/parse/_utils.py +14 -0
- soothe_cli-0.5.30/src/soothe_cli/runtime/parse/message_processing.py +51 -0
- soothe_cli-0.5.30/src/soothe_cli/runtime/parse/tool_message_format.py +17 -0
- soothe_cli-0.5.30/src/soothe_cli/runtime/parse/tool_result.py +15 -0
- soothe_cli-0.5.30/src/soothe_cli/runtime/state/transcript.py +24 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/runtime/transport/session.py +62 -2
- soothe_cli-0.5.30/src/soothe_cli/runtime/wire/display_text.py +15 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/runtime/wire/message_text.py +3 -3
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/tui/app/_app.py +3 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/tui/app/_execution.py +38 -10
- soothe_cli-0.5.30/src/soothe_cli/tui/app/_history.py +335 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/tui/app/_model.py +58 -2
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/tui/app/_startup.py +6 -2
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/tui/app/app.tcss +26 -6
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/tui/command_registry.py +9 -3
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/tui/commands/slash_commands.py +6 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/tui/model_config.py +10 -4
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/tui/sessions.py +74 -27
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/tui/textual_adapter.py +2 -2
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/tui/tips.py +1 -1
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/tui/widgets/autopilot_dashboard.py +2 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/tui/widgets/chat_input.py +29 -2
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/tui/widgets/clipboard.py +30 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/tui/widgets/file_change_preview.py +64 -60
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/tui/widgets/loop_selector.py +64 -9
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/tui/widgets/messages.py +12 -17
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/tui/widgets/status.py +6 -6
- soothe_cli-0.5.28/src/soothe_cli/runtime/parse/_utils.py +0 -30
- soothe_cli-0.5.28/src/soothe_cli/runtime/parse/message_processing.py +0 -551
- soothe_cli-0.5.28/src/soothe_cli/runtime/parse/tool_message_format.py +0 -115
- soothe_cli-0.5.28/src/soothe_cli/runtime/parse/tool_result.py +0 -141
- soothe_cli-0.5.28/src/soothe_cli/runtime/state/transcript.py +0 -208
- soothe_cli-0.5.28/src/soothe_cli/runtime/wire/display_text.py +0 -64
- soothe_cli-0.5.28/src/soothe_cli/tui/app/_history.py +0 -969
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/README.md +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/pyproject.toml +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/__init__.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/cli/__init__.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/cli/commands/__init__.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/cli/commands/loop_cmd.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/cli/commands/run_cmd.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/cli/execution/__init__.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/cli/execution/daemon.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/cli/execution/daemon_errors.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/cli/execution/headless.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/cli/execution/headless_renderer.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/cli/execution/launcher.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/cli/main.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/config/__init__.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/config/cli_config.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/config/loader.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/config/logging_setup.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/runtime/__init__.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/runtime/headless/processor.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/runtime/headless/processor_state.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/runtime/parse/tool_call_resolution.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/runtime/policy/display_policy.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/runtime/policy/essential_events.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/runtime/policy/tui_trace_log.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/runtime/presentation/async_renderer_protocol.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/runtime/presentation/duration_format.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/runtime/presentation/engine.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/runtime/presentation/explore_task_display.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/runtime/presentation/renderer_base.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/runtime/presentation/renderer_protocol.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/runtime/state/file_tracker.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/runtime/state/session_stats.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/runtime/state/step_router.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/runtime/state/stream_accumulator.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/runtime/task_scope.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/runtime/turn/pipeline.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/runtime/turn/prepare.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/runtime/wire/chunk_filter.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/runtime/wire/messages.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/tui/__init__.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/tui/_cli_context.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/tui/_env_vars.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/tui/_version.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/tui/app/__init__.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/tui/app/_commands.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/tui/app/_messages_mixin.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/tui/app/_module_init.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/tui/app/_ui.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/tui/binding.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/tui/commands/__init__.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/tui/commands/command_router.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/tui/commands/subagent_routing.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/tui/config.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/tui/file_change_notify.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/tui/file_change_renderers.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/tui/hooks.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/tui/input.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/tui/media_utils.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/tui/path_utils.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/tui/preview_limits.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/tui/project_utils.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/tui/skills/__init__.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/tui/skills/invocation.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/tui/skills/load.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/tui/theme.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/tui/tool_display.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/tui/unicode_security.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/tui/update_check.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/tui/widgets/__init__.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/tui/widgets/_links.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/tui/widgets/autocomplete.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/tui/widgets/autopilot_screen.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/tui/widgets/diff.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/tui/widgets/editor.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/tui/widgets/history.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/tui/widgets/loading.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/tui/widgets/mcp_viewer.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/tui/widgets/message_store.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/tui/widgets/model_selector.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/tui/widgets/notification_settings.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/tui/widgets/theme_selector.py +0 -0
- {soothe_cli-0.5.28 → soothe_cli-0.5.30}/src/soothe_cli/tui/widgets/welcome.py +0 -0
|
@@ -91,7 +91,7 @@ def run(
|
|
|
91
91
|
detail = client.get_goal(goal_id)
|
|
92
92
|
goal = detail.get("goal") or {}
|
|
93
93
|
status = goal.get("status", "unknown")
|
|
94
|
-
if status in ("completed", "failed", "suspended"):
|
|
94
|
+
if status in ("completed", "failed", "cancelled", "suspended"):
|
|
95
95
|
typer.echo(f"Goal {goal_id[:8]}: {status}")
|
|
96
96
|
if status == "failed":
|
|
97
97
|
sys.exit(1)
|
|
@@ -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."""
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Re-export shim for ``soothe_sdk.display._text_utils`` (RFC-413).
|
|
2
|
+
|
|
3
|
+
These utilities live in the SDK so the daemon-resident ``CardBinder`` can
|
|
4
|
+
reuse them. This module preserves the original CLI import path.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from soothe_sdk.display._text_utils import (
|
|
10
|
+
normalize_tool_name,
|
|
11
|
+
text_looks_like_error,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
__all__ = ["normalize_tool_name", "text_looks_like_error"]
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Re-export shim for ``soothe_sdk.display.message_processing`` (RFC-413).
|
|
2
|
+
|
|
3
|
+
These helpers live in the SDK so the daemon-resident ``CardBinder`` can
|
|
4
|
+
reuse them. This module preserves the original CLI import path used
|
|
5
|
+
across the runtime, TUI, and tests.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
# Underscore-prefixed names below are re-exported intentionally — they are
|
|
11
|
+
# imported by other CLI modules (tool_call_resolution, widgets/messages, etc.)
|
|
12
|
+
# and CLI tests. Keep them in __all__ to keep `ruff` from stripping them.
|
|
13
|
+
from soothe_sdk.display.message_processing import (
|
|
14
|
+
_normalize_tool_name_for_arg_map,
|
|
15
|
+
_pending_or_overlay_id_matches_lookup,
|
|
16
|
+
_resolve_pending_lookup_tool_name,
|
|
17
|
+
accumulate_tool_call_chunks,
|
|
18
|
+
coerce_tool_call_args_to_dict,
|
|
19
|
+
coerce_tool_call_entry_to_dict,
|
|
20
|
+
extract_tool_args_dict,
|
|
21
|
+
extract_tool_brief,
|
|
22
|
+
finalize_pending_tool_call,
|
|
23
|
+
ingest_tool_call_stream_state,
|
|
24
|
+
normalize_tool_calls_list,
|
|
25
|
+
richest_pending_args_for_lookup,
|
|
26
|
+
seed_pending_tool_calls_from_message,
|
|
27
|
+
tool_calls_have_any_arg_dict,
|
|
28
|
+
tool_ids_touched_by_stream_message,
|
|
29
|
+
tool_lookup_step_id,
|
|
30
|
+
try_parse_pending_tool_call_args,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"_normalize_tool_name_for_arg_map",
|
|
35
|
+
"_pending_or_overlay_id_matches_lookup",
|
|
36
|
+
"_resolve_pending_lookup_tool_name",
|
|
37
|
+
"accumulate_tool_call_chunks",
|
|
38
|
+
"coerce_tool_call_args_to_dict",
|
|
39
|
+
"coerce_tool_call_entry_to_dict",
|
|
40
|
+
"extract_tool_args_dict",
|
|
41
|
+
"extract_tool_brief",
|
|
42
|
+
"finalize_pending_tool_call",
|
|
43
|
+
"ingest_tool_call_stream_state",
|
|
44
|
+
"normalize_tool_calls_list",
|
|
45
|
+
"richest_pending_args_for_lookup",
|
|
46
|
+
"seed_pending_tool_calls_from_message",
|
|
47
|
+
"tool_calls_have_any_arg_dict",
|
|
48
|
+
"tool_ids_touched_by_stream_message",
|
|
49
|
+
"tool_lookup_step_id",
|
|
50
|
+
"try_parse_pending_tool_call_args",
|
|
51
|
+
]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Re-export shim for ``soothe_sdk.display.tool_message_format`` (RFC-413)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from soothe_sdk.display.tool_message_format import (
|
|
6
|
+
format_content_block_for_tool_display,
|
|
7
|
+
format_tool_message_content,
|
|
8
|
+
run_python_envelope_indicates_failure,
|
|
9
|
+
try_parse_run_python_result_envelope,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"format_content_block_for_tool_display",
|
|
14
|
+
"format_tool_message_content",
|
|
15
|
+
"run_python_envelope_indicates_failure",
|
|
16
|
+
"try_parse_run_python_result_envelope",
|
|
17
|
+
]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Re-export shim for ``soothe_sdk.display.tool_result`` (RFC-413)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from soothe_sdk.display.tool_result import (
|
|
6
|
+
ToolResultPayload,
|
|
7
|
+
extract_tool_result_payload,
|
|
8
|
+
infer_tool_output_suggests_error,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"ToolResultPayload",
|
|
13
|
+
"extract_tool_result_payload",
|
|
14
|
+
"infer_tool_output_suggests_error",
|
|
15
|
+
]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Transcript message models for TUI display.
|
|
2
|
+
|
|
3
|
+
These types live in ``soothe_sdk.display.transcript_types`` so they can be
|
|
4
|
+
shared with the daemon-resident ``CardBinder`` (RFC-413). This module
|
|
5
|
+
re-exports them to preserve the CLI's existing import paths.
|
|
6
|
+
|
|
7
|
+
DOM virtualization lives in ``soothe_cli.tui.widgets.message_store``.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from soothe_sdk.display.transcript_types import (
|
|
13
|
+
UPDATABLE_FIELDS,
|
|
14
|
+
MessageData,
|
|
15
|
+
MessageType,
|
|
16
|
+
ToolStatus,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"UPDATABLE_FIELDS",
|
|
21
|
+
"MessageData",
|
|
22
|
+
"MessageType",
|
|
23
|
+
"ToolStatus",
|
|
24
|
+
]
|
|
@@ -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:
|
|
@@ -381,6 +390,57 @@ class TuiDaemonSession:
|
|
|
381
390
|
await connect_websocket_with_retries(self._rpc_client)
|
|
382
391
|
self._rpc_connected = True
|
|
383
392
|
|
|
393
|
+
async def fetch_loop_cards(self, loop_id: str) -> SimpleNamespace:
|
|
394
|
+
"""Fetch the daemon's bound display-card snapshot for a loop.
|
|
395
|
+
|
|
396
|
+
RFC-413: returns a populated ledger (eagerly backfilled if the loop
|
|
397
|
+
has no ``cards.jsonl`` yet) so resume can render through the same
|
|
398
|
+
binder that produced the original cards.
|
|
399
|
+
|
|
400
|
+
Args:
|
|
401
|
+
loop_id: AgentLoop id.
|
|
402
|
+
|
|
403
|
+
Returns:
|
|
404
|
+
``SimpleNamespace`` with ``cards: list[dict]``, ``seq: int``,
|
|
405
|
+
``success: bool``. On error, ``cards=[]`` and ``success=False`` so
|
|
406
|
+
the caller can fall back to the legacy resume path.
|
|
407
|
+
"""
|
|
408
|
+
lid = str(loop_id or "").strip()
|
|
409
|
+
if not lid:
|
|
410
|
+
return SimpleNamespace(cards=[], seq=0, success=False)
|
|
411
|
+
|
|
412
|
+
async with self._rpc_lock:
|
|
413
|
+
await self._ensure_rpc_connected()
|
|
414
|
+
try:
|
|
415
|
+
resp = await self._rpc_client.request_response(
|
|
416
|
+
{"type": "loop_cards_fetch", "loop_id": lid},
|
|
417
|
+
response_type="loop_cards_fetch_response",
|
|
418
|
+
timeout=30.0,
|
|
419
|
+
)
|
|
420
|
+
except Exception:
|
|
421
|
+
logger.warning(
|
|
422
|
+
"loop_cards_fetch failed for loop %s",
|
|
423
|
+
lid[:16],
|
|
424
|
+
exc_info=True,
|
|
425
|
+
)
|
|
426
|
+
return SimpleNamespace(cards=[], seq=0, success=False)
|
|
427
|
+
|
|
428
|
+
raw_cards = resp.get("cards")
|
|
429
|
+
cards = list(raw_cards) if isinstance(raw_cards, list) else []
|
|
430
|
+
seq = int(resp.get("seq") or 0)
|
|
431
|
+
context_tokens_raw = resp.get("context_tokens")
|
|
432
|
+
context_tokens = (
|
|
433
|
+
context_tokens_raw
|
|
434
|
+
if isinstance(context_tokens_raw, int) and context_tokens_raw >= 0
|
|
435
|
+
else 0
|
|
436
|
+
)
|
|
437
|
+
return SimpleNamespace(
|
|
438
|
+
cards=cards,
|
|
439
|
+
seq=seq,
|
|
440
|
+
context_tokens=context_tokens,
|
|
441
|
+
success=True,
|
|
442
|
+
)
|
|
443
|
+
|
|
384
444
|
async def aget_loop_state(self, loop_id: str) -> Any:
|
|
385
445
|
"""Load agent-loop state channels from the daemon (``loop_state_get`` RPC).
|
|
386
446
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Re-export shim for ``soothe_sdk.display.text_extract`` (RFC-413)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from soothe_sdk.display.text_extract import (
|
|
6
|
+
extract_ai_text_for_display,
|
|
7
|
+
extract_user_text_for_display,
|
|
8
|
+
normalize_stream_message,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"extract_ai_text_for_display",
|
|
13
|
+
"extract_user_text_for_display",
|
|
14
|
+
"normalize_stream_message",
|
|
15
|
+
]
|
|
@@ -25,7 +25,7 @@ def extract_text_from_message_content(content: Any) -> str:
|
|
|
25
25
|
parts.append(block)
|
|
26
26
|
elif isinstance(block, dict) and "text" in block:
|
|
27
27
|
parts.append(str(block["text"]))
|
|
28
|
-
return "".join(parts)
|
|
28
|
+
return "\n".join(parts)
|
|
29
29
|
return ""
|
|
30
30
|
|
|
31
31
|
|
|
@@ -41,7 +41,7 @@ def extract_plain_text_from_stream_message(msg: Any) -> str:
|
|
|
41
41
|
if text:
|
|
42
42
|
texts.append(str(text))
|
|
43
43
|
if texts:
|
|
44
|
-
return "".join(texts)
|
|
44
|
+
return "\n".join(texts)
|
|
45
45
|
if hasattr(msg, "content"):
|
|
46
46
|
return extract_text_from_message_content(getattr(msg, "content", None))
|
|
47
47
|
if isinstance(msg, dict):
|
|
@@ -54,7 +54,7 @@ def extract_plain_text_from_stream_message(msg: Any) -> str:
|
|
|
54
54
|
text = block.get("text", "")
|
|
55
55
|
if text:
|
|
56
56
|
texts.append(str(text))
|
|
57
|
-
return "".join(texts)
|
|
57
|
+
return "\n".join(texts)
|
|
58
58
|
content = body.get("content", "")
|
|
59
59
|
if isinstance(content, str):
|
|
60
60
|
return content
|
|
@@ -224,6 +224,9 @@ class SootheApp(
|
|
|
224
224
|
|
|
225
225
|
self._agent_running = False
|
|
226
226
|
|
|
227
|
+
self._bg_event_worker: Worker[None] | None = None
|
|
228
|
+
"""Background daemon event consumer worker (cancelled on active turn start)."""
|
|
229
|
+
|
|
227
230
|
self._server_startup_error: str | None = None
|
|
228
231
|
"""Set when daemon bootstrap fails; persists for the session lifetime."""
|
|
229
232
|
|
|
@@ -246,7 +246,12 @@ class _ExecutionMixin:
|
|
|
246
246
|
# the wire ``clarification_answer`` flag plus the ``clarification_answers``
|
|
247
247
|
# list, then clears the persisted flag so a follow-up turn is treated
|
|
248
248
|
# as a new goal.
|
|
249
|
-
|
|
249
|
+
#
|
|
250
|
+
# Use ``_send_to_agent`` (not a direct ``await _run_agent_task``) so the
|
|
251
|
+
# resumed turn runs in a Textual worker. Awaiting the task inline blocks
|
|
252
|
+
# the message handler — and therefore the event loop — until the loop
|
|
253
|
+
# next pauses, which freezes scrolling and chat-input focus.
|
|
254
|
+
await self._send_to_agent(payload_text)
|
|
250
255
|
|
|
251
256
|
async def _handle_shell_command(self, command: str) -> None:
|
|
252
257
|
"""Handle a shell command (! prefix).
|
|
@@ -335,10 +340,13 @@ class _ExecutionMixin:
|
|
|
335
340
|
self._shell_process = None
|
|
336
341
|
self._shell_running = False
|
|
337
342
|
self._shell_worker = None
|
|
338
|
-
|
|
339
|
-
|
|
343
|
+
|
|
344
|
+
# Restore input focus first so the user can type immediately.
|
|
340
345
|
if self._chat_input:
|
|
341
346
|
self._chat_input.set_cursor_active(active=True)
|
|
347
|
+
|
|
348
|
+
if was_interrupted:
|
|
349
|
+
await self._mount_message(AppMessage("Command interrupted"))
|
|
342
350
|
try:
|
|
343
351
|
await self._maybe_drain_deferred()
|
|
344
352
|
except Exception:
|
|
@@ -485,10 +493,10 @@ class _ExecutionMixin:
|
|
|
485
493
|
elif cmd == "/help":
|
|
486
494
|
await self._mount_message(UserMessage(command))
|
|
487
495
|
help_body = (
|
|
488
|
-
"Commands: /quit, /clear, /editor, /autopilot, /mcp, "
|
|
496
|
+
"Commands: /quit, /clear, /editor, /autopilot <task>, /autopilot-dashboard, /mcp, "
|
|
489
497
|
"/model [--model-params JSON] [--default], /notifications, "
|
|
490
498
|
"/reload, /skill:<name>, /theme, "
|
|
491
|
-
"/tokens, /
|
|
499
|
+
"/tokens, /resume, "
|
|
492
500
|
"/research, /explore, /plan, /«subagent» (when configured), "
|
|
493
501
|
"/update, /auto-update, /changelog, /docs, /feedback, /help\n\n"
|
|
494
502
|
"Interactive Features:\n"
|
|
@@ -571,7 +579,7 @@ class _ExecutionMixin:
|
|
|
571
579
|
await self._mount_message(AppMessage(f"Started new loop: {new_loop_id}"))
|
|
572
580
|
elif cmd == "/editor":
|
|
573
581
|
await self.action_open_editor()
|
|
574
|
-
elif cmd == "/
|
|
582
|
+
elif cmd == "/resume":
|
|
575
583
|
await self._show_loop_selector()
|
|
576
584
|
elif cmd == "/update":
|
|
577
585
|
await self._handle_update_command()
|
|
@@ -628,7 +636,19 @@ class _ExecutionMixin:
|
|
|
628
636
|
args = command.strip()[len("/skill-creator") :].strip()
|
|
629
637
|
rewritten = f"/skill:skill-creator {args}" if args else "/skill:skill-creator"
|
|
630
638
|
await self._handle_skill_command(rewritten)
|
|
631
|
-
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":
|
|
632
652
|
await self._show_autopilot_dashboard()
|
|
633
653
|
elif cmd == "/mcp":
|
|
634
654
|
await self._show_mcp_viewer()
|
|
@@ -813,6 +833,12 @@ class _ExecutionMixin:
|
|
|
813
833
|
return
|
|
814
834
|
self._agent_running = True
|
|
815
835
|
|
|
836
|
+
# Cancel background event consumer so it doesn't compete for
|
|
837
|
+
# WebSocket reads with the active turn's iter_turn_chunks().
|
|
838
|
+
if self._bg_event_worker is not None:
|
|
839
|
+
self._bg_event_worker.cancel()
|
|
840
|
+
self._bg_event_worker = None
|
|
841
|
+
|
|
816
842
|
if self._chat_input:
|
|
817
843
|
self._chat_input.set_cursor_active(active=False)
|
|
818
844
|
|
|
@@ -950,12 +976,14 @@ class _ExecutionMixin:
|
|
|
950
976
|
self._agent_running = False
|
|
951
977
|
self._agent_worker = None
|
|
952
978
|
|
|
953
|
-
#
|
|
954
|
-
|
|
955
|
-
|
|
979
|
+
# Restore input focus FIRST so the user can start typing immediately
|
|
980
|
+
# while remaining cleanup (spinner, tokens, deferred) runs.
|
|
956
981
|
if self._chat_input:
|
|
957
982
|
self._chat_input.set_cursor_active(active=True)
|
|
958
983
|
|
|
984
|
+
# Remove spinner if present
|
|
985
|
+
await self._set_spinner(None)
|
|
986
|
+
|
|
959
987
|
# Ensure token display is restored (in case of early cancellation).
|
|
960
988
|
# Pass the cached approximate flag so an interrupted "+" isn't clobbered.
|
|
961
989
|
self._show_tokens(approximate=self._tokens_approximate)
|