| // Copyright (C) 2026 The Android Open Source Project |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| import type {Trace} from '../../public/trace'; |
| import type {ContextProviderRegistration} from './api'; |
| |
| /** One sampled context item, ready for the strip and the prompt. */ |
| export interface ContextItem { |
| /** The provider's stable id; the strip uses it to track toggled-off items. */ |
| readonly id: string; |
| /** Human-readable label shown in the strip (the provider's summary). */ |
| readonly label: string; |
| /** The raw payload sent to the model (also shown on hover / expand). */ |
| readonly payload: string; |
| } |
| |
| /** |
| * The concrete registry behind the public registerContextProvider. Core and |
| * plugin-contributed providers both land here; the chat panel samples it to |
| * build the context strip and the prompt. |
| */ |
| export class ContextRegistry { |
| private readonly providers = new Map<string, ContextProviderRegistration>(); |
| |
| /** Register a context provider. Throws if the id is already registered. */ |
| registerContextProvider(provider: ContextProviderRegistration): void { |
| if (this.providers.has(provider.id)) { |
| throw new Error(`Context provider "${provider.id}" already registered`); |
| } |
| this.providers.set(provider.id, provider); |
| } |
| |
| /** |
| * Sample every provider into context items. Providers returning undefined or |
| * throwing are skipped, so one broken contributor can't take out the prompt. |
| */ |
| buildContextItems(): ContextItem[] { |
| const items: ContextItem[] = []; |
| for (const provider of this.providers.values()) { |
| let snapshot; |
| try { |
| snapshot = provider.getContext(); |
| } catch { |
| continue; |
| } |
| if (snapshot === undefined) continue; |
| items.push({ |
| id: provider.id, |
| label: snapshot.summary, |
| payload: |
| typeof snapshot.data === 'string' |
| ? snapshot.data |
| : JSON.stringify(snapshot.data), |
| }); |
| } |
| return items; |
| } |
| |
| /** |
| * The invariant payload-format explanations, for the system prompt. Sorted by |
| * provider id so the assembled prompt is byte-identical across turns. |
| */ |
| descriptions(): string[] { |
| return Array.from(this.providers.values()) |
| .filter((p) => p.description !== undefined && p.description !== '') |
| .sort((a, b) => a.id.localeCompare(b.id)) |
| .map((p) => p.description!); |
| } |
| } |
| |
| /** |
| * Register the core context providers (page, viewport, selection) owned by the |
| * Intelletto plugin itself. Richer providers come from the plugins that own |
| * them. |
| */ |
| export function registerCoreContextProviders( |
| reg: ContextRegistry, |
| trace: Trace, |
| ): void { |
| // The current page (where the user is looking). Never returns undefined - |
| // the strip is never empty and never silent about what's being sent. |
| reg.registerContextProvider({ |
| id: 'dev.perfetto.Intelletto#page', |
| getContext() { |
| const route = trace.getCurrentRoute(); |
| const page = `${route.page}${route.subpage}` || '/viewer'; |
| return { |
| summary: `Page: ${page}`, |
| data: `Current page route: ${page}`, |
| }; |
| }, |
| }); |
| |
| // The timeline viewport: the time bounds currently visible. Like the page, |
| // it always has a value, so the model always knows what window the user is |
| // looking at. |
| reg.registerContextProvider({ |
| id: 'dev.perfetto.Intelletto#viewport', |
| description: `Viewport context payloads (kind "viewport"): |
| - "start" and "end" are the trace-processor-nanosecond bounds of the time range |
| currently visible on the timeline.`, |
| getContext() { |
| const span = trace.timeline.visibleWindow.toTimeSpan(); |
| return { |
| summary: 'Visible time range', |
| data: { |
| kind: 'viewport', |
| start: Number(span.start), |
| end: Number(span.end), |
| }, |
| }; |
| }, |
| }); |
| |
| // The current selection, if any. |
| reg.registerContextProvider({ |
| id: 'dev.perfetto.Intelletto#selection', |
| description: `Selection context payloads: |
| - "ts", "dur", "start" and "end" are trace-processor nanoseconds. |
| - For kind "track_event", "eventId" is the row id in the SQL table backing the |
| track (e.g. the "id" column of "slice"). |
| - "trackUri" identifies a timeline track and is accepted verbatim by tools |
| taking track URIs.`, |
| getContext() { |
| const sel = trace.selection.selection; |
| switch (sel.kind) { |
| case 'track_event': |
| return { |
| summary: 'Selected event', |
| data: { |
| kind: 'track_event', |
| trackUri: sel.trackUri, |
| eventId: sel.eventId, |
| ts: Number(sel.ts), |
| dur: sel.dur === undefined ? undefined : Number(sel.dur), |
| }, |
| }; |
| case 'area': |
| return { |
| summary: 'Selected time range', |
| data: { |
| kind: 'area', |
| start: Number(sel.start), |
| end: Number(sel.end), |
| trackUris: sel.trackUris, |
| }, |
| }; |
| case 'track': |
| return { |
| summary: 'Selected track', |
| data: {kind: 'track', trackUri: sel.trackUri}, |
| }; |
| default: |
| return undefined; |
| } |
| }, |
| }); |
| } |