db229621642b75d42d7627db0a27ebc126f0b81e
3
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> |
||
|
|
4c78215e31 |
Narrow a chat before it starts, and find a file rather than spell it
Six things, all found by using the thing rather than by reading it. The scope menu only appeared once a chat existed, on the reasoning that there was no row to post to. True, and the wrong conclusion: the harness puts a tool's guidance in front of the model the moment the tool is offered, so the menu could not be reached until after the model had been told how to keep notes and handed the tools to do it -- and switching it off then does not un-send that turn. It is on the new-chat screen now and writes nothing: `_scope_context` builds a stand-in Chat, which is `draft.as_chat`'s trick again, and the switches ride along with the first message. Checked means on and a browser submits only the ticked boxes, so every gate also renders a hidden input naming it and `start_chat` subtracts one list from the other; inverting the control would read backwards under a menu that says everything is on unless you say otherwise. Only the off ones are written, because absent means on and one representation of it is what keeps "why is this off?" to a single answer. Nothing is validated against the offered set, since scope_json narrows after every gate -- naming a gate that was never offered switches off something that was not on. Then the scheduling instructions, audited against a 4B model on this machine rather than against my own reading of them. Ten realistic requests, ten compiled, twice over -- so the prompt is sound. What was not sound was `describe`, which built a phrase by joining fragments and read "Every the 1st at 09:00" for the commonest monthly schedule there is, and "Every of January" for a month with no day. That string is the whole of what somebody sees before approving a schedule and the whole of what the model is told about its own chat, so a phrase nobody can parse is a review step nobody performs. It reads as English now, collapses Monday-to-Friday to "every weekday" and seven days to "every day", and every case in the test is a rule that model actually produced. The one mistake it made was naming Wednesday for "every other tuesday", so the weekday numbering is spelled out rather than left as "0-6, Monday is 0": getting that wrong is the error here that still looks like a working schedule. Roughly one call in six also came back empty -- a local runner swapping models under the request will do that -- so an unusable reply is asked for once more before giving up. Not on an LLMError: an endpoint that refused will refuse again, and the reader is better served by the form than by waiting twice for the same answer. Canvas asked for a typed path, which was the last control in the application expecting somebody to remember an absolute path on another machine -- the same complaint the folder page's directory field answered with a picker. /browse takes pick=file and the same fragment makes files buttons, because a second copy of that listing is a second place for the path arithmetic to be got subtly differently. The button carries data-canvas-open rather than an hx-post since the path is not known until the dialog closes, and ui.js posts it through htmx.ajax so the response lands in the panel exactly as every other canvas action's does. The key is `agent:<path>`, so a file opened by hand and one opened by the model are one tab rather than two spellings of it. The tabs already existed and already closed; they now square off at the bottom and the active one takes the body's background, so which is selected is structural rather than a tint nobody can see in a theme they did not choose. Highlighting was already there for every language named and is checked for fifteen of them. Three smaller ones. Tabs kept their scroll position, so switching from a long panel to a short one left the browser clamping to that panel's bottom: the end of it above a screen of nothing, which reads as a page that failed to load. Nothing in CSS can reset a scroll position. The sidebar's footer and the composer sit either side of one vertical edge and were both content-sized, so their top borders met it at different heights and read as one line that had been broken -- `--footer-height` is a calc of the pieces the footer is built from, applied as a min-height to both, which is exactly what `--header-height` already does at the top of the shell. And "Add a workflow" sat flush against the list it adds to, stated as an adjacency because `.btn-row` is right to carry no margin everywhere else it appears. Both pieces of JavaScript were driven under a DOM stub before committing, which is how the tab listener's delegation and the canvas button's six behaviours were checked at all -- `node --check` parses a file that does nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
5766446b84 |
Files, open beside the conversation
A third side panel, built the way the terminal is and filled the way the inspector is: tabs holding open files. Project files over SFTP in an agent chat; notes, skills, knowledge documents, this chat's text attachments and its own scratch document everywhere. Read with pygments, edited in a plain textarea, saved with a conflict check. A bug found on the way in, and the reason this needed its own read path. `ssh.read_file` ends in `clean_output`, which strips ANSI escapes and decodes with errors="replace" -- right for the output of a command, and fatal for an editor: open a file containing an escape byte, press Save, and you have silently rewritten it with the escapes gone and every undecodable byte replaced by U+FFFD. `read_text`/`write_text` decode strictly, report binary rather than mangling it, carry an mtime:size token for a file that moved underneath, and refuse an oversize write rather than truncating -- `write_file` truncates because a model is told how many bytes it wrote, and somebody pressing Save is not. The model-facing pair is untouched: what it returns is a contract a model has been shown. A truncated read opens read-only for the mirror-image reason. Six sources go through one dispatch table, for the reason tool_labels.py is a table: six independently written permission checks is how one ends up written slightly differently, and that failure looks like editing somebody else's note. A save on a project file bypasses agent/policy.py, which makes it the fourth documented exception to "the modes do not govern the keyboard" and the first that writes. Same argument as the terminal panel -- whoever owns the credential could write the file with scp -- but the consequence is larger and is now said out loud rather than left to be inferred. The model opens tabs from the file tools it was already calling, so no new schema and no tokens. It never brings one to the front: an agent reads forty files in a long reply, and taking the screen each time would drag somebody through all of them and lose any edit in progress. Only the strip is streamed, guarded on truthiness so the frame can never blank itself -- an empty one would close every open tab, the approval card you could press twice with the sign reversed. Both halves are settled on the server, which is why canvas.js needs no guard against a swap at all. No vendored editor. CodeMirror 6 needs a bundler, which is hard rule 1; CodeMirror 5 would be a larger payload than xterm on every page, and xterm is the one heavy dependency precisely because it loads only where it can be used. So: server-rendered highlighting for reading, a textarea for writing, and the panel says there is no colour while you type rather than pretending. Also here: a scratch document per chat, with `scratch_write` at RISK_READ on plan_update's argument, and a test pinning the three numbers that decide a panel's width -- LAYOUT_BOUNDS drops an unknown variable silently, so a panel missing from it has a drag handle that works and forgets. Driven under a DOM stub and against the running application. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |