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 (
No ACP activity yet
{emptyDescription}
{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/ChannelBrowserDialog.tsx b/desktop/src/features/channels/ui/ChannelBrowserDialog.tsx
index 2fc794bb2..32616259f 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,34 @@ 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 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 +104,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
+ : matchingChannels;
+
+ 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,144 +230,199 @@ 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 (
@@ -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/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({
onQueryChange(event.target.value)}
onKeyDown={handleKeyDown}
placeholder="Find in channel"
+ spellCheck={false}
type="text"
value={query}
/>
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/CreateChannelDialog.tsx b/desktop/src/features/sidebar/ui/CreateChannelDialog.tsx
index 9dcf5f411..95fe3a5bc 100644
--- a/desktop/src/features/sidebar/ui/CreateChannelDialog.tsx
+++ b/desktop/src/features/sidebar/ui/CreateChannelDialog.tsx
@@ -1,17 +1,22 @@
-import { Lock, Zap } from "lucide-react";
+import { ClockFading, Hash } from "lucide-react";
import * as React from "react";
import { useChannelTemplatesQuery } from "@/features/channel-templates/hooks";
import type { ChannelTemplate, ChannelVisibility } from "@/shared/api/types";
+import { cn } from "@/shared/lib/cn";
import { Button } from "@/shared/ui/button";
import { ChooserDialogContent } from "@/shared/ui/chooser-dialog-content";
import { Dialog } from "@/shared/ui/dialog";
import { Input } from "@/shared/ui/input";
-import { Switch } from "@/shared/ui/switch";
+import { Tabs, TabsList, TabsTrigger } from "@/shared/ui/tabs";
import { Textarea } from "@/shared/ui/textarea";
/** Default TTL for ephemeral channels: 1 day of inactivity. */
const EPHEMERAL_TTL_SECONDS = 86400;
+const CREATE_FIELD_SHELL_CLASS =
+ "rounded-xl border border-input bg-background shadow-xs transition-colors duration-150 ease-out hover:border-muted-foreground/40 hover:bg-muted/70 focus-within:border-muted-foreground/50 focus-within:bg-muted/70";
+const CREATE_FIELD_CONTROL_CLASS =
+ "border-0 bg-transparent shadow-none outline-none ring-0 placeholder:text-muted-foreground/45 focus:bg-transparent focus:outline-hidden focus-visible:ring-0";
type ChannelKind = "stream" | "forum";
@@ -136,7 +141,10 @@ export function CreateChannelDialog({
>
{/* Description */}
@@ -210,91 +228,137 @@ export function CreateChannelDialog({
(optional)
-
- {/* Options */}
-
-
-
-
+ 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 */}
+
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/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/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