From 135f89527d39bc11f326e9cd40d1bda8822a6168 Mon Sep 17 00:00:00 2001 From: alejandrobailo Date: Wed, 22 Jul 2026 16:40:08 +0200 Subject: [PATCH] fix(ui): make Lighthouse page context automatic --- .../chat/lighthouse-v2-chat-view.tsx | 18 +---- .../panel/lighthouse-panel-chat.test.tsx | 53 +------------ .../lighthouse/_lib/chat-store.test.ts | 45 ----------- .../(prowler)/lighthouse/_lib/chat-store.ts | 29 +------- .../lighthouse/context-chip.test.tsx | 74 +++---------------- ui/components/lighthouse/context-chip.tsx | 52 ++++++------- 6 files changed, 38 insertions(+), 233 deletions(-) diff --git a/ui/app/(prowler)/lighthouse/_components/chat/lighthouse-v2-chat-view.tsx b/ui/app/(prowler)/lighthouse/_components/chat/lighthouse-v2-chat-view.tsx index 03178ca203..49ad5462fe 100644 --- a/ui/app/(prowler)/lighthouse/_components/chat/lighthouse-v2-chat-view.tsx +++ b/ui/app/(prowler)/lighthouse/_components/chat/lighthouse-v2-chat-view.tsx @@ -21,7 +21,7 @@ import { type LighthouseV2SupportedModel, type LighthouseV2SupportedProvider, } from "@/app/(prowler)/lighthouse/_types"; -import { LighthouseContextControl } from "@/components/lighthouse/context-chip"; +import { LighthouseCurrentContextBadge } from "@/components/lighthouse/context-chip"; import { Card } from "@/components/shadcn"; import { Combobox, @@ -68,14 +68,11 @@ export function LighthouseV2ChatView({ lastSubmission, selectedModelSelection, modelPreferenceSaving, - isContextEnabled, setInput, dismissFeedback, selectModel, submitMessage, retryLastMessage, - disableContext, - enableContext, } = state; const { modelsByProvider, supportedProviders } = config; const connectedConfigurations = config.configurations.filter( @@ -146,14 +143,7 @@ export function LighthouseV2ChatView({ onRetry: () => void retryLastMessage(), onDismissFeedback: dismissFeedback, contextControl: supportsAutomaticContext ? ( - + ) : undefined, canSend, input, @@ -224,9 +214,7 @@ export function LighthouseV2ChatView({ footer={emptyStateFooter} compact={surface === LIGHTHOUSE_CHAT_SURFACE.PANEL} suggestions={ - supportsAutomaticContext && isContextEnabled - ? currentContext.page.suggestions - : undefined + supportsAutomaticContext ? currentContext.page.suggestions : undefined } /> ); diff --git a/ui/app/(prowler)/lighthouse/_components/panel/lighthouse-panel-chat.test.tsx b/ui/app/(prowler)/lighthouse/_components/panel/lighthouse-panel-chat.test.tsx index 2b1d12dafd..d75a164061 100644 --- a/ui/app/(prowler)/lighthouse/_components/panel/lighthouse-panel-chat.test.tsx +++ b/ui/app/(prowler)/lighthouse/_components/panel/lighthouse-panel-chat.test.tsx @@ -198,10 +198,8 @@ describe("LighthousePanelChat", () => { const modelControl = await screen.findByLabelText( "Current model: OpenAI gpt-5.1", ); - const contextControl = screen.getByRole("button", { - name: "Disable Findings context", - }); - expect(modelControl.nextElementSibling).toBe(contextControl); + const contextBadge = screen.getByLabelText("Findings context"); + expect(modelControl.nextElementSibling).toBe(contextBadge); }); it("sends the current page context from the side panel", async () => { @@ -245,53 +243,6 @@ describe("LighthousePanelChat", () => { ); }); - it("keeps context disabled and restores global suggestions for the conversation", async () => { - // Given - const user = userEvent.setup(); - createSessionMock.mockResolvedValue({ - data: session("session-without-context", "Question"), - }); - sendMessageMock.mockResolvedValue({ - data: { - task: { - id: "task-without-context", - name: "lighthouse-run", - state: "executing", - }, - }, - }); - render(); - const input = await screen.findByRole("textbox", { name: "Message" }); - - // When - await user.click( - screen.getByRole("button", { name: "Disable Findings context" }), - ); - - // Then - const disabledContext = screen.getByRole("button", { - name: "Enable Findings context", - }); - expect(disabledContext).toHaveAttribute("aria-pressed", "false"); - expect(disabledContext).toHaveTextContent("@ Findings"); - expect( - screen.getByRole("button", { name: "Critical findings" }), - ).toBeInTheDocument(); - - // When - await user.type(input, "Question{Enter}"); - - // Then - await waitFor(() => - expect(sendMessageMock).toHaveBeenCalledWith({ - sessionId: "session-without-context", - displayText: "Question", - provider: "openai", - model: "gpt-5.1", - }), - ); - }); - it("opens a recent chat in place without navigating", async () => { // Given const user = userEvent.setup(); diff --git a/ui/app/(prowler)/lighthouse/_lib/chat-store.test.ts b/ui/app/(prowler)/lighthouse/_lib/chat-store.test.ts index 59fdaceeca..9b6921771c 100644 --- a/ui/app/(prowler)/lighthouse/_lib/chat-store.test.ts +++ b/ui/app/(prowler)/lighthouse/_lib/chat-store.test.ts @@ -185,51 +185,6 @@ describe("createLighthouseChatStore", () => { }); }); - it("retries with the original snapshot even when current context was disabled", async () => { - const store = makeStore(); - const context = findingsContext(); - await store.getState().submitMessage("Prioritize findings", context); - eventSources[0].fail(2 /* EventSource.CLOSED */); - store.getState().disableContext(); - - await store.getState().retryLastMessage(); - - expect(sendMessageMock).toHaveBeenNthCalledWith(2, { - sessionId: "session-1", - displayText: "Prioritize findings", - context, - provider: "openai", - model: "gpt-5.1", - }); - expect(store.getState().isContextEnabled).toBe(false); - }); - - it("keeps context disabled for the conversation and restores it for a new chat", async () => { - // Given - const store = makeStore(); - store.getState().disableContext(); - - // When - await store - .getState() - .submitMessage("Question without context", findingsContext()); - - // Then - expect(store.getState().isContextEnabled).toBe(false); - expect(sendMessageMock).toHaveBeenCalledWith({ - sessionId: "session-1", - displayText: "Question without context", - provider: "openai", - model: "gpt-5.1", - }); - - // When - store.getState().resetToNewChat(); - - // Then - expect(store.getState().isContextEnabled).toBe(true); - }); - it("degrades oversized context before sending without blocking the message", async () => { // Given const store = makeStore(); diff --git a/ui/app/(prowler)/lighthouse/_lib/chat-store.ts b/ui/app/(prowler)/lighthouse/_lib/chat-store.ts index 6ab08524d1..876fd180c3 100644 --- a/ui/app/(prowler)/lighthouse/_lib/chat-store.ts +++ b/ui/app/(prowler)/lighthouse/_lib/chat-store.ts @@ -65,7 +65,6 @@ export interface LighthouseChatState { /** @deprecated Use lastSubmission so retries can preserve their context snapshot. */ lastSubmittedText: string | null; lastSubmission: LighthouseChatSubmission | null; - isContextEnabled: boolean; selectedModelSelection: LighthouseV2ModelSelection | null; modelPreferenceSaving: boolean; setSessionUrlSyncEnabled: (enabled: boolean) => void; @@ -77,8 +76,6 @@ export interface LighthouseChatState { context?: LighthouseContextEnvelope, ) => Promise; retryLastMessage: () => Promise; - disableContext: () => void; - enableContext: () => void; openSession: (sessionId: string) => Promise; resetToNewChat: () => void; handleSessionArchived: (sessionId: string) => void; @@ -90,10 +87,6 @@ export interface LighthouseChatSubmission { context?: LighthouseContextEnvelope; } -interface LighthouseChatSubmitOptions { - bypassContextGate?: boolean; -} - export type LighthouseChatStore = StoreApi; export function selectLighthouseChatCanSend( @@ -270,7 +263,6 @@ export function createLighthouseChatStore( const submitMessageInternal = async ( displayText: string, context?: LighthouseContextEnvelope, - submitOptions: LighthouseChatSubmitOptions = {}, ): Promise => { if (!displayText.trim()) return; if (!get().selectedModelSelection) { @@ -279,11 +271,7 @@ export function createLighthouseChatStore( } if (!selectLighthouseChatCanSend(get())) return; - const shouldUseContext = - submitOptions.bypassContextGate === true || get().isContextEnabled; - const contextSnapshot = shouldUseContext - ? prepareLighthouseContext(context) - : undefined; + const contextSnapshot = prepareLighthouseContext(context); set({ isSubmitting: true }); try { @@ -366,7 +354,6 @@ export function createLighthouseChatStore( isLoadingSession: false, lastSubmittedText: null, lastSubmission: null, - isContextEnabled: true, selectedModelSelection: resolveInitialModelSelection( connectedConfigurations, config.modelsByProvider, @@ -381,10 +368,6 @@ export function createLighthouseChatStore( dismissFeedback: () => set({ feedback: null }), - disableContext: () => set({ isContextEnabled: false }), - - enableContext: () => set({ isContextEnabled: true }), - selectModel: async (selection) => { // The selection drives the model used for the next message, so it stays // applied even if persisting it as the provider's default model fails — @@ -416,13 +399,7 @@ export function createLighthouseChatStore( retryLastMessage: async () => { const submission = get().lastSubmission; if (!submission) return; - await submitMessageInternal( - submission.displayText, - submission.context, - { - bypassContextGate: true, - }, - ); + await submitMessageInternal(submission.displayText, submission.context); }, openSession: async (sessionId) => { @@ -439,7 +416,6 @@ export function createLighthouseChatStore( isLoadingSession: true, lastSubmittedText: null, lastSubmission: null, - isContextEnabled: true, streamState: createInitialLighthouseV2StreamState(), }); syncSessionUrl(sessionId); @@ -467,7 +443,6 @@ export function createLighthouseChatStore( isLoadingSession: false, lastSubmittedText: null, lastSubmission: null, - isContextEnabled: true, streamState: createInitialLighthouseV2StreamState(), }); syncSessionUrl(null); diff --git a/ui/components/lighthouse/context-chip.test.tsx b/ui/components/lighthouse/context-chip.test.tsx index 006bf9a465..e586ef437c 100644 --- a/ui/components/lighthouse/context-chip.test.tsx +++ b/ui/components/lighthouse/context-chip.test.tsx @@ -1,86 +1,32 @@ import { render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; -import { describe, expect, it, vi } from "vitest"; +import { describe, expect, it } from "vitest"; import type { LighthouseContextEnvelope } from "@/types/lighthouse-context"; import { LighthouseContextBadge, - LighthouseContextControl, + LighthouseCurrentContextBadge, } from "./context-chip"; -describe("LighthouseContextControl", () => { - it("should show enabled context as a pressed badge and explain disabling it", async () => { +describe("LighthouseCurrentContextBadge", () => { + it("should show current context as read-only and explain automatic inclusion", async () => { // Given const user = userEvent.setup(); - const onDisable = vi.fn(); - render( - , - ); - const contextControl = screen.getByRole("button", { - name: "Disable Findings context", - }); + render(); + const contextBadge = screen.getByLabelText("Findings context"); // When - await user.hover(contextControl); + await user.hover(contextBadge); // Then - expect(contextControl).toHaveAttribute("aria-pressed", "true"); - expect(contextControl).toHaveTextContent("@ Findings +1"); - expect(await screen.findByRole("tooltip")).toHaveTextContent( - "Click to stop including Findings context in new messages.", - ); - - // When - await user.click(contextControl); - - // Then - expect(onDisable).toHaveBeenCalledOnce(); - }); - - it("should keep the same badge label when disabled and explain enabling it", async () => { - // Given - const user = userEvent.setup(); - const onEnable = vi.fn(); - render( - , - ); - const contextControl = screen.getByRole("button", { - name: "Enable Findings context", - }); - - // When - await user.hover(contextControl); - - // Then - expect(contextControl).toHaveAttribute("aria-pressed", "false"); - expect(contextControl).toHaveTextContent("@ Findings +1"); + expect(contextBadge).toHaveTextContent("@ Findings +1"); expect( - screen.queryByText("+ Add Findings context"), + screen.queryByRole("button", { name: /Findings context/ }), ).not.toBeInTheDocument(); expect(await screen.findByRole("tooltip")).toHaveTextContent( - "Click to include Findings context in new messages.", + "Findings context will be included in your next message.", ); - - // When - await user.click(contextControl); - - // Then - expect(onEnable).toHaveBeenCalledOnce(); }); }); diff --git a/ui/components/lighthouse/context-chip.tsx b/ui/components/lighthouse/context-chip.tsx index f197f820a5..0909138dc8 100644 --- a/ui/components/lighthouse/context-chip.tsx +++ b/ui/components/lighthouse/context-chip.tsx @@ -12,43 +12,27 @@ import { type LighthouseContextEnvelope, } from "@/types/lighthouse-context"; -interface LighthouseContextControlProps { +interface LighthouseCurrentContextBadgeProps { context: LighthouseContextEnvelope | undefined; - pageLabel: string; - enabled: boolean; - selectionCount: number; - onDisable: () => void; - onEnable: () => void; } -export function LighthouseContextControl({ +export function LighthouseCurrentContextBadge({ context, - pageLabel, - enabled, - selectionCount, - onDisable, - onEnable, -}: LighthouseContextControlProps) { +}: LighthouseCurrentContextBadgeProps) { if (!context) return null; + const { pageLabel, selectionCount } = getContextBadgeContent(context); return ( - - + - {enabled - ? `Click to stop including ${pageLabel} context in new messages.` - : `Click to include ${pageLabel} context in new messages.`} + {pageLabel} context will be included in your next message. ); @@ -59,13 +43,7 @@ export function LighthouseContextBadge({ }: { context: LighthouseContextEnvelope; }) { - const page = context.items.find( - (item) => item.kind === LIGHTHOUSE_CONTEXT_KIND.PAGE, - ); - const pageLabel = page?.label ?? "Context"; - const selectionCount = context.items.filter( - (item) => item.source === LIGHTHOUSE_CONTEXT_SOURCE.SELECTION, - ).length; + const { pageLabel, selectionCount } = getContextBadgeContent(context); return ( @@ -81,6 +59,18 @@ export function LighthouseContextBadge({ ); } +function getContextBadgeContent(context: LighthouseContextEnvelope) { + const page = context.items.find( + (item) => item.kind === LIGHTHOUSE_CONTEXT_KIND.PAGE, + ); + const pageLabel = page?.label ?? "Context"; + const selectionCount = context.items.filter( + (item) => item.source === LIGHTHOUSE_CONTEXT_SOURCE.SELECTION, + ).length; + + return { pageLabel, selectionCount }; +} + function LighthouseContextTooltip({ context, }: {