ec12c3a981
A folder was a name and nothing else -- and not even that, since PATCH could rename one and nothing in the interface ever called it. It now carries a description, a system prompt, and seeds for the model, the kind and the agent target, with a settings page behind the row. The prompt is a fourth rung on the ladder, chat > folder > model > instance, and it goes above the model deliberately: a model's prompt describes the model wherever it is used, a folder's describes this piece of work whichever model is pointed at it. It is read when a reply is built rather than copied when a chat is made, so editing it reaches the chats already there, and the walk up the parents is bounded and cycle-safe because it runs on the request path. `api/pages.py` mirrors the ladder for the settings panel and had to gain the same rung -- a panel naming the wrong source is worse than one naming none, because it is believed. The seeds fill in what the request left empty and nothing it filled in: the folder says what this work usually needs, the screen in front of somebody says what they want this time. `ssh_profile_id` is a plain string rather than a foreign key, for the reason `compacted_through_id` is, so it is validated on read. Getting *into* a folder needed fixing too. `/api/chats/start` has accepted a folder_id since folders existed and nothing ever sent one, so the only route in was to make the chat elsewhere and move it. There is a New chat here on the row now, and `?folder=` on the new-chat screen. Naming is a themed dialog, and deliberately not htmx's hx-prompt: htmx calls the browser's prompt() synchronously and only then fires htmx:prompt with the answer already in hand, so intercepting the event cannot supply a different one and the grey box appears anyway. `data-prompt` follows the data-confirm-button shape instead -- swallow the click, ask, write the answer into hx-vals, click again behind a guard. JSON.stringify rather than concatenation, or a folder called `"` produces hx-vals that does not parse and the rename silently does nothing. Driven under a DOM stub, and there is a test that no template brings hx-prompt back. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
105 lines
3.9 KiB
HTML
105 lines
3.9 KiB
HTML
{% from "_macros.html" import icon, brand, model_avatar %}
|
|
{#
|
|
Sidebar: brand, new chat, pinned models, the folder tree, then unfiled chats.
|
|
|
|
"New chat" is a link, not a button that creates a row. The chat is written
|
|
when the first message is sent, so opening one and walking away leaves
|
|
nothing behind.
|
|
#}
|
|
<aside class="sidebar" id="sidebar">
|
|
<div class="sidebar__header">
|
|
{{ brand(uid="side") }}
|
|
</div>
|
|
|
|
{% if can.get("chat.create") or can.get("folder.manage") %}
|
|
<div class="sidebar__actions">
|
|
{% if can.get("chat.create") %}
|
|
{# 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") %}
|
|
{# Asks for the name rather than making "New folder" and leaving somebody to
|
|
find the rename. `data-prompt` writes the answer into hx-vals before the
|
|
request goes out; see ui.js for why this is not htmx's own hx-prompt. #}
|
|
<button class="btn btn--icon" hx-post="/api/folders" hx-swap="none"
|
|
data-prompt="What should this folder be called?"
|
|
data-prompt-title="New folder" data-prompt-field="name"
|
|
data-prompt-label="Create"
|
|
aria-label="New folder" title="New folder">
|
|
{{ icon("folder") }}
|
|
</button>
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
{# Every 10s, refresh the unread dots and announce anything that finished
|
|
while this page was showing something else. Out-of-band spans only, so the
|
|
folder tree keeps its open/closed state. #}
|
|
<div hidden hx-get="/api/chats/unread" hx-trigger="every 10s"
|
|
hx-swap="none"></div>
|
|
|
|
<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. #}
|
|
<div class="nav-group">
|
|
<div class="nav-group__label">Pinned models</div>
|
|
{% for model in pinned_models %}
|
|
<a class="nav-item nav-item--model" href="/chat?model={{ model.model_id }}">
|
|
{{ model_avatar(model, cls="nav-item__avatar") }}
|
|
<span class="nav-item__label">{{ model.label }}</span>
|
|
</a>
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% include "partials/_sidebar_tree.html" %}
|
|
</nav>
|
|
|
|
<div class="sidebar__footer">
|
|
{% if can.get("library.use") %}
|
|
<a class="nav-item" href="/library/knowledge">
|
|
{{ icon("archive", "icon--sm") }}
|
|
<span class="nav-item__label">Library</span>
|
|
</a>
|
|
{% endif %}
|
|
|
|
{% if can.get("agent.ssh") %}
|
|
<a class="nav-item" href="/agents">
|
|
{{ icon("server", "icon--sm") }}
|
|
<span class="nav-item__label">Connections</span>
|
|
</a>
|
|
{% endif %}
|
|
|
|
<a class="nav-item" href="/settings">
|
|
{{ icon("user", "icon--sm") }}
|
|
<span class="nav-item__label">{{ user.name }}</span>
|
|
{% if user.is_admin %}<span class="badge badge--leaf">admin</span>{% endif %}
|
|
</a>
|
|
|
|
<div class="sidebar__tools">
|
|
{% if user.is_admin %}
|
|
<a class="btn btn--icon" href="/admin" aria-label="Admin settings" title="Admin settings">
|
|
{{ icon("shield") }}
|
|
</a>
|
|
{% endif %}
|
|
<button class="btn btn--icon" type="button" data-theme-toggle
|
|
aria-label="Switch theme" title="Switch theme">
|
|
<span class="theme-icon theme-icon--dark">{{ icon("moon") }}</span>
|
|
<span class="theme-icon theme-icon--light">{{ icon("sun") }}</span>
|
|
</button>
|
|
<span class="spacer"></span>
|
|
<form method="post" action="/auth/logout">
|
|
<button class="btn btn--icon" type="submit" aria-label="Sign out" title="Sign out">
|
|
{{ icon("logout") }}
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</aside>
|