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
+9 -2
View File
@@ -30,8 +30,15 @@
<div class="canvas__inner" id="canvas-inner"
{# Before a chat exists this fetches nothing until `draft.js` rewrites it,
which is what "intersect once" gives for free: the panel is hidden until
it is opened, so the trigger has not fired yet. #}
hx-get="{{ "/api/chats/" ~ chat.id ~ "/canvas" if chat else "" }}"
it is opened, so the trigger has not fired yet.
The attribute is *omitted* rather than left empty, and that distinction is
the whole bug it fixes: htmx looks for the attribute's presence, not for a
value -- `if(s(t,"hx-"+r))` is `hasAttribute` -- so `hx-get=""` is a real
request for the empty path, which the browser resolves against the current
document. Opening the canvas on the new-chat screen therefore fetched the
new-chat screen and swapped the entire site into this panel. #}
{% if chat %}hx-get="/api/chats/{{ chat.id }}/canvas"{% endif %}
hx-trigger="intersect once"
hx-target="this" hx-swap="innerHTML">
<div class="panel-head">
+8
View File
@@ -89,6 +89,12 @@
{% endif %}
{% endif %}
{# Before a chat exists, both of these depend on a decision being made in
the browser -- the kind toggle and the connection select -- which the
server cannot see. So they render carrying `data-agent-only` and start
`hidden`; `ui.js` shows them when `lembas:agent-target` names a
connection and hides them again when it stops. On a chat that exists
the server's answer is complete and neither attribute appears. #}
{% if canvas_enabled %}
{# Nearest the conversation of the three, being the widest and the one
most likely to be open beside it. All three share one slot: at
@@ -96,6 +102,7 @@
chat. #}
<button class="btn btn--icon" type="button" aria-label="Canvas"
title="Open a file beside the conversation"
{% if not chat %}data-agent-only hidden{% endif %}
aria-expanded="false" data-toggle="#canvas" data-toggle-group="side">
{{ icon("file-text") }}
</button>
@@ -106,6 +113,7 @@
toggle group in app.js. #}
<button class="btn btn--icon" type="button" aria-label="Terminal"
title="Open a shell on {{ agent_profile.name if agent_profile else 'this connection' }}"
{% if not chat %}data-agent-only hidden{% endif %}
aria-expanded="false" data-toggle="#terminal" data-toggle-group="side">
{{ icon("terminal") }}
</button>