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>
This commit is contained in:
Jaroslav Beneš
2026-08-02 19:49:34 +02:00
parent 439f1a5d84
commit 52770d7ab1
30 changed files with 2028 additions and 80 deletions
+44 -19
View File
@@ -35,7 +35,11 @@
/* Whether this shell tells us where commands begin and end -- "live",
"loading" or "none". Everything the three buttons do keys off it. */
var integration = "loading";
var autoSend = false;
/* "off" | "copy" | "send". Three states rather than a boolean, because the
old one did the wrong one of them: it appended into the composer, on top of
whatever was being typed there. A select rather than a cycling button --
a button cannot say which of three states it is in. */
var autoMode = "off";
var lastCommand = null;
function say(text, isError) {
@@ -169,8 +173,12 @@
and the buttons fetch what they need when they are pressed. */
if (integration !== "live") { integration = "live"; applyIntegration(); }
showLast(payload.command);
if (autoSend) {
capture(true).then(function (text) { intoComposer(text, true); });
if (autoMode !== "off") {
capture(true).then(function (text) {
if (!text) return;
if (autoMode === "copy") return intoComposer(text, true);
sendStraightToChat(text);
});
}
return;
}
@@ -287,10 +295,10 @@
var usable = integration === "live";
auto.disabled = !usable;
auto.title = usable
? "Attach every command you run to your next message"
? "What to do with each command you run"
: "This shell did not load LLeMbas's command markers, so there is no way " +
"to tell where one command's output ends.";
if (!usable && autoSend) setAuto(false);
if (!usable && autoMode !== "off") setAuto("off");
}
function showLast(command) {
@@ -301,16 +309,32 @@
slot.textContent = lastCommand ? lastCommand.summary : "";
}
function setAuto(on) {
autoSend = !!on;
var button = panel.querySelector("[data-terminal-auto]");
if (button) {
button.setAttribute("aria-pressed", autoSend ? "true" : "false");
button.classList.toggle("is-active", autoSend);
}
say(autoSend
? "Every command you run will be attached to your next message."
: "Commands are no longer attached automatically.");
var AUTO_SAID = {
off: "Commands are no longer attached automatically.",
copy: "Every command you run will be put into the message box.",
send: "Every command you run will be sent as a message on its own."
};
function setAuto(mode) {
autoMode = AUTO_SAID[mode] ? mode : "off";
var select = panel && panel.querySelector("[data-terminal-auto]");
if (select && select.value !== autoMode) select.value = autoMode;
say(AUTO_SAID[autoMode]);
}
/* Sent, not typed. The composer is left entirely alone -- somebody may be
half-way through a sentence in it, and overwriting that is the complaint
this replaces. The thread receives whatever the server decides the message
is: a streaming pair, or a single queued bubble if a reply is already being
written. Nothing here needs to know which. */
function sendStraightToChat(text) {
var url = panel.dataset.url.replace(/\/terminal\/ws$/, "/messages");
if (!window.htmx) return;
window.htmx.ajax("POST", url, {
target: "#thread",
swap: "beforeend",
values: { content: text }
});
}
/* A selection always wins, in every state. People rely on it, and it is the
@@ -427,10 +451,11 @@
event.preventDefault();
return copyToClipboard();
}
if (event.target.closest("[data-terminal-auto]")) {
event.preventDefault();
return setAuto(!autoSend);
}
});
panel.addEventListener("change", function (event) {
var select = event.target.closest("[data-terminal-auto]");
if (select) setAuto(select.value);
});
/* xterm holds colours as values, not as variables, so a theme change has