Skip to content

Conversation

@humanagent
Copy link
Collaborator

@humanagent humanagent commented Oct 25, 2025

Rework XMTP CLI to v2 with centralized useXMTP state, command palette, keyboard navigation, and updated entrypoint in src/cli.tsx

This PR replaces the old in-component XMTP logic with a hook-driven architecture and a new layout, adds a command palette and keyboard navigation, and updates the CLI entrypoint and help to v2 usage and commands.

📍Where to Start

Start with the new CLI flow in App within src/cli.tsx, then review useXMTP in src/hooks/useXMTP.ts to understand agent initialization, conversation loading, and streaming, followed by the composed UI in src/components/Layout.tsx.


📊 Macroscope summarized 4b7fd02. 16 files reviewed, 84 issues evaluated, 76 issues filtered, 2 comments posted

🗂️ Filtered Issues

src/cli-old.tsx — 0 comments posted, 28 evaluated, 25 filtered
  • line 38: Help text contradicts implemented command for changing conversation: help shows /c <number> while the app handles /chat <number>. This mismatch causes user-entered /c to be unrecognized and /chat to be undocumented, creating uncertainty about the correct command and a visible runtime UX failure. [ Low confidence ]
  • line 49: Help text claims XMTP_CLIENT_WALLET_KEY and XMTP_CLIENT_DB_ENCRYPTION_KEY are required, but the implementation falls back to generating a private key and random DB encryption key when they are missing. This contradiction creates uncertainty about required configuration and expected behavior. [ Low confidence ]
  • line 73: isGroup uses conversation.constructor.name === "Group" for type/instance detection. Relying on constructor names is brittle and can fail under minification, differing realms, or subclassing, leading to misclassification. If misclassified, the code may call getEthereumAddress on a group or skip DM-specific rendering incorrectly, causing wrong UI and potential errors. [ Low confidence ]
  • line 74: The isGroup type guard relies on conversation.constructor.name === "Group", which is a fragile runtime check. Constructor names can be altered by minification, bundlers, cross-realm objects, or SDK changes. If it misclassifies a Group as not a group, downstream code assumes a DM and accesses DM-only fields, leading to runtime errors. This misclassification is reachable and causes incorrect behavior in Header when it casts to Dm and uses peerInboxId. [ Low confidence ]
  • line 77: isDm relies on conversation.constructor.name === "Dm" to discriminate the Conversation subtype. Using constructor.name is a fragile runtime check: it can fail under minification/obfuscation, cross-realm objects, subclassing, or SDK implementation changes where the class name differs (e.g., renamed, mangled). A false negative will cause valid DM conversations to be treated as non-DM, altering behavior (e.g., skipping DM-specific logic when scanning existing conversations) without an explicit error. Prefer a stable discriminant provided by the SDK (e.g., a kind property or an official type guard) or instanceof Dm if the Dm constructor is imported from the same realm. [ Low confidence ]
  • line 81: isEthAddress validates only by checking startsWith("0x") and length === 42. It does not verify that the 40 characters after 0x are valid hexadecimal digits. This will treat any 42-character string starting with 0x (including invalid characters like 'Z' or Unicode letters) as an Ethereum address. In the example usage, this can cause newGroupWithIdentifiers or newDmWithIdentifier to be called with invalid Ethereum identifiers, likely resulting in downstream API errors or hard-to-diagnose failures. Additionally, this function rejects uppercase 0X prefixes, which are sometimes encountered, causing valid addresses to be misclassified and routed down the non-Ethereum path. [ Low confidence ]
  • line 91: error is typed as unknown and is cast to Error (const err = error as Error;) and then err.message is used to build the error string. If error is not actually an Error (e.g., a string, number, plain object without a message field, or null/undefined), err.message will be undefined, producing an unhelpful message like "<context>: undefined". This silently drops the original error value and yields misleading output rather than a graceful fallback. [ Low confidence ]
  • line 94: Comment says Auto-clear error after specified time (default 5 seconds) but the implementation does not provide a default. clearAfter is optional and only if it is truthy does the timeout run. When clearAfter is undefined, the error will not auto-clear, contradicting the stated default behavior. [ Already posted ]
  • line 95: if (clearAfter) treats 0 as falsy and will not schedule the timeout when clearAfter is 0. If 0 is provided to mean immediate clearing (a valid number), this leads to unexpected behavior. The check should explicitly test for clearAfter !== undefined or >= 0 depending on intended contract. [ Low confidence ]
  • line 95: The timeout created to auto-clear errors is not tracked or canceled. Multiple calls to handleError will schedule overlapping timers, allowing an earlier timer to clear a later error, causing race conditions and inconsistent UI/CLI state. Additionally, if the owning component or process is torn down before the timer fires, setError("") may attempt to update state after unmount, causing warnings or errors in React-like environments. A single paired cleanup is missing: store the timeout ID, cancel prior pending clears on new errors, and clear on unmount. [ Low confidence ]
  • line 216: In Header, the DM branch accesses (conversation as Dm).peerInboxId.slice(0, 16) based solely on isGroup(conversation) being false. If isGroup misclassifies a Group (or any non-DM conversation) as not a group, conversation is not a Dm, making peerInboxId undefined and causing a runtime error on .slice. This path is reachable due to the fragile isGroup guard and lacks a safe narrowing check for Dm. [ Low confidence ]
  • line 323: getEthereumAddress assumes conversation.members() returns an iterable array. If members() returns null, undefined, or a non-iterable due to transient errors or SDK behavior, the for...of loop will throw, causing the effect to reject and the UI to fail without recovery. There is no defensive check for empty/missing members. [ Already posted ]
  • line 325: getEthereumAddress may return the current user’s own Ethereum address instead of the peer’s address in a DM. It iterates all members() and returns the first Ethereum identifier found, without excluding self. For DMs, you typically want the other participant’s identifier; returning self results in incorrect labeling of the DM in the UI. [ Already posted ]
  • line 346: The async effect loadAddresses does not handle errors from getEthereumAddress or conversation.members(). If either rejects, the promise returned by loadAddresses will reject, which in React can surface as an unhandled error and the component will remain stuck with loading set to true. There is no try/catch nor a fallback to set loading to false on failure, so the UI can show "Loading conversations..." indefinitely and the error is not contained. [ Already posted ]
  • line 346: The async effect loadAddresses does not guard against race conditions when conversations changes while a previous load is in flight. Multiple invocations can overlap; a slower, older invocation may complete last and overwrite addressMap and loading with stale data. There is no cancellation or staleness check, leading to potential display of wrong addresses and incorrect loading state transitions. [ Low confidence ]
  • line 389: DM conversations without a resolvable Ethereum address are silently dropped from the rendered list. In the map callback, when peerShort is falsy (no address found), the function returns undefined and renders nothing for that conversation, yielding incomplete UI with no fallback label (e.g., a placeholder or conv.name). This violates the visible contract of listing conversations and leaves users with no way to select or view such DMs. [ Low confidence ]
  • line 400: Conversation list silently omits DM entries when addressMap[conv.id] is undefined, returning nothing from the map() callback. This results in inconsistent or missing list items, while selection uses indices from the full conversations array. Users may see fewer items than actually exist, and indices will not match the visible list. [ Low confidence ]
  • line 445: Error timeout created by setErrorWithTimeout is not cleaned up on component unmount, potentially causing setState calls after unmount. There is no useEffect teardown to clear errorTimeout when the component exits or when a new timeout is set in quick succession beyond the explicit local clear. This can lead to memory leak warnings and violates single paired cleanup after introducing a timer effect. [ Low confidence ]
  • line 462: Environment variable handling incorrectly generates both walletKey and dbEncryptionKey if either is missing, discarding any provided value. The condition if (!walletKey || !dbEncryptionKey) regenerates both values, meaning a caller who sets only XMTP_CLIENT_DB_ENCRYPTION_KEY but not XMTP_CLIENT_WALLET_KEY will have their encryption key silently replaced. Each missing value should be generated independently to preserve any provided key. [ Low confidence ]
  • line 488: finalInboxState[0] is accessed without verifying the array has at least one element. If Client.inboxStateFromInboxIds returns an empty array (e.g., transient network error or unknown inbox ID), finalInboxState[0] will be undefined and accessing .installations.length will throw. [ Low confidence ]
  • line 618: formatMessage labels messages not sent by the current user with a shortened version of the current user's own address, rather than the actual sender. This misidentifies message senders in the UI. It should resolve and display the peer's identifier/address for incoming messages. [ Already posted ]
  • line 649: The message stream is never cleaned up or cancelled on component unmount or when exiting, causing resource leaks and potential stray updates. client.conversations.streamAllMessages() is started, but there is no paired cleanup call or cancellation token, and no useEffect teardown. This violates at-most-once and single paired cleanup guarantees after introducing an external resource. [ Low confidence ]
  • line 852: Potentially invalid env value propagated as XmtpEnv without runtime validation. parseArgs() assigns const env = (process.env.XMTP_ENV as XmtpEnv) || "production"; which preserves any non-empty, possibly invalid string as env. This value is returned typed as XmtpEnv and passed to <App env={env} />, likely into Agent.create. If XMTP_ENV is set to an unsupported value, downstream code may fail at runtime. A defensive validation (e.g., check against allowed set) is needed. [ Low confidence ]
  • line 876: Unsafe call to privateKeyToAddress with potentially empty or invalid private key in parseArgs() dev auto-detect path. When no --agent is provided and env === "dev", the code reads process.env.XMTP_WALLET_KEY || "" into autoAgentAddressKey and immediately calls privateKeyToAddress(autoAgentAddressKey as \0x${string}`). If the env var is not set (empty string) or is not a valid 0x-prefixed 32-byte key, viem/accounts` will throw at runtime. There is no try/catch around this call, so the CLI will crash instead of handling the error or falling back. [ Already posted ]
  • line 902: Unsafe call to privateKeyToAddress with potentially invalid key in main() when no agents are specified. The code reads walletKey = process.env.XMTP_WALLET_KEY || "", checks only that it is truthy, and then calls privateKeyToAddress(walletKey as \0x${string}`). If XMTP_WALLET_KEYis set but malformed (e.g., missing0xprefix, wrong length),privateKeyToAddress` will throw at runtime and crash the CLI. [ Already posted ]
src/cli.tsx — 0 comments posted, 10 evaluated, 10 filtered
  • line 57: Help text documents environment variables XMTP_CLIENT_WALLET_KEY and XMTP_CLIENT_DB_ENCRYPTION_KEY, but parseArgs() uses process.env.XMTP_WALLET_KEY to auto-detect the agent address. This contradiction means users following the help will set XMTP_CLIENT_WALLET_KEY, and auto-detection will silently fail because the code reads XMTP_WALLET_KEY. This creates uncertainty about which variable is correct and yields incorrect runtime behavior (no auto-detected agent). [ Low confidence ]
  • line 141: Arrow keys inside an active conversation do nothing. In useKeyboard, the up/down arrow handlers call onSwitchConversation?.("prev"|"next") when inMainMenu is false, but the App component does not supply onSwitchConversation. As a result, arrow navigation within a conversation produces no state change and no visible effect. [ Already posted ]
  • line 154: The custom suggestions key handler checks key.tab to complete a suggestion. Ink's useInput key object does not document a tab property in all versions; relying on key.tab may result in non-functional tab completion. If Ink does not set key.tab, this branch will never execute. Consider handling input === "\t" or verifying Ink's API for tab support. [ Low confidence ]
  • line 163: Pressing Enter while command suggestions are shown can execute the selected command via the custom useInput handler AND also trigger the onInputSubmit (text input submit) path in handleInputSubmit, leading to double-application or contradictory outcomes. For example, selecting /toggle-sidebar via suggestions runs command.action(), but handleInputSubmit doesn't recognize /toggle-sidebar (only /back, /list, /chat, /new, /refresh, /exit), which then emits an "Unknown command" error. This violates at-most-once execution and contract parity for commands in the suggestions path. [ Low confidence ]
  • line 210: Error timeout created in setErrorWithTimeout is never cleaned up on component unmount, which can cause setState calls (setError, setErrorTimeout) after the component has unmounted. This is a runtime bug leading to memory leaks and React warnings. Add a cleanup effect to clear the timeout on unmount. Offending logic is in setErrorWithTimeout where const timeout = setTimeout(...) is stored in errorTimeout state without a corresponding teardown. [ Low confidence ]
  • line 408: After refreshConversations() completes in the /refresh command, the code reports Found ${conversations.length} conversations using the hook's conversations state captured before the refresh. Since refreshConversations updates conversations asynchronously, the count can be stale or incorrect. The message should derive from the returned list length or from the updated state (e.g., read after the state update) to maintain contract parity. [ Already posted ]
  • line 497: env is derived as (process.env.XMTP_ENV as XmtpEnv) || "production" without validating the actual value against the allowed XmtpEnv domain. If XMTP_ENV is set to an invalid string (e.g., "prod"), it will be treated as a truthy value and passed through as the env prop to <App>, potentially causing downstream misconfiguration or errors. A type assertion does not enforce runtime validity; a whitelist check is needed. [ Out of scope ]
  • line 518: Behavioral contract change: auto-detection of agent address is now performed regardless of env (dev vs production). Previously it was gated on env === "dev". This can cause unintended agent selection in production if XMTP_WALLET_KEY is set, altering externally visible behavior without a clear opt-in. Combined with the env var name mismatch in help, users may not realize production auto-detection is active or may fail to activate it if they set the documented variable. [ Low confidence ]
  • line 521: parseArgs() calls privateKeyToAddress(walletKey as 0x${string}) with walletKey derived from process.env.XMTP_WALLET_KEY without validating the format. If XMTP_WALLET_KEY is set but malformed (e.g., missing 0x prefix or wrong length), privateKeyToAddress will throw, causing the CLI to crash during startup. Previously this auto-detection was restricted to env === "dev"; now it runs unconditionally, making the crash reachable in production environments when agents is empty. [ Already posted ]
  • line 529: Configuration env var mismatch can lead to a confusing runtime state: parseArgs uses XMTP_WALLET_KEY to auto-detect and populate agents, which are passed as agentIdentifiers, while the XMTP client initialization (in useXMTP) expects XMTP_CLIENT_WALLET_KEY and will generate a random private key if it’s not set. If only XMTP_WALLET_KEY is provided, the app may start with a randomly generated agent that doesn’t match the agentIdentifiers, creating conversations from a different identity than intended. This is a contract inconsistency between CLI argument parsing and client initialization. Align the env var names or cascade the same key to both places so the agent identity matches the provided identifiers. [ Low confidence ]
src/components/ChatView.tsx — 0 comments posted, 2 evaluated, 2 filtered
  • line 27: In ChatView, height is used to compute visibleMessages = messages.slice(-height) and passed to minHeight={height}. If height is 0 or negative, slice(-height) will behave unintuitively (e.g., height=0 returns all messages; height=-5 returns from index 5), and minHeight may be invalid for the layout, causing incorrect rendering. There is no validation ensuring height is a positive integer. [ Already posted ]
  • line 52: When conversation is null, ChatView renders ConversationSelector with conversations={allConversations} defaulting to [] and selectedIndex={selectedConversationIndex} defaulting to 0. If the ConversationSelector implementation assumes the selected index points to a valid element, passing 0 with an empty array can cause an out-of-bounds access and a runtime error. There is no guard adjusting the index when the list is empty or ensuring it is within bounds. [ Already posted ]
src/components/CommandPalette.tsx — 0 comments posted, 6 evaluated, 5 filtered
  • line 12: The component does not use the Command.action function defined by the Command interface. Instead, it forwards the selected Command to onExecute. This creates a contract mismatch: callers supplying commands with action set may reasonably expect the palette to invoke command.action(). Unless explicitly documented, this can lead to commands never executing their intended actions. [ Low confidence ]
  • line 38: selectedIndex is only reset on query change (useEffect([query])). If the commands prop changes (e.g., new data is loaded, items removed), selectedIndex may point to a now-invalid position. There is no guard to clamp or reset selectedIndex on commands change, enabling out-of-bounds selection and undefined execution targets. [ Already posted ]
  • line 58: onExecute is invoked before onClose without error containment. If onExecute throws (e.g., user command fails), onClose is not called, leaving the palette open and violating the lifecycle expectation documented in the UI footer. Wrap execution to ensure onClose is always invoked exactly once. [ Low confidence ]
  • line 59: selectedIndex can become out-of-bounds relative to filteredCommands, leading to onExecute receiving undefined at runtime. Two reachable cases: (1) when filteredCommands.length === 0, the up-arrow handler sets selectedIndex to -1 (filteredCommands.length - 1), and if commands later becomes non-empty without a query change, pressing Enter passes filteredCommands[-1] (undefined) to onExecute; (2) when commands changes (shrinks) without a query change, selectedIndex may remain larger than the new filteredCommands.length - 1, causing filteredCommands[selectedIndex] to be undefined. The only guard is filteredCommands.length > 0, which does not prevent out-of-bounds or negative index access. [ Already posted ]
  • line 111: When rendering, the list is sliced to 10 items (filteredCommands.slice(0, 10)), but selection navigation wraps across the full filteredCommands.length. This allows selectedIndex to point at items beyond the first 10, which are not rendered/highlighted. Pressing Enter will execute a non-visible item, violating the visible selection invariant and causing confusing behavior. [ Already posted ]
src/components/CommandSuggestions.tsx — 0 comments posted, 3 evaluated, 3 filtered
  • line 47: CommandSuggestions highlights based on selectedIndex compared against indices of suggestions.slice(0, 6). If selectedIndex is negative, NaN, or refers to an item outside the sliced range (>= 6 or >= suggestions.length), no item is highlighted, causing inconsistent selection UI. There’s no clamping or normalization to ensure selectedIndex maps within the rendered subset, so selection can silently disappear when the list is long. [ Already posted ]
  • line 50: suggestion.id is used as the React key (<Box key={suggestion.id}>). If id values are not unique across the rendered slice, React reconciliation can behave incorrectly (items reordering, incorrect reuse), causing subtle UI bugs. There’s no explicit guarantee or validation of uniqueness at runtime. [ Low confidence ]
  • line 52: Text receives color={RED} where RED is a hex string ("#fc4c34"). Ink’s Text color prop is typically limited to named ANSI colors; passing a hex string may be ignored or fall back, resulting in missing highlight color at runtime. This causes the selected item arrow and name to potentially render without the intended color. [ Low confidence ]
src/components/ConversationSelector.tsx — 0 comments posted, 5 evaluated, 5 filtered
  • line 11: The ConversationSelectorProps declares a required onSelect callback prop, but the ConversationSelector component never destructures or invokes onSelect. This breaks contract parity: callers may pass onSelect expecting selection behavior, yet the component provides no path to use it. It also creates ambiguity about how selection is handled and can lead to inconsistent UX where "Enter" does nothing within this component. [ Low confidence ]
  • line 22: Hex color strings are passed to Ink components via the color prop (e.g., color={RED} where RED is "#fc4c34") and backgroundColor with "#2a2a2a". Depending on Ink version, Text/Box may only accept named colors, not hex strings, leading to missing colors or inconsistent rendering. Consider using supported named colors or props that explicitly accept hex (or verify Ink supports hex values). [ Low confidence ]
  • line 71: Selected index is not validated or clamped. If selectedIndex is less than -1 or greater than the last conversation index, no item is highlighted and the UI becomes ambiguous. There is no fallback state or error indicator when selectedIndex is out-of-range, making it harder to recover from navigation state bugs. [ Code style ]
  • line 76: Keys for list items use conv.id without validating uniqueness. If conversations contains duplicate ids, React reconciliation can produce stale or incorrect row updates, violating the rule-validated sequence constraint (uniqueness) and causing rendering artifacts. [ Code style ]
  • line 115: formatTime(conv.lastMessageAt) is called when conv.lastMessageAt is truthy but without validating it is a Date instance. If lastMessageAt is a non-Date value (e.g., a string due to deserialization), formatTime will attempt getTime() and may yield NaN-based calculations or throw when calling toLocaleDateString, causing runtime errors or malformed output. Guard with instanceof Date (and !isNaN(date.getTime())) before invoking formatTime. [ Low confidence ]
src/components/Sidebar.tsx — 0 comments posted, 5 evaluated, 4 filtered
  • line 32: The component assumes conversations is always a non-null array and immediately reads conversations.length. If a caller passes null or undefined (e.g., due to an async fetch not yet resolved or an integration without TS checks), this will throw a runtime error. Consider a defensive default (e.g., conversations ?? []) or a prop-level guard. [ Low confidence ]
  • line 47: Using conv.id as the React key without ensuring uniqueness can cause incorrect list diffing/rendering when two conversations share the same id. React relies on unique keys to preserve element identity; duplicates can lead to items being merged, reordered incorrectly, or failing to update. Consider enforcing unique IDs or fallback to a composite key (e.g., index + id) with a clear uniqueness guarantee. [ Low confidence ]
  • line 49: The component uses hex color strings (RED = "#fc4c34", DIM_RED = "#cc3f2a") via the Text color prop (e.g., color={RED}). Depending on Ink version, Text may only accept named colors rather than hex strings. If hex is unsupported, colors may be ignored or could throw at runtime. Use supported color names or Ink’s mechanisms (e.g., Chalk hex via custom rendering) to ensure consistent behavior. [ Low confidence ]
  • line 73: Long-name truncation ignores the available width. For conv.name.length > 20, the component renders a fixed "${first6}...${last6}" (15 characters) regardless of the container width. If width is less than the space required by the line elements, this can overflow/clobber layout. Conversely, if width is much larger, the name is unnecessarily truncated. The truncation strategy should consider width to avoid overflow and use available space. [ Low confidence ]
src/hooks/useXMTP.ts — 0 comments posted, 5 evaluated, 5 filtered
  • line 51: In getEthereumAddress, the code assumes member.accountIdentifiers is a defined array and calls .find(...) on it. If accountIdentifiers is undefined or null for any member, this will throw a runtime error. There is no guard or fallback to handle missing identifiers. [ Already posted ]
  • line 80: In toConversationInfo, the else branch casts conversation to Dm (const dm = conversation as Dm;) and then uses dm.peerInboxId.slice(0, 16). If isGroup misclassifies a Group or if conversation is not a Dm for any reason, peerInboxId may be undefined, causing a runtime TypeError when calling .slice on undefined. This path lacks any guard to verify peerInboxId is present. [ Low confidence ]
  • line 204: The XMTP message stream is never torn down on unmount or conversation change. streamRef.current is assigned and the for-await loop keeps running and calling setMessages even after unmount, which can cause memory leaks and setState-after-unmount warnings. The effect cleanup only clears the periodic refresh interval, not the message stream. [ Low confidence ]
  • line 301: Message streaming in useXMTP does not handle conversation switching. startMessageStream starts a single global stream and filters messages by the conv.id passed on first start. Later calls (e.g., from setCurrentConversationById) won't start a new stream because isStreamingRef.current is already true, and the filtering continues to target the original conversation only. This breaks live updates for subsequent selected conversations. [ Already posted ]
  • line 365: Stale state update in useXMTP.findOrCreateConversation. After creating a conversation, the code uses setConversations([...conversations, convInfo]). Because conversations is captured from the hook's closure, concurrent updates or rapid successive additions can be lost (classic React state staleness). Use the functional updater setConversations(prev => [...prev, convInfo]) to avoid dropping entries. [ Low confidence ]
src/store/state.ts — 0 comments posted, 6 evaluated, 6 filtered
  • line 12: env: (process.env.XMTP_ENV as XmtpEnv) || "production" silently trusts and coerces the environment string. If XMTP_ENV is set to an invalid value (not a member of XmtpEnv), the cast suppresses type safety and stores an invalid env at runtime. Additionally, an empty string in XMTP_ENV will be treated as falsy and coerced to "production", potentially masking configuration errors. Downstream consumers relying on valid XmtpEnv semantics can misbehave or select the wrong environment. [ Low confidence ]
  • line 12: process.env.XMTP_ENV is read directly in a frontend store initializer (useStore), which can crash at runtime in browser environments where process is undefined. Many bundlers expose import.meta.env or inline process.env.* during build; if that substitution is not present, accessing process.env throws a ReferenceError at store creation time. This occurs at env: (process.env.XMTP_ENV as XmtpEnv) || "production" and would take down the app during initialization. [ Low confidence ]
  • line 39: setAgent sets connectionStatus to "connected" regardless of the validity of the provided agent (including when agent is null). This can yield an inconsistent state: agent is null, address and inboxId are empty strings, yet connectionStatus says connected. That violates the implied invariant that a connected state requires a non-null agent. [ Low confidence ]
  • line 47: setCurrentConversation(null) sets selectedConversationIndex to 0, which contradicts the store’s design where -1 represents “New Chat / no selection”. This can select the first conversation even when currentConversation is explicitly cleared. In the case where conversations is empty, it also sets an out-of-range index 0. Example usage shows the need to immediately call selectConversation(-1) to counteract this. The setter should set selectedConversationIndex to -1 when conversation is null. [ Already posted ]
  • line 48: setCurrentConversation(conversation) sets selectedConversationIndex to the result of findIndex, which is -1 if the provided conversation isn’t present in conversations. This yields a mismatch: currentConversation is set to a non-null value, but selectedConversationIndex becomes -1, which elsewhere represents “New Chat”. The UI and navigation logic can become inconsistent. The setter should handle -1 (not found) explicitly instead of silently assigning -1. [ Already posted ]
  • line 97: selectConversation(index) updates only selectedConversationIndex and does not update currentConversation. This creates possible divergence between the selected index and the currentConversation object used elsewhere. For example, navigation and selection will change the index, but currentConversation can remain pointing to a stale/previous conversation or null. This breaks interface parity between actions that change conversation selection vs. conversation object and leads to inconsistent UI/state. [ Already posted ]
src/utils/formatters.ts — 0 comments posted, 8 evaluated, 8 filtered
  • line 9: formatMessage calls message.sentAt.toLocaleTimeString(...) without validating that message.sentAt is a valid Date. If sentAt is missing, null, not a Date instance, or an invalid Date, this can throw a runtime RangeError or cause an unexpected failure. There is no guard or fallback. [ Low confidence ]
  • line 16: formatMessage displays the wrong sender for non-self messages: it uses selfAddress (the viewer's address) instead of any field from the message itself. As written, every incoming message from others will be labeled with the viewer's truncated address rather than the actual sender, which is a correctness bug and misleading to users. The fallback to message.senderInboxId only occurs when selfAddress is falsy, not when it is present but unrelated to the sender. [ Already posted ]
  • line 19: formatMessage uses message.senderInboxId.slice(...) without guarding for message.senderInboxId being missing or non-string. If it is undefined or null, this will throw a runtime error. The path is reachable when selfAddress is falsy for a non-self message. [ Already posted ]
  • line 26: formatMessage attempts to stringify non-string message.content using JSON.stringify inside a try block (pretty-print), then falls back inside the catch to a second JSON.stringify without a surrounding try/catch. If the object contains circular references or unsupported types (e.g., BigInt values), JSON.stringify will throw again and bubble out, causing a runtime error. There is no final safe fallback. [ Low confidence ]
  • line 28: formatMessage can return content as undefined when message.content is undefined: JSON.stringify(undefined) returns undefined. This violates the declared FormattedMessage contract (content: string) and can cause downstream consumers expecting a string to fail at runtime (e.g., calling string methods). There is no check or default for this case. [ Low confidence ]
  • line 45: formatAddress can compute a negative end when length < 3 (end = length - start - 3). Passing a negative value into address.slice(-end) alters semantics (becomes a positive index), which can produce output longer than the requested length or otherwise malformed. No guard ensures length is large enough, so small length values lead to unexpected results. [ Already posted ]
  • line 56: formatTime treats future dates as "now": if date is in the future, diffMs is negative and diffMins < 1 is true, so it returns "now". This is misleading for future timestamps and breaks time semantics. No guard differentiates past vs future. [ Already posted ]
  • line 61: formatTime lacks validation for date being a valid Date. If date.getTime() returns NaN (invalid date), all comparisons will be false, leading to the final toLocaleDateString call, which can throw a RangeError for an invalid time value. There is no guard or fallback for invalid dates. [ Already posted ]
src/utils/helpers.ts — 0 comments posted, 3 evaluated, 3 filtered
  • line 4: The type guard isGroup relies on conversation.constructor.name === "Group", which is brittle at runtime. If the SDK classes are minified, proxied, or instantiated across realms, constructor.name may not equal "Group" even for a Group. This can cause toConversationInfo to misclassify a Group as a Dm, leading to unsafe casts and downstream property access errors. [ Low confidence ]
  • line 8: The type guard isDm uses conversation.constructor.name === "Dm", which is brittle for the same reasons as isGroup. If used elsewhere (it is imported), it can misclassify and lead to unsafe casts and property access. Even if not exercised in the shown code, it represents a reachable runtime risk when called. [ Low confidence ]
  • line 19: handleError casts unknown to Error and uses err.message. If error is a non-Error value (e.g., a string or a plain object), err.message will be undefined and the function returns a message like "context: undefined", losing the original error content. While it avoids crashes, it silently drops data and produces unhelpful output. [ Code style ]

@humanagent humanagent changed the title Update v1 Oct 25, 2025
@humanagent humanagent merged commit 9bd9c75 into main Oct 25, 2025
1 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants