From d1929ff432216ee3cb978d4de9990051c6cc01ac Mon Sep 17 00:00:00 2001 From: Dalton Sterritt Date: Wed, 27 May 2026 15:57:07 -0600 Subject: [PATCH] Fix vault assistant approve-all indexing and add export/duplicate features (#326) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Pull Request: Vault Agent Bugfixes, Export, Duplication & Performance ## Overview Fixes 7 issues in the vault assistant and lorebook editor, adds export and duplication functionality, and improves input responsiveness across vault components. --- ## Changes ### Bug Fixes #### 1. ApproveAll racing (Issue 1) `VaultLorebookEditorContent.handleApproveAll()` now delegates to `vaultEditor.approveAll(service)` via an `onApproveAllAsync` callback, processing changes sequentially instead of racing individual approvals. Added dedup in `previewLorebook` for create overlays. Resyncs local `entries` after batch approval. **Files:** `VaultLorebookEditorContent.svelte`, `VaultEntityEditPanel.svelte`, `InteractiveVaultAssistant.svelte`, `vaultEditorStore.svelte.ts` #### 2. Simultaneous deletes corrupt indices (Issue 2) Re-index entries after each approval to prevent index corruption when multiple deletes are processed in batch. **Files:** `vaultEditorStore.svelte.ts` #### 3. Escape closes lorebook instead of stopping agent (Issue 3) Added `onEscapeKeydown` prop using bits-ui's `EscapeLayer` so Escape stops generation instead of closing the editor. **Files:** `VaultEntityEditPanel.svelte` #### 4. Assistant closes immediately on re-open (Issue 6) A `mounted` guard flag in `onOpenChange` handlers prevents spurious close events during the bits-ui Dialog mount cycle. **Files:** `InteractiveVaultAssistant.svelte`, `VaultEntityEditPanel.svelte` #### 5. Blank-entry corruption from empty strings (Issue 1 v2) AI was sending empty strings (`""`) for `description` in `update_entry` calls, overwriting existing descriptions. Applied consistent empty-string filtering across all 4 merge paths: tool handler (`cleanUpdates`), `applyLorebookEntryChange`, `previewLorebook` overlay, and optimistic `handleApproveEntry`. Also added `VaultLorebookEntry` import to `vaultEditorStore.svelte.ts` for type safety and improved tool descriptions to clarify which fields allow empty values. **Files:** `lorebook.ts`, `InteractiveVaultService.ts`, `vaultEditorStore.svelte.ts`, `VaultLorebookEditorContent.svelte` #### 6. Scroll position leaks between lorebook entries When navigating between lorebook entries, the textarea's `scrollTop` from the previous entry carried over. If the new entry was shorter, its content was partially hidden. Fixed by resetting `scrollTop` to 0 in a `$effect` keyed on the `data` object reference. **Files:** `VaultLorebookEntryFields.svelte` --- ### New Features #### 7. Export vault entities (Issue 4) New export modal (`VaultExportModal.svelte`) that exports vault characters, lorebooks, and scenarios using the existing `LorebookImportExport` service. Export functions for vault entities live in a new file (`src/lib/services/lorebookImportExport/export/vault.ts`), re-exported through the service's public API. **Bug fix — save dialog not opening for SillyTavern/Text on large lorebooks:** Two issues were found and fixed: - **Combined filter group rejected by Tauri dialog**: The `saveFile` filter used a single combined group `extensions: ['json', 'txt']` instead of separate filter groups per extension. This caused the save dialog to silently fail for SillyTavern and Text formats on certain lorebooks. Fixed by splitting into `{ name: 'JSON', extensions: ['json'] }` and `{ name: 'Text', extensions: ['txt'] }`, matching the existing lorebook export pattern. - **Null aliases/keywords causing TypeError in format converters**: `VaultLorebookEntry.aliases` and `.keywords` are typed `string[]` but can be `null` at runtime (database NULL entries). The Aventura JSON export survived because `JSON.stringify` serializes `null` safely, but SillyTavern's spread operator (`...entry.aliases`) and Text's `.length` call both threw `TypeError` on `null`, silently aborting before the save dialog opened. Fixed with `?? []` guards in `vaultEntryToEntryLike`, `entryToSillyTavern`, and `exportToText`. **Files:** `VaultExportModal.svelte` (new), `vault.ts` (new), `convert.ts`, `formats.ts`, `public-api.ts`, `VaultPanel.svelte` #### 8. Duplicate vault entities (Issue 5) Added `duplicate()` method to all 3 vault stores (`characterVault`, `lorebookVault`, `scenarioVault`) that deep-clones an entity with a "(Copy)" suffix. A Copy button appears on hover on `VaultCard`. **Files:** `characterVault.svelte.ts`, `lorebookVault.svelte.ts`, `scenarioVault.svelte.ts`, `VaultCard.svelte` --- ### Performance Improvements #### 9. Debounced vault search + `$derived.by` filtering The vault panel search input was bound directly to `searchQuery`, triggering full array filtering (iterate all items, filter by text/tags/favorites) on every keystroke via template `{@const}` — which also recomputed on every parent render. Split into `searchInput` (raw) → debounced `searchQuery` (300ms). Replaced `{@const filteredItems = getFilteredItems(...)}` with `filteredByTab = $derived.by(...)` — cached per tab. **Files:** `VaultPanel.svelte` #### 10. VaultAssistantInput extracted from InteractiveVaultAssistant The 1286-line assistant component re-traversed its full template on every keystroke. Extracted the chat textarea + send button + keyboard hint into a lightweight `VaultAssistantInput.svelte` child component with local `inputValue` state. Keystrokes now only re-render ~20 lines. **Files:** `VaultAssistantInput.svelte` (new), `InteractiveVaultAssistant.svelte` #### 11. `$derived` function → `$derived.by` in RuntimeVariableManager `grouped` and `entityTypeCounts` used `$derived(() => {...})` storing a **function** whose body re-executed on every `grouped()` call in the template. Changed to `$derived.by(() => {...})` so the computed value is cached. **Files:** `RuntimeVariableManager.svelte` #### 12. Pre-sliced `$derived` arrays in UniversalVaultCard `.slice(0, N)` calls in template expressions created new arrays on every render. Added `$derived` variables (`visibleTraits`, `visibleEntryCounts`, `visibleScenarioTags`) so slicing happens only when dependencies change. **Files:** `UniversalVaultCard.svelte` --- ## Verification - `npm run check` — 0 errors, 0 warnings - `npm run lint` — 0 errors, 181 pre-existing boundary-import warnings (unchanged) ## Summary by CodeRabbit * **New Features** * Export vault entities (lorebooks, characters, scenarios) with format selection and save dialog. * Export button on vault cards and an export modal in the panel. * Duplicate vault items from list cards. * Batch “Approve All” for lorebook pending entries. * New multiline assistant input with send, append, mount-aware close, and Escape-to-abort. * **Bug Fixes** * Prevent accidental overwrites from empty-string fields during lorebook updates. * Debounced search for snappier filtering. * Preserve entry textarea scroll position and more robust handling of optional export fields. * Improved pending-change application and reindexing for approvals. [![Review Change Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/AventurasTeam/Aventuras/pull/326?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack) --- package-lock.json | 64 ++-- src/lib/components/layout/Sidebar.svelte | 8 +- src/lib/components/story/ActionInput.svelte | 7 +- .../vault/InteractiveVaultAssistant.svelte | 123 ++++--- .../vault/UniversalVaultCard.svelte | 17 +- .../vault/VaultAssistantInput.svelte | 81 +++++ .../vault/VaultEntityEditPanel.svelte | 11 +- .../components/vault/VaultExportModal.svelte | 146 ++++++++ .../vault/VaultLorebookEditorContent.svelte | 52 ++- .../vault/VaultLorebookEntryFields.svelte | 7 + src/lib/components/vault/VaultPanel.svelte | 71 +++- .../prompts/RuntimeVariableManager.svelte | 16 +- .../components/vault/shared/VaultCard.svelte | 36 +- src/lib/services/ai/sdk/tools/lorebook.ts | 13 +- .../ai/vault/InteractiveVaultService.ts | 41 ++- .../lorebookImportExport/export/convert.ts | 2 +- .../lorebookImportExport/export/formats.ts | 7 +- .../lorebookImportExport/export/vault.ts | 173 ++++++++++ .../lorebookImportExport/export/write.ts | 16 +- .../lorebookImportExport/public-api.ts | 3 + src/lib/stores/characterVault.svelte.ts | 21 ++ src/lib/stores/lorebookVault.svelte.ts | 20 ++ src/lib/stores/scenarioVault.svelte.ts | 23 ++ src/lib/stores/vaultEditorStore.svelte.ts | 314 +++++++++++++++++- 24 files changed, 1102 insertions(+), 170 deletions(-) create mode 100644 src/lib/components/vault/VaultAssistantInput.svelte create mode 100644 src/lib/components/vault/VaultExportModal.svelte create mode 100644 src/lib/services/lorebookImportExport/export/vault.ts diff --git a/package-lock.json b/package-lock.json index 7f33f222..4d8daa4f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1686,9 +1686,9 @@ "license": "MIT" }, "node_modules/@sveltejs/acorn-typescript": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.9.tgz", - "integrity": "sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA==", + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.10.tgz", + "integrity": "sha512-4WfKk68eTih+MiJD4fSbxN7E8kVBmTMPWHUPYjvl2N0rMs53YLTT8/YjKU5Dtnz5LqDjl7LEw4U7lXR2W3J5WA==", "license": "MIT", "peerDependencies": { "acorn": "^8.9.0" @@ -1705,18 +1705,18 @@ } }, "node_modules/@sveltejs/kit": { - "version": "2.58.0", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.58.0.tgz", - "integrity": "sha512-kT9GCN8yJTkCK1W+Gi/bvGooWAM7y7WXP+yd+rf6QOIjyoK1ERPrMwSufXJUNu2pMWIqruhFvmz+LbOqsEmKmA==", + "version": "2.61.0", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.61.0.tgz", + "integrity": "sha512-beYjgUux5ITbZeL0vn6gipZlsQiXF1/08C/3F+vlbDvthb/CTgYpZsYPdRIi9RxgTwRSkKIvnxyl+ViZlX4q5A==", "dev": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.0.0", - "@sveltejs/acorn-typescript": "^1.0.5", + "@sveltejs/acorn-typescript": "^1.0.9", "@types/cookie": "^0.6.0", - "acorn": "^8.14.1", + "acorn": "^8.16.0", "cookie": "^0.6.0", - "devalue": "^5.6.4", + "devalue": "^5.8.1", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", @@ -2586,6 +2586,7 @@ "version": "8.58.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.1.tgz", "integrity": "sha512-io/dV5Aw5ezwzfPBBWLoT+5QfVtP8O7q4Kftjn5azJ88bYyp/ZMCsyW1lpKK46EXJcaYMZ1JtYj+s/7TdzmQMw==", + "devOptional": true, "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2634,9 +2635,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, "license": "MIT", "dependencies": { @@ -3479,9 +3480,9 @@ } }, "node_modules/devalue": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.7.1.tgz", - "integrity": "sha512-MUbZ586EgQqdRnC4yDrlod3BEdyvE4TapGYHMW2CiaW+KkkFmWEFqBUaLltEZCGi0iFXCEjRF0OjF0DV2QHjOA==", + "version": "5.8.1", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.8.1.tgz", + "integrity": "sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==", "license": "MIT" }, "node_modules/electron-to-chromium": { @@ -3939,13 +3940,20 @@ } }, "node_modules/esrap": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.4.tgz", - "integrity": "sha512-suICpxAmZ9A8bzJjEl/+rLJiDKC0X4gYWUxT6URAWBLvlXmtbZd5ySMu/N2ZGEtMCAmflUDPSehrP9BQcsGcSg==", + "version": "2.2.9", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.9.tgz", + "integrity": "sha512-4KijP+NxCWthMCUC3qHbE6n4vCjqgJS1uAYKhuT/GWfFTf1Qyive2TgOjep+gzbSzRfnNyaN/UU9YmdOt8Eg0A==", "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15", + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "peerDependencies": { "@typescript-eslint/types": "^8.2.0" + }, + "peerDependenciesMeta": { + "@typescript-eslint/types": { + "optional": true + } } }, "node_modules/esrecurse": { @@ -5857,23 +5865,23 @@ } }, "node_modules/svelte": { - "version": "5.55.2", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.55.2.tgz", - "integrity": "sha512-z41M/hi0ZPTzrwVKLvB/R1/Oo08gL1uIib8HZ+FncqxxtY9MLb01emg2fqk+WLZ/lNrrtNDFh7BZLDxAHvMgLw==", + "version": "5.55.9", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.55.9.tgz", + "integrity": "sha512-fTjjT8cHLDwigcu2j3pv7Jq04LklXevPB8uBgyHNiTXv+RMNvVnrjS4UEYrLMkhuq1vpCodHjiW+z/95SDs/fg==", "license": "MIT", "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", - "@sveltejs/acorn-typescript": "^1.0.5", + "@sveltejs/acorn-typescript": "^1.0.10", "@types/estree": "^1.0.5", "@types/trusted-types": "^2.0.7", "acorn": "^8.12.1", "aria-query": "5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", - "devalue": "^5.6.4", + "devalue": "^5.8.1", "esm-env": "^1.2.1", - "esrap": "^2.2.4", + "esrap": "^2.2.9", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", @@ -6491,9 +6499,9 @@ "license": "MIT" }, "node_modules/ws": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", - "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", "license": "MIT", "engines": { "node": ">=10.0.0" diff --git a/src/lib/components/layout/Sidebar.svelte b/src/lib/components/layout/Sidebar.svelte index 8b8cf6d3..a41d0653 100644 --- a/src/lib/components/layout/Sidebar.svelte +++ b/src/lib/components/layout/Sidebar.svelte @@ -54,6 +54,12 @@ } let innerWidth = $state(0) + let scrollContainer = $state(null) + + $effect(() => { + void ui.sidebarTab + if (scrollContainer) scrollContainer.scrollTop = 0 + }) @@ -86,7 +92,7 @@ -
+
diff --git a/src/lib/components/story/ActionInput.svelte b/src/lib/components/story/ActionInput.svelte index b7017bbd..26e4b304 100644 --- a/src/lib/components/story/ActionInput.svelte +++ b/src/lib/components/story/ActionInput.svelte @@ -95,6 +95,7 @@ let isRawActionChoice = $state(false) let stopRequested = false let activeAbortController: AbortController | null = null + let textareaRef: HTMLTextAreaElement | null = $state(null) let lastImageGenContext = $state(null) let isManualImageGenRunning = $state(false) @@ -786,7 +787,6 @@ ui.setGenerating(false) ui.setGenerationStatus('') activeAbortController = null - stopRequested = false // Android: always stop the foreground service when generation ends if (useBackgroundService) { @@ -969,6 +969,7 @@ isRawActionChoice = false inputValue = '' + if (textareaRef) textareaRef.scrollTop = 0 const embeddedImages = await database.getEmbeddedImagesForStory(story.currentStory.id) ui.createRetryBackup( @@ -1005,7 +1006,7 @@ } async function handleStopGeneration() { - if (!ui.isGenerating || ui.isRetryingLastMessage) return + if (stopRequested || ui.isRetryingLastMessage) return stopRequested = true activeAbortController?.abort() @@ -1236,6 +1237,7 @@