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 +6 -0
- package/index.js +4 -3
- package/package.json +2 -1
- package/public/app.js +23 -11
- package/public/index.html +0 -1
- package/suggestions.js +16 -4
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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
10
|
-
|
|
11
|
-
|
|
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.
|