soothe-cli 0.7.5__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.5 → soothe_cli-0.7.7}/PKG-INFO +1 -1
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/cli/execution/daemon.py +1 -1
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/runtime/policy/display_policy.py +3 -3
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/runtime/presentation/renderer_protocol.py +1 -1
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/runtime/presentation/subagent_task_display.py +10 -7
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/runtime/state/file_tracker.py +62 -12
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/runtime/transport/session.py +1 -1
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/app/_history.py +2 -2
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/app/_messages_mixin.py +27 -3
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/app/app.tcss +0 -27
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/binding.py +1 -1
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/command_registry.py +7 -2
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/commands/slash_commands.py +8 -2
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/commands/subagent_routing.py +5 -4
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/file_change_notify.py +13 -5
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/file_change_renderers.py +72 -15
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/input.py +8 -5
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/textual_adapter.py +125 -51
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/theme.py +3 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/widgets/chat_input.py +46 -3
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/widgets/file_change_preview.py +17 -35
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/widgets/messages/cognition_goal_tree.py +18 -2
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/widgets/messages/cognition_step.py +1 -2
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/widgets/messages/cognition_step_activity.py +21 -14
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/widgets/messages/cognition_subagent.py +1 -1
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/widgets/messages/diff_message.py +1 -20
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/widgets/status.py +1 -1
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/widgets/welcome.py +1 -1
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/.gitignore +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/README.md +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/pyproject.toml +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/__init__.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/cli/__init__.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/cli/commands/__init__.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/cli/commands/autopilot_cmd.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/cli/commands/config_cmd.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/cli/commands/cron_cmd.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/cli/commands/loop_cmd.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/cli/commands/run_cmd.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/cli/commands/status_cmd.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/cli/execution/__init__.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/cli/execution/daemon_errors.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/cli/execution/headless.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/cli/execution/headless_renderer.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/cli/execution/launcher.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/cli/main.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/config/__init__.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/config/cli_config.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/config/loader.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/config/logging_setup.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/runtime/__init__.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/runtime/headless/processor.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/runtime/headless/processor_state.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/runtime/parse/message_processing.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/runtime/parse/tool_call_resolution.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/runtime/parse/tool_result.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/runtime/policy/essential_events.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/runtime/policy/tui_trace_log.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/runtime/presentation/duration_format.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/runtime/presentation/engine.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/runtime/presentation/renderer_base.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/runtime/state/session_stats.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/runtime/state/step_router.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/runtime/state/stream_accumulator.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/runtime/state/transcript.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/runtime/turn/pipeline.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/runtime/turn/prepare.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/runtime/wire/chunk_filter.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/runtime/wire/display_text.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/runtime/wire/message_text.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/runtime/wire/messages.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/__init__.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/_cli_context.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/_env_vars.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/_version.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/app/__init__.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/app/_app.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/app/_commands.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/app/_execution.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/app/_model.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/app/_module_init.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/app/_startup.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/app/_ui.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/commands/__init__.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/commands/command_router.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/config.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/hooks.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/markdown_theme.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/media_utils.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/model_config.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/path_utils.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/preview_limits.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/project_utils.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/sessions.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/shell_color.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/skills/__init__.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/skills/invocation.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/skills/load.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/spinner_labels.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/tips.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/tool_display.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/unicode_security.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/update_check.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/widgets/__init__.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/widgets/_links.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/widgets/autocomplete.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/widgets/autopilot_dashboard.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/widgets/clipboard.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/widgets/context_viewer.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/widgets/diff.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/widgets/editor.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/widgets/help_screen.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/widgets/history.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/widgets/loading.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/widgets/loop_selector.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/widgets/mcp_viewer.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/widgets/message_store.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/widgets/messages/__init__.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/widgets/messages/_helpers.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/widgets/messages/app.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/widgets/messages/assistant.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/widgets/messages/clarification.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/widgets/messages/cognition_reason.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/widgets/messages/error.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/widgets/messages/skill.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/widgets/messages/user.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/widgets/model_selector.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/widgets/notification_settings.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/widgets/plan_quick_view_overlay.py +0 -0
- {soothe_cli-0.7.5 → soothe_cli-0.7.7}/src/soothe_cli/tui/widgets/theme_selector.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.5 → 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.5 → 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
|
|
|
@@ -8,15 +8,15 @@ from dataclasses import dataclass, field
|
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
from typing import Any, Literal
|
|
10
10
|
|
|
11
|
+
from soothe_sdk.tools.metadata import get_file_write_tool_names
|
|
12
|
+
|
|
11
13
|
from soothe_cli.tui.preview_limits import APPROVAL_DIFF_MAX_LINES
|
|
12
14
|
|
|
13
15
|
logger = logging.getLogger(__name__)
|
|
14
16
|
|
|
15
17
|
FileOpStatus = Literal["pending", "success", "error"]
|
|
16
18
|
|
|
17
|
-
FILE_CHANGE_TOOLS: frozenset[str] =
|
|
18
|
-
{"write_file", "edit_file", "edit_file_lines", "delete_file"}
|
|
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
22
|
|
|
@@ -150,6 +150,22 @@ def resolve_physical_path(path_str: str | None, assistant_id: str | None) -> Pat
|
|
|
150
150
|
return None
|
|
151
151
|
|
|
152
152
|
|
|
153
|
+
def parse_insert_line_arg(args: dict[str, Any]) -> int | None:
|
|
154
|
+
"""Parse ``line`` from insert_lines tool args (1-indexed).
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
Line number or None when missing or not an integer.
|
|
158
|
+
"""
|
|
159
|
+
line = args.get("line")
|
|
160
|
+
if isinstance(line, bool):
|
|
161
|
+
return None
|
|
162
|
+
if isinstance(line, int):
|
|
163
|
+
return line
|
|
164
|
+
if isinstance(line, float) and line.is_integer():
|
|
165
|
+
return int(line)
|
|
166
|
+
return None
|
|
167
|
+
|
|
168
|
+
|
|
153
169
|
def parse_line_range_args(args: dict[str, Any]) -> tuple[int, int] | None:
|
|
154
170
|
"""Parse ``start_line`` and ``end_line`` from tool args (1-indexed inclusive).
|
|
155
171
|
|
|
@@ -188,6 +204,26 @@ def extract_line_range_text(content: str, start_line: int, end_line: int) -> str
|
|
|
188
204
|
return "".join(lines[start_line - 1 : end_line])
|
|
189
205
|
|
|
190
206
|
|
|
207
|
+
def apply_insert_lines_to_content(content: str, line: int, insert_content: str) -> str | None:
|
|
208
|
+
"""Insert ``insert_content`` before line ``line`` (matches middleware semantics).
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
Modified file text, or None when ``line`` is out of range for ``content``.
|
|
212
|
+
"""
|
|
213
|
+
lines = content.splitlines(keepends=True)
|
|
214
|
+
total = len(lines)
|
|
215
|
+
if total == 0 and content:
|
|
216
|
+
lines = [content]
|
|
217
|
+
total = 1
|
|
218
|
+
if line < 1 or line > total + 1:
|
|
219
|
+
return None
|
|
220
|
+
new_lines = insert_content.splitlines(keepends=True)
|
|
221
|
+
if new_lines and not new_lines[-1].endswith("\n"):
|
|
222
|
+
new_lines[-1] += "\n"
|
|
223
|
+
lines[line - 1 : line - 1] = new_lines
|
|
224
|
+
return "".join(lines)
|
|
225
|
+
|
|
226
|
+
|
|
191
227
|
def apply_edit_file_lines_to_content(
|
|
192
228
|
content: str,
|
|
193
229
|
start_line: int,
|
|
@@ -393,14 +429,28 @@ def track_file_operation(
|
|
|
393
429
|
tracker.start_operation(tool_name, args, tcid)
|
|
394
430
|
|
|
395
431
|
|
|
396
|
-
def
|
|
397
|
-
"""
|
|
398
|
-
if
|
|
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"):
|
|
399
435
|
return "Deleted"
|
|
400
|
-
if
|
|
401
|
-
return "
|
|
402
|
-
if
|
|
403
|
-
return "
|
|
404
|
-
if
|
|
405
|
-
return "
|
|
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"
|
|
406
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)
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def file_change_action_label(record: FileOperationRecord) -> str:
|
|
454
|
+
"""Human-readable label for a completed file operation (chat diff header)."""
|
|
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
|
|
@@ -97,22 +97,11 @@ Screen {
|
|
|
97
97
|
margin: 0;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
.file-change-preview-label {
|
|
101
|
-
text-style: bold;
|
|
102
|
-
margin: 0;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
100
|
.file-change-preview-body {
|
|
106
101
|
color: $text-muted;
|
|
107
102
|
margin: 0;
|
|
108
103
|
}
|
|
109
104
|
|
|
110
|
-
.file-change-preview Markdown {
|
|
111
|
-
margin: 0;
|
|
112
|
-
padding: 0;
|
|
113
|
-
height: auto;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
105
|
.file-change-preview-section-label {
|
|
117
106
|
height: auto;
|
|
118
107
|
margin: 0;
|
|
@@ -131,22 +120,6 @@ Screen {
|
|
|
131
120
|
}
|
|
132
121
|
|
|
133
122
|
/* Diff styling — previews and DiffMessage (fg + bg + padding) */
|
|
134
|
-
.diff-removed {
|
|
135
|
-
height: auto;
|
|
136
|
-
color: $text-error;
|
|
137
|
-
background: $error-muted;
|
|
138
|
-
padding: 0;
|
|
139
|
-
margin: 0;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
.diff-added {
|
|
143
|
-
height: auto;
|
|
144
|
-
color: $text-success;
|
|
145
|
-
background: $success-muted;
|
|
146
|
-
padding: 0;
|
|
147
|
-
margin: 0;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
123
|
.diff-context {
|
|
151
124
|
height: auto;
|
|
152
125
|
color: $text-muted;
|
|
@@ -274,7 +274,7 @@ def message_from_widget(widget: Widget) -> MessageData:
|
|
|
274
274
|
if isinstance(widget, FileChangePreviewWidget):
|
|
275
275
|
path = str(widget.data.get("file_path") or widget.data.get("path") or "")
|
|
276
276
|
summary = f"{widget._action_label}: {path}" if path else widget._action_label
|
|
277
|
-
return MessageData(type=MessageType.APP, content=summary or "
|
|
277
|
+
return MessageData(type=MessageType.APP, content=summary or "Change", id=widget_id)
|
|
278
278
|
|
|
279
279
|
if isinstance(widget, SummarizationMessage):
|
|
280
280
|
return MessageData(
|
|
@@ -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
|
|
@@ -6,11 +6,13 @@ import asyncio
|
|
|
6
6
|
import logging
|
|
7
7
|
from typing import TYPE_CHECKING, Any
|
|
8
8
|
|
|
9
|
-
from soothe_cli.runtime.state.file_tracker import
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
from soothe_cli.runtime.state.file_tracker import (
|
|
10
|
+
FILE_CHANGE_TOOLS,
|
|
11
|
+
file_change_label_from_preview_data,
|
|
12
|
+
parse_insert_line_arg,
|
|
13
|
+
parse_line_range_args,
|
|
13
14
|
)
|
|
15
|
+
from soothe_cli.tui.file_change_renderers import build_file_change_preview
|
|
14
16
|
|
|
15
17
|
if TYPE_CHECKING:
|
|
16
18
|
from soothe_cli.runtime.state.file_tracker import FileOperationRecord
|
|
@@ -76,12 +78,18 @@ async def mount_file_change_preview(
|
|
|
76
78
|
return
|
|
77
79
|
if tool_name == "edit_file_lines" and parse_line_range_args(args) is None:
|
|
78
80
|
return
|
|
81
|
+
if tool_name == "insert_lines" and parse_insert_line_arg(args) is None:
|
|
82
|
+
return
|
|
83
|
+
if tool_name == "delete_lines" and parse_line_range_args(args) is None:
|
|
84
|
+
return
|
|
85
|
+
if tool_name == "apply_diff" and not str(args.get("diff") or "").strip():
|
|
86
|
+
return
|
|
79
87
|
|
|
80
88
|
built = build_file_change_preview(tool_name, args, assistant_id=assistant_id)
|
|
81
89
|
if built is None:
|
|
82
90
|
return
|
|
83
91
|
widget_cls, data = built
|
|
84
|
-
label =
|
|
92
|
+
label = file_change_label_from_preview_data(tool_name, data)
|
|
85
93
|
try:
|
|
86
94
|
widget = widget_cls(data, action_label=label)
|
|
87
95
|
widget.id = textual_widget_id("file-preview", tcid)
|
|
@@ -7,7 +7,10 @@ from typing import Any
|
|
|
7
7
|
from soothe_cli.runtime.state.file_tracker import (
|
|
8
8
|
FILE_CHANGE_TOOLS,
|
|
9
9
|
FileOperationRecord,
|
|
10
|
+
apply_edit_file_lines_to_content,
|
|
11
|
+
apply_insert_lines_to_content,
|
|
10
12
|
extract_line_range_text,
|
|
13
|
+
parse_insert_line_arg,
|
|
11
14
|
parse_line_range_args,
|
|
12
15
|
read_physical_file_text,
|
|
13
16
|
resolve_physical_path,
|
|
@@ -18,23 +21,11 @@ from soothe_cli.tui.widgets.file_change_preview import (
|
|
|
18
21
|
EditFileLinesPreviewWidget,
|
|
19
22
|
EditFilePreviewWidget,
|
|
20
23
|
FileChangePreviewWidget,
|
|
21
|
-
|
|
24
|
+
InsertLinesPreviewWidget,
|
|
22
25
|
WriteFilePreviewWidget,
|
|
23
26
|
unified_diff_body_lines,
|
|
24
27
|
)
|
|
25
28
|
|
|
26
|
-
_PREVIEW_LABELS: dict[str, str] = {
|
|
27
|
-
"write_file": "Writing file",
|
|
28
|
-
"edit_file": "Editing file",
|
|
29
|
-
"edit_file_lines": "Editing lines",
|
|
30
|
-
"delete_file": "Deleting file",
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def file_change_preview_label(tool_name: str) -> str:
|
|
35
|
-
"""Short header label for a pending filesystem tool preview."""
|
|
36
|
-
return _PREVIEW_LABELS.get(tool_name, "File change")
|
|
37
|
-
|
|
38
29
|
|
|
39
30
|
def build_file_change_preview(
|
|
40
31
|
tool_name: str,
|
|
@@ -92,7 +83,7 @@ def build_file_change_preview(
|
|
|
92
83
|
if tool_name == "edit_file_lines":
|
|
93
84
|
line_range = parse_line_range_args(args)
|
|
94
85
|
if line_range is None:
|
|
95
|
-
return
|
|
86
|
+
return None
|
|
96
87
|
start_line, end_line = line_range
|
|
97
88
|
new_string = str(args.get("new_content") or "")
|
|
98
89
|
before = ""
|
|
@@ -109,6 +100,62 @@ def build_file_change_preview(
|
|
|
109
100
|
"new_string": new_string,
|
|
110
101
|
}
|
|
111
102
|
|
|
103
|
+
if tool_name == "insert_lines":
|
|
104
|
+
line = parse_insert_line_arg(args)
|
|
105
|
+
if line is None:
|
|
106
|
+
return None
|
|
107
|
+
insert_content = str(args.get("content") or "")
|
|
108
|
+
before = ""
|
|
109
|
+
physical = resolve_physical_path(path_str, assistant_id)
|
|
110
|
+
if physical and physical.is_file():
|
|
111
|
+
before = read_physical_file_text(physical) or ""
|
|
112
|
+
after_text = apply_insert_lines_to_content(before, line, insert_content) or before
|
|
113
|
+
return InsertLinesPreviewWidget, {
|
|
114
|
+
"file_path": path_str,
|
|
115
|
+
"insert_line": line,
|
|
116
|
+
"diff_lines": unified_diff_body_lines(before, after_text),
|
|
117
|
+
"old_string": before,
|
|
118
|
+
"new_string": after_text,
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if tool_name == "delete_lines":
|
|
122
|
+
line_range = parse_line_range_args(args)
|
|
123
|
+
if line_range is None:
|
|
124
|
+
return None
|
|
125
|
+
start_line, end_line = line_range
|
|
126
|
+
before = ""
|
|
127
|
+
physical = resolve_physical_path(path_str, assistant_id)
|
|
128
|
+
if physical and physical.is_file():
|
|
129
|
+
before = read_physical_file_text(physical) or ""
|
|
130
|
+
old_segment = extract_line_range_text(before, start_line, end_line) if before else ""
|
|
131
|
+
after_text = (
|
|
132
|
+
apply_edit_file_lines_to_content(before, start_line, end_line, "") if before else ""
|
|
133
|
+
)
|
|
134
|
+
if after_text is None:
|
|
135
|
+
after_text = before
|
|
136
|
+
return EditFileLinesPreviewWidget, {
|
|
137
|
+
"file_path": path_str,
|
|
138
|
+
"start_line": start_line,
|
|
139
|
+
"end_line": end_line,
|
|
140
|
+
"diff_lines": unified_diff_body_lines(before, after_text),
|
|
141
|
+
"old_string": old_segment,
|
|
142
|
+
"new_string": "",
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if tool_name == "apply_diff":
|
|
146
|
+
diff_text = str(args.get("diff") or "")
|
|
147
|
+
before = ""
|
|
148
|
+
physical = resolve_physical_path(path_str, assistant_id)
|
|
149
|
+
if physical and physical.is_file():
|
|
150
|
+
before = read_physical_file_text(physical) or ""
|
|
151
|
+
diff_lines = [ln for ln in diff_text.splitlines() if ln.strip()]
|
|
152
|
+
return EditFilePreviewWidget, {
|
|
153
|
+
"file_path": path_str,
|
|
154
|
+
"diff_lines": diff_lines,
|
|
155
|
+
"old_string": before,
|
|
156
|
+
"new_string": "",
|
|
157
|
+
}
|
|
158
|
+
|
|
112
159
|
if tool_name == "delete_file":
|
|
113
160
|
content = ""
|
|
114
161
|
physical = resolve_physical_path(path_str, assistant_id)
|
|
@@ -124,7 +171,7 @@ def build_file_change_preview(
|
|
|
124
171
|
"total_lines": len(lines),
|
|
125
172
|
}
|
|
126
173
|
|
|
127
|
-
return
|
|
174
|
+
return None
|
|
128
175
|
|
|
129
176
|
|
|
130
177
|
def update_preview_data_from_record(data: dict[str, Any], record: FileOperationRecord) -> None:
|
|
@@ -161,6 +208,16 @@ def update_preview_data_from_record(data: dict[str, Any], record: FileOperationR
|
|
|
161
208
|
if line_range is not None:
|
|
162
209
|
data["start_line"], data["end_line"] = line_range
|
|
163
210
|
|
|
211
|
+
if record.tool_name == "insert_lines":
|
|
212
|
+
line = parse_insert_line_arg(record.args)
|
|
213
|
+
if line is not None:
|
|
214
|
+
data["insert_line"] = line
|
|
215
|
+
|
|
216
|
+
if record.tool_name == "delete_lines":
|
|
217
|
+
line_range = parse_line_range_args(record.args)
|
|
218
|
+
if line_range is not None:
|
|
219
|
+
data["start_line"], data["end_line"] = line_range
|
|
220
|
+
|
|
164
221
|
if record.diff:
|
|
165
222
|
data["diff_lines"] = record.diff.splitlines()
|
|
166
223
|
data["old_string"] = record.before_content or ""
|
|
@@ -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}]"
|