fix(desktop): hydrate popped-out agent activity

Seed native agent conversation windows with the opener's current observer events so the Activity transcript matches the right-side panel immediately.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Thomas Petersen
2026-06-15 11:28:38 -04:00
parent 915b5290d6
commit bdb3bb1ee1
4 changed files with 98 additions and 2 deletions

View File

@@ -1,5 +1,6 @@
import { WebviewWindow } from "@tauri-apps/api/webviewWindow";
import type { ObserverEvent } from "@/features/agents/ui/agentSessionTypes";
import type { ChannelType, ManagedAgent } from "@/shared/api/types";
import { normalizePubkey } from "@/shared/lib/pubkey";
@@ -10,6 +11,7 @@ export type OpenAgentConversationWindowInput = {
channelId: string;
channelName: string;
channelType: ChannelType | null;
observerEvents?: readonly ObserverEvent[];
};
const DEFAULT_WIDTH = 460;
@@ -29,6 +31,28 @@ function windowLabel(channelId: string, agentPubkey: string): string {
);
}
export function agentConversationSeedStorageKey(
channelId: string,
agentPubkey: string,
): string {
return `buzz:agent-window-seed:${windowLabel(channelId, agentPubkey)}`;
}
function writeObserverSeed(input: OpenAgentConversationWindowInput) {
if (!input.observerEvents?.length) {
return;
}
window.localStorage.setItem(
agentConversationSeedStorageKey(input.channelId, input.agentPubkey),
JSON.stringify({
agentPubkey: input.agentPubkey,
events: input.observerEvents,
writtenAt: Date.now(),
}),
);
}
/**
* Open (or focus) a real OS window hosting the agent's live activity log and a
* composer to chat with it. The window loads the same frontend at the
@@ -39,6 +63,7 @@ export async function openAgentConversationWindow(
input: OpenAgentConversationWindowInput,
): Promise<void> {
const label = windowLabel(input.channelId, input.agentPubkey);
writeObserverSeed(input);
const existing = await WebviewWindow.getByLabel(label);
if (existing) {

View File

@@ -272,6 +272,15 @@ export function getAgentTranscript(
return state?.items ?? EMPTY_TRANSCRIPT;
}
export function seedAgentObserverEvents(
agentPubkey: string,
events: readonly ObserverEvent[],
) {
for (const event of events) {
appendAgentEvent(agentPubkey, event);
}
}
export function useManagedAgentObserverBridge(
agents: readonly Pick<ManagedAgent, "pubkey" | "status">[],
) {

View File

@@ -11,7 +11,11 @@ import {
useActiveAgentTurnsBridge,
} from "@/features/agents/activeAgentTurnsStore";
import { isManagedAgentActive } from "@/features/agents/lib/managedAgentControlActions";
import { useManagedAgentObserverBridge } from "@/features/agents/observerRelayStore";
import { agentConversationSeedStorageKey } from "@/features/agents/lib/openAgentConversationWindow";
import {
seedAgentObserverEvents,
useManagedAgentObserverBridge,
} from "@/features/agents/observerRelayStore";
import { ManagedAgentSessionPanel } from "@/features/agents/ui/ManagedAgentSessionPanel";
import { useChannelsQuery } from "@/features/channels/hooks";
import { mergeAgentNamesIntoProfiles } from "@/features/channels/ui/useChannelActivityTyping";
@@ -27,6 +31,7 @@ import { Badge } from "@/shared/ui/badge";
import { Button } from "@/shared/ui/button";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip";
import { UserAvatar } from "@/shared/ui/UserAvatar";
import type { ObserverEvent } from "./agentSessionTypes";
type AgentWindowScreenProps = {
channelId: string;
@@ -36,6 +41,22 @@ type AgentWindowScreenProps = {
channelType: ChannelType | null;
};
type ObserverSeedPayload = {
agentPubkey?: unknown;
events?: unknown;
};
function parseObserverSeed(raw: string): ObserverEvent[] | null {
try {
const parsed = JSON.parse(raw) as ObserverSeedPayload;
return Array.isArray(parsed.events)
? (parsed.events as ObserverEvent[])
: null;
} catch {
return null;
}
}
/**
* Chrome-less conversation view that fills its own OS window (opened via
* `openAgentConversationWindow`). Renders the agent's live activity log and a
@@ -50,6 +71,43 @@ export function AgentWindowScreen({
channelType,
}: AgentWindowScreenProps) {
const { ref: scrollRef, onScroll } = useStickToBottom<HTMLDivElement>();
const seedStorageKey = React.useMemo(
() => agentConversationSeedStorageKey(channelId, agentPubkey),
[channelId, agentPubkey],
);
const applyObserverSeed = React.useCallback(
(raw: string | null) => {
if (!raw) {
return;
}
const events = parseObserverSeed(raw);
if (!events?.length) {
return;
}
seedAgentObserverEvents(agentPubkey, events);
},
[agentPubkey],
);
React.useEffect(() => {
applyObserverSeed(window.localStorage.getItem(seedStorageKey));
window.localStorage.removeItem(seedStorageKey);
function handleStorage(event: StorageEvent) {
if (
event.storageArea !== window.localStorage ||
event.key !== seedStorageKey
) {
return;
}
applyObserverSeed(event.newValue);
window.localStorage.removeItem(seedStorageKey);
}
window.addEventListener("storage", handleStorage);
return () => window.removeEventListener("storage", handleStorage);
}, [applyObserverSeed, seedStorageKey]);
const managedAgentsQuery = useManagedAgentsQuery();
const relayAgentsQuery = useRelayAgentsQuery();

View File

@@ -18,7 +18,10 @@ import {
usePersonasQuery,
useRelayAgentsQuery,
} from "@/features/agents/hooks";
import { useManagedAgentObserverBridge } from "@/features/agents/observerRelayStore";
import {
getAgentObserverSnapshot,
useManagedAgentObserverBridge,
} from "@/features/agents/observerRelayStore";
import { openAgentConversationWindow } from "@/features/agents/lib/openAgentConversationWindow";
import {
mergeMessages,
@@ -440,6 +443,7 @@ export function ChannelScreen({
channelId: activeChannel.id,
channelName: activeChannel.name,
channelType: activeChannel.channelType,
observerEvents: getAgentObserverSnapshot(agent.pubkey, true).events,
});
},
[activeChannel, channelAgentSessionAgents],