mirror of
https://github.com/langchain-ai/deepagents.git
synced 2026-07-22 09:45:24 -04:00
feat(code): gate paste auto-collapse behind display.collapse_pastes (#4473)
Add `display.collapse_pastes` (`[ui].collapse_pastes` / `DEEPAGENTS_CODE_COLLAPSE_PASTES`) to toggle auto-collapsing of large chat-input pastes. --- The CLI always collapsed large chat-input pastes into compact `[Pasted text #N]` placeholders. This adds a `display.collapse_pastes` config option (env `DEEPAGENTS_CODE_COLLAPSE_PASTES` / `[ui].collapse_pastes` in `config.toml`, default enabled) so users can turn the behavior off and have pasted text inserted verbatim. The `ChatInput`/`ChatTextArea` paste paths now consult the resolved setting before collapsing. Made by [Open SWE](https://openswe.vercel.app/agents/e782ab75-8eb0-1100-d501-1f872b11c286) --------- Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
This commit is contained in:
@@ -34,6 +34,16 @@ AUTO_UPDATE = "DEEPAGENTS_CODE_AUTO_UPDATE"
|
||||
"""Toggle automatic app updates. Enabled by default; set to a falsy value
|
||||
('0', 'false', 'no', 'off', or empty) to opt out."""
|
||||
|
||||
COLLAPSE_PASTES = "DEEPAGENTS_CODE_COLLAPSE_PASTES"
|
||||
"""Collapse large chat-input pastes into `[Pasted text #N +M lines]` placeholders.
|
||||
|
||||
Enabled by default; set to a falsy value (`0`, `false`, `no`, `off`, or empty)
|
||||
to disable auto-collapsing so pasted text is inserted verbatim. Parsed by
|
||||
`classify_env_bool` (an unrecognized value falls through to the config value
|
||||
rather than forcing the default). Also settable via `[ui].collapse_pastes` in
|
||||
config.toml.
|
||||
"""
|
||||
|
||||
DEBUG = "DEEPAGENTS_CODE_DEBUG"
|
||||
"""Enable verbose debug logging and preserve the server subprocess log.
|
||||
|
||||
|
||||
@@ -820,6 +820,15 @@ _STATIC_OPTIONS: tuple[ConfigOption, ...] = (
|
||||
env_var=_env_vars.SHOW_SCROLLBAR,
|
||||
toml_keys=("ui", "show_scrollbar"),
|
||||
),
|
||||
ConfigOption(
|
||||
key="display.collapse_pastes",
|
||||
group="Display",
|
||||
summary="Collapse large chat-input pastes into compact placeholders.",
|
||||
kind=OptionKind.BOOL,
|
||||
default=True,
|
||||
env_var=_env_vars.COLLAPSE_PASTES,
|
||||
toml_keys=("ui", "collapse_pastes"),
|
||||
),
|
||||
ConfigOption(
|
||||
key="display.hide_cwd",
|
||||
group="Display",
|
||||
|
||||
@@ -151,6 +151,33 @@ def _should_collapse_chat_paste(text: str) -> bool:
|
||||
return detect_mode_prefix(text) is None and should_collapse_paste(text)
|
||||
|
||||
|
||||
def _load_collapse_pastes() -> bool:
|
||||
"""Resolve whether large chat-input pastes are collapsed into placeholders.
|
||||
|
||||
Reads `DEEPAGENTS_CODE_COLLAPSE_PASTES`, then `[ui].collapse_pastes` in
|
||||
`~/.deepagents/config.toml`, defaulting to enabled.
|
||||
|
||||
Returns:
|
||||
The resolved preference (defaults to `True`).
|
||||
"""
|
||||
from deepagents_code.config_manifest import (
|
||||
get_option,
|
||||
load_config_toml,
|
||||
resolve_scalar,
|
||||
)
|
||||
|
||||
option = get_option("display.collapse_pastes")
|
||||
if option is None:
|
||||
# Unreachable unless the manifest key is renamed without updating this
|
||||
# literal; log so that mismatch surfaces instead of silently defaulting.
|
||||
logger.warning(
|
||||
"Unknown config option %r; defaulting to enabled", "display.collapse_pastes"
|
||||
)
|
||||
return True
|
||||
value, _ = resolve_scalar(option, toml_data=load_config_toml())
|
||||
return bool(value)
|
||||
|
||||
|
||||
class CompletionOption(Static):
|
||||
"""A clickable completion option in the autocomplete popup."""
|
||||
|
||||
@@ -1006,6 +1033,15 @@ class ChatTextArea(TextArea):
|
||||
return True
|
||||
return self.text.startswith("/")
|
||||
|
||||
def _paste_collapse_enabled(self) -> bool:
|
||||
"""Return whether large pastes should be collapsed into placeholders.
|
||||
|
||||
Reads the owning `ChatInput`'s resolved preference, defaulting to
|
||||
enabled when the owner is not yet attached.
|
||||
"""
|
||||
owner = self._chat_input_owner
|
||||
return owner is None or owner._collapse_pastes
|
||||
|
||||
def _should_start_paste_burst(self, char: str) -> bool:
|
||||
"""Return whether a keypress should start paste-burst buffering.
|
||||
|
||||
@@ -1052,7 +1088,7 @@ class ChatTextArea(TextArea):
|
||||
self.post_message(self.PastedPaths(payload, parsed.paths))
|
||||
return
|
||||
|
||||
if _should_collapse_chat_paste(payload):
|
||||
if self._paste_collapse_enabled() and _should_collapse_chat_paste(payload):
|
||||
self.post_message(self.PastedText(payload))
|
||||
return
|
||||
|
||||
@@ -1387,7 +1423,7 @@ class ChatTextArea(TextArea):
|
||||
self.post_message(self.PastedPaths(event.text, parsed.paths))
|
||||
return
|
||||
|
||||
if _should_collapse_chat_paste(event.text):
|
||||
if self._paste_collapse_enabled() and _should_collapse_chat_paste(event.text):
|
||||
# Intercept the paste so Textual's default _on_paste doesn't insert
|
||||
# the full text. ChatInput stores the content and inserts a compact
|
||||
# placeholder instead.
|
||||
@@ -1658,6 +1694,12 @@ class ChatInput(Vertical):
|
||||
self._pasted_contents: dict[int, PastedContent] = {}
|
||||
self._next_paste_id = 1
|
||||
|
||||
# Whether large pastes are collapsed into `[Pasted text #N +M lines]`
|
||||
# placeholders.
|
||||
# Gated by `display.collapse_pastes` (env / `[ui].collapse_pastes`);
|
||||
# when disabled, pasted text is inserted verbatim.
|
||||
self._collapse_pastes = _load_collapse_pastes()
|
||||
|
||||
# Guard flag: set True before programmatically stripping the mode
|
||||
# prefix character so the resulting text-change event does not
|
||||
# re-evaluate mode.
|
||||
@@ -2408,7 +2450,7 @@ class ChatInput(Vertical):
|
||||
parsed = self._parse_dropped_path_payload(pasted)
|
||||
if parsed is not None:
|
||||
self._insert_pasted_paths(pasted, parsed.paths)
|
||||
elif _should_collapse_chat_paste(pasted):
|
||||
elif self._collapse_pastes and _should_collapse_chat_paste(pasted):
|
||||
self._collapse_and_insert_paste(pasted)
|
||||
else:
|
||||
self._text_area.insert(pasted)
|
||||
|
||||
@@ -4351,6 +4351,26 @@ class TestPasteCollapseHelpers:
|
||||
text = "hello [Pasted text #1] world"
|
||||
assert expand_paste_refs(text, {}) == text
|
||||
|
||||
def test_load_collapse_pastes_default_enabled(self, monkeypatch) -> None:
|
||||
"""The loader defaults to enabled when nothing overrides it."""
|
||||
from deepagents_code import config_manifest
|
||||
from deepagents_code._env_vars import COLLAPSE_PASTES
|
||||
from deepagents_code.widgets import chat_input
|
||||
|
||||
monkeypatch.delenv(COLLAPSE_PASTES, raising=False)
|
||||
monkeypatch.setattr(config_manifest, "load_config_toml", dict)
|
||||
assert chat_input._load_collapse_pastes() is True
|
||||
|
||||
def test_load_collapse_pastes_env_disables(self, monkeypatch) -> None:
|
||||
"""A falsy env var disables paste collapsing in the loader."""
|
||||
from deepagents_code import config_manifest
|
||||
from deepagents_code._env_vars import COLLAPSE_PASTES
|
||||
from deepagents_code.widgets import chat_input
|
||||
|
||||
monkeypatch.setenv(COLLAPSE_PASTES, "0")
|
||||
monkeypatch.setattr(config_manifest, "load_config_toml", dict)
|
||||
assert chat_input._load_collapse_pastes() is False
|
||||
|
||||
|
||||
class TestPasteCollapseIntegration:
|
||||
"""Integration tests for paste collapsing in ChatInput."""
|
||||
@@ -4384,6 +4404,64 @@ class TestPasteCollapseIntegration:
|
||||
assert chat._text_area.text == "short text"
|
||||
assert len(chat._pasted_contents) == 0
|
||||
|
||||
async def test_large_paste_inserted_verbatim_when_disabled(self) -> None:
|
||||
"""With collapsing disabled, a large paste is inserted in full."""
|
||||
big_text = "z" * 900
|
||||
app = _RecordingApp()
|
||||
async with app.run_test() as pilot:
|
||||
chat = app.query_one(ChatInput)
|
||||
assert chat._text_area is not None
|
||||
chat._collapse_pastes = False
|
||||
|
||||
chat.handle_external_paste(big_text)
|
||||
await pilot.pause()
|
||||
|
||||
assert chat._text_area.text == big_text
|
||||
assert "[Pasted text #1]" not in chat._text_area.text
|
||||
assert len(chat._pasted_contents) == 0
|
||||
|
||||
async def test_bracketed_paste_not_collapsed_when_disabled(self) -> None:
|
||||
"""With collapsing disabled, `_on_paste` does not collapse the paste.
|
||||
|
||||
Exercises the production `_on_paste` path (gated via
|
||||
`_paste_collapse_enabled()` -> owner `_collapse_pastes`) rather than
|
||||
`handle_external_paste`, which gates on `_collapse_pastes` directly.
|
||||
The verbatim insert is left to Textual's base `TextArea._on_paste`
|
||||
(invoked separately by MRO dispatch), so this only asserts that our
|
||||
handler took the deferral branch: no placeholder, no stored content.
|
||||
Were the helper to ignore the owner and collapse anyway, both
|
||||
assertions would fail.
|
||||
"""
|
||||
big_text = "z" * 900
|
||||
app = _RecordingApp()
|
||||
async with app.run_test() as pilot:
|
||||
chat = app.query_one(ChatInput)
|
||||
assert chat._text_area is not None
|
||||
chat._collapse_pastes = False
|
||||
|
||||
await chat._text_area._on_paste(events.Paste(big_text))
|
||||
await pilot.pause()
|
||||
|
||||
assert "[Pasted text #1]" not in chat._text_area.text
|
||||
assert len(chat._pasted_contents) == 0
|
||||
|
||||
async def test_paste_burst_flush_inserted_verbatim_when_disabled(self) -> None:
|
||||
"""With collapsing disabled, a buffered paste burst inserts in full."""
|
||||
big_text = "q" * 900
|
||||
app = _RecordingApp()
|
||||
async with app.run_test() as pilot:
|
||||
chat = app.query_one(ChatInput)
|
||||
assert chat._text_area is not None
|
||||
chat._collapse_pastes = False
|
||||
|
||||
chat._text_area._paste_burst_buffer = big_text
|
||||
await chat._text_area._flush_paste_burst()
|
||||
await pilot.pause()
|
||||
|
||||
assert chat._text_area.text == big_text
|
||||
assert "[Pasted text #1]" not in chat._text_area.text
|
||||
assert len(chat._pasted_contents) == 0
|
||||
|
||||
async def test_multi_line_paste_inserts_placeholder(self) -> None:
|
||||
"""Pasting text with > 2 newlines inserts a placeholder."""
|
||||
multi_line = "\n".join(f"line {i}" for i in range(5))
|
||||
|
||||
@@ -853,6 +853,32 @@ def test_resolve_bool_env_uses_truthy_semantics(monkeypatch) -> None:
|
||||
assert resolve_scalar(opt, toml_data={})[0] is False
|
||||
|
||||
|
||||
def test_collapse_pastes_default_enabled() -> None:
|
||||
"""Paste collapsing is enabled by default when unset."""
|
||||
opt = get_option("display.collapse_pastes")
|
||||
assert opt is not None
|
||||
assert opt.kind is OptionKind.BOOL
|
||||
assert resolve_scalar(opt, toml_data={}) == (True, "default")
|
||||
|
||||
|
||||
def test_collapse_pastes_env_disables(monkeypatch) -> None:
|
||||
"""A falsy env var disables paste collapsing."""
|
||||
opt = get_option("display.collapse_pastes")
|
||||
assert opt is not None
|
||||
monkeypatch.setenv(opt.env_var, "0")
|
||||
assert resolve_scalar(opt, toml_data={})[0] is False
|
||||
|
||||
|
||||
def test_collapse_pastes_toml_disables() -> None:
|
||||
"""A `[ui].collapse_pastes = false` entry disables paste collapsing."""
|
||||
opt = get_option("display.collapse_pastes")
|
||||
assert opt is not None
|
||||
assert resolve_scalar(opt, toml_data={"ui": {"collapse_pastes": False}}) == (
|
||||
False,
|
||||
"config.toml",
|
||||
)
|
||||
|
||||
|
||||
def test_thread_relative_time_default_matches_runtime_loader() -> None:
|
||||
"""Fresh thread config shows relative timestamps by default."""
|
||||
opt = get_option("threads.relative_time")
|
||||
|
||||
Reference in New Issue
Block a user