-
+
Raw ACP
JSON-RPC payloads
@@ -40,7 +40,7 @@ export function RawEventRail({ events }: { events: ObserverEvent[] }) {
#{event.seq} {" "}
{describeRawEvent(event)}
-
+
{JSON.stringify(event.payload, null, 2)}
diff --git a/desktop/src/features/agents/ui/RelayDirectorySection.tsx b/desktop/src/features/agents/ui/RelayDirectorySection.tsx
index 18e6ea358d..d1ac88c620 100644
--- a/desktop/src/features/agents/ui/RelayDirectorySection.tsx
+++ b/desktop/src/features/agents/ui/RelayDirectorySection.tsx
@@ -95,7 +95,7 @@ export function RelayDirectorySection({
className="w-full border-collapse text-left text-sm"
data-testid="relay-directory-table"
>
-
+
Agent
Status
@@ -121,7 +121,7 @@ export function RelayDirectorySection({
diff --git a/desktop/src/features/agents/ui/RespondToField.tsx b/desktop/src/features/agents/ui/RespondToField.tsx
index 055022a8d2..dd36c52c65 100644
--- a/desktop/src/features/agents/ui/RespondToField.tsx
+++ b/desktop/src/features/agents/ui/RespondToField.tsx
@@ -211,7 +211,7 @@ function AllowlistPicker({
>
Allowed pubkeys
-
+
{allowlist.length} selected
@@ -241,7 +241,7 @@ function AllowlistPicker({
{allowlist.map((pubkey) => (
diff --git a/desktop/src/features/agents/ui/TeamDialog.tsx b/desktop/src/features/agents/ui/TeamDialog.tsx
index 2e2eb2856b..f91e537e1c 100644
--- a/desktop/src/features/agents/ui/TeamDialog.tsx
+++ b/desktop/src/features/agents/ui/TeamDialog.tsx
@@ -422,7 +422,7 @@ export function TeamDialog({
/>
{persona.displayName}
diff --git a/desktop/src/features/agents/ui/TeamsSection.tsx b/desktop/src/features/agents/ui/TeamsSection.tsx
index 145fc22f4d..4d0e4b66d5 100644
--- a/desktop/src/features/agents/ui/TeamsSection.tsx
+++ b/desktop/src/features/agents/ui/TeamsSection.tsx
@@ -172,7 +172,7 @@ export function TeamsSection({
) : null}
{team.version ? (
-
+
v{team.version}
) : null}
@@ -199,13 +199,13 @@ export function TeamsSection({
{visible.map((persona) => (
))}
{overflow > 0 ? (
-
+
+{overflow}
) : null}
diff --git a/desktop/src/features/channels/ui/AddChannelBotPersonasSection.tsx b/desktop/src/features/channels/ui/AddChannelBotPersonasSection.tsx
index f24b358c53..e4e6ef9a5d 100644
--- a/desktop/src/features/channels/ui/AddChannelBotPersonasSection.tsx
+++ b/desktop/src/features/channels/ui/AddChannelBotPersonasSection.tsx
@@ -49,7 +49,7 @@ function SelectionChipButton({
{persona.displayName}
{isInChannel ? (
-
+
✓ Already in this channel
) : null}
-
+
{promptPreview(persona.systemPrompt)}
diff --git a/desktop/src/features/channels/ui/AddChannelBotTeamsSection.tsx b/desktop/src/features/channels/ui/AddChannelBotTeamsSection.tsx
index acb12eb0c5..2879902c73 100644
--- a/desktop/src/features/channels/ui/AddChannelBotTeamsSection.tsx
+++ b/desktop/src/features/channels/ui/AddChannelBotTeamsSection.tsx
@@ -124,7 +124,7 @@ export function AddChannelBotTeamsSection({
{inChannelCount > 0 ? (
{team.name}
{team.description ? (
-
+
{team.description}
) : null}
@@ -158,10 +158,10 @@ export function AddChannelBotTeamsSection({
>
-
+
{persona.displayName}
{personaInChannel ? (
diff --git a/desktop/src/features/channels/ui/AgentSessionThreadPanel.tsx b/desktop/src/features/channels/ui/AgentSessionThreadPanel.tsx
index ae633bbf30..5b72e5b92c 100644
--- a/desktop/src/features/channels/ui/AgentSessionThreadPanel.tsx
+++ b/desktop/src/features/channels/ui/AgentSessionThreadPanel.tsx
@@ -79,10 +79,7 @@ export function AgentSessionThreadPanel({
const agentHeaderActions = (
{isLive && isWorking ? (
-
+
Live
@@ -92,7 +89,7 @@ export function AgentSessionThreadPanel({
{
diff --git a/desktop/src/features/channels/ui/BotActivityBar.tsx b/desktop/src/features/channels/ui/BotActivityBar.tsx
index 3fc609a8bc..d9139935f6 100644
--- a/desktop/src/features/channels/ui/BotActivityBar.tsx
+++ b/desktop/src/features/channels/ui/BotActivityBar.tsx
@@ -184,8 +184,8 @@ export function BotActivityComposerAction({
className={cn(
"border border-background",
isInline
- ? "!h-[18px] !w-[18px] shadow-xs ring-1 ring-primary/25 text-[7px]"
- : "!h-5 !w-5 text-[8px]",
+ ? "!h-[18px] !w-[18px] shadow-xs ring-1 ring-primary/25 text-3xs"
+ : "!h-5 !w-5 text-3xs",
)}
displayName={agent.name}
key={agent.pubkey}
@@ -193,7 +193,7 @@ export function BotActivityComposerAction({
))}
{typingAgents.length > 2 ? (
-
+
+{typingAgents.length - 2}
) : null}
@@ -240,7 +240,7 @@ export function BotActivityComposerAction({
>
{agent.name}
diff --git a/desktop/src/features/channels/ui/ChannelCanvas.tsx b/desktop/src/features/channels/ui/ChannelCanvas.tsx
index 8b379f3674..314d31ee11 100644
--- a/desktop/src/features/channels/ui/ChannelCanvas.tsx
+++ b/desktop/src/features/channels/ui/ChannelCanvas.tsx
@@ -121,11 +121,7 @@ export function ChannelCanvas({
className="rounded-2xl border border-border/70 bg-muted/20 px-4 py-3"
data-testid="channel-canvas-content"
>
-
+
) : (
diff --git a/desktop/src/features/channels/ui/ChannelMemberInviteCard.tsx b/desktop/src/features/channels/ui/ChannelMemberInviteCard.tsx
index e787a31920..ae6c2eb5fb 100644
--- a/desktop/src/features/channels/ui/ChannelMemberInviteCard.tsx
+++ b/desktop/src/features/channels/ui/ChannelMemberInviteCard.tsx
@@ -142,7 +142,7 @@ export function ChannelMemberInviteCard({
Add members
{inviteTargets.length > 0 ? (
-
+
{inviteTargets.length} selected
) : null}
@@ -168,7 +168,7 @@ export function ChannelMemberInviteCard({
{selectedInvitees.map((invitee) => (
diff --git a/desktop/src/features/channels/ui/ChannelScreenHeader.tsx b/desktop/src/features/channels/ui/ChannelScreenHeader.tsx
index dce074bb6a..cda079449d 100644
--- a/desktop/src/features/channels/ui/ChannelScreenHeader.tsx
+++ b/desktop/src/features/channels/ui/ChannelScreenHeader.tsx
@@ -105,7 +105,7 @@ export function ChannelScreenHeader({
) : (
@@ -170,7 +170,7 @@ function DmHeaderParticipantStack({
style={{ zIndex: stackItemCount }}
>
- +{hiddenCount}
+ +{hiddenCount}
) : null}
diff --git a/desktop/src/features/channels/ui/QuickBotBar.tsx b/desktop/src/features/channels/ui/QuickBotBar.tsx
index 3d4d243857..bacd5035f5 100644
--- a/desktop/src/features/channels/ui/QuickBotBar.tsx
+++ b/desktop/src/features/channels/ui/QuickBotBar.tsx
@@ -75,7 +75,7 @@ export function QuickBotBar({ personas, pending, onAdd }: QuickBotBarProps) {
src={rewriteRelayUrl(persona.avatarUrl)}
/>
) : (
-
+
{initials}
)}
diff --git a/desktop/src/features/forum/ui/ForumPostCard.tsx b/desktop/src/features/forum/ui/ForumPostCard.tsx
index 2ce878aee7..4bd5f0f087 100644
--- a/desktop/src/features/forum/ui/ForumPostCard.tsx
+++ b/desktop/src/features/forum/ui/ForumPostCard.tsx
@@ -116,7 +116,7 @@ export function ForumPostCard({
{feedHeadline(item)}
-
+
{item.channelName ? (
-
+
#{item.channelName}
) : null}
-
+
{formatRelativeTime(item.createdAt)}
diff --git a/desktop/src/features/home/ui/InboxDetailPane.tsx b/desktop/src/features/home/ui/InboxDetailPane.tsx
index 8b504744fc..79ca0ee4bb 100644
--- a/desktop/src/features/home/ui/InboxDetailPane.tsx
+++ b/desktop/src/features/home/ui/InboxDetailPane.tsx
@@ -313,7 +313,7 @@ export function InboxDetailPane({
{isThreadContextLoading ? (
-
+
Loading context...
) : null}
diff --git a/desktop/src/features/home/ui/InboxListPane.tsx b/desktop/src/features/home/ui/InboxListPane.tsx
index ac3e59b1b1..fa51d46a80 100644
--- a/desktop/src/features/home/ui/InboxListPane.tsx
+++ b/desktop/src/features/home/ui/InboxListPane.tsx
@@ -67,7 +67,7 @@ export function InboxListPane({
{item.isActionRequired ? (
-
+
Needs action
) : null}
@@ -182,14 +182,13 @@ export function InboxListPane({
content={item.preview}
interactive={false}
mentionNames={item.mentionNames}
- tight
/>
diff --git a/desktop/src/features/home/ui/InboxMessageRow.tsx b/desktop/src/features/home/ui/InboxMessageRow.tsx
index 8852488b94..81062f109f 100644
--- a/desktop/src/features/home/ui/InboxMessageRow.tsx
+++ b/desktop/src/features/home/ui/InboxMessageRow.tsx
@@ -131,7 +131,6 @@ export function InboxMessageRow({
className="max-w-full text-left text-sm text-foreground"
content={message.content}
mentionNames={message.mentionNames}
- tight
/>
{isAgent ? (
-
+
bot
) : null}
-
+
{formatRelativeTime(note.createdAt)}
-
+
diff --git a/desktop/src/features/huddle/components/HuddleIndicator.tsx b/desktop/src/features/huddle/components/HuddleIndicator.tsx
index 0d29342ced..075318803e 100644
--- a/desktop/src/features/huddle/components/HuddleIndicator.tsx
+++ b/desktop/src/features/huddle/components/HuddleIndicator.tsx
@@ -291,7 +291,7 @@ export function HuddleIndicator({
{/* Participant count badge */}
{participantCount > 0 && (
-
+
{participantCount}
)}
diff --git a/desktop/src/features/huddle/components/MicControls.tsx b/desktop/src/features/huddle/components/MicControls.tsx
index e5fc0c7b43..129829f53e 100644
--- a/desktop/src/features/huddle/components/MicControls.tsx
+++ b/desktop/src/features/huddle/components/MicControls.tsx
@@ -217,7 +217,7 @@ export function MicControls({
className={cn("h-3 w-3 shrink-0", !isPttMode && "invisible")}
/>
Push to Talk
-
+
{pushToTalkShortcut}
@@ -465,7 +465,7 @@ export function DeviceList({
})}
{showChangeHint && (
-
+
Change takes effect on next huddle
)}
diff --git a/desktop/src/features/huddle/components/ParticipantList.tsx b/desktop/src/features/huddle/components/ParticipantList.tsx
index 2fe565a816..33ac281d1d 100644
--- a/desktop/src/features/huddle/components/ParticipantList.tsx
+++ b/desktop/src/features/huddle/components/ParticipantList.tsx
@@ -54,7 +54,7 @@ export function HuddleParticipantsControl({
>
{participants.length > 1 && (
-
+
{participants.length}
)}
@@ -90,7 +90,7 @@ export function HuddleParticipantsControl({
avatarUrl={profile.avatarUrl ?? null}
label={profile.displayName || pubkey.slice(0, 6)}
className={cn(
- "h-8 w-8 rounded-full text-[10px]",
+ "h-8 w-8 rounded-full text-2xs",
isActive &&
"ring-2 ring-green-500 ring-offset-1 ring-offset-background",
)}
@@ -150,7 +150,7 @@ function HexAvatar({
role="img"
className={cn(
"flex items-center justify-center rounded-full font-semibold shadow-xs",
- size === "lg" ? "h-8 w-8 text-[10px]" : "h-7 w-7 text-[9px]",
+ size === "lg" ? "h-8 w-8 text-2xs" : "h-7 w-7 text-2xs",
isActive &&
"ring-2 ring-green-500 ring-offset-1 ring-offset-background",
)}
diff --git a/desktop/src/features/messages/lib/mentionHighlightExtension.ts b/desktop/src/features/messages/lib/mentionHighlightExtension.ts
index 53f7636744..cee06b6a4a 100644
--- a/desktop/src/features/messages/lib/mentionHighlightExtension.ts
+++ b/desktop/src/features/messages/lib/mentionHighlightExtension.ts
@@ -5,7 +5,7 @@ import { Decoration, DecorationSet } from "@tiptap/pm/view";
export const mentionHighlightKey = new PluginKey("mentionHighlight");
/**
- * TipTap extension that applies inline `mention-highlight` decorations
+ * TipTap extension that applies inline `mention-chip` decorations
* to `@Name` and `#channel-name` patterns in the document.
*
* Accepts `names` (display names) and `channelNames` storage options.
@@ -264,14 +264,14 @@ function buildDecorations(
node.text,
pos,
mentionPatterns,
- "mention-highlight",
+ "mention-chip",
);
addMatchesForPatterns(
decorations,
node.text,
pos,
agentMentionPatterns,
- "mention-highlight agent-mention-highlight",
+ "mention-chip agent-mention-highlight",
{ hideMentionPrefix: true },
);
addMatchesForPatterns(
@@ -279,7 +279,7 @@ function buildDecorations(
node.text,
pos,
channelPatterns,
- "mention-highlight",
+ "mention-chip",
);
});
diff --git a/desktop/src/features/messages/lib/useRichTextEditor.ts b/desktop/src/features/messages/lib/useRichTextEditor.ts
index 0fd30b897a..d54e3c8a23 100644
--- a/desktop/src/features/messages/lib/useRichTextEditor.ts
+++ b/desktop/src/features/messages/lib/useRichTextEditor.ts
@@ -11,6 +11,8 @@ import { Selection, TextSelection } from "@tiptap/pm/state";
import { isMacPlatform } from "@/shared/lib/platform";
import type { CustomEmoji } from "@/shared/lib/remarkCustomEmoji";
+import { MESSAGE_MARKDOWN_CLASS } from "@/shared/ui/mentionChip";
+
import {
MentionHighlightExtension,
mentionHighlightKey,
@@ -325,8 +327,7 @@ export function useRichTextEditor({
attributes: {
autocapitalize: "none",
autocorrect: "off",
- class:
- "min-h-0 resize-none overflow-y-hidden border-0 bg-transparent px-0 py-0 text-sm leading-6 text-foreground md:leading-6 shadow-none focus-visible:ring-0 caret-foreground outline-hidden prose-sm max-w-none",
+ class: `${MESSAGE_MARKDOWN_CLASS} min-h-0 resize-none overflow-y-hidden border-0 bg-transparent px-0 py-0 text-sm leading-relaxed text-foreground shadow-none focus-visible:ring-0 caret-foreground outline-hidden max-w-none`,
"data-testid": "message-input",
spellcheck: "false",
},
diff --git a/desktop/src/features/messages/ui/ComposerAttachments.tsx b/desktop/src/features/messages/ui/ComposerAttachments.tsx
index 7bfc0b8146..c00ec5ffb6 100644
--- a/desktop/src/features/messages/ui/ComposerAttachments.tsx
+++ b/desktop/src/features/messages/ui/ComposerAttachments.tsx
@@ -116,7 +116,7 @@ export const ComposerAttachments = React.memo(function ComposerAttachments({
>
-
+
{label}
diff --git a/desktop/src/features/messages/ui/DayDivider.tsx b/desktop/src/features/messages/ui/DayDivider.tsx
index e04fd0edbe..a76b8a7f1e 100644
--- a/desktop/src/features/messages/ui/DayDivider.tsx
+++ b/desktop/src/features/messages/ui/DayDivider.tsx
@@ -6,7 +6,7 @@ export function DayDivider({ label }: { label: string }) {
data-testid="message-timeline-day-divider"
data-day-label={label}
>
-
+
{label}
diff --git a/desktop/src/features/messages/ui/DiffViewer.css b/desktop/src/features/messages/ui/DiffViewer.css
index d6747173dd..dcc97fb788 100644
--- a/desktop/src/features/messages/ui/DiffViewer.css
+++ b/desktop/src/features/messages/ui/DiffViewer.css
@@ -52,7 +52,7 @@
}
.buzz-diff-theme .diff {
- font-size: 12px;
+ font-size: 0.75rem;
}
.buzz-diff-theme .diff td {
@@ -68,7 +68,7 @@
padding: 0.125rem 0.5rem;
border-right: 1px solid hsl(var(--border) / 0.65);
color: hsl(var(--muted-foreground));
- font-size: 11px;
+ font-size: 0.6875rem;
}
.buzz-diff-theme .buzz-diff-code {
@@ -86,7 +86,7 @@
padding: 0.2rem 0.75rem;
background: hsl(var(--muted) / 0.35);
color: hsl(var(--muted-foreground));
- font-size: 11px;
+ font-size: 0.6875rem;
}
.buzz-diff-theme .diff-gutter-omit::before {
diff --git a/desktop/src/features/messages/ui/DiffViewer.tsx b/desktop/src/features/messages/ui/DiffViewer.tsx
index 4c9e501e45..239fd02110 100644
--- a/desktop/src/features/messages/ui/DiffViewer.tsx
+++ b/desktop/src/features/messages/ui/DiffViewer.tsx
@@ -36,7 +36,7 @@ function FileChangeBadge({
return (
{showFileHeader ? (
-
+
{label}
-
+
{DIFF_TYPE_LABELS[diffType]}
diff --git a/desktop/src/features/messages/ui/MessageComposer.tsx b/desktop/src/features/messages/ui/MessageComposer.tsx
index 3d7bd2e64a..8c02ae079e 100644
--- a/desktop/src/features/messages/ui/MessageComposer.tsx
+++ b/desktop/src/features/messages/ui/MessageComposer.tsx
@@ -849,7 +849,7 @@ export function MessageComposer({
data-testid="edit-target"
>
-
+
Editing message
@@ -872,7 +872,7 @@ export function MessageComposer({
data-testid="reply-target"
>
-
+
Replying to {replyTarget.author}
diff --git a/desktop/src/features/messages/ui/MessageHeader.tsx b/desktop/src/features/messages/ui/MessageHeader.tsx
new file mode 100644
index 0000000000..c9a5f219ba
--- /dev/null
+++ b/desktop/src/features/messages/ui/MessageHeader.tsx
@@ -0,0 +1,50 @@
+import type * as React from "react";
+
+import { cn } from "@/shared/lib/cn";
+
+type MessageHeaderRowProps = {
+ children: React.ReactNode;
+ className?: string;
+};
+
+export function MessageHeaderRow({
+ children,
+ className,
+}: MessageHeaderRowProps) {
+ return (
+
+ {children}
+
+ );
+}
+
+type MessageAuthorTextProps = {
+ as?: "div" | "h3" | "span";
+ children: React.ReactNode;
+ className?: string;
+ hoverUnderline?: boolean;
+};
+
+export function MessageAuthorText({
+ as: Component = "span",
+ children,
+ className,
+ hoverUnderline = false,
+}: MessageAuthorTextProps) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/desktop/src/features/messages/ui/MessageRow.tsx b/desktop/src/features/messages/ui/MessageRow.tsx
index 4c3ead6c5f..d5ab5000dd 100644
--- a/desktop/src/features/messages/ui/MessageRow.tsx
+++ b/desktop/src/features/messages/ui/MessageRow.tsx
@@ -20,6 +20,7 @@ import {
import { Markdown } from "@/shared/ui/markdown";
import type { VideoReviewContext } from "@/shared/ui/VideoPlayer";
import { MessageActionBar } from "./MessageActionBar";
+import { MessageAuthorText, MessageHeaderRow } from "./MessageHeader";
import { MessageTimestamp } from "./MessageTimestamp";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip";
@@ -193,7 +194,7 @@ export const MessageRow = React.memo(
);
@@ -213,7 +213,7 @@ export const MessageRow = React.memo(
const isThreadReplyLayout = layoutVariant === "thread-reply";
const guideBleedPx = isThreadReplyLayout ? 4 : 0;
- const avatarSizeClass = "!h-9 !w-9";
+ const avatarSizeClass = "!h-10 !w-10";
const avatarButtonRadiusClass = "rounded-full";
const respondToDotColor =
@@ -250,13 +250,9 @@ export const MessageRow = React.memo(
);
const authorNode = message.pubkey ? (
-
- {message.author}
-
+
{message.author}
) : (
-
- {message.author}
-
+
{message.author}
);
const actionBarNode = (
@@ -414,8 +410,8 @@ export const MessageRow = React.memo(
) : (
{avatarNode}
)}
-
-
+
+
{message.pubkey ? (
{authorNode}
@@ -439,7 +435,7 @@ export const MessageRow = React.memo(
{message.personaDisplayName}
) : null}
-
+
{messageBodyNode}
>
@@ -464,8 +460,8 @@ export const MessageRow = React.memo(
) : (
{avatarNode}
)}
-
-
+
+
{message.pubkey ? (
{authorNode}
@@ -489,7 +485,7 @@ export const MessageRow = React.memo(
{message.personaDisplayName}
) : null}
-
+
{messageBodyNode}
>
diff --git a/desktop/src/features/messages/ui/MessageThreadPanel.tsx b/desktop/src/features/messages/ui/MessageThreadPanel.tsx
index 9307540eba..358ebbce99 100644
--- a/desktop/src/features/messages/ui/MessageThreadPanel.tsx
+++ b/desktop/src/features/messages/ui/MessageThreadPanel.tsx
@@ -514,7 +514,7 @@ export function MessageThreadPanel({
{!isAtBottom ? (
scrollToBottom("smooth")}
size="sm"
diff --git a/desktop/src/features/messages/ui/MessageThreadSummaryRow.tsx b/desktop/src/features/messages/ui/MessageThreadSummaryRow.tsx
index 8a9e1a5bea..7b650b8b04 100644
--- a/desktop/src/features/messages/ui/MessageThreadSummaryRow.tsx
+++ b/desktop/src/features/messages/ui/MessageThreadSummaryRow.tsx
@@ -34,7 +34,7 @@ function ParticipantAvatar({
>
diff --git a/desktop/src/features/messages/ui/MessageTimeline.tsx b/desktop/src/features/messages/ui/MessageTimeline.tsx
index b232822509..f1b78cbe5a 100644
--- a/desktop/src/features/messages/ui/MessageTimeline.tsx
+++ b/desktop/src/features/messages/ui/MessageTimeline.tsx
@@ -279,7 +279,7 @@ export const MessageTimeline = React.memo(function MessageTimeline({
)}
>
{
scrollToBottom("smooth");
diff --git a/desktop/src/features/messages/ui/MessageTimestamp.tsx b/desktop/src/features/messages/ui/MessageTimestamp.tsx
index e8285d173f..3a28508f7c 100644
--- a/desktop/src/features/messages/ui/MessageTimestamp.tsx
+++ b/desktop/src/features/messages/ui/MessageTimestamp.tsx
@@ -11,7 +11,7 @@ export function MessageTimestamp({
return (
-
+
{time}
diff --git a/desktop/src/features/messages/ui/SystemMessageRow.tsx b/desktop/src/features/messages/ui/SystemMessageRow.tsx
index eab1983f5b..d0ca23b796 100644
--- a/desktop/src/features/messages/ui/SystemMessageRow.tsx
+++ b/desktop/src/features/messages/ui/SystemMessageRow.tsx
@@ -133,7 +133,7 @@ function SystemMessageAvatar({
const avatar = (
@@ -169,12 +169,12 @@ function SystemMessageAvatar({
>
@@ -345,7 +345,7 @@ export const SystemMessageRow = React.memo(function SystemMessageRow({
/>
-
-
+
{description.action}
diff --git a/desktop/src/features/messages/ui/TimelineMessageList.tsx b/desktop/src/features/messages/ui/TimelineMessageList.tsx
index 64a900082c..9f66b161e0 100644
--- a/desktop/src/features/messages/ui/TimelineMessageList.tsx
+++ b/desktop/src/features/messages/ui/TimelineMessageList.tsx
@@ -84,7 +84,6 @@ export const TimelineMessageList = React.memo(function TimelineMessageList({
isSendingVideoReviewComment = false,
onSendVideoReviewComment,
onToggleReaction,
- personaLookup,
profiles,
searchActiveMessageId = null,
searchMatchingMessageIds,
@@ -184,10 +183,8 @@ export const TimelineMessageList = React.memo(function TimelineMessageList({
{footer}
diff --git a/desktop/src/features/messages/ui/TypingIndicatorRow.tsx b/desktop/src/features/messages/ui/TypingIndicatorRow.tsx
index 890407153b..009392409e 100644
--- a/desktop/src/features/messages/ui/TypingIndicatorRow.tsx
+++ b/desktop/src/features/messages/ui/TypingIndicatorRow.tsx
@@ -113,8 +113,8 @@ export function TypingIndicatorRow({
label={label}
className={cn(
isActivityVariant
- ? "h-[18px] w-[18px] text-[7px]"
- : "h-5 w-5 text-[8px]",
+ ? "h-[18px] w-[18px] text-3xs"
+ : "h-5 w-5 text-3xs",
)}
iconClassName={
isActivityVariant ? "h-2.5 w-2.5" : "h-4 w-4"
diff --git a/desktop/src/features/messages/ui/UnreadDivider.tsx b/desktop/src/features/messages/ui/UnreadDivider.tsx
index 5994f08fc2..e07213fab7 100644
--- a/desktop/src/features/messages/ui/UnreadDivider.tsx
+++ b/desktop/src/features/messages/ui/UnreadDivider.tsx
@@ -11,7 +11,7 @@ export function UnreadDivider() {
data-testid="message-unread-divider"
>
-
+
New
diff --git a/desktop/src/features/onboarding/ui/AvatarStep.tsx b/desktop/src/features/onboarding/ui/AvatarStep.tsx
index 9938c96b58..15deee89ad 100644
--- a/desktop/src/features/onboarding/ui/AvatarStep.tsx
+++ b/desktop/src/features/onboarding/ui/AvatarStep.tsx
@@ -86,7 +86,7 @@ function AvatarPreview({
>
0 && "buzz-avatar-squish",
)}
data-testid="onboarding-avatar-preview-emoji"
diff --git a/desktop/src/features/onboarding/ui/MembershipDenied.tsx b/desktop/src/features/onboarding/ui/MembershipDenied.tsx
index a3bfc5c879..99f46701f0 100644
--- a/desktop/src/features/onboarding/ui/MembershipDenied.tsx
+++ b/desktop/src/features/onboarding/ui/MembershipDenied.tsx
@@ -177,7 +177,7 @@ export function MembershipDenied({
This will use this Nostr identity:
-
+
{shortenNpub(previewNpub)}
diff --git a/desktop/src/features/onboarding/ui/NostrKeyImportForm.tsx b/desktop/src/features/onboarding/ui/NostrKeyImportForm.tsx
index 305d7c6d5e..d105ad1816 100644
--- a/desktop/src/features/onboarding/ui/NostrKeyImportForm.tsx
+++ b/desktop/src/features/onboarding/ui/NostrKeyImportForm.tsx
@@ -227,7 +227,7 @@ export function NostrKeyImportForm({
This will use this Nostr identity:
-
+
{shortenNpub(previewNpub)}
diff --git a/desktop/src/features/profile/ui/UserProfilePopover.tsx b/desktop/src/features/profile/ui/UserProfilePopover.tsx
index 8dc516cd2c..070f7da754 100644
--- a/desktop/src/features/profile/ui/UserProfilePopover.tsx
+++ b/desktop/src/features/profile/ui/UserProfilePopover.tsx
@@ -222,7 +222,7 @@ export function UserProfilePopover({
) : null}
{profile?.displayName ? (
-
+
{truncatePubkey(pubkey)}
) : null}
diff --git a/desktop/src/features/pulse/ui/AgentActivityCard.tsx b/desktop/src/features/pulse/ui/AgentActivityCard.tsx
index 3c2e8a7a3b..c5a7aa84e0 100644
--- a/desktop/src/features/pulse/ui/AgentActivityCard.tsx
+++ b/desktop/src/features/pulse/ui/AgentActivityCard.tsx
@@ -75,7 +75,7 @@ export function AgentActivityCard({
{displayName}
{agentStatus ?
: null}
-
+
{formatRelativeTime(group.latestAt)}
@@ -113,7 +113,7 @@ export function AgentActivityCard({
-
+
{formatRelativeTime(note.createdAt)}
diff --git a/desktop/src/features/pulse/ui/NoteCard.tsx b/desktop/src/features/pulse/ui/NoteCard.tsx
index 7942376674..2785bf7e66 100644
--- a/desktop/src/features/pulse/ui/NoteCard.tsx
+++ b/desktop/src/features/pulse/ui/NoteCard.tsx
@@ -191,7 +191,7 @@ export function NoteCard({
{isAgent ? (
-
+
bot
) : null}
@@ -213,7 +213,7 @@ export function NoteCard({
) : null}
-
+
diff --git a/desktop/src/features/pulse/ui/PulseTabBar.tsx b/desktop/src/features/pulse/ui/PulseTabBar.tsx
index 937682c7ed..3ba3b0777a 100644
--- a/desktop/src/features/pulse/ui/PulseTabBar.tsx
+++ b/desktop/src/features/pulse/ui/PulseTabBar.tsx
@@ -13,7 +13,7 @@ type PulseTabBarProps = {
};
const tabButtonClassName =
- "h-7 rounded-full border border-transparent px-1.5 text-[10.5px] font-medium text-muted-foreground data-[active=true]:border-border/70 data-[active=true]:bg-background/80 data-[active=true]:text-foreground data-[active=true]:shadow-xs data-[active=true]:backdrop-blur-sm";
+ "h-7 rounded-full border border-transparent px-1.5 text-2xs font-medium text-muted-foreground data-[active=true]:border-border/70 data-[active=true]:bg-background/80 data-[active=true]:text-foreground data-[active=true]:shadow-xs data-[active=true]:backdrop-blur-sm";
export function PulseTabBar({
activeTab,
@@ -103,7 +103,7 @@ export function PulseTabBar({
>
Agents
{relayAgents.length > 0 ? (
-
+
{relayAgents.length}
) : null}
diff --git a/desktop/src/features/search/ui/TopbarSearch.tsx b/desktop/src/features/search/ui/TopbarSearch.tsx
index f830c74d04..2d3ec49958 100644
--- a/desktop/src/features/search/ui/TopbarSearch.tsx
+++ b/desktop/src/features/search/ui/TopbarSearch.tsx
@@ -266,7 +266,7 @@ export function TopbarSearch({
spellCheck={false}
value={query}
/>
-
+
⌘K
@@ -283,7 +283,7 @@ export function TopbarSearch({
style={POPOVER_SHADOW_STYLE}
>
{debouncedQuery.length < MIN_SEARCH_QUERY_LENGTH ? (
-
+
Type at least two characters for live suggestions.
) : searchQuery.isLoading && results.length === 0 ? (
@@ -359,7 +359,7 @@ export function TopbarSearch({
: truncateResultText(result.hit.content)}
-
+
{result.kind === "channel"
? "Channel"
: `${describeSearchHit(result.hit)} · ${formatRelativeTime(result.hit.createdAt)}`}
diff --git a/desktop/src/features/settings/ui/ChannelTemplatesSettingsCard.tsx b/desktop/src/features/settings/ui/ChannelTemplatesSettingsCard.tsx
index b933c61a2e..de2527bfcc 100644
--- a/desktop/src/features/settings/ui/ChannelTemplatesSettingsCard.tsx
+++ b/desktop/src/features/settings/ui/ChannelTemplatesSettingsCard.tsx
@@ -216,7 +216,7 @@ function TemplateRow({
{template.name}
{template.isBuiltin ? (
-
+
built-in
) : null}
@@ -756,7 +756,7 @@ function RuntimeRow({
) : (
)}
diff --git a/desktop/src/features/settings/ui/DoctorSettingsPanel.tsx b/desktop/src/features/settings/ui/DoctorSettingsPanel.tsx
index 4b7022baff..d78eab3f97 100644
--- a/desktop/src/features/settings/ui/DoctorSettingsPanel.tsx
+++ b/desktop/src/features/settings/ui/DoctorSettingsPanel.tsx
@@ -109,7 +109,7 @@ function RuntimeRow({
{runtime.label}
{runtime.command ? (
-
+
{runtime.command}
) : null}
@@ -134,21 +134,21 @@ function RuntimeRow({
{runtime.underlyingCliPath &&
runtime.underlyingCliPath !== runtime.binaryPath ? (
-
+
CLI: {" "}
{runtime.underlyingCliPath}
-
+
ACP adapter: {" "}
{runtime.binaryPath}
) : (
<>
-
+
{runtime.binaryPath}
-
+
ACP support built-in — no separate adapter needed.
>
@@ -158,7 +158,7 @@ function RuntimeRow({
<>
CLI detected at{" "}
-
+
{runtime.underlyingCliPath ?? "unknown path"}
{" "}
but ACP adapter not found.
@@ -176,7 +176,7 @@ function RuntimeRow({
<>
ACP adapter found at{" "}
-
+
{runtime.binaryPath ?? "unknown path"}
{" "}
but the {runtime.label} CLI is not installed.
diff --git a/desktop/src/features/settings/ui/NotificationSettingsCard.tsx b/desktop/src/features/settings/ui/NotificationSettingsCard.tsx
index 05a47e03c9..3e765ebe2b 100644
--- a/desktop/src/features/settings/ui/NotificationSettingsCard.tsx
+++ b/desktop/src/features/settings/ui/NotificationSettingsCard.tsx
@@ -179,7 +179,7 @@ export function NotificationSettingsCard({
{SLOT_LABELS[slot]}
{comingSoon ? (
-
+
Coming soon
) : null}
diff --git a/desktop/src/features/settings/ui/ProfileSettingsCard.tsx b/desktop/src/features/settings/ui/ProfileSettingsCard.tsx
index ddd10cee00..70a15d900d 100644
--- a/desktop/src/features/settings/ui/ProfileSettingsCard.tsx
+++ b/desktop/src/features/settings/ui/ProfileSettingsCard.tsx
@@ -570,7 +570,7 @@ export function ProfileSettingsCard({
>
0 && "buzz-avatar-squish",
)}
data-testid="profile-avatar-preview-emoji"
diff --git a/desktop/src/features/settings/ui/SoundPicker.tsx b/desktop/src/features/settings/ui/SoundPicker.tsx
index 716290f335..f09aa556cc 100644
--- a/desktop/src/features/settings/ui/SoundPicker.tsx
+++ b/desktop/src/features/settings/ui/SoundPicker.tsx
@@ -113,7 +113,7 @@ export function SoundPicker({
{name}
{name === recommended ? (
-
+
rec.
) : null}
diff --git a/desktop/src/features/sidebar/ui/AppSidebar.tsx b/desktop/src/features/sidebar/ui/AppSidebar.tsx
index 8a7303e2d0..302d4f93a9 100644
--- a/desktop/src/features/sidebar/ui/AppSidebar.tsx
+++ b/desktop/src/features/sidebar/ui/AppSidebar.tsx
@@ -465,7 +465,7 @@ export function AppSidebar({
{homeBadgeCount > 0 ? (
{Math.min(homeBadgeCount, 99)}
@@ -513,7 +513,7 @@ export function AppSidebar({
{shouldShowAgentCount ? (
{totalAgentCount}
diff --git a/desktop/src/features/sidebar/ui/MoreUnreadButton.tsx b/desktop/src/features/sidebar/ui/MoreUnreadButton.tsx
index 22f9119f02..e0b2d027b1 100644
--- a/desktop/src/features/sidebar/ui/MoreUnreadButton.tsx
+++ b/desktop/src/features/sidebar/ui/MoreUnreadButton.tsx
@@ -3,7 +3,7 @@ import type * as React from "react";
import { Button } from "@/shared/ui/button";
const MORE_UNREAD_BUTTON_CLASS =
- "h-7 min-h-7 gap-1.5 rounded-full border-0 bg-primary px-2.5 text-[11px] font-medium text-primary-foreground shadow-md hover:bg-primary/90 [&_svg]:size-4";
+ "h-7 min-h-7 gap-1.5 rounded-full border-0 bg-primary px-2.5 text-2xs font-medium text-primary-foreground shadow-md hover:bg-primary/90 [&_svg]:size-4";
export function MoreUnreadButton({
bottomClassName = "bottom-0",
diff --git a/desktop/src/features/sidebar/ui/SidebarProfileCard.tsx b/desktop/src/features/sidebar/ui/SidebarProfileCard.tsx
index 300662b542..5d010fa313 100644
--- a/desktop/src/features/sidebar/ui/SidebarProfileCard.tsx
+++ b/desktop/src/features/sidebar/ui/SidebarProfileCard.tsx
@@ -88,7 +88,7 @@ export function SidebarProfileCard({
const workspaceLabel = activeWorkspace?.name ?? "No workspace";
const readonlyWorkspaceLabel = (
-
+
🐝
{workspaceLabel}
diff --git a/desktop/src/features/sidebar/ui/SidebarSection.tsx b/desktop/src/features/sidebar/ui/SidebarSection.tsx
index cf78b9668f..8666ec8d60 100644
--- a/desktop/src/features/sidebar/ui/SidebarSection.tsx
+++ b/desktop/src/features/sidebar/ui/SidebarSection.tsx
@@ -68,7 +68,7 @@ function DmChannelIcon({
return (
@@ -83,7 +83,7 @@ function DmChannelIcon({
diff --git a/desktop/src/features/user-status/ui/SetStatusDialog.tsx b/desktop/src/features/user-status/ui/SetStatusDialog.tsx
index d436838d5f..d581aff005 100644
--- a/desktop/src/features/user-status/ui/SetStatusDialog.tsx
+++ b/desktop/src/features/user-status/ui/SetStatusDialog.tsx
@@ -119,7 +119,7 @@ export function SetStatusDialog({
{emoji ? (
{
event.stopPropagation();
setEmoji("");
diff --git a/desktop/src/features/workflows/ui/WorkflowCard.tsx b/desktop/src/features/workflows/ui/WorkflowCard.tsx
index 577bdbabdb..cd15a0f52f 100644
--- a/desktop/src/features/workflows/ui/WorkflowCard.tsx
+++ b/desktop/src/features/workflows/ui/WorkflowCard.tsx
@@ -85,7 +85,7 @@ export function WorkflowCard({
-
+
{channelName ? {channelName} : null}
{triggerSummary ? {triggerSummary} : null}
diff --git a/desktop/src/features/workflows/ui/WorkflowDetailPanel.tsx b/desktop/src/features/workflows/ui/WorkflowDetailPanel.tsx
index f610caee12..e727cf9e8c 100644
--- a/desktop/src/features/workflows/ui/WorkflowDetailPanel.tsx
+++ b/desktop/src/features/workflows/ui/WorkflowDetailPanel.tsx
@@ -186,7 +186,7 @@ export function WorkflowDetailPanel({
-
+
{new Date(
run.createdAt * 1000,
@@ -216,10 +216,10 @@ export function WorkflowDetailPanel({
{isSelected ? (
-
+
Execution Trace
{approvalsQuery.isFetching ? (
-
+
Refreshing approvals...
) : null}
diff --git a/desktop/src/features/workflows/ui/WorkflowRunTrace.tsx b/desktop/src/features/workflows/ui/WorkflowRunTrace.tsx
index a1e6b2775e..76fc9ecf88 100644
--- a/desktop/src/features/workflows/ui/WorkflowRunTrace.tsx
+++ b/desktop/src/features/workflows/ui/WorkflowRunTrace.tsx
@@ -94,7 +94,7 @@ export function WorkflowRunTrace({
{Object.keys(step.output).length > 0 ? (
-
+
Output
@@ -104,7 +104,7 @@ export function WorkflowRunTrace({
) : null}
{step.error ? (
-
+
Error
@@ -114,7 +114,7 @@ export function WorkflowRunTrace({
) : null}
{pendingApproval ? (
-
+
Pending approval
diff --git a/desktop/src/features/workspaces/ui/WorkspaceSwitcher.tsx b/desktop/src/features/workspaces/ui/WorkspaceSwitcher.tsx
index 73bf8b1ae6..851236b146 100644
--- a/desktop/src/features/workspaces/ui/WorkspaceSwitcher.tsx
+++ b/desktop/src/features/workspaces/ui/WorkspaceSwitcher.tsx
@@ -132,7 +132,7 @@ export function WorkspaceSwitcher({
diff --git a/desktop/src/shared/styles/globals.css b/desktop/src/shared/styles/globals.css
index 8963b6d038..2c93537290 100644
--- a/desktop/src/shared/styles/globals.css
+++ b/desktop/src/shared/styles/globals.css
@@ -512,9 +512,9 @@
/* ── Tiptap rich-text composer ──────────────────────────────────────── */
.rich-text-composer .tiptap {
outline: none;
- min-height: 1.5rem; /* single line height */
- font-size: 0.875rem;
- line-height: 1.5rem;
+ min-height: calc(var(--text-sm) * var(--leading-relaxed));
+ font-size: var(--text-sm);
+ line-height: var(--leading-relaxed);
}
.rich-text-composer .tiptap p {
@@ -555,16 +555,8 @@
text-decoration: line-through;
}
-.rich-text-composer .tiptap code {
- border-radius: 0.375rem;
- background: hsl(var(--muted));
- padding: 0.125rem 0.375rem;
- font-family: ui-monospace, monospace;
- font-size: 0.8125rem;
-}
-
.rich-text-composer .tiptap pre {
- border-radius: 0.75rem;
+ border-radius: var(--radius);
border: 1px solid hsl(var(--border) / 0.7);
background: hsl(var(--muted) / 0.6);
padding: 0.375rem 0.75rem;
@@ -572,14 +564,6 @@
margin: 0.25rem 0;
}
-.rich-text-composer .tiptap pre code {
- background: none;
- padding: 0;
- border-radius: 0;
- font-size: 0.8125rem;
- line-height: 1.5rem;
-}
-
.rich-text-composer .tiptap blockquote {
border-left: 2px solid hsl(var(--border));
padding-left: 1rem;
@@ -620,45 +604,110 @@
margin: 0.5rem 0;
}
-.rich-text-composer .tiptap .mention-highlight {
- display: inline-block;
- border-radius: 4px;
- background: hsl(var(--primary) / 0.15);
- padding: 3px 4px 2px;
- font-size: 15px;
+/* Inline chips — timeline markdown + composer share `.message-markdown` scope.
+ Body copy is text-sm; inline code labels sit one step down (text-xs).
+ All chip variants share one box height so mono labels stay balanced. */
+.message-markdown {
+ --inline-chip-padding-block-start: 0.1875rem;
+ --inline-chip-padding-block-end: 0.125rem;
+ --inline-chip-padding-inline: 0.25rem;
+ --inline-chip-min-height: calc(
+ var(--text-sm) +
+ var(--inline-chip-padding-block-start) +
+ var(--inline-chip-padding-block-end)
+ );
+ --inline-code-font-size: var(--text-xs);
+ --agent-icon-size: 0.95em;
+ --agent-icon-gap: 0.125rem;
+ --agent-icon-top: 0.125rem;
+}
+
+.message-markdown .mention-chip,
+.message-markdown .inline-code-chip,
+.message-markdown :not(pre) > code {
+ box-sizing: border-box;
+ display: inline-flex;
+ align-items: center;
+ min-height: var(--inline-chip-min-height);
+ padding: var(--inline-chip-padding-block-start)
+ var(--inline-chip-padding-inline) var(--inline-chip-padding-block-end);
+ border-radius: calc(var(--radius) - 4px);
+ font-weight: 500;
line-height: 1;
+ vertical-align: baseline;
+ box-decoration-break: clone;
+ -webkit-box-decoration-break: clone;
+}
+
+.message-markdown .mention-chip {
+ font-size: inherit;
+ background: hsl(var(--primary) / 0.15);
color: hsl(var(--primary));
+}
+
+.message-markdown .inline-code-chip,
+.message-markdown :not(pre) > code {
+ font-size: var(--inline-code-font-size);
+ font-family: ui-monospace, monospace;
+ background: hsl(var(--muted));
+ color: inherit;
+}
+
+/* Code block content — higher specificity than the inline chips above; kept
+ after them so descending-specificity ordering stays clean. */
+.rich-text-composer .tiptap pre code {
+ background: none;
+ padding: 0;
+ border-radius: 0;
+ font-size: var(--text-sm);
+ line-height: var(--text-sm--line-height);
font-weight: 500;
}
-.rich-text-composer .tiptap .agent-mention-at-hidden {
- color: transparent;
- font-size: 0;
- line-height: 0;
+.mention-chip-hover {
+ transition-property:
+ color, background-color, border-color, outline-color, text-decoration-color,
+ fill, stroke;
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+ transition-duration: 150ms;
}
-.rich-text-composer .tiptap .agent-mention-highlight,
-[data-mention].agent-mention-highlight {
- display: inline-flex;
- align-items: center;
- gap: 2px;
- background: hsl(var(--primary) / 0.18);
- color: hsl(var(--primary) / 0.92);
+.mention-chip-hover:hover {
+ background: hsl(var(--primary) / 0.25);
+ color: hsl(var(--primary) / 0.9);
}
-[data-mention].agent-mention-highlight {
- transform: translateY(2px);
+.message-markdown .agent-mention-at-hidden {
+ display: inline-block;
+ width: 0;
+ max-width: 0;
+ overflow: hidden;
+ opacity: 0;
+ padding: 0;
+ margin: 0;
+ border: 0;
+ vertical-align: baseline;
+ line-height: 1;
}
-.rich-text-composer .tiptap .agent-mention-highlight::before,
-[data-mention].agent-mention-highlight::before {
+.message-markdown .agent-mention-highlight {
+ position: relative;
+ padding-left: calc(
+ var(--inline-chip-padding-inline) +
+ var(--agent-icon-size) +
+ var(--agent-icon-gap)
+ );
+}
+
+.message-markdown .agent-mention-highlight::before {
content: "";
- display: inline-block;
- width: 0.85em;
- height: 0.85em;
- flex: 0 0 auto;
+ position: absolute;
+ top: var(--agent-icon-top);
+ left: var(--inline-chip-padding-inline);
+ width: var(--agent-icon-size);
+ height: var(--agent-icon-size);
background: currentColor;
- transform: translateY(-0.25px);
+ pointer-events: none;
-webkit-mask:
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 8V4H8'/%3E%3Crect width='16' height='12' x='4' y='8' rx='2'/%3E%3Cpath d='M2 14h2'/%3E%3Cpath d='M20 14h2'/%3E%3Cpath d='M15 13v2'/%3E%3Cpath d='M9 13v2'/%3E%3C/svg%3E")
center / contain no-repeat;
diff --git a/desktop/src/shared/ui/UserAvatar.tsx b/desktop/src/shared/ui/UserAvatar.tsx
index 5b9e980da4..4c1971c5cb 100644
--- a/desktop/src/shared/ui/UserAvatar.tsx
+++ b/desktop/src/shared/ui/UserAvatar.tsx
@@ -9,8 +9,8 @@ import { Avatar, AvatarFallback, AvatarImage } from "@/shared/ui/avatar";
type UserAvatarSize = "xs" | "sm" | "md";
const sizeClasses: Record = {
- xs: "h-5 w-5 text-[8px]",
- sm: "h-6 w-6 text-[9px]",
+ xs: "h-5 w-5 text-3xs",
+ sm: "h-6 w-6 text-2xs",
md: "h-10 w-10 text-xs",
};
diff --git a/desktop/src/shared/ui/VideoPlayer.tsx b/desktop/src/shared/ui/VideoPlayer.tsx
index cdc23b5421..b7ace0b928 100644
--- a/desktop/src/shared/ui/VideoPlayer.tsx
+++ b/desktop/src/shared/ui/VideoPlayer.tsx
@@ -535,7 +535,7 @@ function VideoScrubber({
style={{ left: `${hoverRatio * 100}%` }}
>
-
+
{formatTimecode(hoverRatio * duration)} / {formatTimecode(duration)}
@@ -970,7 +970,7 @@ export function VideoPlayer({
)}
{formatTimecode(currentTime)}
@@ -984,7 +984,7 @@ export function VideoPlayer({
testIdPrefix="video-inline"
/>
{formatTimecode(duration)}
@@ -1773,12 +1773,12 @@ function VideoReviewDialog({
{formatTimecode(currentTime)}
{replyTarget ? (
-
+
Replying to {replyTarget.comment.author}
) : (
+
onSeek(item.seconds ?? 0)}
@@ -1949,7 +1949,7 @@ function VideoReviewCommentBody({
displayName={item.comment.author}
size="xs"
/>
-
+
{item.comment.author}
diff --git a/desktop/src/shared/ui/badge.tsx b/desktop/src/shared/ui/badge.tsx
index fdb5d91b9c..d7318f5e19 100644
--- a/desktop/src/shared/ui/badge.tsx
+++ b/desktop/src/shared/ui/badge.tsx
@@ -4,7 +4,7 @@ import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/shared/lib/cn";
const badgeVariants = cva(
- "inline-flex items-center rounded-full px-2 pb-[3px] pt-[5px] text-[10px] font-semibold uppercase leading-none tracking-[0.18em]",
+ "inline-flex items-center rounded-full px-2 pb-[3px] pt-[5px] text-2xs font-semibold uppercase leading-none tracking-[0.18em]",
{
variants: {
variant: {
diff --git a/desktop/src/shared/ui/markdown.tsx b/desktop/src/shared/ui/markdown.tsx
index 3afd15aacf..c2c06a2581 100644
--- a/desktop/src/shared/ui/markdown.tsx
+++ b/desktop/src/shared/ui/markdown.tsx
@@ -42,8 +42,10 @@ import remarkSpoilers from "@/shared/lib/remarkSpoilers";
import remarkMessageLinks from "@/features/messages/lib/remarkMessageLinks";
import { Button } from "@/shared/ui/button";
import {
+ INLINE_CODE_CHIP_CLASS,
MENTION_CHIP_BASE_CLASSES,
MENTION_CHIP_HOVER_CLASSES,
+ MESSAGE_MARKDOWN_CLASS,
} from "@/shared/ui/mentionChip";
import { MODAL_BACKDROP_BLUR_CLASS } from "@/shared/ui/modalBackdrop";
import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover";
@@ -86,7 +88,7 @@ const MAX_CACHE_ENTRIES = 100;
const MAX_LOADED_LANGUAGES = 30;
const MAX_HIGHLIGHT_LINES = 150;
const CODE_BLOCK_CLASS =
- "code-block-lines block min-w-full whitespace-pre font-mono text-[13px] leading-6 text-foreground";
+ "code-block-lines block min-w-full whitespace-pre font-mono text-sm font-medium text-foreground";
const DIFF_ADD_RE = /\s*\/\/\s*\[!code\s*\+\+\]\s*$/;
const DIFF_REMOVE_RE = /\s*\/\/\s*\[!code\s*--\]\s*$/;
@@ -231,7 +233,6 @@ function messageLinkUrlTransform(value: string, key: string): string {
type MarkdownProps = {
channelNames?: string[];
className?: string;
- compact?: boolean;
content: string;
customEmoji?: CustomEmoji[];
imetaByUrl?: ImetaLookup;
@@ -240,12 +241,9 @@ type MarkdownProps = {
mentionNames?: string[];
mentionPubkeysByName?: Record;
searchQuery?: string;
- tight?: boolean;
videoReviewContext?: VideoReviewContext;
};
-type MarkdownVariant = "default" | "compact" | "tight";
-
/**
* Inline image embed with click-to-zoom lightbox and right-click download.
*
@@ -953,22 +951,12 @@ function SpoilerInline({
}
function createMarkdownComponents(
- variant: MarkdownVariant,
runtimeRef: React.RefObject,
interactive = true,
): Components {
- const paragraphClassName =
- variant === "tight"
- ? "leading-5"
- : variant === "compact"
- ? "leading-6"
- : "leading-7";
- const listItemClassName =
- variant === "tight" ? "my-0.5 [&_p]:inline" : "my-1 [&_p]:inline";
- const listClassName =
- variant === "tight"
- ? "space-y-0.5 pl-6 marker:text-muted-foreground"
- : "space-y-1 pl-6 marker:text-muted-foreground";
+ const paragraphClassName = "leading-relaxed";
+ const listItemClassName = "my-1 [&_p]:inline";
+ const listClassName = "space-y-1 pl-6 marker:text-muted-foreground";
return {
spoiler: ({
@@ -1080,13 +1068,7 @@ function createMarkdownComponents(
}
return (
-
+
{children}
);
@@ -1106,6 +1088,21 @@ function createMarkdownComponents(
{children}
),
+ h4: ({ children }) => (
+
+ {children}
+
+ ),
+ h5: ({ children }) => (
+
+ {children}
+
+ ),
+ h6: ({ children }) => (
+
+ {children}
+
+ ),
hr: () => ,
img: ({ alt, src }) => {
const { imetaByUrl } = runtimeRef.current;
@@ -1268,7 +1265,11 @@ function createMarkdownComponents(
type="button"
data-channel-link=""
aria-label={`Open channel ${channelName}`}
- className="rounded-md bg-primary/15 px-1 py-0.5 text-sm font-medium text-primary cursor-pointer hover:bg-primary/25 transition-colors"
+ className={cn(
+ "cursor-pointer",
+ MENTION_CHIP_BASE_CLASSES,
+ MENTION_CHIP_HOVER_CLASSES,
+ )}
onClick={() => {
onOpenChannel(channel.id);
}}
@@ -1279,10 +1280,7 @@ function createMarkdownComponents(
}
return (
-
+
{children}
);
@@ -1316,7 +1314,11 @@ function createMarkdownComponents(
data-message-link=""
aria-label={`Open message in ${channelLabel}`}
title={href}
- className="rounded-md bg-primary/15 px-1 py-0.5 text-sm font-medium text-primary cursor-pointer hover:bg-primary/25 transition-colors"
+ className={cn(
+ "cursor-pointer",
+ MENTION_CHIP_BASE_CLASSES,
+ MENTION_CHIP_HOVER_CLASSES,
+ )}
onClick={() => {
onOpenMessageLink(parsed.value);
}}
@@ -1331,7 +1333,6 @@ function createMarkdownComponents(
function MarkdownInner({
channelNames,
className,
- compact = false,
content,
customEmoji,
imetaByUrl,
@@ -1340,14 +1341,8 @@ function MarkdownInner({
mentionNames,
mentionPubkeysByName,
searchQuery,
- tight = false,
videoReviewContext,
}: MarkdownProps) {
- const variant: MarkdownVariant = tight
- ? "tight"
- : compact
- ? "compact"
- : "default";
const { channels: rawChannels } = useChannelNavigation();
const channels = useStableArray(rawChannels);
const { goChannel } = useAppNavigation();
@@ -1383,8 +1378,8 @@ function MarkdownInner({
});
const components = React.useMemo(
- () => createMarkdownComponents(variant, runtimeRef, interactive),
- [variant, runtimeRef, interactive],
+ () => createMarkdownComponents(runtimeRef, interactive),
+ [runtimeRef, interactive],
);
// biome-ignore lint/suspicious/noExplicitAny: PluggableList type not directly importable
@@ -1435,52 +1430,21 @@ function MarkdownInner({
return (
*:first-child]:mt-0 [&>*:last-child]:mb-0",
- // Base owl: p+p, list+p, etc.
- "[&>*+*]:mt-2",
- // Headings: flat push/pull — size does the hierarchy work
- "[&>*+h1]:mt-2.5 [&>*+h2]:mt-2.5 [&>*+h3]:mt-2.5",
- "[&>h1+*]:mt-0.5 [&>h2+*]:mt-0.5 [&>h3+*]:mt-0.5",
- // Blockquotes: breathe above and below
- "[&>*+blockquote]:mt-3 [&>blockquote+*]:mt-3",
- // Code blocks: breathe above and below
- "[&>*+[data-code-block]]:mt-3 [&>[data-code-block]+*]:mt-3",
- // Tables: breathe above and below
- "[&>*+[data-table-block]]:mt-3 [&>[data-table-block]+*]:mt-3",
- // hr: clear section divider
- "[&>*+hr]:mt-3.5 [&>hr+*]:mt-3.5",
- // Lists after paragraphs: tighter to feel related
- "[&>p+ul]:mt-1 [&>p+ol]:mt-1 [&>div+ul]:mt-1 [&>div+ol]:mt-1",
- ].join(" ")
- : compact
- ? [
- "max-w-none break-words text-[15px] leading-6 text-foreground/90",
- "[&>*:first-child]:mt-0 [&>*:last-child]:mb-0",
- "[&>*+*]:mt-2",
- "[&>*+h1]:mt-3 [&>*+h2]:mt-3 [&>*+h3]:mt-3",
- "[&>h1+*]:mt-0.5 [&>h2+*]:mt-0.5 [&>h3+*]:mt-0.5",
- "[&>*+blockquote]:mt-3 [&>blockquote+*]:mt-3",
- "[&>*+[data-code-block]]:mt-3 [&>[data-code-block]+*]:mt-3",
- "[&>*+[data-table-block]]:mt-3 [&>[data-table-block]+*]:mt-3",
- "[&>*+hr]:mt-3.5 [&>hr+*]:mt-3.5",
- "[&>p+ul]:mt-1 [&>p+ol]:mt-1 [&>div+ul]:mt-1 [&>div+ol]:mt-1",
- ].join(" ")
- : [
- "max-w-none break-words text-sm leading-7 text-foreground/90",
- "[&>*:first-child]:mt-0 [&>*:last-child]:mb-0",
- "[&>*+*]:mt-3",
- "[&>*+h1]:mt-3.5 [&>*+h2]:mt-3.5 [&>*+h3]:mt-3.5",
- "[&>h1+*]:mt-0.5 [&>h2+*]:mt-0.5 [&>h3+*]:mt-0.5",
- "[&>*+blockquote]:mt-3.5 [&>blockquote+*]:mt-3.5",
- "[&>*+[data-code-block]]:mt-3.5 [&>[data-code-block]+*]:mt-3.5",
- "[&>*+[data-table-block]]:mt-3.5 [&>[data-table-block]+*]:mt-3.5",
- "[&>*+hr]:mt-4 [&>hr+*]:mt-4",
- "[&>p+ul]:mt-1.5 [&>p+ol]:mt-1.5 [&>div+ul]:mt-1.5 [&>div+ol]:mt-1.5",
- ].join(" "),
+ MESSAGE_MARKDOWN_CLASS,
+ [
+ "max-w-none break-words text-sm leading-relaxed text-foreground",
+ "[&>*:first-child]:mt-0 [&>*:last-child]:mb-0",
+ "[&>*+*]:mt-3",
+ "[&>p+p]:mt-1.5",
+ "[&>*+h1]:mt-3.5 [&>*+h2]:mt-3.5 [&>*+h3]:mt-3.5 [&>*+h4]:mt-3.5 [&>*+h5]:mt-3.5 [&>*+h6]:mt-3.5",
+ "[&>h1+*]:mt-0.5 [&>h2+*]:mt-0.5 [&>h3+*]:mt-0.5 [&>h4+*]:mt-0.5 [&>h5+*]:mt-0.5 [&>h6+*]:mt-0.5",
+ "[&>h1+h2]:mt-1.5! [&>h2+h3]:mt-1.5! [&>h3+h4]:mt-1.5! [&>h4+h5]:mt-1.5! [&>h5+h6]:mt-1.5!",
+ "[&>*+blockquote]:mt-3.5 [&>blockquote+*]:mt-3.5",
+ "[&>*+[data-code-block]]:mt-3.5 [&>[data-code-block]+*]:mt-3.5",
+ "[&>*+[data-table-block]]:mt-3.5 [&>[data-table-block]+*]:mt-3.5",
+ "[&>*+hr]:mt-4 [&>hr+*]:mt-4",
+ "[&>p+ul]:mt-1.5 [&>p+ol]:mt-1.5 [&>div+ul]:mt-1.5 [&>div+ol]:mt-1.5",
+ ].join(" "),
className,
)}
>
@@ -1496,10 +1460,8 @@ export const Markdown = React.memo(
(prev, next) =>
prev.content === next.content &&
prev.className === next.className &&
- prev.compact === next.compact &&
prev.customEmoji === next.customEmoji &&
prev.interactive === next.interactive &&
- prev.tight === next.tight &&
prev.agentMentionPubkeysByName === next.agentMentionPubkeysByName &&
prev.mentionPubkeysByName === next.mentionPubkeysByName &&
shallowArrayEqual(prev.mentionNames, next.mentionNames) &&
diff --git a/desktop/src/shared/ui/mentionChip.ts b/desktop/src/shared/ui/mentionChip.ts
index ddb6756fcc..f980367dc3 100644
--- a/desktop/src/shared/ui/mentionChip.ts
+++ b/desktop/src/shared/ui/mentionChip.ts
@@ -1,5 +1,9 @@
-export const MENTION_CHIP_BASE_CLASSES =
- "inline-block rounded-[4px] bg-primary/15 px-1 pt-[3px] pb-[2px] text-[15px] font-medium leading-none text-primary";
+export const MENTION_CHIP_BASE_CLASSES = "mention-chip";
-export const MENTION_CHIP_HOVER_CLASSES =
- "transition-colors hover:bg-primary/25 hover:text-primary/90";
+export const MENTION_CHIP_HOVER_CLASSES = "mention-chip-hover";
+
+/** Wrapper on rendered message Markdown — scopes inline chip CSS. */
+export const MESSAGE_MARKDOWN_CLASS = "message-markdown";
+
+/** Inline `` `code` `` chip — matches mention chip rhythm in message bodies. */
+export const INLINE_CODE_CHIP_CLASS = "inline-code-chip";
diff --git a/desktop/tailwind.config.js b/desktop/tailwind.config.js
index 6422fc81ac..221cbe8ed5 100644
--- a/desktop/tailwind.config.js
+++ b/desktop/tailwind.config.js
@@ -2,6 +2,15 @@
export default {
theme: {
extend: {
+ // Sub-`text-xs` ramp for meta text (timestamps, count badges, tracking
+ // labels) and tiny glyphs. Defined in rem so Cmd +/- zoom — which scales
+ // the root font-size — keeps scaling them. Do NOT reintroduce
+ // arbitrary `text-[…rem]` / `text-[…px]` literals; the px-text guard
+ // rejects them. Stock scale picks up from here: xs (12px), sm (14px)…
+ fontSize: {
+ "2xs": "0.6875rem", // 11px — meta-text workhorse (timestamps, badges)
+ "3xs": "0.5rem", // 8px — tiny glyphs / micro labels
+ },
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
diff --git a/desktop/tests/e2e/mentions.spec.ts b/desktop/tests/e2e/mentions.spec.ts
index cbfc8b1ced..75faaee205 100644
--- a/desktop/tests/e2e/mentions.spec.ts
+++ b/desktop/tests/e2e/mentions.spec.ts
@@ -197,7 +197,7 @@ test("selecting a person mention inserts @Name into input", async ({
await dropdown.getByText("bob").click();
await expect(input).toHaveText("Hey @bob ");
- const mentionChip = input.locator(".mention-highlight", {
+ const mentionChip = input.locator(".mention-chip", {
hasText: "@bob",
});
await expect(mentionChip).toBeVisible();
@@ -415,7 +415,7 @@ test("profile-only agents without public respond-to are hidden from mentions", a
const dropdown = autocomplete(page);
await expect(dropdown).not.toBeVisible();
- await expect(input.locator(".mention-highlight")).toHaveCount(0);
+ await expect(input.locator(".mention-chip")).toHaveCount(0);
});
test("mentioning an in-channel stopped managed agent starts it before sending", async ({
@@ -712,11 +712,11 @@ test("selecting a non-member agent from a DM inserts @Name into input", async ({
const dropdown = autocomplete(page);
await expect(dropdown.getByText("charlie")).toBeVisible();
await expect(autocomplete(page)).toHaveCount(1);
- await expect(input.locator(".mention-highlight")).toHaveCount(0);
+ await expect(input.locator(".mention-chip")).toHaveCount(0);
await input.press("Enter");
await expect(input).toHaveText("@charlie ");
- await expect(input.locator(".mention-highlight")).toBeVisible();
+ await expect(input.locator(".mention-chip")).toBeVisible();
});
test("do nothing sends a non-member mention without inviting", async ({
diff --git a/scripts/check-px-text-core.mjs b/scripts/check-px-text-core.mjs
new file mode 100644
index 0000000000..2c122763eb
--- /dev/null
+++ b/scripts/check-px-text-core.mjs
@@ -0,0 +1,126 @@
+import { promises as fs } from "node:fs";
+import path from "node:path";
+
+/**
+ * Shared "no hardcoded px text size" guard.
+ *
+ * Zoom (Cmd +/-) scales the root font-size, so only **rem**-based text
+ * scales. Hardcoded px text sizes (`text-[15px]`, `font-size: 15px`) freeze
+ * against zoom — that's the timeline regression we fixed. This guard stops new
+ * px text sizes from creeping back in. Use a rem-based Tailwind token instead
+ * (e.g. the stock `text-base`, `text-sm`, `text-xs` scale — chat === base).
+ *
+ * It flags:
+ * - Tailwind arbitrary text-size utilities: `text-[NNpx]`, `text-[N.NNrem]`,
+ * `text-[N.NNem]` — any arbitrary font-size literal. Use a named token
+ * (`text-2xs`, `text-3xs`, or the stock `text-base`/`text-sm`/`text-xs`
+ * scale) so the size lives in `tailwind.config.js` as rem and stays
+ * consistent. px literals freeze against zoom; arbitrary rem literals
+ * re-fragment the scale we just consolidated.
+ * - CSS px font sizes: `font-size: NNpx`
+ *
+ * Decorative/chrome exceptions (avatar initials sized to a fixed avatar box,
+ * the `text-[6rem]` emoji glyph, etc.) live in the `overrides` allowlist
+ * supplied by each app.
+ */
+
+// Any arbitrary Tailwind text-size literal — px, rem, or em. Color literals
+// like `text-[#fff]` or `text-[var(--x)]` don't match (no unit-bearing number).
+const TEXT_ARBITRARY_RE = /\btext-\[\d+(?:\.\d+)?(?:px|rem|em)\]/g;
+// Match the CSS `font-size` property, but NOT custom properties like
+// `--font-size:` (third-party widget vars) which merely contain the substring.
+const FONT_SIZE_PX_RE = /(? {
+ const fullPath = path.join(directory, entry.name);
+ if (entry.isDirectory()) {
+ return walkFiles(fullPath);
+ }
+ return [fullPath];
+ }),
+ );
+ return files.flat();
+}
+
+/**
+ * @param {object} options
+ * @param {string} options.projectRoot Absolute path the rule roots resolve against.
+ * @param {Array<{root: string, extensions: Set}>} options.rules Where to scan.
+ * @param {string} options.label Human label for the failure header.
+ * @param {Set} [options.overrides] Allowlisted "relativePath:lineNumber" entries.
+ * @param {string} options.scriptPath Path mentioned in the failure hint.
+ */
+export async function runPxTextCheck({
+ projectRoot,
+ rules,
+ label,
+ overrides = new Set(),
+ scriptPath,
+}) {
+ const candidateFiles = (
+ await Promise.all(
+ rules.map((rule) => {
+ const dir = path.join(projectRoot, rule.root);
+ return fs
+ .access(dir)
+ .then(() => walkFiles(dir))
+ .catch(() => []);
+ }),
+ )
+ ).flat();
+
+ const violations = [];
+
+ for (const filePath of candidateFiles) {
+ const relativePath = path.relative(projectRoot, filePath);
+ const rule = rules.find((r) =>
+ relativePath.startsWith(`${r.root}${path.sep}`),
+ );
+ if (!rule) {
+ continue;
+ }
+ if (!rule.extensions.has(path.extname(relativePath))) {
+ continue;
+ }
+ // Optional per-rule basename allowlist — scopes the scan to specific files.
+ if (rule.files && !rule.files.has(path.basename(relativePath))) {
+ continue;
+ }
+
+ const content = await fs.readFile(filePath, "utf8");
+ const lines = content.split(/\r?\n/);
+ lines.forEach((line, index) => {
+ const lineNumber = index + 1;
+ const key = `${relativePath}:${lineNumber}`;
+ if (overrides.has(key)) {
+ return;
+ }
+ const matches = [
+ ...(line.match(TEXT_ARBITRARY_RE) ?? []),
+ ...(line.match(FONT_SIZE_PX_RE) ?? []),
+ ];
+ for (const match of matches) {
+ violations.push({ relativePath, lineNumber, match });
+ }
+ });
+ }
+
+ if (violations.length > 0) {
+ console.error(`${label} px-text check failed:`);
+ for (const v of violations) {
+ console.error(`- ${v.relativePath}:${v.lineNumber}: ${v.match}`);
+ }
+ console.error(
+ "Use a rem-based Tailwind text token (e.g. the stock `text-base`, " +
+ "`text-sm`, `text-xs` scale, or the `text-2xs` / `text-3xs` meta-text " +
+ "tokens) so the text scales with Cmd +/- zoom and stays on one scale. " +
+ "If this size is " +
+ "genuinely decorative/chrome (not readable message text), add a " +
+ `narrowly scoped \`relativePath:lineNumber\` exception in \`${scriptPath}\`.`,
+ );
+ process.exit(1);
+ }
+}