soothe-cli 0.7.15__tar.gz → 0.7.16__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.15 → soothe_cli-0.7.16}/PKG-INFO +1 -1
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/tui/app/_execution.py +4 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/tui/app/_messages_mixin.py +126 -43
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/tui/config.py +11 -5
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/tui/theme.py +1 -1
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/tui/widgets/chat_input.py +23 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/tui/widgets/context_viewer.py +183 -46
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/tui/widgets/file_change_preview.py +2 -1
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/tui/widgets/loop_selector.py +8 -4
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/tui/widgets/messages/_helpers.py +10 -7
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/tui/widgets/messages/assistant.py +1 -1
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/tui/widgets/messages/cognition_goal_tree.py +1 -1
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/tui/widgets/messages/cognition_reason.py +1 -1
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/tui/widgets/messages/cognition_step.py +1 -1
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/tui/widgets/messages/cognition_subagent.py +2 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/tui/widgets/messages/user.py +18 -1
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/tui/widgets/plan_quick_view_overlay.py +26 -1
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/.gitignore +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/README.md +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/pyproject.toml +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/__init__.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/cli/__init__.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/cli/commands/__init__.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/cli/commands/autopilot_cmd.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/cli/commands/config_cmd.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/cli/commands/cron_cmd.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/cli/commands/loop_cmd.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/cli/commands/run_cmd.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/cli/commands/status_cmd.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/cli/execution/__init__.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/cli/execution/daemon.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/cli/execution/daemon_errors.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/cli/execution/headless.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/cli/execution/headless_renderer.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/cli/execution/launcher.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/cli/main.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/config/__init__.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/config/cli_config.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/config/loader.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/config/logging_setup.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/runtime/__init__.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/runtime/headless/processor.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/runtime/headless/processor_state.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/runtime/parse/message_processing.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/runtime/parse/tool_call_resolution.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/runtime/parse/tool_result.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/runtime/policy/display_policy.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/runtime/policy/essential_events.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/runtime/policy/tui_trace_log.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/runtime/presentation/duration_format.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/runtime/presentation/engine.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/runtime/presentation/renderer_base.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/runtime/presentation/renderer_protocol.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/runtime/presentation/subagent_task_display.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/runtime/state/file_tracker.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/runtime/state/session_stats.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/runtime/state/step_router.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/runtime/state/stream_accumulator.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/runtime/state/transcript.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/runtime/token_events_debug.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/runtime/token_usage.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/runtime/transport/session.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/runtime/turn/pipeline.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/runtime/turn/prepare.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/runtime/wire/chunk_filter.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/runtime/wire/message_text.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/runtime/wire/messages.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/tui/__init__.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/tui/_cli_context.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/tui/_env_vars.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/tui/_version.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/tui/app/__init__.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/tui/app/_app.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/tui/app/_history.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/tui/app/_model.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/tui/app/_module_init.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/tui/app/_startup.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/tui/app/_ui.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/tui/app/app.tcss +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/tui/binding.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/tui/command_registry.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/tui/commands/__init__.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/tui/commands/command_router.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/tui/commands/slash_commands.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/tui/commands/subagent_routing.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/tui/file_change_notify.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/tui/file_change_renderers.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/tui/hooks.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/tui/input.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/tui/markdown_theme.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/tui/media_utils.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/tui/model_config.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/tui/path_utils.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/tui/preview_limits.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/tui/project_utils.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/tui/sessions.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/tui/shell_color.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/tui/skills/__init__.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/tui/skills/invocation.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/tui/skills/load.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/tui/spinner_labels.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/tui/textual_adapter.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/tui/tips.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/tui/tool_display.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/tui/unicode_security.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/tui/update_check.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/tui/widgets/__init__.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/tui/widgets/_links.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/tui/widgets/autocomplete.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/tui/widgets/clipboard.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/tui/widgets/context_data.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/tui/widgets/diff.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/tui/widgets/editor.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/tui/widgets/help_screen.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/tui/widgets/history.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/tui/widgets/loading.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/tui/widgets/mcp_viewer.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/tui/widgets/message_store.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/tui/widgets/messages/__init__.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/tui/widgets/messages/app.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/tui/widgets/messages/clarification.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/tui/widgets/messages/cognition_step_activity.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/tui/widgets/messages/diff_message.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/tui/widgets/messages/error.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/tui/widgets/messages/skill.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/tui/widgets/model_selector.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/tui/widgets/notification_settings.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/tui/widgets/status.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/tui/widgets/theme_selector.py +0 -0
- {soothe_cli-0.7.15 → soothe_cli-0.7.16}/src/soothe_cli/tui/widgets/welcome.py +0 -0
|
@@ -179,6 +179,7 @@ class _ExecutionMixin:
|
|
|
179
179
|
self._pending_messages.append(QueuedMessage(text=value, mode=mode))
|
|
180
180
|
queued_widget = QueuedUserMessage(value)
|
|
181
181
|
self._queued_widgets.append(queued_widget)
|
|
182
|
+
self._refresh_queued_goal_tips()
|
|
182
183
|
await self._mount_message(queued_widget)
|
|
183
184
|
return
|
|
184
185
|
|
|
@@ -417,12 +418,14 @@ class _ExecutionMixin:
|
|
|
417
418
|
if self._agent_running or self._shell_running:
|
|
418
419
|
queued_widget = QueuedUserMessage(command)
|
|
419
420
|
self._queued_widgets.append(queued_widget)
|
|
421
|
+
self._refresh_queued_goal_tips()
|
|
420
422
|
await self._mount_message(queued_widget)
|
|
421
423
|
|
|
422
424
|
async def _mount_output() -> None:
|
|
423
425
|
# Remove the ephemeral queued widget, then mount real output.
|
|
424
426
|
if queued_widget in self._queued_widgets:
|
|
425
427
|
self._queued_widgets.remove(queued_widget)
|
|
428
|
+
self._refresh_queued_goal_tips()
|
|
426
429
|
with suppress(Exception):
|
|
427
430
|
await queued_widget.remove()
|
|
428
431
|
await self._mount_message(UserMessage(command))
|
|
@@ -908,6 +911,7 @@ class _ExecutionMixin:
|
|
|
908
911
|
if self._queued_widgets:
|
|
909
912
|
widget = self._queued_widgets.popleft()
|
|
910
913
|
await widget.remove()
|
|
914
|
+
self._refresh_queued_goal_tips()
|
|
911
915
|
|
|
912
916
|
await self._process_message(msg.text, msg.mode)
|
|
913
917
|
except Exception:
|
|
@@ -43,6 +43,70 @@ logger = logging.getLogger(__name__)
|
|
|
43
43
|
class _MessagesMixin:
|
|
44
44
|
"""Message widget lifecycle, store management, queue, interrupt/quit, toggles, editor, and events."""
|
|
45
45
|
|
|
46
|
+
def _refresh_queued_goal_tips(self) -> None:
|
|
47
|
+
"""Show queue interaction tips on the most recently queued goal only."""
|
|
48
|
+
if not self._queued_widgets:
|
|
49
|
+
return
|
|
50
|
+
tip_index = -1
|
|
51
|
+
if self._pending_messages and self._pending_messages[-1].mode == "normal":
|
|
52
|
+
pending_tail_index = len(self._pending_messages) - 1
|
|
53
|
+
if pending_tail_index < len(self._queued_widgets):
|
|
54
|
+
tip_index = pending_tail_index
|
|
55
|
+
for index, widget in enumerate(self._queued_widgets):
|
|
56
|
+
with suppress(Exception):
|
|
57
|
+
widget.set_show_queue_tips(index == tip_index if tip_index >= 0 else False)
|
|
58
|
+
|
|
59
|
+
def _has_pending_chat_input(self) -> bool:
|
|
60
|
+
"""Return whether chat input has draft content that should be preserved."""
|
|
61
|
+
chat_input = self._chat_input
|
|
62
|
+
return bool(
|
|
63
|
+
chat_input
|
|
64
|
+
and (
|
|
65
|
+
chat_input.value.strip()
|
|
66
|
+
or chat_input.mode != "normal"
|
|
67
|
+
or chat_input._current_suggestions
|
|
68
|
+
)
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
def _can_run_queued_goal_now_from_enter(self) -> bool:
|
|
72
|
+
"""Return whether Enter should cancel the current goal for queued-head run."""
|
|
73
|
+
if not self._agent_running or not self._pending_messages:
|
|
74
|
+
return False
|
|
75
|
+
if self._has_pending_chat_input():
|
|
76
|
+
return False
|
|
77
|
+
queue_head = self._pending_messages[0]
|
|
78
|
+
return queue_head.mode == "normal"
|
|
79
|
+
|
|
80
|
+
def run_queued_goal_now_from_enter(self) -> bool:
|
|
81
|
+
"""Cancel the running goal so queued head can start immediately.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
`True` when an interrupt was triggered, otherwise `False`.
|
|
85
|
+
"""
|
|
86
|
+
if not self._can_run_queued_goal_now_from_enter():
|
|
87
|
+
return False
|
|
88
|
+
return self._interrupt_running_goal_preserving_queue()
|
|
89
|
+
|
|
90
|
+
def _interrupt_running_goal_preserving_queue(self) -> bool:
|
|
91
|
+
"""Interrupt current agent turn while keeping queued goals intact."""
|
|
92
|
+
if not (self._agent_running and self._agent_worker):
|
|
93
|
+
return False
|
|
94
|
+
if self._daemon_session is not None:
|
|
95
|
+
self.run_worker(
|
|
96
|
+
self._interrupt_daemon_agent_turn(discard_queue=False),
|
|
97
|
+
exclusive=False,
|
|
98
|
+
group="daemon-interrupt",
|
|
99
|
+
)
|
|
100
|
+
else:
|
|
101
|
+
self.run_worker(
|
|
102
|
+
self._tear_down_interrupt_ui(),
|
|
103
|
+
exclusive=False,
|
|
104
|
+
group="interrupt-ui",
|
|
105
|
+
)
|
|
106
|
+
self._cancel_worker(self._agent_worker, discard_queue=False)
|
|
107
|
+
self._quit_pending = False
|
|
108
|
+
return True
|
|
109
|
+
|
|
46
110
|
async def _load_loop_history(
|
|
47
111
|
self,
|
|
48
112
|
*,
|
|
@@ -322,40 +386,42 @@ class _MessagesMixin:
|
|
|
322
386
|
"Messages container (#messages) not found during clear; UI may be out of sync with message store"
|
|
323
387
|
)
|
|
324
388
|
|
|
325
|
-
def
|
|
326
|
-
"""
|
|
327
|
-
|
|
328
|
-
If the chat input is empty the evicted text is restored there so the
|
|
329
|
-
user can edit and re-submit. Otherwise the message is discarded. The
|
|
330
|
-
toast message distinguishes between the two outcomes.
|
|
331
|
-
|
|
332
|
-
Caller must ensure `_pending_messages` is non-empty. A defensive guard
|
|
333
|
-
is included in case of async TOCTOU races.
|
|
334
|
-
"""
|
|
389
|
+
def _pop_last_queued_entry(self) -> Any | None:
|
|
390
|
+
"""Pop the latest queued message and paired widget, if available."""
|
|
335
391
|
if not self._pending_messages:
|
|
336
|
-
return
|
|
392
|
+
return None
|
|
337
393
|
msg = self._pending_messages.pop()
|
|
338
394
|
if self._queued_widgets:
|
|
339
395
|
widget = self._queued_widgets.pop()
|
|
340
396
|
widget.remove()
|
|
397
|
+
self._refresh_queued_goal_tips()
|
|
341
398
|
else:
|
|
342
399
|
logger.warning(
|
|
343
400
|
"Queued-widget deque empty while pending-messages was not; widget/message tracking may be out of sync"
|
|
344
401
|
)
|
|
402
|
+
return msg
|
|
403
|
+
|
|
404
|
+
def _restore_last_queued_goal_to_input(self) -> bool:
|
|
405
|
+
"""Restore the latest queued goal into an empty chat input for editing."""
|
|
406
|
+
msg = self._pop_last_queued_entry()
|
|
407
|
+
if msg is None:
|
|
408
|
+
return False
|
|
345
409
|
|
|
346
410
|
if not self._chat_input:
|
|
347
411
|
logger.warning(
|
|
348
412
|
"Chat input unavailable during queue pop; message text cannot be restored: %s",
|
|
349
413
|
msg.text[:60],
|
|
350
414
|
)
|
|
351
|
-
|
|
352
|
-
return
|
|
415
|
+
return False
|
|
353
416
|
|
|
354
|
-
if
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
417
|
+
if self._has_pending_chat_input():
|
|
418
|
+
logger.warning(
|
|
419
|
+
"Queue restore requested while input has draft content; skipping restore"
|
|
420
|
+
)
|
|
421
|
+
return False
|
|
422
|
+
self._chat_input.value = msg.text
|
|
423
|
+
self.notify("Queued goal moved to input", timeout=2)
|
|
424
|
+
return True
|
|
359
425
|
|
|
360
426
|
def _discard_queue(self) -> None:
|
|
361
427
|
"""Clear pending messages, deferred actions, and queued widgets."""
|
|
@@ -365,6 +431,25 @@ class _MessagesMixin:
|
|
|
365
431
|
self._queued_widgets.clear()
|
|
366
432
|
self._deferred_actions.clear()
|
|
367
433
|
|
|
434
|
+
def _cancel_last_queued_message(self) -> bool:
|
|
435
|
+
"""Cancel the most recently queued goal without restoring it to input."""
|
|
436
|
+
msg = self._pop_last_queued_entry()
|
|
437
|
+
if msg is None:
|
|
438
|
+
return False
|
|
439
|
+
self.notify(f"Cancelled queued goal: {msg.text[:60]}", timeout=2)
|
|
440
|
+
return True
|
|
441
|
+
|
|
442
|
+
def edit_queued_goal_from_up(self) -> bool:
|
|
443
|
+
"""Move the latest queued goal back to input for editing."""
|
|
444
|
+
if not self._pending_messages:
|
|
445
|
+
return False
|
|
446
|
+
if self._has_pending_chat_input():
|
|
447
|
+
return False
|
|
448
|
+
queue_tail = self._pending_messages[-1]
|
|
449
|
+
if queue_tail.mode != "normal":
|
|
450
|
+
return False
|
|
451
|
+
return self._restore_last_queued_goal_to_input()
|
|
452
|
+
|
|
368
453
|
def _defer_action(self, action: DeferredAction) -> None:
|
|
369
454
|
"""Queue a deferred action, replacing any existing action of the same kind.
|
|
370
455
|
|
|
@@ -434,7 +519,7 @@ class _MessagesMixin:
|
|
|
434
519
|
worker.cancel()
|
|
435
520
|
|
|
436
521
|
async def _interrupt_daemon_agent_turn(self, *, discard_queue: bool = True) -> None:
|
|
437
|
-
"""Stop in-flight UI
|
|
522
|
+
"""Stop in-flight UI and request daemon-side cancel.
|
|
438
523
|
|
|
439
524
|
UI teardown runs first so Ctrl+C does not leave the thinking spinner or
|
|
440
525
|
goal-tree running rows active while the daemon winds down (which can take
|
|
@@ -442,17 +527,24 @@ class _MessagesMixin:
|
|
|
442
527
|
|
|
443
528
|
Args:
|
|
444
529
|
discard_queue: When ``False``, preserve queued user goals so they
|
|
445
|
-
run after the cancelled turn finishes cleanup.
|
|
530
|
+
run after the cancelled turn finishes cleanup. In this mode,
|
|
531
|
+
the local worker is left running when daemon cancel succeeds.
|
|
446
532
|
"""
|
|
447
533
|
await self._tear_down_interrupt_ui()
|
|
448
534
|
session = self._daemon_session
|
|
449
535
|
worker = self._agent_worker
|
|
536
|
+
cancel_sent = False
|
|
450
537
|
if session is not None:
|
|
451
538
|
try:
|
|
452
539
|
await session.cancel_remote_query()
|
|
540
|
+
cancel_sent = True
|
|
453
541
|
except Exception:
|
|
454
542
|
logger.warning("Failed to send cancel to daemon", exc_info=True)
|
|
455
|
-
|
|
543
|
+
# For Enter-triggered queued-goal handoff (discard_queue=False), avoid
|
|
544
|
+
# force-cancelling the local worker after /cancel is accepted.
|
|
545
|
+
# Let the active stream finish naturally so queue-drain ordering stays
|
|
546
|
+
# stable and the loop subscription is not torn down mid-handoff.
|
|
547
|
+
if worker is not None and (discard_queue or not cancel_sent):
|
|
456
548
|
self._cancel_worker(worker, discard_queue=discard_queue)
|
|
457
549
|
|
|
458
550
|
def action_copy_selection(self) -> None:
|
|
@@ -476,11 +568,7 @@ class _MessagesMixin:
|
|
|
476
568
|
so Ctrl+C is reserved for interrupt/quit only.
|
|
477
569
|
"""
|
|
478
570
|
# Check if input has pending content (text, mode, or completion)
|
|
479
|
-
has_pending_input = self.
|
|
480
|
-
self._chat_input.value.strip()
|
|
481
|
-
or self._chat_input.mode != "normal"
|
|
482
|
-
or self._chat_input._current_suggestions
|
|
483
|
-
)
|
|
571
|
+
has_pending_input = self._has_pending_chat_input()
|
|
484
572
|
|
|
485
573
|
# If shell command is running: clear input first, then kill shell
|
|
486
574
|
if self._shell_running and self._shell_worker:
|
|
@@ -498,20 +586,7 @@ class _MessagesMixin:
|
|
|
498
586
|
self._chat_input.clear_input()
|
|
499
587
|
self._quit_pending = False
|
|
500
588
|
return
|
|
501
|
-
|
|
502
|
-
self.run_worker(
|
|
503
|
-
self._interrupt_daemon_agent_turn(discard_queue=False),
|
|
504
|
-
exclusive=False,
|
|
505
|
-
group="daemon-interrupt",
|
|
506
|
-
)
|
|
507
|
-
else:
|
|
508
|
-
self.run_worker(
|
|
509
|
-
self._tear_down_interrupt_ui(),
|
|
510
|
-
exclusive=False,
|
|
511
|
-
group="interrupt-ui",
|
|
512
|
-
)
|
|
513
|
-
self._cancel_worker(self._agent_worker, discard_queue=False)
|
|
514
|
-
self._quit_pending = False
|
|
589
|
+
self._interrupt_running_goal_preserving_queue()
|
|
515
590
|
return
|
|
516
591
|
|
|
517
592
|
# Double Ctrl+C to quit (same detach path as Ctrl+D and /quit)
|
|
@@ -534,13 +609,14 @@ class _MessagesMixin:
|
|
|
534
609
|
self.set_timer(quit_timeout, lambda: setattr(self, "_quit_pending", False))
|
|
535
610
|
|
|
536
611
|
def action_dismiss_ui(self) -> None:
|
|
537
|
-
"""Handle Escape — dismiss overlays and
|
|
612
|
+
"""Handle Escape — dismiss overlays and optionally cancel queued goals.
|
|
538
613
|
|
|
539
614
|
Priority order:
|
|
540
615
|
1. If modal screen is active, dismiss it
|
|
541
616
|
2. If plan quick-view overlay is open, collapse it
|
|
542
|
-
3. If
|
|
543
|
-
4. If
|
|
617
|
+
3. If input is idle and queue has a normal goal, cancel queued tail
|
|
618
|
+
4. If completion popup is open, dismiss it
|
|
619
|
+
5. If input is in command/shell mode, exit to normal mode
|
|
544
620
|
"""
|
|
545
621
|
from contextlib import suppress
|
|
546
622
|
|
|
@@ -568,6 +644,13 @@ class _MessagesMixin:
|
|
|
568
644
|
|
|
569
645
|
# Close completion popup or exit slash/shell command mode
|
|
570
646
|
if self._chat_input:
|
|
647
|
+
# When queue has pending goals and input is idle, Esc cancels the
|
|
648
|
+
# latest queued normal goal (without interrupting current work).
|
|
649
|
+
if not self._has_pending_chat_input():
|
|
650
|
+
queue_tail = self._pending_messages[-1] if self._pending_messages else None
|
|
651
|
+
if queue_tail is not None and queue_tail.mode == "normal":
|
|
652
|
+
if self._cancel_last_queued_message():
|
|
653
|
+
return
|
|
571
654
|
if self._chat_input.dismiss_completion():
|
|
572
655
|
return
|
|
573
656
|
if self._chat_input.exit_mode():
|
|
@@ -221,10 +221,10 @@ class CharsetMode(StrEnum):
|
|
|
221
221
|
"""Character set mode for TUI display."""
|
|
222
222
|
|
|
223
223
|
UNICODE = "unicode"
|
|
224
|
-
"""Always use Unicode glyphs (e.g.
|
|
224
|
+
"""Always use Unicode glyphs (e.g. `●`, `✓`, `…`)."""
|
|
225
225
|
|
|
226
226
|
ASCII = "ascii"
|
|
227
|
-
"""Always use ASCII-safe fallbacks (e.g. `
|
|
227
|
+
"""Always use ASCII-safe fallbacks (e.g. `[*]`, `[OK]`, `...`)."""
|
|
228
228
|
|
|
229
229
|
AUTO = "auto"
|
|
230
230
|
"""Detect charset support at runtime and pick Unicode or ASCII."""
|
|
@@ -234,7 +234,9 @@ class CharsetMode(StrEnum):
|
|
|
234
234
|
class Glyphs:
|
|
235
235
|
"""Character glyphs for TUI display."""
|
|
236
236
|
|
|
237
|
-
tool_prefix: str #
|
|
237
|
+
tool_prefix: str # ● vs [*]
|
|
238
|
+
file_edit_prefix: str # ■ vs [#]
|
|
239
|
+
subagent_prefix: str # ◆ vs [S]
|
|
238
240
|
ellipsis: str # … vs ...
|
|
239
241
|
checkmark: str # ✓ vs [OK]
|
|
240
242
|
error: str # ✗ vs [X]
|
|
@@ -270,7 +272,9 @@ class Glyphs:
|
|
|
270
272
|
|
|
271
273
|
|
|
272
274
|
UNICODE_GLYPHS = Glyphs(
|
|
273
|
-
tool_prefix="
|
|
275
|
+
tool_prefix="●",
|
|
276
|
+
file_edit_prefix="■",
|
|
277
|
+
subagent_prefix="◆",
|
|
274
278
|
ellipsis="…",
|
|
275
279
|
checkmark="✓",
|
|
276
280
|
error="✗",
|
|
@@ -301,7 +305,9 @@ UNICODE_GLYPHS = Glyphs(
|
|
|
301
305
|
"""Glyph set for terminals with full Unicode support."""
|
|
302
306
|
|
|
303
307
|
ASCII_GLYPHS = Glyphs(
|
|
304
|
-
tool_prefix="
|
|
308
|
+
tool_prefix="[*]",
|
|
309
|
+
file_edit_prefix="[#]",
|
|
310
|
+
subagent_prefix="[S]",
|
|
305
311
|
ellipsis="...",
|
|
306
312
|
checkmark="[OK]",
|
|
307
313
|
error="[X]",
|
|
@@ -99,7 +99,7 @@ LC_TOOL = LC_AMBER
|
|
|
99
99
|
# Design principles:
|
|
100
100
|
# - Consistent visual style across all four card types
|
|
101
101
|
# - Restrained grayscale for secondary activity text
|
|
102
|
-
# - Stateful
|
|
102
|
+
# - Stateful card-prefix glyphs for card headers; ``⎿`` gutter for body lines
|
|
103
103
|
# ---------------------------------------------------------------------------
|
|
104
104
|
|
|
105
105
|
# Grayscale base colors (neutral, non-distracting)
|
|
@@ -669,10 +669,33 @@ class ChatTextArea(TextArea):
|
|
|
669
669
|
event.prevent_default()
|
|
670
670
|
event.stop()
|
|
671
671
|
value = self.text.strip()
|
|
672
|
+
if not value:
|
|
673
|
+
trigger = getattr(self.app, "run_queued_goal_now_from_enter", None)
|
|
674
|
+
if callable(trigger):
|
|
675
|
+
try:
|
|
676
|
+
if bool(trigger()):
|
|
677
|
+
return
|
|
678
|
+
except Exception: # noqa: BLE001
|
|
679
|
+
logger.debug(
|
|
680
|
+
"Queued-goal Enter shortcut failed; keeping empty-enter noop",
|
|
681
|
+
exc_info=True,
|
|
682
|
+
)
|
|
672
683
|
if value:
|
|
673
684
|
self.post_message(self.Submitted(value))
|
|
674
685
|
return
|
|
675
686
|
|
|
687
|
+
# Up on empty input can pull the latest queued goal back for editing.
|
|
688
|
+
if event.key == "up" and not self.text:
|
|
689
|
+
trigger_edit = getattr(self.app, "edit_queued_goal_from_up", None)
|
|
690
|
+
if callable(trigger_edit):
|
|
691
|
+
try:
|
|
692
|
+
if bool(trigger_edit()):
|
|
693
|
+
event.prevent_default()
|
|
694
|
+
event.stop()
|
|
695
|
+
return
|
|
696
|
+
except Exception: # noqa: BLE001
|
|
697
|
+
logger.debug("Queued-goal edit shortcut failed", exc_info=True)
|
|
698
|
+
|
|
676
699
|
# Up/Down arrow: only navigate history at input boundaries.
|
|
677
700
|
# Up requires cursor at position (0, 0); Down requires cursor at
|
|
678
701
|
# the very end. When already browsing history, either boundary
|