soothe-cli 0.5.1__tar.gz → 0.5.3__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.5.1 → soothe_cli-0.5.3}/PKG-INFO +1 -1
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/cli/commands/run_cmd.py +25 -7
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/cli/main.py +19 -4
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/config/cli_config.py +1 -1
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/tui/_env_vars.py +4 -1
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/tui/app/_app.py +16 -96
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/tui/app/_commands.py +27 -30
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/tui/app/_execution.py +13 -50
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/tui/app/_history.py +144 -89
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/tui/app/_messages_mixin.py +4 -4
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/tui/app/_model.py +27 -79
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/tui/app/_module_init.py +6 -46
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/tui/app/_startup.py +33 -240
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/tui/config.py +10 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/tui/daemon_session.py +9 -4
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/tui/textual_adapter/_turn.py +60 -68
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/tui/textual_adapter/_turn_helpers.py +56 -47
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/tui/update_check.py +46 -15
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/tui/widgets/message_store.py +14 -14
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/tui/widgets/messages.py +135 -118
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/tui/widgets/welcome.py +30 -39
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/.gitignore +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/README.md +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/pyproject.toml +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/__init__.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/cli/__init__.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/cli/commands/__init__.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/cli/commands/autopilot_cmd.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/cli/commands/loop_cmd.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/cli/execution/__init__.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/cli/execution/daemon.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/cli/execution/headless.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/cli/execution/headless_renderer.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/cli/execution/launcher.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/cli/stream/__init__.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/cli/stream/context.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/cli/stream/display_line.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/cli/stream/formatter.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/cli/stream/pipeline.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/cli/stream/task_scope.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/config/__init__.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/plan/__init__.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/plan/rich_tree.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/shared/__init__.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/shared/commands/__init__.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/shared/commands/command_router.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/shared/commands/slash_commands.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/shared/commands/subagent_routing.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/shared/config_loader.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/shared/core/__init__.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/shared/core/event_processor.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/shared/core/presentation_engine.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/shared/core/processor_state.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/shared/core/renderer_protocol.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/shared/duration_format.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/shared/events/__init__.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/shared/events/display_policy.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/shared/events/essential_events.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/shared/events/explore_task_display.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/shared/events/stream_accumulator.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/shared/events/tui_trace_log.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/shared/rendering/__init__.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/shared/rendering/async_renderer_protocol.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/shared/rendering/renderer_base.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/shared/tools/__init__.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/shared/tools/_utils.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/shared/tools/message_processing.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/shared/tools/rendering.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/shared/tools/tool_call_resolution.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/shared/tools/tool_card_payload.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/shared/tools/tool_card_visibility.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/shared/tools/tool_formatters/__init__.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/shared/tools/tool_formatters/base.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/shared/tools/tool_formatters/execution.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/shared/tools/tool_formatters/fallback.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/shared/tools/tool_formatters/file_ops.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/shared/tools/tool_formatters/goal_formatter.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/shared/tools/tool_formatters/media.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/shared/tools/tool_formatters/structured.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/shared/tools/tool_formatters/subagent.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/shared/tools/tool_formatters/web.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/shared/tools/tool_message_format.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/shared/tools/tool_output_formatter.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/tui/__init__.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/tui/_ask_user_types.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/tui/_cli_context.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/tui/_session_stats.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/tui/_version.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/tui/app/__init__.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/tui/app/_ui.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/tui/app/app.tcss +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/tui/command_registry.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/tui/file_ops.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/tui/formatting.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/tui/hooks.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/tui/input.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/tui/media_utils.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/tui/message_display_filter.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/tui/model_config.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/tui/output.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/tui/preview_limits.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/tui/project_utils.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/tui/sessions.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/tui/skills/__init__.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/tui/skills/invocation.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/tui/skills/load.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/tui/textual_adapter/__init__.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/tui/textual_adapter/_adapter.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/tui/textual_adapter/_stream_formatting.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/tui/textual_adapter/_stream_messages.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/tui/theme.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/tui/tool_display.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/tui/unicode_security.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/tui/widgets/__init__.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/tui/widgets/_links.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/tui/widgets/approval.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/tui/widgets/ask_user.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/tui/widgets/autocomplete.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/tui/widgets/autopilot_dashboard.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/tui/widgets/autopilot_screen.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/tui/widgets/chat_input.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/tui/widgets/clipboard.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/tui/widgets/diff.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/tui/widgets/editor.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/tui/widgets/history.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/tui/widgets/loading.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/tui/widgets/loop_selector.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/tui/widgets/mcp_viewer.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/tui/widgets/model_selector.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/tui/widgets/notification_settings.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/tui/widgets/status.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/tui/widgets/theme_selector.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/tui/widgets/tool_renderers.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/tui/widgets/tool_widgets.py +0 -0
- {soothe_cli-0.5.1 → soothe_cli-0.5.3}/src/soothe_cli/tui/widgets/tools.py +0 -0
|
@@ -24,25 +24,29 @@ def run_impl(
|
|
|
24
24
|
streaming_enabled: bool | None = None,
|
|
25
25
|
streaming_mode: str | None = None,
|
|
26
26
|
*,
|
|
27
|
+
tui_with_prompt: bool = False,
|
|
27
28
|
config_path: str | None = None,
|
|
28
29
|
) -> None:
|
|
29
30
|
"""Core implementation for running Soothe agent.
|
|
30
31
|
|
|
31
32
|
Args:
|
|
32
|
-
prompt: Optional prompt
|
|
33
|
+
prompt: Optional user message; non-empty prompt defaults to a headless
|
|
34
|
+
one-shot run unless ``tui_with_prompt`` is set or a loop is being
|
|
35
|
+
resumed (``resume_loop_id``).
|
|
33
36
|
resume_loop_id: Existing loop id to attach to (optional)
|
|
34
|
-
no_tui:
|
|
37
|
+
no_tui: Require headless mode (must include a non-empty prompt)
|
|
35
38
|
autonomous: Enable autonomous iteration mode
|
|
36
39
|
max_iterations: Max iterations for autonomous mode
|
|
37
40
|
streaming_enabled: Override daemon streaming enabled setting (RFC-614)
|
|
38
41
|
streaming_mode: Override daemon streaming mode ('streaming' or 'batch')
|
|
42
|
+
tui_with_prompt: When True with a prompt, open the TUI instead of headless.
|
|
39
43
|
"""
|
|
40
44
|
startup_start = time.perf_counter()
|
|
41
45
|
|
|
42
46
|
try:
|
|
43
47
|
cfg = load_config(config_path)
|
|
44
48
|
log_level = resolve_cli_log_level(logging_level=cfg.logging_level)
|
|
45
|
-
log_file = Path(SOOTHE_HOME) / "logs" / "
|
|
49
|
+
log_file = Path(SOOTHE_HOME) / "logs" / "cli.log"
|
|
46
50
|
setup_logging(log_level, log_file=log_file)
|
|
47
51
|
|
|
48
52
|
# PostgreSQL availability check (requires daemon-side config)
|
|
@@ -62,17 +66,31 @@ def run_impl(
|
|
|
62
66
|
|
|
63
67
|
run_start = time.perf_counter()
|
|
64
68
|
|
|
65
|
-
|
|
66
|
-
|
|
69
|
+
has_prompt = bool(prompt and str(prompt).strip())
|
|
70
|
+
attaching_loop = bool(resume_loop_id and str(resume_loop_id).strip())
|
|
71
|
+
|
|
72
|
+
if tui_with_prompt and has_prompt:
|
|
73
|
+
use_headless = False
|
|
74
|
+
elif no_tui and not has_prompt:
|
|
75
|
+
typer.echo(
|
|
76
|
+
"Error: --no-tui requires a non-empty --prompt (-p).",
|
|
77
|
+
err=True,
|
|
78
|
+
)
|
|
79
|
+
sys.exit(1)
|
|
80
|
+
elif no_tui:
|
|
81
|
+
use_headless = True
|
|
82
|
+
else:
|
|
83
|
+
use_headless = has_prompt and not attaching_loop
|
|
84
|
+
|
|
85
|
+
if use_headless:
|
|
67
86
|
run_headless(
|
|
68
87
|
cfg,
|
|
69
|
-
prompt
|
|
88
|
+
str(prompt).strip(),
|
|
70
89
|
resume_loop_id=resume_loop_id,
|
|
71
90
|
autonomous=autonomous,
|
|
72
91
|
max_iterations=max_iterations,
|
|
73
92
|
)
|
|
74
93
|
else:
|
|
75
|
-
# TUI mode (with optional initial prompt)
|
|
76
94
|
run_tui(cfg, resume_loop_id=resume_loop_id, initial_prompt=prompt)
|
|
77
95
|
|
|
78
96
|
run_elapsed_s = time.perf_counter() - run_start
|
|
@@ -55,12 +55,24 @@ def main(
|
|
|
55
55
|
prompt: Annotated[
|
|
56
56
|
str | None,
|
|
57
57
|
typer.Option(
|
|
58
|
-
"--prompt",
|
|
58
|
+
"--prompt",
|
|
59
|
+
"-p",
|
|
60
|
+
help="User message; runs a one-shot headless query by default (use --tui for TUI).",
|
|
59
61
|
),
|
|
60
62
|
] = None,
|
|
61
63
|
no_tui: Annotated[ # noqa: FBT002
|
|
62
64
|
bool,
|
|
63
|
-
typer.Option(
|
|
65
|
+
typer.Option(
|
|
66
|
+
"--no-tui",
|
|
67
|
+
help="Headless mode (requires --prompt). Same as default when -p is set.",
|
|
68
|
+
),
|
|
69
|
+
] = False,
|
|
70
|
+
tui_with_prompt: Annotated[ # noqa: FBT002
|
|
71
|
+
bool,
|
|
72
|
+
typer.Option(
|
|
73
|
+
"--tui",
|
|
74
|
+
help="With --prompt/-p, open the interactive TUI and auto-submit the prompt.",
|
|
75
|
+
),
|
|
64
76
|
] = False,
|
|
65
77
|
streaming: Annotated[
|
|
66
78
|
bool | None,
|
|
@@ -81,13 +93,15 @@ def main(
|
|
|
81
93
|
) -> None:
|
|
82
94
|
"""Soothe CLI - Intelligent AI assistant client.
|
|
83
95
|
|
|
84
|
-
Run without arguments for interactive TUI mode, or
|
|
96
|
+
Run without arguments for interactive TUI mode, or pass --prompt for a one-shot
|
|
97
|
+
headless query (stdout, then exit).
|
|
85
98
|
|
|
86
99
|
Note: This is the CLI client. Use 'soothed' command to manage the daemon server.
|
|
87
100
|
|
|
88
101
|
Examples:
|
|
89
102
|
soothe # Interactive TUI mode
|
|
90
|
-
soothe -p "Research AI advances" #
|
|
103
|
+
soothe -p "Research AI advances" # One-shot headless (non-TUI) query
|
|
104
|
+
soothe -p "Hello" --tui # TUI with an auto-submitted prompt
|
|
91
105
|
soothe loop list # List AgentLoop instances
|
|
92
106
|
"""
|
|
93
107
|
# Handle -h/--help flag
|
|
@@ -112,6 +126,7 @@ def main(
|
|
|
112
126
|
max_iterations=None,
|
|
113
127
|
streaming_enabled=streaming,
|
|
114
128
|
streaming_mode=streaming_mode,
|
|
129
|
+
tui_with_prompt=tui_with_prompt,
|
|
115
130
|
)
|
|
116
131
|
|
|
117
132
|
|
|
@@ -24,7 +24,7 @@ class CLIConfig:
|
|
|
24
24
|
daemon_host: str = "127.0.0.1"
|
|
25
25
|
daemon_port: int = 8765
|
|
26
26
|
|
|
27
|
-
# logging_level: DEBUG/INFO/… for ~/.soothe/logs/
|
|
27
|
+
# logging_level: DEBUG/INFO/… for ~/.soothe/logs/cli.log; None = default INFO.
|
|
28
28
|
logging_level: str | None = None
|
|
29
29
|
|
|
30
30
|
# Output streaming overrides (RFC-614)
|
|
@@ -29,7 +29,7 @@ from __future__ import annotations
|
|
|
29
29
|
# ---------------------------------------------------------------------------
|
|
30
30
|
|
|
31
31
|
AUTO_UPDATE = "SOOTHE_CLI_AUTO_UPDATE"
|
|
32
|
-
"""
|
|
32
|
+
"""Override automatic CLI updates: '1'/'true'/'yes' on, '0'/'false'/'no' off. On by default when unset."""
|
|
33
33
|
|
|
34
34
|
DEBUG = "SOOTHE_CLI_DEBUG"
|
|
35
35
|
"""Enable verbose debug logging to a file."""
|
|
@@ -43,6 +43,9 @@ EXTRA_SKILLS_DIRS = "SOOTHE_CLI_EXTRA_SKILLS_DIRS"
|
|
|
43
43
|
NO_UPDATE_CHECK = "SOOTHE_CLI_NO_UPDATE_CHECK"
|
|
44
44
|
"""Disable automatic update checking when set."""
|
|
45
45
|
|
|
46
|
+
UPDATE_CHECK = "SOOTHE_CLI_UPDATE_CHECK"
|
|
47
|
+
"""Force-enable startup PyPI update checks ('1', 'true', or 'yes'). On by default."""
|
|
48
|
+
|
|
46
49
|
SERVER_ENV_PREFIX = "SOOTHE_CLI_SERVER_"
|
|
47
50
|
"""Environment variable prefix used to pass CLI config to the server subprocess."""
|
|
48
51
|
|
|
@@ -10,7 +10,6 @@ from pathlib import Path
|
|
|
10
10
|
from typing import TYPE_CHECKING, Any, ClassVar
|
|
11
11
|
|
|
12
12
|
if TYPE_CHECKING:
|
|
13
|
-
from langgraph.pregel import Pregel
|
|
14
13
|
from textual.app import ComposeResult
|
|
15
14
|
from textual.worker import Worker
|
|
16
15
|
|
|
@@ -133,22 +132,8 @@ class SootheApp(
|
|
|
133
132
|
"""App-level keybindings for interrupt, quit, toggles, and approval menu
|
|
134
133
|
navigation."""
|
|
135
134
|
|
|
136
|
-
class ServerReady(Message):
|
|
137
|
-
"""Posted by the background server-startup worker on success."""
|
|
138
|
-
|
|
139
|
-
def __init__( # noqa: D107
|
|
140
|
-
self,
|
|
141
|
-
agent: Any, # noqa: ANN401
|
|
142
|
-
server_proc: Any, # noqa: ANN401
|
|
143
|
-
mcp_server_info: list[Any] | None,
|
|
144
|
-
) -> None:
|
|
145
|
-
super().__init__()
|
|
146
|
-
self.agent = agent
|
|
147
|
-
self.server_proc = server_proc
|
|
148
|
-
self.mcp_server_info = mcp_server_info
|
|
149
|
-
|
|
150
135
|
class ServerStartFailed(Message):
|
|
151
|
-
"""Posted
|
|
136
|
+
"""Posted when daemon bootstrap or background connection fails."""
|
|
152
137
|
|
|
153
138
|
def __init__(self, error: Exception) -> None: # noqa: D107
|
|
154
139
|
super().__init__()
|
|
@@ -165,63 +150,30 @@ class SootheApp(
|
|
|
165
150
|
def __init__(
|
|
166
151
|
self,
|
|
167
152
|
*,
|
|
168
|
-
|
|
153
|
+
daemon_config: Any,
|
|
169
154
|
assistant_id: str | None = None,
|
|
170
155
|
auto_approve: bool = False,
|
|
171
156
|
cwd: str | Path | None = None,
|
|
172
157
|
resume_loop_id: str | None = None,
|
|
173
|
-
resume_loop_intent: str | None = None,
|
|
174
158
|
initial_prompt: str | None = None,
|
|
175
159
|
initial_skill: str | None = None,
|
|
176
160
|
mcp_server_info: list[dict[str, Any]] | None = None,
|
|
177
161
|
profile_override: dict[str, Any] | None = None,
|
|
178
|
-
server_proc: Any | None = None,
|
|
179
|
-
server_kwargs: dict[str, Any] | None = None,
|
|
180
|
-
mcp_preload_kwargs: dict[str, Any] | None = None,
|
|
181
|
-
model_kwargs: dict[str, Any] | None = None,
|
|
182
|
-
daemon_config: Any | None = None,
|
|
183
162
|
**kwargs: Any,
|
|
184
163
|
) -> None:
|
|
185
|
-
"""Initialize the
|
|
164
|
+
"""Initialize the Textual application (daemon-backed execution only).
|
|
186
165
|
|
|
187
166
|
Args:
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
`None` when `resume_loop_intent` is provided (resolved asynchronously).
|
|
196
|
-
resume_loop_intent: Raw resume intent from `-r` flag.
|
|
197
|
-
|
|
198
|
-
`'__MOST_RECENT__'` for bare `-r`, a loop id for
|
|
199
|
-
`-r <id>`, or `None` for new sessions.
|
|
200
|
-
|
|
201
|
-
Resolved via `_resolve_resume_loop_intent`
|
|
202
|
-
during `_start_server_background`.
|
|
203
|
-
|
|
204
|
-
Requires `server_kwargs` to be set; ignored otherwise.
|
|
205
|
-
initial_prompt: Optional prompt to auto-submit when session starts
|
|
167
|
+
daemon_config: Loaded Soothe configuration (WebSocket URL, etc.).
|
|
168
|
+
assistant_id: Agent identifier for memory storage.
|
|
169
|
+
auto_approve: Whether to start with auto-approve enabled.
|
|
170
|
+
cwd: Current working directory to display.
|
|
171
|
+
resume_loop_id: Initial AgentLoop id when attaching to an existing loop.
|
|
172
|
+
initial_prompt: Optional prompt to auto-submit when session starts.
|
|
206
173
|
initial_skill: Optional skill name to invoke when session starts.
|
|
207
174
|
mcp_server_info: MCP server metadata for the `/mcp` viewer.
|
|
208
|
-
profile_override: Extra profile fields from
|
|
209
|
-
|
|
210
|
-
the CLI override, including model selection details and
|
|
211
|
-
on-demand `create_model()` calls.
|
|
212
|
-
server_proc: LangGraph server process for the interactive session.
|
|
213
|
-
server_kwargs: When provided, server startup is deferred.
|
|
214
|
-
|
|
215
|
-
The app shows a "Connecting..." state and starts the server in
|
|
216
|
-
the background using these kwargs
|
|
217
|
-
for `start_server_and_get_agent`.
|
|
218
|
-
mcp_preload_kwargs: Kwargs for `_preload_session_mcp_server_info`,
|
|
219
|
-
run concurrently with server startup when `server_kwargs` is set.
|
|
220
|
-
model_kwargs: Kwargs for deferred `create_model()`.
|
|
221
|
-
|
|
222
|
-
When provided, model creation runs in a background worker after
|
|
223
|
-
first paint instead of blocking startup.
|
|
224
|
-
**kwargs: Additional arguments passed to parent
|
|
175
|
+
profile_override: Extra profile fields from ``--profile-override``.
|
|
176
|
+
**kwargs: Additional arguments passed to the Textual ``App``.
|
|
225
177
|
"""
|
|
226
178
|
super().__init__(**kwargs)
|
|
227
179
|
|
|
@@ -230,8 +182,6 @@ class SootheApp(
|
|
|
230
182
|
# Apply saved theme preference (or default)
|
|
231
183
|
self.theme = _load_theme_preference()
|
|
232
184
|
|
|
233
|
-
self._agent = agent
|
|
234
|
-
|
|
235
185
|
self._assistant_id = assistant_id
|
|
236
186
|
|
|
237
187
|
self._auto_approve = auto_approve
|
|
@@ -242,8 +192,6 @@ class SootheApp(
|
|
|
242
192
|
# Named `_lc_loop_id` to avoid colliding with Textual's App._thread_id.
|
|
243
193
|
self._lc_loop_id = resume_loop_id
|
|
244
194
|
|
|
245
|
-
self._resume_loop_intent = resume_loop_intent
|
|
246
|
-
|
|
247
195
|
self._initial_prompt = initial_prompt
|
|
248
196
|
|
|
249
197
|
self._initial_skill = (
|
|
@@ -254,14 +202,6 @@ class SootheApp(
|
|
|
254
202
|
|
|
255
203
|
self._profile_override = profile_override
|
|
256
204
|
|
|
257
|
-
self._server_proc = server_proc
|
|
258
|
-
|
|
259
|
-
self._server_kwargs = server_kwargs
|
|
260
|
-
|
|
261
|
-
self._mcp_preload_kwargs = mcp_preload_kwargs
|
|
262
|
-
|
|
263
|
-
self._model_kwargs = model_kwargs
|
|
264
|
-
|
|
265
205
|
self._daemon_config = daemon_config
|
|
266
206
|
|
|
267
207
|
self._daemon_session: Any | None = None
|
|
@@ -269,14 +209,9 @@ class SootheApp(
|
|
|
269
209
|
self._daemon_skills_wire: list[dict[str, Any]] = []
|
|
270
210
|
"""Cached ``skills_list_response`` rows when the TUI uses ``TuiDaemonSession``."""
|
|
271
211
|
|
|
272
|
-
self._connecting =
|
|
273
|
-
# Extract sandbox type from server kwargs for trace metadata.
|
|
274
|
-
# ServerConfig.__post_init__ normalizes "none" → None, but server_kwargs carries
|
|
275
|
-
# the raw argparse value, so guard against both.
|
|
212
|
+
self._connecting = True
|
|
276
213
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
self._sandbox_type: str | None = raw if raw and raw != "none" else None
|
|
214
|
+
self._sandbox_type: str | None = None
|
|
280
215
|
|
|
281
216
|
self._model_override: str | None = None
|
|
282
217
|
|
|
@@ -304,11 +239,7 @@ class SootheApp(
|
|
|
304
239
|
self._agent_running = False
|
|
305
240
|
|
|
306
241
|
self._server_startup_error: str | None = None
|
|
307
|
-
"""Set when
|
|
308
|
-
session lifetime (server failure is terminal).
|
|
309
|
-
|
|
310
|
-
Shown in place of the generic 'Agent not configured' message.
|
|
311
|
-
"""
|
|
242
|
+
"""Set when daemon bootstrap fails; persists for the session lifetime."""
|
|
312
243
|
|
|
313
244
|
self._shell_process: asyncio.subprocess.Process | None = None
|
|
314
245
|
"""Shell command process tracking for interruption (! commands)."""
|
|
@@ -391,18 +322,9 @@ class SootheApp(
|
|
|
391
322
|
|
|
392
323
|
self._image_tracker = MediaTracker()
|
|
393
324
|
|
|
394
|
-
def _remote_agent(self) -> Any: # noqa: ANN401
|
|
395
|
-
"""Return the agent if it appears to be a remote agent, or `None`.
|
|
396
|
-
|
|
397
|
-
Returns `None` when no agent is configured or the agent is a local graph.
|
|
398
|
-
"""
|
|
399
|
-
# RemoteAgent module doesn't exist in this package; always return None.
|
|
400
|
-
# When the SDK provides a RemoteAgent class, this can be re-implemented.
|
|
401
|
-
return None
|
|
402
|
-
|
|
403
325
|
def _runtime_backend_ready(self) -> bool:
|
|
404
|
-
"""Return whether the app has a
|
|
405
|
-
return self._daemon_session is not None
|
|
326
|
+
"""Return whether the app has a connected daemon session."""
|
|
327
|
+
return self._daemon_session is not None
|
|
406
328
|
|
|
407
329
|
def get_theme_variable_defaults(self) -> dict[str, str]:
|
|
408
330
|
"""Return custom CSS variable defaults for the current theme.
|
|
@@ -434,8 +356,6 @@ class SootheApp(
|
|
|
434
356
|
mcp_tool_count=self._mcp_tool_count,
|
|
435
357
|
workspace_path=self._cwd,
|
|
436
358
|
connecting=self._connecting,
|
|
437
|
-
resuming=self._resume_loop_intent is not None,
|
|
438
|
-
local_server=self._server_kwargs is not None,
|
|
439
359
|
id="welcome-banner",
|
|
440
360
|
)
|
|
441
361
|
yield Container(id="messages")
|
|
@@ -9,7 +9,6 @@ from contextlib import suppress
|
|
|
9
9
|
from typing import TYPE_CHECKING, Any
|
|
10
10
|
|
|
11
11
|
if TYPE_CHECKING:
|
|
12
|
-
from langchain_core.runnables import RunnableConfig
|
|
13
12
|
from textual.content import Content
|
|
14
13
|
|
|
15
14
|
from textual.app import ScreenStackError
|
|
@@ -191,24 +190,25 @@ class _CommandsMixin:
|
|
|
191
190
|
self._update_tokens(0)
|
|
192
191
|
# Clear status message (e.g., "Interrupted" from previous session)
|
|
193
192
|
self._update_status("")
|
|
194
|
-
# New AgentLoop (daemon) or new local loop id
|
|
195
193
|
if self._session_state:
|
|
196
|
-
if self._daemon_session is
|
|
194
|
+
if self._daemon_session is None:
|
|
195
|
+
await self._mount_message(
|
|
196
|
+
AppMessage("Not connected to the daemon; cannot start a new loop.")
|
|
197
|
+
)
|
|
198
|
+
else:
|
|
197
199
|
status_event = await self._daemon_session.new_loop()
|
|
198
200
|
new_loop_id = (
|
|
199
201
|
str(status_event.get("loop_id", "")) or self._session_state.reset_loop()
|
|
200
202
|
)
|
|
201
203
|
self._session_state.loop_id = new_loop_id
|
|
202
204
|
self._lc_loop_id = new_loop_id
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
self._clear_loop_model_override()
|
|
211
|
-
await self._mount_message(AppMessage(f"Started new loop: {new_loop_id}"))
|
|
205
|
+
try:
|
|
206
|
+
banner = self.query_one("#welcome-banner", WelcomeBanner)
|
|
207
|
+
banner.update_loop_id(new_loop_id)
|
|
208
|
+
except NoMatches:
|
|
209
|
+
pass
|
|
210
|
+
self._clear_loop_model_override()
|
|
211
|
+
await self._mount_message(AppMessage(f"Started new loop: {new_loop_id}"))
|
|
212
212
|
elif cmd == "/editor":
|
|
213
213
|
await self.action_open_editor()
|
|
214
214
|
elif cmd == "/loops":
|
|
@@ -367,13 +367,6 @@ class _CommandsMixin:
|
|
|
367
367
|
report += "\nTheme registry reload failed. Check config.yml for errors."
|
|
368
368
|
await self._mount_message(AppMessage(report))
|
|
369
369
|
|
|
370
|
-
# Re-discover skills so autocomplete reflects any new/removed skills
|
|
371
|
-
if self._daemon_config is None:
|
|
372
|
-
self.run_worker(
|
|
373
|
-
self._discover_skills(),
|
|
374
|
-
exclusive=True,
|
|
375
|
-
group="startup-skill-discovery",
|
|
376
|
-
)
|
|
377
370
|
if self._daemon_session is not None:
|
|
378
371
|
self.run_worker(
|
|
379
372
|
self._refresh_daemon_skills_catalog(),
|
|
@@ -424,22 +417,26 @@ class _CommandsMixin:
|
|
|
424
417
|
Returns:
|
|
425
418
|
Token count as an integer, or `None` if state is unavailable.
|
|
426
419
|
"""
|
|
427
|
-
if not self.
|
|
420
|
+
if not self._lc_loop_id:
|
|
428
421
|
return None
|
|
429
422
|
try:
|
|
430
|
-
from langchain_core.messages
|
|
431
|
-
|
|
432
|
-
)
|
|
423
|
+
from langchain_core.messages import messages_from_dict
|
|
424
|
+
from langchain_core.messages.utils import count_tokens_approximately
|
|
433
425
|
|
|
434
|
-
|
|
435
|
-
"configurable": {"thread_id": self._lc_loop_id},
|
|
436
|
-
}
|
|
437
|
-
state = await self._agent.aget_state(config)
|
|
438
|
-
if not state or not state.values:
|
|
426
|
+
if self._daemon_session is None:
|
|
439
427
|
return None
|
|
440
|
-
|
|
441
|
-
|
|
428
|
+
snap = await self._daemon_session.aget_loop_state(self._lc_loop_id)
|
|
429
|
+
vals = getattr(snap, "values", None)
|
|
430
|
+
if not isinstance(vals, dict):
|
|
442
431
|
return None
|
|
432
|
+
raw = vals.get("messages")
|
|
433
|
+
if not isinstance(raw, list) or not raw:
|
|
434
|
+
return None
|
|
435
|
+
if isinstance(raw[0], dict):
|
|
436
|
+
messages = messages_from_dict(raw)
|
|
437
|
+
else:
|
|
438
|
+
messages = raw
|
|
439
|
+
|
|
443
440
|
return count_tokens_approximately(messages)
|
|
444
441
|
except Exception: # best-effort for /tokens display
|
|
445
442
|
logger.debug("Failed to retrieve conversation token count", exc_info=True)
|
|
@@ -10,10 +10,7 @@ import sys
|
|
|
10
10
|
import time
|
|
11
11
|
import webbrowser
|
|
12
12
|
from contextlib import suppress
|
|
13
|
-
from typing import
|
|
14
|
-
|
|
15
|
-
if TYPE_CHECKING:
|
|
16
|
-
from langchain_core.runnables import RunnableConfig
|
|
13
|
+
from typing import Any, Literal
|
|
17
14
|
|
|
18
15
|
from textual.app import ScreenStackError
|
|
19
16
|
from textual.containers import VerticalScroll
|
|
@@ -532,24 +529,25 @@ class _ExecutionMixin:
|
|
|
532
529
|
self._update_tokens(0)
|
|
533
530
|
# Clear status message (e.g., "Interrupted" from previous session)
|
|
534
531
|
self._update_status("")
|
|
535
|
-
# New AgentLoop (daemon) or new local loop id
|
|
536
532
|
if self._session_state:
|
|
537
|
-
if self._daemon_session is
|
|
533
|
+
if self._daemon_session is None:
|
|
534
|
+
await self._mount_message(
|
|
535
|
+
AppMessage("Not connected to the daemon; cannot start a new loop.")
|
|
536
|
+
)
|
|
537
|
+
else:
|
|
538
538
|
status_event = await self._daemon_session.new_loop()
|
|
539
539
|
new_loop_id = (
|
|
540
540
|
str(status_event.get("loop_id", "")) or self._session_state.reset_loop()
|
|
541
541
|
)
|
|
542
542
|
self._session_state.loop_id = new_loop_id
|
|
543
543
|
self._lc_loop_id = new_loop_id
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
self._clear_loop_model_override()
|
|
552
|
-
await self._mount_message(AppMessage(f"Started new loop: {new_loop_id}"))
|
|
544
|
+
try:
|
|
545
|
+
banner = self.query_one("#welcome-banner", WelcomeBanner)
|
|
546
|
+
banner.update_loop_id(new_loop_id)
|
|
547
|
+
except NoMatches:
|
|
548
|
+
pass
|
|
549
|
+
self._clear_loop_model_override()
|
|
550
|
+
await self._mount_message(AppMessage(f"Started new loop: {new_loop_id}"))
|
|
553
551
|
elif cmd == "/editor":
|
|
554
552
|
await self.action_open_editor()
|
|
555
553
|
elif cmd == "/loops":
|
|
@@ -708,13 +706,6 @@ class _ExecutionMixin:
|
|
|
708
706
|
report += "\nTheme registry reload failed. Check config.yml for errors."
|
|
709
707
|
await self._mount_message(AppMessage(report))
|
|
710
708
|
|
|
711
|
-
# Re-discover skills so autocomplete reflects any new/removed skills
|
|
712
|
-
if self._daemon_config is None:
|
|
713
|
-
self.run_worker(
|
|
714
|
-
self._discover_skills(),
|
|
715
|
-
exclusive=True,
|
|
716
|
-
group="startup-skill-discovery",
|
|
717
|
-
)
|
|
718
709
|
if self._daemon_session is not None:
|
|
719
710
|
self.run_worker(
|
|
720
711
|
self._refresh_daemon_skills_catalog(),
|
|
@@ -759,33 +750,6 @@ class _ExecutionMixin:
|
|
|
759
750
|
AppMessage("Skills require a daemon connection. Connect to a daemon first.")
|
|
760
751
|
)
|
|
761
752
|
|
|
762
|
-
async def _get_conversation_token_count(self) -> int | None:
|
|
763
|
-
"""Return the approximate conversation-only token count.
|
|
764
|
-
|
|
765
|
-
Returns:
|
|
766
|
-
Token count as an integer, or `None` if state is unavailable.
|
|
767
|
-
"""
|
|
768
|
-
if not self._agent:
|
|
769
|
-
return None
|
|
770
|
-
try:
|
|
771
|
-
from langchain_core.messages.utils import (
|
|
772
|
-
count_tokens_approximately,
|
|
773
|
-
)
|
|
774
|
-
|
|
775
|
-
config: RunnableConfig = {
|
|
776
|
-
"configurable": {"thread_id": self._lc_loop_id},
|
|
777
|
-
}
|
|
778
|
-
state = await self._agent.aget_state(config)
|
|
779
|
-
if not state or not state.values:
|
|
780
|
-
return None
|
|
781
|
-
messages = state.values.get("messages", [])
|
|
782
|
-
if not messages:
|
|
783
|
-
return None
|
|
784
|
-
return count_tokens_approximately(messages)
|
|
785
|
-
except Exception: # best-effort for /tokens display
|
|
786
|
-
logger.debug("Failed to retrieve conversation token count", exc_info=True)
|
|
787
|
-
return None
|
|
788
|
-
|
|
789
753
|
async def _handle_user_message(self, message: str) -> None:
|
|
790
754
|
"""Handle a user message to send to the agent.
|
|
791
755
|
|
|
@@ -878,7 +842,6 @@ class _ExecutionMixin:
|
|
|
878
842
|
try:
|
|
879
843
|
await execute_task_textual(
|
|
880
844
|
user_input=message,
|
|
881
|
-
agent=self._agent,
|
|
882
845
|
daemon_session=self._daemon_session,
|
|
883
846
|
assistant_id=self._assistant_id,
|
|
884
847
|
session_state=self._session_state,
|