mirror of
https://github.com/langchain-ai/deepagents.git
synced 2026-07-21 17:25:26 -04:00
fix(code): show "Loading..." in /threads agent dropdown while loading (#4101)
The `/threads` agent filter dropdown now shows "Loading..." while threads are still loading. --- The agent dropdown in the `/threads` switcher is populated from visible threads (plus the configured `/agents` list), so before the disk load completes it showed only the "All agents" sentinel. Now it shows a "Loading..." label while the load is pending and no agent names are known yet, signaling that more options are on the way. Once the load completes, the dropdown rebuilds to the normal "All agents" + agent options. Made by [Open SWE](https://openswe.vercel.app/agents/e181c209-eed2-59bc-fbaa-7d77f84c4333) --------- Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
This commit is contained in:
@@ -115,6 +115,15 @@ _SCOPE_VALUE_CWD = "cwd"
|
||||
_SCOPE_VALUE_ALL = "all"
|
||||
_AGENT_SELECT_ID = "thread-agent-select"
|
||||
_AGENT_VALUE_ALL = "__all__"
|
||||
_AGENT_VALUE_LOADING = "__loading__"
|
||||
# Label shown in the agent dropdown while the disk load is still pending and no
|
||||
# agent names are known yet. The options list is derived from visible threads
|
||||
# (plus the configured `/agents` list), so before the load completes it would
|
||||
# otherwise show only the "All agents" sentinel; "Loading..." signals that more
|
||||
# options may appear. Uses a distinct transient value so the final "All agents"
|
||||
# label refreshes when the completed load swaps in the real sentinel. Matches
|
||||
# the "Loading threads..." empty-state copy.
|
||||
_AGENT_LABEL_LOADING = "Loading..."
|
||||
# Display label and filter key for threads with no stored `agent_name`. Shared
|
||||
# between the Agent column renderer, the agent dropdown options, and the filter
|
||||
# predicate so all three read identically. The parentheses distinguish the
|
||||
@@ -1053,12 +1062,21 @@ class ThreadSelectorScreen(ModalScreen[str | None]):
|
||||
(and the `_UNKNOWN_AGENT_LABEL` sentinel for threads with no agent name)
|
||||
filterable.
|
||||
|
||||
While the disk load is still pending and no agent names are known yet,
|
||||
returns a single `("Loading...", _AGENT_VALUE_LOADING)` option so the
|
||||
dropdown signals that more options are on the way rather than implying
|
||||
"All agents" is the only choice.
|
||||
|
||||
Returns:
|
||||
List of `(label, value)` pairs; the first entry is always
|
||||
`("All agents", _AGENT_VALUE_ALL)`.
|
||||
`("All agents", _AGENT_VALUE_ALL)` once the load has completed
|
||||
(or `("Loading...", _AGENT_VALUE_LOADING)` while it is still
|
||||
pending with no known agents).
|
||||
"""
|
||||
names = {t.get("agent_name") or _UNKNOWN_AGENT_LABEL for t in self._threads}
|
||||
names.update(self._available_agent_names)
|
||||
if not names and not self._disk_load_complete:
|
||||
return [(_AGENT_LABEL_LOADING, _AGENT_VALUE_LOADING)]
|
||||
ordered = sorted(names, key=str.casefold)
|
||||
return [("All agents", _AGENT_VALUE_ALL), *((n, n) for n in ordered)]
|
||||
|
||||
@@ -1256,9 +1274,15 @@ class ThreadSelectorScreen(ModalScreen[str | None]):
|
||||
classes="thread-controls-label",
|
||||
markup=False,
|
||||
)
|
||||
agent_options = self._collect_agent_options()
|
||||
agent_value = (
|
||||
_AGENT_VALUE_LOADING
|
||||
if agent_options[0][1] == _AGENT_VALUE_LOADING
|
||||
else _AGENT_VALUE_ALL
|
||||
)
|
||||
yield ThreadScopeSelect(
|
||||
self._collect_agent_options(),
|
||||
value=_AGENT_VALUE_ALL,
|
||||
agent_options,
|
||||
value=agent_value,
|
||||
allow_blank=False,
|
||||
compact=True,
|
||||
id=_AGENT_SELECT_ID,
|
||||
@@ -2312,6 +2336,8 @@ class ThreadSelectorScreen(ModalScreen[str | None]):
|
||||
if event.select.id == _AGENT_SELECT_ID:
|
||||
if self._confirming_delete:
|
||||
return
|
||||
if event.value == _AGENT_VALUE_LOADING:
|
||||
return
|
||||
new_agent = None if event.value == _AGENT_VALUE_ALL else str(event.value)
|
||||
if new_agent == self._filter_agent:
|
||||
return
|
||||
|
||||
@@ -15,6 +15,7 @@ from textual.containers import Container, Horizontal, Vertical, VerticalScroll
|
||||
from textual.css.query import NoMatches
|
||||
from textual.screen import ModalScreen
|
||||
from textual.widgets import Checkbox, Input, Select, Static
|
||||
from textual.widgets._select import SelectCurrent
|
||||
|
||||
from deepagents_code.app import DeepAgentsApp, _ThreadHistoryPayload
|
||||
from deepagents_code.sessions import ThreadInfo
|
||||
@@ -4276,6 +4277,67 @@ class TestThreadSelectorAgentFilter:
|
||||
options = screen._collect_agent_options()
|
||||
assert options[0] == ("All agents", _AGENT_VALUE_ALL)
|
||||
|
||||
def test_collect_agent_options_loading_while_pending(self) -> None:
|
||||
"""While loading with no known agents, the dropdown shows 'Loading...'."""
|
||||
from deepagents_code.widgets.thread_selector import (
|
||||
_AGENT_LABEL_LOADING,
|
||||
_AGENT_VALUE_ALL,
|
||||
_AGENT_VALUE_LOADING,
|
||||
)
|
||||
|
||||
screen = ThreadSelectorScreen(
|
||||
current_thread=None,
|
||||
initial_threads=None,
|
||||
filter_cwd=None,
|
||||
)
|
||||
assert screen._disk_load_complete is False
|
||||
assert screen._collect_agent_options() == [
|
||||
(_AGENT_LABEL_LOADING, _AGENT_VALUE_LOADING)
|
||||
]
|
||||
# Once the disk load completes with no threads, fall back to "All agents".
|
||||
screen._disk_load_complete = True
|
||||
assert screen._collect_agent_options() == [("All agents", _AGENT_VALUE_ALL)]
|
||||
|
||||
async def test_agent_select_label_refreshes_after_empty_load(self) -> None:
|
||||
"""The loading placeholder is replaced by the final all-agents label."""
|
||||
from deepagents_code.widgets.thread_selector import (
|
||||
_AGENT_SELECT_ID,
|
||||
_AGENT_VALUE_ALL,
|
||||
_AGENT_VALUE_LOADING,
|
||||
)
|
||||
|
||||
load_started = asyncio.Event()
|
||||
load_finished = asyncio.Event()
|
||||
|
||||
async def list_threads_after_signal(**_: object) -> list[ThreadInfo]:
|
||||
load_started.set()
|
||||
await load_finished.wait()
|
||||
return []
|
||||
|
||||
with (
|
||||
patch("deepagents_code.sessions.list_threads", list_threads_after_signal),
|
||||
_patch_columns(),
|
||||
_patch_available_agents([]),
|
||||
):
|
||||
app = ThreadSelectorTestApp()
|
||||
async with app.run_test() as pilot:
|
||||
app.show_selector()
|
||||
await asyncio.wait_for(load_started.wait(), timeout=1)
|
||||
await pilot.pause()
|
||||
|
||||
screen = app.screen
|
||||
assert isinstance(screen, ThreadSelectorScreen)
|
||||
agent_select = screen.query_one(f"#{_AGENT_SELECT_ID}", Select)
|
||||
assert agent_select.value == _AGENT_VALUE_LOADING
|
||||
assert str(agent_select.query_one(SelectCurrent).label) == "Loading..."
|
||||
|
||||
load_finished.set()
|
||||
await pilot.pause()
|
||||
await pilot.pause()
|
||||
|
||||
assert agent_select.value == _AGENT_VALUE_ALL
|
||||
assert str(agent_select.query_one(SelectCurrent).label) == "All agents"
|
||||
|
||||
def test_collect_agent_options_sorted_unique(self) -> None:
|
||||
"""collect_agent_options returns sorted unique agent names."""
|
||||
screen = ThreadSelectorScreen(
|
||||
|
||||
Reference in New Issue
Block a user