refactor(ui): remove the skill launcher flag and keep the chip rail (#13097)

This commit is contained in:
Alejandro Bailo authored and GitHub committed 2026-10-19 20:19:07 +02:00
1 parent 76cde39b08
commit 4d922da2a6
12 files changed
+22 -477

No files matched your search

@@ -1,90 +0,0 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import { LighthouseSkillsBlock } from "./lighthouse-skills-block";
describe("LighthouseSkillsBlock", () => {
it("should render every skill with its description", () => {
// Given / When
render(
<LighthouseSkillsBlock onLaunchSkill={vi.fn()} onAskAnything={vi.fn()} />,
);
// Then
expect(screen.getByText("Lighthouse AI Skills")).toBeInTheDocument();
expect(screen.getByText("Contextual Fix")).toBeInTheDocument();
expect(screen.getByText("Triage Decision")).toBeInTheDocument();
expect(screen.getByText("Systemic Scope")).toBeInTheDocument();
expect(screen.getByText("Compliance Impact")).toBeInTheDocument();
expect(
screen.getByText("Is this real, and if not, close it out"),
).toBeInTheDocument();
});
it("should launch the clicked skill", async () => {
// Given
const user = userEvent.setup();
const onLaunchSkill = vi.fn();
render(
<LighthouseSkillsBlock
onLaunchSkill={onLaunchSkill}
onAskAnything={vi.fn()}
/>,
);
// When
await user.click(screen.getByRole("button", { name: /Triage Decision/ }));
// Then
expect(onLaunchSkill).toHaveBeenCalledOnce();
expect(onLaunchSkill.mock.calls[0][0]).toMatchObject({
id: "triage-decision",
});
});
it("should render disabled skills as coming soon and not launch them", async () => {
// Given
const user = userEvent.setup();
const onLaunchSkill = vi.fn();
render(
<LighthouseSkillsBlock
onLaunchSkill={onLaunchSkill}
onAskAnything={vi.fn()}
/>,
);
// Then
const card = screen.getByRole("button", { name: /Compliance Impact/ });
expect(card).toBeDisabled();
expect(screen.getByText("Coming soon")).toBeInTheDocument();
// When
await user.click(card);
// Then
expect(onLaunchSkill).not.toHaveBeenCalled();
});
it("should keep the free-form fallback available", async () => {
// Given
const user = userEvent.setup();
const onAskAnything = vi.fn();
render(
<LighthouseSkillsBlock
onLaunchSkill={vi.fn()}
onAskAnything={onAskAnything}
/>,
);
// When
await user.click(
screen.getByRole("button", {
name: /ask Lighthouse anything about this finding/i,
}),
);
// Then
expect(onAskAnything).toHaveBeenCalledOnce();
});
});
@@ -1,92 +0,0 @@
"use client";
import { ArrowRight } from "lucide-react";
import { LighthouseIcon } from "@/components/icons";
import { Card } from "@/components/shadcn/card/card";
import { getAllSkills } from "@/lib/lighthouse/skills/registry";
import type { LighthouseSkillDefinition } from "@/types/lighthouse-skills";
interface LighthouseSkillsBlockProps {
onLaunchSkill: (skill: LighthouseSkillDefinition) => void;
onAskAnything: () => void;
}
// Finding-detail CTA: a grid of launchable agentic workflows plus
// the free-form "ask anything" fallback that the old gradient banner offered.
export function LighthouseSkillsBlock({
onLaunchSkill,
onAskAnything,
}: LighthouseSkillsBlockProps) {
return (
<Card variant="lighthouse" className="gap-3 p-4">
<div className="flex items-center gap-2">
<LighthouseIcon size={16} />
<span className="text-text-neutral-primary text-sm font-semibold">
Lighthouse AI Skills
</span>
<span className="text-text-neutral-tertiary ml-auto text-xs">
Run in chat
</span>
</div>
<div className="grid grid-cols-1 gap-2 @md:grid-cols-2">
{getAllSkills().map((skill) => (
<SkillCard
key={skill.id}
skill={skill}
onLaunch={() => onLaunchSkill(skill)}
/>
))}
</div>
<button
type="button"
onClick={onAskAnything}
className="text-text-lighthouse self-end text-xs underline-offset-2 hover:underline"
>
Or ask Lighthouse anything about this finding →
</button>
</Card>
);
}
function SkillCard({
skill,
onLaunch,
}: {
skill: LighthouseSkillDefinition;
onLaunch: () => void;
}) {
const Icon = skill.icon;
return (
<button
type="button"
onClick={onLaunch}
disabled={!skill.enabled}
className="group border-border-neutral-secondary bg-bg-neutral-secondary hover:bg-bg-neutral-tertiary disabled:hover:bg-bg-neutral-secondary flex items-start gap-2.5 rounded-lg border p-3 text-left transition-colors disabled:cursor-not-allowed disabled:opacity-60"
>
<Icon
className="text-text-lighthouse mt-0.5 size-4 shrink-0"
aria-hidden
/>
<span className="flex min-w-0 flex-col gap-0.5">
<span className="text-text-neutral-primary flex items-center gap-1.5 text-sm font-medium">
{skill.name}
{skill.enabled ? (
<ArrowRight
className="text-text-lighthouse size-3.5 opacity-0 transition-opacity group-hover:opacity-100"
aria-hidden
/>
) : (
<span className="text-text-neutral-tertiary text-xs font-normal">
Coming soon
</span>
)}
</span>
<span className="text-text-neutral-secondary text-xs leading-snug">
{skill.description}
</span>
</span>
</button>
);
}
@@ -24,9 +24,9 @@ interface LighthouseSkillsRailProps {
onSubmitPrompt: (text: string) => void;
}
// Finding-detail skill launcher, "dropdown" experiment variant (chip rail):
// one compact row under the finding title — direct-launch chips plus a +N
// trigger opening the same skills menu used by the table row surfaces.
// Finding-detail skill launcher (chip rail): one compact row under the
// finding title — direct-launch chips plus a +N trigger opening the same
// skills menu used by the table row surfaces.
export function LighthouseSkillsRail({
onLaunchSkill,
onSubmitPrompt,
@@ -28,7 +28,6 @@ const {
mockRequestPanelChatMessage,
mockRequestPanelSkillLaunch,
mockIsCloud,
mockUseSkillLauncherVariant,
mockCurrentLighthouseContext,
} = vi.hoisted(() => ({
mockGetComplianceIcon: vi.fn((_: string) => null as string | null),
@@ -43,7 +42,6 @@ const {
mockRequestPanelChatMessage: vi.fn(),
mockRequestPanelSkillLaunch: vi.fn(),
mockIsCloud: vi.fn(() => true),
mockUseSkillLauncherVariant: vi.fn(() => "card"),
mockCurrentLighthouseContext: {
schemaVersion: 1,
transport: "inline",
@@ -310,10 +308,6 @@ vi.mock("@/components/icons", () => ({
LighthouseIcon: () => null,
}));
vi.mock("./use-skill-launcher-variant", () => ({
useSkillLauncherVariant: mockUseSkillLauncherVariant,
}));
vi.mock("@/components/icons/services/IconServices", () => ({
JiraIcon: () => null,
}));
@@ -571,7 +565,6 @@ afterEach(() => {
(_: string) => null as string | null,
);
mockIsCloud.mockReturnValue(true);
mockUseSkillLauncherVariant.mockReturnValue("card");
useSidePanelStore.setState({
isOpen: false,
selectedTab: SIDE_PANEL_TAB.AI_CHAT,
@@ -840,82 +833,7 @@ const mockResourceRow: FindingResourceRow = {
lastSeenAt: null,
};
describe("ResourceDetailDrawerContent — Lighthouse AI", () => {
it("should open the Lighthouse tab without starting a conversation", async () => {
// Given
const user = userEvent.setup();
useSidePanelStore.setState({
isOpen: true,
selectedTab: SIDE_PANEL_TAB.CONTEXT,
});
render(
<ResourceDetailDrawerContent
isLoading={false}
isNavigating={false}
checkMeta={mockCheckMeta}
currentIndex={0}
totalResources={1}
currentFinding={mockFinding}
otherFindings={[]}
onNavigatePrev={vi.fn()}
onNavigateNext={vi.fn()}
onMuteComplete={vi.fn()}
/>,
);
// When: the free-form fallback only navigates to the chat tab
await user.click(
screen.getByRole("button", {
name: /ask Lighthouse anything about this finding/i,
}),
);
// Then — no conversation is started on the user's behalf
expect(mockRequestPanelChatMessage).not.toHaveBeenCalled();
expect(useSidePanelStore.getState()).toMatchObject({
isOpen: true,
selectedTab: SIDE_PANEL_TAB.AI_CHAT,
});
// And launching a skill goes through the skill launcher with the context
await user.click(screen.getByRole("button", { name: /Triage Decision/ }));
expect(mockRequestPanelSkillLaunch).toHaveBeenCalledWith(
expect.objectContaining({ id: "triage-decision" }),
mockCurrentLighthouseContext,
);
});
it("should hide the action when the Lighthouse panel tab is unavailable", () => {
// Given
mockIsCloud.mockReturnValue(false);
// When
render(
<ResourceDetailDrawerContent
isLoading={false}
isNavigating={false}
checkMeta={mockCheckMeta}
currentIndex={0}
totalResources={1}
currentFinding={mockFinding}
otherFindings={[]}
onNavigatePrev={vi.fn()}
onNavigateNext={vi.fn()}
onMuteComplete={vi.fn()}
/>,
);
// Then
expect(screen.queryByText("Lighthouse AI Skills")).not.toBeInTheDocument();
expect(
screen.queryByRole("button", {
name: /ask Lighthouse anything about this finding/i,
}),
).not.toBeInTheDocument();
});
});
describe("ResourceDetailDrawerContent — skill launcher experiment", () => {
describe("ResourceDetailDrawerContent — Lighthouse skills rail", () => {
const renderDrawer = (overrides: { isNavigating?: boolean } = {}) =>
render(
<ResourceDetailDrawerContent
@@ -932,15 +850,13 @@ describe("ResourceDetailDrawerContent — skill launcher experiment", () => {
/>,
);
it("should swap the footer card for the header chip rail on the dropdown variant", async () => {
it("should launch a skill from the header chip rail", async () => {
// Given
const user = userEvent.setup();
mockUseSkillLauncherVariant.mockReturnValue("dropdown");
renderDrawer();
// Then — control card gone, rail chips present. The always-open dropdown
// mock repeats every skill as a menu button, hence getAllByRole.
expect(screen.queryByText("Lighthouse AI Skills")).not.toBeInTheDocument();
// Then — rail chips present. The always-open dropdown mock repeats every
// skill as a menu button, hence getAllByRole.
expect(
screen.getAllByRole("button", { name: "Contextual Fix" }).length,
).toBeGreaterThan(0);
@@ -964,7 +880,6 @@ describe("ResourceDetailDrawerContent — skill launcher experiment", () => {
it("should start a fresh conversation from the rail prompt", async () => {
// Given
const user = userEvent.setup();
mockUseSkillLauncherVariant.mockReturnValue("dropdown");
renderDrawer();
// When
@@ -989,7 +904,6 @@ describe("ResourceDetailDrawerContent — skill launcher experiment", () => {
it("should hide the rail outside cloud and while navigating", () => {
// Given
mockUseSkillLauncherVariant.mockReturnValue("dropdown");
mockIsCloud.mockReturnValue(false);
// When
@@ -1012,21 +926,6 @@ describe("ResourceDetailDrawerContent — skill launcher experiment", () => {
screen.queryByRole("button", { name: "Contextual Fix" }),
).not.toBeInTheDocument();
});
it("should keep the card control for unresolved or unknown variants", () => {
// Given — the hook already collapses those to "card"; the drawer treats
// anything that is not exactly "dropdown" as control.
mockUseSkillLauncherVariant.mockReturnValue("card");
// When
renderDrawer();
// Then
expect(screen.getByText("Lighthouse AI Skills")).toBeInTheDocument();
expect(
screen.queryByRole("button", { name: "More Lighthouse skills" }),
).not.toBeInTheDocument();
});
});
describe("ResourceDetailDrawerContent — remediation code editors", () => {
@@ -1914,7 +1813,9 @@ describe("ResourceDetailDrawerContent — header skeleton while navigating", ()
expect(screen.getByText("security")).toBeInTheDocument();
expect(screen.queryByText("Status Extended:")).not.toBeInTheDocument();
expect(screen.queryByText("uid-1")).not.toBeInTheDocument();
expect(screen.queryByText("Lighthouse AI Skills")).not.toBeInTheDocument();
expect(
screen.queryByRole("button", { name: "More Lighthouse skills" }),
).not.toBeInTheDocument();
});
it("should keep the overview tab shell visible with section skeletons when navigating to a different check", () => {
@@ -91,10 +91,7 @@ import type {
UpdateFindingTriageInput,
} from "@/types/findings-triage";
import { JIRA_DISPATCH_TARGET } from "@/types/integrations";
import {
SKILL_LAUNCHER_VARIANT,
type LighthouseSkillDefinition,
} from "@/types/lighthouse-skills";
import type { LighthouseSkillDefinition } from "@/types/lighthouse-skills";
import { Muted } from "../../muted";
import { DeltaIndicator } from "../delta-indicator";
@@ -105,11 +102,9 @@ import {
} from "../finding-triage-cells";
import { DeltaValues, NotificationIndicator } from "../notification-indicator";
import { LighthouseSkillsBlock } from "./lighthouse-skills-block";
import { LighthouseSkillsRail } from "./lighthouse-skills-rail";
import { ResourceDetailSkeleton } from "./resource-detail-skeleton";
import type { CheckMeta } from "./use-resource-detail-drawer";
import { useSkillLauncherVariant } from "./use-skill-launcher-variant";
const OTHER_FINDINGS_ACTION_CELL_CLASS =
"sticky right-0 z-20 min-w-12 last:rounded-r-none! overflow-visible bg-bg-neutral-secondary before:pointer-events-none before:absolute before:inset-y-0 before:-left-8 before:w-8 before:bg-gradient-to-r before:from-transparent before:to-bg-neutral-secondary before:content-[''] group-hover:bg-bg-neutral-tertiary group-hover:before:to-bg-neutral-tertiary";
@@ -322,10 +317,6 @@ export function ResourceDetailDrawerContent({
const searchParams = useSearchParams();
const openSidePanel = useSidePanelStore((state) => state.openPanel);
const lighthouseContext = useLighthouseCurrentContext();
// A/B experiment: PostHog decides between the footer card (control) and
// the header chip rail. Falls back to the card until the flag resolves.
const isDropdownLauncher =
useSkillLauncherVariant() === SKILL_LAUNCHER_VARIANT.DROPDOWN;
const [isMuteModalOpen, setIsMuteModalOpen] = useState(false);
const [optimisticallyMutedIds, setOptimisticallyMutedIds] = useState<
Set<string>
@@ -471,19 +462,11 @@ export function ResourceDetailDrawerContent({
return result;
};
// Navigation only: the panel picks up the focused finding as context on its
// own, so no conversation is started on the user's behalf.
const handleOpenLighthouseChat = () => {
openSidePanel(SIDE_PANEL_TAB.AI_CHAT);
};
const handleLaunchSkill = (skill: LighthouseSkillDefinition) => {
openSidePanel(SIDE_PANEL_TAB.AI_CHAT);
requestPanelSkillLaunch(skill, lighthouseContext.context);
};
// TODO(experiment): capture launch origin + skill id via cloud's
// trackEvent/ANALYTICS_EVENTS once the flag readout event lands.
const handleSubmitPrompt = (text: string) => {
openSidePanel(SIDE_PANEL_TAB.AI_CHAT);
requestPanelChatMessage(text, lighthouseContext.context);
@@ -596,9 +579,8 @@ export function ResourceDetailDrawerContent({
</div>
)}
{/* Skill launcher experiment, "dropdown" variant: chip rail under the
title instead of the footer card. */}
{isCloud() && !isNavigating && isDropdownLauncher && (
{/* Lighthouse skill launcher: chip rail under the title. */}
{isCloud() && !isNavigating && (
<LighthouseSkillsRail
onLaunchSkill={handleLaunchSkill}
onSubmitPrompt={handleSubmitPrompt}
@@ -1314,14 +1296,6 @@ export function ResourceDetailDrawerContent({
</TabsContent>
</Tabs>
</div>
{/* Lighthouse AI Skills (design 1d) — the experiment's card control */}
{isCloud() && !isNavigating && !isDropdownLauncher && (
<LighthouseSkillsBlock
onLaunchSkill={handleLaunchSkill}
onAskAnything={handleOpenLighthouseChat}
/>
)}
</div>
);
}
@@ -1,44 +0,0 @@
import { renderHook } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
const { useFeatureFlagVariantMock } = vi.hoisted(() => ({
useFeatureFlagVariantMock: vi.fn(),
}));
vi.mock("@/lib/feature-flags/client", () => ({
useFeatureFlagVariant: useFeatureFlagVariantMock,
}));
import { useSkillLauncherVariant } from "./use-skill-launcher-variant";
describe("useSkillLauncherVariant", () => {
beforeEach(() => {
useFeatureFlagVariantMock.mockReset();
});
it("should read the skill launcher flag with card as the fallback", () => {
useFeatureFlagVariantMock.mockReturnValue("card");
renderHook(() => useSkillLauncherVariant());
expect(useFeatureFlagVariantMock).toHaveBeenCalledWith(
"finding-detail-skill-launcher",
["card", "dropdown"],
"card",
);
});
it.each([
["dropdown", "dropdown"],
["card", "card"],
])(
"should return the %s variant the flag module resolves",
(_label, variant) => {
useFeatureFlagVariantMock.mockReturnValue(variant);
const { result } = renderHook(() => useSkillLauncherVariant());
expect(result.current).toBe(variant);
},
);
});
@@ -1,17 +0,0 @@
"use client";
import { FEATURE_FLAG, useFeatureFlagVariant } from "@/lib/feature-flags";
import {
SKILL_LAUNCHER_VARIANT,
type SkillLauncherVariant,
} from "@/types/lighthouse-skills";
// Unresolved or unknown variants (including OSS builds that never init
// PostHog) fall back to the card control.
export function useSkillLauncherVariant(): SkillLauncherVariant {
return useFeatureFlagVariant(
FEATURE_FLAG.SKILL_LAUNCHER,
Object.values(SKILL_LAUNCHER_VARIANT),
SKILL_LAUNCHER_VARIANT.CARD,
);
}
+8 -66
View File
@@ -1,27 +1,17 @@
import { renderHook } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
const { useFeatureFlagEnabledMock, useFeatureFlagVariantKeyMock } = vi.hoisted(
() => ({
useFeatureFlagEnabledMock: vi.fn(),
useFeatureFlagVariantKeyMock: vi.fn(),
}),
);
const { useFeatureFlagEnabledMock } = vi.hoisted(() => ({
useFeatureFlagEnabledMock: vi.fn(),
}));
vi.mock("posthog-js/react", () => ({
useFeatureFlagEnabled: useFeatureFlagEnabledMock,
useFeatureFlagVariantKey: useFeatureFlagVariantKeyMock,
}));
import {
useFeatureFlag,
useFeatureFlagState,
useFeatureFlagVariant,
} from "./client";
import { useFeatureFlag, useFeatureFlagState } from "./client";
import { FEATURE_FLAG, FEATURE_FLAG_STATE } from "./shared";
const VARIANTS = ["card", "dropdown"] as const;
describe("useFeatureFlagState", () => {
beforeEach(() => {
useFeatureFlagEnabledMock.mockReset();
@@ -62,19 +52,15 @@ describe("useFeatureFlag", () => {
it("should pass the flag key through to PostHog", () => {
useFeatureFlagEnabledMock.mockReturnValue(undefined);
renderHook(() => useFeatureFlag(FEATURE_FLAG.SKILL_LAUNCHER));
renderHook(() => useFeatureFlag(FEATURE_FLAG.ISSUES));
expect(useFeatureFlagEnabledMock).toHaveBeenCalledWith(
"finding-detail-skill-launcher",
);
expect(useFeatureFlagEnabledMock).toHaveBeenCalledWith("issues-feature");
});
it("should return true when the flag is enabled", () => {
useFeatureFlagEnabledMock.mockReturnValue(true);
const { result } = renderHook(() =>
useFeatureFlag(FEATURE_FLAG.SKILL_LAUNCHER),
);
const { result } = renderHook(() => useFeatureFlag(FEATURE_FLAG.ISSUES));
expect(result.current).toBe(true);
});
@@ -85,52 +71,8 @@ describe("useFeatureFlag", () => {
])("should return false when the flag is %s", (_label, flagValue) => {
useFeatureFlagEnabledMock.mockReturnValue(flagValue);
const { result } = renderHook(() =>
useFeatureFlag(FEATURE_FLAG.SKILL_LAUNCHER),
);
const { result } = renderHook(() => useFeatureFlag(FEATURE_FLAG.ISSUES));
expect(result.current).toBe(false);
});
});
describe("useFeatureFlagVariant", () => {
beforeEach(() => {
useFeatureFlagVariantKeyMock.mockReset();
});
it("should pass the flag key through to PostHog", () => {
useFeatureFlagVariantKeyMock.mockReturnValue(undefined);
renderHook(() =>
useFeatureFlagVariant(FEATURE_FLAG.SKILL_LAUNCHER, VARIANTS, "card"),
);
expect(useFeatureFlagVariantKeyMock).toHaveBeenCalledWith(
"finding-detail-skill-launcher",
);
});
it("should return the resolved key when it is a known variant", () => {
useFeatureFlagVariantKeyMock.mockReturnValue("dropdown");
const { result } = renderHook(() =>
useFeatureFlagVariant(FEATURE_FLAG.SKILL_LAUNCHER, VARIANTS, "card"),
);
expect(result.current).toBe("dropdown");
});
it.each([
["an unknown variant", "weird"],
["unresolved", undefined],
["a boolean value", true],
])("should fall back when the flag resolves to %s", (_label, flagValue) => {
useFeatureFlagVariantKeyMock.mockReturnValue(flagValue);
const { result } = renderHook(() =>
useFeatureFlagVariant(FEATURE_FLAG.SKILL_LAUNCHER, VARIANTS, "card"),
);
expect(result.current).toBe("card");
});
});
+1 -16
View File
@@ -1,9 +1,6 @@
"use client";
import {
useFeatureFlagEnabled,
useFeatureFlagVariantKey,
} from "posthog-js/react";
import { useFeatureFlagEnabled } from "posthog-js/react";
import {
FEATURE_FLAG_STATE,
@@ -31,15 +28,3 @@ export function useFeatureFlagState(flag: FeatureFlag): FeatureFlagState {
export function useFeatureFlag(flag: FeatureFlag): boolean {
return useFeatureFlagState(flag) === FEATURE_FLAG_STATE.ON;
}
// Multivariate flag. Returns the resolved key when it is one of `variants`;
// unresolved, boolean or unknown values fall back to `fallback`.
export function useFeatureFlagVariant<T extends string>(
flag: FeatureFlag,
variants: readonly T[],
fallback: T,
): T {
const value = useFeatureFlagVariantKey(flag);
const match = variants.find((variant) => variant === value);
return match ?? fallback;
}
-1
View File
@@ -5,7 +5,6 @@ import { FEATURE_FLAG, FEATURE_FLAG_STATE } from "./shared";
describe("FEATURE_FLAG", () => {
it("should map each flag to the key PostHog knows it by", () => {
expect(FEATURE_FLAG).toEqual({
SKILL_LAUNCHER: "finding-detail-skill-launcher",
ISSUES: "issues-feature",
BILLING_SYSTEM_METRONOME: "BILLING_SYSTEM_METRONOME",
});
-3
View File
@@ -3,9 +3,6 @@
// lives here, including the ones only the cloud fork reads, so the registry
// never diverges between repos.
export const FEATURE_FLAG = {
// Multivariate flag deciding which skill launcher the finding detail drawer
// renders ("card" | "dropdown").
SKILL_LAUNCHER: "finding-detail-skill-launcher",
// Boolean flag gating the experimental Issues feature (contextual findings).
// Read by cloud only.
ISSUES: "issues-feature",
-10
View File
@@ -1,15 +1,5 @@
import type { LucideIcon } from "lucide-react";
// Variants of the FEATURE_FLAG.SKILL_LAUNCHER experiment (see
// lib/feature-flags). Anything other than "dropdown" renders the card control.
export const SKILL_LAUNCHER_VARIANT = {
CARD: "card",
DROPDOWN: "dropdown",
} as const;
export type SkillLauncherVariant =
(typeof SKILL_LAUNCHER_VARIANT)[keyof typeof SKILL_LAUNCHER_VARIANT];
export const LIGHTHOUSE_SKILL_ID = {
CONTEXTUAL_FIX: "contextual-fix",
TRIAGE_DECISION: "triage-decision",