Extension guides
Renderer context
Every contribution receives the current project, session, and agent view, a typed channel, and a small set of desktop actions. Treat the context as read-only render input.
Context shape
interface RendererContext<Protocol extends ExtensionProtocol> {
extension: { id: string; name: string };
project: { path: string; name: string };
session: { file: string | null; name?: string };
agent: {
status: "idle" | "starting" | "ready" | "error" | "exited";
running: boolean;
model?: {
provider: string;
id: string;
name?: string;
reasoning?: boolean;
contextWindow?: number;
};
thinkingLevel: string;
};
channel: RendererChannel<Protocol>;
actions: RendererActions;
}Read view state
The context object is rebuilt when visible state changes. Read project, session, and agent fields during render rather than copying them into component state. A new chat has session.file: null; design that empty state explicitly.
function SessionStatus({ context }: { context: RendererContext }) {
if (!context.session.file) return <p>Start a chat to load session data.</p>;
return (
<p>
{context.session.name ?? "Untitled session"}
{context.agent.running ? " · Running" : " · Idle"}
</p>
);
}Use the channel in effects
channel.call and channel.on keep stable identities until the extension reloads. Destructure those functions and use them as effect dependencies; do not depend on the full context object.
const { call, on } = context.channel;
useEffect(() => {
let active = true;
void call("state").then((state) => {
if (active) setState(state);
});
const unsubscribe = on("changed", setState);
return () => {
active = false;
unsubscribe();
};
}, [call, on]);Desktop actions
notify(message, tone?)shows a NativePi notification attributed to the extension.insertIntoComposer(text)edits the active draft without sending it.openExternal(url)opens an HTTP or HTTPS URL in the default browser.openFile(file, location?)opens a project-relative file in the preferred editor.revealFile(file)reveals a project-relative file in the platform file manager.copyText(text)writes plain text to the active client's clipboard.
<Button
onClick={async () => {
try {
await context.actions.openFile("src/index.ts", { line: 24, column: 3 });
} catch (error) {
context.actions.notify(String(error), "error");
}
}}
>
Open source
</Button>Choose a surface
The same context reaches every slot. Read Contribution slots to choose where your component belongs.