Skip to content

Develop - #123

Merged
ucswift merged 6 commits into
masterfrom
develop
Aug 8, 2026
Merged

Develop#123
ucswift merged 6 commits into
masterfrom
develop

Conversation

@ucswift

@ucswift ucswift commented Aug 8, 2026

Copy link
Copy Markdown
Member

Pull Request Description

This PR introduces a department-scoped feature flag system to the Resgrid Dispatch mobile app, with the Chat system as the first feature gated behind it.

What was added

  1. Feature Flags API Client (src/api/feature-flags/feature-flags.ts): New API integration to evaluate feature toggles from the v4 FeatureToggles API, supporting both bulk retrieval (GetAll) and single-flag state checks (GetState).

  2. Feature Flags Store (src/stores/feature-flags/store.ts): A persisted Zustand store that fetches and caches feature flags. It retains flags on fetch failure to keep gating stable while offline, and unknown flags default to disabled until the server confirms them.

  3. Chat Feature Gating: The Chat.System flag controls all chat surfaces:

    • App initialization (_layout.tsx): Feature flags are fetched after security rights load; the SignalR chat hub only connects when chat is enabled.
    • All chat screens (chat.tsx, chatbot.tsx, chat/[channelId].tsx, chat/thread/[messageId].tsx): Redirect to home when chat is disabled, and skip data-fetching effects.
    • Side menu (side-menu.tsx): Chat and Assistant menu items are hidden when the flag is off.
  4. Documentation: Updated audio stream refactoring docs with Expo SDK 56 migration guidance for expo-audio/expo-av.

Summary by CodeRabbit

  • New Features
    • Added feature-flag support for retrieving and managing application settings.
    • Chat and Assistant navigation and connections are now controlled by chat availability.
    • Chat screens show loading states and redirect to Home when chat is unavailable.
    • Added chatbot message actions, including copy, edit, flag, and pin.
    • Assistant messages now support tailored actions, and urgent replies can be disabled.
  • Bug Fixes
    • Improved chatbot response handling and chat acknowledgement processing.
  • Documentation
    • Added Expo SDK 56 audio streaming migration guidance.
  • Tests
    • Added comprehensive feature-flag persistence and fail-closed coverage.

@Resgrid-Bot

This comment has been minimized.

@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@ucswift, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 19 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3f2a29d1-2ace-4017-ad24-f3a583f2d537

📥 Commits

Reviewing files that changed from the base of the PR and between 2dba29a and 6fd09bf.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (4)
  • package.json
  • src/app/chat/[channelId].tsx
  • src/components/chat/__tests__/chat-utils.test.ts
  • src/components/chat/chat-utils.ts
📝 Walkthrough

Walkthrough

Added API-backed feature flags with persisted Zustand state. The ChatSystem flag now controls chat hub initialization, chat screens, chatbot loading, thread and channel requests, redirects, and sidebar visibility. Updated chatbot response handling and added Expo audio migration guidance.

Changes

Feature flag chat gating

Layer / File(s) Summary
Feature flag API and store
src/api/feature-flags/feature-flags.ts, src/stores/feature-flags/store.ts, src/stores/feature-flags/__tests__/store.test.ts
Added typed API helpers and a persisted, identity-aware store with fail-closed status handling and chat-specific hooks. Added tests for fetching, persistence, identity changes, failures, and defaults.
Chat initialization gate
src/app/(app)/_layout.tsx, src/stores/signalr/signalr-store.ts
Loads feature flags after security rights and connects or disconnects the chat hub according to ChatSystem.
Chat surface and route gating
src/app/(app)/chat.tsx, src/app/(app)/chatbot.tsx, src/app/chat/..., src/components/sidebar/side-menu.tsx
Shows loading while chat status is unresolved, redirects disabled routes to /home, guards chat data operations, and hides Chat and Assistant menu entries when disabled.
Chatbot contracts and message behavior
src/models/v4/chat/chatbotModels.ts, src/api/chat/chatbot.ts, src/components/chat/..., src/stores/chat/store.ts
Uses { Data } chatbot responses, adds assistant message actions, disables urgent thread replies, filters assistant actions, and ignores self-originated acknowledgement events.

Audio migration guidance

Layer / File(s) Summary
Expo audio migration requirements
docs/audio-stream-refactoring.md
Documents the Expo SDK 56 expo-audio migration, expo-av removal, device testing, and SDK 54 compatibility constraints.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Sequence Diagram(s)

sequenceDiagram
  participant TabLayout
  participant FeatureFlagStore
  participant FeatureFlagAPI
  participant SignalRStore
  participant ChatScreen

  TabLayout->>FeatureFlagStore: fetch feature flags
  FeatureFlagStore->>FeatureFlagAPI: getAllFeatureFlags()
  FeatureFlagAPI-->>FeatureFlagStore: ChatSystem state
  alt ChatSystem enabled
    TabLayout->>SignalRStore: connectChatHub()
    ChatScreen->>FeatureFlagStore: read chat status
    ChatScreen->>SignalRStore: fetch chat data
  else ChatSystem disabled
    TabLayout->>SignalRStore: skip chat connection
    ChatScreen-->>ChatScreen: redirect to /home
  end
Loading

Possibly related PRs

  • Resgrid/Dispatch#72: Modifies the sidebar component that now filters Chat and Assistant entries by ChatSystem.
  • Resgrid/Dispatch#113: Modifies app initialization and SignalR integration used by the new chat feature-flag gate.
  • Resgrid/Dispatch#122: Adds the chat and chatbot integration points updated by this change.

Suggested reviewers: github-actions

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title "Develop" is too generic and does not identify the feature-flag and chat-gating changes. Replace the title with a concise description such as "Add feature flags to gate chat initialization and access".
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch develop

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/app/chat/[channelId].tsx (1)

69-78: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Gate the remaining channel requests.

Lines 69-78 skip channel activation, but the presence request at Lines 82-94 and markChannelRead at Lines 97-101 still run when chat is disabled. A disabled deep link can therefore read presence or update chat state before navigation completes.

Add isChatEnabled guards and dependencies to both effects.

Proposed guard changes
 useEffect(() => {
+  if (!isChatEnabled) return;
   const ids = (members ?? []).map((m) => m.UserId).filter((id): id is string => !!id && id !== currentUserId);
   if (ids.length === 0) return;
   // ...
-}, [members, currentUserId]);
+}, [members, currentUserId, isChatEnabled]);

 useEffect(() => {
-  if (channelId && inverted.length > 0) {
+  if (isChatEnabled && channelId && inverted.length > 0) {
     void useChatStore.getState().markChannelRead(channelId);
   }
-}, [channelId, inverted.length]);
+}, [channelId, inverted.length, isChatEnabled]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/chat/`[channelId].tsx around lines 69 - 78, Update the
presence-request effect and the markChannelRead effect in the chat channel
component to return early when isChatEnabled is false, matching the existing
guard around setActiveChannel and channel loading. Add isChatEnabled to both
effects’ dependency arrays while preserving their current behavior when chat is
enabled.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/audio-stream-refactoring.md`:
- Around line 9-10: Update the later Overview, Key Changes, and Installation
sections in the audio-stream refactoring document to consistently describe the
expo-audio SDK 56 migration using createAudioPlayer, setAudioModeAsync,
AudioPlayer, and playbackStatusUpdate; remove or explicitly label any remaining
expo-av guidance as an SDK 54 baseline.
- Around line 3-5: Update the “Expo SDK 56 migration requirement” guidance to
sequence the expo-audio dependency update during the SDK 56 upgrade rather than
before it, and describe alignment through the SDK upgrade process. If specifying
a package version, use the tested SDK 56-compatible expo-audio version;
otherwise remove the unsupported prerequisite-version wording while retaining
the expo-av replacement requirement.

In `@src/stores/feature-flags/store.ts`:
- Around line 62-66: Separate unresolved feature flags from disabled flags by
adding non-persisted current-department resolution state alongside the enabled
value in useFeatureFlag within src/stores/feature-flags/store.ts#L62-L66. Update
the redirect/rendering logic in src/app/(app)/chat.tsx#L109-L112,
src/app/(app)/chatbot.tsx#L68-L71, src/app/chat/[channelId].tsx#L239-L242, and
src/app/chat/thread/[messageId].tsx#L101-L104 to defer redirects while
unresolved, using ternary rendering for pending, enabled, and disabled states;
the store change is the root fix and all listed route sites require
corresponding updates.
- Around line 55-58: Scope feature-flag persistence to the current
DepartmentCode, or clear the feature-flags store before logout and department
changes. Update the feature-flags store around the feature-flags-storage
configuration and ensure stale flags cannot survive identity changes or fetch
failures for a new department.

---

Outside diff comments:
In `@src/app/chat/`[channelId].tsx:
- Around line 69-78: Update the presence-request effect and the markChannelRead
effect in the chat channel component to return early when isChatEnabled is
false, matching the existing guard around setActiveChannel and channel loading.
Add isChatEnabled to both effects’ dependency arrays while preserving their
current behavior when chat is enabled.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 0aaa6653-a6c7-4828-bf10-32dd7eb6935c

📥 Commits

Reviewing files that changed from the base of the PR and between f66174d and bb6b160.

📒 Files selected for processing (9)
  • docs/audio-stream-refactoring.md
  • src/api/feature-flags/feature-flags.ts
  • src/app/(app)/_layout.tsx
  • src/app/(app)/chat.tsx
  • src/app/(app)/chatbot.tsx
  • src/app/chat/[channelId].tsx
  • src/app/chat/thread/[messageId].tsx
  • src/components/sidebar/side-menu.tsx
  • src/stores/feature-flags/store.ts

Comment on lines +3 to +5
## Expo SDK 56 migration requirement

Before upgrading Dispatch to Expo SDK 56, upgrade `expo-audio` to the SDK 56-compatible version and replace all remaining `expo-av` audio usage with `expo-audio`. SDK 56 no longer provides the legacy Expo Modules Core header required by `expo-av` 16, so leaving `expo-av` installed can break the iOS archive build.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Locate document and relevant docs/config:"
fd -a 'audio-stream-refactoring\.md|app\.json|app\.config\.(js|ts)|package\.json|expo\.config\.(js|ts)|babel\.config' . | sed 's#^\./##' | head -200

echo
echo "Show audio-stream-reffactoring.md context:"
if [ -f docs/audio-stream-refactoring.md ]; then
  nl -ba docs/audio-stream-refactoring.md | sed -n '1,220p'
fi

echo
echo "Find expo versions and audio packages:"
rg -n '"expo"|"react-native"|"expo-audio"|"expo-av"|"babel-preset-expo"|SDK 56|SDK 54' -S --glob '!node_modules' --glob '!dist' --glob '!build' .

Repository: Resgrid/Dispatch

Length of output: 433


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Audio-refactoring context:"
awk '{ printf "%6d	%s\n", NR, $0 }' docs/audio-stream-refactoring.md | sed -n '1,220p'

echo
echo "Package/Expo config snippets:"
for f in app.config.ts babel.config.js package.json; do
  echo "--- $f ---"
  awk '{ printf "%6d	%s\n", NR, $0 }' "$f" | sed -n '1,220p'
done

echo
echo "Search relevant strings:"
rg -n '"expo"|"react-native"|"expo-audio"|"expo-av"|"babel-preset-expo"|SDK 56|SDK 54' -S --glob '!node_modules' --glob '!dist' --glob '!build' .

Repository: Resgrid/Dispatch

Length of output: 27844


🌐 Web query:

Expo SDK 54 expo-audio 1.1 installable expo-audio 16 expo-audio SDK 56 compatibility expo doc

💡 Result:

The expo-audio library is fully compatible with and designed for modern Expo SDKs, including SDK 54 and SDK 56. 1. Expo SDK 54: expo-audio 1.1 is compatible and was the standard for audio in SDK 54 [1][2]. During the SDK 54 lifecycle, users were encouraged to migrate to expo-audio from the deprecated expo-av [3][4]. You can install the version appropriate for your SDK using the command: npx expo install expo-audio [5]. 2. Expo SDK 56: expo-audio version 56.x is the version associated with SDK 56 [6][7]. The library received significant updates in SDK 56, including a new useAudioStream hook and various live-streaming improvements [8][9]. Important Note on Versions: In the Expo ecosystem, you should not manually choose a version number like 16 for expo-audio unless it specifically aligns with your SDK. Instead, always use the command npx expo install expo-audio [5]. This command ensures that the Expo CLI automatically installs the specific version of the package that is tested and compatible with your current Expo SDK version [5]. Using version 16 with SDK 56 is likely incorrect, as the package versions for expo-audio follow the SDK release numbering (e.g., version 56.x for SDK 56) [6][7]. Always verify your dependencies using npx expo-doctor after installation [5].

Citations:


🌐 Web query:

docs.expo.dev "expo-audio" "Expo SDK 56"

💡 Result:

In Expo SDK 56, the expo-audio library is officially supported and includes several key updates [1][2]. Key features and improvements for expo-audio in SDK 56 include: - New useAudioStream Hook: This hook provides real-time access to microphone buffer data, enabling new possibilities for audio processing [1][3]. - Live-Stream Enhancements: - An isLiveStream option is now available for the lock-screen on iOS [1][2]. - A playsInSilentMode option has been added for Android [1][3]. - The AudioStatus object has been updated to include new fields: isLive, currentOffsetFromLive, and error [1][2]. You can access the official documentation for this version at https://docs.expo.dev/versions/v56.0.0/sdk/audio/ [1][2].

Citations:


🌐 Web query:

Expo docs "Before upgrading" "During the upgrade" package compatibility SDK 56 expo install

💡 Result:

To upgrade your project to Expo SDK 56, follow these recommended practices regarding preparation, package compatibility, and the installation process [1][2][3]. Preparation and Best Practices Before starting the upgrade, it is strongly recommended that you read the official SDK 56 changelog and breaking changes documentation [1][4]. This allows you to identify necessary configuration adjustments specific to your application before beginning the actual upgrade process [1]. Additionally, use a dedicated git branch for the upgrade to isolate changes [2][3]. If you are still using Expo Go for development, it is recommended to migrate to development builds first, as this provides a more stable baseline for testing and debugging during and after the upgrade [5]. It is also advised to upgrade one major change at a time (e.g., upgrading the SDK version first, then enabling the New Architecture separately) to make it easier to isolate and troubleshoot any issues [5][4][6]. Package Compatibility and Installation During the upgrade, you should prioritize ensuring that your dependencies are compatible with the new SDK version [2][3]. The primary command for updating your Expo-related packages is [2][3]: npx expo install expo@^56.0.0 After updating the core expo package, run the package-aware installer to automatically align your remaining dependencies with the requirements of SDK 56 [2][3]: npx expo install --fix Post-Upgrade Verification Once the installation is complete, it is crucial to verify your project's health. The recommended next step is to run Expo Doctor, which is designed to detect mismatched versions, incompatible dependencies, and configuration issues [2][3]: npx expo-doctor If your project utilizes Expo Router, be aware that SDK 56 includes internal migration work; if you encounter issues, you may need to use the provided codemod to handle transitions between file-based routing and React Navigation imports [2][3]: npx expo-codemod sdk-56-expo-router-react-navigation-replace src Finally, rebuild your development builds and test thoroughly before deploying to production [2][3].

Citations:


Sequence the SDK 56 package update with the SDK 56 upgrade.

Expo’s SDK upgrade process updates Expo packages with the SDK and uses npx expo install --fix to align dependencies. Keep this guidance focused on the SDK upgrade, or change “Before upgrading” to “During the SDK 56 upgrade” and add the tested expo-audio package version.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/audio-stream-refactoring.md` around lines 3 - 5, Update the “Expo SDK 56
migration requirement” guidance to sequence the expo-audio dependency update
during the SDK 56 upgrade rather than before it, and describe alignment through
the SDK upgrade process. If specifying a package version, use the tested SDK
56-compatible expo-audio version; otherwise remove the unsupported
prerequisite-version wording while retaining the expo-av replacement
requirement.

Source: MCP tools

Comment on lines +9 to +10
- Migrate `src/hooks/use-ptt.ts`, `src/components/calls/call-audio-modal.tsx`, `src/stores/app/audio-stream-store.ts`, and `src/services/audio.service.ts` to `createAudioPlayer`, `setAudioModeAsync`, `AudioPlayer`, and `playbackStatusUpdate`.
- Remove `expo-av` from `package.json`, the lockfile, tests/mocks, and the Expo Doctor exclusion after no imports remain.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== file excerpt =="
if [ -f docs/audio-stream-refactoring.md ]; then
  wc -l docs/audio-stream-refactoring.md
  sed -n '1,220p' docs/audio-stream-refactoring.md
else
  echo "docs/audio-stream-refactoring.md not found"
fi

echo
echo "== expo-av mentions in docs/audio-stream-refactoring.md =="
rg -n "expo-av|expo-audio|audio-av|createAudioPlayer|setAudioModeAsync|AudioPlayer|playbackStatusUpdate|Migrate|Remove" docs/audio-stream-refactoring.md || true

Repository: Resgrid/Dispatch

Length of output: 7247


🌐 Web query:

Expo SDK 56 expo-av expo-audio expo-audio SDK 56 createAudioPlayer AudioPlayer

💡 Result:

In Expo SDK 56, the expo-audio library is the standard tool for audio playback and recording, having replaced the deprecated expo-av package [1][2]. To work with audio, you primarily use the useAudioPlayer hook or, in advanced scenarios where the player must persist beyond a component's lifecycle, the createAudioPlayer function [3][4]. 1. useAudioPlayer(source, options): This is the recommended approach for most use cases [3]. It is a React hook that automatically manages the lifecycle of the AudioPlayer instance, ensuring that resources are properly disposed of when the component unmounts [5][6]. 2. createAudioPlayer(source, options): This function allows you to create an AudioPlayer instance manually [3][5]. Because this instance is not automatically managed by a component's lifecycle, it is your responsibility to call the release method on the player when it is no longer needed to prevent memory leaks [3][4]. Key differences when migrating from expo-av include: - Time is measured in seconds rather than milliseconds [7]. - Audio loads immediately upon creation [7]. - There is no automatic reset to the beginning when playback finishes; if you wish to replay, you must manually call player.seekTo(0) before calling play [7][8]. For additional functionality, expo-audio also provides the useAudioRecorder hook for recording [7][3].

Citations:


Align expo-av references with the migration goal.

Lines 9-10 remove expo-av, but later sections describe expo-av, the Overview, the Key Changes, and the Installation steps still instruct using expo-av. Rewrite those sections to describe the expo-audio SDK 56 implementation, or label them as an SDK 54 baseline only.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/audio-stream-refactoring.md` around lines 9 - 10, Update the later
Overview, Key Changes, and Installation sections in the audio-stream refactoring
document to consistently describe the expo-audio SDK 56 migration using
createAudioPlayer, setAudioModeAsync, AudioPlayer, and playbackStatusUpdate;
remove or explicitly label any remaining expo-av guidance as an SDK 54 baseline.

Source: MCP tools

Comment on lines +55 to +58
{
name: 'feature-flags-storage',
storage: createJSONStorage(() => zustandStorage),
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate persistence reset or rehydration calls for this store.
rg -n -C 3 'feature-flags-storage|featureFlagsStore\.(persist\.)?(clearStorage|rehydrate)|featureFlagsStore\.setState' src

# Inspect authentication and department-transition paths for a feature-flag reset.
rg -n -C 3 '\b(signOut|logout|signedOut|DepartmentCode)\b' src

Repository: Resgrid/Dispatch

Length of output: 24927


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== feature-flags store outline =="
ast-grep outline src/stores/feature-flags/store.ts --view expanded || true

echo "== feature-flags store contents =="
cat -n src/stores/feature-flags/store.ts

echo "== feature-flags imports/usages =="
rg -n -C 4 'featureFlagsStore|feature.*flags|Flags|Chat\.System' src --glob '!src/translations/**'

echo "== auth store relevant logout path =="
sed -n '120,175p' src/stores/auth/store.tsx | cat -n

echo "== any clearStorage/rehydrate / storage reset =="
rg -n 'clearStorage|rehydrate\(\)|setPersist|persist\.clearStorage|name:\s*feature-flags-storage' src --glob '!src/translations/**'

Repository: Resgrid/Dispatch

Length of output: 21012


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== app/_layout.tsx relevant initialization and gating =="
sed -n '330,410p' src/app/(' '(app))/_layout.tsx | cat -n
sed -n '520,555p' src/app/(' '(app))/_layout.tsx | cat -n

echo "== security/store identity change helpers and DepartmentCode state =="
cat -n src/stores/security/store.ts

echo "== all DepartmentCode identifiers =="
rg -n -C 3 'DepartmentCode|departmentCode|setDepartment|Department' src/stores src/services src/app src/components \
  --glob '!src/translations/**' --glob '!src/__tests__/**' \
  | head -n 220

echo "== imports/features around auth/store and security =="
rg -n -C 3 'useSecurityStore|securityStore|useAuthStore|useAuth|logout\(|signOut' src/app src/hooks src/stores src/components \
  --glob '!src/translations/**' --glob '!src/__tests__/**' \
  | head -n 260

echo "== targeted reset calls =="
rg -n 'clearStorage|featureFlagsStore\.(persist\.)?(clearStorage|rehydrate)|featureFlagsStore\.setState\(|logout\(\)\s*$|signOut\(\)\s*$' src --glob '!src/translations/**'

Repository: Resgrid/Dispatch

Length of output: 264


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== app/_layout.tsx relevant initialization and gating =="
sed -n '330,410p' 'src/app/(app)/_layout.tsx' | cat -n
sed -n '520,555p' 'src/app/(app)/_layout.tsx' | cat -n

echo "== security/store identity change helpers and DepartmentCode state =="
cat -n src/stores/security/store.ts

echo "== all DepartmentCode identifiers =="
rg -n -C 3 'DepartmentCode|departmentCode|setDepartment|Department' src/stores src/services src/app src/components \
  --glob '!src/translations/**' --glob '!src/__tests__/**' \
  | head -n 220

echo "== imports/features around auth/store and security =="
rg -n -C 3 'useSecurityStore|securityStore|useAuthStore|useAuth|logout\(|signOut' src/app src/hooks src/stores src/components \
  --glob '!src/translations/**' --glob '!src/__tests__/**' \
  | head -n 260

echo "== targeted reset calls =="
rg -n 'clearStorage|featureFlagsStore\.(persist\.)?(clearStorage|rehydrate)|featureFlagsStore\.setState\(|logout\(\)\s*$|signOut\(\)\s*$' src --glob '!src/translations/**'

Repository: Resgrid/Dispatch

Length of output: 44059


Reset or scope feature-flag persistence on sign-out and department changes.

feature-flags-storage is device-wide, while getAllFeatureFlags() evaluates flags for the current department. src/app/(app)/_layout.tsx fetches flags after rights, but saveFlags preserves the previous flags on fetch failure and src/stores/auth/store.tsx logout does not clear them. A prior department can leave Chat.System: true for the next department. Use a DepartmentCode-scoped storage key or clear this store before identity changes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/stores/feature-flags/store.ts` around lines 55 - 58, Scope feature-flag
persistence to the current DepartmentCode, or clear the feature-flags store
before logout and department changes. Update the feature-flags store around the
feature-flags-storage configuration and ensure stale flags cannot survive
identity changes or fetch failures for a new department.

Comment thread src/stores/feature-flags/store.ts

/** Evaluates every active flag for the caller's department. */
export const getAllFeatureFlags = async (signal?: AbortSignal) => {
const response = await api.get<FeatureTogglesResult>(`${FEATURE_TOGGLES}/GetAll`, { signal });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules high

Unguarded external HTTP call via api.get violates Rule [27] by lacking contextual error mapping. Wrap the call in a try/catch block, attach the operation name and endpoint, and map errors to a feature-flags domain error or safe default.

Kody rule violation: Add try-catch blocks for external calls

Prompt for LLM

File src/api/feature-flags/feature-flags.ts:

Line 29:

Unguarded external HTTP call via `api.get` violates Rule [27] by lacking contextual error mapping. Wrap the call in a try/catch block, attach the operation name and endpoint, and map errors to a feature-flags domain error or safe default.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

Comment thread src/app/(app)/_layout.tsx
// Connect the realtime chat hub (best-effort; chat may be disabled per department)
try {
await useSignalRStore.getState().connectChatHub();
// Connect the realtime chat hub only when the Chat.System feature flag is on for

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Bug high

Feature flag bypass occurs in useSignalRLifecycle, which unconditionally reconnects the chat hub on app resume via signalRStore.connectChatHub() at use-signalr-lifecycle.ts:121, defeating the Chat.System gate at _layout.tsx:182. Thread the feature flag into the resume reconnect path in handleAppResume to skip connectChatHub() when featureFlagsStore.getState().isEnabled(FeatureFlagKeys.ChatSystem) is false.

// Gate must also be enforced in use-signalr-lifecycle.ts handleAppResume:
// const hubs = [signalRStore.connectUpdateHub(), signalRStore.connectGeolocationHub()];
// if (featureFlagsStore.getState().isEnabled(FeatureFlagKeys.ChatSystem)) {
//   hubs.push(signalRStore.connectChatHub());
// }
// const results = await Promise.allSettled(hubs);
Prompt for LLM

File src/app/(app)/_layout.tsx:

Line 180:

Feature flag bypass occurs in `useSignalRLifecycle`, which unconditionally reconnects the chat hub on app resume via `signalRStore.connectChatHub()` at `use-signalr-lifecycle.ts:121`, defeating the `Chat.System` gate at `_layout.tsx:182`. Thread the feature flag into the resume reconnect path in `handleAppResume` to skip `connectChatHub()` when `featureFlagsStore.getState().isEnabled(FeatureFlagKeys.ChatSystem)` is false.

Suggested Code:

// Gate must also be enforced in use-signalr-lifecycle.ts handleAppResume:
// const hubs = [signalRStore.connectUpdateHub(), signalRStore.connectGeolocationHub()];
// if (featureFlagsStore.getState().isEnabled(FeatureFlagKeys.ChatSystem)) {
//   hubs.push(signalRStore.connectChatHub());
// }
// const results = await Promise.allSettled(hubs);

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

Comment thread src/app/(app)/_layout.tsx
context: { platform: Platform.OS },
});

await featureFlagsStore.getState().fetchFlags();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules high

Unguarded awaited fetchFlags() call risks unhandled rejections that fail app initialization. Wrap the call in a try/catch block and log the error context to comply with Rule [1].

Kody rule violation: Handle async operations with proper error handling

Prompt for LLM

File src/app/(app)/_layout.tsx:

Line 158:

Unguarded awaited `fetchFlags()` call risks unhandled rejections that fail app initialization. Wrap the call in a try/catch block and log the error context to comply with Rule [1].

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

Comment thread src/app/(app)/chat.tsx

// Chat.System feature flag off: no chat for this department.
if (!isChatEnabled) {
return <Redirect href={'/home' as Href} />;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules low

Magic string '/home' for a finite application route reduces maintainability and risks typos. Define route names as a Route enum or const tuple instead of using raw strings.

Kody rule violation: Use enums instead of magic strings

Prompt for LLM

File src/app/(app)/chat.tsx:

Line 111:

Magic string '/home' for a finite application route reduces maintainability and risks typos. Define route names as a Route enum or const tuple instead of using raw strings.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

const menuItems = getMenuItems(t);
const isChatEnabled = useIsChatEnabled();
// Chat and the assistant are gated by the Chat.System feature flag.
const menuItems = getMenuItems(t).filter((item) => (item.id === 'chat' || item.id === 'assistant' ? isChatEnabled : true));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules low

Duplicated inline string literals 'chat' and 'assistant' as menu-item identifiers risk drift across getMenuItems and other components. Define these shared keys as centralized constants, such as MenuItemIds.Chat and MenuItemIds.Assistant, to ensure consistency.

Kody rule violation: Centralize string constants

Prompt for LLM

File src/components/sidebar/side-menu.tsx:

Line 100:

Duplicated inline string literals 'chat' and 'assistant' as menu-item identifiers risk drift across `getMenuItems` and other components. Define these shared keys as centralized constants, such as `MenuItemIds.Chat` and `MenuItemIds.Assistant`, to ensure consistency.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

flags: {},
isLoaded: false,
error: null,
fetchFlags: async () => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules low

Missing JSDoc on the async fetchFlags method hides its failure semantics, specifically that it swallows errors and sets the error state rather than rejecting. Add a JSDoc block to document the resolve value, rejection conditions, and await usage to satisfy Rule [22].

Kody rule violation: Document async/Promise behavior and errors

Prompt for LLM

File src/stores/feature-flags/store.ts:

Line 34:

Missing JSDoc on the async `fetchFlags` method hides its failure semantics, specifically that it swallows errors and sets the `error` state rather than rejecting. Add a JSDoc block to document the resolve value, rejection conditions, and await usage to satisfy Rule [22].

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

@Resgrid-Bot

This comment has been minimized.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/stores/feature-flags/store.ts (1)

66-91: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Bind feature-flag responses to the active identity.

A request can complete after its user or department changes. The store can then persist flags for a previous identity, including unscoped flags with identityKey: null.

  • src/stores/feature-flags/store.ts#L66-L91: commit results only when the captured non-null identity still matches the active identity and the request is the latest fetch.
  • src/stores/feature-flags/store.ts#L23-L47: treat an absent identity as unresolved for new API results. Do not persist unscoped flag data.
  • src/stores/feature-flags/__tests__/store.test.ts#L66-L173: add deferred-promise tests for identity changes during successful and failed requests.

As per coding guidelines, generate tests for all generated logic.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/stores/feature-flags/store.ts` around lines 66 - 91, Update
src/stores/feature-flags/store.ts:66-91 in fetchFlags to commit success or
failure results only when the captured non-null identity still matches the
active identity and the request remains the latest fetch; otherwise ignore the
response. Update src/stores/feature-flags/store.ts:23-47 so absent identities
are treated as unresolved for new API results and unscoped flags are never
persisted. Add deferred-promise tests in
src/stores/feature-flags/__tests__/store.test.ts:66-173 covering identity
changes during both successful and failed requests.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/stores/feature-flags/__tests__/store.test.ts`:
- Around line 66-173: Add a fetch race test in the fetchFlags suite using a
deferred getAllFeatureFlags promise: start fetchFlags under one identity, change
identity before resolving the promise, then resolve with flags and await the
original fetch. Verify the stale response cannot update flags or identityKey,
and cover the generated identity-transition logic with the relevant assertions.

In `@src/stores/signalr/signalr-store.ts`:
- Around line 625-631: Update the disabled-feature guard in connectChatHub to
call disconnectChatHub before returning when ChatSystem is disabled. Preserve
the existing log and ensure the disconnect occurs for already-connected hubs as
well as inactive ones.

---

Outside diff comments:
In `@src/stores/feature-flags/store.ts`:
- Around line 66-91: Update src/stores/feature-flags/store.ts:66-91 in
fetchFlags to commit success or failure results only when the captured non-null
identity still matches the active identity and the request remains the latest
fetch; otherwise ignore the response. Update
src/stores/feature-flags/store.ts:23-47 so absent identities are treated as
unresolved for new API results and unscoped flags are never persisted. Add
deferred-promise tests in
src/stores/feature-flags/__tests__/store.test.ts:66-173 covering identity
changes during both successful and failed requests.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 196a1d42-0c74-4a3e-a0ef-70733f9c3a15

📥 Commits

Reviewing files that changed from the base of the PR and between bb6b160 and d52002f.

📒 Files selected for processing (7)
  • src/app/(app)/chat.tsx
  • src/app/(app)/chatbot.tsx
  • src/app/chat/[channelId].tsx
  • src/app/chat/thread/[messageId].tsx
  • src/stores/feature-flags/__tests__/store.test.ts
  • src/stores/feature-flags/store.ts
  • src/stores/signalr/signalr-store.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/app/chat/[channelId].tsx

Comment on lines +66 to +173
describe('fetchFlags', () => {
it('should store flags and stamp the current identity on success', async () => {
getAllFeatureFlags.mockResolvedValue({
Data: [{ Key: FeatureFlagKeys.ChatSystem, Enabled: true, Value: null }],
});

await featureFlagsStore.getState().fetchFlags();

const state = featureFlagsStore.getState();
expect(state.flags[FeatureFlagKeys.ChatSystem]).toEqual({ enabled: true, value: null });
expect(state.isLoaded).toBe(true);
expect(state.error).toBeNull();
expect(state.identityKey).toBe('user-1:dept-1');
});

it('should keep persisted flags on failure for the same identity', async () => {
featureFlagsStore.setState({
flags: { [FeatureFlagKeys.ChatSystem]: { enabled: true, value: null } },
isLoaded: true,
identityKey: 'user-1:dept-1',
});
getAllFeatureFlags.mockRejectedValue(new Error('network down'));

await featureFlagsStore.getState().fetchFlags();

const state = featureFlagsStore.getState();
expect(state.flags[FeatureFlagKeys.ChatSystem]?.enabled).toBe(true);
expect(state.identityKey).toBe('user-1:dept-1');
expect(state.error).toBe('network down');
});

it('should clear flags from a different department before fetching so a failed fetch cannot reuse them', async () => {
featureFlagsStore.setState({
flags: { [FeatureFlagKeys.ChatSystem]: { enabled: true, value: null } },
isLoaded: true,
identityKey: 'user-1:dept-old',
});
getAllFeatureFlags.mockRejectedValue(new Error('network down'));

await featureFlagsStore.getState().fetchFlags();

const state = featureFlagsStore.getState();
expect(state.flags).toEqual({});
// Fail-closed: the failed fetch still resolves the flags so consumers stop waiting.
expect(state.isLoaded).toBe(true);
expect(state.identityKey).toBeNull();
});

it('should clear flags from a different account before fetching', async () => {
featureFlagsStore.setState({
flags: { [FeatureFlagKeys.ChatSystem]: { enabled: true, value: null } },
isLoaded: true,
identityKey: 'user-other:dept-1',
});
getAllFeatureFlags.mockRejectedValue(new Error('network down'));

await featureFlagsStore.getState().fetchFlags();

expect(featureFlagsStore.getState().flags).toEqual({});
});

it('should replace another identity flags with fresh ones on success', async () => {
featureFlagsStore.setState({
flags: { [FeatureFlagKeys.ChatSystem]: { enabled: true, value: null } },
isLoaded: true,
identityKey: 'user-other:dept-other',
});
getAllFeatureFlags.mockResolvedValue({
Data: [{ Key: FeatureFlagKeys.ChatSystem, Enabled: false, Value: null }],
});

await featureFlagsStore.getState().fetchFlags();

const state = featureFlagsStore.getState();
expect(state.flags[FeatureFlagKeys.ChatSystem]?.enabled).toBe(false);
expect(state.identityKey).toBe('user-1:dept-1');
});

it('should keep flags on failure when department is unknown but the user matches', async () => {
setIdentity('user-1', null);
featureFlagsStore.setState({
flags: { [FeatureFlagKeys.ChatSystem]: { enabled: true, value: null } },
isLoaded: true,
identityKey: 'user-1:dept-1',
});
getAllFeatureFlags.mockRejectedValue(new Error('network down'));

await featureFlagsStore.getState().fetchFlags();

const state = featureFlagsStore.getState();
expect(state.flags[FeatureFlagKeys.ChatSystem]?.enabled).toBe(true);
expect(state.identityKey).toBe('user-1:dept-1');
});

it('should clear flags when department is unknown and the user differs', async () => {
setIdentity('user-2', null);
featureFlagsStore.setState({
flags: { [FeatureFlagKeys.ChatSystem]: { enabled: true, value: null } },
isLoaded: true,
identityKey: 'user-1:dept-1',
});
getAllFeatureFlags.mockRejectedValue(new Error('network down'));

await featureFlagsStore.getState().fetchFlags();

expect(featureFlagsStore.getState().flags).toEqual({});
});
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add an identity-transition test for pending fetches.

The tests change identity only before fetchFlags() starts. Add deferred API promises, change the identity before resolution, and verify that the earlier response cannot set flags or identityKey.

As per coding guidelines, generate tests for all generated logic.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/stores/feature-flags/__tests__/store.test.ts` around lines 66 - 173, Add
a fetch race test in the fetchFlags suite using a deferred getAllFeatureFlags
promise: start fetchFlags under one identity, change identity before resolving
the promise, then resolve with flags and await the original fetch. Verify the
stale response cannot update flags or identityKey, and cover the generated
identity-transition logic with the relevant assertions.

Source: Coding guidelines

Comment thread src/stores/signalr/signalr-store.ts
const currentUserId = useAuthStore((s) => s.userId);
const isModerator = !!securityStore((s) => s.rights)?.IsAdmin;
const chatStatus = useChatSystemStatus();
const isChatEnabled = chatStatus === 'enabled';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules low

Magic string comparison introduces silent failures if the store value changes or a typo occurs. Define a const tuple like const ChatSystemStatus = { Enabled: 'enabled', Unknown: 'unknown', Disabled: 'disabled' } as const in the feature-flags store and reference ChatSystemStatus.Enabled to enforce compile-time safety.

Kody rule violation: Use enums instead of magic strings

Prompt for LLM

File src/app/chat/[channelId].tsx:

Line 43:

Magic string comparison introduces silent failures if the store value changes or a typo occurs. Define a const tuple like `const ChatSystemStatus = { Enabled: 'enabled', Unknown: 'unknown', Disabled: 'disabled' } as const` in the feature-flags store and reference `ChatSystemStatus.Enabled` to enforce compile-time safety.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.


// Chat.System feature flag off: block deep links (push notifications, stale routes).
if (chatStatus === 'disabled') {
return <Redirect href={'/home' as Href} />;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules low

Hardcoded route path '/home' scatters definitions, causing renames or typos to evade compile-time checks. Reference a centralized routes constant, such as ROUTES.HOME or AppRoutes.Home, defined in a single navigation module.

Kody rule violation: Centralize string constants

Prompt for LLM

File src/app/chat/[channelId].tsx:

Line 254:

Hardcoded route path '/home' scatters definitions, causing renames or typos to evade compile-time checks. Reference a centralized routes constant, such as `ROUTES.HOME` or `AppRoutes.Home`, defined in a single navigation module.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.


export const useIsChatEnabled = () => useFeatureFlag(FeatureFlagKeys.ChatSystem);

export type FeatureFlagStatus = 'unknown' | 'enabled' | 'disabled';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules low

Duplicate source of truth: The type FeatureFlagStatus manually duplicates the runtime string literals ('unknown','enabled','disabled') returned on lines 118 and 120. Declare const FEATURE_FLAG_STATUSES = ['unknown','enabled','disabled'] as const; and derive type FeatureFlagStatus = typeof FEATURE_FLAG_STATUSES[number]; to synchronize definitions.

Kody rule violation: Derive TypeScript types from validation schemas

Prompt for LLM

File src/stores/feature-flags/store.ts:

Line 109:

Duplicate source of truth: The type `FeatureFlagStatus` manually duplicates the runtime string literals ('unknown','enabled','disabled') returned on lines 118 and 120. Declare `const FEATURE_FLAG_STATUSES = ['unknown','enabled','disabled'] as const;` and derive `type FeatureFlagStatus = typeof FEATURE_FLAG_STATUSES[number];` to synchronize definitions.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

@Resgrid-Bot

This comment has been minimized.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/app/chat/[channelId].tsx (1)

70-79: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Gate every chat side effect with isChatEnabled.

This effect is gated, but the presence request at Lines 83-95 and the read update at Lines 97-102 still run from cached state while chat is unknown or disabled. Add isChatEnabled guards and dependencies to both effects so a disabled department does not send chat requests.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/chat/`[channelId].tsx around lines 70 - 79, Gate the presence-request
effect and read-update effect alongside the existing channel effect by returning
early unless isChatEnabled is true. Add isChatEnabled to both effects’
dependency arrays, while preserving their existing behavior when chat is
enabled.
src/app/chat/thread/[messageId].tsx (2)

36-41: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Show a user-visible error when thread loading fails.

The catch block only logs the failure. A network or offline error leaves the same empty state as a thread with no replies, without a retry or status message. Add translated error feedback and a retry action, or expose the cached/offline state.

As per coding guidelines, “Handle errors gracefully and provide user feedback” and “Implement proper offline support.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/chat/thread/`[messageId].tsx around lines 36 - 41, Update the
thread-loading flow in the useEffect around getThread so failures set
user-visible translated error state instead of only logging, while retaining the
messageId context in logger.error. Render that state with a retry action that
re-invokes thread loading, or clearly expose the cached/offline state, and
preserve the existing empty-replies behavior for successful responses.

Source: Coding guidelines


36-41: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Keep fetched replies scoped to the active messageId.

When this screen instance receives a new messageId, fetchedReplies keeps the previous thread's data. The request also writes its result without checking that it is still current. Replies from one thread can therefore appear in another thread, and a failed request can leave the wrong replies visible.

Clear the state when messageId changes and ignore stale responses.

Proposed fix
  useEffect(() => {
-    if (!messageId || !isChatEnabled) return;
+    if (!messageId || !isChatEnabled) {
+      setFetchedReplies([]);
+      return;
+    }
+    let cancelled = false;
+    setFetchedReplies([]);
+
     getThread(messageId, undefined, 50)
-      .then((response) => setFetchedReplies(response.Data ?? []))
+      .then((response) => {
+        if (!cancelled) setFetchedReplies(response.Data ?? []);
+      })
       .catch((error) => logger.error({ message: 'chat: failed to load thread', context: { error, messageId } }));
+
+    return () => {
+      cancelled = true;
+    };
  }, [messageId, isChatEnabled]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/chat/thread/`[messageId].tsx around lines 36 - 41, Update the
useEffect tied to messageId and isChatEnabled to clear fetchedReplies when the
active messageId changes, and ignore both successful and failed results from
requests started for an earlier messageId. Ensure only the current thread’s
response can update state or leave existing replies visible.
🧹 Nitpick comments (2)
src/components/chat/message-actions-sheet.tsx (1)

31-31: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use React.FC for these prop-bearing components.

  • src/components/chat/message-actions-sheet.tsx#L31-L31: declare MessageActionsSheet as React.FC<MessageActionsSheetProps>.
  • src/components/chat/message-composer.tsx#L32-L32: declare MessageComposer as React.FC<MessageComposerProps>.

As per coding guidelines, “Utilize React.FC for defining functional components with props.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/chat/message-actions-sheet.tsx` at line 31, Update
MessageActionsSheet in src/components/chat/message-actions-sheet.tsx:31 to use
React.FC<MessageActionsSheetProps>, and update MessageComposer in
src/components/chat/message-composer.tsx:32 to use
React.FC<MessageComposerProps>, preserving their existing props and behavior.

Source: Coding guidelines

src/app/(app)/chatbot.tsx (1)

76-76: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Use a stable no-op reaction callback.

renderItem creates a new onToggleReaction function for every item render. Define a typed no-op callback outside the component, then pass that callback.

As per coding guidelines, “Avoid anonymous functions in renderItem or event handlers to prevent re-renders.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/`(app)/chatbot.tsx at line 76, Define a typed, stable no-op reaction
callback outside the component, then replace the inline onToggleReaction
function in renderItem’s MessageBubble with that shared callback. Preserve the
existing no-op behavior while avoiding a new function allocation on each item
render.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/app/`(app)/chatbot.tsx:
- Around line 188-193: Update the edit-message save handler around
setEditMessage and the editMessage call so an empty editText.trim() cannot
silently close the sheet: disable the Save action while trimmed text is empty or
display a validation message and keep the editor open. Preserve saving only when
editMessage, chatbotChannelId, and non-empty trimmed text are present.

In `@src/app/chat/`[channelId].tsx:
- Around line 257-260: Resolve or fetch the channel metadata before the screen’s
initialization and rendering flow, then apply the ChatChannelType.Chatbot
redirect once channel is known. Update the channel-loading logic around the
existing channel check so cold and stale deep links cannot render the generic
conversation before redirecting to the dedicated chatbot route.

---

Outside diff comments:
In `@src/app/chat/`[channelId].tsx:
- Around line 70-79: Gate the presence-request effect and read-update effect
alongside the existing channel effect by returning early unless isChatEnabled is
true. Add isChatEnabled to both effects’ dependency arrays, while preserving
their existing behavior when chat is enabled.

In `@src/app/chat/thread/`[messageId].tsx:
- Around line 36-41: Update the thread-loading flow in the useEffect around
getThread so failures set user-visible translated error state instead of only
logging, while retaining the messageId context in logger.error. Render that
state with a retry action that re-invokes thread loading, or clearly expose the
cached/offline state, and preserve the existing empty-replies behavior for
successful responses.
- Around line 36-41: Update the useEffect tied to messageId and isChatEnabled to
clear fetchedReplies when the active messageId changes, and ignore both
successful and failed results from requests started for an earlier messageId.
Ensure only the current thread’s response can update state or leave existing
replies visible.

---

Nitpick comments:
In `@src/app/`(app)/chatbot.tsx:
- Line 76: Define a typed, stable no-op reaction callback outside the component,
then replace the inline onToggleReaction function in renderItem’s MessageBubble
with that shared callback. Preserve the existing no-op behavior while avoiding a
new function allocation on each item render.

In `@src/components/chat/message-actions-sheet.tsx`:
- Line 31: Update MessageActionsSheet in
src/components/chat/message-actions-sheet.tsx:31 to use
React.FC<MessageActionsSheetProps>, and update MessageComposer in
src/components/chat/message-composer.tsx:32 to use
React.FC<MessageComposerProps>, preserving their existing props and behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 19ce0c0d-2d80-47da-9432-aca46f62062e

📥 Commits

Reviewing files that changed from the base of the PR and between d52002f and 2dba29a.

📒 Files selected for processing (10)
  • src/api/chat/chatbot.ts
  • src/app/(app)/chat.tsx
  • src/app/(app)/chatbot.tsx
  • src/app/chat/[channelId].tsx
  • src/app/chat/thread/[messageId].tsx
  • src/components/chat/message-actions-sheet.tsx
  • src/components/chat/message-composer.tsx
  • src/models/v4/chat/chatbotModels.ts
  • src/stores/chat/store.ts
  • src/stores/signalr/signalr-store.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/stores/signalr/signalr-store.ts

Comment thread src/app/(app)/chatbot.tsx
Comment on lines +188 to +193
onPress={() => {
if (editMessage && chatbotChannelId && editText.trim()) {
void useChatStore.getState().editMessage(editMessage.ChatMessageId, chatbotChannelId, editText.trim());
}
setEditMessage(null);
}}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Do not silently discard an empty edit.

If editText.trim() is empty, the handler closes the sheet without saving or user feedback. Disable Save until text is non-empty, or show a validation message.

As per coding guidelines, “Handle errors gracefully and provide user feedback.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/`(app)/chatbot.tsx around lines 188 - 193, Update the edit-message
save handler around setEditMessage and the editMessage call so an empty
editText.trim() cannot silently close the sheet: disable the Save action while
trimmed text is empty or display a validation message and keep the editor open.
Preserve saving only when editMessage, chatbotChannelId, and non-empty trimmed
text are present.

Source: Coding guidelines

Comment thread src/app/chat/[channelId].tsx
Comment thread src/app/(app)/chatbot.tsx
const renderItem = useCallback(
({ item }: { item: ChatMessageResultData }) => (
<MessageBubble message={item} isOwn={!!item.SenderUserId && item.SenderUserId === currentUserId} showSender={false} currentUserId={currentUserId} onLongPress={() => undefined} onToggleReaction={() => undefined} />
<MessageBubble message={item} isOwn={!!item.SenderUserId && item.SenderUserId === currentUserId} showSender={false} currentUserId={currentUserId} onLongPress={setActionsMessage} onToggleReaction={() => undefined} />

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules high

Unnecessary re-renders occur in src/app/(app)/chatbot.tsx (lines 154, 158-161, 164, 168-171, 175, 188), src/app/chat/thread/[messageId].tsx:134, src/components/chat/message-actions-sheet.tsx:99, and src/components/chat/message-composer.tsx:138 because inline arrow functions and .bind() calls in JSX props create new function instances on every render. Move these function definitions outside the render method.

Kody rule violation: Avoid using .bind() or arrow functions in JSX props

Prompt for LLM

File src/app/(app)/chatbot.tsx:

Line 76:

Unnecessary re-renders occur in `src/app/(app)/chatbot.tsx` (lines 154, 158-161, 164, 168-171, 175, 188), `src/app/chat/thread/[messageId].tsx:134`, `src/components/chat/message-actions-sheet.tsx:99`, and `src/components/chat/message-composer.tsx:138` because inline arrow functions and `.bind()` calls in JSX props create new function instances on every render. Move these function definitions outside the render method.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

Comment thread src/app/(app)/chatbot.tsx
className="bg-primary-600"
onPress={() => {
if (editMessage && chatbotChannelId && editText.trim()) {
void useChatStore.getState().editMessage(editMessage.ChatMessageId, chatbotChannelId, editText.trim());

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules high

Unhandled promise rejection risk exists in src/app/(app)/chatbot.tsx (lines 154, 161) because the promise returned by editMessage is discarded with the void operator, silently swallowing errors. Wrap the call in a try/catch block or chain a .catch() handler to display an error toast on failure.

Kody rule violation: Handle async operations with proper error handling

Prompt for LLM

File src/app/(app)/chatbot.tsx:

Line 190:

Unhandled promise rejection risk exists in `src/app/(app)/chatbot.tsx` (lines 154, 161) because the promise returned by `editMessage` is discarded with the `void` operator, silently swallowing errors. Wrap the call in a try/catch block or chain a `.catch()` handler to display an error toast on failure.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

</Pressable>
{allowUrgent ? (
<Pressable className="p-2" onPress={() => setUrgent((prev) => !prev)} disabled={disabled} accessibilityLabel={t('chat.urgent')}>
<AlertTriangle size={22} color={urgent ? '#dc2626' : '#6b7280'} />

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules low

Hardcoded hex color literals '#dc2626' and '#6b7280' in src/components/chat/message-composer.tsx duplicate values and make palette or theme changes error-prone. Extract these repeated strings into named constants like COLOR_ERROR and COLOR_MUTED within a centralized theme module.

Kody rule violation: Centralize string constants

Prompt for LLM

File src/components/chat/message-composer.tsx:

Line 139:

Hardcoded hex color literals `'#dc2626'` and `'#6b7280'` in `src/components/chat/message-composer.tsx` duplicate values and make palette or theme changes error-prone. Extract these repeated strings into named constants like `COLOR_ERROR` and `COLOR_MUTED` within a centralized theme module.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

@Resgrid-Bot

Resgrid-Bot commented Aug 8, 2026

Copy link
Copy Markdown

Code Review Completed! 🔥

The code review was successfully completed based on your current configurations.

Kody Guide: Usage and Configuration
Interacting with Kody
  • Request a Review: Ask Kody to review your PR manually by adding a comment with the @kody start-review command at the root of your PR.

  • Validate Business Logic: Ask Kody to validate your code against business rules by adding a comment with the @kody -v business-logic command.

  • Provide Feedback: Help Kody learn and improve by reacting to its comments with a 👍 for helpful suggestions or a 👎 if improvements are needed.

Current Kody Configuration
Review Options

The following review options are enabled or disabled:

Options Enabled
Bug
Performance
Security
Business Logic

Access your configuration settings here.

@ucswift

ucswift commented Aug 8, 2026

Copy link
Copy Markdown
Member Author

Approve

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This PR is approved.

@ucswift
ucswift merged commit 945dc9b into master Aug 8, 2026
11 of 12 checks passed
Comment on lines +76 to +79
void useChatStore
.getState()
.fetchChannels()
.finally(() => setResolveAttempted(true));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules high

Unhandled promise rejection occurs if fetchChannels() rejects because the void promise chain relies solely on .finally(). Add a .catch() handler to swallow and log the error before executing the finally block.

Kody rule violation: Handle async operations with proper error handling

Prompt for LLM

File src/app/chat/[channelId].tsx:

Line 76 to 79:

Unhandled promise rejection occurs if `fetchChannels()` rejects because the `void` promise chain relies solely on `.finally()`. Add a `.catch()` handler to swallow and log the error before executing the finally block.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

Comment on lines +76 to +79
void useChatStore
.getState()
.fetchChannels()
.finally(() => setResolveAttempted(true));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules high

Unguarded network call to fetchChannels() silently propagates failures without context mapping. Wrap the invocation in a try/catch to log the error via logger.error and map it to application-level errors.

Kody rule violation: Add try-catch blocks for external calls

Prompt for LLM

File src/app/chat/[channelId].tsx:

Line 76 to 79:

Unguarded network call to `fetchChannels()` silently propagates failures without context mapping. Wrap the invocation in a `try/catch` to log the error via `logger.error` and map it to application-level errors.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

Comment on lines +123 to 127
try {
return await Clipboard.setStringAsync(text);
} catch {
return false;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Bug high

Falsy return value from Clipboard.setStringAsync causes native callers in [channelId].tsx:351 and chatbot.tsx:161 to falsely trigger the "copy unavailable" toast on success. Await the call separately and return an explicit boolean to reflect the actual state.

try {
  await Clipboard.setStringAsync(text);
  return true;
} catch {
  return false;
}
Prompt for LLM

File src/components/chat/chat-utils.ts:

Line 123 to 127:

Falsy return value from `Clipboard.setStringAsync` causes native callers in `[channelId].tsx:351` and `chatbot.tsx:161` to falsely trigger the "copy unavailable" toast on success. Await the call separately and return an explicit boolean to reflect the actual state.

Suggested Code:

  try {
    await Clipboard.setStringAsync(text);
    return true;
  } catch {
    return false;
  }

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

Comment on lines +125 to +126
} catch {
return false;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules high

Silent exception swallowing in the Clipboard.setStringAsync catch block causes native clipboard write failures to become invisible during debugging. Log the captured error with context via logger.error before returning false.

Kody rule violation: Avoid empty catch blocks

Prompt for LLM

File src/components/chat/chat-utils.ts:

Line 125 to 126:

Silent exception swallowing in the `Clipboard.setStringAsync` catch block causes native clipboard write failures to become invisible during debugging. Log the captured error with context via `logger.error` before returning `false`.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

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