Skip to content

Add Expo kitchen sink example#25

Open
batuhan wants to merge 1 commit intomainfrom
feat/examples-kitchen-sink-cleanup
Open

Add Expo kitchen sink example#25
batuhan wants to merge 1 commit intomainfrom
feat/examples-kitchen-sink-cleanup

Conversation

@batuhan
Copy link
Member

@batuhan batuhan commented Mar 8, 2026

Summary

  • add a new Expo Router kitchen sink example under examples/kitchen-sink
  • switch the example to the latest published @beeper/desktop-api release instead of a vendored local SDK copy
  • align the app entry/config with current Expo CLI defaults and add a local Yarn lockfile

Testing

  • npx expo config --json
  • npx expo export --platform web --output-dir /tmp/kitchen-sink-export

@coderabbitai
Copy link

coderabbitai bot commented Mar 8, 2026

📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • Added a comprehensive kitchen-sink example application demonstrating core functionality
    • Implemented a tabbed interface with Inbox for viewing chats, Search for finding messages and conversations, Lab for testing operations, and Settings for profile management
    • Added real-time chat updates with WebSocket support and automatic polling fallback
    • Enabled cross-profile connection management with token-based authentication
    • Included search capabilities across chats and messages with result filtering

Walkthrough

This PR introduces a complete kitchen-sink example application for Expo/React Native, featuring profile management, real-time chat with WebSocket/polling transport fallback, cross-target search, and a capability lab for testing. Includes app screens, reusable components, API client integration, Tanstack Query-based data fetching, persistent profile storage with cross-platform token handling, and type definitions.

Changes

Cohort / File(s) Summary
Project Configuration
examples/kitchen-sink/.gitignore, examples/kitchen-sink/package.json, examples/kitchen-sink/tsconfig.json, examples/kitchen-sink/app.json
Establishes project metadata, dependencies, TypeScript configuration with strict mode, module aliases, and build artifact exclusions.
Type Definitions
examples/kitchen-sink/types/profile.ts, examples/kitchen-sink/types/view-models.ts
Defines connection profiles, transport modes, probe metadata, capabilities, inbox items, search results, and lab operations for strongly-typed state management.
App Layout & Navigation
examples/kitchen-sink/app/_layout.tsx, examples/kitchen-sink/app/(tabs)/_layout.tsx, examples/kitchen-sink/app/+not-found.tsx
Implements root layout with provider initialization and profile bootstrapping; tab-based navigation with four main screens; 404 fallback screen.
Screen Components
examples/kitchen-sink/app/(tabs)/index.tsx, examples/kitchen-sink/app/(tabs)/search.tsx, examples/kitchen-sink/app/(tabs)/lab.tsx, examples/kitchen-sink/app/(tabs)/settings.tsx, examples/kitchen-sink/app/chat/[chatID].tsx
Implements inbox with responsive chat list and thread panel; full-text search across chats and messages; capability lab for testing SDK operations; profile/settings management; and dynamic chat route screen.
Presentational Components
examples/kitchen-sink/components/active-profile-banner.tsx, examples/kitchen-sink/components/chat-row.tsx, examples/kitchen-sink/components/empty-state.tsx, examples/kitchen-sink/components/json-panel.tsx, examples/kitchen-sink/components/screen-shell.tsx, examples/kitchen-sink/components/section-card.tsx, examples/kitchen-sink/components/status-chip.tsx, examples/kitchen-sink/components/search-result-card.tsx, examples/kitchen-sink/components/profile-editor.tsx, examples/kitchen-sink/components/thread-panel.tsx
Reusable UI components for profile display, chat rows, search results, settings sections, JSON output, and GiftedChat-based message threading with transport mode indicators.
Custom Hooks
examples/kitchen-sink/hooks/use-active-profile.ts, examples/kitchen-sink/hooks/use-realtime-transport.ts
Provides external store synchronization for profile state and initializes per-profile real-time transport with chat ID subscriptions and polling fallback.
API Client & Session Transport
examples/kitchen-sink/lib/api/client.ts, examples/kitchen-sink/lib/api/session.ts
Creates type-safe desktop API client with environment-specific configuration; implements per-profile WebSocket session management with automatic polling fallback, attachment-based subscriptions, and query cache invalidation on message/chat events.
Query Management
examples/kitchen-sink/lib/query/query-client.ts, examples/kitchen-sink/lib/query/keys.ts, examples/kitchen-sink/lib/query/queries.ts, examples/kitchen-sink/lib/query/mutations.ts
Configures Tanstack Query client with 5s stale time and 5m garbage collection; defines typed query keys and option builders for profiles, chats, messages, search, and lab operations; implements mutations for profile lifecycle and message sending with cache invalidation.
Storage & Persistence
examples/kitchen-sink/lib/storage/profiles.ts, examples/kitchen-sink/lib/storage/tokens.ts
Implements pub-sub profile store with localStorage/secure storage persistence, supports active profile tracking, probe result caching, and token management with web/native environment detection.
Data Mapping & Utilities
examples/kitchen-sink/lib/mappers/index.ts
Transforms desktop API types (DesktopChat, DesktopMessage, DesktopInfo) into view models (InboxChatItem, GiftedSDKMessage, SearchResultItem) with derived fields, timestamp formatting, network labels, and capability flag derivation.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant App as App/RootLayout
    participant Store as ProfileStore
    participant QueryClient as Tanstack Query
    participant Transport as SessionTransport
    participant API as Desktop API
    
    User->>App: Launch app
    App->>Store: initialize()
    Store->>Store: Load profiles from localStorage
    Store-->>App: Return snapshot
    
    Note over App: Profile ready, render InboxScreen
    
    User->>App: Inbox loads
    App->>QueryClient: Fetch chats query
    QueryClient->>API: createDesktopClient(activeProfile)
    API-->>QueryClient: Client ready
    QueryClient->>API: client.chats()
    API-->>QueryClient: Return chat list
    
    par Realtime Setup
        App->>Transport: useRealtimeTransport(activeProfile)
        Transport->>API: WebSocket connect (chat subscriptions)
        alt WebSocket available
            API-->>Transport: Connected (realtime mode)
        else WebSocket unavailable
            Transport->>API: Polling fallback enabled
            API-->>Transport: Polling mode
        end
    end
    
    Transport->>QueryClient: Invalidate chats on message upsert
    QueryClient->>API: Refetch affected chats/messages
    API-->>QueryClient: Updated data
    
    User->>App: Click chat item
    App->>QueryClient: Fetch messages for chatID
    QueryClient->>API: client.messages(chatID)
    API-->>QueryClient: Return message list
    App->>App: Render ThreadPanel with GiftedChat
    
    User->>App: Send message
    App->>QueryClient: useSendMessageMutation
    QueryClient->>API: client.sendMessage()
    API-->>QueryClient: Message created
    QueryClient->>QueryClient: Invalidate chat/message caches
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding a new Expo kitchen sink example. It is specific, accurate, and directly reflects the primary objective of the changeset.
Description check ✅ Passed The description is well-related to the changeset, providing specific details about the new example, the switch to published SDK, config alignment, and testing steps taken.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/examples-kitchen-sink-cleanup

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 16

🧹 Nitpick comments (4)
examples/kitchen-sink/components/search-result-card.tsx (1)

35-38: Prefer the view-model's timestampLabel when it's available.

SearchResultItem already carries a presentation-ready timestamp label, but this recomputes a locale string during render. Using the prepared label first keeps web/native output consistent and avoids dropping mapper-specific formatting.

Suggested fix
-      {item.timestamp ? (
+      {item.timestampLabel ? (
         <Text selectable style={{ color: "#8e755e", fontSize: 12 }}>
-          {new Date(item.timestamp).toLocaleString()}
+          {item.timestampLabel}
         </Text>
+      ) : item.timestamp ? (
+        <Text selectable style={{ color: "#8e755e", fontSize: 12 }}>
+          {new Date(item.timestamp).toLocaleString()}
+        </Text>
       ) : null}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/kitchen-sink/components/search-result-card.tsx` around lines 35 -
38, The render currently creates a locale string from item.timestamp; change it
to prefer the view-model's presentation-ready label by using item.timestampLabel
when truthy and falling back to new Date(item.timestamp).toLocaleString()
otherwise. Update the JSX inside the conditional that renders the timestamp (the
block referencing item.timestamp) to display item.timestampLabel first, ensure
the code performs a safe truthy check on item.timestampLabel, and keep the
existing Text props/style and fallback behavior unchanged.
examples/kitchen-sink/types/profile.ts (1)

1-1: Reuse the shared DesktopInfo type here.

ProbeSummary.info is a hand-copied version of the DesktopInfo model already exported from examples/kitchen-sink/lib/api/client.ts, so the two shapes can drift independently. Pulling the shared type in here keeps the probe payload aligned with the client contract.

Suggested refactor
+import type { DesktopInfo } from '../lib/api/client';
+
 export type ConnectionKind = "easymatrix" | "desktop-api" | "unknown";
@@
-  info?: {
-    app: {
-      bundle_id: string;
-      name: string;
-      version: string;
-    };
-    platform: {
-      arch: string;
-      os: string;
-      release?: string;
-    };
-    server: {
-      base_url: string;
-      hostname: string;
-      mcp_enabled: boolean;
-      port: number;
-      remote_access: boolean;
-      status: string;
-    };
-    endpoints: {
-      mcp: string;
-      spec: string;
-      ws_events: string;
-      oauth: {
-        authorization_endpoint: string;
-        introspection_endpoint: string;
-        registration_endpoint: string;
-        revocation_endpoint: string;
-        token_endpoint: string;
-        userinfo_endpoint: string;
-      };
-    };
-  };
+  info?: DesktopInfo;

Also applies to: 25-57

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/kitchen-sink/types/profile.ts` at line 1, Replace the hand-copied
DesktopInfo shape in this file with the shared DesktopInfo type exported from
examples/kitchen-sink/lib/api/client.ts: import the DesktopInfo type and use it
for ProbeSummary.info (and any other duplicated types between lines ~25-57)
instead of re-declaring the fields; keep ConnectionKind as-is but remove the
duplicate interface/alias definitions and update any references to the local
copy to reference DesktopInfo so the probe payload matches the client contract.
examples/kitchen-sink/lib/storage/profiles.ts (1)

53-65: Use the SDK's canonical local default URL here.

The SDK source treats only http://localhost:23373 as its built-in local default. Seeding the example with http://127.0.0.1:23373 makes it look "overridden", so any localhost-specific behavior behind that check won't apply. Please verify whether this should match the SDK default exactly.

Suggested change
-      baseURL: "http://127.0.0.1:23373",
+      baseURL: "http://localhost:23373",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/kitchen-sink/lib/storage/profiles.ts` around lines 53 - 65, The
default profile in makeDefaultProfiles currently sets baseURL to
"http://127.0.0.1:23373" for the "easymatrix-local" ConnectionProfile; update it
to use the SDK's canonical local default "http://localhost:23373" so the SDK's
localhost-specific behavior/triggers recognize it. Change only the baseURL value
in the profile with id "easymatrix-local" inside makeDefaultProfiles to match
the SDK default.
examples/kitchen-sink/lib/api/client.ts (1)

142-148: Avoid hiding SDK drift behind as unknown as DesktopClient.

This cast removes the compiler's ability to tell us when the published @beeper/desktop-api surface diverges from the hand-written DesktopClient contract. A thin adapter object that satisfies DesktopClient keeps the example honest when the package changes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/kitchen-sink/lib/api/client.ts` around lines 142 - 148, The code is
hiding API drift by using "as unknown as DesktopClient" on the BeeperDesktop
instance; instead create a thin adapter object that explicitly implements the
DesktopClient surface and delegates to the BeeperDesktop instance so the
TypeScript compiler can check compatibility. Instantiate BeeperDesktop (keep
baseURL, accessToken, dangerouslyAllowBrowser, timeout, maxRetries) into a local
variable (e.g., beeper) and return an object that maps the DesktopClient
methods/properties to beeper's methods/properties and use the "satisfies
DesktopClient" operator on that adapter to ensure the published
`@beeper/desktop-api` shape is enforced at compile time. Ensure all required
DesktopClient members are forwarded so the adapter fails to compile if the
upstream API changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@examples/kitchen-sink/app/`(tabs)/index.tsx:
- Around line 52-56: The effect that syncs selection to the visible list
(useEffect referencing wideLayout, selectedChatID, filteredChats,
setSelectedChatID) must check whether selectedChatID still exists in
filteredChats and update accordingly: if wideLayout is true and selectedChatID
is missing from filteredChats, setSelectedChatID to filteredChats[0]?.id (or
undefined/null to clear if filteredChats is empty); keep the existing behavior
that picks the first visible chat when there is no selection, but add the
missing-membership check so the wide layout never renders a stale thread.

In `@examples/kitchen-sink/app/`(tabs)/lab.tsx:
- Line 38: The Run button currently only increments runNonce but runNonce is not
used in the query key so clicking Run with identical operation/params doesn't
re-execute; modify the component to snapshot the submitted request when the Run
button is pressed (e.g., create a submittedRequest state holding {operation,
params, runNonce} and update it in the Run handler via setSubmittedRequest) and
then use that submittedRequest as the React Query key/input for the data fetch
(replace direct use of operation/params in the query with
submittedRequest.operation/submittedRequest.params and include
submittedRequest.runNonce in the key) so every Run press (even with same inputs)
triggers a new fetch; update references around runNonce, setRunNonce, operation,
params, and the query hook to read from submittedRequest instead of the live
inputs.

In `@examples/kitchen-sink/app/`(tabs)/search.tsx:
- Around line 87-101: The UI currently treats failed searches the same as
zero-result searches; update the render logic in the search component (around
chatsQuery, messagesQuery, combined, SearchResultCard, EmptyState, router.push)
to surface errors separately: if chatsQuery.isError or messagesQuery.isError
render an error EmptyState (or distinct ErrorState) showing the corresponding
error message(s) from chatsQuery.error and messagesQuery.error (and short
contextual text like "Chats search failed" / "Messages search failed"),
otherwise keep the existing pending/results/no-results branches; ensure the "No
results" branch only runs when both queries are not pending, not error, and
combined.length === 0.
- Around line 21-44: The "No results" empty state is shown even when searches
are disabled (no activeProfile); update the results rendering logic to only
display empty-results when a real search ran — i.e., check the query status of
chatsQuery and messagesQuery (use their isIdle/isFetched or isSuccess flags and
.isError as needed) and/or the enabled condition (activeProfile &&
submittedQuery.trim()) before rendering the "No results" UI, and similarly
adjust any duplicate logic around lines 83-103; reference chatsQuery,
messagesQuery, activeProfile, and submittedQuery when making the conditional
change so the empty state appears only if at least one query was actually
executed.

In `@examples/kitchen-sink/app/chat/`[chatID].tsx:
- Line 11: The array literal passed to useRealtimeTransport is recreated each
render causing normalizedChatIDs to change; memoize it first (derive a stable
chatIDs value from chatID using useMemo with [chatID] as the dependency) and
pass that memoized chatIDs into useRealtimeTransport (keep existing
activeProfile). This ensures useRealtimeTransport and its internal
useMemo/useEffect (in use-realtime-transport.ts) only react when chatID actually
changes.

In `@examples/kitchen-sink/components/profile-editor.tsx`:
- Around line 24-30: The effect using useEffect to rehydrate state from
initialProfile is overwriting in-progress edits when the profile object identity
changes; instead only initialize or reset when the actual profile values change
(or on mount) by replacing the broad dependency on initialProfile with specific
value-based dependencies (e.g., initialProfile?.label, initialProfile?.baseURL,
initialProfile?.accessToken, initialProfile?.kind,
initialProfile?.prefersQueryTokenRealtime) or by initializing local state once
from initialProfile (in useState) and removing the effect, or guard updates with
an “isDirty” flag so
setLabel/setBaseURL/setAccessToken/setKind/setPrefersQueryTokenRealtime run only
when creating a new form or when the real profile id/value changes rather than
on any object identity replacement.
- Around line 109-121: When saving via the onSave call in the Pressable handler,
clear probe metadata if connection details changed: detect whether baseURL,
accessToken, or kind differ from initialProfile (or if there is no
initialProfile) and, when they differ, set lastProbeAt and lastProbeResult to
undefined/null instead of carrying forward initialProfile?.lastProbeAt /
lastProbeResult; otherwise preserve the existing probe fields. Update the
payload passed to onSave (the object constructed in the Pressable onPress) to
implement this conditional clearing.

In `@examples/kitchen-sink/components/screen-shell.tsx`:
- Around line 13-20: The ScrollView in the ScreenShell component should include
keyboardShouldPersistTaps="handled" so taps on Pressable/TextInput controls fire
while the keyboard is visible; update the ScrollView JSX (the ScrollView element
in screen-shell.tsx) to add the keyboardShouldPersistTaps="handled" prop
alongside contentInsetAdjustmentBehavior and contentContainerStyle.

In `@examples/kitchen-sink/components/thread-panel.tsx`:
- Around line 43-49: The thread UI can still invoke createDesktopClient when the
profile is incomplete (missing accessToken or baseURL); update the component so
both messagesQuery (messagesQueryOptions) and sendMutation
(useSendMessageMutation) are made inert until profile.accessToken AND
profile.baseURL AND chatID are present: set messagesQuery.enabled to
Boolean(profile.accessToken && profile.baseURL && chatID) (instead of only
accessToken) and change the useSendMessageMutation usage so it does not
construct a real client when profile is incomplete—either call a stable noop
version of useSendMessageMutation when profile/baseURL/token are missing or have
the hook accept an enabled flag and early-return a noop mutate; apply the same
guard to the other occurrence around lines 78-84 to prevent createDesktopClient
from being reached with blank credentials.

In `@examples/kitchen-sink/hooks/use-active-profile.ts`:
- Around line 1-3: This file has Prettier formatting violations; reformat
hooks/use-active-profile.ts to satisfy the project's Prettier rules (fix import
spacing/order for "useEffect" and "useSyncExternalStore" and the import of
"profileStore", remove extra blank lines or trailing whitespace, and adjust
formatting around the code referenced on lines 21-22) — run the project's
formatter (e.g., npm run format or prettier --write) or apply the repo's
editorconfig/Prettier settings so the file is consistently formatted and the
lint job stops failing.

In `@examples/kitchen-sink/hooks/use-realtime-transport.ts`:
- Around line 12-15: normalizedChatIDs is unstable because empty or
whitespace-only chatIDs normalize to [] (should be ["*"]) and callers passing
['*'] inline create a fresh array each render; update the useMemo for
normalizedChatIDs to: trim and filter chatIDs, treat empty results or
all-whitespace input as the wildcard, and if the normalized set contains "*"
return a single shared constant (e.g., STAR) instead of a new array so identity
is stable; ensure you still dedupe via Set and return Array.from for other cases
so the effect that depends on normalizedChatIDs doesn't teardown/recreate on
ordinary rerenders (update the useMemo that computes normalizedChatIDs and the
constant used for the wildcard).

In `@examples/kitchen-sink/lib/api/session.ts`:
- Around line 202-207: The call to buildWebSocketURL(connection.profile) can
throw synchronously for an empty/invalid connection.profile.baseURL and abort
ensureProfileRealtime(); wrap the URL creation in a try/catch (or validate
connection.profile.baseURL with URL constructor safely) inside
ensureProfileRealtime so any error is caught, set the profile's lastError (or
the same error reporting path used elsewhere) and fall back to polling instead
of letting the exception propagate; update the subsequent reconnectToken
construction to only run if the URL build succeeded (use the same check around
buildWebSocketURL() and reconnectToken creation).
- Around line 227-300: Handlers for the WebSocket (socket) can run after a newer
socket has been assigned to connection.socket; guard each handler (onopen,
onmessage, onerror, onclose) by comparing the captured socket instance to the
current connection.socket and bail out if they differ, so only the active socket
mutates connection and snapshot; update the anonymous handlers in the socket
setup to first if (connection.socket !== socket) return; before doing any
setSnapshot, connection.socket assignment, connection.manualClose checks, or
invalidateRealtimePayload calls.

In `@examples/kitchen-sink/lib/query/queries.ts`:
- Line 19: query keys like queryKeys.probe(profile.id) only use profile.id so
React Query will reuse data when a profile's connection (baseURL/accessToken)
changes; update these keys (e.g., in probe, accounts, chats, messages queries at
the referenced locations) to include a non-sensitive connection revision field
from the profile (such as profile.connectionRevision or a hash of baseURL and a
token-omitted flag) or, alternatively, ensure profileStore.saveProfile()
triggers query invalidation for queryKeys.profilePrefix(profile.id) when
connection settings change; adjust the key expressions where queryKey:
queryKeys.probe(profile.id) (and the similar occurrences) are created to include
that revision or add an invalidation call after saving connection edits.
- Around line 20-38: The queryFn in queries.ts currently only updates
profileStore.updateProbe(profile.id, probe) after a successful
client.info.retrieve(), so failures leave lastProbeResult stale; wrap the
info.retrieve() and probe creation in a try/catch: on success behave as before
(use deriveCapabilitiesFromInfo, fill endpoints, serverName, errorMessage:
null), on error build a failure ProbeSummary (checkedAt ISO timestamp, status
set to an appropriate failure value such as "unreachable" or the error-derived
status, endpoints/serverName/appVersion null or empty, capabilities empty, and
errorMessage set to the caught error message), call
profileStore.updateProbe(profile.id, failureProbe) in the catch, then rethrow
the error so React Query still surfaces it. Ensure references: queryFn,
createDesktopClient, client.info.retrieve, profileStore.updateProbe,
deriveCapabilitiesFromInfo, and ProbeSummary.

In `@examples/kitchen-sink/lib/storage/profiles.ts`:
- Around line 161-166: Multiple mutators call commit(nextSnapshot) after
awaiting persistProfiles, which overwrites the global snapshot and causes lost
updates (e.g., deleteProfile vs updateProbe). Fix by serializing writes:
introduce a single-writer mutex or a write-queue around commit (or change commit
to accept an updater function that receives the latest snapshot and returns a
new snapshot and only then persists), ensure functions like deleteProfile,
updateProbe, and any code that calls commit use the mutex/queue or pass an
updater to commit so persistence and snapshot replace happen atomically against
the latest state, and keep persistProfiles usage inside the serialized section
to prevent races.

---

Nitpick comments:
In `@examples/kitchen-sink/components/search-result-card.tsx`:
- Around line 35-38: The render currently creates a locale string from
item.timestamp; change it to prefer the view-model's presentation-ready label by
using item.timestampLabel when truthy and falling back to new
Date(item.timestamp).toLocaleString() otherwise. Update the JSX inside the
conditional that renders the timestamp (the block referencing item.timestamp) to
display item.timestampLabel first, ensure the code performs a safe truthy check
on item.timestampLabel, and keep the existing Text props/style and fallback
behavior unchanged.

In `@examples/kitchen-sink/lib/api/client.ts`:
- Around line 142-148: The code is hiding API drift by using "as unknown as
DesktopClient" on the BeeperDesktop instance; instead create a thin adapter
object that explicitly implements the DesktopClient surface and delegates to the
BeeperDesktop instance so the TypeScript compiler can check compatibility.
Instantiate BeeperDesktop (keep baseURL, accessToken, dangerouslyAllowBrowser,
timeout, maxRetries) into a local variable (e.g., beeper) and return an object
that maps the DesktopClient methods/properties to beeper's methods/properties
and use the "satisfies DesktopClient" operator on that adapter to ensure the
published `@beeper/desktop-api` shape is enforced at compile time. Ensure all
required DesktopClient members are forwarded so the adapter fails to compile if
the upstream API changes.

In `@examples/kitchen-sink/lib/storage/profiles.ts`:
- Around line 53-65: The default profile in makeDefaultProfiles currently sets
baseURL to "http://127.0.0.1:23373" for the "easymatrix-local"
ConnectionProfile; update it to use the SDK's canonical local default
"http://localhost:23373" so the SDK's localhost-specific behavior/triggers
recognize it. Change only the baseURL value in the profile with id
"easymatrix-local" inside makeDefaultProfiles to match the SDK default.

In `@examples/kitchen-sink/types/profile.ts`:
- Line 1: Replace the hand-copied DesktopInfo shape in this file with the shared
DesktopInfo type exported from examples/kitchen-sink/lib/api/client.ts: import
the DesktopInfo type and use it for ProbeSummary.info (and any other duplicated
types between lines ~25-57) instead of re-declaring the fields; keep
ConnectionKind as-is but remove the duplicate interface/alias definitions and
update any references to the local copy to reference DesktopInfo so the probe
payload matches the client contract.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b7668273-127b-492a-ad74-65fc42410a59

📥 Commits

Reviewing files that changed from the base of the PR and between 7d3347e and 8e10f93.

⛔ Files ignored due to path filters (7)
  • examples/kitchen-sink/assets/android-icon-background.png is excluded by !**/*.png
  • examples/kitchen-sink/assets/android-icon-foreground.png is excluded by !**/*.png
  • examples/kitchen-sink/assets/android-icon-monochrome.png is excluded by !**/*.png
  • examples/kitchen-sink/assets/favicon.png is excluded by !**/*.png
  • examples/kitchen-sink/assets/icon.png is excluded by !**/*.png
  • examples/kitchen-sink/assets/splash-icon.png is excluded by !**/*.png
  • examples/kitchen-sink/yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (35)
  • examples/kitchen-sink/.gitignore
  • examples/kitchen-sink/app.json
  • examples/kitchen-sink/app/(tabs)/_layout.tsx
  • examples/kitchen-sink/app/(tabs)/index.tsx
  • examples/kitchen-sink/app/(tabs)/lab.tsx
  • examples/kitchen-sink/app/(tabs)/search.tsx
  • examples/kitchen-sink/app/(tabs)/settings.tsx
  • examples/kitchen-sink/app/+not-found.tsx
  • examples/kitchen-sink/app/_layout.tsx
  • examples/kitchen-sink/app/chat/[chatID].tsx
  • examples/kitchen-sink/components/active-profile-banner.tsx
  • examples/kitchen-sink/components/chat-row.tsx
  • examples/kitchen-sink/components/empty-state.tsx
  • examples/kitchen-sink/components/json-panel.tsx
  • examples/kitchen-sink/components/profile-editor.tsx
  • examples/kitchen-sink/components/screen-shell.tsx
  • examples/kitchen-sink/components/search-result-card.tsx
  • examples/kitchen-sink/components/section-card.tsx
  • examples/kitchen-sink/components/status-chip.tsx
  • examples/kitchen-sink/components/thread-panel.tsx
  • examples/kitchen-sink/hooks/use-active-profile.ts
  • examples/kitchen-sink/hooks/use-realtime-transport.ts
  • examples/kitchen-sink/lib/api/client.ts
  • examples/kitchen-sink/lib/api/session.ts
  • examples/kitchen-sink/lib/mappers/index.ts
  • examples/kitchen-sink/lib/query/keys.ts
  • examples/kitchen-sink/lib/query/mutations.ts
  • examples/kitchen-sink/lib/query/queries.ts
  • examples/kitchen-sink/lib/query/query-client.ts
  • examples/kitchen-sink/lib/storage/profiles.ts
  • examples/kitchen-sink/lib/storage/tokens.ts
  • examples/kitchen-sink/package.json
  • examples/kitchen-sink/tsconfig.json
  • examples/kitchen-sink/types/profile.ts
  • examples/kitchen-sink/types/view-models.ts
📜 Review details
🧰 Additional context used
🧬 Code graph analysis (15)
examples/kitchen-sink/app/chat/[chatID].tsx (3)
examples/kitchen-sink/hooks/use-active-profile.ts (1)
  • useActiveProfile (19-28)
examples/kitchen-sink/hooks/use-realtime-transport.ts (1)
  • useRealtimeTransport (7-36)
examples/kitchen-sink/components/thread-panel.tsx (1)
  • ThreadPanel (34-146)
examples/kitchen-sink/app/(tabs)/settings.tsx (8)
examples/kitchen-sink/hooks/use-active-profile.ts (2)
  • useActiveProfile (19-28)
  • useProfileStoreSnapshot (5-17)
examples/kitchen-sink/lib/query/mutations.ts (3)
  • useSaveProfileMutation (9-23)
  • useDeleteProfileMutation (25-40)
  • useSwitchActiveProfileMutation (42-46)
examples/kitchen-sink/lib/query/queries.ts (1)
  • probeQueryOptions (17-41)
examples/kitchen-sink/components/screen-shell.tsx (1)
  • ScreenShell (11-58)
examples/kitchen-sink/components/section-card.tsx (1)
  • SectionCard (10-40)
examples/kitchen-sink/components/status-chip.tsx (1)
  • StatusChip (3-25)
examples/kitchen-sink/components/empty-state.tsx (1)
  • EmptyState (3-23)
examples/kitchen-sink/components/profile-editor.tsx (1)
  • ProfileEditor (15-154)
examples/kitchen-sink/components/active-profile-banner.tsx (2)
examples/kitchen-sink/types/profile.ts (2)
  • ConnectionProfile (63-73)
  • TransportMode (3-3)
examples/kitchen-sink/components/status-chip.tsx (1)
  • StatusChip (3-25)
examples/kitchen-sink/components/search-result-card.tsx (1)
examples/kitchen-sink/types/view-models.ts (1)
  • SearchResultItem (21-31)
examples/kitchen-sink/lib/query/mutations.ts (5)
examples/kitchen-sink/types/profile.ts (1)
  • ConnectionProfile (63-73)
examples/kitchen-sink/lib/storage/profiles.ts (1)
  • profileStore (192-304)
examples/kitchen-sink/lib/query/keys.ts (1)
  • queryKeys (1-37)
examples/kitchen-sink/lib/api/session.ts (1)
  • sessionTransportStore (303-332)
examples/kitchen-sink/lib/api/client.ts (1)
  • createDesktopClient (130-149)
examples/kitchen-sink/app/(tabs)/index.tsx (10)
examples/kitchen-sink/hooks/use-active-profile.ts (1)
  • useActiveProfile (19-28)
examples/kitchen-sink/hooks/use-realtime-transport.ts (1)
  • useRealtimeTransport (7-36)
examples/kitchen-sink/lib/query/queries.ts (1)
  • chatsQueryOptions (53-65)
examples/kitchen-sink/components/screen-shell.tsx (1)
  • ScreenShell (11-58)
examples/kitchen-sink/components/active-profile-banner.tsx (1)
  • ActiveProfileBanner (6-43)
examples/kitchen-sink/components/empty-state.tsx (1)
  • EmptyState (3-23)
examples/kitchen-sink/components/section-card.tsx (1)
  • SectionCard (10-40)
examples/kitchen-sink/lib/query/keys.ts (1)
  • chat (18-20)
examples/kitchen-sink/components/chat-row.tsx (1)
  • ChatRow (6-47)
examples/kitchen-sink/components/thread-panel.tsx (1)
  • ThreadPanel (34-146)
examples/kitchen-sink/app/_layout.tsx (4)
examples/kitchen-sink/hooks/use-active-profile.ts (1)
  • useActiveProfile (19-28)
examples/kitchen-sink/hooks/use-realtime-transport.ts (1)
  • useRealtimeTransport (7-36)
examples/kitchen-sink/lib/storage/profiles.ts (1)
  • profileStore (192-304)
examples/kitchen-sink/lib/query/query-client.ts (1)
  • queryClient (3-13)
examples/kitchen-sink/hooks/use-realtime-transport.ts (3)
examples/kitchen-sink/types/profile.ts (1)
  • ConnectionProfile (63-73)
examples/kitchen-sink/lib/query/query-client.ts (1)
  • queryClient (3-13)
examples/kitchen-sink/lib/api/session.ts (2)
  • sessionTransportStore (303-332)
  • ensureProfileRealtime (334-363)
examples/kitchen-sink/hooks/use-active-profile.ts (1)
examples/kitchen-sink/lib/storage/profiles.ts (1)
  • profileStore (192-304)
examples/kitchen-sink/lib/query/queries.ts (6)
examples/kitchen-sink/types/profile.ts (2)
  • ConnectionProfile (63-73)
  • ProbeSummary (16-61)
examples/kitchen-sink/lib/query/keys.ts (3)
  • queryKeys (1-37)
  • probe (6-8)
  • chat (18-20)
examples/kitchen-sink/lib/api/client.ts (1)
  • createDesktopClient (130-149)
examples/kitchen-sink/lib/mappers/index.ts (4)
  • deriveCapabilitiesFromInfo (76-89)
  • mapChatToInboxItem (91-112)
  • mapMessageToGiftedMessage (114-139)
  • mapSearchResults (141-174)
examples/kitchen-sink/lib/storage/profiles.ts (1)
  • profileStore (192-304)
examples/kitchen-sink/types/view-models.ts (1)
  • LabOperation (40-46)
examples/kitchen-sink/lib/api/session.ts (3)
examples/kitchen-sink/types/profile.ts (2)
  • TransportMode (3-3)
  • ConnectionProfile (63-73)
examples/kitchen-sink/lib/query/query-client.ts (1)
  • queryClient (3-13)
examples/kitchen-sink/lib/query/keys.ts (1)
  • queryKeys (1-37)
examples/kitchen-sink/types/view-models.ts (1)
examples/kitchen-sink/lib/api/client.ts (1)
  • DesktopMessage (50-81)
examples/kitchen-sink/lib/mappers/index.ts (3)
examples/kitchen-sink/lib/api/client.ts (4)
  • DesktopMessage (50-81)
  • DesktopInfo (5-37)
  • DesktopChatListItem (95-97)
  • DesktopChat (83-93)
examples/kitchen-sink/types/profile.ts (1)
  • CapabilityFlags (5-14)
examples/kitchen-sink/types/view-models.ts (2)
  • InboxChatItem (4-19)
  • GiftedSDKMessage (33-38)
examples/kitchen-sink/app/(tabs)/lab.tsx (10)
examples/kitchen-sink/types/view-models.ts (1)
  • LabOperation (40-46)
examples/kitchen-sink/hooks/use-active-profile.ts (2)
  • useActiveProfile (19-28)
  • useProfileStoreSnapshot (5-17)
examples/kitchen-sink/hooks/use-realtime-transport.ts (1)
  • useRealtimeTransport (7-36)
examples/kitchen-sink/lib/query/queries.ts (1)
  • labQueryOptions (115-147)
examples/kitchen-sink/components/screen-shell.tsx (1)
  • ScreenShell (11-58)
examples/kitchen-sink/components/active-profile-banner.tsx (1)
  • ActiveProfileBanner (6-43)
examples/kitchen-sink/components/section-card.tsx (1)
  • SectionCard (10-40)
scripts/utils/make-dist-package-json.cjs (1)
  • value (5-5)
examples/kitchen-sink/components/empty-state.tsx (1)
  • EmptyState (3-23)
examples/kitchen-sink/components/json-panel.tsx (1)
  • JSONPanel (3-29)
examples/kitchen-sink/lib/api/client.ts (1)
src/client.ts (1)
  • baseURL (257-259)
🪛 ESLint
examples/kitchen-sink/lib/query/query-client.ts

[error] 1-1: Replace "@tanstack/react-query" with '@tanstack/react-query'

(prettier/prettier)

examples/kitchen-sink/types/profile.ts

[error] 1-1: Replace "easymatrix"·|·"desktop-api"·|·"unknown" with 'easymatrix'·|·'desktop-api'·|·'unknown'

(prettier/prettier)


[error] 3-3: Replace "idle"·|·"connecting"·|·"realtime"·|·"polling" with 'idle'·|·'connecting'·|·'realtime'·|·'polling'

(prettier/prettier)

examples/kitchen-sink/lib/query/mutations.ts

[error] 1-1: Replace "@tanstack/react-query" with '@tanstack/react-query'

(prettier/prettier)


[error] 3-3: Replace "../api/client" with '../api/client'

(prettier/prettier)


[error] 4-4: Replace "../api/session" with '../api/session'

(prettier/prettier)


[error] 5-5: Replace "./keys" with './keys'

(prettier/prettier)


[error] 6-6: Replace "../storage/profiles" with '../storage/profiles'

(prettier/prettier)


[error] 7-7: Replace "../../types/profile" with '../../types/profile'

(prettier/prettier)


[error] 13-14: Replace ⏎······input:·Partial<ConnectionProfile>·&·Pick<ConnectionProfile,·"label"·|·"baseURL">, with input:·Partial<ConnectionProfile>·&·Pick<ConnectionProfile,·'label'·|·'baseURL'>)·=>

(prettier/prettier)


[error] 15-15: Replace )·=> with ·

(prettier/prettier)


[error] 53-56: Replace ⏎··profile:·ConnectionProfile,⏎··chatID:·string,⏎ with profile:·ConnectionProfile,·chatID:·string

(prettier/prettier)


[error] 69-69: Replace "chats" with 'chats'

(prettier/prettier)


[error] 75-77: Replace ⏎············query.queryKey[2]·===·"messages"·&&⏎··········· with ·query.queryKey[2]·===·'messages'·&&

(prettier/prettier)

examples/kitchen-sink/hooks/use-realtime-transport.ts

[error] 1-1: Replace "react" with 'react'

(prettier/prettier)


[error] 2-2: Replace "@tanstack/react-query" with '@tanstack/react-query'

(prettier/prettier)


[error] 4-4: Replace "../lib/api/session" with '../lib/api/session'

(prettier/prettier)


[error] 5-5: Replace "../types/profile" with '../types/profile'

(prettier/prettier)


[error] 7-10: Replace ⏎··profile:·ConnectionProfile·|·null,⏎··chatIDs?:·string[],⏎ with profile:·ConnectionProfile·|·null,·chatIDs?:·string[]

(prettier/prettier)


[error] 13-13: Replace "*" with '*'

(prettier/prettier)


[error] 14-14: Replace "*")·?·["*" with '*')·?·['*'

(prettier/prettier)


[error] 29-33: Replace ⏎····normalizedChatIDs,⏎····profile,⏎····queryClient,⏎·· with normalizedChatIDs,·profile,·queryClient

(prettier/prettier)

examples/kitchen-sink/hooks/use-active-profile.ts

[error] 1-1: Replace "react" with 'react'

(prettier/prettier)


[error] 3-3: Replace "../lib/storage/profiles" with '../lib/storage/profiles'

(prettier/prettier)


[error] 21-22: Delete ⏎···

(prettier/prettier)

examples/kitchen-sink/lib/query/queries.ts

[error] 1-1: Replace "@tanstack/react-query" with '@tanstack/react-query'

(prettier/prettier)


[error] 3-3: Replace "../api/client" with '../api/client'

(prettier/prettier)


[error] 9-9: Replace "../mappers" with '../mappers'

(prettier/prettier)


[error] 10-10: Replace "./keys" with './keys'

(prettier/prettier)


[error] 11-11: Replace "../storage/profiles" with '../storage/profiles'

(prettier/prettier)


[error] 12-12: Replace "../../types/profile" with '../../types/profile'

(prettier/prettier)


[error] 13-13: Replace "../../types/view-models" with '../../types/view-models'

(prettier/prettier)


[error] 53-56: Replace ⏎··profile:·ConnectionProfile,⏎··filters:·RecordValues·=·{·limit:·75·},⏎ with profile:·ConnectionProfile,·filters:·RecordValues·=·{·limit:·75·}

(prettier/prettier)


[error] 79-82: Replace ⏎··········(left,·right)·=>⏎············Number(right.createdAt)·-·Number(left.createdAt),⏎········ with (left,·right)·=>·Number(right.createdAt)·-·Number(left.createdAt)

(prettier/prettier)


[error] 87-90: Replace ⏎··profile:·ConnectionProfile,⏎··params:·RecordValues,⏎ with profile:·ConnectionProfile,·params:·RecordValues

(prettier/prettier)


[error] 101-104: Replace ⏎··profile:·ConnectionProfile,⏎··params:·RecordValues,⏎ with profile:·ConnectionProfile,·params:·RecordValues

(prettier/prettier)


[error] 126-126: Replace "info" with 'info'

(prettier/prettier)


[error] 128-128: Replace "accounts" with 'accounts'

(prettier/prettier)


[error] 130-130: Replace "chats.list" with 'chats.list'

(prettier/prettier)


[error] 134-134: Replace "chats.search" with 'chats.search'

(prettier/prettier)


[error] 138-138: Replace "messages.search" with 'messages.search'

(prettier/prettier)


[error] 142-142: Replace "focus" with 'focus'

(prettier/prettier)

examples/kitchen-sink/lib/api/session.ts

[error] 1-1: Replace "@tanstack/react-query" with '@tanstack/react-query'

(prettier/prettier)


[error] 3-3: Replace "../query/keys" with '../query/keys'

(prettier/prettier)


[error] 4-4: Replace "../../types/profile" with '../../types/profile'

(prettier/prettier)


[error] 30-30: Replace "idle" with 'idle'

(prettier/prettier)


[error] 38-38: Replace "*" with '*'

(prettier/prettier)


[error] 39-39: Replace "*" with '*'

(prettier/prettier)


[error] 40-40: Replace "*" with '*'

(prettier/prettier)


[error] 57-57: Replace "" with ''

(prettier/prettier)


[error] 58-58: Replace "unknown" with 'unknown'

(prettier/prettier)


[error] 59-59: Replace "" with ''

(prettier/prettier)


[error] 60-60: Replace "" with ''

(prettier/prettier)


[error] 69-69: Replace "" with ''

(prettier/prettier)


[error] 77-80: Replace ⏎··connection:·ManagedConnection,⏎··updates:·Partial<SessionSnapshot>,⏎ with connection:·ManagedConnection,·updates:·Partial<SessionSnapshot>

(prettier/prettier)


[error] 89-89: Replace "" with ''

(prettier/prettier)


[error] 93-93: Replace "dangerouslyUseTokenInQuery" with 'dangerouslyUseTokenInQuery'

(prettier/prettier)


[error] 96-96: Replace "https:" with 'https:'

(prettier/prettier)


[error] 97-97: Replace "wss:" with 'wss:'

(prettier/prettier)


[error] 98-98: Replace "http:" with 'http:'

(prettier/prettier)


[error] 99-99: Replace "ws:" with 'ws:'

(prettier/prettier)


[error] 107-107: Replace "*" with '*'

(prettier/prettier)


[error] 110-114: Replace ⏎··profileID:·string,⏎··queryClient:·QueryClient,⏎··payload:·unknown,⏎ with profileID:·string,·queryClient:·QueryClient,·payload:·unknown

(prettier/prettier)


[error] 115-115: Replace "object" with 'object'

(prettier/prettier)


[error] 124-124: Replace "chat.upserted" with 'chat.upserted'

(prettier/prettier)


[error] 130-130: Replace "chats" with 'chats'

(prettier/prettier)


[error] 136-139: Replace ⏎····typedPayload.type·===·"message.upserted"·||⏎····typedPayload.type·===·"message.deleted"⏎·· with typedPayload.type·===·'message.upserted'·||·typedPayload.type·===·'message.deleted'

(prettier/prettier)


[error] 148-148: Replace "chats"·||·(key[2]·===·"messages" with 'chats'·||·(key[2]·===·'messages'

(prettier/prettier)


[error] 161-161: Replace "subscriptions.set" with 'subscriptions.set'

(prettier/prettier)


[error] 178-178: Replace "Realtime·requires·an·access·token." with 'Realtime·requires·an·access·token.'

(prettier/prettier)


[error] 180-180: Replace "web" with 'web'

(prettier/prettier)


[error] 183-183: Replace "Web·realtime·requires·query-token·auth·to·be·enabled·for·this·profile." with 'Web·realtime·requires·query-token·auth·to·be·enabled·for·this·profile.'

(prettier/prettier)


[error] 194-194: Replace "polling" with 'polling'

(prettier/prettier)


[error] 206-206: Replace "query"·:·"header" with 'query'·:·'header'

(prettier/prettier)


[error] 207-207: Replace "|" with '|'

(prettier/prettier)


[error] 221-221: Replace "connecting" with 'connecting'

(prettier/prettier)


[error] 236-236: Replace "web"·||·connection.profile.prefersQueryTokenRealtime with 'web'·||·connection.profile.prefersQueryTokenRealtime·?

(prettier/prettier)


[error] 237-237: Delete ·?

(prettier/prettier)


[error] 238-238: Delete ··

(prettier/prettier)


[error] 239-239: Delete ··

(prettier/prettier)


[error] 240-240: Delete ··

(prettier/prettier)


[error] 241-241: Delete ··

(prettier/prettier)


[error] 242-242: Delete ··

(prettier/prettier)


[error] 251-251: Replace "realtime" with 'realtime'

(prettier/prettier)


[error] 264-264: Replace "subscriptions.updated" with 'subscriptions.updated'

(prettier/prettier)


[error] 282-282: Replace "Realtime·connection·failed." with 'Realtime·connection·failed.'

(prettier/prettier)


[error] 290-290: Replace "polling"·:·"idle" with 'polling'·:·'idle'

(prettier/prettier)


[error] 297-297: Replace "polling" with 'polling'

(prettier/prettier)

examples/kitchen-sink/lib/storage/profiles.ts

[error] 1-1: Replace "expo-sqlite/localStorage/install" with 'expo-sqlite/localStorage/install'

(prettier/prettier)


[error] 3-3: Replace "expo-secure-store" with 'expo-secure-store'

(prettier/prettier)


[error] 5-5: Replace "../../types/profile" with '../../types/profile'

(prettier/prettier)


[error] 7-7: Replace "easymatrix-sdk-lab:profiles:v1" with 'easymatrix-sdk-lab:profiles:v1'

(prettier/prettier)


[error] 8-8: Replace "easymatrix-sdk-lab:active-profile:v1" with 'easymatrix-sdk-lab:active-profile:v1'

(prettier/prettier)


[error] 9-9: Replace "easymatrix-sdk-lab:token:" with 'easymatrix-sdk-lab:token:'

(prettier/prettier)


[error] 11-11: Replace "accessToken" with 'accessToken'

(prettier/prettier)


[error] 32-32: Replace "web" with 'web'

(prettier/prettier)


[error] 50-50: Replace "" with ''

(prettier/prettier)


[error] 56-56: Replace "easymatrix-local" with 'easymatrix-local'

(prettier/prettier)


[error] 57-57: Replace "EasyMatrix·Local" with 'EasyMatrix·Local'

(prettier/prettier)


[error] 58-58: Replace "easymatrix" with 'easymatrix'

(prettier/prettier)


[error] 59-59: Replace "http://127.0.0.1:23373" with 'http://127.0.0.1:23373'

(prettier/prettier)


[error] 60-60: Replace "" with ''

(prettier/prettier)


[error] 67-67: Replace "desktop-api-custom" with 'desktop-api-custom'

(prettier/prettier)


[error] 68-68: Replace "Custom·Desktop·API" with 'Custom·Desktop·API'

(prettier/prettier)


[error] 69-69: Replace "desktop-api" with 'desktop-api'

(prettier/prettier)


[error] 70-70: Replace "" with ''

(prettier/prettier)


[error] 71-71: Replace "" with ''

(prettier/prettier)


[error] 89-89: Replace "string" with 'string'

(prettier/prettier)


[error] 98-98: Replace "" with ''

(prettier/prettier)


[error] 100-100: Replace "" with ''

(prettier/prettier)


[error] 121-121: Replace "" with ''

(prettier/prettier)


[error] 133-136: Replace ⏎··profiles:·ConnectionProfile[],⏎··activeProfileId:·string·|·null,⏎ with profiles:·ConnectionProfile[],·activeProfileId:·string·|·null

(prettier/prettier)


[error] 150-152: Replace ⏎····profiles.map((profile)·=>·setStoredToken(profile.id,·profile.accessToken.trim())),⏎·· with profiles.map((profile)·=>·setStoredToken(profile.id,·profile.accessToken.trim()))

(prettier/prettier)


[error] 170-172: Replace ⏎····?·await·hydrateProfiles(storedProfiles)⏎··· with ·?·await·hydrateProfiles(storedProfiles)

(prettier/prettier)


[error] 173-176: Replace ⏎····localStorage.getItem(ACTIVE_PROFILE_KEY)·??⏎····profiles[0]?.id·??⏎··· with ·localStorage.getItem(ACTIVE_PROFILE_KEY)·??·profiles[0]?.id·??

(prettier/prettier)


[error] 181-183: Replace ·profiles.some((profile)·=>·profile.id·===·activeProfileID)⏎······?·activeProfileID⏎······:·(profiles[0]?.id·??·null) with ⏎······profiles.some((profile)·=>·profile.id·===·activeProfileID)·?·activeProfileID·:·profiles[0]?.id·??·null

(prettier/prettier)


[error] 217-217: Replace "label"·|·"baseURL" with 'label'·|·'baseURL'

(prettier/prettier)


[error] 222-224: Replace ⏎······?·profiles.findIndex((profile)·=>·profile.id·===·input.id)⏎····· with ·?·profiles.findIndex((profile)·=>·profile.id·===·input.id)

(prettier/prettier)


[error] 230-230: Replace "unknown" with 'unknown'

(prettier/prettier)


[error] 232-232: Replace "" with ''

(prettier/prettier)


[error] 235-237: Replace ⏎········existingProfile?.prefersQueryTokenRealtime·??⏎······· with ·existingProfile?.prefersQueryTokenRealtime·??

(prettier/prettier)


[error] 267-269: Replace ⏎··········?·(profiles[0]?.id·??·null)⏎········· with ·?·profiles[0]?.id·??·null

(prettier/prettier)


[error] 289-289: Insert ·?

(prettier/prettier)


[error] 290-290: Delete

(prettier/prettier)


[error] 291-291: Delete ··

(prettier/prettier)


[error] 292-292: Replace ············ with ··········

(prettier/prettier)


[error] 293-293: Delete ··

(prettier/prettier)


[error] 294-294: Delete ··

(prettier/prettier)


[error] 295-295: Delete ··

(prettier/prettier)

examples/kitchen-sink/types/view-models.ts

[error] 1-1: Replace "react-native-gifted-chat" with 'react-native-gifted-chat'

(prettier/prettier)


[error] 2-2: Replace "../lib/api/client" with '../lib/api/client'

(prettier/prettier)


[error] 7-7: Replace "single"·|·"group" with 'single'·|·'group'

(prettier/prettier)


[error] 23-23: Replace "chat"·|·"message" with 'chat'·|·'message'

(prettier/prettier)


[error] 40-46: Replace ⏎··|·"info"⏎··|·"accounts"⏎··|·"chats.list"⏎··|·"chats.search"⏎··|·"messages.search"⏎··|·"focus" with ·'info'·|·'accounts'·|·'chats.list'·|·'chats.search'·|·'messages.search'·|·'focus'

(prettier/prettier)

examples/kitchen-sink/lib/storage/tokens.ts

[error] 1-1: Replace "expo-secure-store" with 'expo-secure-store'

(prettier/prettier)


[error] 4-4: Replace "web" with 'web'

(prettier/prettier)


[error] 13-13: Replace "" with ''

(prettier/prettier)


[error] 15-15: Replace "" with ''

(prettier/prettier)

examples/kitchen-sink/lib/mappers/index.ts

[error] 1-6: Replace ⏎··DesktopChat,⏎··DesktopChatListItem,⏎··DesktopInfo,⏎··DesktopMessage,⏎}·from·"../api/client" with ·DesktopChat,·DesktopChatListItem,·DesktopInfo,·DesktopMessage·}·from·'../api/client'

(prettier/prettier)


[error] 8-8: Replace "../../types/profile" with '../../types/profile'

(prettier/prettier)


[error] 9-13: Replace ⏎··GiftedSDKMessage,⏎··InboxChatItem,⏎··SearchResultItem,⏎}·from·"../../types/view-models" with ·GiftedSDKMessage,·InboxChatItem,·SearchResultItem·}·from·'../../types/view-models'

(prettier/prettier)


[error] 24-24: Replace "img" with 'img'

(prettier/prettier)


[error] 25-25: Replace "Image·attachment" with 'Image·attachment'

(prettier/prettier)


[error] 26-26: Replace "video" with 'video'

(prettier/prettier)


[error] 27-27: Replace "Video·attachment" with 'Video·attachment'

(prettier/prettier)


[error] 28-28: Replace "audio" with 'audio'

(prettier/prettier)


[error] 29-29: Replace "Audio·attachment" with 'Audio·attachment'

(prettier/prettier)


[error] 31-31: Replace "Attachment" with 'Attachment'

(prettier/prettier)


[error] 37-37: Replace "No·messages·yet" with 'No·messages·yet'

(prettier/prettier)


[error] 42-42: Replace "Message" with 'Message'

(prettier/prettier)


[error] 47-47: Replace "No·activity" with 'No·activity'

(prettier/prettier)


[error] 51-51: Replace "Unknown·time" with 'Unknown·time'

(prettier/prettier)


[error] 55-55: Replace "just·now" with 'just·now'

(prettier/prettier)


[error] 70-70: Replace "unknown" with 'unknown'

(prettier/prettier)


[error] 73-73: Replace "unknown" with 'unknown'

(prettier/prettier)


[error] 77-77: Replace "ready" with 'ready'

(prettier/prettier)


[error] 91-93: Replace ⏎··chat:·DesktopChatListItem·|·DesktopChat,⏎ with chat:·DesktopChatListItem·|·DesktopChat

(prettier/prettier)


[error] 94-94: Replace "preview" with 'preview'

(prettier/prettier)


[error] 100-100: Replace "group"·?·"Untitled·group"·:·"Untitled·chat" with 'group'·?·'Untitled·group'·:·'Untitled·chat'

(prettier/prettier)


[error] 114-116: Replace ⏎··message:·DesktopMessage,⏎ with message:·DesktopMessage

(prettier/prettier)


[error] 118-118: Replace "·" with '·'

(prettier/prettier)


[error] 122-122: Replace "Message" with 'Message'

(prettier/prettier)


[error] 125-125: Replace "__self__" with '__self__'

(prettier/prettier)


[error] 133-133: Replace "NOTICE" with 'NOTICE'

(prettier/prettier)


[error] 147-147: Replace "chat" with 'chat'

(prettier/prettier)


[error] 150-150: Replace "Untitled·chat" with 'Untitled·chat'

(prettier/prettier)


[error] 151-151: Replace "Open·conversation" with 'Open·conversation'

(prettier/prettier)


[error] 152-152: Replace "Open·conversation" with 'Open·conversation'

(prettier/prettier)


[error] 159-159: Replace "message" with 'message'

(prettier/prettier)


[error] 163-163: Replace "Message" with 'Message'

(prettier/prettier)


[error] 164-164: Replace "Message" with 'Message'

(prettier/prettier)

examples/kitchen-sink/lib/query/keys.ts

[error] 3-3: Replace "profile" with 'profile'

(prettier/prettier)


[error] 7-7: Replace "profile",·profileID,·"probe" with 'profile',·profileID,·'probe'

(prettier/prettier)


[error] 11-11: Replace "profile",·profileID,·"accounts" with 'profile',·profileID,·'accounts'

(prettier/prettier)


[error] 15-15: Replace "profile",·profileID,·"chats" with 'profile',·profileID,·'chats'

(prettier/prettier)


[error] 19-19: Replace "profile",·profileID,·"chat" with 'profile',·profileID,·'chat'

(prettier/prettier)


[error] 23-23: Replace "profile",·profileID,·"messages" with 'profile',·profileID,·'messages'

(prettier/prettier)


[error] 27-27: Replace "profile",·profileID,·"chats-search" with 'profile',·profileID,·'chats-search'

(prettier/prettier)


[error] 31-31: Replace "profile",·profileID,·"messages-search" with 'profile',·profileID,·'messages-search'

(prettier/prettier)


[error] 35-35: Replace "profile",·profileID,·"lab" with 'profile',·profileID,·'lab'

(prettier/prettier)

examples/kitchen-sink/lib/api/client.ts

[error] 1-1: Replace "@beeper/desktop-api" with '@beeper/desktop-api'

(prettier/prettier)


[error] 3-3: Replace "../../types/profile" with '../../types/profile'

(prettier/prettier)


[error] 59-59: Replace "TEXT" with 'TEXT'

(prettier/prettier)


[error] 60-60: Replace "NOTICE" with 'NOTICE'

(prettier/prettier)


[error] 61-61: Replace "IMAGE" with 'IMAGE'

(prettier/prettier)


[error] 62-62: Replace "VIDEO" with 'VIDEO'

(prettier/prettier)


[error] 63-63: Replace "VOICE" with 'VOICE'

(prettier/prettier)


[error] 64-64: Replace "AUDIO" with 'AUDIO'

(prettier/prettier)


[error] 65-65: Replace "FILE" with 'FILE'

(prettier/prettier)


[error] 66-66: Replace "STICKER" with 'STICKER'

(prettier/prettier)


[error] 67-67: Replace "LOCATION" with 'LOCATION'

(prettier/prettier)


[error] 68-68: Replace "REACTION" with 'REACTION'

(prettier/prettier)


[error] 72-72: Replace "unknown"·|·"img"·|·"video"·|·"audio" with 'unknown'·|·'img'·|·'video'·|·'audio'

(prettier/prettier)


[error] 87-87: Replace "single"·|·"group" with 'single'·|·'group'

(prettier/prettier)


[error] 111-114: Replace ⏎······chatID:·string,⏎······params?:·Record<string,·unknown>,⏎···· with chatID:·string,·params?:·Record<string,·unknown>

(prettier/prettier)


[error] 115-117: Replace ⏎······params?:·Record<string,·unknown>,⏎···· with params?:·Record<string,·unknown>

(prettier/prettier)


[error] 127-127: Replace "" with ''

(prettier/prettier)


[error] 135-135: Replace "A·base·URL·is·required·before·creating·a·Desktop·API·client." with 'A·base·URL·is·required·before·creating·a·Desktop·API·client.'

(prettier/prettier)


[error] 139-139: Replace "An·access·token·is·required·before·creating·a·Desktop·API·client." with 'An·access·token·is·required·before·creating·a·Desktop·API·client.'

(prettier/prettier)


[error] 145-145: Replace "web" with 'web'

(prettier/prettier)

🪛 GitHub Check: lint
examples/kitchen-sink/hooks/use-realtime-transport.ts

[failure] 14-14:
Replace "*")·?·["*" with '*')·?·['*'


[failure] 13-13:
Replace "*" with '*'


[failure] 7-7:
Replace ⏎··profile:·ConnectionProfile·|·null,⏎··chatIDs?:·string[],⏎ with profile:·ConnectionProfile·|·null,·chatIDs?:·string[]


[failure] 5-5:
Replace "../types/profile" with '../types/profile'


[failure] 4-4:
Replace "../lib/api/session" with '../lib/api/session'


[failure] 2-2:
Replace "@tanstack/react-query" with '@tanstack/react-query'


[failure] 1-1:
Replace "react" with 'react'

examples/kitchen-sink/hooks/use-active-profile.ts

[failure] 21-21:
Delete ⏎···


[failure] 3-3:
Replace "../lib/storage/profiles" with '../lib/storage/profiles'


[failure] 1-1:
Replace "react" with 'react'

🔇 Additional comments (10)
examples/kitchen-sink/app.json (1)

1-41: LGTM!

The Expo configuration is well-structured with appropriate platform-specific settings, experimental typed routes enabled for type-safe navigation, and the necessary plugins for the kitchen-sink features (router, secure-store, sqlite).

examples/kitchen-sink/tsconfig.json (1)

1-17: LGTM!

Standard Expo TypeScript configuration with strict mode enabled and a convenient path alias for cleaner imports.

examples/kitchen-sink/app/+not-found.tsx (1)

1-39: LGTM!

Clean implementation of the 404 screen with proper use of expo-router's Link with asChild pattern for the navigation button.

examples/kitchen-sink/.gitignore (1)

1-42: LGTM!

Comprehensive gitignore covering Expo build artifacts, sensitive credentials, and generated native folders. Ignoring package-lock.json aligns with the PR's use of Yarn.

examples/kitchen-sink/components/empty-state.tsx (1)

1-24: LGTM!

Simple, well-typed presentational component for empty states.

examples/kitchen-sink/components/status-chip.tsx (1)

1-26: LGTM!

Clean status chip component with well-defined tone variants and proper typing.

examples/kitchen-sink/components/json-panel.tsx (1)

1-30: LGTM!

Clean JSON display component with appropriate web-specific monospace font handling via process.env.EXPO_OS. For a demo/lab app, the direct JSON.stringify usage is acceptable.

examples/kitchen-sink/package.json (1)

25-35: No changes needed. react-native-web: ^0.21.0 is compatible with React 19 (peer dependencies allow react and react-dom ^18.0.0 || ^19.0.0).

			> Likely an incorrect or invalid review comment.
examples/kitchen-sink/types/view-models.ts (1)

33-38: Nice boundary for GiftedSDKMessage.

Keeping sdkMessage on the chat UI model preserves access to the raw Desktop API payload without leaking it through every component prop.

examples/kitchen-sink/lib/query/mutations.ts (1)

64-79: Nice scoped invalidation after send.

Refreshing only the chat lists and the active thread keeps post-send updates cheap and avoids blowing away unrelated profile data.

Comment on lines +52 to +56
useEffect(() => {
if (wideLayout && !selectedChatID && filteredChats[0]?.id) {
setSelectedChatID(filteredChats[0].id);
}
}, [filteredChats, selectedChatID, wideLayout]);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Keep selectedChatID in sync with the visible list.

After a filter change or profile switch, selectedChatID can point at a chat that is no longer in filteredChats, so the wide layout keeps rendering a stale thread. Re-select the first visible chat or clear the selection when the current one drops out.

Suggested fix
   useEffect(() => {
-    if (wideLayout && !selectedChatID && filteredChats[0]?.id) {
-      setSelectedChatID(filteredChats[0].id);
+    if (!wideLayout) {
+      return;
+    }
+
+    const firstVisibleChatID = filteredChats[0]?.id ?? null;
+    if (!firstVisibleChatID) {
+      if (selectedChatID !== null) {
+        setSelectedChatID(null);
+      }
+      return;
+    }
+
+    if (!selectedChatID || !filteredChats.some((chat) => chat.id === selectedChatID)) {
+      setSelectedChatID(firstVisibleChatID);
     }
   }, [filteredChats, selectedChatID, wideLayout]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
if (wideLayout && !selectedChatID && filteredChats[0]?.id) {
setSelectedChatID(filteredChats[0].id);
}
}, [filteredChats, selectedChatID, wideLayout]);
useEffect(() => {
if (!wideLayout) {
return;
}
const firstVisibleChatID = filteredChats[0]?.id ?? null;
if (!firstVisibleChatID) {
if (selectedChatID !== null) {
setSelectedChatID(null);
}
return;
}
if (!selectedChatID || !filteredChats.some((chat) => chat.id === selectedChatID)) {
setSelectedChatID(firstVisibleChatID);
}
}, [filteredChats, selectedChatID, wideLayout]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/kitchen-sink/app/`(tabs)/index.tsx around lines 52 - 56, The effect
that syncs selection to the visible list (useEffect referencing wideLayout,
selectedChatID, filteredChats, setSelectedChatID) must check whether
selectedChatID still exists in filteredChats and update accordingly: if
wideLayout is true and selectedChatID is missing from filteredChats,
setSelectedChatID to filteredChats[0]?.id (or undefined/null to clear if
filteredChats is empty); keep the existing behavior that picks the first visible
chat when there is no selection, but add the missing-membership check so the
wide layout never renders a stale thread.

const transport = useRealtimeTransport(activeProfile, ["*"]);
const [operation, setOperation] = useState<LabOperation>("info");
const [paramsText, setParamsText] = useState("{\n \"query\": \"matrix\"\n}");
const [runNonce, setRunNonce] = useState(0);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

The Run button is not actually driving execution.

Once runNonce > 0, changing operation or params auto-fetches because the query key changes, but clicking Run again with the same inputs does nothing because runNonce never participates in the query state. Snapshot the submitted request on button press and query from that snapshot.

Suggested fix
-  const [runNonce, setRunNonce] = useState(0);
+  const [submittedRun, setSubmittedRun] = useState<{
+    operation: LabOperation;
+    params: Record<string, unknown>;
+    nonce: number;
+  } | null>(null);
@@
-  const activeQuery = useQuery(
-    activeProfile
+  const activeQuery = useQuery(
+    activeProfile && submittedRun
       ? {
-          ...labQueryOptions(activeProfile, operation, params),
-          enabled: runNonce > 0,
+          ...labQueryOptions(activeProfile, submittedRun.operation, submittedRun.params),
+          queryKey: [
+            'profile',
+            activeProfile.id,
+            'lab',
+            submittedRun.operation,
+            submittedRun.params,
+            submittedRun.nonce,
+          ],
         }
       : {
-          queryKey: ["profile", "none", "lab", operation, params],
+          queryKey: ['profile', 'none', 'lab', submittedRun?.operation ?? operation, submittedRun?.params ?? {}],
           queryFn: async () => {
             throw new Error("No active profile.");
           },
           enabled: false,
         },
   );
@@
-  const compareQuery = useQuery(
-    compareProfile
+  const compareQuery = useQuery(
+    compareProfile && submittedRun
       ? {
-          ...labQueryOptions(compareProfile, operation, params),
-          enabled: runNonce > 0,
+          ...labQueryOptions(compareProfile, submittedRun.operation, submittedRun.params),
+          queryKey: [
+            'profile',
+            compareProfile.id,
+            'lab',
+            submittedRun.operation,
+            submittedRun.params,
+            submittedRun.nonce,
+          ],
         }
       : {
-          queryKey: ["profile", "none-compare", "lab", operation, params],
+          queryKey: [
+            'profile',
+            'none-compare',
+            'lab',
+            submittedRun?.operation ?? operation,
+            submittedRun?.params ?? {},
+          ],
           queryFn: async () => {
             throw new Error("No comparison profile.");
           },
           enabled: false,
         },
   );
@@
-            onPress={() => setRunNonce((value) => value + 1)}
+            onPress={() =>
+              setSubmittedRun({ operation, params, nonce: Date.now() })
+            }
@@
-      {runNonce === 0 ? (
+      {!submittedRun ? (

Also applies to: 49-78, 125-127, 167-172

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/kitchen-sink/app/`(tabs)/lab.tsx at line 38, The Run button
currently only increments runNonce but runNonce is not used in the query key so
clicking Run with identical operation/params doesn't re-execute; modify the
component to snapshot the submitted request when the Run button is pressed
(e.g., create a submittedRequest state holding {operation, params, runNonce} and
update it in the Run handler via setSubmittedRequest) and then use that
submittedRequest as the React Query key/input for the data fetch (replace direct
use of operation/params in the query with
submittedRequest.operation/submittedRequest.params and include
submittedRequest.runNonce in the key) so every Run press (even with same inputs)
triggers a new fetch; update references around runNonce, setRunNonce, operation,
params, and the query hook to read from submittedRequest instead of the live
inputs.

Comment on lines +21 to +44
const chatsQuery = useQuery(
activeProfile
? {
...chatsSearchQueryOptions(activeProfile, { query: submittedQuery, limit: 12 }),
enabled: Boolean(submittedQuery.trim()),
}
: {
queryKey: ["profile", "none", "chats-search", { query: submittedQuery, limit: 12 }],
queryFn: async () => [],
enabled: false,
},
);
const messagesQuery = useQuery(
activeProfile
? {
...messagesSearchQueryOptions(activeProfile, { query: submittedQuery, limit: 12 }),
enabled: Boolean(submittedQuery.trim()),
}
: {
queryKey: ["profile", "none", "messages-search", { query: submittedQuery, limit: 12 }],
queryFn: async () => [],
enabled: false,
},
);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don’t show “No results” when no search can run.

If there is no active profile, both queries stay disabled, but submitting a term still falls through to the results section and can render the empty-results state. That misrepresents “search was never executed” as “search returned zero matches.”

Also applies to: 83-103

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/kitchen-sink/app/`(tabs)/search.tsx around lines 21 - 44, The "No
results" empty state is shown even when searches are disabled (no
activeProfile); update the results rendering logic to only display empty-results
when a real search ran — i.e., check the query status of chatsQuery and
messagesQuery (use their isIdle/isFetched or isSuccess flags and .isError as
needed) and/or the enabled condition (activeProfile && submittedQuery.trim())
before rendering the "No results" UI, and similarly adjust any duplicate logic
around lines 83-103; reference chatsQuery, messagesQuery, activeProfile, and
submittedQuery when making the conditional change so the empty state appears
only if at least one query was actually executed.

Comment on lines +87 to +101
{submittedQuery ? (
<SectionCard title="Results" description={`Submitted query: ${submittedQuery}`}>
{(chatsQuery.isPending || messagesQuery.isPending) ? (
<EmptyState title="Searching" description="Dispatching both search requests." />
) : null}
{combined.map((item) => (
<SearchResultCard
key={item.id}
item={item}
onPress={() => router.push({ pathname: "/chat/[chatID]", params: { chatID: item.chatID } })}
/>
))}
{!chatsQuery.isPending && !messagesQuery.isPending && combined.length === 0 ? (
<EmptyState title="No results" description="Try a different literal term or verify the target is fully indexed." />
) : null}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Surface query failures separately from empty results.

A failed chats.search or messages.search currently lands in the same UI path as a legitimate zero-result search. Network/auth errors will read as “No results,” which makes debugging and user recovery much harder.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/kitchen-sink/app/`(tabs)/search.tsx around lines 87 - 101, The UI
currently treats failed searches the same as zero-result searches; update the
render logic in the search component (around chatsQuery, messagesQuery,
combined, SearchResultCard, EmptyState, router.push) to surface errors
separately: if chatsQuery.isError or messagesQuery.isError render an error
EmptyState (or distinct ErrorState) showing the corresponding error message(s)
from chatsQuery.error and messagesQuery.error (and short contextual text like
"Chats search failed" / "Messages search failed"), otherwise keep the existing
pending/results/no-results branches; ensure the "No results" branch only runs
when both queries are not pending, not error, and combined.length === 0.

export default function ChatRouteScreen() {
const { chatID } = useLocalSearchParams<{ chatID: string }>();
const { activeProfile } = useActiveProfile();
const transport = useRealtimeTransport(activeProfile, chatID ? [chatID] : ["*"]);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, let's check the structure and find the files mentioned
find . -name "[chatID].tsx" -o -name "use-realtime-transport.ts" | head -20

Repository: beeper/desktop-api-js

Length of output: 120


🏁 Script executed:

# List files in the examples/kitchen-sink directory to understand structure
ls -la examples/kitchen-sink/app/chat/ 2>/dev/null || echo "Directory not found, searching for relevant files"
fd "chatID" --type f

Repository: beeper/desktop-api-js

Length of output: 278


🏁 Script executed:

# Search for the use-realtime-transport hook
fd "use-realtime-transport" --type f

Repository: beeper/desktop-api-js

Length of output: 118


🏁 Script executed:

cat -n examples/kitchen-sink/app/chat/\[chatID\].tsx

Repository: beeper/desktop-api-js

Length of output: 1080


🏁 Script executed:

cat -n examples/kitchen-sink/hooks/use-realtime-transport.ts

Repository: beeper/desktop-api-js

Length of output: 1436


🏁 Script executed:

# Let's verify if there are other usages of useRealtimeTransport to understand the pattern
rg "useRealtimeTransport" --type tsx --type ts -B 2 -A 2

Repository: beeper/desktop-api-js

Length of output: 92


🏁 Script executed:

# Search for other usages of useRealtimeTransport
rg "useRealtimeTransport" -B 2 -A 2

Repository: beeper/desktop-api-js

Length of output: 5144


Memoize the chatIDs argument passed to useRealtimeTransport.

This array literal is recreated on every render. In examples/kitchen-sink/hooks/use-realtime-transport.ts, the hook's useMemo dependency on chatIDs (line 15) causes normalizedChatIDs to be recalculated every render, which then triggers the useEffect (lines 24-33) to re-run, leading to repeated ensureProfileRealtime() calls and potential reconnect loops.

Suggested fix
+import { useMemo } from "react";
 import { Stack, useLocalSearchParams } from "expo-router";
 import { View } from "react-native";
@@
   const { chatID } = useLocalSearchParams<{ chatID: string }>();
   const { activeProfile } = useActiveProfile();
-  const transport = useRealtimeTransport(activeProfile, chatID ? [chatID] : ["*"]);
+  const chatIDs = useMemo(() => (chatID ? [chatID] : ["*"]), [chatID]);
+  const transport = useRealtimeTransport(activeProfile, chatIDs);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const transport = useRealtimeTransport(activeProfile, chatID ? [chatID] : ["*"]);
const chatIDs = useMemo(() => (chatID ? [chatID] : ["*"]), [chatID]);
const transport = useRealtimeTransport(activeProfile, chatIDs);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/kitchen-sink/app/chat/`[chatID].tsx at line 11, The array literal
passed to useRealtimeTransport is recreated each render causing
normalizedChatIDs to change; memoize it first (derive a stable chatIDs value
from chatID using useMemo with [chatID] as the dependency) and pass that
memoized chatIDs into useRealtimeTransport (keep existing activeProfile). This
ensures useRealtimeTransport and its internal useMemo/useEffect (in
use-realtime-transport.ts) only react when chatID actually changes.

Comment on lines +202 to +207
const url = buildWebSocketURL(connection.profile);
const reconnectToken = [
connection.profile.baseURL,
connection.profile.accessToken,
connection.profile.prefersQueryTokenRealtime ? "query" : "header",
].join("|");
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Catch malformed baseURL before opening realtime.

buildWebSocketURL() uses new URL(...), so an empty or invalid profile.baseURL will throw synchronously here and abort ensureProfileRealtime() instead of degrading to polling with a surfaced error in lastError.

Suggested fix
-  const url = buildWebSocketURL(connection.profile);
+  let url: string;
+  try {
+    url = buildWebSocketURL(connection.profile);
+  } catch (error) {
+    closeSocket(connection);
+    setSnapshot(connection, {
+      mode: 'polling',
+      chatIDs: unionChatIDs(connection),
+      lastError: error instanceof Error ? error.message : String(error),
+      connectedAt: null,
+    });
+    return;
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const url = buildWebSocketURL(connection.profile);
const reconnectToken = [
connection.profile.baseURL,
connection.profile.accessToken,
connection.profile.prefersQueryTokenRealtime ? "query" : "header",
].join("|");
let url: string;
try {
url = buildWebSocketURL(connection.profile);
} catch (error) {
closeSocket(connection);
setSnapshot(connection, {
mode: 'polling',
chatIDs: unionChatIDs(connection),
lastError: error instanceof Error ? error.message : String(error),
connectedAt: null,
});
return;
}
const reconnectToken = [
connection.profile.baseURL,
connection.profile.accessToken,
connection.profile.prefersQueryTokenRealtime ? "query" : "header",
].join("|");
🧰 Tools
🪛 ESLint

[error] 206-206: Replace "query"·:·"header" with 'query'·:·'header'

(prettier/prettier)


[error] 207-207: Replace "|" with '|'

(prettier/prettier)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/kitchen-sink/lib/api/session.ts` around lines 202 - 207, The call to
buildWebSocketURL(connection.profile) can throw synchronously for an
empty/invalid connection.profile.baseURL and abort ensureProfileRealtime(); wrap
the URL creation in a try/catch (or validate connection.profile.baseURL with URL
constructor safely) inside ensureProfileRealtime so any error is caught, set the
profile's lastError (or the same error reporting path used elsewhere) and fall
back to polling instead of letting the exception propagate; update the
subsequent reconnectToken construction to only run if the URL build succeeded
(use the same check around buildWebSocketURL() and reconnectToken creation).

Comment on lines +227 to +300
const SocketCtor = WebSocket as unknown as {
new (
url: string,
protocols?: string | string[],
options?: { headers?: Record<string, string> },
): WebSocket;
};

const socket =
process.env.EXPO_OS === "web" || connection.profile.prefersQueryTokenRealtime
? new WebSocket(url)
: new SocketCtor(url, undefined, {
headers: {
Authorization: `Bearer ${connection.profile.accessToken.trim()}`,
},
});

connection.manualClose = false;
connection.socket = socket;
connection.reconnectToken = reconnectToken;

socket.onopen = () => {
sendSubscriptions(connection);
setSnapshot(connection, {
mode: "realtime",
chatIDs: unionChatIDs(connection),
lastError: null,
connectedAt: new Date().toISOString(),
});
};

socket.onmessage = (event) => {
try {
const payload = JSON.parse(String(event.data)) as {
type?: string;
chatIDs?: string[];
};
if (payload.type === "subscriptions.updated") {
setSnapshot(connection, {
chatIDs: normalizeChatIDs(payload.chatIDs),
});
return;
}
if (connection.queryClient) {
invalidateRealtimePayload(connection.profile.id, connection.queryClient, payload);
}
} catch (error) {
setSnapshot(connection, {
lastError: error instanceof Error ? error.message : String(error),
});
}
};

socket.onerror = () => {
setSnapshot(connection, {
lastError: "Realtime connection failed.",
});
};

socket.onclose = () => {
connection.socket = null;
if (connection.manualClose) {
setSnapshot(connection, {
mode: connection.attachments.size > 0 ? "polling" : "idle",
connectedAt: null,
});
connection.manualClose = false;
return;
}
setSnapshot(connection, {
mode: "polling",
connectedAt: null,
});
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Ignore callbacks from stale sockets.

After closeSocket() you can assign a replacement socket before the old one has finished closing. These handlers still mutate the shared connection, so a late onclose from the old socket can null out connection.socket or flip the snapshot back to polling while the new socket is already active.

Suggested fix
   const socket =
     process.env.EXPO_OS === 'web' || connection.profile.prefersQueryTokenRealtime
       ? new WebSocket(url)
       : new SocketCtor(url, undefined, {
           headers: {
             Authorization: `Bearer ${connection.profile.accessToken.trim()}`,
           },
         });
+  const isActiveSocket = () => connection.socket === socket;
 
   connection.manualClose = false;
   connection.socket = socket;
   connection.reconnectToken = reconnectToken;
 
   socket.onopen = () => {
+    if (!isActiveSocket()) {
+      return;
+    }
     sendSubscriptions(connection);
     setSnapshot(connection, {
       mode: "realtime",
       chatIDs: unionChatIDs(connection),
       lastError: null,
       connectedAt: new Date().toISOString(),
     });
   };
 
   socket.onmessage = (event) => {
+    if (!isActiveSocket()) {
+      return;
+    }
     try {
       const payload = JSON.parse(String(event.data)) as {
         type?: string;
         chatIDs?: string[];
       };
@@
   };
 
   socket.onerror = () => {
+    if (!isActiveSocket()) {
+      return;
+    }
     setSnapshot(connection, {
       lastError: "Realtime connection failed.",
     });
   };
 
   socket.onclose = () => {
+    const wasManuallyClosed = connection.manualClose && connection.socket === null;
+    if (!isActiveSocket() && !wasManuallyClosed) {
+      return;
+    }
     connection.socket = null;
     if (connection.manualClose) {
       setSnapshot(connection, {
         mode: connection.attachments.size > 0 ? "polling" : "idle",
         connectedAt: null,
🧰 Tools
🪛 ESLint

[error] 236-236: Replace "web"·||·connection.profile.prefersQueryTokenRealtime with 'web'·||·connection.profile.prefersQueryTokenRealtime·?

(prettier/prettier)


[error] 237-237: Delete ·?

(prettier/prettier)


[error] 238-238: Delete ··

(prettier/prettier)


[error] 239-239: Delete ··

(prettier/prettier)


[error] 240-240: Delete ··

(prettier/prettier)


[error] 241-241: Delete ··

(prettier/prettier)


[error] 242-242: Delete ··

(prettier/prettier)


[error] 251-251: Replace "realtime" with 'realtime'

(prettier/prettier)


[error] 264-264: Replace "subscriptions.updated" with 'subscriptions.updated'

(prettier/prettier)


[error] 282-282: Replace "Realtime·connection·failed." with 'Realtime·connection·failed.'

(prettier/prettier)


[error] 290-290: Replace "polling"·:·"idle" with 'polling'·:·'idle'

(prettier/prettier)


[error] 297-297: Replace "polling" with 'polling'

(prettier/prettier)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/kitchen-sink/lib/api/session.ts` around lines 227 - 300, Handlers
for the WebSocket (socket) can run after a newer socket has been assigned to
connection.socket; guard each handler (onopen, onmessage, onerror, onclose) by
comparing the captured socket instance to the current connection.socket and bail
out if they differ, so only the active socket mutates connection and snapshot;
update the anonymous handlers in the socket setup to first if (connection.socket
!== socket) return; before doing any setSnapshot, connection.socket assignment,
connection.manualClose checks, or invalidateRealtimePayload calls.


export function probeQueryOptions(profile: ConnectionProfile) {
return queryOptions({
queryKey: queryKeys.probe(profile.id),
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Make the query keys change when the connection changes.

These keys only include profile.id, and profileStore.saveProfile() keeps that ID when a profile is edited. If a user changes baseURL or accessToken on an existing profile, React Query can reuse probe/accounts/chats/messages data from the old server/account until something explicitly invalidates it. Add a non-sensitive connection revision to the keys, or invalidate queryKeys.profilePrefix(profile.id) whenever connection settings change.

Also applies to: 45-45, 58-58, 73-73, 92-92, 106-106, 121-121

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/kitchen-sink/lib/query/queries.ts` at line 19, query keys like
queryKeys.probe(profile.id) only use profile.id so React Query will reuse data
when a profile's connection (baseURL/accessToken) changes; update these keys
(e.g., in probe, accounts, chats, messages queries at the referenced locations)
to include a non-sensitive connection revision field from the profile (such as
profile.connectionRevision or a hash of baseURL and a token-omitted flag) or,
alternatively, ensure profileStore.saveProfile() triggers query invalidation for
queryKeys.profilePrefix(profile.id) when connection settings change; adjust the
key expressions where queryKey: queryKeys.probe(profile.id) (and the similar
occurrences) are created to include that revision or add an invalidation call
after saving connection edits.

Comment on lines +20 to +38
queryFn: async (): Promise<ProbeSummary> => {
const client = createDesktopClient(profile);
const info = await client.info.retrieve();
const probe: ProbeSummary = {
checkedAt: new Date().toISOString(),
status: info.server.status,
appVersion: info.app.version,
endpoints: {
wsEvents: info.endpoints.ws_events,
spec: info.endpoints.spec,
mcp: info.endpoints.mcp,
},
info,
capabilities: deriveCapabilitiesFromInfo(info),
serverName: info.app.name,
errorMessage: null,
};
await profileStore.updateProbe(profile.id, probe);
return probe;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Persist failed probes too.

profileStore.updateProbe() only runs after info.retrieve() succeeds. Any auth/network/baseURL failure leaves lastProbeResult stuck on the previous success (or null), so the UI can keep stale capabilities and loses the actual failure reason. Wrap this in try/catch, write a failure ProbeSummary, then rethrow if you still want React Query to expose the error.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/kitchen-sink/lib/query/queries.ts` around lines 20 - 38, The queryFn
in queries.ts currently only updates profileStore.updateProbe(profile.id, probe)
after a successful client.info.retrieve(), so failures leave lastProbeResult
stale; wrap the info.retrieve() and probe creation in a try/catch: on success
behave as before (use deriveCapabilitiesFromInfo, fill endpoints, serverName,
errorMessage: null), on error build a failure ProbeSummary (checkedAt ISO
timestamp, status set to an appropriate failure value such as "unreachable" or
the error-derived status, endpoints/serverName/appVersion null or empty,
capabilities empty, and errorMessage set to the caught error message), call
profileStore.updateProbe(profile.id, failureProbe) in the catch, then rethrow
the error so React Query still surfaces it. Ensure references: queryFn,
createDesktopClient, client.info.retrieve, profileStore.updateProbe,
deriveCapabilitiesFromInfo, and ProbeSummary.

Comment on lines +161 to +166
async function commit(nextSnapshot: ProfileStoreSnapshot): Promise<ProfileStoreSnapshot> {
await persistProfiles(nextSnapshot.profiles, nextSnapshot.activeProfileId);
snapshot = nextSnapshot;
emit();
return snapshot;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Serialize profileStore writes.

Each mutator snapshots module state, awaits storage I/O, then overwrites the full store through commit(). That makes lost updates real: for example, deleteProfile() can remove an entry and a concurrent updateProbe() can commit its stale copy and resurrect it. Funnel writes through a single promise queue / mutex, or make commit() apply a functional updater against the latest snapshot.

Also applies to: 216-303

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/kitchen-sink/lib/storage/profiles.ts` around lines 161 - 166,
Multiple mutators call commit(nextSnapshot) after awaiting persistProfiles,
which overwrites the global snapshot and causes lost updates (e.g.,
deleteProfile vs updateProbe). Fix by serializing writes: introduce a
single-writer mutex or a write-queue around commit (or change commit to accept
an updater function that receives the latest snapshot and returns a new snapshot
and only then persists), ensure functions like deleteProfile, updateProbe, and
any code that calls commit use the mutex/queue or pass an updater to commit so
persistence and snapshot replace happen atomically against the latest state, and
keep persistProfiles usage inside the serialized section to prevent races.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant