Live Markdown, stop, rewind, custom picker, dialogs
Seven things. **Reasoning starts closed.** The answer is what the reader is waiting for; the thinking is one click away. **Image borders.** .attachments__image was a block-level <a>, so its border stretched the full column around a narrow picture. inline-block, and the frame is the picture. Same fix for the composer thumbnail. **Markdown now renders during the stream.** The generator re-renders the answer so far and sends it as a `render` event at most every 100ms, swapped with innerHTML, instead of appending escaped tokens and formatting everything at the end. Re-rendering whole rather than appending is the point: a list or a code fence is only correct once its context exists, and partial syntax resolves itself as more arrives. Measured against a live model: 29 render events, formatting visible from the first content token. **Stop button.** A stop request goes into an in-process set the generator checks between chunks; whatever arrived is kept, because a half-written answer the reader chose to cut short is still worth having. Measured: stream ended 0.2s after the request, 1155 characters preserved, message marked stopped rather than errored. Navigating away does the same thing via CancelledError. **Rewind and edit.** Edit one of your own turns and everything after it is deleted, then the conversation runs on from there. Deliberately not branching: that needs a UI for choosing between versions, and "go back and try again from here" is what was asked for. The form states how many messages will be discarded before you confirm. **Custom model picker.** A <select> renders only text in an <option>, so it can never show an avatar. Built from buttons and a hidden input, with descriptions, capability tags, a filter box past eight models, and arrow-key navigation written out by hand since there is no native widget doing it. **Notification system.** lembas.notify/confirm/prompt in ui.js, built on <dialog> so focus trapping, Escape and page inertness come from the browser. htmx:confirm is intercepted, so every existing hx-confirm gets the themed dialog with no change at the call site; the browser's grey confirm() is gone from every template. 230 tests, ruff clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -19,7 +19,7 @@ lembas info # paths + counts, useful when confused
|
||||
lembas secret-key # generate LEMBAS_SECRET_KEY
|
||||
lembas create-admin # create or promote an admin
|
||||
|
||||
pytest # 212 tests, ~8s
|
||||
pytest # 230 tests, ~9s
|
||||
ruff check . # lint (line length 100)
|
||||
python scripts/build_artwork.py # regenerate all SVG artwork
|
||||
python scripts/fetch_vendor.py # verify vendored JS against the lockfile
|
||||
@@ -191,6 +191,34 @@ null in the composer; `files.claim()` binds them, and only unclaimed rows owned
|
||||
by that user, so a forged id cannot pull in someone else's file. Abandoned ones
|
||||
are swept at startup.
|
||||
|
||||
**Markdown renders progressively, server-side.** The stream sends `render`
|
||||
events carrying the whole answer re-rendered from Markdown, at most every
|
||||
`RENDER_INTERVAL`, swapped with `innerHTML`. Appending raw tokens instead would
|
||||
mean formatting only appearing at the end -- a list or code fence is only
|
||||
correct once its context exists, so partial output must be re-rendered whole
|
||||
rather than appended to.
|
||||
|
||||
**Stopping a stream is an in-process set.** `api/chats.py:_CANCELLED` holds
|
||||
message ids the reader asked to stop; the generator checks it between chunks.
|
||||
Correct for the single-worker deployment this ships with; multiple workers
|
||||
would need it in the database or a broker.
|
||||
|
||||
**Editing rewinds, it does not branch.** `POST .../messages/{id}/edit` rewrites
|
||||
a user turn and **deletes everything after it**. Branching would need a UI for
|
||||
choosing between versions; "go back and try again from here" is what was asked
|
||||
for and what other clients do. `Message.parent_id` still exists unused.
|
||||
|
||||
**Dialogs and toasts are ours, not the browser's.** `static/js/ui.js` provides
|
||||
`lembas.notify/confirm/prompt`, and intercepts htmx's `htmx:confirm` so every
|
||||
existing `hx-confirm` gets the themed dialog with no change at the call site.
|
||||
Plain forms opt in with `data-confirm`, lone submit buttons with
|
||||
`data-confirm-button`. Never add a `window.confirm` back.
|
||||
|
||||
**The model picker is hand-built.** A `<select>` renders only text in an
|
||||
`<option>` -- no avatar, no description, no badges. `chat/_model_picker.html`
|
||||
plus the picker block in `ui.js`; the value lives in a hidden input so it still
|
||||
behaves as a form field.
|
||||
|
||||
**Chats are created lazily.** There is no endpoint that makes an empty chat.
|
||||
"New chat" is a link to `/chat`, which renders a composer with no row behind
|
||||
it; `POST /api/chats/start` writes the chat together with its first message.
|
||||
|
||||
Reference in New Issue
Block a user