Expand announcement demo conversations

This commit is contained in:
kenny lopez
2026-07-17 17:11:29 +02:00
parent 26abbda843
commit d16d405a64
6 changed files with 527 additions and 68 deletions

View File

@@ -23,6 +23,8 @@ type ProviderErrorBody = {
message?: string;
};
type OpenAiReasoningEffort = "low" | "minimal" | "none";
type AgentEnvironment = Record<string, string | undefined>;
function nonEmptyString(value: unknown) {
@@ -198,6 +200,22 @@ function extractOpenAiText(body: unknown) {
return text || null;
}
export function announcementDemoOpenAiReasoningEffort(
model: string,
): OpenAiReasoningEffort | null {
const normalizedModel = model.trim().toLowerCase();
if (/^gpt-5\.(?:4|5|6)(?:[.-]|$)/.test(normalizedModel)) {
return "none";
}
if (/^gpt-5(?:[.-]|$)/.test(normalizedModel)) {
return "minimal";
}
if (/^o[1-9](?:[.-]|$)/.test(normalizedModel)) {
return "low";
}
return null;
}
function extractAnthropicText(body: unknown) {
if (typeof body !== "object" || body === null) {
return null;
@@ -215,6 +233,7 @@ function extractAnthropicText(body: unknown) {
}
async function requestOpenAiResponse(input: AnnouncementDemoAgentRequest) {
const reasoningEffort = announcementDemoOpenAiReasoningEffort(input.model);
const response = await fetch("https://api.openai.com/v1/responses", {
method: "POST",
headers: {
@@ -225,7 +244,8 @@ async function requestOpenAiResponse(input: AnnouncementDemoAgentRequest) {
model: input.model,
instructions: input.systemPrompt,
input: input.messages,
max_output_tokens: 350,
max_output_tokens: 2_000,
...(reasoningEffort ? { reasoning: { effort: reasoningEffort } } : {}),
store: false,
}),
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
@@ -239,6 +259,18 @@ async function requestOpenAiResponse(input: AnnouncementDemoAgentRequest) {
const text = extractOpenAiText(body);
if (!text) {
const responseDetails = body as {
status?: unknown;
incomplete_details?: { reason?: unknown };
};
if (
responseDetails.status === "incomplete" &&
responseDetails.incomplete_details?.reason === "max_output_tokens"
) {
throw new Error(
"OpenAI ran out of output tokens before producing visible text.",
);
}
throw new Error("OpenAI returned a response without any text.");
}
return text;

View File

@@ -11,9 +11,11 @@ import { CommunityOnboardingProvider } from "@/features/onboarding/communityOnbo
import {
ANNOUNCEMENT_DEMO_AGENTS,
ANNOUNCEMENT_DEMO_COMMUNITY_NAME,
ANNOUNCEMENT_DEMO_INITIAL_READ_CHANNEL_IDS,
ANNOUNCEMENT_DEMO_PEOPLE,
ANNOUNCEMENT_DEMO_SECTION_STORE,
} from "@/testing/announcementDemoFixtures";
import { localReadStateKey } from "@/features/channels/readState/readStateFormat";
import { ThemeProvider } from "@/shared/theme/ThemeProvider";
import { EmojiBurstProvider } from "@/shared/ui/EmojiBurstProvider";
import { PoofBurstProvider } from "@/shared/ui/PoofBurstProvider";
@@ -66,6 +68,8 @@ function configureMockBridgeFromUrl() {
return;
}
// The native recording build intentionally reuses the browser mock bridge;
// only live model calls leave the webview through the local provider proxy.
const e2eWindow = window as E2eWindow;
if (isAnnouncementDemo) {
e2eWindow.__BUZZ_E2E__ = {
@@ -104,10 +108,22 @@ function configureMockBridgeFromUrl() {
if (isAnnouncementDemo) {
const relayStorageScope = encodeURIComponent(mockRelayWsUrl);
const initialReadAt = new Date().toISOString();
window.localStorage.setItem(
`${CHANNEL_SECTIONS_STORAGE_KEY_PREFIX}:${E2E_DEFAULT_PUBKEY}:${relayStorageScope}`,
JSON.stringify(ANNOUNCEMENT_DEMO_SECTION_STORE),
);
window.localStorage.setItem(
localReadStateKey(E2E_DEFAULT_PUBKEY),
JSON.stringify(
Object.fromEntries(
ANNOUNCEMENT_DEMO_INITIAL_READ_CHANNEL_IDS.map((channelId) => [
channelId,
initialReadAt,
]),
),
),
);
window.localStorage.setItem(
`${SELF_PROFILE_STORAGE_KEY_PREFIX}:${mockRelayWsUrl}:${E2E_DEFAULT_PUBKEY}`,
JSON.stringify({

View File

@@ -1,7 +1,10 @@
import assert from "node:assert/strict";
import test from "node:test";
import { resolveAnnouncementDemoAgentRequest } from "../../announcementDemoAgentPlugin.ts";
import {
announcementDemoOpenAiReasoningEffort,
resolveAnnouncementDemoAgentRequest,
} from "../../announcementDemoAgentPlugin.ts";
const baseRequest = {
provider: null,
@@ -53,3 +56,10 @@ test("prefers credentials entered in the app over environment fallbacks", () =>
assert.equal(request.apiKey, "in-app-key");
assert.equal(request.model, "in-app-model");
});
test("uses the smallest supported reasoning effort for short demo replies", () => {
assert.equal(announcementDemoOpenAiReasoningEffort("gpt-5.4-mini"), "none");
assert.equal(announcementDemoOpenAiReasoningEffort("gpt-5"), "minimal");
assert.equal(announcementDemoOpenAiReasoningEffort("o3-mini"), "low");
assert.equal(announcementDemoOpenAiReasoningEffort("gpt-4.1-mini"), null);
});

View File

@@ -25,7 +25,13 @@ export const ANNOUNCEMENT_DEMO_AGENTS = [
avatarUrl: "/demo/agents/fizz.png",
systemPrompt:
"You are Fizz, an energetic maker who turns ideas into action. Be upbeat, practical, and decisive. Help users plan, create, solve problems, and finish work. Add occasional bee wordplay or 🐝✨—keep it charming, never distracting.",
channelNames: ["flight-path", "design", "marketing", "queen-bee-launch"],
channelNames: [
"flight-path",
"engineering",
"design",
"marketing",
"queen-bee-launch",
],
},
{
pubkey: "71".repeat(32),
@@ -33,7 +39,13 @@ export const ANNOUNCEMENT_DEMO_AGENTS = [
avatarUrl: "/demo/agents/honey.png",
systemPrompt:
"You are Honey, a warm and thoughtful communicator. Help users write clearly, organize ideas, brainstorm, summarize, and prepare for conversations. Be kind, creative, and concise. Add occasional bee wordplay or 🍯🐝—keep it sweet, never excessive.",
channelNames: ["flight-path", "design", "marketing", "queen-bee-launch"],
channelNames: [
"flight-path",
"engineering",
"design",
"marketing",
"queen-bee-launch",
],
},
{
pubkey: "72".repeat(32),
@@ -41,47 +53,104 @@ export const ANNOUNCEMENT_DEMO_AGENTS = [
avatarUrl: "/demo/agents/bumble.png",
systemPrompt:
"You are Bumble, a curious and adventurous researcher. Explore questions, compare options, check assumptions, and explain what you find clearly. Be candid when uncertain and favor useful evidence. Add occasional bee wordplay or 🐝🔎—keep it playful, never chaotic.",
channelNames: ["flight-path", "design", "marketing", "queen-bee-launch"],
channelNames: [
"flight-path",
"engineering",
"design",
"marketing",
"queen-bee-launch",
],
},
] as const;
export type AnnouncementDemoAgentName =
(typeof ANNOUNCEMENT_DEMO_AGENTS)[number]["name"];
export const ANNOUNCEMENT_DEMO_LIVE_CONVERSATION = {
channelName: "flight-path",
steps: [
{
delayMs: 1_800,
export const ANNOUNCEMENT_DEMO_LIVE_CONVERSATIONS = [
{
channelName: "flight-path",
agentOrder: ["Fizz", "Honey", "Bumble"],
humanClose: {
delayMs: 1_000,
author: "producer",
content: "That’s the version. I can cut to that — nice swarm work 🐝",
},
steps: [
{
delayMs: 2_400,
author: "engineer",
content:
"Small thing: the desktop-to-mobile handoff still feels a little fast.",
agentMentions: [],
startsAgentChain: false,
},
{
delayMs: 2_800,
author: "designer",
content: "Yeah — I want one extra beat on the sent message.",
agentMentions: [],
startsAgentChain: false,
},
{
delayMs: 2_600,
author: "producer",
content: "That would give the camera somewhere to land too.",
agentMentions: [],
startsAgentChain: false,
},
{
delayMs: 2_800,
author: "engineer",
content:
"@Fizz can you turn that into a clean three-beat capture plan?",
agentMentions: ["Fizz"],
startsAgentChain: true,
},
],
},
{
channelName: "engineering",
agentOrder: ["Bumble", "Fizz", "Honey"],
humanClose: {
delayMs: 1_100,
author: "engineer",
content:
"Small thing: the desktop-to-mobile handoff still feels a little fast.",
agentMentions: [],
startsAgentChain: false,
"Perfect. That gives me the patch and the verification path — I’m on it.",
},
{
delayMs: 2_200,
author: "designer",
content: "Yeah — I want one extra beat on the sent message.",
agentMentions: [],
startsAgentChain: false,
},
{
delayMs: 2_000,
author: "producer",
content: "That would give the camera somewhere to land too.",
agentMentions: [],
startsAgentChain: false,
},
{
delayMs: 2_200,
author: "engineer",
content: "@Fizz can you turn that into a clean three-beat capture plan?",
agentMentions: ["Fizz"],
startsAgentChain: true,
},
],
} as const;
steps: [
{
delayMs: 2_600,
author: "qa",
content:
"I can still reproduce one duplicate message after waking the laptop.",
agentMentions: [],
startsAgentChain: false,
},
{
delayMs: 3_000,
author: "engineer",
content: "Only on the first reconnect?",
agentMentions: [],
startsAgentChain: false,
},
{
delayMs: 2_800,
author: "qa",
content: "Yep. One duplicate, then the subscription settles.",
agentMentions: [],
startsAgentChain: false,
},
{
delayMs: 3_000,
author: "engineer",
content:
"@Bumble can you trace the likely path and pull Fizz and Honey into a fix plan?",
agentMentions: ["Bumble"],
startsAgentChain: true,
},
],
},
] as const;
export const ANNOUNCEMENT_DEMO_PEOPLE = {
viewer: {
@@ -220,6 +289,25 @@ export const ANNOUNCEMENT_DEMO_CHANNELS = [
["producer", "member"],
],
},
{
id: "4e91b4d2-8207-4d63-9b1a-3c72a8f01142",
name: "engineering",
channelType: "stream",
visibility: "open",
description: "Desktop, relay, and infrastructure engineering",
topic: "Reconnect reliability and the next release candidate",
purpose: "Debug together, review changes, and keep the build healthy.",
createdBy: "engineer",
lastMessageMinutesAgo: 5,
memberRoles: [
["engineer", "owner"],
["viewer", "admin"],
["qa", "member"],
["data", "member"],
["support", "member"],
["designer", "member"],
],
},
{
id: "b5e2f8a1-3c44-5912-9e67-4a8d1f2b3c4e",
name: "design",
@@ -357,6 +445,18 @@ export const ANNOUNCEMENT_DEMO_DMS = [
},
] as const;
// Start the recording workspace with a believable mix of read and unread
// conversations. Channels omitted from this list retain their unread state.
export const ANNOUNCEMENT_DEMO_INITIAL_READ_CHANNEL_IDS = [
"9dae0116-799b-5071-a0a8-fdd30a91a35d", // general
"1c7e1c02-87bb-5e88-b2da-5a7a9432d0c9", // flight-path
"94a444a4-c0a3-5966-ab05-530c6ddc2301", // mobile
"a27e1ee9-76a6-5bdf-a5d5-1d85610dad11", // product-ideas
"1be1dcdb-4c31-5a8c-81de-ac102552ca10", // launch-notes
"7eb9f239-9393-50b0-bd76-d85eef0511c7", // Jordan Brooks DM
"d1ec7000-d000-4000-8000-000000000001", // Priya Shah DM
] as const;
export const ANNOUNCEMENT_DEMO_SECTION_STORE = {
version: 1,
sections: [
@@ -373,6 +473,7 @@ export const ANNOUNCEMENT_DEMO_SECTION_STORE = {
"9a1657ac-f7aa-5db0-b632-d8bbeb6dfb50": "the-hive",
"9dae0116-799b-5071-a0a8-fdd30a91a35d": "the-hive",
"1c7e1c02-87bb-5e88-b2da-5a7a9432d0c9": "product-garden",
"4e91b4d2-8207-4d63-9b1a-3c72a8f01142": "product-garden",
"b5e2f8a1-3c44-5912-9e67-4a8d1f2b3c4e": "product-garden",
"94a444a4-c0a3-5966-ab05-530c6ddc2301": "product-garden",
"a27e1ee9-76a6-5bdf-a5d5-1d85610dad11": "product-garden",
@@ -798,6 +899,142 @@ export const ANNOUNCEMENT_DEMO_MESSAGES: Record<
reactions: [{ emoji: "🎬", authors: ["producer", "marketing"] }],
},
],
"4e91b4d2-8207-4d63-9b1a-3c72a8f01142": [
{
id: "announcement-demo-engineering-old-1",
author: "engineer",
minutesAgo: 6_240,
content:
"I moved reconnect ownership into the subscription manager. Less state crossing the bridge now.",
},
{
id: "announcement-demo-engineering-old-2",
author: "qa",
minutesAgo: 6_231,
content: "Nice. I’ll run the sleep / wake matrix against it.",
},
{
id: "announcement-demo-engineering-old-3",
author: "qa",
minutesAgo: 6_198,
content: "First ten loops are clean.",
reactions: [{ emoji: "✅", authors: ["engineer", "viewer"] }],
},
{
id: "announcement-demo-engineering-old-4",
author: "engineer",
minutesAgo: 6_194,
content: "love to hear it",
},
{
id: "announcement-demo-engineering-old-5",
author: "data",
minutesAgo: 4_330,
content: "Reconnect p95 dropped from 2.8s to 1.1s on the canary cohort.",
reactions: [{ emoji: "⚡", authors: ["engineer", "qa"] }],
},
{
id: "announcement-demo-engineering-old-6",
author: "engineer",
minutesAgo: 4_318,
content: "That lines up with removing the second backoff window.",
},
{
id: "announcement-demo-engineering-old-7",
author: "support",
minutesAgo: 2_915,
content: "Two preview teams reported a duplicated message after sleep.",
},
{
id: "announcement-demo-engineering-old-8",
author: "engineer",
minutesAgo: 2_910,
content: "Same OS version?",
},
{
id: "announcement-demo-engineering-old-9",
author: "support",
minutesAgo: 2_904,
content: "Both on macOS 15. One Intel, one Apple silicon.",
},
{
id: "announcement-demo-engineering-old-10",
author: "engineer",
minutesAgo: 1_860,
content:
"Reproduced. The old generation can deliver once after the new one starts.",
},
{
id: "announcement-demo-engineering-pr",
author: "engineer",
minutesAgo: 1_842,
content:
"Guard is up for review: https://github.com/block/buzz/pull/2037",
reactions: [{ emoji: "👀", authors: ["qa", "viewer"] }],
},
{
id: "announcement-demo-engineering-old-11",
author: "qa",
minutesAgo: 1_831,
content: "Pulling it now.",
},
{
id: "announcement-demo-engineering-old-12",
author: "qa",
minutesAgo: 1_804,
content: "Twenty reconnects, no duplicates.",
reactions: [{ emoji: "🙌", authors: ["engineer", "support"] }],
},
{
id: "announcement-demo-engineering-1",
author: "engineer",
minutesAgo: 94,
content: "Tightened the generation guard this morning.",
},
{
id: "announcement-demo-engineering-2",
author: "engineer",
minutesAgo: 92,
content: "```ts\nif (generation !== activeGeneration) return;\n```",
},
{
id: "announcement-demo-engineering-3",
author: "qa",
minutesAgo: 76,
content: "Manual reconnect and offline recovery both look good.",
},
{
id: "announcement-demo-engineering-4",
author: "data",
minutesAgo: 61,
content:
"Telemetry is clean too — no doubled fan-out in the latest sample.",
},
{
id: "announcement-demo-engineering-linear",
author: "engineer",
minutesAgo: 43,
content:
"I put the remaining soak checklist in [BUZ-519 · Reconnect reliability](https://linear.app/honeycomb/issue/BUZ-519/reconnect-reliability).",
reactions: [{ emoji: "✅", authors: ["qa"] }],
},
{
id: "announcement-demo-engineering-5",
author: "support",
minutesAgo: 22,
content: "Customer replay passed on both affected machines.",
},
{
id: "announcement-demo-engineering-6",
author: "engineer",
minutesAgo: 5,
content: "Release build is green. I’m doing one last sleep / wake pass.",
reactions: [
{ emoji: "🚀", authors: ["qa", "viewer"] },
{ emoji: "🙏", authors: ["support"] },
],
},
],
"b5e2f8a1-3c44-5912-9e67-4a8d1f2b3c4e": [
{
id: "announcement-demo-design-old-1",

View File

@@ -53,7 +53,7 @@ import {
ANNOUNCEMENT_DEMO_AGENTS,
ANNOUNCEMENT_DEMO_CHANNELS,
ANNOUNCEMENT_DEMO_DMS,
ANNOUNCEMENT_DEMO_LIVE_CONVERSATION,
ANNOUNCEMENT_DEMO_LIVE_CONVERSATIONS,
ANNOUNCEMENT_DEMO_MESSAGES,
ANNOUNCEMENT_DEMO_PEOPLE,
ANNOUNCEMENT_DEMO_PROJECT_SUBJECTS,
@@ -2460,6 +2460,32 @@ const mockChannels: MockChannel[] = [
createMockMember(BOB_PUBKEY, "member", 900),
],
}),
createMockChannel({
id: "4e91b4d2-8207-4d63-9b1a-3c72a8f01142",
name: "engineering-demo",
channel_type: "stream",
visibility: "open",
description: "Desktop, relay, and infrastructure engineering",
topic: "Reconnect reliability and the next release candidate",
purpose: "Debug together, review changes, and keep the build healthy.",
last_message_at: isoMinutesAgo(5),
archived_at: null,
created_by: ALICE_PUBKEY,
topic_set_by: ALICE_PUBKEY,
topic_set_at: isoMinutesAgo(100),
purpose_set_by: ALICE_PUBKEY,
purpose_set_at: isoMinutesAgo(110),
topic_required: false,
max_members: null,
nip29_group_id: null,
created_minutes_ago: 1310,
updated_minutes_ago: 5,
members: [
createMockMember(ALICE_PUBKEY, "owner", 1310),
createMockMember(MOCK_IDENTITY_PUBKEY, "member", 1170),
createMockMember(BOB_PUBKEY, "member", 890),
],
}),
createMockChannel({
id: "94a444a4-c0a3-5966-ab05-530c6ddc2301",
name: "agents",
@@ -2721,7 +2747,7 @@ let mockWebsocketSendMutexWedged = false;
const realSockets = new Map<number, WebSocket>();
let mockManagedAgents: MockManagedAgent[] = [];
const pendingAnnouncementDemoAgentResponses = new Set<string>();
let announcementDemoLiveConversationStarted = false;
const startedAnnouncementDemoLiveConversations = new Set<string>();
let mockGlobalAgentConfig: MockGlobalAgentConfig = {
env_vars: {},
provider: null,
@@ -4216,6 +4242,32 @@ type AnnouncementDemoAgentReply = {
error?: string;
};
function getAnnouncementDemoLiveConversation(channelId: string) {
const channel = mockChannels.find((candidate) => candidate.id === channelId);
return ANNOUNCEMENT_DEMO_LIVE_CONVERSATIONS.find(
(conversation) => conversation.channelName === channel?.name,
);
}
function getAnnouncementDemoNextAgentName(
channelId: string,
currentAgentPubkey: string,
): AnnouncementDemoAgentName | null {
const currentAgent = ANNOUNCEMENT_DEMO_AGENTS.find(
(candidate) =>
normalizePubkey(candidate.pubkey) === normalizePubkey(currentAgentPubkey),
);
if (!currentAgent) {
return null;
}
const agentOrder =
getAnnouncementDemoLiveConversation(channelId)?.agentOrder ??
ANNOUNCEMENT_DEMO_AGENTS.map((agent) => agent.name);
const currentIndex = agentOrder.indexOf(currentAgent.name);
return agentOrder[currentIndex + 1] ?? null;
}
function getAnnouncementDemoProviderConfig(
agent: MockManagedAgent,
): AnnouncementDemoProviderConfig {
@@ -4285,21 +4337,18 @@ function getAnnouncementDemoAgentTargets(channelId: string, event: RelayEvent) {
channel.channel_type === "dm"
? new Set(channel.members.map((member) => normalizePubkey(member.pubkey)))
: new Set<string>();
const sourceAgentIndex = isAgentAuthored
? ANNOUNCEMENT_DEMO_AGENTS.findIndex(
(candidate) =>
normalizePubkey(candidate.pubkey) === normalizePubkey(event.pubkey),
)
: -1;
const nextAgentName = isAgentAuthored
? getAnnouncementDemoNextAgentName(channelId, event.pubkey)
: null;
return mockManagedAgents.filter((agent) => {
const pubkey = normalizePubkey(agent.pubkey);
const isActive = agent.status === "running" || agent.status === "deployed";
if (isAgentAuthored) {
const targetAgentIndex = ANNOUNCEMENT_DEMO_AGENTS.findIndex(
const targetDefinition = ANNOUNCEMENT_DEMO_AGENTS.find(
(candidate) => normalizePubkey(candidate.pubkey) === pubkey,
);
if (targetAgentIndex !== sourceAgentIndex + 1) {
if (targetDefinition?.name !== nextAgentName) {
return false;
}
}
@@ -4310,6 +4359,7 @@ function getAnnouncementDemoAgentTargets(channelId: string, event: RelayEvent) {
}
function getAnnouncementDemoNextAgent(
channelId: string,
sourceEvent: RelayEvent,
agent: MockManagedAgent,
) {
@@ -4320,11 +4370,13 @@ function getAnnouncementDemoNextAgent(
return null;
}
const currentIndex = ANNOUNCEMENT_DEMO_AGENTS.findIndex(
(candidate) =>
normalizePubkey(candidate.pubkey) === normalizePubkey(agent.pubkey),
const nextAgentName = getAnnouncementDemoNextAgentName(
channelId,
agent.pubkey,
);
const nextDefinition = ANNOUNCEMENT_DEMO_AGENTS.find(
(candidate) => candidate.name === nextAgentName,
);
const nextDefinition = ANNOUNCEMENT_DEMO_AGENTS[currentIndex + 1];
if (!nextDefinition) {
return null;
}
@@ -4374,7 +4426,7 @@ function getAnnouncementDemoSystemPrompt(
channel?.channel_type === "dm"
? "a direct message"
: `#${channel?.name ?? "a team channel"}`;
const nextAgent = getAnnouncementDemoNextAgent(sourceEvent, agent);
const nextAgent = getAnnouncementDemoNextAgent(channelId, sourceEvent, agent);
const isAgentChainEvent = sourceEvent.tags.some(
(tag) => tag[0] === ANNOUNCEMENT_DEMO_AGENT_CHAIN_TAG,
);
@@ -4383,7 +4435,7 @@ function getAnnouncementDemoSystemPrompt(
: isAgentChainEvent
? " This is the final turn in a short agent collaboration: respond directly to the previous agent, share one useful check or refinement, and close the loop without handing off again."
: "";
return `${basePrompt}\n\nYou are replying in ${location}. Use the conversation context, sound like a real colleague, and do not prefix the reply with your name.${collaborationInstruction}`;
return `${basePrompt}\n\nYou are replying in ${location}. Use the conversation context, sound like a real colleague, and do not prefix the reply with your name. Keep the reply to one to three short sentences with no lists or code blocks.${collaborationInstruction}`;
}
async function requestAnnouncementDemoAgentReply(
@@ -4392,6 +4444,8 @@ async function requestAnnouncementDemoAgentReply(
sourceEvent: RelayEvent,
) {
const provider = getAnnouncementDemoProviderConfig(agent);
// The local Vite proxy keeps provider credentials out of the webview while
// still allowing the native demo to use the settings entered in the app.
const response = await fetch("/__announcement-demo/agent-response", {
method: "POST",
headers: { "Content-Type": "application/json" },
@@ -4437,7 +4491,7 @@ function publishAnnouncementDemoAgentReply(
(tag) => tag[0] === ANNOUNCEMENT_DEMO_AGENT_CHAIN_TAG,
);
const nextAgent = continueAgentChain
? getAnnouncementDemoNextAgent(sourceEvent, agent)
? getAnnouncementDemoNextAgent(channelId, sourceEvent, agent)
: null;
const mentionPubkeys = [
sourceEvent.pubkey,
@@ -4472,16 +4526,24 @@ function publishAnnouncementDemoAgentReply(
ANNOUNCEMENT_DEMO_AGENT_HANDOFF_DELAY_MS,
);
} else if (isAgentChainEvent && continueAgentChain) {
const producerPubkey = getAnnouncementDemoPersonPubkey("producer");
const humanClose = getAnnouncementDemoLiveConversation(channelId)
?.humanClose ?? {
delayMs: 1_000,
author: "producer" as const,
content: "That’s the version. I can cut to that — nice swarm work 🐝",
};
const closingAuthorPubkey = getAnnouncementDemoPersonPubkey(
humanClose.author,
);
window.setTimeout(() => {
emitMockTypingIndicator(channelId, producerPubkey);
emitMockTypingIndicator(channelId, closingAuthorPubkey);
window.setTimeout(
() =>
emitMockChannelMessage(
channelId,
"That’s the version. I can cut to that — nice swarm work 🐝",
humanClose.content,
null,
producerPubkey,
closingAuthorPubkey,
9,
undefined,
[["announcement-demo-live", "1"]],
@@ -4490,7 +4552,7 @@ function publishAnnouncementDemoAgentReply(
),
650,
);
}, ANNOUNCEMENT_DEMO_HUMAN_CLOSE_DELAY_MS);
}, humanClose.delayMs);
}
}
@@ -4501,7 +4563,6 @@ const ANNOUNCEMENT_DEMO_AGENT_WORKING_DELAY_MS = 350;
const ANNOUNCEMENT_DEMO_AGENT_MIN_TURN_MS = 1_800;
const ANNOUNCEMENT_DEMO_AGENT_REACTION_CLEANUP_DELAY_MS = 2_500;
const ANNOUNCEMENT_DEMO_AGENT_HANDOFF_DELAY_MS = 700;
const ANNOUNCEMENT_DEMO_HUMAN_CLOSE_DELAY_MS = 700;
function publishAnnouncementDemoAgentReaction(
channelId: string,
@@ -4635,25 +4696,25 @@ function queueAnnouncementDemoAgentResponses(
function maybeStartAnnouncementDemoLiveConversation(channelId: string) {
if (
announcementDemoLiveConversationStarted ||
startedAnnouncementDemoLiveConversations.has(channelId) ||
!getConfig()?.mock?.announcementDemo
) {
return;
}
const channel = mockChannels.find((candidate) => candidate.id === channelId);
if (channel?.name !== ANNOUNCEMENT_DEMO_LIVE_CONVERSATION.channelName) {
const conversation = getAnnouncementDemoLiveConversation(channelId);
if (!conversation) {
return;
}
announcementDemoLiveConversationStarted = true;
startedAnnouncementDemoLiveConversations.add(channelId);
let elapsedMs = 0;
for (const step of ANNOUNCEMENT_DEMO_LIVE_CONVERSATION.steps) {
for (const step of conversation.steps) {
elapsedMs += step.delayMs;
const authorPubkey = getAnnouncementDemoPersonPubkey(step.author);
window.setTimeout(
() => emitMockTypingIndicator(channelId, authorPubkey),
Math.max(0, elapsedMs - 850),
Math.max(0, elapsedMs - 1_000),
);
window.setTimeout(() => {
const agentMentionPubkeys = (

View File

@@ -3,7 +3,7 @@ import { expect, test } from "@playwright/test";
test("announcement demo loads its workspace, people, and projects", async ({
page,
}) => {
test.setTimeout(60_000);
test.setTimeout(75_000);
const agentReply =
"I’d lead with the handoff moment, then land on the shared launch room. That gives the story a clear before-and-after.";
const fizzReply =
@@ -12,6 +12,14 @@ test("announcement demo loads its workspace, people, and projects", async ({
"Frame it as one thought moving with you: send, surface, arrive together. @Bumble, can you sanity-check the sequence? 🍯";
const bumbleReply =
"The sequence tracks. Keep the notification visible for a full beat before the cut, and the handoff will read without narration. 🐝🔎";
const engineeringBumbleReply =
"The duplicate points to the previous subscription generation delivering once after reconnect. @Fizz, can you turn that into the smallest safe guard?";
const engineeringFizzReply =
"I’d reject callbacks whose generation no longer matches the active subscription, then add a sleep-wake regression case. @Honey, can you package the rollout check?";
const engineeringHoneyReply =
"Ship the guard behind the existing reconnect path, run the twenty-cycle soak, and call out duplicate delivery in the release checklist. That closes the loop cleanly.";
const engineeringHumanClose =
"Perfect. That gives me the patch and the verification path — I’m on it.";
const humanClose =
"That’s the version. I can cut to that — nice swarm work 🐝";
const fizzReplyVisible = fizzReply.replace("@Honey", "Honey");
@@ -34,8 +42,14 @@ test("announcement demo loads its workspace, people, and projects", async ({
const isFinalTurn = body.systemPrompt?.includes(
"final turn in a short agent collaboration",
);
const text =
body.systemPrompt?.includes("You are Fizz") && isCollaborativeTurn
const isEngineering = body.systemPrompt?.includes("in #engineering");
const text = isEngineering
? body.systemPrompt?.includes("You are Bumble") && isCollaborativeTurn
? engineeringBumbleReply
: body.systemPrompt?.includes("You are Fizz") && isCollaborativeTurn
? engineeringFizzReply
: engineeringHoneyReply
: body.systemPrompt?.includes("You are Fizz") && isCollaborativeTurn
? fizzReply
: body.systemPrompt?.includes("You are Honey") && isCollaborativeTurn
? honeyReply
@@ -99,6 +113,54 @@ test("announcement demo loads its workspace, people, and projects", async ({
).toBeVisible();
await expect(page.getByTestId("channel-DM")).toHaveCount(3);
const unreadChannels = [
"announcements",
"engineering",
"design",
"marketing",
"queen-bee-launch",
] as const;
const readChannels = [
"general",
"flight-path",
"mobile",
"product-ideas",
"launch-notes",
] as const;
for (const channel of unreadChannels) {
await expect(
page
.getByTestId(`channel-${channel}`)
.locator("[data-testid^='channel-unread-']"),
).toBeVisible();
}
for (const channel of readChannels) {
await expect(
page
.getByTestId(`channel-${channel}`)
.locator("[data-testid^='channel-unread-']"),
).toHaveCount(0);
}
const mayaDm = page
.getByTestId("channel-DM")
.filter({ hasText: "Maya Chen" });
const jordanDm = page
.getByTestId("channel-DM")
.filter({ hasText: "Jordan Brooks" });
const priyaDm = page
.getByTestId("channel-DM")
.filter({ hasText: "Priya Shah" });
await expect(
mayaDm.locator("xpath=..").getByTestId("channel-unread-DM"),
).toBeVisible();
await expect(
jordanDm.locator("xpath=..").getByTestId("channel-unread-DM"),
).toHaveCount(0);
await expect(
priyaDm.locator("xpath=..").getByTestId("channel-unread-DM"),
).toHaveCount(0);
await page.getByTestId("channel-flight-path").click();
const channelTimeline = page.getByTestId("message-timeline");
await expect(channelTimeline).toContainText("Marcus Reed");
@@ -210,6 +272,47 @@ test("announcement demo loads its workspace, people, and projects", async ({
await page.getByTestId("send-message").click();
await expect(channelTimeline).toContainText(agentReply, { timeout: 10_000 });
await page.getByTestId("channel-engineering").click();
await expect(channelTimeline).toContainText(
"Release build is green. I’m doing one last sleep / wake pass.",
);
await expect(
channelTimeline.locator('[data-link-preview="github-pull-request"]'),
).toBeVisible();
await expect(
channelTimeline.locator('[data-link-preview="linear-issue"]'),
).toBeVisible();
await expect(channelTimeline).toContainText(
"I can still reproduce one duplicate message after waking the laptop.",
{ timeout: 5_000 },
);
await expect(channelTimeline).toContainText("Only on the first reconnect?");
await expect(channelTimeline).toContainText(
"One duplicate, then the subscription settles.",
);
await expect(channelTimeline).toContainText(
"Bumble can you trace the likely path and pull Fizz and Honey into a fix plan?",
{ timeout: 10_000 },
);
await expect(channelTimeline).toContainText(
engineeringBumbleReply.replace("@Fizz", "Fizz"),
{ timeout: 5_000 },
);
await expect(channelTimeline).toContainText(
engineeringFizzReply.replace("@Honey", "Honey"),
{ timeout: 5_000 },
);
await expect(channelTimeline).toContainText(engineeringHoneyReply, {
timeout: 5_000,
});
await expect(channelTimeline).toContainText(engineeringHumanClose, {
timeout: 5_000,
});
const engineeringAgentRows = page
.getByTestId("message-row")
.filter({ has: page.locator('img[src^="/demo/agents/"]') });
await expect(engineeringAgentRows).toHaveCount(3);
const populatedChannels = [
["announcements", "Final smoke pass is clean"],
["general", "Please nobody breathe on main"],