SSE event streaming POC + @opake/react hooks + shared lint config
Three interlocking changes landing as one coherent unit:
1. WASM-owned SSE consumer + TreeKeeper + watchDirectory
- Move SSE event consumption from TypeScript into Rust/WASM.
New `crates/opake-core/src/sse/` (parser, consumer, transport,
reconnect, mock) + `tree_keeper/` with per-context directory
trees that apply events incrementally and fire watcher
callbacks on change.
- `FileManager::watchDirectory(uri, handler)` subscription API.
Handler receives `DirectoryTreeSnapshot | null` (null = directory
deleted, watcher auto-closes).
- `Opake::startSseConsumer(url?)` / `stopSseConsumer()` with
cancellation via `sse_started` flag checked in the consumer
loop, plus `TreeKeeper::uninstall_all()` on stop so account
switches don't leave a previous user's ContentKeys resident.
- Proposal events dispatched via `schedule_proposal_sync` with a
per-keyring 2s debounce (bounded `thread_local!` HashMap, cleared
on fire or stop). Detached via `spawn_local` so the consumer
loop never blocks across an /api/keyrings round-trip.
- Delete old JS SSE path (`event-stream.ts`, `sse-consumer.ts`,
`SSEConfig`, `onWorkspaceUpdated`, `markWrite`). Tree idempotency
replaces write-correlation for self-echo handling.
- `directory-sync` TaskDef is now CLI-only. Web daemon has no
handler; core registry comment is explicit about this.
2. @opake/react subscription hooks
- New `FileManagerCache` (refcounted) + `OpakeProvider` with
automatic SSE start/stop tied to provider lifecycle (triggers
TreeKeeper wipe on unmount for account-switch safety).
- `useFileManager(keyringUri)`, `useDirectory(keyringUri, dirUri)`,
`useSseConsumer(appviewUrl?)` — subscription-based reads backed
by `watchDirectory`. State is commit-keyed so stale-deps don't
flash into view mid-transition.
- 32 unit tests via vitest + @testing-library/react + jsdom with a
hand-rolled MockOpake / MockFileManager.
- Existing react-query hooks (useTree, useUpload, etc.) stay; add
@deprecated JSDoc pointing at useDirectory.
3. Unified ESLint + Prettier config across workspaces
- Root `eslint.config.base.ts` exports `makeBaseConfig` with the
shared strict-TS, react-hooks, functional, security, sonarjs
rule set. Each package's own `eslint.config.ts` imports it and
layers file-level overrides for legitimate exceptions.
- Root `.prettierrc` / `.prettierignore` (apps/web keeps its own
prettierrc for the Tailwind plugin).
- Add `lint` / `format` scripts to @opake/sdk, @opake/daemon,
@opake/react matching apps/web.
- Hoist eslint deps into root package.json; remove duplicates
from apps/web.
- Fix real findings: React 19 `react-hooks/refs` anti-pattern in
OpakeProvider (useRef-during-render replaced with useMemo),
prefer-optional-chain in file-manager.ts, use-unknown-in-catch
on rejection callbacks.
Review fixes folded in from pre-commit Opake Review pass:
- 60s-fallback comments rewritten to tell the truth about web vs CLI.
- `watcherInitialFireConsumed` eager-fire swallow removed; closes
the drift window between syncAndLoadTree and watcher installation.
- `currentDirectoryUri` cleared synchronously on deletion before
dispatching loadDirectory(null).
- `SseEvent::keyring_uri()` method with exhaustive match so a new
proposal variant becomes a compile error.
Deferred (tracked in memory cleanup sweep):
- `stop_sse_consumer` reaching directly into TreeKeeper — ideally
moves to `WasmOpakeHandle::wipeState()` so a future logout-without-
SSE path can share the wipe.
- Provider inlines `useSseConsumer` start/stop logic — different
semantics, unifying costs more than it saves.
- document_update proposals can't route to workspace owners via
SSE (lexicon has no keyring field); fix pending document editing
landing in the web client.