soothe-cli 0.9.6__tar.gz → 0.9.7__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.9.6 → soothe_cli-0.9.7}/PKG-INFO +4 -3
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/pyproject.toml +3 -2
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/cli/commands/loop_cmd.py +38 -2
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/cli/commands/status_cmd.py +9 -1
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/cli/execution/headless_renderer.py +3 -0
- soothe_cli-0.9.7/src/soothe_cli/cli/execution/launcher.py +130 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/cli/main.py +11 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/config/cli_config.py +5 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/runtime/state/step_router.py +20 -2
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/tui/app/_app.py +30 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/tui/app/_execution.py +47 -9
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/tui/app/_history.py +32 -138
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/tui/app/_messages_mixin.py +235 -5
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/tui/app/_startup.py +19 -5
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/tui/app/app.tcss +3 -2
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/tui/binding.py +16 -0
- soothe_cli-0.9.7/src/soothe_cli/tui/card_wire.py +24 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/tui/commands/command_router.py +3 -3
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/tui/commands/subagent_routing.py +20 -9
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/tui/markdown_theme.py +109 -29
- soothe_cli-0.9.7/src/soothe_cli/tui/mermaid_render.py +96 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/tui/preview_limits.py +4 -1
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/tui/textual_adapter.py +404 -385
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/tui/theme.py +6 -0
- soothe_cli-0.9.7/src/soothe_cli/tui/tips.py +84 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/tui/tool_display.py +2 -5
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/tui/widgets/file_change_preview.py +1 -1
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/tui/widgets/loading.py +8 -6
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/tui/widgets/messages/_helpers.py +0 -2
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/tui/widgets/messages/app.py +2 -2
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/tui/widgets/messages/assistant.py +59 -15
- soothe_cli-0.9.7/src/soothe_cli/tui/widgets/messages/clarification.py +548 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/tui/widgets/messages/cognition_goal_tree.py +7 -8
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/tui/widgets/messages/cognition_reason.py +1 -1
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/tui/widgets/messages/cognition_step.py +30 -30
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/tui/widgets/messages/cognition_step_activity.py +139 -51
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/tui/widgets/messages/cognition_subagent.py +28 -44
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/tui/widgets/messages/diff_message.py +1 -1
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/tui/widgets/messages/error.py +1 -1
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/tui/widgets/messages/skill.py +1 -1
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/tui/widgets/status.py +2 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/tui/widgets/welcome.py +0 -7
- soothe_cli-0.9.6/src/soothe_cli/cli/execution/launcher.py +0 -46
- soothe_cli-0.9.6/src/soothe_cli/runtime/policy/essential_events.py +0 -68
- soothe_cli-0.9.6/src/soothe_cli/tui/tips.py +0 -27
- soothe_cli-0.9.6/src/soothe_cli/tui/widgets/messages/clarification.py +0 -224
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/.gitignore +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/README.md +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/__init__.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/cli/__init__.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/cli/commands/__init__.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/cli/commands/autopilot_cmd.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/cli/commands/config_cmd.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/cli/commands/cron_cmd.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/cli/commands/run_cmd.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/cli/execution/__init__.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/cli/execution/daemon.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/cli/execution/daemon_errors.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/cli/execution/headless.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/config/__init__.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/config/loader.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/config/logging_setup.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/runtime/__init__.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/runtime/headless/processor.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/runtime/headless/processor_state.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/runtime/parse/tool_call_resolution.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/runtime/policy/display_policy.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/runtime/policy/tui_trace_log.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/runtime/presentation/duration_format.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/runtime/presentation/engine.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/runtime/presentation/id_format.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/runtime/presentation/renderer_base.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/runtime/presentation/renderer_protocol.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/runtime/presentation/subagent_task_display.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/runtime/state/file_tracker.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/runtime/state/session_stats.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/runtime/state/stream_accumulator.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/runtime/token_events_debug.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/runtime/token_usage.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/runtime/transport/session.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/runtime/turn/prepare.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/runtime/wire/chunk_filter.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/runtime/wire/message_text.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/runtime/wire/messages.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/tui/__init__.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/tui/_cli_context.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/tui/_env_vars.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/tui/_version.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/tui/app/__init__.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/tui/app/_model.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/tui/app/_module_init.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/tui/app/_ui.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/tui/command_registry.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/tui/commands/__init__.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/tui/commands/slash_commands.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/tui/config.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/tui/file_change_notify.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/tui/file_change_renderers.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/tui/hooks.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/tui/input.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/tui/media_utils.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/tui/model_config.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/tui/path_utils.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/tui/project_utils.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/tui/sessions.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/tui/shell_color.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/tui/skills/__init__.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/tui/skills/invocation.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/tui/skills/load.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/tui/spinner_labels.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/tui/unicode_security.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/tui/update_check.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/tui/widgets/__init__.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/tui/widgets/_links.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/tui/widgets/autocomplete.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/tui/widgets/chat_input.py +2 -2
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/tui/widgets/clipboard.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/tui/widgets/context_data.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/tui/widgets/context_viewer.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/tui/widgets/diff.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/tui/widgets/editor.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/tui/widgets/help_screen.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/tui/widgets/history.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/tui/widgets/loop_selector.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/tui/widgets/mcp_viewer.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/tui/widgets/message_store.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/tui/widgets/messages/__init__.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/tui/widgets/messages/user.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/tui/widgets/model_selector.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/tui/widgets/notification_settings.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/tui/widgets/plan_quick_view_overlay.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/tui/widgets/router_profile_selector.py +0 -0
- {soothe_cli-0.9.6 → soothe_cli-0.9.7}/src/soothe_cli/tui/widgets/theme_selector.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: soothe-cli
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.7
|
|
4
4
|
Summary: Soothe CLI client - communicates with daemon via WebSocket
|
|
5
5
|
Project-URL: Homepage, https://github.com/mirasoth/soothe
|
|
6
6
|
Project-URL: Documentation, https://soothe.readthedocs.io
|
|
@@ -22,8 +22,9 @@ Requires-Dist: pillow<12.0.0,>=10.0.0
|
|
|
22
22
|
Requires-Dist: python-dotenv<2.0.0,>=1.0.0
|
|
23
23
|
Requires-Dist: pyyaml<7.0.0,>=6.0.0
|
|
24
24
|
Requires-Dist: rich<15.0.0,>=13.0.0
|
|
25
|
-
Requires-Dist: soothe-client-python<2.0.0,>=1.0.
|
|
26
|
-
Requires-Dist: soothe-sdk<2.0.0,>=1.0.
|
|
25
|
+
Requires-Dist: soothe-client-python<2.0.0,>=1.0.5
|
|
26
|
+
Requires-Dist: soothe-sdk<2.0.0,>=1.0.7
|
|
27
|
+
Requires-Dist: termaid<1.0.0,>=0.7.1
|
|
27
28
|
Requires-Dist: textual<10.0.0,>=8.0.0
|
|
28
29
|
Requires-Dist: typer<2.0.0,>=0.9.0
|
|
29
30
|
Requires-Dist: websockets<16.0,>=12.0
|
|
@@ -23,11 +23,12 @@ classifiers = [
|
|
|
23
23
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
24
24
|
]
|
|
25
25
|
dependencies = [
|
|
26
|
-
"soothe-sdk>=1.0.
|
|
27
|
-
"soothe-client-python>=1.0.
|
|
26
|
+
"soothe-sdk>=1.0.7,<2.0.0", # events/display/wire contracts
|
|
27
|
+
"soothe-client-python>=1.0.5,<2.0.0", # WebSocket transport client
|
|
28
28
|
"typer>=0.9.0,<2.0.0", # CLI framework
|
|
29
29
|
"textual>=8.0.0,<10.0.0", # TUI framework
|
|
30
30
|
"rich>=13.0.0,<15.0.0", # Console output
|
|
31
|
+
"termaid>=0.7.1,<1.0.0", # Mermaid → terminal diagram (goal-completion reports)
|
|
31
32
|
"pyyaml>=6.0.0,<7.0.0", # Config loading
|
|
32
33
|
"python-dotenv>=1.0.0,<2.0.0", # .env loading
|
|
33
34
|
"websockets>=12.0,<16.0", # WebSocket transport
|
|
@@ -9,6 +9,7 @@ All loop operations use daemon WebSocket RPC; the daemon must be running.
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
11
|
import asyncio
|
|
12
|
+
import logging
|
|
12
13
|
import sys
|
|
13
14
|
from typing import Annotated, Any
|
|
14
15
|
|
|
@@ -26,6 +27,7 @@ from soothe_client import (
|
|
|
26
27
|
from soothe_cli.runtime import load_config
|
|
27
28
|
|
|
28
29
|
console = Console()
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
29
31
|
|
|
30
32
|
# Create loop command group
|
|
31
33
|
loop_app = typer.Typer(help="Manage StrangeLoop instances")
|
|
@@ -636,11 +638,19 @@ def continue_loop(
|
|
|
636
638
|
str | None,
|
|
637
639
|
typer.Option("--prompt", "-p", help="Optional prompt to send after continuing."),
|
|
638
640
|
] = None,
|
|
641
|
+
resume: Annotated[
|
|
642
|
+
bool,
|
|
643
|
+
typer.Option(
|
|
644
|
+
"--resume",
|
|
645
|
+
help="Resume from the daemon's last execution step index.",
|
|
646
|
+
),
|
|
647
|
+
] = False,
|
|
639
648
|
) -> None:
|
|
640
649
|
"""Continue execution on existing loop.
|
|
641
650
|
|
|
642
651
|
Behavior:
|
|
643
652
|
- Resolve target loop (explicit `LOOP_ID` or most-recent loop)
|
|
653
|
+
- When resuming, fetch execution state (iteration/step/status) from daemon
|
|
644
654
|
- Launch TUI on that loop
|
|
645
655
|
- Optionally submit initial prompt in the resumed session
|
|
646
656
|
|
|
@@ -648,6 +658,7 @@ def continue_loop(
|
|
|
648
658
|
soothe loop continue
|
|
649
659
|
soothe loop continue loop_abc123
|
|
650
660
|
soothe loop continue loop_abc123 --prompt "translate to chinese"
|
|
661
|
+
soothe loop continue loop_abc123 --resume
|
|
651
662
|
"""
|
|
652
663
|
config = load_config()
|
|
653
664
|
ws_url = websocket_url_from_config(config)
|
|
@@ -658,6 +669,31 @@ def continue_loop(
|
|
|
658
669
|
if loop_id:
|
|
659
670
|
console.print(f"[info]Continuing loop: {resolved_loop_id}[/info]")
|
|
660
671
|
|
|
672
|
+
# Fetch execution state to show where the loop will pick up.
|
|
673
|
+
# Best-effort: non-fatal if the daemon cannot answer (older daemon, etc.)
|
|
674
|
+
if resume:
|
|
675
|
+
try:
|
|
676
|
+
state = asyncio.run(
|
|
677
|
+
protocol1_rpc(
|
|
678
|
+
ws_url,
|
|
679
|
+
"loop_execution_state_fetch",
|
|
680
|
+
{"loop_id": resolved_loop_id},
|
|
681
|
+
)
|
|
682
|
+
)
|
|
683
|
+
if "error" not in state:
|
|
684
|
+
step_index = state.get("step_index", 0)
|
|
685
|
+
iteration = state.get("iteration", 0)
|
|
686
|
+
status = state.get("status", "unknown")
|
|
687
|
+
plan = state.get("plan")
|
|
688
|
+
console.print(
|
|
689
|
+
f"[info]Resuming from iteration {iteration}, "
|
|
690
|
+
f"step {step_index} ({status})[/info]"
|
|
691
|
+
)
|
|
692
|
+
if plan:
|
|
693
|
+
console.print(f"[dim] Plan: {plan}[/dim]")
|
|
694
|
+
except Exception:
|
|
695
|
+
logger.warning("Failed to fetch execution state for loop %s", resolved_loop_id[:16])
|
|
696
|
+
|
|
661
697
|
from soothe_cli.cli.commands.run_cmd import run_impl
|
|
662
698
|
|
|
663
699
|
run_impl(
|
|
@@ -677,8 +713,8 @@ def resume_loop(
|
|
|
677
713
|
typer.Option("--prompt", "-p", help="Optional prompt to send after continuing."),
|
|
678
714
|
] = None,
|
|
679
715
|
) -> None:
|
|
680
|
-
"""
|
|
681
|
-
continue_loop(loop_id, prompt)
|
|
716
|
+
"""Resume execution on existing loop from the daemon's last step index."""
|
|
717
|
+
continue_loop(loop_id, prompt, resume=True)
|
|
682
718
|
|
|
683
719
|
|
|
684
720
|
@loop_app.command("detach")
|
|
@@ -58,12 +58,13 @@ def _render_unified_status_table(
|
|
|
58
58
|
daemon_live: bool = True,
|
|
59
59
|
daemon_version: str | None = None,
|
|
60
60
|
core_version: str | None = None,
|
|
61
|
+
started_at: str | None = None,
|
|
61
62
|
) -> Table:
|
|
62
63
|
"""Render unified status table without duplicated info.
|
|
63
64
|
|
|
64
65
|
Sections:
|
|
65
66
|
- Connection: WebSocket URL, Soothe Home
|
|
66
|
-
- Daemon: Running, Threads, PID, Versions (only when daemon is live)
|
|
67
|
+
- Daemon: Running, Threads, PID, Versions, Started (only when daemon is live)
|
|
67
68
|
"""
|
|
68
69
|
table = Table(title="Soothe Status")
|
|
69
70
|
table.add_column("Section", style="dim", width=12)
|
|
@@ -82,6 +83,8 @@ def _render_unified_status_table(
|
|
|
82
83
|
table.add_row("Daemon", "Status", "[green]Running[/green]")
|
|
83
84
|
if daemon_pid:
|
|
84
85
|
table.add_row("", "PID", str(daemon_pid))
|
|
86
|
+
if started_at:
|
|
87
|
+
table.add_row("", "Started", started_at)
|
|
85
88
|
if active_threads is not None:
|
|
86
89
|
table.add_row("", "Active Threads", str(active_threads))
|
|
87
90
|
if daemon_version:
|
|
@@ -176,6 +179,7 @@ def daemon_status(
|
|
|
176
179
|
"port_live": status.get("port_live", True),
|
|
177
180
|
"active_threads": status.get("active_threads", 0),
|
|
178
181
|
"daemon_pid": status.get("daemon_pid"),
|
|
182
|
+
"started_at": status.get("started_at"),
|
|
179
183
|
"daemon_version": status.get("daemon_version"),
|
|
180
184
|
"core_version": status.get("core_version"),
|
|
181
185
|
"readiness_state": status.get("readiness_state", "unknown"),
|
|
@@ -192,6 +196,7 @@ def daemon_status(
|
|
|
192
196
|
daemon_pid = status.get("daemon_pid")
|
|
193
197
|
daemon_version = status.get("daemon_version")
|
|
194
198
|
core_version = status.get("core_version")
|
|
199
|
+
started_at = status.get("started_at")
|
|
195
200
|
# Use readiness_state from daemon_status RPC (already includes state + message)
|
|
196
201
|
readiness_state_from_status = (
|
|
197
202
|
{
|
|
@@ -213,6 +218,7 @@ def daemon_status(
|
|
|
213
218
|
daemon_live=True,
|
|
214
219
|
daemon_version=daemon_version,
|
|
215
220
|
core_version=core_version,
|
|
221
|
+
started_at=started_at,
|
|
216
222
|
)
|
|
217
223
|
console.print(table)
|
|
218
224
|
|
|
@@ -302,6 +308,7 @@ def status_main(
|
|
|
302
308
|
output["daemon"]["port_live"] = status.get("port_live", True)
|
|
303
309
|
output["daemon"]["active_threads"] = status.get("active_threads", 0)
|
|
304
310
|
output["daemon"]["daemon_pid"] = status.get("daemon_pid")
|
|
311
|
+
output["daemon"]["started_at"] = status.get("started_at")
|
|
305
312
|
output["daemon"]["daemon_version"] = status.get("daemon_version")
|
|
306
313
|
output["daemon"]["core_version"] = status.get("core_version")
|
|
307
314
|
output["daemon"]["readiness_state"] = status.get("readiness_state", "unknown")
|
|
@@ -357,6 +364,7 @@ def status_main(
|
|
|
357
364
|
daemon_live=True,
|
|
358
365
|
daemon_version=daemon_version,
|
|
359
366
|
core_version=core_version,
|
|
367
|
+
started_at=status.get("started_at"),
|
|
360
368
|
)
|
|
361
369
|
console.print(table)
|
|
362
370
|
|
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
Emits RFC-614 loop-tagged assistant text for the main graph (empty LangGraph namespace)
|
|
4
4
|
and loop-tagged finals (including replayed ``goal_completion`` from IG-355). Subgraph
|
|
5
5
|
namespaced prose is suppressed unless loop-tagged. Stderr is used for errors.
|
|
6
|
+
|
|
7
|
+
UI transcript SoT is ``soothe.card.*`` (TUI / appkit); headless keeps raw ``messages``
|
|
8
|
+
phases for stdout and does not project card frames.
|
|
6
9
|
"""
|
|
7
10
|
|
|
8
11
|
from __future__ import annotations
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""TUI execution mode."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import sys
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from soothe_client import protocol1_rpc, websocket_url_from_config
|
|
9
|
+
|
|
10
|
+
from soothe_cli.config import CLIConfig
|
|
11
|
+
|
|
12
|
+
# Module-level storage for the last TUI result (for post-exit tip display)
|
|
13
|
+
# Use Any to avoid circular import issues at module load time
|
|
14
|
+
_last_app_result: Any = None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_last_app_result() -> Any:
|
|
18
|
+
"""Return the result from the most recent TUI run.
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
The AppResult from the last TUI session, or None if TUI hasn't run.
|
|
22
|
+
"""
|
|
23
|
+
return _last_app_result
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _resume_gate(cfg: CLIConfig, resume_loop_id: str) -> dict[str, Any] | None:
|
|
27
|
+
"""Fetch execution state for a resumed loop before launching the TUI.
|
|
28
|
+
|
|
29
|
+
Returns the ``{plan, step_index, iteration, status}`` snapshot on success,
|
|
30
|
+
or ``None`` when the daemon is unreachable or the RPC fails (non-fatal —
|
|
31
|
+
resume proceeds regardless).
|
|
32
|
+
"""
|
|
33
|
+
try:
|
|
34
|
+
ws_url = websocket_url_from_config(cfg)
|
|
35
|
+
response = asyncio.run(
|
|
36
|
+
protocol1_rpc(
|
|
37
|
+
ws_url,
|
|
38
|
+
"loop_execution_state_fetch",
|
|
39
|
+
{"loop_id": resume_loop_id},
|
|
40
|
+
)
|
|
41
|
+
)
|
|
42
|
+
except Exception:
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
if isinstance(response, dict) and "error" not in response:
|
|
46
|
+
return response
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _prompt_resume(resume_loop_id: str, state: dict[str, Any]) -> bool:
|
|
51
|
+
"""Prompt the user to confirm resume of an active loop.
|
|
52
|
+
|
|
53
|
+
Returns ``True`` if the user confirms (Enter / y), ``False`` to discard
|
|
54
|
+
(n / any other input). Non-interactive stdin (no TTY) defaults to resuming.
|
|
55
|
+
"""
|
|
56
|
+
step_index = state.get("step_index", 0)
|
|
57
|
+
iteration = state.get("iteration", 0)
|
|
58
|
+
status = state.get("status", "unknown")
|
|
59
|
+
typer.echo(
|
|
60
|
+
f"Active loop found: {resume_loop_id} (iteration {iteration}, step {step_index}, {status})"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
if not sys.stdin.isatty():
|
|
64
|
+
# Non-interactive: auto-resume.
|
|
65
|
+
typer.echo("Non-interactive stdin — resuming.")
|
|
66
|
+
return True
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
answer = typer.prompt(
|
|
70
|
+
"Resume this loop?",
|
|
71
|
+
default="y",
|
|
72
|
+
show_default=True,
|
|
73
|
+
)
|
|
74
|
+
except (EOFError, KeyboardInterrupt):
|
|
75
|
+
return False
|
|
76
|
+
|
|
77
|
+
return answer.strip().lower() in ("y", "yes", "")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def run_tui(
|
|
81
|
+
cfg: CLIConfig,
|
|
82
|
+
*,
|
|
83
|
+
resume_loop_id: str | None = None,
|
|
84
|
+
initial_prompt: str | None = None,
|
|
85
|
+
) -> None:
|
|
86
|
+
"""Launch the Textual TUI (with daemon auto-start)."""
|
|
87
|
+
global _last_app_result
|
|
88
|
+
|
|
89
|
+
# Resume gate: surface where the loop will pick up from the daemon's
|
|
90
|
+
# execution state before the TUI attaches.
|
|
91
|
+
if resume_loop_id:
|
|
92
|
+
state = _resume_gate(cfg, resume_loop_id)
|
|
93
|
+
if state is not None:
|
|
94
|
+
status = state.get("status", "unknown")
|
|
95
|
+
# Auto-resume loops in running/paused state only; terminal/idle
|
|
96
|
+
# states fall through to normal startup.
|
|
97
|
+
if status in ("running", "paused"):
|
|
98
|
+
should_resume = cfg.auto_resume or _prompt_resume(resume_loop_id, state)
|
|
99
|
+
if not should_resume:
|
|
100
|
+
typer.echo("Loop discarded. Starting fresh session.")
|
|
101
|
+
resume_loop_id = None
|
|
102
|
+
else:
|
|
103
|
+
step_index = state.get("step_index", 0)
|
|
104
|
+
iteration = state.get("iteration", 0)
|
|
105
|
+
plan = state.get("plan")
|
|
106
|
+
typer.echo(
|
|
107
|
+
f"Resuming loop {resume_loop_id}: "
|
|
108
|
+
f"iteration {iteration}, step {step_index} ({status})"
|
|
109
|
+
)
|
|
110
|
+
if plan:
|
|
111
|
+
typer.echo(f" Plan: {plan}")
|
|
112
|
+
else:
|
|
113
|
+
typer.echo(f"Loop {resume_loop_id} is {status} — starting fresh session.")
|
|
114
|
+
resume_loop_id = None
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
from soothe_cli.tui import run_textual_tui
|
|
118
|
+
|
|
119
|
+
result = run_textual_tui(
|
|
120
|
+
config=cfg,
|
|
121
|
+
resume_loop_id=resume_loop_id,
|
|
122
|
+
initial_prompt=initial_prompt,
|
|
123
|
+
)
|
|
124
|
+
_last_app_result = result
|
|
125
|
+
except ImportError:
|
|
126
|
+
typer.echo(
|
|
127
|
+
"Error: Textual is required for the TUI. Install: pip install 'textual>=0.40.0'",
|
|
128
|
+
err=True,
|
|
129
|
+
)
|
|
130
|
+
sys.exit(1)
|
|
@@ -147,6 +147,16 @@ def main(
|
|
|
147
147
|
),
|
|
148
148
|
),
|
|
149
149
|
] = None,
|
|
150
|
+
auto_resume: Annotated[
|
|
151
|
+
bool,
|
|
152
|
+
typer.Option(
|
|
153
|
+
"--auto-resume/--no-auto-resume",
|
|
154
|
+
help=(
|
|
155
|
+
"Auto-resume an active loop on startup without prompting. "
|
|
156
|
+
"Default: prompt ([Enter] to resume / n to discard)."
|
|
157
|
+
),
|
|
158
|
+
),
|
|
159
|
+
] = False,
|
|
150
160
|
show_help: Annotated[ # noqa: FBT002
|
|
151
161
|
bool,
|
|
152
162
|
typer.Option("--help", "-h", is_flag=True, help="Show this message and exit."),
|
|
@@ -203,6 +213,7 @@ def main(
|
|
|
203
213
|
output_streaming_enabled=streaming,
|
|
204
214
|
output_streaming_mode=streaming_mode,
|
|
205
215
|
clarification_mode=mode,
|
|
216
|
+
auto_resume=auto_resume,
|
|
206
217
|
soothe_home=home_path,
|
|
207
218
|
)
|
|
208
219
|
set_runtime_config(cli_cfg)
|
|
@@ -43,6 +43,11 @@ class CLIConfig:
|
|
|
43
43
|
clarification_mode: str | None = None
|
|
44
44
|
"""'manual' (relay to human) or 'auto' (veritas auto-answer). None = auto-detect from TTY."""
|
|
45
45
|
|
|
46
|
+
# Resume behavior: when the launcher finds an active loop on startup,
|
|
47
|
+
# auto-resume it (True) or prompt the user (False, default).
|
|
48
|
+
auto_resume: bool = False
|
|
49
|
+
"""Auto-resume active loops on CLI startup without prompting (default: prompt)."""
|
|
50
|
+
|
|
46
51
|
# Paths
|
|
47
52
|
soothe_home: Path = field(default_factory=lambda: Path(SOOTHE_HOME))
|
|
48
53
|
|
|
@@ -11,6 +11,7 @@ this module adds TUI-oriented buffers and lifecycle for multiple concurrent step
|
|
|
11
11
|
from __future__ import annotations
|
|
12
12
|
|
|
13
13
|
from collections import deque
|
|
14
|
+
from collections.abc import Callable
|
|
14
15
|
from dataclasses import dataclass, field
|
|
15
16
|
from typing import Any, TypeAlias
|
|
16
17
|
|
|
@@ -331,7 +332,7 @@ class StepTaskRouter:
|
|
|
331
332
|
|
|
332
333
|
Some providers emit unified ids like ``{step}:s:call_<uuid>`` that do not
|
|
333
334
|
encode a task index. In that case allocate a stable per-step synthetic
|
|
334
|
-
index so task activity rows
|
|
335
|
+
index so task activity rows stay 1:1 with delegated task markers.
|
|
335
336
|
"""
|
|
336
337
|
sid = str(step_id).strip()
|
|
337
338
|
tcid = str(tool_call_id).strip()
|
|
@@ -407,6 +408,23 @@ class StepTaskRouter:
|
|
|
407
408
|
)
|
|
408
409
|
)
|
|
409
410
|
|
|
411
|
+
def take_pending_main_tools_matching(
|
|
412
|
+
self,
|
|
413
|
+
predicate: Callable[[PendingMainTool], bool],
|
|
414
|
+
) -> list[PendingMainTool]:
|
|
415
|
+
"""Remove and return buffered main tools for which ``predicate`` is true."""
|
|
416
|
+
if not self._pending_main_tools:
|
|
417
|
+
return []
|
|
418
|
+
taken: list[PendingMainTool] = []
|
|
419
|
+
still: dict[str, PendingMainTool] = {}
|
|
420
|
+
for key, item in self._pending_main_tools.items():
|
|
421
|
+
if predicate(item):
|
|
422
|
+
taken.append(item)
|
|
423
|
+
else:
|
|
424
|
+
still[key] = item
|
|
425
|
+
self._pending_main_tools = still
|
|
426
|
+
return taken
|
|
427
|
+
|
|
410
428
|
def route_pending_main_tools(
|
|
411
429
|
self,
|
|
412
430
|
step_cards: dict[str, StepWidget],
|
|
@@ -697,7 +715,7 @@ class StepTaskRouter:
|
|
|
697
715
|
return list(self._pending_subgraph_tools.values())
|
|
698
716
|
|
|
699
717
|
def discard_pending_subgraph_tool(self, ns_key: tuple[str, ...], lookup_id: str) -> None:
|
|
700
|
-
"""Drop a buffered subgraph tool after routing it to a
|
|
718
|
+
"""Drop a buffered subgraph tool after routing it to a step/orphan card."""
|
|
701
719
|
self._pending_subgraph_tools.pop(_subgraph_pending_key(ns_key, lookup_id), None)
|
|
702
720
|
|
|
703
721
|
@property
|
|
@@ -37,6 +37,7 @@ from soothe_cli.tui.app._module_init import (
|
|
|
37
37
|
)
|
|
38
38
|
from soothe_cli.tui.app._startup import _StartupMixin
|
|
39
39
|
from soothe_cli.tui.app._ui import _UIMixin
|
|
40
|
+
from soothe_cli.tui.tips import TipRotator
|
|
40
41
|
from soothe_cli.tui.widgets.chat_input import ChatInput
|
|
41
42
|
from soothe_cli.tui.widgets.loading import LoadingWidget
|
|
42
43
|
from soothe_cli.tui.widgets.message_store import MessageStore
|
|
@@ -227,6 +228,9 @@ class SootheApp(
|
|
|
227
228
|
self._default_session_tip: str = ""
|
|
228
229
|
self._status_notification_timer: Timer | None = None
|
|
229
230
|
self._status_notification_active = False
|
|
231
|
+
# Rotating tip source + interval timer for the status footer.
|
|
232
|
+
self._tip_rotator: TipRotator = TipRotator()
|
|
233
|
+
self._tip_rotation_timer: Timer | None = None
|
|
230
234
|
|
|
231
235
|
self._chat_input: ChatInput | None = None
|
|
232
236
|
self._plan_quick_view_overlay: PlanQuickViewOverlay | None = None
|
|
@@ -371,6 +375,32 @@ class SootheApp(
|
|
|
371
375
|
if self._status_bar is not None and not self._status_notification_active:
|
|
372
376
|
self._status_bar.set_session_tip(cleaned)
|
|
373
377
|
|
|
378
|
+
def start_tip_rotation(self, interval: float = 12.0) -> None:
|
|
379
|
+
"""Cycle through rotating tips in the status footer at a fixed interval.
|
|
380
|
+
|
|
381
|
+
Args:
|
|
382
|
+
interval: Seconds between tip rotations.
|
|
383
|
+
"""
|
|
384
|
+
self.stop_tip_rotation()
|
|
385
|
+
self._tip_rotation_timer = self.set_interval(interval, self._rotate_session_tip)
|
|
386
|
+
|
|
387
|
+
def stop_tip_rotation(self) -> None:
|
|
388
|
+
"""Cancel the rotating-tip interval timer if one is running."""
|
|
389
|
+
if self._tip_rotation_timer is not None:
|
|
390
|
+
with suppress(Exception):
|
|
391
|
+
self._tip_rotation_timer.stop()
|
|
392
|
+
self._tip_rotation_timer = None
|
|
393
|
+
|
|
394
|
+
def _rotate_session_tip(self) -> None:
|
|
395
|
+
"""Advance to the next tip and push it to the status footer.
|
|
396
|
+
|
|
397
|
+
Transient notifications take precedence: rotation is skipped while a
|
|
398
|
+
notification is active so it can run its timeout uninterrupted.
|
|
399
|
+
"""
|
|
400
|
+
if self._status_notification_active:
|
|
401
|
+
return
|
|
402
|
+
self.set_default_session_tip(self._tip_rotator.next_tip())
|
|
403
|
+
|
|
374
404
|
def _set_status_notification(self, message: str, *, timeout: float | None = None) -> None:
|
|
375
405
|
"""Render a transient notification in the status-tip area."""
|
|
376
406
|
text = (message or "").strip()
|
|
@@ -53,6 +53,28 @@ InputMode = Literal["normal", "shell", "command"]
|
|
|
53
53
|
|
|
54
54
|
logger = logging.getLogger(__name__)
|
|
55
55
|
|
|
56
|
+
_PLAN_REVIEW_ACTIONS = frozenset({"Approve", "Reject", "More comments"})
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def clarification_wire_content(answers: list[str]) -> str:
|
|
60
|
+
"""Human-readable turn content for a clarification submit (not a new goal).
|
|
61
|
+
|
|
62
|
+
Plan-review actions use a stable ``Plan review: …`` prefix so a dropped
|
|
63
|
+
``clarification_answer`` flag cannot turn bare ``Reject`` into Pass1 TASK.
|
|
64
|
+
"""
|
|
65
|
+
non_empty = [a for a in answers if str(a).strip()]
|
|
66
|
+
if not non_empty:
|
|
67
|
+
return ""
|
|
68
|
+
first = str(answers[0]).strip() if answers else ""
|
|
69
|
+
if first in _PLAN_REVIEW_ACTIONS:
|
|
70
|
+
comments = str(answers[1]).strip() if len(answers) > 1 else ""
|
|
71
|
+
if comments:
|
|
72
|
+
return f"Plan review: {first} — {comments}"
|
|
73
|
+
return f"Plan review: {first}"
|
|
74
|
+
if len(non_empty) == 1:
|
|
75
|
+
return non_empty[0]
|
|
76
|
+
return " | ".join(f"A{i + 1}: {a}" for i, a in enumerate(answers) if str(a).strip())
|
|
77
|
+
|
|
56
78
|
|
|
57
79
|
class _ExecutionMixin:
|
|
58
80
|
"""Agent execution, message routing, queue, shell commands, and daemon events."""
|
|
@@ -229,6 +251,23 @@ class _ExecutionMixin:
|
|
|
229
251
|
# the user can still see what they answered.
|
|
230
252
|
adapter._clarification_input_by_step.pop(event.step_id, None)
|
|
231
253
|
|
|
254
|
+
# A stale empty remount (resume re-emit) can leave another interactive
|
|
255
|
+
# plan-review card. Disable extras so a second click cannot fire a
|
|
256
|
+
# "Reject" turn after clarification_answered cleared the pending flag.
|
|
257
|
+
for sid, other in list(adapter._clarification_input_by_step.items()):
|
|
258
|
+
if sid == event.step_id:
|
|
259
|
+
continue
|
|
260
|
+
try:
|
|
261
|
+
other._submitted = True # noqa: SLF001
|
|
262
|
+
other.add_class("is-submitted")
|
|
263
|
+
for btn in getattr(other, "_action_buttons", {}).values():
|
|
264
|
+
btn.disabled = True
|
|
265
|
+
for inp in getattr(other, "_inputs", []) or []:
|
|
266
|
+
inp.disabled = True
|
|
267
|
+
except Exception: # noqa: BLE001
|
|
268
|
+
logger.debug("Failed to disarm leftover clarification widget", exc_info=True)
|
|
269
|
+
adapter._clarification_input_by_step.pop(sid, None)
|
|
270
|
+
|
|
232
271
|
non_empty = [a for a in event.answers if a.strip()]
|
|
233
272
|
if not non_empty:
|
|
234
273
|
return
|
|
@@ -237,16 +276,16 @@ class _ExecutionMixin:
|
|
|
237
276
|
# concatenated string. ``content`` carries a human-readable summary
|
|
238
277
|
# for clients that look at it; the authoritative payload is the
|
|
239
278
|
# ``clarification_answers`` wire field.
|
|
240
|
-
payload_text = (
|
|
241
|
-
non_empty[0]
|
|
242
|
-
if len(non_empty) == 1
|
|
243
|
-
else " | ".join(f"A{i + 1}: {a}" for i, a in enumerate(event.answers) if a.strip())
|
|
244
|
-
)
|
|
279
|
+
payload_text = clarification_wire_content(list(event.answers))
|
|
245
280
|
adapter._clarification_answers_pending = list(event.answers)
|
|
281
|
+
# Always resume clarification from a widget submit — even if a prior
|
|
282
|
+
# clarification_answered cleared ``_clarification_pending`` (empty
|
|
283
|
+
# remount / race). Without this, "Reject" is treated as a new goal.
|
|
284
|
+
adapter._clarification_pending = True
|
|
246
285
|
|
|
247
286
|
# Hand off to the standard turn pipeline. ``execute_task_textual``
|
|
248
|
-
# snapshots ``adapter._clarification_pending``
|
|
249
|
-
#
|
|
287
|
+
# snapshots ``adapter._clarification_pending`` and sets the wire
|
|
288
|
+
# ``clarification_answer`` flag plus the ``clarification_answers``
|
|
250
289
|
# list, then clears the persisted flag so a follow-up turn is treated
|
|
251
290
|
# as a new goal.
|
|
252
291
|
#
|
|
@@ -469,8 +508,7 @@ class _ExecutionMixin:
|
|
|
469
508
|
|
|
470
509
|
# RFC-454 daemon *routing* commands (/deep_research, /plan, /browser_use, …):
|
|
471
510
|
# send the full line as a normal user turn so ``parse_subagent_from_input``
|
|
472
|
-
#
|
|
473
|
-
# ``cmd == "/deep_research …"`` never matches the bare single-token handlers below.
|
|
511
|
+
# strips the slash token and sets preferred_subagent (same as headless CLI).
|
|
474
512
|
full_stripped = command.strip()
|
|
475
513
|
first_word, query = parse_slash_command(full_stripped)
|
|
476
514
|
if first_word:
|