Skip to content

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.