feat: v4.6.0 — AI assistant, collaboration, knowledge base, and 15 more features

- AI Assistant plugin: multi-provider chat (OpenAI/Anthropic/Ollama/LM Studio),
  summarize/improve/translate commands, proofread via ai:analyze; calls
  proxied through main so API keys stay out of the renderer
- Collaboration plugin: anchor-based comments in .comments/ sidecars with
  drift detection and F8 navigation
- Local knowledge base: [[wiki-links]] with click-to-create + Backlinks panel
- Crash recovery: debounced session snapshots with restore prompt on launch
- Version history: pre-save snapshots, History panel with restore/diff/delete
- Real PDF encryption: swap pdf-lib for @cantoo/pdf-lib (probe-driven UI)
- XLSX export (native workbooks via JSZip), ODT headers/footers + page size
- Offline KaTeX (bundled CSS+fonts), local-first PlantUML rendering
- Editor: vim mode toggle, snippet Tab-expansion, zen word-goal setter,
  writing heatmap, writing-studio panels wired with rail icons
- Quick Note global scratchpad (Ctrl+Alt+Q), markdownconverter:// deep links,
  REPL first-run confirmation
- Fix: Ctrl+Shift+P collision, pandoc converter availability check, CLI
  dangling --css/--reference-doc flags, dead converter button

8 new test suites; 613 tests green; lint clean
This commit is contained in:
2026-09-05 20:48:39 +05:30
parent b83ba91731
commit c4dcbd8caf
105 changed files with 5695 additions and 195 deletions
+84
View File
@@ -0,0 +1,84 @@
/**
* AI Assistant prompt builder + proofread parser tests (pure module, jsdom
* not required but harmless under the default environment).
*/
const {
buildTaskPrompt,
buildProofreadPrompt,
parseProofreadIssues,
} = require('../src/plugins/built-in/ai-assistant/prompts');
describe('AI Assistant prompts', () => {
describe('buildTaskPrompt', () => {
it('builds a summarize prompt embedding the text', () => {
const { system, user } = buildTaskPrompt('summarize', 'the text here');
expect(system).toMatch(/summar/i);
expect(user).toContain('the text here');
});
it('passes the target language through for translate', () => {
const { user } = buildTaskPrompt('translate', 'bonjour', 'English');
expect(user).toContain('English');
expect(user).toContain('bonjour');
});
it('throws on unknown actions', () => {
expect(() => buildTaskPrompt('nope', 'x')).toThrow(/Unknown AI task/);
});
it('tolerates missing text', () => {
expect(buildTaskPrompt('explain', undefined).user).toBe('Explain the following:\n\n');
});
});
describe('buildProofreadPrompt', () => {
it('asks for a strict JSON array', () => {
const { system, user } = buildProofreadPrompt('Some text.');
expect(system).toMatch(/JSON array/);
expect(user).toContain('Some text.');
});
});
describe('parseProofreadIssues', () => {
it('parses a clean JSON array', () => {
const issues = parseProofreadIssues(
'[{"type":"Spelling","message":"teh","suggestion":"the"}]'
);
expect(issues).toEqual([{ type: 'spelling', message: 'teh', suggestion: 'the' }]);
});
it('strips code fences and surrounding prose', () => {
const issues = parseProofreadIssues(
'Here you go:\n```json\n[{"type":"grammar","message":"run on","suggestion":"split"}]\n```\nDone!'
);
expect(issues).toHaveLength(1);
expect(issues[0].suggestion).toBe('split');
});
it('tolerates trailing commas', () => {
const issues = parseProofreadIssues(
'[{"type":"style","message":"wordy","suggestion":"cut",},]'
);
expect(issues).toHaveLength(1);
});
it('returns [] for empty/garbage input instead of throwing', () => {
expect(parseProofreadIssues('')).toEqual([]);
expect(parseProofreadIssues('no json at all')).toEqual([]);
expect(parseProofreadIssues('{"not":"an array"}')).toEqual([]);
expect(parseProofreadIssues(null)).toEqual([]);
});
it('drops malformed entries and caps the list length', () => {
const many = Array.from({ length: 300 }, (_, i) => ({
type: 'grammar',
message: `m${i}`,
suggestion: 's',
}));
expect(parseProofreadIssues(JSON.stringify(many))).toHaveLength(100);
const mixed = parseProofreadIssues('[{"message":"ok","suggestion":"x"},null,42,{"foo":1}]');
// null/number entries dropped; {foo:1} dropped for lacking message+ suggestion
expect(mixed).toHaveLength(1);
});
});
});