soothe-cli 0.7.6__tar.gz → 0.7.7__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.6 → soothe_cli-0.7.7}/PKG-INFO +1 -1
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/cli/execution/daemon.py +1 -1
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/runtime/policy/display_policy.py +3 -3
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/runtime/presentation/renderer_protocol.py +1 -1
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/runtime/presentation/subagent_task_display.py +10 -7
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/runtime/state/file_tracker.py +21 -24
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/runtime/transport/session.py +1 -1
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/app/_history.py +2 -2
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/app/_messages_mixin.py +27 -3
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/app/app.tcss +0 -22
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/command_registry.py +7 -2
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/commands/slash_commands.py +8 -2
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/commands/subagent_routing.py +5 -4
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/file_change_notify.py +2 -2
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/file_change_renderers.py +4 -5
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/input.py +8 -5
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/textual_adapter.py +101 -50
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/widgets/chat_input.py +46 -3
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/widgets/file_change_preview.py +0 -22
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/widgets/messages/cognition_goal_tree.py +18 -2
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/widgets/messages/cognition_subagent.py +1 -1
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/widgets/messages/diff_message.py +1 -20
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/.gitignore +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/README.md +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/pyproject.toml +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/__init__.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/cli/__init__.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/cli/commands/__init__.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/cli/commands/autopilot_cmd.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/cli/commands/config_cmd.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/cli/commands/cron_cmd.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/cli/commands/loop_cmd.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/cli/commands/run_cmd.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/cli/commands/status_cmd.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/cli/execution/__init__.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/cli/execution/daemon_errors.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/cli/execution/headless.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/cli/execution/headless_renderer.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/cli/execution/launcher.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/cli/main.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/config/__init__.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/config/cli_config.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/config/loader.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/config/logging_setup.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/runtime/__init__.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/runtime/headless/processor.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/runtime/headless/processor_state.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/runtime/parse/message_processing.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/runtime/parse/tool_call_resolution.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/runtime/parse/tool_result.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/runtime/policy/essential_events.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/runtime/policy/tui_trace_log.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/runtime/presentation/duration_format.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/runtime/presentation/engine.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/runtime/presentation/renderer_base.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/runtime/state/session_stats.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/runtime/state/step_router.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/runtime/state/stream_accumulator.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/runtime/state/transcript.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/runtime/turn/pipeline.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/runtime/turn/prepare.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/runtime/wire/chunk_filter.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/runtime/wire/display_text.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/runtime/wire/message_text.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/runtime/wire/messages.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/__init__.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/_cli_context.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/_env_vars.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/_version.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/app/__init__.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/app/_app.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/app/_commands.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/app/_execution.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/app/_model.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/app/_module_init.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/app/_startup.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/app/_ui.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/binding.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/commands/__init__.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/commands/command_router.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/config.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/hooks.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/markdown_theme.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/media_utils.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/model_config.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/path_utils.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/preview_limits.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/project_utils.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/sessions.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/shell_color.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/skills/__init__.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/skills/invocation.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/skills/load.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/spinner_labels.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/theme.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/tips.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/tool_display.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/unicode_security.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/update_check.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/widgets/__init__.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/widgets/_links.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/widgets/autocomplete.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/widgets/autopilot_dashboard.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/widgets/clipboard.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/widgets/context_viewer.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/widgets/diff.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/widgets/editor.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/widgets/help_screen.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/widgets/history.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/widgets/loading.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/widgets/loop_selector.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/widgets/mcp_viewer.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/widgets/message_store.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/widgets/messages/__init__.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/widgets/messages/_helpers.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/widgets/messages/app.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/widgets/messages/assistant.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/widgets/messages/clarification.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/widgets/messages/cognition_reason.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/widgets/messages/cognition_step.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/widgets/messages/cognition_step_activity.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/widgets/messages/error.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/widgets/messages/skill.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/widgets/messages/user.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/widgets/model_selector.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/widgets/notification_settings.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/widgets/plan_quick_view_overlay.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/widgets/status.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/widgets/theme_selector.py +0 -0
- {soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/tui/widgets/welcome.py +0 -0
|
@@ -280,7 +280,7 @@ async def _run_headless_session_once(
|
|
|
280
280
|
query_started = True
|
|
281
281
|
elif (state == "idle" and query_started) or state == "stopped":
|
|
282
282
|
loop_clock = asyncio.get_event_loop()
|
|
283
|
-
drain_deadline = loop_clock.time() +
|
|
283
|
+
drain_deadline = loop_clock.time() + 0.5
|
|
284
284
|
while loop_clock.time() < drain_deadline:
|
|
285
285
|
try:
|
|
286
286
|
nxt = await asyncio.wait_for(client.read_event(), timeout=0.25)
|
|
@@ -134,9 +134,9 @@ class DisplayPolicy:
|
|
|
134
134
|
"""Check if this is a plan-related event."""
|
|
135
135
|
return event_type.startswith("soothe.cognition.plan.")
|
|
136
136
|
|
|
137
|
-
def
|
|
138
|
-
"""Check if this is a
|
|
139
|
-
return event_type.startswith("soothe.subagent.
|
|
137
|
+
def is_deep_research_event(self, event_type: str) -> bool:
|
|
138
|
+
"""Check if this is a Deep Research subagent event."""
|
|
139
|
+
return event_type.startswith("soothe.subagent.deep_research.")
|
|
140
140
|
|
|
141
141
|
def is_internal_event(self, event_type: str) -> bool:
|
|
142
142
|
"""Check if this is an internal (never-shown) event."""
|
{soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/runtime/presentation/renderer_protocol.py
RENAMED
|
@@ -141,7 +141,7 @@ class RendererProtocol(Protocol):
|
|
|
141
141
|
Catch-all for events not covered by specific callbacks.
|
|
142
142
|
|
|
143
143
|
Args:
|
|
144
|
-
event_type: Full event type string (e.g., ``soothe.subagent.
|
|
144
|
+
event_type: Full event type string (e.g., ``soothe.subagent.deep_research.started``).
|
|
145
145
|
data: Event payload.
|
|
146
146
|
namespace: Subagent namespace tuple (empty for main agent).
|
|
147
147
|
task_scope: When subgraph streams are bound to a Task tool call (IG-334).
|
{soothe_cli-0.7.6 → soothe_cli-0.7.7}/src/soothe_cli/runtime/presentation/subagent_task_display.py
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Format subagent Task-scope assistant blobs for CLI/TUI.
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Deep Research emits structured JSON in subgraph assistant streams. Clients show
|
|
4
4
|
user-facing summaries only; internal planning JSON is suppressed.
|
|
5
5
|
"""
|
|
6
6
|
|
|
@@ -11,8 +11,8 @@ from collections.abc import Iterator
|
|
|
11
11
|
from typing import Any
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
def
|
|
15
|
-
"""True when ``obj`` is
|
|
14
|
+
def _is_deep_research_internal_json_object(obj: dict[str, Any]) -> bool:
|
|
15
|
+
"""True when ``obj`` is Deep Research engine scratch (not a user-facing report)."""
|
|
16
16
|
keys = set(obj.keys())
|
|
17
17
|
if "sub_questions" in keys:
|
|
18
18
|
return True
|
|
@@ -89,21 +89,24 @@ def format_subagent_task_assistant_for_display(
|
|
|
89
89
|
|
|
90
90
|
Args:
|
|
91
91
|
raw: Full assistant text from a subgraph namespace.
|
|
92
|
-
subagent_type: Built-in subagent id when known (``
|
|
92
|
+
subagent_type: Built-in subagent id when known (``deep_research``, …).
|
|
93
93
|
|
|
94
94
|
Returns:
|
|
95
95
|
Scrubbed one-line text, or ``""`` when only internal payloads remain.
|
|
96
96
|
"""
|
|
97
97
|
agent = (subagent_type or "").strip().lower()
|
|
98
98
|
text = raw
|
|
99
|
-
if agent
|
|
99
|
+
if agent in ("deep_research", "academic_research"):
|
|
100
100
|
stripped = text.strip()
|
|
101
101
|
internal_present = any(
|
|
102
|
-
|
|
102
|
+
_is_deep_research_internal_json_object(obj)
|
|
103
|
+
for obj in _iter_embedded_json_objects(stripped)
|
|
103
104
|
)
|
|
104
105
|
if internal_present:
|
|
105
106
|
return ""
|
|
106
|
-
text = _strip_concatenated_json_objects(
|
|
107
|
+
text = _strip_concatenated_json_objects(
|
|
108
|
+
text, predicate=_is_deep_research_internal_json_object
|
|
109
|
+
)
|
|
107
110
|
return (text or "").strip()
|
|
108
111
|
|
|
109
112
|
|
|
@@ -19,16 +19,6 @@ FileOpStatus = Literal["pending", "success", "error"]
|
|
|
19
19
|
FILE_CHANGE_TOOLS: frozenset[str] = get_file_write_tool_names()
|
|
20
20
|
"""Filesystem tools that produce before/after diffs in the TUI chat."""
|
|
21
21
|
|
|
22
|
-
_FILE_CHANGE_PREVIEW_LABELS: dict[str, str] = {
|
|
23
|
-
"write_file": "Writing",
|
|
24
|
-
"edit_file": "Editing",
|
|
25
|
-
"edit_file_lines": "Editing",
|
|
26
|
-
"insert_lines": "Inserting",
|
|
27
|
-
"delete_lines": "Deleting",
|
|
28
|
-
"apply_diff": "Patching",
|
|
29
|
-
"delete_file": "Deleting",
|
|
30
|
-
}
|
|
31
|
-
|
|
32
22
|
|
|
33
23
|
def _safe_read(path: Path) -> str | None:
|
|
34
24
|
"""Read file content, returning None on failure.
|
|
@@ -439,21 +429,28 @@ def track_file_operation(
|
|
|
439
429
|
tracker.start_operation(tool_name, args, tcid)
|
|
440
430
|
|
|
441
431
|
|
|
442
|
-
def
|
|
443
|
-
"""
|
|
444
|
-
|
|
432
|
+
def file_change_label(tool_name: str, *, is_new_file: bool = False) -> str:
|
|
433
|
+
"""Single-word past-tense prefix for a file-change card header."""
|
|
434
|
+
if tool_name in ("delete_file", "delete_lines"):
|
|
435
|
+
return "Deleted"
|
|
436
|
+
if tool_name == "write_file":
|
|
437
|
+
return "Created" if is_new_file else "Written"
|
|
438
|
+
if tool_name in ("edit_file", "edit_file_lines"):
|
|
439
|
+
return "Edited"
|
|
440
|
+
if tool_name == "insert_lines":
|
|
441
|
+
return "Inserted"
|
|
442
|
+
if tool_name == "apply_diff":
|
|
443
|
+
return "Patched"
|
|
444
|
+
return "Changed"
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def file_change_label_from_preview_data(tool_name: str, data: dict[str, Any]) -> str:
|
|
448
|
+
"""Derive a preview header label from renderer-built widget data."""
|
|
449
|
+
is_new = bool(data.get("is_new_file")) if tool_name == "write_file" else False
|
|
450
|
+
return file_change_label(tool_name, is_new_file=is_new)
|
|
445
451
|
|
|
446
452
|
|
|
447
453
|
def file_change_action_label(record: FileOperationRecord) -> str:
|
|
448
454
|
"""Human-readable label for a completed file operation (chat diff header)."""
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
if record.tool_name == "write_file" and not (record.before_content or ""):
|
|
452
|
-
return "Created"
|
|
453
|
-
if record.tool_name == "write_file":
|
|
454
|
-
return "Written"
|
|
455
|
-
if record.tool_name in ("edit_file", "edit_file_lines", "delete_lines", "apply_diff"):
|
|
456
|
-
return "Updated"
|
|
457
|
-
if record.tool_name == "insert_lines":
|
|
458
|
-
return "Inserted"
|
|
459
|
-
return "Changed"
|
|
455
|
+
is_new = record.tool_name == "write_file" and not (record.before_content or "")
|
|
456
|
+
return file_change_label(record.tool_name, is_new_file=is_new)
|
|
@@ -26,7 +26,7 @@ logger = logging.getLogger(__name__)
|
|
|
26
26
|
|
|
27
27
|
# Match headless daemon client: brief read window after ``idle`` so stream events
|
|
28
28
|
# that arrive slightly after status are not dropped (``cli/execution/daemon.py``).
|
|
29
|
-
_POST_IDLE_DRAIN_DEADLINE_S =
|
|
29
|
+
_POST_IDLE_DRAIN_DEADLINE_S = 0.5
|
|
30
30
|
|
|
31
31
|
# Align with ``bootstrap_loop_session`` daemon-ready wait (RFC-450 §8.2).
|
|
32
32
|
_RPC_HANDSHAKE_TIMEOUT_S = 20.0
|
|
@@ -24,7 +24,7 @@ from soothe_sdk.display.text_extract import (
|
|
|
24
24
|
normalize_stream_message,
|
|
25
25
|
)
|
|
26
26
|
from soothe_sdk.display.transcript_types import MessageData
|
|
27
|
-
from soothe_sdk.ux.loop_stream import
|
|
27
|
+
from soothe_sdk.ux.loop_stream import is_stream_terminal
|
|
28
28
|
from textual.content import Content
|
|
29
29
|
|
|
30
30
|
from soothe_cli.tui.app._module_init import _LoopHistoryPayload
|
|
@@ -339,7 +339,7 @@ class _HistoryMixin:
|
|
|
339
339
|
|
|
340
340
|
if isinstance(message, (AIMessage, AIMessageChunk)):
|
|
341
341
|
extracted = extract_ai_text_for_display(message)
|
|
342
|
-
is_terminal =
|
|
342
|
+
is_terminal = is_stream_terminal(message)
|
|
343
343
|
if extracted:
|
|
344
344
|
# Deduplicate immediate replayed AI chunks after reconnect/resubscribe.
|
|
345
345
|
if last_ai_chunk_by_ns.get(ns_key) == extracted:
|
|
@@ -399,6 +399,23 @@ class _MessagesMixin:
|
|
|
399
399
|
)
|
|
400
400
|
)
|
|
401
401
|
|
|
402
|
+
_INTERRUPT_UI_MESSAGE = "Stream cancelled"
|
|
403
|
+
|
|
404
|
+
async def _tear_down_interrupt_ui(self, message: str | None = None) -> None:
|
|
405
|
+
"""Immediately stop in-flight step/goal UI when the user interrupts a turn.
|
|
406
|
+
|
|
407
|
+
Daemon cancel and worker teardown can take several seconds; without an
|
|
408
|
+
eager UI pass the thinking spinner and goal-tree running rows stay live.
|
|
409
|
+
"""
|
|
410
|
+
label = message or self._INTERRUPT_UI_MESSAGE
|
|
411
|
+
adapter = getattr(self, "_ui_adapter", None)
|
|
412
|
+
if adapter is not None:
|
|
413
|
+
if adapter._tool_to_step or adapter._tool_display_by_call_id:
|
|
414
|
+
adapter.finalize_pending_tools_with_error(label)
|
|
415
|
+
if adapter._current_step_messages or adapter._goal_tree_message is not None:
|
|
416
|
+
adapter.finalize_pending_steps_with_error(label)
|
|
417
|
+
await self._set_spinner(None)
|
|
418
|
+
|
|
402
419
|
def _cancel_worker(self, worker: Worker[None] | None, *, discard_queue: bool = True) -> None:
|
|
403
420
|
"""Cancel an active worker, optionally discarding the pending message queue.
|
|
404
421
|
|
|
@@ -414,15 +431,17 @@ class _MessagesMixin:
|
|
|
414
431
|
worker.cancel()
|
|
415
432
|
|
|
416
433
|
async def _interrupt_daemon_agent_turn(self, *, discard_queue: bool = True) -> None:
|
|
417
|
-
"""
|
|
434
|
+
"""Stop in-flight UI, send daemon ``/cancel``, then cancel the local worker.
|
|
418
435
|
|
|
419
|
-
|
|
420
|
-
|
|
436
|
+
UI teardown runs first so Ctrl+C does not leave the thinking spinner or
|
|
437
|
+
goal-tree running rows active while the daemon winds down (which can take
|
|
438
|
+
several seconds on long execute steps).
|
|
421
439
|
|
|
422
440
|
Args:
|
|
423
441
|
discard_queue: When ``False``, preserve queued user goals so they
|
|
424
442
|
run after the cancelled turn finishes cleanup.
|
|
425
443
|
"""
|
|
444
|
+
await self._tear_down_interrupt_ui()
|
|
426
445
|
session = self._daemon_session
|
|
427
446
|
worker = self._agent_worker
|
|
428
447
|
if session is not None:
|
|
@@ -483,6 +502,11 @@ class _MessagesMixin:
|
|
|
483
502
|
group="daemon-interrupt",
|
|
484
503
|
)
|
|
485
504
|
else:
|
|
505
|
+
self.run_worker(
|
|
506
|
+
self._tear_down_interrupt_ui(),
|
|
507
|
+
exclusive=False,
|
|
508
|
+
group="interrupt-ui",
|
|
509
|
+
)
|
|
486
510
|
self._cancel_worker(self._agent_worker, discard_queue=False)
|
|
487
511
|
self._quit_pending = False
|
|
488
512
|
return
|
|
@@ -102,12 +102,6 @@ Screen {
|
|
|
102
102
|
margin: 0;
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
.file-change-preview Markdown {
|
|
106
|
-
margin: 0;
|
|
107
|
-
padding: 0;
|
|
108
|
-
height: auto;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
105
|
.file-change-preview-section-label {
|
|
112
106
|
height: auto;
|
|
113
107
|
margin: 0;
|
|
@@ -126,22 +120,6 @@ Screen {
|
|
|
126
120
|
}
|
|
127
121
|
|
|
128
122
|
/* Diff styling — previews and DiffMessage (fg + bg + padding) */
|
|
129
|
-
.diff-removed {
|
|
130
|
-
height: auto;
|
|
131
|
-
color: $text-error;
|
|
132
|
-
background: $error-muted;
|
|
133
|
-
padding: 0;
|
|
134
|
-
margin: 0;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
.diff-added {
|
|
138
|
-
height: auto;
|
|
139
|
-
color: $text-success;
|
|
140
|
-
background: $success-muted;
|
|
141
|
-
padding: 0;
|
|
142
|
-
margin: 0;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
123
|
.diff-context {
|
|
146
124
|
height: auto;
|
|
147
125
|
color: $text-muted;
|
|
@@ -114,8 +114,13 @@ COMMANDS: tuple[SlashCommand, ...] = (
|
|
|
114
114
|
hidden_keywords="continue history sessions loops",
|
|
115
115
|
),
|
|
116
116
|
SlashCommand(
|
|
117
|
-
name="/
|
|
118
|
-
description="Route prompt to
|
|
117
|
+
name="/deep_research",
|
|
118
|
+
description="Route prompt to deep_research subagent (usage: /deep_research <query>)",
|
|
119
|
+
bypass_tier=BypassTier.QUEUED,
|
|
120
|
+
),
|
|
121
|
+
SlashCommand(
|
|
122
|
+
name="/academic_research",
|
|
123
|
+
description="Route prompt to academic_research subagent (usage: /academic_research <query>)",
|
|
119
124
|
bypass_tier=BypassTier.QUEUED,
|
|
120
125
|
),
|
|
121
126
|
SlashCommand(
|
|
@@ -274,10 +274,16 @@ COMMANDS: dict[str, dict[str, Any]] = {
|
|
|
274
274
|
},
|
|
275
275
|
# Daemon routing commands (3)
|
|
276
276
|
"/plan": {"location": "daemon", "type": "routing", "description": "Trigger plan mode"},
|
|
277
|
-
"/
|
|
277
|
+
"/deep_research": {
|
|
278
278
|
"location": "daemon",
|
|
279
279
|
"type": "routing",
|
|
280
|
-
"description": "Route query to
|
|
280
|
+
"description": "Route query to Deep Research subagent",
|
|
281
|
+
"requires_query": True,
|
|
282
|
+
},
|
|
283
|
+
"/academic_research": {
|
|
284
|
+
"location": "daemon",
|
|
285
|
+
"type": "routing",
|
|
286
|
+
"description": "Route query to Academic Research subagent",
|
|
281
287
|
"requires_query": True,
|
|
282
288
|
},
|
|
283
289
|
}
|
|
@@ -5,12 +5,13 @@ from __future__ import annotations
|
|
|
5
5
|
# Display names for known soothe core subagents (IG-517).
|
|
6
6
|
SUBAGENT_DISPLAY_NAMES: dict[str, str] = {
|
|
7
7
|
"planner": "Planner",
|
|
8
|
-
"
|
|
8
|
+
"deep_research": "Deep Research",
|
|
9
|
+
"academic_research": "Academic Research",
|
|
9
10
|
"browser_use": "Browser",
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
# Lowercase ids matched after ``/`` for preferred_subagent routing (core only).
|
|
13
|
-
SUBAGENT_SLASH_ROUTE_IDS: tuple[str, ...] = ("
|
|
14
|
+
SUBAGENT_SLASH_ROUTE_IDS: tuple[str, ...] = ("deep_research", "academic_research")
|
|
14
15
|
|
|
15
16
|
BUILTIN_SUBAGENT_NAMES: list[str] = list(SUBAGENT_SLASH_ROUTE_IDS)
|
|
16
17
|
|
|
@@ -33,7 +34,7 @@ def get_subagent_display_name(technical_name: str) -> str:
|
|
|
33
34
|
def parse_subagent_from_input(user_input: str) -> tuple[str | None, str]:
|
|
34
35
|
"""Parse subagent subcommand from user input.
|
|
35
36
|
|
|
36
|
-
Detects subagent routing commands (e.g. ``/
|
|
37
|
+
Detects subagent routing commands (e.g. ``/deep_research``)
|
|
37
38
|
and extracts the subagent name along with the cleaned input text.
|
|
38
39
|
|
|
39
40
|
Args:
|
|
@@ -45,7 +46,7 @@ def parse_subagent_from_input(user_input: str) -> tuple[str | None, str]:
|
|
|
45
46
|
The subcommand is removed from ``cleaned_text``.
|
|
46
47
|
|
|
47
48
|
Examples:
|
|
48
|
-
``"/
|
|
49
|
+
``"/deep_research check this"`` -> ``("deep_research", "check this")``
|
|
49
50
|
``"hello world"`` -> ``(None, "hello world")``
|
|
50
51
|
"""
|
|
51
52
|
first_match: tuple[int, str] | None = None
|
|
@@ -8,7 +8,7 @@ from typing import TYPE_CHECKING, Any
|
|
|
8
8
|
|
|
9
9
|
from soothe_cli.runtime.state.file_tracker import (
|
|
10
10
|
FILE_CHANGE_TOOLS,
|
|
11
|
-
|
|
11
|
+
file_change_label_from_preview_data,
|
|
12
12
|
parse_insert_line_arg,
|
|
13
13
|
parse_line_range_args,
|
|
14
14
|
)
|
|
@@ -89,7 +89,7 @@ async def mount_file_change_preview(
|
|
|
89
89
|
if built is None:
|
|
90
90
|
return
|
|
91
91
|
widget_cls, data = built
|
|
92
|
-
label =
|
|
92
|
+
label = file_change_label_from_preview_data(tool_name, data)
|
|
93
93
|
try:
|
|
94
94
|
widget = widget_cls(data, action_label=label)
|
|
95
95
|
widget.id = textual_widget_id("file-preview", tcid)
|
|
@@ -21,7 +21,6 @@ from soothe_cli.tui.widgets.file_change_preview import (
|
|
|
21
21
|
EditFileLinesPreviewWidget,
|
|
22
22
|
EditFilePreviewWidget,
|
|
23
23
|
FileChangePreviewWidget,
|
|
24
|
-
GenericFilePreviewWidget,
|
|
25
24
|
InsertLinesPreviewWidget,
|
|
26
25
|
WriteFilePreviewWidget,
|
|
27
26
|
unified_diff_body_lines,
|
|
@@ -84,7 +83,7 @@ def build_file_change_preview(
|
|
|
84
83
|
if tool_name == "edit_file_lines":
|
|
85
84
|
line_range = parse_line_range_args(args)
|
|
86
85
|
if line_range is None:
|
|
87
|
-
return
|
|
86
|
+
return None
|
|
88
87
|
start_line, end_line = line_range
|
|
89
88
|
new_string = str(args.get("new_content") or "")
|
|
90
89
|
before = ""
|
|
@@ -104,7 +103,7 @@ def build_file_change_preview(
|
|
|
104
103
|
if tool_name == "insert_lines":
|
|
105
104
|
line = parse_insert_line_arg(args)
|
|
106
105
|
if line is None:
|
|
107
|
-
return
|
|
106
|
+
return None
|
|
108
107
|
insert_content = str(args.get("content") or "")
|
|
109
108
|
before = ""
|
|
110
109
|
physical = resolve_physical_path(path_str, assistant_id)
|
|
@@ -122,7 +121,7 @@ def build_file_change_preview(
|
|
|
122
121
|
if tool_name == "delete_lines":
|
|
123
122
|
line_range = parse_line_range_args(args)
|
|
124
123
|
if line_range is None:
|
|
125
|
-
return
|
|
124
|
+
return None
|
|
126
125
|
start_line, end_line = line_range
|
|
127
126
|
before = ""
|
|
128
127
|
physical = resolve_physical_path(path_str, assistant_id)
|
|
@@ -172,7 +171,7 @@ def build_file_change_preview(
|
|
|
172
171
|
"total_lines": len(lines),
|
|
173
172
|
}
|
|
174
173
|
|
|
175
|
-
return
|
|
174
|
+
return None
|
|
176
175
|
|
|
177
176
|
|
|
178
177
|
def update_preview_data_from_record(data: dict[str, Any], record: FileOperationRecord) -> None:
|
|
@@ -839,21 +839,24 @@ def compose_paste_into_input(
|
|
|
839
839
|
return f"{existing_text[:start]}{pasted_text}{existing_text[end:]}"
|
|
840
840
|
|
|
841
841
|
|
|
842
|
-
def abbreviate_pasted_input_display(text: str) -> str:
|
|
843
|
-
"""Build a compact
|
|
842
|
+
def abbreviate_pasted_input_display(text: str, *, paste_index: int = 1) -> str:
|
|
843
|
+
"""Build a compact paste token for large pasted payloads.
|
|
844
844
|
|
|
845
845
|
The full ``text`` is retained separately for submission; this string is
|
|
846
846
|
only for on-screen display in the input widget.
|
|
847
847
|
|
|
848
848
|
Args:
|
|
849
849
|
text: Full pasted content.
|
|
850
|
+
paste_index: 1-based counter for multiple pastes in the same input.
|
|
850
851
|
|
|
851
852
|
Returns:
|
|
852
|
-
Abbreviated display
|
|
853
|
+
Abbreviated display token (e.g. ``[Pasted text #1 +35 lines]``).
|
|
853
854
|
"""
|
|
854
855
|
lines = text.splitlines()
|
|
855
856
|
if not lines and text:
|
|
856
857
|
lines = [text]
|
|
857
858
|
n_lines = len(lines) if lines else 1
|
|
858
|
-
|
|
859
|
-
|
|
859
|
+
if n_lines > 1:
|
|
860
|
+
extra = n_lines - 1
|
|
861
|
+
return f"[Pasted text #{paste_index} +{extra} lines]"
|
|
862
|
+
return f"[Pasted text #{paste_index}]"
|