An edge that is not drawn, and a panel that stopped eating the site

`hx-get=""` is not "fetch nothing". htmx looks for the attribute, not for a
value, so the empty one the canvas rendered before a chat existed was a real
request for the empty path -- which the browser resolves against the current
document. Opening the canvas on the new-chat screen fetched the new-chat screen
and swapped the whole site into the panel. The attribute is omitted now, and a
test refuses an empty verb anywhere on the page.

Which panels can exist is the server's answer; which are offered is the
browser's. Both need an agent chat on a chosen connection, and before a chat
exists those are controls in the composer -- so answering with the first profile
offered a terminal on an ordinary chat with nothing selected. They follow
`lembas:agent-target` now, and an open panel whose target goes away is closed
rather than left showing one machine under another's name.

`.tabs__body` is only sometimes the scroller: true where the tabs are a bounded
flex child, false under the admin layout, where the page scrolls instead. So
setting its scrollTop on every tab change had never once run on /admin/prompts,
silently, while the reader was dragged to the bottom of a document that had just
got shorter. The rule names the position now, and the handler finds the
container that actually scrolls.

The two top borders come off. They were what made the misalignment at the bottom
of the shell visible; `--footer-height` stays, because two ends at different
heights are visible without a line to prove it. The top of the shell keeps its
line -- there, everything is `--header-height` and aligns by construction.

And one version. pyproject carried its own copy and had drifted three minors
from the one everything actually reads.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jaroslav Beneš
2026-08-06 09:42:06 +02:00
parent 00ce04addf
commit fa8c8ab5e8
18 changed files with 1423 additions and 984 deletions
+64 -12
View File
@@ -136,10 +136,10 @@ def test_the_finished_bubble_repeats_the_live_container_s_id():
def test_switching_a_tab_puts_its_body_back_at_the_top():
"""A tab is a radio and a panel is shown by CSS, so switching one changes
nothing about `.tabs__body` -- the element that actually scrolls. Read half
way down a long panel, switch to a short one, and the browser clamps the
kept scrollTop to that panel's bottom: what lands on screen is the end of it
above a screen of nothing, which reads as a page that failed to load.
nothing about the container that scrolls. Read half way down a long panel,
switch to a short one, and the browser clamps the kept scrollTop to that
panel's bottom: what lands on screen is the end of it above a screen of
nothing, which reads as a page that failed to load.
Nothing in CSS can reset a scroll position. Driven under a DOM stub before
committing; what is pinned here is that the listener is delegated and keyed
@@ -151,15 +151,38 @@ def test_switching_a_tab_puts_its_body_back_at_the_top():
assert "scrollTop = 0" in SOURCE
def test_the_shell_has_one_line_along_its_bottom_edge():
"""The sidebar footer and the composer sit either side of the same vertical
edge and were both content-sized, so their top borders met it at different
heights and read as one line that had been broken.
def test_the_tab_reset_finds_the_container_that_actually_scrolls():
"""Resetting `.tabs__body` was a no-op on /admin/prompts for the whole life
of the fix, because there the tabs sit inside `.admin-scroll > .admin-page`
-- a plain block -- so the body has `height: auto` and the page is what
scrolls. Setting scrollTop on an element that does not scroll says nothing
and fails nothing.
Neither can match the other by accident: the footer's height depends on
which entries a reader's permissions allow, and the composer's on how much
has been typed. So both take a `min-height` from one token, the way
`--header-height` already does this at the top of the shell.
So the handler walks up for the first ancestor that can scroll, and the CSS
stops claiming `.tabs__body` is one everywhere. Both halves are pinned:
either alone leaves the bug.
"""
admin = (ROOT / "web/static/css/admin.css").read_text(encoding="utf-8")
# The scroller rule names where the tabs must be, not just the class.
assert ".main > .tabs > .tabs__body" in admin
assert "\n.tabs__body {" not in admin
assert "overflowY" in SOURCE
assert "scrollHeight > " in SOURCE
def test_the_two_ends_of_the_shell_stay_level():
"""The sidebar footer and the composer sit either side of the same vertical
edge and are both content-sized, so without a common floor they end at
different heights: the footer's depends on which entries a reader's
permissions allow, and the composer's on how much has been typed.
They used to carry a top border each, which is how the misalignment was
noticed. The borders are gone -- content scrolls under both edges, and an
undrawn edge reads better than one that has to be aligned -- and the shared
`min-height` stays, because two ends at different heights is visible without
a border to prove it.
"""
tokens = (ROOT / "web/static/css/tokens.css").read_text(encoding="utf-8")
app = (ROOT / "web/static/css/app.css").read_text(encoding="utf-8")
@@ -172,6 +195,35 @@ def test_the_shell_has_one_line_along_its_bottom_edge():
assert "min-height: var(--footer-height)" in app
assert "min-height: var(--footer-height)" in chat
# And neither draws the line again. Scoped to each rule's own block, so an
# unrelated border elsewhere in either file does not fail this.
footer = app.split(".sidebar__footer {")[1].split("}")[0]
composer = chat.split(".composer {")[1].split("}")[0]
assert "border-top" not in footer
assert "border-top" not in composer
def test_the_panel_buttons_wait_for_a_connection_before_they_appear():
"""The canvas and the terminal both need an agent chat on a chosen
connection. Before a chat exists both of those live in the composer -- a
radio group and a select -- so the server cannot answer, and answering with
the first profile anyway offered a terminal on an ordinary chat with nothing
selected. Pressing it opened a panel that could not work.
So the buttons render hidden and follow the event the composer already
dispatches. Driven under a DOM stub; pinned here is that they are keyed on
an attribute rather than on their own ids -- a third panel would otherwise
need this written a third time.
"""
index = (TEMPLATES / "chat/index.html").read_text(encoding="utf-8")
assert index.count("{% if not chat %}data-agent-only hidden{% endif %}") == 2
assert "[data-agent-only]" in SOURCE
assert "lembas:agent-target" in SOURCE
# An open panel pointed at a connection that has gone is closed, or it shows
# one machine's files under a heading naming another.
assert "setPanel" in SOURCE
def test_the_canvas_open_button_posts_through_htmx():
"""The button cannot carry an `hx-post`: the path is not known until the