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 (
-