From 725dc351d750d177cf5b534175d5515e9e5925ef Mon Sep 17 00:00:00 2001 From: Wes Date: Tue, 21 Apr 2026 11:42:11 -0600 Subject: [PATCH] Desktop UX polish: chat input, agent bulk actions, starting badge (#378) Co-authored-by: Claude Opus 4.6 (1M context) --- crates/sprout-media/src/validation.rs | 6 +- desktop/src-tauri/tauri.conf.json | 4 +- desktop/src/features/agents/ui/AgentsView.tsx | 7 ++ .../features/agents/ui/ManagedAgentRow.tsx | 63 +++++++------ .../agents/ui/ManagedAgentsSection.tsx | 67 ++++++++++++-- .../features/agents/ui/PersonaIdentity.tsx | 5 +- desktop/src/features/agents/ui/TeamDialog.tsx | 5 +- .../agents/ui/useManagedAgentActions.ts | 90 +++++++++++++++++++ .../channels/ui/ChannelBrowserDialog.tsx | 9 +- .../src/features/channels/ui/ChannelPane.tsx | 12 +-- .../channels/ui/MembersSidebarMemberCard.tsx | 7 +- desktop/src/features/home/ui/HomeView.tsx | 6 +- .../messages/ui/ChannelAutocomplete.tsx | 5 +- .../messages/ui/MentionAutocomplete.tsx | 5 +- .../features/messages/ui/MessageComposer.tsx | 12 +-- .../messages/ui/MessageComposerToolbar.tsx | 2 +- .../src/features/messages/ui/MessageRow.tsx | 9 +- .../messages/ui/MessageThreadPanel.tsx | 17 ++-- .../features/messages/ui/MessageTimeline.tsx | 4 +- .../messages/ui/TypingIndicatorRow.tsx | 66 +++++++------- .../src/features/search/ui/SearchDialog.tsx | 5 +- .../settings/ui/DoctorSettingsPanel.tsx | 2 +- .../features/workflows/ui/WorkflowCard.tsx | 20 ++--- .../workflows/ui/WorkflowDetailPanel.tsx | 27 +++--- .../workflows/ui/WorkflowRunTrace.tsx | 24 ++++- desktop/src/shared/ui/badge.tsx | 37 ++++++++ 26 files changed, 356 insertions(+), 160 deletions(-) create mode 100644 desktop/src/shared/ui/badge.tsx diff --git a/crates/sprout-media/src/validation.rs b/crates/sprout-media/src/validation.rs index ec1f32d1fe..7e65b44cca 100644 --- a/crates/sprout-media/src/validation.rs +++ b/crates/sprout-media/src/validation.rs @@ -245,10 +245,8 @@ fn check_moov_before_mdat(path: &Path) -> Result<(), MediaError> { b"moov" => { moov_seen = true; } - b"mdat" => { - if !moov_seen { - return Err(MediaError::MoovNotAtFront); - } + b"mdat" if !moov_seen => { + return Err(MediaError::MoovNotAtFront); } _ => {} } diff --git a/desktop/src-tauri/tauri.conf.json b/desktop/src-tauri/tauri.conf.json index b5ae4486f5..feafa248bb 100644 --- a/desktop/src-tauri/tauri.conf.json +++ b/desktop/src-tauri/tauri.conf.json @@ -24,7 +24,9 @@ "hiddenTitle": true, "dragDropEnabled": false, "trafficLightPosition": { "x": 12, "y": 22 }, - "backgroundThrottling": "disabled" + "backgroundThrottling": "disabled", + "minWidth": 800, + "minHeight": 500 } ], "security": { diff --git a/desktop/src/features/agents/ui/AgentsView.tsx b/desktop/src/features/agents/ui/AgentsView.tsx index 4ae2dfe687..a3e2c5c743 100644 --- a/desktop/src/features/agents/ui/AgentsView.tsx +++ b/desktop/src/features/agents/ui/AgentsView.tsx @@ -124,12 +124,19 @@ export function AgentsView() { } logLoading={agents.managedAgentLogQuery.isLoading} personaLabelsById={personas.personaLabelsById} + presenceLoaded={agents.managedPresenceQuery.isSuccess} presenceLookup={agents.managedPresenceQuery.data ?? {}} onAddToChannel={(agent) => { agents.setActionNoticeMessage(null); agents.setActionErrorMessage(null); agents.setAgentToAddToChannel(agent); }} + onBulkRemoveStopped={() => { + void agents.handleBulkRemoveStopped(); + }} + onBulkStopRunning={() => { + void agents.handleBulkStopRunning(); + }} onCreate={() => { agents.setIsCreateOpen(true); }} diff --git a/desktop/src/features/agents/ui/ManagedAgentRow.tsx b/desktop/src/features/agents/ui/ManagedAgentRow.tsx index 32bf9ab134..707afba893 100644 --- a/desktop/src/features/agents/ui/ManagedAgentRow.tsx +++ b/desktop/src/features/agents/ui/ManagedAgentRow.tsx @@ -16,6 +16,7 @@ import { } from "lucide-react"; import { PresenceDot } from "@/features/presence/ui/PresenceBadge"; +import { Badge } from "@/shared/ui/badge"; import type { ManagedAgent, PresenceLookup, @@ -43,6 +44,7 @@ export function ManagedAgentRow({ logError, logLoading, personaLabelsById, + presenceLoaded, presenceLookup, onAddToChannel, onDelete, @@ -60,6 +62,7 @@ export function ManagedAgentRow({ logError: Error | null; logLoading: boolean; personaLabelsById: Record; + presenceLoaded: boolean; presenceLookup: PresenceLookup; onAddToChannel: (agent: ManagedAgent) => void; onDelete: (pubkey: string) => void; @@ -116,7 +119,8 @@ export function ManagedAgentRow({ presenceStatus={presenceStatus} /> @@ -135,7 +139,8 @@ export function ManagedAgentRow({ presenceStatus={presenceStatus} /> @@ -215,9 +220,7 @@ function AgentSummary({

{agent.name}

{personaLabel ? ( - - {personaLabel} - + {personaLabel} ) : null}
@@ -234,12 +237,9 @@ function AgentSummary({ {channelNames.length > 0 ? (
{channelNames.map((name) => ( - + # {name} - + ))}
) : null} @@ -250,11 +250,13 @@ function AgentSummary({ } function StatusBlock({ - isActive, + presenceLoaded, + presenceStatus, processDetail, status, }: { - isActive: boolean; + presenceLoaded: boolean; + presenceStatus: PresenceStatus | undefined; processDetail: string; status: ManagedAgent["status"]; }) { @@ -263,7 +265,11 @@ function StatusBlock({

Status

- +

{processDetail}

); @@ -444,29 +450,32 @@ function AgentActionsMenu({ function AgentOriginBadge({ agent }: { agent: ManagedAgent }) { return ( - + {agent.backend.type === "local" ? "Local" : "Remote"} - + ); } function AgentStatusBadge({ - isActive, + presenceLoaded, + presenceStatus, status, }: { - isActive: boolean; + presenceLoaded: boolean; + presenceStatus: PresenceStatus | undefined; status: ManagedAgent["status"]; }) { + const isActive = status === "running" || status === "deployed"; + const isStarting = + presenceLoaded && + status === "running" && + (!presenceStatus || presenceStatus === "offline"); + + const variant = isStarting ? "warning" : isActive ? "default" : "secondary"; + return ( - - {status.replace(/_/g, " ")} - + + {isStarting ? "Starting\u2026" : status.replace(/_/g, " ")} + ); } diff --git a/desktop/src/features/agents/ui/ManagedAgentsSection.tsx b/desktop/src/features/agents/ui/ManagedAgentsSection.tsx index e4d2d88613..1664e99f7b 100644 --- a/desktop/src/features/agents/ui/ManagedAgentsSection.tsx +++ b/desktop/src/features/agents/ui/ManagedAgentsSection.tsx @@ -1,4 +1,14 @@ +import { Ellipsis, OctagonX, Trash2 } from "lucide-react"; + +import { isManagedAgentActive } from "@/features/agents/lib/managedAgentControlActions"; import type { ManagedAgent, PresenceLookup } from "@/shared/api/types"; +import { Button } from "@/shared/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/shared/ui/dropdown-menu"; import { Skeleton } from "@/shared/ui/skeleton"; import { CreateNewButton } from "./CreateNewButton"; import { ManagedAgentRow } from "./ManagedAgentRow"; @@ -15,8 +25,11 @@ export function ManagedAgentsSection({ logError, logLoading, personaLabelsById, + presenceLoaded, presenceLookup, onAddToChannel, + onBulkRemoveStopped, + onBulkStopRunning, onCreate, onDelete, onMintToken, @@ -37,8 +50,11 @@ export function ManagedAgentsSection({ logError: Error | null; logLoading: boolean; personaLabelsById: Record; + presenceLoaded: boolean; presenceLookup: PresenceLookup; onAddToChannel: (agent: ManagedAgent) => void; + onBulkRemoveStopped: () => void; + onBulkStopRunning: () => void; onCreate: () => void; onDelete: (pubkey: string) => void; onMintToken: (pubkey: string, name: string) => void; @@ -48,6 +64,10 @@ export function ManagedAgentsSection({ onToggleStartOnAppLaunch: (pubkey: string, startOnAppLaunch: boolean) => void; selectedLogAgentPubkey: string | null; }) { + const runningCount = agents.filter((a) => isManagedAgentActive(a)).length; + const stoppedCount = agents.filter( + (a) => a.status === "stopped" || a.status === "not_deployed", + ).length; return (
@@ -59,11 +79,47 @@ export function ManagedAgentsSection({ Agent profiles and process state — local and remote.

- +
+ {agents.length > 0 ? ( + + + + + e.preventDefault()} + > + + + Stop all running ({runningCount}) + + + + Remove all stopped ({stoppedCount}) + + + + ) : null} + +
{isLoading ? ( @@ -111,6 +167,7 @@ export function ManagedAgentsSection({ } logLoading={selectedLogAgentPubkey === agent.pubkey && logLoading} personaLabelsById={personaLabelsById} + presenceLoaded={presenceLoaded} presenceLookup={presenceLookup} onAddToChannel={onAddToChannel} onDelete={onDelete} diff --git a/desktop/src/features/agents/ui/PersonaIdentity.tsx b/desktop/src/features/agents/ui/PersonaIdentity.tsx index d7511bb740..3b129cc42e 100644 --- a/desktop/src/features/agents/ui/PersonaIdentity.tsx +++ b/desktop/src/features/agents/ui/PersonaIdentity.tsx @@ -4,6 +4,7 @@ import { ProfileAvatar } from "@/features/profile/ui/ProfileAvatar"; import type { AgentPersona } from "@/shared/api/types"; import { cn } from "@/shared/lib/cn"; import { promptPreview } from "@/shared/lib/promptPreview"; +import { Badge } from "@/shared/ui/badge"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip"; type PersonaIdentityProps = { @@ -35,9 +36,7 @@ export function PersonaIdentity({ {persona.displayName}

{showBuiltInBadge ? ( - - Built-in - + Built-in ) : null} {showPromptTooltip && preview ? ( diff --git a/desktop/src/features/agents/ui/TeamDialog.tsx b/desktop/src/features/agents/ui/TeamDialog.tsx index d160a28405..9636bd9e76 100644 --- a/desktop/src/features/agents/ui/TeamDialog.tsx +++ b/desktop/src/features/agents/ui/TeamDialog.tsx @@ -9,6 +9,7 @@ import type { } from "@/shared/api/types"; import { useFileImportZone } from "@/shared/hooks/useFileImportZone"; import { cn } from "@/shared/lib/cn"; +import { Badge } from "@/shared/ui/badge"; import { Button } from "@/shared/ui/button"; import { Checkbox } from "@/shared/ui/checkbox"; import { @@ -425,9 +426,7 @@ export function TeamDialog({ /> {persona.displayName} {persona.isBuiltIn ? ( - - Built-in - + Built-in ) : null} ); diff --git a/desktop/src/features/agents/ui/useManagedAgentActions.ts b/desktop/src/features/agents/ui/useManagedAgentActions.ts index 062318f2ef..c91d4f7f02 100644 --- a/desktop/src/features/agents/ui/useManagedAgentActions.ts +++ b/desktop/src/features/agents/ui/useManagedAgentActions.ts @@ -18,8 +18,11 @@ import type { CreateManagedAgentResponse, ManagedAgent, } from "@/shared/api/types"; +import { removeChannelMember } from "@/shared/api/tauri"; +import { normalizePubkey } from "@/shared/lib/pubkey"; import { deleteManagedAgentWithRules, + isManagedAgentActive, startManagedAgentWithRules, stopManagedAgentWithRules, } from "../lib/managedAgentControlActions"; @@ -141,6 +144,22 @@ export function useManagedAgentActions() { } } + function getAgentChannelIds(pubkey: string): string[] { + const normalized = normalizePubkey(pubkey); + const relayAgent = (relayAgentsQuery.data ?? []).find( + (ra) => normalizePubkey(ra.pubkey) === normalized, + ); + return relayAgent?.channelIds ?? []; + } + + async function removeAgentFromAllChannels(pubkey: string) { + const channelIds = getAgentChannelIds(pubkey); + if (channelIds.length === 0) return; + await Promise.allSettled( + channelIds.map((channelId) => removeChannelMember(channelId, pubkey)), + ); + } + async function handleDelete(pubkey: string) { clearFeedback(); try { @@ -154,6 +173,7 @@ export function useManagedAgentActions() { relayAgents: relayAgentsQuery.data ?? [], }); if (result.cancelled) return; + await removeAgentFromAllChannels(pubkey); if (logAgentPubkey === pubkey) { setLogAgentPubkey(null); } @@ -224,6 +244,74 @@ export function useManagedAgentActions() { void relayAgentsQuery.refetch(); } + async function runBulkAction( + targets: ManagedAgent[], + confirmLabel: string, + failureNoun: string, + action: (agent: ManagedAgent) => Promise, + ): Promise { + if (targets.length === 0) return false; + const confirmed = window.confirm( + `${confirmLabel} ${targets.length} agent${targets.length === 1 ? "" : "s"}?`, + ); + if (!confirmed) return false; + clearFeedback(); + const results = await Promise.allSettled(targets.map(action)); + const failures = results.filter((r) => r.status === "rejected"); + if (failures.length > 0) { + setActionErrorMessage( + `${failures.length} of ${targets.length} ${failureNoun}${failures.length === 1 ? "" : "s"} failed.`, + ); + } + return true; + } + + async function handleBulkStopRunning() { + await runBulkAction( + managedAgents.filter((a) => isManagedAgentActive(a)), + "Stop", + "stop", + (a) => + stopManagedAgentWithRules({ + agent: a, + channels: channelsQuery.data ?? [], + relayAgents: relayAgentsQuery.data ?? [], + stopManagedAgent: stopMutation.mutateAsync, + }), + ); + } + + async function handleBulkRemoveStopped() { + const targets = managedAgents.filter( + (a) => a.status === "stopped" || a.status === "not_deployed", + ); + const executed = await runBulkAction( + targets, + "Remove", + "removal", + async (a) => { + await deleteManagedAgent(a); + await removeAgentFromAllChannels(a.pubkey); + }, + ); + if ( + executed && + logAgentPubkey && + targets.some((a) => a.pubkey === logAgentPubkey) + ) { + setLogAgentPubkey(null); + } + } + + async function deleteManagedAgent(agent: ManagedAgent) { + const isDeployedRemote = + agent.backend.type === "provider" && agent.backendAgentId; + await deleteMutation.mutateAsync({ + pubkey: agent.pubkey, + forceRemoteDelete: isDeployedRemote ? true : undefined, + }); + } + const isPending = startMutation.isPending || stopMutation.isPending || @@ -264,6 +352,8 @@ export function useManagedAgentActions() { handleToggleStartOnAppLaunch, handleMintToken, handleAddedToChannel, + handleBulkStopRunning, + handleBulkRemoveStopped, // Refetch helpers (for cross-domain use) refetchManagedAgents: () => void managedAgentsQuery.refetch(), refetchRelayAgents: () => void relayAgentsQuery.refetch(), diff --git a/desktop/src/features/channels/ui/ChannelBrowserDialog.tsx b/desktop/src/features/channels/ui/ChannelBrowserDialog.tsx index 2a5a7303fd..f166332108 100644 --- a/desktop/src/features/channels/ui/ChannelBrowserDialog.tsx +++ b/desktop/src/features/channels/ui/ChannelBrowserDialog.tsx @@ -17,6 +17,7 @@ import { DialogHeader, DialogTitle, } from "@/shared/ui/dialog"; +import { Badge } from "@/shared/ui/badge"; import { Input } from "@/shared/ui/input"; import { Button } from "@/shared/ui/button"; @@ -379,13 +380,9 @@ function ChannelCard({

{channel.name}

-

- {channel.channelType} -

+ {channel.channelType} {channel.archivedAt ? ( -

- archived -

+ archived ) : null}
diff --git a/desktop/src/features/channels/ui/ChannelPane.tsx b/desktop/src/features/channels/ui/ChannelPane.tsx index 83c4a4d5aa..7832c8b7cb 100644 --- a/desktop/src/features/channels/ui/ChannelPane.tsx +++ b/desktop/src/features/channels/ui/ChannelPane.tsx @@ -228,12 +228,6 @@ export const ChannelPane = React.memo(function ChannelPane({ onToggleReaction={onToggleReaction} targetMessageId={targetMessageId} /> - +
{threadHeadMessage ? ( diff --git a/desktop/src/features/channels/ui/MembersSidebarMemberCard.tsx b/desktop/src/features/channels/ui/MembersSidebarMemberCard.tsx index 8b5625ea07..10631be008 100644 --- a/desktop/src/features/channels/ui/MembersSidebarMemberCard.tsx +++ b/desktop/src/features/channels/ui/MembersSidebarMemberCard.tsx @@ -29,6 +29,7 @@ import { DropdownMenuSubTrigger, DropdownMenuTrigger, } from "@/shared/ui/dropdown-menu"; +import { Badge } from "@/shared/ui/badge"; type MembersSidebarMemberCardProps = { canChangeRole: boolean; @@ -119,12 +120,12 @@ export function MembersSidebarMemberCard({ {managedAgent ? ( <> - {formatManagedAgentStatus(managedAgent)} - + ) : null} diff --git a/desktop/src/features/home/ui/HomeView.tsx b/desktop/src/features/home/ui/HomeView.tsx index 5373dc2176..38a67aa1da 100644 --- a/desktop/src/features/home/ui/HomeView.tsx +++ b/desktop/src/features/home/ui/HomeView.tsx @@ -31,7 +31,7 @@ function HomeLoadingState() { return (
-
+
{["mentions", "actions"].map((section) => (
@@ -161,8 +161,6 @@ export function HomeView({ const showNeedsAction = filter === "all" || filter === "needs_action"; const showActivity = filter === "all" || filter === "activity"; const showAgentActivity = filter === "all" || filter === "agent_activity"; - const singleColumn = filter !== "all"; - return (
@@ -193,7 +191,7 @@ export function HomeView({ ) : null} -
+
{showMentions ? ( #{suggestion.name} - - {suggestion.channelType} - + {suggestion.channelType} ))}
diff --git a/desktop/src/features/messages/ui/MentionAutocomplete.tsx b/desktop/src/features/messages/ui/MentionAutocomplete.tsx index 6f661e982d..8e35b9e5f6 100644 --- a/desktop/src/features/messages/ui/MentionAutocomplete.tsx +++ b/desktop/src/features/messages/ui/MentionAutocomplete.tsx @@ -1,5 +1,6 @@ import * as React from "react"; +import { Badge } from "@/shared/ui/badge"; import { cn } from "@/shared/lib/cn"; export type MentionSuggestion = { @@ -70,9 +71,7 @@ export const MentionAutocomplete = React.memo(function MentionAutocomplete({ ({suggestion.personaName}) ) : suggestion.role ? ( - - {suggestion.role} - + {suggestion.role} ) : null} ))} diff --git a/desktop/src/features/messages/ui/MessageComposer.tsx b/desktop/src/features/messages/ui/MessageComposer.tsx index 08d0a55827..c96ac5122f 100644 --- a/desktop/src/features/messages/ui/MessageComposer.tsx +++ b/desktop/src/features/messages/ui/MessageComposer.tsx @@ -17,7 +17,6 @@ import { } from "@/features/messages/lib/normalizeMentionClipboard"; import { useRichTextEditor } from "@/features/messages/lib/useRichTextEditor"; import { useTypingBroadcast } from "@/features/messages/useTypingBroadcast"; -import { cn } from "@/shared/lib/cn"; import { Button } from "@/shared/ui/button"; import { ChannelAutocomplete } from "./ChannelAutocomplete"; import { ComposerAttachments } from "./ComposerAttachments"; @@ -51,7 +50,6 @@ type MessageComposerProps = { body: string; id: string; } | null; - showTopBorder?: boolean; typingParentEventId?: string | null; typingRootEventId?: string | null; }; @@ -68,7 +66,6 @@ export function MessageComposer({ onSend, placeholder, replyTarget = null, - showTopBorder = true, typingParentEventId = null, typingRootEventId = null, }: MessageComposerProps) { @@ -499,15 +496,10 @@ export function MessageComposer({ // ── Render ────────────────────────────────────────────────────────── return ( -