Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
0d56b58
feat: quick-add agent popover with two entry points
tellaho May 20, 2026
d1b7aeb
fix: address review feedback — criticals + mediums
tellaho May 20, 2026
138ea9b
fix: move role=listbox to item list wrapper for valid ARIA
tellaho May 20, 2026
0bb63ae
Move Add agent button to sit immediately left of settings gear
tellaho May 20, 2026
c1bc5dc
Revert "Move Add agent button to sit immediately left of settings gear"
tellaho May 20, 2026
f6b3c5d
Move Add agent trigger to Members sidebar header row
tellaho May 20, 2026
91f398f
fix: clip dropdown menu items to container border-radius
tellaho May 20, 2026
940935f
fix: clip QuickAddAgentPopover items to container border-radius
tellaho May 20, 2026
d8771fe
feat: hybrid multi-select with team support in QuickAddAgentPopover
tellaho May 20, 2026
170a229
Move Add button inline with header, show only in multi-select mode
tellaho May 20, 2026
82ca654
Reorganize popover: nest agents under team headers, hover-reveal chec…
tellaho May 20, 2026
923bb38
Simplify popover: strip teams/multi-select, back to flat quick-add list
tellaho May 20, 2026
3acc220
Reshape AddChannelBotDialog with progressive disclosure
tellaho May 20, 2026
2e09796
Add Select button with animated team chips to popover header
tellaho May 21, 2026
917398f
Use Toggle for team chips, move Add button below More Options
tellaho May 21, 2026
85da5dd
Fix: stay in select mode when deselecting agents, stagger toggle anim…
tellaho May 21, 2026
a3b425d
Polish: secondary→primary Select button, animated checkboxes, truncat…
tellaho May 21, 2026
f54727e
Fix Select button: keep label as 'Select', use outline variant for vi…
tellaho May 21, 2026
ff513e9
Fix Select button: ghost+border for transparent bg, no bg-background …
tellaho May 21, 2026
dcf0655
Remove agent checkboxes from select mode, keep it clean
tellaho May 21, 2026
596c632
Restore checkbox without animation — static div, no motion
tellaho May 21, 2026
e0045b4
Polish: first team fades only, 2nd+ translate+fade; fix header padding
tellaho May 21, 2026
e0a5b25
fix: match header right padding to top padding on Select button
tellaho May 21, 2026
c1717da
Match Toggle height to Button sm (h-8) for visual parity
tellaho May 21, 2026
1054ca6
fix: remove extra py-1.5 on team-chips container causing header heigh…
tellaho May 21, 2026
5bad647
Fix button hierarchy: toggles use accent, Select uses secondary
tellaho May 21, 2026
df78384
Rethink: Select button always outline, label toggles Select/Cancel, d…
tellaho May 21, 2026
b682fb7
Keep title visible, toggles to the right, gradient scroll affordance
tellaho May 21, 2026
f15c436
Fix 's' animation: opacity only, no width transition
tellaho May 21, 2026
afa4abd
Move team toggles to own row between header and list with layout anim…
tellaho May 21, 2026
cb7b83b
feat: add subtle toggle variant (outline-only state change), use it f…
tellaho May 21, 2026
d5f3db1
fix: colored border-primary on subtle toggle pressed state, reduce to…
tellaho May 21, 2026
b014115
Add bg-primary/10 to subtle toggle selected state, fix row spacing
tellaho May 21, 2026
7b4c1de
fix: border-2 on subtle toggle pressed state
tellaho May 21, 2026
30e7097
feat: animated checkbox with layout transitions, absolute Add button …
tellaho May 21, 2026
8e6608c
fix: checkbox animates width continuously instead of mount/unmount — …
tellaho May 21, 2026
6481789
fix: team filter row always mounted — animate height continuously, no…
tellaho May 21, 2026
bcc4587
fix: remove gap-2.5 from row, animate marginRight on checkbox, wrap c…
tellaho May 21, 2026
16dc009
fix: reduce bottom padding on floating Add button
tellaho May 21, 2026
a607415
fix: bump More options button py-2 → py-2.5
tellaho May 21, 2026
cae7f6e
fix: Add button pb-1.5 → pb-1
tellaho May 21, 2026
3d09cd9
fix: Add button px-3 → px-1
tellaho May 21, 2026
7c96f87
fix: badge mask border-background → border-popover to match popover s…
tellaho May 21, 2026
cb84834
fix: remove running tag, use opacity-50 on inactive agent avatars ins…
tellaho May 21, 2026
3177848
fix: remove opacity-50 on inactive avatars — looked disabled
tellaho May 21, 2026
c2c62fa
fix: deselect team toggle when individual agents are unchecked
tellaho May 21, 2026
d8e29de
revert: restore AddChannelBotDialog to pre-branch state (remove progr…
tellaho May 21, 2026
aafe5c2
fix: increase section spacing in AddChannelBotDialog (space-y-5 → spa…
tellaho May 21, 2026
29681d3
feat: add X remove button for in-channel agents in quick-add popover
tellaho May 21, 2026
90fa35e
fix: opacity-70 on in-channel agent avatar + label, X stays full opacity
tellaho May 21, 2026
79f59d4
fix: opacity-70 → opacity-50 on in-channel agent avatar + label
tellaho May 21, 2026
abdb683
fix: popover stays open after add/remove, items don't reorder while open
tellaho May 21, 2026
7038c4c
fix: prepend newly created agents to top of list instead of appending
tellaho May 21, 2026
4944671
fix: use ref-based frozen key order — bulletproof stable item positio…
tellaho May 21, 2026
ac7727d
fix: remove frozen-order code, use layout animation for reorder, in-c…
tellaho May 21, 2026
2f378cb
fix: debounce item list (300ms) so name + membership changes batch in…
tellaho May 21, 2026
aa6b1f1
revert: remove debounce/spring, back to simple motion layout
tellaho May 21, 2026
af30286
feat: optimistic member updates + decompose QuickAddAgentPopover
tellaho May 22, 2026
c65fd6e
feat: optimistic remove — agent vanishes instantly from sidebar
tellaho May 22, 2026
4bb06b8
feat: sort sidebar bots online-first, then alphabetical
tellaho May 22, 2026
0b72f1a
fix: pass context to single-add mutation so reuse guard fires
tellaho May 22, 2026
8cf10d3
chore: bump file size limits for hooks with optimistic updates
tellaho May 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions desktop/scripts/check-file-sizes.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ const overrides = new Map([
["src-tauri/src/managed_agents/teams.rs", 580], // built-in team registry (Kit & Scout) + merge_teams + validate_team_deletion + JSON export/import + tests
["src-tauri/src/managed_agents/persona_card.rs", 970], // PNG/ZIP/MD persona card codec + pack-zip detection + nested root finder + provider/model/namePool fields + 27 unit tests
["src/app/AppShell.tsx", 815], // message edit state + handlers + ChannelPane edit prop threading + scrollback pagination + workflows view + projects view + memory-leak safeguards + home-badge state lifted here so it consumes the same NIP-RS read-state as the sidebar (single ReadStateManager)
["src/features/channels/hooks.ts", 550], // canvas query + mutation hooks + DM hide mutation
["src/features/channels/hooks.ts", 560], // canvas query + mutation hooks + DM hide mutation + optimistic remove
["src/features/channels/ui/ChannelManagementSheet.tsx", 800],
["src/features/channels/ui/ChannelPane.tsx", 520], // composer/timeline/sidebar orchestration + anchored agent activity footers
["src/features/channels/ui/ChannelScreen.tsx", 550], // profile panel state + mutual exclusion wiring + ProfilePanelProvider context + agent typing classification
Expand All @@ -55,7 +55,7 @@ const overrides = new Map([
["src-tauri/src/managed_agents/types.rs", 715], // ManagedAgentRecord/Summary + Create/Update request structs + RespondTo enum + validate_respond_to_allowlist + tests + persona/agent env_vars field
["src-tauri/src/managed_agents/backend.rs", 700], // provider IPC, validation, discovery, binary resolution + tests + redact_secrets_with for user env values + env_secrets_from_request + redact_env_values_in (shared with model discovery)
["src/features/huddle/HuddleContext.tsx", 650], // huddle lifecycle context + joinHuddle + connectAndSetupMedia shared helper + activeSpeakers/isReconnecting state + PTT (reusable AudioContext) + TTS subscription + mic level analyser (10fps throttle) + agent pubkey refresh
["src/features/agents/hooks.ts", 540], // agent query/mutation surface now includes built-in persona library activation + useUpdateManagedAgentMutation
["src/features/agents/hooks.ts", 610], // agent query/mutation surface + optimistic member updates + reuse-guard context fetch
["src/features/agents/ui/AgentsView.tsx", 880], // remote agent lifecycle controls + persona/team management + persona import-update dialog wiring + built-in catalog/library state orchestration
["src/features/agents/ui/UnifiedAgentsSection.tsx", 570], // unified persona-grouped agent view with collapsible groups, bulk actions, drag-drop import, empty/loading states
["src/features/agents/ui/ManagedAgentRow.tsx", 530], // EditAgentDialog integration + provider/local branching
Expand Down
83 changes: 82 additions & 1 deletion desktop/src/features/agents/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
discoverAcpProviders,
discoverBackendProviders,
discoverManagedAgentPrereqs,
getChannelMembers,
getManagedAgentLog,
listManagedAgents,
listRelayAgents,
Expand All @@ -38,6 +39,7 @@ import {
import type {
AgentPersona,
AgentTeam,
ChannelMember,
CreateManagedAgentInput,
CreatePersonaInput,
CreateTeamInput,
Expand All @@ -46,6 +48,7 @@ import type {
UpdatePersonaInput,
UpdateTeamInput,
} from "@/shared/api/types";
import { normalizePubkey } from "@/shared/lib/pubkey";
import type {
AttachManagedAgentToChannelInput,
AttachManagedAgentToChannelResult,
Expand Down Expand Up @@ -356,6 +359,29 @@ export function useAttachManagedAgentToChannelMutation(

return attachManagedAgentToChannel(channelId, input);
},
onMutate: async (input) => {
if (!channelId) return;
const membersKey = ["channels", channelId, "members"];
await queryClient.cancelQueries({ queryKey: membersKey });
const previous =
queryClient.getQueryData<ChannelMember[]>(membersKey) ?? [];
const optimistic: ChannelMember = {
pubkey: normalizePubkey(input.agent.pubkey),
role: input.role ?? "bot",
joinedAt: new Date().toISOString(),
displayName: input.agent.name,
};
queryClient.setQueryData<ChannelMember[]>(membersKey, [
...previous,
optimistic,
]);
return { previous, membersKey };
},
onError: (_err, _vars, context) => {
if (context?.membersKey) {
queryClient.setQueryData(context.membersKey, context.previous);
}
},
onSettled: async () => {
await invalidateAgentQueries(queryClient, channelId);
},
Expand Down Expand Up @@ -392,7 +418,39 @@ export function useCreateChannelManagedAgentMutation(channelId: string | null) {
throw new Error("No channel selected.");
}

return createChannelManagedAgent(channelId, input);
const [managedAgents, members] = await Promise.all([
listManagedAgents(),
getChannelMembers(channelId),
]);
const channelMemberPubkeys = new Set(
members.map((m) => normalizePubkey(m.pubkey)),
);

return createChannelManagedAgent(channelId, input, {
managedAgents,
channelMemberPubkeys,
});
},
onSuccess: (result) => {
if (!channelId) return;
const membersKey = ["channels", channelId, "members"];
const current =
queryClient.getQueryData<ChannelMember[]>(membersKey) ?? [];
const alreadyPresent = current.some(
(m) =>
normalizePubkey(m.pubkey) === normalizePubkey(result.agent.pubkey),
);
if (!alreadyPresent) {
queryClient.setQueryData<ChannelMember[]>(membersKey, [
...current,
{
pubkey: normalizePubkey(result.agent.pubkey),
role: "bot",
joinedAt: new Date().toISOString(),
displayName: result.agent.name,
},
]);
}
},
onSettled: async () => {
await invalidateAgentQueries(queryClient, channelId);
Expand All @@ -415,6 +473,29 @@ export function useCreateChannelManagedAgentsMutation(

return createChannelManagedAgents(channelId, inputs);
},
onSuccess: (result) => {
if (!channelId) return;
const membersKey = ["channels", channelId, "members"];
const current =
queryClient.getQueryData<ChannelMember[]>(membersKey) ?? [];
const existingPubkeys = new Set(
current.map((m) => normalizePubkey(m.pubkey)),
);
const newMembers: ChannelMember[] = result.successes
.filter((s) => !existingPubkeys.has(normalizePubkey(s.agent.pubkey)))
.map((s) => ({
pubkey: normalizePubkey(s.agent.pubkey),
role: "bot" as const,
joinedAt: new Date().toISOString(),
displayName: s.agent.name,
}));
if (newMembers.length > 0) {
queryClient.setQueryData<ChannelMember[]>(membersKey, [
...current,
...newMembers,
]);
}
},
onSettled: async () => {
await invalidateAgentQueries(queryClient, channelId);
},
Expand Down
18 changes: 18 additions & 0 deletions desktop/src/features/agents/lib/sortProviders.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { AcpProvider } from "@/shared/api/types";

/**
* Sort ACP providers with "goose" first, then alphabetically by label.
* Used by any surface that presents a provider list to the user.
*/
export function sortProviders(
providers: readonly AcpProvider[],
): AcpProvider[] {
return [...providers].sort((left, right) => {
const leftPriority = left.id === "goose" ? 0 : 1;
const rightPriority = right.id === "goose" ? 0 : 1;
if (leftPriority !== rightPriority) {
return leftPriority - rightPriority;
}
return left.label.localeCompare(right.label);
});
}
21 changes: 21 additions & 0 deletions desktop/src/features/channels/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,14 @@ import type {
AddChannelMembersInput,
Channel,
ChannelDetail,
ChannelMember,
CreateChannelInput,
OpenDmInput,
SetChannelPurposeInput,
SetChannelTopicInput,
UpdateChannelInput,
} from "@/shared/api/types";
import { normalizePubkey } from "@/shared/lib/pubkey";

export const channelsQueryKey = ["channels"] as const;
const channelDetailQueryKey = (channelId: string) =>
Expand Down Expand Up @@ -410,6 +412,25 @@ export function useRemoveChannelMemberMutation(channelId: string | null) {

await removeChannelMemberWithManagedAgentCleanup(channelId, pubkey);
},
onMutate: async (pubkey) => {
if (!channelId) return;
const membersKey = channelMembersQueryKey(channelId);
await queryClient.cancelQueries({ queryKey: membersKey });
const previous =
queryClient.getQueryData<ChannelMember[]>(membersKey) ?? [];
queryClient.setQueryData<ChannelMember[]>(
membersKey,
previous.filter(
(m) => normalizePubkey(m.pubkey) !== normalizePubkey(pubkey),
),
);
return { previous, membersKey };
},
onError: (_err, _vars, context) => {
if (context?.membersKey) {
queryClient.setQueryData(context.membersKey, context.previous);
}
},
onSettled: async () => {
await Promise.all([
invalidateChannelState(queryClient, channelId),
Expand Down
2 changes: 1 addition & 1 deletion desktop/src/features/channels/ui/AddChannelBotDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -477,7 +477,7 @@ export function AddChannelBotDialog({
footerClassName="justify-end gap-2"
footerTestId="add-channel-bot-dialog-footer"
headerTestId="add-channel-bot-dialog-header"
scrollAreaClassName="space-y-5"
scrollAreaClassName="space-y-6"
scrollAreaTestId="add-channel-bot-dialog-scroll-area"
title="Add agents"
>
Expand Down
56 changes: 31 additions & 25 deletions desktop/src/features/channels/ui/ChannelMembersBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,36 @@ import {
useManagedAgentsQuery,
useRelayAgentsQuery,
} from "@/features/agents/hooks";
import { sortProviders } from "@/features/agents/lib/sortProviders";
import { useChannelMembersQuery } from "@/features/channels/hooks";
import type { Channel } from "@/shared/api/types";
import { normalizePubkey } from "@/shared/lib/pubkey";
import { Button } from "@/shared/ui/button";
import { AddChannelBotDialog } from "./AddChannelBotDialog";
import { QuickAddAgentPopover } from "./QuickAddAgentPopover";

type ChannelMembersBarProps = {
channel: Channel;
currentPubkey?: string;
isAddBotDialogOpen?: boolean;
onAddBotDialogOpenChange?: (open: boolean) => void;
onManageChannel: () => void;
onToggleMembers: () => void;
};

export function ChannelMembersBar({
channel,
currentPubkey,
isAddBotDialogOpen,
onAddBotDialogOpenChange,
onManageChannel,
onToggleMembers,
}: ChannelMembersBarProps) {
const [isAddBotOpen, setIsAddBotOpen] = React.useState(false);
// Dialog state: controlled externally if props provided, otherwise local.
const [localIsAddBotOpen, setLocalIsAddBotOpen] = React.useState(false);
const isAddBotOpen = isAddBotDialogOpen ?? localIsAddBotOpen;
const setIsAddBotOpen = onAddBotDialogOpenChange ?? setLocalIsAddBotOpen;
const [isQuickAddOpen, setIsQuickAddOpen] = React.useState(false);
const { startHuddle, isStarting: isStartingHuddle } = useHuddle();
const queryClient = useQueryClient();
const membersQuery = useChannelMembersQuery(channel.id);
Expand All @@ -39,16 +49,7 @@ export function ChannelMembersBar({
const members = membersQuery.data ?? [];
const memberCount = membersQuery.data?.length ?? channel.memberCount;
const providers = React.useMemo(
() =>
[...(providersQuery.data ?? [])].sort((left, right) => {
const leftPriority = left.id === "goose" ? 0 : 1;
const rightPriority = right.id === "goose" ? 0 : 1;
if (leftPriority !== rightPriority) {
return leftPriority - rightPriority;
}

return left.label.localeCompare(right.label);
}),
() => sortProviders(providersQuery.data ?? []),
[providersQuery.data],
);
const normalizedCurrentPubkey = currentPubkey
Expand All @@ -73,7 +74,8 @@ export function ChannelMembersBar({

previousChannelIdRef.current = channel.id;
setIsAddBotOpen(false);
}, [channel.id]);
setIsQuickAddOpen(false);
}, [channel.id, setIsAddBotOpen]);

const dialogErrorMessage =
providersQuery.error instanceof Error
Expand All @@ -87,20 +89,24 @@ export function ChannelMembersBar({
return (
<React.Fragment>
<div className="flex items-center gap-1">
<Button
aria-label="Add agent"
className="h-7 w-7 rounded-full"
data-testid="channel-add-bot-trigger"
disabled={!canAddAgents}
onClick={() => {
setIsAddBotOpen(true);
}}
size="icon"
type="button"
variant="outline"
<QuickAddAgentPopover
channelId={channel.id}
open={isQuickAddOpen}
onOpenChange={setIsQuickAddOpen}
onMoreOptions={() => setIsAddBotOpen(true)}
>
<Plus className="h-3 w-3" />
</Button>
<Button
aria-label="Add agent"
className="h-7 w-7 rounded-full"
data-testid="channel-add-bot-trigger"
disabled={!canAddAgents}
size="icon"
type="button"
variant="outline"
>
<Plus className="h-3 w-3" />
</Button>
</QuickAddAgentPopover>

<HuddleIndicator
className="h-7 w-7"
Expand Down
4 changes: 4 additions & 0 deletions desktop/src/features/channels/ui/ChannelScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ export function ChannelScreen({
string | null
>(null);
const [isMembersSidebarOpen, setIsMembersSidebarOpen] = React.useState(false);
const [isAddBotDialogOpen, setIsAddBotDialogOpen] = React.useState(false);
const [openThreadHeadId, setOpenThreadHeadId] = React.useState<string | null>(
null,
);
Expand Down Expand Up @@ -436,7 +437,9 @@ export function ChannelScreen({
activeChannelTitle={activeChannelTitle}
activeDmPresenceStatus={activeDmPresenceStatus}
currentPubkey={currentPubkey}
isAddBotDialogOpen={isAddBotDialogOpen}
isJoining={joinChannelMutation.isPending}
onAddBotDialogOpenChange={setIsAddBotDialogOpen}
onJoinChannel={joinChannelMutation.mutateAsync}
onManageChannel={openChannelManagement}
onToggleMembers={() => setIsMembersSidebarOpen((prev) => !prev)}
Expand Down Expand Up @@ -530,6 +533,7 @@ export function ChannelScreen({
currentPubkey={currentPubkey}
open={isMembersSidebarOpen}
onOpenChange={setIsMembersSidebarOpen}
onOpenAddBotDialog={() => setIsAddBotDialogOpen(true)}
onViewActivity={handleOpenAgentSession}
/>
</ProfilePanelProvider>
Expand Down
6 changes: 6 additions & 0 deletions desktop/src/features/channels/ui/ChannelScreenHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ type ChannelScreenHeaderProps = {
activeChannelTitle: string;
activeDmPresenceStatus: PresenceStatus | null;
currentPubkey?: string;
isAddBotDialogOpen?: boolean;
isJoining?: boolean;
onAddBotDialogOpenChange?: (open: boolean) => void;
onJoinChannel?: () => Promise<void>;
onManageChannel: () => void;
onToggleMembers: () => void;
Expand All @@ -26,7 +28,9 @@ export function ChannelScreenHeader({
activeChannelTitle,
activeDmPresenceStatus,
currentPubkey,
isAddBotDialogOpen,
isJoining = false,
onAddBotDialogOpenChange,
onJoinChannel,
onManageChannel,
onToggleMembers,
Expand Down Expand Up @@ -56,6 +60,8 @@ export function ChannelScreenHeader({
<ChannelMembersBar
channel={activeChannel}
currentPubkey={currentPubkey}
isAddBotDialogOpen={isAddBotDialogOpen}
onAddBotDialogOpenChange={onAddBotDialogOpenChange}
onManageChannel={onManageChannel}
onToggleMembers={onToggleMembers}
/>
Expand Down
Loading
Loading