Skip to content

feat(desktop): extract sub-components from MessagesApp.tsx (partial #657)#1877

Merged
jaylfc merged 1 commit into
jaylfc:devfrom
hognek:feat/657-messagesapp-extract
Jul 19, 2026
Merged

feat(desktop): extract sub-components from MessagesApp.tsx (partial #657)#1877
jaylfc merged 1 commit into
jaylfc:devfrom
hognek:feat/657-messagesapp-extract

Conversation

@hognek

@hognek hognek commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Problem

MessagesApp.tsx was the second-largest file in the desktop SPA at 2,856 lines. Extracting sub-components improves testability and maintainability. Closes #657.

What's done (3 commits)

Commit 1 — extract 5 components:

  • ChannelSidebar.tsx (1,137 lines)
  • MessageList.tsx (846 lines)
  • MessageInput.tsx (197 lines)
  • ReactionBar.tsx (50 lines)
  • types.ts (48 lines)

Commit 2 — wire ReactionBar + MessageInput
Commit 3 — wire MessageList (430 lines replaced)

Result: MessagesApp.tsx 2,856 → 2,424 lines (-432, -15%)

Verification

  • ✓ 8/8 render-helpers tests pass (vitest)
  • ✓ Production build succeeds (vite build, 18.45s)

Files changed

6 files, +2,458 / -1,061

Summary by CodeRabbit

  • New Features
    • Added a responsive channel sidebar with project, archived, unread, connection-status, and external coordination views.
    • Improved message threads with reactions, editing, deletion states, attachments, copying, emoji selection, replies, search, and pinned-message access.
    • Added a streamlined composer supporting slash commands, mentions, attachments, image pasting, retries, and mobile safe-area spacing.
    • Added clearer archived-channel restore and permanent-delete controls.
  • Refactor
    • Reorganized chat navigation, message rendering, and message composition into dedicated components while preserving existing functionality.

@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

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

Next review available in: 2 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: b9daf347-687b-42b5-a4f0-c1e31d7bd9d7

📥 Commits

Reviewing files that changed from the base of the PR and between b6acf8f and b00c142.

📒 Files selected for processing (1)
  • desktop/src/apps/chat/MessageList.tsx
📝 Walkthrough

Walkthrough

Changes

Messages UI refactor

Layer / File(s) Summary
Chat contracts and channel sidebar
desktop/src/apps/chat/types.ts, desktop/src/apps/chat/ChannelSidebar.tsx, desktop/src/apps/MessagesApp.tsx
Adds shared chat types and moves responsive channel, project, archive, connection, and bus rendering into ChannelSidebar.
Message thread rendering
desktop/src/apps/chat/MessageList.tsx, desktop/src/apps/chat/ReactionBar.tsx, desktop/src/apps/MessagesApp.tsx
Moves message headers, rows, reactions, attachments, typing indicators, copy actions, and scrolling into MessageList and ReactionBar.
Message composer component
desktop/src/apps/chat/MessageInput.tsx, desktop/src/apps/MessagesApp.tsx
Moves keyboard submission, slash commands, mentions, attachments, paste uploads, and archived-channel handling into MessageInput.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

  • jaylfc/taOS#812: Overlaps with message content rendering and clipboard-copy behavior.
  • jaylfc/taOS#833: Overlaps with emoji reactions, day separators, and message-list UX.
  • jaylfc/taOS#880: Overlaps with mobile channel-list rendering and relativeTime usage.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR extracts four components, but it omits ThreadPanel and the requested Vitest/Testing Library coverage, so it only partially meets #657. Add ThreadPanel extraction plus tests for the extracted components, WebSocket reconnection, and markdown renderer edge cases.
✅ 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 clearly states the main change: extracting sub-components from MessagesApp, and marks the work as partial.
Out of Scope Changes check ✅ Passed All changes stay within the MessagesApp extraction effort and related shared chat UI types, with no unrelated functionality added.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@gitar-bot

gitar-bot Bot commented Jul 17, 2026

Copy link
Copy Markdown

Gitar is working

Gitar

@hognek
hognek marked this pull request as ready for review July 17, 2026 15:15
@qodo-code-review

Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

Comment thread desktop/src/apps/chat/MessageList.tsx Outdated
}));

// Expose scroll-to-latest via ref
useEffect(() => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

WARNING: Mount-time auto-scroll doesn't sync autoScrollRef / atBottom

This effect scrolls to the bottom on first mount whenever messages.length > 0, but it never sets autoScrollRef.current = true (unlike scrollToLatest and MessageListHandle.scrollToBottom). In the parent, autoScrollRef decides whether incoming messages auto-scroll or increment the "new messages" badge. After this mount scroll the user is visually at the bottom, yet autoScrollRef may still be false, so the next arriving message can incorrectly surface a "X new" badge even though the viewport is already at the latest. Consider setting autoScrollRef.current = true / setAtBottom(true) here, or gating the effect on initial-load intent.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

const messageListRef = useRef<HTMLDivElement>(null);
const messagesEndRef = useRef<HTMLDivElement>(null);

useImperativeHandle(ref, () => ({

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

SUGGESTION: useImperativeHandle is called without a dependency array, so the handle object is re-created on every render.

Add an explicit dependency array since the handle only reads the stable messagesEndRef.

Suggested change
useImperativeHandle(ref, () => ({
useImperativeHandle(ref, () => ({
scrollToBottom: () => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
},
}), [messagesEndRef]);

Reply with @kilocode-bot fix it to have Kilo Code address this issue.

@kilo-code-bot

kilo-code-bot Bot commented Jul 17, 2026

Copy link
Copy Markdown

Code Review Summary

Status: No New Issues Found (incremental) | Recommendation: Merge

Overview

Severity Count
CRITICAL 0
WARNING 0
SUGGESTION 0

Incremental Changes (commit b00c142)

The only file changed since the previous review was desktop/src/apps/chat/MessageList.tsx (+1 / -8). All three prior Kilo findings on that file are resolved by this commit:

  • Line 195useImperativeHandle now uses an explicit dependency array ([]), so the handle is no longer re-created every render. (previous SUGGESTION fixed)
  • Line 1useEffect import removed since it is no longer used after the mount-time effect was deleted. (consistent with the change)
  • N/A (mount auto-scroll) — the orphaned mount-time auto-scroll useEffect was removed, eliminating the autoScrollRef/atBottom desync that could surface a spurious "X new" badge. (previous WARNING resolved)

The PR description notes the message-list-drop-target class lost during extraction is also restored in this commit. No new issues were introduced by the changed lines.

Out-of-scope (unchanged since previous review)

Active inline comments on ChannelSidebar.tsx (lines 548, 1130), MessageInput.tsx (line 88), MessagesApp.tsx (lines 1584, 1773, 1948), and MessageList.tsx (line 379) belong to files/regions not touched by this incremental commit. They remain valid but are not re-reported here.

Files Reviewed (1 changed)
  • desktop/src/apps/chat/MessageList.tsx - 3 prior issues resolved, 0 new issues
Previous Review Summaries (2 snapshots, latest commit e0fd90f)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit e0fd90f)

Status: No New Issues Found (incremental) | Recommendation: Merge

Overview

Severity Count
CRITICAL 0
WARNING 0
SUGGESTION 0

Incremental Changes (commit e0fd90f)

The only file changed since the previous review was desktop/src/apps/chat/MessageList.tsx (+3 / -10). All three prior Kilo findings on that file are resolved by this commit:

  • Line 195useImperativeHandle now uses an explicit dependency array ([]), so the handle is no longer re-created every render. (previous SUGGESTION fixed)
  • Line 318 / 321 — the message-list-drop-target class is restored on the scroll container. (previous SUGGESTION fixed)
  • Line ~202 — the orphaned mount-time auto-scroll useEffect was removed, eliminating the autoScrollRef/atBottom desync that could surface a spurious "X new" badge. (previous WARNING resolved)

No new issues were introduced by the changed lines.

Out-of-scope (unchanged since previous review)

Six active inline comments on ChannelSidebar.tsx, MessageInput.tsx, MessagesApp.tsx (lines 548, 1130, 88, 1586, 1775, 1950) and one on MessageList.tsx (line 379) belong to files/regions not touched by this incremental commit. They remain valid but are not re-reported here.

Files Reviewed (1 changed)
  • desktop/src/apps/chat/MessageList.tsx - 3 prior issues resolved, 0 new issues

Previous review (commit b6acf8f)

Status: 3 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 2
Issue Details (click to expand)

CRITICAL

File Line Issue

WARNING

File Line Issue
desktop/src/apps/chat/MessageList.tsx 202 Mount-time auto-scroll does not sync autoScrollRef/atBottom, so a "X new" badge can appear even when the viewport is already at the latest message.

SUGGESTION

File Line Issue
desktop/src/apps/chat/MessageList.tsx 195 useImperativeHandle invoked without a dependency array; re-creates the handle every render.
desktop/src/apps/chat/MessageList.tsx ~329 (scroll container) The scroll container lost the message-list-drop-target class the original MessagesApp div had. Verify no CSS/JS relied on it (drag highlight is preserved via conditional ring-* classes).
Files Reviewed (6 files)
  • desktop/src/apps/MessagesApp.tsx - wiring verified; behavior preserved (ReactionBar, MessageInput, MessageList wired; AttachmentsBar moved into MessageInput; refs replaced by MessageListHandle).
  • desktop/src/apps/chat/ChannelSidebar.tsx - extracted component.
  • desktop/src/apps/chat/MessageInput.tsx - extracted component; onPaste prop added and forwarded to Textarea.
  • desktop/src/apps/chat/MessageList.tsx - 2 issues (see above).
  • desktop/src/apps/chat/ReactionBar.tsx - extracted presentational component.
  • desktop/src/apps/chat/types.ts - shared types.

Fix these issues in Kilo Cloud


Reviewed by hy3:free · Input: 50.7K · Output: 1.6K · Cached: 78.4K

@coderabbitai coderabbitai 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.

Actionable comments posted: 7

🧹 Nitpick comments (2)
desktop/src/apps/chat/MessageList.tsx (1)

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

Move shared rendering helpers out of MessagesApp.tsx.

MessagesApp renders MessageList, while this import points back to the parent module, creating a circular dependency and forcing component tests to load the monolith. Move these helpers into a dependency-neutral chat utility module.

🤖 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 `@desktop/src/apps/chat/MessageList.tsx` at line 29, Move renderContent,
dayLabel, relativeTime, toMs, and resolveAuthorDisplayState out of
MessagesApp.tsx into a dependency-neutral chat utility module, then update
MessageList and MessagesApp imports to use that module. Remove the parent-module
import from MessageList while preserving each helper’s existing behavior and
exports.
desktop/src/apps/MessagesApp.tsx (1)

204-212: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Move these helpers out of MessagesApp to break the import cycle.

MessageList consumes these exports while MessagesApp imports MessageList. Put them in a neutral chat utility module so the extracted component does not depend on its parent.

🤖 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 `@desktop/src/apps/MessagesApp.tsx` around lines 204 - 212, Move the exported
toMs and relativeTime helpers out of MessagesApp into a neutral chat utility
module, then update both MessagesApp and MessageList imports to use that module.
Remove the helper definitions and their exports from MessagesApp so MessageList
no longer depends on its parent and the import cycle is eliminated.
🤖 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 `@desktop/src/apps/chat/ChannelSidebar.tsx`:
- Around line 502-548: Update the project and project-channel <details> elements
in ChannelSidebar to use the supplied expansion state: bind their open values to
projectsExpanded and projectChannelExpanded, and invoke the corresponding
expansion callbacks from onToggle. Preserve the existing rendering and selection
behavior while ensuring desktop reflects and updates the controlled state.
- Around line 1100-1130: Update the archived actions container around the
restore and delete buttons to use a focus-within visibility utility alongside
group-hover, so the controls become visible when the channel row or its
descendants receive keyboard focus while preserving the existing pointer-hover
behavior.

In `@desktop/src/apps/chat/MessageInput.tsx`:
- Around line 80-88: Update MessageInput’s handleKeyDown and button submission
flow to share a single canSend predicate that accounts for valid uploads rather
than merely a non-empty attachment array. Process mention suggestion selection
and IME composition Enter before routing Enter to onSend, and ensure keyboard
submission uses the same upload gating as the button.

In `@desktop/src/apps/chat/MessageList.tsx`:
- Around line 380-386: Update the message grouping logic in MessageList so
showAuthor is true when showDaySeparator is true, even if the previous message
has the same author; ensure the first message of each new day displays the
author identity alongside the day separator.

In `@desktop/src/apps/MessagesApp.tsx`:
- Around line 1760-1775: Remove the original channel header rendered immediately
above MessageList in the MessagesApp component, leaving MessageList as the sole
owner of the channel header, controls, and pin popover. Preserve the existing
MessageList props and channel behavior.
- Around line 1933-1950: Update the MessageInput usage in MessagesApp to pass
the existing inputRef into the extracted component, and attach that forwarded
ref to MessageInput’s underlying textarea. Preserve the current ref’s use for
focus, cursor-aware mentions, and resizing while eliminating the unrelated
internal-only ref.
- Around line 1584-1586: Update the channel-selection handler passed as
onSelectChannel in MessagesApp so selecting the currently active channel also
clears busSelected, rather than relying only on an effect triggered by
selectedChannel changes. Preserve normal setSelectedChannel behavior for channel
changes while ensuring the bus viewer closes when reselecting the preserved
channel.

---

Nitpick comments:
In `@desktop/src/apps/chat/MessageList.tsx`:
- Line 29: Move renderContent, dayLabel, relativeTime, toMs, and
resolveAuthorDisplayState out of MessagesApp.tsx into a dependency-neutral chat
utility module, then update MessageList and MessagesApp imports to use that
module. Remove the parent-module import from MessageList while preserving each
helper’s existing behavior and exports.

In `@desktop/src/apps/MessagesApp.tsx`:
- Around line 204-212: Move the exported toMs and relativeTime helpers out of
MessagesApp into a neutral chat utility module, then update both MessagesApp and
MessageList imports to use that module. Remove the helper definitions and their
exports from MessagesApp so MessageList no longer depends on its parent and the
import cycle is eliminated.
🪄 Autofix (Beta)

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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 21bdb5b3-339b-455c-a6c1-8725b66b697e

📥 Commits

Reviewing files that changed from the base of the PR and between 96a33cb and b6acf8f.

📒 Files selected for processing (6)
  • desktop/src/apps/MessagesApp.tsx
  • desktop/src/apps/chat/ChannelSidebar.tsx
  • desktop/src/apps/chat/MessageInput.tsx
  • desktop/src/apps/chat/MessageList.tsx
  • desktop/src/apps/chat/ReactionBar.tsx
  • desktop/src/apps/chat/types.ts

Comment on lines +502 to +548
{!scope?.projectId && projectGroups.length > 0 && (
<details className="px-3 mt-2">
<summary className="cursor-pointer text-[10px] font-semibold uppercase tracking-wider text-white/30 py-1">
Projects
</summary>
{projectGroups.map((g) => (
<details key={g.id} className="ml-2 mt-1">
<summary className="cursor-pointer text-xs text-white/60 py-1">
{g.name}
</summary>
<div className="ml-2 mt-0.5">
{g.channels.map((ch) => (
<button
key={ch.id}
type="button"
onClick={() => onSelectChannel(ch.id)}
aria-pressed={selectedChannel === ch.id}
aria-label={`Channel ${ch.name}`}
title={
ch.settings?.kind === "a2a"
? "Agent coordination — mention @<slug> to hand off."
: undefined
}
className={`w-full text-left text-xs py-1 px-2 rounded flex items-center gap-1.5 ${
selectedChannel === ch.id
? "bg-white/10"
: "hover:bg-white/5"
}`}
>
{ch.settings?.kind === "a2a" && (
<Bot
size={12}
aria-hidden
style={{
color: "rgba(255,255,255,0.6)",
flexShrink: 0,
}}
/>
)}
{ch.name}
</button>
))}
</div>
</details>
))}
</details>
)}

Copy link
Copy Markdown

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

Honor the supplied project expansion state on desktop.

Both <details> elements are uncontrolled and default closed, ignoring projectsExpanded, projectChannelExpanded, and their callbacks. Wire open/onToggle, or use the controlled button pattern from the mobile implementation.

🤖 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 `@desktop/src/apps/chat/ChannelSidebar.tsx` around lines 502 - 548, Update the
project and project-channel <details> elements in ChannelSidebar to use the
supplied expansion state: bind their open values to projectsExpanded and
projectChannelExpanded, and invoke the corresponding expansion callbacks from
onToggle. Preserve the existing rendering and selection behavior while ensuring
desktop reflects and updates the controlled state.

Comment on lines +1100 to +1130
<div className="hidden group-hover:flex items-center shrink-0 pr-1">
<button
type="button"
onClick={() =>
onRestoreArchivedChannel(ch.id, ch.name)
}
disabled={!hasAgent}
aria-label={`Restore archived channel ${ch.name}`}
title={
hasAgent
? "Restore agent"
: "Agent entry missing — delete only"
}
className={`p-1 rounded transition-colors ${
hasAgent
? "text-white/30 hover:text-emerald-400 hover:bg-emerald-500/10 cursor-pointer"
: "text-white/15 cursor-not-allowed"
}`}
>
<RotateCcw size={12} aria-hidden="true" />
</button>
<button
type="button"
onClick={() => onDeleteArchivedChannel(ch.id)}
aria-label={`Permanently delete archived channel ${ch.name}`}
title="Delete permanently"
className="p-1 rounded text-white/30 hover:text-red-400 hover:bg-red-500/10 transition-colors cursor-pointer"
>
<Trash2 size={12} aria-hidden="true" />
</button>
</div>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Make archived actions keyboard-accessible.

The restore and delete buttons use hidden group-hover:flex, so they cannot receive focus without a pointer. Reveal them when the channel row has keyboard focus as well.

Proposed fix
-              <div className="hidden group-hover:flex items-center shrink-0 pr-1">
+              <div className="hidden group-hover:flex group-focus-within:flex items-center shrink-0 pr-1">
📝 Committable suggestion

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

Suggested change
<div className="hidden group-hover:flex items-center shrink-0 pr-1">
<button
type="button"
onClick={() =>
onRestoreArchivedChannel(ch.id, ch.name)
}
disabled={!hasAgent}
aria-label={`Restore archived channel ${ch.name}`}
title={
hasAgent
? "Restore agent"
: "Agent entry missing — delete only"
}
className={`p-1 rounded transition-colors ${
hasAgent
? "text-white/30 hover:text-emerald-400 hover:bg-emerald-500/10 cursor-pointer"
: "text-white/15 cursor-not-allowed"
}`}
>
<RotateCcw size={12} aria-hidden="true" />
</button>
<button
type="button"
onClick={() => onDeleteArchivedChannel(ch.id)}
aria-label={`Permanently delete archived channel ${ch.name}`}
title="Delete permanently"
className="p-1 rounded text-white/30 hover:text-red-400 hover:bg-red-500/10 transition-colors cursor-pointer"
>
<Trash2 size={12} aria-hidden="true" />
</button>
</div>
<div className="hidden group-hover:flex group-focus-within:flex items-center shrink-0 pr-1">
<button
type="button"
onClick={() =>
onRestoreArchivedChannel(ch.id, ch.name)
}
disabled={!hasAgent}
aria-label={`Restore archived channel ${ch.name}`}
title={
hasAgent
? "Restore agent"
: "Agent entry missing — delete only"
}
className={`p-1 rounded transition-colors ${
hasAgent
? "text-white/30 hover:text-emerald-400 hover:bg-emerald-500/10 cursor-pointer"
: "text-white/15 cursor-not-allowed"
}`}
>
<RotateCcw size={12} aria-hidden="true" />
</button>
<button
type="button"
onClick={() => onDeleteArchivedChannel(ch.id)}
aria-label={`Permanently delete archived channel ${ch.name}`}
title="Delete permanently"
className="p-1 rounded text-white/30 hover:text-red-400 hover:bg-red-500/10 transition-colors cursor-pointer"
>
<Trash2 size={12} aria-hidden="true" />
</button>
</div>
🤖 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 `@desktop/src/apps/chat/ChannelSidebar.tsx` around lines 1100 - 1130, Update
the archived actions container around the restore and delete buttons to use a
focus-within visibility utility alongside group-hover, so the controls become
visible when the channel row or its descendants receive keyboard focus while
preserving the existing pointer-hover behavior.

Comment on lines +80 to +88
const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
onSend();
}
},
[onSend],
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Handle composer state before routing Enter to onSend.

When mention suggestions are open, Enter sends the unfinished draft instead of selecting the highlighted mention, and IME composition Enter can submit partial text. The keyboard path also bypasses upload gating, while a failed attachment incorrectly counts as sendable merely because the array is non-empty.

Use one canSend predicate for both keyboard and button submission, and process mention/IME keys first.

Proposed fix
   const textareaRef = useRef<HTMLTextAreaElement>(null);
+  const attachmentsReady = pendingAttachments.every(
+    (attachment) =>
+      Boolean(attachment.record) &&
+      !attachment.uploading &&
+      !attachment.error,
+  );
+  const canSend =
+    !isArchived &&
+    attachmentsReady &&
+    (value.trim().length > 0 || pendingAttachments.length > 0);

   const handleKeyDown = useCallback(
     (e: React.KeyboardEvent) => {
+      if (e.nativeEvent.isComposing) return;
+
+      if (mention && mentionCandidates.length > 0 && !showSlash) {
+        if (e.key === "ArrowDown" || e.key === "ArrowUp") {
+          e.preventDefault();
+          const delta = e.key === "ArrowDown" ? 1 : -1;
+          onMentionSelChange(
+            (mentionSel + delta + mentionCandidates.length) %
+              mentionCandidates.length,
+          );
+          return;
+        }
+        if (e.key === "Enter") {
+          e.preventDefault();
+          onInsertMention(undefined);
+          return;
+        }
+        if (e.key === "Escape") {
+          e.preventDefault();
+          onDismissMention();
+          return;
+        }
+      }
+
       if (e.key === "Enter" && !e.shiftKey) {
         e.preventDefault();
-        onSend();
+        if (canSend) onSend();
       }
     },
-    [onSend],
+    [
+      canSend,
+      mention,
+      mentionCandidates,
+      mentionSel,
+      onDismissMention,
+      onInsertMention,
+      onMentionSelChange,
+      onSend,
+      showSlash,
+    ],
   );
...
-              disabled={
-                (!value.trim() && pendingAttachments.length === 0) ||
-                isArchived ||
-                pendingAttachments.some((a) => a.uploading)
-              }
+              disabled={!canSend}

Also applies to: 119-143, 179-186

🤖 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 `@desktop/src/apps/chat/MessageInput.tsx` around lines 80 - 88, Update
MessageInput’s handleKeyDown and button submission flow to share a single
canSend predicate that accounts for valid uploads rather than merely a non-empty
attachment array. Process mention suggestion selection and IME composition Enter
before routing Enter to onSend, and ensure keyboard submission uses the same
upload gating as the button.

Comment on lines +380 to +386
const prev = i > 0 ? messages[i - 1] : undefined;
const showAuthor = !prev || prev.author_id !== msg.author_id;
const prevDay = prev
? new Date(toMs(prev.created_at)).toDateString()
: null;
const currDay = new Date(toMs(msg.created_at)).toDateString();
const showDaySeparator = !prev || prevDay !== currDay;

Copy link
Copy Markdown

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

Show the author again after a day separator.

Consecutive messages from the same author remain grouped across midnight, leaving the first message under the new date without its avatar or name.

Proposed fix
           const isAgent = msg.author_type === "agent";
           const prev = i > 0 ? messages[i - 1] : undefined;
-          const showAuthor = !prev || prev.author_id !== msg.author_id;
           const prevDay = prev
             ? new Date(toMs(prev.created_at)).toDateString()
             : null;
           const currDay = new Date(toMs(msg.created_at)).toDateString();
           const showDaySeparator = !prev || prevDay !== currDay;
+          const showAuthor =
+            !prev || prev.author_id !== msg.author_id || showDaySeparator;
📝 Committable suggestion

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

Suggested change
const prev = i > 0 ? messages[i - 1] : undefined;
const showAuthor = !prev || prev.author_id !== msg.author_id;
const prevDay = prev
? new Date(toMs(prev.created_at)).toDateString()
: null;
const currDay = new Date(toMs(msg.created_at)).toDateString();
const showDaySeparator = !prev || prevDay !== currDay;
const prev = i > 0 ? messages[i - 1] : undefined;
const prevDay = prev
? new Date(toMs(prev.created_at)).toDateString()
: null;
const currDay = new Date(toMs(msg.created_at)).toDateString();
const showDaySeparator = !prev || prevDay !== currDay;
const showAuthor =
!prev || prev.author_id !== msg.author_id || showDaySeparator;
🤖 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 `@desktop/src/apps/chat/MessageList.tsx` around lines 380 - 386, Update the
message grouping logic in MessageList so showAuthor is true when
showDaySeparator is true, even if the previous message has the same author;
ensure the first message of each new day displays the author identity alongside
the day separator.

Comment on lines +1584 to +1586
selectedChannel={selectedChannel}
onSelectChannel={setSelectedChannel}
unread={unread}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Clear bus mode directly when selecting a channel.

If bus mode preserves channel A, clicking channel A again makes setSelectedChannel a no-op, so the effect never clears busSelected and the bus viewer remains open.

Proposed fix
-      onSelectChannel={setSelectedChannel}
+      onSelectChannel={(id) => {
+        setBusSelected(null);
+        setSelectedChannel(id);
+      }}
📝 Committable suggestion

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

Suggested change
selectedChannel={selectedChannel}
onSelectChannel={setSelectedChannel}
unread={unread}
selectedChannel={selectedChannel}
onSelectChannel={(id) => {
setBusSelected(null);
setSelectedChannel(id);
}}
unread={unread}
🤖 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 `@desktop/src/apps/MessagesApp.tsx` around lines 1584 - 1586, Update the
channel-selection handler passed as onSelectChannel in MessagesApp so selecting
the currently active channel also clears busSelected, rather than relying only
on an effect triggered by selectedChannel changes. Preserve normal
setSelectedChannel behavior for channel changes while ensuring the bus viewer
closes when reselecting the preserved channel.

Comment thread desktop/src/apps/MessagesApp.tsx
Comment on lines +1933 to +1950
<MessageInput
value={input}
onChange={(v) => !isCurrentArchived && handleInputChange(v)}
onSend={sendMessage}
channel={currentChannel}
isArchived={isCurrentArchived}
isMobile={isMobile}
keyboardInset={keyboardInset}
slashCommands={slashCommands}
showSlash={showSlash}
slashQuery={slashQuery}
slashAgent={slashAgent}
mention={mention}
mentionCandidates={mentionCandidates}
mentionSel={mentionSel}
onMentionSelChange={setMentionSel}
onInsertMention={insertMention}
onDismissMention={() => setMention(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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Reconnect the parent textarea ref.

MessagesApp still uses inputRef for focus, cursor-aware mentions, and resizing, but MessageInput owns an unrelated internal ref and receives no parent ref. Prefill focus now no-ops, and mention detection falls back to assuming the cursor is at the end. Pass and attach the parent ref to the extracted textarea.

🤖 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 `@desktop/src/apps/MessagesApp.tsx` around lines 1933 - 1950, Update the
MessageInput usage in MessagesApp to pass the existing inputRef into the
extracted component, and attach that forwarded ref to MessageInput’s underlying
textarea. Preserve the current ref’s use for focus, cursor-aware mentions, and
resizing while eliminating the unrelated internal-only ref.

hognek added a commit to hognek/tinyagentos that referenced this pull request Jul 18, 2026
- Add missing dependency array to useImperativeHandle (was re-creating handle every render)
- Remove mount-time auto-scroll useEffect that desyncs autoScrollRef/atBottom
- Restore message-list-drop-target class lost during component extraction

All three issues from Kilo review resolved. Parent MessagesApp already handles
initial scroll via autoScrollRef + scrollToBottom() in its own useEffect.
@hognek

hognek commented Jul 18, 2026

Copy link
Copy Markdown
Contributor Author

Kilo bot-fix pushed (commit e0fd90f):

All 3 Kilo findings on MessageList.tsx resolved:

  1. WARNING (line 202) — Mount-time auto-scroll desyncs autoScrollRef/atBottom: Removed the redundant useEffect. Parent MessagesApp already handles initial scroll via autoScrollRef + scrollToBottom() in its own [messages] effect.

  2. SUGGESTION (line 195) — useImperativeHandle without dep array: Added [] dependency array.

  3. SUGGESTION (~line 329) — Missing message-list-drop-target class: Restored class on the scroll container div.

Vitest suite running (all passing so far). Please re-review.

- Add missing dependency array to useImperativeHandle (was re-creating handle every render)
- Remove mount-time auto-scroll useEffect that desyncs autoScrollRef/atBottom
- Restore message-list-drop-target class lost during component extraction

All three issues from Kilo review resolved. Parent MessagesApp already handles
initial scroll via autoScrollRef + scrollToBottom() in its own useEffect.
@hognek
hognek force-pushed the feat/657-messagesapp-extract branch from e0fd90f to b00c142 Compare July 18, 2026 17:23
@jaylfc
jaylfc merged commit 0f51a5f into jaylfc:dev Jul 19, 2026
9 checks passed
jaylfc pushed a commit that referenced this pull request Jul 19, 2026
* test(desktop): add MessageList component tests (Vitest + RTL)

Add comprehensive test suite for the extracted MessageList component
(PR #1877, upstream #657). Covers:

- Empty state / no-channel placeholder
- Message rendering with author display
- Day separators and new-message divider
- Deleted message tombstones
- Editing state (MessageEditor)
- Pending/streaming/error message states
- Edited indicator
- Reactions (ReactionBar)
- Thread indicators
- Dead agent badges (inactive/removed)
- Scroll-to-bottom button
- Channel header variants (topic/group/dm)
- Canvas attachment buttons
- Pin request affordance
- Typing footer
- Pinned messages popover
- Mobile keyboard inset
- Imperative ref handle (scrollToBottom)

49 tests, all passing.

* fix(desktop): address 6 Kilo findings on MessageList tests

- Replace vacuous data-reaction/data-typing selectors with real DOM checks
- Make day-label assertions locale-independent (query selectors)
- Scope reaction/member-count text matchers to avoid ambiguity

---------

Co-authored-by: Hogne <227774406+hognek@users.noreply.github.com>
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