Three things that said one thing and did another

All three shipped in the last two commits, and all three are the same kind of
mistake: an interface that looks right and is not.

The folder settings page could not be scrolled. `.main` is a flex column with
`min-height: 0`, so a `.page` dropped straight into it overflows the viewport
with nothing to scroll -- Save and Back end up below the bottom of the window,
reachable by zooming out or by dragging the prompt textarea up out of the way.
Every other page of this shape already wraps its content in `.admin-scroll`;
this one did not. The two class names that scroll are one rule in admin.css
precisely so this is a wrapper somebody forgot rather than a value they got
wrong, and now it is noted.

The project directory was a text box, on the one screen that asks for an
absolute path on another machine. It is the same button-and-hidden-field the
new-chat screen uses, wired by `[data-dir-field]` in ui.js -- scoped to that
attribute so this and the composer's own handler cannot both answer one click
and open two dialogs. The composer keeps its own because it does more: it
follows the selected profile's default directory until somebody picks their
own, which only means something while a chat is being created. With no
connection chosen it says so rather than opening onto nothing, and Clear is
always there, because browsing somewhere and changing your mind before saving
needs a way back to "no opinion" as much as clearing a saved one does.

And "New chat" did not follow the Chat/Agent switch. The button sits above the
scroll area rather than inside the tree the switch swaps, so it went on saying
"New chat" over a list of agent chats. It moves to its own partial and arrives
out of band, the way the chat title already does. Renaming it to something
neutral would have hidden the bug rather than fixed it, and would have cost the
`?kind=agent` preselection the label is there to explain.

The tests that existed asserted a page load, which re-renders the button
anyway -- which is exactly why nobody saw it. The new ones assert the fragment.
The directory field was driven under a DOM stub first.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jaroslav Beneš
2026-08-04 10:40:01 +02:00
parent 5766446b84
commit 20040f53a8
9 changed files with 241 additions and 31 deletions
+37
View File
@@ -77,6 +77,43 @@ def test_an_unknown_kind_is_refused_rather_than_stored(client: TestClient, db, r
assert "sidebar_kind" not in (user.settings_json or {})
def test_the_new_chat_button_follows_the_switch(client: TestClient, db, registered):
"""It sits above the scroll area rather than inside the tree, so a swap of
the tree alone left it saying "New chat" while agent chats were listed
underneath -- saying one thing and doing another, which is the shape of
failure the switch itself was arranged to avoid.
Asserted on the *fragment*, not on a page load: a page load re-renders the
button anyway, which is exactly why this went unnoticed.
"""
_add_connection(db)
_enable_agents(db)
response = client.post("/api/preferences/sidebar-kind", data={"kind": "agent"})
assert 'id="sidebar-actions"' in response.text
assert 'hx-swap-oob="true"' in response.text
assert "New agent chat" in response.text
assert 'href="/chat?kind=agent"' in response.text
response = client.post("/api/preferences/sidebar-kind", data={"kind": "chat"})
assert "New agent chat" not in response.text
assert 'href="/chat"' in response.text
def test_a_page_load_carries_no_stray_out_of_band_row(
client: TestClient, db, registered
):
"""`hx-swap-oob` on a full page load would be a duplicate element sitting in
the tree, waiting to be swapped by the next unrelated request."""
_add_connection(db)
_enable_agents(db)
page = client.get("/chat").text
assert page.count('id="sidebar-actions"') == 1
assert 'id="sidebar-actions"\n hx-swap-oob' not in page
assert "hx-swap-oob" not in page.split('id="sidebar-tree"')[1][:2000]
def test_the_switch_survives_a_page_load(client: TestClient, db, registered):
_add_connection(db)
_enable_agents(db)