mirror of
https://github.com/block/buzz.git
synced 2026-11-09 10:51:42 +00:00
Expand announcement demo conversations
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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"],
|
||||
|
||||
Reference in New Issue
Block a user