soothe-cli 0.7.3__tar.gz → 0.7.5__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.3 → soothe_cli-0.7.5}/.gitignore +6 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/PKG-INFO +1 -1
- soothe_cli-0.7.5/src/soothe_cli/cli/commands/config_cmd.py +103 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/cli/commands/loop_cmd.py +13 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/cli/commands/run_cmd.py +2 -2
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/cli/main.py +2 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/runtime/headless/processor.py +1 -1
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/runtime/parse/tool_call_resolution.py +12 -2
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/runtime/presentation/engine.py +1 -1
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/runtime/presentation/renderer_protocol.py +1 -1
- soothe_cli-0.7.5/src/soothe_cli/runtime/presentation/subagent_task_display.py +112 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/runtime/state/step_router.py +1 -1
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/runtime/transport/session.py +64 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/app/_app.py +7 -3
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/app/_execution.py +40 -19
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/app/_history.py +103 -31
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/app/_messages_mixin.py +83 -50
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/app/_module_init.py +3 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/app/_startup.py +0 -7
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/app/_ui.py +19 -2
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/app/app.tcss +3 -5
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/command_registry.py +6 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/commands/command_router.py +1 -1
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/config.py +4 -23
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/preview_limits.py +2 -2
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/textual_adapter.py +327 -118
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/theme.py +23 -89
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/tips.py +1 -1
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/widgets/file_change_preview.py +25 -9
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/widgets/loading.py +7 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/widgets/message_store.py +10 -1
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/widgets/messages/_helpers.py +74 -8
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/widgets/messages/app.py +16 -7
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/widgets/messages/assistant.py +73 -15
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/widgets/messages/clarification.py +26 -9
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/widgets/messages/cognition_goal_tree.py +34 -12
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/widgets/messages/cognition_reason.py +3 -22
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/widgets/messages/cognition_step.py +13 -39
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/widgets/messages/cognition_step_activity.py +0 -2
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/widgets/messages/cognition_subagent.py +7 -3
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/widgets/messages/diff_message.py +1 -1
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/widgets/messages/error.py +9 -10
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/widgets/messages/skill.py +7 -17
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/widgets/welcome.py +44 -77
- soothe_cli-0.7.3/src/soothe_cli/runtime/presentation/explore_task_display.py +0 -89
- soothe_cli-0.7.3/src/soothe_cli/tui/widgets/autopilot_screen.py +0 -65
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/README.md +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/pyproject.toml +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/__init__.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/cli/__init__.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/cli/commands/__init__.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/cli/commands/autopilot_cmd.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/cli/commands/cron_cmd.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/cli/commands/status_cmd.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/cli/execution/__init__.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/cli/execution/daemon.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/cli/execution/daemon_errors.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/cli/execution/headless.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/cli/execution/headless_renderer.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/cli/execution/launcher.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/config/__init__.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/config/cli_config.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/config/loader.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/config/logging_setup.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/runtime/__init__.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/runtime/headless/processor_state.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/runtime/parse/message_processing.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/runtime/parse/tool_result.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/runtime/policy/display_policy.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/runtime/policy/essential_events.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/runtime/policy/tui_trace_log.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/runtime/presentation/duration_format.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/runtime/presentation/renderer_base.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/runtime/state/file_tracker.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/runtime/state/session_stats.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/runtime/state/stream_accumulator.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/runtime/state/transcript.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/runtime/turn/pipeline.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/runtime/turn/prepare.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/runtime/wire/chunk_filter.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/runtime/wire/display_text.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/runtime/wire/message_text.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/runtime/wire/messages.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/__init__.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/_cli_context.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/_env_vars.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/_version.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/app/__init__.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/app/_commands.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/app/_model.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/binding.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/commands/__init__.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/commands/slash_commands.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/commands/subagent_routing.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/file_change_notify.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/file_change_renderers.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/hooks.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/input.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/markdown_theme.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/media_utils.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/model_config.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/path_utils.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/project_utils.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/sessions.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/shell_color.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/skills/__init__.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/skills/invocation.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/skills/load.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/spinner_labels.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/tool_display.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/unicode_security.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/update_check.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/widgets/__init__.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/widgets/_links.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/widgets/autocomplete.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/widgets/autopilot_dashboard.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/widgets/chat_input.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/widgets/clipboard.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/widgets/context_viewer.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/widgets/diff.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/widgets/editor.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/widgets/help_screen.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/widgets/history.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/widgets/loop_selector.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/widgets/mcp_viewer.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/widgets/messages/__init__.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/widgets/messages/user.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/widgets/model_selector.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/widgets/notification_settings.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/widgets/plan_quick_view_overlay.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/widgets/status.py +0 -0
- {soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/tui/widgets/theme_selector.py +0 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Config management CLI commands for the soothe daemon."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
from typing import Annotated, Any
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.panel import Panel
|
|
13
|
+
from soothe_sdk.client import (
|
|
14
|
+
WebSocketClient,
|
|
15
|
+
request_daemon_config_reload,
|
|
16
|
+
websocket_url_from_config,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
from soothe_cli.config.loader import load_config
|
|
20
|
+
|
|
21
|
+
console = Console()
|
|
22
|
+
|
|
23
|
+
# Create config command group
|
|
24
|
+
config_app = typer.Typer(help="Manage daemon configuration", no_args_is_help=True)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
async def _trigger_config_reload(ws_url: str, timeout: float = 5.0) -> dict[str, Any]:
|
|
28
|
+
"""Trigger config reload via WebSocket RPC.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
ws_url: WebSocket URL to connect to.
|
|
32
|
+
timeout: Request timeout in seconds.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
Response dict from daemon, or error dict on failure.
|
|
36
|
+
"""
|
|
37
|
+
client = WebSocketClient(url=ws_url)
|
|
38
|
+
try:
|
|
39
|
+
await client.connect()
|
|
40
|
+
return await request_daemon_config_reload(client, timeout=timeout)
|
|
41
|
+
except Exception as e:
|
|
42
|
+
return {"success": False, "error": str(e)}
|
|
43
|
+
finally:
|
|
44
|
+
await client.close()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@config_app.command("reload")
|
|
48
|
+
def config_reload(
|
|
49
|
+
json_output: Annotated[
|
|
50
|
+
bool,
|
|
51
|
+
typer.Option("--json", help="Output as JSON."),
|
|
52
|
+
] = False,
|
|
53
|
+
) -> None:
|
|
54
|
+
"""Trigger config hot-reload on the running daemon.
|
|
55
|
+
|
|
56
|
+
Sends a config_reload RPC request to the daemon, which triggers immediate
|
|
57
|
+
reload of watched config files (config.yml and daemon.yml). Requires the
|
|
58
|
+
daemon to have hot-reload enabled via daemon.enable_config_reload().
|
|
59
|
+
|
|
60
|
+
Examples:
|
|
61
|
+
soothe config reload
|
|
62
|
+
soothe config reload --json
|
|
63
|
+
"""
|
|
64
|
+
config = load_config()
|
|
65
|
+
ws_url = websocket_url_from_config(config)
|
|
66
|
+
|
|
67
|
+
# Trigger reload
|
|
68
|
+
result = asyncio.run(_trigger_config_reload(ws_url, timeout=5.0))
|
|
69
|
+
|
|
70
|
+
if json_output:
|
|
71
|
+
output = {
|
|
72
|
+
"websocket_url": ws_url,
|
|
73
|
+
"success": result.get("success", False),
|
|
74
|
+
}
|
|
75
|
+
if result.get("error"):
|
|
76
|
+
output["error"] = result.get("error")
|
|
77
|
+
if result.get("message"):
|
|
78
|
+
output["message"] = result.get("message")
|
|
79
|
+
console.print_json(json.dumps(output))
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
if result.get("success"):
|
|
83
|
+
console.print(
|
|
84
|
+
Panel(
|
|
85
|
+
f"WebSocket URL: {ws_url}\n"
|
|
86
|
+
f"Status: [green]Reload triggered[/green]\n"
|
|
87
|
+
f"Message: {result.get('message', 'Config reload initiated')}",
|
|
88
|
+
title="Config Reload",
|
|
89
|
+
border_style="green",
|
|
90
|
+
)
|
|
91
|
+
)
|
|
92
|
+
else:
|
|
93
|
+
error_msg = result.get("error", "Unknown error")
|
|
94
|
+
if result.get("message"):
|
|
95
|
+
error_msg = result.get("message")
|
|
96
|
+
console.print(
|
|
97
|
+
Panel(
|
|
98
|
+
f"WebSocket URL: {ws_url}\nStatus: [red]Failed[/red]\nError: {error_msg}",
|
|
99
|
+
title="Config Reload",
|
|
100
|
+
border_style="red",
|
|
101
|
+
)
|
|
102
|
+
)
|
|
103
|
+
sys.exit(1)
|
|
@@ -720,6 +720,18 @@ def continue_loop(
|
|
|
720
720
|
)
|
|
721
721
|
|
|
722
722
|
|
|
723
|
+
@loop_app.command("resume")
|
|
724
|
+
def resume_loop(
|
|
725
|
+
loop_id: Annotated[str | None, typer.Argument(help="Loop identifier to continue")] = None,
|
|
726
|
+
prompt: Annotated[
|
|
727
|
+
str | None,
|
|
728
|
+
typer.Option("--prompt", "-p", help="Optional prompt to send after continuing."),
|
|
729
|
+
] = None,
|
|
730
|
+
) -> None:
|
|
731
|
+
"""Alias for ``continue`` — resume execution on existing loop."""
|
|
732
|
+
continue_loop(loop_id, prompt)
|
|
733
|
+
|
|
734
|
+
|
|
723
735
|
@loop_app.command("detach")
|
|
724
736
|
def detach_loop(
|
|
725
737
|
loop_id: Annotated[str, typer.Argument(help="Loop identifier to detach")],
|
|
@@ -873,6 +885,7 @@ __all__ = [
|
|
|
873
885
|
"prune_loop_branches",
|
|
874
886
|
"delete_loop",
|
|
875
887
|
"continue_loop",
|
|
888
|
+
"resume_loop",
|
|
876
889
|
"detach_loop",
|
|
877
890
|
"attach_loop",
|
|
878
891
|
"new_loop",
|
|
@@ -100,7 +100,7 @@ def run_impl(
|
|
|
100
100
|
result = get_last_app_result()
|
|
101
101
|
if result is not None and result.loop_id:
|
|
102
102
|
typer.echo(
|
|
103
|
-
f"💡 To continue this loop: soothe loop
|
|
103
|
+
f"💡 To continue this loop: soothe loop resume {result.loop_id}",
|
|
104
104
|
err=True,
|
|
105
105
|
)
|
|
106
106
|
|
|
@@ -113,7 +113,7 @@ def run_impl(
|
|
|
113
113
|
result = get_last_app_result()
|
|
114
114
|
if result is not None and result.loop_id:
|
|
115
115
|
typer.echo(
|
|
116
|
-
f"💡 To continue this loop: soothe loop
|
|
116
|
+
f"💡 To continue this loop: soothe loop resume {result.loop_id}",
|
|
117
117
|
err=True,
|
|
118
118
|
)
|
|
119
119
|
sys.exit(130)
|
|
@@ -228,6 +228,7 @@ def main(
|
|
|
228
228
|
# ---------------------------------------------------------------------------
|
|
229
229
|
|
|
230
230
|
from soothe_cli.cli.commands.autopilot_cmd import app as _autopilot_app # noqa: E402
|
|
231
|
+
from soothe_cli.cli.commands.config_cmd import config_app as _config_app # noqa: E402
|
|
231
232
|
from soothe_cli.cli.commands.cron_cmd import app as _cron_app # noqa: E402
|
|
232
233
|
from soothe_cli.cli.commands.loop_cmd import loop_app as _loop_app # noqa: E402
|
|
233
234
|
from soothe_cli.cli.commands.status_cmd import status_app as _status_app # noqa: E402
|
|
@@ -237,6 +238,7 @@ for _sub_app, _name in (
|
|
|
237
238
|
(_loop_app, "loop"),
|
|
238
239
|
(_autopilot_app, "autopilot"),
|
|
239
240
|
(_cron_app, "cron"),
|
|
241
|
+
(_config_app, "config"),
|
|
240
242
|
):
|
|
241
243
|
add_help_alias(_sub_app)
|
|
242
244
|
app.add_typer(_sub_app, name=_name)
|
|
@@ -242,7 +242,7 @@ class EventProcessor:
|
|
|
242
242
|
"""Headless stdout: only RFC-614 loop-tagged finals (IG-343 / IG-345).
|
|
243
243
|
|
|
244
244
|
Suppresses unphased execute-wave narration on the main graph; ``goal_completion``,
|
|
245
|
-
``
|
|
245
|
+
``goal_completion``, ``chitchat``, etc. are routed via ``assistant_output_phase`` before this path.
|
|
246
246
|
|
|
247
247
|
Subgraph streams without RFC-614 phases are skipped in ``_handle_ai_message`` before
|
|
248
248
|
this runs; loop-tagged subgraph finals use ``_dispatch_loop_tagged_assistant_text``.
|
|
@@ -152,7 +152,7 @@ def should_ingest_tool_for_step_stats(
|
|
|
152
152
|
"""Whether the TUI should register a tool row for step-card stats.
|
|
153
153
|
|
|
154
154
|
Main execute-graph tools are tracked as soon as ``tool_call_id`` and ``tool_name``
|
|
155
|
-
are known; display does not wait for streamed args. Subgraph
|
|
155
|
+
are known; display does not wait for streamed args. Subgraph tools often
|
|
156
156
|
arrive with placeholder kwargs before real args.
|
|
157
157
|
"""
|
|
158
158
|
from soothe_sdk.ux.task_namespace import is_step_level_task_tool_id
|
|
@@ -165,7 +165,7 @@ def should_ingest_tool_for_step_stats(
|
|
|
165
165
|
return True
|
|
166
166
|
if is_step_card_scope:
|
|
167
167
|
return not is_step_level_task_tool_id(tcid)
|
|
168
|
-
# Subgraph
|
|
168
|
+
# Subgraph tools often arrive with ``{"_subgraph_tool": true}`` before real args.
|
|
169
169
|
return is_task_level_subgraph_tool_call_id(tcid)
|
|
170
170
|
|
|
171
171
|
|
|
@@ -456,6 +456,11 @@ def materialize_ai_blocks_with_resolved_tools(
|
|
|
456
456
|
append + backfill + merge passes).
|
|
457
457
|
"""
|
|
458
458
|
tool_only = [b for b in expanded_blocks if isinstance(b, dict) and is_toolish_display_block(b)]
|
|
459
|
+
is_text_only_chunk = (
|
|
460
|
+
bool(expanded_blocks)
|
|
461
|
+
and not tool_only
|
|
462
|
+
and all(isinstance(b, dict) and b.get("type") == "text" for b in expanded_blocks)
|
|
463
|
+
)
|
|
459
464
|
resolved = resolve_tool_invocations_for_display(
|
|
460
465
|
message,
|
|
461
466
|
tool_only,
|
|
@@ -491,6 +496,11 @@ def materialize_ai_blocks_with_resolved_tools(
|
|
|
491
496
|
|
|
492
497
|
for r in resolved:
|
|
493
498
|
if r.tool_call_id not in seen_tool_ids:
|
|
499
|
+
# Plain text stream deltas must not inherit stale ``tool_calls`` from
|
|
500
|
+
# the LangChain message object — that appended phantom tool blocks and
|
|
501
|
+
# forced a flush+pop between every text token (weather-style trivial).
|
|
502
|
+
if is_text_only_chunk:
|
|
503
|
+
continue
|
|
494
504
|
out.append(
|
|
495
505
|
{
|
|
496
506
|
"type": "tool_call",
|
|
@@ -49,7 +49,7 @@ class PresentationEngine:
|
|
|
49
49
|
|
|
50
50
|
@property
|
|
51
51
|
def final_answer_locked(self) -> bool:
|
|
52
|
-
"""True after a custom final or
|
|
52
|
+
"""True after a custom final or chitchat-phase response was emitted for this turn."""
|
|
53
53
|
return self._state.final_answer_locked
|
|
54
54
|
|
|
55
55
|
def mark_final_answer_locked(self) -> None:
|
{soothe_cli-0.7.3 → soothe_cli-0.7.5}/src/soothe_cli/runtime/presentation/renderer_protocol.py
RENAMED
|
@@ -141,7 +141,7 @@ class RendererProtocol(Protocol):
|
|
|
141
141
|
Catch-all for events not covered by specific callbacks.
|
|
142
142
|
|
|
143
143
|
Args:
|
|
144
|
-
event_type: Full event type string (e.g., ``soothe.subagent.
|
|
144
|
+
event_type: Full event type string (e.g., ``soothe.subagent.tacitus.started``).
|
|
145
145
|
data: Event payload.
|
|
146
146
|
namespace: Subagent namespace tuple (empty for main agent).
|
|
147
147
|
task_scope: When subgraph streams are bound to a Task tool call (IG-334).
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Format subagent Task-scope assistant blobs for CLI/TUI.
|
|
2
|
+
|
|
3
|
+
Tacitus emits structured JSON in subgraph assistant streams. Clients show
|
|
4
|
+
user-facing summaries only; internal planning JSON is suppressed.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from collections.abc import Iterator
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _is_tacitus_internal_json_object(obj: dict[str, Any]) -> bool:
|
|
15
|
+
"""True when ``obj`` is Tacitus engine scratch (not a user-facing report)."""
|
|
16
|
+
keys = set(obj.keys())
|
|
17
|
+
if "sub_questions" in keys:
|
|
18
|
+
return True
|
|
19
|
+
if "queries" in keys and "query" not in keys:
|
|
20
|
+
return True
|
|
21
|
+
if "is_sufficient" in keys or "follow_up_queries" in keys or "knowledge_gap" in keys:
|
|
22
|
+
return True
|
|
23
|
+
return False
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _iter_embedded_json_objects(raw: str) -> Iterator[dict[str, Any]]:
|
|
27
|
+
"""Yield dict objects embedded anywhere in ``raw`` (prose + concatenated JSON)."""
|
|
28
|
+
dec = json.JSONDecoder()
|
|
29
|
+
i = 0
|
|
30
|
+
s = raw
|
|
31
|
+
n = len(s)
|
|
32
|
+
while i < n:
|
|
33
|
+
brace = s.find("{", i)
|
|
34
|
+
if brace < 0:
|
|
35
|
+
break
|
|
36
|
+
try:
|
|
37
|
+
obj, end = dec.raw_decode(s, brace)
|
|
38
|
+
except json.JSONDecodeError:
|
|
39
|
+
i = brace + 1
|
|
40
|
+
continue
|
|
41
|
+
if isinstance(obj, dict):
|
|
42
|
+
yield obj
|
|
43
|
+
i = end
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _strip_concatenated_json_objects(raw: str, *, predicate) -> str:
|
|
47
|
+
"""Remove JSON objects matching ``predicate``; keep surrounding prose."""
|
|
48
|
+
stripped = raw.strip()
|
|
49
|
+
if not stripped:
|
|
50
|
+
return raw
|
|
51
|
+
|
|
52
|
+
dec_objects: list[tuple[int, int, dict[str, Any]]] = []
|
|
53
|
+
dec = json.JSONDecoder()
|
|
54
|
+
i = 0
|
|
55
|
+
n = len(stripped)
|
|
56
|
+
while i < n:
|
|
57
|
+
while i < n and stripped[i].isspace():
|
|
58
|
+
i += 1
|
|
59
|
+
if i >= n:
|
|
60
|
+
break
|
|
61
|
+
try:
|
|
62
|
+
obj, end = dec.raw_decode(stripped, i)
|
|
63
|
+
except json.JSONDecodeError:
|
|
64
|
+
break
|
|
65
|
+
if isinstance(obj, dict) and predicate(obj):
|
|
66
|
+
dec_objects.append((i, end, obj))
|
|
67
|
+
i = end
|
|
68
|
+
|
|
69
|
+
if not dec_objects:
|
|
70
|
+
return raw
|
|
71
|
+
|
|
72
|
+
parts: list[str] = []
|
|
73
|
+
cursor = 0
|
|
74
|
+
for start, end, _obj in dec_objects:
|
|
75
|
+
if start > cursor:
|
|
76
|
+
parts.append(stripped[cursor:start])
|
|
77
|
+
cursor = end
|
|
78
|
+
if cursor < n:
|
|
79
|
+
parts.append(stripped[cursor:])
|
|
80
|
+
return " ".join("".join(parts).split())
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def format_subagent_task_assistant_for_display(
|
|
84
|
+
raw: str,
|
|
85
|
+
*,
|
|
86
|
+
subagent_type: str | None = None,
|
|
87
|
+
) -> str:
|
|
88
|
+
"""Return display-safe assistant text for a delegated task namespace.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
raw: Full assistant text from a subgraph namespace.
|
|
92
|
+
subagent_type: Built-in subagent id when known (``tacitus``, …).
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
Scrubbed one-line text, or ``""`` when only internal payloads remain.
|
|
96
|
+
"""
|
|
97
|
+
agent = (subagent_type or "").strip().lower()
|
|
98
|
+
text = raw
|
|
99
|
+
if agent == "tacitus":
|
|
100
|
+
stripped = text.strip()
|
|
101
|
+
internal_present = any(
|
|
102
|
+
_is_tacitus_internal_json_object(obj) for obj in _iter_embedded_json_objects(stripped)
|
|
103
|
+
)
|
|
104
|
+
if internal_present:
|
|
105
|
+
return ""
|
|
106
|
+
text = _strip_concatenated_json_objects(text, predicate=_is_tacitus_internal_json_object)
|
|
107
|
+
return (text or "").strip()
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
__all__ = [
|
|
111
|
+
"format_subagent_task_assistant_for_display",
|
|
112
|
+
]
|
|
@@ -445,7 +445,7 @@ class StepTaskRouter:
|
|
|
445
445
|
) -> bool:
|
|
446
446
|
"""Register one subgraph tool row on an already-resolved parent step card."""
|
|
447
447
|
if _is_task_metadata_subgraph_tool(item):
|
|
448
|
-
# Inner
|
|
448
|
+
# Inner subagent ``task`` chunks are not user-facing tool stats; ingesting
|
|
449
449
|
# them used to rewrite the main ``{step}:s:task:…`` delegation row args.
|
|
450
450
|
return True
|
|
451
451
|
display = str(item.display_key or "").strip()
|
|
@@ -552,6 +552,70 @@ class TuiDaemonSession:
|
|
|
552
552
|
success=True,
|
|
553
553
|
)
|
|
554
554
|
|
|
555
|
+
async def fetch_loop_history(self, loop_id: str) -> SimpleNamespace:
|
|
556
|
+
"""Fetch goal display snapshots plus live card tail (RFC-631).
|
|
557
|
+
|
|
558
|
+
Args:
|
|
559
|
+
loop_id: StrangeLoop id.
|
|
560
|
+
|
|
561
|
+
Returns:
|
|
562
|
+
``SimpleNamespace`` with ``goals``, ``live_cards``, ``live_goal_index``,
|
|
563
|
+
``context_tokens``, and ``success``.
|
|
564
|
+
"""
|
|
565
|
+
lid = str(loop_id or "").strip()
|
|
566
|
+
if not lid:
|
|
567
|
+
return SimpleNamespace(
|
|
568
|
+
goals=[],
|
|
569
|
+
live_cards=[],
|
|
570
|
+
live_goal_index=None,
|
|
571
|
+
context_tokens=0,
|
|
572
|
+
success=False,
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
async with self._rpc_lock:
|
|
576
|
+
await self._ensure_rpc_connected()
|
|
577
|
+
try:
|
|
578
|
+
resp = await self._rpc_client.request(
|
|
579
|
+
"loop_history_fetch",
|
|
580
|
+
{"loop_id": lid},
|
|
581
|
+
timeout=30.0,
|
|
582
|
+
)
|
|
583
|
+
except Exception:
|
|
584
|
+
logger.warning(
|
|
585
|
+
"loop_history_fetch failed for loop %s",
|
|
586
|
+
lid[:16],
|
|
587
|
+
exc_info=True,
|
|
588
|
+
)
|
|
589
|
+
return SimpleNamespace(
|
|
590
|
+
goals=[],
|
|
591
|
+
live_cards=[],
|
|
592
|
+
live_goal_index=None,
|
|
593
|
+
context_tokens=0,
|
|
594
|
+
success=False,
|
|
595
|
+
)
|
|
596
|
+
|
|
597
|
+
goals_raw = resp.get("goals")
|
|
598
|
+
goals = list(goals_raw) if isinstance(goals_raw, list) else []
|
|
599
|
+
live_raw = resp.get("live_cards")
|
|
600
|
+
live_cards = list(live_raw) if isinstance(live_raw, list) else []
|
|
601
|
+
live_goal_index = resp.get("live_goal_index")
|
|
602
|
+
if live_goal_index is not None and not isinstance(live_goal_index, int):
|
|
603
|
+
live_goal_index = None
|
|
604
|
+
context_tokens_raw = resp.get("context_tokens")
|
|
605
|
+
context_tokens = (
|
|
606
|
+
context_tokens_raw
|
|
607
|
+
if isinstance(context_tokens_raw, int) and context_tokens_raw >= 0
|
|
608
|
+
else 0
|
|
609
|
+
)
|
|
610
|
+
success = bool(resp.get("success", True))
|
|
611
|
+
return SimpleNamespace(
|
|
612
|
+
goals=goals,
|
|
613
|
+
live_cards=live_cards,
|
|
614
|
+
live_goal_index=live_goal_index,
|
|
615
|
+
context_tokens=context_tokens,
|
|
616
|
+
success=success,
|
|
617
|
+
)
|
|
618
|
+
|
|
555
619
|
async def aget_loop_state(self, loop_id: str) -> Any:
|
|
556
620
|
"""Load StrangeLoop state channels from the daemon (``loop_state_get`` RPC).
|
|
557
621
|
|
|
@@ -300,6 +300,12 @@ class SootheApp(
|
|
|
300
300
|
self._message_store = MessageStore()
|
|
301
301
|
"""Message virtualization store."""
|
|
302
302
|
|
|
303
|
+
self._loop_history_load_lock = asyncio.Lock()
|
|
304
|
+
"""Serialize resume history loads to prevent duplicate widget mounts."""
|
|
305
|
+
|
|
306
|
+
self._loop_history_loaded_for: str | None = None
|
|
307
|
+
"""Loop id whose transcript was last painted by ``_load_loop_history``."""
|
|
308
|
+
|
|
303
309
|
self._hydrate_scheduled = False
|
|
304
310
|
"""Whether a hydrate task has been queued via `call_later`."""
|
|
305
311
|
|
|
@@ -355,8 +361,7 @@ class SootheApp(
|
|
|
355
361
|
|
|
356
362
|
Most styling uses Textual's built-in variables (`$primary`,
|
|
357
363
|
`$text-muted`, `$error-muted`, etc.). This override injects the
|
|
358
|
-
app-specific variables (`$mode-bash`, `$mode-command`, `$
|
|
359
|
-
`$skill-hover`, `$tool`, `$tool-hover`, `$cognition`, `$cognition-hover`)
|
|
364
|
+
app-specific variables (`$mode-bash`, `$mode-command`, `$cognition`)
|
|
360
365
|
that have no Textual equivalent.
|
|
361
366
|
|
|
362
367
|
Returns:
|
|
@@ -378,7 +383,6 @@ class SootheApp(
|
|
|
378
383
|
yield WelcomeBanner(
|
|
379
384
|
loop_id=self._lc_loop_id,
|
|
380
385
|
mcp_tool_count=self._mcp_tool_count,
|
|
381
|
-
workspace_path=self._cwd,
|
|
382
386
|
id="welcome-banner",
|
|
383
387
|
)
|
|
384
388
|
yield Container(id="messages")
|
|
@@ -494,6 +494,10 @@ class _ExecutionMixin:
|
|
|
494
494
|
elif cmd == "/help":
|
|
495
495
|
await self._show_help_screen()
|
|
496
496
|
|
|
497
|
+
elif cmd == "/goals":
|
|
498
|
+
await self._mount_message(UserMessage(command))
|
|
499
|
+
await self._show_goal_history()
|
|
500
|
+
|
|
497
501
|
elif cmd in {"/changelog", "/docs", "/feedback"}:
|
|
498
502
|
await self._open_url_command(command, cmd)
|
|
499
503
|
elif cmd == "/version":
|
|
@@ -889,25 +893,42 @@ class _ExecutionMixin:
|
|
|
889
893
|
self._inflight_turn_stats = turn_stats
|
|
890
894
|
self._inflight_turn_start = time.monotonic()
|
|
891
895
|
try:
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
896
|
+
for attempt in (1, 2):
|
|
897
|
+
try:
|
|
898
|
+
await execute_task_textual(
|
|
899
|
+
user_input=message,
|
|
900
|
+
daemon_session=self._daemon_session,
|
|
901
|
+
assistant_id=self._assistant_id,
|
|
902
|
+
session_state=self._session_state,
|
|
903
|
+
adapter=self._ui_adapter,
|
|
904
|
+
image_tracker=self._image_tracker,
|
|
905
|
+
sandbox_type=self._sandbox_type,
|
|
906
|
+
workspace=self._cwd,
|
|
907
|
+
message_kwargs=message_kwargs,
|
|
908
|
+
context=CLIContext(
|
|
909
|
+
model=self._model_override,
|
|
910
|
+
model_params=self._model_params_override or {},
|
|
911
|
+
),
|
|
912
|
+
turn_stats=turn_stats,
|
|
913
|
+
skip_daemon_send_turn=skip_daemon_send_turn,
|
|
914
|
+
clarification_mode=getattr(self, "_clarification_mode", None),
|
|
915
|
+
is_shutting_down=lambda: getattr(self, "_exit", False),
|
|
916
|
+
)
|
|
917
|
+
break
|
|
918
|
+
except Exception as e:
|
|
919
|
+
if (
|
|
920
|
+
attempt == 1
|
|
921
|
+
and self._daemon_session is not None
|
|
922
|
+
and is_daemon_connection_error(e)
|
|
923
|
+
):
|
|
924
|
+
try:
|
|
925
|
+
await self._daemon_session.ensure_connected()
|
|
926
|
+
logger.info("Retrying turn after daemon reconnect")
|
|
927
|
+
skip_daemon_send_turn = False
|
|
928
|
+
continue
|
|
929
|
+
except (ConnectionError, OSError, TimeoutError):
|
|
930
|
+
pass
|
|
931
|
+
raise
|
|
911
932
|
except Exception as e: # Resilient tool rendering
|
|
912
933
|
logger.exception("Agent execution failed")
|
|
913
934
|
if is_daemon_connection_error(e):
|