mirror of
https://github.com/block/buzz.git
synced 2026-11-12 20:31:12 +00:00
**Category:** new-feature **User Impact:** Users can keep selected agents addressed across consecutive messages without retyping their handles. **Problem:** Repeated conversations with agents require manually typing the same mentions on every turn, which adds friction and makes recipients easy to omit. **Solution:** The composer can now keep agents automatically addressed per channel, either from the mention controls or after a successful inline mention. Addressed agents remain visible in the toolbar, apply to channel threads, survive send failures safely, and never cross community boundaries. ## Changes <details> <summary>File changes</summary> **desktop/src-tauri/src/events/message_tags.rs** Preserves the automatic-address marker on validated mention reference tags. **desktop/src/features/channels/ui/ChannelPane.tsx** Wires the appropriate channel, thread, inbox, or forum composer context without leaking audiences across surfaces. **desktop/src/features/communities/useCommunityInit.ts** Clears composer audience state when the active community changes. **desktop/src/features/forum/ui/ForumComposer.tsx** Wires the appropriate channel, thread, inbox, or forum composer context without leaking audiences across surfaces. **desktop/src/features/forum/ui/ForumComposerAutocompletes.tsx** Wires the appropriate channel, thread, inbox, or forum composer context without leaking audiences across surfaces. **desktop/src/features/home/ui/InboxDetailPane.tsx** Wires the appropriate channel, thread, inbox, or forum composer context without leaking audiences across surfaces. **desktop/src/features/messages/lib/agentAddressMention.d.mts** Defines helpers and types for marked automatic-address mention tags. **desktop/src/features/messages/lib/agentAddressMention.mjs** Defines helpers and types for marked automatic-address mention tags. **desktop/src/features/messages/lib/agentAddressMention.test.mjs** Covers the automatic-address behavior and its failure, keyboard, layout, or persistence boundaries. **desktop/src/features/messages/lib/applyEditTagOverlay.mjs** Preserves automatic-address metadata when edited message tags are overlaid. **desktop/src/features/messages/lib/applyEditTagOverlay.test.mjs** Covers the automatic-address behavior and its failure, keyboard, layout, or persistence boundaries. **desktop/src/features/messages/lib/autoPinMentionedAgentsPreference.test.mjs** Covers the automatic-address behavior and its failure, keyboard, layout, or persistence boundaries. **desktop/src/features/messages/lib/autoPinMentionedAgentsPreference.ts** Stores the preference that keeps explicitly mentioned agents addressed for later messages. **desktop/src/features/messages/lib/extractMentionPersonas.ts** Separates persona recipients from the composer mention orchestration. **desktop/src/features/messages/lib/persistentAgentAudience.test.mjs** Covers the automatic-address behavior and its failure, keyboard, layout, or persistence boundaries. **desktop/src/features/messages/lib/persistentAgentAudience.ts** Maintains bounded, in-memory, channel-scoped automatic agent audiences. **desktop/src/features/messages/lib/useMentionSelection.ts** Centralizes mention picker selection state and agent-first selection behavior. **desktop/src/features/messages/lib/useMentions.ts** Exposes explicit picker origins and selection controls while preserving inline mention behavior. **desktop/src/features/messages/ui/ComposerAddressControls.test.mjs** Covers the automatic-address behavior and its failure, keyboard, layout, or persistence boundaries. **desktop/src/features/messages/ui/ComposerAddressControls.tsx** Renders compact addressed-agent avatars and the automatic-mention management entry point. **desktop/src/features/messages/ui/MentionAutocomplete.test.mjs** Covers the automatic-address behavior and its failure, keyboard, layout, or persistence boundaries. **desktop/src/features/messages/ui/MentionAutocomplete.tsx** Adds automatic-mention controls and options to the existing mention picker. **desktop/src/features/messages/ui/MessageAgentAddressPrefix.tsx** Shows which agents were automatically addressed on a sent message. **desktop/src/features/messages/ui/MessageComposer.tsx** Integrates automatic audiences, picker controls, accessible feedback, shortcuts, and send behavior. **desktop/src/features/messages/ui/MessageComposer.types.ts** Defines the simplified channel audience context shared by composer hosts. **desktop/src/features/messages/ui/MessageComposerToolbar.tsx** Places automatic-address controls in the composer toolbar without crowding narrow layouts. **desktop/src/features/messages/ui/MessageRow.tsx** Displays automatic-address metadata alongside sent message content. **desktop/src/features/messages/ui/MessageThreadPanel.tsx** Wires the appropriate channel, thread, inbox, or forum composer context without leaking audiences across surfaces. **desktop/src/features/messages/ui/composerAgentKeyboard.test.mjs** Covers the automatic-address behavior and its failure, keyboard, layout, or persistence boundaries. **desktop/src/features/messages/ui/persistentAgentAudienceHosts.test.mjs** Covers the automatic-address behavior and its failure, keyboard, layout, or persistence boundaries. **desktop/src/features/messages/ui/useAddressMentionPulse.test.mjs** Covers the automatic-address behavior and its failure, keyboard, layout, or persistence boundaries. **desktop/src/features/messages/ui/useAddressMentionPulse.ts** Provides success and failure animation signals for addressed-agent controls. **desktop/src/features/messages/ui/useAgentAddressLockPicker.test.mjs** Covers the automatic-address behavior and its failure, keyboard, layout, or persistence boundaries. **desktop/src/features/messages/ui/useAgentAddressLockPicker.ts** Coordinates adding, removing, and announcing automatically addressed agents. **desktop/src/features/messages/ui/useAlwaysAddressShortcut.ts** Implements the platform-aware shortcut for toggling automatic addressing. **desktop/src/features/messages/ui/useAutoPinMentionedAgents.ts** Promotes successfully sent inline agent mentions and provides a single undoable notification. **desktop/src/features/messages/ui/useComposerMentionPicker.ts** Opens the mention picker without rewriting the current draft. **desktop/src/features/messages/ui/useMentionSendFlow.helpers.test.mjs** Covers the automatic-address behavior and its failure, keyboard, layout, or persistence boundaries. **desktop/src/features/messages/ui/useMentionSendFlow.helpers.ts** Merges automatic and inline recipients, marks outgoing tags, and restores failed sends safely. **desktop/src/features/messages/ui/useMentionSendFlow.ts** Merges automatic and inline recipients, marks outgoing tags, and restores failed sends safely. **desktop/src/features/messages/ui/usePersistentAgentMentionHydration.ts** Removes the prior draft-text hydration approach now that automatic audiences stay at composer ingress. **desktop/src/features/settings/ui/AgentsSettingsPanel.tsx** Replaces the old global behavior with explicit composer-level automatic-mention controls. **desktop/src/features/settings/ui/PreventSleepSettingsCard.tsx** Replaces the old global behavior with explicit composer-level automatic-mention controls. **desktop/src/shared/lib/keyboard-shortcuts.ts** Defines the user-facing automatic-address keyboard shortcut label. **desktop/src/shared/ui/VideoReviewCommentMarkdown.tsx** Allows automatic-address prefixes to compose with video review timecodes. **desktop/tests/e2e/persistent-agent-audience.spec.ts** Covers the automatic-address behavior and its failure, keyboard, layout, or persistence boundaries. </details> ## Reproduction Steps 1. Open a channel with one or more agents and open the mention picker from the composer. 2. Select an agent for automatic mentions, then send several messages without retyping the handle; confirm the agent remains in the composer control and receives each message. 3. Mention another agent inline, send successfully, and confirm the agent becomes automatically addressed; use the notification's Undo action to reverse it. 4. Open a thread in the same channel and confirm the same addressed agents are available there. 5. Remove an agent from the composer control and confirm later messages stop addressing it. 6. Switch communities and confirm addressed agents do not carry into the other community. ## Screenshots All states below use the dark Buzz theme with a selected lilac accent. ### Addressed composer Selected agents stay visible at the composer ingress without adding handles to the draft.  ### Open mention menu The @ ingress opens the existing mention menu and shows which agents are already addressed.  ### Mention options The inline options pane controls whether a successful one-time agent mention carries into later messages.  ### Agent settings The same preference is available in **Settings → Agents → Conversations**.  --------- Signed-off-by: Taylor Ho <taylorkmho@gmail.com> Co-authored-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz> Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz>
190 lines
6.6 KiB
TypeScript
190 lines
6.6 KiB
TypeScript
/**
|
|
* E2E regression for the wrong-channel send bug.
|
|
*
|
|
* Repro: compose a message in channel A that tags a non-member managed agent
|
|
* (which forces the slow `add_channel_members` path), submit, then immediately
|
|
* switch to channel B before the agent-attach await resolves. Without the fix,
|
|
* the message lands in B's timeline; with the fix it must land in A's.
|
|
*
|
|
* The `addChannelMembersDelayMs` bridge knob holds the `add_channel_members`
|
|
* handler open long enough for the channel click to race the in-flight send.
|
|
*/
|
|
|
|
import { expect, test } from "@playwright/test";
|
|
|
|
import { installMockBridge } from "../helpers/bridge";
|
|
|
|
// A managed agent that is NOT a member of any channel in the seed data.
|
|
const OUT_OF_CHANNEL_BOT_PUBKEY =
|
|
"ee00000000000000000000000000000000000000000000000000000000000001";
|
|
|
|
/** Locator scoped to the mention autocomplete dropdown inside the composer. */
|
|
function autocomplete(page: import("@playwright/test").Page) {
|
|
return page
|
|
.getByTestId("message-composer")
|
|
.getByTestId("mention-autocomplete");
|
|
}
|
|
|
|
async function readCommandLog(page: import("@playwright/test").Page) {
|
|
return page.evaluate(() => {
|
|
return (
|
|
(window as Window & { __BUZZ_E2E_COMMANDS__?: string[] })
|
|
.__BUZZ_E2E_COMMANDS__ ?? []
|
|
);
|
|
});
|
|
}
|
|
|
|
function commandCount(commands: string[], command: string) {
|
|
return commands.filter((c) => c === command).length;
|
|
}
|
|
|
|
// The channel timeline renders off a `useDeferredValue` snapshot; poll for the
|
|
// pending marker to clear before asserting on freshly-sent content.
|
|
async function waitForTimelineSettled(page: import("@playwright/test").Page) {
|
|
await expect(page.locator("[data-render-pending]")).toHaveCount(0);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Main regression: message always lands in the compose-time channel
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test("message with agent mention lands in compose-time channel despite mid-send navigation", async ({
|
|
page,
|
|
}) => {
|
|
const MESSAGE_TEXT = `send-binding-repro-${Date.now()}`;
|
|
|
|
// Install bridge with:
|
|
// - a managed agent that is NOT in general (forces add_channel_members path)
|
|
// - a 500ms delay on add_channel_members to open the race window
|
|
await installMockBridge(page, {
|
|
addChannelMembersDelayMs: 500,
|
|
managedAgents: [
|
|
{
|
|
pubkey: OUT_OF_CHANNEL_BOT_PUBKEY,
|
|
name: "BotA",
|
|
status: "running",
|
|
// No channelNames → agent is not a member of any channel
|
|
},
|
|
],
|
|
});
|
|
|
|
await page.goto("/");
|
|
await page.getByTestId("channel-general").click();
|
|
await expect(page.getByTestId("chat-title")).toHaveText("general");
|
|
|
|
// Type a message that mentions the out-of-channel agent
|
|
const input = page.getByTestId("message-input");
|
|
await input.fill("@BotA");
|
|
|
|
const dropdown = autocomplete(page);
|
|
const botRow = dropdown.locator("button", { hasText: "BotA" });
|
|
await expect(botRow).toBeVisible();
|
|
await expect(botRow.getByText("not in channel")).toBeVisible();
|
|
// Select BotA from the autocomplete
|
|
await input.press("Enter");
|
|
await page.keyboard.type(` ${MESSAGE_TEXT}`);
|
|
|
|
// Verify the agent is addressed before submitting.
|
|
await expect(input).toHaveText(` ${MESSAGE_TEXT}`);
|
|
await expect(
|
|
page.getByTestId(`composer-address-lock-${OUT_OF_CHANNEL_BOT_PUBKEY}`),
|
|
).toBeVisible();
|
|
|
|
// Snapshot the baseline command count before sending
|
|
const baselineCommands = await readCommandLog(page);
|
|
const baselineAddCount = commandCount(
|
|
baselineCommands,
|
|
"add_channel_members",
|
|
);
|
|
|
|
// Submit the message — this triggers the async add_channel_members path
|
|
await page.getByTestId("send-message").click();
|
|
|
|
// Immediately switch to channel-agents BEFORE the 500ms delay resolves.
|
|
// This is the race the fix closes.
|
|
await page.getByTestId("channel-agents").click();
|
|
await expect(page.getByTestId("chat-title")).toHaveText("agents");
|
|
|
|
// Wait for add_channel_members to fire (confirms the race window opened and
|
|
// the fix's captured channel id was used for the agent-attach call).
|
|
await expect
|
|
.poll(async () =>
|
|
commandCount(await readCommandLog(page), "add_channel_members"),
|
|
)
|
|
.toBeGreaterThan(baselineAddCount);
|
|
|
|
// Let the in-flight send finish (500ms delay + buffer).
|
|
await page.waitForTimeout(800);
|
|
|
|
// --- Assert message landed in general (compose-time channel) ---
|
|
await page.getByTestId("channel-general").click();
|
|
await expect(page.getByTestId("chat-title")).toHaveText("general");
|
|
await waitForTimelineSettled(page);
|
|
|
|
// The message must appear in general's timeline (not the switched-to channel).
|
|
await expect(page.getByTestId("message-timeline")).toContainText(
|
|
MESSAGE_TEXT,
|
|
);
|
|
|
|
// --- Assert message did NOT land in agents (switched-to channel) ---
|
|
await page.getByTestId("channel-agents").click();
|
|
await expect(page.getByTestId("chat-title")).toHaveText("agents");
|
|
await waitForTimelineSettled(page);
|
|
|
|
await expect(page.getByTestId("message-timeline")).not.toContainText(
|
|
MESSAGE_TEXT,
|
|
);
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Invariant: without mid-send navigation, normal agent-mention send still works
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test("message with agent mention delivers correctly when no channel switch occurs", async ({
|
|
page,
|
|
}) => {
|
|
const MESSAGE_TEXT = `no-switch-verify-${Date.now()}`;
|
|
|
|
await installMockBridge(page, {
|
|
addChannelMembersDelayMs: 0,
|
|
managedAgents: [
|
|
{
|
|
pubkey: OUT_OF_CHANNEL_BOT_PUBKEY,
|
|
name: "BotA",
|
|
status: "running",
|
|
},
|
|
],
|
|
});
|
|
|
|
await page.goto("/");
|
|
await page.getByTestId("channel-general").click();
|
|
await expect(page.getByTestId("chat-title")).toHaveText("general");
|
|
|
|
const input = page.getByTestId("message-input");
|
|
await input.fill("@BotA");
|
|
const dropdown = autocomplete(page);
|
|
await expect(dropdown.locator("button", { hasText: "BotA" })).toBeVisible();
|
|
await input.press("Enter");
|
|
await page.keyboard.type(` ${MESSAGE_TEXT}`);
|
|
|
|
const baselineCommands = await readCommandLog(page);
|
|
const baselineAddCount = commandCount(
|
|
baselineCommands,
|
|
"add_channel_members",
|
|
);
|
|
|
|
await page.getByTestId("send-message").click();
|
|
|
|
// Wait for the agent-attach step to complete before asserting the timeline.
|
|
await expect
|
|
.poll(async () =>
|
|
commandCount(await readCommandLog(page), "add_channel_members"),
|
|
)
|
|
.toBeGreaterThan(baselineAddCount);
|
|
|
|
await waitForTimelineSettled(page);
|
|
await expect(page.getByTestId("message-timeline")).toContainText(
|
|
MESSAGE_TEXT,
|
|
);
|
|
});
|