soothe-cli 1.0.3__tar.gz → 1.0.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/PKG-INFO +2 -2
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/pyproject.toml +1 -1
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/cli/main.py +2 -2
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/commands/slash_commands.py +1 -1
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/config/cli_config.py +2 -2
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/display/tips.py +1 -1
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/app/_app.py +32 -15
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/app/_execution.py +12 -16
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/app/_messages_mixin.py +196 -38
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/app/_model.py +1 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/app/_startup.py +7 -3
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/app/_ui.py +30 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/app/app.tcss +12 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/composer_mode.py +22 -16
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/textual_adapter.py +9 -6
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/widgets/help_screen.py +1 -1
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/widgets/loading.py +71 -1
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/widgets/messages/cognition_goal_tree.py +13 -2
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/widgets/messages/cognition_step.py +20 -2
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/widgets/plan_quick_view_overlay.py +2 -2
- soothe_cli-1.0.5/src/soothe_cli/tui/widgets/queued_goals_bar.py +384 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/widgets/status.py +22 -65
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/.gitignore +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/README.md +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/__init__.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/_cli_context.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/_env_vars.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/_version.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/card_wire.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/cli/__init__.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/cli/commands/__init__.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/cli/commands/autopilot_cmd.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/cli/commands/config_cmd.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/cli/commands/cron_cmd.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/cli/commands/loop_cmd.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/cli/commands/run_cmd.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/cli/commands/status_cmd.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/cli/execution/__init__.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/cli/execution/daemon.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/cli/execution/daemon_errors.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/cli/execution/headless.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/cli/execution/headless_renderer.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/cli/execution/launcher.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/cli/soothecli.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/commands/__init__.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/commands/binding.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/commands/command_registry.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/commands/command_router.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/commands/subagent_routing.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/config/__init__.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/config/loader.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/config/logging_setup.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/display/__init__.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/display/markdown_theme.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/display/media_utils.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/display/mermaid_render.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/display/path_utils.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/display/preview_limits.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/display/shell_color.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/display/spinner_labels.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/display/theme.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/display/tool_display.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/display/unicode_security.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/loops/__init__.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/loops/sessions.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/model_config.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/project_utils.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/runtime/__init__.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/runtime/headless/processor.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/runtime/headless/processor_state.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/runtime/parse/tool_call_resolution.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/runtime/policy/tui_trace_log.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/runtime/presentation/duration_format.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/runtime/presentation/engine.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/runtime/presentation/id_format.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/runtime/presentation/renderer_base.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/runtime/presentation/renderer_protocol.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/runtime/presentation/step_id_format.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/runtime/presentation/subagent_task_display.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/runtime/state/file_tracker.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/runtime/state/session_stats.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/runtime/state/step_router.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/runtime/state/stream_accumulator.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/runtime/token_events_debug.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/runtime/token_usage.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/runtime/transport/session.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/runtime/turn/prepare.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/runtime/wire/chunk_filter.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/runtime/wire/message_text.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/runtime/wire/messages.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/settings/__init__.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/settings/_console_impl.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/settings/bootstrap.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/settings/core.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/settings/glyphs.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/settings/provider.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/settings/shell_allow.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/settings/skills_dirs.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/settings/stream_config.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/__init__.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/app/__init__.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/app/_clarification.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/app/_entrypoints.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/app/_history.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/app/_model_params.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/app/_terminal.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/app/_theme_prefs.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/app/_types.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/file_change_notify.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/file_change_renderers.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/input.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/skills/__init__.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/skills/invocation.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/skills/load.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/widgets/__init__.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/widgets/_links.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/widgets/autocomplete.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/widgets/chat_input.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/widgets/clipboard.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/widgets/context_data.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/widgets/context_viewer.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/widgets/diff.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/widgets/editor.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/widgets/file_change_preview.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/widgets/history.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/widgets/loop_selector.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/widgets/mcp_viewer.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/widgets/message_store.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/widgets/messages/__init__.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/widgets/messages/_helpers.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/widgets/messages/app.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/widgets/messages/assistant.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/widgets/messages/cognition_reason.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/widgets/messages/cognition_step_activity.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/widgets/messages/diff_message.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/widgets/messages/error.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/widgets/messages/skill.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/widgets/messages/structured_ask_user.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/widgets/messages/user.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/widgets/model_selector.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/widgets/notification_settings.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/widgets/pinned_goal_bar.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/widgets/router_profile_selector.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/widgets/theme_selector.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/tui/widgets/welcome.py +0 -0
- {soothe_cli-1.0.3 → soothe_cli-1.0.5}/src/soothe_cli/update_check.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: soothe-cli
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.5
|
|
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
|
|
@@ -21,7 +21,7 @@ Requires-Dist: pillow<12.0.0,>=10.0.0
|
|
|
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<15.0.0,>=13.0.0
|
|
24
|
-
Requires-Dist: soothe-client-python<2.0.0,>=1.0.
|
|
24
|
+
Requires-Dist: soothe-client-python<2.0.0,>=1.0.21
|
|
25
25
|
Requires-Dist: soothe-sdk<2.0.0,>=1.0.12
|
|
26
26
|
Requires-Dist: termaid<1.0.0,>=0.7.1
|
|
27
27
|
Requires-Dist: textual<10.0.0,>=8.0.0
|
|
@@ -23,7 +23,7 @@ classifiers = [
|
|
|
23
23
|
]
|
|
24
24
|
dependencies = [
|
|
25
25
|
"soothe-sdk>=1.0.12,<2.0.0", # events/display/wire contracts
|
|
26
|
-
"soothe-client-python>=1.0.
|
|
26
|
+
"soothe-client-python>=1.0.21,<2.0.0", # WebSocket transport client; matched to latest release v1.0.21
|
|
27
27
|
"typer>=0.9.0,<2.0.0", # CLI framework
|
|
28
28
|
"textual>=8.0.0,<10.0.0", # TUI framework
|
|
29
29
|
"rich>=13.0.0,<15.0.0", # Console output
|
|
@@ -198,10 +198,10 @@ def main(
|
|
|
198
198
|
help=(
|
|
199
199
|
"Auto-show the in-flow plan panel while a goal is executing "
|
|
200
200
|
"and hide it when the goal completes. Ctrl+t toggles "
|
|
201
|
-
"thereafter. Default:
|
|
201
|
+
"thereafter. Default: disabled (hidden)."
|
|
202
202
|
),
|
|
203
203
|
),
|
|
204
|
-
] =
|
|
204
|
+
] = False,
|
|
205
205
|
show_version: Annotated[ # noqa: FBT002
|
|
206
206
|
bool,
|
|
207
207
|
typer.Option("--version", help="Show version and exit."),
|
|
@@ -154,7 +154,7 @@ KEYBOARD_SHORTCUTS: dict[str, str] = {
|
|
|
154
154
|
"Ctrl+V": "Paste image from clipboard as [image N] attachment",
|
|
155
155
|
"Ctrl+T": "Toggle plan panel above thinking row",
|
|
156
156
|
"Ctrl+O": "Toggle expand/collapse of the most recent skill or tool card",
|
|
157
|
-
"Shift+Tab": "Cycle composer mode (Auto → Manual → Plan → Ask)",
|
|
157
|
+
"Shift+Tab": "Cycle composer mode (Auto → Bypass → Manual → Plan → Ask)",
|
|
158
158
|
}
|
|
159
159
|
|
|
160
160
|
|
|
@@ -33,9 +33,9 @@ class CLIConfig:
|
|
|
33
33
|
"""Markdown appearance preset (``match-app``, ``langchain``, ``standard``, …)."""
|
|
34
34
|
|
|
35
35
|
# Plan panel visibility preference
|
|
36
|
-
plan_panel_default_visible: bool =
|
|
36
|
+
plan_panel_default_visible: bool = False
|
|
37
37
|
"""When True, auto-show the in-flow plan panel while a goal is executing and
|
|
38
|
-
hide it when the goal completes. Default
|
|
38
|
+
hide it when the goal completes. Default False (hidden; Ctrl+t to open)."""
|
|
39
39
|
|
|
40
40
|
# Output streaming overrides (RFC-614)
|
|
41
41
|
output_streaming_enabled: bool | None = None
|
|
@@ -40,7 +40,7 @@ from soothe_cli.tui.app._types import (
|
|
|
40
40
|
from soothe_cli.tui.app._ui import _UIMixin
|
|
41
41
|
from soothe_cli.tui.composer_mode import normalize_composer_mode
|
|
42
42
|
from soothe_cli.tui.widgets.chat_input import ChatInput
|
|
43
|
-
from soothe_cli.tui.widgets.loading import LoadingWidget
|
|
43
|
+
from soothe_cli.tui.widgets.loading import LoadingWidget, TipRow
|
|
44
44
|
from soothe_cli.tui.widgets.message_store import MessageStore
|
|
45
45
|
from soothe_cli.tui.widgets.messages import (
|
|
46
46
|
AssistantMessage,
|
|
@@ -48,6 +48,7 @@ from soothe_cli.tui.widgets.messages import (
|
|
|
48
48
|
)
|
|
49
49
|
from soothe_cli.tui.widgets.pinned_goal_bar import PinnedGoalBar
|
|
50
50
|
from soothe_cli.tui.widgets.plan_quick_view_overlay import PlanQuickViewOverlay
|
|
51
|
+
from soothe_cli.tui.widgets.queued_goals_bar import QueuedGoalsBar
|
|
51
52
|
from soothe_cli.tui.widgets.status import StatusBar
|
|
52
53
|
from soothe_cli.tui.widgets.welcome import WelcomeBanner
|
|
53
54
|
|
|
@@ -133,6 +134,13 @@ class SootheApp(
|
|
|
133
134
|
show=False,
|
|
134
135
|
priority=True,
|
|
135
136
|
),
|
|
137
|
+
Binding(
|
|
138
|
+
"ctrl+q",
|
|
139
|
+
"focus_queued_goals",
|
|
140
|
+
"Focus Queued Goals",
|
|
141
|
+
show=False,
|
|
142
|
+
priority=True,
|
|
143
|
+
),
|
|
136
144
|
]
|
|
137
145
|
"""App-level keybindings for dismiss, quit, and navigation."""
|
|
138
146
|
|
|
@@ -226,10 +234,11 @@ class SootheApp(
|
|
|
226
234
|
self._mcp_tool_count = sum(len(s.tools) for s in (mcp_server_info or []))
|
|
227
235
|
|
|
228
236
|
self._status_bar: StatusBar | None = None
|
|
237
|
+
self._tip_row: TipRow | None = None
|
|
229
238
|
self._default_session_tip: str = ""
|
|
230
239
|
self._status_notification_timer: Timer | None = None
|
|
231
240
|
self._status_notification_active = False
|
|
232
|
-
# Rotating tip source + interval timer for the
|
|
241
|
+
# Rotating tip source + interval timer for the thinking row.
|
|
233
242
|
self._tip_rotator: TipRotator = TipRotator()
|
|
234
243
|
self._tip_rotation_timer: Timer | None = None
|
|
235
244
|
|
|
@@ -381,11 +390,11 @@ class SootheApp(
|
|
|
381
390
|
"""Persist and render the fallback tip shown when no notification is active."""
|
|
382
391
|
cleaned = (tip or "").strip()
|
|
383
392
|
self._default_session_tip = cleaned
|
|
384
|
-
if self.
|
|
385
|
-
self.
|
|
393
|
+
if self._tip_row is not None and not self._status_notification_active:
|
|
394
|
+
self._tip_row.set_tip(cleaned)
|
|
386
395
|
|
|
387
396
|
def start_tip_rotation(self, interval: float = 12.0) -> None:
|
|
388
|
-
"""Cycle through rotating tips in the
|
|
397
|
+
"""Cycle through rotating tips in the thinking row at a fixed interval.
|
|
389
398
|
|
|
390
399
|
Args:
|
|
391
400
|
interval: Seconds between tip rotations.
|
|
@@ -401,7 +410,7 @@ class SootheApp(
|
|
|
401
410
|
self._tip_rotation_timer = None
|
|
402
411
|
|
|
403
412
|
def _rotate_session_tip(self) -> None:
|
|
404
|
-
"""Advance to the next tip and push it to the
|
|
413
|
+
"""Advance to the next tip and push it to the thinking row.
|
|
405
414
|
|
|
406
415
|
Transient notifications take precedence: rotation is skipped while a
|
|
407
416
|
notification is active so it can run its timeout uninterrupted.
|
|
@@ -411,7 +420,7 @@ class SootheApp(
|
|
|
411
420
|
self.set_default_session_tip(self._tip_rotator.next_tip())
|
|
412
421
|
|
|
413
422
|
def _set_status_notification(self, message: str, *, timeout: float | None = None) -> None:
|
|
414
|
-
"""Render a transient notification in the
|
|
423
|
+
"""Render a transient notification in the thinking-row tip area."""
|
|
415
424
|
text = (message or "").strip()
|
|
416
425
|
if not text:
|
|
417
426
|
return
|
|
@@ -420,8 +429,8 @@ class SootheApp(
|
|
|
420
429
|
self._status_notification_timer.stop()
|
|
421
430
|
self._status_notification_timer = None
|
|
422
431
|
self._status_notification_active = True
|
|
423
|
-
if self.
|
|
424
|
-
self.
|
|
432
|
+
if self._tip_row is not None:
|
|
433
|
+
self._tip_row.set_notification(text)
|
|
425
434
|
|
|
426
435
|
duration = timeout if timeout and timeout > 0 else 3.0
|
|
427
436
|
self._status_notification_timer = self.set_timer(duration, self._clear_status_notification)
|
|
@@ -430,8 +439,8 @@ class SootheApp(
|
|
|
430
439
|
"""Restore the default tip after transient notification timeout."""
|
|
431
440
|
self._status_notification_timer = None
|
|
432
441
|
self._status_notification_active = False
|
|
433
|
-
if self.
|
|
434
|
-
self.
|
|
442
|
+
if self._tip_row is not None:
|
|
443
|
+
self._tip_row.set_tip(self._default_session_tip)
|
|
435
444
|
|
|
436
445
|
def notify( # type: ignore[override]
|
|
437
446
|
self,
|
|
@@ -442,7 +451,7 @@ class SootheApp(
|
|
|
442
451
|
timeout: float | None = None,
|
|
443
452
|
markup: bool = True, # noqa: ARG002
|
|
444
453
|
) -> None:
|
|
445
|
-
"""Display notifications in the
|
|
454
|
+
"""Display notifications in the thinking-row tip area instead of toast bubbles."""
|
|
446
455
|
text = str(message or "").strip()
|
|
447
456
|
if not text:
|
|
448
457
|
return
|
|
@@ -488,14 +497,22 @@ class SootheApp(
|
|
|
488
497
|
# In-flow plan panel: sits above the sticky bottom chrome so Ctrl+t
|
|
489
498
|
# never covers the thinking row or chat input. Auto-shows while a goal
|
|
490
499
|
# is executing when CLIConfig.plan_panel_default_visible is True
|
|
491
|
-
# (
|
|
492
|
-
plan_visible = getattr(self._daemon_config, "plan_panel_default_visible",
|
|
500
|
+
# (--plan-panel); default is False (hidden, Ctrl+t to open).
|
|
501
|
+
plan_visible = getattr(self._daemon_config, "plan_panel_default_visible", False)
|
|
493
502
|
yield PlanQuickViewOverlay(
|
|
494
503
|
id="plan-quick-view-overlay",
|
|
495
504
|
default_visible=plan_visible,
|
|
496
505
|
)
|
|
497
506
|
with Container(id="bottom-app-container"):
|
|
498
|
-
|
|
507
|
+
# Queued goals bar — inside the docked bottom container, above
|
|
508
|
+
# the thinking row and chat input. Hidden when the queue is
|
|
509
|
+
# empty; shows all pending queued goals as selectable rows.
|
|
510
|
+
# Placed inside #bottom-app-container (rather than as a separate
|
|
511
|
+
# docked sibling) because Textual does not stack multiple
|
|
512
|
+
# docked-to-same-edge widgets — they overlap instead.
|
|
513
|
+
yield QueuedGoalsBar(id="queued-goals")
|
|
514
|
+
with Container(id="thinking-status"):
|
|
515
|
+
yield TipRow(id="tip-row")
|
|
499
516
|
yield ChatInput(
|
|
500
517
|
cwd=self._cwd,
|
|
501
518
|
image_tracker=self._image_tracker,
|
|
@@ -189,10 +189,7 @@ class _ExecutionMixin:
|
|
|
189
189
|
await self._process_message(value, mode)
|
|
190
190
|
return
|
|
191
191
|
self._pending_messages.append(QueuedMessage(text=value, mode=mode))
|
|
192
|
-
queued_widget = QueuedUserMessage(value)
|
|
193
|
-
self._queued_widgets.append(queued_widget)
|
|
194
192
|
self._refresh_queued_goal_tips()
|
|
195
|
-
await self._mount_message(queued_widget)
|
|
196
193
|
return
|
|
197
194
|
|
|
198
195
|
await self._process_message(value, mode)
|
|
@@ -576,14 +573,12 @@ class _ExecutionMixin:
|
|
|
576
573
|
if self._agent_running or self._shell_running:
|
|
577
574
|
queued_widget = QueuedUserMessage(command)
|
|
578
575
|
self._queued_widgets.append(queued_widget)
|
|
579
|
-
self._refresh_queued_goal_tips()
|
|
580
576
|
await self._mount_message(queued_widget)
|
|
581
577
|
|
|
582
578
|
async def _mount_output() -> None:
|
|
583
579
|
# Remove the ephemeral queued widget, then mount real output.
|
|
584
580
|
if queued_widget in self._queued_widgets:
|
|
585
581
|
self._queued_widgets.remove(queued_widget)
|
|
586
|
-
self._refresh_queued_goal_tips()
|
|
587
582
|
with suppress(Exception):
|
|
588
583
|
await queued_widget.remove()
|
|
589
584
|
await self._mount_message(UserMessage(command))
|
|
@@ -677,6 +672,7 @@ class _ExecutionMixin:
|
|
|
677
672
|
elif cmd == "/clear":
|
|
678
673
|
self._pending_messages.clear()
|
|
679
674
|
self._queued_widgets.clear()
|
|
675
|
+
self._refresh_queued_goal_tips()
|
|
680
676
|
await self._clear_messages()
|
|
681
677
|
self._reset_loop_token_usage(None)
|
|
682
678
|
# Clear status message (e.g., "Interrupted" from previous session)
|
|
@@ -941,6 +937,10 @@ class _ExecutionMixin:
|
|
|
941
937
|
)
|
|
942
938
|
)
|
|
943
939
|
return
|
|
940
|
+
except Exception as exc:
|
|
941
|
+
logger.warning("Unexpected error connecting to daemon", exc_info=True)
|
|
942
|
+
await self._mount_message(ErrorMessage(f"Unexpected connection error. {exc}"))
|
|
943
|
+
return
|
|
944
944
|
self._agent_running = True
|
|
945
945
|
|
|
946
946
|
# Stop passive reads so active turn streaming has sole websocket access.
|
|
@@ -1093,6 +1093,11 @@ class _ExecutionMixin:
|
|
|
1093
1093
|
continue
|
|
1094
1094
|
except (ConnectionError, OSError, TimeoutError):
|
|
1095
1095
|
pass
|
|
1096
|
+
except Exception:
|
|
1097
|
+
logger.debug(
|
|
1098
|
+
"Unexpected error during daemon reconnect retry",
|
|
1099
|
+
exc_info=True,
|
|
1100
|
+
)
|
|
1096
1101
|
raise
|
|
1097
1102
|
except Exception as e: # Resilient tool rendering
|
|
1098
1103
|
# Attach-only idle timeout is benign: the prior turn had already
|
|
@@ -1156,12 +1161,7 @@ class _ExecutionMixin:
|
|
|
1156
1161
|
self._processing_pending = True
|
|
1157
1162
|
try:
|
|
1158
1163
|
msg = self._pending_messages.popleft()
|
|
1159
|
-
|
|
1160
|
-
# Remove the ephemeral queued-message widget
|
|
1161
|
-
if self._queued_widgets:
|
|
1162
|
-
widget = self._queued_widgets.popleft()
|
|
1163
|
-
await widget.remove()
|
|
1164
|
-
self._refresh_queued_goal_tips()
|
|
1164
|
+
self._refresh_queued_goal_tips()
|
|
1165
1165
|
|
|
1166
1166
|
await self._process_message(msg.text, msg.mode)
|
|
1167
1167
|
except Exception:
|
|
@@ -1292,11 +1292,7 @@ class _ExecutionMixin:
|
|
|
1292
1292
|
if self._pending_messages:
|
|
1293
1293
|
msg = self._pending_messages.popleft()
|
|
1294
1294
|
prompt = msg.text
|
|
1295
|
-
|
|
1296
|
-
widget = self._queued_widgets.popleft()
|
|
1297
|
-
with suppress(Exception):
|
|
1298
|
-
await widget.remove()
|
|
1299
|
-
self._refresh_queued_goal_tips()
|
|
1295
|
+
self._refresh_queued_goal_tips()
|
|
1300
1296
|
if msg.mode != "normal":
|
|
1301
1297
|
# Shell/command still need their normal handlers.
|
|
1302
1298
|
await self._process_message(msg.text, msg.mode)
|
|
@@ -38,6 +38,7 @@ from soothe_cli.tui.widgets.messages import (
|
|
|
38
38
|
SkillMessage,
|
|
39
39
|
SummarizationMessage,
|
|
40
40
|
)
|
|
41
|
+
from soothe_cli.tui.widgets.queued_goals_bar import QueuedGoalsBar
|
|
41
42
|
|
|
42
43
|
logger = logging.getLogger(__name__)
|
|
43
44
|
|
|
@@ -66,17 +67,40 @@ class _MessagesMixin:
|
|
|
66
67
|
"""Message widget lifecycle, store management, queue, interrupt/quit, toggles, editor, and events."""
|
|
67
68
|
|
|
68
69
|
def _refresh_queued_goal_tips(self) -> None:
|
|
69
|
-
"""
|
|
70
|
-
|
|
70
|
+
"""Sync the queued goals bar with the current queue state.
|
|
71
|
+
|
|
72
|
+
Feeds the current ``_pending_messages`` snapshot to the
|
|
73
|
+
``QueuedGoalsBar`` so it re-renders all rows. Shows queue
|
|
74
|
+
interaction tips on the bar when the tail goal is a normal-mode
|
|
75
|
+
goal and the agent is running.
|
|
76
|
+
"""
|
|
77
|
+
self._sync_queued_goals_bar()
|
|
78
|
+
if not self._pending_messages:
|
|
71
79
|
return
|
|
72
|
-
|
|
80
|
+
show_tips = False
|
|
73
81
|
if self._pending_messages and self._pending_messages[-1].mode == "normal":
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
82
|
+
show_tips = True
|
|
83
|
+
try:
|
|
84
|
+
bar = self.query_one("#queued-goals", QueuedGoalsBar)
|
|
85
|
+
except Exception:
|
|
86
|
+
# NoMatches, ScreenStackError, or no app/screen in test stubs.
|
|
87
|
+
return
|
|
88
|
+
with suppress(Exception):
|
|
89
|
+
bar.set_show_tips(show_tips)
|
|
90
|
+
|
|
91
|
+
def _sync_queued_goals_bar(self) -> None:
|
|
92
|
+
"""Push the current ``_pending_messages`` snapshot to the bar widget.
|
|
93
|
+
|
|
94
|
+
Safe to call when the bar is not yet mounted (e.g. during startup
|
|
95
|
+
or in unit tests using stubs) — the call is silently skipped.
|
|
96
|
+
"""
|
|
97
|
+
try:
|
|
98
|
+
bar = self.query_one("#queued-goals", QueuedGoalsBar)
|
|
99
|
+
except Exception:
|
|
100
|
+
# NoMatches, ScreenStackError, or no app/screen in test stubs.
|
|
101
|
+
return
|
|
102
|
+
goals = list(self._pending_messages)
|
|
103
|
+
bar.set_goals(goals)
|
|
80
104
|
|
|
81
105
|
def _has_pending_chat_input(self) -> bool:
|
|
82
106
|
"""Return whether chat input has draft content that should be preserved."""
|
|
@@ -369,7 +393,11 @@ class _MessagesMixin:
|
|
|
369
393
|
return False
|
|
370
394
|
|
|
371
395
|
def _assistant_card_already_visible(self, card: Any) -> bool:
|
|
372
|
-
"""True when stream already mounted this assistant body
|
|
396
|
+
"""True when the stream already mounted this assistant body.
|
|
397
|
+
|
|
398
|
+
Also suppresses the raw plan ASSISTANT card when a plan-review
|
|
399
|
+
`StructuredAskUserWidget` with overlapping body is mounted.
|
|
400
|
+
"""
|
|
373
401
|
from soothe_sdk.display.transcript_types import MessageType
|
|
374
402
|
|
|
375
403
|
adapter = getattr(self, "_ui_adapter", None)
|
|
@@ -388,6 +416,54 @@ class _MessagesMixin:
|
|
|
388
416
|
return True
|
|
389
417
|
# Only inspect the latest assistant card.
|
|
390
418
|
break
|
|
419
|
+
# Plan-mode: suppress ASSISTANT card when a plan-review widget already
|
|
420
|
+
# shows the same plan body.
|
|
421
|
+
if self._plan_review_widget_shows_text(text):
|
|
422
|
+
return True
|
|
423
|
+
return False
|
|
424
|
+
|
|
425
|
+
@staticmethod
|
|
426
|
+
def _text_overlaps_plan_body(text: str, body: str) -> bool:
|
|
427
|
+
"""True when `text` and `body` overlap (exact, prefix, substring, or 200-char prefix).
|
|
428
|
+
|
|
429
|
+
Substring checks require the shorter string to be at least 50 chars
|
|
430
|
+
to avoid false positives.
|
|
431
|
+
"""
|
|
432
|
+
if not text or not body:
|
|
433
|
+
return False
|
|
434
|
+
if text == body or text.startswith(body) or body.startswith(text):
|
|
435
|
+
return True
|
|
436
|
+
# Substring containment: handles preface trimming (e.g. "Here is the
|
|
437
|
+
# plan.\n\n## Plan:..." card vs "## Plan:..." widget body). Require the
|
|
438
|
+
# shorter string to be at least 50 chars to avoid false positives.
|
|
439
|
+
shorter = body if len(body) <= len(text) else text
|
|
440
|
+
longer = text if shorter is body else body
|
|
441
|
+
if len(shorter) >= 50 and shorter in longer:
|
|
442
|
+
return True
|
|
443
|
+
# Handle truncation: compare a significant prefix of the shorter text.
|
|
444
|
+
prefix_len = min(200, len(text), len(body))
|
|
445
|
+
if prefix_len >= 50 and text[:prefix_len] == body[:prefix_len]:
|
|
446
|
+
return True
|
|
447
|
+
return False
|
|
448
|
+
|
|
449
|
+
def _plan_review_widget_shows_text(self, text: str) -> bool:
|
|
450
|
+
"""True when a mounted plan-review widget's body overlaps `text`.
|
|
451
|
+
|
|
452
|
+
Only plan-review widgets (`_is_plan_review`) are considered, so
|
|
453
|
+
non-plan ASSISTANT cards are never suppressed.
|
|
454
|
+
"""
|
|
455
|
+
adapter = getattr(self, "_ui_adapter", None)
|
|
456
|
+
if adapter is None:
|
|
457
|
+
return False
|
|
458
|
+
by_step = getattr(adapter, "_clarification_input_by_step", None) or {}
|
|
459
|
+
for widget in by_step.values():
|
|
460
|
+
if not getattr(widget, "_is_plan_review", False):
|
|
461
|
+
continue
|
|
462
|
+
body = str(getattr(widget, "_body_markdown", "") or "").strip()
|
|
463
|
+
if not body:
|
|
464
|
+
continue
|
|
465
|
+
if self._text_overlaps_plan_body(text, body):
|
|
466
|
+
return True
|
|
391
467
|
return False
|
|
392
468
|
|
|
393
469
|
def _cognition_reason_already_visible(self, card: Any) -> bool:
|
|
@@ -624,18 +700,15 @@ class _MessagesMixin:
|
|
|
624
700
|
await self._set_spinner(None)
|
|
625
701
|
|
|
626
702
|
def _pop_last_queued_entry(self) -> Any | None:
|
|
627
|
-
"""Pop the latest queued message
|
|
703
|
+
"""Pop the latest queued message, if available.
|
|
704
|
+
|
|
705
|
+
Removes the tail entry from ``_pending_messages`` and refreshes
|
|
706
|
+
the ``QueuedGoalsBar`` so the row disappears.
|
|
707
|
+
"""
|
|
628
708
|
if not self._pending_messages:
|
|
629
709
|
return None
|
|
630
710
|
msg = self._pending_messages.pop()
|
|
631
|
-
|
|
632
|
-
widget = self._queued_widgets.pop()
|
|
633
|
-
widget.remove()
|
|
634
|
-
self._refresh_queued_goal_tips()
|
|
635
|
-
else:
|
|
636
|
-
logger.warning(
|
|
637
|
-
"Queued-widget deque empty while pending-messages was not; widget/message tracking may be out of sync"
|
|
638
|
-
)
|
|
711
|
+
self._refresh_queued_goal_tips()
|
|
639
712
|
return msg
|
|
640
713
|
|
|
641
714
|
def _restore_last_queued_goal_to_input(self) -> bool:
|
|
@@ -661,11 +734,10 @@ class _MessagesMixin:
|
|
|
661
734
|
return True
|
|
662
735
|
|
|
663
736
|
def _discard_queue(self) -> None:
|
|
664
|
-
"""Clear pending messages, deferred actions, and queued
|
|
737
|
+
"""Clear pending messages, deferred actions, and queued goals bar."""
|
|
665
738
|
self._pending_messages.clear()
|
|
666
|
-
for w in self._queued_widgets:
|
|
667
|
-
w.remove()
|
|
668
739
|
self._queued_widgets.clear()
|
|
740
|
+
self._refresh_queued_goal_tips()
|
|
669
741
|
self._deferred_actions.clear()
|
|
670
742
|
|
|
671
743
|
def _cancel_last_queued_message(self) -> bool:
|
|
@@ -687,6 +759,86 @@ class _MessagesMixin:
|
|
|
687
759
|
return False
|
|
688
760
|
return self._restore_last_queued_goal_to_input()
|
|
689
761
|
|
|
762
|
+
def cancel_queued_goal_at_index(self, index: int) -> bool:
|
|
763
|
+
"""Cancel the queued goal at ``index`` without restoring it to input.
|
|
764
|
+
|
|
765
|
+
Args:
|
|
766
|
+
index: Zero-based index into ``_pending_messages`` (0 = head/oldest).
|
|
767
|
+
|
|
768
|
+
Returns:
|
|
769
|
+
``True`` when a goal was cancelled, ``False`` when the index is
|
|
770
|
+
out of range.
|
|
771
|
+
"""
|
|
772
|
+
if index < 0 or index >= len(self._pending_messages):
|
|
773
|
+
return False
|
|
774
|
+
# deque doesn't support del[] — convert to list, remove, rebuild.
|
|
775
|
+
items = list(self._pending_messages)
|
|
776
|
+
msg = items.pop(index)
|
|
777
|
+
self._pending_messages.clear()
|
|
778
|
+
self._pending_messages.extend(items)
|
|
779
|
+
self._refresh_queued_goal_tips()
|
|
780
|
+
self.notify(f"Cancelled queued goal: {msg.text[:60]}", timeout=2)
|
|
781
|
+
return True
|
|
782
|
+
|
|
783
|
+
def edit_queued_goal_at_index(self, index: int) -> bool:
|
|
784
|
+
"""Move the queued goal at ``index`` into the chat input for editing.
|
|
785
|
+
|
|
786
|
+
Only normal-mode goals can be edited; shell/command goals are skipped
|
|
787
|
+
to avoid clobbering the input with a mode prefix.
|
|
788
|
+
|
|
789
|
+
Args:
|
|
790
|
+
index: Zero-based index into ``_pending_messages`` (0 = head/oldest).
|
|
791
|
+
|
|
792
|
+
Returns:
|
|
793
|
+
``True`` when the goal was moved to input, ``False`` otherwise.
|
|
794
|
+
"""
|
|
795
|
+
if index < 0 or index >= len(self._pending_messages):
|
|
796
|
+
return False
|
|
797
|
+
if self._has_pending_chat_input():
|
|
798
|
+
return False
|
|
799
|
+
items = list(self._pending_messages)
|
|
800
|
+
msg = items[index]
|
|
801
|
+
if msg.mode != "normal":
|
|
802
|
+
return False
|
|
803
|
+
items.pop(index)
|
|
804
|
+
self._pending_messages.clear()
|
|
805
|
+
self._pending_messages.extend(items)
|
|
806
|
+
if self._chat_input:
|
|
807
|
+
self._chat_input.value = msg.text
|
|
808
|
+
self._refresh_queued_goal_tips()
|
|
809
|
+
self.notify("Queued goal moved to input", timeout=2)
|
|
810
|
+
return True
|
|
811
|
+
|
|
812
|
+
def submit_queued_goal_at_index(self, index: int) -> bool:
|
|
813
|
+
"""Submit the queued goal at ``index`` for immediate execution.
|
|
814
|
+
|
|
815
|
+
Pops the goal from the queue and processes it right away, bypassing
|
|
816
|
+
the normal FIFO order. Only valid when no agent/worker is already
|
|
817
|
+
running, since submitting while busy would just re-queue the goal.
|
|
818
|
+
|
|
819
|
+
Args:
|
|
820
|
+
index: Zero-based index into ``_pending_messages`` (0 = head/oldest).
|
|
821
|
+
|
|
822
|
+
Returns:
|
|
823
|
+
``True`` when the goal was submitted, ``False`` when the index is
|
|
824
|
+
out of range or an agent is already running.
|
|
825
|
+
"""
|
|
826
|
+
if index < 0 or index >= len(self._pending_messages):
|
|
827
|
+
return False
|
|
828
|
+
if self._agent_running or self._shell_running or self._processing_pending:
|
|
829
|
+
self.notify("Agent is busy — goal stays queued", timeout=2)
|
|
830
|
+
return False
|
|
831
|
+
items = list(self._pending_messages)
|
|
832
|
+
msg = items.pop(index)
|
|
833
|
+
self._pending_messages.clear()
|
|
834
|
+
self._pending_messages.extend(items)
|
|
835
|
+
self._refresh_queued_goal_tips()
|
|
836
|
+
import asyncio
|
|
837
|
+
|
|
838
|
+
asyncio.ensure_future(self._process_message(msg.text, msg.mode))
|
|
839
|
+
self.notify(f"Submitting: {msg.text[:60]}", timeout=2)
|
|
840
|
+
return True
|
|
841
|
+
|
|
690
842
|
def _defer_action(self, action: DeferredAction) -> None:
|
|
691
843
|
"""Queue a deferred action, replacing any existing action of the same kind.
|
|
692
844
|
|
|
@@ -891,13 +1043,25 @@ class _MessagesMixin:
|
|
|
891
1043
|
|
|
892
1044
|
# Close completion popup or exit slash/shell command mode
|
|
893
1045
|
if self._chat_input:
|
|
894
|
-
# When queue has pending goals and input is idle, Esc cancels
|
|
895
|
-
#
|
|
1046
|
+
# When queue has pending goals and input is idle, Esc cancels a
|
|
1047
|
+
# queued normal goal (without interrupting current work). If the
|
|
1048
|
+
# QueuedGoalsBar is focused, cancel the selected goal; otherwise
|
|
1049
|
+
# cancel the latest (tail) goal.
|
|
896
1050
|
if not self._has_pending_chat_input():
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
1051
|
+
cancelled = False
|
|
1052
|
+
try:
|
|
1053
|
+
bar = self.query_one("#queued-goals", QueuedGoalsBar)
|
|
1054
|
+
except Exception:
|
|
1055
|
+
# NoMatches, ScreenStackError, or no app/screen in test stubs.
|
|
1056
|
+
bar = None
|
|
1057
|
+
if bar is not None and self.focused is bar:
|
|
1058
|
+
cancelled = bar.cancel_selected()
|
|
1059
|
+
if not cancelled:
|
|
1060
|
+
queue_tail = self._pending_messages[-1] if self._pending_messages else None
|
|
1061
|
+
if queue_tail is not None and queue_tail.mode == "normal":
|
|
1062
|
+
cancelled = self._cancel_last_queued_message()
|
|
1063
|
+
if cancelled:
|
|
1064
|
+
return
|
|
901
1065
|
if self._chat_input.dismiss_completion():
|
|
902
1066
|
return
|
|
903
1067
|
if self._chat_input.exit_mode():
|
|
@@ -983,7 +1147,7 @@ class _MessagesMixin:
|
|
|
983
1147
|
"""Shift+Tab: navigate loop selector when active, otherwise cycle mode.
|
|
984
1148
|
|
|
985
1149
|
- In the LoopSelectorScreen, defer to its filter navigation.
|
|
986
|
-
- On the main screen, cycle composer mode Auto → Manual → Plan → Ask.
|
|
1150
|
+
- On the main screen, cycle composer mode Auto → Bypass → Manual → Plan → Ask.
|
|
987
1151
|
"""
|
|
988
1152
|
from soothe_cli.tui.widgets.loop_selector import LoopSelectorScreen
|
|
989
1153
|
|
|
@@ -995,15 +1159,9 @@ class _MessagesMixin:
|
|
|
995
1159
|
def cycle_composer_mode(self) -> None:
|
|
996
1160
|
"""Advance composer mode and refresh the status-bar badge.
|
|
997
1161
|
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
immediately; no toast is emitted because the badge is the feedback.
|
|
1002
|
-
|
|
1003
|
-
When a goal is running and the new mode is Auto or Manual, the mode is
|
|
1004
|
-
also hot-swapped on the daemon's live `LoopRuntimeContext` via the
|
|
1005
|
-
`loop_set_clarification_mode` RPC so the next clarification uses the
|
|
1006
|
-
new policy without waiting for the next turn.
|
|
1162
|
+
Cycle: Auto → Bypass → Manual → Plan → Ask → Auto. When a goal is
|
|
1163
|
+
running and the new mode is Auto or Manual, hot-swaps the daemon's
|
|
1164
|
+
live clarification policy via `loop_set_clarification_mode`.
|
|
1007
1165
|
"""
|
|
1008
1166
|
from soothe_cli.tui.composer_mode import next_composer_mode
|
|
1009
1167
|
|
|
@@ -486,6 +486,7 @@ class _ModelMixin:
|
|
|
486
486
|
# Clear conversation (similar to /clear, without creating a new loop)
|
|
487
487
|
self._pending_messages.clear()
|
|
488
488
|
self._queued_widgets.clear()
|
|
489
|
+
self._refresh_queued_goal_tips()
|
|
489
490
|
await self._clear_messages()
|
|
490
491
|
self._reset_loop_token_usage(None)
|
|
491
492
|
self._update_status("")
|
|
@@ -24,6 +24,7 @@ from soothe_cli.tui.app._types import (
|
|
|
24
24
|
TextualSessionState,
|
|
25
25
|
)
|
|
26
26
|
from soothe_cli.tui.widgets.chat_input import ChatInput
|
|
27
|
+
from soothe_cli.tui.widgets.loading import TipRow
|
|
27
28
|
from soothe_cli.tui.widgets.messages import (
|
|
28
29
|
AppMessage,
|
|
29
30
|
ErrorMessage,
|
|
@@ -129,6 +130,7 @@ class _StartupMixin:
|
|
|
129
130
|
self.watch(chat, "scroll_y", self._on_chat_scroll_y_changed)
|
|
130
131
|
|
|
131
132
|
self._status_bar = self.query_one("#status-bar", StatusBar)
|
|
133
|
+
self._tip_row = self.query_one("#tip-row", TipRow)
|
|
132
134
|
self._chat_input = self.query_one("#input-area", ChatInput)
|
|
133
135
|
with suppress(NoMatches):
|
|
134
136
|
from soothe_cli.tui.widgets.plan_quick_view_overlay import PlanQuickViewOverlay
|
|
@@ -142,8 +144,11 @@ class _StartupMixin:
|
|
|
142
144
|
# hard-coded "auto" fallback before the daemon-default seed runs).
|
|
143
145
|
self._status_bar.set_clarification_mode(self._composer_mode)
|
|
144
146
|
|
|
145
|
-
# Seed the
|
|
147
|
+
# Seed the thinking-row tip with the first rotating tip and start rotation.
|
|
148
|
+
# The tip row is visible while the loop is idle; `_set_spinner` hides it
|
|
149
|
+
# when the agent starts working and restores it when the loop goes idle.
|
|
146
150
|
self.set_default_session_tip(self._tip_rotator.next_tip())
|
|
151
|
+
self._show_tip_row()
|
|
147
152
|
self.start_tip_rotation()
|
|
148
153
|
|
|
149
154
|
# Focus the input immediately so the cursor is visible on first paint
|
|
@@ -641,9 +646,8 @@ class _StartupMixin:
|
|
|
641
646
|
# Discard any messages queued while connecting
|
|
642
647
|
if self._pending_messages:
|
|
643
648
|
self._pending_messages.clear()
|
|
644
|
-
for w in self._queued_widgets:
|
|
645
|
-
w.remove()
|
|
646
649
|
self._queued_widgets.clear()
|
|
650
|
+
self._refresh_queued_goal_tips()
|
|
647
651
|
self._deferred_actions.clear()
|
|
648
652
|
|
|
649
653
|
@staticmethod
|