diff --git a/desktop/src/features/messages/ui/ComposerMentionOverlay.tsx b/desktop/src/features/messages/ui/ComposerMentionOverlay.tsx
new file mode 100644
index 000000000..dd28334ea
--- /dev/null
+++ b/desktop/src/features/messages/ui/ComposerMentionOverlay.tsx
@@ -0,0 +1,76 @@
+import type React from "react";
+
+type Segment = { type: "text" | "mention" | "channel"; value: string };
+
+const MENTION_OR_CHANNEL_RE = /@\S+|#[a-zA-Z0-9][\w-]*/g;
+
+function parseSegments(text: string): Segment[] {
+ const segments: Segment[] = [];
+ let lastIndex = 0;
+
+ for (const match of text.matchAll(MENTION_OR_CHANNEL_RE)) {
+ const matchStart = match.index;
+ if (matchStart > lastIndex) {
+ segments.push({ type: "text", value: text.slice(lastIndex, matchStart) });
+ }
+
+ const value = match[0];
+ segments.push({
+ type: value.startsWith("@") ? "mention" : "channel",
+ value,
+ });
+ lastIndex = matchStart + value.length;
+ }
+
+ if (lastIndex < text.length) {
+ segments.push({ type: "text", value: text.slice(lastIndex) });
+ }
+
+ return segments;
+}
+
+type ComposerMentionOverlayProps = {
+ content: string;
+ scrollTop: number;
+};
+
+export function ComposerMentionOverlay({
+ content,
+ scrollTop,
+}: ComposerMentionOverlayProps) {
+ const segments = parseSegments(content);
+
+ return (
+
+ {
+ segments.reduce<{ offset: number; nodes: React.ReactNode[] }>(
+ (acc, segment) => {
+ const key = `${acc.offset}`;
+ if (segment.type === "mention" || segment.type === "channel") {
+ acc.nodes.push(
+
+ {segment.value}
+ ,
+ );
+ } else {
+ acc.nodes.push(
+
+ {segment.value}
+ ,
+ );
+ }
+ acc.offset += segment.value.length;
+ return acc;
+ },
+ { offset: 0, nodes: [] },
+ ).nodes
+ }
+
+ );
+}
diff --git a/desktop/src/features/messages/ui/MessageComposer.tsx b/desktop/src/features/messages/ui/MessageComposer.tsx
index 3aea663f0..3e5f2f003 100644
--- a/desktop/src/features/messages/ui/MessageComposer.tsx
+++ b/desktop/src/features/messages/ui/MessageComposer.tsx
@@ -11,6 +11,7 @@ import {
import { Button } from "@/shared/ui/button";
import { Textarea } from "@/shared/ui/textarea";
import { ChannelAutocomplete } from "./ChannelAutocomplete";
+import { ComposerMentionOverlay } from "./ComposerMentionOverlay";
import {
MentionAutocomplete,
type MentionSuggestion,
@@ -69,6 +70,7 @@ export function MessageComposer({
const pendingSelectionRef = React.useRef(null);
const draftSelectionRef = React.useRef({ end: 0, start: 0 });
const [isEmojiPickerOpen, setIsEmojiPickerOpen] = React.useState(false);
+ const [composerScrollTop, setComposerScrollTop] = React.useState(0);
const lineHeightRef = React.useRef(null);
// Keep contentRef in sync — no extra re-render, just a ref assignment.
@@ -101,6 +103,7 @@ export function MessageComposer({
setPendingImeta([]);
setUploadState({ status: "idle" });
setIsEmojiPickerOpen(false);
+ setComposerScrollTop(0);
mentions.clearMentions();
channelLinks.clearChannels();
draftSelectionRef.current = { end: 0, start: 0 };
@@ -309,6 +312,13 @@ export function MessageComposer({
[onUploaded],
);
+ const handleScroll = React.useCallback(
+ (event: React.UIEvent) => {
+ setComposerScrollTop(event.currentTarget.scrollTop);
+ },
+ [],
+ );
+
const submitMessage = React.useCallback(async () => {
const trimmed = contentRef.current.trim();
const currentPendingImeta = pendingImetaRef.current;
@@ -556,29 +566,41 @@ export function MessageComposer({
) : null}
-