blob: 0a9430b20d01d0c23489edf4eab4008820684762 [file] [edit]
// 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;
}
},
});
}