mirror of
https://github.com/AventurasTeam/Aventuras.git
synced 2026-10-19 09:34:41 +00:00
1023 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
47d52a822b
|
Harden the HTTP plugin transport: body timeouts, no Origin, decompression, OS roots (#622)
## Summary Remote HTTP goes through Tauri's HTTP plugin, and the plugin's defaults fall short of what the app's callers need. A response body that stalled after the headers hung forever. Local servers (stock Ollama, ComfyUI) rejected the webview `Origin` the plugin attaches. Responses arrived uncompressed, TLS ignored the OS trust store, and the plugin kept a cookie jar nobody used. Vault portrait downloads bypassed the plugin altogether. This branch fixes each of these and routes every plugin call through one wrapper, `httpFetch`. ## Changes ### Timeouts and aborts - **Request guard** (`utils/idleTimeout.ts`): `createRequestGuard` combines the header timer, the caller's abort signal and a per-read idle timer on the body. `withIdleTimeout` re-wraps the body so a stalled read errors with `RequestTimeoutError`. The timer runs only while a read waits on upstream, so a slow consumer never trips it. `createTimeoutFetch`, `imageFetch` and `imageGetFetch` all use the guard in place of their own timer/abort code. - **Timeout errors**: `RequestTimeoutError` (`utils/error.ts`) is deliberately not named `TimeoutError`, which the AI SDK treats as an abort. `mapError` also turns the plugin's bare-string and plain-`Error` abort rejections into an `AbortError` when the caller aborted. `imageFetch` no longer reports a caller-caused `AbortError` as a timeout. - **LLM fetch**: error responses and non-JSON (streaming) responses get the idle timer. Buffered JSON responses don't; the header timer bounds them, since `dispose()` runs after `response.text()`. - **Fake provider**: new `stream-stall` model (some text, then silence with the connection open), documented in `docs/development/testing.md`. ### One entry point into the plugin - **`httpFetch`** (`utils/http.ts`) wraps the plugin's `fetch` and sets `Origin: ''` unless the caller sets one. The plugin drops an empty `Origin` under `unsafe-headers`, so local servers no longer see the webview origin. A caller's own `Origin` and `Referer` are sent as set. - All plugin callers now use it: `createTimeoutFetch`, the image fetch helpers, ComfyUI's `fetchApi`, `pingModel`, `corsFetch` and the updater. An ESLint `no-restricted-imports` rule rejects any other import of the plugin's `fetch` outside `http.ts` and tests. ### Plugin features (`src-tauri/Cargo.toml`) - `tauri-plugin-http` default features are off and its list is spelled out, so the `cookies` jar stays out: `rustls-tls`, `http2`, `charset`, `system-proxy`, plus the three below. The plugin's cookie jar is gone, and startup deletes the `.cookies` file an older build left in the app cache dir (`lib.rs`). - **`gzip`, `brotli`, `zstd`**: requests send `Accept-Encoding: gzip, br, zstd` and responses are decoded in Rust before the WebView sees them. - **`unsafe-headers`**: needed to drop `Origin`. It lifts the plugin's whole forbidden-header filter (`Host`, `Cookie`, `Content-Length`, `Sec-*`, …), so `httpFetch` is where to filter if untrusted input ever feeds request headers. Every caller today is app code. `docs/architecture/overview.md` records this. - **`rustls-tls-native-roots`** on Windows and Linux only: the OS trust store is added to the bundled roots. Android and iOS have no store `rustls-native-certs` can read, and on macOS every request would walk the keychain, so those keep the bundled roots. The target-specific entry also sets `default-features = false`, or Cargo would merge the defaults (cookies included) back in. ### Vault portraits - Importing a card with a `sourceUrl` downloads the portrait with `fetchPublicImageBase64` (public URLs and redirects only, 15s timeout) instead of a bare WebView `fetch`. The download runs alongside `sanitize`. The result is accepted only if `sniffImageMime` recognises the bytes; otherwise the import keeps the URL as the portrait, as before. - **`sniffImageMime`** (`utils/image.ts`) identifies PNG, APNG, JPEG, GIF, WebP, AVIF, HEIC and HEIF from the leading bytes. Unlike `base64ImageMime`, it returns `null` rather than defaulting to PNG. - `fetchPublicImageBase64` accepts `timeoutMs`. ### iOS - `NSLocalNetworkUsageDescription` now also names local AI servers, since the app connects to them over the LAN. ## Changes outside the main purpose - `@tauri-apps/plugin-http` 2.8.2 → 2.8.3, with the matching `tauri-plugin-http` crate. The lockfile update also moved `tauri-plugin-fs` 2.7.0 → 2.7.1. - In `fetch.ts` and `fetchAdapter.ts`, the debug-log bodies moved into `logErrorResponse`, `logStreamResponse` and `logImageResponse`, and `normalizeHeaders` / the FormData key listing were simplified. Behaviour is unchanged. ## Testing New unit tests cover `idleTimeout`, `httpFetch`, the `fetch.ts` and `fetchAdapter.ts` guard integration, and `sniffImageMime`. The Rust and platform changes (Cargo features, native roots, `Origin`, decompression, the iOS string) have no automated coverage and need a release build to check: - Run the fake provider with the `stream-stall` model and confirm the generation errors after the timeout instead of hanging. - On Windows and Android, point the app at stock Ollama on loopback and confirm requests succeed without `OLLAMA_ORIGINS`. - On Windows or Linux, reach an HTTPS server whose CA is only in the OS store. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_019V1AUE2StsGUjVV6KzUx2G <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added safeguards for stalled AI requests, with clearer timeout errors and support for cancelling requests. * Character imports can now fetch and validate portrait images, with a fallback to the original image URL. * Improved image format detection for PNG, JPEG, GIF, WebP, AVIF, HEIC, and HEIF files. * Updated the local network permission message to include connections to local AI servers. * **Bug Fixes** * Improved handling of request headers and response timeouts across AI, discovery, and update requests. * App startup now attempts to clear cached cookies. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com> |
||
|
|
8a04130e90
|
Keep a generation running in the background on iOS (#626)
Add a `generation` Tauri plugin: a BGContinuedProcessingTask keeps the app process alive and shows the generation in the Dynamic Island, with stage text, stage-based progress and a final outcome message. An inaudible <audio> loop keeps WebKit's content process, and so the pipeline's JS, running while the app is hidden. The keep-alive is reference-counted so translation and the post-turn tasks hold it too, and the completion notification is sent when it goes idle. Raise the iOS minimum to 26, show the background-generation setting on iOS, and document the design and findings in docs/development/ios-background-generation.md. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Background generation is now supported on iOS, with progress updates and handling for completed, failed, or interrupted tasks. * Background Generation settings appear on supported platforms, with information tailored to Android and iOS. * Generation can continue during background tasks. Notifications reflect completion or interruption when appropriate. If the operating system ends generation before narration is saved, the app may restore a matching backup or report an error. * **Changes** * The minimum iOS version for the app and downloadable IPA is now iOS 26. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com> |
||
|
|
6ef35f9f30
|
Report automatic chapter creation in a card in the story (#619)
## Summary When a turn pushes a story past its chapter threshold, the app analyses the span, writes a chapter summary and runs lore management. None of that was visible from the story. It started after the turn's activity record had closed, and the analysis and summary had no indicator at all. Lore management showed only on the Lorebook tab. Their failures went to the log and nowhere else, and a failed lore management run was never retried for that span. This PR reports that work in the story. A chapter creation card appears after the turn that cut the chapter and carries its own activity record. ## Changes **The turn's record covers its post-narration work** - Style review and chapter check now record under **After the turn**. The record stays open until they settle, but the input is freed as soon as the narration is in, as before. Neither step is recorded when it isn't due. - Until a card appears, the spinner under the latest entry says what's still running: "Checking for a new chapter...", "Reviewing the prose style...", or both. **Chapter creation card** (`ChapterCreationCard.svelte`, `services/chapterCreation/`, `stores/chapterCards.svelte.ts`) - **What it is:** virtual, never a story entry, held for the session only, and shown only on its own story and branch. It sits right after the narration of the turn whose check cut the chapter. It starts as soon as the check decides, without waiting for the style review. - **Status line:** left-aligned, with the record's total time beside it: "Writing a summary", "Updating the lorebook", then "Finished". - **Record:** the chapter summary, then lore management. - Each model call is named, from the moment it starts, for why the loop makes it: "Review the lorebook against Chapter 7", "Continue after reading Chapter 5", "Resolve 2 open duplicate groups", "Sum up: step limit reached". - Each tool call is a step beside the model calls. Chapter reads are LLM steps with their retry attempts beneath them. - **Saving changes** has one step per lorebook write. The agent only approves changes; they're written after it finishes, in a loop now moved into `saveLoreSession`. - **When it finishes:** with banners on, **Go to Chapter N** (lands on the banner) and **Dismiss**. With banners off, "Chapter creation is finished" and **Dismiss**. - **On failure:** the status reads "Writing a summary — failed" or "Updating the lorebook — failed", with only "failed" in red. - A failed summary offers **Retry** and **Dismiss**. Retry is offered only while the span is still the first one no chapter covers. - Failed lore management, or a run refused because another lore run (such as Tidy) holds the branch, offers **Restart lore management** and **Delete incomplete chapter**. Delete confirms first and says how many lore changes stay. - Both kinds of failure also raise a warning toast, since the reader may already be on the next turn or another branch. **Activity recorder** (`services/activity/`) - Holds several records open at once. A step goes to the record of its parent, to the record named by the new `recordId` option, or else to the newest open turn, which is where parentless generation steps already went. - Records come in two kinds: `turn`, bounded by `RETAINED_TURNS`, and `chapter`, which lives as long as its card. `reopenRecord` lets a retry or restart record into the same card, and returns a new object so views showing it re-read it. - The streaming entry now finds its record by its own entry id, not "newest open turn". **When a report opens** - A step that finished with an error it worked around, such as a salvaged classification, now opens the report like a failed step. - A failure that arrives after the reader expanded or collapsed a report opens it again. Each record counts its failures, and a stored choice applies only while that count is unchanged. **Branch safety and overlap** - The chapter number now comes from the chapters captured at the turn, not from whichever branch is open. `story.addChapter` takes the chapter's scope: it always writes the row, but adds the chapter to the loaded list only while that branch is open. - Lore management's input thunk refuses to start once the turn's branch is no longer open, instead of reasoning over another branch's lorebook. - A turn skips its chapter check while a card on that branch is still summarizing or running lore management, or while any lore management run holds the branch. Previously, two automatic checks could cut overlapping chapters. **Errors recorded instead of only logged** - The style review, chapter check, summary and lore management now record their failures with reasons. - The style reviewer now reports the failure it absorbs on its result. - `aiService.queryChapter` returns a chapter answer together with the failure it absorbed. The retrieval path is unchanged. **Docs:** `docs/architecture/overview.md` (activity reporting, chapter creation cards, background tasks) and `docs/architecture/lore-management.md` (when writes happen, the lock's skipped report, how a run reads in the record). ## Known gaps, left for later - **A failure is forgotten on restart.** The card is held in memory only, so after the app restarts, a chapter whose lore management failed has nothing pointing at it. - **No rollback for a partial save.** A lore run that fails while saving can't be rolled back: the writes before the failure stay. - **No retry attempts under agent model calls.** Agent model calls (retrieval and lore management) record no attempt rows, because the agent factory doesn't use the activity middleware. ## Testing - `npm run check`: 0 errors, 0 warnings. - `npm test`: 2524 passed, 9 skipped. New or extended tests cover: - the recorder's routing, retention, reopening and failure count - `ChapterService` check and create - the background coordinator's two stages, retry, and the card starting before the style review settles - the card state transitions and warnings - the lore coordinator's recorded step - `saveLoreSession` - the lore service's model-call and tool steps, and the step labels - the style review scheduler - activity parents reaching each model call - `npm run lint`: no errors (existing warnings only); rustfmt and clippy are clean. - **Not yet verified in the running app:** - the card's forms with banners on and off, and the three reporting settings - the failure flows (summary, lore, Tidy skip) with Retry, Restart and Delete - a branch switch mid-run - the toasts and the post-turn spinner - the report re-opening on a later failure 🤖 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 chapter-creation cards showing progress and failures, with options to retry summaries, restart lore management, or dismiss incomplete work. Completed cards can link to the chapter’s starting point. * Chapter creation can proceed while a style review is still running, with progress and outcomes shown in activity records. * Activity records now show progress and outcomes for chapter checks, summaries, style reviews, and lore management, including concurrent chapter work. * **Bug Fixes** * Prevented chapter work from applying to a story or branch that has changed, and avoided chapter checks or manual creation while related work is underway. * Limited chapter deletion to the last chapter and improved failure visibility in activity records. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
4c5ea04784
|
Keep a customized profile name when the provider changes (#628)
## Summary In the API connection settings, changing a profile's provider overwrote its name with the new provider's default name, discarding any name the user had typed. The name now follows the provider only while it is empty or still the previous provider's default; a customized name is left alone. ## Changes - `handleProviderTypeChange` in `api-connection.svelte` compares the trimmed form name against the outgoing provider's default name (and empty) before replacing it. The check runs before `formProviderType` is updated, so it reads the previous provider. - Removes the stale `// Fix #1` comment above the handler. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01JDs8mVYZvmUU8woSPik9oJ <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Switching providers now preserves custom profile names. The profile name changes to the new provider’s default only when it is blank or matches the previous provider’s default. <!-- end of auto-generated comment: release notes by coderabbit.ai --> Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com> |
||
|
|
a44a8cdb6f
|
Keep GPU rendering on NVIDIA Wayland outside the AppImage (#627)
Disable explicit sync instead of the DMABUF renderer, which forced CPU rendering on NVIDIA Wayland. The DMABUF fallback now applies only inside an AppImage, and variables already set in the environment are respected. - `src-tauri/src/main.rs`: env var handling - `docs/development/platform-support.md`: documents the behaviour 🤖 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 compatibility for Linux users running NVIDIA Wayland sessions by disabling explicit synchronization when no existing setting is present. * Limited the WebKit renderer workaround to AppImage launches and preserved any value already set by the user. <!-- end of auto-generated comment: release notes by coderabbit.ai --> Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com> |
||
|
|
5e4d802e5f
|
WIP, Part 1 of 2 - State Tracking: record every world-state write, and create checkpoints at past entries (#618)
## Summary Labs → State Tracking can now create a checkpoint at a past entry, not only at the latest one. To make that trustworthy, every world-state write is recorded while State Tracking is on, not only the classifier's. Revert on Delete and regenerate undo all of it, and the reader chooses whether their own and Tidy's changes are kept. Nothing changes while State Tracking is off: no rows, no flags, no new writes. The design is in [`docs/architecture/state-tracking.md`](docs/architecture/state-tracking.md), which is new. ## Changes **Recording** - Migration `040_world_state_changes.sql` adds a table of change, header and break records. They're keyed by entry, ordered within it, and removed with the entry. - Writers covered: - the classifier (`applyClassificationResult` diffs the state, so it no longer writes `world_state_delta`); - lore management, both after a chapter and Tidy; - the Duplicates merge; - lorebook file imports; - manual add, edit and delete in the character, location, item, quest and lorebook panels; - generated portraits. - Every record carries its origin: `classifier`, `image`, `chapter`, `tidy` or `manual`. - Chapter lore attaches to the chapter's last entry. Tidy attaches to the last completed entry when it started. - Every classified narration gets a header record, even when nothing changed. The header says whether tracking ran without a break since the previous one, checked against `trackingEnabledSince`. - Turning State Tracking off takes a closing snapshot of each branch written to since it was turned on. - `world_state_delta` stays and is still read for entries that have it and no new records. **Reversal** - A pure module, `services/stateTracking/`, plans a reversal over an in-memory state. - Revert on Delete runs that plan in one transaction, so a revert lands whole or not at all. Deleted rows come back under their original ids. - After a revert, the clock is the end time of the last remaining entry. **Past checkpoints** - A past entry's state is rebuilt from the nearest full state after it: a snapshot, a checkpoint on the branch line, or the live state. The records in between are undone, and the live story is never touched. - The rebuild is refused when that stretch isn't fully tracked. The form then lists every gap by entry number. - An Advanced fold can overrule a refusal. The result is prefilled as an "Approximate — " checkpoint. **Delete and regenerate** - Deleting an entry is confirmed in a blocking modal. It names the range being deleted and lists your own and Tidy's changes, each group with its own keep box. A switch deletes only that entry, without a rollback. - The modal and the checkpoint form say whether lore management after a chapter is undone, naming the chapters and their entries. - With State Tracking on, regenerate rewinds through the records the way Delete does, and the retry backup no longer copies the world. With it off, the in-memory copy also holds the branch's own lorebook rows. - Either way, a chapter that ends on a removed entry is now dropped instead of failing the restore with "Unknown restore error". The real database error is now shown for other restore failures. - A regenerate that would undo chapter lore or your own or Tidy's changes asks first in the same modal. **UI and data flow** - Checkpoints and branches are created in modals. The branch one is shared by the entry and the Branches panel. The phone drawers close by swiping down. - Inherited entries get a lineage marker and no Edit or Delete. - The landmark filter can show snapshots, with "Create checkpoint here" on each. - Export, import and sync carry the new records with ids remapped. An export appends a break wherever this device can't vouch that tracking ran unbroken. ## Not in this PR - A switch for a chapter created by hand to skip reading the story after it, and making manual chapter creation wait for a running lore session. - Export and import don't carry automatic snapshots, so an imported story's rebuilds start farther away. This is documented as a known limitation. - Deleting a chapter by hand isn't recorded, so a revert across it leaves the chapter as it is. - Retiring the `world_state_delta` column and converting old data is a follow-up. - Portraits aren't recorded, so the image data stays out of exports, syncs and memory. A revert across a tombstone keeps the portrait, since the stored row is untouched. With Lightweight Branches off, a delete removes the row, and a revert across it brings the character back without its portrait. ## Testing - `npm run check`: 0 errors, 0 warnings. - `npm test`: 2580 passed, 9 skipped. This includes unit tests for the reversal engine, run resolution, recording and the messages. Source checks confirm that every store write method records, and that the past-checkpoint paths never write the live story. - `npm run lint:ts`: 0 errors. There are 234 warnings, the same as on `master`. - The pre-push hooks passed. - Checked in the running app on desktop and Android: - the delete modal and its keep choices; - past checkpoints with their refusal checklist and the overrule; - the snapshot rows in the navigation panel; - the lineage marker. - **Not yet checked in the running app:** - the regenerate rewind and its confirmation; - the chapter lore line; - reverting across a chapter's end; - the clock after a revert; - skipped-snapshot warnings; - a SillyTavern import with State Tracking on. 🤖 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 State Tracking for world and lorebook changes, with snapshots, history-aware reversions, and options to preserve manual or Tidy changes. * Create checkpoints from eligible earlier narration entries, preview whether past state can be rebuilt, and find snapshots in story navigation. * Show snapshots as navigation landmarks when State Tracking is enabled and the option is turned on. * Story exports and syncs can include State Tracking records, which are restored when importing supported exports. * Branch creation now uses a modal that shows the checkpoint the new branch will use. * Preview changes before deleting or regenerating entries, with options to keep selected manual or Tidy changes. * **Bug Fixes** * Lorebook import completion now waits for imported entries to be saved. * Turning off State Tracking saves closing snapshots first; if saving fails, you can choose whether to proceed. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
d498f4ab8e
|
Let the vault assistant see pending and unsaved changes, and edit images (#623)
## Summary
The vault assistant read the saved vault only. A follow-up to its own
unapproved proposal, or to something the user had typed into the editor
and not saved, was answered against stale data. Its tools and the Active
Context record now see the vault with pending proposals and unsaved
view-mode edits laid over the saved state.
The branch also adds an `edit_image` tool (Reference / Img2Img profile)
and makes the image tools report failures instead of claiming success.
## Updating your prompt pack
For people who have edited **Interactive Lorebook**, or who use a custom
or imported pack. Built-in templates you haven't touched update on their
own. None of these steps break anything if skipped; the assistant just
keeps the old wording.
### 1. System: recommended
Without it the assistant isn't told `edit_image` exists, and is still
told to assume generation succeeded even though failures are now
reported to it. In the toolset table, in the **images** row:
Find:
```
Generate character portraits (from visual descriptors) and general images. Set generated images as character portraits. Always assume generation succeeded; never retry unless asked.
```
Replace:
```
Generate character portraits (from visual descriptors) and general images, or edit an existing image (a generated one or a character's current portrait) through the Reference profile. Set generated images as character portraits. A success result means the user can see the image; on failure, tell the user and don't retry unless asked.
```
### 2. User: recommended
Without it the assistant isn't told that the record it receives already
includes its pending proposals and the user's unsaved edits, or what
`unsavedFields` means.
Find:
```
Current state of the entity from the Active Context (sent again whenever it changes
```
Replace:
```
Current state of the entity from the Active Context, as the editor shows it: changes you proposed that are not approved yet and edits the user has not saved are already applied, and an `unsavedFields` key lists the fields that differ from the saved version (sent again whenever it changes
```
### 3. User: optional
Only affects lorebook records. Tells the assistant that `entryCount`
leaves out proposed new entries.
Find:
```
`read_entry` for those{% else %}
```
Replace:
```
`read_entry` for those, and `entryCount` leaves out proposed new entries, which are listed under `pendingCreates`{% else %}
```
New variables: none.
## Changes
### Effective vault state
- `effectiveVault.ts` (new) lays pending proposals, then unsaved
view-mode edits, over the saved characters, scenarios and lorebooks. It
also reports which fields of an entity are unsaved and what pending
changes do to a lorebook's entries.
- `InteractiveVaultService` builds its tools and the focused-entity
record from that state. Tool results carry `unsavedFields`;
`list_entries`, `read_entry`, `list_lorebooks` and
`read_lorebook_summary` flag entries with a `pending` action, and
proposed new entries appear under `pendingCreates` (`entryCount` counts
the entries the lorebook has now, not proposed ones).
- `VaultState` takes `pendingChanges` and `viewDraft` in place of
`activeEntries`; lorebook entry lookups go through the effective
lorebooks.
- `VaultEntityEditPanel` publishes unsaved view-mode edits as
`vaultEditor.viewDraft`. A view-mode Save now approves the pending
proposals its form already held (`vaultEditor.markSaved`), so they don't
stay pending after their values were written.
- `vaultMerge.ts` gains `entityUpdates` and `withEntryUpdate`, shared by
the editor store, the service and `effectiveVault`. Its helpers are
typed per entity kind (`EntityRecord`, `EntityKind`) instead of
`Record<string, unknown>`, and `composePendingOnto` returns a shallow
copy rather than a JSON round trip.
- `docs/architecture/prompts.md` describes the behaviour.
### `edit_image` tool
- Edits an image given as `imageId` (generated this session) or
`characterId` (that character's current portrait, including a pending or
unsaved one) with an `instruction`, through the Reference profile. The
result is a new image; `set_portrait` accepts its id.
- Fails with an explanation when no Reference profile is configured, its
credentials are missing, or the provider/model can't take a reference
image (`supportsReferenceImages`, new in the image registry along with
the now-exported `toRawBase64`).
- Added to the `images` tool category and to the assistant's
inline-image rendering.
### Changes to existing behaviour
- `generate_standard_image` returned `success: true` when image
generation was disabled, credentials were missing, or the provider
threw. It now returns `success: false` with the reason. Its tool
description says a success result means the user can see the image.
- `generate_portrait` checks portrait credentials up front, and always
generates; previously it returned success without an image when the
character had no visual descriptors.
- Approving an update now resolves the earlier pending updates it
overrides. A later proposal is built on the state the earlier one left,
so approving it alone used to leave the earlier one pending, and that
one would lay its older value back over the approved one. Fields the
approval replaces are dropped from earlier updates to the same entity or
entry, and an update with nothing left is marked approved. Array fields
(traits, tags, NPCs) keep merging by intent and are untouched.
- Character and scenario update changes now require `previous` in
`vaultPendingChangeSchema` (it was optional), so the guards that skipped
updates without it are gone.
- `VaultEntityEditPanel` clears its dirty flag when the viewed or edited
target changes, so edits typed for one entity no longer stop the form
from syncing to the next.
- Opening the assistant from an entity editor hands the viewer a typed
placeholder change instead of a cast clone of the whole entity: a
`create` carrying the record for a character or scenario, and an
`update` with the name, description and tags for a lorebook.
- The "images" row of the Interactive Lorebook template no longer tells
the assistant to assume generation succeeded (see the prompt pack steps
above).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01PKqQfDSjJaeQE9gU9k2tpa
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* The AI can account for pending changes and unsaved edits when
discussing vault characters, scenarios, and lorebooks, including
proposed lorebook entries.
* Added image editing for generated images and saved character portraits
using a configured Reference profile. Edited images are saved as new
images, leaving the originals unchanged.
* **Bug Fixes**
* Image-generation failures are now reported accurately instead of being
treated as successful generations.
* Lorebook entry updates now preserve valid field values consistently.
* Saving view-mode edits now updates the change status and queues a
conversation save.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
d7f58f7a03
|
Type-check vitest.config.ts and note TypeScript 6's strict default (#616)
## Summary Follow-up to #615. `vitest.config.ts` wasn't in any tsconfig project, so editors checked it with default options and reported `Cannot find module 'vitest/config'`. `tsconfig.json` now includes it. A one-line comment also records that `strict` is on because TypeScript 6 enables it by default, which is why the file doesn't set it. ## Changes - **`tsconfig.json`**: adds `vitest.config.ts` to `include`, so `check` now type-checks it too. Adds a comment noting that `strict` comes from TypeScript 6's default. ## Testing - `npm run check`: 0 errors, 0 warnings. `vitest.config.ts` is in the project, and `vitest/config` resolves. - `npm test`: 2451 passed, 9 skipped. - `npm run lint:fix`: no remaining errors. - Strict mode confirmed with a throwaway probe file, deleted afterwards. `tsc -p tsconfig.json` reported TS7006 (implicit `any`) and TS2322 (`null` assigned to `string`). 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01VrXVKz8zBJAvnxa6SKhJtB <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Chores** * Updated development configuration to keep project checks aligned with the current TypeScript defaults. * No changes to app features or behavior are included in this release. <!-- end of auto-generated comment: release notes by coderabbit.ai --> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
507f3b360e
|
Upgrade to SvelteKit 3, TypeScript 6 and vitest 5, and bump dependencies (#615)
## Summary
This PR brings every dependency up to date for October 2026. It includes
these major upgrades:
- **SvelteKit 3** and **adapter-static 4**
- **TypeScript 6**
- **vitest 5**
- **@ai-sdk/xai 5**
- **gpt-tokenizer 4**
All other dependencies move to the latest release within their current
major. SvelteKit 3 no longer generates the `$lib` alias, so every
`src/lib` import is now a `#lib/…` subpath import. That rewrite accounts
for most of the 530 changed files.
## Changes
### SvelteKit 3
- **`svelte.config.js` is removed.** Kit 3 refuses to start while it
exists. Its `preprocess` and `adapter` options now go in the
`sveltekit()` call in `vite.config.js`.
- **`version.pollInterval: 0`.** Otherwise Kit 3 polls
`_app/version.json` every hour. App updates ship through the Tauri
updater.
- **`tsconfig.json`** extends the generated `$app/tsconfig` and drops
the options that config already sets. It keeps `sourceMap` and `strict`,
and includes `src` and `vite.config.js`.
- **`runed` override.** `runed` (a dependency of bits-ui) declares an
optional peer on `@sveltejs/kit ^2.21.0`, and npm still enforces that
range. An override points it at the root kit. Only runed's separate
`runed/kit` entry uses Kit, and bits-ui never imports that entry.
### `$lib` → `#lib`
- `package.json` declares `"imports": { "#lib/*": "./src/lib/*" }`.
Vite, TypeScript and vitest all resolve it natively, so the hand-rolled
`$lib` alias in `vitest.config.ts` is gone.
- Every specifier includes its real extension (`.ts`, `.svelte.ts`,
`/index.ts`) because subpath imports don't resolve without one.
`$app/tsconfig` enables `allowImportingTsExtensions`, so `.ts` stays
`.ts` and is not rewritten to `.js`.
- The rewrite is mechanical. It starts from `sv migrate sveltekit-3
--tasks lib-alias`, then adds the extensions. Prettier reflowed the
imports that the longer paths pushed over the line limit.
### TypeScript 6
- `@saintno/comfyui-sdk` 0.3.1 is its latest release, and it declares a
required peer on `typescript ^5.5.3`. An override points that peer at
the root `typescript`.
### @ai-sdk/xai 5
- v5 removes the Chat Completions API. The provider's default model
already used the Responses API, so requests are unchanged.
- The parallel-tool-calls preset in `presetResolution.ts` now uses
`XaiLanguageModelResponsesOptions` and its `parallelToolCalls` key.
`XaiProviderOptions` was an alias for the Chat options, and the
Responses model never read their `parallel_function_calling` key.
### gpt-tokenizer 4 and vitest 5
- Neither needs code changes. gpt-tokenizer 4 only drops its UMD
bundles, and the ESM entry still exports the `o200k_base` `countTokens`
and `encode`. The test suite passes under vitest 5, which clears mocks
by default and applies stricter async assertions.
### Minor-version bumps
- **@lucide/svelte 1.55** makes the base `Icon` component require
`iconNode`. The icon maps in `IconPicker`, `RuntimeVariableCard`,
`RuntimeVariableDisplay` and `RuntimeVariableEditor` now use the
exported `LucideIcon` type.
- **`command-empty.svelte`** now forwards its bindable `ref`, like its
sibling command components.
- **`@types/node`** is pinned to `^24` to match the Node 24 runtime.
- The `lefthook` entry in `allowScripts` is updated to the new version.
- `@tauri-apps/*` packages stay on their current versions.
### Removed
- **`tw-animate-css`**: its CSS import no longer exists, so its
`animate-in` classes did nothing.
- **`picomatch`**: no code imports it, and vite, tinyglobby and vitest
each depend on their own version.
### Docs
- `CLAUDE.md` and `README.md` now say SvelteKit 3. `CLAUDE.md` adds the
`#lib` import rule.
- `docs/development/platform-support.md` now points at `$app/tsconfig`,
and `docs/development/testing.md` shows the `#lib` mock path.
## Testing
- `npm run check`: 0 errors, 0 warnings.
- `npm test`: 2451 passed, 9 skipped.
- `npm run lint:ts`: no errors.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01VrXVKz8zBJAvnxa6SKhJtB
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
|
||
|
|
82f536bae9
|
Tighten the Gradle and npm cache setup from #613's review (#614)
## Summary Follow-ups from a review of #613, which merged before they landed. None of them affects correctness. Gradle's cache key no longer depends on untracked files, Gradle's read-only rule is now explicit, the npm action respects a caller's cache dir, and the docs and comments are brought up to date. ## Changes - **`build-android.yml`**: - Setup Java and Setup Gradle now run before `npm-install`. The basic provider hashes `**/*.gradle*` across the workspace, so it used to walk `node_modules/` and `.npm-cache/`. It now sees only tracked files. The key should be unchanged, since neither folder holds a matching file. - `cache-read-only: ${{ github.ref != 'refs/heads/master' }}` replaces setup-gradle's hidden default-branch rule. It's the same rule npm-install and rust-cache use. - The comment states rules instead of explaining what's absent. - **`.github/actions/npm-install`**: on Linux, a caller-set `npm_config_cache` now wins, as it already did on other OSes. The rationale comment is cut to one line pointing at the docs. - **`eslint.config.js`**: ignores `.npm-cache/`. Flat config skips neither `.gitignore` entries nor dot-folders, so `eslint .` in CI walked the whole npm cache. - **`build-desktop.yml`**: the zstd comment now says zstd also keeps the container's npm entry matching the host's, not only compression speed. - **`docs/development/ci-caching.md`**: - The expected footprint now includes Gradle (843 MB) and npm (~3 × 140 MB), about 6.7 GB in total. It also covers the brief overflow when a dependency update and a `*.gradle*` change land together: the basic provider has no restore-keys or cleanup. - The Gradle intro is in the present tense. - Explains why an absolute path like `/tmp/npm-cache` doesn't work: `actions/cache` stores entries relative to `GITHUB_WORKSPACE`, and the container and host workspaces sit at different depths. ## Testing - `aube test` passes. Prettier and ESLint are clean on the changed files, and ESLint reports a probe file in `.npm-cache/` as ignored. - Local `lint:ts` and `check` fail on files this PR doesn't touch (two `no-unused-vars` errors and `@codemirror` duplicate types). The cause is a local install difference: #613's CI `lint-and-typecheck`, run on the same TS and Svelte code, passed. This PR's CI is the check. - After merge, the Android job's Setup Gradle log should show `cache is read-only` on PR refs, and the same `setup-java-Linux-X64-gradle-…` key as before on `master`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01FsdTn4ZMvVxxETgpvb1Li3 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Chores** * Updated build and dependency caching across Android and Linux workflows, including respecting custom npm cache settings. * Adjusted Android build setup and cache access based on the branch being built. * No changes to app features or behavior are included in this update. <!-- end of auto-generated comment: release notes by coderabbit.ai --> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
75609aa1d1
|
Cache Gradle again and share one npm cache entry on Linux (#613)
## Summary #612 turned off the Gradle cache to save quota. The Android job is already the longest in every CI run, and measured runs show that without the cache it takes about **1m45s longer**. This PR turns the cache back on. The Rust dependency hash from #612 freed most of the quota needed for its 843 MB entry, and this PR frees another ~140 MB by deduplicating the Linux npm cache. ## Changes - **`build-android.yml`**: `setup-gradle` drops `cache-disabled: true`. It keeps `cache-provider: basic` (MIT, read-only off `master`). - **`.github/actions/npm-install`**: on Linux, the action sets `npm_config_cache` to `.npm-cache` inside the checkout (gitignored) and caches that literal path. Windows keeps its Dev Drive path, and macOS is unchanged. - Desktop Linux runs in an `ubuntu:22.04` container (`HOME=/github/home`) and Android runs on the host. With `~/.npm`, the two jobs had the same `npm-Linux-X64-…` key but saved two identical ~140 MB entries. `actions/cache` versions an entry by its literal `path`. - Matching the literal path alone isn't enough. `actions/cache` stores tar entries relative to `GITHUB_WORKSPACE`, so restoring a container `~/.npm` archive on the host would extract to `/home/runner/github/home/.npm`. That still counts as a hit, so the save is skipped. `.npm-cache` is the same directory in both environments and sits at the same position relative to the workspace. It can't be a sibling `../.npm-cache`: `actions/cache/save` rejects `..` in a path and only logs a warning. - **`docs/development/ci-caching.md`**: updates the Decisions table, adds the measured Gradle timings, and adds a new `## npm` section. **`release.md`** is updated to match. ### Gradle timings (Android job) | Phase | Uncached (38192218726 / 38197882367) | Cached (38111960581 / 38071840978) | |---|---|---| | `setup-gradle` restore | 1–2.5 s | 12–17 s | | Gradle startup to first Rust handoff | 92 s / 96 s | ~24 s | | Packaging after last Rust target | ~120 s | ~79 s | | `:app:lintUniversalDebug` | 49 s / 46 s | 37 s / 36 s | ### Effect - The first `master` CI run after this merges is cold for Gradle and for Linux npm. Each saves one entry: 843 MB for Gradle, and ~140 MB for npm in place of the two current Linux entries. - The two old `npm-Linux-X64-…` entries can be deleted by hand or left to eviction. ## Testing - `npm run check`, `npm test` and `npm run lint:fix` pass. This PR only changes YAML and docs. - CI caching can only be verified after merge, because PR runs don't save caches. After the merge, two dispatched `ci.yml` runs on `master` should show: - one `npm-Linux-X64` entry; - `Cache restored from key: npm-Linux-X64-…` in both the desktop Linux and Android logs; - no `Downloading https://services.gradle.org` line on Android in the second run. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01FsdTn4ZMvVxxETgpvb1Li3 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Chores** * Updated Linux dependency caching so containerized desktop and Android builds can share the npm cache. * Re-enabled Gradle caching for Android builds, with read-only caching on non-default branches. These changes affect build workflows and do not add or change app features. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
84db0be979
|
Key Rust caches on the dependency set and stop caching Gradle (#612)
## Summary The repository's Actions caches reached 9.57 GB of GitHub's 10 GB quota, and the Rust caches had roughly doubled in ten days. When `Cargo.lock` changes, rust-cache restores the previous entry as a fallback, cargo builds the new dependency versions beside the old ones, and the save keeps both, because its cleanup matches artifacts by crate name only. Every dependency update stacked another generation onto every platform's cache. This change puts a hash of the Rust dependency set into each rust-cache `key`. The key is part of rust-cache's restore prefix, so a dependency change has nothing to fall back to: it builds cold once per platform, and each saved entry holds a single generation. It also stops caching Gradle, whose 843 MB entry only saved dependency downloads. ## Changes - **`scripts/ci/cargo-deps-hash.js`** prints the first 16 hex characters of a SHA-256 over `Cargo.lock`'s registry and git packages (dropping the `aventura` workspace crate) and `src-tauri/Cargo.toml` without its `[package]` version line. A release version bump leaves it unchanged; a dependency version, feature or profile change does not. - **`.github/actions/rust-cache`** (new composite action) runs the script and calls `swatinem/rust-cache@v2` with `key: <key>-<hash>`, `workspaces: '<working-directory>/src-tauri -> target'` and `save-if` on `master` only. If the script fails, the step fails rather than saving under a key without the hash. Inputs: `key`, `working-directory` (default `.`) and `cache-directories`. - **Every rust-cache step uses the action:** `build-desktop.yml` (`<target>-<hash>`, with the Dev Drive path as `working-directory`), `build-android.yml` (`android-v2-<hash>`, still persisting the generated Kotlin), `build-ios.yml` (`ios-<hash>`) and `rust-lint.yml`'s Clippy job (`clippy-<hash>`). `rust-lint.yml` also runs when the script or the action changes. - **Gradle is not cached.** `setup-gradle` keeps `cache-provider: basic` and adds `cache-disabled: true`, so it still validates the wrapper and writes the job summary. - **`docs/development/ci-caching.md`** (new) records what each cache holds and why it is configured that way: the diagnostic runs, how rust-cache keys, restores and prunes, the options weighed against the dependency hash, the Gradle provider comparison, and the decision to keep `armv7`. `release.md` keeps the rules and links there, and its generated-Kotlin explanation moves there. `ci.yml`'s comments, `ios-build-target.md` and `docs/README.md` are updated to match. ### Effect - The first `master` run after this merges, and the first after any later dependency, feature or profile change, builds every Rust cache cold. A PR that makes such a change also builds cold. - Release runs keep hitting: the version bump changes neither this hash nor rust-cache's own lock hash. - The existing, oversized entries stay until GitHub evicts them as least recently used. Deleting them by hand (`gh cache list --key v0-rust-`, then `gh cache delete <id>`) frees the space right away. - Android runs download the Gradle distribution and dependencies each time. ## Testing - New unit tests for `cargoDepsHash`: unchanged by an app version bump and by CRLF line endings, changed by a registry package version and by a dependency feature. - `node scripts/ci/cargo-deps-hash.js` on this branch prints `8cdb41fe24f6ce46`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01AhkosChqt73QngC3UqQB9C <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Chores** * Android, desktop, iOS, and Rust lint workflows now use shared Rust caching that reflects dependency changes. * Android builds no longer cache Gradle data. * CI workflow descriptions now reflect the updated cache behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
b7bd906fd6
|
Update Tauri packages and Rust dependencies (#610)
## Summary Updates the Tauri JS packages and Rust crates to their latest 2.x releases, moves `zip` to 9, documents how the HTTP plugin's network transport differs from the WebView's `fetch`, and stops PR runs of Clippy from saving their own Rust cache. ## Changes **JS (`package.json`, `package-lock.json`)** | Package | From | To | | --- | --- | --- | | `@tauri-apps/api` | 2.11.1 | 2.12.3 | | `@tauri-apps/cli` | 2.11.4 | 2.12.2 | | `@tauri-apps/plugin-dialog` | 2.7.2 | 2.8.2 | | `@tauri-apps/plugin-fs` | 2.5.1 | 2.7.0 | | `@tauri-apps/plugin-http` | 2.5.9 | 2.8.2 | | `@tauri-apps/plugin-notification` | 2.3.3 | 2.6.0 | | `@tauri-apps/plugin-opener` | 2.5.4 | 2.7.1 | | `@tauri-apps/plugin-process` | 2.3.1 | 2.4.0 | | `@tauri-apps/plugin-sql` | 2.4.0 | 2.5.0 | | `@tauri-apps/plugin-updater` | 2.10.1 | 2.13.3 | **Rust (`src-tauri/Cargo.toml`, `src-tauri/Cargo.lock`)** - `Cargo.lock` is refreshed, bringing the matching Rust plugin crates to the same versions as the JS packages above, and `tauri` to 2.12.3. - `zip` 8 → 9 (still `default-features = false`, `deflate` only). - tokio's `macros` feature moves from the main dependency to `[dev-dependencies]`, so release builds no longer compile it. **Docs (`docs/architecture/overview.md`)** - New **Network transport** section: remote HTTP goes through `@tauri-apps/plugin-http` and which helper to use for each case, and what the plugin changes for the server on the other end (an `Origin` header that can't be dropped, webpki roots instead of the OS trust store, no response decompression). - Records a known, unverified limitation: stock Ollama doesn't allow the `http://tauri.localhost` origin, so Windows and Android requests may need `OLLAMA_ORIGINS=http://tauri.localhost`. **CI (`.github/workflows/rust-lint.yml`, `docs/development/release.md`)** - Rust Lint also runs on pushes to `master` that touch Rust sources or the Cargo manifest or lockfile. - The Clippy job's `rust-cache` step saves only on `master` (`save-if`), the same rule the build workflows use. PRs restore the master-scoped cache instead of saving a ~600 MiB entry that no other run can read and that pushes master's build and npm caches toward eviction. - `release.md` describes the new trigger and the cache rule. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01Mp6GGHh2aY397TuZXyUXKX <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Chores** * Updated the app’s underlying components for dialogs, file access, networking, notifications, database access, and software updates, along with related tooling. * Updated supporting components for asynchronous tasks and archive handling. * Improved Rust lint workflow triggers and cache handling. * No user-facing feature or behavior changes are noted. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com> |
||
|
|
8552b87b94
|
Keep Agent Profile task assignments across a reset and a restart (#609)
## Summary When tasks are spread across custom Agent Profiles, resetting Agent Profiles to defaults can lose them. The reset looks right, but after a restart most tasks are missing from every profile card, and running them fails with "Agent Profile not found". Resetting again doesn't help: the loss comes back on every restart. A reset with every task already on a default profile is unaffected, for example after changing only the profiles' models. ## Cause The reset saved the task-to-profile map once per task: 30 un-awaited `setServicePresetId` calls, each writing the whole map as it stood at that moment. Each write is a separate IPC call into the SQL plugin, and nothing guarantees they commit in order. If tasks were on their default profiles before the reset, every partial snapshot equals the defaults and the race leaves no trace. If they were on custom profiles, the reset deletes those profiles, and a stale snapshot landing last leaves tasks pointing at them. Those tasks then fall into a group no profile card renders, so they disappear rather than showing as Unassigned. Each later reset could save the previous broken map again, because the early snapshots carry it. ## Changes - **Reset saves once.** `handleResetProfiles` now awaits the store's existing `resetServicePresetAssignments()`, which writes the full default map in one call. The per-task loop is gone. - **Assignments to a missing profile resolve to Unassigned.** A new pure `clearDanglingAssignments` in `settingsMigrations.ts` runs in `settings.init()` after the assignments load. A task whose profile no longer exists shows under Unassigned, where it can be reassigned. Running it reports "not assigned to an Agent Profile" instead of "Agent Profile not found". This also repairs a database already in the broken state. The cleanup isn't written back at startup; the next assignment change saves it. - `docs/architecture/ai-services.md`: one line on the load-time behaviour. Not in scope: queuing `setSetting` writes per key in the database layer. Other settings saves have the same ordering exposure in principle, but each saves one complete snapshot per action, so none produces a partial map like this. ## Testing - New unit tests for `clearDanglingAssignments`: missing ids cleared, valid ids and Unassigned kept, idempotent, input not mutated. - `npm run check`, `npm test` and `npm run lint` pass. - In the running app: - Started on a database already in the broken state: the missing tasks appeared under Unassigned. - Put tasks on a custom profile, reset, restarted: all 30 tasks were back on their default profiles. - Ran a story turn using suggestions and translation: no "not found" error. 🤖 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** * Removed service preset assignments that reference presets no longer available, while preserving valid and empty assignments. * Resetting profiles now clears service preset assignments consistently, with errors surfaced if the reset fails. * Saving service preset assignments in quick succession no longer risks overwriting newer changes. * **Improvements** * Updating built-in presets to match provider defaults now preserves custom presets. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
20b35b5e79
|
Run Clippy on push and in CI, and split lint into lint:ts and lint:rust (#608)
## Summary
The Rust crate is format-checked but never linted. This fixes the
crate's existing Clippy warnings and adds `cargo clippy -D warnings` to
the pre-push hook and to CI. To keep Rust and TypeScript linting
independent, `lint` is split into `lint:ts` and `lint:rust`, and hooks,
CI and the release script call the half they need.
## Changes
- **Clippy fixes.** In `backup.rs`, two nested `if let … { if … }`
blocks become let-chains (`collapsible_if`). In `migration_patch.rs`,
`apply_checksum_patch` takes `&Path` instead of `&PathBuf` (`ptr_arg`).
No behaviour change.
- **Scripts.** `package.json` adds `lint:ts` / `lint:ts:fix` (ESLint)
and `lint:rust` / `lint:rust:fix` (`cargo fmt` plus `cargo +1.99.0
clippy --all-targets --all-features -D warnings`). `lint` and `lint:fix`
run both halves through `scripts/pm.js`.
- **Pre-push hook.** `lefthook.yml` runs `lint:ts` and `lint:rust`
through `scripts/pm.js`, under the same globs as before.
- **CI.** `rust-lint.yml` gains a `clippy` job on Rust 1.99.0 with the
Linux WebKit/GTK build dependencies and a Rust cache.
`lint-and-typecheck.yml` runs `lint:ts`.
- **Release script.** `scripts/release.js` runs `lint:ts:fix`, so a
release never needs the Rust toolchain or rewrites Rust code.
- **Docs.** `CLAUDE.md`, `README.md` and `docs/development/release.md`
describe the split scripts, the hook commands, the Clippy job, and how
to bump the pinned toolchain.
### Changes to existing behaviour
- `npm run lint` and `npm run lint:fix` now also lint the Rust crate, so
they need `cargo` and the 1.99.0 toolchain with Clippy (`rustup
toolchain install 1.99.0 --component clippy`). `lint:ts` / `lint:ts:fix`
keep the old ESLint-only behaviour.
- The pre-push commands are renamed `lint` → `lint-ts` and `rust-fmt` →
`lint-rust`. Anything that skips or overrides them by name
(`LEFTHOOK_EXCLUDE`, `lefthook-local.yml`) needs the new names.
## Test plan
- [ ] `npm run lint:rust` passes
- [ ] The "Rust Lint" `Clippy` job runs on this PR and passes
- [ ] `npm run lint` runs ESLint, then rustfmt and Clippy
- [ ] Pushing a commit that touches only `.rs` files runs `lint-rust`
but not `lint-ts` or `check`
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01Mp6GGHh2aY397TuZXyUXKX
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Developer Tooling**
* TypeScript and Rust linting run separately, with Rust formatting and
Clippy checks included in the standard lint workflow.
* Rust lint checks also run in continuous integration, with warnings
treated as errors.
* Pre-push checks now run both TypeScript and Rust linting. Release
preparation runs TypeScript formatting and lint fixes.
* **Documentation**
* Updated build-script descriptions to reflect the TypeScript and Rust
lint commands and their fix options.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
48ee16fdc5
|
Branch tree: scroll sideways instead of truncating names, and keep the side panels apart (#602)
## Summary On a phone the Branches panel lives in a 288px drawer and each nesting level costs 16px of indent, so by the fourth level a branch name had about 16px left and was unreadable. Since the panel unfolds the path to the branch being read, a reader on a deep branch always opened it onto cut-off rows. Separately, the sidebar could open on top of an open navigation panel, by the right edge swipe or by the header's right-panel control. The other direction was already blocked. ## Changes **Branch tree** - The tree scrolls sideways inside the sidebar. Rows take their natural width and names are never truncated. The header, the create-branch form and the empty state stay put. The sidebar body itself no longer scrolls sideways. - Revealing the branch being read now also brings its row and name into view, sideways and vertically. It sets the scroll positions directly rather than calling `scrollIntoView`, which would also scroll the page and shift the app sideways while the sidebar is still sliding in. - A sideways swipe over the tree scrolls the tree while the tree could still scroll that way when the swipe began. At the tree's edge, or when the tree fits, the swipe changes tab or closes the sidebar as anywhere else. A swipe that carries the tree to its edge stops there. The tree does this by stopping the `touchend` with a native listener; `swipe.ts` is unchanged. **Side panels** - Both edge swipes now listen only while neither panel is open. - Where the panels overlay the story, the right-panel button and the Panels menu's "Right panel" close the navigation panel before opening the sidebar. Side by side on a wide screen, both stay open. `docs/architecture/overview.md`, "Edge swipes", describes both. It also gets the one missing blank line that made Prettier fail on that file. ## Test plan - [x] `npm run check`, `npm run lint`, `npm test` - [ ] Emulator, branch four levels deep: the name reads in full by scrolling the tree; the current-branch highlight spans the row; the header and create form do not move - [ ] Emulator: a deep branch's rename field and its confirm/cancel are reached by scrolling the tree - [ ] Emulator: opening on a deep branch lands on its row; switching to a shallow one scrolls back; scrolling away without switching stays put - [ ] Emulator: a swipe over a tree that can scroll that way scrolls it without switching tab; at the tree's edge it switches tab; over a tree that fits it switches tab; edge swipes still work - [ ] Emulator, navigation panel open: a right-edge swipe does not open the sidebar; Panels → Right panel closes the navigation panel and opens the sidebar - [ ] Desktop: no sideways scrollbar for a tree that fits, a scrollbar rather than a cut name for one that does not; with both panels docked, the right-panel button leaves the navigation panel open 🤖 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** * The branch view shows the current branch’s ancestors, scrolls the selected branch into view, and supports horizontal scrolling for wide or deeply nested trees. Branch names are no longer truncated. * **Bug Fixes** * Switching stories while viewing the same branch now expands its ancestors again. * Mobile sidebar and navigation interactions now avoid opening both panels at once. Left-edge swipes open the sidebar only when a story is active and both panels are closed; resizing while both panels are open closes navigation. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
26e5a433e8
|
Check Rust formatting on push and in CI (#607)
## Summary
The Rust crate is never format-checked, so `src-tauri/` has drifted from
`cargo fmt`. This formats it once and adds a `cargo fmt --check` gate to
the pre-push hook and to CI, so it stays formatted. Each check now only
runs when files it covers have changed.
## Changes
- **Format the crate.** `cargo fmt` over `src-tauri/`: import ordering
to the edition-2024 style, a trailing `;` after a `return` in a `match`
arm, wrapped long `format!` calls, and the `fixups` table in
`migration_patch.rs` expanded to one field per line. No behaviour
change.
- **Pre-push hook.** `lefthook.yml` gains a `rust-fmt` command running
`cargo fmt --manifest-path src-tauri/Cargo.toml --check`. The `lint`,
`check` and `rust-fmt` commands each have a `glob` and run only when the
pushed commits touch a matching file; once triggered they run over the
whole project. `glob_matcher: doublestar` is set so `**` patterns match
as written.
- **CI.** New `.github/workflows/rust-lint.yml` ("Rust Lint") runs the
same `cargo fmt --check` on pull requests to `master` that touch Rust
sources, `Cargo.toml`, `Cargo.lock`, or the workflow itself.
- **Docs.** `docs/development/release.md` describes the new hook
command, the globs, and the workflow.
### Changes to existing behaviour
- `lint-and-typecheck.yml` now skips pull requests that touch only
`src-tauri/`, `docs/` or Markdown, via `paths-ignore`.
- `lint-and-typecheck.yml` no longer lists `develop` and `dev` as target
branches; only `master` triggers it.
## Test plan
- [ ] `cargo fmt --manifest-path src-tauri/Cargo.toml --check` passes
- [ ] The "Rust Lint" check runs on this PR and passes
- [ ] Pushing a commit that touches only `.rs` files runs `rust-fmt` but
not `lint` or `check`
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01Mp6GGHh2aY397TuZXyUXKX
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* Improved migration checksum compatibility for databases affected by
line-ending differences, including an additional migration version.
* **Chores**
* Added automated Rust formatting checks for relevant pull requests and
manual runs.
* Updated pre-push checks to run when relevant files change, and
adjusted which changes trigger pull-request lint and type checks.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
1195d048f9
|
Move the Rust crate to edition 2024 (#605)
## Summary Moves the `aventura` crate from Rust edition 2021 to 2024, which is the Rust floor `CLAUDE.md` names for new code. ## Changes - `src-tauri/Cargo.toml`: `edition = "2024"`. - `src-tauri/src/main.rs`: edition 2024 makes `std::env::set_var` unsafe, so the NVIDIA/Wayland `WEBKIT_DISABLE_DMABUF_RENDERER` call (Linux only) is wrapped in an `unsafe` block. It runs first in `main`, before `aventura_lib::run()` starts any thread. No other source changes were needed. ## Verification [CI run](https://github.com/AventurasTeam/Aventuras/actions/runs/38110082406) on this branch passed for all six build jobs: Windows, Linux, macOS (Intel and ARM), Android and iOS. The Linux job compiles the `cfg(target_os = "linux")` block that changed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01Mp6GGHh2aY397TuZXyUXKX <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Chores** * Updated the app’s underlying build configuration. This does not change how the app works for users. * **Compatibility** * The existing Linux NVIDIA/Wayland workaround continues to behave as before; its detection and settings are unchanged. * **User-visible changes** * No new features or changes to the app’s interface or behavior are included in this update. <!-- end of auto-generated comment: release notes by coderabbit.ai --> Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com> |
||
|
|
ec8f40d253
|
Share one OpenAI-compatible image request across OpenAI, NanoGPT and Pollinations (#603)
## Summary
OpenAI, NanoGPT and Pollinations each built their own image request, and
each handled reference images differently: OpenAI and NanoGPT sent only
the first one, and Pollinations put a data URL in a GET query string.
This moves all three onto one OpenAI-compatible request and makes
reference-image handling consistent, so a provider or model only
receives references it accepts, and a reference given as a URL is
fetched only from a public host.
## Changes
- **Shared request** (`openaiCompatible.ts`): `POST
{baseUrl}/images/generations` as JSON, or `/images/edits` as multipart
when there are references (`image` for one, repeated `image[]` for
several). The MIME type and file extension come from the image's magic
bytes. A `url` result is downloaded to base64 without sending the API
key.
- **OpenAI, NanoGPT, Pollinations** call it and keep their own model
discovery and size handling. Pollinations now generates against
`gen.pollinations.ai/v1` instead of the GET endpoint.
- **Registry**: references go only to providers that send them
(`PROVIDER_READS_REFERENCES`), are cut to the model's new
`maxReferenceImages`, and are converted to raw base64 before any adapter
sees them: a `data:` prefix is stripped and an `http(s)` URL is
downloaded. A reference that cannot be fetched is dropped and the image
is generated without it. DALL·E models are limited to 1, NanoGPT models
without `supportsMultipleImg2Img` to 1.
- **Public-host fetch**: a reference URL is downloaded with
`fetchPublicImageBase64`, which refuses a URL that `isPublicHttpUrl`
(`utils/url.ts`) does not accept and checks each redirect the same way,
up to 3. `isPublicHttpUrl` rejects non-`http(s)` schemes, private,
loopback, link-local and other reserved IPv4/IPv6 literals (including
IPv4-mapped IPv6), single-label names, and `.localhost`, `.local`,
`.internal` and `.home.arpa`. It judges the name only, so a public name
that resolves to a private address is not caught. `imageGetFetch` gains
`followRedirects: false`, which returns a 3xx to the caller instead of
following it.
- **Google**: references are sent on `gemini-*` image models only. The
SDK sends any other id down the Imagen path, which rejects input images,
so those models now report `supportsImg2Img: false` and
`maxReferenceImages: 0`.
- **OpenRouter**: the data URL uses the detected MIME type instead of
always `image/png`.
- **`utils/image.ts`**: `base64ImageMime`, `base64ToBytes`, and
`bytesToBase64`, which encodes in chunks instead of one
`String.fromCharCode` call per byte.
- Documented in `docs/architecture/overview.md`.
### Changes outside the main purpose
- NanoGPT's image base URL default is now
`https://api.nano-gpt.com/api/v1`. The previous default,
`https://nano-gpt.com/api/v1`, is still the host copied onto image
profiles migrated from an API profile, so it is treated as unset and
mapped to the new default.
## Tests
New tests cover the shared request (`openaiCompatible.test.ts`), the
three adapters (`openaiBackedAdapters.test.ts`), the registry's
reference filtering, download and dropping of failed references
(`registry.test.ts`), the public-host fetch and its redirect handling
(`fetchAdapter.test.ts`), `isPublicHttpUrl` (`url.test.ts`), Google's
reference gating (`google.test.ts`), and the base64 helpers
(`image.test.ts`).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01Mp6GGHh2aY397TuZXyUXKX
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
- **New Features**
- Image generation supports reference images across compatible
providers, including images supplied by URL or data URL, subject to each
model’s image limit.
- Gemini image models can use reference images; models without
reference-image support continue to receive text-only prompts.
- **Bug Fixes**
- OpenAI-compatible providers handle image generation and edits
consistently, including multiple reference images.
- Reference images sent to OpenRouter retain their detected image
format.
- NanoGPT uses the current service address and handles legacy addresses.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
|
||
|
|
7d07627cc0
|
Landmark filter, first/last entry rows, and a landing that shows the action above (#598)
## Summary - **Landmark filter.** A filter heads the Navigation tab's landmark list, in the form of the Timeline's reference-point filter. It has three remembered options: first and last entry, chapter borders (chapter starts and "The Story Continues" together), and checkpoints (checkpoint rows and the branch's origin row). The icon stays amber while any option is off, including on hover; the Timeline's reference-point filter gets the same hover treatment. - **First entry / Last entry rows.** The list gains a row on the earliest entry of the branch as read and one on its last. A single entry gets only the first. Both stay on the current branch in every navigation mode. - **"The Story Continues" note.** That row carries an info icon explaining that it points at the first entry after the last chapter: shown after a short hover with a mouse, on tap on touch. - **Landing position.** A jump lifts the view so the entry above the landing shows with its card's top border: whole when within 30% of the viewport, clipped at that height when taller, with half the gap between cards as the margin above it so the card before stays out of view. ## Notes - Settings are `nav_show_first_last`, `nav_show_chapters` and `nav_show_checkpoints`, read at start-up next to `gallery_newest_first`. No schema, export or sync changes. - The "Orphaned" section is not filtered; it is where orphaned checkpoints are cleared out. - `docs/architecture/overview.md` describes the rows, the options and the landing rule. ## Test plan - [x] `npm run check`, `npm test` for `storyNavigation` (new cases for the rows and for `contextLift`), `npm run lint` - [x] In the app: untick each option and see the matching rows leave and the filter turn amber, restart and see it remembered - [x] In the app: jump to a narration from a landmark and see the player action above it with its top border and a small margin, with the previous narration out of view 🤖 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 first-entry and last-entry landmarks to story navigation, with distinct icons and an informational popover for tail landmarks. * Added filters for first/last entries, chapters, and checkpoints. Filter selections are saved and restored between sessions. * Added clearer empty-state messaging when filters hide all landmarks. * **Improvements** * Adjusted entry landing to reveal surrounding story context and continue following narration when landing on the last entry. * Kept the active timeline filter button amber on hover. * Resetting settings restores all landmark categories to visible. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
272ab6a37a
|
Release the version bump from the current branch (#600)
## Summary `scripts/release.js` no longer cuts a `release/vX.Y.Z` branch and fast-forwards the original branch afterwards. It now refuses to run from any branch but `master`, commits the version bump there, tags it, and pushes `master` and the tag in one atomic push. The version on `master` is then always the version that was tagged, and the tag can never point at a commit `master` lacks. ## Changes **Release script (`scripts/release.js`)** - Removes the `release/vX.Y.Z` branch and the best-effort merge-back step, along with the `--no-merge-back` flag. - Pre-flight fails unless the current branch is `master` (or reports a detached HEAD), before anything is written. - Pushes `master` and the tag with `git push --atomic`. - Pre-flight also checks that the remote copy of the branch is an ancestor of `HEAD`, using the SHA from the existing `ls-remote` call. A remote with commits the checkout lacks fails up front, instead of rejecting the push after the bump is committed and tagged. - Rollback now runs `git reset --hard` to the starting commit and deletes the tag, replacing the checkout-and-delete-branch cleanup. **Docs and CI comments** - `docs/development/release.md`: the "Cutting a New Release" steps, preconditions and rollback description match the new flow. - `.github/workflows/ci.yml` and the matching paragraph in `release.md`: comments describing the bump push as a fast-forward now say it is a push to `master`. ## Testing Not run. `release.js` has no tests, and I did not run `npm run release -- patch --dry-run`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01NH3m3rkE2Mm9Bz5Eonpfxz <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Chores** * Releases are now created from `master`; release attempts from other branches or detached checkouts are rejected. * The version update and release tag are pushed together. Checks catch existing tags and remote changes not present locally, and failed pushes restore the branch to its starting point. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com> |
||
|
|
fbd75cb4e8 | chore: bump version to 0.7.12 | ||
|
|
9f5ae59a60
|
iOS follow-ups: iPadOS detection, backup restore, app icon, and a first pass of iOS bug fixes (#559)
**Summary** Follow-up to the iOS build target (#541). It fixes problems found while running the build on real iOS/iPadOS WebViews, and it adds the missing app icon step. Every behavioural change is behind `isIos()` or lives in an iOS-only file, so other platforms take the same code path as before. **Changes** *Platform detection and restore* - **iPadOS detection:** an iPad's WKWebView reports a Mac user agent by default, so `isIos()` and `isMobileDevice()` missed it. The update check took the desktop path and open dialogs kept filters iOS can't map. A Mac UA with `maxTouchPoints > 1` now counts as iOS, with tests in `platform.test.ts`. - **Backup restore:** the iOS file picker returns a `file://` URL that the native restore can't `std::fs`-open. iOS now stages it through `import_saf_to_temp`, as Android does. - **Restore exit:** the restore flow shows a toast and waits before `exit(0)`, so quitting doesn't look like a crash. *Config and build* - **Updater string:** the updater text now mentions iOS. - **iOS config:** new `src-tauri/tauri.ios.conf.json` sets `minimumSystemVersion` to 16.4, the frontend's WebKit floor for Tailwind v4 `@property`, cascade layers and `color-mix()`. It also empties `build.features`, so `tauri ios dev` no longer needs the manual `--config` workaround. - **App icon:** `tauri ios init` fills the Xcode AppIcon set with the Tauri placeholder. `scripts/sync-ios-icons.sh` copies `src-tauri/icons/ios` over it and fails if the catalog names an icon the Aventuras set lacks. `build-ios-unsigned.sh` and `bootstrap-ios.yml` both run it. *UI and services* - **Update dialog:** on iOS the dialog points to a sideloading tool (AltStore, Sideloadly or TrollStore), since the `.ipa` is unsigned and Safari can only save it. - **TTS volume:** `HTMLMediaElement.volume` is a no-op on iOS. The settings now note this and disable the volume override switch and slider there. - **ComfyUI:** WKWebView's App Transport Security can silently block raw `fetch()` to a ComfyUI host outside the local-network exception. - `fetchModelList` now uses Tauri's HTTP plugin on iOS and forwards `timeoutMs`. - The SDK's `fetchApi()` (checkpoints, samplers, prompt queueing, image retrieval) is patched on the instance, iOS only, to use the same plugin. *Docs* - `docs/development/ios-build-target.md` and `docs/development/release.md` are updated. That includes the new config file and corrections to two stale claims. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Improved iOS Support** * Backup restoration supports selected files and displays completion instructions before the app closes or prompts you to restart. * Image generation connections are more reliable. * Update instructions explain how to install iOS downloads with a sideloading tool. * Volume controls are unavailable with an explanatory notice when the selected provider does not support them on iOS. * iOS app builds require iOS 16.4 or later and include synchronized app icons. * **Bug Fixes** * Improved iPad recognition, including iPadOS devices that identify as Mac computers. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
e30299b7ca
|
Add a newest-first toggle to the story gallery (#589)
## Summary The story gallery lists images oldest first, so on a long story the image you just generated is at the bottom. A header button now flips the order. The choice is one global setting (`gallery_newest_first`), applied to every story, and persists across restarts. ## Changes - **Order toggle** — `GalleryTab` shows a clock-arrow button in the header. `ui.galleryNewestFirst` holds the choice, `ui.toggleGalleryOrder()` persists it, and `settings.svelte.ts` loads it at startup. The SQL order and the per-story cache stay chronological; the gallery reverses a derived copy for display, so the grid, lightbox, counter and prefetch all read the same array. Exports are unchanged. ### Other changes - **Button accessible name** — `Button` now sets `aria-label` from `label`, then `title`, when it is icon-sized or responsive (text hidden below `sm`). An explicit `aria-label` wins. The logic is in `accessibleName.ts` with a unit test. The order toggle relies on this instead of a hand-written label. - **`GalleryTab` cleanup** (same file as the toggle): - Select-all is derived from the selection, which is now a `SvelteSet`. - The lazy-load action is an attachment; the keydown listener is `<svelte:window onkeydown>`. - The edit-modal id state is dropped; the id is passed to `handleEditImageSubmit`. - Previous, next and close buttons get `aria-label`s, and the prompt `<label>` is tied to its textarea. - **Platform support docs** — `docs/development/platform-support.md` records the Chrome 111 / Safari 16.4 floor and the rule for checking new APIs against it (Vite adds no polyfills and `check` won't catch it). `CLAUDE.md` and `docs/README.md` link it, and the README download table lists per-platform minimums. The minimums are derived from documentation and not tested on devices. ## Testing - Not run in this session: `aubr check`, `aube test`, `aubr lint`. - Not verified in the running app. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_0124fsnKeF2Y31LmyBhZxmsb <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Gallery images can now be sorted newest-first or oldest-first, with the selected order saved between sessions. * Buttons provide accessible names based on their labels or titles when needed. * **Documentation** * Installation guidance now lists minimum supported platform and web-engine versions, with a link to platform-support details. * Contributor guidance clarifies browser compatibility requirements for JavaScript, CSS, and Web APIs. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com> |
||
|
|
7a03c09be0
|
Default chapterBufferTail to off (#590)
## Summary The Advanced Settings switch `chapterBufferTail` now defaults to off. After a chapter, the Lore Management tail is sized by the character budget and floor, counted from the chapter's end, instead of by the story's Buffer Messages. ## Changes - `LORE_MANAGEMENT_DEFAULTS.chapterBufferTail` is `false` in `src/lib/services/ai/core/defaults.ts`. - `docs/architecture/lore-management.md` says "off by default". ## Testing - `aubr test` passes. - `aubr check` and `aubr lint` pass in the pre-push hook. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_0124fsnKeF2Y31LmyBhZxmsb <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Behavior Changes** * Chapter tail buffering is now off by default. This changes the default behavior for lore management; existing settings may continue to determine behavior where explicitly configured. <!-- end of auto-generated comment: release notes by coderabbit.ai --> Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com> |
||
|
|
c9439c168b
|
Import a lorebook file into the open branch (#588)
## Summary Importing a lorebook file from the story's Lorebook panel wrote every entry to the main branch (`convert.ts` sets `branchId: null`), whichever branch was open. The modal then refreshed the panel with `database.getEntries(storyId)`, which reads every branch's rows. The import now writes to the open branch and adds only the rows it wrote to the panel. The modal also can't be closed, and the file can't be swapped, while an import is running. ## Changes **Import service (`lorebookImportExport`)** - `ImportOptions` takes a `scope: BranchScope` instead of `storyId`. `importEntries` stamps each entry with the scope's `storyId` and `branchId`. - Each entry goes through `withUniqueEntryTerms` before it is saved, so the returned row has the same aliases and keywords the database stores. - `ImportResult` gains `entries`: the rows that were saved. An entry that fails to save is left out. **Story store** - New `appendImportedLorebookEntries(entries, expected)` adds the imported rows to `lorebookEntries`, skipping any id already shown, and invalidates the retrieval cache. It does nothing if another story or branch is open by the time the import finishes. **Import modal (`LorebookImportModal.svelte`)** - Reads `story.currentScope` when the import starts, passes it to the import, and hands it to `appendImportedLorebookEntries` in place of the `getEntries` reload. - While `importing`, the modal ignores Escape, outside clicks and the close gesture, and the "Choose a different file" link is disabled. - Whether to show the Aventura-export warnings is decided when the import starts. **Docs** - `persistence.md` notes that the lorebook file import returns its stored rows with the term dedupe applied. ## Testing `orchestrator.test.ts` moves to the `scope` option and adds cases for: the entry's `storyId` and `branchId` come from the scope; the returned row is the object passed to `addEntry` with duplicate keywords removed; a row that fails to save is absent from `entries` and counted in `errors`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01AxzbtNTqPRQVSwukyiRxqf <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Improvements** * Lorebook imports now go to the currently open story branch, and successfully saved entries appear in the story without reloading all entries. * Import results now reflect entries that were actually saved; entries that fail to save are excluded from the imported list. * While an import is in progress, the modal cannot be dismissed and the selected file cannot be changed. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com> |
||
|
|
6c6831b14e
|
Dedupe and match lorebook terms by termKey (#587)
## Summary
Lorebook aliases and keywords were compared in several different ways:
matching lowercased, the cleaner folded punctuation and accents
(`foldName`), the entry form compared exact strings, and nothing deduped
on write. This makes `termKey` (trim, NFC, lowercase, whitespace runs
read as one space) the single identity for a lorebook term, and applies
it wherever terms are matched, entered, stored, loaded, merged or
imported.
## Changes
**`termKey` and matching**
- `termKey` now reads each run of whitespace as one space.
- `entityNameMatches` normalizes the name and the search text with
`termKey` instead of `toLowerCase().trim()`, so matching and deduping
agree.
**Cleaner (`lorebook/entryFields.ts`)**
- `cleanKeywords` compares with `termKey` instead of `foldName`.
`"Kaelen's"` and `"Kaelens"` match different prose, so both are kept.
- A term with no letter or digit is still dropped as `empty`.
- `cleanAliases` is `cleanKeywords` with an empty alias list, replacing
a second copy of the loop.
**Write and load (`utils/lorebookTerms.ts`, `database.ts`)**
- New helpers `uniqueInjectionTerms`, `withUniqueEntryTerms` and
`withUniqueVaultEntryTerms` dedupe aliases and injection keywords with
`uniqueTerms`.
- `addEntry`, `bulkInsertEntries`, `updateEntry`, `addVaultLorebook` and
`updateVaultLorebook` apply them on write.
- `mapEntry`, `getEntryPreviews` and `mapVaultLorebook` apply them on
load, so legacy rows are cleaned as they are read.
- A stored `injection` that is not an object reads as the default; vault
`entries` that are not a list read as `[]`.
**Stores and import paths**
- The story store applies the same pass to what it keeps in memory
(`addLorebookEntry`, the bulk add, `updateLorebookEntry`).
- The lorebook vault store applies it in `create`, `update` and its
three import paths, so the list on screen is the list that was stored.
- The SillyTavern parser builds keywords with `uniqueTerms`.
**Editors**
- `LorebookEntryForm` add handlers use `uniqueTerms`.
- `VaultLorebookEntryFields` uses the existing `TermsInput` for aliases
and keywords in place of the comma-split inputs.
**Entry merge (`mergeEntities.ts`)**
- The alias and keyword unions dedupe by `termKey`.
- An alias or keyword the entry's name or aliases already match is left
out, since it can never add a match. This includes an absorbed name that
the survivor's name already matches.
**Docs**
- `lore-management.md` and `persistence.md` describe the above.
## Changes outside the main purpose
- `bulkInsertEntries` built its row placeholder with 21 `?` for an
18-column insert. It now builds the placeholder from the column list.
- The wizard `{#each}` blocks over model output (`Step2Lorebook`,
`Step3Setting`, `Step6Portraits`, `StepImportReview`,
`StepImportSelection`) are keyed by index instead of by name, so two
entries with the same name no longer produce duplicate keys.
- `foldName` and `entityNameMatches` doc comments are shortened and no
longer carry history.
## Testing
New and extended tests: `lorebookTerms.test.ts`, `text.test.ts`
(`termKey`, `entityNameMatches`), `entryFields.test.ts`,
`mergeEntities.test.ts`, `import/parse.test.ts`, and
`database.repeatedTerms.test.ts` covering the writers and row mappers.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_011cK3g9bUSYXTa58JhHVxAv
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Keywords and aliases can be edited as individual terms rather than
comma-separated text.
* Generation activity can show timing and details for keyword matching.
* **Bug Fixes**
* Duplicate and blank keywords and aliases are filtered consistently
when entries are edited, imported, merged, or saved, while preserving
the first spelling of each term.
* Matching treats repeated whitespace and equivalent Unicode forms
consistently, while keeping accented and unaccented terms distinct.
* Malformed vault lorebook data is handled safely when entries are read.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
|
||
|
|
e469a57c8c
|
Fix crash on characters with repeated traits (#585)
## Summary
A character with a repeated trait (for example "honest" twice) threw
`each_key_duplicate` in the Character Panel and never mounted, because
its trait chips were keyed by their own text. Lorebook aliases and
keywords, and every other chip list keyed by value, had the same
failure. This PR stops repeats from being written or loaded, and stops
those lists from depending on unique values.
## Changes
**Keys**
- All 29 `{#each}` blocks keyed by their own item now key by index. The
lorebook form removes a chip by index, so one of two identical chips
goes, not both.
- A local ESLint rule, `local/no-self-keyed-each`, rejects `{#each x as
item (item)}` in `.svelte` files.
- `RuntimeVariableManager` loops over the constant entity-type list and
skips empty groups, so a section keeps its index when an earlier type
gains its first variable. The per-type variable count now comes from the
grouping instead of a second pass.
**Trait identity (`src/lib/utils/text.ts`)**
- `termKey` is trimmed, NFC-normalised and lowercased. Case, padding and
composed/decomposed forms merge; width forms, accents and punctuation
stay distinct.
- `uniqueTerms` drops blanks and repeats by `termKey`, keeps the first
spelling, and returns `[]` for anything that is not an array.
- `parseTerms` turns comma-separated text into a term list.
- `applyTermChanges` removes by `termKey`, then adds.
**Writes and loads**
- `database.ts` dedupes `traits` and `translatedTraits` on the character
and vault-character writers, and on the row mappers for legacy rows. A
stored `translated_traits` that is not an array reads as `null`, so the
panel falls back to `traits`.
- `characterVault` (add, update, `.aventuras` and card imports),
`story.updateCharacter`, checkpoint restore and the classifier's
new-character paths normalise traits before keeping them in memory.
- The classifier and the vault assistant's character tool share
`applyTermChanges`. `removeTraits` now matches case-insensitively, and
the assistant's `replaceTraits` is deduped in its pending change.
- Card personality text, translated traits and `characterCardImport` go
through `parseTerms` or one `personalityTraits` helper.
**Inputs**
- New `TermsInput` owns the comma-separated text and hands its parent a
list without blanks or repeats. It rewrites the text only when `value`
changes from outside, never while the user types.
- The Character Panel edit, Vault character form, Vault scenario NPCs,
and the wizard's protagonist, supporting-cast and SillyTavern-import
steps use it.
- Wizard stores hold traits as `string[]` instead of comma text.
**Docs**
- `docs/architecture/persistence.md` notes that trait lists are
normalised on write and on load.
## Behaviour changes outside the crash
- The classifier's trait update keeps the first spelling of a trait that
differs only in case; it used to keep the last.
- The vault assistant's `removeTraits` used exact matching and now
ignores case, and a term in both `addTraits` and `removeTraits` is kept
rather than removed.
## Testing
- `src/lib/utils/text.test.ts` covers `termKey`, `uniqueTerms`,
`parseTerms` and `applyTermChanges`.
- `src/lib/services/database.repeatedTerms.test.ts` covers repeats and
non-array values on load and write.
- `check`, `test` and `lint` pass.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01ViW48cgArG9aGQuwiNK6VZ
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Added a terms editor for entering character traits as individual items
in character forms and the setup wizard.
* **Improvements**
* Trait lists are now consistently trimmed and deduplicated across
editing, imports, translations, and saving.
* Repeated aliases and keywords can be removed individually.
* Variable groups without entries are no longer displayed.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
25af2145b2
|
Give the Vault assistant the state of the entity it was opened from (#577)
## Summary
When the Vault assistant is opened from a character, lorebook or
scenario editor, it now knows that entity's current state instead of
only its name and ID. The `interactive-lorebook` prompt is rendered
through the template engine like other prompts, so the Active Context
text and the per-message wrapper live in the template rather than in
TypeScript.
- The system prompt is rendered once per conversation and carries the
Active Context section for the focused entity. The entity name goes
through the `json` filter, so a name with quotes or line breaks stays
one quoted string.
- Each user message is wrapped in a new `interactive-lorebook-user`
half. It includes the focused entity's JSON record only on a turn where
the record differs from the last one sent, so earlier turns in the
history are never rewritten. Lorebooks send a summary only; their
entries stay behind `list_entries` / `read_entry`.
- If the entity has been deleted from the vault, the next turn carries a
note saying so.
- The service owns the focus. Loading a saved conversation clears it
(along with the toolset seeded from it) and re-renders the system
prompt, and the component reads it back through `getFocusedEntity()`.
## Updating your prompt pack
Only people who edited **Interactive Lorebook**, or who use a custom or
imported pack, need this. Built-in templates they haven't touched update
on their own. The new User part is added to every pack automatically.
### System: required
Without it, the assistant is no longer told which entity it was opened
from, and the `{{ focusedEntityType }}s` toolset is still pre-loaded
without explanation. The Active Context section used to be appended in
code; it is now part of the template.
Find:
```
or a place in a scenario as an NPC.
```
Replace:
```
or a place in a scenario as an NPC.{% if focusedEntityType != blank %}
## Active Context
The user opened this assistant from the {{ focusedEntityType }} editor for {{ focusedEntityName | json }} (ID: `{{ focusedEntityId }}`). The `{{ focusedEntityType }}s` toolset is pre-loaded. When the user refers to "this character", "this lorebook", "this scenario", or uses pronouns referencing an entity without naming it, assume they mean this one.{% endif %}
```
New variables: `focusedEntityType`, `focusedEntityId`,
`focusedEntityName`.
The **Interactive Lorebook (User)** part is added to packs that lack it.
If yours is empty, the message is sent unwrapped and the record and
removal note never reach the model.
New variables available to the User part: `userMessage`,
`focusedEntityType`, `focusedEntityRecord`, `focusedEntityRemoved`.
## Changes
- `InteractiveVaultService`: renders the system prompt and each user
message through `ContextBuilder.forPack(undefined)`; tracks
`vaultSummary`, `focusedEntity` and `lastSentRecord`; marks a record as
sent only once the turn is in the history; adds `getFocusedEntity()`;
`loadConversation` and `reset` clear the focus state. `loadConversation`
parses the saved row and renders the system prompt before it replaces
any state, so a load that fails leaves the current conversation as it
was.
- `memory.ts`: `interactive-lorebook` gains the Active Context section
and a `userContent` half.
- `variables.ts`, `sampleContext.ts`, `TestVariablesModal.svelte`:
declare the new variables, give them sample values, and group the Vault
assistant variables under their own "Interactive Vault" heading in Test
Variables (`userMessage` moves there from the lorebook group).
- `InteractiveVaultAssistant.svelte`: `initializeService` no longer
takes a `focused` argument, and sending a message uses the service's
focus. `VaultState` loses `activeCharacterId` / `activeScenarioId`,
which nothing reads any more.
- `docs/architecture/prompts.md` and a comment in `context-builder.ts`:
the interactive-lorebook template is no longer described as external.
Changes to code that already exists on the base branch:
- `character.ts`, `scenario.ts`, `lorebook.ts`: the object
`read_character`, `read_scenario` and `read_lorebook_summary` return is
extracted into `toCharacterDetails`, `toScenarioDetails` and
`toLorebookSummary`, exported from `tools/index.ts`, so the focused
record has the same shape as the tool output. The tool results are
unchanged.
## Testing
- `InteractiveVaultService.test.ts`: rendering of the system and user
halves, record sent once and resent on change, removal note, focus
cleared on loading a saved conversation, a failed turn not marking the
record sent, and a load that fails (a prompt render that rejects, or a
corrupt row) leaving the current conversation and focus untouched.
- `memory.test.ts`: the template renders the Active Context and the user
half as intended, and keeps an entity name with quotes and line breaks
on one line.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_011E2hSgDzTNnxZBbmcV7gUV
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* The Vault assistant now uses current context for the focused
character, scenario, or lorebook, and notes when that entity changes or
is removed.
* Added Interactive Vault prompt variables and sample values for
focused-entity details and conversation history.
* Lorebook summaries and character and scenario details are available
through assistant tools.
* **Bug Fixes**
* Loading a saved conversation now clears the previous focus and loaded
tools; failed loads leave the current conversation unchanged.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
|
||
|
|
832c2806e4
|
Link Vault Assistant tool results to their own pending changes (#576)
## Summary When the Vault Assistant made several change-producing tool calls in one step, the tool calls showed the wrong pending changes. Each tool result was linked to the most recent pending change not already linked. The SDK runs a step's tool calls together, so every change already exists before the first result is emitted: the first call showed the last call's change, the rest showed none, and a failed call could show a sibling's change. Each tool result is now linked to the change it reports by id. ## Changes - `InteractiveVaultService`: a tool result is linked to the pending change whose id it carries, in `pendingChange.id` or, for tools that return only the id, in `changeId`. A result that carries neither, or an id that matches no change, gets no linked change. Replaces the latest-change heuristic and its `linkedChangeIds` set. - `SendMessageResult` gains `unlinkedChangeIds`: the pending changes no tool result carried, which the UI could never show. `InteractiveVaultAssistant` raises an error toast when the list is not empty. - `set_portrait` returns `changeId`; its result carried neither field. - The tool set is typed instead of being widened to `Record<string, unknown>` and cast back to `ToolSet`, so the tool-result handler reads `event.output` without recovering its shape by cast. - Tests: the fake agent stream can run the real tool `execute`s before emitting events. New tests cover parallel lorebook entry updates, parallel character deletions, a failed call beside a successful one, and `set_portrait` beside a deletion. A classification test fails when a registered tool is in neither the change-creating nor the non-change list, and each change-creating tool is checked to return its change. One more test covers `unlinkedChangeIds`. ## Test plan - [x] `aubr check`, `aube test`, `aubr lint` 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_011E2hSgDzTNnxZBbmcV7gUV <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Pending changes are matched to AI tool calls using explicit change identifiers, including when several calls run in parallel. Lorebook updates, character deletions, and portrait updates are linked to their corresponding changes. * Failed tool calls are not associated with pending changes created by successful sibling calls. * Portrait update results include the identifier for their pending change. * When changes cannot be linked to a tool call, the assistant reports how many were dropped. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com> |
||
|
|
8e21cd629f
|
Make the errors surface in the activity report (#569)
## Summary The generation activity report could not be relied on when something went wrong: - steps were swallowing the exceptions they produced, with no chance for a user to see them - a step whose call failed usually closed as finished, because the services absorb their failures into a fallback; - several requests behind one step — retries, the narrator's empty-answer loop — showed as a single row; - a step could stay *in progress* indefinitely while a retry loop ran out of sight, and the narrator's own errors were swallowed and re-sent as empty answers. This makes the report readable in full and makes it say why each step failed, request by request, without changing what a turn does. ## Changes **Display** - The timeline is uncapped, read by scrolling the story like the reasoning block. Times are rounded to the nearest second, sit in a fixed column, and are left blank for a step recorded after the fact without a duration (an agent tool call, a cached selection) instead of reading a default `0s`. - The collapsed line wraps instead of being cut, and on a narrow card shares its row with the story time. - The latest turn's timeline opens and the previous one folds; a reader's own choice sticks. Records are no longer hidden when a turn ends, and the session keeps the last 20. - Sending scrolls to the end of the story, and the story view no longer jumps when the streaming card is replaced by the entry. - The sparkle marks the model calls themselves, not the steps that contain them. **Failure reporting** - A failed step carries its reason, worded once by `describeActivityError`: status and provider message for an API error, `Failed after N attempts, with no retries left: <last reason>` for exhausted retries, a lost connection named as such rather than the HTTP plugin's `error decoding response body`. - Services that absorb a failure into a fallback — translation, suggestions, action choices, timeline fill, Tier 3 selection, scene analysis, the background image, the classifier's `_error` — still close the step they serve as failed (`failStep`) and return the same fallback as before. - A failure is told once, on the deepest step that carries it; every step above it is marked with a red ⚠. When the only failures beneath a step are attempts its request got past, the ⚠ is in the text colour. - A turn ends with an outcome (`turnOutcome`); only a halted turn says *Failed* on the collapsed line. A halted turn's record moves to its error entry, which is marked by `metadata.source` rather than recognised by its text, and every other system entry gets edit, delete and regenerate. The "Generation failed" banner above the input is removed: it duplicated the error card. - The error card for a turn without narration reads *The narration could not be generated. Please try again.* — it no longer claims a number of attempts. **Requests and retries** - `activityMiddleware` sits inside `retryOn429Middleware`: a request that needs a second attempt gets a row per attempt (the first backfilled) and a row per wait the retry middleware schedules. The SDK's own retries show as attempts. - The narrator's empty-answer loop reports *passes*, kept apart from the transport attempts inside them. A single pass has no container; it is grouped into `Pass 1` only once a second follows. An empty pass is a failure. - Within a pass, `Waiting for model` gives way to `Generating` as soon as the response starts, so a slow first token and an empty answer are both visible. **Narrator errors** - `streamText` reports a failed request as an `error` part rather than by throwing. The narrator dropped it, so every narrator failure read as an empty answer and was re-sent twice. The part is now rethrown, with a provider's in-stream error object turned into an `Error` with its code and message. - A stream that fails after text has arrived keeps that text as the narration and reports the failure without halting: the request was paid for, and the reader can still regenerate. One that fails before any text is passed again, like an empty answer; a request refused outright is not. **Phases** - `Updating world` and `Saving translation` are steps of their own, so a failed write shows where it happened. `Updating world` reads *partly applied* after a salvaged classification and *fallback bookkeeping only* after a failed one. - Agent Mode's hand-off is a `Handed off · N images queued` step under *Images*: the images finish after the turn, and the record does not follow them. - A phase switched off in settings is not reported; one switched on but missing its profile is struck through with that reason. **Stop** - Once the narration is saved, Stop dims, and a tap explains with a toast that the turn is finishing steps that cannot be interrupted. Nothing after the narration observes the abort, so Stop used to free the input while the turn still held the generation lease: Send was offered and refused, the story could not be left, and the report went on counting. "Generate a different response", which refuses Stop throughout, now explains its disabled Stop the same way. **Tooling** - `scripts/fake-provider.mjs` (`npm run fake-provider`) is an OpenAI-compatible chat and image API whose behaviour is chosen by the requested model name: rate limits, 4xx and 5xx, hangs, empty, malformed and partly valid answers, cut streams. Setup and the model list are in `docs/development/testing.md`. `docs/architecture/overview.md` describes the reporting model, the narrator's passes and the Stop refusal. ## Testing - New unit tests for `describeActivityError`, `turnOutcome`, the recorder (including `groupChildren` and untimed steps), the tree readings (`failuresShownBelow`, `failureMarks`, `stepsAboveLLMSteps`), `trackPhase`, the retry and activity middleware, the narrator's chunk translation, each phase's reporting, and `storyEntries`. - `reportingDoesNotAlterTurn.test.ts` and `absorbedFailures.test.ts` check that reporting changes nothing a turn does and that every absorbed failure still returns its fallback. - Every call a turn makes was run against the fake provider in the desktop app, each in the failures it can meet; the display was also checked on Android. - No test renders a component; the timeline, the collapsed line and the Stop button were checked in the running app. ## Known gaps - The retrieval agent builds its model elsewhere and its retries are not reported. - Stop still cannot cancel work after the narration is saved; this only stops it from claiming to. - On Android a tap on Stop while the narration streams can fail to register; that is separate from this change. 🤖 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** * Generation activity reports now show clearer step summaries, elapsed times, failures, recoveries, and retry attempts. More activity history is retained, and reports are visible by default. * Generation errors include clearer explanations. Narration already produced is preserved if a later step fails, and failed generations can be retried from the story. * The story view scrolls to the latest entry when requested during generation. * Added a fake AI provider for testing successful responses and failure scenarios. * Story creation no longer waits for background-image generation to finish. * **Bug Fixes** * Stop controls now reflect when generation can no longer be interrupted. * Generation and translation failures are more consistently shown in activity reports. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Tony Frazier <16245649+a-frazier@users.noreply.github.com> |
||
|
|
cfcb7f19fe
|
Show chapter banners in the story view (#575)
## Summary Adds chapter banners to the story view: a header before each chapter's first entry showing its number, title and summary, with links to the previous and next banner. The un-chapterized tail gets an unnumbered "The Story Continues" banner. Banners are off by default and toggled by a new **Show Chapter Banners** setting under Interface. The navigation panel lists the same chapter starts as landmarks whether or not banners are shown. ## Changes - `utils/chapterBanners.ts`: `buildChapterBanners` places a banner before each chapter's first entry and before the tail, in story order rather than `number` order. A chapter needs both its ends in the loaded entries. `lastResolvedChapterEnd` is shared with `getUnchapterizedEntries`, so the tail boundary has one definition. - `story.chapterBanners` is derived once on the story store; `StoryView` reads it when the setting is on and renders `ChapterBanner` inside the entry's `data-entry-id` wrapper, so jumps to the entry land on the banner. - `StoryNavPanel` / `buildLandmarks`: chapter and tail landmarks, sorted ahead of other rows on the same entry. A new `switchesBranch` field on `Landmark` keeps chapter and tail rows from switching branch in "Switch to checkpoint branch" mode, since a chapter's start can lie in an ancestor's history. - `utils/storyPin.ts` (`repinDecision`): a reader pinned to the bottom is re-pinned when the story resizes in place (a banner appearing, a summary edited), but not while editing, switching branch, scrolled up, or when the layout (window or entry count) changed. - `showChapterBanners` UI setting, persisted as `show_chapter_banners`. - `docs/architecture/overview.md` describes the above. - Tests for `chapterBanners`, `storyPin`, and the new landmark behaviour. Outside the main purpose: - Adds a `font-story` Tailwind utility and uses it for `.story-text`; no change in the resulting font. - `StoryEntry` uses `text-primary-foreground` instead of `text-white` on primary backgrounds. - The OLED theme's `--primary` is now a light grey (`hsl(0 0% 85%)`) with a black `--primary-foreground`. ## Test plan - [x] `aubr check`, `aube test`, `aubr lint` - [x] With the setting on, banners appear before each chapter start and before the tail; prev/next links jump to the neighbouring banner - [x] With it off, no banners; chapter landmarks still appear in the navigation panel and jump to the chapter's first entry - [x] Pinned at the bottom, toggling the setting or editing a chapter summary keeps the view at the bottom - [x] In "Switch to checkpoint branch" mode, chapter rows do not switch branch - [x] OLED theme: primary buttons and badges are legible 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01VUWcK2a3GyMV5MpyECftfw <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added optional chapter banners to the story view, with summaries and navigation between chapters. Enable them in Interface settings. * Added chapter and tail entries to story navigation. * **Bug Fixes** * Improved automatic scrolling when story content changes, while avoiding unwanted jumps during editing or branch switches. * Updated OLED theme colors and story image indicators for clearer contrast. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com> |
||
|
|
df824e633d
|
Keep the wizard's response length and narrator reinforcement on the new story (#570)
## Summary Stories created through the wizard always came out with response length Dynamic and narrator reinforcement Full, whatever the user picked. `createStoryFromWizard` rebuilt the settings object field by field and left out `targetLength` and `narratorReinforcement`. ## Changes - `createStoryFromWizard` now takes `settings: StorySettings` and stores it as given, instead of copying a hand-picked subset. A per-story setting added to `StorySettings` later reaches wizard stories without touching this method. - Drop the `ImageGenerationMode` import this left unused. The wizard already supplies both values (`wizard.svelte.ts` passes `narrative.targetLength` and `narrative.narratorReinforcement`); they were only discarded here. ## Testing Not run. I haven't run `aubr check`, `aube test` or `aubr lint` for this PR, and nothing exercises this path. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_018uUdnC4MdqXJmKtA5MxnQD <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Refactor** * Story creation now accepts the standard story settings format and passes those settings through directly. <!-- end of auto-generated comment: release notes by coderabbit.ai --> Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com> |
||
|
|
47279128b5
|
Persist the generated Kotlin in Android's Rust cache (#565)
## Summary The Android build's Rust cache now also stores the Kotlin that `tauri` and `wry` generate into the gitignored `src-tauri/gen/android/app/src/main/java/com/karelian/aventura/generated/` directory. Their build scripts declare `rerun-if-changed` on those files, so on a fresh checkout cargo sees them as missing and marks both crates dirty. Every plugin and `tauri-runtime-wry` behind them is then rebuilt in each of the four cargo builds. ## Changes - `build-android.yml`: add `cache-directories` for the generated Kotlin directory to the `swatinem/rust-cache` step, so the files are restored alongside `target/` and keep the mtimes the cached fingerprints expect. - `build-android.yml`: bump the cache key from `android` to `android-v2`. `cache-directories` is not part of the key and an exact hit never re-saves, so the existing entry would never pick up the new path. The next `master` run is cold as a result. - `docs/development/release.md`: document the behaviour in the caching section, including the expected size of the saving, why the second `aarch64` build still recompiles the app crate, and how to see cargo's dirty reasons (`CARGO_TERM_VERBOSE: true`). Separate from the above: `release.md` said `ci.yml` warms `build-desktop.yml` and `build-android.yml`. It already runs `build-ios.yml` too, so the doc now names all three. ## Test plan - [ ] Run `ci.yml` on `master` after merge (cold build under `android-v2`), then a second run, and confirm the Android build's cargo output no longer recompiles the plugin crates and `tauri-runtime-wry`. Cargo only prints dirty reasons with `CARGO_TERM_VERBOSE: true` set on the build step. 🤖 Generated with [Claude Code](https://claude.ai/code/session_011v1hp3cfZZveW19mH4tkes) https://claude.ai/code/session_011v1hp3cfZZveW19mH4tkes <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Chores** * Updated Android build caching to include generated Kotlin sources and use a refreshed cache version. This keeps cached build data aligned with the current build inputs. No changes to app features or behavior are included. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com> |
||
|
|
76082458b7
|
Clean the Style Reviewer's input of narration markup (#560)
## Summary The Style Reviewer judges prose habits, and its findings are injected into the narrator's system prompt as `<style_guidance>`. Given raw entries, it also judged the markup (`<pic>` prompts, Visual Prose HTML, `### Time | Place` headers, `---` rules), so a finding about markup became an instruction to the narrator. This adds one configurable narration cleaner, puts the reviewer and the classifier on it, and adds a **Clean Input** switch for the reviewer. The reviewer now reads plain prose, and the classifier still reads the header text that carries the time and place. The HTML stage parses with the DOM. The narrator's markup renders as real HTML (`marked` passes well-formed tags through raw; Visual Prose goes through `template.innerHTML`), so the cleaner reproduces what the reader sees: `<Kael>` is an invisible unknown element on screen and vanishes in the cleaned text too, and entities decode as the browser decodes them. A DOM parse also handles tag case, hyphenated names, unclosed tags and entities without a hand-kept tag list. Design, per-option behaviour and the experiment behind it: `docs/architecture/narration-cleaning.md`. ## Changes - **`utils/narrationClean.ts`** (new): per-markup modes (`keep` / `unwrap` / `remove`) and presets `CLEAN_NONE`, `CLEAN_FOR_REVIEW`, `CLEAN_FOR_CLASSIFICATION`. - The HTML stage parses with `template.innerHTML` (inert: scripts don't run and nothing loads) and walks the tree. Unknown and custom elements are unwrapped, `<style>`, `<script>` and comments are dropped, `<em>` / `<strong>` become `*` / `**`, block ends break lines, and table cells are separated by a space. - A `<` that does not begin a well-formed tag is escaped before the parse, copying `marked`'s inline `tag` rule, so `3 < 5`, `x<y and then…` and `<https://example.com>` stay text. A test runs a table of inputs through `marked`'s `Lexer.lexInline` to keep the two in agreement across upgrades. - `<pic>` tags (`<pic … />` and `<pic …></pic>`) are removed by a regex stage that runs first. - **Style Reviewer**: `buildReviewPassages` cleans each entry, skips one that cleans to nothing, and numbers the rest. **Clean Input** (Advanced Settings, `styleReviewer.cleanInput`, default on) switches to `CLEAN_NONE` through the same path. - **Classifier**: moved onto `CLEAN_FOR_CLASSIFICATION`, which unwraps heading and bold-line text for the time and place fields. `stripNarratorMarkup` is removed. - **New devDependency: `jsdom`.** The default test environment stays `node`; a file that reaches the HTML stage opts in with `// @vitest-environment jsdom` (`narrationClean.test.ts`, `styleReviewPassages.test.ts`). `devEngines` moves to `^24.15.0`, the floor jsdom 30 needs; `.nvmrc` resolves to the latest 24.x. - CRLF blank lines collapse in the final pass. - Docs: `narration-cleaning.md` (new), `testing.md`, the docs index and the `CLAUDE.md` table. ## Behaviour to know about - The HTML stage needs a `document` when the text contains `<` or `&`. The app always has one (SPA, `ssr = false`); under `node` the stage throws a `ReferenceError`. - In Visual Prose the browser parses the raw content and is stricter than `marked`, so a malformed tag there hides text on screen while the cleaner keeps it. The Visual Prose prompt asks for valid HTML, and the cleaner shows the reviewer the extra text. - `pic: 'keep'` combined with `html: 'unwrap'` drops the tag as an unknown element and keeps the prose around it. - A whole-line bold speaker or name label (`**Kael**`) is removed as a heading under `boldLines: 'remove'`; **Clean Input** switches that off. - jsdom stands in for the WebView's parser in tests, and the manual check below covers the real WebView. ## Test plan - [x] `check`, `test` and `lint:fix` pass (2012 tests) - [ ] In `tauri dev`, with Visual Prose on and inline images on, trigger a style review and check the dev log: the rendered `passages` variable is plain prose, and the prose after a `<pic>` tag is kept - [ ] A narration with a literal `<Kael>` reads the same to the reviewer as it does in the story view - [ ] The classifier fills time and place from a `### Time | Place` header and from a Visual Prose passage - [ ] The **Clean Input** switch turns cleaning off 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01YHkuCEJRUTBnsZaUnhfR9N <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added a “Clean Input” setting for Style Reviewer, enabled by default. When on, it cleans narration before review; when off, original text is retained. * Improved narration cleanup for reviews and classification, including handling of markup, images, and layout. * **Chores** * Updated the minimum required Node.js version. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com> |
||
|
|
205cf38de0
|
Save analyzed-image portraits through the story store (#564)
## Summary When scene analysis asks for a character portrait, the generated image was written straight to the database with `database.updateCharacter`, using the character read when the turn started. That write skipped the story store, so: - the open story's in-memory characters never received the portrait; - on a branch, it bypassed copy-on-write and wrote to whichever row was read at the start; - it overwrote any portrait the character had been given in the meantime; - it landed even if the user had since switched to another story or branch. Portraits now go through a new `story.saveGeneratedPortrait`, and the analyzed-image pipeline matches scene names once, ignoring case, accents and punctuation, then tracks each character by canonical id so a copy-on-write swap mid-turn doesn't lose it. The same pass stops scene analysis from queueing duplicate portraits and lets scenes in the same batch use a portrait that is still generating as their reference. ## Changes **Saving the portrait** - `StoryStore.saveGeneratedPortrait(scope, character, portrait)` drops the portrait when the story or branch it was generated for is no longer open, when the character no longer exists, or when the character already has a portrait. Otherwise it saves through `updateCharacter`, which handles copy-on-write and updates `story.characters`. - `ImageGenerationContext` gains `branchId`, `getCharacters` (replacing the `allCharacters` snapshot) and `onPortraitGenerated`. `ActionInput` and `StoryEntry` wire `onPortraitGenerated` to `saveGeneratedPortrait` with the turn's scope. `ImagePhase` uses a narrower `PhaseImageContext` that omits these three fields, which its caller supplies. - `ActionInput.buildPipelineDependencies` takes the generation lease as its `BranchScope` instead of a bare story id. - `docs/architecture/overview.md` notes that analyzed images outlive the generation lease and lists the conditions under which a late portrait is still saved. **Resolving characters** (new `services/ai/image/portraitReferences.ts`) - `resolveScenes` matches each scene's character names against the present characters once, using `sameEntityName`, which ignores case, accents and punctuation, and matches nothing for a name that folds to the empty string. Unmatched names and repeated characters are dropped from `depicted`. A portrait scene is dropped if its first name matches nobody, or if an earlier scene already asked for the same character's portrait. - Present characters are refreshed from the live list by canonical id, so the pipeline sees a copy-on-write override's current state, including a portrait saved since the turn started. `canonicalId` and `findLiveCharacter` are added to `utils/branchScope.ts` and shared by the service and the store. - Before queueing, a portrait scene is skipped if its character has gone or already has a portrait, saved or still generating. **Pending portraits as references** - `AIService` keeps a `PendingPortraits` map keyed by branch scope and canonical id. It lives on the service rather than the turn, so a portrait started late in one turn is still visible to the next. Entries clear when their portrait settles, including on failure, so a retry isn't blocked. - Portrait scenes are queued before other scenes. A reference-mode scene collects up to three sources (a saved portrait, or a pending one) for its first three depicted characters and waits for the pending ones before generating. - If every reference it waited on fails, the scene generates without references using the standard profile, model and size, and its image record is updated to match. - The prompt's lists of characters with and without portraits count a pending portrait as one, so the model is less likely to ask for a second. ## Testing - New `portraitReferences.test.ts` covers canonical-id refresh across copy-on-write swaps, pending-portrait tracking (per branch, cleared on failure, not cleared by an older settle), reference source ordering and the three-character limit, scene resolution (name folding, dropped unknown or duplicate portraits, order kept), and reference URL resolution when portraits fail. - No test covers `saveGeneratedPortrait` or the Svelte wiring; the store is a `.svelte.ts` module and can't be imported by a test. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01TVcjT1wrvKtDhWGVnojeiY <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Scene image generation can use existing or newly generated character portraits as references to improve visual consistency. If portrait references are unavailable, image generation continues without them. * Portraits can be matched to current characters and used in portrait scenes before other scenes are generated. * **Bug Fixes** * Generated portraits are saved only when the character remains available in the active story and branch, preventing outdated results from changing another story or branch. * Lore entries already incorporated into a merge are no longer left behind. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com> |
||
|
|
1aa68321c5
|
One hover rule for styling and behaviour, hybrids included (#558)
## Summary Follow-up to #553. The app answered "can the reader hover?" three ways, and on a hybrid device (a touchscreen laptop or a touch Chromebook, whose trackpad is the primary input) they disagreed: - `can-hover` (CSS) requires that no input is coarse, so hover-revealed controls are always shown there. - `pointer-fine:` sized those shown controls by the primary pointer, so a finger got the compact desktop targets. - `supportsHover()` (JS) asked about the primary input only, so jumps raised no confirmation and nothing stood in for the tooltip. The rest of the branch and story navigation panels' touch-target sizes still followed window width (`sm:`), so a wide touch tablet got small targets and a narrow desktop window got large ones. A story navigation row even reserved room for its action buttons by width while the buttons sized by pointer. ## Changes - **One condition, two copies.** `supportsHover()` now tests `(hover: hover)` and not `(any-pointer: coarse)`, the same condition as `can-hover`. `platform.test.ts` reads `app.css` and checks the two agree, so editing one alone fails. The `matchMedia`-less fallback (`!isAndroid()`) is unchanged. - **Hybrids behave like touch devices in JS too.** On a touchscreen laptop, jumps from the branch panel, story navigation panel, chapter list and timeline now raise their confirmation, and tapping an orphaned checkpoint says why nothing happened, as on a phone. - **Compact sizes follow the same rule.** `pointer-fine:` is gone, and every `sm:` touch-target size in `BranchPanel` and `StoryNavPanel` is now `can-hover:`: header buttons, rename confirm/cancel, the expand chevrons and their spacer, the create-branch form's buttons, row heights, and the room a row reserves for its actions. Full-size targets wherever a finger is in play, compact ones only where hovering can be relied on, at any window width. The 32px icon button, repeated 14 times, is now one `tap-target` utility in `app.css`, and icon sizes read `size-4 can-hover:size-3.5`. - Not changed: width-driven layout, touch-target sizes in other components, and Enter-to-send, which keys on touch hardware for a keyboard reason rather than a hover one. ## Testing - New `utils/platform.test.ts`: mouse or trackpad without touch → can hover; phone or tablet → cannot; touchscreen laptop → cannot (the case the old check got wrong); neither → cannot; no `matchMedia` → follows the user agent; and `app.css` still carries the same condition. - `npm run check` (0 errors), `npm run lint` (0 errors), `npm test` (1965 passed). - Checked in the running app, before the review follow-ups, on a desktop without touch, at full width and narrowed below `sm`: compact sizes, row actions hidden until hover, row text stops short of its actions, no toast on jumps. Checked on an Android emulator (API 36), portrait and landscape: full-size targets throughout, jumps confirmed. - Not run on a touchscreen laptop, for lack of one. DevTools' device mode makes touch the primary input, so it cannot reproduce a hybrid; that case rests on the unit tests and the CSS condition. 🤖 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** * Branch and story-navigation controls now adapt their sizing to device input capabilities. Touch-capable devices get larger, easier-to-tap targets, while hover-capable devices retain more compact controls. * Hover detection now checks whether the primary input can hover and whether any input is coarse, helping controls behave consistently on devices that support both touch and hover. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
af217618b4
|
Show hover-revealed row controls on touch devices (#553)
## Summary Ten controls that reveal on hover were fully transparent on touch devices, while staying tappable. Tailwind v4 wraps `group-hover:` in `@media (hover: hover)`, so `opacity-0 group-hover:opacity-100` hides a control everywhere and unhides it only where hovering is possible. On Android, tapping the right spot deleted or edited something with nothing drawn there. Five other controls avoided that by hiding on `sm:` and up, which answers a capability question with window width: a narrow desktop window showed them permanently, and a wide touch screen hid them. The app already decides hover by capability elsewhere (`supportsHover()` in `utils/platform.ts`). This PR puts every hover-revealed control on that rule. ## Changes - **One variant for the rule.** `can-hover` in `app.css` matches when the primary input can hover and no input is coarse (`(hover: hover)` with a nested `not all and (any-pointer: coarse)`), so touchscreen laptops and touch Chromebooks show the controls too. It is used at every site below and in `TimeAnchorModal`, which had the inline form. - **Invisible on touch, now visible:** - the tag manager's edit and delete buttons (Vault → Tags), and the colour palette of a tag being edited, which only opened on row hover - the remove-portrait button in the Vault character form (also in the SillyTavern import's character editor) - the Vault assistant's approve/reject buttons in the pending-changes list, and "Use" / "Set as portrait" on generated images - the SillyTavern import's per-character edit, save-to-vault and delete - the new-story wizard's lorebook remove (step 2), supporting-cast edit and delete (step 5), and remove-portrait buttons (step 6) - **Keyed on width, now keyed on capability:** the branch panel's rename and delete, the story navigation panel's row actions, the Vault assistant's conversation actions, and the prompt pack card's actions. The branch panel's and story navigation panel's row actions also take their compact size under `pointer-fine:` instead of `sm:`, so touch keeps 32px targets at any width. - **Overlays.** The Discovery card's View/Import overlay is laid out only where the input can hover (it was `sm:` and up), so on a wide touch screen a tap opens the details instead of hitting an invisible Import. The gallery's View overlay is hover-only and `pointer-events-none`, so it no longer swallows clicks on a failed image's Retry, on any device. - Each site reads `can-hover:opacity-0 group-hover:opacity-100`: hidden until hover where the input can hover, visible from the start where it cannot, at any width. An open pack-card menu keeps its trigger visible through `aria-expanded:opacity-100`; the story navigation panel keeps its focus reveal as plain `focus-within:`. - Left as is: the gradient over unplaced illustrations in a story entry (decorative, not a control). The reveals outrank the hide on specificity, not source order: `group-hover`, `aria-expanded` and `focus-within` are 0,2,0 against 0,1,0 for `can-hover:opacity-0`, which Tailwind 4.3.3 emits after them. Checked by compiling the classes. ## Testing - `npm run check` (0 errors), `npm run lint` (0 errors), `npm test` (1933 passed). Class-only change; there is no DOM in the test environment, so nothing here is unit-tested. - Checked in the running app on desktop, at full width and narrowed below `sm`, and on an Android emulator (API 36) in portrait and landscape, for the controls in the first commit. The review follow-ups (overlays, tag colour palette, the named variant, branch button sizing, the coarse-pointer condition) have not been run in the app yet. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
35a8e28523
|
Treat a missing or unrecognised story image mode as Text Only (#554)
## Summary
A story with no saved image mode was shown as **Text Only** in Story
Settings, but the turn pipeline read the same missing value as Agent
Mode (`?? 'agentic'` in `ActionInput`). So a story whose settings said
"No images will be generated" generated images on every turn, which
matches a report of a story "set to Text Only" still producing them.
The missing value is historical data. Stories created before the setting
existed carry none, stories created while the agentic mode was still
called `'auto'` carry a value that no reader recognises (generation
skipped it and Story Settings selected no option), and imports and syncs
carry either one unchanged. Every story either wizard creates today
stores one of the three current modes.
## Changes
- **The mode is checked once, when a story is read.** `mapStory` passes
settings through `readStorySettings` (`utils/storySettings.ts`), which
drops an `imageGenerationMode` that is not `'none'`, `'agentic'` or
`'inline'`, so the story reads as having no mode chosen.
`StorySettings.imageGenerationMode` is then true to its type for every
story in memory. Nothing is written back, so an older export or sync
payload is handled the same way whenever it arrives. Documented in
`persistence.md`.
- **Generation treats no mode as Text Only.** Readers that act on the
mode use `storyImageMode(settings)` (`?? 'none'`), replacing `??
'agentic'` in the pipeline. `ActionInput` reads it once per turn, and
the image step gets it from `ImagePhase` rather than re-reading the
store. The image context and both image phases take a required
`ImageGenerationMode`, so the phase's unreachable `!== 'agentic'` branch
and its skip reason are gone. `generateImagesForNarrative` returns early
for `'none'`, so the service enforces Text Only itself. The older inline
flag is not consulted. Background images keep their own switch.
- **Story Settings stops presenting a default as a choice.** With no
mode, `WritingStyleFields` selects no option and shows: "This story has
no image mode set, or its saved mode isn't recognized. It runs as Text
Only until you choose one." Choosing any option saves it and clears the
hint. The wizards always hold a valid mode and never show it. The option
list is built from `IMAGE_GENERATION_MODES`.
## Behaviour change
Stories with no saved mode that were silently getting automatic images
stop getting them, including stories from before the setting whose only
image field is the old inline flag. Their settings now say so, and
choosing Agent Mode or Inline Mode brings images back.
## Testing
- `utils/storySettings.test.ts`: each valid mode is kept; `'auto'`, an
unknown string, a non-string and `null` are dropped with the rest of the
settings intact; the old inline flag and `null` settings pass through.
`utils/image.test.ts` and `imageUtils.test.ts` cover the no-mode case,
and the slot-agreement tests pass `{ imageGenerationMode: 'agentic' }`
explicitly, since `{}` now means Text Only.
- `npm run check` (0 errors), `npm run lint` (0 errors), `npm test`
(1947 passed).
- Checked in the running app on the first commit, with an image profile
configured: stories with no mode and with `'auto'` show no selection and
the hint, generate no scene images, report the image step as skipped,
and offer no manual image action; choosing Agent Mode generates images
and shows it selected; choosing Text Only selects it and clears the
hint; both wizards show a selected mode and never the hint; a wizard
story created with Text Only stores `"none"`. The review restructure has
not been run in the app yet.
🤖 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**
* Stories with a missing or unrecognized image-generation setting now
run in Text Only mode until a valid mode is selected. The settings panel
shows no preselected option and explains that image generation is off.
* Image generation now consistently respects the story’s saved mode
across writing, streaming, and image requests. Background image
generation remains available even when standard image generation is off.
* Invalid saved image-generation values are ignored, while valid saved
modes are preserved.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
0913b5ce7d
|
Route dev/build/release scripts through aube when it installed node_modules (#552)
## Summary - Adds `scripts/package-manager.js`, which detects whether `aube` or `npm` owns the current `node_modules` (by checking for `node_modules/.aube-state`, then confirming `aube` is still on `PATH`) and builds the right `[cmd, args]` pair for `spawnSync`/`execFileSync`, including the Windows `npm.cmd` workaround. - Adds `scripts/pm.js`, a thin CLI (`node scripts/pm.js <args...>`) that runs that detected package manager with the given arguments and forwards its exit status. - Routes `lefthook.yml`'s `pre-push` `lint`/`check` commands and `tauri.conf.json`'s `beforeDevCommand`/`beforeBuildCommand` through `scripts/pm.js`, so they use aube wherever aube did the install and npm otherwise. - `scripts/release.js` keeps the version bump on npm (npm owns `package-lock.json`), but now runs its `format`/`lint:fix` steps through the detected package manager. When that manager is aube, it adds `--no-install --frozen-lockfile` so aube doesn't re-resolve the lockfile npm just rewrote. - Documents the new detection/routing behavior in `docs/development/release.md`. ## Test plan - [ ] `npm test` / `aube test` passes, including the new `scripts/package-manager.test.js` coverage for `detectPackageManager` and `commandFor` on both platforms. - [ ] `npm run lint` / `aubr lint` and `npm run check` / `aubr check` pass. - [ ] Manually verify `npx tauri dev` still launches correctly on a checkout with only npm installed, and on one where aube performed the install. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01YHkuCEJRUTBnsZaUnhfR9N <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Chores** * Development, build, pre-push checks, and release tasks now use the package manager detected for the project, supporting both npm and Aube. * Command execution accounts for platform differences, including Windows, and the existing lint memory limit is preserved. * These changes apply to development and release workflows; they do not change application features or behavior for end users. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
8fbb71d427
|
Add iOS as a build target (#541)
# PR Summary ## What this does Adds iOS as a build target. CI (`release.yml`, `pre-release.yml`, `ci.yml`) now builds an **unsigned iOS-device ARM64 `.ipa`** on `macos-15` — with **no Apple certificates, provisioning profiles, or signing secrets** used or required anywhere — and uploads it as a workflow artifact (and as a release asset when publishing), exactly like the Android APK leg. Existing platform builds (Windows, Debian/Fedora Linux, Android, macOS Intel/ARM64) are untouched: **no file under `src-tauri/` was modified** — no `Cargo.toml`, no `Cargo.lock`, no `tauri.conf.json`, no capabilities, no Rust sources. The app was audited first and was already almost iOS-ready (mobile entry point, `staticlib` crate type, all Rust deps support `aarch64-apple-ios`, iOS icons present, frontend mobile handling in place). ## How the unsigned build works 1. `scripts/build-ios-unsigned.sh` patches `src-tauri/gen/apple/project.yml` (idempotently) to set `CODE_SIGNING_ALLOWED=NO`, `CODE_SIGNING_REQUIRED=NO`, `CODE_SIGN_IDENTITY=""`, `CODE_SIGN_STYLE=Manual` on the iOS target, then regenerates the Xcode project. This is required because neither the Tauri CLI's signing defaults nor xcodebuild runner args reach the signing validation in `xcodebuild archive`. 2. `tauri ios build --target aarch64 --archive-only` produces the `.xcarchive` without the CLI's signed-export phase. 3. The script merges iOS `Info.plist` keys (camera, local-network, ATS local-networking, export-compliance) into the built app, packages `Payload/Aventuras.app` into a versioned `.ipa`, and verifies it (arm64 binary, unsigned, correct layout) before exiting 0. ## Changes | File | Change | | --- | --- | | `scripts/build-ios-unsigned.sh` | **new** — full unsigned iOS pipeline (macOS-only) | | `.github/workflows/build-ios.yml` | **new** — reusable CI leg mirroring `build-android.yml` | | `.github/workflows/bootstrap-ios.yml` | **new** — one-shot workflow that generates the `gen/apple` Xcode scaffold and smoke-tests the unsigned build | | `.github/workflows/release.yml` / `pre-release.yml` / `ci.yml` | additive `build-ios` jobs + path triggers | | `src/lib/utils/platform.ts` | additive `isIos()` | | `src/lib/services/updater.ts` | iOS routes through the existing mobile update check; picks the `.ipa` asset (Android/desktop behavior unchanged) | | `docs/development/release.md`, `README.md`, `docs/architecture/overview.md`, `CLAUDE.md` | iOS documentation | | `docs/development/ios-build-target.md` | **new** — full account: audit, decisions, every CI failure and fix, limitations, verification | ## Verification - Local: `npm run check` (0 errors), `npm test` (1890 passed), `npm run lint` (0 errors), `npm run build`, `cargo check`, YAML job-graph validation of every touched workflow, the `project.yml` patcher tested against a template mock, and every Tauri CLI flag verified against the actual `cli-darwin-arm64@2.11.4` binary. - CI: the iOS leg runs end-to-end green on `macos-15` — scaffold generation, Rust cross-compile, unsigned archive, `.ipa` packaging, artifact upload. Android stayed green throughout; desktop legs were unaffected by this work. ## Notes / limitations (also documented in `docs/development/ios-build-target.md`) - The `.ipa` is **unsigned by design**: stock iOS will not install it — it targets sideloading tools (AltStore, Sideloadly, TrollStore) or a future signing step. - No in-app updater on iOS (the updater plugin supports neither mobile platform); iOS users get the release page / `.ipa` asset in the browser via the existing mobile check. - `src-tauri/gen/apple` is not yet committed; `build-ios.yml` self-heals by running `tauri ios init` on the runner when it's absent (~1–2 min overhead). Run the **"Bootstrap iOS scaffold"** workflow once and commit its artifact to remove that. - Local `tauri ios dev` needs `--config src-tauri/tauri.release.conf.json` (devtools plugin is desktop-only). <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added unsigned iOS app builds for testing and release distribution. - iOS builds are now included in CI and release workflows. - iOS release checks link to a matching IPA when available, with a fallback to the release page. - Added iOS platform detection. - **Documentation** - Added iOS download and build guidance, including Mac and Xcode requirements. - **Bug Fixes** - iOS builds now fail validation if required app metadata, executable files, or expected IPA components are missing. <!-- end of auto-generated comment: release notes by coderabbit.ai --> |
||
|
|
3eab16c3d1
|
Import Aventuras exports literally, without AI (#539)
## Summary
Aventuras routed its own exports back through import paths built for
untrusted external formats. A vault character export is a raw
`VaultCharacter`, which satisfies `isV1Card` (it has `name` and
`description`), so re-importing it parsed it as a SillyTavern V1 card,
sent it to the AI sanitizer, and dropped the portrait, visual
descriptors, tags, favourite state and metadata. Scenarios took the same
route through `clean`. Story lorebook exports were a raw `Entry[]` with
record ids, story ids and per-story state, and the vault lorebook
importer re-classified every entry with a model regardless of the file.
This PR makes the round trip literal: what the app exports comes back
with no model call, no field loss, and fresh local identity.
## Changes
- **One exchange envelope.** Vault characters, scenarios and lorebooks,
and lorebooks exported from a story, are written as `{ format:
"aventuras-exchange", formatVersion, entity, exportedAt, data }`. `data`
is the record's portable content only: no ids, timestamps, provenance,
derived metadata, database-local references or per-story entry state.
Metadata passes a per-entity allowlist. New module:
`services/exchange/`.
- **Exchange-first import.** All three vault importers and the lorebook
parser check for the marker before any external parser. A valid document
is saved directly. A document that declares the format but fails
validation (bad version, wrong or unknown entity, invalid payload) is
rejected with a specific error and never falls through to the card path
or AI. Unmarked files take the existing path unchanged. The setup and
SillyTavern import wizards take cards only; given an exchange file they
point to the Vault.
- **AI classification follows the format.** `importEntries` classifies
only SillyTavern lorebooks. The vault importer saves an Aventura export
as parsed. The story import modal replaces the AI checkbox with an
explanation for Aventura files.
- **Story-side entry fields.** A story lorebook export carries
`hiddenInfo` and `loreManagementBlacklisted`, so a story-to-story
transfer is literal. The vault keeps its story-agnostic shape: importing
such a file into the vault drops both and says so in a toast.
`VaultLorebookEntry` is unchanged.
- **Scenario starting time** travels, normalized through `storyTime`'s
`normalizeTime` on import.
- **Versioning.** Same major imports, ignoring unknown fields, with a
warning for a newer minor. A higher major is rejected with "update
Aventuras".
- **Exports.** The Aventura format description now says per-story entry
state is not included. An empty vault lorebook can be exported in
Aventura format; a story lorebook export still needs entries.
- **Docs.** New `docs/architecture/exchange-format.md`, with rows in
`CLAUDE.md` and `docs/README.md`.
## Breaking: pre-envelope lorebook exports
The raw `Entry[]` lorebook export is no longer recognised and reports
"unknown lorebook format". Recognising it was what kept alive the
converter branch that copied a source story's `state` verbatim, entry
ids included, into the destination. Users with an old export re-export
it from the source story or vault. Old character and scenario exports
are indistinguishable from cards and keep taking the card path, as
today.
SillyTavern and plain-text exports are unchanged. `.avt` story backup
and sync are untouched.
## Testing
- New unit tests: `services/exchange/exchange.test.ts` (envelope,
version cases, schemas, allowlist, round trips for all three entities,
starting time), `export/formats.test.ts`, `import/orchestrator.test.ts`
(classification gate), `import/roundTrip.test.ts` (story to story, story
to vault). `import/parse.test.ts` covers the envelope, an invalid
declared file, and the raw array being rejected.
- `npm test` (1921 passed), `npm run check` (0 errors), `npm run lint`
(0 errors).
- Checked in the running app: the story import modal with an Aventura
export, the export format description, and the vault toast for a story
lorebook import.
🤖 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**
* Characters, scenarios, and lorebooks can be exported and imported
using the Aventuras exchange format.
* Aventuras lorebook exports include the story title. Import previews
show the lorebook name and explain that entries are imported without AI
classification.
* Aventuras exchange files can be imported directly into the vault.
* **Changes**
* Story-specific entry state isn’t retained when importing an Aventuras
lorebook into the vault; a warning may appear when story-only fields
aren’t retained.
* Existing SillyTavern and text lorebook formats remain available.
* Unsupported or invalid exchange files display relevant errors or
warnings.
* **Bug Fixes**
* Lorebook exports with no entries are rejected for all formats.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
2291324cb2
|
Let the prompt pack word Response Length (#538)
## Summary
Response Length's text now comes from the prompt pack instead of the
application. The narrator templates branch on the story's raw
`targetResponseLength` (`dynamic`, `short`, `medium`, `long`), the same
way they already branch on `narratorReinforcement`, `pov` and `tense`,
so a pack author can reword what each length asks for.
Before, `formatLengthInstruction` composed one of eight hardcoded
sentences (four lengths × two modes) and handed it over as `{{
lengthInstruction }}`. A pack could not change it, and the length
buttons advertised paragraph counts copied from those sentences.
## What changes
- **Templates.** The adventure and creative-writing system prompts pick
their `# Format` length line with `{% case targetResponseLength %}`. The
shipped sentences are moved over byte-for-byte; an unknown or missing
value falls back to dynamic.
- **`lengthInstruction` is gone everywhere.** Action choices used the
same name for an unrelated constant ("Keep each choice concise but
specific - typically 5-15 words."), overwriting the narrator's value in
its own context. That sentence now lives in the action-choices template
itself.
- **Length buttons** read Dynamic / Short / Medium / Long, with no
paragraph counts, since what a length means is now the pack's to say.
`WritingStyleFields` loses its `mode` prop, which only existed to pick
between the two sets of counts.
- **Availability check.** Story Settings disables Response Length or
Narrator Reinforcement, with a reason, when neither narrator prompt that
will actually run reads the variable. It now checks both prompts (the
turn message and the effective system prompt) for both settings, through
one helper that asks the Liquid parser which variables a template reads,
so comments, raw blocks, string literals, `assign` targets and
properties of other objects don't count. A template that doesn't parse
counts as a reference, so the setting stays enabled. The new-story
wizard runs the same check against the pack and mode chosen in it. The
SillyTavern import offers neither setting, so its stories start at
`dynamic` and `full`, as before.
- **Variables.** `targetResponseLength` and `narratorReinforcement` are
registered as system enums next to `mode`/`pov`/`tense`, so Test
Variables offers their values as a dropdown and an exported pack
documents them.
- **Test Variables** shows on every pack. It was still hidden on packs
without custom variables, a leftover from before the modal listed system
and runtime variables too.
The context key `targetLength` is renamed to `targetResponseLength` for
templates; the stored story setting and the `TargetLength` type keep
their names. `responseLength` was considered and rejected: next to
`narrativeResponse` it reads as that text's measured length.
## Breaking for custom packs and custom prompts
`{{ lengthInstruction }}` no longer has a value, so any template still
rendering it renders nothing there: the narrator's `# Format` gets an
empty `- ` bullet, and action choices lose their choice-length hint.
Only the built-in pack is updated for you. `refreshDefaultPackTemplates`
rewrites `default-pack` alone, and the pack-level refresh exists only
for it. So this affects:
- **every custom pack created before this change**, edited or not,
because each was seeded with the old templates;
- **a story's custom system prompt** that uses the token;
- **pack files exported before this change**, when imported.
For the narrator, Response Length is then disabled with a reason until
the template branches on `targetResponseLength`. Nothing flags the
action-choices case.
### Release-note instructions
- **Custom pack whose narrator or action-choices templates you never
edited:** open the Adventure, Creative Writing and Action Choices
templates and click **Reset to default** on each. It works on custom
packs and writes the shipped text.
- **Edited narrator template, or a custom system prompt:** replace `- {{
lengthInstruction }}` with the `{% case targetResponseLength %}` block
from the built-in template for the same mode.
- **Edited action-choices template:** replace `{{ lengthInstruction }}`
with `Keep each choice concise but specific - typically 5-15 words.`
## Commits
1. **Let the prompt pack word Response Length**: templates, removal of
`lengthInstruction`, the shared reference helper, the Story Settings
check, the buttons and variable registration.
2. **Check narrator settings in the new-story wizard, and show Test
Variables on every pack**: the check moves into
`resolveNarratorSettingAvailability` so the wizard can call it, plus the
Test Variables gate and shorter variable descriptions.
Review follow-ups: failed availability lookups are caught and leave both
controls enabled; reference detection moved from regexes to the parser;
the wrapper modules are gone (`narratorSettingReasons` calls
`variableIsHonoured` directly); the Story Settings variable reference
lists both settings with their values.
`docs/architecture/prompts.md` is updated, and `story-time.md` no longer
points at the removed detection.
## Testing
- `npm run check`: 0 errors. `npm test`: 1922 passed. `npm run lint`: 0
errors. The two extra warnings are imports of
`$lib/services/prompts/templates` and `$lib/services/templates/engine`,
neither of which has an `index.ts`; existing importers of both get the
same warning.
- New tests cover the reference helper for both variables, the length
line in each mode (distinct per length, fallback to dynamic), the
action-choices sentence, and the availability reasons.
- In the running app: a Short adventure turn and a Long creative-writing
turn carry the matching sentence. A custom prompt with only `{{
lengthInstruction }}` disables the setting with its reason. The wizard
disables both settings on a pack that ignores them and re-enables them
after switching back to the built-in pack. The buttons show no counts.
Test Variables opens on a pack with no custom variables and shows both
new variables as dropdowns.
## Not in this PR
POV and tense text for the wizard and action-choice prompts is composed
in code the same way (`povInstruction`, `povPerspective`,
`povPerspectiveInstructions`, `tenseInstruction`), and the creative
opening prompts render `- POV: POV: …`. That is a separate change.
🤖 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**
* Writing settings now offer Dynamic, Short, Medium, and Long response
lengths. Prompt packs determine how each length affects narration.
* Settings explain when a prompt pack or custom prompt does not support
response length or narrator reinforcement.
* The Test Variables button is available whenever variable testing is
enabled.
* **Improvements**
* Action choices are guided to be concise and specific, typically 5–15
words.
* Prompt variable references now include response length and narrator
reinforcement.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
c43da108f6
|
Remove the never-written retry entry type (#535)
## Summary Removes `'retry'` from `StoryEntry.type` and every place that special-cased it. `retry` was declared in the initial commit alongside an icon and an amber border, but nothing has ever written an entry with that type: no commit passes it to `addEntry`/`addStoryEntry`, and regenerate saves its result as `narration` (retry state lives separately in `stories.retry_state`). It looks like a planned "alternate response" marker that was never built. It still cost something. A few readers treated it as narration (`StoryEntry.svelte`, `ChapterEntryList.svelte`, the prompt role labels), while most compare against `'narration'` only, so a `retry` row would have been dropped from lore prose, refused by regenerate, and skipped by export and rollback. Removing the value is simpler than teaching every reader about it. ## No migration The repo was created 2026-01-01 and its oldest release (v0.1.0, 2026-01-03) descends from the initial commit, so no release could have stored a `retry` entry. `.avt` imports and sync only carry what a release wrote. There is nothing to convert on load or import. ## Commits Ordered so each one type-checks on its own: readers first, the type last. 1. **UI readers**: `StoryEntry`, `StoryView` and `ChapterEntryList` no longer treat `retry` specially. `isNarrationLike` becomes a plain `entry.type === 'narration'`. 2. **Test fixtures**: drop `retry` entries from `storyEntries`, `newChapter` and `recentContent` tests. 3. **The type**: drop it from the union, the prompt role-label map (keyed by the union) and the data-model line in `docs/architecture/overview.md`. The retry feature itself (`retry_state`, retry backups, the regenerate flow) is untouched. ## Testing `aubr check` (0 errors), `aube test` (1873 passed), `aubr lint:fix` (0 errors). `rg "'retry'|\bretry:" src` finds only an unrelated `.inline-image-retry` CSS class. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_015erbkuMWWwFH4uvMNapi59 --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
10d62a5e19
|
Read a bounded, contiguous tail after a new chapter in lore management (#534)
## Summary A chapter-triggered lore management run was shown the *newest* entries under a character budget. On a long unchaptered tail that could leave a gap between the end of the new chapter and the first entry the agent saw. This PR makes the post-chapter excerpt start right after the chapter, and tells the agent when the excerpt stops short of the present. ## Changes - **Contiguous tail after a chapter.** After a chapter, the agent reads `chapterBuffer` (the story's Buffer Messages) prose entries starting immediately after the chapter, oldest first. The batch importer and Tidy lorebook keep the existing newest-first character budget. - **New setting: "Limit post-chapter story to Buffer Messages"** (`loreManagement.chapterBufferTail`, Advanced Settings, on by default). Off, the agent reads the character budget and floor counted from the chapter's end instead. - **Prompt tells the agent when the story continues.** If the excerpt ends before the present, the prompt says so and instructs the agent not to mark entries outdated because the excerpt leaves something unresolved. - **`LoreManagementService` owns tail selection.** It now takes raw `recentEntries`, `tokenThreshold` and `chapterBuffer` and applies the selection itself, via the new `loreRecentEntries` helper in `lorebook/newChapter.ts` (which also reports `continues`). This removes `recentEntryLimit` from the coordinator chain and the old selector from `recentTail`. - Updates `docs/architecture/lore-management.md`. ## Testing - Added unit tests for `loreRecentEntries` (limit, budget-from-chapter-start, `continues`), the coordinator, and the prompt template. - Run `aubr check`, `aube test` and `aubr lint` before merging. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01L5rEHTjgpYURonQPJgjo88 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added a Lore Management setting to limit post-chapter story context to the configured Buffer Messages count. When disabled, Lore Management uses approximately 2.5 chapters of text. * Lore Management now selects recent story entries according to the available context and can indicate when the displayed story continues beyond the excerpt. * **Bug Fixes** * Lore Management prompts now show a continuation note when the story continues, including when no recent story text is displayed, helping prevent unresolved events from being treated as outdated. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
c745d4fa45
|
Give lore management the just-written chapter in full (#528)
## Summary
Lore management currently only sees chapter *summaries*, including a
summary of the chapter that just triggered it — so the freshest content
is the most compressed. This hands the triggering chapter to lore
management verbatim instead, on both the automatic and manual paths, and
drops its summary from the chapter list so it isn't shown twice.
- **New setting: Send full text of new chapter** (Advanced Settings →
Lore Management → Send full text of new chapter, on by default). With it
off, every chapter stays a summary line as before.
- **Automatic path:** `ChapterService` now returns the entries it
summarized (`chapterEntries`), and `BackgroundTaskCoordinator` merges `{
chapter, entries }` into the lore session. The session doesn't re-read
the store, so switching stories before it starts can't swap the entries
out.
- **Manual path:** `createManualChapter` returns the created `Chapter`
(or `null` on refusal) instead of a boolean. `MemoryView` passes it to
`runManualLoreManagement`, which resolves its entries.
- **`newChapter.ts`:** `loreChapterContext` builds the full-text section
(the chapter's `[ACTION]`/`[NARRATIVE]` prose plus its characters and
locations) and the summary list together. A summary is dropped only when
the section replaces it: a chapter whose entries are all blank keeps its
summary. `renderLoreProse` is now the only renderer for both the new
chapter and the recent story, so the recent story also leaves out
non-prose and blank entries.
- **`runLoreManagement`'s trailing params become a `LoreRunOptions`
object** (`mode`/`pov`/`tense`/`tokenThreshold`/`newChapter`).
`mode`/`pov`/`tense` are now required all the way down and reach the
Liquid render context. Before, they were registered as template
variables but dropped before the render call, so a custom prompt using
`{{ mode }}` would validate cleanly and render blank.
- **Prompt:** the lore-management template gets a `newChapterSection`
block, after duplicates and before the recent story. New
`hasNewChapter`/`hasRecentStory` flags make the tools guidance and the
closing "go back over…" instruction name only the material the session
actually has.
- The chapter list is no longer deep-cloned through JSON. It's already
projected to fresh `{number, title, summary}` objects, so no Svelte
proxy reaches the tools.
- `docs/architecture/lore-management.md` documents the handover, the
setting, and the batch-importer exception.
## Test plan
- [x] `aube test`: unit tests for
`renderLoreProse`/`loreChapterContext`, `newChapter` forwarding in the
coordinator, a check that the batch importer passes none, and render
tests for the template's `hasChapters`/`hasNewChapter`/`hasRecentStory`
conditionals and where the block goes
- [x] `aubr check` / `aubr lint`
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01NyFJz1NuUVgsvnCrGyr6jm
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Lore management can now use the full text of a newly written chapter
instead of its summary. This option is enabled by default and can be
changed in Advanced Settings.
* Lore management prompts now adapt their guidance to the available
chapters and story text.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
|
||
|
|
3254c0a7ed
|
Assert, check and reconcile story time, and start a new story at its opening's time (#530)
## Why
Every entry carries an in-story start and end, and all of them are the
classifier's output: a four-bucket time progression applied turn by
turn. When it drifts — an hour-long conversation logged as a few
minutes, a night skipped twice, a clock that runs backwards across a
regeneration — nothing reports it, and nothing lets a reader who knows
the right answer say so. The drift then feeds chapter spans, rollback
deltas and checkpoint clocks, each a copy of the wrong value.
New stories made it worse from the first entry: every one started with a
null clock read as `Y1 D1 00:00`, whatever hour its opening described.
## What changed
**Anchors**
- A reader can assert when an entry ended. That is an anchor: stored in
a new `time_anchors` table (migration 038), one per entry, removed with
its entry. It is the only stored time that is not derived output, and
the only thing a reconciliation measures from.
- Anchors are passive. Setting one changes nothing by itself; it becomes
a boundary a range can be selected against.
**Detection**
- A read-only analysis reports findings in two severities. *Defects* are
provably wrong (missing stamps, a backwards span, overlapping entries, a
chapter span that disagrees with its entries). *Suspected* findings are
only unusual (a zero start, an implausible jump, a flatline, a gap
between entries) and sit behind a filter.
- The anomalies screen opens a reconciliation on the range that holds a
finding.
**Reconciliation**
- The reader picks a range between two boundaries: an anchor, the
story's start or end, or a fork point. Only a fork point walls the
timeline; checkpoints are not boundaries. A boundary without an anchor
resolves to its entry's recorded ending.
- The range is weighed as alternating entry durations and the intervals
between them. Each interval carries a policy the reader chooses, and
only the default keeps it as recorded. The ladder shows it on narrow
screens, with gaps foldable.
- The preview *is* the result: one pure calculation
(`services/storyTime/reconcile.ts`) feeds both, and applying writes
exactly what was shown. Applying also recomputes the chapter spans
covering the range, rewrites the delta clocks a rollback would restore,
reseeds a checkpoint whose last entry moved, and moves the story clock
only when the range reaches the branch's last entry. All of it commits
together, after revalidating the inputs the preview was built on.
- A *Reference points* section asserts or edits a range's two ends in
place.
**Timeline panel and entry adjustments**
- A Timeline tab in the navigation panel lists boundaries and roles —
story start, fork point in amber, anchors, and checkpoints behind a
filter — with one control per row.
- Beside each narration entry, *Timeline adjustments* offers *Edit in
place* and *Create/Edit the anchor*. Editing one entry's times is
planned as a one-entry reconciliation, so it carries every guarantee
above. It is never refused: on an entry branches were forked from, it
says how many branches and intervals the edit reaches.
- An entry's time always shows both ends, with *not recorded* for a
missing one; an equal pair is a zero-length entry, which is worth
seeing.
**A new story's starting time**
- The story creation wizard's opening step asks when the story begins,
which is when its opening ends. The story is created with that time on
its clock and on the opening entry, and cannot be created without one.
It is a reading, not an anchor.
- Each opening the step offers carries its own start: imported from a
scenario (which may state it), written by hand, guidance for generation,
and the generated result, whose note says whether generation returned
it, it is the guidance, or the reader set it. An alternate greeting
arrives with no start of its own.
- Generation gets the start as it gets the title, through `TIME: opening
ends at {{ storyStartingTime }}` in the four opening templates, or a
request to suggest one. A pack whose template predates the variable
cannot pass it on; the step detects that, as story settings do for
Response Length, and disables the guidance field.
- A Scenario Vault entry can state a start (migration 039), and every
Quick Start seed carries one.
**Phone**
- One header menu opens either side panel.
- Timeline adjustments open as a submenu below their row. While it is
open the rest of the menu is locked, and a tap elsewhere only closes the
submenu.
## Notable
- **Migrations 038 and 039** add `time_anchors` and
`scenario_vault.starting_time`. Anchors travel through `.avt` export and
import via the id map.
- **Editing an entry from before a fork changes its text for every
branch below it, but not the state derived from it** (world state at the
fork, rollback deltas, the fork checkpoint's clock). This is
pre-existing and unchanged; it is written up in
`docs/architecture/overview.md` because entry time editing makes it
easier to reach.
- **Focus in the reconcile screen:** the dialog's scroll region is its
first tabbable element, so the focus trap's fallback never scrolls the
surface to the top. The rule is in `overview.md`.
- `overview.md` also gains the panels-and-gestures rules, and the docs
index was updated for both.
## Known, and out of scope
- The wizard's opening step already chose its opening in ways that
surprise: *Use This Opening* reads a copy of the imported opening that
nothing sets, the fallback ignores the selected alternate greeting, and
nothing says which opening will be used. This PR adds the starting time
to whichever opening the step already picks and leaves that choice
alone; it belongs in a wizard change.
## Testing
`npm run check`, `npm test` (1840 passed) and `npm run lint` pass. New
unit tests cover story-time parsing and arithmetic, boundary resolution,
the analysis, the reconcile calculation, the reconciliation plan
(chapter spans, deltas, checkpoint clocks, the story clock),
starting-time detection and the templates' rendering of it. Two Rust
tests in `time_anchors.rs` cover the table's cascade and
one-anchor-per-entry rule; `cargo test --lib time_anchors` passes.
Used throughout development in the running app, on desktop and in the
Android emulator.
🤖 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 a Timeline panel with anomaly checks, reference points, and
tools to reconcile story times across ranges.
* Readers can edit entry times and add or update time anchors with
notes.
* Story setup and scenarios support starting times, which can guide
generated openings.
* Mobile users can switch between left and right panels from the Panels
menu.
* Time anchors are preserved when exporting and importing stories.
* **Improvements**
* Years and days now display using one-based counts, and entry time
ranges remain visible when times are missing.
* The navigation panel now has separate Timeline and Navigation tabs.
* Entry time edits that end before they begin are flagged, and invalid
times cannot be saved.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
b94074c7ca
|
Edit a story's title, genre and description from its library card (#526)
## Why A library story card shows a title, a genre and a description, but only the title could be changed after the wizard — through an inline input squeezed into the card header, too narrow to show a long title. Genre and description, which both feed generation, were frozen at whatever the wizard or an import left behind. ## What changed **Edit dialog** - The pencil on a card opens an *Edit Story* dialog with title, genre and description together. Every field wraps and grows, so a long title or description is read and edited whole. Title and genre are single values that wrap for display: Enter does nothing in them and a pasted line break becomes a space. Ctrl/Cmd+Enter saves from any field. - One save writes every changed field in a single `updateStory`, replacing `renameStory`. A save that changes nothing writes nothing, so opening and saving does not bump the story up the library. A blank title cannot be saved; a blank genre or description clears it. - A muted line says that genre and description are used when generating new entries. **Genre colour** - Under the genre field, a folded *Badge colour* row previews the badge and unfolds into ten preset swatches plus *Default*. The pick is saved per story as `StorySettings.genreColor`, a palette key — no migration, and it travels through export, import and sync with the rest of the settings. - Without a pick, the six preset genres keep exactly the colours they had; any other genre stays neutral. An unknown key (say, from a newer build via sync) falls back to the name mapping rather than rendering uncoloured. **Closing** - Cancel, Escape, an outside click and a mobile swipe all close without saving, and ask *Discard your changes?* first when anything was edited. The desktop header drops its X, leaving Cancel. - On mobile, a swipe on an edited sheet is caught before vaul closes it: the sheet settles back and asks, rather than leaving the screen. **Scrolling** - One scroller: on desktop the fields between a pinned header and footer; on mobile the whole sheet, header and footer included. The fields drop `Textarea`'s half-viewport cap, which had given a long description a scrollbar inside a scrolling dialog. ## Notable - **The swipe guard leans on vaul internals.** vaul has no veto for a swipe close, but its content's `onpointerup` calls ours just before it measures the swipe. `createDrawerSwipeGuard` (in `ui/drawer/swipe-guard.ts`) predicts vaul's close with its own rule — distance past `closeThreshold`, or flick velocity — and, when the dialog is dirty, commits the sheet to rest so vaul finds nothing to act on, then animates the settle itself. It copies vaul's velocity threshold and settle transition, so it wants a recheck on `vaul-svelte` upgrades. If a dirty close ever slips through, a fallback lets the old drawer finish closing and release its scroll lock, then mounts a fresh one with the question. Reopening the same drawer mid-close left an invisible sheet holding the lock, freezing the app. - **`autosize` keeps scrolled ancestors in place.** Collapsing a textarea to `auto` to measure it could clamp a scrolled container, which jumped the dialog on every keystroke once the description outgrew it. This applies to every autosizing textarea. - **Shared components, backward compatible:** `ResponsiveModal.Header` gains `closeButton` (default on); `WizardExitConfirm` takes its explanation text as a prop (the wizard's wording is the default); the drawer's `closeThreshold` default is now the exported `DRAWER_CLOSE_THRESHOLD`. - **Enter on a card's own buttons no longer opens the story.** The card's Enter handler caught the key bubbling from its edit and delete buttons. - The dialog lives once in `LibraryView`, mounted fresh per edit, not in each card — so its fields never sit under the card's clickable root. ## Testing `npm run check`, `npm test` and `npm run lint` pass; 7 new unit tests cover the badge colour resolution, including preset colours unchanged, a saved pick winning, and an unknown key falling back. Tried on mobile during development: the drawer, the single scroller and the swipe-to-confirm path. Not yet exercised in a running app: export and re-import of a story with a picked colour, and a generation after editing genre and description (they reach the prompt through the unchanged context builder). 🤖 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 a dialog for editing story titles, genres, descriptions, and badge colors. - Added customizable genre badge colors with sensible fallbacks. - Added responsive editing with protection against accidentally discarding unsaved changes. - **Bug Fixes** - Prevented drawers from closing during protected editing. - Preserved scroll positions while text fields expand. - Avoided updating modification timestamps when story details are unchanged. - **Improvements** - Story edits are now handled through a consistent library-wide dialog. - Added options to hide desktop modal close buttons and customize wizard exit messaging. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
dcc02b72b8
|
Show entry numbers in the memory panel and the story view (#520)
## Why Entry numbers already exist and are already how a reader names a place in the story — the navigation panel jumps to one, the timeline panel labels its findings with one, a narration's Response info reports its own. But the number is only ever visible where one of those surfaces happens to print it. Reading the story itself, a reader cannot see where they are; and the Memory panel, which is entirely about which stretch of story a chapter covers, never named that stretch — a chapter card said `26 entries` and its expanded list showed untitled excerpts with no way back to the text they came from. ## What changed **Memory panel** - A chapter card reports the range of entries it covers beside the count, marked with a milestone glyph: `26 entries (3 → 29)`. The two figures are derived independently — on a story whose numbering has gaps they disagree, and both are true, so neither is adjusted to match the other. A one-entry chapter names its entry once rather than as a range to itself. - Each row of an expanded chapter shows its entry number, and the row itself jumps to that entry. The list says so above it. - The ten-row cap on that list is gone. It hid entries the card's new range advertises. **Story view** - A new Appearance setting, *Show Entry Number and Time*, off by default, puts a narration's number and the in-story time it covers on the entry, each marked by a glyph rather than a label. The number keeps a fixed place on the header at every width; the time sits there where there is room and moves to a row of its own where there is not. - Player actions and system entries are excluded: an action is an instant with no span to show, and a system entry is an application message rather than story. Where a narration records no time, it says so rather than showing nothing. **Entry header sizing** — a fix found along the way The header hid its chips and folded its toolbar on viewport breakpoints, which could never fire on desktop: `tauri.conf.json` floors the window at 800px and the gate sat at 640px. Worse, the thing that actually narrows the story column is a panel opening beside it, which a media query cannot see at all. The entry card is now a query container and its header gives way against its own width, in order: the in-story time drops to its own row, then the turn-duration chip goes, then the token count, then the buttons fold into the overflow menu. ## Notable - **The duration chip and its fallback are decided by one measurement.** The chip toggles the generation-activity timeline, and its fallback is a toggle in Response info — which renders in a portal outside the card, where no container query can reach. Gating the two separately left a band of widths where the chip had gone and the overflow menu had not yet arrived, so a report auto-opened during generation could not be closed. One measured card width now decides both, so exactly one of them exists at any width. - `jumpToEntry` consolidates the guard-request-switch-confirm sequence that `StoryNavPanel` and `BranchPanel` had each inlined. They had already drifted: one closed the nav panel on mobile and the other the sidebar; one reported "not in this branch" on a miss and the other reported nothing. - `retry` entries are treated as narrations. No code path creates one any more, and every other reader in the tree already treats an old `retry` entry as a narration. ## Testing `npm run check`, `npm test` and `npm run lint` all pass; 11 new unit tests cover `jumpToEntry` and `entryNumberRange`, including the gapped-numbering case. Not yet exercised: the narrowest header grades need a card under ~27rem, which an 800px window floor cannot produce — those want the Android emulator. The container also establishes a containing block for fixed-position descendants; the Response info popover portals to the document body and so escapes it, but the overflow menu is worth a look in a running app. 🤖 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 an option to show entry numbers and in-story times on story entries. * Chapter cards now display entry counts and entry number ranges. * Chapter entry lists now show all entries with numbered navigation buttons. * Improved navigation to entries, including mobile panel handling and clearer confirmations. * **UI Improvements** * Updated entry layouts to adapt across screen sizes. * Activity details remain accessible when space is limited. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
23df4e48da
|
Shrink the page for the Android keyboard, and read edge swipes off the shell (#521)
## What Two platform-level fixes on Android that every screen inherits. **The soft keyboard shrinks the page instead of covering it.** `enableEdgeToEdge()` opts the window out of the resizing `adjustResize` would do from API 30, and the soft-input mode resolved to pan. Nothing shortened the WebView, and Chromium was never told the keyboard existed: measured over DevTools on an API 36 emulator, `innerHeight` and `visualViewport.height` both stayed at the full screen with the keyboard up. The activity now takes the IME inset itself and sets it as the WebView's bottom margin, which shortens the laid-out view and with it the layout viewport. `AndroidBridge.getInsets()` reports the bottom inset net of the keyboard, so the navigation-bar strip the page reserved above it goes away. Below API 30 the window still resizes on its own, and the soft-input mode is set to resize explicitly so that path cannot resolve to pan. The bottom drawer's own `repositionInputs` is switched off, so the keyboard has one owner. **Edge swipes are read off the shell.** The invisible strips that caught edge swipes took every tap that landed on them, including the outer half of the header's panel button and of Send on a phone. The shell's own swipe action now receives every gesture and the handlers keep only those that start within the edge band. ## Why a reviewer should look at - `MainActivity.kt`: the inset listener passes the insets on to the view (`ViewCompat.onApplyWindowInsets`) so Chromium keeps seeing the cutout and system bars. - `docs/architecture/overview.md`, "Edge swipes" and "The soft keyboard": the rationale, including the alternatives that were tried and failed (padding the WebView, `interactive-widget=resizes-content`). - A panel must never reach the opposite edge's band, since swipes inside a panel bubble up to the shell. On a phone neither does: the sidebar is capped at 288px, the navigation panel at `100vw - 3rem`. ## Verified Emulator, API 36, story screen with the action input focused: | State | innerHeight | visualViewport | `--sab` | | --- | --- | --- | --- | | before, keyboard up | 800 | 800 | 48px | | after, keyboard up | 497 | 497 | 0px | | after, keyboard closed | 800 | 800 | 48px | `npm run check`, `npm run lint` and `npm test` pass. Not yet verified on a real phone, nor on a device below Android 11. 🤖 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 edge-swipe gestures for opening the sidebar or navigation panel while viewing a story. - Swipe detection adapts to desktop and mobile edge widths and only opens panels when appropriate. - **Bug Fixes** - Improved drawer input handling by disabling automatic input repositioning on Android while retaining it on other platforms. - Prevented swipe gestures from triggering when they start in text-entry fields. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |