litura-app 0.3.6 → 0.3.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,12 @@ number carries breaking changes.
5
5
 
6
6
  ## Unreleased
7
7
 
8
+ ## 0.3.7 — 2026-09-13
9
+
10
+ - Litura is now also a desktop app. A Tauri shell picks the writing folder, starts the same server on a free port, and shows it in its own window; it carries its own Node runtime, so nothing has to be installed first.
11
+ - The review panel is one list again. Wording marks and review findings sat in separate sections repeating the same reason on every row; now they share a single list in document order, each row naming the passage and where the remark came from, with the reason on the card a click away. The header button says how many remarks are waiting instead of a bare number.
12
+ - The desktop app updates itself: it asks GitHub on launch and from `Check for Updates…`, installs only what you accept and what its signing key verifies, and restarts. In that build the npm update switch is gone, since it would name a second, wrong way to update.
13
+
8
14
  ## 0.3.6 — 2026-09-07
9
15
 
10
16
  - Published the latest editor interface and behavior updates together with the product audit and local draft history.
package/README.md CHANGED
@@ -1,3 +1,5 @@
1
+ <img src="public/logo.svg" alt="" width="72">
2
+
1
3
  # Litura
2
4
 
3
5
  Litura is a local, AI-assisted text editor for making prose sharper and less generic without taking control away from its author. The name comes from Latin: a correction, erasure, or visible revision in a manuscript.
@@ -7,32 +9,50 @@ Litura is a local, AI-assisted text editor for making prose sharper and less gen
7
9
  ## What it does
8
10
 
9
11
  - Keeps focus on working document in a distraction-free editor.
10
- - Shows an AI score for texts; click it to see which words and phrases raised it.
12
+ - Counts local wording signals; click the count in the header for every remark waiting on the draft, in document order, and jump to one.
11
13
  - Reviews sentences as you finish them — switchable off in settings — and reviews the full document on demand.
12
14
  - Highlights named, checkable prose and structure problems, including vague attribution, filler, buried points, broken paragraph promises, and abrupt topic shifts.
13
- - Offers three replace-in-place alternatives when you ask for them — `Offer rewrites` on a finding card, or a message with a passage attached. Nothing changes until you choose an option. `Try again` asks for three more.
14
- - Lets you dismiss a finding you disagree with, and jump between findings by clicking the counter in the header.
15
+ - Offers three replace-in-place alternatives when you ask for them — `Options` on a finding card, or a message with a passage attached. Nothing changes until you choose an option. `Try again` asks for three more.
16
+ - Lets you dismiss a finding you disagree with, and step between findings with `F8` and `Shift+F8`.
15
17
  - Provides a compact chat for discussing the whole draft.
16
- - Suggests short continuations below the current line; press `Tab` to accept or `Escape` to dismiss.
18
+ - Suggests short continuations below the current line; press `Tab` to accept or `Escape` to dismiss or cancel. `Cmd/Ctrl+Enter` requests a continuation even when automatic suggestions are off.
17
19
 
18
- Right-click a selection to attach it to the composer, or press `Cmd/Ctrl+K` to attach the current selection or focus the chat. `Cmd/Ctrl+S` downloads the draft; dropping a text file on the editor opens it.
20
+ Clicking a mark only reads it the card or reason appears and nothing is attached. A bubble beside the click offers `Add to context` and rewrites; selecting text offers `Add to context`, or press `Cmd/Ctrl+K` to attach the current selection or focus the chat. `Cmd/Ctrl+S` downloads the draft; dropping a text file on the editor opens it.
19
21
 
20
22
  ## Data and model access
21
23
 
22
24
  The draft, findings, conversation, and selected model are stored in browser local storage and survive a reload.
23
25
 
24
- The draft is also mirrored to `draft.md` in the folder Litura was started from, so it survives cleared browser storage and opens in any editor. The browser copy stays authoritative while the app runs; if the file changes underneath it, Litura says so and offers to load it rather than merging or overwriting silently. Nothing leaves the machine except what a model request sends to the selected provider — and, if you switch it on in settings, a once-a-day question to the npm registry about new versions.
26
+ The draft is also mirrored to `draft.md` in the folder Litura was started from, so it survives cleared browser storage and opens in any editor. The browser copy stays authoritative while the app runs; if the file changes underneath it, Litura says so and offers to load it rather than merging or overwriting silently. Nothing leaves the machine except what a model request sends to the selected provider — and one question about new versions: to the npm registry if you switch that on in settings, or to GitHub when the desktop app starts.
25
27
 
26
28
 
27
29
  ## Run
28
30
 
29
- Litura requires Node.js 20 or newer.
31
+ Two ways, one program. The desktop app is the same server inside a window and
32
+ needs nothing installed; the command line is the same window in your browser and
33
+ needs Node.js 20 or newer.
34
+
35
+ ### Desktop
36
+
37
+ Download the build for your machine from
38
+ [Releases](https://github.com/vadimchirkov/litura/releases): `.dmg` on macOS,
39
+ `.exe` on Windows, `.AppImage` or `.deb` on Linux. On first launch Litura asks
40
+ which folder holds your writing and edits `draft.md` there; `File → Open Folder…`
41
+ picks a different one. It carries its own Node runtime, checks for updates on
42
+ launch, and installs one only when you say so.
43
+
44
+ The macOS builds are not yet signed with an Apple Developer ID, so the first
45
+ launch is refused: open `System Settings → Privacy & Security`, find Litura near
46
+ the bottom, and press `Open Anyway`. Once is enough — later updates install
47
+ without asking again.
48
+
49
+ ### Command line
30
50
 
31
51
  ```bash
32
52
  npx litura-app
33
53
  ```
34
54
 
35
- Run it in the folder that holds your writing: Litura edits `draft.md` there and reads a `style.md` next to it if one exists. The browser opens on [http://127.0.0.1:3456](http://127.0.0.1:3456), or the next free port if that one is taken.
55
+ Run it in the folder that holds your writing: Litura edits `draft.md` there and reads a `style.md` next to it if one exists. The browser opens on [http://127.0.0.1:3000](http://127.0.0.1:3000), or the next free port if that one is taken.
36
56
 
37
57
  The npm package is `litura-app` — `litura` belongs to an unrelated project. The command it installs is `litura`.
38
58
 
@@ -47,7 +67,13 @@ npm start
47
67
 
48
68
  ## Updating
49
69
 
50
- Running `npx litura-app` is the update: for a bare package name npm re-resolves the registry on every run, so each start picks up the latest release. The exceptions are a global install, which shadows that check and needs `npm i -g litura-app`, and a clone, which needs `git pull`.
70
+ The desktop app checks GitHub on launch and offers what it finds. Accepting
71
+ downloads a bundle, verifies it against a signing key baked into the app, and
72
+ restarts; declining changes nothing and the offer returns next launch.
73
+ `Litura → Check for Updates…` asks on demand. Because the app owns this, its
74
+ settings have no npm update switch.
75
+
76
+ On the command line, running `npx litura-app` is the update: for a bare package name npm re-resolves the registry on every run, so each start picks up the latest release. The exceptions are a global install, which shadows that check and needs `npm i -g litura-app`, and a clone, which needs `git pull`.
51
77
 
52
78
  ```bash
53
79
  npx litura-app --check-update
@@ -71,7 +97,7 @@ Optional environment defaults:
71
97
  PI_PROVIDER=anthropic
72
98
  PI_MODEL=claude-sonnet-4-6
73
99
  PI_THINKING_LEVEL=medium
74
- PORT=3456
100
+ PORT=3000
75
101
  STYLE_FILE=./style.md
76
102
  DRAFT_FILE=./draft.md
77
103
  ```
@@ -87,6 +113,44 @@ npm run check
87
113
 
88
114
  The server uses Node's native HTTP module. The browser UI is vanilla JavaScript with CodeMirror 6 and is bundled with esbuild. Source changes belong in `src/app.js`; `public/app.js` is generated.
89
115
 
116
+ `npm run check:suggestions` tests insertion, cancellation and scheduling without model calls. `npm run eval:suggestions` makes real calls to the configured model using bilingual examples and records output and latency in `tmp/suggestion-evaluation.json`. See [the evaluation guide](docs/suggestion-evaluation.md) for review criteria and prompt/context experiments.
117
+
118
+ The desktop shell needs a Rust toolchain and [Tauri's system
119
+ dependencies](https://tauri.app/start/prerequisites/):
120
+
121
+ ```bash
122
+ npm run desktop
123
+ ```
124
+
125
+ That bundles the frontend, bundles the server into `dist/server.mjs`, downloads
126
+ the Node runtime into `src-tauri/binaries/` (checksummed against the release,
127
+ gitignored, ~120 MB), and opens the window. `npm run desktop:build` produces
128
+ installers in `src-tauri/target/<triple>/release/bundle/`.
129
+
130
+ Name the target when your Rust toolchain and the Tauri CLI disagree about the
131
+ machine — an Intel `rustc` on Apple silicon, for instance, ends in a bundler
132
+ looking for a sidecar nobody fetched:
133
+
134
+ ```bash
135
+ npm run desktop:build -- --target aarch64-apple-darwin
136
+ ```
137
+
138
+ After a build, `npm run smoke:desktop` starts the bundle against a throwaway
139
+ folder and checks that it serves that folder's draft and gives the port back
140
+ when the window dies. The UI needs no desktop build to test: it is the same page
141
+ a browser gets from `npm run test:browser`, with `?shell=desktop` added to the
142
+ URL for the desktop layout.
143
+
144
+ Updates are signed with a minisign key that is not in this repository. CI reads
145
+ it from the `TAURI_SIGNING_PRIVATE_KEY` and `TAURI_SIGNING_PRIVATE_KEY_PASSWORD`
146
+ secrets; the matching public key sits in `src-tauri/tauri.conf.json`, and an
147
+ installed app refuses any bundle that key did not sign. Generate a new pair with
148
+ `npx tauri signer generate` — and remember that replacing the public key strands
149
+ every copy already installed.
150
+
151
+ The app icon in `src-tauri/icons/` is `public/logo.svg` on a cream rounded
152
+ square; regenerate the set with `npx tauri icon src-tauri/icons/app-icon.svg`.
153
+
90
154
  See [SPEC.md](SPEC.md) for product behavior, API contracts, and implementation boundaries.
91
155
 
92
156
  ## License
package/SPEC.md CHANGED
@@ -19,7 +19,7 @@ The product does not claim to determine whether text was written by AI. Its revi
19
19
 
20
20
  ```text
21
21
  +---------------------------------------------------------------+
22
- | Litura draft.md v saved Slop Score 65 settings |
22
+ | [logo] Style 3 settings |
23
23
  +---------------------------------------------------------------+
24
24
  | |
25
25
  | WORKING DOCUMENT | finding card |
@@ -41,12 +41,11 @@ The current product has one working-document pane. There is no separate context
41
41
 
42
42
  ### Header
43
43
 
44
- One row, and nothing in it spends a model call. Everything about the file is behind its name, the save status rides beside it, and the local score sits at the far end. The header is the frame the draft is read in.
44
+ One row, and nothing in it spends a model call. Everything about the file is behind the logo, and the local score sits at the far end. The header is the frame the draft is read in — it carries no file name and no status line of its own.
45
45
 
46
- - `Litura` wordmark.
47
- - Document menu, labelled with the draft's file name: `Open a file…`, `Export a copy`, `History`.
48
- - Save status: `Saved`, `Saving…`, `Unsaved changes`, a save error, or a paused autosave after a conflict.
49
- - Local slop score, labelled `Slop Score`, when the draft is predominantly Latin script.
46
+ - Logo button opening the document menu: `Open a file…`, `Export a copy`, `History`.
47
+ - Save trouble — a save error, or a paused autosave after a conflict — is reported in the chat stream, where the writer is already reading. Routine saves are silent.
48
+ - Local style signals, labelled `Style` with a count of marked passages, when the draft is predominantly Latin script. Hidden otherwise.
50
49
  - Settings button.
51
50
 
52
51
  The selected model is named in settings, not in the header: it is chosen rarely and read never.
@@ -55,13 +54,14 @@ Actions that spend a model call are not here: they live with the assistant, abov
55
54
 
56
55
  ### Rail
57
56
 
58
- The rail floats in the margin the centred text column already leaves empty, so the draft sits exactly where it sat before any card existed. It appears when that margin can hold a 240-pixel card and a gutter — measured from the text column's own padding, not assumed at a breakpoint — which is a window of about 1160 pixels or wider.
57
+ The rail floats in the margin the centred text column already leaves empty, so the draft sits exactly where it sat before any card existed. It appears when that margin can hold a 300-pixel card and a gutter — measured from the text column's own padding, not assumed at a breakpoint.
59
58
 
60
- - A remark about a place in the draft lives there: finding cards, and the rewrite strip while options are being tried on. Without a rail the cards stack in the panel and the strip returns to the action row.
61
- - A card names the problem, why it is one, and the editing direction after an arrow. Hovering it lights the passage it belongs to; the open card gets a firmer border, no color, to stand out among its neighbours what is attached is already named in the composer chip. The dismiss control appears on hover and on keyboard focus.
62
- - Each card is positioned against the passage it anchors to. Cards that would overlap are pushed down in document order; a card whose passage has scrolled out of view is hidden with its mark.
63
- - In a narrower window there is no rail and the same nodes stack in the panel instead. Crossing that width moves them; nothing is rebuilt or lost.
64
- - Only the cards take clicks. The empty margin around them still belongs to the editor.
59
+ - A remark about a place in the draft lives there: the focused finding card, with its rewrite options opening inside it while they are being tried on. Without a rail the focused card lives in the panel instead.
60
+ - Only one card stands in the rail at a time. The marks in the draft are the overview and the navigation: clicking one focuses its card, and `F8` and `Shift+F8` step through them in document order. The focused card names its place (`2 of 5`) with `‹` `›` controls.
61
+ - A card names the problem, why it is one, and the editing direction after an arrow. Hovering it lights the passage it belongs to; the open card gets a firmer border, no color. The dismiss control appears on hover and on keyboard focus.
62
+ - The card is positioned against the passage it anchors to; a card whose passage has scrolled out of view is hidden with its mark. A kept option re-anchors the card to the new text so the `Undo` stays beside what it would put back.
63
+ - In a narrower window there is no rail and the focused card shows in the panel instead. Crossing that width moves it; nothing is rebuilt or lost.
64
+ - Only the card takes clicks. The empty margin around it still belongs to the editor.
65
65
 
66
66
  The panel keeps what is about the draft as a whole: typed messages, replies, `Review draft`, review status, and the composer.
67
67
 
@@ -89,24 +89,24 @@ The panel keeps what is about the draft as a whole: typed messages, replies, `Re
89
89
  - One field, no mode control. What the message does is decided by what is attached: a passage the writer selected is a rewrite target, a finding is something to ask about, and nothing attached is a conversation about the draft. The placeholder says which of the three is active.
90
90
  - `Enter` sends; `Shift+Enter` inserts a newline.
91
91
  - `Escape` cancels an active request or detaches the current selection.
92
- - A single control above the stream closes what the assistant is showing and opens it again: `×` over an open conversation, `Conversation` over a closed one — and no control at all while the stream is empty, so the toggle never flips over an empty panel. One press puts away both the turns and the finding cards standing in the rail — two `×` in two places for one idea is two presses to get a clean screen. The composer is not part of it: it is how the writer talks to the draft at all, so it is always there.
92
+ - A single control closes what the assistant is showing and opens it again: a `×` badge straddling the panel's top corner while open, `Conversation` over a closed one — and no control at all while the stream is empty, so the toggle never flips over an empty panel. One press puts away both the turns and the finding cards standing in the rail — two `×` in two places for one idea is two presses to get a clean screen. The composer is not part of it: it is how the writer talks to the draft at all, so it is always there.
93
93
  - Closing is a view, not an edit. The turns stay in memory and in storage, applied options stay with their `Undo`, and the findings and their marks stay in the draft — clicking a mark builds its card again. Remark cards and undecided option groups are put away. Anything new arriving opens the stream by itself, so a reply never lands somewhere the writer cannot see; merely moving cards between the rail and the stream on resize never opens it. There is no control that discards the conversation; replacing the document does that, and the server only ever reads the last 20 turns.
94
94
  - A failed model call is one short sentence naming the cause — a rejected key, a rate limit, an overloaded provider — and one button doing something about it. The sentence never carries the remedy as a second clause: `Try again` is the remedy. Where retrying cannot help there is no retry, and a key problem offers `Open settings` instead. The provider's payload goes to the tooltip and the console, where it is there for a bug report and nowhere else.
95
95
 
96
96
  ## 3. Writing workflows
97
97
 
98
- ### 3.1 Local slop score
98
+ ### 3.1 Local style signals
99
99
 
100
- The browser computes a score from 0 to 100 using:
100
+ The browser marks passages using:
101
101
 
102
102
  - known English AI-tell words and phrases;
103
103
  - sentence-length variation;
104
104
  - moving lexical diversity;
105
105
  - repeated three-word sequences.
106
106
 
107
- For passages shorter than 40 words or three sentences, only the lexical component is used. The score is hidden for predominantly non-Latin text because the word lists are English-specific. It is a heuristic, never sent to the model, and not an authorship probability.
107
+ The header shows a count of marked passages a number the writer can trace back to their own text — never a blended formula. The control is hidden for predominantly non-Latin text because the word lists are English-specific. The underlying heuristic is never sent to the model and is not an authorship probability; it quietly also ranks rewrite options best-first.
108
108
 
109
- Clicking it adds a card naming the matched tells as they appear in the draft, a line for each structural axis that reads badly, and what the number does not measure. A number the writer cannot trace back to their own text is only something to argue with.
109
+ Clicking it opens a card naming each marked tell as it appears in the draft with its reason inline, plus rhythm notes only for structural axes that read badly. To rewrite a phrase, click its mark in the draft for the context and rewrite actions. A number the writer cannot trace back to their own text is only something to argue with.
110
110
 
111
111
  ### 3.2 Automatic review
112
112
 
@@ -137,7 +137,7 @@ The `Review draft` action clears current findings and audits the entire non-empt
137
137
 
138
138
  The prompt caps these at four words for `pattern` and twelve each for `reason` and `fix`: the card is read at a glance beside the draft, and a paragraph of explanation there is not read at all.
139
139
 
140
- Each returned quote is anchored to a non-overlapping occurrence in the current document. Findings appear as a quiet tinted band with a hairline under it — a remark to weigh, not a spell-checker's error to clear — and move with edits outside their ranges. Editing inside a range removes its mark. The attached passage uses the same tint one step stronger, so the two states never read alike.
140
+ Each returned quote is anchored to a non-overlapping occurrence in the current document. Findings appear as a quiet amber band with a hairline under it — a remark to weigh, neither a spell-checker's error to clear nor a blue selection to act on — and move with edits outside their ranges. Editing inside a range removes its mark. The attached passage keeps a blue mark of its own: a tint for a plain selection, an underline over a finding's band, so the remark and the working attachment never read alike.
141
141
 
142
142
  A finding card carries a dismiss control. Dismissing removes the mark, the card, any alternatives requested for it, and cancels an in-flight request, so the counter only reports findings the author has not rejected.
143
143
 
@@ -149,33 +149,33 @@ single mandatory paragraph template.
149
149
 
150
150
  ### 3.4 Finding and selection rewrites
151
151
 
152
- A passage can be attached to the composer in three ways:
152
+ A passage can be attached to the composer in three ways, all explicit:
153
153
 
154
- - click a review mark;
155
- - select text and open the context menu;
156
- - press `Cmd/Ctrl+K` with a selection.
154
+ - the bubble's `Add to context` on a review mark, a style mark, or a text selection;
155
+ - press `Cmd/Ctrl+K` with a selection;
156
+ - the `Options` / rewrite action on a finding card or in the bubble.
157
157
 
158
- An attached passage is marked in the draft with a tinted highlight that survives the editor losing focus, and moves with edits like a review mark. A finding already carries its own mark, so clicking one keeps that mark's look rather than laying the selection tint over it the tracking is the same either way, only a plain selection needs the tint to show anything at all. For the same reason the composer chip is for selections only, reading `Selected text`: a finding is named on its own card beside the line, and the placeholder says what the next message will do with it. A chip on top of that read as the sentence having been pulled into the composer, which is not what happened.
158
+ Clicking a mark never attaches: reading a remark arms nothing. A click opens the finding's card (or the style mark's reason bubble) and places the caret where it was clicked, leaving the keyboard in the editor, so a marked sentence stays as editable as any other text. A real selection wins over the mark underneath it.
159
159
 
160
- Clicking a mark places the caret where it was clicked and leaves the keyboard in the editor, so a marked sentence stays as editable as any other text. `Cmd/Ctrl+K` and the context menu move focus to the composer instead, because both are explicit requests to instruct.
160
+ An attached passage is marked in the draft with a tinted highlight that survives the editor losing focus, and moves with edits like a review mark. A finding already carries its own mark, so attaching one keeps that mark's look rather than laying the selection tint over it the tracking is the same either way, only a plain selection needs the tint to show anything at all. The composer chip names what is attached: `Finding: <pattern>` or the selected text. The placeholder says what the next message will do with it.
161
161
 
162
162
  Editing inside the attached passage ends the attachment and cancels an in-flight rewrite: the author has taken the sentence over, and alternatives generated for the old wording no longer apply.
163
163
 
164
- Clicking any finding attaches exactly the passage its mark covers and shows its card. It makes no model request: the card carries a single `Options` control, and reading the remark and fixing the sentence by hand is a complete outcome. Clicking the same mark again returns to the card already in the stream rather than repeating the remark.
164
+ Clicking any finding shows its card and offers a bubble with `Add to context` and `Suggest rewrites`. It makes no model request: the card carries a single `Options` control, and reading the remark and fixing the sentence by hand is a complete outcome. Clicking the same mark again returns to its card rather than repeating the remark.
165
165
 
166
166
  There is one replacement scope, and it is the quoted passage. The tint in the draft and the range a rewrite would replace are the same span, so a finding never silently rewrites more than it marked, and the card asks for nothing before it can be used. A structural finding names a problem with the paragraph around its quote; its rewrite still replaces the quote, and the whole document goes to the model as context. A change that has to move material between paragraphs is a conversation: typing in the composer with a finding attached opens one.
167
167
 
168
168
  For an ordinary selection, the next composer message becomes the rewrite instruction.
169
169
 
170
- The server returns exactly three strings, shown as a stack of option cards in the chat stream. Clicking a card replaces only the attached range; the remaining cards then retire. Requesting the options hides the finding card it has said its piece and a failed request brings it back. The group carries a refresh-icon `Try again` control that discards the three and re-requests them with the same instruction and the same attached passage.
170
+ The server returns exactly three strings. For a finding they open as a stack of option cards inside its own card, beside the passage they would replace; for an ordinary selection they open as a stack in the chat stream. Clicking a card replaces only the attached range in a single redraw; the new passage flashes briefly so the eye lands on the change, and the remaining cards then retire. Only one undecided group stays up: fresh options retire earlier undecided ones, while applied ones keep their Undo. The group carries a refresh-icon `Try again` control that discards the three and re-requests them with the same instruction and the same attached passage.
171
171
 
172
- The client ranks what the model already produced by the resulting whole-document local score and shows that score delta on each card for Latin-script drafts. Colour marks only a material move; a point or two on an already-clean draft stays uncoloured so the options do not all read as bad. The score is never fed to the model.
172
+ The client ranks what the model already produced best-first by the resulting whole-document local score, shown nowhere. The score is never fed to the model. The hover-to-preview hint shows once, until the first applied option; the Try again control always stays.
173
173
 
174
- While the options are up the attached passage stays highlighted in the draft, and hovering or focusing a card previews that option where it will live — substituted into the paragraph with the sentences around it, on a plain white sheet with no red/green diff. The document itself is untouched while the writer is choosing, so nothing is autosaved, no finding is re-anchored, and undo stays clean until the writer keeps one. `Enter` or `Space` on a focused card applies it; `Escape` dismisses the hover preview. Clicking into the draft only drops the hover preview — the cards stay, because choosing is a click on a card.
174
+ While the options are up the attached passage stays highlighted in the draft, and hovering or focusing a card previews that option where it will live — substituted into the paragraph with the sentences around it, on a plain white sheet with no red/green diff. Moving between cards keeps the preview across a short delay, so the gaps between cards don't flicker it. The document itself is untouched while the writer is choosing, so nothing is autosaved, no finding is re-anchored, and undo stays clean until the writer keeps one. `Enter` or `Space` on a focused card applies it; `Escape` dismisses the hover preview. Clicking into the draft only drops the hover preview — the cards stay, because choosing is a click on a card.
175
175
 
176
176
  A coloured word-level diff (added/removed runs animated in reading order, via `wordDiff`) is deliberately not on this path. It is kept in the tree as a noted option for another place (e.g. history compare), where showing what changed word by word is the job.
177
177
 
178
- Keeping an option leaves only that option in the chat — the other cards step aside — with a single `Undo` on the kept card itself, with no timer: it stays good until the passage changes under it. `Undo` puts back exactly what was replaced and only while it is still there to put back; keeping also takes a History snapshot, which is the longer way back. Choosing an option closes the chat stream; the kept card and its `Undo` survive in the stream history. Closing discards remark cards and undecided option groups but keeps applied ones. Nothing commits on a timer.
178
+ Keeping an option leaves only that option — the other cards step aside — with a single `Undo` on the kept card itself, with no timer: it stays good until the passage changes under it. `Undo` puts back exactly what was replaced and only while it is still there to put back; keeping also takes a History snapshot, which is the longer way back. A kept finding option stays in its card beside the changed passage; a kept selection option stays in the stream history, and choosing it closes the chat stream. Closing discards remark cards and undecided option groups but keeps applied ones: kept finding options move into the stream so their `Undo` survives the close. Nothing commits on a timer.
179
179
 
180
180
  ### 3.5 Draft chat
181
181
 
@@ -195,12 +195,16 @@ After 900 milliseconds without typing, a suggestion may be requested when:
195
195
  - the selection is collapsed;
196
196
  - the caret is at the end of a paragraph or before a blank line;
197
197
  - the current line is not an `/idea` command.
198
+ - the editor has focus, the page is visible, and composition is finished.
198
199
 
199
- A failed continuation says so in the status line rather than going quiet: a feature that fails in silence is indistinguishable from a broken one. The model returns a 5-15 word continuation. Known local tell words veto the result. A valid continuation appears in a block below the current line without entering the document.
200
+ Only typing (including composition) starts this timer. Paste, deletion, Undo, Redo, loading a document and accepting an AI edit cancel pending suggestions without requesting another one.
201
+
202
+ A failed continuation says so in the status line; the next successful request clears its error. The model may return an empty continuation when the thought is finished or continuing would invent facts or the author's intent. Non-empty output is a single line of at most 15 words; one word or punctuation alone is allowed. The provider returns `NONE`, `TEXT: …` for new words or punctuation, or `WORD: …` for missing letters, so prose quotation marks do not need JSON escaping. The HTTP response still uses `{ suggestion: string }`. Invalid output is reported as an error and never inserted. Spacing is applied at the insertion boundary; repeated tails are removed only at word boundaries. A valid continuation appears in a block below the current line, is announced to assistive technology, and does not enter the document until accepted.
200
203
 
201
204
  - `Tab`: insert the suggestion at its original cursor position.
202
- - `Escape`: dismiss it.
203
- - Typing or moving the cursor: dismiss it and cancel pending work.
205
+ - `Escape`: dismiss it and cancel pending work, even before an answer arrives.
206
+ - `Cmd/Ctrl+Enter`: request a continuation explicitly, including when automatic suggestions are off. This permits short non-empty drafts and reports when there is nothing useful to suggest.
207
+ - Typing, changing the selection, leaving the editor, hiding the page, changing the model or pausing saves: dismiss or invalidate the suggestion and cancel pending work. An answer is displayed only for the same document, full selection, model and active request with the editor still focused.
204
208
 
205
209
  ### 3.7 `/idea` expansion
206
210
 
@@ -223,9 +227,9 @@ The settings dialog:
223
227
  - switches automatic review and continuation suggestions on or off, both on by default;
224
228
  - switches the npm update check on or off, on by default.
225
229
 
226
- The switches take effect immediately; the provider, model, and reasoning selection is applied by `Save`.
230
+ All settings take effect immediately; there is no Save button.
227
231
 
228
- When no model is selected, the review, rewrite, and chat actions re-check Pi status once, then report the missing setup in the composer and open this dialog instead of failing silently. Automatic review and continuation suggestions stay silent and make no request.
232
+ When no model is selected, the review, rewrite, chat, and manual-continuation actions re-check Pi status once, then report the missing setup in the composer and open this dialog instead of failing silently. Automatic review and continuation suggestions stay silent and make no request.
229
233
 
230
234
  ## 4. State and privacy
231
235
 
@@ -271,6 +275,25 @@ Every model-backed action sends the current full document to the selected provid
271
275
  - **Model runtime:** `@earendil-works/pi-ai` and `@earendil-works/pi-coding-agent`.
272
276
  - **Streaming:** Server-Sent Events for `/idea` and `/chat`.
273
277
  - **Non-streaming:** JSON for status, credentials, review, rewrite, and suggestions.
278
+ - **Desktop shell:** Tauri 2, a window and a menu around the same server.
279
+
280
+ ### Desktop shell
281
+
282
+ The desktop app is a packaging of this server, not a second implementation. The Rust shell picks the writing folder, starts the server, points a window at it, and owns updates; documents, review, and model access stay where they already are. No Tauri JavaScript API reaches the page — the webview loads `http://127.0.0.1:<port>` like any browser would, which is why `src/app.js` is identical in both builds.
283
+
284
+ The server is a Tauri sidecar: an official Node binary, fetched and checksummed at build time by `scripts/fetch-node.mjs`, running `dist/server.mjs` — `index.js` and its dependencies bundled into one file by `scripts/build-server.mjs`. The writer installs no runtime of their own.
285
+
286
+ Three pieces of the contract between them are invisible from the browser, so `safetycheck.js` asserts them:
287
+
288
+ - `PORT=0` lets the operating system pick, so a second copy of Litura is not a startup failure.
289
+ - With `LITURA_SIDECAR` set the server prints `LITURA_READY <url>` on stdout. The shell waits for that line before it navigates the window, and opens no browser of its own.
290
+ - The same flag makes the server exit when its stdin closes. A shell that dies takes its server with it instead of leaving one holding the draft's lock file.
291
+
292
+ `LITURA_ROOT` tells the bundled server where `public/`, `package.json`, and `style.md` are, since the single-file bundle no longer sits beside them. Nothing else about the server changes.
293
+
294
+ On macOS the window has no title bar of its own: the draft's header is the title bar, and the system's window buttons are drawn over it. The shell asks for the page with `?shell=desktop`, and an inline script in `index.html` turns that into a class the header uses to leave the buttons room — a query string rather than a header, so a reload keeps it. Windows and Linux keep their frame: dropping it there means reimplementing minimise, maximise, close, and window dragging, which would need Tauri's JavaScript API inside a page that is deliberately kept free of it.
295
+
296
+ The folder choice is one line of text in the app's config directory (`folder.txt`), written when the writer picks a folder and read at every launch. `File → Open Folder…` writes it and restarts the app rather than teaching the server to switch files mid-session: a different draft is a different server.
274
297
 
275
298
  `npm start` bundles the frontend before starting the server in a source checkout. If the build fails, startup stops with an error. The published package has no `src/` directory and serves its bundled frontend without rebuilding. `public/app.js` is generated and should not be edited directly.
276
299
 
@@ -289,7 +312,17 @@ litura/
289
312
  |- selfcheck.js assertion-based checks
290
313
  |- deepcheck.js model-backed essay and paragraph checks
291
314
  |- style.md writing constraints injected into model prompts
292
- |- plugin.json webview plugin manifest (/write, port 3456)
315
+ |- plugin.json webview plugin manifest (/write, port 3000)
316
+ |- scripts/
317
+ | |- build-server.mjs bundles index.js into dist/server.mjs for the desktop build
318
+ | |- fetch-node.mjs downloads and checksums the Node runtime the desktop build ships
319
+ | `- smoke-desktop.mjs starts a built bundle and checks it serves and stops
320
+ |- src-tauri/
321
+ | |- src/main.rs desktop shell: folder, sidecar, window, menu, updater
322
+ | |- tauri.conf.json bundle targets, resources, updater endpoint and public key
323
+ | |- shell/index.html holding page shown until the server is listening
324
+ | |- icons/ generated app icon set
325
+ | `- binaries/ fetched Node runtime, gitignored
293
326
  |- src/
294
327
  | `- app.js frontend source
295
328
  `- public/
@@ -364,7 +397,7 @@ An SSE error is emitted as `{"error":"..."}` when headers have already been sent
364
397
 
365
398
  If `style.md` is missing, the server logs one warning and continues without it.
366
399
 
367
- Review detects problems but does not rewrite. Rewrite, suggestion, idea, and chat prompts have separate output contracts and token limits.
400
+ Review detects problems but does not rewrite. Rewrite, suggestion, idea, and chat prompts have separate output contracts and token limits. Continuations use the shared builder in `suggestions.js`, with explicit abstention and word-joining rules. The production path keeps the full style guide and document; reduced prompts and local context are evaluation-only experiments.
368
401
 
369
402
  The rewrite prompt leads with the selection, then the instruction, then the containing sentence with the selection replaced by a `___` slot, and only then the full document with the selection marked. A variant longer than three times the selection is treated as a whole-document rewrite: the request is repeated once with the scope restated. Unparseable answers are retried up to three attempts in total.
370
403
 
@@ -377,11 +410,14 @@ Each review pass validates diagnostic codes against its assigned levels and chec
377
410
  | `PI_PROVIDER` | Preferred Pi provider ID | Selected from authenticated models |
378
411
  | `PI_MODEL` | Preferred Pi model ID | Selected from authenticated models |
379
412
  | `PI_THINKING_LEVEL` | Preferred reasoning level | `medium` |
380
- | `PORT` | Local HTTP port; the next free port up to +10 is used if taken | `3456` |
413
+ | `PORT` | Local HTTP port; the next free port up to +10 is used if taken | `3000` |
381
414
  | `STYLE_FILE` | Writing style guide path | `<cwd>/style.md`, else the bundled `style.md` |
382
415
  | `DRAFT_FILE` | Draft mirror path | `<cwd>/draft.md` |
383
416
  | `LITURA_NO_OPEN` | Set to skip opening the browser at startup | unset |
384
417
  | `LITURA_OFFLINE_MODELS` | Set to stop Litura refreshing provider model lists | unset |
418
+ | `LITURA_SIDECAR` | Set by the desktop shell: ready line, no browser, exit with stdin, updates handled by the shell | unset |
419
+ | `LITURA_ROOT` | Directory holding `public/`, `package.json`, and `style.md` | the server's own directory |
420
+ | `LITURA_NODE_VERSION` | Node runtime the desktop build ships | `v24.21.0` |
385
421
 
386
422
  ## 9. Distribution and updates
387
423
 
@@ -389,6 +425,12 @@ Litura is published to npm as `litura-app`; the command it installs is `litura`.
389
425
 
390
426
  `litura --version` prints the running version. `litura --check-update` prints the published version next to it, and reports an unpublished package rather than comparing against nothing. Neither flag ranks the two versions: a local build legitimately runs ahead of the registry, and ordering semver correctly would be a dependency.
391
427
 
428
+ The desktop app is distributed as signed bundles attached to the GitHub release for the tag: `.dmg` and `.app.tar.gz` per macOS architecture, an NSIS installer on Windows, `.deb` and `.AppImage` on Linux. `.github/workflows/desktop.yml` builds one matrix entry per target and uploads them, along with the `latest.json` the updater reads.
429
+
430
+ Updates there are the shell's, not npm's. On launch and from `Litura → Check for Updates…` it asks the release endpoint; an answer is installed only after the writer accepts, is verified against a minisign public key compiled into the app, and is followed by a restart. A launch-time check that cannot reach GitHub says nothing; a check the writer asked for reports why it failed. Because a second update path would be a second wrong answer, `/api/version` returns `managed: "desktop"` under `LITURA_SIDECAR`, contacts npm for nothing, and the frontend hides the update section of settings.
431
+
432
+ The signing key is not in the repository. CI reads it from `TAURI_SIGNING_PRIVATE_KEY` and `TAURI_SIGNING_PRIVATE_KEY_PASSWORD`; the public half lives in `tauri.conf.json`. Replacing it strands every installed copy, which will accept nothing the old key did not sign.
433
+
392
434
  In the browser, `Ask npm about new versions` in settings is on by default and stored in `wa-updatecheck`. Off, the browser calls `/api/version` without `?check=1` and the server contacts nothing. On, it asks at most once every 24 hours, caches the answer in `wa-update`, and shows a header badge linking to the changelog when the published version differs from the running one. Switching it off hides the badge rather than leaving a stale one.
393
435
 
394
436
  ### Releasing
@@ -418,6 +460,10 @@ npm run check:deep
418
460
 
419
461
  `npm run check:deep` sends synthetic failures and clean controls through the configured Pi model and the exact production review prompts. It covers the seven reader-structure levels, exact quote anchoring, targeted-review scope, all four valid paragraph progressions, and paired positive/control cases for selected generic prose diagnostics. Use `DEEP_CASE=name` to run matching cases and `DEEP_RUNS=3` to measure repeatability. The command makes model requests and is therefore kept out of the fast check.
420
462
 
463
+ `npm run smoke:desktop` runs a built bundle — the `.app` on macOS, the binary beside its resources elsewhere — against a disposable folder and a moved `HOME`, so it never meets the writer's own config or draft. It asserts the three things only a packaged build can get wrong: the app starts and prints its port, the server inside it serves the seeded draft from the folder it was given, and killing the shell releases the port. It needs a display; a headless Linux runner puts `xvfb-run -a` in front. `.github/workflows/desktop.yml` runs it after every platform's build.
464
+
465
+ The UI itself needs neither: the page in the desktop window is the same page the server gives a browser, so `npm run test:browser` — the real routes against a fake provider — covers it, and adding `?shell=desktop` to that URL shows the desktop layout. What has no automated coverage is the window: the native dialogs, the menu, and the updater's own code.
466
+
421
467
  The model checks are regression tests, not an independent accuracy benchmark. Set `DEEP_REPORT=results.json` to save individual findings and failures. See [review-evaluation.md](docs/review-evaluation.md) for source provenance, development results, limitations, and the manual browser smoke check. There is no automated browser end-to-end suite.
422
468
 
423
469
  ## 11. Current boundaries