soothe-cli 0.4.8__tar.gz → 0.5.0__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.4.8 → soothe_cli-0.5.0}/PKG-INFO +4 -4
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/README.md +1 -1
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/pyproject.toml +2 -2
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/cli/commands/autopilot_cmd.py +2 -2
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/cli/commands/loop_cmd.py +80 -87
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/cli/commands/run_cmd.py +7 -5
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/cli/execution/daemon.py +21 -9
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/cli/execution/headless.py +2 -2
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/cli/execution/launcher.py +2 -2
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/cli/main.py +1 -5
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/shared/__init__.py +0 -4
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/shared/commands/command_router.py +33 -14
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/shared/commands/slash_commands.py +15 -66
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/shared/core/event_processor.py +12 -13
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/shared/core/presentation_engine.py +1 -1
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/shared/core/processor_state.py +3 -3
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/shared/events/tui_trace_log.py +1 -0
- soothe_cli-0.5.0/src/soothe_cli/tui/app/__init__.py +19 -0
- soothe_cli-0.5.0/src/soothe_cli/tui/app/_app.py +451 -0
- soothe_cli-0.5.0/src/soothe_cli/tui/app/_commands.py +446 -0
- soothe_cli-0.5.0/src/soothe_cli/tui/app/_execution.py +980 -0
- soothe_cli-0.5.0/src/soothe_cli/tui/app/_history.py +855 -0
- soothe_cli-0.5.0/src/soothe_cli/tui/app/_messages_mixin.py +810 -0
- soothe_cli-0.5.0/src/soothe_cli/tui/app/_model.py +726 -0
- soothe_cli-0.5.0/src/soothe_cli/tui/app/_module_init.py +501 -0
- soothe_cli-0.5.0/src/soothe_cli/tui/app/_startup.py +973 -0
- soothe_cli-0.5.0/src/soothe_cli/tui/app/_ui.py +606 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/command_registry.py +2 -2
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/config.py +4 -4
- soothe_cli-0.5.0/src/soothe_cli/tui/daemon_session.py +206 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/model_config.py +20 -88
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/output.py +1 -1
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/preview_limits.py +1 -1
- soothe_cli-0.5.0/src/soothe_cli/tui/sessions.py +380 -0
- soothe_cli-0.5.0/src/soothe_cli/tui/textual_adapter/__init__.py +111 -0
- soothe_cli-0.5.0/src/soothe_cli/tui/textual_adapter/_adapter.py +266 -0
- soothe_cli-0.5.0/src/soothe_cli/tui/textual_adapter/_stream_formatting.py +285 -0
- soothe_cli-0.5.0/src/soothe_cli/tui/textual_adapter/_stream_messages.py +256 -0
- soothe_cli-0.4.8/src/soothe_cli/tui/textual_adapter.py → soothe_cli-0.5.0/src/soothe_cli/tui/textual_adapter/_turn.py +42 -1115
- soothe_cli-0.5.0/src/soothe_cli/tui/textual_adapter/_turn_helpers.py +379 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/widgets/_links.py +6 -1
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/widgets/approval.py +2 -2
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/widgets/clipboard.py +67 -33
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/widgets/history.py +1 -1
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/widgets/loop_selector.py +4 -5
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/widgets/messages.py +129 -39
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/widgets/welcome.py +43 -7
- soothe_cli-0.4.8/src/soothe_cli/cli/commands/thread_cmd.py +0 -412
- soothe_cli-0.4.8/src/soothe_cli/tui/app.py +0 -5636
- soothe_cli-0.4.8/src/soothe_cli/tui/daemon_session.py +0 -331
- soothe_cli-0.4.8/src/soothe_cli/tui/sessions.py +0 -1359
- soothe_cli-0.4.8/src/soothe_cli/tui/widgets/thread_selector.py +0 -1817
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/.gitignore +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/__init__.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/cli/__init__.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/cli/commands/__init__.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/cli/execution/__init__.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/cli/execution/headless_renderer.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/cli/stream/__init__.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/cli/stream/context.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/cli/stream/display_line.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/cli/stream/formatter.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/cli/stream/pipeline.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/cli/stream/task_scope.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/config/__init__.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/config/cli_config.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/plan/__init__.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/plan/rich_tree.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/shared/commands/__init__.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/shared/commands/subagent_routing.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/shared/config_loader.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/shared/core/__init__.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/shared/core/renderer_protocol.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/shared/events/__init__.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/shared/events/display_policy.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/shared/events/essential_events.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/shared/events/explore_task_display.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/shared/events/stream_accumulator.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/shared/rendering/__init__.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/shared/rendering/async_renderer_protocol.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/shared/rendering/renderer_base.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/shared/tools/__init__.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/shared/tools/_utils.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/shared/tools/message_processing.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/shared/tools/rendering.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/shared/tools/tool_call_resolution.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/shared/tools/tool_card_payload.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/shared/tools/tool_card_visibility.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/shared/tools/tool_formatters/__init__.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/shared/tools/tool_formatters/base.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/shared/tools/tool_formatters/execution.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/shared/tools/tool_formatters/fallback.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/shared/tools/tool_formatters/file_ops.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/shared/tools/tool_formatters/goal_formatter.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/shared/tools/tool_formatters/media.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/shared/tools/tool_formatters/structured.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/shared/tools/tool_formatters/subagent.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/shared/tools/tool_formatters/web.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/shared/tools/tool_message_format.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/shared/tools/tool_output_formatter.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/__init__.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/_ask_user_types.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/_cli_context.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/_env_vars.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/_session_stats.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/_version.py +0 -0
- {soothe_cli-0.4.8/src/soothe_cli/tui → soothe_cli-0.5.0/src/soothe_cli/tui/app}/app.tcss +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/file_ops.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/formatting.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/hooks.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/input.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/media_utils.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/message_display_filter.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/project_utils.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/skills/__init__.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/skills/invocation.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/skills/load.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/theme.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/tool_display.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/unicode_security.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/update_check.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/widgets/__init__.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/widgets/ask_user.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/widgets/autocomplete.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/widgets/autopilot_dashboard.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/widgets/autopilot_screen.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/widgets/chat_input.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/widgets/diff.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/widgets/editor.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/widgets/loading.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/widgets/mcp_viewer.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/widgets/message_store.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/widgets/model_selector.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/widgets/notification_settings.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/widgets/status.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/widgets/theme_selector.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/widgets/tool_renderers.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/widgets/tool_widgets.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.5.0}/src/soothe_cli/tui/widgets/tools.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: soothe-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Soothe CLI client - communicates with daemon via WebSocket
|
|
5
5
|
Project-URL: Homepage, https://github.com/OpenSoothe/soothe
|
|
6
6
|
Project-URL: Documentation, https://soothe.readthedocs.io
|
|
@@ -21,8 +21,8 @@ Requires-Python: <4.0,>=3.11
|
|
|
21
21
|
Requires-Dist: python-dotenv<2.0.0,>=1.0.0
|
|
22
22
|
Requires-Dist: pyyaml<7.0.0,>=6.0.0
|
|
23
23
|
Requires-Dist: rich>=13.0.0
|
|
24
|
-
Requires-Dist: soothe-sdk<1.0.0,>=0.
|
|
25
|
-
Requires-Dist: textual>=0.
|
|
24
|
+
Requires-Dist: soothe-sdk<1.0.0,>=0.5.0
|
|
25
|
+
Requires-Dist: textual>=8.0.0
|
|
26
26
|
Requires-Dist: typer<1.0.0,>=0.9.0
|
|
27
27
|
Requires-Dist: websockets>=12.0
|
|
28
28
|
Provides-Extra: dev
|
|
@@ -71,7 +71,7 @@ This package is the **client** component that communicates with the Soothe daemo
|
|
|
71
71
|
|
|
72
72
|
- `soothe-sdk>=0.2.0` - WebSocket client, protocol, types
|
|
73
73
|
- `typer>=0.9.0` - CLI framework
|
|
74
|
-
- `textual>=0.
|
|
74
|
+
- `textual>=8.0.0` - TUI framework
|
|
75
75
|
- `rich>=13.0.0` - Console output
|
|
76
76
|
|
|
77
77
|
## Configuration
|
|
@@ -36,7 +36,7 @@ This package is the **client** component that communicates with the Soothe daemo
|
|
|
36
36
|
|
|
37
37
|
- `soothe-sdk>=0.2.0` - WebSocket client, protocol, types
|
|
38
38
|
- `typer>=0.9.0` - CLI framework
|
|
39
|
-
- `textual>=0.
|
|
39
|
+
- `textual>=8.0.0` - TUI framework
|
|
40
40
|
- `rich>=13.0.0` - Console output
|
|
41
41
|
|
|
42
42
|
## Configuration
|
|
@@ -23,9 +23,9 @@ classifiers = [
|
|
|
23
23
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
24
24
|
]
|
|
25
25
|
dependencies = [
|
|
26
|
-
"soothe-sdk>=0.
|
|
26
|
+
"soothe-sdk>=0.5.0,<1.0.0", # WebSocket client, protocol, types
|
|
27
27
|
"typer>=0.9.0,<1.0.0", # CLI framework
|
|
28
|
-
"textual>=0.
|
|
28
|
+
"textual>=8.0.0", # TUI framework
|
|
29
29
|
"rich>=13.0.0", # Console output
|
|
30
30
|
"pyyaml>=6.0.0,<7.0.0", # Config loading
|
|
31
31
|
"python-dotenv>=1.0.0,<2.0.0", # .env loading
|
|
@@ -1,13 +1,9 @@
|
|
|
1
|
-
"""Loop management CLI commands for
|
|
2
|
-
|
|
3
|
-
Replaces thread-based commands with loop-based commands.
|
|
4
|
-
Users interact with loops (threads are internal implementation detail).
|
|
1
|
+
"""Loop management CLI commands for AgentLoop instances.
|
|
5
2
|
|
|
6
3
|
RFC-503: Loop-First User Experience
|
|
7
4
|
RFC-504: Loop Management CLI Commands
|
|
8
5
|
|
|
9
|
-
All loop operations
|
|
10
|
-
The daemon must be running for loop commands to work.
|
|
6
|
+
All loop operations use daemon WebSocket RPC; the daemon must be running.
|
|
11
7
|
"""
|
|
12
8
|
|
|
13
9
|
from __future__ import annotations
|
|
@@ -83,6 +79,55 @@ async def _rpc(
|
|
|
83
79
|
await client.close()
|
|
84
80
|
|
|
85
81
|
|
|
82
|
+
def _resolve_continue_loop_id(ws_url: str, loop_id: str | None) -> str:
|
|
83
|
+
"""Resolve target loop ID for `loop continue`.
|
|
84
|
+
|
|
85
|
+
If `loop_id` is omitted, chooses the most recent loop, preferring active
|
|
86
|
+
statuses such as `running` and `detached`.
|
|
87
|
+
"""
|
|
88
|
+
if loop_id:
|
|
89
|
+
return loop_id
|
|
90
|
+
|
|
91
|
+
response = asyncio.run(
|
|
92
|
+
_rpc(
|
|
93
|
+
ws_url,
|
|
94
|
+
"send_loop_list",
|
|
95
|
+
{"filter_dict": None, "limit": 20},
|
|
96
|
+
"loop_list_response",
|
|
97
|
+
)
|
|
98
|
+
)
|
|
99
|
+
if "error" in response:
|
|
100
|
+
typer.echo(f"Error: {response['error']}", err=True)
|
|
101
|
+
sys.exit(1)
|
|
102
|
+
|
|
103
|
+
loops = response.get("loops", [])
|
|
104
|
+
if not loops:
|
|
105
|
+
typer.echo(
|
|
106
|
+
"Error: No loops found. Start one first with `soothe loop new`.",
|
|
107
|
+
err=True,
|
|
108
|
+
)
|
|
109
|
+
sys.exit(1)
|
|
110
|
+
|
|
111
|
+
preferred_statuses = {"running", "detached"}
|
|
112
|
+
selected = next(
|
|
113
|
+
(loop for loop in loops if loop.get("status") in preferred_statuses),
|
|
114
|
+
loops[0],
|
|
115
|
+
)
|
|
116
|
+
selected_loop_id = str(selected.get("loop_id", "")).strip()
|
|
117
|
+
if not selected_loop_id:
|
|
118
|
+
typer.echo(
|
|
119
|
+
"Error: Unable to resolve loop ID from loop list response.",
|
|
120
|
+
err=True,
|
|
121
|
+
)
|
|
122
|
+
sys.exit(1)
|
|
123
|
+
|
|
124
|
+
console.print(
|
|
125
|
+
"[info]No LOOP_ID provided; using most recent loop: "
|
|
126
|
+
f"{selected_loop_id} ({selected.get('status', 'unknown')})[/info]"
|
|
127
|
+
)
|
|
128
|
+
return selected_loop_id
|
|
129
|
+
|
|
130
|
+
|
|
86
131
|
@loop_app.command("list")
|
|
87
132
|
def list_loops(
|
|
88
133
|
status: Annotated[
|
|
@@ -96,8 +141,6 @@ def list_loops(
|
|
|
96
141
|
) -> None:
|
|
97
142
|
"""List all AgentLoop instances.
|
|
98
143
|
|
|
99
|
-
Replaces: soothe thread list
|
|
100
|
-
|
|
101
144
|
Examples:
|
|
102
145
|
soothe loop list
|
|
103
146
|
soothe loop list --status running
|
|
@@ -129,7 +172,7 @@ def list_loops(
|
|
|
129
172
|
table = Table(title="AgentLoops")
|
|
130
173
|
table.add_column("Loop ID", style="cyan")
|
|
131
174
|
table.add_column("Status", style="green")
|
|
132
|
-
table.add_column("
|
|
175
|
+
table.add_column("Contexts", justify="right")
|
|
133
176
|
table.add_column("Goals", justify="right")
|
|
134
177
|
table.add_column("Switches", justify="right")
|
|
135
178
|
table.add_column("Created", style="dim")
|
|
@@ -157,8 +200,6 @@ def describe_loop(
|
|
|
157
200
|
) -> None:
|
|
158
201
|
"""Show detailed loop information.
|
|
159
202
|
|
|
160
|
-
Replaces: soothe thread describe
|
|
161
|
-
|
|
162
203
|
Example:
|
|
163
204
|
soothe loop show loop_abc123
|
|
164
205
|
soothe loop show loop_abc123 --verbose
|
|
@@ -196,13 +237,12 @@ def describe_loop(
|
|
|
196
237
|
)
|
|
197
238
|
)
|
|
198
239
|
|
|
199
|
-
#
|
|
200
|
-
# RFC-503: Hide thread IDs, show only thread count
|
|
240
|
+
# Internal checkpoint context counts from loop metadata
|
|
201
241
|
console.print(
|
|
202
242
|
Panel(
|
|
203
|
-
f"Internal
|
|
204
|
-
f"
|
|
205
|
-
title="
|
|
243
|
+
f"Internal contexts: {len(loop.get('thread_ids', []))}\n"
|
|
244
|
+
f"Context switches: {loop.get('total_thread_switches', 0)}",
|
|
245
|
+
title="Checkpoint contexts (internal)",
|
|
206
246
|
border_style="dim",
|
|
207
247
|
)
|
|
208
248
|
)
|
|
@@ -211,7 +251,7 @@ def describe_loop(
|
|
|
211
251
|
console.print(
|
|
212
252
|
Panel(
|
|
213
253
|
f"Goals Completed: {loop.get('total_goals_completed', 0)}\n"
|
|
214
|
-
f"
|
|
254
|
+
f"Context switches: {loop.get('total_thread_switches', 0)}\n"
|
|
215
255
|
f"Duration: {format_duration(loop.get('total_duration_ms', 0))}\n"
|
|
216
256
|
f"Tokens Used: {format_tokens(loop.get('total_tokens_used', 0))}",
|
|
217
257
|
title="Execution Summary",
|
|
@@ -370,9 +410,7 @@ def delete_loop(
|
|
|
370
410
|
) -> None:
|
|
371
411
|
"""Delete loop entirely.
|
|
372
412
|
|
|
373
|
-
Removes loop directory
|
|
374
|
-
|
|
375
|
-
Replaces: soothe thread delete
|
|
413
|
+
Removes this loop's run directory and related artifacts.
|
|
376
414
|
|
|
377
415
|
Example:
|
|
378
416
|
soothe loop delete loop_abc123
|
|
@@ -405,7 +443,7 @@ def delete_loop(
|
|
|
405
443
|
console.print(
|
|
406
444
|
f"[warning]Warning: This will permanently delete {loop_id} and all associated data:[/warning]"
|
|
407
445
|
)
|
|
408
|
-
console.print(f" - {len(loop.get('thread_ids', []))} internal
|
|
446
|
+
console.print(f" - {len(loop.get('thread_ids', []))} internal checkpoint contexts")
|
|
409
447
|
console.print(f" - {loop.get('total_goals_completed', 0)} goal execution records")
|
|
410
448
|
console.print(" - Working memory spills")
|
|
411
449
|
|
|
@@ -432,9 +470,7 @@ def delete_loop(
|
|
|
432
470
|
console.print(" Removed checkpoint database")
|
|
433
471
|
console.print(" Removed metadata")
|
|
434
472
|
console.print(" Removed working memory spills")
|
|
435
|
-
console.print(
|
|
436
|
-
"[dim] Preserved thread checkpoints (run `soothe thread delete` to remove)[/dim]"
|
|
437
|
-
)
|
|
473
|
+
console.print("[dim] LangGraph checkpoints may remain until pruned separately[/dim]")
|
|
438
474
|
|
|
439
475
|
|
|
440
476
|
# Helper functions
|
|
@@ -520,7 +556,7 @@ def format_anchor_summary(anchors: list[dict[str, Any]]) -> str:
|
|
|
520
556
|
line = f" iteration {anchor['iteration']}: [dim]{anchor['checkpoint_id']}[/dim] "
|
|
521
557
|
line += f"({anchor['anchor_type']})"
|
|
522
558
|
|
|
523
|
-
#
|
|
559
|
+
# Context refresh when loop scope (LangGraph thread_id) changes between anchors
|
|
524
560
|
if anchor["iteration"] > 0:
|
|
525
561
|
prev_anchors = [a for a in anchors if a["iteration"] == anchor["iteration"] - 1]
|
|
526
562
|
if prev_anchors and prev_anchors[0]["thread_id"] != anchor["thread_id"]:
|
|
@@ -538,10 +574,10 @@ def render_ascii_tree(tree: dict[str, Any]) -> None:
|
|
|
538
574
|
for iteration in main_line:
|
|
539
575
|
iter_num = iteration["iteration"]
|
|
540
576
|
|
|
541
|
-
#
|
|
577
|
+
# Iteration marker (IDs omitted in UI)
|
|
542
578
|
console.print(f" iteration {iter_num}")
|
|
543
579
|
|
|
544
|
-
#
|
|
580
|
+
# Context refresh when the tree marks a switch
|
|
545
581
|
if iteration.get("thread_switch"):
|
|
546
582
|
console.print(" [cyan][context refreshed][/cyan]")
|
|
547
583
|
|
|
@@ -561,7 +597,7 @@ def render_ascii_tree(tree: dict[str, Any]) -> None:
|
|
|
561
597
|
console.print("\n[bold red]Failed Branches:[/bold red]")
|
|
562
598
|
|
|
563
599
|
for branch in branches:
|
|
564
|
-
#
|
|
600
|
+
# Branch identity only (no per-anchor checkpoint id in UI)
|
|
565
601
|
console.print(f" [dim]{branch['branch_id']}[/dim] (iteration {branch['iteration']})")
|
|
566
602
|
console.print(f" ├─ [dim]{branch['root_checkpoint']}[/dim] [root] ← Rewind point")
|
|
567
603
|
|
|
@@ -632,7 +668,7 @@ def render_dot_tree(tree: dict[str, Any]) -> None:
|
|
|
632
668
|
|
|
633
669
|
@loop_app.command("continue")
|
|
634
670
|
def continue_loop(
|
|
635
|
-
loop_id: Annotated[str, typer.Argument(help="Loop identifier to continue")],
|
|
671
|
+
loop_id: Annotated[str | None, typer.Argument(help="Loop identifier to continue")] = None,
|
|
636
672
|
prompt: Annotated[
|
|
637
673
|
str | None,
|
|
638
674
|
typer.Option("--prompt", "-p", help="Optional prompt to send after continuing."),
|
|
@@ -640,73 +676,30 @@ def continue_loop(
|
|
|
640
676
|
) -> None:
|
|
641
677
|
"""Continue execution on existing loop.
|
|
642
678
|
|
|
643
|
-
Replaces: soothe thread continue <thread_id>
|
|
644
|
-
|
|
645
679
|
Behavior:
|
|
646
|
-
-
|
|
647
|
-
-
|
|
648
|
-
-
|
|
649
|
-
- Display loop status
|
|
680
|
+
- Resolve target loop (explicit `LOOP_ID` or most-recent loop)
|
|
681
|
+
- Launch TUI on that loop
|
|
682
|
+
- Optionally submit initial prompt in the resumed session
|
|
650
683
|
|
|
651
684
|
Example:
|
|
685
|
+
soothe loop continue
|
|
652
686
|
soothe loop continue loop_abc123
|
|
653
687
|
soothe loop continue loop_abc123 --prompt "translate to chinese"
|
|
654
688
|
"""
|
|
655
689
|
config = load_config()
|
|
656
690
|
ws_url = websocket_url_from_config(config)
|
|
657
691
|
_require_daemon(ws_url)
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
692
|
+
resolved_loop_id = _resolve_continue_loop_id(ws_url, loop_id)
|
|
693
|
+
from soothe_cli.cli.commands.run_cmd import run_impl
|
|
694
|
+
|
|
695
|
+
run_impl(
|
|
696
|
+
prompt=prompt,
|
|
697
|
+
resume_loop_id=resolved_loop_id,
|
|
698
|
+
no_tui=False,
|
|
699
|
+
autonomous=False,
|
|
700
|
+
max_iterations=None,
|
|
667
701
|
)
|
|
668
702
|
|
|
669
|
-
if "error" in response:
|
|
670
|
-
typer.echo(f"Error: {response['error']}", err=True)
|
|
671
|
-
sys.exit(1)
|
|
672
|
-
|
|
673
|
-
console.print(f"[success]Attached to loop {loop_id}[/success]")
|
|
674
|
-
|
|
675
|
-
# Show loop status
|
|
676
|
-
status_response = asyncio.run(
|
|
677
|
-
_rpc(
|
|
678
|
-
ws_url,
|
|
679
|
-
"send_loop_get",
|
|
680
|
-
{"loop_id": loop_id, "verbose": False},
|
|
681
|
-
"loop_get_response",
|
|
682
|
-
)
|
|
683
|
-
)
|
|
684
|
-
|
|
685
|
-
loop = status_response.get("loop", {})
|
|
686
|
-
console.print(
|
|
687
|
-
Panel(
|
|
688
|
-
f"Status: {loop.get('status', 'unknown')}\n"
|
|
689
|
-
f"Goals: {loop.get('total_goals_completed', 0)} completed\n"
|
|
690
|
-
f"Internal Threads: {len(loop.get('thread_ids', []))}",
|
|
691
|
-
title=f"Loop: {loop_id}",
|
|
692
|
-
)
|
|
693
|
-
)
|
|
694
|
-
|
|
695
|
-
# Execute prompt if provided
|
|
696
|
-
if prompt:
|
|
697
|
-
input_response = asyncio.run(
|
|
698
|
-
_rpc(
|
|
699
|
-
ws_url,
|
|
700
|
-
"send_loop_input",
|
|
701
|
-
{"loop_id": loop_id, "content": prompt},
|
|
702
|
-
"loop_input_response",
|
|
703
|
-
)
|
|
704
|
-
)
|
|
705
|
-
if "error" in input_response:
|
|
706
|
-
typer.echo(f"Error: {input_response['error']}", err=True)
|
|
707
|
-
sys.exit(1)
|
|
708
|
-
console.print("[info]Prompt sent to loop[/info]")
|
|
709
|
-
|
|
710
703
|
|
|
711
704
|
@loop_app.command("detach")
|
|
712
705
|
def detach_loop(
|
|
@@ -716,7 +709,7 @@ def detach_loop(
|
|
|
716
709
|
|
|
717
710
|
Behavior:
|
|
718
711
|
- Unsubscribe client from loop events
|
|
719
|
-
- Loop
|
|
712
|
+
- Loop keeps running on the daemon
|
|
720
713
|
- Loop checkpoint saved at detachment point
|
|
721
714
|
- Client can reattach later with 'soothe loop attach'
|
|
722
715
|
|
|
@@ -799,7 +792,7 @@ def attach_loop(
|
|
|
799
792
|
Panel(
|
|
800
793
|
f"Status: {loop.get('status', 'unknown')}\n"
|
|
801
794
|
f"Goals: {loop.get('total_goals_completed', 0)} completed\n"
|
|
802
|
-
f"Internal
|
|
795
|
+
f"Internal contexts: {len(loop.get('thread_ids', []))}",
|
|
803
796
|
title=f"Loop: {loop_id} (Reattached)",
|
|
804
797
|
)
|
|
805
798
|
)
|
|
@@ -17,18 +17,20 @@ logger = logging.getLogger(__name__)
|
|
|
17
17
|
|
|
18
18
|
def run_impl(
|
|
19
19
|
prompt: str | None,
|
|
20
|
-
|
|
20
|
+
resume_loop_id: str | None,
|
|
21
21
|
no_tui: bool, # noqa: FBT001
|
|
22
22
|
autonomous: bool, # noqa: FBT001
|
|
23
23
|
max_iterations: int | None,
|
|
24
24
|
streaming_enabled: bool | None = None,
|
|
25
25
|
streaming_mode: str | None = None,
|
|
26
|
+
*,
|
|
27
|
+
config_path: str | None = None,
|
|
26
28
|
) -> None:
|
|
27
29
|
"""Core implementation for running Soothe agent.
|
|
28
30
|
|
|
29
31
|
Args:
|
|
30
32
|
prompt: Optional prompt for headless mode
|
|
31
|
-
|
|
33
|
+
resume_loop_id: Existing loop id to attach to (optional)
|
|
32
34
|
no_tui: Force headless mode
|
|
33
35
|
autonomous: Enable autonomous iteration mode
|
|
34
36
|
max_iterations: Max iterations for autonomous mode
|
|
@@ -38,7 +40,7 @@ def run_impl(
|
|
|
38
40
|
startup_start = time.perf_counter()
|
|
39
41
|
|
|
40
42
|
try:
|
|
41
|
-
cfg = load_config()
|
|
43
|
+
cfg = load_config(config_path)
|
|
42
44
|
log_level = resolve_cli_log_level(logging_level=cfg.logging_level)
|
|
43
45
|
log_file = Path(SOOTHE_HOME) / "logs" / "soothe-cli.log"
|
|
44
46
|
setup_logging(log_level, log_file=log_file)
|
|
@@ -65,13 +67,13 @@ def run_impl(
|
|
|
65
67
|
run_headless(
|
|
66
68
|
cfg,
|
|
67
69
|
prompt or "",
|
|
68
|
-
|
|
70
|
+
resume_loop_id=resume_loop_id,
|
|
69
71
|
autonomous=autonomous,
|
|
70
72
|
max_iterations=max_iterations,
|
|
71
73
|
)
|
|
72
74
|
else:
|
|
73
75
|
# TUI mode (with optional initial prompt)
|
|
74
|
-
run_tui(cfg,
|
|
76
|
+
run_tui(cfg, resume_loop_id=resume_loop_id, initial_prompt=prompt)
|
|
75
77
|
|
|
76
78
|
run_elapsed_s = time.perf_counter() - run_start
|
|
77
79
|
typer.echo(f"Total running time: {run_elapsed_s:.2f}s", err=True)
|
|
@@ -13,7 +13,7 @@ from typing import Any
|
|
|
13
13
|
|
|
14
14
|
import typer
|
|
15
15
|
from soothe_sdk.client import (
|
|
16
|
-
|
|
16
|
+
bootstrap_loop_session,
|
|
17
17
|
connect_websocket_with_retries,
|
|
18
18
|
websocket_url_from_config,
|
|
19
19
|
)
|
|
@@ -30,11 +30,19 @@ _SESSION_BOOTSTRAP_TIMEOUT_S = 5.0
|
|
|
30
30
|
_QUERY_START_TIMEOUT_S = 20.0
|
|
31
31
|
|
|
32
32
|
|
|
33
|
+
def _is_loop_scoped_event(event: dict[str, Any], *, active_loop_id: str) -> bool:
|
|
34
|
+
"""Return whether a daemon frame belongs to the active AgentLoop session."""
|
|
35
|
+
event_type = event.get("type", "")
|
|
36
|
+
if event_type not in {"status", "event"}:
|
|
37
|
+
return True
|
|
38
|
+
return event.get("loop_id") == active_loop_id
|
|
39
|
+
|
|
40
|
+
|
|
33
41
|
async def run_headless_via_daemon(
|
|
34
42
|
cfg: Any,
|
|
35
43
|
prompt: str,
|
|
36
44
|
*,
|
|
37
|
-
|
|
45
|
+
resume_loop_id: str | None = None,
|
|
38
46
|
autonomous: bool = False,
|
|
39
47
|
max_iterations: int | None = None,
|
|
40
48
|
) -> int:
|
|
@@ -51,27 +59,27 @@ async def run_headless_via_daemon(
|
|
|
51
59
|
try:
|
|
52
60
|
await connect_websocket_with_retries(client)
|
|
53
61
|
cli_ws = os.environ.get("SOOTHE_CLI_WORKSPACE", "").strip() or os.getcwd()
|
|
54
|
-
status_event = await
|
|
62
|
+
status_event = await bootstrap_loop_session(
|
|
55
63
|
client,
|
|
56
|
-
|
|
64
|
+
resume_loop_id=resume_loop_id,
|
|
57
65
|
verbosity="normal",
|
|
58
66
|
workspace=cli_ws,
|
|
59
|
-
|
|
60
|
-
subscription_timeout_s=_SESSION_BOOTSTRAP_TIMEOUT_S,
|
|
67
|
+
subscribe_timeout_s=_SESSION_BOOTSTRAP_TIMEOUT_S,
|
|
61
68
|
)
|
|
62
69
|
if status_event.get("type") == "error":
|
|
63
70
|
typer.echo(f"Daemon error: {status_event.get('message', 'unknown')}", err=True)
|
|
64
71
|
return 1
|
|
65
72
|
|
|
66
|
-
|
|
67
|
-
if not
|
|
68
|
-
typer.echo("Error: No
|
|
73
|
+
active_loop_id = status_event.get("loop_id")
|
|
74
|
+
if not active_loop_id:
|
|
75
|
+
typer.echo("Error: No loop_id after session bootstrap", err=True)
|
|
69
76
|
return 1
|
|
70
77
|
|
|
71
78
|
subagent_name, cleaned_prompt = parse_subagent_from_input(prompt)
|
|
72
79
|
|
|
73
80
|
await asyncio.wait_for(
|
|
74
81
|
client.send_input(
|
|
82
|
+
active_loop_id,
|
|
75
83
|
cleaned_prompt if subagent_name else prompt,
|
|
76
84
|
autonomous=autonomous,
|
|
77
85
|
max_iterations=max_iterations,
|
|
@@ -104,6 +112,8 @@ async def run_headless_via_daemon(
|
|
|
104
112
|
break
|
|
105
113
|
|
|
106
114
|
event_type = event.get("type", "")
|
|
115
|
+
if not _is_loop_scoped_event(event, active_loop_id=active_loop_id):
|
|
116
|
+
continue
|
|
107
117
|
|
|
108
118
|
if event_type == "error":
|
|
109
119
|
typer.echo(f"Daemon error: {event.get('message', 'unknown')}", err=True)
|
|
@@ -132,6 +142,8 @@ async def run_headless_via_daemon(
|
|
|
132
142
|
break
|
|
133
143
|
if not nxt:
|
|
134
144
|
break
|
|
145
|
+
if not _is_loop_scoped_event(nxt, active_loop_id=active_loop_id):
|
|
146
|
+
continue
|
|
135
147
|
processor.process_event(nxt)
|
|
136
148
|
|
|
137
149
|
processor.process_event(event)
|
|
@@ -22,7 +22,7 @@ def run_headless(
|
|
|
22
22
|
cfg: CLIConfig,
|
|
23
23
|
prompt: str,
|
|
24
24
|
*,
|
|
25
|
-
|
|
25
|
+
resume_loop_id: str | None = None,
|
|
26
26
|
autonomous: bool = False,
|
|
27
27
|
max_iterations: int | None = None,
|
|
28
28
|
) -> None:
|
|
@@ -77,7 +77,7 @@ def run_headless(
|
|
|
77
77
|
return await run_headless_via_daemon(
|
|
78
78
|
cfg,
|
|
79
79
|
prompt,
|
|
80
|
-
|
|
80
|
+
resume_loop_id=resume_loop_id,
|
|
81
81
|
autonomous=autonomous,
|
|
82
82
|
max_iterations=max_iterations,
|
|
83
83
|
)
|
|
@@ -10,7 +10,7 @@ from soothe_cli.config import CLIConfig
|
|
|
10
10
|
def run_tui(
|
|
11
11
|
cfg: CLIConfig,
|
|
12
12
|
*,
|
|
13
|
-
|
|
13
|
+
resume_loop_id: str | None = None,
|
|
14
14
|
initial_prompt: str | None = None,
|
|
15
15
|
) -> None:
|
|
16
16
|
"""Launch the Textual TUI (with daemon auto-start)."""
|
|
@@ -19,7 +19,7 @@ def run_tui(
|
|
|
19
19
|
|
|
20
20
|
run_textual_tui(
|
|
21
21
|
config=cfg,
|
|
22
|
-
|
|
22
|
+
resume_loop_id=resume_loop_id,
|
|
23
23
|
initial_prompt=initial_prompt,
|
|
24
24
|
)
|
|
25
25
|
except ImportError:
|
|
@@ -106,7 +106,7 @@ def main(
|
|
|
106
106
|
|
|
107
107
|
run_impl(
|
|
108
108
|
prompt=prompt,
|
|
109
|
-
|
|
109
|
+
resume_loop_id=None,
|
|
110
110
|
no_tui=no_tui,
|
|
111
111
|
autonomous=False,
|
|
112
112
|
max_iterations=None,
|
|
@@ -118,15 +118,11 @@ def main(
|
|
|
118
118
|
# ---------------------------------------------------------------------------
|
|
119
119
|
# Sub-command groups (nested Typer apps)
|
|
120
120
|
# ---------------------------------------------------------------------------
|
|
121
|
-
# Thread: read-only diagnostics per RFC-503 (Loop-First UX). Lifecycle
|
|
122
|
-
# management lives under `soothe loop <subcommand>`.
|
|
123
121
|
|
|
124
122
|
from soothe_cli.cli.commands.autopilot_cmd import app as _autopilot_app # noqa: E402
|
|
125
123
|
from soothe_cli.cli.commands.loop_cmd import loop_app as _loop_app # noqa: E402
|
|
126
|
-
from soothe_cli.cli.commands.thread_cmd import thread_app as _thread_app # noqa: E402
|
|
127
124
|
|
|
128
125
|
for _sub_app, _name in (
|
|
129
|
-
(_thread_app, "thread"),
|
|
130
126
|
(_loop_app, "loop"),
|
|
131
127
|
(_autopilot_app, "autopilot"),
|
|
132
128
|
):
|
|
@@ -21,8 +21,6 @@ from soothe_sdk.utils import setup_logging
|
|
|
21
21
|
# Import from commands subdirectory
|
|
22
22
|
from soothe_cli.shared.commands.slash_commands import (
|
|
23
23
|
KEYBOARD_SHORTCUTS,
|
|
24
|
-
SLASH_COMMANDS,
|
|
25
|
-
parse_autonomous_command,
|
|
26
24
|
show_commands,
|
|
27
25
|
show_config,
|
|
28
26
|
show_history,
|
|
@@ -108,8 +106,6 @@ __all__ = [
|
|
|
108
106
|
"update_name_map_from_tool_calls",
|
|
109
107
|
# Slash commands (IG-176)
|
|
110
108
|
"KEYBOARD_SHORTCUTS",
|
|
111
|
-
"SLASH_COMMANDS",
|
|
112
|
-
"parse_autonomous_command",
|
|
113
109
|
"show_commands",
|
|
114
110
|
"show_config",
|
|
115
111
|
"show_history",
|