The menu that never appeared, and the reason it never did
composer.js built its menu lazily inside show(), and refresh() wrote list.innerHTML before calling it. `list` is null until build() has run, so the first `/` or `@` ever typed threw a TypeError and took the handler with it. The menu has never appeared in any browser. That is why /compact "isn't there": nothing was. I shipped it having only run `node --check`, which parses the file happily. So this also brings the thing that catches it: a DOM stub driven under node -- not committed, hard rule 1 stands, it is an instrument like curl. It reproduced the crash in one run and immediately found two more: choosing a command from the menu left `/help` sitting in the box so the next Enter ran it again, and Tab completed nothing. Tab now completes and Enter runs, which is the split that matters for a command taking an argument. `.select--sm` was used three times and defined nowhere. I deleted the copy in chat.css and left a comment saying it "is defined once, in app.css", where it did not exist -- so those selects fell back to plain `.select`: width 100% in a flex row where four siblings wanted the same, all of them shrinking together until each was a few characters wide, and half a rem taller than everything beside them. That was the whole of "the connection switch needs to be wider". The connection and directory move to the topbar. They cannot change -- update_chat refuses both with a 409 -- so they are facts about the chat, of a kind with the Temporary badge, not controls on the message. The mode stays by the box. Compaction says it is working. It makes a model call that takes seconds and had no indicator anywhere: `hx-indicator` appears nowhere in this codebase, and the Generation.status channel that says "Summarising earlier messages…" for the automatic path cannot be borrowed, because it lives in the streaming bubble and this endpoint refuses to run while any message is unfinished. The overflow menu now runs the same code as /compact rather than posting for itself, so there is one implementation, one spinner, and one place the endpoint's four carefully written 409s finally reach somebody. /effort, low medium high, per chat with a per-model default. It goes out twice because there is no field that works everywhere: OpenAI and vLLM read reasoning_effort, llama.cpp's own docs say other values "have no effect" and its maintainer says the field "simply gets dropped without error or logging" -- what reaches gpt-oss behind it is chat_template_kwargs. Both are sent, and only once an effort has been chosen, so a provider strict about unknown parameters sees exactly the request it always did until somebody opts in. The control appears only on a model marked `reasoning`, a flag that has existed since the beginning with no reader at all. Mentions and recognised commands are marked as you type -- a mirror behind the textarea holding the same text with every character transparent, contributing nothing but a rounded rectangle, so a pixel of drift is a misplaced rectangle rather than a doubled glyph. A command is marked only when it resolves, so `/thoughts on this` visibly is not one before you send it. And again in the transcript, where user turns had no render step at all and now escape before they inject. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -20,7 +20,7 @@ lembas info # paths + counts, useful when confused
|
||||
lembas secret-key # generate LEMBAS_SECRET_KEY
|
||||
lembas create-admin # create or promote an admin
|
||||
|
||||
pytest # 981 tests, ~56s
|
||||
pytest # 1009 tests, ~58s
|
||||
# PLAN.md tracks what is and is not built
|
||||
ruff check . # lint (line length 100)
|
||||
python scripts/build_artwork.py # regenerate artwork (SVG + PWA icons;
|
||||
@@ -136,7 +136,7 @@ src/lembas/
|
||||
templates/ Jinja
|
||||
static/ css, js, vendor, img, sw.js
|
||||
js/commands.js the / table, and the keyboard that does the same jobs
|
||||
js/composer.js the menu that / and @ open, over the message box
|
||||
js/composer.js the menu / and @ open, and the mirror that marks them
|
||||
assets/ SVG masters and PWA icons (generated)
|
||||
deploy/ systemd unit, nginx vhost, install/update scripts
|
||||
```
|
||||
@@ -401,12 +401,59 @@ every name worth having. Watch the double-count — collapsing a parent subsumes
|
||||
child already collapsed, and adding both savings stops the loop early believing
|
||||
it has made room it has not.
|
||||
|
||||
**There is no test runner for the JavaScript, so drive it under a DOM stub.**
|
||||
Hard rule 1 keeps Node out of the *project*; it does not stop using the `node`
|
||||
on this machine as a development instrument, the way `curl` is used. This is
|
||||
not a nicety. `composer.js` built its menu lazily inside `show()` while
|
||||
`refresh()` wrote to `list` before calling it — so the first `/` or `@` ever
|
||||
typed threw on a null and took the handler with it, and the menu never appeared
|
||||
in any browser for the whole life of the feature. `node --check` parses that
|
||||
file happily. A forty-line stub of `document`, `window` and `fetch` that fires
|
||||
one `input` event catches it in a second, and caught two more on the same run:
|
||||
choosing a command from the menu left `/help` sitting in the box, and Tab did
|
||||
not complete. Anything touching these files gets driven before it is committed.
|
||||
|
||||
**Two things must be sized the same or the composer's highlighting slides off.**
|
||||
A `<textarea>` cannot style its own contents, so `.composer__mirror` sits behind
|
||||
it holding the same text with every character transparent, contributing nothing
|
||||
but a rounded rectangle behind each token. Every property that decides where a
|
||||
character lands — font, size, line height, letter spacing, padding, wrapping —
|
||||
is declared once for both. The usual version of this trick hides the textarea's
|
||||
text and shows the mirror's; drawing only backgrounds instead means a pixel of
|
||||
drift is a rectangle slightly out of place rather than a doubled glyph. The
|
||||
scroll positions are synced, because the textarea scrolls past
|
||||
`data-max-height`.
|
||||
|
||||
**A slash command must never swallow a message.** `static/js/commands.js`
|
||||
intercepts only an exact match against its table; `//` escapes, and anything
|
||||
unrecognised is sent as written. Eating somebody's message because it began with
|
||||
a slash is a far worse failure than an unknown command, and it is the one the
|
||||
implementation has to be arranged around rather than patched for afterwards.
|
||||
|
||||
**Reasoning effort goes out twice, and only when it is set.** There is no field
|
||||
that works everywhere. OpenAI and vLLM read `reasoning_effort`; llama.cpp's own
|
||||
documentation says other values "have no effect", its maintainer says
|
||||
"llama-server cannot support reasoning_effort at all" and that the field "simply
|
||||
gets dropped without error or logging", and what actually reaches a gpt-oss
|
||||
behind it is `chat_template_kwargs`. So `chat.apply_effort` writes both. The
|
||||
second half is what makes that safe: neither field appears unless a chat has an
|
||||
effort set, so a provider strict about unknown parameters sees exactly the
|
||||
request it always did until somebody opts in. `EFFORTS` lives in `services/chat.py`
|
||||
and the command, the control and the admin default all read it, so they cannot
|
||||
disagree about what a valid effort is.
|
||||
|
||||
**A control that writes needs a form it is allowed to be outside of.** Two
|
||||
selects in the composer — the agent mode and the effort — belong to empty
|
||||
`<form>` elements that are siblings of the composer's own form, referenced by
|
||||
`form="…"`. A form cannot nest inside another; the browser silently drops the
|
||||
inner one, and the control then posts nothing at all.
|
||||
|
||||
**Unused columns are worse than missing ones.** `Model.params_json` documented
|
||||
itself as "default sampling params applied to new chats using this model" and
|
||||
was applied nowhere for its entire existence, which is how a per-model default
|
||||
effort looked like it needed a new column. `_new_chat` seeds from it now. It is
|
||||
empty on every existing row, so honouring it changed nothing for anyone.
|
||||
|
||||
**`@` inserts a reference *and* attaches the contents.** The token stays in the
|
||||
sentence so "change the thing in @main.py" reads as one, and the file arrives as
|
||||
an attachment chip — the same component every other attach path returns, so the
|
||||
|
||||
Reference in New Issue
Block a user