Files
paseo/plugin-examples/composer-controls/client/controls.tsx
T
Mohamed Boudra b10647cae3 feat(plugins): let a plugin put the cursor in any agent's composer
A pill could insert text but left the cursor elsewhere, so the user had
to click the composer before typing. Plugins can address any agent's
composer, and an author cannot know where the user is, so focus() owns
revealing the agent as well as focusing it.

A running agent's composer holds one pending request and navigates to
the agent, completing when an input that can take the keyboard attaches.
Drafts and the New workspace composer have nothing to navigate to and
must not pull the user back, so they reject when off screen.
2026-11-09 15:39:46 +01:00

81 lines
2.4 KiB
TypeScript

import { useCallback } from "react";
import { useComposer } from "@getpaseo/plugin/client";
import { Action, Select, Toggle } from "@getpaseo/plugin/client/ui";
type ContextLength = "short" | "long";
const CONTEXT_OPTIONS = [
{ value: "short", label: "Short", description: "Answer from the files you name." },
{ value: "long", label: "Long", description: "Read the surrounding code first." },
] as const;
const CONTEXT_KEY = "composer-controls.contextLength";
export const REVIEW_KEY = "composer-controls.needsReview";
/** Reads and writes agent metadata, so the choice is saved with the agent. */
export function ContextLength() {
const composer = useComposer();
const current: ContextLength = composer.agent.metadata[CONTEXT_KEY] === "long" ? "long" : "short";
const change = useCallback(
({ value }: { value: ContextLength }) => composer.setMetadata({ [CONTEXT_KEY]: value }),
[composer],
);
return (
<Select
title="Context length"
icon="Gauge"
value={current}
options={CONTEXT_OPTIONS}
onValueChange={change}
/>
);
}
export function ReviewFlag() {
const composer = useComposer();
const change = useCallback(
({ value }: { value: boolean }) => composer.setMetadata({ [REVIEW_KEY]: value ? true : null }),
[composer],
);
return (
<Toggle
title="Needs review"
icon="Flag"
value={composer.agent.metadata[REVIEW_KEY] === true}
onValueChange={change}
/>
);
}
/**
* Changes a provider option. Only Codex reports `plan_mode`, so the control is disabled for
* other providers. The promise it returns keeps the control pending, and a rejection reaches the
* user as a toast.
*/
export function PlanFirst() {
const composer = useComposer();
const change = useCallback(
({ value }: { value: boolean }) => composer.setOptions({ plan_mode: value }),
[composer],
);
return (
<Toggle
title="Plan first"
icon="ListTodo"
disabled={composer.agent.provider !== "codex"}
value={composer.agent.options.plan_mode === true}
onValueChange={change}
/>
);
}
/** Works the same in the agent, draft and New workspace composers. */
export function Checklist() {
const composer = useComposer();
const insert = useCallback(async () => {
composer.insertText("- [ ] Tests\n- [ ] Docs\n");
await composer.focus();
}, [composer]);
return <Action title="Insert checklist" icon="ListChecks" onPress={insert} />;
}