The composer decides what a chat is, and the topbar stops trying
The mode select in the topbar posted with hx-post against a route that only answers PATCH, so every change returned 405 and the mode never moved. htmx shows nothing when a request fails, so the control looked like it worked: the select stayed where you put it and the server ignored you. It has never worked. Two more of the same kind. A mode could not be chosen at all until the chat existed, so reaching Plan meant sending something in Manual first and letting the model answer under the wrong rules. And the project directory box was real and submitted, but unlabelled and squeezed to a few characters by the select beside it, so it read as broken -- which is how it was reported. So the kind, the connection, the directory and the mode move out of the strip above the text and into one toolbar row beneath it, where attach and send already are. The directory becomes a button that opens a browser over SFTP, because a path is something you would rather find than spell. `scan_dir` is new beside `list_dir`: a picker has to tell a directory from a file before it can draw the row, and `list_dir` backs a tool whose contract is a list of names and must not change under a model mid-conversation. Browsing is a person clicking, not a model calling, so it does not pass through policy.py -- the same argument the terminal panel rests on. It does mean Manual mode has a second exception now. Also: .chip was two components with one name, and the attachment card won, so the Chat/Agent pills silently wore its padding. --radius-md was used twice and declared nowhere, so both fell back to 0. .btn.is-active has been set by syncToggles since the terminal landed and styled by nothing. Enter-to-send ignored isComposing, so committing an IME candidate sent the message. The terminal had five colours of a sixteen-colour palette, with fallbacks from a palette that no longer exists. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -333,6 +333,17 @@
|
||||
return;
|
||||
}
|
||||
|
||||
/* A menu item is an action, so the menu has served its purpose the moment
|
||||
one is pressed. Only `choose` used to close anything, which left the
|
||||
attach menu standing open over the composer after picking from it. The
|
||||
item's own handler -- htmx, or the [data-attach] and [data-toggle]
|
||||
delegates in app.js -- still runs; this only puts the menu away. */
|
||||
var item = event.target.closest('[data-picker-menu] [role="menuitem"]');
|
||||
if (item) {
|
||||
close(item.closest("[data-picker]"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!event.target.closest("[data-picker-menu]")) closeAll(null);
|
||||
});
|
||||
|
||||
@@ -489,13 +500,21 @@ document.addEventListener("lembas:notify", function (event) {
|
||||
*/
|
||||
(function () {
|
||||
function wire(root) {
|
||||
var kind = root.querySelector("#chat-kind") ||
|
||||
root.parentNode.querySelector("#chat-kind");
|
||||
var extra = root.querySelector(".composer__kind-agent");
|
||||
var kind = root.querySelector("#chat-kind");
|
||||
var extra = root.querySelector("[data-agent-extra]");
|
||||
var picker = root.querySelector('select[name="ssh_profile_id"]');
|
||||
var dir = root.querySelector('input[name="project_dir"]');
|
||||
var dir = root.querySelector("[data-dir-value]");
|
||||
var dirLabel = root.querySelector("[data-dir-label]");
|
||||
if (!kind || !extra) return;
|
||||
|
||||
/* The directory is a hidden field plus a button, so the two have to be set
|
||||
together or the button shows one path and the form submits another. */
|
||||
function setDir(value) {
|
||||
if (!dir) return;
|
||||
dir.value = value || "";
|
||||
if (dirLabel) dirLabel.textContent = value || "the login directory";
|
||||
}
|
||||
|
||||
function sync() {
|
||||
var chosen = root.querySelector('input[name="kind_choice"]:checked');
|
||||
var agent = chosen && chosen.value === "agent";
|
||||
@@ -503,17 +522,37 @@ document.addEventListener("lembas:notify", function (event) {
|
||||
extra.hidden = !agent;
|
||||
}
|
||||
|
||||
function profileDefault() {
|
||||
var option = picker && picker.options[picker.selectedIndex];
|
||||
return (option && option.dataset.dir) || "";
|
||||
}
|
||||
|
||||
root.addEventListener("change", function (event) {
|
||||
if (event.target.name === "kind_choice") sync();
|
||||
// Following the profile's own directory is a convenience, not a rule:
|
||||
// once someone has typed their own it is left alone.
|
||||
// once someone has chosen their own it is left alone.
|
||||
if (event.target === picker && dir && !dir.dataset.touched) {
|
||||
var option = picker.options[picker.selectedIndex];
|
||||
dir.value = (option && option.dataset.dir) || "";
|
||||
setDir(profileDefault());
|
||||
}
|
||||
});
|
||||
if (dir) dir.addEventListener("input", function () { dir.dataset.touched = "1"; });
|
||||
|
||||
root.addEventListener("click", function (event) {
|
||||
if (!event.target.closest("[data-dir-browse]")) return;
|
||||
event.preventDefault();
|
||||
var profileId = picker ? picker.value : "";
|
||||
if (!profileId) return;
|
||||
window.lembas.chooseDirectory(profileId, dir.value, function (chosen) {
|
||||
dir.dataset.touched = "1";
|
||||
setDir(chosen);
|
||||
});
|
||||
});
|
||||
|
||||
sync();
|
||||
/* Seeded from whichever profile the select is actually showing, not from
|
||||
the first in the list -- an unverified first profile renders `disabled`,
|
||||
so the two disagreed and the box offered a directory on a machine the
|
||||
chat was not going to use. */
|
||||
setDir(profileDefault());
|
||||
}
|
||||
|
||||
function scan() {
|
||||
|
||||
Reference in New Issue
Block a user