diff --git a/web/src/features/messages/MessageBubble.tsx b/web/src/features/messages/MessageBubble.tsx
index 3a2d8df3..f1958e90 100644
--- a/web/src/features/messages/MessageBubble.tsx
+++ b/web/src/features/messages/MessageBubble.tsx
@@ -144,8 +144,19 @@ export function MessageRow({
.catch(() => onError?.('重试发送失败'));
};
+ // In selectMode, the WHOLE row toggles selection (not just the checkbox) -- confirmed by the user
+ // that tapping only the small checkbox target is the wrong UX for a bulk-select list. A recalled
+ // message renders a spacer instead of a checkbox (see below) and must stay unselectable, so this
+ // guards the same way.
+ const rowSelectable = selectMode && !isRecalled(msg) && typeof msg.seq === 'number';
+ const toggleThisRow = () => rowSelectable && onToggleSelect?.(msg.seq as number);
+
return (
-
+
{selectMode ? (
// A recalled message (content nulled out, rendered as the "⚠ 消息已删除" placeholder below)
// has nothing real to forward/favorite -- an empty flex-none spacer of the same size keeps
@@ -154,9 +165,11 @@ export function MessageRow({
isRecalled(msg) ? (
) : (
-