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:
+19
-11
@@ -18,6 +18,8 @@ from lembas.db.models import KIND_AGENT, Chat, Connection, Model, SshProfile, Us
|
||||
from lembas.services import settings_store
|
||||
from lembas.services.agent import policy
|
||||
|
||||
from .conftest import control_named
|
||||
|
||||
|
||||
def _agent_chat(db, *, mode: str = policy.MODE_MANUAL) -> Chat:
|
||||
"""An agent chat pointed at a profile that is never actually connected to.
|
||||
@@ -93,22 +95,28 @@ def test_the_mode_form_uses_a_method_the_route_serves(client: TestClient, db, re
|
||||
assert chat.agent_mode == policy.MODE_MANUAL
|
||||
|
||||
|
||||
def test_the_rendered_form_patches(client: TestClient, db, registered):
|
||||
"""And that the template actually carries it, since that is where it broke.
|
||||
def test_the_mode_select_carries_its_own_verb(client: TestClient, db, registered):
|
||||
"""The request must hang off the element the event fires on.
|
||||
|
||||
Pinned to the mode form by its id rather than to any `hx-patch` on the
|
||||
page: the model picker and the system-prompt box patch the same URL, so a
|
||||
looser assertion would have passed throughout the entire life of the bug.
|
||||
This is the invariant, and the previous version of this test did not check
|
||||
it. `hx-patch` lived on an empty sibling `<form>` that the select pointed at
|
||||
with `form="…"`, which was enough to make the markup look right and enough
|
||||
to make every assertion here pass -- while htmx bound the `change` listener
|
||||
to the form, and `change` fires on the select and bubbles to its *ancestors*
|
||||
only. The mode never once reached the database.
|
||||
|
||||
So: assert on the control, by name, whichever element that turns out to be.
|
||||
"""
|
||||
chat = _agent_chat(db)
|
||||
body = client.get(f"/chat/{chat.id}").text
|
||||
|
||||
assert 'id="agent-mode-form"' in body
|
||||
form = body[body.index('id="agent-mode-form"') :][:200]
|
||||
assert f'hx-patch="/api/chats/{chat.id}"' in form
|
||||
assert "hx-post" not in form
|
||||
# And that the select outside it is actually submitted by it.
|
||||
assert 'form="agent-mode-form"' in body
|
||||
select = control_named(body, "agent_mode")
|
||||
assert select["hx-patch"] == f"/api/chats/{chat.id}"
|
||||
assert "hx-post" not in select
|
||||
# And the empty form still scopes the values, so the PATCH carries this
|
||||
# field alone rather than the whole composer -- `project_dir` in a PATCH is
|
||||
# a 409.
|
||||
assert select["form"] == "agent-mode-form"
|
||||
|
||||
|
||||
def test_the_mode_is_offered_beside_the_composer_not_in_the_topbar(
|
||||
|
||||
Reference in New Issue
Block a user