Skip to content

feat(chat): make v0.0.1 standalone with Cave optional - #61

Merged
BunsDev merged 9 commits into
mainfrom
feat/standalone-chat
Sep 4, 2026
Merged

feat(chat): make v0.0.1 standalone with Cave optional#61
BunsDev merged 9 commits into
mainfrom
feat/standalone-chat

Conversation

@BunsDev

@BunsDev BunsDev commented Sep 1, 2026

Copy link
Copy Markdown
Member

Why

The app blocked every user behind a Cave connection gate. A first public
release could not be opened at all without a paired Cave instance, which
contradicts shipping v0.0.1 as a standalone product.

Cave Client v1 also exposes no write operationCAVE_OPERATIONS in
@opencoven/sdk-core lists 13 ops and none of them create a message. So no
amount of gate tuning would have made the app writable; reads and writes had to
become separate ports.

Design approved in docs/standalone-query-adapter-design.md (commit 2ed75f9).

What changed

New local layer (src/lib/local/)

File Role
chat-records.ts Record shapes + the ChatBackend port. commit() takes whole records so a message append and its conversation's updatedAt bump land atomically.
indexeddb-backend.ts Durable backend. Both object stores written in one readwrite transaction.
memory-backend.ts Fallback reporting isDurable() === false.
chat-store.ts In-memory index, keyset cursors, subscriptions.
local-query-adapter.ts The existing QueryAdapter interface over local storage.
chat-writer.ts WriteResult with 'unsupported' as a first-class outcome.
chat-source.ts Pairs an adapter with a writer.

Wiring

  • src/app.tsx — local source mounts immediately; Cave connects only after the
    user clicks Connect to Cave. The installation-ID gate now disables Cave
    pairing only
    (it feeds pairingIdentity and nothing else). A dropped Cave
    connection falls back to local rather than showing an empty view.
  • src/chat-shell.tsx — exactly two new optional props (revision, writer)
    plus a composer. The read path is unchanged.
  • src/chat-composer.tsx / .css — the composer.

Decisions worth reviewing

Keyset cursors, not offset. createManualPageWalk aborts a walk the moment
a cursor value repeats, and offset cursors repeat as soon as a row is inserted
mid-walk. Cursors therefore name the last row served ({v,t,i}, base64url).

The local adapter reimplements QueryAdapter rather than wrapping
createQueryAdapter.
Every reason that adapter exists — network latency,
request dedup, abort handling, TTL caching over a socket — is absent here. Reads
are synchronous map lookups; a cache in front of them would only add a window in
which the UI shows data the store has already replaced.

No assistant reply is fabricated. There is no model backend in this release.
The composer states that no reply will arrive rather than inventing canned text.

The memory fallback is never silent. When IndexedDB is unavailable the
composer says the messages are kept in memory only and will be lost.

APP_PHASE deliberately unchanged. It mirrors a value in
src-tauri/src/metadata.rs that the conformance lock pins, so it moves only
alongside a Rust change and a repin. Flagged for a follow-up, not smuggled in
here.

Verification

  • corepack pnpm typecheck — clean
  • corepack pnpm lint — clean except the 7 pre-existing noDescendingSpecificity
    warnings in src/demo/chat-demo.css (owned by the warnings lane)
  • corepack pnpm vitest run409 passed, 61 skipped, 0 failed
  • corepack pnpm build — succeeds
  • Real Chromium run against the built bundle: created a conversation, sent a
    message, reloaded the page and the message survived via IndexedDB, with
    zero console errors and the Cave source correctly disabled.

27 new tests cover cursor round-tripping, a full page walk driven through the
real createManualPageWalk, cursor-echo and no-repeat guarantees, commit
atomicity, rollback on backend failure, corrupt-row sanitizing, orphan-message
dropping, validation codes, and a shared adapter-contract suite. src/app.test.tsx
was rewritten for the opt-in Cave flow; the three e2e specs were updated to match.

Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com

Copilot AI lite review requested due to automatic review settings September 1, 2026 07:18

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR makes the Chat app usable as a standalone product by introducing a local storage-backed chat source (IndexedDB with in-memory fallback) and making Cave pairing optional/opt-in, while keeping the existing read-path QueryAdapter contract intact.

Changes:

  • Added a local chat persistence layer (ChatStore + backends) and a local QueryAdapter implementation.
  • Split read/write concerns via a ChatWriter so local is writable while Cave remains read-only.
  • Updated app and shell wiring/UI to mount local immediately, add a composer + “New conversation”, and gate Cave behind an explicit “Connect to Cave” flow (tests updated accordingly).

Reviewed changes

Copilot reviewed 18 out of 18 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/lib/local/chat-records.ts Defines local record shapes, backend port, and sanitization utilities.
src/lib/local/indexeddb-backend.ts Implements durable IndexedDB backend for local records.
src/lib/local/memory-backend.ts Adds non-durable in-memory backend fallback when IndexedDB is unavailable.
src/lib/local/chat-store.ts In-memory index + keyset pagination + atomic commit semantics over a backend.
src/lib/local/local-query-adapter.ts Implements existing QueryAdapter interface over the local store.
src/lib/local/chat-writer.ts Introduces a write port with explicit unsupported outcomes for Cave.
src/lib/local/chat-source.ts Packages adapter + writer into switchable sources (local vs cave).
src/lib/local/local-chat.test.ts Adds comprehensive unit coverage for local cursors, paging, writes, adapter contract, and fallback.
src/lib/app-metadata.ts Updates app metadata strings to reflect standalone/local-first positioning.
src/chat-shell.tsx Adds optional writer/revision props, composer rendering, and “New” conversation button.
src/chat-composer.tsx New message composer for local writes with durability disclosure and error display.
src/chat-composer.css Styling for source bar, Cave connect surface, composer, and “New conversation” button.
src/app.tsx Rewires startup: local mounts immediately; Cave becomes opt-in and source-switchable.
src/app.test.tsx Updates app tests for opt-in Cave, local-first behavior, and composer behavior.
e2e/connection-gate.spec.ts Adjusts browser expectations to local-first and composer behavior without Cave.
e2e/app.tauri-mock.spec.ts Updates mocked Tauri e2e flow to opt in to Cave before asserting Cave UI.
e2e/app.spec.ts Updates baseline e2e test to assert shell mounts without a blocking gate.
docs/standalone-query-adapter-design.md Adds design doc describing the standalone/local-first approach and constraints.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/lib/local/chat-store.ts
Comment thread src/chat-composer.tsx
Comment thread src/lib/local/indexeddb-backend.ts
BunsDev and others added 8 commits September 2, 2026 00:08
Design-only. No implementation code.

Records the constraints that shape the standalone chat lane, each
verified against the tree rather than assumed:

- Cave Client v1 exposes no write operation, so writes can never be
  routed to Cave and must not share the read port.
- installationId feeds Cave pairing only, so it must not gate app start.
- ChatShell couples to the QueryAdapter type, not to Cave, so a local
  implementation of the existing interface renders local data unchanged.
- manual-page-walk enforces exact cursor echo and no cursor reuse, which
  forces keyset rather than offset cursors.
- The conformance lock pins src-tauri and scripts, not the web layer.

Proposes three narrow ports (ChatStore, ChatWriter, ChatSource) with the
existing QueryAdapter interface kept byte-identical, IndexedDB
persistence with an explicitly non-durable memory fallback, and demotion
of ConnectionGate from entry gate to opt-in connect surface.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The app blocked every user behind a Cave connection gate, so a first
public release could not be used at all without a paired Cave instance.
Cave Client v1 also exposes no write operation, so no amount of gate
tuning would have made the app writable.

Adds a local chat source and demotes Cave from an entry gate to an
opt-in, read-only second source.

- src/lib/local/chat-records.ts: record shapes and the ChatBackend port.
  commit() takes whole records because appending a message also bumps its
  conversation's updatedAt, and a crash between those two writes would
  order a conversation by a timestamp no message justifies.
- src/lib/local/indexeddb-backend.ts: durable backend. Both stores are
  written in one readwrite transaction. Chosen over localStorage for the
  atomicity and for the absence of a ~5 MB ceiling.
- src/lib/local/memory-backend.ts: fallback that reports isDurable() as
  false, so the UI states plainly that nothing is being saved instead of
  implying persistence it cannot deliver.
- src/lib/local/chat-store.ts: in-memory index with keyset cursors.
  createManualPageWalk aborts a walk when a cursor repeats, and offset
  cursors repeat as soon as a row is inserted mid-walk, so cursors name
  the last row served rather than a position.
- src/lib/local/local-query-adapter.ts: the existing QueryAdapter
  interface over local storage. It reimplements rather than wraps
  createQueryAdapter because every reason that adapter exists (latency,
  dedup, aborts, TTL over a socket) is absent here; a cache in front of
  synchronous map reads would only add a window showing replaced data.
- src/lib/local/chat-writer.ts: WriteResult with 'unsupported' as a
  first-class outcome, so a Cave-backed composer disables itself rather
  than failing per keystroke.
- src/app.tsx: local source mounts immediately; Cave connects only after
  the user opts in. The installation ID gate now disables Cave pairing
  only, since it feeds pairingIdentity and nothing else. A dropped Cave
  connection falls back to local rather than showing an empty view.
- src/chat-shell.tsx: two optional props (revision, writer) plus a
  composer. The read path is unchanged.

No assistant reply is fabricated. There is no model backend in this
release, so the composer states that no reply will arrive rather than
inventing canned text.

APP_PHASE is deliberately unchanged: it mirrors a value in
src-tauri/src/metadata.rs that the conformance lock pins, so it moves
only alongside a Rust change and a repin.

Verified: typecheck, lint (7 pre-existing CSS warnings), 409 unit tests,
vite build, and a real Chromium run confirming a sent message survives a
reload through IndexedDB with no console errors.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: BunsDev <68980965+BunsDev@users.noreply.github.com>
…tion

Co-authored-by: BunsDev <68980965+BunsDev@users.noreply.github.com>
Document the implementation details that evolved from the original proposal so the design record does not describe the shipped PR as an unimplemented draft.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: BunsDev <68980965+BunsDev@users.noreply.github.com>
@BunsDev
BunsDev merged commit 0ae46f8 into main Sep 4, 2026
10 of 19 checks passed
@BunsDev
BunsDev deleted the feat/standalone-chat branch September 4, 2026 09:29
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