soothe-cli 0.4.8__tar.gz → 0.4.9__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.4.9}/PKG-INFO +3 -3
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/README.md +1 -1
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/pyproject.toml +1 -1
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/cli/commands/loop_cmd.py +63 -55
- soothe_cli-0.4.9/src/soothe_cli/tui/app/__init__.py +19 -0
- soothe_cli-0.4.9/src/soothe_cli/tui/app/_app.py +453 -0
- soothe_cli-0.4.9/src/soothe_cli/tui/app/_commands.py +454 -0
- soothe_cli-0.4.9/src/soothe_cli/tui/app/_execution.py +978 -0
- soothe_cli-0.4.9/src/soothe_cli/tui/app/_history.py +860 -0
- soothe_cli-0.4.9/src/soothe_cli/tui/app/_messages_mixin.py +827 -0
- soothe_cli-0.4.9/src/soothe_cli/tui/app/_model.py +731 -0
- soothe_cli-0.4.9/src/soothe_cli/tui/app/_module_init.py +507 -0
- soothe_cli-0.4.9/src/soothe_cli/tui/app/_startup.py +987 -0
- soothe_cli-0.4.9/src/soothe_cli/tui/app/_ui.py +606 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/model_config.py +3 -3
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/preview_limits.py +1 -1
- soothe_cli-0.4.9/src/soothe_cli/tui/textual_adapter/__init__.py +29 -0
- soothe_cli-0.4.9/src/soothe_cli/tui/textual_adapter/_adapter.py +266 -0
- soothe_cli-0.4.9/src/soothe_cli/tui/textual_adapter/_stream_formatting.py +285 -0
- soothe_cli-0.4.9/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.4.9/src/soothe_cli/tui/textual_adapter/_turn.py +41 -1114
- soothe_cli-0.4.9/src/soothe_cli/tui/textual_adapter/_turn_helpers.py +379 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/widgets/_links.py +6 -1
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/widgets/clipboard.py +67 -33
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/widgets/messages.py +129 -39
- soothe_cli-0.4.8/src/soothe_cli/tui/app.py +0 -5636
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/.gitignore +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/__init__.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/cli/__init__.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/cli/commands/__init__.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/cli/commands/autopilot_cmd.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/cli/commands/run_cmd.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/cli/commands/thread_cmd.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/cli/execution/__init__.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/cli/execution/daemon.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/cli/execution/headless.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/cli/execution/headless_renderer.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/cli/execution/launcher.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/cli/main.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/cli/stream/__init__.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/cli/stream/context.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/cli/stream/display_line.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/cli/stream/formatter.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/cli/stream/pipeline.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/cli/stream/task_scope.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/config/__init__.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/config/cli_config.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/plan/__init__.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/plan/rich_tree.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/shared/__init__.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/shared/commands/__init__.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/shared/commands/command_router.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/shared/commands/slash_commands.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/shared/commands/subagent_routing.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/shared/config_loader.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/shared/core/__init__.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/shared/core/event_processor.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/shared/core/presentation_engine.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/shared/core/processor_state.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/shared/core/renderer_protocol.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/shared/events/__init__.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/shared/events/display_policy.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/shared/events/essential_events.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/shared/events/explore_task_display.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/shared/events/stream_accumulator.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/shared/events/tui_trace_log.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/shared/rendering/__init__.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/shared/rendering/async_renderer_protocol.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/shared/rendering/renderer_base.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/shared/tools/__init__.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/shared/tools/_utils.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/shared/tools/message_processing.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/shared/tools/rendering.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/shared/tools/tool_call_resolution.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/shared/tools/tool_card_payload.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/shared/tools/tool_card_visibility.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/shared/tools/tool_formatters/__init__.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/shared/tools/tool_formatters/base.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/shared/tools/tool_formatters/execution.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/shared/tools/tool_formatters/fallback.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/shared/tools/tool_formatters/file_ops.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/shared/tools/tool_formatters/goal_formatter.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/shared/tools/tool_formatters/media.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/shared/tools/tool_formatters/structured.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/shared/tools/tool_formatters/subagent.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/shared/tools/tool_formatters/web.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/shared/tools/tool_message_format.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/shared/tools/tool_output_formatter.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/__init__.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/_ask_user_types.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/_cli_context.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/_env_vars.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/_session_stats.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/_version.py +0 -0
- {soothe_cli-0.4.8/src/soothe_cli/tui → soothe_cli-0.4.9/src/soothe_cli/tui/app}/app.tcss +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/command_registry.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/config.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/daemon_session.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/file_ops.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/formatting.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/hooks.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/input.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/media_utils.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/message_display_filter.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/output.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/project_utils.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/sessions.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/skills/__init__.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/skills/invocation.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/skills/load.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/theme.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/tool_display.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/unicode_security.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/update_check.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/widgets/__init__.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/widgets/approval.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/widgets/ask_user.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/widgets/autocomplete.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/widgets/autopilot_dashboard.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/widgets/autopilot_screen.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/widgets/chat_input.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/widgets/diff.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/widgets/editor.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/widgets/history.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/widgets/loading.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/widgets/loop_selector.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/widgets/mcp_viewer.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/widgets/message_store.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/widgets/model_selector.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/widgets/notification_settings.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/widgets/status.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/widgets/theme_selector.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/widgets/thread_selector.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/widgets/tool_renderers.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/widgets/tool_widgets.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/widgets/tools.py +0 -0
- {soothe_cli-0.4.8 → soothe_cli-0.4.9}/src/soothe_cli/tui/widgets/welcome.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: soothe-cli
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.9
|
|
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
|
|
@@ -22,7 +22,7 @@ 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
24
|
Requires-Dist: soothe-sdk<1.0.0,>=0.4.0
|
|
25
|
-
Requires-Dist: textual>=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
|
|
@@ -25,7 +25,7 @@ classifiers = [
|
|
|
25
25
|
dependencies = [
|
|
26
26
|
"soothe-sdk>=0.4.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
|
|
@@ -83,6 +83,55 @@ async def _rpc(
|
|
|
83
83
|
await client.close()
|
|
84
84
|
|
|
85
85
|
|
|
86
|
+
def _resolve_continue_loop_id(ws_url: str, loop_id: str | None) -> str:
|
|
87
|
+
"""Resolve target loop ID for `loop continue`.
|
|
88
|
+
|
|
89
|
+
If `loop_id` is omitted, chooses the most recent loop, preferring active
|
|
90
|
+
statuses such as `running` and `detached`.
|
|
91
|
+
"""
|
|
92
|
+
if loop_id:
|
|
93
|
+
return loop_id
|
|
94
|
+
|
|
95
|
+
response = asyncio.run(
|
|
96
|
+
_rpc(
|
|
97
|
+
ws_url,
|
|
98
|
+
"send_loop_list",
|
|
99
|
+
{"filter_dict": None, "limit": 20},
|
|
100
|
+
"loop_list_response",
|
|
101
|
+
)
|
|
102
|
+
)
|
|
103
|
+
if "error" in response:
|
|
104
|
+
typer.echo(f"Error: {response['error']}", err=True)
|
|
105
|
+
sys.exit(1)
|
|
106
|
+
|
|
107
|
+
loops = response.get("loops", [])
|
|
108
|
+
if not loops:
|
|
109
|
+
typer.echo(
|
|
110
|
+
"Error: No loops found. Start one first with `soothe loop new`.",
|
|
111
|
+
err=True,
|
|
112
|
+
)
|
|
113
|
+
sys.exit(1)
|
|
114
|
+
|
|
115
|
+
preferred_statuses = {"running", "detached"}
|
|
116
|
+
selected = next(
|
|
117
|
+
(loop for loop in loops if loop.get("status") in preferred_statuses),
|
|
118
|
+
loops[0],
|
|
119
|
+
)
|
|
120
|
+
selected_loop_id = str(selected.get("loop_id", "")).strip()
|
|
121
|
+
if not selected_loop_id:
|
|
122
|
+
typer.echo(
|
|
123
|
+
"Error: Unable to resolve loop ID from loop list response.",
|
|
124
|
+
err=True,
|
|
125
|
+
)
|
|
126
|
+
sys.exit(1)
|
|
127
|
+
|
|
128
|
+
console.print(
|
|
129
|
+
"[info]No LOOP_ID provided; using most recent loop: "
|
|
130
|
+
f"{selected_loop_id} ({selected.get('status', 'unknown')})[/info]"
|
|
131
|
+
)
|
|
132
|
+
return selected_loop_id
|
|
133
|
+
|
|
134
|
+
|
|
86
135
|
@loop_app.command("list")
|
|
87
136
|
def list_loops(
|
|
88
137
|
status: Annotated[
|
|
@@ -632,7 +681,7 @@ def render_dot_tree(tree: dict[str, Any]) -> None:
|
|
|
632
681
|
|
|
633
682
|
@loop_app.command("continue")
|
|
634
683
|
def continue_loop(
|
|
635
|
-
loop_id: Annotated[str, typer.Argument(help="Loop identifier to continue")],
|
|
684
|
+
loop_id: Annotated[str | None, typer.Argument(help="Loop identifier to continue")] = None,
|
|
636
685
|
prompt: Annotated[
|
|
637
686
|
str | None,
|
|
638
687
|
typer.Option("--prompt", "-p", help="Optional prompt to send after continuing."),
|
|
@@ -643,70 +692,29 @@ def continue_loop(
|
|
|
643
692
|
Replaces: soothe thread continue <thread_id>
|
|
644
693
|
|
|
645
694
|
Behavior:
|
|
646
|
-
-
|
|
647
|
-
-
|
|
648
|
-
-
|
|
649
|
-
- Display loop status
|
|
695
|
+
- Resolve target loop (explicit `LOOP_ID` or most-recent loop)
|
|
696
|
+
- Launch TUI on that loop
|
|
697
|
+
- Optionally submit initial prompt in the resumed session
|
|
650
698
|
|
|
651
699
|
Example:
|
|
700
|
+
soothe loop continue
|
|
652
701
|
soothe loop continue loop_abc123
|
|
653
702
|
soothe loop continue loop_abc123 --prompt "translate to chinese"
|
|
654
703
|
"""
|
|
655
704
|
config = load_config()
|
|
656
705
|
ws_url = websocket_url_from_config(config)
|
|
657
706
|
_require_daemon(ws_url)
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
707
|
+
resolved_loop_id = _resolve_continue_loop_id(ws_url, loop_id)
|
|
708
|
+
from soothe_cli.cli.commands.run_cmd import run_impl
|
|
709
|
+
|
|
710
|
+
run_impl(
|
|
711
|
+
prompt=prompt,
|
|
712
|
+
thread_id=resolved_loop_id,
|
|
713
|
+
no_tui=False,
|
|
714
|
+
autonomous=False,
|
|
715
|
+
max_iterations=None,
|
|
667
716
|
)
|
|
668
717
|
|
|
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
718
|
|
|
711
719
|
@loop_app.command("detach")
|
|
712
720
|
def detach_loop(
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Public API for the app sub-package."""
|
|
2
|
+
|
|
3
|
+
from soothe_cli.tui.app._app import SootheApp
|
|
4
|
+
from soothe_cli.tui.app._module_init import (
|
|
5
|
+
AppResult,
|
|
6
|
+
TextualSessionState,
|
|
7
|
+
run_textual_app,
|
|
8
|
+
run_textual_tui,
|
|
9
|
+
save_theme_preference,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"SootheApp",
|
|
14
|
+
"AppResult",
|
|
15
|
+
"TextualSessionState",
|
|
16
|
+
"run_textual_app",
|
|
17
|
+
"run_textual_tui",
|
|
18
|
+
"save_theme_preference",
|
|
19
|
+
]
|
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
"""SootheApp: main Textual application class, composed from mixin modules."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import logging
|
|
7
|
+
import time
|
|
8
|
+
from collections import deque
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import TYPE_CHECKING, Any, ClassVar
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from langgraph.pregel import Pregel
|
|
14
|
+
from textual.app import ComposeResult
|
|
15
|
+
from textual.worker import Worker
|
|
16
|
+
|
|
17
|
+
from soothe_cli.tui.skills.load import ExtendedSkillMetadata
|
|
18
|
+
from soothe_cli.tui.textual_adapter import TextualUIAdapter
|
|
19
|
+
from soothe_cli.tui.widgets.approval import ApprovalMenu
|
|
20
|
+
from soothe_cli.tui.widgets.ask_user import AskUserMenu
|
|
21
|
+
|
|
22
|
+
from textual.app import App
|
|
23
|
+
from textual.binding import Binding, BindingType
|
|
24
|
+
from textual.containers import Container, Vertical, VerticalScroll
|
|
25
|
+
from textual.message import Message
|
|
26
|
+
from textual.widgets import Static
|
|
27
|
+
|
|
28
|
+
from soothe_cli.tui import theme
|
|
29
|
+
from soothe_cli.tui._session_stats import SessionStats
|
|
30
|
+
from soothe_cli.tui.app._commands import _CommandsMixin
|
|
31
|
+
from soothe_cli.tui.app._execution import _ExecutionMixin
|
|
32
|
+
from soothe_cli.tui.app._history import _HistoryMixin
|
|
33
|
+
from soothe_cli.tui.app._messages_mixin import _MessagesMixin
|
|
34
|
+
from soothe_cli.tui.app._model import _ModelMixin
|
|
35
|
+
from soothe_cli.tui.app._module_init import (
|
|
36
|
+
DeferredAction,
|
|
37
|
+
QueuedMessage,
|
|
38
|
+
TextualSessionState,
|
|
39
|
+
_load_theme_preference,
|
|
40
|
+
)
|
|
41
|
+
from soothe_cli.tui.app._startup import _StartupMixin
|
|
42
|
+
from soothe_cli.tui.app._ui import _UIMixin
|
|
43
|
+
from soothe_cli.tui.widgets.chat_input import ChatInput
|
|
44
|
+
from soothe_cli.tui.widgets.loading import LoadingWidget
|
|
45
|
+
from soothe_cli.tui.widgets.message_store import MessageStore
|
|
46
|
+
from soothe_cli.tui.widgets.messages import (
|
|
47
|
+
QueuedUserMessage,
|
|
48
|
+
)
|
|
49
|
+
from soothe_cli.tui.widgets.status import StatusBar
|
|
50
|
+
from soothe_cli.tui.widgets.welcome import WelcomeBanner
|
|
51
|
+
|
|
52
|
+
logger = logging.getLogger(__name__)
|
|
53
|
+
_monotonic = time.monotonic
|
|
54
|
+
|
|
55
|
+
InputMode = (
|
|
56
|
+
"normal" # Literal type alias — actual value used in _module_init; here for isinstance guards
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class SootheApp(
|
|
61
|
+
App,
|
|
62
|
+
_StartupMixin,
|
|
63
|
+
_HistoryMixin,
|
|
64
|
+
_CommandsMixin,
|
|
65
|
+
_ModelMixin,
|
|
66
|
+
_ExecutionMixin,
|
|
67
|
+
_UIMixin,
|
|
68
|
+
_MessagesMixin,
|
|
69
|
+
):
|
|
70
|
+
"""Main Textual application for Soothe.
|
|
71
|
+
|
|
72
|
+
SOOTHE: Migrated from Soothe, now connects to Soothe daemon backend.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
TITLE = "Soothe" # SOOTHE: Changed title
|
|
76
|
+
"""Textual application title."""
|
|
77
|
+
|
|
78
|
+
CSS_PATH = "app.tcss"
|
|
79
|
+
"""Path to the Textual CSS stylesheet for the app layout."""
|
|
80
|
+
|
|
81
|
+
ENABLE_COMMAND_PALETTE = False
|
|
82
|
+
"""Disable Textual's built-in command palette in favor of the custom slash
|
|
83
|
+
command system."""
|
|
84
|
+
|
|
85
|
+
SCROLL_SENSITIVITY_Y = 1.0
|
|
86
|
+
"""Vertical scroll speed (reduced from Textual default for finer control)."""
|
|
87
|
+
|
|
88
|
+
BINDINGS: ClassVar[list[BindingType]] = [
|
|
89
|
+
Binding("escape", "interrupt", "Interrupt", show=False, priority=True),
|
|
90
|
+
Binding(
|
|
91
|
+
"ctrl+c",
|
|
92
|
+
"quit_or_interrupt",
|
|
93
|
+
"Quit/Interrupt",
|
|
94
|
+
show=False,
|
|
95
|
+
priority=True,
|
|
96
|
+
),
|
|
97
|
+
Binding("ctrl+d", "quit_app", "Quit", show=False, priority=True),
|
|
98
|
+
Binding("ctrl+t", "toggle_auto_approve", "Toggle Auto-Approve", show=False),
|
|
99
|
+
Binding(
|
|
100
|
+
"shift+tab",
|
|
101
|
+
"toggle_auto_approve",
|
|
102
|
+
"Toggle Auto-Approve",
|
|
103
|
+
show=False,
|
|
104
|
+
priority=True,
|
|
105
|
+
),
|
|
106
|
+
Binding(
|
|
107
|
+
"ctrl+o",
|
|
108
|
+
"toggle_tool_output",
|
|
109
|
+
"Toggle Tool Output",
|
|
110
|
+
show=False,
|
|
111
|
+
priority=True,
|
|
112
|
+
),
|
|
113
|
+
Binding(
|
|
114
|
+
"ctrl+x",
|
|
115
|
+
"open_editor",
|
|
116
|
+
"Open Editor",
|
|
117
|
+
show=False,
|
|
118
|
+
priority=True,
|
|
119
|
+
),
|
|
120
|
+
# Approval menu keys (handled at App level for reliability)
|
|
121
|
+
Binding("up", "approval_up", "Up", show=False),
|
|
122
|
+
Binding("k", "approval_up", "Up", show=False),
|
|
123
|
+
Binding("down", "approval_down", "Down", show=False),
|
|
124
|
+
Binding("j", "approval_down", "Down", show=False),
|
|
125
|
+
Binding("enter", "approval_select", "Select", show=False),
|
|
126
|
+
Binding("y", "approval_yes", "Yes", show=False),
|
|
127
|
+
Binding("1", "approval_yes", "Yes", show=False),
|
|
128
|
+
Binding("2", "approval_auto", "Auto", show=False),
|
|
129
|
+
Binding("a", "approval_auto", "Auto", show=False),
|
|
130
|
+
Binding("3", "approval_no", "No", show=False),
|
|
131
|
+
Binding("n", "approval_no", "No", show=False),
|
|
132
|
+
]
|
|
133
|
+
"""App-level keybindings for interrupt, quit, toggles, and approval menu
|
|
134
|
+
navigation."""
|
|
135
|
+
|
|
136
|
+
class ServerReady(Message):
|
|
137
|
+
"""Posted by the background server-startup worker on success."""
|
|
138
|
+
|
|
139
|
+
def __init__( # noqa: D107
|
|
140
|
+
self,
|
|
141
|
+
agent: Any, # noqa: ANN401
|
|
142
|
+
server_proc: Any, # noqa: ANN401
|
|
143
|
+
mcp_server_info: list[Any] | None,
|
|
144
|
+
) -> None:
|
|
145
|
+
super().__init__()
|
|
146
|
+
self.agent = agent
|
|
147
|
+
self.server_proc = server_proc
|
|
148
|
+
self.mcp_server_info = mcp_server_info
|
|
149
|
+
|
|
150
|
+
class ServerStartFailed(Message):
|
|
151
|
+
"""Posted by the background server-startup worker on failure."""
|
|
152
|
+
|
|
153
|
+
def __init__(self, error: Exception) -> None: # noqa: D107
|
|
154
|
+
super().__init__()
|
|
155
|
+
self.error = error
|
|
156
|
+
|
|
157
|
+
class DaemonReady(Message):
|
|
158
|
+
"""Posted by the background daemon-connect worker on success."""
|
|
159
|
+
|
|
160
|
+
def __init__(self, session: Any, status_event: dict[str, Any]) -> None: # noqa: D107, ANN401
|
|
161
|
+
super().__init__()
|
|
162
|
+
self.session = session
|
|
163
|
+
self.status_event = status_event
|
|
164
|
+
|
|
165
|
+
def __init__(
|
|
166
|
+
self,
|
|
167
|
+
*,
|
|
168
|
+
agent: Pregel | None = None,
|
|
169
|
+
assistant_id: str | None = None,
|
|
170
|
+
auto_approve: bool = False,
|
|
171
|
+
cwd: str | Path | None = None,
|
|
172
|
+
thread_id: str | None = None,
|
|
173
|
+
resume_thread: str | None = None,
|
|
174
|
+
initial_prompt: str | None = None,
|
|
175
|
+
initial_skill: str | None = None,
|
|
176
|
+
mcp_server_info: list[dict[str, Any]] | None = None,
|
|
177
|
+
profile_override: dict[str, Any] | None = None,
|
|
178
|
+
server_proc: Any | None = None,
|
|
179
|
+
server_kwargs: dict[str, Any] | None = None,
|
|
180
|
+
mcp_preload_kwargs: dict[str, Any] | None = None,
|
|
181
|
+
model_kwargs: dict[str, Any] | None = None,
|
|
182
|
+
daemon_config: Any | None = None,
|
|
183
|
+
**kwargs: Any,
|
|
184
|
+
) -> None:
|
|
185
|
+
"""Initialize the Deep Agents application.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
agent: Pre-configured LangGraph agent, or `None` when server
|
|
189
|
+
startup is deferred via `server_kwargs`.
|
|
190
|
+
assistant_id: Agent identifier for memory storage
|
|
191
|
+
auto_approve: Whether to start with auto-approve enabled
|
|
192
|
+
cwd: Current working directory to display
|
|
193
|
+
thread_id: Thread ID for the session.
|
|
194
|
+
|
|
195
|
+
`None` when `resume_thread` is provided (resolved asynchronously).
|
|
196
|
+
resume_thread: Raw resume intent from `-r` flag.
|
|
197
|
+
|
|
198
|
+
`'__MOST_RECENT__'` for bare `-r`, a thread ID string for
|
|
199
|
+
`-r <id>`, or `None` for new sessions.
|
|
200
|
+
|
|
201
|
+
Resolved via `_resolve_resume_thread`
|
|
202
|
+
during `_start_server_background`.
|
|
203
|
+
|
|
204
|
+
Requires `server_kwargs` to be set; ignored otherwise.
|
|
205
|
+
initial_prompt: Optional prompt to auto-submit when session starts
|
|
206
|
+
initial_skill: Optional skill name to invoke when session starts.
|
|
207
|
+
mcp_server_info: MCP server metadata for the `/mcp` viewer.
|
|
208
|
+
profile_override: Extra profile fields from `--profile-override`,
|
|
209
|
+
retained so later profile-aware behavior stays consistent with
|
|
210
|
+
the CLI override, including model selection details and
|
|
211
|
+
on-demand `create_model()` calls.
|
|
212
|
+
server_proc: LangGraph server process for the interactive session.
|
|
213
|
+
server_kwargs: When provided, server startup is deferred.
|
|
214
|
+
|
|
215
|
+
The app shows a "Connecting..." state and starts the server in
|
|
216
|
+
the background using these kwargs
|
|
217
|
+
for `start_server_and_get_agent`.
|
|
218
|
+
mcp_preload_kwargs: Kwargs for `_preload_session_mcp_server_info`,
|
|
219
|
+
run concurrently with server startup when `server_kwargs` is set.
|
|
220
|
+
model_kwargs: Kwargs for deferred `create_model()`.
|
|
221
|
+
|
|
222
|
+
When provided, model creation runs in a background worker after
|
|
223
|
+
first paint instead of blocking startup.
|
|
224
|
+
**kwargs: Additional arguments passed to parent
|
|
225
|
+
"""
|
|
226
|
+
super().__init__(**kwargs)
|
|
227
|
+
|
|
228
|
+
self._register_custom_themes()
|
|
229
|
+
|
|
230
|
+
# Apply saved theme preference (or default)
|
|
231
|
+
self.theme = _load_theme_preference()
|
|
232
|
+
|
|
233
|
+
self._agent = agent
|
|
234
|
+
|
|
235
|
+
self._assistant_id = assistant_id
|
|
236
|
+
|
|
237
|
+
self._auto_approve = auto_approve
|
|
238
|
+
|
|
239
|
+
self._cwd = str(cwd) if cwd else str(Path.cwd())
|
|
240
|
+
|
|
241
|
+
self._lc_loop_id = thread_id
|
|
242
|
+
"""LangChain loop identifier (thread_id in langgraph internals).
|
|
243
|
+
|
|
244
|
+
Named `_lc_loop_id` to reflect RFC-503 loop-first UX while avoiding
|
|
245
|
+
collision with Textual's `App._thread_id`.
|
|
246
|
+
"""
|
|
247
|
+
|
|
248
|
+
self._resume_thread_intent = resume_thread
|
|
249
|
+
|
|
250
|
+
self._initial_prompt = initial_prompt
|
|
251
|
+
|
|
252
|
+
self._initial_skill = (
|
|
253
|
+
initial_skill.strip().lower() if initial_skill and initial_skill.strip() else None
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
self._mcp_server_info = mcp_server_info
|
|
257
|
+
|
|
258
|
+
self._profile_override = profile_override
|
|
259
|
+
|
|
260
|
+
self._server_proc = server_proc
|
|
261
|
+
|
|
262
|
+
self._server_kwargs = server_kwargs
|
|
263
|
+
|
|
264
|
+
self._mcp_preload_kwargs = mcp_preload_kwargs
|
|
265
|
+
|
|
266
|
+
self._model_kwargs = model_kwargs
|
|
267
|
+
|
|
268
|
+
self._daemon_config = daemon_config
|
|
269
|
+
|
|
270
|
+
self._daemon_session: Any | None = None
|
|
271
|
+
|
|
272
|
+
self._daemon_skills_wire: list[dict[str, Any]] = []
|
|
273
|
+
"""Cached ``skills_list_response`` rows when the TUI uses ``TuiDaemonSession``."""
|
|
274
|
+
|
|
275
|
+
self._connecting = server_kwargs is not None or daemon_config is not None
|
|
276
|
+
# Extract sandbox type from server kwargs for trace metadata.
|
|
277
|
+
# ServerConfig.__post_init__ normalizes "none" → None, but server_kwargs carries
|
|
278
|
+
# the raw argparse value, so guard against both.
|
|
279
|
+
|
|
280
|
+
raw = (server_kwargs or {}).get("sandbox_type")
|
|
281
|
+
|
|
282
|
+
self._sandbox_type: str | None = raw if raw and raw != "none" else None
|
|
283
|
+
|
|
284
|
+
self._model_override: str | None = None
|
|
285
|
+
|
|
286
|
+
self._model_params_override: dict[str, Any] | None = None
|
|
287
|
+
|
|
288
|
+
self._mcp_tool_count = sum(len(s.tools) for s in (mcp_server_info or []))
|
|
289
|
+
|
|
290
|
+
self._status_bar: StatusBar | None = None
|
|
291
|
+
|
|
292
|
+
self._chat_input: ChatInput | None = None
|
|
293
|
+
|
|
294
|
+
self._quit_pending = False
|
|
295
|
+
|
|
296
|
+
self._session_state: TextualSessionState | None = None
|
|
297
|
+
|
|
298
|
+
self._ui_adapter: TextualUIAdapter | None = None
|
|
299
|
+
|
|
300
|
+
self._pending_approval_widget: ApprovalMenu | None = None
|
|
301
|
+
|
|
302
|
+
self._pending_ask_user_widget: AskUserMenu | None = None
|
|
303
|
+
# Agent task tracking for interruption
|
|
304
|
+
|
|
305
|
+
self._agent_worker: Worker[None] | None = None
|
|
306
|
+
|
|
307
|
+
self._agent_running = False
|
|
308
|
+
|
|
309
|
+
self._server_startup_error: str | None = None
|
|
310
|
+
"""Set when the background server fails to start; persists for the
|
|
311
|
+
session lifetime (server failure is terminal).
|
|
312
|
+
|
|
313
|
+
Shown in place of the generic 'Agent not configured' message.
|
|
314
|
+
"""
|
|
315
|
+
|
|
316
|
+
self._shell_process: asyncio.subprocess.Process | None = None
|
|
317
|
+
"""Shell command process tracking for interruption (! commands)."""
|
|
318
|
+
|
|
319
|
+
self._shell_worker: Worker[None] | None = None
|
|
320
|
+
|
|
321
|
+
self._shell_running = False
|
|
322
|
+
|
|
323
|
+
self._loading_widget: LoadingWidget | None = None
|
|
324
|
+
|
|
325
|
+
self._context_tokens: int = 0
|
|
326
|
+
"""Local cache of the last total-context token count.
|
|
327
|
+
|
|
328
|
+
Source of truth is `_context_tokens` in graph state; this is a sync
|
|
329
|
+
copy for the status bar.
|
|
330
|
+
"""
|
|
331
|
+
|
|
332
|
+
self._tokens_approximate: bool = False
|
|
333
|
+
"""Whether the cached token count is stale (interrupted generation)."""
|
|
334
|
+
|
|
335
|
+
self._last_typed_at: float | None = None
|
|
336
|
+
"""Typing-aware approval deferral state."""
|
|
337
|
+
|
|
338
|
+
self._approval_placeholder: Static | None = None
|
|
339
|
+
|
|
340
|
+
self._update_available: tuple[bool, str | None] = (False, None)
|
|
341
|
+
"""Update availability state — set by _check_for_updates, read on exit."""
|
|
342
|
+
|
|
343
|
+
self._session_stats: SessionStats = SessionStats()
|
|
344
|
+
"""Cumulative usage stats across all turns in this session."""
|
|
345
|
+
|
|
346
|
+
self._inflight_turn_stats: SessionStats | None = None
|
|
347
|
+
"""Stats for the currently executing turn.
|
|
348
|
+
|
|
349
|
+
Held here so `exit()` can merge them synchronously before the event loop
|
|
350
|
+
tears down (e.g. `Ctrl+D` during a pending tool call).
|
|
351
|
+
"""
|
|
352
|
+
|
|
353
|
+
self._inflight_turn_start: float = 0.0
|
|
354
|
+
"""Monotonic timestamp when the current turn started."""
|
|
355
|
+
|
|
356
|
+
self._pending_messages: deque[QueuedMessage] = deque()
|
|
357
|
+
"""User message queue for sequential processing."""
|
|
358
|
+
|
|
359
|
+
self._queued_widgets: deque[QueuedUserMessage] = deque()
|
|
360
|
+
|
|
361
|
+
self._processing_pending = False
|
|
362
|
+
|
|
363
|
+
self._thread_switching = False
|
|
364
|
+
|
|
365
|
+
self._model_switching = False
|
|
366
|
+
self._detaching = False
|
|
367
|
+
|
|
368
|
+
self._deferred_actions: list[DeferredAction] = []
|
|
369
|
+
"""Deferred actions executed after the current busy state resolves."""
|
|
370
|
+
|
|
371
|
+
self._message_store = MessageStore()
|
|
372
|
+
"""Message virtualization store."""
|
|
373
|
+
|
|
374
|
+
self._hydrate_scheduled = False
|
|
375
|
+
"""Whether a hydrate task has been queued via `call_later`."""
|
|
376
|
+
|
|
377
|
+
self._hydrate_in_progress = False
|
|
378
|
+
"""Whether `_hydrate_messages_above` is currently running."""
|
|
379
|
+
|
|
380
|
+
self._last_hydration_check_mono: float = 0.0
|
|
381
|
+
"""Monotonic timestamp of the last scroll-triggered hydration check."""
|
|
382
|
+
|
|
383
|
+
self._startup_task: asyncio.Task[None] | None = None
|
|
384
|
+
"""Startup task reference (set in on_mount)."""
|
|
385
|
+
|
|
386
|
+
self._discovered_skills: list[ExtendedSkillMetadata] = []
|
|
387
|
+
"""Cached skill metadata from daemon RPC (populated by startup
|
|
388
|
+
discovery worker, refreshed on `/reload`).
|
|
389
|
+
"""
|
|
390
|
+
|
|
391
|
+
# Lazily imported here to avoid pulling image dependencies into
|
|
392
|
+
# argument parsing paths.
|
|
393
|
+
from soothe_cli.tui.input import MediaTracker
|
|
394
|
+
|
|
395
|
+
self._image_tracker = MediaTracker()
|
|
396
|
+
|
|
397
|
+
def _remote_agent(self) -> Any: # noqa: ANN401
|
|
398
|
+
"""Return the agent if it appears to be a remote agent, or `None`.
|
|
399
|
+
|
|
400
|
+
Returns `None` when no agent is configured or the agent is a local graph.
|
|
401
|
+
"""
|
|
402
|
+
# RemoteAgent module doesn't exist in this package; always return None.
|
|
403
|
+
# When the SDK provides a RemoteAgent class, this can be re-implemented.
|
|
404
|
+
return None
|
|
405
|
+
|
|
406
|
+
def _runtime_backend_ready(self) -> bool:
|
|
407
|
+
"""Return whether the app has a usable execution backend."""
|
|
408
|
+
return self._daemon_session is not None or self._agent is not None
|
|
409
|
+
|
|
410
|
+
def get_theme_variable_defaults(self) -> dict[str, str]:
|
|
411
|
+
"""Return custom CSS variable defaults for the current theme.
|
|
412
|
+
|
|
413
|
+
Most styling uses Textual's built-in variables (`$primary`,
|
|
414
|
+
`$text-muted`, `$error-muted`, etc.). This override injects the
|
|
415
|
+
app-specific variables (`$mode-bash`, `$mode-command`, `$skill`,
|
|
416
|
+
`$skill-hover`, `$tool`, `$tool-hover`, `$cognition`, `$cognition-hover`)
|
|
417
|
+
that have no Textual equivalent.
|
|
418
|
+
|
|
419
|
+
Returns:
|
|
420
|
+
Dict of CSS variable names to hex color values.
|
|
421
|
+
"""
|
|
422
|
+
colors = theme.get_theme_colors(self)
|
|
423
|
+
return theme.get_css_variable_defaults(colors=colors)
|
|
424
|
+
|
|
425
|
+
def compose(self) -> ComposeResult:
|
|
426
|
+
"""Compose the application layout.
|
|
427
|
+
|
|
428
|
+
Yields:
|
|
429
|
+
UI components for the main chat area and status bar.
|
|
430
|
+
"""
|
|
431
|
+
# Main chat area with scrollable messages
|
|
432
|
+
# VerticalScroll tracks user scroll intent for better auto-scroll behavior
|
|
433
|
+
with VerticalScroll(id="chat"):
|
|
434
|
+
with Vertical(id="chat-body"):
|
|
435
|
+
yield WelcomeBanner(
|
|
436
|
+
thread_id=self._lc_loop_id,
|
|
437
|
+
mcp_tool_count=self._mcp_tool_count,
|
|
438
|
+
connecting=self._connecting,
|
|
439
|
+
resuming=self._resume_thread_intent is not None,
|
|
440
|
+
local_server=self._server_kwargs is not None,
|
|
441
|
+
id="welcome-banner",
|
|
442
|
+
)
|
|
443
|
+
yield Container(id="messages")
|
|
444
|
+
with Container(id="bottom-app-container"):
|
|
445
|
+
yield Container(id="thinking-status")
|
|
446
|
+
yield ChatInput(
|
|
447
|
+
cwd=self._cwd,
|
|
448
|
+
image_tracker=self._image_tracker,
|
|
449
|
+
id="input-area",
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
# Status bar at bottom
|
|
453
|
+
yield StatusBar(cwd=self._cwd, id="status-bar")
|