a16510aba853c193731d07097e858ee1b759c1a1
5
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
a16510aba8
|
The interface speaks Slovak
969 strings, an instance default and a per-person choice, and no half-done corner: the admin prose is translated too. Design and reasoning: LLeMbas.wiki/Translations. KEYED BY THE ENGLISH SENTENCE A missing entry renders the key, which is the English -- so an untranslated string looks as it always did, an English instance is byte-for-byte 1.6.0, and a half-finished catalogue is a half-translated page rather than a page of dotted key names. The cost is that editing an English sentence orphans its translation silently, which is what tests/test_translations.py asserts in both directions. No gettext: .po -> .mo is a build step and this project does not have one. A JINJA GLOBAL, AND THEREFORE A CONTEXTVAR `t()` is a global for the reason `brand` already documents -- render() is bypassed by 25 TemplateResponse calls and 8 get_template().render() calls, the latter being the SSE frames, which have no Request at all. A global is bound once at import and the language is per person, so the active language is a ContextVar set per request. 🚨 `get_current_user` had to become `async def`. FastAPI runs a sync dependency in a threadpool, and anyio copies the context in and discards it on the way out -- so the language was set where nothing could see it and every page rendered in the instance's language whatever anybody had chosen, with no error anywhere. NOT TRANSLATED, ON PURPOSE Everything a model reads: the 60 prompt fragments, and the dates in harness.py, schedule/runner.py and schedule/compile.py. Only `i18n.stamp` is localised, and only where a person reads it -- with the *format* translatable as well as the words, because "26. septembra 2026" is a different pattern rather than the same one with different words in it. A process locale is not an option: global, not thread-safe, two people's pages at once. A second fragment telling models to answer in the reader's language was written during this work and removed: `core.style` has said it since long before, and test_an_empty_override_turns_a_fragment_off caught the duplicate. THE BULK PASS 1213 sites wrapped by a one-off script that only touched patterns it could not misread. It got three wrong in a way that mattered -- `t('…')` inside `attr="…"` where the sentence held an apostrophe, closing the Jinja string and 500ing two pages whose partials no test renders. tests/test_translations.py now compiles all 110 templates. It also wrapped the product's own name, an SSH key header, a keystroke hint and an example URL, all taken back out: a string is not translatable just because it is a string. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
92070d7879
|
Six things that looked like they worked
None of these fails loudly and two of them correct themselves if you reload, which is why five were found by reading rather than by anybody reporting them. The reply that lost its author is the one worth knowing about: the frame that replaces a bubble when a reply lands was looking the models up as nobody, and "no user" answers "no models" rather than "all models" -- so every finished reply swapped the model's avatar for the plain mark and put the instance's name where the model's should be, until the next page load put it back. Beside it: a concurrency quota enforced on two of the six paths that start a reply, including neither of the two most used; a custom theme whose success and warning colours moved the text and left the background behind; a phone shell sized to one viewport inside a document sized to another, which is the reported scroll past the bottom of Settings; a whole conversation's Markdown rendered on every page load and read by nothing; a skip guard inert since it was written; and an endpoint nothing has ever called. The scroll fix folded five near-identical scroller rules into one, which is also where the containment they were all missing now lives. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
3b1632069c |
Compaction: a button, and automatically when the window fills
A long conversation eventually just stops working. Compaction summarises the earlier turns and sends the summary in their place. The messages are kept. They stay in the transcript behind a collapsed divider and simply stop being part of the request, which is what makes the button safe to press and automatic compaction safe to have at all: a summary that came out badly is a bad turn, not a lost conversation. Stored on the Chat, not as a synthetic Message. A synthetic row needs a role -- `system` breaks the one-system-message rule the moment build_messages emits it beside the harness, and user/assistant makes it a turn people can edit, regenerate from and copy, indistinguishable from a real one in all four places a bubble is rendered. Worse, "editing rewinds, it does not branch" would silently delete it and leave no marker that compaction had happened at all. The summary goes out as a user turn and an assistant turn, not one. A leading assistant breaks templates requiring the first non-system message to be user; a lone leading user produces user, user whenever the kept history starts on a user turn -- which it always does, because the cutoff lands on a finished reply. compacted_through_id is a plain id rather than a foreign key: migrations.py compiles only the column type, so a REFERENCES clause would exist on a fresh database and not on an upgraded one, and a constraint half the fleet has is worse than none. cutoff_message validates it on every read instead, and a rewind past the boundary clears it. Compacting again summarises only the delta, with the previous summary supplied to be subsumed. Re-summarising the whole chat each time grows quadratically and eventually exceeds the window it is protecting. Automatically at the top of _run, not in post_message: that route's contract is to return immediately and leave the slow part to a resumable connection, and it also means build_request is called once, after compaction, with no second assembly path. The trigger is the last reply's recorded usage plus an estimate of the new turn -- retrospective because true prompt_tokens are only knowable after a response, plus the delta because otherwise fifty thousand characters pasted into the composer overflow a window that read 90% last turn. It never fires when the context length is unknown. It does fire on estimated counts, which is safe here precisely because nothing is lost. _maybe_compact never raises: a failure logs and sends the uncompacted request. A `status` event says "Summarising earlier messages…" in the meantime, because a silent multi-second pause before the first token is what a hang looks like. The wording is three fragments under Admin - Prompts. Clearing task.compact turns compaction off entirely. Also adds compaction.moment(): SQLite does not store the offset, so a row loaded from disk is naive while one in the session's identity map keeps its tzinfo, and comparing the two raises. Every comparison here is between exactly those. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
50484b2483 |
One thread template, one sidebar toggle
chat/index.html and chat/_thread.html held the same loop, so anything added to the conversation -- a compaction divider, say -- would have had to be written into both and kept in step by hand. index.html includes the partial instead. The sidebar toggle was a raw inline onclick, the only one left in the application. app.js already delegates [data-toggle="#selector"] and gives open/close, aria-expanded and an is-active button state for free; the chat settings gear has used it all along. Also deletes the .sidebar[data-collapsed="true"] rule, which nothing has ever set. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
5f020ef33f |
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> |