Files
LLeMbas/src/lembas/web/templates/chat/_terminal.html
T
Jaroslav BenešandClaude Opus 5 52770d7ab1 Two selects that never wrote anything, and a queue
The approval card in Auto mode and the missing /effort were one bug. Both
selects hung their hx-patch on an empty sibling form reached by form="…",
and htmx binds a trigger to the annotated element: change fires on the
select and bubbles to its ancestors, which a sibling is not. The live rows
read agent_mode=manual and params_json={} while the browser showed Auto and
Effort: high. policy.py was never involved.

The verb moves onto the control; the empty form stays as value scoping,
which is the half of the CLAUDE.md note that was right. conftest gains
control_named so a test asserts the element carrying the name carries the
verb, rather than asserting the markup that was there throughout.

The composer's highlight was a third instance of the same carelessness in
CSS: .tok-mention is written for the transcript and scoped to nothing, so
the mirror painted its token in accent-coloured monospace over the
textarea's own text. Scoped under .msg; the mirror restates transparency
and font rather than inheriting them, and bleeds by box-shadow.

/effort is now offered before the first prompt and _new_chat reads it.
/index re-walks the project directory on demand, file_write drops the
listing it just invalidated, and the index ladder falls through to SFTP on
a host that refuses exec instead of returning nothing.

A second message during a reply is queued rather than starting a second
concurrent generation: a real Message row with queued set, so it survives a
restart and can be withdrawn. _drain hands one on at the end of a reply,
_inject takes one in at a tool-round boundary so an agent can be steered
mid-task. Stop leaves the queue undelivered. The terminal's Auto toggle
becomes off/copy/send, and send posts straight to the chat without touching
the composer.

@ now offers notes, skills, this chat's attachments and a URL to fetch; a
knowledge base attaches as a reference rather than a copy. copy_document
carries provenance, which was the one attach path that dropped it.

Also fixes an unrelated live bug: the round loop compared against the
global MAX_ROUNDS of 3 while sizing itself from the agent budget of 40, so
agent replies stopped after three rounds and reported forty.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 19:49:34 +02:00

82 lines
3.6 KiB
HTML

{% from "_macros.html" import icon %}
{#
The terminal panel: a fourth child of .shell, to the left of the inspector and
never open beside it. Empty on a page load -- xterm is created the first time
the panel is opened, so the 280KB it costs is paid by somebody who asked for a
shell rather than by everyone who opened a chat.
What is typed here is not run past the chat's mode or its allow and deny
lists. Those govern the model, which reads pages and files it did not write;
the person at the keyboard holds the credential and could open the same shell
with an ssh client.
#}
<aside class="terminal" id="terminal" hidden aria-label="Terminal"
data-terminal
data-url="/api/chats/{{ chat.id }}/terminal/ws"
data-label="{{ agent_profile.name if agent_profile else 'this connection' }}"
data-dir="{{ chat.project_dir }}"
data-resize-target>
{#
The left edge, dragged. A separator rather than a decoration: it takes
focus and answers the arrow keys, or the panel is only resizable with a
mouse and the grip is a focus trap that does nothing.
#}
<div class="panel-resize" data-resize="--terminal-width" data-resize-min="384"
role="separator" aria-orientation="vertical" tabindex="0"
aria-label="Resize the terminal">
{{ icon("grip", "icon--sm") }}
</div>
<div class="panel-head">
<h2 class="panel-head__title">
{{ icon("terminal", "icon--sm") }}
<span>{{ agent_profile.name if agent_profile else "Terminal" }}</span>
<span class="terminal__where" data-terminal-where>{{ chat.project_dir }}</span>
</h2>
{#
Copy, Send and Auto. All three need to know where one command ends and
the next begins, which is what the shell integration provides; without it
the first two fall back to scraping the screen and say so, and Auto is
disabled rather than degraded. Forty arbitrary lines attached to every
message is worse than nothing attached at all.
#}
<button class="btn btn--icon btn--sm" type="button" data-terminal-copy
title="Copy the last command and its output"
aria-label="Copy the last command and its output">
{{ icon("copy", "icon--sm") }}
</button>
<button class="btn btn--icon btn--sm" type="button" data-terminal-send
title="Put the last command and its output into the message box"
aria-label="Send to chat">
{{ icon("arrow-up", "icon--sm") }}
</button>
{#
Three states, not two. A cycling icon button cannot say which of three it
is in, and this one decides whether things are sent to a model without
being asked again -- so it says so in words.
Not remembered between page loads on purpose: a switch that forwards every
command you run to a model is not something to inherit from last week.
#}
<select class="select select--sm" data-terminal-auto
aria-label="What to do when a command finishes"
title="What to do with each command you run">
<option value="off" selected>Auto: off</option>
<option value="copy">Auto: copy</option>
<option value="send">Auto: send</option>
</select>
<button class="btn btn--icon btn--sm" type="button" data-toggle="#terminal"
aria-label="Close terminal">
{{ icon("x", "icon--sm") }}
</button>
</div>
<div class="terminal__screen" data-terminal-screen></div>
<div class="terminal__status">
<span class="terminal__message" data-terminal-message>Connecting…</span>
<span class="terminal__last" data-terminal-last></span>
<span>Ctrl+Shift+C / V</span>
</div>
</aside>