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:
@@ -1248,6 +1248,11 @@
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
/* The sidebar's copy fills its column rather than sitting at its content
|
||||
width: it is the heading for everything below it, not a control in a row. */
|
||||
.segmented--grow { display: flex; margin: 0 0 var(--sp-3); }
|
||||
.segmented--grow .segmented__option { flex: 1; }
|
||||
.segmented--grow .segmented__option span { flex: 1; justify-content: center; }
|
||||
|
||||
/* --- A plan, and the way to carry it out ----------------------------------- */
|
||||
.plan {
|
||||
|
||||
@@ -275,15 +275,22 @@
|
||||
#}
|
||||
{% if not chat and agent_profiles %}
|
||||
<div class="composer__context" data-agent-picker>
|
||||
<input type="hidden" name="kind" value="chat" id="chat-kind">
|
||||
{# Seeded from `?kind=`, which is how the sidebar's Agent side opens
|
||||
this screen already on the right fork. `ui.js` reads the checked
|
||||
radio when it wires the picker, so the hidden field and the
|
||||
revealed connection follow from this and nothing else. #}
|
||||
<input type="hidden" name="kind" value="{{ starting_kind | default('chat') }}"
|
||||
id="chat-kind">
|
||||
|
||||
<div class="segmented" role="group" aria-label="Kind of chat">
|
||||
<label class="segmented__option">
|
||||
<input type="radio" name="kind_choice" value="chat" checked>
|
||||
<input type="radio" name="kind_choice" value="chat"
|
||||
{{ '' if starting_kind == 'agent' else 'checked' }}>
|
||||
<span>{{ icon("chat", "icon--sm") }} Chat</span>
|
||||
</label>
|
||||
<label class="segmented__option">
|
||||
<input type="radio" name="kind_choice" value="agent">
|
||||
<input type="radio" name="kind_choice" value="agent"
|
||||
{{ 'checked' if starting_kind == 'agent' }}>
|
||||
<span>{{ icon("bolt", "icon--sm") }} Agent</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,10 @@
|
||||
<div class="nav-item {% if chat and chat.id == chat_item.id %}is-active{% endif %}"
|
||||
data-chat-id="{{ chat_item.id }}">
|
||||
<a class="nav-item__link" href="/chat/{{ chat_item.id }}">
|
||||
{{ icon("chat", "icon--sm") }}
|
||||
{# The kind, in the one place a person looks. Legible even with the switch
|
||||
off, which is what it is for: a chat that can run commands should not look
|
||||
like one that cannot. #}
|
||||
{{ icon("terminal" if chat_item.kind == "agent" else "chat", "icon--sm") }}
|
||||
<span class="nav-item__label" id="chat-link-label-{{ chat_item.id }}">{{ chat_item.title }}</span>
|
||||
{# Toggled out of band by the unread poll; see /api/chats/unread. #}
|
||||
<span id="unread-{{ chat_item.id }}" class="unread-dot"
|
||||
|
||||
@@ -32,10 +32,12 @@
|
||||
<div class="folder__contents" x-show="open" x-cloak>
|
||||
{# Bound once: the loop and the "Empty" check must be looking at the same
|
||||
list, or a folder holding only archived chats claims to be empty while
|
||||
showing them. #}
|
||||
{% set listed = folder.visible_chats %}
|
||||
showing them. Narrowed by the sidebar's switch, so a folder shows one
|
||||
kind at a time -- a folder is free to hold both. #}
|
||||
{% set listed = folder.visible_chats(sidebar_kind | default("")) %}
|
||||
{% set shown = folder.visible_children(sidebar_kind | default("")) %}
|
||||
|
||||
{% for child in folder.children %}
|
||||
{% for child in shown %}
|
||||
{% with folder = child %}
|
||||
{% include "partials/_folder.html" %}
|
||||
{% endwith %}
|
||||
@@ -45,7 +47,7 @@
|
||||
{% include "partials/_chat_link.html" %}
|
||||
{% endfor %}
|
||||
|
||||
{% if not folder.children and not listed %}
|
||||
{% if not shown and not listed %}
|
||||
<p class="nav-empty">Empty</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -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>
|
||||
@@ -14,8 +14,12 @@
|
||||
{% if can.get("chat.create") or can.get("folder.manage") %}
|
||||
<div class="sidebar__actions">
|
||||
{% if can.get("chat.create") %}
|
||||
<a class="btn btn--primary btn--grow" href="/chat">
|
||||
{{ icon("plus", "icon--sm") }} New chat
|
||||
{# Carries the side the switch is on, so "New chat" on the Agent side opens
|
||||
the new-chat screen already set to an agent chat. #}
|
||||
<a class="btn btn--primary btn--grow"
|
||||
href="/chat{{ '?kind=agent' if sidebar_kind == 'agent' else '' }}">
|
||||
{{ icon("plus", "icon--sm") }}
|
||||
{{ "New agent chat" if sidebar_kind == "agent" else "New chat" }}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if can.get("folder.manage") %}
|
||||
@@ -33,7 +37,7 @@
|
||||
<div hidden hx-get="/api/chats/unread" hx-trigger="every 10s"
|
||||
hx-swap="none"></div>
|
||||
|
||||
<nav class="sidebar__scroll" id="sidebar-tree" aria-label="Chats">
|
||||
<nav class="sidebar__scroll" aria-label="Chats">
|
||||
{% if pinned_models and can.get("chat.create") %}
|
||||
{# Shortcuts to start a chat with a particular model. These link rather than
|
||||
post, so no chat exists until something is actually said. #}
|
||||
@@ -48,25 +52,7 @@
|
||||
</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">Chats</div>
|
||||
{% if unfiled_chats %}
|
||||
{% for chat_item in unfiled_chats %}
|
||||
{% include "partials/_chat_link.html" %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p class="nav-empty">No chats yet. The road begins here.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% include "partials/_sidebar_tree.html" %}
|
||||
</nav>
|
||||
|
||||
<div class="sidebar__footer">
|
||||
|
||||
Reference in New Issue
Block a user