soothe-cli 0.11.0__tar.gz → 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/PKG-INFO +2 -2
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/pyproject.toml +1 -1
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/cli/main.py +2 -2
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/config/cli_config.py +2 -2
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/app/__init__.py +3 -7
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/app/_app.py +7 -7
- soothe_cli-1.0.0/src/soothe_cli/tui/app/_clarification.py +41 -0
- soothe_cli-1.0.0/src/soothe_cli/tui/app/_entrypoints.py +95 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/app/_execution.py +6 -40
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/app/_history.py +1 -1
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/app/_messages_mixin.py +26 -17
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/app/_model.py +2 -2
- soothe_cli-1.0.0/src/soothe_cli/tui/app/_model_params.py +97 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/app/_startup.py +1 -1
- soothe_cli-1.0.0/src/soothe_cli/tui/app/_terminal.py +75 -0
- soothe_cli-1.0.0/src/soothe_cli/tui/app/_theme_prefs.py +91 -0
- soothe_cli-1.0.0/src/soothe_cli/tui/app/_types.py +112 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/widgets/help_screen.py +1 -1
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/widgets/messages/structured_ask_user.py +209 -168
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/widgets/plan_quick_view_overlay.py +2 -2
- soothe_cli-0.11.0/src/soothe_cli/tui/app/_module_init.py +0 -435
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/.gitignore +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/README.md +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/__init__.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/_cli_context.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/_env_vars.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/_version.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/card_wire.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/cli/__init__.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/cli/commands/__init__.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/cli/commands/autopilot_cmd.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/cli/commands/config_cmd.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/cli/commands/cron_cmd.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/cli/commands/loop_cmd.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/cli/commands/run_cmd.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/cli/commands/status_cmd.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/cli/execution/__init__.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/cli/execution/daemon.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/cli/execution/daemon_errors.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/cli/execution/headless.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/cli/execution/headless_renderer.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/cli/execution/launcher.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/cli/soothecli.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/commands/__init__.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/commands/binding.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/commands/command_registry.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/commands/command_router.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/commands/slash_commands.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/commands/subagent_routing.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/config/__init__.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/config/loader.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/config/logging_setup.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/display/__init__.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/display/markdown_theme.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/display/media_utils.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/display/mermaid_render.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/display/path_utils.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/display/preview_limits.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/display/shell_color.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/display/spinner_labels.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/display/theme.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/display/tips.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/display/tool_display.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/display/unicode_security.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/loops/__init__.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/loops/sessions.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/model_config.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/project_utils.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/runtime/__init__.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/runtime/headless/processor.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/runtime/headless/processor_state.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/runtime/parse/tool_call_resolution.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/runtime/policy/tui_trace_log.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/runtime/presentation/duration_format.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/runtime/presentation/engine.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/runtime/presentation/id_format.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/runtime/presentation/renderer_base.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/runtime/presentation/renderer_protocol.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/runtime/presentation/step_id_format.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/runtime/presentation/subagent_task_display.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/runtime/state/file_tracker.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/runtime/state/session_stats.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/runtime/state/step_router.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/runtime/state/stream_accumulator.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/runtime/token_events_debug.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/runtime/token_usage.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/runtime/transport/session.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/runtime/turn/prepare.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/runtime/wire/chunk_filter.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/runtime/wire/message_text.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/runtime/wire/messages.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/settings/__init__.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/settings/_console_impl.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/settings/bootstrap.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/settings/core.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/settings/glyphs.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/settings/provider.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/settings/shell_allow.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/settings/skills_dirs.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/settings/stream_config.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/__init__.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/app/_ui.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/app/app.tcss +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/composer_mode.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/file_change_notify.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/file_change_renderers.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/input.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/skills/__init__.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/skills/invocation.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/skills/load.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/textual_adapter.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/widgets/__init__.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/widgets/_links.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/widgets/autocomplete.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/widgets/chat_input.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/widgets/clipboard.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/widgets/context_data.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/widgets/context_viewer.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/widgets/diff.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/widgets/editor.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/widgets/file_change_preview.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/widgets/history.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/widgets/loading.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/widgets/loop_selector.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/widgets/mcp_viewer.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/widgets/message_store.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/widgets/messages/__init__.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/widgets/messages/_helpers.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/widgets/messages/app.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/widgets/messages/assistant.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/widgets/messages/cognition_goal_tree.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/widgets/messages/cognition_reason.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/widgets/messages/cognition_step.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/widgets/messages/cognition_step_activity.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/widgets/messages/diff_message.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/widgets/messages/error.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/widgets/messages/skill.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/widgets/messages/user.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/widgets/model_selector.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/widgets/notification_settings.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/widgets/pinned_goal_bar.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/widgets/router_profile_selector.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/widgets/status.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/widgets/theme_selector.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/tui/widgets/welcome.py +0 -0
- {soothe_cli-0.11.0 → soothe_cli-1.0.0}/src/soothe_cli/update_check.py +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: soothe-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 1.0.0
|
|
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
|
|
7
7
|
Project-URL: Repository, https://github.com/mirasoth/soothe
|
|
8
8
|
License: MIT
|
|
9
9
|
Keywords: cli,client,soothe,tui,websocket
|
|
10
|
-
Classifier: Development Status ::
|
|
10
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
11
11
|
Classifier: Intended Audience :: Developers
|
|
12
12
|
Classifier: License :: OSI Approved :: MIT License
|
|
13
13
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -11,7 +11,7 @@ license = { text = "MIT" }
|
|
|
11
11
|
requires-python = ">=3.11,<3.14"
|
|
12
12
|
keywords = ["soothe", "cli", "client", "tui", "websocket"]
|
|
13
13
|
classifiers = [
|
|
14
|
-
"Development Status ::
|
|
14
|
+
"Development Status :: 5 - Production/Stable",
|
|
15
15
|
"Intended Audience :: Developers",
|
|
16
16
|
"License :: OSI Approved :: MIT License",
|
|
17
17
|
"Programming Language :: Python :: 3",
|
|
@@ -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: enabled."
|
|
202
202
|
),
|
|
203
203
|
),
|
|
204
|
-
] =
|
|
204
|
+
] = True,
|
|
205
205
|
show_version: Annotated[ # noqa: FBT002
|
|
206
206
|
bool,
|
|
207
207
|
typer.Option("--version", help="Show version and exit."),
|
|
@@ -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 = True
|
|
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 True (Ctrl+t to close)."""
|
|
39
39
|
|
|
40
40
|
# Output streaming overrides (RFC-614)
|
|
41
41
|
output_streaming_enabled: bool | None = None
|
|
@@ -1,13 +1,9 @@
|
|
|
1
1
|
"""Public API for the app sub-package."""
|
|
2
2
|
|
|
3
3
|
from soothe_cli.tui.app._app import SootheApp
|
|
4
|
-
from soothe_cli.tui.app.
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
run_textual_app,
|
|
8
|
-
run_textual_tui,
|
|
9
|
-
save_theme_preference,
|
|
10
|
-
)
|
|
4
|
+
from soothe_cli.tui.app._entrypoints import run_textual_app, run_textual_tui
|
|
5
|
+
from soothe_cli.tui.app._theme_prefs import save_theme_preference
|
|
6
|
+
from soothe_cli.tui.app._types import AppResult, TextualSessionState
|
|
11
7
|
|
|
12
8
|
__all__ = [
|
|
13
9
|
"SootheApp",
|
|
@@ -30,13 +30,13 @@ from soothe_cli.tui.app._execution import _ExecutionMixin
|
|
|
30
30
|
from soothe_cli.tui.app._history import _HistoryMixin
|
|
31
31
|
from soothe_cli.tui.app._messages_mixin import _MessagesMixin
|
|
32
32
|
from soothe_cli.tui.app._model import _ModelMixin
|
|
33
|
-
from soothe_cli.tui.app.
|
|
33
|
+
from soothe_cli.tui.app._startup import _StartupMixin
|
|
34
|
+
from soothe_cli.tui.app._theme_prefs import _load_theme_preference
|
|
35
|
+
from soothe_cli.tui.app._types import (
|
|
34
36
|
DeferredAction,
|
|
35
37
|
QueuedMessage,
|
|
36
38
|
TextualSessionState,
|
|
37
|
-
_load_theme_preference,
|
|
38
39
|
)
|
|
39
|
-
from soothe_cli.tui.app._startup import _StartupMixin
|
|
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
|
|
@@ -486,10 +486,10 @@ class SootheApp(
|
|
|
486
486
|
)
|
|
487
487
|
yield Container(id="messages")
|
|
488
488
|
# In-flow plan panel: sits above the sticky bottom chrome so Ctrl+t
|
|
489
|
-
# never covers the thinking row or chat input.
|
|
490
|
-
#
|
|
491
|
-
#
|
|
492
|
-
plan_visible = getattr(self._daemon_config, "plan_panel_default_visible",
|
|
489
|
+
# never covers the thinking row or chat input. Auto-shows while a goal
|
|
490
|
+
# is executing when CLIConfig.plan_panel_default_visible is True
|
|
491
|
+
# (default); --no-plan-panel disables it.
|
|
492
|
+
plan_visible = getattr(self._daemon_config, "plan_panel_default_visible", True)
|
|
493
493
|
yield PlanQuickViewOverlay(
|
|
494
494
|
id="plan-quick-view-overlay",
|
|
495
495
|
default_visible=plan_visible,
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Clarification wire-content formatting for plan-review and tool-approval submits.
|
|
2
|
+
|
|
3
|
+
Pure utility — no mixin state dependency. Extracted from `_execution.py` to
|
|
4
|
+
keep the execution mixin focused on agent execution and message routing.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
_PLAN_REVIEW_ACTIONS = frozenset({"Approve", "Reject", "Refine"})
|
|
10
|
+
_TOOL_APPROVAL_ACTIONS = frozenset({"Approve", "Reject", "Edit"})
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def clarification_wire_content(answers: list[str], *, origin_node: str = "") -> str:
|
|
14
|
+
"""Human-readable turn content for a clarification submit (not a new goal).
|
|
15
|
+
|
|
16
|
+
Option-selector actions use a stable `<prefix>: …` header so a dropped
|
|
17
|
+
`clarification_answer` flag cannot turn a bare action into Pass1 TASK.
|
|
18
|
+
Plan review → `Plan review:` (Refine carries refinement text in
|
|
19
|
+
`answers[1]`). Tool approval → `Tool approval:` (Edit carries revised
|
|
20
|
+
instructions in `answers[1]`). When `origin_node` is empty (legacy
|
|
21
|
+
callers) the action label alone selects the prefix, preserving the safety
|
|
22
|
+
net for any selector-origin submit.
|
|
23
|
+
"""
|
|
24
|
+
non_empty = [a for a in answers if str(a).strip()]
|
|
25
|
+
if not non_empty:
|
|
26
|
+
return ""
|
|
27
|
+
first = str(answers[0]).strip() if answers else ""
|
|
28
|
+
is_tool_approval = origin_node == "tool_approval"
|
|
29
|
+
is_selector_action = first in _PLAN_REVIEW_ACTIONS or first in _TOOL_APPROVAL_ACTIONS
|
|
30
|
+
if is_selector_action:
|
|
31
|
+
refinement = str(answers[1]).strip() if len(answers) > 1 else ""
|
|
32
|
+
# Origin-aware prefix; when origin is unknown (legacy caller), default
|
|
33
|
+
# to "Plan review" to preserve the pre-RFC behavior where the action
|
|
34
|
+
# label alone selected the plan-review prefix.
|
|
35
|
+
prefix = "Tool approval" if is_tool_approval else "Plan review"
|
|
36
|
+
if refinement:
|
|
37
|
+
return f"{prefix}: {first} — {refinement}"
|
|
38
|
+
return f"{prefix}: {first}"
|
|
39
|
+
if len(non_empty) == 1:
|
|
40
|
+
return non_empty[0]
|
|
41
|
+
return " | ".join(f"A{i + 1}: {a}" for i, a in enumerate(answers) if str(a).strip())
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""App entry points and slash-command URL constants.
|
|
2
|
+
|
|
3
|
+
`run_textual_app` and `run_textual_tui` are the public async and sync entry
|
|
4
|
+
points for launching the Textual TUI. `_COMMAND_URLS` maps slash-commands
|
|
5
|
+
that just open a browser to their target URLs.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import os
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from soothe_cli._version import CHANGELOG_URL, DOCS_URL
|
|
16
|
+
from soothe_cli.tui.app._terminal import init_terminal_integration
|
|
17
|
+
from soothe_cli.tui.app._types import AppResult
|
|
18
|
+
|
|
19
|
+
_COMMAND_URLS: dict[str, str] = {
|
|
20
|
+
"/changelog": CHANGELOG_URL,
|
|
21
|
+
"/docs": DOCS_URL,
|
|
22
|
+
"/feedback": "https://github.com/mirasoth/soothe/issues/new/choose",
|
|
23
|
+
}
|
|
24
|
+
"""Slash-command to URL mapping for commands that just open a browser."""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
async def run_textual_app(
|
|
28
|
+
*,
|
|
29
|
+
daemon_config: Any, # noqa: ANN401
|
|
30
|
+
assistant_id: str | None = None,
|
|
31
|
+
cwd: str | Path | None = None,
|
|
32
|
+
resume_loop_id: str | None = None,
|
|
33
|
+
initial_prompt: str | None = None,
|
|
34
|
+
initial_skill: str | None = None,
|
|
35
|
+
mcp_server_info: list[dict[str, Any]] | None = None,
|
|
36
|
+
profile_override: dict[str, Any] | None = None,
|
|
37
|
+
) -> AppResult:
|
|
38
|
+
"""Run the Textual TUI (daemon execution only)."""
|
|
39
|
+
from soothe_cli.tui.app._app import SootheApp # deferred to avoid circular import
|
|
40
|
+
|
|
41
|
+
# Disable iTerm2 cursor guide before Textual takes over the terminal.
|
|
42
|
+
# Previously this was an import-time side effect; now explicit.
|
|
43
|
+
init_terminal_integration()
|
|
44
|
+
|
|
45
|
+
app = SootheApp(
|
|
46
|
+
daemon_config=daemon_config,
|
|
47
|
+
assistant_id=assistant_id,
|
|
48
|
+
cwd=cwd,
|
|
49
|
+
resume_loop_id=resume_loop_id,
|
|
50
|
+
initial_prompt=initial_prompt,
|
|
51
|
+
initial_skill=initial_skill,
|
|
52
|
+
mcp_server_info=mcp_server_info,
|
|
53
|
+
profile_override=profile_override,
|
|
54
|
+
)
|
|
55
|
+
try:
|
|
56
|
+
await app.run_async()
|
|
57
|
+
finally:
|
|
58
|
+
if app._daemon_session is not None:
|
|
59
|
+
from soothe_cli.runtime.transport.session import TUI_EXIT_HANDSHAKE_TIMEOUT_S
|
|
60
|
+
|
|
61
|
+
await app._daemon_session.close(handshake_timeout=TUI_EXIT_HANDSHAKE_TIMEOUT_S)
|
|
62
|
+
|
|
63
|
+
return AppResult(
|
|
64
|
+
return_code=app.return_code or 0,
|
|
65
|
+
loop_id=app._lc_loop_id,
|
|
66
|
+
session_stats=app._session_stats,
|
|
67
|
+
update_available=app._update_available,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def run_textual_tui(
|
|
72
|
+
config: Any, # noqa: ANN401
|
|
73
|
+
resume_loop_id: str | None = None,
|
|
74
|
+
initial_prompt: str | None = None,
|
|
75
|
+
) -> AppResult:
|
|
76
|
+
"""Launch the Textual TUI with optional loop attachment and initial prompt.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
config: Soothe configuration used for daemon-backed startup.
|
|
80
|
+
resume_loop_id: Loop id to attach to when starting the TUI
|
|
81
|
+
initial_prompt: Auto-submit prompt on launch
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
# Caller cwd is forwarded as the loop workspace hint.
|
|
85
|
+
# Daemon workspace is ephemeral TEMP unless SOOTHE_WORKSPACE env set.
|
|
86
|
+
cwd = os.getcwd()
|
|
87
|
+
|
|
88
|
+
return asyncio.run(
|
|
89
|
+
run_textual_app(
|
|
90
|
+
resume_loop_id=resume_loop_id,
|
|
91
|
+
initial_prompt=initial_prompt,
|
|
92
|
+
daemon_config=config,
|
|
93
|
+
cwd=cwd,
|
|
94
|
+
)
|
|
95
|
+
)
|
|
@@ -10,7 +10,7 @@ import sys
|
|
|
10
10
|
import time
|
|
11
11
|
import webbrowser
|
|
12
12
|
from contextlib import suppress
|
|
13
|
-
from typing import Any
|
|
13
|
+
from typing import Any
|
|
14
14
|
|
|
15
15
|
from textual.app import ScreenStackError
|
|
16
16
|
from textual.containers import VerticalScroll
|
|
@@ -30,11 +30,13 @@ from soothe_cli.cli.execution.daemon_errors import (
|
|
|
30
30
|
from soothe_cli.display import theme
|
|
31
31
|
from soothe_cli.display.shell_color import shell_subprocess_env, wrap_shell_command_for_color
|
|
32
32
|
from soothe_cli.runtime.state.session_stats import SessionStats
|
|
33
|
-
from soothe_cli.tui.app.
|
|
34
|
-
|
|
33
|
+
from soothe_cli.tui.app._clarification import clarification_wire_content
|
|
34
|
+
from soothe_cli.tui.app._entrypoints import _COMMAND_URLS
|
|
35
|
+
from soothe_cli.tui.app._model_params import _extract_model_params_flag
|
|
36
|
+
from soothe_cli.tui.app._types import (
|
|
35
37
|
DeferredAction,
|
|
38
|
+
InputMode,
|
|
36
39
|
QueuedMessage,
|
|
37
|
-
_extract_model_params_flag,
|
|
38
40
|
)
|
|
39
41
|
from soothe_cli.tui.widgets.chat_input import ChatInput
|
|
40
42
|
from soothe_cli.tui.widgets.messages import (
|
|
@@ -49,44 +51,8 @@ from soothe_cli.tui.widgets.welcome import WelcomeBanner
|
|
|
49
51
|
|
|
50
52
|
_monotonic = time.monotonic
|
|
51
53
|
|
|
52
|
-
InputMode = Literal["normal", "shell", "command"]
|
|
53
|
-
|
|
54
54
|
logger = logging.getLogger(__name__)
|
|
55
55
|
|
|
56
|
-
_PLAN_REVIEW_ACTIONS = frozenset({"Approve", "Reject", "Refine"})
|
|
57
|
-
_TOOL_APPROVAL_ACTIONS = frozenset({"Approve", "Reject", "Edit"})
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
def clarification_wire_content(answers: list[str], *, origin_node: str = "") -> str:
|
|
61
|
-
"""Human-readable turn content for a clarification submit (not a new goal).
|
|
62
|
-
|
|
63
|
-
Option-selector actions use a stable `<prefix>: …` header so a dropped
|
|
64
|
-
`clarification_answer` flag cannot turn a bare action into Pass1 TASK.
|
|
65
|
-
Plan review → `Plan review:` (Refine carries refinement text in
|
|
66
|
-
`answers[1]`). Tool approval → `Tool approval:` (Edit carries revised
|
|
67
|
-
instructions in `answers[1]`). When `origin_node` is empty (legacy
|
|
68
|
-
callers) the action label alone selects the prefix, preserving the safety
|
|
69
|
-
net for any selector-origin submit.
|
|
70
|
-
"""
|
|
71
|
-
non_empty = [a for a in answers if str(a).strip()]
|
|
72
|
-
if not non_empty:
|
|
73
|
-
return ""
|
|
74
|
-
first = str(answers[0]).strip() if answers else ""
|
|
75
|
-
is_tool_approval = origin_node == "tool_approval"
|
|
76
|
-
is_selector_action = first in _PLAN_REVIEW_ACTIONS or first in _TOOL_APPROVAL_ACTIONS
|
|
77
|
-
if is_selector_action:
|
|
78
|
-
refinement = str(answers[1]).strip() if len(answers) > 1 else ""
|
|
79
|
-
# Origin-aware prefix; when origin is unknown (legacy caller), default
|
|
80
|
-
# to "Plan review" to preserve the pre-RFC behavior where the action
|
|
81
|
-
# label alone selected the plan-review prefix.
|
|
82
|
-
prefix = "Tool approval" if is_tool_approval else "Plan review"
|
|
83
|
-
if refinement:
|
|
84
|
-
return f"{prefix}: {first} — {refinement}"
|
|
85
|
-
return f"{prefix}: {first}"
|
|
86
|
-
if len(non_empty) == 1:
|
|
87
|
-
return non_empty[0]
|
|
88
|
-
return " | ".join(f"A{i + 1}: {a}" for i, a in enumerate(answers) if str(a).strip())
|
|
89
|
-
|
|
90
56
|
|
|
91
57
|
class _ExecutionMixin:
|
|
92
58
|
"""Agent execution, message routing, queue, shell commands, and daemon events."""
|
|
@@ -11,7 +11,7 @@ from soothe_sdk.display import card_binder as _binder
|
|
|
11
11
|
from soothe_sdk.display.transcript_types import MessageData
|
|
12
12
|
from textual.content import Content
|
|
13
13
|
|
|
14
|
-
from soothe_cli.tui.app.
|
|
14
|
+
from soothe_cli.tui.app._types import _LoopHistoryPayload
|
|
15
15
|
from soothe_cli.tui.widgets.messages import AppMessage
|
|
16
16
|
|
|
17
17
|
logger = logging.getLogger(__name__)
|
|
@@ -17,11 +17,13 @@ from textual.app import App
|
|
|
17
17
|
from textual.containers import Container, VerticalScroll
|
|
18
18
|
from textual.css.query import NoMatches
|
|
19
19
|
|
|
20
|
-
from soothe_cli.tui.app.
|
|
20
|
+
from soothe_cli.tui.app._terminal import (
|
|
21
21
|
_ITERM_CURSOR_GUIDE_ON,
|
|
22
|
+
_write_iterm_escape,
|
|
23
|
+
)
|
|
24
|
+
from soothe_cli.tui.app._types import (
|
|
22
25
|
DeferredAction,
|
|
23
26
|
_LoopHistoryPayload,
|
|
24
|
-
_write_iterm_escape,
|
|
25
27
|
)
|
|
26
28
|
from soothe_cli.tui.widgets.file_change_preview import FileChangePreviewWidget
|
|
27
29
|
from soothe_cli.tui.widgets.messages import (
|
|
@@ -1212,15 +1214,15 @@ class _MessagesMixin:
|
|
|
1212
1214
|
def on_app_focus(self) -> None:
|
|
1213
1215
|
"""Restore primary input focus when the terminal regains OS focus.
|
|
1214
1216
|
|
|
1215
|
-
|
|
1216
|
-
the
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
focusable widget (e.g., a step card control) currently owns focus, so
|
|
1220
|
-
the user does not lose an in-progress answer to a tab-out and back.
|
|
1217
|
+
Fires when focus returns from an external program (e.g. a browser
|
|
1218
|
+
link). Re-focuses the primary input unless a modal is open, another
|
|
1219
|
+
focusable widget owns focus, or an unsubmitted QA card is active
|
|
1220
|
+
(its own focus guard handles recapture).
|
|
1221
1221
|
"""
|
|
1222
1222
|
if self.screen.is_modal:
|
|
1223
1223
|
return
|
|
1224
|
+
if self._has_active_qa_widget():
|
|
1225
|
+
return
|
|
1224
1226
|
focused = self.focused
|
|
1225
1227
|
primary = self._primary_text_input()
|
|
1226
1228
|
if primary is not None:
|
|
@@ -1235,15 +1237,12 @@ class _MessagesMixin:
|
|
|
1235
1237
|
self._chat_input.focus_input()
|
|
1236
1238
|
|
|
1237
1239
|
def on_click(self, _event: Click) -> None:
|
|
1238
|
-
"""Focus the chat input when
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
click that Textual just used to focus them. Skip the refocus whenever
|
|
1245
|
-
the click landed on a focusable widget — Textual's default focus
|
|
1246
|
-
handling already does the right thing there.
|
|
1240
|
+
"""Focus the chat input when a click lands on non-focusable chrome.
|
|
1241
|
+
|
|
1242
|
+
Bubbles for every click, so an unconditional refocus would steal
|
|
1243
|
+
focus from inline focusable widgets. Skip when the click target is
|
|
1244
|
+
focusable, a text selection is active, or an unsubmitted QA card is
|
|
1245
|
+
on screen (scrolling to review a plan must not yank focus).
|
|
1247
1246
|
"""
|
|
1248
1247
|
if not self._chat_input:
|
|
1249
1248
|
return
|
|
@@ -1254,6 +1253,8 @@ class _MessagesMixin:
|
|
|
1254
1253
|
return
|
|
1255
1254
|
if self._click_landed_on_focusable(_event):
|
|
1256
1255
|
return
|
|
1256
|
+
if self._has_active_qa_widget():
|
|
1257
|
+
return
|
|
1257
1258
|
self.call_after_refresh(self.focus_primary_input)
|
|
1258
1259
|
|
|
1259
1260
|
def _click_landed_on_focusable(self, event: Click) -> bool:
|
|
@@ -1277,6 +1278,14 @@ class _MessagesMixin:
|
|
|
1277
1278
|
node = getattr(node, "parent", None)
|
|
1278
1279
|
return False
|
|
1279
1280
|
|
|
1281
|
+
def _has_active_qa_widget(self) -> bool:
|
|
1282
|
+
"""Return True when an unsubmitted StructuredAskUserWidget is mounted."""
|
|
1283
|
+
adapter = getattr(self, "_ui_adapter", None)
|
|
1284
|
+
if adapter is None:
|
|
1285
|
+
return False
|
|
1286
|
+
by_step = getattr(adapter, "_clarification_input_by_step", None) or {}
|
|
1287
|
+
return any(not getattr(w, "_submitted", False) for w in by_step.values())
|
|
1288
|
+
|
|
1280
1289
|
def on_text_selected(self, _event: TextSelected) -> None:
|
|
1281
1290
|
"""Copy selected transcript text on mouse release.
|
|
1282
1291
|
|
|
@@ -17,9 +17,9 @@ from textual.css.query import NoMatches
|
|
|
17
17
|
from textual.theme import Theme
|
|
18
18
|
|
|
19
19
|
from soothe_cli.display import theme
|
|
20
|
-
from soothe_cli.tui.app.
|
|
20
|
+
from soothe_cli.tui.app._theme_prefs import save_theme_preference
|
|
21
|
+
from soothe_cli.tui.app._types import (
|
|
21
22
|
DeferredAction,
|
|
22
|
-
save_theme_preference,
|
|
23
23
|
)
|
|
24
24
|
from soothe_cli.tui.widgets.messages import AppMessage, ErrorMessage
|
|
25
25
|
from soothe_cli.tui.widgets.welcome import WelcomeBanner
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Parser for the `--model-params` flag in `/model` slash-command arguments."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import shlex
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _extract_model_params_flag(raw_arg: str) -> tuple[str, dict[str, Any] | None]:
|
|
11
|
+
"""Extract `--model-params` and its JSON value from a `/model` arg string.
|
|
12
|
+
|
|
13
|
+
Handles quoted (`'...'` / `"..."`) and bare `{...}` values with balanced
|
|
14
|
+
braces so that JSON containing spaces works without quoting.
|
|
15
|
+
|
|
16
|
+
Note:
|
|
17
|
+
The bare-brace mode counts `{` / `}` characters without awareness of
|
|
18
|
+
JSON string contents. Values that contain literal braces inside strings
|
|
19
|
+
(e.g., `{"stop": "end}here"}`) will mis-parse. Users should quote the
|
|
20
|
+
value in that case.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
raw_arg: The argument string after `/model `.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
Tuple of `(remaining_args, parsed_dict | None)`. Returns `None` for the
|
|
27
|
+
dict when the flag is absent.
|
|
28
|
+
|
|
29
|
+
Raises:
|
|
30
|
+
ValueError: If the value is missing, has unclosed quotes,
|
|
31
|
+
unbalanced braces, or is not valid JSON.
|
|
32
|
+
TypeError: If the parsed JSON is not a dict.
|
|
33
|
+
"""
|
|
34
|
+
flag = "--model-params"
|
|
35
|
+
idx = raw_arg.find(flag)
|
|
36
|
+
if idx == -1:
|
|
37
|
+
return raw_arg, None
|
|
38
|
+
|
|
39
|
+
before = raw_arg[:idx].rstrip()
|
|
40
|
+
after = raw_arg[idx + len(flag) :].lstrip()
|
|
41
|
+
|
|
42
|
+
if not after:
|
|
43
|
+
msg = "--model-params requires a JSON object value"
|
|
44
|
+
raise ValueError(msg)
|
|
45
|
+
|
|
46
|
+
# Determine the JSON string boundaries.
|
|
47
|
+
if after[0] in {"'", '"'}:
|
|
48
|
+
quote = after[0]
|
|
49
|
+
end = -1
|
|
50
|
+
backslash_count = 0
|
|
51
|
+
for i, ch in enumerate(after[1:], start=1):
|
|
52
|
+
if ch == "\\":
|
|
53
|
+
backslash_count += 1
|
|
54
|
+
continue
|
|
55
|
+
if ch == quote and backslash_count % 2 == 0:
|
|
56
|
+
end = i
|
|
57
|
+
break
|
|
58
|
+
backslash_count = 0
|
|
59
|
+
if end == -1:
|
|
60
|
+
msg = f"Unclosed {quote} in --model-params value"
|
|
61
|
+
raise ValueError(msg)
|
|
62
|
+
# Parse the quoted token with shlex so escaped quotes are unescaped.
|
|
63
|
+
json_str = shlex.split(after[: end + 1], posix=True)[0]
|
|
64
|
+
rest = after[end + 1 :].lstrip()
|
|
65
|
+
elif after[0] == "{":
|
|
66
|
+
# Walk forward to find the matching closing brace.
|
|
67
|
+
depth = 0
|
|
68
|
+
end = -1
|
|
69
|
+
for i, ch in enumerate(after):
|
|
70
|
+
if ch == "{":
|
|
71
|
+
depth += 1
|
|
72
|
+
elif ch == "}":
|
|
73
|
+
depth -= 1
|
|
74
|
+
if depth == 0:
|
|
75
|
+
end = i
|
|
76
|
+
break
|
|
77
|
+
if end == -1:
|
|
78
|
+
msg = "Unbalanced braces in --model-params value"
|
|
79
|
+
raise ValueError(msg)
|
|
80
|
+
json_str = after[: end + 1]
|
|
81
|
+
rest = after[end + 1 :].lstrip()
|
|
82
|
+
else:
|
|
83
|
+
# Non-brace, non-quoted — take the next whitespace-delimited token.
|
|
84
|
+
parts = after.split(None, 1)
|
|
85
|
+
json_str = parts[0]
|
|
86
|
+
rest = parts[1] if len(parts) > 1 else ""
|
|
87
|
+
|
|
88
|
+
remaining = f"{before} {rest}".strip()
|
|
89
|
+
try:
|
|
90
|
+
params = json.loads(json_str)
|
|
91
|
+
except json.JSONDecodeError:
|
|
92
|
+
msg = f'Invalid JSON in --model-params: {json_str!r}. Expected format: --model-params \'{{"key": "value"}}\''
|
|
93
|
+
raise ValueError(msg) from None
|
|
94
|
+
if not isinstance(params, dict):
|
|
95
|
+
msg = "--model-params must be a JSON object, got " + type(params).__name__
|
|
96
|
+
raise TypeError(msg)
|
|
97
|
+
return remaining, params
|
|
@@ -20,7 +20,7 @@ from textual.containers import VerticalScroll
|
|
|
20
20
|
from textual.css.query import NoMatches
|
|
21
21
|
|
|
22
22
|
from soothe_cli._version import CHANGELOG_URL
|
|
23
|
-
from soothe_cli.tui.app.
|
|
23
|
+
from soothe_cli.tui.app._types import (
|
|
24
24
|
TextualSessionState,
|
|
25
25
|
)
|
|
26
26
|
from soothe_cli.tui.widgets.chat_input import ChatInput
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""iTerm2 terminal integration: cursor guide escape sequences and lifecycle.
|
|
2
|
+
|
|
3
|
+
The cursor guide (highlight cursor line) causes visual artifacts when Textual
|
|
4
|
+
takes over the terminal in alternate screen mode. We disable it before the app
|
|
5
|
+
launches and restore on exit. Both atexit and exit() override are used for
|
|
6
|
+
defense-in-depth: atexit catches abnormal termination (SIGTERM, unhandled
|
|
7
|
+
exceptions), while exit() ensures restoration before Textual's cleanup.
|
|
8
|
+
|
|
9
|
+
Unlike the previous catch-all module, this module performs **no** side effects
|
|
10
|
+
at import time. Call `init_terminal_integration()` explicitly before starting
|
|
11
|
+
the Textual app.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import os
|
|
17
|
+
|
|
18
|
+
# Detection: check env vars AND that stderr is a TTY (avoids false positives
|
|
19
|
+
# when env vars are inherited but running in non-TTY context like CI)
|
|
20
|
+
_IS_ITERM = (
|
|
21
|
+
(
|
|
22
|
+
os.environ.get("LC_TERMINAL", "") == "iTerm2"
|
|
23
|
+
or os.environ.get("TERM_PROGRAM", "") == "iTerm.app"
|
|
24
|
+
)
|
|
25
|
+
and hasattr(os, "isatty")
|
|
26
|
+
and os.isatty(2)
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# iTerm2 cursor guide escape sequences (OSC 1337)
|
|
30
|
+
# Format: OSC 1337 ; HighlightCursorLine=<yes|no> ST
|
|
31
|
+
# Where OSC = ESC ] (0x1b 0x5d) and ST = ESC \ (0x1b 0x5c)
|
|
32
|
+
_ITERM_CURSOR_GUIDE_OFF = "\x1b]1337;HighlightCursorLine=no\x1b\\"
|
|
33
|
+
_ITERM_CURSOR_GUIDE_ON = "\x1b]1337;HighlightCursorLine=yes\x1b\\"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _write_iterm_escape(sequence: str) -> None:
|
|
37
|
+
"""Write an iTerm2 escape sequence to stderr.
|
|
38
|
+
|
|
39
|
+
Silently fails if the terminal is unavailable (redirected, closed, broken
|
|
40
|
+
pipe). This is a cosmetic feature, so failures should never crash the app.
|
|
41
|
+
"""
|
|
42
|
+
if not _IS_ITERM:
|
|
43
|
+
return
|
|
44
|
+
try:
|
|
45
|
+
import sys
|
|
46
|
+
|
|
47
|
+
if sys.__stderr__ is not None:
|
|
48
|
+
sys.__stderr__.write(sequence)
|
|
49
|
+
sys.__stderr__.flush()
|
|
50
|
+
except OSError:
|
|
51
|
+
# Terminal may be unavailable (redirected, closed, broken pipe)
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _restore_cursor_guide() -> None:
|
|
56
|
+
"""Restore iTerm2 cursor guide on exit.
|
|
57
|
+
|
|
58
|
+
Registered with atexit to ensure the cursor guide is re-enabled
|
|
59
|
+
when the CLI exits, regardless of how the exit occurs.
|
|
60
|
+
"""
|
|
61
|
+
_write_iterm_escape(_ITERM_CURSOR_GUIDE_ON)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def init_terminal_integration() -> None:
|
|
65
|
+
"""Disable the iTerm2 cursor guide and register restore-on-exit.
|
|
66
|
+
|
|
67
|
+
Call this explicitly before starting the Textual app. This replaces the
|
|
68
|
+
previous import-time side effect so that merely importing the module does
|
|
69
|
+
not write to the terminal.
|
|
70
|
+
"""
|
|
71
|
+
_write_iterm_escape(_ITERM_CURSOR_GUIDE_OFF)
|
|
72
|
+
if _IS_ITERM:
|
|
73
|
+
import atexit
|
|
74
|
+
|
|
75
|
+
atexit.register(_restore_cursor_guide)
|