Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 11 additions & 27 deletions src/app/AppShell.navigation.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -573,7 +573,7 @@ vi.mock("./ui/AppShellContent", () => ({
onTagHomeComposerSkill,
onSelectSession,
onStartProjectChat,
onStartChatWithPrompt,
onResolveBerdyAgent,
}) => {
const starterTasks = useStarterTasks();
const activeView = targetLocation.view;
Expand Down Expand Up @@ -625,7 +625,11 @@ vi.mock("./ui/AppShellContent", () => ({
</button>
<button
type="button"
onClick={() => onStartChatWithPrompt?.("How do projects work?")}
onClick={() => {
void onResolveBerdyAgent?.().then((personaId) => {
if (personaId) onTagHomeComposerAgent?.(personaId);
});
}}
>
Ask Berdy from Home
</button>
Expand Down Expand Up @@ -2882,7 +2886,7 @@ describe("AppShell global navigation", () => {
});
});

it("starts Berdy help prompts with the bundled Berdy persona", async () => {
it("resolves the bundled Berdy persona for Home", async () => {
const personaId = "/Users/test/.agents/agents/berdy.md";
useAgentStore.setState({
personas: [
Expand All @@ -2904,24 +2908,11 @@ describe("AppShell global navigation", () => {
);

await waitFor(() => {
expect(useChatStore.getState().queuedMessageBySession).toMatchObject({
"created-session": [
{
payload: {
text: "How do projects work?",
persona: { kind: "persona", id: personaId },
showInComposer: false,
},
},
],
});
expect(
useChatSessionStore.getState().getSession("created-session"),
).toMatchObject({ personaId });
expect(screen.getByText("Berdy")).toBeInTheDocument();
});
});

it("restores a missing bundled Berdy agent before starting a chat", async () => {
it("restores a missing bundled Berdy agent before tagging it", async () => {
const personaId = "/Users/test/.agents/agents/berdy.md";
mockListPersonas.mockResolvedValue([
{
Expand All @@ -2943,16 +2934,11 @@ describe("AppShell global navigation", () => {

await waitFor(() => {
expect(mockRepairBundledAgent).toHaveBeenCalledWith("berdy.md");
expect(
useChatSessionStore.getState().getSession("created-session"),
).toMatchObject({ personaId });
expect(screen.getByText("Berdy")).toBeInTheDocument();
});
expect(mockRepairBundledAgent.mock.invocationCallOrder[0]).toBeLessThan(
mockListPersonas.mock.invocationCallOrder[0],
);
expect(mockListPersonas.mock.invocationCallOrder[0]).toBeLessThan(
mockAcpCreateSession.mock.invocationCallOrder[0],
);
expect(mockToastError).not.toHaveBeenCalledWith(
"Berdy couldn't start a chat. Try again.",
);
Expand Down Expand Up @@ -2981,9 +2967,7 @@ describe("AppShell global navigation", () => {

await waitFor(() => {
expect(mockListPersonas).toHaveBeenCalled();
expect(
useChatSessionStore.getState().getSession("created-session"),
).toMatchObject({ personaId });
expect(screen.getByText("Berdy")).toBeInTheDocument();
});
});

Expand Down
27 changes: 13 additions & 14 deletions src/app/AppShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2986,8 +2986,10 @@ export function AppShell({
],
);

const handleStartChatWithBerdy = useCallback(
async (text: string): Promise<boolean> => {
const handleResolveBerdyAgent = useCallback(async (): Promise<
string | null
> => {
try {
const store = useAgentStore.getState();
let personaId = findBerdyPersonaId(store.personas);

Expand Down Expand Up @@ -3027,19 +3029,16 @@ export function AppShell({

if (!personaId) {
toast.error(t("home:onboarding.callout.agentUnavailable"));
return false;
return null;
}

return new Promise((resolve) => {
handleGlobalCompose(
text,
{ personaId },
{ showQueuedHandoff: false, onSettled: resolve },
);
});
},
[handleGlobalCompose, t],
);
return personaId;
} catch (error) {
console.error("Failed to resolve the bundled Berdy agent:", error);
toast.error(t("home:onboarding.callout.agentUnavailable"));
return null;
}
}, [t]);

const handleGlobalComposerExpand = useCallback(
(payload: GlobalComposerExpandPayload): Promise<boolean> => {
Expand Down Expand Up @@ -5001,7 +5000,7 @@ export function AppShell({
onStartChatFromProject={handleStartChatFromProject}
onStartProjectChat={handleStartProjectChat}
onStartChatWithSkill={handleStartChatWithSkill}
onStartChatWithPrompt={handleStartChatWithBerdy}
onResolveBerdyAgent={handleResolveBerdyAgent}
onExitSearch={handleExitSearch}
onOpenExtension={handleOpenExtensionFromSearch}
onOpenAgent={handleStartChatWithAgent}
Expand Down
8 changes: 3 additions & 5 deletions src/app/ui/AppShellContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,7 @@ interface AppShellContentProps {
onStartChatFromProject: (project: ProjectInfo) => void;
onStartProjectChat: (projectId: string) => void;
onStartChatWithSkill: (skill: SkillInfo, projectId?: string | null) => void;
onStartChatWithPrompt: (
prompt: string,
) => boolean | undefined | Promise<boolean | undefined>;
onResolveBerdyAgent: () => Promise<string | null>;
onExitSearch: () => void;
onOpenExtension: (entry: ExtensionEntry) => void;
onOpenAgent: (agentId: string) => void;
Expand Down Expand Up @@ -166,7 +164,7 @@ export function AppShellContent({
onStartChatFromProject,
onStartProjectChat,
onStartChatWithSkill,
onStartChatWithPrompt,
onResolveBerdyAgent,
onExitSearch,
onOpenExtension,
onOpenAgent,
Expand Down Expand Up @@ -221,7 +219,7 @@ export function AppShellContent({
onOpenSkills={() => onNavigateSkills(null)}
onOpenAutomations={openHomeAutomations}
onHydratePinnedChatSessions={onHydratePinnedChatSessions}
onStartChatWithPrompt={onStartChatWithPrompt}
onResolveBerdyAgent={onResolveBerdyAgent}
viewportLeftOcclusionPx={homeViewportLeftOcclusionPx}
/>
);
Expand Down
2 changes: 1 addition & 1 deletion src/features/design-system/ui/designSystemSections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,6 @@ export const DESIGN_SYSTEM_COMPONENT_SECTIONS: Array<{
{ id: "component-context-menu", label: "Context Menu" },
{ id: "component-file-context-menu", label: "File Context Menu" },
{ id: "component-image-lightbox", label: "Image Lightbox" },
{ id: "component-input-group", label: "Input Group" },
{ id: "component-input", label: "Input" },
{ id: "component-label", label: "Label" },
{ id: "component-detail-page-shell", label: "Detail Page Shell" },
Expand Down Expand Up @@ -162,6 +161,7 @@ export const DESIGN_SYSTEM_UNUSED_COMPONENT_SECTIONS: Array<{
{ id: "component-form", label: "Form" },
{ id: "component-berd-logo", label: "Berd Logo" },
{ id: "component-hover-card", label: "Hover Card" },
{ id: "component-input-group", label: "Input Group" },
{ id: "component-input-otp", label: "Input OTP" },
{ id: "component-menubar", label: "Menubar" },
{ id: "component-navigation-menu", label: "Navigation Menu" },
Expand Down
4 changes: 3 additions & 1 deletion src/features/experiments/ExperimentsSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,9 @@ export function ExperimentsSettings({
const visibleRegistry = useMemo(
() =>
getVisibleExperimentRegistry(registry).filter(
(definition) => !HIDDEN_EXPERIMENT_IDS.has(definition.id),
(definition) =>
!HIDDEN_EXPERIMENT_IDS.has(definition.id) &&
(definition.settingsVisibility !== "dev" || import.meta.env.DEV),
),
[registry],
);
Expand Down
32 changes: 29 additions & 3 deletions src/features/experiments/__tests__/ExperimentsSettings.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,32 @@ describe("ExperimentsSettings", () => {
expect(resetHomeForOnboardingExperienceMock).toHaveBeenCalledOnce();
});

it("hides onboarding experiment controls outside dev builds", () => {
vi.stubEnv("DEV", false);
renderWithProviders(<ExperimentsSettings />);

expect(
screen.queryByText(
i18n.t("experiments.starterTasks.title", { ns: "settings" }),
),
).not.toBeInTheDocument();
expect(
screen.queryByText(
i18n.t("experiments.berdyOnboarding.title", { ns: "settings" }),
),
).not.toBeInTheDocument();
expect(
screen.queryByText(
i18n.t("experiments.firstRunOnboarding.title", { ns: "settings" }),
),
).not.toBeInTheDocument();
expect(
screen.queryByRole("region", {
name: i18n.t("experiments.onboarding.title", { ns: "settings" }),
}),
).not.toBeInTheDocument();
});

it("preserves first-run state when reset-all preparation fails", async () => {
vi.stubEnv("DEV", true);
resetHomeForOnboardingExperienceMock.mockResolvedValueOnce(false);
Expand Down Expand Up @@ -230,8 +256,8 @@ describe("ExperimentsSettings", () => {
expect(resetOnboardingTourExperienceMock).toHaveBeenCalledOnce();
});

it("syncs Berdy onboarding when its experiment is toggled", async () => {
vi.stubEnv("DEV", false);
it("syncs Berdy onboarding when its dev-only experiment is toggled", async () => {
vi.stubEnv("DEV", true);
const user = userEvent.setup();
renderWithProviders(<ExperimentsSettings />);

Expand All @@ -243,7 +269,7 @@ describe("ExperimentsSettings", () => {
}),
);

expect(syncOnboardingExperimentStateMock).toHaveBeenCalledWith(true);
expect(syncOnboardingExperimentStateMock).toHaveBeenCalledWith(false);
});

it("does not advertise the retired macOS 26 voice requirement", () => {
Expand Down
5 changes: 5 additions & 0 deletions src/features/experiments/experimentDefinitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ export interface ExperimentDefinition {
defaultEnabled?: boolean;
/** Opt-out of development's global experiment auto-enable behavior. */
manualEnableOnly?: boolean;
/** Limit this experiment's Settings controls without changing runtime state. */
settingsVisibility?: "all" | "dev";
config?: Record<string, ExperimentConfigControl>;
}

Expand Down Expand Up @@ -98,6 +100,7 @@ export const EXPERIMENT_DEFINITIONS = [
id: STARTER_TASKS_EXPERIMENT_ID,
titleKey: "experiments.starterTasks.title",
descriptionKey: "experiments.starterTasks.description",
settingsVisibility: "dev",
},
{
id: VOICE_CONVERSATION_EXPERIMENT_ID,
Expand All @@ -119,12 +122,14 @@ export const EXPERIMENT_DEFINITIONS = [
id: BERDY_ONBOARDING_EXPERIMENT_ID,
titleKey: "experiments.berdyOnboarding.title",
descriptionKey: "experiments.berdyOnboarding.description",
settingsVisibility: "dev",
},
{
id: FIRST_RUN_ONBOARDING_EXPERIMENT_ID,
titleKey: "experiments.firstRunOnboarding.title",
descriptionKey: "experiments.firstRunOnboarding.description",
defaultEnabled: false,
manualEnableOnly: true,
settingsVisibility: "dev",
},
] as const satisfies readonly ExperimentDefinition[];
19 changes: 13 additions & 6 deletions src/features/home/ui/HomeView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,7 @@ export interface HomeViewProps {
onCreateProject?: () => void;
onOpenSkills?: () => void;
onOpenAutomations?: () => void;
onStartChatWithPrompt?: (
prompt: string,
) => boolean | undefined | Promise<boolean | undefined>;
onResolveBerdyAgent?: () => Promise<string | null>;
onHydratePinnedChatSessions?: (sessionIds: string[]) => void;
viewportLeftOcclusionPx?: number;
}
Expand All @@ -90,7 +88,7 @@ export function HomeView({
onCreateProject,
onOpenSkills,
onOpenAutomations,
onStartChatWithPrompt,
onResolveBerdyAgent,
onHydratePinnedChatSessions,
viewportLeftOcclusionPx = 0,
}: HomeViewProps) {
Expand All @@ -112,6 +110,7 @@ export function HomeView({
const starterLayoutArrangementAttemptedRef = useRef(false);

const [tourOpen, setTourOpen] = useState(false);
const tourCompleteRef = useRef<(() => void) | null>(null);
const berdyOnboardingExperiment = useExperiment(
BERDY_ONBOARDING_EXPERIMENT_ID,
);
Expand Down Expand Up @@ -565,12 +564,19 @@ export function HomeView({
return () => window.removeEventListener("keydown", handleReloadOnboarding);
}, [reloadOnboardingTourForDev]);

const handleStartTour = useCallback(() => {
const handleStartTour = useCallback((onComplete?: () => void) => {
tourCompleteRef.current = onComplete ?? null;
setTourOpen(true);
}, []);

const handleTourOpenChange = useCallback((open: boolean) => {
setTourOpen(open);
if (!open) tourCompleteRef.current = null;
}, []);

const handleTourComplete = useCallback(() => {
tourCompleteRef.current?.();
tourCompleteRef.current = null;
}, []);

useEffect(() => {
Expand Down Expand Up @@ -688,12 +694,13 @@ export function HomeView({
onOpenSkills={onOpenSkills}
onOpenAutomations={onOpenAutomations}
onStartOnboardingTour={handleStartTour}
onStartChatWithPrompt={onStartChatWithPrompt}
onResolveBerdyAgent={onResolveBerdyAgent}
/>
) : null}
<OnboardingTourDialog
open={berdyOnboardingEnabled && tourOpen}
onOpenChange={handleTourOpenChange}
onComplete={handleTourComplete}
/>
{loadStatus === "loading" ? (
<div className="relative h-full w-full bg-dot-grid">
Expand Down
4 changes: 2 additions & 2 deletions src/features/home/ui/WidgetCanvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,7 @@ export function WidgetCanvas({
onOpenSkills,
onOpenAutomations,
onStartOnboardingTour,
onStartChatWithPrompt,
onResolveBerdyAgent,
}: WidgetCanvasProps) {
const { t } = useTranslation("home");
const resolvedRecenterLabel =
Expand Down Expand Up @@ -735,7 +735,7 @@ export function WidgetCanvas({
onOpenSkills={onOpenSkills}
onOpenAutomations={onOpenAutomations}
onStartOnboardingTour={onStartOnboardingTour}
onStartChatWithPrompt={onStartChatWithPrompt}
onResolveBerdyAgent={onResolveBerdyAgent}
/>
{catalogEntry.hideResizeHandle ? null : (
<button
Expand Down
4 changes: 2 additions & 2 deletions src/features/home/ui/WidgetFrame.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export function WidgetFrame({
onOpenSkills,
onOpenAutomations,
onStartOnboardingTour,
onStartChatWithPrompt,
onResolveBerdyAgent,
}: WidgetFrameProps) {
const { t } = useTranslation("home");
const catalogEntry = HOME_WIDGET_CATALOG_BY_ID[instance.type];
Expand Down Expand Up @@ -217,7 +217,7 @@ export function WidgetFrame({
onOpenSkills={onOpenSkills}
onOpenAutomations={onOpenAutomations}
onStartOnboardingTour={onStartOnboardingTour}
onStartChatWithPrompt={onStartChatWithPrompt}
onResolveBerdyAgent={onResolveBerdyAgent}
onRemoveWidget={handleRemove}
/>
</fieldset>
Expand Down
Loading