* feat(electron): error handling foundation
Adds the infrastructure for a coherent client-side error story:
- `lib/errors.ts`: canonical ApiError + QuotaExceededError, `toUserMessage`
(friendly strings for ApiError/ZodError/network errors, strips Electron
IPC message prefixes), `logError` (expected), `reportError` (unexpected).
- `lib/query-client.ts`: QueryClient factory with sane retry defaults (no
retry on 4xx except 408/429, 2 retries otherwise; 0 mutation retries),
`QueryCache` onError logs + opts in via `meta.toastOnError`, and
`MutationCache` onError toasts `toUserMessage(err)` by default with
`meta.suppressToast` as the opt-out.
- `components/app-error-boundary.tsx` + `error-fallback.tsx`: two boundaries
(top-level outside the router, route-level inside) with a Card-based
fallback offering 'Go home' + 'Try again'. Route boundary resets on
pathname change and clears React Query error cache on retry.
- `main/ipc-utils.ts` + main.ts migration: `safeHandle` wraps ipcMain.handle
so main-process failures log with full stack and surface a sanitized
message to the renderer. `link:fetch-metadata` keeps its null contract
but now logs.
- `useCreateParticle` opts out of the global toast (compose-overlay renders
its own quota UX) so nothing double-toasts.
Render crashes now have a recovery UI, every mutation gets a free error
toast, and silent-catch cleanup + Sentry land in follow-up PRs.
* refactor(errors): defer mutation errors to global handler
Now that MutationCache toasts via toUserMessage by default, the per-hook
onError duplicates drop away. Also normalizes inline query-error UI and
surfaces a previously-silent failure.
Mutations — removed redundant onError toasts:
- network-selector: useAcceptInvitation, createNetwork (inline)
- network-settings: useInviteMembers, useRevokeInvitation, useRemoveMember
- network-billing: useCreateCheckoutSession, useCreatePortalSession
(onSuccess toasts stay — they carry domain context like network name)
Queries — consistent inline error UX via toUserMessage:
- network-selector: failed-to-load state gets a "Try again" button
- network-billing: "Couldn't load billing" includes friendly reason
- network-settings: useNetworkInvitations failure now surfaces a hint
(previously rendered as "0 pending" — silently wrong)
Trimmed noisy JSDoc from PR 1 files (errors.ts, query-client.ts,
app-error-boundary.tsx, error-fallback.tsx, ipc-utils.ts).
* refactor(errors): route silent catches through logError/reportError
Every catch now either surfaces, re-throws, or calls logError with a scope
tag. No more empty catches or bare console.error:
- auth-store: signInToFirebase / restoreSession / signOut paths gain
logError context. Behavior is unchanged (best-effort local sign-out,
fall back to login on restore failure).
- use-stream-autoplay: Audio.play() and download-URL fetches log their
failures instead of dropping silently (both are nice-to-haves so UX
stays silent — but we can now trace "why didn't autoplay trigger?").
- pusher-client: ws errors / parse failures / server errors / listener
crashes all routed through logError, and listener bugs (which silently
break user flows) now go through reportError so they're actually
surfaced in observability.
- settings-page: email-notifications toggle now toasts on failure
instead of silently reverting with no explanation.
- huddle-app: screen-share failures use logError.
* feat(errors): add Sentry observability behind a sinks facade
logError / reportError now route through a pair of sinks installed at
bootstrap by each process (main + every renderer entry). No call site
knows about Sentry — if sentryDsn is empty, the sinks simply aren't
installed and logError/reportError stay console-only.
- appConfig.sentryDsn: per-env string (empty for now — populate when
ops creates the DSNs). Empty is the no-op mode for dev.
- lib/errors.ts: installErrorSinks({ capture, breadcrumb }) gates
Sentry.captureException / Sentry.addBreadcrumb. Everything flows
through toUserMessage and the two existing call types.
- lib/sentry.ts: initSentryRenderer() for the main window + autoplay,
huddle, and screen-record renderers.
- main/sentry.ts: initSentryMain() runs before anything else in
main.ts to catch bootstrap failures. Captures uncaught exceptions
and the crash reporter automatically.
- main/ipc-utils.ts::safeHandle now routes through reportError.
- main.ts::fetchLinkMetadata logs via logError.
* chore: cleanup reexports
* fix(errors): use relative import so main-process bundle resolves
Rollup in vite.main.config.ts doesn't know about the `@/` alias (only the
renderer configs do). After PR 4 made `lib/errors.ts` reachable from main,
its `@/config/env` import broke the main build with:
[vite]: Rollup failed to resolve import "@/config/env" from "lib/errors.ts"
Swap to a relative import — the module is now genuinely isomorphic.
* build(vite): resolve @/ alias in main + preload configs
The four renderer vite configs all resolve "@" to ./src; main and preload
didn't, so any main-reachable file using @/ imports would break the
bundle. Mirror the same alias block in both configs and revert
lib/errors.ts + src/main/* to the project's @/ convention.
* fix invalid tsconfig field
* comment
* feat(errors): fallback handlers for unhandled rejections and errors
Sentry's global integrations natively capture window.onerror /
unhandledrejection (renderer) and uncaughtException / unhandledRejection
(main). When a DSN is configured we let Sentry own those paths to avoid
double-capturing. When it isn't (dev, unconfigured prod), we attach
minimal listeners that route through reportError so stray promise
rejections and uncaught errors at least hit the console and the facade.
This closes the one gap where a bare click-handler promise rejection
(no try/catch, not a useMutation) would otherwise be invisible.
* Revert "feat(errors): fallback handlers for unhandled rejections and errors"
This reverts commit ed84b29c6b.
---------
Co-authored-by: Claude <[email protected]>
Co-authored-by: Arjun from Flowy Labs <[email protected]>
Adds an implicit-consent disclaimer under the sign-in "Continue" button
and a new "Legal" section in the settings page. Both link out to the
policies hosted on flowylabs.ai via the existing openExternal bridge.
https://claude.ai/code/session_01U6gT7XFQ8Rtm3FStsHG63j
Co-authored-by: Claude <[email protected]>
* setup firebase custom token
* docs
* docs
* feat: allow admin removing members from a network
* fix: properly handle fallback avatar and names
This is especially helpful in the case of members who were removed from
a network
Hardcoded ⌘ glyphs in compose hints were misleading on non-mac
platforms. Add a tiny platform helper sourced from the existing
window.electronWindow.platform bridge and substitute "Ctrl" off mac.
Closes#167.
https://claude.ai/code/session_01YZY4wUqACT7mgibHr2Yncx
Co-authored-by: Claude <[email protected]>
Lets a particle's creator delete their own message from the TopBar
dropdown. Other viewers see a "This particle was deleted" tombstone in
place and playback auto-advances after ~2s, keeping indices stable for
concurrent watchers.
- Add optional deleted_at / deleted_by_human_id to non-container
particle variants and isParticleDeleted helper.
- Add softDeleteParticle Firestore helper.
- New DeleteParticleOverlay confirmation and DeletedParticleView
tombstone.
- Hide reactions (bar + 1-7 keybinding) on tombstoned particles.
- Show "Deleted particle" + Trash2 icon in the stream list preview.
Closes#146https://claude.ai/code/session_01M2ShnZPvWfQzzvuu3Xm8b9
Co-authored-by: Claude <[email protected]>
Closed streams grow unbounded as teams age and can reach 1000s of items,
while open streams stay bounded by active work. Switch the streams list
to subscribe per-status so each tab only pulls what it renders, and cap
the closed tab to a 50-item window that grows via a Load more action.
The open tab remains unbounded so per-row realtime features — autoplay,
huddle indicators — keep full coverage of the streams that matter.
Closed streams are archived and don't need push behavior, which side-
steps the autoplay baseline-reset and huddle-on-page-2 risks that made
pagination complicated for the open tab.
Refs flowy-live/llink#138.
Co-authored-by: Claude <[email protected]>
* setup infra for pusher service
* setup client sdk for pusher service
* fix: ping parse failure
* fix: send pong back to client
avoid disconnections every 2.5 minutes
* increase replicas
* feat: show presence and compose indicator
* refactor: use interfaces for function params
* cleanup message retention code
* support open / closed streams
- Tabs for viewing separately
- Context menu to close / open streams
- Update stream particle status field
* ui tweak
* show stream state in stream-view
* ui tweaks
* cleanup message retention from orion api
* add token endpoint for livekit
* fix: invalid type passed to hook
* fix: inject livekit env variables for orion
* fix: show controls indicator above stream # shortcut
* return livekit server url from api
* simple huddle implementation with streams
* set human name in livekit room context
* simplify deployment tooling
* join huddle with audio automatically
* feat: show when there is an active huddle
This introduces a webhook which listens to events from livekit and
updates our firestore stream particle. It keeps the client simple,
reacting to changes to firestore docs.
* use headphones icon for huddles
* fix: screenshare not working in electron
The default VideoConference component from livekit doesn't support
screenshare in electron. This attempts to compose our own layout with
livekit components ourselves and introduces our own flow for
screenshare.
* prototype with two windows
* use deps in separate window render process
* enhance autoplay into desktop overlay
* fix: autoplay window not applying tailwind styles
This generates the transcript and shows the caption experience on the
client side for media particles. It also simplifies other side effects
that we must perform such as updating the `last_child_created_at` field
for stream and container particles.
Making the firestore subcollections the same name helps with creating
indices and queries across subcollections of different depths. This also
prevents the need for conditional logic for network root / top-level
particles.
* refactor: update api and client to reference humanIds
* fix: prevent deletion of network member
This may cause various side effects if there is data in other services
which reference this member
This does a major refactor to using particleId for state of playback
instead of index, since index would change if the particle children data
does partial hydrations and a particle's index changes. Also it
abstracts much of the playback details from the view component.
* chore: only set visibility for container particles
* create reusable controls indicator for reply or new
* compress the size of top bar
* refactor: restructure state, routing, and more
* introduce stream compose flow
* feat: compose new stream full flow
* implement stream player
* fix: prevent redirect for signed object urls
* fix: implement stream playback cleaner structure
* refactor: layout file name
* feat: show stream name in breadcrumbs
* chore: tweak padding
* chore: adjust position of audio bars
* feat: show latest particle preview in stream list
* fix: remove console log
* refactor: reorder classes
* fix: avoid passing in updated_at to firestore particle
* refactor: extract properties for container particles to flat fields in firestore
* make the stream previews look alive
* feat: show audio bars during audio clip playback
* feat: order streams by last child creation
* feat: playback where I left off
* chore: remove unused store
* fix: recording mode not using shared state
* chore: clean unused variable
* remove unused imports
* fix: improve controls indicator immersion
* feat: show playback progress in bar & auto-play text
* feat: auto-exit stream on playback completion
* fix: jittery media playback progress
* fix: navigate during state change is invalid with react router
* fix: buggy exit progress when changing clips
* feat: add app icon
* update package.json info
* feat: only show streams visible to me
* feat: show seen indicator on particles
* fix: prevent unnecessary effects
* fix: play new particle after playback is ended
* use contols indicator for exit timer
* plumb for firestore
* chore: cleanup orion api to only include essentials
* setup boilerplate for data and rendering
This includes zod types creation for API response validation, and
exploration of path based resolution of rendering particles.
* chore: structure container particles for rendering children
* wire firestore crud for particles
* integrate visibility to particles
* docs: explain particle view resolver