Support relay agents in channel activity

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Thomas Petersen
2026-05-06 22:31:44 -04:00
parent 8ee5db096e
commit e8126b3d0c
4 changed files with 80 additions and 19 deletions

View File

@@ -35,8 +35,8 @@ const overrides = new Map([
["src/app/AppShell.tsx", 860], // message edit state + handlers + ChannelPane edit prop threading + scrollback pagination + workflows view + memory-leak safeguards
["src/features/channels/hooks.ts", 550], // canvas query + mutation hooks + DM hide mutation
["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
["src/features/channels/ui/ChannelPane.tsx", 525], // composer/timeline/sidebar orchestration + anchored agent activity footers
["src/features/channels/ui/ChannelScreen.tsx", 610], // profile panel state + mutual exclusion wiring + ProfilePanelProvider context + relay-aware agent typing classification
["src/features/messages/hooks.ts", 500], // message query/mutation hooks + optimistic updates
["src/features/messages/ui/MessageComposer.tsx", 700], // media upload handlers (paste, drop, dialog) + channelId reset effect + edit mode (pre-fill, save, cancel, escape)
["src/features/settings/ui/SettingsView.tsx", 600],

View File

@@ -1,8 +1,9 @@
use std::collections::HashMap;
use tauri::AppHandle;
use tauri::{AppHandle, Manager};
use crate::{
app_state::AppState,
managed_agents::{
append_log_marker, login_shell_path, managed_agent_log_path, missing_command_message,
normalize_agent_args, open_log_file, resolve_command, ManagedAgentProcess,
@@ -561,6 +562,11 @@ pub fn spawn_agent_child(
} else {
command.env_remove("SPROUT_AUTH_TAG");
}
if let Some(state) = app.try_state::<AppState>() {
if let Ok(keys) = state.keys.lock() {
command.env("SPROUT_ACP_AGENT_OWNER", keys.public_key().to_hex());
}
}
command.env("SPROUT_ACP_RELAY_OBSERVER", "true");

View File

@@ -16,6 +16,7 @@ import { MembersSidebar } from "@/features/channels/ui/MembersSidebar";
import {
useManagedAgentsQuery,
usePersonasQuery,
useRelayAgentsQuery,
} from "@/features/agents/hooks";
import { useManagedAgentObserverBridge } from "@/features/agents/observerRelayStore";
import {
@@ -52,6 +53,7 @@ import { AgentSessionProvider } from "@/shared/context/AgentSessionContext";
import { ProfilePanelProvider } from "@/shared/context/ProfilePanelContext";
import { useChannelAgentSessions } from "./useChannelAgentSessions";
import { useChannelProfilePanel } from "./useChannelProfilePanel";
import type { BotActivityAgent } from "./BotActivityBar";
type ChannelScreenProps = {
activeChannel: Channel | null;
currentIdentity?: Identity;
@@ -165,46 +167,66 @@ export function ChannelScreen({
enabled: messageProfilePubkeys.length > 0,
});
const managedAgentsQuery = useManagedAgentsQuery();
const relayAgentsQuery = useRelayAgentsQuery();
useManagedAgentObserverBridge(managedAgentsQuery.data ?? []);
const { botTypingEntries, humanTypingPubkeys } = React.useMemo<{
botTypingEntries: TypingIndicatorEntry[];
humanTypingPubkeys: string[];
}>(() => {
const localAgentSet = new Set(
(managedAgentsQuery.data ?? [])
.filter((agent) => agent.backend.type === "local")
.map((agent) => agent.pubkey.toLowerCase()),
const managedAgentSet = new Set(
(managedAgentsQuery.data ?? []).map((agent) =>
agent.pubkey.toLowerCase(),
),
);
const relayAgentSet = new Set(
(relayAgentsQuery.data ?? []).map((agent) => agent.pubkey.toLowerCase()),
);
const channelTypingEntries = typingEntries.filter(
(entry) => entry.threadHeadId === null,
);
const agentTypingEntries = typingEntries.filter((entry) =>
localAgentSet.has(entry.pubkey.toLowerCase()),
const agentTypingEntries = typingEntries.filter(
(entry) =>
managedAgentSet.has(entry.pubkey.toLowerCase()) ||
relayAgentSet.has(entry.pubkey.toLowerCase()),
);
return {
botTypingEntries: agentTypingEntries,
humanTypingPubkeys: channelTypingEntries
.filter((entry) => !localAgentSet.has(entry.pubkey.toLowerCase()))
.filter(
(entry) =>
!managedAgentSet.has(entry.pubkey.toLowerCase()) &&
!relayAgentSet.has(entry.pubkey.toLowerCase()),
)
.map((entry) => entry.pubkey),
};
}, [managedAgentsQuery.data, typingEntries]);
}, [managedAgentsQuery.data, relayAgentsQuery.data, typingEntries]);
const messageProfiles = React.useMemo(() => {
const base =
mergeCurrentProfileIntoLookup(
messageProfilesQuery.data?.profiles,
currentProfile,
) ?? {};
// Merge managed agent names so system messages resolve instantly
// Merge agent names so system messages and activity pills resolve instantly
// (without waiting for the relay profile batch query).
const agents = managedAgentsQuery.data ?? [];
const merged = { ...base };
for (const agent of agents) {
for (const agent of managedAgentsQuery.data ?? []) {
const key = agent.pubkey.toLowerCase();
if (!merged[key]?.displayName) {
merged[key] = {
...merged[key],
displayName: agent.name,
avatarUrl: null,
avatarUrl: merged[key]?.avatarUrl ?? null,
nip05Handle: null,
};
}
}
for (const agent of relayAgentsQuery.data ?? []) {
const key = agent.pubkey.toLowerCase();
if (!merged[key]?.displayName) {
merged[key] = {
...merged[key],
displayName: agent.name,
avatarUrl: merged[key]?.avatarUrl ?? null,
nip05Handle: null,
};
}
@@ -214,9 +236,43 @@ export function ChannelScreen({
currentProfile,
managedAgentsQuery.data,
messageProfilesQuery.data?.profiles,
relayAgentsQuery.data,
]);
const channelMembersQuery = useChannelMembersQuery(activeChannel?.id ?? null);
const channelMembers = channelMembersQuery.data;
const activityAgents = React.useMemo<BotActivityAgent[]>(() => {
const byPubkey = new Map<string, BotActivityAgent>();
for (const agent of managedAgentsQuery.data ?? []) {
byPubkey.set(agent.pubkey.toLowerCase(), {
name: agent.name,
pubkey: agent.pubkey,
});
}
for (const agent of relayAgentsQuery.data ?? []) {
const key = agent.pubkey.toLowerCase();
if (!byPubkey.has(key)) {
byPubkey.set(key, { name: agent.name, pubkey: agent.pubkey });
}
}
for (const member of channelMembers ?? []) {
const key = member.pubkey.toLowerCase();
if (member.role === "bot" && !byPubkey.has(key)) {
byPubkey.set(key, {
name:
member.displayName ??
messageProfiles[key]?.displayName ??
`Agent ${member.pubkey.slice(0, 8)}`,
pubkey: member.pubkey,
});
}
}
return [...byPubkey.values()];
}, [
channelMembers,
managedAgentsQuery.data,
messageProfiles,
relayAgentsQuery.data,
]);
const personasQuery = usePersonasQuery();
const personaLookup = React.useMemo(() => {
const agents = managedAgentsQuery.data ?? [];
@@ -470,6 +526,7 @@ export function ChannelScreen({
<React.Suspense fallback={<ViewLoadingFallback kind="channel" />}>
<ChannelPane
activeChannel={activeChannel}
activityAgents={activityAgents}
agentSessionAgents={channelAgentSessionAgents}
botTypingEntries={botTypingEntries}
channelFind={channelFind}

View File

@@ -47,10 +47,8 @@ export function useChannelAgentSessions({
channelMembers.map((member) => normalizePubkey(member.pubkey)),
);
return managedAgents.filter(
(agent) =>
agent.backend.type === "local" &&
memberPubkeys.has(normalizePubkey(agent.pubkey)),
return managedAgents.filter((agent) =>
memberPubkeys.has(normalizePubkey(agent.pubkey)),
);
}, [channelMembers, managedAgents]);