mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-02 22:52:58 +00:00
fix(plugins): composer option resets and model checks match the documented commands
A draft kept `null` as an option value, so `setOptions({ key: null })` sent the null to the
provider and creation failed. A null edit now removes the key from every draft layer, as the patch
does on a running agent. A running agent's `setModel` accepted ids its catalog does not list; it
now rejects them the way a draft does.
The composer controls imported the toast host through the toast context, which pulled the host's
animation module into every module that loads plugins. They use the API-only context instead.
The migration guide's registration table gets the `addComposerItem` row its test requires.
This commit is contained in:
@@ -14,6 +14,9 @@ import type {
|
||||
* - `remembered`: the provider's last-used control values.
|
||||
* - `edits`: toggles made in this draft.
|
||||
*
|
||||
* An edit of `null` removes the key from every layer, the way a patch does on a running agent, so
|
||||
* the provider's default applies again.
|
||||
*
|
||||
* Choosing a provider by hand starts the options over; applying a profile replaces the provider
|
||||
* and the options in one step, so the provider change that follows does not clear them.
|
||||
*/
|
||||
@@ -119,10 +122,14 @@ export function ownDraftOptions(state: DraftOptionsState): ProviderOptions | und
|
||||
([key]) => controlKeys.has(key) && !(key in state.seed),
|
||||
),
|
||||
);
|
||||
const own = { ...state.seed, ...remembered, ...state.edits };
|
||||
const own = withoutRemovals({ ...state.seed, ...remembered, ...state.edits });
|
||||
return Object.keys(own).length > 0 ? own : undefined;
|
||||
}
|
||||
|
||||
function withoutRemovals(options: ProviderOptions): ProviderOptions {
|
||||
return Object.fromEntries(Object.entries(options).filter(([, value]) => value !== null));
|
||||
}
|
||||
|
||||
export type DraftControlsRequest = Pick<
|
||||
AgentSessionConfig,
|
||||
"provider" | "cwd" | "modeId" | "model" | "thinkingOptionId" | "providerOptions"
|
||||
@@ -173,7 +180,7 @@ export function viewDraftOptions(
|
||||
options:
|
||||
resolved.requestKey === current.requestKey
|
||||
? resolved.options
|
||||
: { ...resolved.options, ...state.edits },
|
||||
: withoutRemovals({ ...resolved.options, ...state.edits }),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import type { SheetHeader } from "@/components/adaptive-modal-sheet";
|
||||
import { Combobox, type ComboboxOption } from "@/components/ui/combobox";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { AgentControlTrigger } from "@/composer/agent-controls/control";
|
||||
import { useToast } from "@/contexts/toast-context";
|
||||
import { useToast } from "@/contexts/toast-api-context";
|
||||
import { toErrorMessage } from "@/utils/error-messages";
|
||||
|
||||
interface ComposerControlBase {
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
type ProviderSelectorProvider,
|
||||
} from "@/provider-selection/provider-selection";
|
||||
import { filterSelectableModels } from "@/provider-selection/model-catalog";
|
||||
import { isUnlistedModel } from "@/provider-selection/resolve-agent-form";
|
||||
import { useSessionStore } from "@/stores/session-store";
|
||||
import { useProvidersSnapshot } from "@/hooks/use-providers-snapshot";
|
||||
import { resolveProviderDefinition } from "@/utils/provider-definitions";
|
||||
@@ -1146,6 +1147,7 @@ export const AgentControls = memo(function AgentControls({
|
||||
metadata: agent.metadata ?? EMPTY_METADATA,
|
||||
setProvider: () => Promise.reject(new Error("A running agent cannot change provider.")),
|
||||
setModel: async (modelId) => {
|
||||
if (isUnlistedModel(models, modelId)) throw new Error(`Unknown model: ${modelId}`);
|
||||
await client.setAgentModel(agentId, modelId);
|
||||
},
|
||||
setMode: async (modeId) => {
|
||||
@@ -1168,6 +1170,7 @@ export const AgentControls = memo(function AgentControls({
|
||||
client,
|
||||
modeControl?.selectedModeId,
|
||||
modelSelection.selectedThinkingId,
|
||||
models,
|
||||
]);
|
||||
|
||||
const commandCenterControls = useMemo<AgentControlCommandCenterSource>(
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
useComposerAgent,
|
||||
useComposerInput,
|
||||
} from "@/composer/session";
|
||||
import { ToastApiProvider, useToast } from "@/contexts/toast-context";
|
||||
import { ToastApiProvider, useToast } from "@/contexts/toast-api-context";
|
||||
|
||||
/**
|
||||
* A phone's overflow draws in a sheet whose content lands at the app root, away from the
|
||||
|
||||
@@ -139,6 +139,22 @@ describe.each(["draft", "new-workspace"] as const)("the %s composer's agent", (c
|
||||
expect(draft.agent().options).toEqual({});
|
||||
});
|
||||
|
||||
it("creates with the provider's default again once a plugin resets an option", async () => {
|
||||
const draft = openDraft(context);
|
||||
const retained = draft.agent();
|
||||
|
||||
await retained.setOptions({ context: "1m", effort: "high" });
|
||||
await retained.setOptions({ context: null });
|
||||
|
||||
expect(draft.agent().options).toEqual({ effort: "high" });
|
||||
expect(draft.request()?.providerOptions).toEqual({ effort: "high" });
|
||||
|
||||
await retained.setOptions({ effort: null });
|
||||
|
||||
expect(draft.agent().options).toEqual({});
|
||||
expect(draft.request()?.providerOptions).toBeUndefined();
|
||||
});
|
||||
|
||||
it("rejects a provider the host cannot run and a model the catalog does not list", async () => {
|
||||
const draft = openDraft(context);
|
||||
const retained = draft.agent();
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} from "@/composer/agent-controls/composer-control";
|
||||
import { useComposerAgent, useComposerInput } from "@/composer/session";
|
||||
import { useIsCompactFormFactor } from "@/constants/layout";
|
||||
import { useToast } from "@/contexts/toast-context";
|
||||
import { useToast } from "@/contexts/toast-api-context";
|
||||
import { useHosts } from "@/runtime/host-runtime";
|
||||
import type { Theme } from "@/styles/theme";
|
||||
import { createPluginClientStateSource } from "../client-state/source";
|
||||
|
||||
@@ -4,9 +4,8 @@ import type { FormPreferences } from "@/hooks/use-form-preferences";
|
||||
import {
|
||||
INITIAL_AGENT_FORM_RESOLUTION,
|
||||
INITIAL_USER_MODIFIED,
|
||||
normalizeSelectedModelId,
|
||||
isUnlistedModel,
|
||||
resolveAgentForm,
|
||||
resolveEffectiveModel,
|
||||
type AgentFormAction,
|
||||
type AgentFormReducerState,
|
||||
} from "@/provider-selection/resolve-agent-form";
|
||||
@@ -75,9 +74,7 @@ export function openAgentForm(catalog: () => AgentFormCatalog): AgentFormStore {
|
||||
const host = catalog();
|
||||
const listed = provider ? host.models.get(provider) : undefined;
|
||||
const models = listed?.length ? listed : null;
|
||||
if (models && normalizeSelectedModelId(modelId) && !resolveEffectiveModel(models, modelId)) {
|
||||
return false;
|
||||
}
|
||||
if (isUnlistedModel(models, modelId)) return false;
|
||||
dispatch({
|
||||
type: "SET_MODEL_FROM_USER",
|
||||
modelId,
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
resolveAgentForm,
|
||||
resolveFormState,
|
||||
resolveEffectiveModel,
|
||||
isUnlistedModel,
|
||||
resolveThinkingOptionId,
|
||||
mergeSelectedComposerPreferences,
|
||||
buildProviderDefinitionMap,
|
||||
@@ -131,6 +132,19 @@ describe("resolveDefaultModel", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("isUnlistedModel", () => {
|
||||
it("rejects an id the provider's catalog lacks, and accepts a listed id or alias", () => {
|
||||
expect(isUnlistedModel(ALIASED_CODEX_MODELS, "not-in-the-catalog")).toBe(true);
|
||||
expect(isUnlistedModel(ALIASED_CODEX_MODELS, "gpt-5.3-codex")).toBe(false);
|
||||
expect(isUnlistedModel(ALIASED_CODEX_MODELS, "gpt-5.3-codex-legacy")).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts any id while the provider has listed no models", () => {
|
||||
expect(isUnlistedModel(null, "anything")).toBe(false);
|
||||
expect(isUnlistedModel([], "anything")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("model aliases", () => {
|
||||
it("canonicalizes a retired preferred model and restores thinking from its alias key", () => {
|
||||
const resolved = resolveFormState(
|
||||
|
||||
@@ -153,6 +153,19 @@ export function resolveEffectiveModel(
|
||||
return findModelByReference(availableModels, modelId) ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* True when the provider has listed its models and this id is not among them. A provider that
|
||||
* lists none accepts any id.
|
||||
*/
|
||||
export function isUnlistedModel(
|
||||
availableModels: AgentModelDefinition[] | null,
|
||||
modelId: string,
|
||||
): boolean {
|
||||
if (!availableModels || availableModels.length === 0) return false;
|
||||
if (!normalizeSelectedModelId(modelId)) return false;
|
||||
return !resolveEffectiveModel(availableModels, modelId);
|
||||
}
|
||||
|
||||
function resolvePreferredThinkingOptionId(input: {
|
||||
availableModels: AgentModelDefinition[] | null;
|
||||
providerPrefs: ProviderPrefs | undefined;
|
||||
|
||||
@@ -80,6 +80,7 @@ Use this table as the complete registration checklist.
|
||||
| `plugin.addClientSlashCommand(command)` in the old root entry | `client.addSlashCommand(command)` in `index.client.tsx` |
|
||||
| `plugin.addClientSide(fn)` in the old root entry | Delete the wrapper and move the body of `fn` into the default client entry function |
|
||||
| `client.addComposerPill(pill)` inside the old client callback | `client.addComposerPill(pill)` inside `index.client.tsx` or an imported `client/` function |
|
||||
| New composer item contribution | `client.addComposerItem(item)` in `index.client.tsx`; see [composer items](reference#composer-items) |
|
||||
| New header contribution | `client.addHeaderButton({ id, workspaceId, button })` |
|
||||
| `plugin.addAttachmentSource(source)` in the old root entry | `client.addAttachmentSource(source)` in `index.client.tsx` |
|
||||
| New settings screen contribution | `client.addSettingsScreen(screen)` in `index.client.tsx`; see [settings screens](reference#settings-screens) |
|
||||
|
||||
@@ -73,6 +73,7 @@ my-plugin/
|
||||
| 旧根入口中的 `plugin.addClientSlashCommand(command)` | `index.client.tsx` 中的 `client.addSlashCommand(command)` |
|
||||
| 旧根入口中的 `plugin.addClientSide(fn)` | 删除这层包装,把 `fn` 的函数体移入默认的客户端入口函数 |
|
||||
| 旧客户端回调中的 `client.addComposerPill(pill)` | `index.client.tsx` 或被导入的 `client/` 函数中的 `client.addComposerPill(pill)` |
|
||||
| 新的编辑器项贡献 | `index.client.tsx` 中的 `client.addComposerItem(item)`;见[编辑器项](reference#composer-items) |
|
||||
| 新的标题栏贡献 | `client.addHeaderButton({ id, workspaceId, button })` |
|
||||
| 旧根入口中的 `plugin.addAttachmentSource(source)` | `index.client.tsx` 中的 `client.addAttachmentSource(source)` |
|
||||
| 新的设置界面贡献 | `index.client.tsx` 中的 `client.addSettingsScreen(screen)`;见[设置界面](reference#settings-screens) |
|
||||
|
||||
Reference in New Issue
Block a user