Skip to content

RG-T117 Chat fix - #126

Merged
ucswift merged 2 commits into
masterfrom
develop
Aug 12, 2026
Merged

RG-T117 Chat fix#126
ucswift merged 2 commits into
masterfrom
develop

Conversation

@ucswift

@ucswift ucswift commented Aug 12, 2026

Copy link
Copy Markdown
Member

Pull Request Description

RG-T117 Chat fix

This PR introduces several improvements to the chat functionality:

New Features & Enhancements

  1. Direct messages with units: Users can now start 1:1 conversations with units (not just personnel). When starting a DM with a unit, the request is sent with TargetUnitId instead of TargetUserId. Units are visually distinguished with a truck icon and a "Unit" badge instead of a user avatar.

  2. Optional group name: Group chat creation no longer requires a name. The server auto-names groups based on their members. The placeholder text and validation were updated accordingly.

  3. Active channel notification suppression: The app now reports the currently viewed conversation to the chat hub via a new SetActiveChannel invocation, allowing the server to suppress push notifications for the conversation the user is actively viewing. This state is also re-established automatically after connection drops/reconnects.

Bug Fixes

  • Excluded pseudo-entries from recipients: Server pseudo-entries (e.g., "Everyone" with empty Type) are no longer incorrectly treated as valid direct message targets.
  • Recipient reload on mode change: Added mode to the effect dependency array so recipients are properly refreshed when switching between DM and group modes.

Summary by CodeRabbit

  • New Features

    • Direct-message conversations can now target units, displayed with dedicated icons and labels.
    • Group conversations can be created without entering a name.
    • Recipient lists refresh automatically when the conversation type changes.
    • Added localized labels for units and optional group names across supported languages.
  • Bug Fixes

    • Improved active-channel synchronization after reconnecting, including reliable clearing and retry behavior.

@Resgrid-Bot

This comment has been minimized.

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The chat UI supports unit recipients for direct messages and optional group names. The chat store queues active-channel markers, retries failed hub updates, and resynchronizes markers after reconnecting. New translations cover unit labels and optional group names.

Changes

Conversation creation updates

Layer / File(s) Summary
Unit recipients and optional group names
src/components/chat/new-conversation-sheet.tsx, src/translations/*.json
Direct messages load unit recipients and submit TargetUnitId for unit targets. Unit recipients use truck icons and labels. Group names are optional, and translations cover the new labels.

Active channel synchronization

Layer / File(s) Summary
Active-channel hub synchronization
src/stores/chat/store.ts, src/stores/chat/__tests__/hub-invoke-args.test.ts
The store sends SetActiveChannel with the channel ID and null, retries failed markers, and resynchronizes active or null markers after reconnecting. Tests cover invocation, retry, and redundant-send behavior.

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

Sequence Diagram(s)

sequenceDiagram
  participant NewConversationSheet
  participant RecipientService
  participant ChatAPI
  NewConversationSheet->>RecipientService: load recipients when mode changes
  RecipientService-->>NewConversationSheet: return people and units
  NewConversationSheet->>ChatAPI: create conversation with TargetUserId or TargetUnitId
Loading
sequenceDiagram
  participant ChatStore
  participant ChatHub
  participant Connection
  ChatStore->>ChatHub: SetActiveChannel(channelId, null)
  ChatHub-->>ChatStore: return success or failure
  Connection->>ChatStore: report reconnect
  ChatStore->>ChatHub: resynchronize active or null marker
Loading

Possibly related PRs

  • Resgrid/Dispatch#122: Both changes modify new-conversation-sheet.tsx and extend conversation creation behavior.

Suggested reviewers: github-actions

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title identifies a chat-related fix and matches the pull request changes, although it does not describe the specific functionality.
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: 3

🧹 Nitpick comments (4)
src/components/chat/new-conversation-sheet.tsx (2)

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

Extract the recipient press handler.

filtered.map creates an anonymous onPress callback for each row on every render. Extract each row into a memoized component and define its press handler with useCallback.

🤖 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/new-conversation-sheet.tsx` at line 184, Extract the
recipient row rendered by filtered.map into a memoized component, moving the
Pressable and its recipient-specific behavior there. In that component, define
the onPress handler with useCallback while preserving the existing dm versus
toggle behavior and submitting-disabled state.

Source: Coding guidelines


66-72: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Add Jest coverage for NewConversationSheet.

Cover unit loading, TargetUnitId, TargetUserId, blank group names, mode changes, and empty or error recipient states. Assert API arguments, the createAdHocChannel payload, and rendered labels.

🤖 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/new-conversation-sheet.tsx` around lines 66 - 72, Add
Jest tests for NewConversationSheet covering unit recipient loading,
TargetUnitId and TargetUserId handling, blank group names, mode changes, and
empty or failed recipient responses. Assert getRecipients arguments, the
createAdHocChannel payload, and the rendered recipient labels across these
scenarios.

Source: Coding guidelines

src/stores/chat/__tests__/hub-invoke-args.test.ts (2)

75-83: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for reconnect re-reporting.

These assertions cover setActiveChannel only. They do not exercise handleChatConnected in src/stores/chat/store.ts, Lines 856-858. Add a Jest test that sets an active channel, calls handleChatConnected, and verifies the SetActiveChannel arguments.

As per coding guidelines, “Generate tests for all components, services and logic generated. Ensure tests run without errors and fix any issues.”

🤖 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/chat/__tests__/hub-invoke-args.test.ts` around lines 75 - 83, The
existing test only verifies setActiveChannel; add coverage for
handleChatConnected reconnect behavior. In the chat store test, set an active
channel, invoke handleChatConnected, and assert mockInvoke receives chatHub,
SetActiveChannel, the active channel, and null.

Source: Coding guidelines


11-11: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Document the SetActiveChannel clear contract.

ChatHub.SetActiveChannel accepts null or an empty channelId to clear the active marker. Update the test comment to use string? channelId.

🤖 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/chat/__tests__/hub-invoke-args.test.ts` at line 11, Update the
`SetActiveChannel` test comment to declare `string? channelId`, documenting that
the channel identifier may be null when clearing the active marker; leave the
`asUnitId` parameter unchanged.
🤖 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/components/chat/new-conversation-sheet.tsx`:
- Around line 66-72: Update the recipient loading and display logic in the
new-conversation sheet to use recipient-neutral or DM-specific translation keys
whenever includeUnits is true, replacing chat.search_people, chat.no_people, and
chat.load_people_failed in that mode. Add the corresponding keys and
translations to every locale while preserving the existing people-only copy for
non-DM modes.
- Around line 107-108: Validate targetId in the createDirectMessage path for
unit recipients before constructing the request, requiring a complete decimal
integer rather than relying on parseInt’s prefix parsing. Reject malformed
values such as “12abc” or “12.5” and avoid sending TargetUnitId when validation
fails; preserve the existing user-recipient request path.

In `@src/stores/chat/store.ts`:
- Around line 288-290: Update reset to explicitly invoke SetActiveChannel with
null arguments when clearing chat state, ensuring the marker is cleared even if
disconnectChatHub or connection.stop() fails. Reuse the existing safeInvoke
mechanism and preserve the active-channel reporting behavior elsewhere.

---

Nitpick comments:
In `@src/components/chat/new-conversation-sheet.tsx`:
- Line 184: Extract the recipient row rendered by filtered.map into a memoized
component, moving the Pressable and its recipient-specific behavior there. In
that component, define the onPress handler with useCallback while preserving the
existing dm versus toggle behavior and submitting-disabled state.
- Around line 66-72: Add Jest tests for NewConversationSheet covering unit
recipient loading, TargetUnitId and TargetUserId handling, blank group names,
mode changes, and empty or failed recipient responses. Assert getRecipients
arguments, the createAdHocChannel payload, and the rendered recipient labels
across these scenarios.

In `@src/stores/chat/__tests__/hub-invoke-args.test.ts`:
- Around line 75-83: The existing test only verifies setActiveChannel; add
coverage for handleChatConnected reconnect behavior. In the chat store test, set
an active channel, invoke handleChatConnected, and assert mockInvoke receives
chatHub, SetActiveChannel, the active channel, and null.
- Line 11: Update the `SetActiveChannel` test comment to declare `string?
channelId`, documenting that the channel identifier may be null when clearing
the active marker; leave the `asUnitId` parameter unchanged.
🪄 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: a87efc22-aca8-429f-8ffd-2fc3257118d7

📥 Commits

Reviewing files that changed from the base of the PR and between d44c39c and 6cac83c.

📒 Files selected for processing (12)
  • src/components/chat/new-conversation-sheet.tsx
  • src/stores/chat/__tests__/hub-invoke-args.test.ts
  • src/stores/chat/store.ts
  • src/translations/ar.json
  • src/translations/de.json
  • src/translations/en.json
  • src/translations/es.json
  • src/translations/fr.json
  • src/translations/it.json
  • src/translations/pl.json
  • src/translations/sv.json
  • src/translations/uk.json

Comment on lines +66 to +72
// DM mode also offers units (Dispatch can open a 1:1 with a unit);
// group membership only supports users, so group mode stays people-only.
const includeUnits = mode === 'dm';
getRecipients(true, includeUnits)
.then((result) => {
if (cancelled) return;
setRecipients((result.Data ?? []).filter(isPersonRecipient));
setRecipients((result.Data ?? []).filter((r) => isPersonRecipient(r) || (includeUnits && isUnitRecipient(r))));

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

Use recipient-neutral copy in direct-message mode.

When includeUnits is true, the list contains people and units. The component still uses chat.search_people, chat.no_people, and chat.load_people_failed. A unit-only result then shows incorrect text. Add recipient-neutral keys or select mode-specific keys, and add them to every locale.

🤖 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/new-conversation-sheet.tsx` around lines 66 - 72, Update
the recipient loading and display logic in the new-conversation sheet to use
recipient-neutral or DM-specific translation keys whenever includeUnits is true,
replacing chat.search_people, chat.no_people, and chat.load_people_failed in
that mode. Add the corresponding keys and translations to every locale while
preserving the existing people-only copy for non-DM modes.

Comment on lines +107 to +108
const targetId = recipientUserId(recipient);
const response = await createDirectMessage(isUnitRecipient(recipient) ? { TargetUnitId: parseInt(targetId, 10) } : { TargetUserId: targetId });

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.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

rg --glob '!node_modules/**' --glob '!dist/**' -n -C 8 \
  'TargetUnitId|CreateDirectMessageInput|recipientUserId|RecipientsResultData|getRecipients' .

Repository: Resgrid/Dispatch

Length of output: 17490


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '%s\n' '--- relevant implementation ---'
sed -n '1,125p' src/components/chat/new-conversation-sheet.tsx
printf '%s\n' '--- recipient model and API types ---'
cat src/models/v4/messages/recipientsResultData.ts
sed -n '1,90p' src/models/v4/chat/chatInputs.ts
printf '%s\n' '--- repository references to recipient ID formats and unit targets ---'
rg --glob '!node_modules/**' --glob '!dist/**' -n -C 3 \
  'includeUnits|RecipientsResultData|TargetUnitId|CreateDirectMessage|GetRecipients|Id: "[^"]*[:][^"]*"' .

Repository: Resgrid/Dispatch

Length of output: 15783


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

node - <<'JS'
function recipientUserId(id) {
  return id.includes(':') ? (id.split(':').pop() ?? id) : id;
}

const cases = ['U:12', 'U:12abc', 'U:12.5', 'U:', 'U:abc', '12', ' 12 '];
for (const raw of cases) {
  const targetId = recipientUserId(raw);
  const parsed = Number.parseInt(targetId, 10);
  const isDecimalInteger = /^[0-9]+$/.test(targetId);
  console.log(JSON.stringify({ raw, targetId, parsed, isDecimalInteger }));
}
JS

printf '%s\n' '--- repository origin ---'
git remote get-url origin 2>/dev/null || true

Repository: Resgrid/Dispatch

Length of output: 706


Validate the unit ID before building the request.

recipientUserId can produce malformed values, and parseInt accepts numeric prefixes such as "12abc" or "12.5" as 12. Require a decimal integer before sending TargetUnitId; otherwise reject the recipient.

🤖 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/new-conversation-sheet.tsx` around lines 107 - 108,
Validate targetId in the createDirectMessage path for unit recipients before
constructing the request, requiring a complete decimal integer rather than
relying on parseInt’s prefix parsing. Reject malformed values such as “12abc” or
“12.5” and avoid sending TargetUnitId when validation fails; preserve the
existing user-recipient request path.

Comment thread src/stores/chat/store.ts Outdated
Comment on lines +288 to +290
// Report the actively viewed conversation to the hub so the server can
// suppress push notifications for it; null clears the marker.
void safeInvoke('SetActiveChannel', channelId ?? null, 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.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 8 'useChatStore.*reset|chat.*reset' src
rg -n -C 8 'disconnectFromHub|disconnectAll|\.stop\(\)' src

Repository: Resgrid/Dispatch

Length of output: 50373


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- tracked candidate files ---'
git ls-files 'src/stores/chat/store.ts' 'src' | rg '(^|/)(chat|hub|store)(/|[^/]*)' | head -n 200

printf '%s\n' '--- store outline ---'
ast-grep outline src/stores/chat/store.ts

printf '%s\n' '--- store lifecycle references ---'
rg -n -C 12 --glob '!src/utils/webview-scripts.ts' \
  'SetActiveChannel|safeInvoke|activeChannelId|(^|[^[:alnum:]_])reset[[:space:]]*[:=]|\breset\s*\(' \
  src/stores/chat/store.ts

printf '%s\n' '--- exact reset and hub lifecycle call sites ---'
rg -n -C 8 --glob '!src/utils/webview-scripts.ts' \
  '(useChatStore|chatStore|ChatStore|disconnectFromHub|disconnectAll|startHub|stopHub|connectToHub|hub\.stop|\.stop\(\)|\.reset\(\))' \
  src --glob '*.{ts,tsx,js,jsx}'

Repository: Resgrid/Dispatch

Length of output: 50373


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re

store = Path("src/stores/chat/store.ts")
lines = store.read_text().splitlines()
for start, end in ((250, 305), (850, 900)):
    print(f"--- {store}:{start}-{end} ---")
    for number in range(start, min(end, len(lines)) + 1):
        print(f"{number}:{lines[number-1]}")

patterns = re.compile(
    r"(useChatStore|chatStore|disconnectFromHub|disconnectAll|"
    r"startHub|stopHub|connectToHub|SetActiveChannel|activeChannelId|"
    r"\.reset\s*\(|\breset\s*[:=])"
)
excluded = {"src/utils/webview-scripts.ts"}
print("--- bounded tracked-source matches ---")
count = 0
for path_text in sorted(Path("src").rglob("*")):
    if not path_text.is_file() or str(path_text) in excluded:
        continue
    if path_text.suffix not in {".ts", ".tsx", ".js", ".jsx"}:
        continue
    for number, line in enumerate(path_text.read_text(errors="replace").splitlines(), 1):
        if patterns.search(line):
            print(f"{path_text}:{number}:{line}")
            count += 1
            if count >= 300:
                raise SystemExit
PY

Repository: Resgrid/Dispatch

Length of output: 50373


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

def show(path, start=1, end=None):
    lines = Path(path).read_text(errors="replace").splitlines()
    end = end or len(lines)
    print(f"--- {path}:{start}-{min(end, len(lines))} ---")
    for number in range(start, min(end, len(lines)) + 1):
        print(f"{number}:{lines[number-1]}")

show("src/app/(app)/_layout.tsx")
show("src/hooks/useSignalR.ts")
PY

printf '%s\n' '--- tracked SignalR and hub paths ---'
git ls-files | rg -i '(^|/)(signalr|hub|.*signal.*|.*hub.*)(/|$|\.)' | head -n 100

Repository: Resgrid/Dispatch

Length of output: 32169


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re

targets = [
    "src/stores/signalr/signalr-store.ts",
    "src/services/signalr.service.ts",
    "src/hooks/use-signalr-lifecycle.ts",
]
patterns = re.compile(
    r"(connectChatHub|disconnectChatHub|connectToHub|disconnectFromHub|"
    r"safeInvoke|SetActiveChannel|stop\(|start\(|isConnected|disconnect)"
)
for name in targets:
    path = Path(name)
    lines = path.read_text(errors="replace").splitlines()
    print(f"--- {name} matching regions ---")
    hits = [i for i, line in enumerate(lines) if patterns.search(line)]
    shown = set()
    for hit in hits:
        start, end = max(0, hit - 8), min(len(lines), hit + 12)
        key = (start, end)
        if key in shown:
            continue
        shown.add(key)
        print(f"[{start+1}-{end}]")
        for number in range(start + 1, end + 1):
            print(f"{number}:{lines[number-1]}")
PY

printf '%s\n' '--- production reset call sites excluding generated source ---'
python3 - <<'PY'
from pathlib import Path
for path in sorted(Path("src").rglob("*")):
    if not path.is_file() or path.suffix not in {".ts", ".tsx", ".js", ".jsx"}:
        continue
    if path.name in {"webview-scripts.ts", "form-render-source.ts"}:
        continue
    for number, line in enumerate(path.read_text(errors="replace").splitlines(), 1):
        if "useChatStore.getState().reset" in line or "useChatStore.reset" in line:
            print(f"{path}:{number}:{line}")
PY

Repository: Resgrid/Dispatch

Length of output: 50372


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re

layout = Path("src/app/(app)/_layout.tsx").read_text()
store = Path("src/stores/chat/store.ts").read_text()
service = Path("src/services/signalr.service.ts").read_text()

teardown_start = layout.index("async function teardownSignedInSession")
teardown = layout[teardown_start:layout.index("\n}\n", teardown_start) + 2]
print("--- lifecycle invariants ---")
print("chat disconnect precedes reset:", teardown.index("disconnectChatHub") < teardown.index("useChatStore.getState().reset"))
print("reset call count in teardown:", teardown.count("useChatStore.getState().reset"))
print("disconnectChatHub awaits service disconnect:", bool(re.search(
    r"disconnectChatHub:\s*async\s*\(\)\s*=>.*?await signalRService\.disconnectFromHub",
    store, re.S
)))
print("disconnect service awaits connection.stop:", bool(re.search(
    r"public async disconnectFromHub\(hubName: string\).*?await connection\.stop\(\)",
    service, re.S
)))
print("disconnectChatHub catches errors without rethrow:", bool(re.search(
    r"disconnectChatHub:\s*async\s*\(\)\s*=>.*?catch \(error\).*?set\(\{ error: err \}\);\s*\n\s*}\s*,",
    store, re.S
)))

print("--- production reset call sites ---")
for path in sorted(Path("src").rglob("*")):
    if not path.is_file() or path.name in {"webview-scripts.ts", "form-render-source.ts"}:
        continue
    if path.suffix not in {".ts", ".tsx", ".js", ".jsx"}:
        continue
    for number, line in enumerate(path.read_text(errors="replace").splitlines(), 1):
        if "useChatStore.getState().reset" in line or "useChatStore.reset" in line:
            print(f"{path}:{number}:{line.strip()}")

print("--- SetActiveChannel call sites ---")
for path in sorted(Path("src").rglob("*")):
    if not path.is_file() or path.name in {"webview-scripts.ts", "form-render-source.ts"}:
        continue
    if path.suffix not in {".ts", ".tsx", ".js", ".jsx"}:
        continue
    for number, line in enumerate(path.read_text(errors="replace").splitlines(), 1):
        if "SetActiveChannel" in line:
            print(f"{path}:{number}:{line.strip()}")
PY

Repository: Resgrid/Dispatch

Length of output: 1267


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

def show(path, start, end):
    lines = Path(path).read_text(errors="replace").splitlines()
    print(f"--- {path}:{start}-{end} ---")
    for number in range(start, min(end, len(lines)) + 1):
        print(f"{number}:{lines[number-1]}")

show("src/stores/chat/store.ts", 1, 90)
show("src/stores/chat/__tests__/hub-invoke-args.test.ts", 1, 115)
show("src/stores/signalr/signalr-store.ts", 817, 833)
show("src/services/signalr.service.ts", 839, 894)
PY

Repository: Resgrid/Dispatch

Length of output: 11164


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 10 'function safeInvoke|const safeInvoke|safeInvoke\s*=' src/stores/chat/store.ts

Repository: Resgrid/Dispatch

Length of output: 894


Clear the active-channel marker in reset.

disconnectChatHub swallows disconnect failures, so reset can run after connection.stop() fails. In that case, reset does not send SetActiveChannel(null, null), and the server marker can remain stale.

🤖 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/chat/store.ts` around lines 288 - 290, Update reset to explicitly
invoke SetActiveChannel with null arguments when clearing chat state, ensuring
the marker is cleared even if disconnectChatHub or connection.stop() fails.
Reuse the existing safeInvoke mechanism and preserve the active-channel
reporting behavior elsewhere.

Comment on lines +124 to 128
if (selected.size === 0) return;
setSubmitting(true);
try {
// Name is optional — the server auto-names the group after its members.
const response = await createAdHocChannel({ Name: groupName.trim(), MemberUserIds: Array.from(selected) });

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 medium

WHAT: createGroup now sends Name: '' (empty string) when no group name is entered, because the !groupName.trim() guard was removed. WHY: an empty string is a distinct wire value from an omitted field — if the server auto-names based on field absence (or requires non-empty), sending "" either creates a blank-named group or returns a 400, defeating the optional-name feature. HOW: make Name optional in CreateAdHocChannelInput and omit it when blank, or confirm the server treats "" as the auto-name trigger.

// Make Name optional in CreateAdHocChannelInput and only send it when provided:
const input: CreateAdHocChannelInput = { MemberUserIds: Array.from(selected) };
const name = groupName.trim();
if (name) input.Name = name;
const response = await createAdHocChannel(input);
Prompt for LLM

File src/components/chat/new-conversation-sheet.tsx:

Line 124 to 128:

WHAT: createGroup now sends `Name: ''` (empty string) when no group name is entered, because the `!groupName.trim()` guard was removed. WHY: an empty string is a distinct wire value from an omitted field — if the server auto-names based on field absence (or requires non-empty), sending `""` either creates a blank-named group or returns a 400, defeating the optional-name feature. HOW: make `Name` optional in CreateAdHocChannelInput and omit it when blank, or confirm the server treats `""` as the auto-name trigger.

Suggested Code:

// Make Name optional in CreateAdHocChannelInput and only send it when provided:
const input: CreateAdHocChannelInput = { MemberUserIds: Array.from(selected) };
const name = groupName.trim();
if (name) input.Name = name;
const response = await createAdHocChannel(input);

Talk to Kody by mentioning @kody

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

Comment thread src/stores/chat/store.ts Outdated
set({ activeChannelId: channelId });
// Report the actively viewed conversation to the hub so the server can
// suppress push notifications for it; null clears the marker.
void safeInvoke('SetActiveChannel', channelId ?? null, null);

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

WHAT: The promise returned by safeInvoke is discarded with the void operator and has no .catch handler. WHY: If safeInvoke rejects, the rejection is unhandled, violating the rule that every async operation must be guarded. HOW: Either attach a .catch (e.g. safeInvoke(...).catch(err => logger.error('SetActiveChannel failed', { channelId, err }))) or wrap in try/catch if using await.

Also found in:

  • src/stores/chat/store.ts:858-858

Kody rule violation: Handle async operations with proper error handling

Prompt for LLM

File src/stores/chat/store.ts:

Line 290:

WHAT: The promise returned by safeInvoke is discarded with the `void` operator and has no .catch handler. WHY: If safeInvoke rejects, the rejection is unhandled, violating the rule that every async operation must be guarded. HOW: Either attach a .catch (e.g. `safeInvoke(...).catch(err => logger.error('SetActiveChannel failed', { channelId, err }))`) or wrap in try/catch if using await.

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 12, 2026

Copy link
Copy Markdown

Kody Review Complete

Great news! 🎉
No issues were found that match your current review configurations.

Keep up the excellent work! 🚀

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.

@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: 1

🤖 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/chat/__tests__/hub-invoke-args.test.ts`:
- Line 168: Update the SetActiveChannel assertion in the hub invocation test to
explicitly verify that mockInvoke was not called with null as either argument,
replacing the expect.anything() matchers with null values.
🪄 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: 8a04a512-9452-410b-85d6-07b5eecb7c98

📥 Commits

Reviewing files that changed from the base of the PR and between 6cac83c and 15204f4.

📒 Files selected for processing (2)
  • src/stores/chat/__tests__/hub-invoke-args.test.ts
  • src/stores/chat/store.ts

useChatStore.getState().handleChatConnected();
await flush();

expect(mockInvoke).not.toHaveBeenCalledWith('chatHub', 'SetActiveChannel', expect.anything(), expect.anything());

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Verify Jest documents that expect.anything() excludes null and undefined.
curl -fsSL https://jestjs.io/docs/expect |
  rg -i -C 2 'anything.*null|null.*anything'

Repository: Resgrid/Dispatch

Length of output: 37902


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="src/stores/chat/__tests__/hub-invoke-args.test.ts"
printf '%s\n' '--- test context ---'
cat -n "$file" | sed -n '130,185p'

printf '%s\n' '--- related symbols and calls ---'
rg -n -C 3 'handleChatConnected|SetActiveChannel|mockInvoke' src/stores/chat

Repository: Resgrid/Dispatch

Length of output: 12193


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- package and Jest configuration ---'
rg -n -C 2 '"jest"|jest.config|testMatch|testRegex' package.json '*jest*' 2>/dev/null || true

printf '%s\n' '--- exact test structure ---'
cat -n src/stores/chat/__tests__/hub-invoke-args.test.ts | sed -n '1,210p'

printf '%s\n' '--- handler definitions and call sites ---'
rg -n -C 8 'handleChatConnected|SetActiveChannel|mockInvoke' src --glob '*.{ts,tsx}'

Repository: Resgrid/Dispatch

Length of output: 25762


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
p = Path("src/stores/chat/__tests__/hub-invoke-args.test.ts")
text = p.read_text()
for i, line in enumerate(text.splitlines(), 1):
    if "SetActiveChannel" in line or "handleChatConnected" in line:
        print(f"{i}: {line}")
PY

printf '%s\n' '--- tracked candidate files ---'
git ls-files | rg '(^|/)(chat|hub).*|src/stores/chat' | head -80

Repository: Resgrid/Dispatch

Length of output: 1896


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="src/stores/chat/__tests__/hub-invoke-args.test.ts"
cat -n "$file" | sed -n '130,185p'
printf '\n--- related code ---\n'
rg -n -C 8 'handleChatConnected|SetActiveChannel|mockInvoke' src/stores/chat

Repository: Resgrid/Dispatch

Length of output: 17346


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- complete reconnect handler ---'
cat -n src/stores/chat/store.ts | sed -n '860,890p'

printf '%s\n' '--- matcher behavior for the calls relevant to this test ---'
node - <<'JS'
const anything = value => value !== null && value !== undefined;
const calls = [
  ['chatHub', 'SetActiveChannel', null, null],
  ['chatHub', 'SetActiveChannel', 'channel-1', null],
];

const matchesOriginal = call =>
  call[0] === 'chatHub' &&
  call[1] === 'SetActiveChannel' &&
  anything(call[2]) &&
  anything(call[3]);

const matchesNullFix = call =>
  call[0] === 'chatHub' &&
  call[1] === 'SetActiveChannel' &&
  call[2] === null &&
  call[3] === null;

for (const call of calls) {
  console.log(JSON.stringify(call), {
    originalNot: !matchesOriginal(call),
    nullFixNot: !matchesNullFix(call),
  });
}
JS

Repository: Resgrid/Dispatch

Length of output: 1791


Assert that the null marker is not resent.

expect.anything() excludes null, so this assertion does not detect SetActiveChannel(null, null). Use null for both arguments.

🤖 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/chat/__tests__/hub-invoke-args.test.ts` at line 168, Update the
SetActiveChannel assertion in the hub invocation test to explicitly verify that
mockInvoke was not called with null as either argument, replacing the
expect.anything() matchers with null values.

Source: Coding guidelines

@ucswift
ucswift merged commit 0d813e6 into master Aug 12, 2026
11 of 12 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