Compare commits

..

14 Commits

Author SHA1 Message Date
Elian Doran
bd1491e6e5 feat(options/i18n): add reference to spell check 2026-04-06 22:08:23 +03:00
Elian Doran
ac35730e3b feat(options/spellcheck): add button to reload app 2026-04-06 21:56:31 +03:00
Elian Doran
00023adbc0 Revert "feat(options/spellcheck): merge into single card"
This reverts commit 7b056fe1af.
2026-04-06 21:53:17 +03:00
Elian Doran
a70142a4dc feat(options/spellcheck): add button to edit custom words 2026-04-06 21:50:54 +03:00
Elian Doran
7b056fe1af feat(options/spellcheck): merge into single card 2026-04-06 21:44:49 +03:00
Elian Doran
467be38bd1 feat(options/spellcheck): improve language selection 2026-04-06 21:39:58 +03:00
Elian Doran
f56482157c chore(ai): update system prompt 2026-04-06 21:25:54 +03:00
Elian Doran
5d0c91d91d fix(spellcheck): don't remove local words every time 2026-04-06 20:46:38 +03:00
Elian Doran
03136611a1 fix(spellcheck): don't merge words every time 2026-04-06 20:44:13 +03:00
Elian Doran
3e7488e4f3 feat(spellcheck): clean up local words 2026-04-06 20:36:51 +03:00
Elian Doran
3ed7d48d42 feat(spellcheck): save new words to custom dictionary 2026-04-06 20:28:22 +03:00
Elian Doran
ef72d89172 fix(spellcheck): custom dictionary not actually saved due to CLS 2026-04-06 20:16:02 +03:00
Elian Doran
ad97071862 feat(spellcheck): basic logic to save words 2026-04-06 20:09:29 +03:00
Elian Doran
2291892946 chore(server): create hidden note for the dictionary 2026-04-06 19:55:42 +03:00
18 changed files with 854 additions and 279 deletions

View File

@@ -122,6 +122,12 @@ Trilium provides powerful user scripting capabilities:
- Third-party components (e.g., mind-map context menu) should use i18next `t()` for their labels, with the English strings added to `en/translation.json` under a dedicated namespace (e.g., `"mind-map"`)
- When a translated string contains **interpolated components** (e.g. links, note references) whose order may vary across languages, use `<Trans>` from `react-i18next` instead of `t()`. This lets translators reorder components freely (e.g. `"<Note/> in <Parent/>"` vs `"in <Parent/>, <Note/>"`)
- When adding a new locale, follow the step-by-step guide in `docs/Developer Guide/Developer Guide/Concepts/Internationalisation Translations/Adding a new locale.md`
- **Server-side translations** (e.g. hidden subtree titles) go in `apps/server/src/assets/translations/en/server.json`, not in the client `translation.json`
### Electron Desktop App
- Desktop entry point: `apps/desktop/src/main.ts`, window management: `apps/server/src/services/window.ts`
- IPC communication: use `electron.ipcMain.on(channel, handler)` on server side, `electron.ipcRenderer.send(channel, data)` on client side
- Electron-only features should check `isElectron()` from `apps/client/src/services/utils.ts` (client) or `utils.isElectron` (server)
### Security Considerations
- Per-note encryption with granular protected sessions
@@ -153,6 +159,20 @@ Trilium provides powerful user scripting capabilities:
- Create new package in `packages/` following existing plugin structure
- Register in `packages/ckeditor5/src/plugins.ts`
### Adding Hidden System Notes
The hidden subtree (`_hidden`) contains system notes with predictable IDs (prefixed with `_`). Defined in `apps/server/src/services/hidden_subtree.ts` via the `HiddenSubtreeItem` interface from `@triliumnext/commons`.
1. Add the note definition to `buildHiddenSubtreeDefinition()` in `apps/server/src/services/hidden_subtree.ts`
2. Add a translation key for the title in `apps/server/src/assets/translations/en/server.json` under `"hidden-subtree"`
3. The note is auto-created on startup by `checkHiddenSubtree()` — uses deterministic IDs so all sync cluster instances generate the same structure
4. Key properties: `id` (must start with `_`), `title`, `type`, `icon` (format: `bx-icon-name` without `bx ` prefix), `attributes`, `children`, `content`
5. Use `enforceAttributes: true` to keep attributes in sync, `enforceBranches: true` for correct placement, `enforceDeleted: true` to remove deprecated notes
6. For launcher bar entries, see `hidden_subtree_launcherbar.ts`; for templates, see `hidden_subtree_templates.ts`
### Writing to Notes from Server Services
- `note.setContent()` requires a CLS (Continuation Local Storage) context — wrap calls in `cls.init(() => { ... })` (from `apps/server/src/services/cls.ts`)
- Operations called from Express routes already have CLS context; standalone services (schedulers, Electron IPC handlers) do not
### Adding New LLM Tools
Tools are defined using `defineTools()` in `apps/server/src/services/llm/tools/` and automatically registered for both the LLM chat and MCP server.

View File

@@ -38,7 +38,7 @@ function setupContextMenu() {
items.push({
title: t("electron_context_menu.add-term-to-dictionary", { term: params.misspelledWord }),
uiIcon: "bx bx-plus",
handler: () => webContents.session.addWordToSpellCheckerDictionary(params.misspelledWord)
handler: () => electron.ipcRenderer.send("add-word-to-dictionary", params.misspelledWord)
});
items.push({ kind: "separator" });

View File

@@ -1498,12 +1498,15 @@
"spellcheck": {
"title": "Spell Check",
"description": "These options apply only for desktop builds, browsers will use their own native spell check.",
"enable": "Enable spellcheck",
"language_code_label": "Language code(s)",
"language_code_placeholder": "for example \"en-US\", \"de-AT\"",
"multiple_languages_info": "Multiple languages can be separated by comma, e.g. \"en-US, de-DE, cs\". ",
"available_language_codes_label": "Available language codes:",
"restart-required": "Changes to the spell check options will take effect after application restart."
"enable": "Check spelling",
"language_code_label": "Spell Check Languages",
"restart-required": "Changes to the spell check options will take effect after application restart.",
"custom_dictionary_title": "Custom Dictionary",
"custom_dictionary_description": "Words added to the dictionary are synced across all your devices.",
"custom_dictionary_edit": "Custom words",
"custom_dictionary_edit_description": "Edit the list of words that should not be flagged by the spell checker. Changes will be visible after a restart.",
"custom_dictionary_open": "Edit dictionary",
"related_description": "Configure spell check languages and custom dictionary."
},
"sync_2": {
"config_title": "Sync Configuration",

View File

@@ -45,3 +45,15 @@
.option-row.centered {
justify-content: center;
}
.option-row-link.use-tn-links {
text-decoration: none;
color: inherit;
margin-inline: calc(-1 * var(--options-card-padding, 15px));
padding-inline: var(--options-card-padding, 15px);
transition: background-color 250ms ease-in-out;
}
.option-row-link:hover {
background: var(--hover-item-background-color);
}

View File

@@ -1,5 +1,7 @@
import { cloneElement, VNode } from "preact";
import "./OptionsRow.css";
import { cloneElement, VNode } from "preact";
import { useUniqueName } from "../../../react/hooks";
interface OptionsRowProps {
@@ -25,4 +27,24 @@ export default function OptionsRow({ name, label, description, children, centere
</div>
</div>
);
}
}
interface OptionsRowLinkProps {
label: string;
description?: string;
href: string;
}
export function OptionsRowLink({ label, description, href }: OptionsRowLinkProps) {
return (
<a href={href} className="option-row option-row-link use-tn-links no-tooltip-preview">
<div className="option-row-label">
<label style={{ cursor: "pointer" }}>{label}</label>
{description && <small className="option-row-description">{description}</small>}
</div>
<div className="option-row-input">
<span className="bx bx-chevron-right" />
</div>
</a>
);
}

View File

@@ -1,24 +1,29 @@
import OptionsSection from "./OptionsSection";
import type { OptionPages } from "../../ContentWidget";
import { t } from "../../../../services/i18n";
import type { OptionPages } from "../../ContentWidget";
import { OptionsRowLink } from "./OptionsRow";
import OptionsSection from "./OptionsSection";
interface RelatedSettingsItem {
title: string;
description?: string;
targetPage: OptionPages;
}
interface RelatedSettingsProps {
items: {
title: string;
targetPage: OptionPages;
}[];
items: RelatedSettingsItem[];
}
export default function RelatedSettings({ items }: RelatedSettingsProps) {
return (
<OptionsSection title={t("settings.related_settings")}>
<nav className="use-tn-links" style={{ padding: 0, margin: 0, listStyleType: "none" }}>
{items.map(item => (
<li>
<a href={`#root/_hidden/_options/${item.targetPage}`}>{item.title}</a>
</li>
))}
</nav>
{items.map((item) => (
<OptionsRowLink
key={item.targetPage}
label={item.title}
description={item.description}
href={`#root/_hidden/_options/${item.targetPage}`}
/>
))}
</OptionsSection>
);
}

View File

@@ -5,13 +5,14 @@ import OptionsRow from "./components/OptionsRow";
import OptionsSection from "./components/OptionsSection";
import { useTriliumOption, useTriliumOptionJson } from "../../react/hooks";
import type { Locale } from "@triliumnext/commons";
import { restartDesktopApp } from "../../../services/utils";
import { isElectron, restartDesktopApp } from "../../../services/utils";
import FormRadioGroup from "../../react/FormRadioGroup";
import FormText from "../../react/FormText";
import RawHtml from "../../react/RawHtml";
import Admonition from "../../react/Admonition";
import Button from "../../react/Button";
import CheckboxList from "./components/CheckboxList";
import RelatedSettings from "./components/RelatedSettings";
import { LocaleSelector } from "./components/LocaleSelector";
export default function InternationalizationOptions() {
@@ -19,8 +20,17 @@ export default function InternationalizationOptions() {
<>
<LocalizationOptions />
<ContentLanguages />
{isElectron() && (
<RelatedSettings items={[
{
title: t("spellcheck.title"),
description: t("spellcheck.related_description"),
targetPage: "_optionsSpellcheck"
}
]} />
)}
</>
)
);
}
function LocalizationOptions() {

View File

@@ -1,57 +1,122 @@
import { useMemo } from "preact/hooks";
import { useCallback, useMemo } from "preact/hooks";
import appContext from "../../../components/app_context";
import { t } from "../../../services/i18n";
import FormCheckbox from "../../react/FormCheckbox";
import FormGroup from "../../react/FormGroup";
import { dynamicRequire, isElectron, restartDesktopApp } from "../../../services/utils";
import Button from "../../react/Button";
import FormText from "../../react/FormText";
import FormTextBox from "../../react/FormTextBox";
import FormToggle from "../../react/FormToggle";
import { useTriliumOption, useTriliumOptionBool } from "../../react/hooks";
import CheckboxList from "./components/CheckboxList";
import OptionsRow from "./components/OptionsRow";
import OptionsSection from "./components/OptionsSection";
import { dynamicRequire, isElectron } from "../../../services/utils";
export default function SpellcheckSettings() {
if (isElectron()) {
return <ElectronSpellcheckSettings />
} else {
return <WebSpellcheckSettings />
return <ElectronSpellcheckSettings />;
}
return <WebSpellcheckSettings />;
}
interface SpellcheckLanguage {
code: string;
name: string;
}
function ElectronSpellcheckSettings() {
const [ spellCheckEnabled, setSpellCheckEnabled ] = useTriliumOptionBool("spellCheckEnabled");
return (
<>
<OptionsSection title={t("spellcheck.title")}>
<FormText>{t("spellcheck.restart-required")}</FormText>
<OptionsRow name="spell-check-enabled" label={t("spellcheck.enable")}>
<FormToggle
switchOnName="" switchOffName=""
currentValue={spellCheckEnabled}
onChange={setSpellCheckEnabled}
/>
</OptionsRow>
<OptionsRow name="restart" centered>
<Button
name="restart-app-button"
text={t("electron_integration.restart-app-button")}
size="micro"
onClick={restartDesktopApp}
/>
</OptionsRow>
</OptionsSection>
{spellCheckEnabled && <SpellcheckLanguages />}
{spellCheckEnabled && <CustomDictionary />}
</>
);
}
function SpellcheckLanguages() {
const [ spellCheckLanguageCode, setSpellCheckLanguageCode ] = useTriliumOption("spellCheckLanguageCode");
const availableLanguageCodes = useMemo(() => {
const selectedCodes = useMemo(() =>
(spellCheckLanguageCode ?? "")
.split(",")
.map((c) => c.trim())
.filter((c) => c.length > 0),
[spellCheckLanguageCode]
);
const setSelectedCodes = useCallback((codes: string[]) => {
setSpellCheckLanguageCode(codes.join(", "));
}, [setSpellCheckLanguageCode]);
const availableLanguages = useMemo<SpellcheckLanguage[]>(() => {
if (!isElectron()) {
return [];
}
const { webContents } = dynamicRequire("@electron/remote").getCurrentWindow();
return webContents.session.availableSpellCheckerLanguages as string[];
}, [])
const { webContents } = dynamicRequire("@electron/remote").getCurrentWindow();
const codes = webContents.session.availableSpellCheckerLanguages as string[];
const displayNames = new Intl.DisplayNames([navigator.language], { type: "language" });
return codes.map((code) => ({
code,
name: displayNames.of(code) ?? code
})).sort((a, b) => a.name.localeCompare(b.name));
}, []);
return (
<OptionsSection title={t("spellcheck.title")}>
<FormText>{t("spellcheck.restart-required")}</FormText>
<FormCheckbox
name="spell-check-enabled"
label={t("spellcheck.enable")}
currentValue={spellCheckEnabled} onChange={setSpellCheckEnabled}
<OptionsSection title={t("spellcheck.language_code_label")}>
<CheckboxList
values={availableLanguages}
keyProperty="code" titleProperty="name"
currentValue={selectedCodes}
onChange={setSelectedCodes}
columnWidth="200px"
/>
<FormGroup name="spell-check-languages" label={t("spellcheck.language_code_label")} description={t("spellcheck.multiple_languages_info")}>
<FormTextBox
placeholder={t("spellcheck.language_code_placeholder")}
currentValue={spellCheckLanguageCode} onChange={setSpellCheckLanguageCode}
/>
</FormGroup>
<FormText>
<strong>{t("spellcheck.available_language_codes_label")} </strong>
{availableLanguageCodes.join(", ")}
</FormText>
</OptionsSection>
)
);
}
function CustomDictionary() {
function openDictionary() {
appContext.triggerCommand("openInPopup", { noteIdOrPath: "_customDictionary" });
}
return (
<OptionsSection title={t("spellcheck.custom_dictionary_title")}>
<FormText>{t("spellcheck.custom_dictionary_description")}</FormText>
<OptionsRow name="custom-dictionary" label={t("spellcheck.custom_dictionary_edit")} description={t("spellcheck.custom_dictionary_edit_description")}>
<Button
name="open-custom-dictionary"
text={t("spellcheck.custom_dictionary_open")}
icon="bx bx-edit"
onClick={openDictionary}
/>
</OptionsRow>
</OptionsSection>
);
}
function WebSpellcheckSettings() {
@@ -59,5 +124,5 @@ function WebSpellcheckSettings() {
<OptionsSection title={t("spellcheck.title")}>
<p>{t("spellcheck.description")}</p>
</OptionsSection>
)
}
);
}

View File

@@ -131,7 +131,7 @@
"tmp": "0.2.5",
"turnish": "1.8.0",
"unescape": "1.0.1",
"vite": "8.0.5",
"vite": "8.0.3",
"ws": "8.20.0",
"xml2js": "0.6.2",
"yauzl": "3.3.0"

View File

@@ -313,6 +313,7 @@
"shared-notes-title": "Shared Notes",
"bulk-action-title": "Bulk Action",
"backend-log-title": "Backend Log",
"custom-dictionary-title": "Custom Dictionary",
"user-hidden-title": "User Hidden",
"launch-bar-templates-title": "Launch Bar Templates",
"base-abstract-launcher-title": "Base Abstract Launcher",

View File

@@ -0,0 +1,171 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import becca from "../becca/becca.js";
import { buildNote } from "../test/becca_easy_mocking.js";
import customDictionary from "./custom_dictionary.js";
vi.mock("./log.js", () => ({
default: {
info: vi.fn(),
error: vi.fn()
}
}));
vi.mock("./sql.js", () => ({
default: {
transactional: (cb: Function) => cb(),
execute: () => {},
replace: () => {},
getMap: () => {},
getValue: () => null,
upsert: () => {}
}
}));
function mockSession(localWords: string[] = []) {
return {
listWordsInSpellCheckerDictionary: vi.fn().mockResolvedValue(localWords),
addWordToSpellCheckerDictionary: vi.fn(),
removeWordFromSpellCheckerDictionary: vi.fn()
} as any;
}
describe("custom_dictionary", () => {
beforeEach(() => {
vi.clearAllMocks();
becca.reset();
buildNote({
id: "_customDictionary",
title: "Custom Dictionary",
type: "code",
content: ""
});
});
describe("loadForSession", () => {
it("does nothing when note is empty and no local words", async () => {
const session = mockSession();
await customDictionary.loadForSession(session);
expect(session.addWordToSpellCheckerDictionary).not.toHaveBeenCalled();
expect(session.removeWordFromSpellCheckerDictionary).not.toHaveBeenCalled();
});
it("imports local words when note is empty (one-time import)", async () => {
const session = mockSession(["hello", "world"]);
await customDictionary.loadForSession(session);
// Words are saved to the note; they're already in the local dictionary so no re-add needed.
expect(session.addWordToSpellCheckerDictionary).not.toHaveBeenCalled();
});
it("does not remove or re-add local words after one-time import", async () => {
const session = mockSession(["hello", "world"]);
await customDictionary.loadForSession(session);
// Words were imported from local, so they already exist — no remove, no re-add.
expect(session.removeWordFromSpellCheckerDictionary).not.toHaveBeenCalled();
expect(session.addWordToSpellCheckerDictionary).not.toHaveBeenCalled();
});
it("loads note words into session when no local words exist", async () => {
becca.reset();
buildNote({
id: "_customDictionary",
title: "Custom Dictionary",
type: "code",
content: "apple\nbanana"
});
const session = mockSession();
await customDictionary.loadForSession(session);
expect(session.addWordToSpellCheckerDictionary).toHaveBeenCalledTimes(2);
expect(session.addWordToSpellCheckerDictionary).toHaveBeenCalledWith("apple");
expect(session.addWordToSpellCheckerDictionary).toHaveBeenCalledWith("banana");
});
it("only adds note words not already in local dictionary", async () => {
becca.reset();
buildNote({
id: "_customDictionary",
title: "Custom Dictionary",
type: "code",
content: "apple\nbanana"
});
// "banana" is already local, so only "apple" needs adding.
const session = mockSession(["banana", "cherry"]);
await customDictionary.loadForSession(session);
expect(session.addWordToSpellCheckerDictionary).toHaveBeenCalledTimes(1);
expect(session.addWordToSpellCheckerDictionary).toHaveBeenCalledWith("apple");
});
it("only removes local words not in the note", async () => {
becca.reset();
buildNote({
id: "_customDictionary",
title: "Custom Dictionary",
type: "code",
content: "apple\nbanana"
});
// "cherry" is not in the note, so it should be removed. "banana" should stay.
const session = mockSession(["banana", "cherry"]);
await customDictionary.loadForSession(session);
expect(session.removeWordFromSpellCheckerDictionary).toHaveBeenCalledTimes(1);
expect(session.removeWordFromSpellCheckerDictionary).toHaveBeenCalledWith("cherry");
});
it("handles note with whitespace and blank lines", async () => {
becca.reset();
buildNote({
id: "_customDictionary",
title: "Custom Dictionary",
type: "code",
content: " apple \n\n banana \n\n"
});
const session = mockSession();
await customDictionary.loadForSession(session);
expect(session.addWordToSpellCheckerDictionary).toHaveBeenCalledTimes(2);
expect(session.addWordToSpellCheckerDictionary).toHaveBeenCalledWith("apple");
expect(session.addWordToSpellCheckerDictionary).toHaveBeenCalledWith("banana");
});
it("does not re-add words removed from the note but present locally", async () => {
becca.reset();
buildNote({
id: "_customDictionary",
title: "Custom Dictionary",
type: "code",
content: "apple\nbanana"
});
// "cherry" was previously in the note but user removed it;
// it still lingers in Electron's local dictionary.
const session = mockSession(["apple", "banana", "cherry"]);
await customDictionary.loadForSession(session);
// "apple" and "banana" are already local — no re-add needed.
expect(session.addWordToSpellCheckerDictionary).not.toHaveBeenCalled();
// "cherry" should be removed from local dictionary.
expect(session.removeWordFromSpellCheckerDictionary).toHaveBeenCalledTimes(1);
expect(session.removeWordFromSpellCheckerDictionary).toHaveBeenCalledWith("cherry");
});
it("handles missing dictionary note gracefully", async () => {
becca.reset(); // no note created
const session = mockSession(["hello"]);
await customDictionary.loadForSession(session);
expect(session.addWordToSpellCheckerDictionary).not.toHaveBeenCalled();
});
});
});

View File

@@ -0,0 +1,113 @@
import type { Session } from "electron";
import becca from "../becca/becca.js";
import cls from "./cls.js";
import log from "./log.js";
const DICTIONARY_NOTE_ID = "_customDictionary";
/**
* Reads the custom dictionary words from the hidden note.
*/
function getWords(): Set<string> {
const note = becca.getNote(DICTIONARY_NOTE_ID);
if (!note) {
return new Set();
}
const content = note.getContent();
if (typeof content !== "string" || !content.trim()) {
return new Set();
}
return new Set(
content.split("\n")
.map((w) => w.trim())
.filter((w) => w.length > 0)
);
}
/**
* Saves the given words to the custom dictionary note, one per line.
*/
function saveWords(words: Set<string>) {
cls.init(() => {
const note = becca.getNote(DICTIONARY_NOTE_ID);
if (!note) {
log.error("Custom dictionary note not found.");
return;
}
const sorted = [...words].sort((a, b) => a.localeCompare(b));
note.setContent(sorted.join("\n"));
});
}
/**
* Adds a single word to the custom dictionary note.
*/
function addWord(word: string) {
const words = getWords();
words.add(word);
saveWords(words);
}
/**
* Removes all words from Electron's local spellchecker dictionary
* so they are not re-imported on subsequent startups.
*/
function clearFromLocalDictionary(session: Session, localWords: string[]) {
for (const word of localWords) {
session.removeWordFromSpellCheckerDictionary(word);
}
log.info(`Cleared ${localWords.length} words from local spellchecker dictionary.`);
}
/**
* Loads the custom dictionary into Electron's spellchecker session,
* performing a one-time import of locally stored words on first use.
*/
async function loadForSession(session: Session) {
const note = becca.getNote(DICTIONARY_NOTE_ID);
if (!note) {
log.error("Custom dictionary note not found.");
return;
}
const noteWords = getWords();
const localWords = await session.listWordsInSpellCheckerDictionary();
let merged = noteWords;
// One-time import: if the note is empty but there are local words, import them.
if (noteWords.size === 0 && localWords.length > 0) {
log.info(`Importing ${localWords.length} words from local spellchecker dictionary.`);
merged = new Set(localWords);
saveWords(merged);
}
// Remove local words that are not in the note (e.g. user removed them manually).
const staleWords = localWords.filter((w) => !merged.has(w));
if (staleWords.length > 0) {
clearFromLocalDictionary(session, staleWords);
}
// Add note words that aren't already in the local dictionary.
const localWordsSet = new Set(localWords);
for (const word of merged) {
if (!localWordsSet.has(word)) {
session.addWordToSpellCheckerDictionary(word);
}
}
if (merged.size > 0) {
log.info(`Loaded ${merged.size} custom dictionary words into spellchecker.`);
}
}
export default {
getWords,
saveWords,
addWord,
loadForSession
};

View File

@@ -93,6 +93,12 @@ function buildHiddenSubtreeDefinition(helpSubtree: HiddenSubtreeItem[]): HiddenS
{ type: "label", name: "fullContentWidth" }
]
},
{
id: "_customDictionary",
title: t("hidden-subtree.custom-dictionary-title"),
type: "code",
icon: "bx-book"
},
{
// place for user scripts hidden stuff (scripts should not create notes directly under hidden root)
id: "_userHidden",

View File

@@ -6,6 +6,7 @@ import url from "url";
import app_info from "./app_info.js";
import cls from "./cls.js";
import customDictionary from "./custom_dictionary.js";
import keyboardActionsService from "./keyboard_actions.js";
import log from "./log.js";
import optionService from "./options.js";
@@ -381,6 +382,12 @@ async function configureWebContents(webContents: WebContents, spellcheckEnabled:
.map((code) => code.trim());
webContents.session.setSpellCheckerLanguages(languageCodes);
customDictionary.loadForSession(webContents.session);
ipcMain.on("add-word-to-dictionary", (_event, word: string) => {
webContents.session.addWordToSpellCheckerDictionary(word);
customDictionary.addWord(word);
});
}
}

View File

@@ -21,7 +21,7 @@
"eslint-config-preact": "2.0.0",
"typescript": "6.0.2",
"user-agent-data-types": "0.4.3",
"vite": "8.0.5",
"vite": "8.0.3",
"vitest": "4.1.2"
},
"eslintConfig": {

View File

@@ -77,7 +77,7 @@
"typescript": "6.0.2",
"typescript-eslint": "8.58.0",
"upath": "2.0.1",
"vite": "8.0.5",
"vite": "8.0.3",
"vite-plugin-dts": "4.5.4",
"vitest": "4.1.2"
},

View File

@@ -19,30 +19,30 @@
"@codemirror/search": "6.6.0",
"@codemirror/state": "6.6.0",
"@codemirror/view": "6.41.0",
"@fsegurai/codemirror-theme-abcdef": "6.2.4",
"@fsegurai/codemirror-theme-abyss": "6.2.4",
"@fsegurai/codemirror-theme-android-studio": "6.2.4",
"@fsegurai/codemirror-theme-andromeda": "6.2.4",
"@fsegurai/codemirror-theme-basic-dark": "6.2.4",
"@fsegurai/codemirror-theme-basic-light": "6.2.4",
"@fsegurai/codemirror-theme-cobalt2": "6.0.4",
"@fsegurai/codemirror-theme-forest": "6.2.4",
"@fsegurai/codemirror-theme-github-dark": "6.2.4",
"@fsegurai/codemirror-theme-github-light": "6.2.4",
"@fsegurai/codemirror-theme-gruvbox-dark": "6.2.4",
"@fsegurai/codemirror-theme-gruvbox-light": "6.2.4",
"@fsegurai/codemirror-theme-material-dark": "6.2.4",
"@fsegurai/codemirror-theme-material-light": "6.2.4",
"@fsegurai/codemirror-theme-monokai": "6.2.4",
"@fsegurai/codemirror-theme-nord": "6.2.4",
"@fsegurai/codemirror-theme-palenight": "6.2.4",
"@fsegurai/codemirror-theme-solarized-dark": "6.2.4",
"@fsegurai/codemirror-theme-solarized-light": "6.2.4",
"@fsegurai/codemirror-theme-tokyo-night-day": "6.2.4",
"@fsegurai/codemirror-theme-tokyo-night-storm": "6.2.4",
"@fsegurai/codemirror-theme-volcano": "6.2.4",
"@fsegurai/codemirror-theme-vscode-dark": "6.2.5",
"@fsegurai/codemirror-theme-vscode-light": "6.2.5",
"@fsegurai/codemirror-theme-abcdef": "6.2.3",
"@fsegurai/codemirror-theme-abyss": "6.2.3",
"@fsegurai/codemirror-theme-android-studio": "6.2.3",
"@fsegurai/codemirror-theme-andromeda": "6.2.3",
"@fsegurai/codemirror-theme-basic-dark": "6.2.3",
"@fsegurai/codemirror-theme-basic-light": "6.2.3",
"@fsegurai/codemirror-theme-cobalt2": "6.0.3",
"@fsegurai/codemirror-theme-forest": "6.2.3",
"@fsegurai/codemirror-theme-github-dark": "6.2.3",
"@fsegurai/codemirror-theme-github-light": "6.2.3",
"@fsegurai/codemirror-theme-gruvbox-dark": "6.2.3",
"@fsegurai/codemirror-theme-gruvbox-light": "6.2.3",
"@fsegurai/codemirror-theme-material-dark": "6.2.3",
"@fsegurai/codemirror-theme-material-light": "6.2.3",
"@fsegurai/codemirror-theme-monokai": "6.2.3",
"@fsegurai/codemirror-theme-nord": "6.2.3",
"@fsegurai/codemirror-theme-palenight": "6.2.3",
"@fsegurai/codemirror-theme-solarized-dark": "6.2.3",
"@fsegurai/codemirror-theme-solarized-light": "6.2.3",
"@fsegurai/codemirror-theme-tokyo-night-day": "6.2.3",
"@fsegurai/codemirror-theme-tokyo-night-storm": "6.2.3",
"@fsegurai/codemirror-theme-volcano": "6.2.3",
"@fsegurai/codemirror-theme-vscode-dark": "6.2.4",
"@fsegurai/codemirror-theme-vscode-light": "6.2.4",
"@replit/codemirror-indentation-markers": "6.5.3",
"@replit/codemirror-lang-nix": "6.0.1",
"@replit/codemirror-vim": "6.3.0",

528
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff