feat(ai): turn raw text into Markdown notes with a local Ollama model - #20
feat(ai): turn raw text into Markdown notes with a local Ollama model#20cygmris wants to merge 1 commit into
Conversation
Adds an opt-in "Local AI" settings section plus two entry points — a command palette action to convert pasted text into a new note, and a toolbar button to tidy the current note (or just the selection). Requests go straight from the browser to the user's own Ollama; note text never reaches the server. Because the Worker runs at the edge and cannot reach the user's loopback address, there is no server-side proxy option here; the CSP `connect-src` is widened to exactly two hardcoded loopback endpoints and nothing else. Two failure modes drove most of the design: 1. Ollama silently drops the front of an over-long prompt and the model then stops cleanly on the part it did see, reporting `finish_reason: "stop"`. Nothing in the API surfaces this. `detectLoss` therefore uses a second, independent heuristic — output far shorter than input on a first-turn conversion — and long input is chunked paragraph-first, then by line, then hard-cut. 2. Offsets captured when the command fires go stale while the model works. With realtime sync enabled the note can change underneath, so a selection edit would land in the wrong place and a whole-note edit would clobber a concurrent change — both silently. Writes now carry the original text and are refused if it no longer matches, keeping the result on screen so it can be copied instead. Browser support is Chrome/Firefox only, stated in the UI rather than only in docs: Safari blocks an HTTPS page from calling http://localhost. Chrome 138+ additionally gates loopback access behind a permission prompt, and an unanswered prompt leaves the request hanging rather than failing, so the connection test carries its own timeout and says so. Note content is treated as untrusted data: the system prompt tells the model to reformat rather than answer, and that instruction follows the user's UI language so weaker local models weight it properly. Tests: 138 passing. Assertions were reverse-verified by breaking each implementation in turn and confirming the matching test goes red.
|
Small update rather than a ping: this has now been running on a live deployment for a few days, and I re-ran the full acceptance on it today rather than trusting the earlier local run. Verified end to end against a real site and a real local model: streaming genuinely arrives incrementally (sampled 17 distinct lengths over one generation), chunking shows One thing worth knowing for anyone testing this on a deployed instance rather than locally: Chrome 138+ requires the Local Network Access permission for a public HTTPS page to reach I have follow-up work stacked on this (#25, and #26 for a larger direction question), so no rush from my side — happy to rebase or split this differently if that makes it easier to review. |
What this adds
An opt-in Local AI settings section, plus two entry points that turn messy text into clean Markdown using the user's own Ollama:
Requests go straight from the browser to the user's own Ollama. Note text never reaches the server. Nothing is enabled by default; the section is empty until a user configures it.
Why the CSP change is unavoidable
The Worker runs at the edge and cannot reach a user's loopback address, so there is no server-side proxy option for this path.
connect-srcis widened to exactly two hardcoded loopback endpoints (http://127.0.0.1:11434,http://localhost:11434) and nothing else — no config value or user input is ever interpolated into that header.Two failure modes that shaped the design
1. Ollama silently truncates over-long input. When a prompt exceeds the context window, Ollama drops the front of it and the model then stops cleanly on the part it did see, reporting
finish_reason: "stop". Nothing in the API surfaces this.detectLosstherefore uses a second, independent heuristic — output far shorter than input on a first-turn conversion — and long input is chunked paragraph-first, then by line, then hard-cut.2. Offsets go stale while the model works. With realtime sync on, the note can change underneath a long generation. A selection edit would then land in the wrong place and a whole-note edit would clobber the concurrent change — both silently. Writes carry the original text and are refused if it no longer matches, keeping the result on screen so it can be copied instead.
Browser support, stated in the UI
http://localhost, so this is Chrome/Firefox only. That limit is shown in the settings panel, not just in docs.Security
Note content is treated as untrusted data: the system prompt instructs the model to reformat rather than answer, and that instruction follows the user's UI language so weaker local models weight it properly. No new credentials, routes, or storage.
Checks run
Assertions were reverse-verified: each implementation was broken in turn to confirm the matching test goes red, then restored.
npm run test:e2edoes not complete on my machine, and neither does it on an unmodified checkout ofmain— wrangler crashes partway through with its own "please create an issue at workers-sdk" message and a note that 4.125.0 may fix it. I did not upgrade wrangler here since that would be an unrelated change topackage-lock.json. Happy to re-run if you have a known-good version.Notes for review
EditorToolbar.tsxgains one button; the shared trigger logic lives infeatures/ai/open-tidy.tsso the palette and the toolbar cannot drift apart.wrangler.toml, persistence, sync, import/export, or backups.