From 28770c7f1e4e4c9b05ded130090a3689b3238861 Mon Sep 17 00:00:00 2001
From: klopez4212
Date: Fri, 12 Jun 2026 15:05:32 +0100
Subject: [PATCH 1/7] Add shared skeleton loader primitives
---
desktop/src/shared/styles/globals.css | 125 ++++++++++++++++++++++++++
desktop/src/shared/ui/skeleton.tsx | 94 +++++++++++++++++--
desktop/src/shared/ui/spinner.tsx | 32 +++++--
3 files changed, 237 insertions(+), 14 deletions(-)
diff --git a/desktop/src/shared/styles/globals.css b/desktop/src/shared/styles/globals.css
index bb3fdb249..3cb2da523 100644
--- a/desktop/src/shared/styles/globals.css
+++ b/desktop/src/shared/styles/globals.css
@@ -2,6 +2,16 @@
@import "tw-animate-css";
@config "../../../tailwind.config.js";
+.sprout-arc-spinner {
+ animation: sprout-arc-spinner-spin 500ms linear infinite;
+}
+
+@keyframes sprout-arc-spinner-spin {
+ to {
+ transform: rotate(360deg);
+ }
+}
+
.buzz-emoji-mart {
align-items: stretch;
display: flex;
@@ -837,6 +847,121 @@
}
}
+:root {
+ --skel-pulse-dur: 1000ms;
+ --skel-pulse-count: infinite;
+ --skel-pulse-min: 0.5;
+ --skel-reveal-dur: 400ms;
+ --skel-reveal-blur: 2px;
+ --skel-reveal-ease: ease-in-out;
+}
+
+.t-skel {
+ position: relative;
+}
+
+.t-skel[data-layout="flow"] {
+ display: grid;
+}
+
+.t-skel-skeleton,
+.t-skel-content {
+ inset: 0;
+ position: absolute;
+}
+
+.t-skel[data-layout="flow"] > .t-skel-skeleton,
+.t-skel[data-layout="flow"] > .t-skel-content {
+ grid-area: 1 / 1;
+ width: 100%;
+}
+
+.t-skel[data-layout="flow"] > .t-skel-skeleton {
+ inset: auto;
+ position: relative;
+}
+
+.t-skel[data-layout="flow"] > .t-skel-content,
+.t-skel[data-layout="flow"].is-revealed > .t-skel-skeleton {
+ inset: 0;
+ position: absolute;
+}
+
+.t-skel[data-layout="flow"].is-revealed > .t-skel-content {
+ inset: auto;
+ position: relative;
+}
+
+.t-skel-skeleton {
+ filter: blur(0);
+ opacity: 1;
+ transition:
+ opacity var(--skel-reveal-dur) var(--skel-reveal-ease),
+ filter var(--skel-reveal-dur) var(--skel-reveal-ease);
+ z-index: 1;
+}
+
+.t-skel-content {
+ filter: blur(var(--skel-reveal-blur));
+ opacity: 0;
+ pointer-events: none;
+ transition:
+ opacity var(--skel-reveal-dur) var(--skel-reveal-ease),
+ filter var(--skel-reveal-dur) var(--skel-reveal-ease);
+ z-index: 2;
+}
+
+.t-skel.is-revealed .t-skel-skeleton {
+ filter: blur(var(--skel-reveal-blur));
+ opacity: 0;
+ pointer-events: none;
+}
+
+.t-skel.is-revealed .t-skel-content {
+ filter: blur(0);
+ opacity: 1;
+ pointer-events: auto;
+}
+
+.t-skel.is-resetting .t-skel-skeleton,
+.t-skel.is-resetting .t-skel-content {
+ transition: none;
+}
+
+.t-skel-bar.is-pulsing,
+.t-skel-skeleton.is-pulsing > :not(:has(.t-skel-bar)) {
+ animation: t-skel-pulse var(--skel-pulse-dur) ease-in-out
+ var(--skel-pulse-count);
+}
+
+.t-skel.is-revealed .t-skel-bar.is-pulsing,
+.t-skel.is-revealed .t-skel-skeleton.is-pulsing > :not(:has(.t-skel-bar)) {
+ animation: none;
+}
+
+@keyframes t-skel-pulse {
+ 0%,
+ 100% {
+ opacity: 1;
+ }
+
+ 50% {
+ opacity: var(--skel-pulse-min);
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .t-skel-skeleton,
+ .t-skel-content {
+ transition: none;
+ }
+
+ .t-skel-bar.is-pulsing,
+ .t-skel-skeleton.is-pulsing > :not(:has(.t-skel-bar)) {
+ animation: none;
+ }
+}
+
@layer components {
.buzz-startup-shell {
min-height: 100dvh;
diff --git a/desktop/src/shared/ui/skeleton.tsx b/desktop/src/shared/ui/skeleton.tsx
index 563cc4c9e..b324eb5da 100644
--- a/desktop/src/shared/ui/skeleton.tsx
+++ b/desktop/src/shared/ui/skeleton.tsx
@@ -1,15 +1,99 @@
+import * as React from "react";
+
import { cn } from "@/shared/lib/cn";
-function Skeleton({
+type SkeletonProps = React.HTMLAttributes & {
+ pulsing?: boolean;
+};
+
+function Skeleton({ className, pulsing = true, ...props }: SkeletonProps) {
+ return (
+
+ );
+}
+
+type SkeletonRevealProps = React.HTMLAttributes & {
+ contentClassName?: string;
+ layout?: "absolute" | "flow";
+ loading: boolean;
+ skeleton: React.ReactNode;
+ skeletonClassName?: string;
+};
+
+function SkeletonReveal({
+ children,
className,
+ contentClassName,
+ layout = "flow",
+ loading,
+ skeleton,
+ skeletonClassName,
...props
-}: React.HTMLAttributes) {
+}: SkeletonRevealProps) {
+ const rootRef = React.useRef(null);
+ const previousLoadingRef = React.useRef(loading);
+ const [isResetting, setIsResetting] = React.useState(false);
+
+ React.useLayoutEffect(() => {
+ const wasLoading = previousLoadingRef.current;
+ previousLoadingRef.current = loading;
+
+ if (!loading || wasLoading) return;
+
+ setIsResetting(true);
+ rootRef.current?.getBoundingClientRect();
+
+ const reset = () => setIsResetting(false);
+ const frameId = globalThis.requestAnimationFrame
+ ? globalThis.requestAnimationFrame(reset)
+ : globalThis.setTimeout(reset, 0);
+
+ return () => {
+ if (typeof frameId === "number") {
+ if (globalThis.cancelAnimationFrame) {
+ globalThis.cancelAnimationFrame(frameId);
+ } else {
+ globalThis.clearTimeout(frameId);
+ }
+ }
+ };
+ }, [loading]);
+
return (
+ >
+
+ {skeleton}
+
+
+ {children}
+
+
);
}
-export { Skeleton };
+export { Skeleton, SkeletonReveal };
diff --git a/desktop/src/shared/ui/spinner.tsx b/desktop/src/shared/ui/spinner.tsx
index e3f37181c..764e39083 100644
--- a/desktop/src/shared/ui/spinner.tsx
+++ b/desktop/src/shared/ui/spinner.tsx
@@ -1,26 +1,40 @@
-import { Loader2 } from "lucide-react";
+import type * as React from "react";
import { cn } from "@/shared/lib/cn";
-type SpinnerProps = React.ComponentPropsWithoutRef<"svg"> & {
+type SpinnerProps = React.ComponentPropsWithoutRef<"span"> & {
className?: string;
- size?: number;
+ size?: number | string;
};
export function Spinner({
+ children,
className,
size,
role = "status",
"aria-label": ariaLabel = "Loading",
+ "aria-hidden": ariaHidden,
+ style,
...rest
}: SpinnerProps) {
+ const isDecorative = ariaHidden === true || ariaHidden === "true";
+
return (
-
+ >
+ {children}
+ {isDecorative ? null : {ariaLabel} }
+
);
}
From bdd5d5c5fe5862a5dc9fda9f3bba72a5946d3e0b Mon Sep 17 00:00:00 2001
From: klopez4212
Date: Fri, 12 Jun 2026 15:06:54 +0100
Subject: [PATCH 2/7] Normalize desktop icon sizing
---
desktop/src/app/AppTopChrome.tsx | 39 ++++++++++++++-----
.../agent-memory/ui/MemorySection.tsx | 8 ++--
.../agents/ui/AgentSessionToolItem.tsx | 4 +-
.../agents/ui/AgentSessionTranscriptList.tsx | 10 ++---
.../features/agents/ui/BatchImportDialog.tsx | 6 +--
desktop/src/features/agents/ui/CopyButton.tsx | 2 +-
.../agents/ui/ManagedAgentSessionPanel.tsx | 15 ++++---
.../src/features/agents/ui/ModelPicker.tsx | 4 +-
.../ui/PersonaCatalogSelectionBadge.tsx | 2 +-
.../src/features/agents/ui/PersonaDialog.tsx | 4 +-
.../features/agents/ui/PersonaIdentity.tsx | 2 +-
.../src/features/agents/ui/RespondToField.tsx | 2 +-
desktop/src/features/agents/ui/TeamDialog.tsx | 4 +-
.../features/agents/ui/TeamImportDialog.tsx | 2 +-
.../src/features/agents/ui/TeamsSection.tsx | 6 +--
.../channels/ui/AddChannelBotDialog.tsx | 2 +-
.../ui/AddChannelBotPersonasSection.tsx | 4 +-
.../channels/ui/AgentSessionThreadPanel.tsx | 2 +-
.../features/channels/ui/BotActivityBar.tsx | 4 +-
.../channels/ui/ChannelManagementSheet.tsx | 2 +-
.../channels/ui/ChannelMemberInviteCard.tsx | 2 +-
.../channels/ui/ChannelScreenHeader.tsx | 2 +-
.../src/features/channels/ui/QuickBotBar.tsx | 2 +-
desktop/src/features/chat/ui/ChatHeader.tsx | 8 ++--
.../features/forum/ui/DeleteActionMenu.tsx | 8 +---
.../forum/ui/ForumComposerCompactLayout.tsx | 7 ++--
.../src/features/forum/ui/ForumPostCard.tsx | 2 +-
.../src/features/home/ui/InboxDetailPane.tsx | 10 +----
.../src/features/home/ui/InboxListPane.tsx | 4 +-
.../features/home/ui/RecentNotesSection.tsx | 2 +-
.../huddle/components/HeadphonesNotice.tsx | 2 +-
.../features/huddle/components/HuddleBar.tsx | 2 +-
.../huddle/components/MicControls.tsx | 8 ++--
.../ui/MeshComputeSettingsCard.tsx | 2 +-
.../mesh-compute/ui/RelayMeshAgentSection.tsx | 2 +-
.../messages/ui/ComposerAttachments.tsx | 6 +--
.../src/features/messages/ui/DiffMessage.tsx | 2 +-
.../messages/ui/DiffMessageExpanded.tsx | 4 +-
.../messages/ui/FormattingToolbar.tsx | 4 +-
.../messages/ui/MentionAutocomplete.tsx | 2 +-
.../messages/ui/TypingIndicatorRow.tsx | 2 +-
.../src/features/onboarding/ui/AvatarStep.tsx | 2 +-
.../onboarding/ui/MembershipDenied.tsx | 11 ++++--
.../onboarding/ui/NostrKeyImportForm.tsx | 2 +-
.../features/onboarding/ui/ProfileStep.tsx | 3 +-
.../src/features/onboarding/ui/SetupStep.tsx | 2 +-
.../src/features/profile/ui/AvatarUpload.tsx | 12 ++++--
.../profile/ui/ProfileAvatarEditor.tsx | 15 +++++--
.../profile/ui/UserProfilePanelSections.tsx | 4 +-
.../profile/ui/UserProfilePopover.tsx | 2 +-
.../projects/ui/ProjectDetailScreen.tsx | 18 ++++-----
.../src/features/projects/ui/ProjectsView.tsx | 6 +--
.../features/pulse/ui/AgentActivityCard.tsx | 6 +--
desktop/src/features/pulse/ui/NoteCard.tsx | 2 +-
desktop/src/features/pulse/ui/PulseView.tsx | 2 +-
.../relay-members/ui/RelayMembersCard.tsx | 4 +-
.../ui/RelayMembersSettingsCard.tsx | 7 +++-
.../src/features/settings/UpdateIndicator.tsx | 25 +++++++-----
.../ui/ChannelTemplatesSettingsCard.tsx | 12 +++---
.../settings/ui/DoctorSettingsPanel.tsx | 6 +--
.../settings/ui/MobilePairingCard.tsx | 4 +-
.../settings/ui/NotificationSettingsCard.tsx | 2 +-
.../settings/ui/ProfileSettingsCard.tsx | 6 +--
.../features/settings/ui/SettingsPanels.tsx | 5 ++-
.../src/features/settings/ui/SoundPicker.tsx | 4 +-
.../sidebar/ui/CustomChannelSection.tsx | 10 ++---
.../features/sidebar/ui/MoreUnreadButton.tsx | 2 +-
.../features/sidebar/ui/SidebarSection.tsx | 2 +-
.../features/workflows/ui/ChannelCombobox.tsx | 10 +++--
.../workflows/ui/WorkflowApprovalCard.tsx | 4 +-
.../features/workflows/ui/WorkflowCard.tsx | 2 +-
.../workflows/ui/WorkflowDetailPanel.tsx | 4 +-
.../workflows/ui/WorkflowFormBuilder.tsx | 4 +-
.../workflows/ui/WorkflowStepCard.tsx | 2 +-
.../ui/WorkflowWebhookHeadersEditor.tsx | 4 +-
.../workflows/ui/workflowFormPrimitives.tsx | 2 +-
.../workspaces/ui/WorkspaceSwitcher.tsx | 14 +++----
desktop/src/shared/ui/VideoPlayer.tsx | 4 +-
desktop/src/shared/ui/import-status-icon.tsx | 4 +-
desktop/src/shared/ui/markdown.tsx | 4 +-
web/src/features/repos/ui/RepoBlobViewer.tsx | 2 +-
web/src/features/repos/ui/RepoRefsSection.tsx | 8 ++--
web/src/features/repos/ui/ReposPage.tsx | 4 +-
83 files changed, 251 insertions(+), 204 deletions(-)
diff --git a/desktop/src/app/AppTopChrome.tsx b/desktop/src/app/AppTopChrome.tsx
index bfe27655f..739c03ecc 100644
--- a/desktop/src/app/AppTopChrome.tsx
+++ b/desktop/src/app/AppTopChrome.tsx
@@ -2,8 +2,10 @@ import { ChevronLeft, ChevronRight } from "lucide-react";
import { TopbarSearch } from "@/features/search/ui/TopbarSearch";
import type { Channel, SearchHit } from "@/shared/api/types";
+import { cn } from "@/shared/lib/cn";
import { Button } from "@/shared/ui/button";
import { SidebarTrigger, useSidebar } from "@/shared/ui/sidebar";
+import { Skeleton } from "@/shared/ui/skeleton";
type AppTopChromeProps = {
canGoBack: boolean;
@@ -16,6 +18,7 @@ type AppTopChromeProps = {
onOpenResult: (hit: SearchHit) => void;
searchHidden?: boolean;
searchFocusRequest: number;
+ searchLoading?: boolean;
};
function GlobalTopDivider() {
@@ -39,6 +42,7 @@ function CenterColumnTopbarSearch({
onOpenChannel,
onOpenResult,
searchFocusRequest,
+ searchLoading = false,
}: Pick<
AppTopChromeProps,
| "channels"
@@ -46,8 +50,11 @@ function CenterColumnTopbarSearch({
| "onOpenChannel"
| "onOpenResult"
| "searchFocusRequest"
+ | "searchLoading"
>) {
const { isResizing, state } = useSidebar();
+ const searchClassName =
+ "pointer-events-auto w-[220px] max-w-full md:w-[300px] lg:w-[360px] xl:w-[420px] 2xl:w-[480px]";
return (
-
+ {searchLoading ? (
+
+
+
+ ) : (
+
+ )}
);
}
const TOP_CHROME_ICON_BUTTON_CLASS =
- "rounded-[4px] text-muted-foreground/70 hover:bg-border/45 hover:text-foreground";
+ "h-7 w-7 rounded-[4px] text-muted-foreground/70 hover:bg-border/45 hover:text-foreground [&_svg]:size-4";
export function AppTopChrome({
canGoBack,
@@ -85,6 +102,7 @@ export function AppTopChrome({
onOpenResult,
searchHidden = false,
searchFocusRequest,
+ searchLoading = false,
}: AppTopChromeProps) {
return (
<>
@@ -94,7 +112,7 @@ export function AppTopChrome({
data-tauri-drag-region
/>
-
+
)}
>
diff --git a/desktop/src/features/agent-memory/ui/MemorySection.tsx b/desktop/src/features/agent-memory/ui/MemorySection.tsx
index 0b2d4f4a4..46ff006e5 100644
--- a/desktop/src/features/agent-memory/ui/MemorySection.tsx
+++ b/desktop/src/features/agent-memory/ui/MemorySection.tsx
@@ -85,7 +85,7 @@ export function MemoryRefreshButton({
>
@@ -169,7 +169,7 @@ function MemoryErrorState({
role="alert"
>
-
+
Couldn't load memory
@@ -196,7 +196,7 @@ function MemoryStaleErrorBanner({ onRetry }: { onRetry: () => void }) {
className="mb-2 flex items-center gap-2 rounded-md border border-warning/30 bg-warning/5 px-2 py-1.5 text-xs"
data-testid="agent-memory-stale-error"
>
-
+
Refresh failed.
{hasDanglingRefs ? (
-
+
) : null}
diff --git a/desktop/src/features/agents/ui/AgentSessionToolItem.tsx b/desktop/src/features/agents/ui/AgentSessionToolItem.tsx
index 09411f2e6..78dd98304 100644
--- a/desktop/src/features/agents/ui/AgentSessionToolItem.tsx
+++ b/desktop/src/features/agents/ui/AgentSessionToolItem.tsx
@@ -81,7 +81,7 @@ export function ToolItem({
) : null}
-
+
{action.label}
{action.value}
-
+
);
}
diff --git a/desktop/src/features/agents/ui/AgentSessionTranscriptList.tsx b/desktop/src/features/agents/ui/AgentSessionTranscriptList.tsx
index ef11f463f..2f1f47e1f 100644
--- a/desktop/src/features/agents/ui/AgentSessionTranscriptList.tsx
+++ b/desktop/src/features/agents/ui/AgentSessionTranscriptList.tsx
@@ -27,7 +27,7 @@ export function AgentSessionTranscriptList({
if (items.length === 0) {
return (
-
+
No ACP activity yet
{emptyDescription}
@@ -126,7 +126,7 @@ function MessageItem({
{isAssistant ? (
-
+
{agentName}
@@ -163,7 +163,7 @@ function ThoughtItem({
{item.title}
-
+
@@ -186,7 +186,7 @@ function MetadataItem({
{item.sections.length} section{item.sections.length === 1 ? "" : "s"}
-
+
{item.sections.map((section) => (
@@ -196,7 +196,7 @@ function MetadataItem({
>
{section.title}
-
+
{section.body.trim() || "No metadata."}
diff --git a/desktop/src/features/agents/ui/BatchImportDialog.tsx b/desktop/src/features/agents/ui/BatchImportDialog.tsx
index 7d09089cb..5e0ba0f6c 100644
--- a/desktop/src/features/agents/ui/BatchImportDialog.tsx
+++ b/desktop/src/features/agents/ui/BatchImportDialog.tsx
@@ -213,12 +213,12 @@ export function BatchImportDialog({
onClick={() => setSkippedExpanded((prev) => !prev)}
type="button"
>
-
+
{skipped.length} file{skipped.length !== 1 ? "s" : ""} skipped
{skippedExpanded ? (
-
+
) : (
-
+
)}
{skippedExpanded ? (
diff --git a/desktop/src/features/agents/ui/CopyButton.tsx b/desktop/src/features/agents/ui/CopyButton.tsx
index fa8183142..a8d0b621f 100644
--- a/desktop/src/features/agents/ui/CopyButton.tsx
+++ b/desktop/src/features/agents/ui/CopyButton.tsx
@@ -20,7 +20,7 @@ export function CopyButton({
type="button"
variant="outline"
>
-
+
{label ?? "Copy"}
);
diff --git a/desktop/src/features/agents/ui/ManagedAgentSessionPanel.tsx b/desktop/src/features/agents/ui/ManagedAgentSessionPanel.tsx
index 4ff97a5bb..3defb7d2b 100644
--- a/desktop/src/features/agents/ui/ManagedAgentSessionPanel.tsx
+++ b/desktop/src/features/agents/ui/ManagedAgentSessionPanel.tsx
@@ -3,7 +3,6 @@ import {
CircleAlert,
CircleDot,
Clock3,
- Loader2,
TerminalSquare,
XCircle,
} from "lucide-react";
@@ -14,6 +13,7 @@ import type { ManagedAgent } from "@/shared/api/types";
import { cn } from "@/shared/lib/cn";
import { Badge } from "@/shared/ui/badge";
import { Skeleton } from "@/shared/ui/skeleton";
+import { Spinner } from "@/shared/ui/spinner";
import { AgentSessionTranscriptList } from "./AgentSessionTranscriptList";
import { RawEventRail } from "./RawEventRail";
import type {
@@ -229,7 +229,7 @@ function ObserverStatusBadge({ state }: { state: ConnectionState }) {
state === "open"
? { label: "Live", Icon: CircleDot, variant: "default" as const }
: state === "connecting"
- ? { label: "Connecting", Icon: Loader2, variant: "secondary" as const }
+ ? { label: "Connecting", variant: "secondary" as const }
: state === "error"
? {
label: "Unavailable",
@@ -239,12 +239,15 @@ function ObserverStatusBadge({ state }: { state: ConnectionState }) {
: state === "closed"
? { label: "Closed", Icon: Clock3, variant: "secondary" as const }
: { label: "Idle", Icon: Clock3, variant: "secondary" as const };
+ const StatusIcon = display.Icon;
return (
-
+ {StatusIcon ? (
+
+ ) : (
+
+ )}
{display.label}
);
@@ -253,7 +256,7 @@ function ObserverStatusBadge({ state }: { state: ConnectionState }) {
function EmptyObserverState() {
return (
-
+
Observer not attached
The live feed is available for local agents started after this update.
diff --git a/desktop/src/features/agents/ui/ModelPicker.tsx b/desktop/src/features/agents/ui/ModelPicker.tsx
index 783b8db54..3e16814f6 100644
--- a/desktop/src/features/agents/ui/ModelPicker.tsx
+++ b/desktop/src/features/agents/ui/ModelPicker.tsx
@@ -93,7 +93,7 @@ export function ModelPicker({
variant="ghost"
>
{displayLabel}
-
+
{loading ? (
-
+
Loading models...
) : error ? (
diff --git a/desktop/src/features/agents/ui/PersonaCatalogSelectionBadge.tsx b/desktop/src/features/agents/ui/PersonaCatalogSelectionBadge.tsx
index fb0058424..0dcd58669 100644
--- a/desktop/src/features/agents/ui/PersonaCatalogSelectionBadge.tsx
+++ b/desktop/src/features/agents/ui/PersonaCatalogSelectionBadge.tsx
@@ -20,7 +20,7 @@ export function PersonaCatalogSelectionBadge({
: "border border-border/70 bg-background/85 text-muted-foreground",
)}
>
- {isActive ? : null}
+ {isActive ? : null}
{isActive
? personaCatalogCopy.selectedState
: personaCatalogCopy.availableState}
diff --git a/desktop/src/features/agents/ui/PersonaDialog.tsx b/desktop/src/features/agents/ui/PersonaDialog.tsx
index 4393187f5..e2e42a14a 100644
--- a/desktop/src/features/agents/ui/PersonaDialog.tsx
+++ b/desktop/src/features/agents/ui/PersonaDialog.tsx
@@ -497,12 +497,12 @@ export function PersonaDialog({
: undefined
}
>
-
+
{importButtonLabel}
{isImportingUpdate ? (
-
+
) : null}
>
diff --git a/desktop/src/features/agents/ui/PersonaIdentity.tsx b/desktop/src/features/agents/ui/PersonaIdentity.tsx
index 3b129cc42..eb2c5c025 100644
--- a/desktop/src/features/agents/ui/PersonaIdentity.tsx
+++ b/desktop/src/features/agents/ui/PersonaIdentity.tsx
@@ -46,7 +46,7 @@ export function PersonaIdentity({
className="flex h-4 w-4 shrink-0 items-center justify-center text-muted-foreground transition-colors hover:text-foreground"
type="button"
>
-
+
diff --git a/desktop/src/features/agents/ui/RespondToField.tsx b/desktop/src/features/agents/ui/RespondToField.tsx
index 2e903bfeb..055022a8d 100644
--- a/desktop/src/features/agents/ui/RespondToField.tsx
+++ b/desktop/src/features/agents/ui/RespondToField.tsx
@@ -258,7 +258,7 @@ function AllowlistPicker({
onClick={() => onRemove(pubkey)}
type="button"
>
-
+
))}
diff --git a/desktop/src/features/agents/ui/TeamDialog.tsx b/desktop/src/features/agents/ui/TeamDialog.tsx
index 06421bc51..2e2eb2856 100644
--- a/desktop/src/features/agents/ui/TeamDialog.tsx
+++ b/desktop/src/features/agents/ui/TeamDialog.tsx
@@ -475,12 +475,12 @@ export function TeamDialog({
: undefined
}
>
-
+
{importButtonLabel}
{isImportingUpdate ? (
-
+
) : null}
>
diff --git a/desktop/src/features/agents/ui/TeamImportDialog.tsx b/desktop/src/features/agents/ui/TeamImportDialog.tsx
index e29d8e486..021548ea5 100644
--- a/desktop/src/features/agents/ui/TeamImportDialog.tsx
+++ b/desktop/src/features/agents/ui/TeamImportDialog.tsx
@@ -130,7 +130,7 @@ export function TeamImportDialog({
{preview ? (
-
+
{preview.name}
diff --git a/desktop/src/features/agents/ui/TeamsSection.tsx b/desktop/src/features/agents/ui/TeamsSection.tsx
index bcbcd7e10..145fc22f4 100644
--- a/desktop/src/features/agents/ui/TeamsSection.tsx
+++ b/desktop/src/features/agents/ui/TeamsSection.tsx
@@ -108,7 +108,7 @@ export function TeamsSection({
onClick={onInstallFromDirectory}
type="button"
>
-
+
Install from directory
-
+
@@ -184,7 +184,7 @@ export function TeamsSection({
className="flex h-4 w-4 shrink-0 items-center justify-center text-muted-foreground transition-colors hover:text-foreground"
type="button"
>
-
+
diff --git a/desktop/src/features/channels/ui/AddChannelBotDialog.tsx b/desktop/src/features/channels/ui/AddChannelBotDialog.tsx
index 139931e63..403fa84c1 100644
--- a/desktop/src/features/channels/ui/AddChannelBotDialog.tsx
+++ b/desktop/src/features/channels/ui/AddChannelBotDialog.tsx
@@ -530,7 +530,7 @@ export function AddChannelBotDialog({
variant="ghost"
>
{runtimeTriggerLabel}
-
+
) : null}
@@ -193,7 +193,7 @@ export function AddChannelBotPersonasSection({
{persona.displayName}
diff --git a/desktop/src/features/channels/ui/AgentSessionThreadPanel.tsx b/desktop/src/features/channels/ui/AgentSessionThreadPanel.tsx
index c35be5657..ae633bbf3 100644
--- a/desktop/src/features/channels/ui/AgentSessionThreadPanel.tsx
+++ b/desktop/src/features/channels/ui/AgentSessionThreadPanel.tsx
@@ -102,7 +102,7 @@ export function AgentSessionThreadPanel({
type="button"
variant="outline"
>
-
+
Stop
diff --git a/desktop/src/features/channels/ui/BotActivityBar.tsx b/desktop/src/features/channels/ui/BotActivityBar.tsx
index d4ee6ff1e..a289200ad 100644
--- a/desktop/src/features/channels/ui/BotActivityBar.tsx
+++ b/desktop/src/features/channels/ui/BotActivityBar.tsx
@@ -201,7 +201,7 @@ export function BotActivityComposerAction({
{isInline ? {visibleStatusLabel} : "working"}
{isInline ? null : (
-
+
)}
@@ -244,7 +244,7 @@ export function BotActivityComposerAction({
displayName={agent.name}
/>
{agent.name}
-
+
);
})}
diff --git a/desktop/src/features/channels/ui/ChannelManagementSheet.tsx b/desktop/src/features/channels/ui/ChannelManagementSheet.tsx
index cda00f0cf..e708dfb9b 100644
--- a/desktop/src/features/channels/ui/ChannelManagementSheet.tsx
+++ b/desktop/src/features/channels/ui/ChannelManagementSheet.tsx
@@ -79,7 +79,7 @@ function MetadataPill({
}) {
return (
-
+
{label}
);
diff --git a/desktop/src/features/channels/ui/ChannelMemberInviteCard.tsx b/desktop/src/features/channels/ui/ChannelMemberInviteCard.tsx
index 7f093294d..4a06ff83e 100644
--- a/desktop/src/features/channels/ui/ChannelMemberInviteCard.tsx
+++ b/desktop/src/features/channels/ui/ChannelMemberInviteCard.tsx
@@ -234,7 +234,7 @@ export function ChannelMemberInviteCard({
}}
type="button"
>
-
+
))}
diff --git a/desktop/src/features/channels/ui/ChannelScreenHeader.tsx b/desktop/src/features/channels/ui/ChannelScreenHeader.tsx
index a873069f7..a8dbeda72 100644
--- a/desktop/src/features/channels/ui/ChannelScreenHeader.tsx
+++ b/desktop/src/features/channels/ui/ChannelScreenHeader.tsx
@@ -60,7 +60,7 @@ export function ChannelScreenHeader({
size="sm"
variant="default"
>
-
+
{isJoining ? "Joining…" : "Join"}
) : (
diff --git a/desktop/src/features/channels/ui/QuickBotBar.tsx b/desktop/src/features/channels/ui/QuickBotBar.tsx
index b4eec62c0..3d4d24385 100644
--- a/desktop/src/features/channels/ui/QuickBotBar.tsx
+++ b/desktop/src/features/channels/ui/QuickBotBar.tsx
@@ -81,7 +81,7 @@ export function QuickBotBar({ personas, pending, onAdd }: QuickBotBarProps) {
)}
{isThisPending ? (
-
+
) : null}
diff --git a/desktop/src/features/chat/ui/ChatHeader.tsx b/desktop/src/features/chat/ui/ChatHeader.tsx
index ba027edaa..90b6c4e7d 100644
--- a/desktop/src/features/chat/ui/ChatHeader.tsx
+++ b/desktop/src/features/chat/ui/ChatHeader.tsx
@@ -32,8 +32,8 @@ type ChatHeaderProps = {
statusBadge?: React.ReactNode;
};
-const HEADER_ICON_CLASS = "h-[14px] w-[14px] text-muted-foreground";
-const CHANNEL_HASH_ICON_CLASS = "h-[14px] w-[14px] translate-y-px";
+const HEADER_ICON_CLASS = "h-4 w-4 text-muted-foreground";
+const CHANNEL_HASH_ICON_CLASS = "h-4 w-4 translate-y-px";
function ChannelIcon({
channelType,
@@ -98,12 +98,12 @@ export function ChatHeader({
const header = (
diff --git a/desktop/src/features/forum/ui/ForumComposerCompactLayout.tsx b/desktop/src/features/forum/ui/ForumComposerCompactLayout.tsx
index e482e0142..848dd7964 100644
--- a/desktop/src/features/forum/ui/ForumComposerCompactLayout.tsx
+++ b/desktop/src/features/forum/ui/ForumComposerCompactLayout.tsx
@@ -5,6 +5,7 @@ import { Plus } from "lucide-react";
import { cn } from "@/shared/lib/cn";
import { Button } from "@/shared/ui/button";
+import { Spinner } from "@/shared/ui/spinner";
type ForumComposerCompactLayoutProps = {
editor: Editor | null;
@@ -45,12 +46,12 @@ export function ForumComposerCompactLayout({
variant="ghost"
>
{isSending ? (
-
) : (
-
+
)}
diff --git a/desktop/src/features/forum/ui/ForumPostCard.tsx b/desktop/src/features/forum/ui/ForumPostCard.tsx
index 92633a0d1..2ce878aee 100644
--- a/desktop/src/features/forum/ui/ForumPostCard.tsx
+++ b/desktop/src/features/forum/ui/ForumPostCard.tsx
@@ -125,7 +125,7 @@ export function ForumPostCard({
{summary && summary.replyCount > 0 ? (
-
+
{summary.replyCount}{" "}
{summary.replyCount === 1 ? "reply" : "replies"}
diff --git a/desktop/src/features/home/ui/InboxDetailPane.tsx b/desktop/src/features/home/ui/InboxDetailPane.tsx
index 2104811b1..8b504744f 100644
--- a/desktop/src/features/home/ui/InboxDetailPane.tsx
+++ b/desktop/src/features/home/ui/InboxDetailPane.tsx
@@ -264,10 +264,7 @@ export function InboxDetailPane({
type="button"
>
{hasChannelContext ? (
-
+
) : null}
{contextLabel}
@@ -279,10 +276,7 @@ export function InboxDetailPane({
title={item.fullTimestampLabel}
>
{hasChannelContext ? (
-
+
) : null}
{contextLabel}
diff --git a/desktop/src/features/home/ui/InboxListPane.tsx b/desktop/src/features/home/ui/InboxListPane.tsx
index f3f2af662..ac3e59b1b 100644
--- a/desktop/src/features/home/ui/InboxListPane.tsx
+++ b/desktop/src/features/home/ui/InboxListPane.tsx
@@ -59,7 +59,7 @@ export function InboxListPane({
-
+
Inbox
@@ -73,7 +73,7 @@ export function InboxListPane({
variant="outline"
>
{activeFilter?.label ?? "All"}
-
+
diff --git a/desktop/src/features/home/ui/RecentNotesSection.tsx b/desktop/src/features/home/ui/RecentNotesSection.tsx
index d786af379..2bacbc838 100644
--- a/desktop/src/features/home/ui/RecentNotesSection.tsx
+++ b/desktop/src/features/home/ui/RecentNotesSection.tsx
@@ -67,7 +67,7 @@ export function RecentNotesSection({
size="sm"
/>
{isAgent ? (
-
+
) : null}
diff --git a/desktop/src/features/huddle/components/HeadphonesNotice.tsx b/desktop/src/features/huddle/components/HeadphonesNotice.tsx
index de588bed7..17a8f343c 100644
--- a/desktop/src/features/huddle/components/HeadphonesNotice.tsx
+++ b/desktop/src/features/huddle/components/HeadphonesNotice.tsx
@@ -24,7 +24,7 @@ export function HeadphonesNotice({ onDismiss }: { onDismiss: () => void }) {
data-testid="huddle-headphones-notice"
className="flex items-center gap-1.5 rounded bg-amber-500/10 px-2 py-1 text-xs text-amber-700 dark:text-amber-300"
>
-
+
Headphones recommended — echo cancellation lands in the next release.
diff --git a/desktop/src/features/huddle/components/HuddleBar.tsx b/desktop/src/features/huddle/components/HuddleBar.tsx
index fb32276b7..d28895185 100644
--- a/desktop/src/features/huddle/components/HuddleBar.tsx
+++ b/desktop/src/features/huddle/components/HuddleBar.tsx
@@ -304,7 +304,7 @@ export function HuddleBar({ className }: HuddleBarProps) {
size="icon"
variant="ghost"
>
-
+
diff --git a/desktop/src/features/huddle/components/MicControls.tsx b/desktop/src/features/huddle/components/MicControls.tsx
index 5730144f5..7cd5d648d 100644
--- a/desktop/src/features/huddle/components/MicControls.tsx
+++ b/desktop/src/features/huddle/components/MicControls.tsx
@@ -67,7 +67,7 @@ export function MicControls({
size="icon"
variant="secondary"
>
-
+
@@ -151,7 +151,7 @@ export function SpeakerControls({
size="icon"
variant="secondary"
>
-
+
@@ -192,7 +192,7 @@ export function DeviceList({
type="button"
>
System default
@@ -207,7 +207,7 @@ export function DeviceList({
type="button"
>
{d.label}
diff --git a/desktop/src/features/mesh-compute/ui/MeshComputeSettingsCard.tsx b/desktop/src/features/mesh-compute/ui/MeshComputeSettingsCard.tsx
index 3af762234..edb08e131 100644
--- a/desktop/src/features/mesh-compute/ui/MeshComputeSettingsCard.tsx
+++ b/desktop/src/features/mesh-compute/ui/MeshComputeSettingsCard.tsx
@@ -244,7 +244,7 @@ export function MeshComputeSettingsCard() {
diff --git a/desktop/src/features/mesh-compute/ui/RelayMeshAgentSection.tsx b/desktop/src/features/mesh-compute/ui/RelayMeshAgentSection.tsx
index 3a9fdb0b4..c0ac1235d 100644
--- a/desktop/src/features/mesh-compute/ui/RelayMeshAgentSection.tsx
+++ b/desktop/src/features/mesh-compute/ui/RelayMeshAgentSection.tsx
@@ -166,7 +166,7 @@ export function RelayMeshAgentSection({
) : null}
{overrides.length > 0 ? (
-
+
Using Relay mesh overrides this agent's {overrides.join(", ")}.
diff --git a/desktop/src/features/messages/ui/ComposerAttachments.tsx b/desktop/src/features/messages/ui/ComposerAttachments.tsx
index 2444895a2..8030c173c 100644
--- a/desktop/src/features/messages/ui/ComposerAttachments.tsx
+++ b/desktop/src/features/messages/ui/ComposerAttachments.tsx
@@ -135,7 +135,7 @@ export const ComposerAttachments = React.memo(function ComposerAttachments({
className="group relative"
>
-
+
{label}
@@ -186,7 +186,7 @@ export const ComposerAttachments = React.memo(function ComposerAttachments({
)}
) : (
@@ -231,7 +231,7 @@ export const ComposerAttachments = React.memo(function ComposerAttachments({
/>
)}
-
+
Close
diff --git a/desktop/src/features/messages/ui/DiffMessage.tsx b/desktop/src/features/messages/ui/DiffMessage.tsx
index ca5336dfc..9189e6309 100644
--- a/desktop/src/features/messages/ui/DiffMessage.tsx
+++ b/desktop/src/features/messages/ui/DiffMessage.tsx
@@ -86,7 +86,7 @@ export default function DiffMessage({
type="button"
variant="ghost"
>
-
+
Expand diff
diff --git a/desktop/src/features/messages/ui/DiffMessageExpanded.tsx b/desktop/src/features/messages/ui/DiffMessageExpanded.tsx
index fa7ec24ba..15ce1703e 100644
--- a/desktop/src/features/messages/ui/DiffMessageExpanded.tsx
+++ b/desktop/src/features/messages/ui/DiffMessageExpanded.tsx
@@ -46,7 +46,7 @@ export default function DiffMessageExpanded({
type="button"
variant={viewType === "unified" ? "secondary" : "ghost"}
>
-
+
Unified
-
+
Split
diff --git a/desktop/src/features/messages/ui/FormattingToolbar.tsx b/desktop/src/features/messages/ui/FormattingToolbar.tsx
index fab4a1bfd..368ef5ea4 100644
--- a/desktop/src/features/messages/ui/FormattingToolbar.tsx
+++ b/desktop/src/features/messages/ui/FormattingToolbar.tsx
@@ -212,13 +212,13 @@ export const FormattingToolbar = React.memo(function FormattingToolbar({
"hover:bg-muted hover:text-foreground",
"focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring",
"disabled:pointer-events-none disabled:opacity-50",
- "[&_svg]:pointer-events-none [&_svg]:size-3.5 [&_svg]:shrink-0",
+ "[&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
item.active
? "bg-primary text-primary-foreground"
: "bg-transparent text-muted-foreground",
)}
>
-
+
diff --git a/desktop/src/features/messages/ui/MentionAutocomplete.tsx b/desktop/src/features/messages/ui/MentionAutocomplete.tsx
index c9a5528cd..94a9fcb2d 100644
--- a/desktop/src/features/messages/ui/MentionAutocomplete.tsx
+++ b/desktop/src/features/messages/ui/MentionAutocomplete.tsx
@@ -98,7 +98,7 @@ export const MentionAutocomplete = React.memo(function MentionAutocomplete({
>
{agentLabel}
diff --git a/desktop/src/features/messages/ui/TypingIndicatorRow.tsx b/desktop/src/features/messages/ui/TypingIndicatorRow.tsx
index bd68bb042..890407153 100644
--- a/desktop/src/features/messages/ui/TypingIndicatorRow.tsx
+++ b/desktop/src/features/messages/ui/TypingIndicatorRow.tsx
@@ -117,7 +117,7 @@ export function TypingIndicatorRow({
: "h-5 w-5 text-[8px]",
)}
iconClassName={
- isActivityVariant ? "h-2.5 w-2.5" : "h-3 w-3"
+ isActivityVariant ? "h-2.5 w-2.5" : "h-4 w-4"
}
/>
diff --git a/desktop/src/features/onboarding/ui/AvatarStep.tsx b/desktop/src/features/onboarding/ui/AvatarStep.tsx
index a99c69013..d339c1d62 100644
--- a/desktop/src/features/onboarding/ui/AvatarStep.tsx
+++ b/desktop/src/features/onboarding/ui/AvatarStep.tsx
@@ -167,7 +167,7 @@ function AvatarStepActions({
{isSaving || isUploadingAvatar ? (
) : (
"Next"
diff --git a/desktop/src/features/onboarding/ui/MembershipDenied.tsx b/desktop/src/features/onboarding/ui/MembershipDenied.tsx
index 4f0618f94..a3bfc5c87 100644
--- a/desktop/src/features/onboarding/ui/MembershipDenied.tsx
+++ b/desktop/src/features/onboarding/ui/MembershipDenied.tsx
@@ -89,7 +89,7 @@ export function MembershipDenied({
Membership required
-
+
Not a member yet
@@ -172,7 +172,7 @@ export function MembershipDenied({
className="flex items-start gap-2 rounded-md border border-primary/30 bg-primary/5 px-3 py-2 text-xs"
data-testid="membership-denied-npub-preview"
>
-
+
This will use this Nostr identity:
@@ -197,7 +197,10 @@ export function MembershipDenied({
type="submit"
>
{isImportingKey ? (
-
+
) : (
"Import key"
)}
@@ -236,7 +239,7 @@ export function MembershipDenied({
}}
type="button"
>
-
+
Use a different key
) : null}
diff --git a/desktop/src/features/onboarding/ui/NostrKeyImportForm.tsx b/desktop/src/features/onboarding/ui/NostrKeyImportForm.tsx
index b251fb9e3..a31e45592 100644
--- a/desktop/src/features/onboarding/ui/NostrKeyImportForm.tsx
+++ b/desktop/src/features/onboarding/ui/NostrKeyImportForm.tsx
@@ -222,7 +222,7 @@ export function NostrKeyImportForm({
className="flex items-start gap-2 rounded-md border border-primary/30 bg-primary/5 px-3 py-2 text-xs"
data-testid="nostr-import-npub-preview"
>
-
+
);
}
diff --git a/desktop/src/features/profile/ui/AvatarUpload.tsx b/desktop/src/features/profile/ui/AvatarUpload.tsx
index 81a5c83cf..dd371a398 100644
--- a/desktop/src/features/profile/ui/AvatarUpload.tsx
+++ b/desktop/src/features/profile/ui/AvatarUpload.tsx
@@ -1,9 +1,10 @@
import * as React from "react";
-import { Camera, Link2, Loader2, Upload, X } from "lucide-react";
+import { Camera, Link2, Upload, X } from "lucide-react";
import { ProfileAvatar } from "@/features/profile/ui/ProfileAvatar";
import { useAvatarUpload } from "@/features/profile/useAvatarUpload";
import { Input } from "@/shared/ui/input";
+import { Spinner } from "@/shared/ui/spinner";
type AvatarUploadProps = {
avatarUrl: string;
@@ -90,7 +91,7 @@ export function AvatarUpload({
title="Remove photo"
type="button"
>
-
+
) : (
@@ -127,9 +128,12 @@ export function AvatarUpload({
type="button"
>
{isUploading ? (
-
+
) : (
-
+
)}
{isUploading ? (
diff --git a/desktop/src/features/profile/ui/ProfileAvatarEditor.tsx b/desktop/src/features/profile/ui/ProfileAvatarEditor.tsx
index 5ae567faf..d2de41973 100644
--- a/desktop/src/features/profile/ui/ProfileAvatarEditor.tsx
+++ b/desktop/src/features/profile/ui/ProfileAvatarEditor.tsx
@@ -1,6 +1,6 @@
import emojiData from "@emoji-mart/data";
import Picker from "@emoji-mart/react";
-import { Link2, Loader2, UploadCloud } from "lucide-react";
+import { Link2, UploadCloud } from "lucide-react";
import { motion } from "motion/react";
import * as React from "react";
@@ -579,7 +579,10 @@ export function ProfileAvatarEditor({
data-testid={`${testIdPrefix}-drop-mask`}
/>
{isUploading ? (
-
+
) : (
@@ -960,7 +966,10 @@ export function ProfileAvatarEditor({
type="button"
>
{donePending ? (
-
+
) : (
"Done"
)}
diff --git a/desktop/src/features/profile/ui/UserProfilePanelSections.tsx b/desktop/src/features/profile/ui/UserProfilePanelSections.tsx
index 05a97e82f..d003ed94e 100644
--- a/desktop/src/features/profile/ui/UserProfilePanelSections.tsx
+++ b/desktop/src/features/profile/ui/UserProfilePanelSections.tsx
@@ -345,7 +345,7 @@ function ProfileHeroDescription({ about }: { about: string }) {
type="button"
>
more
-
+
) : null}
{expanded ? (
@@ -356,7 +356,7 @@ function ProfileHeroDescription({ about }: { about: string }) {
type="button"
>
less
-
+
) : null}
diff --git a/desktop/src/features/profile/ui/UserProfilePopover.tsx b/desktop/src/features/profile/ui/UserProfilePopover.tsx
index d6e3d277c..dcceb0a27 100644
--- a/desktop/src/features/profile/ui/UserProfilePopover.tsx
+++ b/desktop/src/features/profile/ui/UserProfilePopover.tsx
@@ -259,7 +259,7 @@ export function UserProfilePopover({
}}
type="button"
>
-
+
View activity log
) : null}
diff --git a/desktop/src/features/projects/ui/ProjectDetailScreen.tsx b/desktop/src/features/projects/ui/ProjectDetailScreen.tsx
index 5f48472f9..f01f383d6 100644
--- a/desktop/src/features/projects/ui/ProjectDetailScreen.tsx
+++ b/desktop/src/features/projects/ui/ProjectDetailScreen.tsx
@@ -30,7 +30,7 @@ function CloneUrlRow({ url }: { url: string }) {
return (
-
+
{url}
{copied ? (
-
+
) : (
-
+
)}
@@ -89,7 +89,7 @@ export function ProjectDetailScreen({ projectId }: ProjectDetailScreenProps) {
size="sm"
variant="ghost"
>
-
+
Back to Projects
@@ -111,7 +111,7 @@ export function ProjectDetailScreen({ projectId }: ProjectDetailScreenProps) {
size="sm"
variant="outline"
>
-
+
Back to Projects
@@ -136,7 +136,7 @@ export function ProjectDetailScreen({ projectId }: ProjectDetailScreenProps) {
size="sm"
variant="ghost"
>
-
+
Back to Projects
@@ -144,7 +144,7 @@ export function ProjectDetailScreen({ projectId }: ProjectDetailScreenProps) {
-
+
{project.name}
{project.description ? (
@@ -178,7 +178,7 @@ export function ProjectDetailScreen({ projectId }: ProjectDetailScreenProps) {
rel="noopener noreferrer"
target="_blank"
>
-
+
{project.webUrl}
@@ -188,7 +188,7 @@ export function ProjectDetailScreen({ projectId }: ProjectDetailScreenProps) {
-
+
Contributors ({project.contributors.length})
diff --git a/desktop/src/features/projects/ui/ProjectsView.tsx b/desktop/src/features/projects/ui/ProjectsView.tsx
index 098eb7238..715fe4c18 100644
--- a/desktop/src/features/projects/ui/ProjectsView.tsx
+++ b/desktop/src/features/projects/ui/ProjectsView.tsx
@@ -86,19 +86,19 @@ export function ProjectsView() {
{project.cloneUrls.length > 0 ? (
-
+
{project.cloneUrls[0]}
) : null}
{project.contributors.length > 0 ? (
-
+
{project.contributors.length}
) : null}
{project.webUrl ? (
-
+
Web
) : null}
diff --git a/desktop/src/features/pulse/ui/AgentActivityCard.tsx b/desktop/src/features/pulse/ui/AgentActivityCard.tsx
index 7fdc10977..3c2e8a7a3 100644
--- a/desktop/src/features/pulse/ui/AgentActivityCard.tsx
+++ b/desktop/src/features/pulse/ui/AgentActivityCard.tsx
@@ -66,7 +66,7 @@ export function AgentActivityCard({
type="button"
>
-
+
@@ -87,9 +87,9 @@ export function AgentActivityCard({
type="button"
>
{expanded ? (
-
+
) : (
-
+
)}
{group.notes.length} updates
diff --git a/desktop/src/features/pulse/ui/NoteCard.tsx b/desktop/src/features/pulse/ui/NoteCard.tsx
index a3e71f89f..794237667 100644
--- a/desktop/src/features/pulse/ui/NoteCard.tsx
+++ b/desktop/src/features/pulse/ui/NoteCard.tsx
@@ -171,7 +171,7 @@ export function NoteCard({
displayName={displayName}
/>
{isAgent ? (
-
+
) : null}
diff --git a/desktop/src/features/pulse/ui/PulseView.tsx b/desktop/src/features/pulse/ui/PulseView.tsx
index 9ea771b38..82aefc14d 100644
--- a/desktop/src/features/pulse/ui/PulseView.tsx
+++ b/desktop/src/features/pulse/ui/PulseView.tsx
@@ -349,7 +349,7 @@ export function PulseView({ currentPubkey }: PulseViewProps) {
className="absolute right-1.5 top-1/2 flex h-8 w-8 -translate-y-1/2 items-center justify-center rounded-full bg-foreground/10 text-foreground transition-colors hover:bg-foreground/15 dark:bg-white/85 dark:text-black dark:hover:bg-white"
type="button"
>
-
+
diff --git a/desktop/src/features/relay-members/ui/RelayMembersCard.tsx b/desktop/src/features/relay-members/ui/RelayMembersCard.tsx
index 38faefa20..ac64b108a 100644
--- a/desktop/src/features/relay-members/ui/RelayMembersCard.tsx
+++ b/desktop/src/features/relay-members/ui/RelayMembersCard.tsx
@@ -121,7 +121,7 @@ function MemberRow({
size="sm"
variant="ghost"
>
-
+
Actions
@@ -231,7 +231,7 @@ export function RelayMembersCard({
onClick={() => setAddDialogOpen(true)}
size="sm"
>
-
+
Add Member
) : null}
diff --git a/desktop/src/features/relay-members/ui/RelayMembersSettingsCard.tsx b/desktop/src/features/relay-members/ui/RelayMembersSettingsCard.tsx
index bc1c60c36..c522e209b 100644
--- a/desktop/src/features/relay-members/ui/RelayMembersSettingsCard.tsx
+++ b/desktop/src/features/relay-members/ui/RelayMembersSettingsCard.tsx
@@ -165,10 +165,10 @@ function RelayMemberRow({
{member.role === "owner" ? (
-
+
) : null}
{member.role === "admin" ? (
-
+
) : null}
{displayName}
{isSelf ? (
@@ -461,10 +461,13 @@ export function RelayMembersSettingsCard({
setSearch(event.target.value)}
placeholder="Search members by name, npub, or role…"
+ spellCheck={false}
type="text"
value={search}
/>
diff --git a/desktop/src/features/settings/UpdateIndicator.tsx b/desktop/src/features/settings/UpdateIndicator.tsx
index 97d8bb6e3..dd004286e 100644
--- a/desktop/src/features/settings/UpdateIndicator.tsx
+++ b/desktop/src/features/settings/UpdateIndicator.tsx
@@ -1,6 +1,8 @@
-import { Loader2, RefreshCcw, RotateCw } from "lucide-react";
+import type { ComponentType } from "react";
+import { RefreshCcw, RotateCw } from "lucide-react";
import { Button } from "@/shared/ui/button";
+import { Spinner } from "@/shared/ui/spinner";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip";
import { useUpdaterContext } from "./hooks/UpdaterProvider";
@@ -9,13 +11,18 @@ import type { UpdateStatus } from "./hooks/use-updater";
const indicatorButtonClass =
"relative text-muted-foreground/80 hover:bg-muted/60 hover:text-foreground";
+type IndicatorIcon = ComponentType<{
+ "aria-hidden"?: boolean;
+ className?: string;
+}>;
+
const variants: Record<
"available" | "downloading" | "installing" | "ready",
{
- Icon: typeof RefreshCcw;
+ Icon: IndicatorIcon;
+ iconClassName?: string;
label: string;
badgeColor: string;
- spin?: boolean;
}
> = {
available: {
@@ -24,16 +31,16 @@ const variants: Record<
badgeColor: "bg-primary",
},
downloading: {
- Icon: Loader2,
+ Icon: Spinner,
+ iconClassName: "h-4 w-4 border-2",
label: "Downloading update\u2026",
badgeColor: "bg-primary",
- spin: true,
},
installing: {
- Icon: Loader2,
+ Icon: Spinner,
+ iconClassName: "h-4 w-4 border-2",
label: "Installing update\u2026",
badgeColor: "bg-primary",
- spin: true,
},
ready: {
Icon: RotateCw,
@@ -62,7 +69,7 @@ export function UpdateIndicator({ className }: { className?: string }) {
return null;
}
- const { Icon, label, badgeColor, spin } = variant;
+ const { Icon, iconClassName = "h-4 w-4", label, badgeColor } = variant;
const isActionable = status.state === "available" || status.state === "ready";
const handleClick =
status.state === "ready"
@@ -87,7 +94,7 @@ export function UpdateIndicator({ className }: { className?: string }) {
type="button"
variant="ghost"
>
-
+
diff --git a/desktop/src/features/settings/ui/ChannelTemplatesSettingsCard.tsx b/desktop/src/features/settings/ui/ChannelTemplatesSettingsCard.tsx
index 0181e1928..b933c61a2 100644
--- a/desktop/src/features/settings/ui/ChannelTemplatesSettingsCard.tsx
+++ b/desktop/src/features/settings/ui/ChannelTemplatesSettingsCard.tsx
@@ -120,7 +120,7 @@ export function ChannelTemplatesSettingsCard() {
type="button"
variant="outline"
>
-
+
Create
@@ -229,13 +229,13 @@ function TemplateRow({
{agentCount > 0 ? (
-
+
{agentCount} {agentCount === 1 ? "agent" : "agents"}
) : null}
{template.canvasTemplate ? (
-
+
canvas
) : null}
@@ -255,11 +255,11 @@ function TemplateRow({
-
+
Edit
-
+
Duplicate
{!template.isBuiltin ? (
@@ -267,7 +267,7 @@ function TemplateRow({
className="text-destructive focus:text-destructive"
onClick={onDelete}
>
-
+
Delete
) : null}
diff --git a/desktop/src/features/settings/ui/DoctorSettingsPanel.tsx b/desktop/src/features/settings/ui/DoctorSettingsPanel.tsx
index a6d3044cd..4b7022baf 100644
--- a/desktop/src/features/settings/ui/DoctorSettingsPanel.tsx
+++ b/desktop/src/features/settings/ui/DoctorSettingsPanel.tsx
@@ -56,9 +56,9 @@ function InstallActions({
variant="outline"
>
{isInstalling ? (
-
+
) : (
-
+
)}
{isInstalling ? "Installing..." : "Install"}
@@ -68,7 +68,7 @@ function InstallActions({
onClick={() => void openUrl(runtime.installInstructionsUrl)}
type="button"
>
-
+
View instructions
diff --git a/desktop/src/features/settings/ui/MobilePairingCard.tsx b/desktop/src/features/settings/ui/MobilePairingCard.tsx
index 2e6c77c49..6ae0850e3 100644
--- a/desktop/src/features/settings/ui/MobilePairingCard.tsx
+++ b/desktop/src/features/settings/ui/MobilePairingCard.tsx
@@ -225,7 +225,7 @@ function PairingDialog({
type="button"
>
{qrUri}
-
+
@@ -332,7 +332,7 @@ export function MobilePairingCard({
-
+
Pair Mobile Device
diff --git a/desktop/src/features/settings/ui/NotificationSettingsCard.tsx b/desktop/src/features/settings/ui/NotificationSettingsCard.tsx
index 55a0c9ab7..05a47e03c 100644
--- a/desktop/src/features/settings/ui/NotificationSettingsCard.tsx
+++ b/desktop/src/features/settings/ui/NotificationSettingsCard.tsx
@@ -227,7 +227,7 @@ export function NotificationSettingsCard({
>
{showComingSoon ? (
<>
-
+
Show less
>
) : (
diff --git a/desktop/src/features/settings/ui/ProfileSettingsCard.tsx b/desktop/src/features/settings/ui/ProfileSettingsCard.tsx
index cfe3c9bb2..66951685c 100644
--- a/desktop/src/features/settings/ui/ProfileSettingsCard.tsx
+++ b/desktop/src/features/settings/ui/ProfileSettingsCard.tsx
@@ -57,7 +57,7 @@ function IdentityRow({
title={`Copy ${label}`}
type="button"
>
-
+
Copy
) : null}
@@ -97,7 +97,7 @@ function EditProfileMetadataButton({
title={accessibleLabel}
type="button"
>
-
+
{actionLabel}
);
@@ -577,7 +577,7 @@ export function ProfileSettingsCard({
this device.
-
+
setSearch(e.target.value)}
placeholder="Search themes..."
+ spellCheck={false}
type="text"
value={search}
/>
@@ -269,7 +272,7 @@ function ThemeSettingsCard() {
type="button"
>
{accentColor === color.value && (
-
+
)}
);
diff --git a/desktop/src/features/settings/ui/SoundPicker.tsx b/desktop/src/features/settings/ui/SoundPicker.tsx
index 5de4f450c..716290f33 100644
--- a/desktop/src/features/settings/ui/SoundPicker.tsx
+++ b/desktop/src/features/settings/ui/SoundPicker.tsx
@@ -135,9 +135,9 @@ export function SoundPicker({
variant="ghost"
>
{isPlaying ? (
-
+
) : (
-
+
)}
diff --git a/desktop/src/features/sidebar/ui/CustomChannelSection.tsx b/desktop/src/features/sidebar/ui/CustomChannelSection.tsx
index 61986d70d..28bad4293 100644
--- a/desktop/src/features/sidebar/ui/CustomChannelSection.tsx
+++ b/desktop/src/features/sidebar/ui/CustomChannelSection.tsx
@@ -255,7 +255,7 @@ function SectionHeaderActions({
title="Mark all as read"
type="button"
>
-
+
) : null}
{onBrowse ? (
@@ -548,7 +548,7 @@ export function CustomChannelSection({
>
-
+
) : null}
-
+
-
+
diff --git a/desktop/src/features/sidebar/ui/MoreUnreadButton.tsx b/desktop/src/features/sidebar/ui/MoreUnreadButton.tsx
index 5ebca7e47..22f9119f0 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-3.5";
+ "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";
export function MoreUnreadButton({
bottomClassName = "bottom-0",
diff --git a/desktop/src/features/sidebar/ui/SidebarSection.tsx b/desktop/src/features/sidebar/ui/SidebarSection.tsx
index 6d9210622..7f1998f02 100644
--- a/desktop/src/features/sidebar/ui/SidebarSection.tsx
+++ b/desktop/src/features/sidebar/ui/SidebarSection.tsx
@@ -202,7 +202,7 @@ export function ChannelMenuButton({
{isMuted ? (
{selected ? formatChannelLabel(selected) : "Select a channel..."}
-
+
-
+
el?.focus()}
className="flex-1 bg-transparent text-sm outline-hidden placeholder:text-muted-foreground"
onChange={(e) => {
@@ -119,6 +120,7 @@ export function ChannelCombobox({
}}
onKeyDown={handleKeyDown}
placeholder="Search channels..."
+ spellCheck={false}
value={query}
/>
@@ -142,7 +144,7 @@ export function ChannelCombobox({
>
diff --git a/desktop/src/features/workflows/ui/WorkflowApprovalCard.tsx b/desktop/src/features/workflows/ui/WorkflowApprovalCard.tsx
index e48f09c46..8c39b01f5 100644
--- a/desktop/src/features/workflows/ui/WorkflowApprovalCard.tsx
+++ b/desktop/src/features/workflows/ui/WorkflowApprovalCard.tsx
@@ -54,7 +54,7 @@ export function WorkflowApprovalCard({ approval }: WorkflowApprovalCardProps) {
}
size="sm"
>
-
+
Approve
-
+
Deny
diff --git a/desktop/src/features/workflows/ui/WorkflowCard.tsx b/desktop/src/features/workflows/ui/WorkflowCard.tsx
index 0a5526a4d..577bdbabd 100644
--- a/desktop/src/features/workflows/ui/WorkflowCard.tsx
+++ b/desktop/src/features/workflows/ui/WorkflowCard.tsx
@@ -89,7 +89,7 @@ export function WorkflowCard({
{channelName ?
{channelName} : null}
{triggerSummary ?
{triggerSummary} : null}
-
+
{new Date(workflow.updatedAt * 1000).toLocaleDateString()}
diff --git a/desktop/src/features/workflows/ui/WorkflowDetailPanel.tsx b/desktop/src/features/workflows/ui/WorkflowDetailPanel.tsx
index faf22857f..f610caee1 100644
--- a/desktop/src/features/workflows/ui/WorkflowDetailPanel.tsx
+++ b/desktop/src/features/workflows/ui/WorkflowDetailPanel.tsx
@@ -93,7 +93,7 @@ export function WorkflowDetailPanel({
size="sm"
variant="outline"
>
-
+
Edit
) : null}
@@ -103,7 +103,7 @@ export function WorkflowDetailPanel({
size="sm"
variant="outline"
>
-
+
{triggerMutation.isPending ? "Triggering..." : "Trigger"}
-
+
{mode === "form" ? "Edit as YAML" : "Back to form"}
@@ -335,7 +335,7 @@ export function WorkflowFormBuilder({
type="button"
variant="outline"
>
-
+
Add step
diff --git a/desktop/src/features/workflows/ui/WorkflowStepCard.tsx b/desktop/src/features/workflows/ui/WorkflowStepCard.tsx
index ac7018ff5..ffe6b3546 100644
--- a/desktop/src/features/workflows/ui/WorkflowStepCard.tsx
+++ b/desktop/src/features/workflows/ui/WorkflowStepCard.tsx
@@ -326,7 +326,7 @@ export function WorkflowStepCard({
type="button"
variant="ghost"
>
-
+
diff --git a/desktop/src/features/workflows/ui/WorkflowWebhookHeadersEditor.tsx b/desktop/src/features/workflows/ui/WorkflowWebhookHeadersEditor.tsx
index 6645ac1a2..14eb33e3d 100644
--- a/desktop/src/features/workflows/ui/WorkflowWebhookHeadersEditor.tsx
+++ b/desktop/src/features/workflows/ui/WorkflowWebhookHeadersEditor.tsx
@@ -48,7 +48,7 @@ export function WorkflowWebhookHeadersEditor({
type="button"
variant="outline"
>
-
+
Add header
@@ -109,7 +109,7 @@ export function WorkflowWebhookHeadersEditor({
type="button"
variant="ghost"
>
-
+
))}
diff --git a/desktop/src/features/workflows/ui/workflowFormPrimitives.tsx b/desktop/src/features/workflows/ui/workflowFormPrimitives.tsx
index e6173ee23..ca0f8a872 100644
--- a/desktop/src/features/workflows/ui/workflowFormPrimitives.tsx
+++ b/desktop/src/features/workflows/ui/workflowFormPrimitives.tsx
@@ -25,7 +25,7 @@ export function FormSelect({
>
{children}
-
+
);
}
diff --git a/desktop/src/features/workspaces/ui/WorkspaceSwitcher.tsx b/desktop/src/features/workspaces/ui/WorkspaceSwitcher.tsx
index 2e7e14d04..89b17a93c 100644
--- a/desktop/src/features/workspaces/ui/WorkspaceSwitcher.tsx
+++ b/desktop/src/features/workspaces/ui/WorkspaceSwitcher.tsx
@@ -121,7 +121,7 @@ export function WorkspaceSwitcher({
data-testid="relay-connection-warning"
role="img"
>
-
+
@@ -154,8 +154,8 @@ export function WorkspaceSwitcher({
)}
@@ -209,7 +209,7 @@ export function WorkspaceSwitcher({
>
{activeWorkspace?.id === workspace.id ? (
-
+
) : null}
@@ -226,7 +226,7 @@ export function WorkspaceSwitcher({
}}
type="button"
>
-
+
))}
@@ -296,7 +296,7 @@ export function WorkspaceSwitcher({
>
{activeWorkspace?.id === workspace.id ? (
-
+
) : null}
{workspace.name}
@@ -311,7 +311,7 @@ export function WorkspaceSwitcher({
}}
type="button"
>
-
+
))}
diff --git a/desktop/src/shared/ui/VideoPlayer.tsx b/desktop/src/shared/ui/VideoPlayer.tsx
index b967b00e9..5dd42bff9 100644
--- a/desktop/src/shared/ui/VideoPlayer.tsx
+++ b/desktop/src/shared/ui/VideoPlayer.tsx
@@ -1627,7 +1627,7 @@ function VideoReviewDialog({
type="button"
onClick={() => setIsEmojiPickerOpen((open) => !open)}
>
-
+
More reactions
@@ -1900,7 +1900,7 @@ function VideoReviewCommentBody({
{reactions.some((reaction) => reaction.reactedByCurrentUser) ? (
) : null}
diff --git a/desktop/src/shared/ui/import-status-icon.tsx b/desktop/src/shared/ui/import-status-icon.tsx
index 9617e44eb..aadb5b9bc 100644
--- a/desktop/src/shared/ui/import-status-icon.tsx
+++ b/desktop/src/shared/ui/import-status-icon.tsx
@@ -16,7 +16,9 @@ export function ImportStatusIcon({
}) {
switch (status) {
case "importing":
- return ;
+ return (
+
+ );
case "done":
return ;
case "error":
diff --git a/desktop/src/shared/ui/markdown.tsx b/desktop/src/shared/ui/markdown.tsx
index b21b45e73..2f4a84663 100644
--- a/desktop/src/shared/ui/markdown.tsx
+++ b/desktop/src/shared/ui/markdown.tsx
@@ -520,7 +520,7 @@ function MarkdownCodeBlock({
type="button"
variant="ghost"
>
-
+
Copy code block
@@ -579,7 +579,7 @@ function FileCard({
className="my-1 inline-flex max-w-sm items-center gap-3 rounded-xl border border-border/70 bg-muted/40 px-3 py-2 text-left no-underline transition-colors hover:bg-muted/70"
>
-
+
diff --git a/web/src/features/repos/ui/RepoBlobViewer.tsx b/web/src/features/repos/ui/RepoBlobViewer.tsx
index 0a53696d3..5adfdd2f0 100644
--- a/web/src/features/repos/ui/RepoBlobViewer.tsx
+++ b/web/src/features/repos/ui/RepoBlobViewer.tsx
@@ -258,7 +258,7 @@ export function RepoBlobPage() {
-
+
{filepath}
{view &&
diff --git a/web/src/features/repos/ui/RepoRefsSection.tsx b/web/src/features/repos/ui/RepoRefsSection.tsx
index 11b38acd7..07fdafa82 100644
--- a/web/src/features/repos/ui/RepoRefsSection.tsx
+++ b/web/src/features/repos/ui/RepoRefsSection.tsx
@@ -22,12 +22,12 @@ export function RepoRefsSection({
<>
-
+
{refs.head.ref}
{refs.head.sha && (
-
+
{refs.head.sha.slice(0, 7)}
)}
@@ -36,13 +36,13 @@ export function RepoRefsSection({
>
)}
-
+
{refs.branches.length}{" "}
{refs.branches.length === 1 ? "branch" : "branches"}
·
-
+
{refs.tags.length} {refs.tags.length === 1 ? "tag" : "tags"}
diff --git a/web/src/features/repos/ui/ReposPage.tsx b/web/src/features/repos/ui/ReposPage.tsx
index 72a972a4a..c738f769a 100644
--- a/web/src/features/repos/ui/ReposPage.tsx
+++ b/web/src/features/repos/ui/ReposPage.tsx
@@ -104,7 +104,7 @@ export function ReposPage() {
- Repositories
+ Repositories
{["a", "b", "c", "d", "e"].map((key) => (
@@ -131,7 +131,7 @@ export function ReposPage() {
- Repositories
+ Repositories
{/* Search + Sort bar */}
From 9f121b0e2bff3adce5bfe84e585fbd5727622fa7 Mon Sep 17 00:00:00 2001
From: klopez4212
Date: Fri, 12 Jun 2026 15:26:04 +0100
Subject: [PATCH 3/7] Update narrow header menu spacing expectation
---
desktop/tests/e2e/messaging.spec.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/desktop/tests/e2e/messaging.spec.ts b/desktop/tests/e2e/messaging.spec.ts
index 610f174e0..825b53b0b 100644
--- a/desktop/tests/e2e/messaging.spec.ts
+++ b/desktop/tests/e2e/messaging.spec.ts
@@ -751,8 +751,8 @@ test("narrow thread view collapses channel header actions into a menu", async ({
throw new Error("Expected header action menu and thread panel bounds");
}
const menuGapPx = threadPanelBox.x - (menuBox.x + menuBox.width);
- expect(menuGapPx).toBeGreaterThanOrEqual(10);
- expect(menuGapPx).toBeLessThanOrEqual(14);
+ expect(menuGapPx).toBeGreaterThanOrEqual(22);
+ expect(menuGapPx).toBeLessThanOrEqual(26);
await menuTrigger.click();
From 64686ef926504bcc702f36b324574cb080df2c9d Mon Sep 17 00:00:00 2001
From: klopez4212
Date: Fri, 12 Jun 2026 15:08:19 +0100
Subject: [PATCH 4/7] Polish channel modal forms
---
.../channels/ui/ChannelBrowserDialog.tsx | 535 ++++++++++--------
.../messages/lib/useRichTextEditor.ts | 3 +
.../src/features/search/ui/ChannelFindBar.tsx | 3 +
.../sidebar/ui/CreateChannelDialog.tsx | 318 +++++++----
.../sidebar/ui/NewDirectMessageDialog.tsx | 4 +-
desktop/src/shared/ui/alert-dialog.tsx | 6 +-
desktop/src/shared/ui/checkbox.tsx | 2 +-
.../src/shared/ui/chooser-dialog-content.tsx | 23 +-
desktop/src/shared/ui/dialog.tsx | 26 +-
desktop/src/shared/ui/input.tsx | 3 +
desktop/src/shared/ui/textarea.tsx | 3 +
11 files changed, 579 insertions(+), 347 deletions(-)
diff --git a/desktop/src/features/channels/ui/ChannelBrowserDialog.tsx b/desktop/src/features/channels/ui/ChannelBrowserDialog.tsx
index 2fc794bb2..22e17c007 100644
--- a/desktop/src/features/channels/ui/ChannelBrowserDialog.tsx
+++ b/desktop/src/features/channels/ui/ChannelBrowserDialog.tsx
@@ -1,52 +1,20 @@
import * as React from "react";
-import {
- Compass,
- FileText,
- Hash,
- LogIn,
- Search,
- Users,
- type LucideIcon,
-} from "lucide-react";
+import { Compass, Search, X, type LucideIcon } from "lucide-react";
import type { Channel } from "@/shared/api/types";
import {
Dialog,
+ DialogClose,
DialogContent,
- DialogDescription,
DialogHeader,
DialogTitle,
} from "@/shared/ui/dialog";
import { Badge } from "@/shared/ui/badge";
-import { Input } from "@/shared/ui/input";
import { Button } from "@/shared/ui/button";
+import { Tabs, TabsList, TabsTrigger } from "@/shared/ui/tabs";
const BROWSE_CHANNELS_SHORTCUT_HINT = "\u21E7\u2318O";
-
-function formatRelativeTime(isoString: string | null) {
- if (!isoString) {
- return "No activity";
- }
-
- const diff = Math.floor((Date.now() - new Date(isoString).getTime()) / 1_000);
-
- if (diff < 60) {
- return "just now";
- }
-
- if (diff < 60 * 60) {
- return `${Math.floor(diff / 60)}m ago`;
- }
-
- if (diff < 60 * 60 * 24) {
- return `${Math.floor(diff / (60 * 60))}h ago`;
- }
-
- return new Intl.DateTimeFormat("en-US", {
- month: "short",
- day: "numeric",
- }).format(new Date(isoString));
-}
+type BrowserTab = "all" | "joined" | "archived";
function BrowseState({
icon: Icon,
@@ -60,7 +28,7 @@ function BrowseState({
return (
-
+
{title}
@@ -88,24 +56,35 @@ export function ChannelBrowserDialog({
onSelectChannel,
}: ChannelBrowserDialogProps) {
const [query, setQuery] = React.useState("");
- const [selectedIndex, setSelectedIndex] = React.useState(0);
+ const [activeTab, setActiveTab] = React.useState("all");
+ const [selectedIndex, setSelectedIndex] = React.useState(null);
const [joiningChannelId, setJoiningChannelId] = React.useState(
null,
);
+ const contentRef = React.useRef(null);
const inputRef = React.useRef(null);
+ const tabListRef = React.useRef(null);
+ const tabTriggerRefs = React.useRef<
+ Record
+ >({
+ all: null,
+ joined: null,
+ archived: null,
+ });
+ const [tabIndicator, setTabIndicator] = React.useState({
+ left: 0,
+ width: 0,
+ });
const deferredQuery = React.useDeferredValue(query.trim().toLowerCase());
const isForumMode = channelTypeFilter === "forum";
const browseTitle = isForumMode ? "Browse Forums" : "Browse Channels";
- const browseDescription = isForumMode
- ? "Discover and join open forums."
- : "Discover and join open channels.";
const searchPlaceholder = isForumMode
? "Search forums by name or description"
: "Search channels by name or description";
const entityLabel = isForumMode ? "forum" : "channel";
- const browsableChannels = React.useMemo(() => {
+ const matchingChannels = React.useMemo(() => {
const filtered = channels.filter(
(channel) =>
channel.channelType !== "dm" &&
@@ -126,53 +105,114 @@ export function ChannelBrowserDialog({
);
}, [channels, channelTypeFilter, deferredQuery]);
- const notJoined = React.useMemo(
- () => browsableChannels.filter((channel) => !channel.isMember),
- [browsableChannels],
+ const currentChannels = React.useMemo(
+ () => matchingChannels.filter((channel) => channel.archivedAt === null),
+ [matchingChannels],
);
- const joined = React.useMemo(
- () => browsableChannels.filter((channel) => channel.isMember),
- [browsableChannels],
+ const joinedChannels = React.useMemo(
+ () => currentChannels.filter((channel) => channel.isMember),
+ [currentChannels],
);
- const hasArchivedJoinedChannels = React.useMemo(
- () => joined.some((channel) => channel.archivedAt !== null),
- [joined],
+
+ const archivedChannels = React.useMemo(
+ () => matchingChannels.filter((channel) => channel.archivedAt !== null),
+ [matchingChannels],
);
- // Flat list for keyboard navigation: not-joined first, then joined
- const allItems = React.useMemo(
- () => [...notJoined, ...joined],
- [notJoined, joined],
+ const visibleChannels =
+ activeTab === "archived"
+ ? archivedChannels
+ : activeTab === "joined"
+ ? joinedChannels
+ : currentChannels;
+
+ const orderedVisibleChannels = React.useMemo(
+ () => [
+ ...visibleChannels.filter((channel) => !channel.isMember),
+ ...visibleChannels.filter((channel) => channel.isMember),
+ ],
+ [visibleChannels],
);
- React.useEffect(() => {
+ const allTabLabel = isForumMode ? "All forums" : "All channels";
+
+ const updateTabIndicator = React.useCallback(() => {
+ const list = tabListRef.current;
+ const trigger = tabTriggerRefs.current[activeTab];
+
+ if (!open || !list || !trigger) {
+ return;
+ }
+
+ const nextIndicator = {
+ left: trigger.offsetLeft,
+ width: trigger.offsetWidth,
+ };
+
+ setTabIndicator((current) =>
+ Math.abs(current.left - nextIndicator.left) < 0.5 &&
+ Math.abs(current.width - nextIndicator.width) < 0.5
+ ? current
+ : nextIndicator,
+ );
+ }, [activeTab, open]);
+
+ React.useLayoutEffect(() => {
+ updateTabIndicator();
+
if (!open) {
- setQuery("");
- setSelectedIndex(0);
- setJoiningChannelId(null);
return;
}
- const timeout = window.setTimeout(() => {
- inputRef.current?.focus();
- inputRef.current?.select();
- }, 0);
+ let isCancelled = false;
+ const updateIfActive = () => {
+ if (!isCancelled) {
+ updateTabIndicator();
+ }
+ };
+ const frameId = window.requestAnimationFrame(updateIfActive);
+ const observer = new ResizeObserver(updateTabIndicator);
+ const list = tabListRef.current;
+
+ void document.fonts.ready.then(updateIfActive);
+
+ if (list) {
+ observer.observe(list);
+ }
+
+ for (const trigger of Object.values(tabTriggerRefs.current)) {
+ if (trigger) {
+ observer.observe(trigger);
+ }
+ }
return () => {
- window.clearTimeout(timeout);
+ isCancelled = true;
+ window.cancelAnimationFrame(frameId);
+ observer.disconnect();
};
+ }, [open, updateTabIndicator]);
+
+ React.useEffect(() => {
+ if (!open) {
+ setQuery("");
+ setActiveTab("all");
+ setSelectedIndex(null);
+ setJoiningChannelId(null);
+ return;
+ }
}, [open]);
React.useEffect(() => {
setSelectedIndex((current) => {
- if (allItems.length === 0) {
- return 0;
+ if (current === null || orderedVisibleChannels.length === 0) {
+ return null;
}
- return Math.min(current, allItems.length - 1);
+ return Math.min(current, orderedVisibleChannels.length - 1);
});
- }, [allItems]);
+ }, [orderedVisibleChannels]);
async function handleJoin(channelId: string) {
setJoiningChannelId(channelId);
@@ -191,45 +231,90 @@ export function ChannelBrowserDialog({
onSelectChannel(channel.id);
}
- const selectedItem = allItems[selectedIndex];
+ const selectedItem =
+ selectedIndex !== null ? orderedVisibleChannels[selectedIndex] : undefined;
+ const emptyTitle =
+ deferredQuery.length > 0
+ ? `No ${entityLabel}s match your search`
+ : activeTab === "archived"
+ ? `No archived ${entityLabel}s`
+ : activeTab === "joined"
+ ? `No joined ${entityLabel}s`
+ : `No ${entityLabel}s to browse`;
+ const emptyDescription =
+ deferredQuery.length > 0
+ ? "Try a different name or keyword."
+ : activeTab === "archived"
+ ? `Archived ${entityLabel}s you have joined will appear here.`
+ : activeTab === "joined"
+ ? `${entityLabel[0].toUpperCase()}${entityLabel.slice(1)}s you join will appear here.`
+ : `All open ${entityLabel}s are available in the sidebar. Create a new ${entityLabel} to get started.`;
return (
{
+ event.preventDefault();
+ contentRef.current?.focus();
+ }}
+ ref={contentRef}
+ showCloseButton={false}
>
-
-
-
-
-
- {browseTitle}
-
- {browseDescription}
-
-
-
+
+ {browseTitle}
+
+
+ Close
+
+
+
+
+ {
setQuery(event.target.value);
- setSelectedIndex(0);
+ setSelectedIndex(null);
}}
onKeyDown={(event) => {
- if (event.key === "ArrowDown" && allItems.length > 0) {
+ if (
+ event.key === "ArrowDown" &&
+ orderedVisibleChannels.length > 0
+ ) {
event.preventDefault();
setSelectedIndex((current) =>
- Math.min(current + 1, allItems.length - 1),
+ current === null
+ ? 0
+ : Math.min(
+ current + 1,
+ orderedVisibleChannels.length - 1,
+ ),
);
return;
}
- if (event.key === "ArrowUp" && allItems.length > 0) {
+ if (
+ event.key === "ArrowUp" &&
+ orderedVisibleChannels.length > 0
+ ) {
event.preventDefault();
- setSelectedIndex((current) => Math.max(current - 1, 0));
+ setSelectedIndex((current) =>
+ current === null
+ ? orderedVisibleChannels.length - 1
+ : Math.max(current - 1, 0),
+ );
return;
}
@@ -244,91 +329,102 @@ export function ChannelBrowserDialog({
}}
placeholder={searchPlaceholder}
ref={inputRef}
+ spellCheck={false}
+ type="text"
value={query}
/>
-
+ 0 ? "opacity-0" : "opacity-100"
+ }`}
+ >
{BROWSE_CHANNELS_SHORTCUT_HINT}
-
+
-
- {browsableChannels.length === 0 ? (
- deferredQuery.length > 0 ? (
-
- ) : (
-
- )
- ) : (
-
- {notJoined.length > 0 ? (
- <>
-
-
- {notJoined.length} {entityLabel}
- {notJoined.length !== 1 ? "s" : ""} to join
-
- Enter to view
-
-
- {notJoined.map((channel) => {
- const flatIndex = allItems.indexOf(channel);
- return (
- {
- void handleJoin(channel.id);
- }}
- onMouseEnter={() => setSelectedIndex(flatIndex)}
- onSelect={() => handleSelect(channel)}
- />
- );
- })}
-
- >
- ) : null}
-
- {joined.length > 0 ? (
- <>
-
- Joined
-
-
- {joined.map((channel) => {
- const flatIndex = allItems.indexOf(channel);
- return (
- setSelectedIndex(flatIndex)}
- onSelect={() => handleSelect(channel)}
- />
- );
- })}
-
- >
- ) : null}
+
+
+
{
+ setActiveTab(value as BrowserTab);
+ setSelectedIndex(null);
+ }}
+ value={activeTab}
+ >
+
+ 0}
+ data-testid="channel-browser-tab-indicator"
+ style={{
+ transform: `translate3d(${tabIndicator.left}px, 0, 0) scaleX(${tabIndicator.width})`,
+ }}
+ />
+ {
+ tabTriggerRefs.current.all = element;
+ }}
+ value="all"
+ >
+ {allTabLabel}
+
+ {
+ tabTriggerRefs.current.joined = element;
+ }}
+ value="joined"
+ >
+ Joined
+
+ {
+ tabTriggerRefs.current.archived = element;
+ }}
+ value="archived"
+ >
+ Archived
+
+
+
+
+
+ {orderedVisibleChannels.length === 0 ? (
+
0 ? Search : Compass}
+ title={emptyTitle}
+ />
+ ) : (
+
+ {orderedVisibleChannels.map((channel, index) => (
+ {
+ void handleJoin(channel.id);
+ }
+ : undefined
+ }
+ onSelect={() => handleSelect(channel)}
+ />
+ ))}
+
+ )}
- )}
-
-
-
- {hasArchivedJoinedChannels
- ? `Showing open ${entityLabel}s and your archived ${entityLabel}s. Private ${entityLabel}s require an invite.`
- : `Showing open ${entityLabel}s. Private ${entityLabel}s require an invite.`}
+
@@ -340,80 +436,77 @@ function ChannelCard({
isJoining,
isSelected,
onJoin,
- onMouseEnter,
onSelect,
}: {
channel: Channel;
isJoining: boolean;
isSelected: boolean;
onJoin?: () => void;
- onMouseEnter: () => void;
onSelect: () => void;
}) {
+ const memberLabel = `${channel.memberCount} ${
+ channel.memberCount === 1 ? "member" : "members"
+ }`;
+
return (
-
-
-
- {channel.channelType === "forum" ? (
-
- ) : (
-
- )}
-
-
-
-
-
+
+
+
+
+ #
+
+
{channel.name}
-
{channel.channelType}
{channel.archivedAt ? (
-
archived
+
+ archived
+
) : null}
-
-
-
- {channel.memberCount}
-
-
- {formatRelativeTime(channel.lastMessageAt)}
-
-
- {channel.description ? (
-
- {channel.description}
-
- ) : null}
+
+ {memberLabel}
+ {channel.description ? (
+ <>
+ ·
+ {channel.description}
+ >
+ ) : null}
+
-
- {!channel.isMember && onJoin ? (
- {
- event.stopPropagation();
- onJoin();
- }}
- size="sm"
- type="button"
- variant="default"
- >
-
- {isJoining ? "Joining..." : "Join"}
-
- ) : null}
-
-
+
+
+ {!channel.isMember && onJoin ? (
+
{
+ event.stopPropagation();
+ onJoin();
+ }}
+ size="sm"
+ type="button"
+ variant="default"
+ >
+ {isJoining ? "Joining..." : "Join"}
+
+ ) : null}
+
);
}
diff --git a/desktop/src/features/messages/lib/useRichTextEditor.ts b/desktop/src/features/messages/lib/useRichTextEditor.ts
index e293aa9ab..0a4885cd5 100644
--- a/desktop/src/features/messages/lib/useRichTextEditor.ts
+++ b/desktop/src/features/messages/lib/useRichTextEditor.ts
@@ -321,9 +321,12 @@ export function useRichTextEditor({
],
editorProps: {
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 md:leading-6 shadow-none focus-visible:ring-0 caret-foreground outline-hidden prose-sm max-w-none",
"data-testid": "message-input",
+ spellcheck: "false",
},
// ArrowUp in an empty composer → edit your last message (Slack
// parity). Handled here in ProseMirror's own DOM `keydown` hook —
diff --git a/desktop/src/features/search/ui/ChannelFindBar.tsx b/desktop/src/features/search/ui/ChannelFindBar.tsx
index 0e306a13e..3babd14cb 100644
--- a/desktop/src/features/search/ui/ChannelFindBar.tsx
+++ b/desktop/src/features/search/ui/ChannelFindBar.tsx
@@ -62,6 +62,8 @@ export function ChannelFindBar({
{/* Description */}
@@ -210,91 +228,136 @@ export function CreateChannelDialog({
(optional)
-
- {/* Options */}
-
-
-
-
- Private — only visible to invited members
-
-
+ Type
+ setEphemeral(value === "temporary")}
+ value={ephemeral ? "temporary" : "ongoing"}
+ >
+
+
+
+
+
+ Ongoing
+
+ For projects, teams, and recurring conversations.
+
+
+
+
+
+
+ Temporary
+
+ For quick discussions that archive automatically when
+ inactive.
+
+
+
+
+
+
+
+ {/* Permissions */}
+
+
+ Permissions
+
+
+
- setVisibility(checked ? "private" : "open")
- }
+ name="create-channel-visibility"
+ onChange={() => setVisibility("open")}
+ selected={visibility === "open"}
+ title="Open"
+ value="open"
+ />
+ setVisibility("private")}
+ selected={visibility === "private"}
+ title="Private"
+ value="private"
/>
-
+
+
+ {/* Template Selector */}
+ {templates.length > 0 ? (
+
-
- Ephemeral — auto-archives after 1 day of inactivity
+ Template{" "}
+
+ (optional)
+
-
+ id="create-channel-template"
+ onChange={(event) => handleTemplateChange(event.target.value)}
+ value={selectedTemplateId ?? ""}
+ >
+ No template
+ {templates.map((template: ChannelTemplate) => (
+
+ {template.name}
+
+ ))}
+
-
-
- {/* Template Selector */}
-
-
- Template{" "}
-
- (optional)
-
-
- handleTemplateChange(event.target.value)}
- value={selectedTemplateId ?? ""}
- >
-
- {templatesQuery.isLoading
- ? "Loading..."
- : templates.length === 0
- ? "No templates created yet"
- : "No template"}
-
- {templates.map((template: ChannelTemplate) => (
-
- {template.name}
-
- ))}
-
-
+ ) : null}
{/* Error */}
{errorMessage ? (
@@ -305,3 +368,66 @@ export function CreateChannelDialog({
);
}
+
+function PermissionOption({
+ description,
+ disabled,
+ name,
+ onChange,
+ selected,
+ title,
+ value,
+}: {
+ description: string;
+ disabled: boolean;
+ name: string;
+ onChange: () => void;
+ selected: boolean;
+ title: string;
+ value: ChannelVisibility;
+}) {
+ return (
+
+
+
+
+
+
+ {title}
+
+ {description}
+
+
+
+ );
+}
diff --git a/desktop/src/features/sidebar/ui/NewDirectMessageDialog.tsx b/desktop/src/features/sidebar/ui/NewDirectMessageDialog.tsx
index 70b34d6a3..7d4720423 100644
--- a/desktop/src/features/sidebar/ui/NewDirectMessageDialog.tsx
+++ b/desktop/src/features/sidebar/ui/NewDirectMessageDialog.tsx
@@ -181,7 +181,7 @@ export function NewDirectMessageDialog({
{formatUserName(user)}
@@ -198,7 +198,7 @@ export function NewDirectMessageDialog({
}}
type="button"
>
-
+
))}
diff --git a/desktop/src/shared/ui/alert-dialog.tsx b/desktop/src/shared/ui/alert-dialog.tsx
index 546cdd223..299b1c565 100644
--- a/desktop/src/shared/ui/alert-dialog.tsx
+++ b/desktop/src/shared/ui/alert-dialog.tsx
@@ -15,7 +15,7 @@ const AlertDialogOverlay = React.forwardRef<
>(({ className, ...props }, ref) => (
>(({ className, ...props }, ref) => (
diff --git a/desktop/src/shared/ui/checkbox.tsx b/desktop/src/shared/ui/checkbox.tsx
index 757512589..41044b2ab 100644
--- a/desktop/src/shared/ui/checkbox.tsx
+++ b/desktop/src/shared/ui/checkbox.tsx
@@ -19,7 +19,7 @@ const Checkbox = React.forwardRef<
-
+
));
diff --git a/desktop/src/shared/ui/chooser-dialog-content.tsx b/desktop/src/shared/ui/chooser-dialog-content.tsx
index 609014203..e802bc736 100644
--- a/desktop/src/shared/ui/chooser-dialog-content.tsx
+++ b/desktop/src/shared/ui/chooser-dialog-content.tsx
@@ -2,12 +2,7 @@ import * as React from "react";
import { cn } from "@/shared/lib/cn";
-import {
- DialogContent,
- DialogDescription,
- DialogHeader,
- DialogTitle,
-} from "./dialog";
+import { DialogContent, DialogHeader, DialogTitle } from "./dialog";
type ChooserDialogContentProps = React.ComponentPropsWithoutRef<
typeof DialogContent
@@ -16,6 +11,7 @@ type ChooserDialogContentProps = React.ComponentPropsWithoutRef<
footer?: React.ReactNode;
footerClassName?: string;
footerTestId?: string;
+ contentClassName?: string;
headerClassName?: string;
headerTestId?: string;
scrollAreaClassName?: string;
@@ -31,7 +27,8 @@ export const ChooserDialogContent = React.forwardRef<
{
children,
className,
- description,
+ contentClassName,
+ description: _description,
footer,
footerClassName,
footerTestId,
@@ -40,11 +37,13 @@ export const ChooserDialogContent = React.forwardRef<
scrollAreaClassName,
scrollAreaTestId,
title,
+ "aria-describedby": ariaDescribedBy,
...props
},
ref,
) => (
{title}
- {description ? (
- {description}
- ) : null}
-
{children}
+
{children}
{footer ? (
diff --git a/desktop/src/shared/ui/dialog.tsx b/desktop/src/shared/ui/dialog.tsx
index a189d4af8..db304fffb 100644
--- a/desktop/src/shared/ui/dialog.tsx
+++ b/desktop/src/shared/ui/dialog.tsx
@@ -21,7 +21,7 @@ const DialogOverlay = React.forwardRef<
return (
& {
+ showCloseButton?: boolean;
+};
+
const DialogContent = React.forwardRef<
React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, children, ...props }, ref) => (
+ DialogContentProps
+>(({ className, children, showCloseButton = true, ...props }, ref) => (
{children}
-
-
- Close
-
+ {showCloseButton ? (
+
+
+ Close
+
+ ) : null}
@@ -74,7 +82,7 @@ const DialogTitle = React.forwardRef<
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
diff --git a/desktop/src/shared/ui/input.tsx b/desktop/src/shared/ui/input.tsx
index e90b062e9..3983bc149 100644
--- a/desktop/src/shared/ui/input.tsx
+++ b/desktop/src/shared/ui/input.tsx
@@ -6,6 +6,9 @@ const Input = React.forwardRef>(
({ className, type, ...props }, ref) => {
return (
(({ className, ...props }, ref) => {
return (
Date: Fri, 12 Jun 2026 15:53:33 +0100
Subject: [PATCH 5/7] Fix channel browser row selection
---
.../features/channels/ui/ChannelBrowserDialog.tsx | 13 ++++++++-----
1 file changed, 8 insertions(+), 5 deletions(-)
diff --git a/desktop/src/features/channels/ui/ChannelBrowserDialog.tsx b/desktop/src/features/channels/ui/ChannelBrowserDialog.tsx
index 22e17c007..6cc8ddc5e 100644
--- a/desktop/src/features/channels/ui/ChannelBrowserDialog.tsx
+++ b/desktop/src/features/channels/ui/ChannelBrowserDialog.tsx
@@ -125,7 +125,7 @@ export function ChannelBrowserDialog({
? archivedChannels
: activeTab === "joined"
? joinedChannels
- : currentChannels;
+ : matchingChannels;
const orderedVisibleChannels = React.useMemo(
() => [
@@ -321,10 +321,10 @@ export function ChannelBrowserDialog({
if (
event.key === "Enter" &&
!event.nativeEvent.isComposing &&
- selectedItem
+ orderedVisibleChannels.length > 0
) {
event.preventDefault();
- handleSelect(selectedItem);
+ handleSelect(selectedItem ?? orderedVisibleChannels[0]);
}
}}
placeholder={searchPlaceholder}
@@ -455,11 +455,14 @@ function ChannelCard({
? "group/channel-row flex min-h-16 items-center gap-4 bg-muted/40 px-4 py-3 transition-colors duration-150 ease-out"
: "group/channel-row flex min-h-16 items-center gap-4 px-4 py-3 transition-colors duration-150 ease-out hover:bg-muted/40"
}
+ data-testid={`browse-channel-${channel.name}`}
>
{
+ event.stopPropagation();
+ onSelect();
+ }}
type="button"
>
From e1619e40cf12b6c37376e503460148b6edfb5fc7 Mon Sep 17 00:00:00 2001
From: klopez4212
Date: Fri, 12 Jun 2026 16:02:24 +0100
Subject: [PATCH 6/7] Label temporary channel type control
---
desktop/src/features/sidebar/ui/CreateChannelDialog.tsx | 1 +
1 file changed, 1 insertion(+)
diff --git a/desktop/src/features/sidebar/ui/CreateChannelDialog.tsx b/desktop/src/features/sidebar/ui/CreateChannelDialog.tsx
index 907d35cfd..95fe3a5bc 100644
--- a/desktop/src/features/sidebar/ui/CreateChannelDialog.tsx
+++ b/desktop/src/features/sidebar/ui/CreateChannelDialog.tsx
@@ -282,6 +282,7 @@ export function CreateChannelDialog({
Date: Fri, 12 Jun 2026 18:51:36 +0100
Subject: [PATCH 7/7] Focus channel browser search on open
---
desktop/src/features/channels/ui/ChannelBrowserDialog.tsx | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/desktop/src/features/channels/ui/ChannelBrowserDialog.tsx b/desktop/src/features/channels/ui/ChannelBrowserDialog.tsx
index 6cc8ddc5e..32616259f 100644
--- a/desktop/src/features/channels/ui/ChannelBrowserDialog.tsx
+++ b/desktop/src/features/channels/ui/ChannelBrowserDialog.tsx
@@ -61,7 +61,6 @@ export function ChannelBrowserDialog({
const [joiningChannelId, setJoiningChannelId] = React.useState(
null,
);
- const contentRef = React.useRef(null);
const inputRef = React.useRef(null);
const tabListRef = React.useRef(null);
const tabTriggerRefs = React.useRef<
@@ -260,9 +259,8 @@ export function ChannelBrowserDialog({
}
onOpenAutoFocus={(event) => {
event.preventDefault();
- contentRef.current?.focus();
+ inputRef.current?.focus({ preventScroll: true });
}}
- ref={contentRef}
showCloseButton={false}
>