Background generation, unread replies, send/stop, PLAN.md
**Replies now run in the background.** Generation was driven by the SSE request, so navigating away or opening another chat cut the answer off mid-sentence. services/generation.py owns the work as its own task and the SSE endpoint merely follows it. Verified: attached briefly, closed the connection, went to another page -- the reply finished anyway, 832 characters, not marked stopped, auto-titled. Reattaching works because both `render` and `reasoning` frames now carry the whole block rather than a delta. A follower arriving late has no earlier fragments to append to, so deltas would leave it permanently missing the beginning. Verified: attached six seconds in and the first frame already contained 517 characters written while nobody watched. **Unread indicator.** A reply that lands with no follower attached marks its chat unread; the sidebar polls every 10s for out-of-band dot spans plus an HX-Trigger that raises a toast. Polled rather than pushed: a browser sitting on another chat has no connection to the one that finished, and an always-on channel per tab is a lot of machinery for a green dot. `unread_notified` stops the same arrival being announced every tick. Follower count is what decides "was anyone watching", so reading it as it arrives does not mark it unread -- verified both ways. **Stop is the send button.** While a reply is being written the send button becomes a red stop square, found via a MutationObserver on the thread since the composer and the streaming bubble are far apart in the document. The in-bubble Stop is gone. **Attachment border removed.** As asked -- an attachment is a picture, and the frame only ever drew at the wrong width. The anchor now shrink-wraps and the img's width/height attributes are overridden so a small image shows at its own size. Adds PLAN.md: what is built, what is not, known limits, and the decisions that look like oversights until you know the reason. 239 tests, ruff clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -20,6 +20,7 @@ lembas secret-key # generate LEMBAS_SECRET_KEY
|
||||
lembas create-admin # create or promote an admin
|
||||
|
||||
pytest # 230 tests, ~9s
|
||||
# PLAN.md tracks what is and is not built
|
||||
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,17 +192,28 @@ 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.
|
||||
**Generation is a background task; the SSE endpoint only follows it.**
|
||||
`services/generation.py` owns the work and the registry; `api/chats.py:_follow`
|
||||
watches a `Generation` and streams what it sees. Closing the connection does
|
||||
NOT stop the reply -- that was the old behaviour and it cut answers off when
|
||||
the reader navigated away. Any route that creates an assistant placeholder must
|
||||
also call `generation.ensure()`.
|
||||
|
||||
**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.
|
||||
**Stream frames carry whole blocks, not deltas.** Both `render` and `reasoning`
|
||||
send the complete text each time. That is what makes reattaching mid-reply
|
||||
work: a follower arriving late has no earlier fragments to append to. It also
|
||||
means Markdown is re-rendered whole, which is required anyway -- a list or code
|
||||
fence is only correct once its context exists.
|
||||
|
||||
**Stopping sets a flag the producer checks.** `generation.request_stop()`;
|
||||
whatever arrived is kept and the message is marked `stopped`, which is distinct
|
||||
from `error`. In-process, so single-worker only.
|
||||
|
||||
**Unread is polled, not pushed.** A browser on another chat has no connection
|
||||
to the one that finished. `/api/chats/unread` returns out-of-band dot spans and
|
||||
an `HX-Trigger` for the toast; `unread_notified` stops the same arrival being
|
||||
announced every tick. Re-rendering the whole sidebar instead would reset the
|
||||
folder open/closed state every 10 seconds.
|
||||
|
||||
**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
|
||||
|
||||
Reference in New Issue
Block a user