<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Added automatic textarea sizing to fit entered content, with height
limits and no manual resizing.
* Added discard confirmation when exiting setup or import wizards after
editing begins.
* Added configurable dismissal behavior for responsive dialogs.
* **Bug Fixes**
* Improved wizard safeguards to prevent accidental loss of in-progress
answers.
* Standardized textarea behavior across editing and configuration
screens.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Follow-up to #421, built on top of #433.
The first three commits are **@Pento95's from #433**, carried over
unchanged — the fork-point action and its mechanics are his work. The
last commit withdraws the landing experiment and layers the changes
discussed in the review on top. #433 can be closed in favour of this.
## The Labs setting goes away
Branch-switch landing shipped behind a toggle with two target modes.
None of the three outcomes should have been a setting:
- With the toggle **off** — the default — switching from a long branch
to a short one still left the view showing nothing but "N earlier
entries hidden". The fix reached only readers who found the toggle.
- **"Last entry"** is what readers want on essentially every switch.
- **"Branching checkpoint"** as a mode sent the reader back to the fork
on *every* switch, including returning to the branch they were actively
writing in. It was a one-time jump modelled as a mode — which is what
#433 makes it.
`branchSwitchLanding` and `branchSwitchLandingTarget` are removed from
the Labs UI, the `ExperimentalFeatures` contract, the defaults and every
logic path. **BREAKING** for anyone who had selected "Branching
checkpoint".
## Ordinary switching gets its own behaviour
This is where the blank view is actually fixed, and it is not the old
"Last entry" mode promoted to a default:
- The entry window is **recomputed for the switched-to branch** rather
than inherited from the branch being left. Inheriting it is what clamps
it to an empty slice when the new branch is shorter.
- The view rests at the **container bottom** — past the last entry and
its action choices, where writing continues — rather than pinning the
last entry's top edge to the viewport top, which left the end of a long
narration below the fold.
## Changes to the fork-point action from #433
The transport is unchanged: the request goes on the ui store via
`requestEntryScroll` / atomic `consumeEntryScroll`, because `StoryView`
is destroyed rather than hidden while another panel is up, so an event
emitted at it would reach nobody. Same for `landOnEntry` returning
`false` untouched when the entry isn't in the active branch's view, the
remount fix, and `closeSidebarOnMobile`.
What changed:
- **The control moves from per-branch rows to one button in the panel
header**, acting on the branch being read. Per-branch buttons are
disabled on most rows — `story.entries` is the active branch's view, so
any branch outside that lineage has its fork entry nowhere in it — and
they spend a row of space on mobile to say so. The active branch always
contains its own fork entry, so the header button is enabled wherever a
branching point exists, i.e. everywhere except main.
- **Landing on an entry lifts the scroll** so the preceding entry stays
partly visible, capped at a third of the viewport so a long narration
cannot displace the fork entry. Without it the fork reads as the start
of the story rather than a departure from something.
- **Where the platform cannot hover**, so a tooltip can never explain
the button, the jump is confirmed with a toast — keyed to
`matchMedia('(hover: none)')` via a new `supportsHover()` helper rather
than viewport width, so it fires on a tablet at any size and never on a
narrow desktop window.
## Notes
- `@lucide/svelte` moves to 1.31.0 for the icon, in both `package.json`
and the lockfile. An earlier revision of this PR bumped only the
lockfile, reasoning that `^1.28.0` already permitted 1.31.0 — but
permitting a working version is not requiring one, and 1.28.0 does not
ship `LayerArrowUp`, so any resolve that ignored the lockfile could have
picked it and failed to build.
- No database migrations, no Rust/Tauri changes. Existing installs keep
two orphaned keys inside the stored `experimental_features` JSON; they
are ignored and no cleanup is proposed.
- `npm run check` clean, 967 tests pass, ESLint clean.
- Known and **not** from this PR: switching to a much longer branch
shows a brief scroll readjustment as the view settles. Reproduces on
#433's build and on unrelated older builds, so it predates both.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Added fork-point navigation to branch panels.
* Branch switches now open at the bottom of the active branch.
* Selecting a fork entry scrolls directly to it with relevant context
visible.
* Mobile sidebars close automatically during branch and entry
navigation.
* Added hover-aware notifications for supported interactions.
* **Changes**
* Removed the experimental Branch Switch Landing settings and related
controls.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Pento <datamanager.software@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Pento <pentolo@outlook.com>
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* AI-generated content, translations, retrieval, and image prompts now
use the selected story or pack context.
* Added consistent style-prompt resolution with fallback support for
custom and default packs.
* Wizard generation and character-card tools now preserve the selected
pack context.
* **Bug Fixes**
* Prevented story switches during generation from redirecting
in-progress AI tasks.
* Improved consistency and accuracy of story-specific prompts across
workflows.
* Updated chapter summaries to follow configured prompt verbosity
guidance.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
- On a narrow screen the header collapses into a disclosure menu. Inside a
story that earns its place — Library, Import/Export, Active Context,
Settings — but every one of those items is story-scoped, so **in the
library the menu opened onto a single item**: a tap spent to reveal one
choice. There the Settings button is now rendered directly instead.
- **The warning badge.** The new button is the narrow-screen twin of the
desktop one, badge included. Behind the menu,
`hasGenerationConfigIssues` only appeared once the menu was opened — the
wrong place for the one control that reports a misconfiguration.
- **`goToLibrary` now closes the menu explicitly.** `showMobileMenu` is
`bind:open` on a dropdown that can now unmount, and `closeStory()`
unmounts it in the same tick — so close-on-select may not get to write
through the binding, leaving the state `true` and the menu open of its
own accord on returning to a story.
- **Bug Fixes**
- Improved recovery when AI-generated classifications contain incomplete
or invalid data, preserving valid changes wherever possible.
- Prevented usable scene and entity updates from being lost when only
part of a response is malformed.
- Added clearer handling for responses that cannot be recovered.
- **User Experience**
- Added a warning toast when some world changes may not have been
applied, with an option to regenerate the response.
* **Bug Fixes**
* Imported story entries now retain reasoning, suggested actions, and
world-state updates.
* References in imported world-state updates and checkpoint snapshots
are remapped correctly, including nested entities and inventory data.
* Missing optional import fields are handled consistently without
causing failures.
* Reopened stories restore available action choices and suggestions from
the latest narration.
* Invalid, empty, or malformed saved choices and suggestions are safely
discarded.
`npm run release` cannot push for anyone without an HTTPS credential
helper:
```
Username for 'https://github.com': …
remote: Password authentication is not supported for Git operations.
```
`scripts/release.js` pushed to a hardcoded
`https://github.com/AventurasTeam/Aventuras.git`. GitHub no longer
accepts a password there, so the push fails outright — while the
contributor's working SSH remote sits unused two lines away. (Present
since `5fd37fcb`, February; it just needed someone to cut a release from
a clone without a helper.)
### The fix
The URL was not arbitrary — releasing to `origin` would happily tag a
**fork**, and hardcoding guaranteed the canonical repository. This keeps
that guarantee and drops the transport:
`pickReleaseRemote` scans `git remote -v` for a remote whose **push URL
points at the release repository** and hands git that remote's name, so
the push uses whatever transport and credentials that remote already
has. When nothing matches it falls back to the HTTPS URL, i.e. today's
behaviour.
Two details that matter:
- **Matching is on the repository path, never the remote name**, so a
fork cannot be selected however it is called. A test pins this.
- **`unkarelian/Aventuras` is accepted alongside
`AventurasTeam/Aventuras`.** GitHub redirects the pre-rename path, so a
remote predating the move still points here — and matching on the
repository *name* alone would sweep up every fork, which is exactly what
the owner check is for.
### Notes
- Extracted into `scripts/remote.js` rather than left inline:
`release.js` cuts a release on import, so it cannot be imported by a
test. 18 cases in `scripts/remote.test.js`, following the `version.js` /
`version.test.js` split from #416.
- The console now prints `upstream (git@github.com:...)` instead of a
bare remote name, so the repository about to be released to stays
visible.
- The failure hint for an unreachable remote now names the HTTPS
fallback as a likely cause.
Both selection services — **Entry Retrieval** (authored lorebook
`Entry[]`) and **World State
Injection** (live tracked entities) — share the same three-tier shape.
This reworks the
boundary between "include the leftover whole" and "ask a model which of
it matters", and adds
a diagnostic for what actually reached the prompt.
## Include-All Budget
The boundary is now a **word budget on the candidate text**, in both
services, replacing World
State's record count (`llmThreshold`).
30 records and 500 words describe the same line only because a live
world-state record averages
~16 words. A lorebook entry averages ~69 — which is exactly why the two
budgets differ
(500 / 1000) where one shared record count could not express the
difference.
- The stored `llmThreshold` is **dropped, not converted**: someone who
raised it to 100 was not
asking for 1600 words of anything, and the new default is calibrated to
do what the old
default did. Dropping the key matters beyond tidiness — the store merges
disk over defaults,
so an unread key is written back into every future save forever.
- Advanced Settings relabels the whole group in those terms:
*Include-All Budget*, *Ask the
Model Above the Budget*, *Max LLM-Selected …*, and stops claiming Tier
3's cap means "there is
no Tier 3" when selection is off — a leftover under the budget still
goes in whole.
## Tier 3 selection
- **The cache returned a different entry set than the one it cached.**
It is now keyed on the
caller, the candidate pool *in order*, and the player's action, and
bounded to
`TIER3_SELECTION_CACHE_POSITIONS` story positions. Order is part of the
key because the answer
lives in index space, and `WorldStateInjector` builds its candidates
from world state the
classifier rewrites every turn. Cleared when the story it was about is
no longer loaded, and
it holds one entry per caller rather than one per question — see the
second commit.
- **The model is asked for indices, not ids.** Ids are never rendered
into the prompt, so it had
never seen one. `selectedIds` → `selectedIndices` across the contract,
and the prompt states
the format. `resolveTier3Selection` tolerates `"1."`, `"#1"` and `"[1]"`
coming back.
- **Wholesale-included entries no longer count as relevance
activations.** They were not chosen
for relevance, and treating them as if they were kept them sticky
afterwards.
- A Tier 2 **second pass** (`tier2SecondPass.ts`) matches what is left
against the *names* of
what the first pass found, so an entry nobody named directly comes in
when something that
names it did. Names and aliases only — descriptions cross-reference each
other by
construction, so seeding with them pulls in half a dense lorebook in one
step. Seeds shorter
than 4 characters, and seeds contained in a longer seed, are dropped.
- `recentContent(..., withRoles)` labels the scene `[Player Action]` /
`[Narrator]` for the
selection prompt, instead of one undifferentiated block. `retry` is
labelled `[Narrator]`: a
model told it was a retry treats the re-roll as an event.
## Retrieval snapshots
`retrievalSnapshot.ts` records what each tier actually contributed, with
token counts, and the
narration entry carries it in `metadata.retrievalSnapshot`. The
in-memory copy dies with the
session, so without it the debug panel was empty on every fresh start
and after every story
switch — exactly when you most want to know what the narrator is being
told. Diagnostic only;
nothing reads it back into a prompt.
- The debug panel shows the injected **World State** block alongside the
lorebook one, both
collapsible.
- The toolbar badge counts the whole active context rather than lorebook
entries alone, and the
button is no longer gated on the lorebook having entries — every story
has live world state.
Reachable from the menu on narrow screens.
- **Stickiness is tracked in story positions, not turns.** A turn
appends both an action and a
narration, so a duration of N covers roughly N/2 turns; the panel
converts for display.
## Fixes along the way
- `ActionInput` passed the **raw player text** to `generateResponse`
while the entry the narrator
reads holds `promptContent`. With translation on, retrieval and
classification worked from a
different wording than the narration — and the retry path already passed
`promptContent`, so
the two disagreed with each other.
- **Retry re-ran retrieval from scratch** — an entire agentic loop, now
that agentic is the
default mode — because the cached result was cleared before the call
site read it.
- `create_entry` / `update_entry` lose `injectionMode` in the **lore
management** toolset only.
It runs unattended, and `always` is a budget decision past every
retrieval threshold, made by
an agent that cannot see the budget. The vault assistant keeps it, where
the user reads the
change before it lands. Removed from the schema rather than ignored
afterwards: the schema is
what the model is shown, so a parameter left in and discarded is a
parameter it spends tokens
choosing.
- The lore management prompt asks explicitly for duplicate consolidation
via `update_entry` +
`delete_entry`, rather than "merge" with no mechanism named.
- `buildWorldStateContext` / `getRelevantLorebookEntries` take an
options object; the positional
`signal` / `activationTracker` pair had already been passed in the wrong
order once.
## Second commit — review fixes
- The selection cache held **one entry per distinct question, retained
until the story was
switched**. Only the most recent answer is ever reusable (the window is
two positions wide), so
every earlier key was growth with no hit rate behind it — and each key
is every candidate id
concatenated, several KB on a large lorebook, held for the whole session
on a WebView heap that
is a hard cap on Android. Now one entry per caller, with the question
compared against the
stored key.
- Comments still describing the World State threshold as a record count
named `llmThreshold`.
- `splitTier1` and `splitTier3` had their doc comments stacked above
`splitTier2`.
### Third commit
`splitTier3` treated a missing `llmSelected` as *selected*, pinned by a
test describing "a
snapshot written before the flag existed" — a state that never existed,
since the snapshot and
the flag ship in the same commit. All four producers set the flag, so
this only decides how a
row from some later source reads; it now reads as wholesale. The two
errors are not symmetric:
"LLM Selected" asserts a call was paid for, "Included Whole" only that
the leftover fitted.
Settings -> Interface -> Updates existed but could not finish the job,
and the "Check on Startup" toggle reported an available update with a
`console.log`.
### Android had no update path at all
`tauri-plugin-updater` declares Android support level `none`, and its
`updater_os()` has branches for linux/macos/windows only. On Android
`target_os` is `"android"`, so `check()` returned `UnsupportedOs`
**before a request was ever sent** — the button read "Failed to check
for updates" on every phone.
That path now reads the GitHub Releases API directly, compares the tag
against `getVersion()`, and opens the `.apk` asset in the browser. There
is no in-app install to add: an APK is installed by the system package
installer, not by the app it replaces.
The comparison needs `src/lib/utils/version.ts`, since `'0.10.0' >
'0.9.0'` is false lexically. It is a plain module with 22 tests, and
returns `false` for an unparseable version rather than guessing —
"unknown means newer" would offer an update on every startup.
### Two desktop cases hand off to the browser instead of installing
- **Unpackaged builds**, as a safety guard. On Linux the plugin's
`extract_path` *is* the running executable, so in `tauri dev` "Download
and install" moved the dev binary into a `TempDir`, wrote the release
AppImage over it, then dropped the `TempDir` — deleting the backup — and
reported success. `getBundleType()` returns `null` for a build the
bundler never touched, which is exactly that case. (Does not fire on
macOS, where `bundle_type()` falls back to `App`; noted in the README.)
- **`.deb` installs**, as a decision. `install_deb` shells out to `dpkg
-i` through `pkexec`, falling back to zenity/kdialog and finally to a
terminal `sudo` that a windowed app has no terminal for. Too many ways
to end half-finished, and the package manager owns that install anyway.
`canInstallInApp` is the branch; `manualInstallReason` supplies the
wording, because a `.deb` user is not on an unsupported platform and a
developer is not a user at all.
### Release notes come from GitHub, not `latest.json`
`latest.json` is written by `tauri-action` at build time, so its `notes`
are the fixed `releaseBody` string in `release.yml` — users saw "See the
assets to download and install this version." The real notes are written
on the release afterwards and can never reach that file, so both paths
now read the release body from the API. Editing a published release's
text updates every client with no rebuild.
The fetch is non-fatal (falls back to `latest.json`; the update is
signed and installs either way) and its notes are used only when the tag
matches the version being offered.
### Also here
- The dialog, on `ResponsiveModal`: centred on desktop, a bottom sheet
on Android. Version, date, rendered notes, download progress, restart
prompt.
- Concurrent checks share one in-flight promise. The startup check and
the Settings button do race, and the loser was told "You're up to date!"
about a request that had not returned.
- Errors are classified (`no-release`, `network`, `unsupported`), so a
404 from an unpublished draft no longer reads as a flat failure.
- The **Auto-download Updates** toggle is removed. It installed
unattended — an NSIS installer mid-session on Windows — and could never
do anything on Android. Off by default, so no existing behaviour
changes. A stored value is ignored, as with any legacy settings key.
- README: how the two paths differ, that `RELEASE_REPO` and
`updater.endpoints` must stay in step, and that **a draft release is
invisible to the updater** — publishing the draft is what ships it.
## Release script
- Version arithmetic extracted into `scripts/version.js`, covered by
`scripts/version.test.js`
(17 cases). `vitest.config.ts` now includes `scripts/**/*.test.js` for
it.
- **Every precondition is checked before anything is written**: a clean
tree, a version that
moves forward, and a tag/branch that exists neither locally *nor on the
remote*. A failure
after that point deletes the branch and tag it created and returns to
the original branch.
- `--dry-run` runs the checks and stops. `--no-merge-back` skips the
fast-forward of the
branch the script was run from.
- Unknown flags are rejected rather than silently ignored.
- The rollback's `reset --hard` is guarded behind a **successful
checkout**: it moves the
*current* branch, so a failed checkout dragged the release branch onto
the start commit.
- Everything after the push is explicitly best-effort and is **not**
rolled back — the tag is
out and the release is building, so a protected branch or a race there
is a reason to print
two commands, not to unpick a published release.
- Only `X.Y.Z` and `X.Y.Z-pre.N` are accepted. Other pre-release
spellings are valid semver
but match neither workflow trigger, so they would tag and build nothing.
- Works on Windows again.
Note the `--` in `npm run release -- <bump>`: without it npm consumes
the flags before the
script sees them. Documented in the README.
## eslint
`eslint-plugin-boundaries` v7 renamed `rules` to `policies` and takes
the element type inside
the `disallow` target rather than in a second outer `to`. The config was
still written for the
v6 shape — it worked, but on the deprecated path.
Verified **warning-for-warning identical** across the repo: 189
warnings, same files, same
positions, before and after.
`mode: 'folder'` dropped where it is already the default.
## Android
`src-tauri/gen/android/app/tauri.build.gradle.kts` is autogenerated by
the Tauri CLI and was
being regenerated on every build, so it showed up as a dirty file
constantly. Untracked and
gitignored.
---
Base: `upstream/master`. `check`, `lint` and the full Vitest suite pass.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Improved release automation with validation, dry-run support, rollback
handling, and optional branch merge-back.
* Added support for stable and prerelease version validation,
comparison, and version bumping.
* **Bug Fixes**
* Added safeguards for repository state, remote references, network
availability, and Windows npm execution.
* **Documentation**
* Expanded release instructions with supported version formats, safety
checks, rollback behavior, and usage options.
* **Tests**
* Added comprehensive coverage for version handling and release-related
scenarios.
## Resolution is an intent, not a pixel count
The backends disagree on what a size even *is*. Google takes an aspect
ratio from a closed
list and has no pixel parameter at all; OpenRouter takes ratio +
`1K`/`2K`/`4K`; NanoGPT
publishes a per-model list of accepted values; only ComfyUI, A1111 and
Pollinations take
arbitrary dimensions. There is no pixel size all of them accept, so the
old pixel selector
was offering the user something most providers could not honour.
It is now an **orientation** (1:1 / 16:9 / 9:16) plus one of four **size
steps**, and each
provider adapter resolves that into whatever its backend takes
(`src/lib/utils/image.ts` +
`image/providers/*`). `registry.test.ts` covers all nine adapters.
- `specToAspectRatio` falls back to the closest ratio **by shape**,
compared in log space —
on a linear scale portrait ratios crowd into (0, 1] while landscape ones
spread over
[1, ∞), which biases every near-miss towards square. It also seeds its
fallback with the
canonical ratio, so a wholly unparseable `supported` list cannot echo
its own garbage back.
- `ImageResolutionSelector`'s accessible name carries the tier and the
cost hint and repeats
the visible dimensions verbatim (WCAG 2.5.3).
- **Stored settings migrate.** `parseImageSpec` reads both the old
`WIDTHxHEIGHT` strings and
the new specs, and `migrateImageGeneration` is idempotent — it runs on
every load, not just
the first after upgrade. Defaults land on the same pixels they replace.
## Model selection
- Manual custom image-model entry, with clearer descriptions and errors.
`Autocomplete` now
says "Type custom value to select..." when it accepts one, instead of
"No items found."
- **Hardcoded fallback model lists removed.** A failed fetch now shows
an empty list and the
error, rather than presenting stale model ids as if they were live —
which is worse than
showing nothing, because the user picks one and the request fails later
with a less useful
message.
## `reasoning_effort` never reached any `createOpenAICompatible`
provider
The SDK spreads unknown provider options into the request body and
*then* assigns
`reasoning_effort` from its own parsed `reasoningEffort`. The later key
wins, so the
snake_case value we sent was overwritten with `undefined` and dropped —
silently, on every
call, for every provider built that way.
Now sent as `reasoningEffort` for nanogpt, llamacpp, lmstudio, ollama,
chutes, nvidia-nim,
openai-compatible and zhipu. Zhipu keeps `thinking`, which is not a key
the SDK knows and so
passes through the spread untouched.
Separately, `nvidia-nim` provider options are looked up under
`nvidiaNim`; the SDK still reads
the hyphenated form but warns that it is deprecated.
## Second commit — review feedback
- **`InlineImageTracker` used the primary size for reference images.**
It switched profile
*and* model to the reference profile for an img2img generation but kept
`imageSettings.size`. A reference model is a different model on a
different backend, so it
was handed a size that backend may not accept. `InlineImageService` and
`aiService.generateImage` already used `referenceSize` — the inline path
was the one
disagreeing. Predates this branch; fixed here because this is the branch
that gives `size`
a type where the mismatch matters.
- `parseImageSpec` takes `unknown`. Its caller is the settings load
path, where the value is
whatever JSON is on disk; a number or boolean reached `value.trim()` and
threw — the one
outcome that path cannot afford.
- `registry.test.ts`'s `ALL_PROVIDERS` claimed to be exhaustive and was
not: an array
annotated `ImageProviderType[]` accepts a short list happily. Now a
`Record<ImageProviderType, true>`, so the claim is enforced by
`svelte-check`.
- Tests for the Zhipu branch of `buildProviderOptions` — the one
provider the
`createOpenAICompatible` loop cannot cover — in both the enabled and
`none` cases.
## Narration length
A per-story **target length** — short / medium / long / dynamic — set in
the setup wizard
and changeable in Story Settings, rendered into the narrator prompt as
`{{ lengthInstruction }}`.
It replaces the hardcoded "around 250 words" and "up to 500 words" lines
the two narrative
templates carried, which were not adjustable and were the same in both
story modes.
- `WritingStyleFields` takes a `mode` prop. The paragraph counts it
displayed were the
adventure ones only, so a Creative Writing story advertised ranges its
own prompt does
not ask for. One table now covers both modes, and both call sites pass
their mode.
- Story Settings **disables the control, with the reason**, when the
prompt that will
actually run does not render the variable. A per-story override replaces
the pack
template outright, and a pack created before the variable existed keeps
its own copy of
the body — in both cases the setting would silently do nothing. That
check is why
`ContextBuilder.resolveTemplate` is no longer private.
- `NarrativeService` fills the variable itself for contexts not built by
`ContextBuilder.forStory`, since the templates render it bare.
- Visible focus ring on the sr-only radio labels, via
`has-[:focus-visible]:` — the items
are children, not peers.
## Orphaned scroll locks
Both modal libraries lock `document.body` while something is open and
clean up on the path
they expect. The path they do not expect is the component being
unmounted while still open,
which is how several modals here close: `SetupWizard.handleClose()`
flips `isOpen` and then
calls `onClose()`, which removes the whole thing from the tree.
`vaul-svelte` restores from
the setter of its `open` box, so a close driven from the other side
never reaches it and
`body` keeps `pointer-events: none` for the rest of the session — an app
that renders
perfectly and ignores every tap.
- Recovery now has **one owner**: `AppShell` watches `document.body` and
releases an orphan
once the close transition has had time to run, replacing four ad-hoc
recoveries.
- `releaseOrphanScrollLock` checks whether anything on screen is
entitled to hold the lock
before releasing, so a modal closing on top of another leaves it alone.
The selector list
covers exactly what `bits-ui` locks for (`preventScroll` defaults to
true only on dialog,
alert-dialog and context-menu) plus `vaul`'s drawer.
- `MODAL_CLOSE_TRANSITION_MS` is stated once instead of guessed per call
site.
## Debug log modal
- The log snapshot is a whole copy of the session's requests and
responses, so it is dropped
**after** the close transition rather than while the DOM still needs it.
- The pending-refresh timer is cleared before its early return. Leaving
it set wedged the
throttle, since the branch that schedules a refresh is guarded on it
being null.
- Throttle window 500ms → 1200ms; the snapshot is not free.
- The floating debug button is hidden with a class rather than
unmounted, so opening and
closing the panel no longer resets its dragged position.
Updates scripts/release.js to remove the --offline flag from cargo
update and automatically run npm run format and npm run lint:fix before
committing release changes.
- **Fix Android Touch Lockup & Keyboard Blur**: Controlled
`ResponsiveModal.Root` with reactive `isOpen` state in `SetupWizard` and
`STImportWizard`. Implemented proper blur on `document.activeElement` to
release Android WebView soft keyboard focus, closed modal state before
unmounting, and added fallback cleanup of `pointer-events`, `overflow`,
and `data-scroll-locked` on `document.body`.
- **Extended Font Size Presets (Fixes#410, Fixes#404)**: Added
`xlarge` (24px) and `xxlarge` (30px) preset options to UI settings,
typography CSS, settings tab, and onboarding welcome screen without
requiring database schema migrations.
- **Wizard Modal Dismissal Protection**: Added `isCreatingStory` state
tracking to `WizardStore` and updated `SetupWizard` to ignore external
close requests (outside click / Escape key) while story creation is in
progress.
- **Copy Message Action**: Added a "Copy" button to the `StoryEntry`
action toolbar with instant visual icon feedback (`Check`) and toast
notification.
- **Fix Button Tooltip Attribute Forwarding**: Fixed a bug in
`button.svelte` where `title` was destructured out of `ButtonProps` and
omitted from standard non-responsive `<button>` elements, restoring
tooltip displays for icon buttons across the application.
- **Character Panel Overflow & Tooltip Enhancements**: Added
`max-height` and vertical scrolling limits to long `relationship` badges
and expanded character details in `CharacterPanel` and
`Step5SupportingCast`. Enhanced action tooltips for character actions
(impersonate/star, edit, save to vault, portrait).
Small fix, and I may well be missing context on why it was written this
way — happy to be told otherwise.
`tauri-plugin-sql` resolves `sqlite:aventura.db` against Tauri's **app
config dir**. Three places in the Rust layer open the same file
directly, and they all resolve `app_data_dir()`:
- `backup.rs` — `db_path()`, used by backup, restore and image export
- `avt_import.rs` — `open_rw_pool()`, the `.avt` image import pass
- `lib.rs` — the setup hook that patches the stored `sqlx` migration
checksums
On Linux those are two different directories (`~/.config/<bundle-id>` vs
`~/.local/share/<bundle-id>`), so these paths point at a file that does
not exist. The checksum patch is guarded by `try_exists()` and so just
silently does nothing; the backup and `.avt` paths hit the missing file
directly.
This changes the three call sites to `app_config_dir()` (and the
matching error strings / doc comment), nothing else.
On testing: I looked at covering this, but every version I tried ended
up asserting Tauri's own path resolver rather than the property that
matters — that these agree with wherever `tauri-plugin-sql` opens the
database. If you would like a test anyway, tell me the shape you would
prefer and I will add it.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* Improved database location handling by consistently using the
application’s configuration directory.
* Updated database access, backup, and migration checks to use the
corrected location.
* Updated related error messaging to reflect the new database path.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Expanded reasoning controls to six levels, including minimal and
extra-high effort.
* Added broader structured-output support across AI providers.
* Improved compatibility with current AI models and custom image
workflows.
* **Bug Fixes**
* Legacy reasoning settings now migrate automatically to the new
disabled state.
* Improved reasoning visibility and provider-specific generation
behavior.
* **Chores**
* Updated runtime components and development tooling for improved
compatibility and reliability.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Memory retrieval
### Agentic retrieval (new)
- **`AgenticRetrievalService`** — an alternative to the static chapter
fill, selectable as the **Memory mode** in Advanced Settings. It runs a
tool-calling loop that reads the past and returns a prose summary. It
deliberately **selects nothing**: which lorebook entries reach the
narrator stays with Entry Retrieval in every mode, so "who put this
entry in the prompt" has one answer again.
- **`grep_chapters`** — a literal-substring search over the raw story
text and the un-chapterized tail, at zero LLM cost. Reports per-chapter
match counts plus a sampled spread of excerpts, each stamped with its
in-story time and labelled `ACTION` or `NARRATIVE` (the corpus includes
what the player typed, and handing that back to the narrator as
established fact is a real failure mode).
- **`inspect_world_state`** — lets the agent check live-tracked
characters, locations, inventory and story beats.
- **A run that hits the step ceiling now spends its last step on the
summary** (`finishOnlyOnLastStep`) instead of producing nothing. A run
that dies is salvaged rather than discarded, since chapter answers cost
a full LLM call each.
- **`MAX_CHAPTER_QUERIES`** caps whole-chapter reads at 3 per run.
Nothing bounded them before: `maxIterations` counts steps, not reads, so
a run could spend every step on a ~17,000-token chapter.
- **Repeated tool calls are deduplicated** — an identical grep replays
the stored answer, and a repeated chapter question is served from the
answer already paid for.
- **A typed, append-only run history** (`retrievalHistory.ts`) replaces
four parallel trackers. It produces a one-line progress note attached to
every tool result (the agent cannot otherwise see its own history) and a
readable transcript for the debug view.
- **`list_chapters` was dropped.** The chapter list is in the prompt
already; a tool returning the same summaries gave the agent two copies
to reconcile.
### Grep quality
- **Excerpt sampling replaces offset paging.** Paging assumed a caller
that walks pages; models either stop at page one or burn their budget
looping. Worse, page one was the *oldest* matches. Sampling returns a
spread instead, and the per-chapter counts stay complete.
- **The sample is weighted by hit count, not passage count.**
`findTextMatches` merges neighbouring matching paragraphs, so counting
passages penalised exactly the chapters where a term concentrates —
twenty mentions in one scene weighed two while four scattered ones
weighed four. The densest chapter in a story could end up quoted
nowhere.
- **A drowning substring search is retried on word boundaries.** A short
name matches inside longer words ("Ren" in "surrender", "children"): I
hit 1,000+ matches and 40 excerpts of unrelated prose. If `wholeWord` is
unset and the search exceeds `GREP_NOISE_RATIO` matches per slot, the
whole-word variant is substituted — but only if it removes at least half
the matches without falling to zero, so a stem like "rune" is left
alone. An explicit `wholeWord: false` is always honoured, and the result
says which search actually ran.
- **A search that is still noisy quotes fewer excerpts and says how to
narrow**, rather than spending the full allowance on prose that matched
by accident.
- **Excerpts are sized in words, not characters**, with each passage's
allowance proportional to its hits, and the cut snapped to sentence
boundaries. Excerpts used to open mid-clause, which is hard to judge a
passage by.
- **`truncateAroundMatch` keeps the whole span of occurrences** when it
fits, instead of re-cutting around the first — otherwise the truncation
undid the merge that made the passage worth showing.
- **Searches survive typography.** Smart quotes, dashes, ellipses and
line breaks inside a paragraph are folded on both sides, so a query
typed with `'` finds prose written with `’`.
- **A chapter number that does not exist is reported as such**, instead
of coming back as "0 matches" — which the instructions teach the agent
to read as "not in the story". The un-chapterized tail is addressable as
chapter `-1` for the same reason.
### Static timeline fill
- **Chapter reads are bounded in code, not by the prompt.** A query
naming three chapters built a 50,000-token prompt and one naming four
built 68,000 — both rejected outright by my 49,152-token server.
`chapterContentBudget.ts` bounds it at `CHAPTER_READ_BUDGET_RATIO` (2.5)
× the story's own `memoryConfig.tokenThreshold`, so it scales with the
user's setting instead of being a number picked in code. A `[TRUNCATED:
...]` marker names what was left out, because a model that is not told
will report on chapters it never saw.
- **The cut is a single stop point.** It used to keep filling later
chapters from whatever tokens were left, returning three chapter
openings and no whole chapter.
- **Questions covering the same chapters are answered in one batched
call**, with a per-question fallback when the batch comes back
incomplete — a provider with weak JSON-schema support would otherwise
lose every answer in the group instead of one.
- **A question whose chapters are a subset of another's is answered from
that group**, so chapter 18's text is not assembled and sent twice. Only
while the wider group fits the budget, though: the read is cut from its
highest chapter down, so a folded question about chapter 19 could
otherwise be answered from a text stopping inside chapter 18.
- **An unanswered question no longer reaches the narrator.** Both
give-up paths return `confidence: 0` and are dropped; they used to
arrive as `A: Unable to answer the question.` under a heading claiming
the material was relevant to the current scene.
### Shared
- **Tier 3 selection is shared** between `EntryRetrievalService` and
`WorldStateInjector` (`tier3Selection.ts`). Results are returned in the
model's own order, because both callers cap the result and candidate
order is an artifact of prompt assembly — for world state it is grouped
by type, so a cap applied to it dropped whole categories regardless of
relevance.
- **Memory retrieval is told what the prompt already contains**
(`alreadyInContext.ts`), so it stops re-deriving the obvious. The
summary is suppressed entirely if either half failed: a partial list is
read as a statement, so naming half of it invites work on the other
half.
- **`RetrievalPhase` runs in two stages** for that reason — world state
and lorebook selection must both finish before memory retrieval starts.
- **The chapter-read budget is derived from the story's own threshold at
every call site.** Three of the four passed nothing and silently read
against the 40,000-token fallback.
---
## World state and lorebook injection
- **`WorldStateInjector`** is split out of the old `EntryInjector`,
which conflated authored lorebook entries with live-tracked world state.
The two are separate data domains and are now injected by separate
services.
- **Tier 1 is no longer capped.** "Always inject" and "the first N of
these" cannot both be true, and the Advanced Settings copy promises the
former. Once the cap became a slider the mismatch was reachable:
narrowing Tier 2/3 noise silently dropped characters standing in the
scene.
- **The protagonist has a slot of their own.** They were excluded from
Tier 1's character list but nothing excluded them from Tier 2 or 3, so
they came back in through name matching and were rendered among the NPCs
— meaning the narrator's knowledge of who the player *is* depended on
whether their name happened to appear recently.
- **Stickiness** carries an entity forward for a few story positions
after its "always include" condition stops holding, so context does not
vanish the instant a character leaves the scene.
- **State sections and relevance sections are kept apart.** Sticky
entries are in Tier 1 precisely *because* their state condition stopped
holding, so routing them through `[INVENTORY]` and `[ACTIVE THREADS]`
told the narrator the player carries a dropped item and is pursuing a
finished quest. Sticky locations were rendered nowhere at all while
still being announced as present.
- **Lorebook retrieval is no longer skipped in agentic mode**, which
removes the fallback it needed.
- **The Recent Entries Window setting works above 10.** A fixed slice at
the call site silently capped a slider that goes to 15.
---
## Prompt ordering and prefix caching
Inference servers reuse the KV cache for the longest shared prefix.
Templates are now ordered by how often each block changes rather than by
how the prompt reads.
- On a 40-chapter story the narrator prompt is ~156k characters, of
which ~54k is byte-identical chapter summaries; with the per-turn world
state in front of them the reusable prefix was **3.6%** of the request.
- The same inversion cost the retrieval agent, the classifier and both
Tier 3 selections their entire prefix — two consecutive classifier calls
shared 201 characters out of 40,000.
- The agentic retrieval prompt was measured at **12,363 tokens of prompt
processing per turn** on llama-server before reordering.
- `narrative.test.ts` pins the ordering, since reversing it breaks
nothing visible.
- The remaining work here — moving the volatile blocks out of the
narrator's *system* message — is not part of this PR.
---
## Prompt packs
- **`036_pack_template_baseline.sql`** adds `baseline_hash`. The startup
refresh compared the stored content's own hash against the baseline, so
it read **every user edit as a stale default and reverted it on every
app start**. `content_hash` and `baseline_hash` are equal only while a
template is untouched, and `setPackTemplateContent` now takes a required
`isBaseline` flag so the distinction is forced at every call site.
- Existing rows are backfilled from `content_hash`, which errs toward
keeping the user's content.
---
## Images
- **`<pic>` tags survive apostrophes and angle brackets.** `prompt="a
knight's blade"` captured `a knight`, fell under the length floor, and
the tag was dropped — silently, mid-narration. A prompt containing `>`
ended the match early and left the literal `<pic ...>` string in the
rendered narration, with both `hasPicTags` and `stripPicTags` agreeing
there was nothing to strip.
- The pattern is exported so `ImageEmbeddingService` cannot drift from
the parser; it had its own copy, so a tag the parser accepted could
still be left un-swapped.
- **Scene analysis salvages a partly-malformed response.** Providers
without real structured-output support return the array directly instead
of the `{ scenes: [...] }` wrapper, or one bad scene among good ones;
the whole batch was rejected. Valid scenes are now kept.
- The non-functional manual "Generate Images" button was removed.
---
## UI and stores
- **Fixed a whole-app deadlock when popping out the debug window.**
Tauri holds `webviews_lock` for the entire emit while the emit blocks on
the main thread, which needs the same lock to answer the new window's
setup calls. Neither side yields and every window freezes with the CPU
at zero. Events are now dropped until the window reports ready — they
are all in `debugLogs`, which is handed over in full anyway.
- **Fixed a CSS leak in custom themes.** A `@keyframes` placeholder
landed in the same chunk as the selector following it, so **every rule
after an animation** went unscoped. Rules before it were scoped
normally, so it looked like it worked.
- **Fixed the settings tab crash.**
- **Renaming a story no longer resurrects a discarded value** — the blur
that fires as the input is torn down could commit it, and whether
removing a focused element fires blur at all differs between engines.
- **Regenerate works without a retry backup** (they live in memory and
do not survive a restart or story switch), and the button is only
offered where the operation will actually be allowed.
- **Lorebook activation data is migrated per story.** It lived under one
global key, so only the last story played had any.
- The memory threshold ceiling was lowered from 100,000, which nothing
could use — a chapter is built to be about one threshold's worth of
tokens, so that asked for a 250k-token read.
- Debug logs can be exported to JSON, and stringify no longer throws on
circular references.
- Advanced Settings reports a control that currently has no effect
**with a reason**, rather than hiding it — in a panel that dense, a
control that vanishes reads as a bug.
---
## Settings
- **Settings migrations extracted to `settingsMigrations.ts`** so they
can be tested. They run unattended over data the user cannot get back,
and the rune store cannot be imported by a test. Each one must be
idempotent (nothing removes legacy keys, so they run on every load) and
silent about untouched values (a stored value equal to the old default
was never a choice).
- **`core/defaults.ts`** is a leaf module holding the defaults both the
settings store and `AI_CONFIG` need. They were duplicated under a
comment asking whoever changed one to remember the other.
- **Agent Profiles** gained `agenticRetrieval`; `bgImageGeneration` is
its own `ServiceId`.
- Tier 3 selections are labelled separately in the API Debug Logs — both
callers passed the same string, and the view filters by it, so there was
no way to look at one without the other.
---
## Correctness fixes found along the way
- **The database is opened from the app config dir, not the app data
dir.** `tauri-plugin-sql` resolves `sqlite:aventura.db` against the
config dir, but `backup.rs`, `avt_import.rs` and the migration checksum
patch in `lib.rs` all used `app_data_dir()`. On Linux those are
different directories, so backup, restore, image export and `.avt` image
import were all pointing at a database that does not exist.
- **A tool's registration condition and the prompt text describing it
now come from the same expression** (`canGrepChapters`,
`hasLiveWorldState`). Both were written twice, so the flag reached the
template while the tool list ignored it — the model was handed a
callable tool its instructions denied existed.
- **Prompt blocks are joined without running into each other.** The
blocks disagree about who owns the blank line in front, so agentic mode
produced `...devoted pet.[RELEVANT STORY DATA...` with no break, and the
static path produced three blank lines.
- **The retrieval agent gets a usable view of the present scene.** Its
recent-narrative cap was below the size of a single entry on a real
save, so the block degenerated to the player's own action — a verbatim
echo of the input printed two lines above it.
- **The tail no longer reaches the agent twice.** `recentNarrative` and
`getUnchapterizedEntries()` are two derivations of the same slice, so
grep could spend its budget quoting prose already in the prompt. It is
split once, and each half goes to one consumer.
- **In-story time is `null` when unknown** on retrieval paths, rather
than falling back to "Year 1, Day 1" — in retrieved material "I don't
know when" and "at the very beginning" are different claims.
- **Android builds get an explicit
`androidx.lifecycle:lifecycle-process` dependency**, which the generated
`WryActivity.kt` needs. It is declared in `app/build.gradle.kts` rather
than `tauri.build.gradle.kts`, since the latter is autogenerated and
would lose it the next time the CLI wrote that file.
---
## Tests
The suite goes from 7 test files to 55, and from roughly 90 tests to
679. Vitest has no DOM environment here, so this is all logic-level --
no component is rendered by any test.
- New coverage for all seven generation phases, the retrieval modules
(`grepSampling`, `retrievalHistory`, `chapterContentBudget`,
`timelineFillGrouping`, `recentTail`, `stickiness`, `alreadyInContext`,
`tier3Selection`), the extracted utils (`text`, `storyTime`,
`promptBlocks`, `recentContent`, `storyEntries`, `cssScope`,
`inlineImageParser`, `htmlStreaming`, `visualDescriptors`,
`dedupeTextModels`), `settingsMigrations`, `advancedPanelView`,
`WorldStateInjector`, `EntryRetrievalService`, `TimelineFillService`,
`AgenticRetrievalService` and pack service.
- Where a rule only exists in a rune store or behind a provider, the
logic is extracted into a plain module with real callers rather than
left untested.
- `narrative.test.ts` pins the prompt ordering;
`WorldStateInjector.test.ts` pins the invariant that anything announced
as already-in-context is actually renderable in the block.
## Housekeeping
Removed verified dead code: `fontDetection.ts`, `FontSelector.svelte`
(already imported by nothing), `EntryInjector.ts` (superseded),
`DiffView.svelte`, `hashApiKey.ts`, and unused exports across
`markdown.ts`, `htmlSanitize.ts`, `visualDescriptors.ts`,
`templates.ts`, `events.ts`, `validation.ts` and the sync/discovery
types.
- Streaming assistant replies now update live with text, reasoning, tool
activity, and a dedicated preview of pending changes.
- Added a stop-generation flow with consistent Escape/stop behavior,
plus improved generation UI (including a blinking cursor).
- Tools can load on demand by category for more focused interactions.
- Conversation titles can be renamed directly from the selector.
- **Bug Fixes**
- Improved persistence so concurrent/rapid generation actions don’t
overwrite each other.
- Preserved in-progress edits when new pending changes arrive.
- Prevented duplicate vault values when composing multiple changes.
- More reliable aborted vs error handling during streaming.
* **New Features**
* Added automatic “Generate chapters from history” for SillyTavern chat
imports, with options to include lorebook updates, timeline estimation,
and classification.
* Included a new chapterization review experience with staged progress
reporting and cancellation.
* Added in-story timeline estimation (chapter summary → elapsed
timeline).
* Show “Generate story images” for any narration entry when agentic
image mode is enabled.
* **Bug Fixes**
* Improved cleanup after chat imports to remove cleared non-main and
stale chapter data.
* Updated story content generation to use a consolidated world-state
snapshot.
* **Improvements**
* Added deterministic chapter boundary planning for batch
chapterization.
* Enhanced AI import error messages with more detailed diagnostics.
* Improved chapterization workflow progress/status handling and cancel
behavior.
The core of this work fixes **Android OutOfMemory crashes** during
export, backup, restore and import, and makes those flows fast. It also
folds in two issue fixes and a few UX/quality improvements.
### 1. Android OOM: native, streamed export / backup / restore
**Problem.** Exports and backups routed hundreds of MB (base64 images,
the full SQLite DB) through the JS/IPC bridge as strings. On Android's
constrained WebView heap this reliably OOM-crashed.
**Fix.** All heavy file I/O now happens natively in Rust and streams
file-to-file — nothing large ever lives in the JS heap or crosses the
bridge:
- `backup_database`, `export_story_avt`, `export_single_image`,
`export_images_zip` write **directly** to the chosen destination.
- On **desktop** the destination is the real path from the save dialog.
- On **Android** the destination is the SAF `content://` URI from the
save dialog, opened natively via the fs plugin's content-URI file
descriptor (`ContentResolver`). Writes stream in ~8 KB blocks, so peak
memory is a single buffer regardless of file size.
- **Restore** mirrors this: the picked `content://` backup is streamed
into a temp file in the app's internal cache dir, then restored
natively.
- The `.avt` exporter fills each image's base64 from SQLite on the
native side, so image bytes never touch the WebView heap.
**Perf.** Backup DB compression dropped from Deflate level 6 → **level
1** (the DB is mostly already-compressed base64 PNGs, so level 1
recovers the base64 bloat at a fraction of the CPU), and the `.avt`
exporter now pulls all images in **one streamed query** instead of one
query per image.
### 2. Native `.avt` import — the OOM's other half
The known limitation this PR originally shipped with ("importing an
extremely large `.avt` still parses JSON in the WebView") turned out to
be a hard failure, not just memory pressure, and it is now fixed.
**Problem.** The Library's Import button read the picked file with an
`<input type="file">` and `file.text()`. That has to materialise the
whole `.avt` as a JS string, and V8 caps a string at `2^29 - 24` bytes
(~512 MiB). A real 546 MB export exceeded it by 1.8% and failed with a
misleading *"not a valid JSON file"* — the file was intact; the read
never delivered it. No amount of RAM helps: the ceiling is structural.
**Fix.** Import now goes through the native picker into a streaming Rust
reader, mirroring the exporter's split — **JS owns the structure, Rust
owns the bytes**. Id remapping, insertion order and foreign keys stay in
TypeScript where they are tested; only the payloads move.
Two passes, both streaming:
1. `avt_read_light` returns the JSON with every `imageData` removed
(serde's `IgnoredAny` skips each base64 without allocating it). JS
parses that and runs the ordinary import.
2. `avt_import_images` re-reads the file and streams each payload
straight into SQLite, using the small old-image-id → new-entry-id table
JS produced.
Re-reading is deliberate: a staging table would need a migration and
would leak orphan rows if an import died half way. Peak memory is one
image regardless of file size — measured at **16 kB vs 108 MB** for a
naive parse of a 100 MB file (`cargo test --lib avt_import -- --ignored
--nocapture`).
The structure and the images cannot share a transaction (the native side
writes over its own connection), so a failed image pass deletes the
half-imported story rather than leaving one with its pictures silently
missing.
**Verified on-device:** the 546 MB `.avt` that previously failed now
imports in ~1 minute.
### 3. Export/import robustness
- New `errMessage()` helper: Tauri command rejections arrive as plain
strings, so the old `e instanceof Error ? … : 'Unknown error'` discarded
the real (often precise) native message. Now surfaced everywhere
export/import can fail.
- Export toasts now report failures instead of failing silently (the
`.avt` gather step used to run outside the try/catch).
### 4. Closes#330 — Pollinations image models missing
The provider filtered out models flagged `paid_only`, hiding valid image
models from the list. Filter removed so all image-capable models appear.
The "Include paid models" toggle is gone with it: the API already
filters by tier server-side from the API key, so the toggle was a
client-side duplicate that was inert wherever the key was actually sent
(images) and misleading where it wasn't (text). The text model fetch now
sends the key too, so the list reflects what the key can really
generate.
### 5. Closes#116 — Adjustable chapter summary length
Chapter summary length is now configurable rather than fixed.
### 6. Generation info popover
Narration entries now have an info popover showing the model / profile /
effort / timestamp used to generate them.
Removed the short-circuiting .includes() check that caused keywords
(like 'hulk') to falsely trigger on substrings (like 'madhulkman').
Replaced it with a dynamic regex using \b word boundaries for
space-separated languages. Added CJK character detection so that Asian
language keywords (which don't use spaces) bypass \b and correctly use
substring matching, since \b does not work between CJK characters.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* Improved keyword matching for CJK and other non-space-separated
scripts (Chinese, Japanese, Korean, Thai, Lao, Khmer, Burmese) using
reliable substring matching.
* Enhanced boundary-aware matching for space-separated languages to
reduce false positives.
* Refined overall search result relevance across mixed-language queries.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Chores**
* Version bumped from 0.7.4 to 0.7.6
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Overview
Fixes 9 bugs in the vault assistant and lorebook editor, adds AI-driven
re-indexing to detect stale entry data after user edits, and adds
composed preview for multi-update entities.
---
## Changes
### Bug Fixes
#### 1. Approved changes replay on chat reopen (Bug 3)
`vaultEditorStore.approve()` / `reject()` / `approveAll()` mutated
change status in memory but never called `service.saveConversation()` to
persist the updated pending-changes array. On reopen, all changes loaded
with `status: 'pending'`. Added `saveConversation` after every
approve/reject/approveAll operation.
**Files:** `InteractiveVaultAssistant.svelte`
#### 2. Standalone editor shows stale data after vault assistant edits
(Bug 1)
The sync `$effect` had an `if (!isEmbedded) return` guard, so standalone
mode never synced from the vault store. Removed the guard so both modes
read from the canonical source.
**Files:** `VaultLorebookEditorContent.svelte`
#### 3. Changes revert on lorebook close/reopen (Bug 2)
`handleSaveClick` constructed `updatedLorebook` from the mount-time deep
copy, silently destroying any entries added by the vault assistant after
mount. Now merges vault-store entries that aren't in the local array
before saving.
**Files:** `VaultLorebookEditorContent.svelte`
#### 4. Pending operations sidebar shows wrong entry names (Bug 4)
`entries` was initialized from `previewLorebook.entries`, which has
pending deletes already applied (truncated). Pending changes still
referenced original indices. Changed `entries` initialization to read
from the canonical vault store (`lorebookVault.getById()?.entries`) in
embedded mode.
**Files:** `VaultLorebookEditorContent.svelte`
#### 5. Close-reopen freeze on vault assistant
Inline `onOpenChange` guard silently blocked `onClose()` when
`isGenerating` was true, leaving the component in an invisible
mounted-but-stuck state. Replaced with `handleOpenChange()` that aborts
generation and always calls `onClose()`.
**Files:** `InteractiveVaultAssistant.svelte`
#### 6. Stale editor data after AI updates in same chat
The sync effect only pulled in entries with **new names** — updates to
existing entries (same name, changed content) were silently dropped when
`entriesDirty` was true. Rewrote the effect to always reconcile both new
and updated entries from the vault store, using `untrack()` to avoid
re-firing on keystrokes. Fixed `handleSaveClick` to not overwrite local
changes with vault data (save should persist local state, only merge new
entries to prevent accidental deletion).
**Files:** `VaultLorebookEditorContent.svelte`
#### 7. Scenario editor doesn't reflect AI-approved updates
VaultScenarioEditor initialized `formData` once from the `scenario` prop
and never re-synced when the underlying scenario in `scenarioVault`
changed. VaultPanel held a stale snapshot of `editingScenario`. Fixed by
adding a sync `$effect` (matching the lorebook editor pattern) and
deriving `editingScenario` from the vault store via `editingScenarioId`.
**Files:** `VaultScenarioEditor.svelte`, `VaultPanel.svelte`
#### 8. Multiple pending updates to the same entity overwrite each other
When two pending updates target the same scenario/character,
`applyScenarioChange`/`applyCharacterChange` passed the full
`change.data` (a complete snapshot) to
`scenarioVault.update()`/`characterVault.update()`, so the second
update's unchanged fields overwrote the first update's changes. Fixed by
computing a delta (`_computeDelta()`) that only includes fields that
differ between `data` and `previous`, so sequential approvals compose
correctly.
**Files:** `InteractiveVaultService.ts`
#### 9. AI sends null/empty values that overwrite existing data
The AI tool calls include null values for fields it omits from an
update. The lorebook entry update path already filtered `v !== ''`, but
scenario and character update paths only filtered `v !== undefined`.
Extended all three paths to also filter `v !== null`, preventing
accidental data loss.
**Files:** `scenario.ts`, `character.ts`
---
### New Features
#### 10. AI lorebook re-indexing
Detects external lorebook edits between AI turns and injects a system
note prompting the AI to refresh entry state.
**Version tracking:** `lorebookVault._entryVersions` map — bumped on
every `update()` call that changes `entries`. Exposed via
`getEntryVersion()`.
**Tool context:** `activeLorebookId` in `LorebookEntryToolContext`.
Entry tools (`list_entries`, `read_entry`, `create_entry`,
`update_entry`, `delete_entry`, `merge_entries`) prefer live
`getLorebookEntries()` lookup over the snapshot `entries` array when
available.
**Change detection:**
`InteractiveVaultService.injectLorebookChangeNote()` — before each
`sendMessageStreaming`, compares the current entry version against
`_knownEntryVersions`. If stale, pushes a system note. Version map is
synced after approvals to avoid redundant notifications.
**Persistence:** `entryVersions` field on `VaultConversation` type,
saved/loaded via `database.saveVaultConversation` /
`loadVaultConversation`. New `035_entry_versions.sql` migration.
**Files:**
- `lorebookVault.svelte.ts` — version tracking
- `lorebook.ts` — `activeLorebookId`, live-data preference
- `InteractiveVaultService.ts` — `injectLorebookChangeNote`, approval
version sync, save/restore
- `InteractiveVaultAssistant.svelte` — wire version check into send flow
- `database.ts` — `entryVersions` save/load
- `types/index.ts` — `entryVersions` on `VaultConversation`
- `035_entry_versions.sql` — migration
- `src-tauri/src/lib.rs` — register migration
#### 11. Revert Changes button in lorebook editor
Adds a "Revert Changes" button next to "Save All Entries" that appears
when entries are dirty. On click, it restores all entries to the
original deep-copied snapshot, clears the `locallyDeleted` set, and
resets `entriesDirty`. The snapshot is captured at mount time from the
canonical vault store (embedded mode) or the lorebook prop (standalone
mode).
**Files:** `VaultLorebookEditorContent.svelte`,
`vaultEditorStore.svelte.ts`
#### 12. Block approve when editor has unsaved changes
`handleApprove` and `handleApproveAll` now check
`vaultEditor.editorDirty` before proceeding. If the lorebook editor has
unsaved local edits, the approval is blocked with a "Save your local
edits before approving changes." error message instead of silently
proceeding (which could lose data).
**Files:** `InteractiveVaultAssistant.svelte`,
`vaultEditorStore.svelte.ts`
#### 13. Composed preview for multi-update entities
When multiple pending updates target the same scenario or character, the
editor and diff cards now show what "Approve All" would produce — all
changes merged — instead of only the individual change's data.
**Diff view:** "After" column shows the composed result with a blue "all
changes" badge when multiple pending updates target the same entity.
**Entity editor:** Form initializes from the composed preview so you see
the final state. Changed-field highlighting diffs composed data against
original `previous`, so all pending changes are highlighted, not just
the active change's fields.
**Store:** `vaultEditorStore.composedData` derived getter merges all
pending changes for the same entity starting from the original
`previous` state, overlaying each change's delta.
**Files:** `vaultEditorStore.svelte.ts`, `VaultEntityEditPanel.svelte`,
`VaultDiffView.svelte`
---
## Verification
- `npm run check` — 0 errors, 0 warnings
- `npm run lint` — 0 errors, 181 pre-existing boundary-import warnings
(unchanged)
- `cargo check` — 0 errors, 1 pre-existing `unused_mut` warning
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Per-lorebook entry version tracking that notifies when lorebook
entries change externally
* "Revert Changes" in lorebook editor (embedded mode) and richer "After"
previews for multi-update edits
* Editor dirty-state tracking and global save shortcut (Ctrl/Cmd+S)
* **Bug Fixes / Improvements**
* Prevent null/empty-string values from overwriting fields on updates
* More robust modal open/close flows, unsaved-change warnings, and
improved save/approve/reject persistence
* Improved merge/apply logic for pending edits and tag rendering
stability
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
# 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)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## 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_stack_entry_start -->
[](https://app.coderabbit.ai/change-stack/AventurasTeam/Aventuras/pull/326?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)
<!-- review_stack_entry_end -->
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
- Adds a new a1111 image provider compatible with AUTOMATIC1111, Forge,
SD.cpp and Koboldcpp.
- Sampler and scheduler lists are fetched live from the running server
(same pattern as ComfyUI), so the dropdowns always reflect what the
instance actually supports
- Fixes several issues in the OpenAI provider introduced alongside it:
model list is now fetched from /v1/models (filtered by dall-e- /
gpt-image- prefix), custom endpoints get a permissive fallback
- fixes image gen model fetch bug (infinite loop)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Added A1111/Stable Diffusion WebUI as a selectable image provider with
provider-specific options (base URL, steps, CFG scale, sampler,
scheduler, negative prompt) and updated profile defaults.
* **Enhancements**
* OpenAI image provider now fetches available models live and handles
custom endpoints; API key usage is applied only when provided.
* Improved model-listing responsiveness and sampler/scheduler discovery
across providers.
* **Chores**
* Minor build configuration formatting update.
[](https://app.coderabbit.ai/change-stack/AventurasTeam/Aventuras/pull/324)
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Pento <pento@fedora.fritz.box>
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Added "Custom System Prompt" UI in Story Settings with per-story
override.
* Prompt editor shows debounced validation, syntax-error feedback, and
unknown-variable warnings.
* Collapsible "Available template variables" reference, load-mode
defaults, Save and Clear override controls.
* Story generation now uses the custom prompt and surfaces errors if a
custom template is invalid.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
fixes: https://github.com/AventurasTeam/Aventuras/issues/320
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Modal-based editor for embedded images letting users choose between
chat-derived or custom prompts.
* Streamlined image regeneration flow with improved prompt
reconstruction and centralized style-prompt handling for consistent
results.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
**Summary**
- New ComfyMode.CustomWorkflow: accepts any ComfyUI API-format workflow
JSON. Auto-detects positive prompt, seed, negative prompt, and SaveImage
node paths via KSampler graph traversal; falls back to title
heuristics. Ambiguous multi-node cases show a picker — writes to
profileCustomWorkflow only on explicit confirm (via pendingWorkflowData
staging state to prevent premature auto-save).
- New ComfyMode.UnetTxt2Img + unet-txt2img-workflow.json for diffusion
model checkpoints (AuraFlow, Flux, Lumina 2). Configurable CLIP type and
weight dtype. CLIP/VAE names auto-detected from ComfyUI at
generation time, cached per baseUrl.
- listModels() now fetches checkpoints, diffusion models, text encoders,
and VAE in parallel; seeds auto-detect caches.
- Shared buildOnFailedHandler extracts structured node-level ComfyUI
errors.
**Bug fixes**
- unetModelNames was a module-level Set — multiple profiles pointing to
different ComfyUI instances would corrupt each other's model lists. Now
Map<baseUrl, Set<string>>.
- Explicit BasicTxt2Img mode was treated as no-override, allowing
auto-detection to silently switch to UNet. hasExplicitOverride now
respects all explicit modes.
- ComfyModes was missing the ComfyMode.CustomWorkflow key, failing
svelte-check (Record<ComfyMode, any> exhaustiveness).
## Summary by CodeRabbit
* **New Features**
* Added ComfyUI "Custom Workflow" and "UNet → Text→Image" modes and a
built-in UNet workflow option.
* **Improvements**
* Workflow upload: JSON validation, required-node detection, enforces
single image output, and blocks saving until workflow is confirmed.
* UNet/CLIP/VAE listing auto-loads per endpoint and clears cached lists
when the base URL changes.
* Provider options (workflow/UNet) persist and autosave.
* **UI**
* Conditional controls: workflow upload/picker, UNet selection, hide
sampler/scheduler/CFG/steps for Custom Workflow, and conditional
Negative Prompt handling.
Make room for a usable nvidia nim provider!
Model health pings
- New pingEnabled flag per profile (opt-in, only shown for OpenRouter /
NIM).
- Periodic low-cost chat/completions probe (max_tokens: 1) classifies
each model as ok / slow / down / auth / rate_limited, with latency and
quota headers captured when available.
- Results persisted in new model_health_cache table (migration 034),
keyed by (provider_id, model_id, api_key_hash). API key hashed to 8-byte
SHA-256 prefix.
- Reactive SvelteMap-based store with DB hydration, concurrency-limited
batch pings (5 parallel), provider-specific TTLs (auto vs manual
trigger).
- Warmup on app start: pings only models actually referenced by main
narrative + generation presets, deduped by provider|baseUrl|apiKey.
- UI: health indicator (signal icon + latency) inline in ModelSelector;
ProfileWarningBanner blocks sending when the main narrative model is
down or auth-failed.
429 retry middleware
- Wraps both doGenerate and doStream with up to 3 retries (10s / 20s /
30s + jitter).
- Respects Retry-After (delta-seconds or HTTP-date), caps at 60s to fail
fast on long cooldowns.
- Abort-aware sleep: user cancellation interrupts the backoff
immediately.
- Note: only catches pre-stream 429s (rejected doStream() promise), not
mid-stream errors.
Other
- NVIDIA NIM profiles now ship with a default hiddenModels list to hide
embedding / guard / reward / vision-only / legacy models from the
selector.
- DatabaseService.init guarded against concurrent double-open via a
shared pending promise.
- updateProfile purges the health cache when apiKey or providerType
changes (prevents orphan rows under stale hashes).
- Fix thinkingConfig for Google models: correct level mapping per model
family — Gemini 2.5 uses thinkingBudget (0 = disabled, -1 = unlimited,
with per-effort budgets), Gemini 3.x uses thinkingLevel
(low/medium/high), Gemma 4 uses thinkingLevel (minimal/high only).
- Dynamic image model fetching: Google image models are now fetched from
the same /models?pageSize=200 endpoint used for text, filtered by
predict method or -image/nano-banana in the model ID. Hardcoded list
replaced with live data + fallback.
- Rewrite Google image provider: replaces direct HTTP :predict calls
with the Vercel AI SDK's generateImage() + google.image(). Supports
Imagen 4/Ultra/Fast and all Gemini image models (gemini-*-image,
nano-banana-*) transparently through the same code path.
- isGoogleImageModel() helper: Gemini image models are excluded from the
text model list and included in the image model list instead.
- Imagen 4 img2img support: imagen-4.0-generate-001 now correctly
reports supportsImg2Img: true.
- Reference profile UX: removed the model filter on the reference
profile selector (all models now visible); replaced with an inline
warning when the selected model doesn't support img2img.
- Rename provider label from Google Imagen to Google AI Studio.
Closes#301
Generally (hopefully) fixes spacing out UI away from android's native UI
(top bar and nav bars).
Tested on an emulated tablet, regular phone and a physical fold 7, with
both 3-button and gesture navigation.
---------
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Problem
When image generation fails, there was no visual feedback:
- Agentic images silently disappeared from the narrative text
- Inline images showed no error placeholder in the rendered content
- The error message stored was the raw JSON API response
- The retry button had no disabled state, allowing multiple concurrent
requests
-
Changes
- ImageEmbeddingService: include failed status in agentic image markers
so they remain visible in the text as a red wavy-underlined link
- StoryEntry.svelte: add failed CSS class for the link, add
flex-direction: column to the placeholder so the retry button is always
visible, add styles for .placeholder-text and .inline-image-retry,
disable the retry button immediately on click
- fetchAdapter.ts: extract the human-readable message from the API error
JSON instead of serializing the whole payload
- InlineImageService: emit ImageAnalysisFailed on generation error so
the UI shows an error toast
---------
Co-authored-by: Pento <federico.datamanager@gmail.com>
Fixes: https://github.com/AventurasTeam/Aventuras/issues/302
Problem:
Editing the last user message previously triggered an automatic story
regeneration (undoing subsequent AI turns). This caused a disruptive
user experience when fixing minor typos or making small adjustments.
Solution:
- Modified StoryEntry.svelte to update the entry content and the retry
backup silently when editing the last user action.
- Decoupled message saving from the triggerRetryLastMessage() call.
- Synchronized the retry backup content to ensure that if the user
manually clicks "Retry" later, the AI uses the updated text.
- Added a UI hint to suggest manual regeneration after significant
message changes.
- Refactored isLastUserAction check into a Svelte 5 derived rune for
improved performance.
Impact:
Users can now fix typos in their last message without losing the current
AI response, while still having the option to manually regenerate the
answer if needed.
**Summary**
- Add createDebouncedSave(fn, delay?) in src/lib/utils/debounce.ts —
returns { trigger, flush } backed by a single shared timer
- Refactor 5 components (MemorySettings, AgentProfiles, MainNarrative,
api-connection, images) to use it, removing ~60 lines of identical timer
boilerplate
**Bug fixes included**
- MemorySettings: local slider state was declared with $derived, making
it read-only in Svelte 5 — replaced with $state seeded from the store at
mount
- api-connection: after await settings.updateProfile() resolves, the
component may already be unmounted; added a mounted guard to prevent
stale writes to saveStatus and a dangling setTimeout
- api-connection / images: onDestroy previously discarded any pending
debounced save; it now flushes it, so changes are not lost when the
settings modal is closed mid-debounce window
- Upgrade tailwindcss and @tailwindcss/vite from 4.1.18 to 4.2.2, fixing
spurious "Invalid declaration" warnings emitted during dev server
startup caused by the v4 CSS parser misreading Svelte <script> block
content
- Remove tailwind.config.ts: the project uses Tailwind v4 CSS-first
configuration (@import 'tailwindcss' in app.css), so the leftover
v3-format config file was never loaded
- Pin @saintno/comfyui-sdk to 0.2.49 — versions ≥ 0.2.50 pull in ink,
react, asciify-image, and the abandoned request package as CLI
dependencies with no benefit for end users
- Update Tauri Rust crates (tauri, tauri-plugin-*) to align with the npm
package versions and resolve the version mismatch warning at build time
---------
Co-authored-by: Pento <pento@fedora.fritz.box>
closes issue #296
Scroll fixes (StoryView.svelte)
- Auto-scroll during post-generation phase ("Updating world…") no longer
fires when Auto Scroll is disabled
- Scroll-to-bottom button now correctly appears/disappears based on
physical scroll position, not just userScrolledUp state
- Scroll-to-bottom button now appears during streaming when Auto Scroll
is off
- Story opens at bottom on load/restart — fixed timing issue where
performScroll ran against an empty container before async entries
arrived; the main scroll effect now handles the initial load
unconditionally
- Streaming→saved entry flicker eliminated: ui.endStreaming() is now
called after story.addEntry(), not before
- Key entries (user_action, narration, retry) are never hidden behind
the "N later entries hidden for performance" collapse indicator, even
when the user scrolled up during streaming — window is extended without
triggering a scroll-to-bottom side effect
- Scroll buttons now fade in/out with a 150ms transition instead of
instantly appearing
- untrack() on the active-panel scroll effect prevents it from re-firing
on every new entry
- RAF cleanup on component unmount
Story content width setting (interface.svelte, settings.svelte.ts,
StoryView.svelte)
- New Story Content Width slider in Settings → Interface, under Font
Size
- 6 steps: Narrow (42rem) → Default (48rem) → Wide → Wider → Very wide →
Extra wide (96rem)
- Persisted to DB; restored on app restart
- Single source of truth: STORY_WIDTH_OPTIONS in settings.svelte.ts
holds key, label, and CSS value — no duplication across files
Sidebar persistence (ui.svelte.ts, settings.svelte.ts)
- Sidebar open/closed state persisted across restarts
- setMobileDefaults() no longer persists to DB (it's a layout
constraint, not a user preference — previously it would overwrite the
desktop sidebar state)
Image height (StoryEntry.svelte)
- Inline and agent-mode generated images capped at 70vh with object-fit:
contain
- Removed hardcoded max-width: 28rem on agent-mode images (inconsistent
with configurable story width)
- Removed vertical margin from .inline-image-display
---------
Co-authored-by: Pento <pento@fedora.fritz.box>
Extracts the duplicated generation parameters form (API Profile, Model,
Temperature, Max Output Tokens, Thinking) into a shared
`GenerationParamsForm` component used by both Main Narrative and Agent
Profiles.
- New `GenerationParamsForm.svelte`: non-linear token slider (1K–32K, 16
index-based steps), expert numeric override (256–262144), temperature
step 0.05 with Focused↔Creative labels, reasoning capability derived
state, NanoGPT auto-reasoning logic
- Agent Profiles: removed `tempPreset` buffer — edits now write directly
to reactive state with 300ms debounced save; `onDestroy` +
`closeEditingPreset` flush prevent data loss on modal close
- `settings.svelte.ts`: new `get hasGenerationConfigIssues` reactive
getter (covers invalid profiles, missing Main Narrative model, missing
model in any agent preset) and `shouldForceHighReasoning()` helper
- `ProfileWarningBanner`: non-dismissible, context-aware CTA (API tab vs
Generation tab)
- Settings button shows a `⚠` badge when config is incomplete
- Both send buttons (desktop + mobile) disabled with tooltip when
generation is blocked
**No DB schema changes.** UI-only refactoring.
---------
Co-authored-by: Pento <pento@fedora.fritz.box>