Repository navigation
feat(agent): add experimental local document assistant - #249
Merged
Merged
Conversation
Deploying document with
|
| Latest commit: |
e81be1d
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://96ae9707.document-7hm.pages.dev |
| Branch Preview URL: | https://feat-local-multilingual-assi.document-7hm.pages.dev |
Writing into a document was silently routed to the in-browser engines (WebLLM / wllama). Sixteen candidates were measured across seven languages and none kept dates, names, actors, negation or modality straight, so a rewrite could quietly turn "not approved" into "approved" -- unacceptable for a zero-tolerance operation. - New pure policy `resolveWritingRoute`: a connected loopback service wins; browser-local engines are used only with an explicit opt-in and are marked experimental; otherwise the request is refused with a localized reason. - Finish the opt-in loopback service: `ProviderId` gains `loopback` (an explicit model is required), exported from `llm/index.ts`, with connection, timeout, cancellation, tool-refusal and incomplete-response cases. - Panel: a settings block for the local service (origin, model, connect, disconnect, status) plus the browser-local writing consent. Only a validated loopback origin and a model name are ever persisted. - Eleven new strings across the seven shell locales. Chat, structured tool plans and offline use keep running on the browser-local model; no cloud or LAN fallback is introduced, and the CSP needed no change because `connect-src` already allows `http:`. Reverse verification: disabling the consent gate turns the policy case red and, after rebuilding the package, the panel case red. Checks: 148 files / 4598 tests, lint:ts, format:check and the production build pass. Real-device Ollama connectivity and a real-browser writing run stay unverified (no local service was available) and are recorded as such in docs/evaluations/2026-10-10-local-model-writing-scope-decision.md.
The writing route only accepted a loopback service, which asks every user to install one. The providers for the alternative were already in the library (OpenAIProvider has baseURL/apiKey, OllamaProvider has ollamaBaseURL, and the Claude/OpenAI/Gemini labels were still in the panel), so this is wiring plus a policy, not new plumbing. Doing it honestly meant changing what the site claims. The core editor and the converters keep the document on the device; a cloud endpoint with the user's own key does not. The help center (all seven locales) and llms.txt now state the two cases instead of one, and the panel always shows where a writing request goes. - `llm/endpoint.ts`: WritingEndpoint (loopback | openai-compatible | anthropic | gemini). A remote endpoint must be HTTPS -- plain HTTP would put the key and the selected text on the wire -- and may not carry credentials, a query or a fragment. A loopback endpoint stays origin-only over http. - `keys.ts`: per-origin key slots. The provider-keyed slots cannot hold two OpenAI-compatible services; the second would overwrite the first. The existing functions are untouched. - `writing-route.ts`: endpoint model with a user-chosen preference (device-first / remote-first). The result carries `dataPath`, so the host can say whether the text stayed or left. - Panel: destination kind, address, model, key, connect/disconnect, status, the preference, and a "writing goes to ..." line. A cloud endpoint reports "configured (reachability not verified)" -- readiness is not reachability. - 23 strings across the seven locales; the vendor names that are legitimately identical in de/es/pt are registered in the locales test rather than given invented translations. Reverse verification: removing the HTTPS requirement turns `agent-endpoint.test.ts` and the route case red. Checks: 149 files / 4610 tests, lint:ts, format:check and the production build pass. Still unverified, and recorded as such: a real cloud request with a real key, real loopback connectivity, and a real-browser writing run.
Offline, a cloud endpoint cannot be reached at all, so a user who configured one got an unexplained network failure on rewrite -- while the two destinations that do work offline (a loopback service, and the in-page engines) were behind a consent gate. The mode the site advertises most loudly was the one with the worst answer. - `writing-route.ts` takes `offline`. A remote endpoint is dropped from the candidates rather than attempted; a malformed endpoint is still rejected, since being offline must not hide a bad configuration. - Its own blocked reason, `offline-needs-device-destination`, so the host explains the case instead of reporting "nothing configured". - The panel follows `online`/`offline`, adds the note next to the resolved destination, and maps the reason to a localized message. - It does not fall back to the browser-local engines on its own: that is exactly the silent substitution the previous commit removed. Consent still decides, and the reported destination keeps saying where the text would go. - Help center (seven locales) now states that a cloud destination is unreachable offline. Reverse verification: removing the offline drop turns the three policy cases and the panel case red. Checks: 149 files / 4615 tests, lint:ts, format:check and the production build pass. `navigator.onLine` is simulated in jsdom, so real-device online/offline switching remains unverified and is recorded as such.
Review of the previous three commits. The load-a-model gate in `submit()` runs before the writing branch and assumes the in-browser engine is the only writing backend. With a cloud endpoint connected and no in-browser model, a rewrite therefore did nothing at all and only asked the user to download a model -- which is precisely the case the endpoint feature exists for (no WebGPU, or no wish to download two gigabytes). Why the tests missed it: the panel cases asserted the destination line but never actually sent a writing request, and the older cases set `ready = true`, which steps around the gate. - The gate is skipped when this request would go to a connected endpoint. Reverse verification: removing that condition turns the new regression case red. - Address, model and key now define the destination, so changing one disconnects the endpoint. Previously the panel reported the new destination while requests still went to the old one, with the old key; changing the address also clears the key field, since a different address is a different service. - `clearEndpointKey` finally has a caller: emptying the key field removes the stored secret instead of leaving it with no way out. - Offline with a configured cloud endpoint says "the configured cloud endpoint (unavailable offline)" rather than "not configured yet". - A cloud endpoint without a key asks for the key instead of reporting a generic failure. - Filling the key field is separated from rendering status, so a key being typed is not overwritten from storage before it is stored. - Drops the 8 superseded loopback-named strings (56 entries across the locales) and the near-dead message added for the case the gate now owns. Checks: 149 files / 4621 tests, lint:ts, format:check and the production build pass.
chaxus
marked this pull request as ready for review
October 10, 2026 05:52
The agent's write tools had unit tests and no end-to-end coverage at all, so the two mechanisms that actually touch a document were only ever exercised against mocks. Three cases now drive the real panel against the real v9 editor: - Excel writes the spreadsheet model directly (`writeExcelCellText`), reached through the closed "read <range>, then set <cell> to <literal>" phrase. - Word goes through the native HTML paste wrapper (`pasteWordHtml`), reached through the write-the-last-answer intent. - Presentations go through `slide_action`. Each asserts the real change and a native Undo round trip, and each was chosen because it is reachable without a model -- which is what makes it usable in CI. The fixture pins the provider to a GGUF path with no model, so the panel's idle auto-load has nothing to download. Reaching the Word case needed one fixture note: conversation history lives in the panel's own store (memory plus opt-in IndexedDB), so there is no record to seed; the case supplies exactly the node `getLastAnswer()` reads. Everything downstream is the real path. Model-driven tool choice therefore stays uncovered -- recorded as a gap rather than implied. That work also exposed a third instance of the model gate over-reaching: `generateDocumentToolSequence` applies the fixed cell phrase *without* the provider, but the gate assumed every "operate document" request needs the model, so that model-free path was unreachable -- and the endpoint exemption added earlier was too broad in the other direction, letting open-ended operations past with no provider to run them. The predicate that decides this is now a named function, `isModelFreeToolRequest`, and the gate asks it instead of guessing. Reverse verification: making the tools branch exempt unconditionally turns the new case red. Checks: 149 files / 4628 tests, lint:ts, format:check and the production build pass; the new spec passes 3/3 in about nine seconds.
… updates Guard native spreadsheet writes and SUM against delayed callbacks after cancellation, preserve undo ownership, and verify normalized numeric values. Cancel stale endpoint connections, preserve provider credentials, propagate abort reasons, and reject truncated Gemini responses. Retain assets needed by live pages, align silent-update regression tests with production builds, and remove redundant code and temporary artifacts.
chaxus
enabled auto-merge (rebase)
October 10, 2026 09:35
auto-merge was automatically disabled
October 10, 2026 09:46
Rebase failed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Add an experimental document assistant behind the explicit
?agent=1opt-in. Support browser-local WebGPU inference with CPU fallback, configurable writing endpoints, model loading and cancellation, and bounded Word, spreadsheet and presentation operations with native Undo and readback verification.Harden delayed spreadsheet writes and SUM cancellation, endpoint connection lifecycle, provider abort propagation, truncated responses, and normalized numeric readback. Retain cached assets needed by live pages and align silent-update tests with the production service-worker build protocol. Remove unused code and transient artifacts while preserving hash-bound evaluation evidence.
Validation
e81be1da: all required GitHub checks passed, including ordinary, Cloudflare Pages and Docker E2E, preview smoke and Cloudflare Pages deployment.Scope and limits
The assistant remains experimental and opt-in. Enabling it starts local model loading after the editor is ready, visible and idle; first downloads can compete for bandwidth, and initialization/inference consume CPU/GPU and memory. Ordinary editor visits do not initialize the assistant.
Archived model screens do not establish reliable seven-language factual-writing quality. Physical mobile devices and the full device/offline matrix remain incomplete. Authenticated custom model URL persistence remains a known limitation. Real cloud API calls were not exercised by this repair pass; provider behavior is covered by deterministic tests.
Integration
GitHub rejected rebase merging for this branch. Squash merging preserves the protected main branch's linear history and retains the tested branch tree as one experimental feature release.