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>
82 lines
3.6 KiB
HTML
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>
|