soothe-cli 0.7.1__tar.gz → 0.7.3__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.7.1 → soothe_cli-0.7.3}/.gitignore +9 -3
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/PKG-INFO +1 -1
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/cli/commands/autopilot_cmd.py +1 -31
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/cli/commands/cron_cmd.py +13 -4
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/cli/commands/status_cmd.py +26 -55
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/cli/execution/daemon.py +2 -2
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/cli/main.py +27 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/config/cli_config.py +3 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/runtime/policy/essential_events.py +3 -1
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/runtime/state/session_stats.py +89 -44
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/runtime/transport/session.py +0 -8
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/runtime/turn/pipeline.py +99 -4
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/runtime/turn/prepare.py +16 -1
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/_env_vars.py +28 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/app/_app.py +15 -3
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/app/_execution.py +11 -32
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/app/_messages_mixin.py +120 -43
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/app/_model.py +15 -10
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/app/_startup.py +99 -41
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/app/_ui.py +42 -5
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/app/app.tcss +13 -1
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/binding.py +0 -2
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/command_registry.py +2 -8
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/commands/slash_commands.py +34 -14
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/commands/subagent_routing.py +3 -8
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/file_change_notify.py +23 -1
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/file_change_renderers.py +41 -0
- soothe_cli-0.7.3/src/soothe_cli/tui/markdown_theme.py +358 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/model_config.py +8 -10
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/preview_limits.py +4 -4
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/sessions.py +8 -0
- soothe_cli-0.7.3/src/soothe_cli/tui/shell_color.py +48 -0
- soothe_cli-0.7.3/src/soothe_cli/tui/spinner_labels.py +55 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/textual_adapter.py +426 -114
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/autopilot_dashboard.py +3 -8
- soothe_cli-0.7.3/src/soothe_cli/tui/widgets/diff.py +228 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/file_change_preview.py +111 -85
- soothe_cli-0.7.3/src/soothe_cli/tui/widgets/help_screen.py +210 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/loading.py +65 -17
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/loop_selector.py +22 -131
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/messages/__init__.py +0 -2
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/messages/_helpers.py +0 -42
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/messages/assistant.py +11 -4
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/messages/clarification.py +6 -14
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/messages/cognition_goal_tree.py +126 -24
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/messages/cognition_reason.py +3 -10
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/messages/cognition_step.py +5 -2
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/messages/cognition_step_activity.py +7 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/messages/skill.py +2 -4
- soothe_cli-0.7.3/src/soothe_cli/tui/widgets/plan_quick_view_overlay.py +150 -0
- soothe_cli-0.7.1/src/soothe_cli/tui/widgets/diff.py +0 -143
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/README.md +0 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/pyproject.toml +0 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/__init__.py +0 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/cli/__init__.py +0 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/cli/commands/__init__.py +0 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/cli/commands/loop_cmd.py +0 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/cli/commands/run_cmd.py +0 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/cli/execution/__init__.py +0 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/cli/execution/daemon_errors.py +0 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/cli/execution/headless.py +0 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/cli/execution/headless_renderer.py +0 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/cli/execution/launcher.py +0 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/config/__init__.py +0 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/config/loader.py +0 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/config/logging_setup.py +0 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/runtime/__init__.py +0 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/runtime/headless/processor.py +0 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/runtime/headless/processor_state.py +0 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/runtime/parse/message_processing.py +0 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/runtime/parse/tool_call_resolution.py +0 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/runtime/parse/tool_result.py +0 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/runtime/policy/display_policy.py +0 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/runtime/policy/tui_trace_log.py +0 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/runtime/presentation/duration_format.py +0 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/runtime/presentation/engine.py +0 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/runtime/presentation/explore_task_display.py +0 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/runtime/presentation/renderer_base.py +0 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/runtime/presentation/renderer_protocol.py +0 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/runtime/state/file_tracker.py +0 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/runtime/state/step_router.py +0 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/runtime/state/stream_accumulator.py +0 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/runtime/state/transcript.py +0 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/runtime/wire/chunk_filter.py +0 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/runtime/wire/display_text.py +0 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/runtime/wire/message_text.py +0 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/runtime/wire/messages.py +0 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/__init__.py +0 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/_cli_context.py +0 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/_version.py +0 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/app/__init__.py +0 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/app/_commands.py +0 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/app/_history.py +0 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/app/_module_init.py +0 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/commands/__init__.py +0 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/commands/command_router.py +0 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/config.py +0 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/hooks.py +0 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/input.py +0 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/media_utils.py +0 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/path_utils.py +0 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/project_utils.py +0 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/skills/__init__.py +0 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/skills/invocation.py +0 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/skills/load.py +0 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/theme.py +0 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/tips.py +0 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/tool_display.py +0 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/unicode_security.py +0 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/update_check.py +0 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/__init__.py +0 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/_links.py +0 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/autocomplete.py +0 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/autopilot_screen.py +0 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/chat_input.py +0 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/clipboard.py +0 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/context_viewer.py +0 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/editor.py +0 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/history.py +0 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/mcp_viewer.py +0 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/message_store.py +0 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/messages/app.py +0 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/messages/cognition_subagent.py +0 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/messages/diff_message.py +0 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/messages/error.py +0 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/messages/user.py +0 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/model_selector.py +0 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/notification_settings.py +0 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/status.py +0 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/theme_selector.py +0 -0
- {soothe_cli-0.7.1 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/welcome.py +0 -0
|
@@ -191,6 +191,15 @@ cython_debug/
|
|
|
191
191
|
# PyPI configuration file
|
|
192
192
|
.pypirc
|
|
193
193
|
|
|
194
|
+
# Soothe local runtime data
|
|
195
|
+
.soothe/
|
|
196
|
+
|
|
197
|
+
# Tool backup files (created by file editing tools)
|
|
198
|
+
.backups/
|
|
199
|
+
|
|
200
|
+
# Large tool result cache
|
|
201
|
+
large_tool_results/
|
|
202
|
+
|
|
194
203
|
# Cursor
|
|
195
204
|
# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
|
|
196
205
|
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
|
|
@@ -206,7 +215,6 @@ __marimo__/
|
|
|
206
215
|
.python-version
|
|
207
216
|
thirdparty/
|
|
208
217
|
.cursor/
|
|
209
|
-
large_tool_results/
|
|
210
218
|
plot_*
|
|
211
219
|
.claude/
|
|
212
220
|
.DS_Store
|
|
@@ -216,5 +224,3 @@ manifest.json
|
|
|
216
224
|
_bmad
|
|
217
225
|
__MACOSX
|
|
218
226
|
.qoder
|
|
219
|
-
.soothe
|
|
220
|
-
.backups/
|
|
@@ -55,7 +55,7 @@ def run(
|
|
|
55
55
|
max_iterations: int | None = typer.Option(
|
|
56
56
|
None,
|
|
57
57
|
"--max-iterations",
|
|
58
|
-
help="Ignored — use daemon config agent.
|
|
58
|
+
help="Ignored — use daemon config agent.autopilot.max_iterations.",
|
|
59
59
|
),
|
|
60
60
|
workspace: str | None = typer.Option(
|
|
61
61
|
None,
|
|
@@ -333,36 +333,6 @@ def cancel_goal(
|
|
|
333
333
|
typer.echo(f"Cancel result: {result.get('status', result)}")
|
|
334
334
|
|
|
335
335
|
|
|
336
|
-
@app.command("approve")
|
|
337
|
-
def approve_goal(
|
|
338
|
-
goal_id: str = typer.Argument(
|
|
339
|
-
..., help="Confirmation ID to approve (use 'inbox' to list pending)."
|
|
340
|
-
),
|
|
341
|
-
) -> None:
|
|
342
|
-
"""Approve a MUST-confirmation goal."""
|
|
343
|
-
client = _require_daemon_ws()
|
|
344
|
-
try:
|
|
345
|
-
result = client.autopilot_approve(goal_id)
|
|
346
|
-
except RuntimeError as exc:
|
|
347
|
-
typer.echo(str(exc), err=True)
|
|
348
|
-
raise typer.Exit(1) from exc
|
|
349
|
-
typer.echo(f"Confirmation approved: {result.get('goal_id', goal_id)}")
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
@app.command("reject")
|
|
353
|
-
def reject_goal(
|
|
354
|
-
goal_id: str = typer.Argument(..., help="Confirmation ID to reject."),
|
|
355
|
-
) -> None:
|
|
356
|
-
"""Reject a proposed goal."""
|
|
357
|
-
client = _require_daemon_ws()
|
|
358
|
-
try:
|
|
359
|
-
result = client.autopilot_reject(goal_id)
|
|
360
|
-
except RuntimeError as exc:
|
|
361
|
-
typer.echo(str(exc), err=True)
|
|
362
|
-
raise typer.Exit(1) from exc
|
|
363
|
-
typer.echo(f"Confirmation rejected: {result.get('goal_id', goal_id)}")
|
|
364
|
-
|
|
365
|
-
|
|
366
336
|
@app.command("resume")
|
|
367
337
|
def resume_goal(
|
|
368
338
|
goal_id: str = typer.Argument(..., help="Goal ID to resume."),
|
|
@@ -85,11 +85,16 @@ def add_job(
|
|
|
85
85
|
try:
|
|
86
86
|
result = client.cron_add(text, priority=priority)
|
|
87
87
|
except RuntimeError as exc:
|
|
88
|
-
|
|
88
|
+
message = str(exc)
|
|
89
|
+
if "Autopilot is disabled" in message:
|
|
90
|
+
console.print(f"[yellow]{message}[/yellow]")
|
|
91
|
+
else:
|
|
92
|
+
typer.echo(f"Error: {message}", err=True)
|
|
89
93
|
sys.exit(1)
|
|
90
94
|
|
|
91
95
|
job = result.get("job") or {}
|
|
92
96
|
job_id = job.get("id", "?")
|
|
97
|
+
is_duplicate = bool(result.get("duplicate"))
|
|
93
98
|
console.print(
|
|
94
99
|
Panel(
|
|
95
100
|
"\n".join(
|
|
@@ -101,10 +106,14 @@ def add_job(
|
|
|
101
106
|
f"[bold]Status:[/bold] [green]{job.get('status', 'pending')}[/green]",
|
|
102
107
|
]
|
|
103
108
|
),
|
|
104
|
-
title="Scheduled Job Created",
|
|
105
|
-
border_style="green",
|
|
109
|
+
title="Scheduled Job Already Exists" if is_duplicate else "Scheduled Job Created",
|
|
110
|
+
border_style="yellow" if is_duplicate else "green",
|
|
106
111
|
)
|
|
107
112
|
)
|
|
113
|
+
if is_duplicate:
|
|
114
|
+
console.print(
|
|
115
|
+
"[yellow]An identical active job is already scheduled; no new job was created.[/yellow]"
|
|
116
|
+
)
|
|
108
117
|
|
|
109
118
|
|
|
110
119
|
@app.command("list")
|
|
@@ -128,7 +137,7 @@ def list_jobs(
|
|
|
128
137
|
"""
|
|
129
138
|
client = _require_cron_client()
|
|
130
139
|
try:
|
|
131
|
-
result = client.
|
|
140
|
+
result = client.cron_list(status=status)
|
|
132
141
|
except RuntimeError as exc:
|
|
133
142
|
typer.echo(f"Error: {exc}", err=True)
|
|
134
143
|
sys.exit(1)
|
|
@@ -50,47 +50,6 @@ async def _fetch_status(ws_url: str, timeout: float = 5.0) -> dict[str, Any]:
|
|
|
50
50
|
await client.close()
|
|
51
51
|
|
|
52
52
|
|
|
53
|
-
async def _fetch_ready_state(ws_url: str, timeout: float = 5.0) -> dict[str, Any] | None:
|
|
54
|
-
"""Fetch daemon readiness state via WebSocket connection_init/ack handshake.
|
|
55
|
-
|
|
56
|
-
Sends ``connection_init`` and waits for ``connection_ack`` which carries
|
|
57
|
-
the daemon's ``readiness_state``.
|
|
58
|
-
|
|
59
|
-
Args:
|
|
60
|
-
ws_url: WebSocket URL.
|
|
61
|
-
timeout: Timeout for handshake.
|
|
62
|
-
|
|
63
|
-
Returns:
|
|
64
|
-
``connection_ack`` result dict (containing ``readiness_state``) or None.
|
|
65
|
-
"""
|
|
66
|
-
import websockets
|
|
67
|
-
|
|
68
|
-
init_msg = json.dumps(
|
|
69
|
-
{
|
|
70
|
-
"proto": "1",
|
|
71
|
-
"type": "connection_init",
|
|
72
|
-
"params": {
|
|
73
|
-
"client_version": "0.5.0",
|
|
74
|
-
"accept_proto": ["1"],
|
|
75
|
-
"capabilities": ["streaming"],
|
|
76
|
-
},
|
|
77
|
-
}
|
|
78
|
-
)
|
|
79
|
-
|
|
80
|
-
try:
|
|
81
|
-
async with asyncio.timeout(timeout):
|
|
82
|
-
async with websockets.connect(ws_url) as ws:
|
|
83
|
-
await ws.send(init_msg)
|
|
84
|
-
for _ in range(3):
|
|
85
|
-
msg = await ws.recv()
|
|
86
|
-
data = json.loads(msg)
|
|
87
|
-
if data.get("type") == "connection_ack":
|
|
88
|
-
return data.get("result", data)
|
|
89
|
-
except Exception:
|
|
90
|
-
pass
|
|
91
|
-
return None
|
|
92
|
-
|
|
93
|
-
|
|
94
53
|
def _render_unified_status_table(
|
|
95
54
|
config: Any,
|
|
96
55
|
ws_url: str,
|
|
@@ -156,10 +115,6 @@ def daemon_status(
|
|
|
156
115
|
bool,
|
|
157
116
|
typer.Option("--json", help="Output as JSON."),
|
|
158
117
|
] = False,
|
|
159
|
-
verbose: Annotated[
|
|
160
|
-
bool,
|
|
161
|
-
typer.Option("--verbose", "-v", help="Show detailed status."),
|
|
162
|
-
] = False,
|
|
163
118
|
) -> None:
|
|
164
119
|
"""Check daemon status from client side.
|
|
165
120
|
|
|
@@ -168,7 +123,6 @@ def daemon_status(
|
|
|
168
123
|
Examples:
|
|
169
124
|
soothe status daemon
|
|
170
125
|
soothe status daemon --json
|
|
171
|
-
soothe status daemon -v
|
|
172
126
|
"""
|
|
173
127
|
config = load_config()
|
|
174
128
|
ws_url = websocket_url_from_config(config)
|
|
@@ -217,11 +171,6 @@ def daemon_status(
|
|
|
217
171
|
)
|
|
218
172
|
sys.exit(1)
|
|
219
173
|
|
|
220
|
-
# Get readiness state for verbose mode
|
|
221
|
-
ready_state = None
|
|
222
|
-
if verbose:
|
|
223
|
-
ready_state = asyncio.run(_fetch_ready_state(ws_url, timeout=5.0))
|
|
224
|
-
|
|
225
174
|
if json_output:
|
|
226
175
|
output = {
|
|
227
176
|
"status": "running",
|
|
@@ -232,10 +181,10 @@ def daemon_status(
|
|
|
232
181
|
"daemon_pid": status.get("daemon_pid"),
|
|
233
182
|
"daemon_version": status.get("daemon_version"),
|
|
234
183
|
"core_version": status.get("core_version"),
|
|
184
|
+
"readiness_state": status.get("readiness_state", "unknown"),
|
|
235
185
|
}
|
|
236
|
-
if
|
|
237
|
-
output["
|
|
238
|
-
output["readiness_message"] = ready_state.get("message")
|
|
186
|
+
if status.get("readiness_message"):
|
|
187
|
+
output["readiness_message"] = status.get("readiness_message")
|
|
239
188
|
console.print_json(json.dumps(output))
|
|
240
189
|
return
|
|
241
190
|
|
|
@@ -246,6 +195,15 @@ def daemon_status(
|
|
|
246
195
|
daemon_pid = status.get("daemon_pid")
|
|
247
196
|
daemon_version = status.get("daemon_version")
|
|
248
197
|
core_version = status.get("core_version")
|
|
198
|
+
# Use readiness_state from daemon_status RPC (already includes state + message)
|
|
199
|
+
readiness_state_from_status = (
|
|
200
|
+
{
|
|
201
|
+
"state": status.get("readiness_state", "unknown"),
|
|
202
|
+
"message": status.get("readiness_message"),
|
|
203
|
+
}
|
|
204
|
+
if status.get("readiness_state")
|
|
205
|
+
else None
|
|
206
|
+
)
|
|
249
207
|
|
|
250
208
|
table = _render_unified_status_table(
|
|
251
209
|
config,
|
|
@@ -254,7 +212,7 @@ def daemon_status(
|
|
|
254
212
|
port_live,
|
|
255
213
|
active_threads,
|
|
256
214
|
daemon_pid,
|
|
257
|
-
|
|
215
|
+
readiness_state_from_status,
|
|
258
216
|
daemon_live=True,
|
|
259
217
|
daemon_version=daemon_version,
|
|
260
218
|
core_version=core_version,
|
|
@@ -349,6 +307,9 @@ def status_main(
|
|
|
349
307
|
output["daemon"]["daemon_pid"] = status.get("daemon_pid")
|
|
350
308
|
output["daemon"]["daemon_version"] = status.get("daemon_version")
|
|
351
309
|
output["daemon"]["core_version"] = status.get("core_version")
|
|
310
|
+
output["daemon"]["readiness_state"] = status.get("readiness_state", "unknown")
|
|
311
|
+
if status.get("readiness_message"):
|
|
312
|
+
output["daemon"]["readiness_message"] = status.get("readiness_message")
|
|
352
313
|
console.print_json(json.dumps(output))
|
|
353
314
|
return
|
|
354
315
|
|
|
@@ -378,6 +339,15 @@ def status_main(
|
|
|
378
339
|
daemon_pid = status.get("daemon_pid")
|
|
379
340
|
daemon_version = status.get("daemon_version")
|
|
380
341
|
core_version = status.get("core_version")
|
|
342
|
+
# Use readiness_state from daemon_status RPC (already includes state + message)
|
|
343
|
+
readiness_state_from_status = (
|
|
344
|
+
{
|
|
345
|
+
"state": status.get("readiness_state", "unknown"),
|
|
346
|
+
"message": status.get("readiness_message"),
|
|
347
|
+
}
|
|
348
|
+
if status.get("readiness_state")
|
|
349
|
+
else None
|
|
350
|
+
)
|
|
381
351
|
|
|
382
352
|
table = _render_unified_status_table(
|
|
383
353
|
config,
|
|
@@ -386,6 +356,7 @@ def status_main(
|
|
|
386
356
|
port_live,
|
|
387
357
|
active_threads,
|
|
388
358
|
daemon_pid,
|
|
359
|
+
readiness_state_from_status,
|
|
389
360
|
daemon_live=True,
|
|
390
361
|
daemon_version=daemon_version,
|
|
391
362
|
core_version=core_version,
|
|
@@ -8,7 +8,6 @@ from __future__ import annotations
|
|
|
8
8
|
|
|
9
9
|
import asyncio
|
|
10
10
|
import logging
|
|
11
|
-
import os
|
|
12
11
|
import signal
|
|
13
12
|
from typing import Any
|
|
14
13
|
|
|
@@ -28,6 +27,7 @@ from soothe_cli.cli.execution.daemon_errors import (
|
|
|
28
27
|
from soothe_cli.cli.execution.headless_renderer import HeadlessCliRenderer
|
|
29
28
|
from soothe_cli.runtime import EventProcessor
|
|
30
29
|
from soothe_cli.runtime.presentation.engine import PresentationEngine
|
|
30
|
+
from soothe_cli.tui._env_vars import resolve_cli_loop_workspace
|
|
31
31
|
from soothe_cli.tui.commands.subagent_routing import parse_subagent_from_input
|
|
32
32
|
|
|
33
33
|
logger = logging.getLogger(__name__)
|
|
@@ -151,7 +151,7 @@ async def _run_headless_session_once(
|
|
|
151
151
|
|
|
152
152
|
try:
|
|
153
153
|
await connect_websocket_with_retries(client)
|
|
154
|
-
cli_ws =
|
|
154
|
+
cli_ws = resolve_cli_loop_workspace()
|
|
155
155
|
# IG-441: three first-class modes (batch / adaptive / streaming).
|
|
156
156
|
# Default is ``adaptive`` for headless runs as well — it gives smooth
|
|
157
157
|
# progress on long synthesis while keeping wire traffic bounded.
|
|
@@ -15,6 +15,12 @@ from soothe_sdk.client.config import SOOTHE_HOME # noqa: E402
|
|
|
15
15
|
|
|
16
16
|
from soothe_cli.config.cli_config import CLIConfig # noqa: E402
|
|
17
17
|
from soothe_cli.config.loader import set_runtime_config # noqa: E402
|
|
18
|
+
from soothe_cli.tui.markdown_theme import ( # noqa: E402
|
|
19
|
+
DEFAULT_MARKDOWN_THEME,
|
|
20
|
+
REGISTRY,
|
|
21
|
+
load_markdown_theme_preference,
|
|
22
|
+
markdown_theme_help,
|
|
23
|
+
)
|
|
18
24
|
|
|
19
25
|
app = typer.Typer(
|
|
20
26
|
name="soothe",
|
|
@@ -101,6 +107,16 @@ def main(
|
|
|
101
107
|
help="Render assistant messages as Markdown in the TUI.",
|
|
102
108
|
),
|
|
103
109
|
] = True,
|
|
110
|
+
markdown_theme: Annotated[
|
|
111
|
+
str | None,
|
|
112
|
+
typer.Option(
|
|
113
|
+
"--markdown-theme",
|
|
114
|
+
help=(
|
|
115
|
+
"Markdown appearance preset for TUI cards "
|
|
116
|
+
f"({markdown_theme_help()}). Default: {DEFAULT_MARKDOWN_THEME}."
|
|
117
|
+
),
|
|
118
|
+
),
|
|
119
|
+
] = None,
|
|
104
120
|
soothe_home: Annotated[
|
|
105
121
|
str | None,
|
|
106
122
|
typer.Option("--soothe-home", help="Soothe home directory (default: ~/.soothe)."),
|
|
@@ -168,11 +184,22 @@ def main(
|
|
|
168
184
|
if mode is not None and mode not in ("manual", "auto"):
|
|
169
185
|
typer.echo(f"Invalid --mode {mode!r}; expected 'manual' or 'auto'.", err=True)
|
|
170
186
|
raise typer.Exit(code=2)
|
|
187
|
+
if markdown_theme is not None and markdown_theme not in REGISTRY:
|
|
188
|
+
typer.echo(
|
|
189
|
+
f"Invalid --markdown-theme {markdown_theme!r}; "
|
|
190
|
+
f"expected one of: {markdown_theme_help()}.",
|
|
191
|
+
err=True,
|
|
192
|
+
)
|
|
193
|
+
raise typer.Exit(code=2)
|
|
194
|
+
resolved_markdown_theme = (
|
|
195
|
+
markdown_theme if markdown_theme is not None else load_markdown_theme_preference()
|
|
196
|
+
)
|
|
171
197
|
cli_cfg = CLIConfig(
|
|
172
198
|
daemon_host=daemon_host,
|
|
173
199
|
daemon_port=daemon_port,
|
|
174
200
|
logging_level=log_level,
|
|
175
201
|
render_markdown=render_markdown,
|
|
202
|
+
markdown_theme=resolved_markdown_theme,
|
|
176
203
|
output_streaming_enabled=streaming,
|
|
177
204
|
output_streaming_mode=streaming_mode,
|
|
178
205
|
clarification_mode=mode,
|
|
@@ -29,6 +29,9 @@ class CLIConfig:
|
|
|
29
29
|
render_markdown: bool = True
|
|
30
30
|
"""Render assistant messages as Markdown in TUI (default True)."""
|
|
31
31
|
|
|
32
|
+
markdown_theme: str = "match-app"
|
|
33
|
+
"""Markdown appearance preset (``match-app``, ``langchain``, ``standard``, …)."""
|
|
34
|
+
|
|
32
35
|
# Output streaming overrides (RFC-614)
|
|
33
36
|
output_streaming_enabled: bool | None = None
|
|
34
37
|
"""Override daemon streaming enabled setting."""
|
|
@@ -24,12 +24,13 @@ STEP_COMPLETE_EVENT_TYPES: Final[frozenset[str]] = frozenset(
|
|
|
24
24
|
)
|
|
25
25
|
|
|
26
26
|
LOOP_REASON_EVENT_TYPE: Final[str] = "soothe.cognition.strange_loop.reasoned"
|
|
27
|
+
INTENT_CLASSIFIED_EVENT_TYPE: Final[str] = "soothe.cognition.intent.classified"
|
|
27
28
|
|
|
28
29
|
ESSENTIAL_PROGRESS_EVENT_TYPES: Final[frozenset[str]] = frozenset(
|
|
29
30
|
set(GOAL_START_EVENT_TYPES)
|
|
30
31
|
| set(STEP_START_EVENT_TYPES)
|
|
31
32
|
| set(STEP_COMPLETE_EVENT_TYPES)
|
|
32
|
-
| {LOOP_REASON_EVENT_TYPE}
|
|
33
|
+
| {LOOP_REASON_EVENT_TYPE, INTENT_CLASSIFIED_EVENT_TYPE}
|
|
33
34
|
)
|
|
34
35
|
|
|
35
36
|
|
|
@@ -56,6 +57,7 @@ def is_step_complete_event_type(event_type: str) -> bool:
|
|
|
56
57
|
__all__ = [
|
|
57
58
|
"ESSENTIAL_PROGRESS_EVENT_TYPES",
|
|
58
59
|
"GOAL_START_EVENT_TYPES",
|
|
60
|
+
"INTENT_CLASSIFIED_EVENT_TYPE",
|
|
59
61
|
"LOOP_REASON_EVENT_TYPE",
|
|
60
62
|
"STEP_COMPLETE_EVENT_TYPES",
|
|
61
63
|
"STEP_START_EVENT_TYPES",
|
|
@@ -8,14 +8,16 @@ config, no widget imports) so that `app.py` can import `SessionStats` and
|
|
|
8
8
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
|
+
import json
|
|
11
12
|
import time
|
|
12
13
|
from dataclasses import dataclass, field
|
|
14
|
+
from typing import Any
|
|
13
15
|
|
|
14
16
|
SpinnerStatus = str | None
|
|
15
17
|
"""Spinner line label, or `None` to hide.
|
|
16
18
|
|
|
17
|
-
Common values include ``Thinking``, ``
|
|
18
|
-
``Tools``
|
|
19
|
+
Common single-word values include ``Thinking``, ``Planning``, ``Executing``,
|
|
20
|
+
``Tools``, ``Offloading``, ``Writing``, and ``Synthesizing``.
|
|
19
21
|
"""
|
|
20
22
|
|
|
21
23
|
|
|
@@ -242,48 +244,91 @@ class TurnEventStats:
|
|
|
242
244
|
self.latency = TurnLatencyStats()
|
|
243
245
|
self.latency.merge(other.latency)
|
|
244
246
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
"""
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
if self.latency is not None
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
247
|
+
@staticmethod
|
|
248
|
+
def _nonzero_fields(**fields: int) -> dict[str, int]:
|
|
249
|
+
return {key: value for key, value in fields.items() if value}
|
|
250
|
+
|
|
251
|
+
def to_log_dict(self) -> dict[str, Any]:
|
|
252
|
+
"""Return structured event counters for ``cli.log`` JSON lines."""
|
|
253
|
+
payload: dict[str, Any] = {"total": self.total}
|
|
254
|
+
|
|
255
|
+
modes = self._nonzero_fields(
|
|
256
|
+
messages=self.messages,
|
|
257
|
+
updates=self.updates,
|
|
258
|
+
custom=self.custom,
|
|
259
|
+
)
|
|
260
|
+
if modes:
|
|
261
|
+
payload["modes"] = modes
|
|
262
|
+
|
|
263
|
+
activity = self._nonzero_fields(
|
|
264
|
+
tool_calls=self.tool_calls,
|
|
265
|
+
tool_results=self.tool_results,
|
|
266
|
+
text_chunks=self.text_chunks,
|
|
267
|
+
)
|
|
268
|
+
if activity:
|
|
269
|
+
payload["activity"] = activity
|
|
270
|
+
|
|
271
|
+
degradation = self._nonzero_fields(
|
|
272
|
+
skipped=self.skipped,
|
|
273
|
+
filtered_early=self.filtered_early,
|
|
274
|
+
heartbeats_dropped=self.heartbeats_dropped,
|
|
275
|
+
post_idle_drained=self.post_idle_drained,
|
|
276
|
+
inbound_dropped=self.inbound_dropped,
|
|
277
|
+
)
|
|
278
|
+
if degradation:
|
|
279
|
+
payload["degradation"] = degradation
|
|
280
|
+
|
|
281
|
+
if self.latency is not None:
|
|
282
|
+
latency: dict[str, int] = {}
|
|
283
|
+
if self.latency.time_to_first_chunk_ms is not None:
|
|
284
|
+
latency["ttfc_ms"] = round(self.latency.time_to_first_chunk_ms)
|
|
285
|
+
if self.latency.synthesis_visible_ms is not None:
|
|
286
|
+
latency["synthesis_ms"] = round(self.latency.synthesis_visible_ms)
|
|
287
|
+
if latency:
|
|
288
|
+
payload["latency"] = latency
|
|
289
|
+
|
|
290
|
+
return payload
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def build_goal_completed_log_event(
|
|
294
|
+
ev_stats: TurnEventStats,
|
|
295
|
+
*,
|
|
296
|
+
status: str,
|
|
297
|
+
goal_progress: str,
|
|
298
|
+
total_steps: int,
|
|
299
|
+
elapsed_seconds: float,
|
|
300
|
+
) -> dict[str, Any]:
|
|
301
|
+
"""Build a structured goal-completion record for ``cli.log``."""
|
|
302
|
+
event: dict[str, Any] = {
|
|
303
|
+
"event": "goal_completed",
|
|
304
|
+
"elapsed_s": round(elapsed_seconds, 1),
|
|
305
|
+
"events": ev_stats.to_log_dict(),
|
|
306
|
+
}
|
|
307
|
+
if status.strip():
|
|
308
|
+
event["status"] = status.strip()
|
|
309
|
+
if goal_progress.strip():
|
|
310
|
+
event["progress"] = goal_progress.strip()
|
|
311
|
+
if total_steps > 0:
|
|
312
|
+
event["steps"] = total_steps
|
|
313
|
+
return event
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def build_turn_finished_log_event(
|
|
317
|
+
ev_stats: TurnEventStats,
|
|
318
|
+
*,
|
|
319
|
+
wall_seconds: float,
|
|
320
|
+
) -> dict[str, Any]:
|
|
321
|
+
"""Build a structured turn-finished record for ``cli.log``."""
|
|
322
|
+
return {
|
|
323
|
+
"event": "turn_finished",
|
|
324
|
+
"wall_s": round(wall_seconds, 1),
|
|
325
|
+
"events": ev_stats.to_log_dict(),
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def format_cli_log_event(payload: dict[str, Any]) -> str:
|
|
330
|
+
"""Serialize a structured CLI log event as compact JSON."""
|
|
331
|
+
return json.dumps(payload, separators=(",", ":"), sort_keys=True)
|
|
287
332
|
|
|
288
333
|
|
|
289
334
|
def format_token_count(count: int) -> str:
|
|
@@ -81,7 +81,6 @@ class TuiDaemonSession:
|
|
|
81
81
|
self._client = WebSocketClient(url=ws_url)
|
|
82
82
|
self._rpc_client = WebSocketClient(url=ws_url)
|
|
83
83
|
self._loop_id: str | None = None
|
|
84
|
-
self._autopilot_mode: str | None = None
|
|
85
84
|
self._read_lock = asyncio.Lock()
|
|
86
85
|
self._rpc_lock = asyncio.Lock()
|
|
87
86
|
self._rpc_connected = False
|
|
@@ -98,11 +97,6 @@ class TuiDaemonSession:
|
|
|
98
97
|
"""Active StrangeLoop id for this WebSocket session."""
|
|
99
98
|
return self._loop_id
|
|
100
99
|
|
|
101
|
-
@property
|
|
102
|
-
def autopilot_mode(self) -> str | None:
|
|
103
|
-
"""Active loop Solo/Autopilot mode when known."""
|
|
104
|
-
return self._autopilot_mode
|
|
105
|
-
|
|
106
100
|
async def connect(self, *, resume_loop_id: str | None = None) -> dict[str, Any]:
|
|
107
101
|
"""Connect and bootstrap a daemon loop session."""
|
|
108
102
|
await connect_websocket_with_retries(self._client)
|
|
@@ -123,8 +117,6 @@ class TuiDaemonSession:
|
|
|
123
117
|
if status_event.get("type") == "error":
|
|
124
118
|
raise RuntimeError(str(status_event.get("message", "daemon bootstrap failed")))
|
|
125
119
|
self._loop_id = status_event.get("loop_id")
|
|
126
|
-
mode = status_event.get("autopilot_mode")
|
|
127
|
-
self._autopilot_mode = str(mode) if mode in ("solo", "autopilot") else None
|
|
128
120
|
return status_event
|
|
129
121
|
|
|
130
122
|
def _resolve_stream_delivery_mode(self) -> str:
|