Two kinds of work, and a switch to say which

The sidebar rendered an agent chat and an ordinary one identically, in one
list, so hours of machine work sat among a morning's questions. A switch
below the pinned models now shows one kind at a time, stored on the account
so it follows the reader to another browser.

Three things it does that are not the obvious version:

The switch is inside the fragment it swaps. Targeting only the tree would
leave the two buttons showing the side you had just left -- the request
works and the interface says otherwise, which is the failure this codebase
keeps cataloguing.

A folder can be emptied by the filter, or have been empty all along, and
only the first is a reason to hide it. `shown_in` is that line: a folder
somebody made a moment ago and has not filled yet stays on both sides, or
it can never be found again, let alone filed into.

With agent chats switched off there is no switch, and the sidebar goes back
to showing everything rather than to one side of a fork nobody can move.
An administrator turning the feature off would otherwise strand whoever
last left the switch on Agents in an empty sidebar with no way out.

The control reuses the composer's `.segmented`, which is the same choice in
a different place, and the verb goes on the input rather than the wrapper.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jaroslav Beneš
2026-08-04 08:19:17 +02:00
co-authored by Claude Opus 5
parent b03dfa24fd
commit d7a614c96b
10 changed files with 455 additions and 48 deletions
@@ -0,0 +1,76 @@
{% from "_macros.html" import icon %}
{#
The Chat/Agent switch, the folder tree and the unfiled chats.
Its own partial because it is rendered from two places: the sidebar on every
page, and `POST /api/preferences/sidebar-kind` when the switch is flicked.
Pinned models stay above it in `sidebar.html` -- they start a chat of either
kind and are not part of what is being switched.
The switch is INSIDE the swapped fragment on purpose. Targeting only the tree
would leave the two buttons showing the side you just left, which is the same
class of failure as a control whose verb goes somewhere the event does not:
the request works and the interface says otherwise. Each button keeps a stable
id so htmx puts focus back on the one that was pressed.
`sidebar_kind` is narrowed already: `sidebar_context` drops a folder holding
nothing of this kind at any depth, so the heading below cannot appear above
nothing. It is still passed down to `_folder.html`, which needs it for its own
contents and for the children it recurses into.
#}
<div id="sidebar-tree">
{% if sidebar_split %}
{#
The same component the new-chat screen uses to pick a kind, which is the
same choice in a different place. The verb goes on the input, not on the
wrapper: `change` fires on the control and bubbles through its DOM
ancestors, and htmx binds its listener to the annotated element itself.
`hx-vals` rather than relying on the input's own value being collected --
the two radios are not inside a form, so what htmx would gather is worth
not depending on.
#}
<div class="segmented segmented--grow" role="group" aria-label="Which chats to show">
{% for option, label, glyph in [("chat", "Chats", "chat"), ("agent", "Agents", "terminal")] %}
<label class="segmented__option">
<input type="radio" name="sidebar_kind" value="{{ option }}"
id="sidebar-kind-{{ option }}"
{{ 'checked' if sidebar_kind == option }}
hx-post="/api/preferences/sidebar-kind"
hx-vals='{"kind": "{{ option }}"}'
hx-trigger="change"
hx-target="#sidebar-tree" hx-swap="outerHTML">
<span>{{ icon(glyph, "icon--sm") }} {{ label }}</span>
</label>
{% endfor %}
</div>
{% endif %}
{% if folders %}
<div class="nav-group">
<div class="nav-group__label">Folders</div>
{% for folder in folders %}
{% include "partials/_folder.html" %}
{% endfor %}
</div>
{% endif %}
<div class="nav-group">
<div class="nav-group__label">
{{ "Agent chats" if sidebar_kind == "agent" else "Chats" }}
</div>
{% if unfiled_chats %}
{% for chat_item in unfiled_chats %}
{% include "partials/_chat_link.html" %}
{% endfor %}
{% else %}
<p class="nav-empty">
{%- if sidebar_kind == "agent" -%}
No agent chats yet. Nothing is stirring out there.
{%- else -%}
No chats yet. The road begins here.
{%- endif -%}
</p>
{% endif %}
</div>
</div>