From d6b55dec7612b322f3b665bf02a5a79f3df0afe9 Mon Sep 17 00:00:00 2001 From: alejandrobailo Date: Wed, 22 Jul 2026 17:18:10 +0200 Subject: [PATCH] feat(ui): prioritize focused Lighthouse context --- .../lighthouse/_lib/chat-store.test.ts | 2 +- ui/lib/lighthouse/context/compiler.ts | 35 +++--- ui/lib/lighthouse/context/context.test.ts | 113 +++++++++++++++++- ui/types/lighthouse-context.ts | 1 + 4 files changed, 132 insertions(+), 19 deletions(-) diff --git a/ui/app/(prowler)/lighthouse/_lib/chat-store.test.ts b/ui/app/(prowler)/lighthouse/_lib/chat-store.test.ts index 59fdaceeca..7dc409eb89 100644 --- a/ui/app/(prowler)/lighthouse/_lib/chat-store.test.ts +++ b/ui/app/(prowler)/lighthouse/_lib/chat-store.test.ts @@ -244,7 +244,7 @@ describe("createLighthouseChatStore", () => { displayText: "Prioritize findings", context: { ...context, - items: context.items.slice(0, 2), + items: context.items.slice(0, 3), }, }), ); diff --git a/ui/lib/lighthouse/context/compiler.ts b/ui/lib/lighthouse/context/compiler.ts index bf08d07e6e..52369fd1ff 100644 --- a/ui/lib/lighthouse/context/compiler.ts +++ b/ui/lib/lighthouse/context/compiler.ts @@ -41,27 +41,15 @@ export function compileLighthouseContext( const seen = new Set(); const items = parsedItems + .sort((left, right) => getItemOrder(left) - getItemOrder(right)) .filter((item) => { const key = `${item.kind}:${item.id}`; if (seen.has(key)) return false; seen.add(key); return true; - }) - .sort((left, right) => getItemOrder(left) - getItemOrder(right)); + }); - const completeContext = buildEnvelopeWithinLimits(items); - if (completeContext) return completeContext; - - const withoutSummaries = items.filter( - (item) => - item.kind === LIGHTHOUSE_CONTEXT_KIND.PAGE || - item.source !== LIGHTHOUSE_CONTEXT_SOURCE.AUTOMATIC, - ); - const selectionContext = buildEnvelopeWithinLimits(withoutSummaries); - if (selectionContext) return selectionContext; - - const page = items.find((item) => item.kind === LIGHTHOUSE_CONTEXT_KIND.PAGE); - return page ? buildEnvelopeWithinLimits([page]) : undefined; + return buildEnvelopeWithProgressiveDegradation(items); } function hasDifferentScope(candidate: unknown, scopeKey: string): boolean { @@ -76,7 +64,8 @@ function hasDifferentScope(candidate: unknown, scopeKey: string): boolean { function getItemOrder(item: LighthouseContextItem): number { if (item.kind === LIGHTHOUSE_CONTEXT_KIND.PAGE) return 0; - return item.source === LIGHTHOUSE_CONTEXT_SOURCE.AUTOMATIC ? 2 : 1; + if (item.source === LIGHTHOUSE_CONTEXT_SOURCE.FOCUSED) return 1; + return item.source === LIGHTHOUSE_CONTEXT_SOURCE.AUTOMATIC ? 3 : 2; } function buildEnvelopeWithinLimits( @@ -94,3 +83,17 @@ function buildEnvelopeWithinLimits( const byteLength = getApiLighthouseContextByteLength(result.data); return byteLength <= LIGHTHOUSE_CONTEXT_MAX_BYTES ? result.data : undefined; } + +function buildEnvelopeWithProgressiveDegradation( + items: LighthouseContextItem[], +): LighthouseContextEnvelope | undefined { + const retainedItems = items.slice(0, 8); + + while (retainedItems.length > 0) { + const context = buildEnvelopeWithinLimits(retainedItems); + if (context) return context; + retainedItems.pop(); + } + + return undefined; +} diff --git a/ui/lib/lighthouse/context/context.test.ts b/ui/lib/lighthouse/context/context.test.ts index fafa5fa903..3883aa488a 100644 --- a/ui/lib/lighthouse/context/context.test.ts +++ b/ui/lib/lighthouse/context/context.test.ts @@ -244,10 +244,51 @@ describe("compileLighthouseContext", () => { "finding:finding-1", ]); }); + + it("should retain the highest-priority duplicate", () => { + // Given + const scopeKey = "findings:/findings"; + const items = [ + { + kind: "page", + id: "findings", + source: "automatic", + scopeKey, + label: "Findings", + path: "/findings", + }, + { + kind: "finding", + id: "finding-1", + source: "selection", + scopeKey, + label: "Selected finding", + findingId: "finding-1", + }, + { + kind: "finding", + id: "finding-1", + source: "focused", + scopeKey, + label: "Focused finding", + findingId: "finding-1", + }, + ]; + + // When + const context = compileLighthouseContext(items, scopeKey); + + // Then + expect(context?.items[1]).toMatchObject({ + id: "finding-1", + source: "focused", + label: "Focused finding", + }); + }); }); describe("when contributors arrive in render order", () => { - it("should order page, selection, and summary items deterministically", () => { + it("should order page, focused, selection, and summary items deterministically", () => { // Given const scopeKey = "findings:/findings"; const items = [ @@ -268,6 +309,14 @@ describe("compileLighthouseContext", () => { label: "Selected finding", findingId: "finding-1", }, + { + kind: "finding", + id: "finding-focused", + source: "focused", + scopeKey, + label: "Focused finding", + findingId: "finding-focused", + }, { kind: "page", id: "findings", @@ -284,6 +333,7 @@ describe("compileLighthouseContext", () => { // Then expect(context?.items.map((item) => item.id)).toEqual([ "findings", + "finding-focused", "finding-1", "findings-summary", ]); @@ -291,7 +341,7 @@ describe("compileLighthouseContext", () => { }); describe("when serialized context exceeds 2 KiB", () => { - it("should remove automatic summaries before dropping the selection", () => { + it("should drop lowest-priority items until the context fits", () => { // Given const scopeKey = "findings:/findings"; const page = { @@ -332,6 +382,7 @@ describe("compileLighthouseContext", () => { expect(context?.items.map((item) => item.id)).toEqual([ "findings", "finding-1", + "summary-0", ]); }); @@ -369,6 +420,64 @@ describe("compileLighthouseContext", () => { }); }); + describe("when context exceeds the eight-item limit", () => { + it("should progressively drop only the lowest-priority items", () => { + // Given + const scopeKey = "findings:/findings"; + const page = { + kind: "page", + id: "findings", + source: "automatic", + scopeKey, + label: "Findings", + path: "/findings", + }; + const focused = { + kind: "finding", + id: "focused", + source: "focused", + scopeKey, + label: "Focused finding", + findingId: "focused", + }; + const selections = Array.from({ length: 2 }, (_, index) => ({ + kind: "finding", + id: `selection-${index}`, + source: "selection", + scopeKey, + label: `Selected finding ${index}`, + findingId: `selection-${index}`, + })); + const summaries = Array.from({ length: 6 }, (_, index) => ({ + kind: "finding", + id: `summary-${index}`, + source: "automatic", + scopeKey, + label: `Summary ${index}`, + findingId: `summary-${index}`, + total: index, + })); + + // When + const context = compileLighthouseContext( + [page, ...summaries, ...selections, focused], + scopeKey, + ); + + // Then + expect(context?.items.map((item) => item.id)).toEqual([ + "findings", + "focused", + "selection-0", + "selection-1", + "summary-0", + "summary-1", + "summary-2", + "summary-3", + ]); + }); + }); + describe("when contributors belong to another page", () => { it("should ignore stale scoped data", () => { // Given / When diff --git a/ui/types/lighthouse-context.ts b/ui/types/lighthouse-context.ts index 5a1989556f..7cc1a6ead0 100644 --- a/ui/types/lighthouse-context.ts +++ b/ui/types/lighthouse-context.ts @@ -13,6 +13,7 @@ export type LighthouseContextKind = export const LIGHTHOUSE_CONTEXT_SOURCE = { AUTOMATIC: "automatic", + FOCUSED: "focused", SELECTION: "selection", MANUAL: "manual", } as const;