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:
Mason Daugherty
2026-06-20 19:26:04 -04:00
committed by GitHub
parent 7ae32b0a60
commit c2d949e876
2 changed files with 91 additions and 3 deletions
@@ -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(