litura-app 0.4.0 → 0.4.2

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.4.2 — 2026-09-13
9
+
10
+ - Clicking at the end of a paragraph now offers a Continue bubble, asking for the same continuation Cmd/Ctrl+Enter would. An explicit ask is allowed to open the next sentence and run longer than a background suggestion, instead of stopping at a finished thought.
11
+ - Fixed the field popovers flashing at their previous position before snapping to the trigger.
12
+ - Removed the unused standalone Rewrite button from the chat chip.
13
+
8
14
  ## 0.4.0 — 2026-09-13
9
15
 
10
16
  - Added spell checking. Misspelled words are marked in the editor using a vendored en_US Hunspell dictionary, checked client-side alongside the existing style-tell markers.
package/index.js CHANGED
@@ -542,20 +542,21 @@ async function handleRequest(req, res) {
542
542
 
543
543
  // POST /suggest — inline ghost-text suggestion (VS Code style)
544
544
  //
545
- // Request: { context, document, cursor }
545
+ // Request: { context, document, cursor, manual }
546
546
  // cursor is the character offset where the ghost text will appear.
547
+ // manual marks a continuation the writer asked for, not one offered.
547
548
  //
548
549
  // Returns: { suggestion: string } — a short natural continuation
549
550
  //
550
551
  if (url.pathname === '/suggest') {
551
- const { context, document: doc, cursor } = body;
552
+ const { context, document: doc, cursor, manual } = body;
552
553
 
553
554
  const at = cursor ?? (doc ?? '').length;
554
555
  const prefix = (doc ?? '').slice(0, at);
555
556
 
556
557
  try {
557
558
  const suggestion = await completeText({
558
- ...suggestionPrompts({ document: doc, cursor: at, context, style: readStyle() }),
559
+ ...suggestionPrompts({ document: doc, cursor: at, context, style: readStyle(), manual: !!manual }),
559
560
  selection: body.agent,
560
561
  continuation: true,
561
562
  signal: AbortSignal.any([signal, AbortSignal.timeout(30_000)]),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "litura-app",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "A local AI-assisted editor for sharper, more human writing",
5
5
  "keywords": [
6
6
  "writing",
@@ -48,6 +48,7 @@
48
48
  "desktop:prepare": "npm run build && npm run build:server && node scripts/fetch-node.mjs",
49
49
  "desktop": "tauri dev",
50
50
  "desktop:build": "tauri build",
51
+ "tauri": "tauri",
51
52
  "smoke:desktop": "node scripts/smoke-desktop.mjs",
52
53
  "check": "node --check index.js && node --check pi.js && node --check review-model.js && node --check deepcheck.js && npm run build && node selfcheck.js && node tests/suggestions.mjs && node safetycheck.js",
53
54
  "test:browser": "node tests/browser-fixture.mjs",
package/public/app.js CHANGED
@@ -19175,8 +19175,8 @@
19175
19175
  empty2.hidden = matched.length > 0;
19176
19176
  if (count) count.textContent = !state.options.length ? "" : matched.length > shown.length ? `Showing ${shown.length} of ${matched.length}` : "";
19177
19177
  }
19178
- function reposition() {
19179
- if (!popover.matches(":popover-open")) return;
19178
+ function reposition(force) {
19179
+ if (!force && !popover.matches(":popover-open")) return;
19180
19180
  const box = trigger.getBoundingClientRect();
19181
19181
  const width = Math.max(box.width, 260);
19182
19182
  popover.style.left = `${Math.min(box.left, window.innerWidth - width - 8)}px`;
@@ -19184,7 +19184,9 @@
19184
19184
  popover.style.width = `${width}px`;
19185
19185
  }
19186
19186
  state.reposition = reposition;
19187
- trigger.addEventListener("click", reposition);
19187
+ popover.addEventListener("beforetoggle", (event) => {
19188
+ if (event.newState === "open") reposition(true);
19189
+ });
19188
19190
  popover.addEventListener("toggle", (event) => {
19189
19191
  if (event.newState !== "open") return;
19190
19192
  search.value = "";
@@ -20042,7 +20044,7 @@
20042
20044
  suggestTimer = null;
20043
20045
  const view = workView;
20044
20046
  const state = view.state;
20045
- if (state.readOnly || loadingDocument || savePaused || !view.hasFocus || document.hidden || view.composing) return;
20047
+ if (state.readOnly || loadingDocument || savePaused || !manual && !view.hasFocus || document.hidden || view.composing) return;
20046
20048
  if (!manual && !autoSuggestOn) return;
20047
20049
  if (!atParagraphEnd(state)) {
20048
20050
  if (manual) setReviewStatus("Place the caret at the end of a paragraph to request a continuation.", void 0, "suggest");
@@ -20058,11 +20060,11 @@
20058
20060
  if (/^\s*\/idea(?:\s|$)/i.test(line.text)) return;
20059
20061
  const version = suggestVersion;
20060
20062
  if (manual && !await ensureAgent()) return;
20061
- if (version !== suggestVersion || !view.hasFocus || !currentAgent()) return;
20063
+ if (version !== suggestVersion || !manual && !view.hasFocus || !currentAgent()) return;
20062
20064
  const agent = currentAgent();
20063
20065
  const job = startJob({ background: !manual });
20064
20066
  suggestAbort = job;
20065
- const isCurrent = () => suggestAbort === job && !job.signal.aborted && version === suggestVersion && !loadingDocument && !savePaused && !document.hidden && view.hasFocus && !view.composing && !view.state.readOnly && (manual || autoSuggestOn) && view.state.doc === state.doc && view.state.selection.eq(state.selection) && JSON.stringify(currentAgent()) === JSON.stringify(agent);
20067
+ const isCurrent = () => suggestAbort === job && !job.signal.aborted && version === suggestVersion && !loadingDocument && !savePaused && !document.hidden && (manual || view.hasFocus) && !view.composing && !view.state.readOnly && (manual || autoSuggestOn) && view.state.doc === state.doc && view.state.selection.eq(state.selection) && JSON.stringify(currentAgent()) === JSON.stringify(agent);
20066
20068
  if (manual) setReviewStatus("Suggesting\u2026", void 0, "suggest");
20067
20069
  try {
20068
20070
  const res = await fetch("/suggest", {
@@ -20071,7 +20073,8 @@
20071
20073
  body: JSON.stringify({
20072
20074
  document: doc2,
20073
20075
  cursor: pos,
20074
- agent
20076
+ agent,
20077
+ manual
20075
20078
  }),
20076
20079
  signal: job.signal
20077
20080
  });
@@ -20518,6 +20521,18 @@ ${message.content}`;
20518
20521
  markBubble.replaceChildren(actions);
20519
20522
  placeBubble(x, y);
20520
20523
  }
20524
+ function showContinueBubble(x, y) {
20525
+ bubbleMode = "continue";
20526
+ const actions = chatEl("div", "mark-bubble-actions");
20527
+ actions.append(bubbleButton("Continue", () => {
20528
+ workView.focus();
20529
+ cancelSuggestion();
20530
+ ghostClear(workView);
20531
+ void suggestFetch(true);
20532
+ }));
20533
+ markBubble.replaceChildren(actions);
20534
+ placeBubble(x, y);
20535
+ }
20521
20536
  function showSpellBubble(marker, x, y) {
20522
20537
  bubbleMode = "spell";
20523
20538
  const actions = chatEl("div", "mark-bubble-actions");
@@ -21152,10 +21167,6 @@ Editing direction: ${activeFinding.fix}`,
21152
21167
  chatInput.closest(".chat-composer")?.classList.toggle("is-busy", busy);
21153
21168
  chatInput.placeholder = busy ? "Working\u2026" : basePlaceholder();
21154
21169
  }
21155
- document.getElementById("chat-rewrite").addEventListener("click", () => {
21156
- const range = attachedRange();
21157
- if (range) requestVariants(chatInput.value.trim() || "Make this passage clearer. Preserve meaning, facts and voice.");
21158
- });
21159
21170
  chatSendButton.addEventListener("click", () => {
21160
21171
  if ([...jobs].some((job) => !job.background)) {
21161
21172
  stopJobs();
@@ -21431,6 +21442,7 @@ Editing direction: ${activeFinding.fix}`,
21431
21442
  const local = event.target.closest?.(".cm-style-marker");
21432
21443
  const marker = local && view.state.field(styleMarkerField).find((item) => item.from === Number(local.dataset.styleFrom));
21433
21444
  if (marker) selectStyleMarker(marker);
21445
+ else if (atParagraphEnd(view.state)) showContinueBubble(at.x, at.y);
21434
21446
  return false;
21435
21447
  }
21436
21448
  const finding = reviewFindings.find((item) => item.id === Number(mark.dataset.slopId));
package/public/index.html CHANGED
@@ -107,7 +107,6 @@
107
107
  what is attached and offers the way out. -->
108
108
  <div class="chat-chip hidden" id="chat-chip">
109
109
  <span class="chat-chip-text" id="chat-chip-text"></span>
110
- <button type="button" id="chat-rewrite">Rewrite</button>
111
110
  <button type="button" id="chat-chip-clear" aria-label="Detach selection">×</button>
112
111
  </div>
113
112
  <div class="chat-row">
package/suggestions.js CHANGED
@@ -1,14 +1,26 @@
1
1
  import { trimOverlap } from './review.js';
2
2
 
3
3
  // Shared by the HTTP route and the evaluation runner so they test the same task.
4
- export function suggestionPrompts({ document = '', cursor = document.length, context, style }) {
4
+ //
5
+ // `manual` is the writer pressing Continue rather than the debounce firing.
6
+ // Silence is the right answer for something nobody asked for; for an explicit
7
+ // press it reads as a broken button, so a finished sentence stops being a
8
+ // reason to abstain. The grounding rules do not move — an asked-for
9
+ // continuation may invent no more than an offered one.
10
+ export function suggestionPrompts({ document = '', cursor = document.length, context, style, manual = false }) {
11
+ const thought = manual
12
+ ? 'Stay on the current thought; when it is already finished, open the next sentence its own line of thought calls for.'
13
+ : 'Stay on the last thought.';
14
+ const silence = manual
15
+ ? 'Return an empty text when the author\'s intent is unclear or continuing would require inventing a fact, source, number, experience or intention. A finished sentence is not itself a reason for silence — the writer asked for the next move.'
16
+ : 'Return an empty text when the thought is complete, the author\'s intent is unclear, or continuing would require inventing a fact, source, number, experience or intention. Silence is better than filler.';
5
17
  return {
6
18
  systemPrompt: [
7
19
  style ? `Follow this writing style guide:\n\n${style}` : '',
8
20
  `You are an inline writing assistant. Fill the cursor position without changing existing text.
9
- Match the draft's language, voice, vocabulary, rhythm and factual uncertainty. Stay on the last thought.
10
- Offer only the smallest useful continuation, at most 15 words. One to three words or just punctuation may be enough.
11
- Return an empty text when the thought is complete, the author's intent is unclear, or continuing would require inventing a fact, source, number, experience or intention. Silence is better than filler.
21
+ Match the draft's language, voice, vocabulary, rhythm and factual uncertainty. ${thought}
22
+ Let the continuation's length follow how well the material supports it, up to 15 words: a word or punctuation when that finishes the thought, a full clause or sentence when the draft or context clearly carries it further.
23
+ ${silence}
12
24
  Complete grammar, punctuation, an unfinished word, or a consequence directly supported by the supplied facts. Never choose a decision or next event for the author. If materially different completions are equally possible, abstain.
13
25
  Specific wording must come from the supplied material. Reuse the objects actually named by the author; do not introduce a more specific type of an object or an unstated way it works. When the existing words in a quotation form a complete phrase, close the quotation instead of inventing more quoted words.
14
26
  Do not invent evidence or claims, restate the draft, add generic conclusions, or repeat or contradict the text after the cursor.