Skip to content

Harden the API, fix data-loss paths, bound prompts, add installers and an eval harness - #5

Merged
JeremySNR merged 27 commits into
ccr-c6ab75dd-8lzqn8from
claude/repo-review-improvements-qj5cya
Oct 10, 2026
Merged

JeremySNR merged 27 commits into
ccr-c6ab75dd-8lzqn8from
claude/repo-review-improvements-qj5cya

Conversation

@JeremySNR

@JeremySNR JeremySNR commented Oct 10, 2026 •

Copy link
Copy Markdown
Owner

Fixes from a whole-repo review, plus the additions it recommended. Each commit is one change with its own tests.

Security

  • Local API locked to the app. Any web page could send no-preflight POSTs to 127.0.0.1:4317, and DNS rebinding gave full read/write, including pointing a model tier at an attacker's host with any env var as the bearer key. Every API call now needs a per-launch token (injected into the served index.html; pnpm dev shares one with Vite). Non-loopback Host and foreign Origin are refused, bodies must be JSON, git refs are validated before reaching git, and api_key_env must end in _API_KEY.
  • Electron shell hardened. Same-origin navigation only, openExternal limited to http/https/mailto, permissions denied, IPC only answered for the app's own origin.

Data loss and correctness

  • Commits contain only what each change wrote. git add -A swept unsaved autosaves into ripple commits, so reverting a ripple could undo unrelated typing. Ripples now checkpoint other work, then commit the edit alone. Minimal regeneration finds the card the prose was written from by hash.
  • Ripple reverts are all-or-nothing. A conflict aborts and names the files (409) instead of leaving the repo mid-revert. Failed proposals can be retried, and editing a proposal keeps the scene's lock and status.
  • Editor saves. A failed autosave keeps the text and says so. Saves are flushed on leaving a scene, refreshes can't overwrite newer typing, and leaving with unsaved edits asks first (also in Electron).
  • Model calls. Transient failures are retried with backoff, failed calls are logged in costs.jsonl, cache pricing is configurable per tier, OpenAI strict mode no longer forces invented values for optional fields, and the Codex child gets an allowlisted environment.
  • CLI and server. Without a terminal, choices that change the book need --yes (a piped quillgon change used to commit a ripple). --yes s01 parses correctly, serve --port 0 works, and server jobs run one at a time.

Scale

Measured with the new pnpm scale-check (sample grown to 96 scenes, 409 facts), at the last scene, approx. tokens:

Prompt Before After
Extractor 11,969 3,018
Card planner 12,744 3,804
Card reviser 11,368 2,700
Reader, whole run 743,440 132,006

Facts are scoped by relevance once the glossary passes 80, the reader keeps a six-scene window, the writer gets a bounded "story so far", and CI fails if any prompt exceeds 8,000 tokens.

Additions

  • Installers: pnpm dist bundles the server with esbuild and builds with electron-builder; release.yml attaches installers to a draft release on v* tags.
  • Eval harness: quillgon eval runs 20 frozen premises and scores slop, style, continuity, structural tells, twist guessing, and cross-book sameness; quillgon eval compare judges two runs both ways.
  • Testing: UI unit tests (happy-dom), CLI tests, a browser e2e job in CI, and ESLint. Tests: 106 → 221.

Verification

  • pnpm lint, pnpm test, pnpm typecheck, pnpm build, pnpm e2e, scripts/ui-refresh.ts, and pnpm scale-check --max-prompt 8000 all pass locally and in CI.
  • The bundled server was run from the merged tree: token enforced, sample opens, all prompts load.

Not verified

  • A packaged installer opening a book. That needs better-sqlite3 rebuilt for Electron, and the build environment's network blocked Electron's headers. The release workflow does the rebuild on GitHub runners but has never run, so macOS and Windows builds are untested.
  • Any live model call. Model-facing code is covered by mocked SDKs and offline runs.

🤖 Generated with Claude Code

https://claude.ai/code/session_016zsDekiv5nExWZqocNHWNL


Generated by Claude Code


Note

High Risk
Touches authentication for the local API, Electron security boundaries, and autosave/commit paths where bugs could block access or lose edits; mitigated by broad new tests and CI e2e, but packaged installers remain partially unverified on macOS/Windows.

Overview
Adds cross-platform desktop packaging (pnpm dist, esbuild server bundle, better-sqlite3 rebuilt for Electron) and a release.yml workflow that builds macOS/Windows/Linux installers and attaches them to draft v* releases. The Electron shell now runs either dev Node+tsx or the bundled server, with tighter navigation/IPC security and server crash recovery.

Locks the local API to the app: per-launch token on every request (injected into served HTML; dev shares one via scripts/dev.mjs / Vite), plus client changes for token headers, SSE auth/reconnect, and keepalive saves.

Reduces editor data-loss risk with debounced serial prose autosave (useProseAutosave), scene reload coordination, an unsaved-changes registry (in-app confirm + Electron close path), promise-based confirm/prompt dialogs, and clearer save-failure UI.

CI and quality gates add pnpm lint (new ESLint config), pnpm scale-check --max-prompt 8000, and a Chromium e2e job (Playwright + screenshot artifacts on failure). Docs cover installers, eval workflow (quillgon eval), and contributing steps.

Desktop UX/a11y improvements include modal focus traps, layered Escape handling, and expanded happy-dom unit tests for hooks and shell behavior. OpenAI-compatible adapter calls get timeouts and classified retryable errors.

Reviewed by Cursor Bugbot for commit f4699a0. Bugbot is set up for automated code reviews on this repo. Configure here.

claude added 19 commits October 10, 2026 19:32
Any web page could send no-preflight POSTs to 127.0.0.1:4317 (the server parsed
text/plain bodies as JSON), and DNS rebinding gave a hostile page full read and
write access, including pointing a model tier at its own host with any
environment variable as the bearer key.

- Require a per-launch token on every API request. The server injects it into
  the index.html it serves; `pnpm dev` shares one token between the API and
  Vite through a small launcher that replaces concurrently.
- Refuse non-loopback Host headers and foreign Origins; send CORS headers only
  for explicitly allowed origins.
- Accept only application/json request bodies, capped at 20 MB.
- Validate git refs and branch names before they reach git, so a value like
  `--output=...` cannot become an option.
- Restrict api_key_env to *_API_KEY names and base_url to http(s) URLs.
- Open a new book before closing the current one, so a failed open leaves the
  app usable.
- Validate the export format, return 500 for programming errors, use unique job
  ids, and resolve the sample fixture with fileURLToPath (Windows, spaces).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016zsDekiv5nExWZqocNHWNL
Every commit ran `git add -A`, so it swept in whatever else was dirty: edits
made in another editor, and prose the author had autosaved but not committed.
A ripple's origin commit could therefore contain an hour of unrelated typing,
and reverting the ripple threw it away. The same sweep committed an edited card
before minimal regeneration read "the committed card", so the reviser was
given identical old and new cards.

- GraphStore.commit stages only the files the store wrote (deletions
  included); GraphStore.checkpoint commits the whole tree for explicit
  checkpoints such as a new book or the step before a revert.
- A ripple first checkpoints other work, then commits the originating edit
  alone (changeFiles maps each change kind to its files). Each round commit
  holds only what the round wrote, tracked by write sequence.
- Minimal regeneration finds the card version whose hash matches the prose's
  generated_from in history, instead of trusting HEAD.
- The repo staging area is reset before each commit; a file written and
  removed before any commit no longer errors; dirty state survives a failed
  or non-repo commit.
- Prompt overrides are written through the store, so they are committed.
- .engine/ and export/ are added to .gitignore of pre-existing repos.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016zsDekiv5nExWZqocNHWNL
- Revert a ripple's commits as one commit with `git revert --no-commit`. If
  any of them conflicts with later work, abort, put HEAD and the tree back,
  and raise RevertConflictError naming the files (HTTP 409). Previously a
  conflict left the book mid-revert with the ripple still open.
- The history revert route uses the same all-or-nothing path.
- A proposal that failed (network, rate limit) is retried by proposing again;
  before, an errored item blocked the round and only rejecting it unblocked it.
- Accepting an edited card proposal keeps the scene's status and lock instead
  of resetting an approved or locked scene to Zod defaults.
- Ripple ids get a random suffix; second-granular ids collided.
- The revert fallback for edits committed before the ripple started now covers
  scene deletions and character edits, restoring their files from history.
- A wording-only prose edit on a scene whose card had also moved no longer
  marks it in sync, which hid the card change from the prose.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016zsDekiv5nExWZqocNHWNL
- Retry rate limits, overload, 5xx and dropped connections with exponential
  backoff (honouring retry-after), on top of the SDKs' own retries. Mid-stream
  failures, which the SDKs do not retry, are now retried. Refusals, bad
  requests, missing keys and exhausted quotas are not.
- Log usage when a call fails, including tokens already billed by earlier
  attempts; costs.jsonl under-counted exactly the expensive failure cases.
- Cache read and write prices are configurable per tier (price_cache_read,
  price_cache_write). Read prices range from 0.025x to 0.1x of input across
  current models, so a fixed 0.1x was wrong for some tiers.
- OpenAI strict mode now gets optional fields as required-but-nullable, and
  those nulls are stripped before Zod validation so defaults apply; strict
  mode used to force the model to invent a value for every optional field.
  Anthropic structured outputs keep optional fields optional.
- Only a rejection of the output schema downgrades an Anthropic schema to
  prompt-only JSON, not any 400 whose message mentions "format".
- The Codex child process gets an allowlisted environment (home, locale,
  temp, proxies, CA certificates) instead of everything except two OpenAI
  variables. Login advice is shown only for authentication errors, and a
  timeout says so.
- The OpenAI-compatible backend has a timeout (QUILLGON_COMPAT_TIMEOUT_MS)
  and typed, retryable errors; the Responses backend reuses its client and
  explains an incomplete response caused by the output limit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016zsDekiv5nExWZqocNHWNL
Measured with a new scale check (pnpm scale-check: the sample grown to 96
scenes and 409 facts), the extractor, card planner and card reviser each sent
the whole fact glossary (about 12k tokens at that size, growing with every
extraction), and the simulated reader sent its entire log to every scene, so a
reader run cost 743k input tokens and grew quadratically.

At the last scene, approx. tokens, before -> after:
  writer           8,310 -> 5,900
  extractor       11,969 -> 3,018
  card planner    12,744 -> 3,804
  card reviser    11,368 -> 2,700
  reader          14,996 -> 1,372 (whole run 743k -> 132k)

- Facts: once the glossary passes 80, send every fact the task names plus the
  most similar ones (local embeddings); the planner prefers facts no scene has
  revealed yet. Small books still get the whole glossary.
- Reader: the latest six log entries in full, plus the predictions and still
  open confusions from older scenes.
- Writer: a deterministic "story so far" from chapter summaries and earlier
  outcomes (bounded, oldest chapters dropped first), and style samples clipped
  to 6,000 characters.
- One words-per-scene rule for the planner and the writer; the writer's target
  ballooned when chapters were carded one at a time.
- New scene numbers ignore id suffixes ("s04-2" was read as 42).
- The cost estimate measures the writer's real context, prices the line editor
  as span edits rather than a whole-scene rewrite, and counts the reader
  window.
- `pnpm scale-check --max-prompt N` fails when any prompt exceeds N tokens.

Store writes stay at 9 ms (96 scenes) and 22 ms (300 scenes), so the index is
left as a full refresh per write.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016zsDekiv5nExWZqocNHWNL
`pnpm test` now also runs apps/desktop/src/**/*.test.{ts,tsx} in a
happy-dom vitest project ("ui"), with @testing-library/react for hooks
and components.

Outline flatten/rebuild move to lib/outline.ts so they can be tested,
and lib/util.ts gains deepEqual (used instead of JSON.stringify for
dirty checks). Tests cover util and the outline row helpers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016zsDekiv5nExWZqocNHWNL
Saves
- Extract SceneEditor's data logic into hooks: useProseAutosave,
  useSceneDetail, useStyleCheck (and useDraft for the style settings).
- Prose autosave failures are caught: the text is kept, a banner and the
  status bar say so, a toast offers Retry, and the next edit retries.
  Saves are serialised so an older PUT never lands after a newer one.
- Unmounting or switching scene flushes a pending save instead of
  dropping it; a failed flush after leaving still toasts with Retry.
  Closing the page sends a pending save with keepalive.
- A refresh (SSE-driven or after a job) no longer overwrites prose typed
  since the last save, nor a dirty card. Jobs flush prose first.
- cardDirty and the style-sheet dirty flag are memoised structural
  comparisons instead of JSON.stringify on every render; the style
  settings no longer reset local edits on every project refresh.
- Unsaved card and style edits are guarded: in-app navigation (go(),
  links, back/forward) asks via the app's Dialog; closing or reloading
  the window is blocked (Electron gets the in-app dialog, then closes).

Errors
- A global unhandledrejection handler toasts; errors already reported
  by job() are marked so they are not toasted twice.
- Catch the remaining unhandled rejections in welcome, build, shell
  (branches, voice check, export), history, ripple, style settings and
  the scene's impact and review dialogs.
- The shared EventSource reconnects with backoff after it closes and
  tells listeners to refresh after a drop (token handling unchanged).

Keyboard and accessibility
- One Escape stack: only the topmost layer (dialog, drawer, palette,
  branch menu) closes.
- useShortcuts attaches once (stable rail toggle, state via ref); the
  ripple key handler has deps and ignores keys while focus is in an
  input, textarea, select, contenteditable or a real button, or while a
  layer is open.
- Dialog and Drawer are labelled by their title, focus their first
  field on open, trap Tab, and restore focus on close.
- Field links its label to a single native control (useId, htmlFor,
  aria-describedby for hints) and names composite controls as a group.
- window.prompt/confirm replaced by the app's Dialog (DialogsProvider:
  useConfirm/usePrompt) for what-if naming, scene deletion, ripple and
  history reverts.
- The branch menu opens on ArrowDown, moves with Up/Down/Home/End and
  returns focus to its button; the ripple list uses a roving tabindex.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016zsDekiv5nExWZqocNHWNL
The sample is found relative to the server's source file, which does not
hold once the server is bundled into a single file for the desktop app.
The packaged app sets QUILLGON_FIXTURE_DIR; without it nothing changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016zsDekiv5nExWZqocNHWNL
`pnpm dist` (or `pnpm -C apps/desktop dist`) builds the UI, then
scripts/prepare-package.mjs stages:

- out/server/server.mjs: electron/server-entry.mjs with @quillgon/server and
  every workspace and npm dependency bundled by esbuild, except
  better-sqlite3. Next to it: the built UI, the sample novella and the
  agents' prompt files (read relative to the bundle).
- out/server/node_modules/better-sqlite3 (+ bindings, file-uri-to-path),
  from a prebuilt Electron binary if one exists, otherwise compiled with
  @electron/rebuild, then checked by loading it in Electron's Node.
- out/app: package.json and the two shell files.

electron-builder packs out/app into app.asar and ships out/server as
resources/server, so no native module lives in the asar. Targets: mac
dmg+zip, win nsis, linux AppImage+deb. Signing is optional: without
certificates the build is unsigned (ad-hoc on macOS).

The dev path (`pnpm desktop`: node + tsx from the source tree) is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016zsDekiv5nExWZqocNHWNL
- Startup failures show an error dialog and quit: a missing node binary
  (with what to install or set), the server exiting before it is ready, or
  no URL within 30 s (QUILLGON_STARTUP_TIMEOUT_MS), with the tail of the
  server's stderr.
- If the server stops later, a dialog offers Restart or Quit; restart
  starts a new server and reloads the window.
- Every web contents stays on the server's origin: will-navigate and
  will-redirect elsewhere are blocked, window.open is always denied, and
  only http, https and mailto URLs are handed to the default browser.
  Webviews are refused; IPC is answered only for the app's own pages.
- All permission requests and checks are denied.
- Packaged: runs resources/server/server.mjs with process.execPath and
  ELECTRON_RUN_AS_NODE=1; the server exits when the shell's pipe closes.
  QUILLGON_SERVER_BUNDLE runs a staged bundle from a source checkout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016zsDekiv5nExWZqocNHWNL
On a v* tag, build on macos-latest, windows-latest and ubuntu-latest with
`pnpm dist` (version from the tag) and attach the dmg/zip, exe, AppImage
and deb to a draft GitHub release. Signing and notarization secrets are
optional. workflow_dispatch builds the same artifacts without a release.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016zsDekiv5nExWZqocNHWNL
README: a "Desktop app and installers" section (what `pnpm dist` builds,
requirements, unsigned installers, Git, API keys from the environment,
Codex needing a checkout, and what has and has not been verified), and
the Electron note now covers both the dev and the packaged server.
CONTRIBUTING: how the packaging fits together and how to test it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016zsDekiv5nExWZqocNHWNL
- CI: a second job installs Playwright's Chromium and runs the full UI
  scenario (pnpm e2e) and the layout checks; the main job enforces the prompt
  budget with pnpm scale-check. The browser scripts find Chromium from
  $CHROME, a system install, or Playwright's own.
- Server jobs run one at a time (later ones report "queued"); they share one
  store, index and git repo. An external file edit that lands during a job is
  reloaded after the queue drains instead of swapping the project mid-job, and
  opening another book, reloading, branch switches, merges and deletes are
  refused (409) while a job runs.
- CLI: boolean flags no longer swallow the next argument (`--yes s01`).
  Without a terminal, confirmations and review choices refuse to answer
  themselves unless --yes is given; a piped `quillgon change` used to accept
  and commit a ripple.
- First CLI tests: argument parsing, validate and status, and the piped
  change flow with a scripted $EDITOR.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016zsDekiv5nExWZqocNHWNL
# Conflicts:
#	apps/desktop/package.json
#	pnpm-lock.yaml
`Number(flags.port) || 4317` turned 0 into 4317, so the desktop shell, which
asks for a free port, always took 4317 and clashed with a running
`pnpm serve`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016zsDekiv5nExWZqocNHWNL
- Electron: the UI blocks unload while a save is pending or failed, and
  Electron cancelled the close silently. A will-prevent-unload handler now
  asks whether to leave without saving.
- Codex: QUILLGON_CODEX_PATH points the SDK at the user's own Codex binary,
  which an installed desktop app does not ship.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016zsDekiv5nExWZqocNHWNL
The research doc puts an eval harness before tuning any prompt against a live
model; nothing measured quality across runs.

- evals/premises.yaml: a frozen set of 20 premises across genres, each with a
  fixed intent, so runs start from identical inputs.
- `quillgon eval` drives each premise through every gate (plot points,
  structure, scene cards, then the opening, the turn and the ending, or every
  scene with --all-scenes) and the simulated reader, then scores the books:
  slop and style violations per 1,000 words, continuity errors, structural
  tells, twists guessed early, engagement, and, across the run, shared
  character names, prose trigram overlap and plan diversity. Results and a
  Markdown report go to evals/runs/<id>/ (gitignored).
- `quillgon eval compare <a> <b>` judges each key scene against its
  counterpart in both orders (a split verdict is a tie) and reports B's win
  rate and the Elo difference it implies.
- Live runs ask for confirmation; --record/--replay keep model responses for
  re-scoring without new calls.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016zsDekiv5nExWZqocNHWNL
The code carried eslint-disable comments but the repo had no ESLint config, so
nothing was linted. Add a flat config with TypeScript's recommended rules and
React's rules of hooks (missing effect dependencies are warnings: most are
deliberate "refetch when the project changes" effects), run it in CI and the
contributing checklist, and fix what it found: unused imports and variables
and a regex with literal double spaces.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016zsDekiv5nExWZqocNHWNL
@vercel

vercel Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
quillgon Skipped Skipped Oct 10, 2026 9:04pm UTC

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Double prompt on Electron close
    • Removed the redundant will-prevent-unload native dialog so only the in-app discard confirm runs on Electron close.

You can send follow-ups to the cloud agent here.

Comment thread apps/desktop/electron/main.cjs Outdated
Remove the will-prevent-unload native dialog. beforeunload already opens
the in-app discard confirm, and preventing unload is what triggers
will-prevent-unload, so both prompts fired and could disagree.
- A ripple round commit no longer picks up an author's autosave in another
  scene that lands during a slow extraction.
- Reverting a scene deletion restores the state entries of characters who were
  not on the scene's card; a character edit's files include its state file;
  the revert fallback restores every file the originating commit touched from
  its parent, rather than guessing from the first file's history.
- Branch names follow git's own rules (apostrophes, accents, + and @ are
  fine); only names git refuses, and anything starting with "-", are rejected.
- Reverts checkpoint again if an autosave lands between the checkpoint and the
  revert, revert merged what-ifs against their first parent, and report an
  unknown commit or other non-conflict failure as such rather than as a
  conflict.
- simple-git resolves a failed command whose stderr is empty, so checks no
  longer pass -q where they rely on a rejection.
- Electron asks once on close with unsaved edits (the main process's native
  prompt), not twice.
- The fact-ranking cache keeps fact vectors only; one-off query vectors were
  filling it.
- The originCommit-less revert path no longer makes a duplicate commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016zsDekiv5nExWZqocNHWNL
…qj5cya' into claude/repo-review-improvements-qj5cya
The branch received a fix for the same double prompt from the other side
(removing Electron's will-prevent-unload dialog). With both fixes there would
be no prompt and Electron would silently refuse to close, so restore the
in-app prompt, which asks and then closes the window.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016zsDekiv5nExWZqocNHWNL

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Resolved by another fix: Delete ripple sweeps unrelated state
    • Confirmed real: changeFiles swept every dirty state.yaml; commit 1620e7d already records only characters whose state the deletion rewrote and scopes origin/revert to those files.

You can send follow-ups to the cloud agent here.

Comment thread packages/core/src/impact.ts Outdated
changeFiles took every uncommitted state.yaml for a scene deletion, so an
unrelated, uncommitted state edit joined the ripple's origin commit and a
revert undid it. GraphStore.deleteScene now returns the characters whose state
had an entry for the scene, the change records them, and both the origin
commit and the revert fallback use exactly those files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016zsDekiv5nExWZqocNHWNL
…artner

Conflicts resolved:
- README: the redesigned layout, with this branch's installer section, CLI
  notes and security notes placed inside it; the source-preview note now
  says installers can be built but none are published.
- API client and server: cover image URLs carry the token as a query
  parameter (an <img> or download link cannot send headers), and the server
  accepts ?token= only for that read-only GET and the event stream.
- Cover generation runs outside the job queue: it reads a snapshot and writes
  only covers/, so it neither waits for nor holds up store jobs.
- Keyboard shortcuts: the stable-callback handler with the new eighth view.

Also: run the covers e2e in CI, use the shared Chromium lookup there, restore
an icon import the covers nav needs, and let ESLint accept rest-omit
destructuring and read-before-assign lets used by the new tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016zsDekiv5nExWZqocNHWNL

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Close can drop unsaved prose
    • Keepalive unload flush now runs whenever prose hasUnsaved (not only while the debounce timer is armed), completing the close-path fix alongside the existing hasUnsaved registration.

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit 6fd0656. Configure here.

Comment thread apps/desktop/src/views/scene.tsx Outdated
Prose waiting to save, mid-save or failed was registered as unsaved only after
a failure, so closing the window did not ask, and the keepalive save covered
only a still-pending debounce. In Electron, closing quits the app and stops the
server, so recent typing could be lost.

The editor now registers its autosave's hasUnsaved() for window unload, with a
flush. In Electron, closing first saves what can be saved and closes without
asking if that was all; it asks only about what is still unsaved. Browsers
show their own prompt while prose is unsaved.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016zsDekiv5nExWZqocNHWNL
Register hasUnsaved for the unload guard so pending or in-flight prose
prompts before close, and keepalive-flush on unload whenever anything is
still unsaved—not only while the debounce timer is armed.
@JeremySNR
JeremySNR merged commit fba2c06 into ccr-c6ab75dd-8lzqn8 Oct 10, 2026
7 checks passed

This branch was previously deployed

1 inactive deployment
Preview — f4699a05 Deployed Oct 10, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants