mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
Merge remote-tracking branch 'origin/main' into kennylopez-agent-catalog-sharing
# Conflicts: # desktop/tests/helpers/bridge.ts
This commit is contained in:
@@ -1,6 +1,15 @@
|
||||
import { ChevronDown } from "lucide-react";
|
||||
|
||||
import type { ChannelTemplate } from "@/shared/api/types";
|
||||
import { cn } from "@/shared/lib/cn";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/shared/ui/dropdown-menu";
|
||||
import { Input } from "@/shared/ui/input";
|
||||
import { Textarea } from "@/shared/ui/textarea";
|
||||
|
||||
@@ -14,6 +23,7 @@ import type { CreateChannelFormState } from "@/features/sidebar/lib/useCreateCha
|
||||
|
||||
const CREATE_LABEL_OPTIONAL_CLASS =
|
||||
"ml-1 text-xs font-normal text-muted-foreground/50";
|
||||
const NO_TEMPLATE_VALUE = "__no-template__";
|
||||
|
||||
export const CREATE_CHANNEL_FORM_ID = "create-channel-form";
|
||||
|
||||
@@ -29,6 +39,9 @@ export function CreateChannelFormFields({
|
||||
form: CreateChannelFormState;
|
||||
}) {
|
||||
const { channelKind, kindLabel, isCreating } = form;
|
||||
const selectedTemplate = form.templates.find(
|
||||
(template) => template.id === form.selectedTemplateId,
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
@@ -104,29 +117,59 @@ export function CreateChannelFormFields({
|
||||
/>
|
||||
|
||||
{form.templates.length > 0 ? (
|
||||
<div className="space-y-1.5">
|
||||
<label
|
||||
className="text-sm font-medium text-foreground"
|
||||
htmlFor="create-channel-template"
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"flex min-h-12 items-center justify-between gap-4 rounded-xl border border-input bg-background px-3 py-3",
|
||||
isCreating && "opacity-50",
|
||||
)}
|
||||
>
|
||||
<span className="text-sm font-medium text-foreground">
|
||||
Template
|
||||
<span className={CREATE_LABEL_OPTIONAL_CLASS}>Optional</span>
|
||||
</label>
|
||||
<select
|
||||
className="flex min-h-11 w-full rounded-xl border border-input bg-muted/40 px-3 py-2 text-sm text-muted-foreground/55 shadow-none transition-colors duration-150 ease-out hover:border-muted-foreground/40 focus:border-muted-foreground/50 focus:text-foreground focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50"
|
||||
data-testid="create-channel-template"
|
||||
disabled={isCreating}
|
||||
id="create-channel-template"
|
||||
onChange={(event) => form.handleTemplateChange(event.target.value)}
|
||||
value={form.selectedTemplateId ?? ""}
|
||||
>
|
||||
<option value="">No template</option>
|
||||
{form.templates.map((template: ChannelTemplate) => (
|
||||
<option key={template.id} value={template.id}>
|
||||
{template.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</span>
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
aria-label={`Template: ${selectedTemplate?.name ?? "No template"}`}
|
||||
className="-mr-2.5 ml-auto h-9 min-w-0 max-w-[60%] justify-end px-2.5 text-right text-sm font-medium text-foreground hover:bg-muted/50"
|
||||
data-testid="create-channel-template"
|
||||
disabled={isCreating}
|
||||
id="create-channel-template"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
<span className="truncate text-right">
|
||||
{selectedTemplate?.name ?? "No template"}
|
||||
</span>
|
||||
<ChevronDown className="size-4 shrink-0 text-muted-foreground/70" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="end"
|
||||
onCloseAutoFocus={(event) => event.preventDefault()}
|
||||
style={{
|
||||
minWidth: "var(--radix-dropdown-menu-trigger-width)",
|
||||
}}
|
||||
>
|
||||
<DropdownMenuRadioGroup
|
||||
onValueChange={(templateId) =>
|
||||
form.handleTemplateChange(
|
||||
templateId === NO_TEMPLATE_VALUE ? "" : templateId,
|
||||
)
|
||||
}
|
||||
value={form.selectedTemplateId ?? NO_TEMPLATE_VALUE}
|
||||
>
|
||||
<DropdownMenuRadioItem value={NO_TEMPLATE_VALUE}>
|
||||
No template
|
||||
</DropdownMenuRadioItem>
|
||||
{form.templates.map((template: ChannelTemplate) => (
|
||||
<DropdownMenuRadioItem key={template.id} value={template.id}>
|
||||
{template.name}
|
||||
</DropdownMenuRadioItem>
|
||||
))}
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { parse as yamlParse } from "yaml";
|
||||
|
||||
import { relayClient } from "@/shared/api/relayClient";
|
||||
import type { ConnectionState } from "@/shared/api/relayClientShared";
|
||||
import type { RelayEvent } from "@/shared/api/types";
|
||||
import type { ChannelTemplate, RelayEvent } from "@/shared/api/types";
|
||||
import { getMarkdownParseCount } from "@/shared/ui/markdown/nodeCache";
|
||||
import { syncAgentTurnsFromEvents } from "@/features/agents/activeAgentTurnsStore";
|
||||
import { recordTimeoutFromRejection } from "@/features/moderation/lib/timeoutStore";
|
||||
@@ -207,6 +207,7 @@ type E2eConfig = {
|
||||
addChannelMembersErrors?: (string | null)[];
|
||||
channelMembersReadDelayMs?: number;
|
||||
createManagedAgentDelayMs?: number;
|
||||
channelTemplates?: ChannelTemplate[];
|
||||
channelsReadError?: string;
|
||||
/** Reject successive mock `get_channels` calls, then resume. */
|
||||
channelsReadErrors?: (string | null)[];
|
||||
@@ -10147,6 +10148,19 @@ export function maybeInstallE2eTauriMocks() {
|
||||
);
|
||||
case "list_teams":
|
||||
return handleListTeams();
|
||||
case "list_channel_templates":
|
||||
return (activeConfig?.mock?.channelTemplates ?? []).map((template) => ({
|
||||
id: template.id,
|
||||
name: template.name,
|
||||
description: template.description,
|
||||
channel_type: template.channelType,
|
||||
visibility: template.visibility,
|
||||
canvas_template: template.canvasTemplate,
|
||||
agents: template.agents,
|
||||
is_builtin: template.isBuiltin,
|
||||
created_at: template.createdAt,
|
||||
updated_at: template.updatedAt,
|
||||
}));
|
||||
case "create_team":
|
||||
return handleCreateTeam(
|
||||
payload as Parameters<typeof handleCreateTeam>[0],
|
||||
|
||||
@@ -1294,6 +1294,44 @@ test("create stream with name and description", async ({ page }) => {
|
||||
await expect(page.getByTestId("chat-title")).toHaveText(channelName);
|
||||
});
|
||||
|
||||
test("create channel template selector matches the lifecycle controls", async ({
|
||||
page,
|
||||
}) => {
|
||||
await installMockBridge(page, {
|
||||
channelTemplates: [
|
||||
{
|
||||
id: "project-kickoff",
|
||||
name: "Project kickoff",
|
||||
description: "Coordinate a new project from planning through launch.",
|
||||
channelType: "stream",
|
||||
visibility: "private",
|
||||
canvasTemplate: null,
|
||||
agents: { personas: [], teams: [] },
|
||||
isBuiltin: false,
|
||||
createdAt: "2026-07-23T00:00:00Z",
|
||||
updatedAt: "2026-07-23T00:00:00Z",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await page.goto("/");
|
||||
await openCreateChannelDialog(page);
|
||||
|
||||
const templateControl = page.getByTestId("create-channel-template");
|
||||
await expect(templateControl).toHaveRole("button");
|
||||
await expect(templateControl).toHaveText("No template");
|
||||
await templateControl.click();
|
||||
await page.getByRole("menuitemradio", { name: "Project kickoff" }).click();
|
||||
|
||||
await expect(templateControl).toHaveText("Project kickoff");
|
||||
await expect(page.getByTestId("create-channel-description")).toHaveValue(
|
||||
"Coordinate a new project from planning through launch.",
|
||||
);
|
||||
await expect(page.getByTestId("create-channel-permissions")).toContainText(
|
||||
"Private",
|
||||
);
|
||||
});
|
||||
|
||||
test("create ephemeral stream shows sidebar and header affordances", async ({
|
||||
page,
|
||||
}) => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Page } from "@playwright/test";
|
||||
import type { RelayEvent } from "@/shared/api/types";
|
||||
import type { ChannelTemplate, RelayEvent } from "../../src/shared/api/types";
|
||||
import { FEATURE_OVERRIDES_STORAGE_KEY, PREVIEW_FEATURE_IDS } from "./features";
|
||||
|
||||
export const TEST_IDENTITIES = {
|
||||
@@ -218,6 +218,7 @@ type MockBridgeOptions = {
|
||||
relayAgents?: MockRelayAgentSeed[];
|
||||
agentListDelayMs?: number;
|
||||
createManagedAgentDelayMs?: number;
|
||||
channelTemplates?: ChannelTemplate[];
|
||||
addChannelMembersDelayMs?: number;
|
||||
/** Sequenced add-member failures. A string fails that call; null succeeds. */
|
||||
addChannelMembersErrors?: (string | null)[];
|
||||
|
||||
Reference in New Issue
Block a user