Skip to content

FE-2039: Let a plugin provide the AI assistant panel's content and title bar - #10103

Merged
kube merged 11 commits into
mainfrom
claude/assistant-custom-panel
Oct 21, 2026
Merged

kube merged 11 commits into
mainfrom
claude/assistant-custom-panel

Conversation

@kube

@kube kube commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds an assistant contribution to the plugin API. A plugin renders any React content in the AI assistant panel and fills its title bar, and the editor owns the panel's frame, placement, resizing, dock toggle and Close.

Editor shows the first running plugin that provides an assistant, and the aiAssistant prop's built-in chat while none runs. Built-in chat does not change, and the empty-net prompt also serves a plugin assistant that takes it. aiAssistant prop and the built-in chat's API are marked deprecated. Replaces #9914.

Before

<Petrinaut handle={handle} aiAssistant={{ transport, messages, onMessages }} />

With a second tab, the built-in chat drew both tabs:

<Petrinaut
  handle={handle}
  aiAssistant={{
    transport,
    primaryLabel: "Chat",
    additionalTab: { label: "Ledger", content: <LedgerPane /> },
  }}
/>

After

The chat lives in the plugin's hook, so onPrompt can send the empty-net prompt's message to it:

import { Chat, useChat } from "@ai-sdk/react";
import { AssistantPanelTitle, definePetrinautPlugin, type PluginHook } from "@hashintel/petrinaut/ui/plugin";
import type { UIMessage } from "ai";

const ChatPlugin = definePetrinautPlugin({ id: "example.chat", name: "Chat", assistant: true });

const ChatPanel = ({ chat }: { chat: Chat<UIMessage> }) => {
  const { messages, setMessages } = useChat({ chat });

  return (
    <>
      <AssistantPanelTitle
        actions={
          messages.length > 0 && (
            <button type="button" onClick={() => setMessages([])}>
              Clear
            </button>
          )
        }
      >
        Chat
      </AssistantPanelTitle>
      <MyChat chat={chat} />
    </>
  );
};

const useChatPlugin: PluginHook<typeof ChatPlugin> = () => {
  const [chat] = useState(() => new Chat({ transport }));

  return {
    assistant: {
      content: <ChatPanel chat={chat} />,
      onPrompt: (text) => chat.sendMessage({ text }),
    },
  };
};

export const chatPlugin = ChatPlugin(useChatPlugin);

<Petrinaut handle={handle} plugins={[chatPlugin]} />

With a second tab, the content draws its own tabs in the title bar:

const ChatPanel = ({ chat }: { chat: Chat<UIMessage> }) => {
  const [tab, setTab] = useState<"chat" | "ledger">("chat");

  return (
    <>
      <AssistantPanelTitle>
        <button type="button" onClick={() => setTab("chat")}>Chat</button>
        <button type="button" onClick={() => setTab("ledger")}>Ledger</button>
      </AssistantPanelTitle>
      {tab === "chat" ? <MyChat chat={chat} /> : <LedgerPane />}
    </>
  );
};

Links

Changes

Plugin API

  • Manifest assistant: true lets a plugin provide the AI assistant

    Its hook returns assistant: { content }.
    Editor shows the first running plugin that declares one, and the aiAssistant prop's chat while none runs.

  • content fills the panel below its title bar

    It renders behind the plugin's error boundary and remounts when another net opens.

  • AssistantPanelTitle fills the title bar from inside content

    children render after the assistant icon, such as a title or tabs.
    actions render between the dock toggle and the Close button.

  • useAssistantPanel() reads whether the panel is open and closes it
  • assistant.onPrompt(text) takes the empty-net prompt's message

    Editor opens the panel, then calls it.
    Prompt shows while the assistant returns onPrompt, or while the built-in chat runs.

  • data-autofocus marks the element that takes focus when the panel opens

    Usually the composer. Without one, the panel takes focus.

  • aiAssistant prop and the built-in chat's API are deprecated

    Every PetrinautAi* type, definePetrinautAiInteractiveTool, executePetrinautAiMutation, ExperimentExecutionCard and the DefaultChatTransport re-export carry @deprecated.
    So do the built-in chat's entry components.
    Each tag names the replacement, a plugin that declares assistant.

Editor

  • Plugin assistants render in a panel frame of their own

    Frame holds the dock spacer, docked and floating placement, resizing, the card and the title bar with the dock toggle and Close.

  • Plugins section tags a plugin that provides the assistant with Assistant

Test coverage

  • editor-assistant.test.tsx:

    Plugin content and title bar in the panel, focus on open, Close from the title bar, and a failing content that keeps the panel's frame.

  • editor-view.test.tsx:

    Empty-net prompt hands its message to onPrompt and opens the panel, and stays hidden for an assistant without one.

  • define-petrinaut-plugin.test.ts:

    Type test: the hook returns assistant exactly when the manifest declares it.

  • Existing @hashintel/petrinaut and website unit suites:

    Run the built-in chat as on main.

How to test

  • Open Petrinaut preview on Vercel
  • Menu > New
  • Submit Build an SIR model in prompt

    Expect built-in chat open and net built

  • Float AI assistant
  • Dock AI assistant
  • Close AI assistant
  • Press ⌘⇧K

    Expect the same conversation and focus in composer

@kube kube self-assigned this Oct 19, 2026
@github-actions github-actions Bot added area/infra Relates to version control, CI, CD or IaC (area) area/libs Relates to first-party libraries/crates/packages (area) type/eng > frontend Owned by the @frontend team type/eng > backend Owned by the @backend team labels Oct 19, 2026
@kube
kube added this pull request to stack #10107 October 19, 2026 22:04
@vercel

vercel Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
petrinaut Ready Ready Preview Oct 21, 2026 11:44am UTC
petrinaut-docs Ready Ready Preview Oct 21, 2026 11:44am UTC
2 Skipped Deployments
Project Deployment Actions Updated
hash Ignored Ignored Preview Oct 21, 2026 11:44am UTC
hashdotdesign-tokens Ignored Ignored Preview Oct 21, 2026 11:44am UTC

Request Review

kube added 2 commits October 20, 2026 13:12
A plugin that declares assistant fills the panel below the editor's title bar and fills the title bar with AssistantPanelTitle. The built-in chat stays as it is and shows while no such plugin runs.
Each built-in chat export, the prop and the chat's entry points carry @deprecated with the replacement: a plugin that declares assistant renders its own chat.
The title slot gave pointer events back to every element with a tabindex, so a tab list with tabindex="-1" covered the drag handle. Only buttons, links, form fields and elements that take keyboard focus opt back in.
TypeScript ignores @deprecated on an export-from line, and the declaration bundler drops comments on re-exported names, so imports from @hashintel/petrinaut/ui were never flagged. The barrel now re-exports a tagged alias of the AI SDK class, as a value and a type.
@kube
kube marked this pull request as ready for review October 20, 2026 15:57
@kube
kube requested a review from a team as a code owner October 20, 2026 15:57
@cursor

cursor Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

PR Summary

Medium Risk
Changes how the AI assistant is integrated and deprecates a large public API, but keeps the built-in chat as fallback when no plugin provides an assistant.

Overview
Plugins can own the AI assistant panel instead of relying on the built-in chat behind the aiAssistant prop. Manifest assistant: true and hook contributions { content, onPrompt? } let the first running assistant plugin render chat UI below a shared shell (dock/float, resize, title bar slots) via AssistantPanelTitle and useAssistantPanel().

Editor wiring chooses plugin vs built-in through useEditorAssistant: when a plugin assistant runs, the aiAssistant prop is ignored; the empty-net prompt calls onPrompt when provided. The built-in AiAssistantPanel and the surrounding PetrinautAi* / voice / interactive-tool APIs are marked deprecated, with DefaultChatTransport re-exported from a local shim. Docs and tests cover the new plugin flow, panel focus/data-autofocus, and error boundaries at place: "assistant".

Reviewed by Cursor Bugbot for commit 33b5cc7. Bugbot is set up for automated code reviews on this repo. Configure here.

The actions container had no box of its own, so a button without position: relative painted under the title bar's drag layer and lost every click. The container is now a positioned row above that layer, hidden while empty so it adds no gap.
…ample

The example's Clear button reads useChat's messages and shows only once there are messages: title bar parts render inside content, so they see its state. It uses the one-call form, which the guide's rules give a plugin that keeps no definition.
The content draws Chat and Ledger tabs in the title bar with AssistantPanelTitle and switches its own body, where the built-in chat took an additionalTab.
An assistant's onPrompt receives the message the user sends from the empty-net prompt, after the editor opens the panel, and the prompt shows while it is set. When the panel opens, the content's element marked data-autofocus takes focus, or else the panel. useEditorAssistant also returns the built-in chat, so the view states once which assistant wins.
The main example keeps the chat in the plugin's hook as an AI SDK Chat, so onPrompt can send the empty-net prompt's message to it, and the content reads it with useChat. The tabs example takes the same Chat, the onPrompt paragraph moves above the examples, and the separate onPrompt snippet goes.
@kube kube changed the title Let a plugin provide the AI assistant panel's content and title bar FE-2039: Let a plugin provide the AI assistant panel's content and title bar Oct 21, 2026
@kube
kube added this pull request to the merge queue Oct 21, 2026
Merged via the queue into main with commit c6f6719 Oct 21, 2026
68 of 69 checks passed
@kube
kube deleted the claude/assistant-custom-panel branch October 21, 2026 13:51
@hash-release hash-release Bot mentioned this pull request Oct 21, 2026

This branch was successfully deployed

3 active (1 outdated) deployments
Preview – petrinaut-docs — 33b5cc70 Deployed Oct 21, 2026 by vercel[bot]
Preview – petrinaut — 33b5cc70 Deployed Oct 21, 2026 by vercel[bot]
Preview – hash — 47694176 Deployed Oct 21, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/infra Relates to version control, CI, CD or IaC (area) area/libs Relates to first-party libraries/crates/packages (area) type/eng > backend Owned by the @backend team type/eng > frontend Owned by the @frontend team

Development

Successfully merging this pull request may close these issues.

3 participants