Conversation
📝 WalkthroughSummary by CodeRabbit
WalkthroughThis 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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes 🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 16
🧹 Nitpick comments (4)
examples/kitchen-sink/components/search-result-card.tsx (1)
35-38: Prefer the view-model'stimestampLabelwhen it's available.
SearchResultItemalready 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 sharedDesktopInfotype here.
ProbeSummary.infois a hand-copied version of theDesktopInfomodel already exported fromexamples/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:23373as its built-in local default. Seeding the example withhttp://127.0.0.1:23373makes 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 behindas unknown as DesktopClient.This cast removes the compiler's ability to tell us when the published
@beeper/desktop-apisurface diverges from the hand-writtenDesktopClientcontract. A thin adapter object thatsatisfies DesktopClientkeeps 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
⛔ Files ignored due to path filters (7)
examples/kitchen-sink/assets/android-icon-background.pngis excluded by!**/*.pngexamples/kitchen-sink/assets/android-icon-foreground.pngis excluded by!**/*.pngexamples/kitchen-sink/assets/android-icon-monochrome.pngis excluded by!**/*.pngexamples/kitchen-sink/assets/favicon.pngis excluded by!**/*.pngexamples/kitchen-sink/assets/icon.pngis excluded by!**/*.pngexamples/kitchen-sink/assets/splash-icon.pngis excluded by!**/*.pngexamples/kitchen-sink/yarn.lockis excluded by!**/yarn.lock,!**/*.lock
📒 Files selected for processing (35)
examples/kitchen-sink/.gitignoreexamples/kitchen-sink/app.jsonexamples/kitchen-sink/app/(tabs)/_layout.tsxexamples/kitchen-sink/app/(tabs)/index.tsxexamples/kitchen-sink/app/(tabs)/lab.tsxexamples/kitchen-sink/app/(tabs)/search.tsxexamples/kitchen-sink/app/(tabs)/settings.tsxexamples/kitchen-sink/app/+not-found.tsxexamples/kitchen-sink/app/_layout.tsxexamples/kitchen-sink/app/chat/[chatID].tsxexamples/kitchen-sink/components/active-profile-banner.tsxexamples/kitchen-sink/components/chat-row.tsxexamples/kitchen-sink/components/empty-state.tsxexamples/kitchen-sink/components/json-panel.tsxexamples/kitchen-sink/components/profile-editor.tsxexamples/kitchen-sink/components/screen-shell.tsxexamples/kitchen-sink/components/search-result-card.tsxexamples/kitchen-sink/components/section-card.tsxexamples/kitchen-sink/components/status-chip.tsxexamples/kitchen-sink/components/thread-panel.tsxexamples/kitchen-sink/hooks/use-active-profile.tsexamples/kitchen-sink/hooks/use-realtime-transport.tsexamples/kitchen-sink/lib/api/client.tsexamples/kitchen-sink/lib/api/session.tsexamples/kitchen-sink/lib/mappers/index.tsexamples/kitchen-sink/lib/query/keys.tsexamples/kitchen-sink/lib/query/mutations.tsexamples/kitchen-sink/lib/query/queries.tsexamples/kitchen-sink/lib/query/query-client.tsexamples/kitchen-sink/lib/storage/profiles.tsexamples/kitchen-sink/lib/storage/tokens.tsexamples/kitchen-sink/package.jsonexamples/kitchen-sink/tsconfig.jsonexamples/kitchen-sink/types/profile.tsexamples/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
LinkwithasChildpattern 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.jsonaligns 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 directJSON.stringifyusage is acceptable.examples/kitchen-sink/package.json (1)
25-35: No changes needed.react-native-web: ^0.21.0is compatible with React 19 (peer dependencies allowreactandreact-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 forGiftedSDKMessage.Keeping
sdkMessageon 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.
| useEffect(() => { | ||
| if (wideLayout && !selectedChatID && filteredChats[0]?.id) { | ||
| setSelectedChatID(filteredChats[0].id); | ||
| } | ||
| }, [filteredChats, selectedChatID, wideLayout]); |
There was a problem hiding this comment.
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.
| 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); |
There was a problem hiding this comment.
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.
| 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, | ||
| }, | ||
| ); |
There was a problem hiding this comment.
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.
| {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} |
There was a problem hiding this comment.
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] : ["*"]); |
There was a problem hiding this comment.
🧩 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 -20Repository: 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 fRepository: beeper/desktop-api-js
Length of output: 278
🏁 Script executed:
# Search for the use-realtime-transport hook
fd "use-realtime-transport" --type fRepository: beeper/desktop-api-js
Length of output: 118
🏁 Script executed:
cat -n examples/kitchen-sink/app/chat/\[chatID\].tsxRepository: beeper/desktop-api-js
Length of output: 1080
🏁 Script executed:
cat -n examples/kitchen-sink/hooks/use-realtime-transport.tsRepository: 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 2Repository: beeper/desktop-api-js
Length of output: 92
🏁 Script executed:
# Search for other usages of useRealtimeTransport
rg "useRealtimeTransport" -B 2 -A 2Repository: 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.
| 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.
| const url = buildWebSocketURL(connection.profile); | ||
| const reconnectToken = [ | ||
| connection.profile.baseURL, | ||
| connection.profile.accessToken, | ||
| connection.profile.prefersQueryTokenRealtime ? "query" : "header", | ||
| ].join("|"); |
There was a problem hiding this comment.
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.
| 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).
| 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, | ||
| }); | ||
| }; |
There was a problem hiding this comment.
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), |
There was a problem hiding this comment.
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.
| 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; |
There was a problem hiding this comment.
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.
| async function commit(nextSnapshot: ProfileStoreSnapshot): Promise<ProfileStoreSnapshot> { | ||
| await persistProfiles(nextSnapshot.profiles, nextSnapshot.activeProfileId); | ||
| snapshot = nextSnapshot; | ||
| emit(); | ||
| return snapshot; | ||
| } |
There was a problem hiding this comment.
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.
Summary
examples/kitchen-sink@beeper/desktop-apirelease instead of a vendored local SDK copyTesting
npx expo config --jsonnpx expo export --platform web --output-dir /tmp/kitchen-sink-export