soothe-cli 1.0.1__tar.gz → 1.0.4__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.1 → soothe_cli-1.0.4}/.gitignore +1 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/PKG-INFO +2 -2
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/pyproject.toml +1 -1
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/cli/main.py +2 -2
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/commands/binding.py +3 -3
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/commands/slash_commands.py +1 -1
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/config/cli_config.py +2 -2
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/display/tips.py +1 -1
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/tui/app/_app.py +17 -15
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/tui/app/_execution.py +13 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/tui/app/_messages_mixin.py +25 -38
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/tui/app/_startup.py +6 -1
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/tui/app/_ui.py +12 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/tui/composer_mode.py +22 -16
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/tui/textual_adapter.py +9 -6
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/tui/widgets/help_screen.py +1 -1
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/tui/widgets/loading.py +71 -1
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/tui/widgets/messages/cognition_goal_tree.py +13 -2
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/tui/widgets/messages/cognition_step.py +20 -2
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/tui/widgets/messages/structured_ask_user.py +118 -57
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/tui/widgets/plan_quick_view_overlay.py +2 -2
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/tui/widgets/status.py +22 -65
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/README.md +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/__init__.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/_cli_context.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/_env_vars.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/_version.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/card_wire.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/cli/__init__.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/cli/commands/__init__.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/cli/commands/autopilot_cmd.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/cli/commands/config_cmd.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/cli/commands/cron_cmd.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/cli/commands/loop_cmd.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/cli/commands/run_cmd.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/cli/commands/status_cmd.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/cli/execution/__init__.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/cli/execution/daemon.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/cli/execution/daemon_errors.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/cli/execution/headless.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/cli/execution/headless_renderer.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/cli/execution/launcher.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/cli/soothecli.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/commands/__init__.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/commands/command_registry.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/commands/command_router.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/commands/subagent_routing.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/config/__init__.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/config/loader.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/config/logging_setup.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/display/__init__.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/display/markdown_theme.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/display/media_utils.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/display/mermaid_render.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/display/path_utils.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/display/preview_limits.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/display/shell_color.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/display/spinner_labels.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/display/theme.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/display/tool_display.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/display/unicode_security.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/loops/__init__.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/loops/sessions.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/model_config.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/project_utils.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/runtime/__init__.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/runtime/headless/processor.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/runtime/headless/processor_state.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/runtime/parse/tool_call_resolution.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/runtime/policy/tui_trace_log.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/runtime/presentation/duration_format.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/runtime/presentation/engine.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/runtime/presentation/id_format.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/runtime/presentation/renderer_base.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/runtime/presentation/renderer_protocol.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/runtime/presentation/step_id_format.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/runtime/presentation/subagent_task_display.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/runtime/state/file_tracker.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/runtime/state/session_stats.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/runtime/state/step_router.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/runtime/state/stream_accumulator.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/runtime/token_events_debug.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/runtime/token_usage.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/runtime/transport/session.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/runtime/turn/prepare.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/runtime/wire/chunk_filter.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/runtime/wire/message_text.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/runtime/wire/messages.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/settings/__init__.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/settings/_console_impl.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/settings/bootstrap.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/settings/core.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/settings/glyphs.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/settings/provider.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/settings/shell_allow.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/settings/skills_dirs.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/settings/stream_config.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/tui/__init__.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/tui/app/__init__.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/tui/app/_clarification.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/tui/app/_entrypoints.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/tui/app/_history.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/tui/app/_model.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/tui/app/_model_params.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/tui/app/_terminal.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/tui/app/_theme_prefs.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/tui/app/_types.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/tui/app/app.tcss +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/tui/file_change_notify.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/tui/file_change_renderers.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/tui/input.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/tui/skills/__init__.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/tui/skills/invocation.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/tui/skills/load.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/tui/widgets/__init__.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/tui/widgets/_links.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/tui/widgets/autocomplete.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/tui/widgets/chat_input.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/tui/widgets/clipboard.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/tui/widgets/context_data.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/tui/widgets/context_viewer.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/tui/widgets/diff.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/tui/widgets/editor.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/tui/widgets/file_change_preview.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/tui/widgets/history.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/tui/widgets/loop_selector.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/tui/widgets/mcp_viewer.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/tui/widgets/message_store.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/tui/widgets/messages/__init__.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/tui/widgets/messages/_helpers.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/tui/widgets/messages/app.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/tui/widgets/messages/assistant.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/tui/widgets/messages/cognition_reason.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/tui/widgets/messages/cognition_step_activity.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/tui/widgets/messages/diff_message.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/tui/widgets/messages/error.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/tui/widgets/messages/skill.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/tui/widgets/messages/user.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/tui/widgets/model_selector.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/tui/widgets/notification_settings.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/tui/widgets/pinned_goal_bar.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/tui/widgets/router_profile_selector.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/tui/widgets/theme_selector.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/tui/widgets/welcome.py +0 -0
- {soothe_cli-1.0.1 → soothe_cli-1.0.4}/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.4
|
|
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."),
|
|
@@ -155,14 +155,14 @@ def message_to_widget(data: MessageData) -> Widget:
|
|
|
155
155
|
is_tool_approval = origin == "tool_approval"
|
|
156
156
|
refine_label = "Edit" if is_tool_approval else "Refine"
|
|
157
157
|
question_spec = {
|
|
158
|
-
"question": "Action for this plan: Approve,
|
|
158
|
+
"question": "Action for this plan: Approve, Reject, or Refine?"
|
|
159
159
|
if not is_tool_approval
|
|
160
160
|
else "Approve this tool call?",
|
|
161
161
|
"header": "Plan review" if not is_tool_approval else "Tool approval",
|
|
162
162
|
"options": [
|
|
163
163
|
{"label": "Approve", "description": "Accept and proceed."},
|
|
164
|
-
{"label": refine_label, "description": "Request changes."},
|
|
165
164
|
{"label": "Reject", "description": "Reject."},
|
|
165
|
+
{"label": refine_label, "description": "Request changes."},
|
|
166
166
|
],
|
|
167
167
|
}
|
|
168
168
|
w = StructuredAskUserWidget(
|
|
@@ -261,7 +261,7 @@ def message_from_widget(widget: Widget) -> MessageData:
|
|
|
261
261
|
# Awaiting state for HITL origins.
|
|
262
262
|
if is_hitl and not widget._submitted: # noqa: SLF001
|
|
263
263
|
if is_plan_review:
|
|
264
|
-
content = "Plan review: awaiting Approve /
|
|
264
|
+
content = "Plan review: awaiting Approve / Reject / Refine"
|
|
265
265
|
else:
|
|
266
266
|
content = "Tool approval: awaiting Approve / Edit / Reject"
|
|
267
267
|
return MessageData(type=MessageType.APP, content=content, id=widget_id)
|
|
@@ -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,
|
|
@@ -226,10 +226,11 @@ class SootheApp(
|
|
|
226
226
|
self._mcp_tool_count = sum(len(s.tools) for s in (mcp_server_info or []))
|
|
227
227
|
|
|
228
228
|
self._status_bar: StatusBar | None = None
|
|
229
|
+
self._tip_row: TipRow | None = None
|
|
229
230
|
self._default_session_tip: str = ""
|
|
230
231
|
self._status_notification_timer: Timer | None = None
|
|
231
232
|
self._status_notification_active = False
|
|
232
|
-
# Rotating tip source + interval timer for the
|
|
233
|
+
# Rotating tip source + interval timer for the thinking row.
|
|
233
234
|
self._tip_rotator: TipRotator = TipRotator()
|
|
234
235
|
self._tip_rotation_timer: Timer | None = None
|
|
235
236
|
|
|
@@ -381,11 +382,11 @@ class SootheApp(
|
|
|
381
382
|
"""Persist and render the fallback tip shown when no notification is active."""
|
|
382
383
|
cleaned = (tip or "").strip()
|
|
383
384
|
self._default_session_tip = cleaned
|
|
384
|
-
if self.
|
|
385
|
-
self.
|
|
385
|
+
if self._tip_row is not None and not self._status_notification_active:
|
|
386
|
+
self._tip_row.set_tip(cleaned)
|
|
386
387
|
|
|
387
388
|
def start_tip_rotation(self, interval: float = 12.0) -> None:
|
|
388
|
-
"""Cycle through rotating tips in the
|
|
389
|
+
"""Cycle through rotating tips in the thinking row at a fixed interval.
|
|
389
390
|
|
|
390
391
|
Args:
|
|
391
392
|
interval: Seconds between tip rotations.
|
|
@@ -401,7 +402,7 @@ class SootheApp(
|
|
|
401
402
|
self._tip_rotation_timer = None
|
|
402
403
|
|
|
403
404
|
def _rotate_session_tip(self) -> None:
|
|
404
|
-
"""Advance to the next tip and push it to the
|
|
405
|
+
"""Advance to the next tip and push it to the thinking row.
|
|
405
406
|
|
|
406
407
|
Transient notifications take precedence: rotation is skipped while a
|
|
407
408
|
notification is active so it can run its timeout uninterrupted.
|
|
@@ -411,7 +412,7 @@ class SootheApp(
|
|
|
411
412
|
self.set_default_session_tip(self._tip_rotator.next_tip())
|
|
412
413
|
|
|
413
414
|
def _set_status_notification(self, message: str, *, timeout: float | None = None) -> None:
|
|
414
|
-
"""Render a transient notification in the
|
|
415
|
+
"""Render a transient notification in the thinking-row tip area."""
|
|
415
416
|
text = (message or "").strip()
|
|
416
417
|
if not text:
|
|
417
418
|
return
|
|
@@ -420,8 +421,8 @@ class SootheApp(
|
|
|
420
421
|
self._status_notification_timer.stop()
|
|
421
422
|
self._status_notification_timer = None
|
|
422
423
|
self._status_notification_active = True
|
|
423
|
-
if self.
|
|
424
|
-
self.
|
|
424
|
+
if self._tip_row is not None:
|
|
425
|
+
self._tip_row.set_notification(text)
|
|
425
426
|
|
|
426
427
|
duration = timeout if timeout and timeout > 0 else 3.0
|
|
427
428
|
self._status_notification_timer = self.set_timer(duration, self._clear_status_notification)
|
|
@@ -430,8 +431,8 @@ class SootheApp(
|
|
|
430
431
|
"""Restore the default tip after transient notification timeout."""
|
|
431
432
|
self._status_notification_timer = None
|
|
432
433
|
self._status_notification_active = False
|
|
433
|
-
if self.
|
|
434
|
-
self.
|
|
434
|
+
if self._tip_row is not None:
|
|
435
|
+
self._tip_row.set_tip(self._default_session_tip)
|
|
435
436
|
|
|
436
437
|
def notify( # type: ignore[override]
|
|
437
438
|
self,
|
|
@@ -442,7 +443,7 @@ class SootheApp(
|
|
|
442
443
|
timeout: float | None = None,
|
|
443
444
|
markup: bool = True, # noqa: ARG002
|
|
444
445
|
) -> None:
|
|
445
|
-
"""Display notifications in the
|
|
446
|
+
"""Display notifications in the thinking-row tip area instead of toast bubbles."""
|
|
446
447
|
text = str(message or "").strip()
|
|
447
448
|
if not text:
|
|
448
449
|
return
|
|
@@ -488,14 +489,15 @@ class SootheApp(
|
|
|
488
489
|
# In-flow plan panel: sits above the sticky bottom chrome so Ctrl+t
|
|
489
490
|
# never covers the thinking row or chat input. Auto-shows while a goal
|
|
490
491
|
# is executing when CLIConfig.plan_panel_default_visible is True
|
|
491
|
-
# (
|
|
492
|
-
plan_visible = getattr(self._daemon_config, "plan_panel_default_visible",
|
|
492
|
+
# (--plan-panel); default is False (hidden, Ctrl+t to open).
|
|
493
|
+
plan_visible = getattr(self._daemon_config, "plan_panel_default_visible", False)
|
|
493
494
|
yield PlanQuickViewOverlay(
|
|
494
495
|
id="plan-quick-view-overlay",
|
|
495
496
|
default_visible=plan_visible,
|
|
496
497
|
)
|
|
497
498
|
with Container(id="bottom-app-container"):
|
|
498
|
-
|
|
499
|
+
with Container(id="thinking-status"):
|
|
500
|
+
yield TipRow(id="tip-row")
|
|
499
501
|
yield ChatInput(
|
|
500
502
|
cwd=self._cwd,
|
|
501
503
|
image_tracker=self._image_tracker,
|
|
@@ -341,6 +341,10 @@ class _ExecutionMixin:
|
|
|
341
341
|
# next pauses, which freezes scrolling and chat-input focus.
|
|
342
342
|
await self._send_to_agent(payload_text)
|
|
343
343
|
|
|
344
|
+
# Return focus to the chat input so the user can type immediately.
|
|
345
|
+
with suppress(Exception):
|
|
346
|
+
self.focus_primary_input()
|
|
347
|
+
|
|
344
348
|
async def _resolve_default_clarification_mode(self) -> str:
|
|
345
349
|
"""Fetch the daemon's default clarification mode for plan approval.
|
|
346
350
|
|
|
@@ -937,6 +941,10 @@ class _ExecutionMixin:
|
|
|
937
941
|
)
|
|
938
942
|
)
|
|
939
943
|
return
|
|
944
|
+
except Exception as exc:
|
|
945
|
+
logger.warning("Unexpected error connecting to daemon", exc_info=True)
|
|
946
|
+
await self._mount_message(ErrorMessage(f"Unexpected connection error. {exc}"))
|
|
947
|
+
return
|
|
940
948
|
self._agent_running = True
|
|
941
949
|
|
|
942
950
|
# Stop passive reads so active turn streaming has sole websocket access.
|
|
@@ -1089,6 +1097,11 @@ class _ExecutionMixin:
|
|
|
1089
1097
|
continue
|
|
1090
1098
|
except (ConnectionError, OSError, TimeoutError):
|
|
1091
1099
|
pass
|
|
1100
|
+
except Exception:
|
|
1101
|
+
logger.debug(
|
|
1102
|
+
"Unexpected error during daemon reconnect retry",
|
|
1103
|
+
exc_info=True,
|
|
1104
|
+
)
|
|
1092
1105
|
raise
|
|
1093
1106
|
except Exception as e: # Resilient tool rendering
|
|
1094
1107
|
# Attach-only idle timeout is benign: the prior turn had already
|
|
@@ -793,20 +793,22 @@ class _MessagesMixin:
|
|
|
793
793
|
def action_quit_or_interrupt(self) -> None:
|
|
794
794
|
"""Handle Ctrl+C — clear input, interrupt work, or exit on double-press.
|
|
795
795
|
|
|
796
|
-
Priority order
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
Interrupting a task resets the idle double-press exit state.
|
|
800
|
-
|
|
801
|
-
When idle, Ctrl+C arms a double-press exit:
|
|
802
|
-
1. First press clears any pending draft and shows a hint.
|
|
803
|
-
2. Second press within the timeout window exits the TUI.
|
|
804
|
-
|
|
805
|
-
Note: Copying selected text is bound to Ctrl+Y (`action_copy_selection`)
|
|
806
|
-
so Ctrl+C is reserved for interrupt/exit behavior.
|
|
796
|
+
Priority order: HITL review card (abandon + focus chat) → running
|
|
797
|
+
task (clear input, then interrupt) → idle double-press exit.
|
|
798
|
+
Ctrl+Y copies selection, so Ctrl+C stays reserved for this path.
|
|
807
799
|
"""
|
|
808
800
|
current_time = time.monotonic()
|
|
809
801
|
|
|
802
|
+
# HITL review card (plan/tool): abandon and return focus to chat.
|
|
803
|
+
if self._has_active_qa_widget():
|
|
804
|
+
widget = self._active_plan_review_widget()
|
|
805
|
+
if widget is not None and not getattr(widget, "_submitted", False):
|
|
806
|
+
widget._abandon()
|
|
807
|
+
self._ctrl_c_pressed_time = None
|
|
808
|
+
if self._chat_input:
|
|
809
|
+
self._chat_input.focus_input()
|
|
810
|
+
return
|
|
811
|
+
|
|
810
812
|
# Check if input has pending content (text, mode, or completion).
|
|
811
813
|
has_pending_input = self._has_pending_chat_input()
|
|
812
814
|
|
|
@@ -981,7 +983,7 @@ class _MessagesMixin:
|
|
|
981
983
|
"""Shift+Tab: navigate loop selector when active, otherwise cycle mode.
|
|
982
984
|
|
|
983
985
|
- In the LoopSelectorScreen, defer to its filter navigation.
|
|
984
|
-
- On the main screen, cycle composer mode Auto → Manual → Plan → Ask.
|
|
986
|
+
- On the main screen, cycle composer mode Auto → Bypass → Manual → Plan → Ask.
|
|
985
987
|
"""
|
|
986
988
|
from soothe_cli.tui.widgets.loop_selector import LoopSelectorScreen
|
|
987
989
|
|
|
@@ -993,15 +995,9 @@ class _MessagesMixin:
|
|
|
993
995
|
def cycle_composer_mode(self) -> None:
|
|
994
996
|
"""Advance composer mode and refresh the status-bar badge.
|
|
995
997
|
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
immediately; no toast is emitted because the badge is the feedback.
|
|
1000
|
-
|
|
1001
|
-
When a goal is running and the new mode is Auto or Manual, the mode is
|
|
1002
|
-
also hot-swapped on the daemon's live `LoopRuntimeContext` via the
|
|
1003
|
-
`loop_set_clarification_mode` RPC so the next clarification uses the
|
|
1004
|
-
new policy without waiting for the next turn.
|
|
998
|
+
Cycle: Auto → Bypass → Manual → Plan → Ask → Auto. When a goal is
|
|
999
|
+
running and the new mode is Auto or Manual, hot-swaps the daemon's
|
|
1000
|
+
live clarification policy via `loop_set_clarification_mode`.
|
|
1005
1001
|
"""
|
|
1006
1002
|
from soothe_cli.tui.composer_mode import next_composer_mode
|
|
1007
1003
|
|
|
@@ -1072,25 +1068,21 @@ class _MessagesMixin:
|
|
|
1072
1068
|
inputs.append(inp)
|
|
1073
1069
|
return inputs
|
|
1074
1070
|
|
|
1075
|
-
def
|
|
1076
|
-
"""
|
|
1071
|
+
def _active_plan_review_widget(self) -> Widget | None:
|
|
1072
|
+
"""Return the active unsubmitted HITL review card, if any."""
|
|
1077
1073
|
adapter = getattr(self, "_ui_adapter", None)
|
|
1078
1074
|
if adapter is None:
|
|
1079
1075
|
return None
|
|
1080
1076
|
by_step = getattr(adapter, "_clarification_input_by_step", None) or {}
|
|
1081
|
-
# Both option-selector origins render Approve / Refine(Edit) / Reject buttons.
|
|
1082
1077
|
for message in by_step.values():
|
|
1083
1078
|
if getattr(message, "_submitted", False):
|
|
1084
1079
|
continue
|
|
1085
1080
|
# String compare — avoid MagicMock truthiness on property access in tests.
|
|
1086
|
-
if getattr(message, "_origin_node", None)
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
approve = buttons.get("approve")
|
|
1092
|
-
if approve is not None and not getattr(approve, "disabled", False):
|
|
1093
|
-
return approve
|
|
1081
|
+
if getattr(message, "_origin_node", None) in (
|
|
1082
|
+
"plan_mode_review",
|
|
1083
|
+
"tool_approval",
|
|
1084
|
+
):
|
|
1085
|
+
return message
|
|
1094
1086
|
return None
|
|
1095
1087
|
|
|
1096
1088
|
def _non_chat_focusable_inputs(self) -> list[Input]:
|
|
@@ -1107,18 +1099,13 @@ class _MessagesMixin:
|
|
|
1107
1099
|
"""Return the single input that should receive typing focus, if unambiguous.
|
|
1108
1100
|
|
|
1109
1101
|
When an inline clarification card is active, its answer field takes
|
|
1110
|
-
precedence over the bottom chat prompt.
|
|
1111
|
-
focuses the Approve action. On modal screens, a lone
|
|
1102
|
+
precedence over the bottom chat prompt. On modal screens, a lone
|
|
1112
1103
|
filter box is focused automatically.
|
|
1113
1104
|
"""
|
|
1114
1105
|
clar_inputs = self._active_clarification_inputs()
|
|
1115
1106
|
if clar_inputs:
|
|
1116
1107
|
return clar_inputs[0]
|
|
1117
1108
|
|
|
1118
|
-
plan_action = self._active_plan_review_action_focus()
|
|
1119
|
-
if plan_action is not None:
|
|
1120
|
-
return plan_action
|
|
1121
|
-
|
|
1122
1109
|
non_chat = self._non_chat_focusable_inputs()
|
|
1123
1110
|
if len(non_chat) == 1:
|
|
1124
1111
|
return non_chat[0]
|
|
@@ -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
|
|
@@ -421,8 +421,10 @@ class _UIMixin:
|
|
|
421
421
|
await self._loading_widget.remove()
|
|
422
422
|
self._loading_widget = None
|
|
423
423
|
self._connect_spinner_start_mono = None
|
|
424
|
+
self._show_tip_row()
|
|
424
425
|
return
|
|
425
426
|
|
|
427
|
+
self._hide_tip_row()
|
|
426
428
|
thinking_status = self.query_one("#thinking-status", Container)
|
|
427
429
|
|
|
428
430
|
connect_start_mono: float | None = None
|
|
@@ -481,6 +483,16 @@ class _UIMixin:
|
|
|
481
483
|
if self._loading_widget is not None:
|
|
482
484
|
self._loading_widget.resume()
|
|
483
485
|
|
|
486
|
+
def _show_tip_row(self) -> None:
|
|
487
|
+
"""Show the rotating tip in the thinking row when the loop is idle."""
|
|
488
|
+
if self._tip_row is not None:
|
|
489
|
+
self._tip_row.display = True
|
|
490
|
+
|
|
491
|
+
def _hide_tip_row(self) -> None:
|
|
492
|
+
"""Hide the tip row so the thinking spinner gets the full row."""
|
|
493
|
+
if self._tip_row is not None:
|
|
494
|
+
self._tip_row.display = False
|
|
495
|
+
|
|
484
496
|
def action_toggle_plan_quick_view(self) -> None:
|
|
485
497
|
"""Expand or collapse the in-flow plan panel above the thinking row."""
|
|
486
498
|
if self.screen.is_modal:
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""TUI composer modes — working-mode hierarchy.
|
|
2
|
+
|
|
3
|
+
Agent sub-modes (auto, bypass, manual) grouped first, then plan and ask.
|
|
4
|
+
Shift+Tab cycle: Auto → Bypass → Manual → Plan → Ask → Auto.
|
|
5
|
+
"""
|
|
2
6
|
|
|
3
7
|
from __future__ import annotations
|
|
4
8
|
|
|
@@ -8,15 +12,25 @@ COMPOSER_MODE_AUTO = "auto"
|
|
|
8
12
|
COMPOSER_MODE_MANUAL = "manual"
|
|
9
13
|
COMPOSER_MODE_PLAN = "plan"
|
|
10
14
|
COMPOSER_MODE_ASK = "ask"
|
|
15
|
+
COMPOSER_MODE_BYPASS = "bypass"
|
|
11
16
|
|
|
17
|
+
# Agent sub-modes grouped first, then standalone working modes.
|
|
12
18
|
COMPOSER_MODE_ORDER: tuple[str, ...] = (
|
|
13
19
|
COMPOSER_MODE_AUTO,
|
|
20
|
+
COMPOSER_MODE_BYPASS,
|
|
14
21
|
COMPOSER_MODE_MANUAL,
|
|
15
22
|
COMPOSER_MODE_PLAN,
|
|
16
23
|
COMPOSER_MODE_ASK,
|
|
17
24
|
)
|
|
18
25
|
VALID_COMPOSER_MODES: frozenset[str] = frozenset(COMPOSER_MODE_ORDER)
|
|
19
26
|
|
|
27
|
+
# Agent working mode sub-modes (full mutating tool surface).
|
|
28
|
+
AGENT_SUB_MODES: frozenset[str] = frozenset(
|
|
29
|
+
{COMPOSER_MODE_AUTO, COMPOSER_MODE_MANUAL, COMPOSER_MODE_BYPASS}
|
|
30
|
+
)
|
|
31
|
+
# Standalone working modes (not agent sub-modes).
|
|
32
|
+
STANDALONE_WORKING_MODES: frozenset[str] = frozenset({COMPOSER_MODE_PLAN, COMPOSER_MODE_ASK})
|
|
33
|
+
|
|
20
34
|
|
|
21
35
|
@dataclass(frozen=True)
|
|
22
36
|
class ComposerWireFields:
|
|
@@ -35,11 +49,7 @@ def normalize_composer_mode(mode: str | None) -> str:
|
|
|
35
49
|
|
|
36
50
|
|
|
37
51
|
def next_composer_mode(current: str) -> str:
|
|
38
|
-
"""Advance
|
|
39
|
-
|
|
40
|
-
Unknown values normalize to `auto` (the default, same as a first
|
|
41
|
-
Shift+Tab from a garbage seed), without advancing past Auto in that step.
|
|
42
|
-
"""
|
|
52
|
+
"""Advance to the next mode in the cycle. Unknown values reset to `auto`."""
|
|
43
53
|
if current not in VALID_COMPOSER_MODES:
|
|
44
54
|
return COMPOSER_MODE_AUTO
|
|
45
55
|
idx = COMPOSER_MODE_ORDER.index(current)
|
|
@@ -47,23 +57,19 @@ def next_composer_mode(current: str) -> str:
|
|
|
47
57
|
|
|
48
58
|
|
|
49
59
|
def resolve_composer_wire_fields(mode: str) -> ComposerWireFields:
|
|
50
|
-
"""Map composer mode to wire
|
|
51
|
-
`interaction_mode`.
|
|
52
|
-
|
|
53
|
-
Args:
|
|
54
|
-
mode: Composer mode (`auto`, `manual`, `plan`, or `ask`).
|
|
60
|
+
"""Map composer mode to wire fields.
|
|
55
61
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
`clarification_mode=auto` plus `interaction_mode=ask`. Slash routing
|
|
60
|
-
in the message still wins over the sticky hint at send time.
|
|
62
|
+
auto → clarification=auto, interaction=None; manual → clarification=manual;
|
|
63
|
+
bypass → clarification=auto, interaction=bypass; plan → interaction=plan;
|
|
64
|
+
ask → interaction=ask. Slash routing in the message wins over the sticky hint.
|
|
61
65
|
"""
|
|
62
66
|
normalized = normalize_composer_mode(mode)
|
|
63
67
|
if normalized == COMPOSER_MODE_PLAN:
|
|
64
68
|
return ComposerWireFields(COMPOSER_MODE_AUTO, None, "plan")
|
|
65
69
|
if normalized == COMPOSER_MODE_ASK:
|
|
66
70
|
return ComposerWireFields(COMPOSER_MODE_AUTO, None, COMPOSER_MODE_ASK)
|
|
71
|
+
if normalized == COMPOSER_MODE_BYPASS:
|
|
72
|
+
return ComposerWireFields(COMPOSER_MODE_AUTO, None, COMPOSER_MODE_BYPASS)
|
|
67
73
|
if normalized == COMPOSER_MODE_MANUAL:
|
|
68
74
|
return ComposerWireFields(COMPOSER_MODE_MANUAL, None, None)
|
|
69
75
|
return ComposerWireFields(COMPOSER_MODE_AUTO, None, None)
|
|
@@ -4121,7 +4121,15 @@ async def execute_task_textual(
|
|
|
4121
4121
|
goal_loop_start_monotonic=goal_loop_start_monotonic,
|
|
4122
4122
|
turn_start_monotonic=start_time,
|
|
4123
4123
|
)
|
|
4124
|
-
|
|
4124
|
+
follow_on = (
|
|
4125
|
+
data.get("follow_on_exec")
|
|
4126
|
+
if isinstance(data, dict)
|
|
4127
|
+
else None
|
|
4128
|
+
)
|
|
4129
|
+
# Skip the terminal footer when a follow-on exec
|
|
4130
|
+
# goal is pending: the daemon immediately enqueues
|
|
4131
|
+
# it, so showing "Done" here is misleading.
|
|
4132
|
+
if adapter._goal_tree_message is not None and not follow_on:
|
|
4125
4133
|
goal_elapsed_start = _goal_loop_elapsed_start(
|
|
4126
4134
|
goal_loop_start_monotonic=goal_loop_start_monotonic,
|
|
4127
4135
|
turn_start_monotonic=start_time,
|
|
@@ -4164,11 +4172,6 @@ async def execute_task_textual(
|
|
|
4164
4172
|
# row stays active through the plan→exec transition;
|
|
4165
4173
|
# the exec goal's ``STRANGE_LOOP_STARTED`` will
|
|
4166
4174
|
# re-anchor and replace it.
|
|
4167
|
-
follow_on = (
|
|
4168
|
-
data.get("follow_on_exec")
|
|
4169
|
-
if isinstance(data, dict)
|
|
4170
|
-
else None
|
|
4171
|
-
)
|
|
4172
4175
|
if adapter._set_spinner and not clarification_pending:
|
|
4173
4176
|
if follow_on:
|
|
4174
4177
|
adapter._plan_approve_follow_on_pending = True
|
|
@@ -69,7 +69,7 @@ def build_keyboard_shortcut_rows() -> list[tuple[str, str]]:
|
|
|
69
69
|
"Toggle plan panel (shown by default; --no-plan-panel disables auto-show)",
|
|
70
70
|
),
|
|
71
71
|
("Ctrl+O", "Toggle expand/collapse of the most recent skill or tool card"),
|
|
72
|
-
("Shift+Tab", "Cycle composer mode (Auto → Manual → Plan → Ask)"),
|
|
72
|
+
("Shift+Tab", "Cycle composer mode (Auto → Bypass → Manual → Plan → Ask)"),
|
|
73
73
|
("@filename", "Autocomplete files and inject content"),
|
|
74
74
|
("/command", "Slash commands (e.g. /help, /clear, /quit)"),
|
|
75
75
|
("!command", "Run shell commands directly"),
|
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
from collections.abc import Callable
|
|
6
6
|
from time import monotonic
|
|
7
|
-
from typing import TYPE_CHECKING
|
|
7
|
+
from typing import TYPE_CHECKING, Any
|
|
8
8
|
|
|
9
9
|
from textual.content import Content
|
|
10
10
|
from textual.widgets import Static
|
|
@@ -305,3 +305,73 @@ class LoadingWidget(Static):
|
|
|
305
305
|
def stop(self) -> None:
|
|
306
306
|
"""Stop the animation (widget will be removed by caller)."""
|
|
307
307
|
self._stop_timer()
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
class TipRow(Static):
|
|
311
|
+
"""Rotating tip / transient notice shown in the thinking row when idle.
|
|
312
|
+
|
|
313
|
+
Sits in the same ``#thinking-status`` container as :class:`LoadingWidget`.
|
|
314
|
+
When the loop is inactive (no spinner) the tip is visible; when the loop
|
|
315
|
+
is active the spinner replaces it and this row is hidden by the caller.
|
|
316
|
+
"""
|
|
317
|
+
|
|
318
|
+
DEFAULT_CSS = """
|
|
319
|
+
TipRow {
|
|
320
|
+
height: 1;
|
|
321
|
+
padding: 0 1;
|
|
322
|
+
margin-top: 1;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
TipRow.notification {
|
|
326
|
+
color: $warning;
|
|
327
|
+
text-style: bold;
|
|
328
|
+
}
|
|
329
|
+
"""
|
|
330
|
+
|
|
331
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
332
|
+
"""Initialize the tip row with no content."""
|
|
333
|
+
super().__init__(*args, **kwargs)
|
|
334
|
+
self._is_notification = False
|
|
335
|
+
self._tip_text: str = ""
|
|
336
|
+
|
|
337
|
+
def _render_tip(self, text: str) -> Content:
|
|
338
|
+
colors = theme.get_theme_colors(self)
|
|
339
|
+
prefix = Content.styled("💡 ", colors.muted)
|
|
340
|
+
body = Content.styled(text, colors.muted)
|
|
341
|
+
return Content.assemble(prefix, body)
|
|
342
|
+
|
|
343
|
+
def _render_notice(self, text: str) -> Content:
|
|
344
|
+
colors = theme.get_theme_colors(self)
|
|
345
|
+
prefix = Content.styled("⚠ ", colors.warning)
|
|
346
|
+
body = Content.styled(text, colors.warning)
|
|
347
|
+
return Content.assemble(prefix, body)
|
|
348
|
+
|
|
349
|
+
def _refresh_content(self) -> None:
|
|
350
|
+
"""Re-render the stored tip/notice text, styled only when mounted."""
|
|
351
|
+
if not self.is_mounted:
|
|
352
|
+
return
|
|
353
|
+
text = self._tip_text
|
|
354
|
+
if not text:
|
|
355
|
+
self.update("")
|
|
356
|
+
elif self._is_notification:
|
|
357
|
+
self.update(self._render_notice(text))
|
|
358
|
+
else:
|
|
359
|
+
self.update(self._render_tip(text))
|
|
360
|
+
|
|
361
|
+
def on_mount(self) -> None:
|
|
362
|
+
"""Render the initial tip content once the widget is in the DOM."""
|
|
363
|
+
self._refresh_content()
|
|
364
|
+
|
|
365
|
+
def set_tip(self, text: str) -> None:
|
|
366
|
+
"""Show a default rotating tip (muted, non-bold)."""
|
|
367
|
+
self._is_notification = False
|
|
368
|
+
self.remove_class("notification")
|
|
369
|
+
self._tip_text = (text or "").strip()
|
|
370
|
+
self._refresh_content()
|
|
371
|
+
|
|
372
|
+
def set_notification(self, text: str) -> None:
|
|
373
|
+
"""Show a transient notice (warning-colored, bold)."""
|
|
374
|
+
self._is_notification = True
|
|
375
|
+
self.add_class("notification")
|
|
376
|
+
self._tip_text = (text or "").strip()
|
|
377
|
+
self._refresh_content()
|
{soothe_cli-1.0.1 → soothe_cli-1.0.4}/src/soothe_cli/tui/widgets/messages/cognition_goal_tree.py
RENAMED
|
@@ -343,9 +343,20 @@ class CognitionGoalTreeMessage(Vertical):
|
|
|
343
343
|
return f"↑{format_token_count(total_in)} ↓{format_token_count(total_out)}"
|
|
344
344
|
|
|
345
345
|
def mark_loop_started(self, started_at: float | None = None) -> None:
|
|
346
|
-
"""Anchor plan-level elapsed time (matches thinking-row turn start).
|
|
347
|
-
|
|
346
|
+
"""Anchor plan-level elapsed time (matches thinking-row turn start).
|
|
347
|
+
|
|
348
|
+
On reuse after a terminal footer (plan-approve → exec follow-on),
|
|
349
|
+
clear the stale footer + step rows so the panel returns to live state.
|
|
350
|
+
"""
|
|
351
|
+
if self._loop_started_at is None or self._footer_visible:
|
|
348
352
|
self._loop_started_at = started_at if started_at is not None else time()
|
|
353
|
+
if self._footer_visible:
|
|
354
|
+
self._footer_visible = False
|
|
355
|
+
self._footer_plain = ""
|
|
356
|
+
self._footer_tone = "muted"
|
|
357
|
+
self._step_order.clear()
|
|
358
|
+
self._steps.clear()
|
|
359
|
+
self._sync_goal_tree_widgets()
|
|
349
360
|
|
|
350
361
|
def _indent_prefix(self) -> str:
|
|
351
362
|
"""Body gutter aligned to the right of the goal header prefix dot.
|