This page organizes common combinations by user outcome. Read the Desktop UI extension map first and generate a working package with the Desktop plugin quickstart.
Use the composer.toolbar Slot with api.ui.ToolbarToggle or api.ui.Button. Wuu owns
toolbar layout, theming, and accessibility; the plugin owns its state and behavior.
api.registerSlot("composer.toolbar", {
id: "my-action",
render() {
return api.react.createElement(
api.ui.Button,
{ onClick: () => console.log("clicked") },
"Run",
);
},
});If the control must read or change the draft, use a conversation.composer Presenter
instead of querying the textarea.
This feature combines three pieces:
- the standard Selection API reads text and screen coordinates;
- the public
data-wuu-component="message"anchor confirms the selection is in chat; - a Composer Presenter invokes the host
set-draftaction.
It does not replace message rendering or mutate private React state:
import type {
ComposerSnapshotV1,
PluginGenerationApi,
PresenterProps,
} from "@wuu/plugin-sdk";
const SET_DRAFT = "conversation.composer.set-draft";
export function activate(api: PluginGenerationApi): void {
const React = api.react;
function SelectionToolbar(input: Readonly<Record<string, unknown>>) {
const props = input.presenter as PresenterProps;
const snapshot = props.snapshot as ComposerSnapshotV1;
const [selection, setSelection] = React.useState<null | {
text: string;
left: number;
top: number;
}>(null);
React.useEffect(() => {
const update = () => {
const current = window.getSelection();
if (!current || current.isCollapsed || current.rangeCount === 0) {
setSelection(null);
return;
}
const node = current.anchorNode;
const element = node instanceof Element ? node : node?.parentElement;
const text = current.toString().trim();
if (!text || !element?.closest('[data-wuu-component="message"]')) {
setSelection(null);
return;
}
const rect = current.getRangeAt(0).getBoundingClientRect();
setSelection({ text, left: rect.left, top: rect.top - 40 });
};
document.addEventListener("selectionchange", update);
return () => document.removeEventListener("selectionchange", update);
}, []);
const append = async () => {
if (!selection || !props.host.actions.includes(SET_DRAFT)) return;
const current = snapshot.draftText?.trimEnd() ?? "";
const quote = `> ${selection.text.replaceAll("\n", "\n> ")}`;
await props.host.invoke(SET_DRAFT, current ? `${current}\n\n${quote}\n\n` : `${quote}\n\n`);
setSelection(null);
};
return React.createElement(
"div",
{ style: { display: "contents" } },
props.fallback,
selection && React.createElement(
"div",
{
style: {
position: "fixed",
left: selection.left,
top: selection.top,
zIndex: 1000,
},
onMouseDown: (event: { preventDefault(): void }) => event.preventDefault(),
},
React.createElement(api.ui.Button, { onClick: append }, "Add to Composer"),
),
);
}
api.registerPresenter({
id: "selection-toolbar",
target: "conversation.composer",
mode: "wrap",
render: (props) => React.createElement(SelectionToolbar, { presenter: props }),
});
}onMouseDown.preventDefault() keeps the browser from clearing the selection before
the click. The plugin stores plain text and coordinates, not host nodes or React
objects. A production implementation should also clamp to the viewport and handle
scrolling, long selections, and localization.
Translation, summary, and explanation buttons can reuse the selection and place
different prompt templates in the draft. Let the user review and add their query by
default. For intentional automatic submission, verify that submit appears in
host.actions before invoking conversation.composer.submit.
Use a View for lists, editors, charts, and management UI:
api.registerViewType({
id: "my-plugin.dashboard",
title: "Dashboard",
persistence: "durable",
render: Dashboard,
});
api.registerViewPlacement({
id: "dashboard-default",
view: "my-plugin.dashboard",
region: "auxiliary",
});Declare a workspaceTools, navigation, or settingsPages entry in plugin.json
that points to the registered View. Wuu owns opening, closing, tabs, and persistence.
- Add content around a message with
conversation.message.before/afterSlots. - Wrap one item with a
conversation.itemPresenter. - Wrap the whole message boundary with the
conversation.messageSurface. - Present tool cards by stable capability with a
conversation.tool-activityPresenter.
Read public snapshots and retain a useful fallback. Do not parse private ThreadItems, guess tool-internal state, or depend on host class names.
For colors, radii, or syntax highlighting, declare a theme in the manifest instead of loading Desktop code:
{
"contributes": {
"themes": [
{
"id": "calm-night",
"name": "Calm Night",
"base": "dark",
"tokens": {
"--wuu-color-canvas": "#151820",
"--wuu-color-accent": "#8fa7ff"
}
}
]
}
}Run wuu plugin validate ., then add the local directory from the Desktop plugin
catalog and choose Approve and enable.
The theme appears under Settings → Appearance; selecting a built-in theme removes
its overrides. See the theme token reference for available
tokens and plugin themes and settings for user actions.
A Desktop module may register a Command or call showConversationCard from a host
event or asynchronous task. Use a Card for short-lived interaction; use a View for
persistent, navigable, or complex UI.
See the plugin authoring reference for settings, Storage, Runtime communication, and packaging rules.