mirror of
https://github.com/langchain-ai/deepagents.git
synced 2026-07-21 17:25:26 -04:00
fix(code): make /timestamps toggle instant via per-footer class (#4095)
Speed up the `/timestamps` toggle so it applies instantly, even on long conversations. --- The `/timestamps` command shows or hides a timestamp footer under each chat message. The original toggle was slow on long conversations because it mounted footers lazily: turning timestamps on walked every message, recomputed `messages.children` and ran an `index()` lookup per message (O(n²) overall), then toggled a `message-timestamps-visible` class on the `#messages` container. That container class was itself a cost. The visibility selector was keyed off `#messages`, so flipping it forced Textual to re-cascade styles across the entire message subtree — O(mounted widgets) work on every toggle. This moves visibility onto the footers themselves: - **Footers are always mounted**, built alongside their message on every path (live messages, restored history, hydrated scrollback). Each is stamped with a `message-timestamp-footer-visible` class at build time when the preference is on, so restored and streamed footers render immediately without waiting for a toggle. - **Toggling no longer mounts or removes anything.** `/timestamps` flips the visible class on the already-mounted footer leaves inside a single `batch_update()`. Because the selector is now compound on the leaf rather than a descendant of `#messages`, only the footers restyle — not the whole subtree. The result is a single, bounded layout pass per toggle regardless of conversation length, with no per-message `index()` scan. Made by [Open SWE](https://openswe.vercel.app/agents/ab4b0f22-d55a-9ff6-0d11-30183d52ac5a) --------- Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
This commit is contained in:
@@ -115,6 +115,16 @@ deadlock detector when two threads cold-import overlapping modules.
|
||||
"""
|
||||
|
||||
_MESSAGE_TIMESTAMP_FOOTER_CLASS = "message-timestamp-footer"
|
||||
"""CSS class applied to individual message timestamp footer widgets."""
|
||||
|
||||
_MESSAGE_TIMESTAMP_FOOTER_VISIBLE_CLASS = "message-timestamp-footer-visible"
|
||||
"""CSS class applied to a footer widget when it should be shown.
|
||||
|
||||
Visibility is toggled on the footer leaves rather than on `#messages`: a class
|
||||
change on the container would force Textual to re-cascade styles across every
|
||||
message subtree (O(mounted widgets)), whereas flipping the leaf footers
|
||||
restyles only the footers.
|
||||
"""
|
||||
|
||||
_TIMESTAMP_FOOTER_EXCLUDED_TYPES: frozenset[MessageType] = frozenset(
|
||||
{MessageType.APP, MessageType.SUMMARIZATION}
|
||||
@@ -133,11 +143,6 @@ def _message_timestamp_footer_id(message_id: str) -> str:
|
||||
return f"{message_id}-timestamp-footer"
|
||||
|
||||
|
||||
def _is_message_timestamp_footer(widget: Widget) -> bool:
|
||||
"""Return whether `widget` is a timestamp footer."""
|
||||
return widget.has_class(_MESSAGE_TIMESTAMP_FOOTER_CLASS)
|
||||
|
||||
|
||||
def _create_model_with_deepagents_import_lock(
|
||||
model_spec: str | None = None,
|
||||
*,
|
||||
@@ -4697,7 +4702,9 @@ class DeepAgentsApp(App):
|
||||
|
||||
for widget, msg_data in reversed(hydrated_widgets):
|
||||
try:
|
||||
footer = self._build_message_timestamp_footer(msg_data)
|
||||
footer = self._build_message_timestamp_footer(
|
||||
msg_data, visible=self._message_timestamps_visible
|
||||
)
|
||||
if first_child:
|
||||
if footer is not None:
|
||||
await messages_container.mount(footer, before=first_child)
|
||||
@@ -7907,7 +7914,9 @@ class DeepAgentsApp(App):
|
||||
nodes: list[Widget] = []
|
||||
for widget, msg_data in zip(widgets, visible, strict=False):
|
||||
nodes.append(widget)
|
||||
footer = self._build_message_timestamp_footer(msg_data)
|
||||
footer = self._build_message_timestamp_footer(
|
||||
msg_data, visible=self._message_timestamps_visible
|
||||
)
|
||||
if footer is not None:
|
||||
nodes.append(footer)
|
||||
await messages_container.mount(*nodes)
|
||||
@@ -7955,38 +7964,59 @@ class DeepAgentsApp(App):
|
||||
)
|
||||
await self._mount_message(AppMessage(f"Could not load history: {e}"))
|
||||
|
||||
def _build_message_timestamp_footer(self, data: MessageData) -> Static | None:
|
||||
"""Build a visible timestamp footer for a message.
|
||||
@staticmethod
|
||||
def _build_message_timestamp_footer(
|
||||
data: MessageData, *, visible: bool
|
||||
) -> Static | None:
|
||||
"""Build a timestamp footer for a message.
|
||||
|
||||
Args:
|
||||
data: Message data carrying the timestamp.
|
||||
visible: Whether the footer should be shown immediately. New
|
||||
footers built while timestamps are on must carry the visible
|
||||
class so they render without waiting for a toggle.
|
||||
|
||||
Returns:
|
||||
A footer widget, or `None` when disabled, when the timestamp is
|
||||
invalid, or when the message type is in
|
||||
`_TIMESTAMP_FOOTER_EXCLUDED_TYPES`.
|
||||
A footer widget, or `None` when the message type is in
|
||||
`_TIMESTAMP_FOOTER_EXCLUDED_TYPES` or when the timestamp is
|
||||
invalid.
|
||||
"""
|
||||
if not self._message_timestamps_visible:
|
||||
return None
|
||||
if data.type in _TIMESTAMP_FOOTER_EXCLUDED_TYPES:
|
||||
return None
|
||||
label = format_message_timestamp(data.timestamp)
|
||||
if label is None:
|
||||
logger.warning("Invalid timestamp for message %s", data.id)
|
||||
return None
|
||||
classes = _MESSAGE_TIMESTAMP_FOOTER_CLASS
|
||||
if visible:
|
||||
classes = f"{classes} {_MESSAGE_TIMESTAMP_FOOTER_VISIBLE_CLASS}"
|
||||
return Static(
|
||||
Content.styled(label, "dim"),
|
||||
id=_message_timestamp_footer_id(data.id),
|
||||
classes=_MESSAGE_TIMESTAMP_FOOTER_CLASS,
|
||||
classes=classes,
|
||||
)
|
||||
|
||||
def _sync_message_timestamps_display(self) -> None:
|
||||
"""Apply the current visibility to every mounted timestamp footer.
|
||||
|
||||
Flips the visible class on the footer leaves directly (not on
|
||||
`#messages`) so a toggle restyles only the footers rather than
|
||||
re-cascading the entire message subtree. `batch_update` coalesces the
|
||||
relayout into a single pass.
|
||||
"""
|
||||
footers = self.query(f".{_MESSAGE_TIMESTAMP_FOOTER_CLASS}")
|
||||
if not footers:
|
||||
return
|
||||
with self.batch_update():
|
||||
footers.set_class(
|
||||
self._message_timestamps_visible,
|
||||
_MESSAGE_TIMESTAMP_FOOTER_VISIBLE_CLASS,
|
||||
)
|
||||
|
||||
async def _toggle_message_timestamp_footers(self) -> None:
|
||||
"""Toggle visible timestamp footers and persist the preference."""
|
||||
self._message_timestamps_visible = not self._message_timestamps_visible
|
||||
if self._message_timestamps_visible:
|
||||
await self._show_message_timestamp_footers()
|
||||
else:
|
||||
await self._hide_message_timestamp_footers()
|
||||
self._sync_message_timestamps_display()
|
||||
await self._persist_message_timestamps_visible()
|
||||
|
||||
async def _persist_message_timestamps_visible(self) -> None:
|
||||
@@ -8009,57 +8039,6 @@ class DeepAgentsApp(App):
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
async def _show_message_timestamp_footers(self) -> None:
|
||||
"""Insert timestamp footers under mounted message widgets."""
|
||||
try:
|
||||
messages = self.query_one("#messages", Container)
|
||||
except NoMatches:
|
||||
return
|
||||
for widget in list(messages.children):
|
||||
if (
|
||||
_is_message_timestamp_footer(widget)
|
||||
or isinstance(widget, QueuedUserMessage)
|
||||
or not widget.id
|
||||
):
|
||||
continue
|
||||
data = self._message_store.get_message(widget.id)
|
||||
if data is None:
|
||||
# Mounted widget without a backing store entry => DOM/store
|
||||
# desync; skip it but leave a breadcrumb (mirrors pruning).
|
||||
logger.debug(
|
||||
"No store entry for mounted widget %s; skipping footer",
|
||||
widget.id,
|
||||
)
|
||||
continue
|
||||
footer_id = _message_timestamp_footer_id(data.id)
|
||||
with suppress(NoMatches):
|
||||
messages.query_one(f"#{footer_id}")
|
||||
continue
|
||||
footer = self._build_message_timestamp_footer(data)
|
||||
if footer is None:
|
||||
continue
|
||||
children = list(messages.children)
|
||||
try:
|
||||
index = children.index(widget)
|
||||
except ValueError:
|
||||
await messages.mount(footer)
|
||||
continue
|
||||
next_child = children[index + 1] if index + 1 < len(children) else None
|
||||
if next_child is not None:
|
||||
await messages.mount(footer, before=next_child)
|
||||
else:
|
||||
await messages.mount(footer)
|
||||
|
||||
async def _hide_message_timestamp_footers(self) -> None:
|
||||
"""Remove all mounted timestamp footers."""
|
||||
try:
|
||||
messages = self.query_one("#messages", Container)
|
||||
except NoMatches:
|
||||
return
|
||||
for widget in list(messages.children):
|
||||
if _is_message_timestamp_footer(widget):
|
||||
await widget.remove()
|
||||
|
||||
async def _mount_message(
|
||||
self,
|
||||
widget: Static | AssistantMessage | ToolCallMessage | SkillMessage,
|
||||
@@ -8101,11 +8080,13 @@ class DeepAgentsApp(App):
|
||||
# Store message data for virtualization
|
||||
message_data = MessageData.from_widget(widget)
|
||||
if not widget.id:
|
||||
# Keep the widget DOM id == store id so timestamp-footer toggling
|
||||
# can map a mounted widget back to its MessageData.
|
||||
# Keep the widget DOM id == store id so pruning can locate a
|
||||
# mounted widget (and its timestamp footer) from its MessageData.
|
||||
widget.id = message_data.id
|
||||
self._message_store.append(message_data)
|
||||
footer = self._build_message_timestamp_footer(message_data)
|
||||
footer = self._build_message_timestamp_footer(
|
||||
message_data, visible=self._message_timestamps_visible
|
||||
)
|
||||
|
||||
await self._mount_before_queued(messages, widget)
|
||||
if footer is not None:
|
||||
|
||||
@@ -22,6 +22,7 @@ Screen {
|
||||
/* Main content goes on base layer by default */
|
||||
|
||||
.message-timestamp-footer {
|
||||
display: none;
|
||||
height: auto;
|
||||
color: $text-muted;
|
||||
text-style: dim;
|
||||
@@ -29,6 +30,15 @@ Screen {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Visibility is toggled per-footer (not via a class on #messages) so a
|
||||
* `/timestamps` toggle restyles only the footer leaves instead of forcing a
|
||||
* re-cascade across every message subtree.
|
||||
*/
|
||||
.message-timestamp-footer.message-timestamp-footer-visible {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Chat area - main scrollable messages area */
|
||||
#chat {
|
||||
height: 1fr;
|
||||
|
||||
@@ -4167,7 +4167,7 @@ class TestMessageTimestampFooters:
|
||||
async def test_toggle_adds_and_removes_footers(
|
||||
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""The `/timestamps` toggle adds and removes visible footer widgets."""
|
||||
"""The `/timestamps` toggle shows and hides existing footer widgets."""
|
||||
previous_tz = os.environ.get("TZ")
|
||||
monkeypatch.setenv("TZ", "UTC")
|
||||
# Sandbox config so the toggle's persistence starts from "hidden" and
|
||||
@@ -4187,23 +4187,21 @@ class TestMessageTimestampFooters:
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
await app._mount_message(UserMessage("hello", id="msg-fixed"))
|
||||
data = app._message_store.get_message("msg-fixed")
|
||||
assert data is not None
|
||||
data.timestamp = 1_704_110_405.0
|
||||
|
||||
footer = app.query_one("#msg-fixed-timestamp-footer", Static)
|
||||
assert footer.display is False
|
||||
|
||||
await app._toggle_message_timestamp_footers()
|
||||
await pilot.pause()
|
||||
|
||||
footer = app.query_one("#msg-fixed-timestamp-footer", Static)
|
||||
rendered = footer.render()
|
||||
assert isinstance(rendered, Content)
|
||||
assert rendered.plain == "Jan 1, 12:00:05 PM"
|
||||
assert footer.display is True
|
||||
|
||||
await app._toggle_message_timestamp_footers()
|
||||
await pilot.pause()
|
||||
|
||||
with pytest.raises(NoMatches):
|
||||
app.query_one("#msg-fixed-timestamp-footer", Static)
|
||||
footer = app.query_one("#msg-fixed-timestamp-footer", Static)
|
||||
assert footer.display is False
|
||||
finally:
|
||||
if previous_tz is None:
|
||||
monkeypatch.delenv("TZ", raising=False)
|
||||
@@ -4211,6 +4209,238 @@ class TestMessageTimestampFooters:
|
||||
monkeypatch.setenv("TZ", previous_tz)
|
||||
self._sync_tz()
|
||||
|
||||
def test_build_footer_formats_timestamp(
|
||||
self, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""Timestamp footer labels are formatted; `visible` stamps the class.
|
||||
|
||||
`visible` is the only build-time lever over footer visibility, so it is
|
||||
asserted in both directions: a footer built with `visible=False` must
|
||||
not carry the visible class, and one built with `visible=True` must.
|
||||
"""
|
||||
from deepagents_code.widgets.message_store import MessageData, MessageType
|
||||
|
||||
previous_tz = os.environ.get("TZ")
|
||||
monkeypatch.setenv("TZ", "UTC")
|
||||
monkeypatch.setattr(
|
||||
"deepagents_code.formatting.uses_24_hour_clock", lambda: False
|
||||
)
|
||||
self._sync_tz()
|
||||
app = DeepAgentsApp()
|
||||
data = MessageData(
|
||||
type=MessageType.USER,
|
||||
content="hello",
|
||||
id="msg-fixed",
|
||||
timestamp=1_704_110_405.0,
|
||||
)
|
||||
|
||||
try:
|
||||
footer = app._build_message_timestamp_footer(data, visible=False)
|
||||
|
||||
assert footer is not None
|
||||
rendered = footer.render()
|
||||
assert isinstance(rendered, Content)
|
||||
assert rendered.plain == "Jan 1, 12:00:05 PM"
|
||||
assert not footer.has_class("message-timestamp-footer-visible")
|
||||
|
||||
visible_footer = app._build_message_timestamp_footer(data, visible=True)
|
||||
assert visible_footer is not None
|
||||
assert visible_footer.has_class("message-timestamp-footer-visible")
|
||||
finally:
|
||||
if previous_tz is None:
|
||||
monkeypatch.delenv("TZ", raising=False)
|
||||
else:
|
||||
monkeypatch.setenv("TZ", previous_tz)
|
||||
self._sync_tz()
|
||||
|
||||
async def test_toggle_positions_footer_after_each_message(self) -> None:
|
||||
"""Message mounting keeps one footer directly after every message."""
|
||||
from deepagents_code.app import _message_timestamp_footer_id
|
||||
|
||||
app = DeepAgentsApp()
|
||||
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
for index in range(5):
|
||||
await app._mount_message(UserMessage("hi", id=f"msg-{index}"))
|
||||
await pilot.pause()
|
||||
|
||||
messages = app.query_one("#messages", Container)
|
||||
children = list(messages.children)
|
||||
for index in range(5):
|
||||
message = app.query_one(f"#msg-{index}", UserMessage)
|
||||
position = children.index(message)
|
||||
footer = children[position + 1]
|
||||
assert footer.id == _message_timestamp_footer_id(f"msg-{index}")
|
||||
|
||||
async def test_repeated_toggles_do_not_mount_or_remove_footers(self) -> None:
|
||||
"""Toggling flips visibility only; it never mounts or removes footers."""
|
||||
app = DeepAgentsApp()
|
||||
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
await app._mount_message(UserMessage("hi", id="msg-dup"))
|
||||
await pilot.pause()
|
||||
|
||||
assert len(app.query("#msg-dup-timestamp-footer")) == 1
|
||||
await app._toggle_message_timestamp_footers()
|
||||
await app._toggle_message_timestamp_footers()
|
||||
await pilot.pause()
|
||||
|
||||
assert len(app.query("#msg-dup-timestamp-footer")) == 1
|
||||
|
||||
async def test_toggle_with_no_footers_is_a_noop(
|
||||
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""Toggling on an empty thread takes the no-footer early-return safely.
|
||||
|
||||
With zero footers mounted, `_sync_message_timestamps_display` queries an
|
||||
empty `DOMQuery` and returns early. This guards that realistic path
|
||||
(open app, run `/timestamps` before sending anything) against a crash,
|
||||
and confirms the visibility flag still flips as the source of truth.
|
||||
"""
|
||||
# Sandbox the config so the toggle's persistence never touches the real
|
||||
# user config on disk.
|
||||
config = tmp_path / "config.toml"
|
||||
monkeypatch.setattr("deepagents_code.model_config.DEFAULT_CONFIG_PATH", config)
|
||||
app = DeepAgentsApp()
|
||||
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
assert not app.query(".message-timestamp-footer")
|
||||
|
||||
before = app._message_timestamps_visible
|
||||
await app._toggle_message_timestamp_footers()
|
||||
await pilot.pause()
|
||||
|
||||
assert not app.query(".message-timestamp-footer")
|
||||
assert app._message_timestamps_visible is not before
|
||||
|
||||
async def test_footers_visible_on_startup_when_preference_saved(
|
||||
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""A saved `visible` preference shows footers on startup without toggling.
|
||||
|
||||
Footers are always mounted; new ones built while the preference is on
|
||||
carry the visible class so they render without waiting for a toggle.
|
||||
This guards the restart-visibility path.
|
||||
"""
|
||||
# Sandbox config with the preference persisted as visible so __init__
|
||||
# loads `True` and new footers build with the visible class.
|
||||
config = tmp_path / "config.toml"
|
||||
config.write_text("[ui]\nshow_message_timestamps = true\n")
|
||||
monkeypatch.setattr("deepagents_code.model_config.DEFAULT_CONFIG_PATH", config)
|
||||
app = DeepAgentsApp()
|
||||
assert app._message_timestamps_visible is True
|
||||
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
await app._mount_message(UserMessage("hello", id="msg-start"))
|
||||
await pilot.pause()
|
||||
|
||||
footer = app.query_one("#msg-start-timestamp-footer", Static)
|
||||
assert footer.has_class("message-timestamp-footer-visible")
|
||||
assert footer.display is True
|
||||
|
||||
async def test_footers_render_for_restored_thread_history(
|
||||
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""Restoring an old thread builds visible footers for its messages."""
|
||||
from deepagents_code.app import _ThreadHistoryPayload
|
||||
from deepagents_code.widgets.message_store import MessageData, MessageType
|
||||
|
||||
config = tmp_path / "config.toml"
|
||||
config.write_text("[ui]\nshow_message_timestamps = true\n")
|
||||
monkeypatch.setattr("deepagents_code.model_config.DEFAULT_CONFIG_PATH", config)
|
||||
app = DeepAgentsApp()
|
||||
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
payload = _ThreadHistoryPayload(
|
||||
[
|
||||
MessageData(
|
||||
type=MessageType.USER,
|
||||
content="restored",
|
||||
id="hist-msg",
|
||||
timestamp=1_704_110_405.0,
|
||||
),
|
||||
# Excluded types never receive a footer, even on restore.
|
||||
MessageData(
|
||||
type=MessageType.APP,
|
||||
content="Resumed thread",
|
||||
id="hist-app",
|
||||
timestamp=1_704_110_406.0,
|
||||
),
|
||||
],
|
||||
0,
|
||||
"",
|
||||
)
|
||||
await app._load_thread_history(
|
||||
thread_id="t-restored", preloaded_payload=payload
|
||||
)
|
||||
await pilot.pause()
|
||||
|
||||
footer = app.query_one("#hist-msg-timestamp-footer", Static)
|
||||
assert footer.display is True
|
||||
# Footer sits directly after its message in the DOM.
|
||||
messages = app.query_one("#messages", Container)
|
||||
children = list(messages.children)
|
||||
anchor = app.query_one("#hist-msg", UserMessage)
|
||||
assert children[children.index(anchor) + 1] is footer
|
||||
# Excluded-type messages get no footer, even on restore.
|
||||
with pytest.raises(NoMatches):
|
||||
app.query_one("#hist-app-timestamp-footer", Static)
|
||||
|
||||
async def test_footers_render_for_hydrated_messages_above(
|
||||
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""Scroll-up hydration of older messages builds visible footers."""
|
||||
from deepagents_code.app import _ThreadHistoryPayload
|
||||
from deepagents_code.widgets.message_store import MessageData, MessageType
|
||||
|
||||
config = tmp_path / "config.toml"
|
||||
config.write_text("[ui]\nshow_message_timestamps = true\n")
|
||||
monkeypatch.setattr("deepagents_code.model_config.DEFAULT_CONFIG_PATH", config)
|
||||
app = DeepAgentsApp()
|
||||
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
# Shrink the window so a small load archives messages above the
|
||||
# visible range, mirroring a long thread scrolled to the bottom.
|
||||
monkeypatch.setattr(app._message_store, "WINDOW_SIZE", 2)
|
||||
payload = _ThreadHistoryPayload(
|
||||
[
|
||||
MessageData(
|
||||
type=MessageType.USER,
|
||||
content=f"m{index}",
|
||||
id=f"hist-{index}",
|
||||
timestamp=1_704_110_400.0 + index,
|
||||
)
|
||||
for index in range(4)
|
||||
],
|
||||
0,
|
||||
"",
|
||||
)
|
||||
await app._load_thread_history(
|
||||
thread_id="t-long", preloaded_payload=payload
|
||||
)
|
||||
await pilot.pause()
|
||||
|
||||
# Older messages start archived (no widget/footer mounted yet).
|
||||
with pytest.raises(NoMatches):
|
||||
app.query_one("#hist-0-timestamp-footer", Static)
|
||||
|
||||
await app._hydrate_messages_above()
|
||||
await pilot.pause()
|
||||
|
||||
footer = app.query_one("#hist-0-timestamp-footer", Static)
|
||||
assert footer.display is True
|
||||
# Footer sits directly after its hydrated message in the DOM.
|
||||
messages = app.query_one("#messages", Container)
|
||||
children = list(messages.children)
|
||||
anchor = app.query_one("#hist-0", UserMessage)
|
||||
assert children[children.index(anchor) + 1] is footer
|
||||
|
||||
async def test_mount_message_adds_footer_when_enabled(self) -> None:
|
||||
"""New messages receive a footer while timestamps are enabled."""
|
||||
app = DeepAgentsApp()
|
||||
|
||||
Reference in New Issue
Block a user