feat(ai): generate a note summary and a title with the local model - #25
Open
cygmris wants to merge 2 commits into
Open
feat(ai): generate a note summary and a title with the local model#25cygmris wants to merge 2 commits into
cygmris wants to merge 2 commits 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.
Two more actions on the existing AI panel, on top of the local Ollama engine: a one-or-two sentence summary and a short title. The summary is inserted as a plain blockquote before the body (after frontmatter and after an H1 if present), so the note list preview shows the summary instead of whatever the first paragraph happens to be. That placement is the whole point of the feature, and it is why the summary is NOT a callout: `> [!NOTE]` leaks the literal `[!NOTE]` marker into the excerpt, because `toPlainText` does not strip it. A unit test runs `deriveExcerpt` over the inserted result and asserts the excerpt starts with the summary and contains neither `>` nor `[!`, so a later switch to callout syntax turns the suite red. The title goes through `normalizeTitle`, which takes the first non-empty line and strips heading markers, paired quotes (straight, curly, and the CJK 「」『』《》 pairs), emphasis markers and a trailing full stop before truncating to `LIMITS.titleMaxLength`. An empty result is refused rather than clearing the existing title. Both reuse the existing panel, engine and staleness guard. The four modes differ only by three lookup tables: system prompt, dialog width and the accept action. No new AI infrastructure, no new settings. The toolbar sparkle button becomes a menu (Tidy / Summarise / Suggest title) reusing the toolbar's own MenuButton, and the command palette gains the matching two commands inside the existing activeNote block.
This was referenced Aug 24, 2026
AI as you write: selection rewrites, drafting at the cursor, and image generation — worth doing?
#26
Open
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.
Stacked on #20 — this branch contains that PR's commit plus one more, so please read #20 first. If #20 lands, this one rebases to a single commit.
What this adds
Two more actions on the AI panel from #20: Summarise and Suggest title, both running on the same local Ollama engine. No new AI infrastructure, no new settings, no new provider.
Why the summary is a plain blockquote and not a callout
The point of the summary is the note list preview. The list shows the start of the body, which is often "let me give some background first…" — you cannot tell what the note is about at a glance.
The summary is inserted before the body (after frontmatter, and after an H1 if there is one) so the preview shows it instead.
That only works with a plain
>blockquote.> [!NOTE]leaks the literal[!NOTE]into the excerpt, becausetoPlainTextdoes not strip the marker. This is measured, not assumed, and there is a unit test that runsderiveExcerptover the inserted result and asserts the excerpt starts with the summary and contains neither>nor[!— so switching to callout syntax turns the suite red rather than silently making list previews worse.Before / after on a real note:
先说点背景。这份笔记是为了验证 AI 摘要功能而写的,开头这几句其实没什么信息量…这篇笔记是关于发布流程的改造,将原本需要人工执行六条命令的流程改为一个脚本…Title normalisation
normalizeTitletakes the first non-empty line, then strips heading markers, paired quotes (straight, curly and the CJK「」『』《》pairs), emphasis markers and a trailing full stop, then truncates toLIMITS.titleMaxLength. If the result is empty it refuses to write rather than clearing the title the user already has.Shape of the change
The four panel modes now differ only by three lookup tables — system prompt, dialog width, accept action — instead of nested ternaries. Summarise reuses the existing
isTargetUnchangedstaleness guard; the title does not need it because it replaces the whole field.The toolbar sparkle button becomes a menu (Tidy / Summarise / Suggest title) built from the toolbar's own
MenuButton, and the command palette gets the two matching commands inside the existingactiveNoteblock, so nothing new shows up when no note is open.Verification
typecheck,test:unit,i18n:check,comments:check,buildall passsummary.tsby breaking the implementation one mutation at a time — 13/13 turn red, including the "switch to callout syntax" mutationCtrl+Zundoes the insert, the title field updates, and with no note open neither the menu items nor the commands appearKnown gap, stated rather than glossed over: I verified that applying a title writes through and updates the field, but I did not separately verify that applying a title is undoable.