soothe-cli 0.7.2__tar.gz → 0.7.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.7.2 → soothe_cli-0.7.3}/PKG-INFO +1 -1
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/cli/commands/autopilot_cmd.py +1 -31
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/cli/commands/cron_cmd.py +13 -4
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/cli/commands/status_cmd.py +26 -55
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/cli/execution/daemon.py +2 -2
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/runtime/state/session_stats.py +89 -44
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/_env_vars.py +28 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/app/_app.py +12 -2
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/app/_execution.py +11 -31
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/app/_messages_mixin.py +120 -43
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/app/_model.py +15 -1
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/app/_startup.py +29 -23
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/app/_ui.py +28 -4
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/app/app.tcss +13 -1
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/command_registry.py +2 -8
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/commands/slash_commands.py +37 -10
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/commands/subagent_routing.py +3 -8
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/file_change_notify.py +23 -1
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/file_change_renderers.py +41 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/preview_limits.py +4 -4
- soothe_cli-0.7.3/src/soothe_cli/tui/shell_color.py +48 -0
- soothe_cli-0.7.3/src/soothe_cli/tui/spinner_labels.py +55 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/textual_adapter.py +397 -106
- soothe_cli-0.7.3/src/soothe_cli/tui/widgets/diff.py +228 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/file_change_preview.py +111 -85
- soothe_cli-0.7.3/src/soothe_cli/tui/widgets/help_screen.py +210 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/loading.py +35 -10
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/messages/assistant.py +8 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/messages/clarification.py +6 -14
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/messages/cognition_goal_tree.py +126 -24
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/messages/cognition_step.py +5 -2
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/messages/cognition_step_activity.py +7 -0
- soothe_cli-0.7.3/src/soothe_cli/tui/widgets/plan_quick_view_overlay.py +150 -0
- soothe_cli-0.7.2/src/soothe_cli/tui/widgets/diff.py +0 -143
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/.gitignore +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/README.md +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/pyproject.toml +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/__init__.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/cli/__init__.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/cli/commands/__init__.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/cli/commands/loop_cmd.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/cli/commands/run_cmd.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/cli/execution/__init__.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/cli/execution/daemon_errors.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/cli/execution/headless.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/cli/execution/headless_renderer.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/cli/execution/launcher.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/cli/main.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/config/__init__.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/config/cli_config.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/config/loader.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/config/logging_setup.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/runtime/__init__.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/runtime/headless/processor.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/runtime/headless/processor_state.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/runtime/parse/message_processing.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/runtime/parse/tool_call_resolution.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/runtime/parse/tool_result.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/runtime/policy/display_policy.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/runtime/policy/essential_events.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/runtime/policy/tui_trace_log.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/runtime/presentation/duration_format.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/runtime/presentation/engine.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/runtime/presentation/explore_task_display.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/runtime/presentation/renderer_base.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/runtime/presentation/renderer_protocol.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/runtime/state/file_tracker.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/runtime/state/step_router.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/runtime/state/stream_accumulator.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/runtime/state/transcript.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/runtime/transport/session.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/runtime/turn/pipeline.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/runtime/turn/prepare.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/runtime/wire/chunk_filter.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/runtime/wire/display_text.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/runtime/wire/message_text.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/runtime/wire/messages.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/__init__.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/_cli_context.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/_version.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/app/__init__.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/app/_commands.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/app/_history.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/app/_module_init.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/binding.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/commands/__init__.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/commands/command_router.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/config.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/hooks.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/input.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/markdown_theme.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/media_utils.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/model_config.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/path_utils.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/project_utils.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/sessions.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/skills/__init__.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/skills/invocation.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/skills/load.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/theme.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/tips.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/tool_display.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/unicode_security.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/update_check.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/__init__.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/_links.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/autocomplete.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/autopilot_dashboard.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/autopilot_screen.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/chat_input.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/clipboard.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/context_viewer.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/editor.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/history.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/loop_selector.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/mcp_viewer.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/message_store.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/messages/__init__.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/messages/_helpers.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/messages/app.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/messages/cognition_reason.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/messages/cognition_subagent.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/messages/diff_message.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/messages/error.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/messages/skill.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/messages/user.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/model_selector.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/notification_settings.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/status.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/theme_selector.py +0 -0
- {soothe_cli-0.7.2 → soothe_cli-0.7.3}/src/soothe_cli/tui/widgets/welcome.py +0 -0
|
@@ -55,7 +55,7 @@ def run(
|
|
|
55
55
|
max_iterations: int | None = typer.Option(
|
|
56
56
|
None,
|
|
57
57
|
"--max-iterations",
|
|
58
|
-
help="Ignored — use daemon config agent.
|
|
58
|
+
help="Ignored — use daemon config agent.autopilot.max_iterations.",
|
|
59
59
|
),
|
|
60
60
|
workspace: str | None = typer.Option(
|
|
61
61
|
None,
|
|
@@ -333,36 +333,6 @@ def cancel_goal(
|
|
|
333
333
|
typer.echo(f"Cancel result: {result.get('status', result)}")
|
|
334
334
|
|
|
335
335
|
|
|
336
|
-
@app.command("approve")
|
|
337
|
-
def approve_goal(
|
|
338
|
-
goal_id: str = typer.Argument(
|
|
339
|
-
..., help="Confirmation ID to approve (use 'inbox' to list pending)."
|
|
340
|
-
),
|
|
341
|
-
) -> None:
|
|
342
|
-
"""Approve a MUST-confirmation goal."""
|
|
343
|
-
client = _require_daemon_ws()
|
|
344
|
-
try:
|
|
345
|
-
result = client.autopilot_approve(goal_id)
|
|
346
|
-
except RuntimeError as exc:
|
|
347
|
-
typer.echo(str(exc), err=True)
|
|
348
|
-
raise typer.Exit(1) from exc
|
|
349
|
-
typer.echo(f"Confirmation approved: {result.get('goal_id', goal_id)}")
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
@app.command("reject")
|
|
353
|
-
def reject_goal(
|
|
354
|
-
goal_id: str = typer.Argument(..., help="Confirmation ID to reject."),
|
|
355
|
-
) -> None:
|
|
356
|
-
"""Reject a proposed goal."""
|
|
357
|
-
client = _require_daemon_ws()
|
|
358
|
-
try:
|
|
359
|
-
result = client.autopilot_reject(goal_id)
|
|
360
|
-
except RuntimeError as exc:
|
|
361
|
-
typer.echo(str(exc), err=True)
|
|
362
|
-
raise typer.Exit(1) from exc
|
|
363
|
-
typer.echo(f"Confirmation rejected: {result.get('goal_id', goal_id)}")
|
|
364
|
-
|
|
365
|
-
|
|
366
336
|
@app.command("resume")
|
|
367
337
|
def resume_goal(
|
|
368
338
|
goal_id: str = typer.Argument(..., help="Goal ID to resume."),
|
|
@@ -85,11 +85,16 @@ def add_job(
|
|
|
85
85
|
try:
|
|
86
86
|
result = client.cron_add(text, priority=priority)
|
|
87
87
|
except RuntimeError as exc:
|
|
88
|
-
|
|
88
|
+
message = str(exc)
|
|
89
|
+
if "Autopilot is disabled" in message:
|
|
90
|
+
console.print(f"[yellow]{message}[/yellow]")
|
|
91
|
+
else:
|
|
92
|
+
typer.echo(f"Error: {message}", err=True)
|
|
89
93
|
sys.exit(1)
|
|
90
94
|
|
|
91
95
|
job = result.get("job") or {}
|
|
92
96
|
job_id = job.get("id", "?")
|
|
97
|
+
is_duplicate = bool(result.get("duplicate"))
|
|
93
98
|
console.print(
|
|
94
99
|
Panel(
|
|
95
100
|
"\n".join(
|
|
@@ -101,10 +106,14 @@ def add_job(
|
|
|
101
106
|
f"[bold]Status:[/bold] [green]{job.get('status', 'pending')}[/green]",
|
|
102
107
|
]
|
|
103
108
|
),
|
|
104
|
-
title="Scheduled Job Created",
|
|
105
|
-
border_style="green",
|
|
109
|
+
title="Scheduled Job Already Exists" if is_duplicate else "Scheduled Job Created",
|
|
110
|
+
border_style="yellow" if is_duplicate else "green",
|
|
106
111
|
)
|
|
107
112
|
)
|
|
113
|
+
if is_duplicate:
|
|
114
|
+
console.print(
|
|
115
|
+
"[yellow]An identical active job is already scheduled; no new job was created.[/yellow]"
|
|
116
|
+
)
|
|
108
117
|
|
|
109
118
|
|
|
110
119
|
@app.command("list")
|
|
@@ -128,7 +137,7 @@ def list_jobs(
|
|
|
128
137
|
"""
|
|
129
138
|
client = _require_cron_client()
|
|
130
139
|
try:
|
|
131
|
-
result = client.
|
|
140
|
+
result = client.cron_list(status=status)
|
|
132
141
|
except RuntimeError as exc:
|
|
133
142
|
typer.echo(f"Error: {exc}", err=True)
|
|
134
143
|
sys.exit(1)
|
|
@@ -50,47 +50,6 @@ async def _fetch_status(ws_url: str, timeout: float = 5.0) -> dict[str, Any]:
|
|
|
50
50
|
await client.close()
|
|
51
51
|
|
|
52
52
|
|
|
53
|
-
async def _fetch_ready_state(ws_url: str, timeout: float = 5.0) -> dict[str, Any] | None:
|
|
54
|
-
"""Fetch daemon readiness state via WebSocket connection_init/ack handshake.
|
|
55
|
-
|
|
56
|
-
Sends ``connection_init`` and waits for ``connection_ack`` which carries
|
|
57
|
-
the daemon's ``readiness_state``.
|
|
58
|
-
|
|
59
|
-
Args:
|
|
60
|
-
ws_url: WebSocket URL.
|
|
61
|
-
timeout: Timeout for handshake.
|
|
62
|
-
|
|
63
|
-
Returns:
|
|
64
|
-
``connection_ack`` result dict (containing ``readiness_state``) or None.
|
|
65
|
-
"""
|
|
66
|
-
import websockets
|
|
67
|
-
|
|
68
|
-
init_msg = json.dumps(
|
|
69
|
-
{
|
|
70
|
-
"proto": "1",
|
|
71
|
-
"type": "connection_init",
|
|
72
|
-
"params": {
|
|
73
|
-
"client_version": "0.5.0",
|
|
74
|
-
"accept_proto": ["1"],
|
|
75
|
-
"capabilities": ["streaming"],
|
|
76
|
-
},
|
|
77
|
-
}
|
|
78
|
-
)
|
|
79
|
-
|
|
80
|
-
try:
|
|
81
|
-
async with asyncio.timeout(timeout):
|
|
82
|
-
async with websockets.connect(ws_url) as ws:
|
|
83
|
-
await ws.send(init_msg)
|
|
84
|
-
for _ in range(3):
|
|
85
|
-
msg = await ws.recv()
|
|
86
|
-
data = json.loads(msg)
|
|
87
|
-
if data.get("type") == "connection_ack":
|
|
88
|
-
return data.get("result", data)
|
|
89
|
-
except Exception:
|
|
90
|
-
pass
|
|
91
|
-
return None
|
|
92
|
-
|
|
93
|
-
|
|
94
53
|
def _render_unified_status_table(
|
|
95
54
|
config: Any,
|
|
96
55
|
ws_url: str,
|
|
@@ -156,10 +115,6 @@ def daemon_status(
|
|
|
156
115
|
bool,
|
|
157
116
|
typer.Option("--json", help="Output as JSON."),
|
|
158
117
|
] = False,
|
|
159
|
-
verbose: Annotated[
|
|
160
|
-
bool,
|
|
161
|
-
typer.Option("--verbose", "-v", help="Show detailed status."),
|
|
162
|
-
] = False,
|
|
163
118
|
) -> None:
|
|
164
119
|
"""Check daemon status from client side.
|
|
165
120
|
|
|
@@ -168,7 +123,6 @@ def daemon_status(
|
|
|
168
123
|
Examples:
|
|
169
124
|
soothe status daemon
|
|
170
125
|
soothe status daemon --json
|
|
171
|
-
soothe status daemon -v
|
|
172
126
|
"""
|
|
173
127
|
config = load_config()
|
|
174
128
|
ws_url = websocket_url_from_config(config)
|
|
@@ -217,11 +171,6 @@ def daemon_status(
|
|
|
217
171
|
)
|
|
218
172
|
sys.exit(1)
|
|
219
173
|
|
|
220
|
-
# Get readiness state for verbose mode
|
|
221
|
-
ready_state = None
|
|
222
|
-
if verbose:
|
|
223
|
-
ready_state = asyncio.run(_fetch_ready_state(ws_url, timeout=5.0))
|
|
224
|
-
|
|
225
174
|
if json_output:
|
|
226
175
|
output = {
|
|
227
176
|
"status": "running",
|
|
@@ -232,10 +181,10 @@ def daemon_status(
|
|
|
232
181
|
"daemon_pid": status.get("daemon_pid"),
|
|
233
182
|
"daemon_version": status.get("daemon_version"),
|
|
234
183
|
"core_version": status.get("core_version"),
|
|
184
|
+
"readiness_state": status.get("readiness_state", "unknown"),
|
|
235
185
|
}
|
|
236
|
-
if
|
|
237
|
-
output["
|
|
238
|
-
output["readiness_message"] = ready_state.get("message")
|
|
186
|
+
if status.get("readiness_message"):
|
|
187
|
+
output["readiness_message"] = status.get("readiness_message")
|
|
239
188
|
console.print_json(json.dumps(output))
|
|
240
189
|
return
|
|
241
190
|
|
|
@@ -246,6 +195,15 @@ def daemon_status(
|
|
|
246
195
|
daemon_pid = status.get("daemon_pid")
|
|
247
196
|
daemon_version = status.get("daemon_version")
|
|
248
197
|
core_version = status.get("core_version")
|
|
198
|
+
# Use readiness_state from daemon_status RPC (already includes state + message)
|
|
199
|
+
readiness_state_from_status = (
|
|
200
|
+
{
|
|
201
|
+
"state": status.get("readiness_state", "unknown"),
|
|
202
|
+
"message": status.get("readiness_message"),
|
|
203
|
+
}
|
|
204
|
+
if status.get("readiness_state")
|
|
205
|
+
else None
|
|
206
|
+
)
|
|
249
207
|
|
|
250
208
|
table = _render_unified_status_table(
|
|
251
209
|
config,
|
|
@@ -254,7 +212,7 @@ def daemon_status(
|
|
|
254
212
|
port_live,
|
|
255
213
|
active_threads,
|
|
256
214
|
daemon_pid,
|
|
257
|
-
|
|
215
|
+
readiness_state_from_status,
|
|
258
216
|
daemon_live=True,
|
|
259
217
|
daemon_version=daemon_version,
|
|
260
218
|
core_version=core_version,
|
|
@@ -349,6 +307,9 @@ def status_main(
|
|
|
349
307
|
output["daemon"]["daemon_pid"] = status.get("daemon_pid")
|
|
350
308
|
output["daemon"]["daemon_version"] = status.get("daemon_version")
|
|
351
309
|
output["daemon"]["core_version"] = status.get("core_version")
|
|
310
|
+
output["daemon"]["readiness_state"] = status.get("readiness_state", "unknown")
|
|
311
|
+
if status.get("readiness_message"):
|
|
312
|
+
output["daemon"]["readiness_message"] = status.get("readiness_message")
|
|
352
313
|
console.print_json(json.dumps(output))
|
|
353
314
|
return
|
|
354
315
|
|
|
@@ -378,6 +339,15 @@ def status_main(
|
|
|
378
339
|
daemon_pid = status.get("daemon_pid")
|
|
379
340
|
daemon_version = status.get("daemon_version")
|
|
380
341
|
core_version = status.get("core_version")
|
|
342
|
+
# Use readiness_state from daemon_status RPC (already includes state + message)
|
|
343
|
+
readiness_state_from_status = (
|
|
344
|
+
{
|
|
345
|
+
"state": status.get("readiness_state", "unknown"),
|
|
346
|
+
"message": status.get("readiness_message"),
|
|
347
|
+
}
|
|
348
|
+
if status.get("readiness_state")
|
|
349
|
+
else None
|
|
350
|
+
)
|
|
381
351
|
|
|
382
352
|
table = _render_unified_status_table(
|
|
383
353
|
config,
|
|
@@ -386,6 +356,7 @@ def status_main(
|
|
|
386
356
|
port_live,
|
|
387
357
|
active_threads,
|
|
388
358
|
daemon_pid,
|
|
359
|
+
readiness_state_from_status,
|
|
389
360
|
daemon_live=True,
|
|
390
361
|
daemon_version=daemon_version,
|
|
391
362
|
core_version=core_version,
|
|
@@ -8,7 +8,6 @@ from __future__ import annotations
|
|
|
8
8
|
|
|
9
9
|
import asyncio
|
|
10
10
|
import logging
|
|
11
|
-
import os
|
|
12
11
|
import signal
|
|
13
12
|
from typing import Any
|
|
14
13
|
|
|
@@ -28,6 +27,7 @@ from soothe_cli.cli.execution.daemon_errors import (
|
|
|
28
27
|
from soothe_cli.cli.execution.headless_renderer import HeadlessCliRenderer
|
|
29
28
|
from soothe_cli.runtime import EventProcessor
|
|
30
29
|
from soothe_cli.runtime.presentation.engine import PresentationEngine
|
|
30
|
+
from soothe_cli.tui._env_vars import resolve_cli_loop_workspace
|
|
31
31
|
from soothe_cli.tui.commands.subagent_routing import parse_subagent_from_input
|
|
32
32
|
|
|
33
33
|
logger = logging.getLogger(__name__)
|
|
@@ -151,7 +151,7 @@ async def _run_headless_session_once(
|
|
|
151
151
|
|
|
152
152
|
try:
|
|
153
153
|
await connect_websocket_with_retries(client)
|
|
154
|
-
cli_ws =
|
|
154
|
+
cli_ws = resolve_cli_loop_workspace()
|
|
155
155
|
# IG-441: three first-class modes (batch / adaptive / streaming).
|
|
156
156
|
# Default is ``adaptive`` for headless runs as well — it gives smooth
|
|
157
157
|
# progress on long synthesis while keeping wire traffic bounded.
|
|
@@ -8,14 +8,16 @@ config, no widget imports) so that `app.py` can import `SessionStats` and
|
|
|
8
8
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
|
+
import json
|
|
11
12
|
import time
|
|
12
13
|
from dataclasses import dataclass, field
|
|
14
|
+
from typing import Any
|
|
13
15
|
|
|
14
16
|
SpinnerStatus = str | None
|
|
15
17
|
"""Spinner line label, or `None` to hide.
|
|
16
18
|
|
|
17
|
-
Common values include ``Thinking``, ``
|
|
18
|
-
``Tools``
|
|
19
|
+
Common single-word values include ``Thinking``, ``Planning``, ``Executing``,
|
|
20
|
+
``Tools``, ``Offloading``, ``Writing``, and ``Synthesizing``.
|
|
19
21
|
"""
|
|
20
22
|
|
|
21
23
|
|
|
@@ -242,48 +244,91 @@ class TurnEventStats:
|
|
|
242
244
|
self.latency = TurnLatencyStats()
|
|
243
245
|
self.latency.merge(other.latency)
|
|
244
246
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
"""
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
if self.latency is not None
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
247
|
+
@staticmethod
|
|
248
|
+
def _nonzero_fields(**fields: int) -> dict[str, int]:
|
|
249
|
+
return {key: value for key, value in fields.items() if value}
|
|
250
|
+
|
|
251
|
+
def to_log_dict(self) -> dict[str, Any]:
|
|
252
|
+
"""Return structured event counters for ``cli.log`` JSON lines."""
|
|
253
|
+
payload: dict[str, Any] = {"total": self.total}
|
|
254
|
+
|
|
255
|
+
modes = self._nonzero_fields(
|
|
256
|
+
messages=self.messages,
|
|
257
|
+
updates=self.updates,
|
|
258
|
+
custom=self.custom,
|
|
259
|
+
)
|
|
260
|
+
if modes:
|
|
261
|
+
payload["modes"] = modes
|
|
262
|
+
|
|
263
|
+
activity = self._nonzero_fields(
|
|
264
|
+
tool_calls=self.tool_calls,
|
|
265
|
+
tool_results=self.tool_results,
|
|
266
|
+
text_chunks=self.text_chunks,
|
|
267
|
+
)
|
|
268
|
+
if activity:
|
|
269
|
+
payload["activity"] = activity
|
|
270
|
+
|
|
271
|
+
degradation = self._nonzero_fields(
|
|
272
|
+
skipped=self.skipped,
|
|
273
|
+
filtered_early=self.filtered_early,
|
|
274
|
+
heartbeats_dropped=self.heartbeats_dropped,
|
|
275
|
+
post_idle_drained=self.post_idle_drained,
|
|
276
|
+
inbound_dropped=self.inbound_dropped,
|
|
277
|
+
)
|
|
278
|
+
if degradation:
|
|
279
|
+
payload["degradation"] = degradation
|
|
280
|
+
|
|
281
|
+
if self.latency is not None:
|
|
282
|
+
latency: dict[str, int] = {}
|
|
283
|
+
if self.latency.time_to_first_chunk_ms is not None:
|
|
284
|
+
latency["ttfc_ms"] = round(self.latency.time_to_first_chunk_ms)
|
|
285
|
+
if self.latency.synthesis_visible_ms is not None:
|
|
286
|
+
latency["synthesis_ms"] = round(self.latency.synthesis_visible_ms)
|
|
287
|
+
if latency:
|
|
288
|
+
payload["latency"] = latency
|
|
289
|
+
|
|
290
|
+
return payload
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def build_goal_completed_log_event(
|
|
294
|
+
ev_stats: TurnEventStats,
|
|
295
|
+
*,
|
|
296
|
+
status: str,
|
|
297
|
+
goal_progress: str,
|
|
298
|
+
total_steps: int,
|
|
299
|
+
elapsed_seconds: float,
|
|
300
|
+
) -> dict[str, Any]:
|
|
301
|
+
"""Build a structured goal-completion record for ``cli.log``."""
|
|
302
|
+
event: dict[str, Any] = {
|
|
303
|
+
"event": "goal_completed",
|
|
304
|
+
"elapsed_s": round(elapsed_seconds, 1),
|
|
305
|
+
"events": ev_stats.to_log_dict(),
|
|
306
|
+
}
|
|
307
|
+
if status.strip():
|
|
308
|
+
event["status"] = status.strip()
|
|
309
|
+
if goal_progress.strip():
|
|
310
|
+
event["progress"] = goal_progress.strip()
|
|
311
|
+
if total_steps > 0:
|
|
312
|
+
event["steps"] = total_steps
|
|
313
|
+
return event
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def build_turn_finished_log_event(
|
|
317
|
+
ev_stats: TurnEventStats,
|
|
318
|
+
*,
|
|
319
|
+
wall_seconds: float,
|
|
320
|
+
) -> dict[str, Any]:
|
|
321
|
+
"""Build a structured turn-finished record for ``cli.log``."""
|
|
322
|
+
return {
|
|
323
|
+
"event": "turn_finished",
|
|
324
|
+
"wall_s": round(wall_seconds, 1),
|
|
325
|
+
"events": ev_stats.to_log_dict(),
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def format_cli_log_event(payload: dict[str, Any]) -> str:
|
|
330
|
+
"""Serialize a structured CLI log event as compact JSON."""
|
|
331
|
+
return json.dumps(payload, separators=(",", ":"), sort_keys=True)
|
|
287
332
|
|
|
288
333
|
|
|
289
334
|
def format_token_count(count: int) -> str:
|
|
@@ -61,3 +61,31 @@ TUI_REFRESH_INTERVAL_MS = "SOOTHE_CLI_TUI_REFRESH_INTERVAL_MS"
|
|
|
61
61
|
Set to throttle frequent refreshes during streaming, reducing UI lag.
|
|
62
62
|
Lower values = more responsive but more CPU load; higher values = smoother.
|
|
63
63
|
"""
|
|
64
|
+
|
|
65
|
+
WORKSPACE = "SOOTHE_CLI_WORKSPACE"
|
|
66
|
+
"""Explicit project directory sent to the daemon on ``loop_new`` (defaults to cwd)."""
|
|
67
|
+
|
|
68
|
+
OMIT_WORKSPACE = "SOOTHE_CLI_OMIT_WORKSPACE"
|
|
69
|
+
"""Optional: when ``1``/``true``/``yes``, omit ``workspace`` on ``loop_new`` (default: send cwd)."""
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def resolve_cli_loop_workspace() -> str | None:
|
|
73
|
+
"""Return the workspace path to send on ``loop_new``, or ``None`` to omit it.
|
|
74
|
+
|
|
75
|
+
By default sends ``cwd``. The daemon ignores host paths that are not present
|
|
76
|
+
on the daemon filesystem (falls back to persisted layout) unless
|
|
77
|
+
``workspace_mount`` is configured (RFC-621). ``SOOTHE_CLI_OMIT_WORKSPACE`` is
|
|
78
|
+
optional — use it only to skip sending ``workspace`` on the wire.
|
|
79
|
+
"""
|
|
80
|
+
import os
|
|
81
|
+
|
|
82
|
+
omit = os.environ.get(OMIT_WORKSPACE, "").strip().lower()
|
|
83
|
+
if omit in ("1", "true", "yes", "on"):
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
explicit = os.environ.get(WORKSPACE, "").strip()
|
|
87
|
+
if explicit.lower() in ("none", "-", "omit"):
|
|
88
|
+
return None
|
|
89
|
+
if explicit:
|
|
90
|
+
return explicit
|
|
91
|
+
return os.getcwd()
|
|
@@ -43,6 +43,7 @@ from soothe_cli.tui.widgets.messages import (
|
|
|
43
43
|
AssistantMessage,
|
|
44
44
|
QueuedUserMessage,
|
|
45
45
|
)
|
|
46
|
+
from soothe_cli.tui.widgets.plan_quick_view_overlay import PlanQuickViewOverlay
|
|
46
47
|
from soothe_cli.tui.widgets.status import StatusBar
|
|
47
48
|
from soothe_cli.tui.widgets.welcome import WelcomeBanner
|
|
48
49
|
|
|
@@ -79,7 +80,7 @@ class SootheApp(
|
|
|
79
80
|
"""Vertical scroll speed (reduced from Textual default for finer control)."""
|
|
80
81
|
|
|
81
82
|
BINDINGS: ClassVar[list[BindingType]] = [
|
|
82
|
-
Binding("escape", "
|
|
83
|
+
Binding("escape", "dismiss_ui", "Dismiss", show=False, priority=True),
|
|
83
84
|
Binding(
|
|
84
85
|
"ctrl+c",
|
|
85
86
|
"quit_or_interrupt",
|
|
@@ -115,8 +116,15 @@ class SootheApp(
|
|
|
115
116
|
"Copy Selection",
|
|
116
117
|
show=False,
|
|
117
118
|
),
|
|
119
|
+
Binding(
|
|
120
|
+
"ctrl+t",
|
|
121
|
+
"toggle_plan_quick_view",
|
|
122
|
+
"Plan View",
|
|
123
|
+
show=False,
|
|
124
|
+
priority=True,
|
|
125
|
+
),
|
|
118
126
|
]
|
|
119
|
-
"""App-level keybindings for
|
|
127
|
+
"""App-level keybindings for dismiss, quit, and navigation."""
|
|
120
128
|
|
|
121
129
|
class ServerStartFailed(Message):
|
|
122
130
|
"""Posted when daemon bootstrap or background connection fails."""
|
|
@@ -216,6 +224,7 @@ class SootheApp(
|
|
|
216
224
|
self._status_bar: StatusBar | None = None
|
|
217
225
|
|
|
218
226
|
self._chat_input: ChatInput | None = None
|
|
227
|
+
self._plan_quick_view_overlay: PlanQuickViewOverlay | None = None
|
|
219
228
|
|
|
220
229
|
self._quit_pending = False
|
|
221
230
|
|
|
@@ -374,6 +383,7 @@ class SootheApp(
|
|
|
374
383
|
)
|
|
375
384
|
yield Container(id="messages")
|
|
376
385
|
with Container(id="bottom-app-container"):
|
|
386
|
+
yield PlanQuickViewOverlay(id="plan-quick-view-overlay")
|
|
377
387
|
yield Container(id="thinking-status")
|
|
378
388
|
yield ChatInput(
|
|
379
389
|
cwd=self._cwd,
|
|
@@ -28,7 +28,6 @@ from soothe_cli.cli.execution.daemon_errors import (
|
|
|
28
28
|
from soothe_cli.runtime.state.session_stats import SessionStats, format_token_count
|
|
29
29
|
from soothe_cli.tui import theme
|
|
30
30
|
from soothe_cli.tui._cli_context import CLIContext
|
|
31
|
-
from soothe_cli.tui._version import DOCS_URL
|
|
32
31
|
from soothe_cli.tui.app._module_init import (
|
|
33
32
|
_COMMAND_URLS,
|
|
34
33
|
DeferredAction,
|
|
@@ -36,6 +35,7 @@ from soothe_cli.tui.app._module_init import (
|
|
|
36
35
|
_extract_model_params_flag,
|
|
37
36
|
)
|
|
38
37
|
from soothe_cli.tui.hooks import dispatch_hook
|
|
38
|
+
from soothe_cli.tui.shell_color import shell_subprocess_env, wrap_shell_command_for_color
|
|
39
39
|
from soothe_cli.tui.widgets.chat_input import ChatInput
|
|
40
40
|
from soothe_cli.tui.widgets.messages import (
|
|
41
41
|
AppMessage,
|
|
@@ -288,10 +288,11 @@ class _ExecutionMixin:
|
|
|
288
288
|
"""
|
|
289
289
|
try:
|
|
290
290
|
proc = await asyncio.create_subprocess_shell(
|
|
291
|
-
command,
|
|
291
|
+
wrap_shell_command_for_color(command),
|
|
292
292
|
stdout=asyncio.subprocess.PIPE,
|
|
293
293
|
stderr=asyncio.subprocess.PIPE,
|
|
294
294
|
cwd=self._cwd,
|
|
295
|
+
env=shell_subprocess_env(),
|
|
295
296
|
start_new_session=(sys.platform != "win32"),
|
|
296
297
|
)
|
|
297
298
|
self._shell_process = proc
|
|
@@ -312,7 +313,7 @@ class _ExecutionMixin:
|
|
|
312
313
|
output += f"\n[stderr]\n{stderr_text}"
|
|
313
314
|
|
|
314
315
|
if output:
|
|
315
|
-
msg = AssistantMessage(
|
|
316
|
+
msg = AssistantMessage(output, render_markdown=False, render_ansi=True)
|
|
316
317
|
await self._mount_message(msg)
|
|
317
318
|
await msg.write_initial_content()
|
|
318
319
|
else:
|
|
@@ -461,7 +462,7 @@ class _ExecutionMixin:
|
|
|
461
462
|
)
|
|
462
463
|
from soothe_cli.tui.commands.slash_commands import COMMANDS as _RFC404_COMMANDS
|
|
463
464
|
|
|
464
|
-
# RFC-454 daemon *routing* commands (/research, /
|
|
465
|
+
# RFC-454 daemon *routing* commands (/research, /plan, optional /«id»):
|
|
465
466
|
# send the full line as a normal user turn so ``parse_subagent_from_input``
|
|
466
467
|
# runs in the daemon adapter (same as headless CLI). Without this branch,
|
|
467
468
|
# ``cmd == "/research …"`` never matches the bare single-token handlers below.
|
|
@@ -484,37 +485,14 @@ class _ExecutionMixin:
|
|
|
484
485
|
self.query_one("#chat", VerticalScroll).anchor()
|
|
485
486
|
return
|
|
486
487
|
|
|
487
|
-
from soothe_cli.tui.config import
|
|
488
|
+
from soothe_cli.tui.config import settings
|
|
488
489
|
|
|
489
490
|
cmd = command.lower().strip()
|
|
490
491
|
|
|
491
492
|
if cmd in {"/quit", "/q"}:
|
|
492
493
|
self._detach_or_exit()
|
|
493
494
|
elif cmd == "/help":
|
|
494
|
-
await self.
|
|
495
|
-
help_body = (
|
|
496
|
-
"Commands: /quit, /clear, /editor, /autopilot <task>, /cron <schedule>, "
|
|
497
|
-
"/context, /mcp, "
|
|
498
|
-
"/model [--model-params JSON] [--default], /notifications, "
|
|
499
|
-
"/reload, /skill:<name>, /theme, "
|
|
500
|
-
"/tokens, /resume, "
|
|
501
|
-
"/research, /explore, /plan, /«subagent» (when configured), "
|
|
502
|
-
"/update, /auto-update, /changelog, /docs, /feedback, /help\n\n"
|
|
503
|
-
"Interactive Features:\n"
|
|
504
|
-
" Enter Submit your message\n"
|
|
505
|
-
f" {newline_shortcut():<15} Insert newline\n"
|
|
506
|
-
" Ctrl+X Open prompt in external editor\n"
|
|
507
|
-
" Shift+Tab Cycle loop selector\n"
|
|
508
|
-
" @filename Auto-complete files and inject content\n"
|
|
509
|
-
" /command Slash commands (/help, /clear, /quit)\n"
|
|
510
|
-
" !command Run shell commands directly\n\n"
|
|
511
|
-
"Docs: "
|
|
512
|
-
)
|
|
513
|
-
help_text = Content.assemble(
|
|
514
|
-
(help_body, "dim italic"),
|
|
515
|
-
(DOCS_URL, TStyle(dim=True, italic=True, link=DOCS_URL)),
|
|
516
|
-
)
|
|
517
|
-
await self._mount_message(AppMessage(help_text))
|
|
495
|
+
await self._show_help_screen()
|
|
518
496
|
|
|
519
497
|
elif cmd in {"/changelog", "/docs", "/feedback"}:
|
|
520
498
|
await self._open_url_command(command, cmd)
|
|
@@ -1031,9 +1009,11 @@ class _ExecutionMixin:
|
|
|
1031
1009
|
self._agent_running = False
|
|
1032
1010
|
self._agent_worker = None
|
|
1033
1011
|
|
|
1034
|
-
# Restore input focus
|
|
1012
|
+
# Restore input focus first so the user can type immediately
|
|
1035
1013
|
# while remaining cleanup (spinner, tokens, deferred) runs.
|
|
1036
|
-
if self.
|
|
1014
|
+
if self._primary_text_input() is not None:
|
|
1015
|
+
self.focus_primary_input()
|
|
1016
|
+
elif self._chat_input:
|
|
1037
1017
|
self._chat_input.set_cursor_active(active=True)
|
|
1038
1018
|
|
|
1039
1019
|
# Remove spinner if present
|