Files
paseo/packages/app/src/provider-selection/provider-selection.ts
T
Mohamed Boudra 1008e9fffc Insert suggested commands into the composer (#8371)
* feat(composer): insert suggested commands into drafts

* fix(composer): close suggestions after command insertion

* fix(composer): place caret after inserted commands
2026-10-31 19:25:16 +07:00

378 lines
12 KiB
TypeScript

import type {
AgentMode,
AgentModelDefinition,
AgentProvider,
ProviderSnapshotEntry,
} from "@getpaseo/protocol/agent-types";
import type { AgentProviderDefinition } from "@getpaseo/protocol/provider-manifest";
import type { DraftCommandTarget } from "@/agent-commands";
import { i18n } from "@/i18n/i18next";
import { compareMatchScores, scoreTextFields } from "@getpaseo/protocol/search/text-match";
import { filterSelectableModels } from "./model-catalog";
export interface ProviderSelectionModelRow {
/**
* Row identity — `provider:modelId`. Used as the React key and as the row's
* stable handle across list rebuilds. The name is historical; it has nothing
* to do with the removed favourites feature.
*/
favoriteKey: string;
provider: string;
providerLabel: string;
modelId: string;
modelLabel: string;
description?: string;
isDefault?: boolean;
}
function buildModelRowKey(provider: string, modelId: string): string {
return `${provider}:${modelId}`;
}
export type ProviderModelSelection =
| { kind: "models"; rows: ProviderSelectionModelRow[] }
| { kind: "loading" }
| { kind: "error"; message: string };
export interface ProviderSelectorProvider {
id: string;
label: string;
modelSelection: ProviderModelSelection;
}
export interface ProviderSelectionState {
provider: AgentProvider | null;
modelId: string;
modeId: string;
thinkingOptionId: string;
availableModels: AgentModelDefinition[];
modeOptions: AgentMode[];
}
export interface ProviderSelectionReadiness {
ok: boolean;
reason?: string;
}
function buildModelRows(
provider: string,
providerLabel: string,
models: AgentModelDefinition[],
): ProviderSelectionModelRow[] {
return models.map((model) => ({
favoriteKey: buildModelRowKey(provider, model.id),
provider,
providerLabel,
modelId: model.id,
modelLabel: model.label,
description: model.description ?? model.id,
isDefault: model.isDefault,
}));
}
function buildSyntheticDefaultRow(
provider: string,
providerLabel: string,
): ProviderSelectionModelRow {
return {
favoriteKey: buildModelRowKey(provider, ""),
provider,
providerLabel,
modelId: "",
modelLabel: i18n.t("providerSelection.defaultModel"),
description: undefined,
isDefault: true,
};
}
function buildModelSelection(
provider: string,
providerLabel: string,
models: AgentModelDefinition[] | null,
): ProviderModelSelection {
if (models === null) {
return { kind: "loading" };
}
const selectableModels = filterSelectableModels(models) ?? [];
if (selectableModels.length === 0) {
return { kind: "models", rows: [buildSyntheticDefaultRow(provider, providerLabel)] };
}
return { kind: "models", rows: buildModelRows(provider, providerLabel, selectableModels) };
}
function buildEntryModelSelection(
entry: ProviderSnapshotEntry,
label: string,
isConnected: boolean,
): ProviderModelSelection {
if ((entry.models?.length ?? 0) > 0) {
return buildModelSelection(entry.provider, label, entry.models ?? null);
}
if (entry.status === "ready") {
return buildModelSelection(entry.provider, label, entry.models ?? null);
}
if (entry.status === "loading") {
return isConnected
? { kind: "loading" }
: { kind: "error", message: i18n.t("providerSelection.readiness.hostDisconnected") };
}
return {
kind: "error",
message:
entry.error ??
(entry.status === "unavailable"
? i18n.t("providerSelection.unavailable")
: i18n.t("providerSelection.unknownError")),
};
}
export function buildProviderSelectorProviders(input: {
providerDefinitions: AgentProviderDefinition[];
modelsByProvider: Map<string, AgentModelDefinition[]>;
}): ProviderSelectorProvider[] {
return input.providerDefinitions.map((definition) => ({
id: definition.id,
label: definition.label,
modelSelection: buildModelSelection(
definition.id,
definition.label,
input.modelsByProvider.has(definition.id)
? (input.modelsByProvider.get(definition.id) ?? [])
: null,
),
}));
}
export function buildSelectableProviderSelectorProviders(
entries: ProviderSnapshotEntry[] | undefined,
isConnected = true,
): ProviderSelectorProvider[] {
return (entries ?? [])
.filter((entry) => entry.enabled)
.map((entry) => {
const label = entry.label ?? entry.provider;
return {
id: entry.provider,
label,
modelSelection: buildEntryModelSelection(entry, label, isConnected),
};
});
}
export function getProviderModelRows(
provider: ProviderSelectorProvider,
): ProviderSelectionModelRow[] {
return provider.modelSelection.kind === "models" ? provider.modelSelection.rows : [];
}
export function getAllProviderModelRows(
providers: ProviderSelectorProvider[],
): ProviderSelectionModelRow[] {
return providers.flatMap(getProviderModelRows);
}
export function resolveSelectedModelLabel(input: {
providers: ProviderSelectorProvider[];
selectedProvider: string;
selectedModel: string;
isLoading: boolean;
}): string {
const selectedProvider = input.selectedProvider.trim();
if (!selectedProvider) {
return input.isLoading
? i18n.t("providerSelection.loading")
: i18n.t("providerSelection.selectModel");
}
const provider = input.providers.find((entry) => entry.id === selectedProvider);
if (!provider) {
return input.isLoading
? i18n.t("providerSelection.loading")
: i18n.t("providerSelection.selectModel");
}
if (provider.modelSelection.kind === "loading") {
return i18n.t("providerSelection.loading");
}
if (provider.modelSelection.kind === "error") {
return i18n.t("providerSelection.error");
}
if (provider.modelSelection.kind !== "models") {
return i18n.t("providerSelection.selectModel");
}
const model = provider.modelSelection.rows.find((entry) => entry.modelId === input.selectedModel);
const selectedModel = input.selectedModel.trim();
if (!model && selectedModel) {
return selectedModel;
}
const defaultModel = provider.modelSelection.rows.find((row) => row.isDefault);
return (
model?.modelLabel ??
defaultModel?.modelLabel ??
provider.modelSelection.rows[0]?.modelLabel ??
i18n.t("providerSelection.selectModel")
);
}
export function buildSelectedTriggerLabel(modelLabel: string): string {
return modelLabel;
}
/**
* Cross-provider result rows need the provider named up front: the same model
* label ships on several providers at once.
*/
export function buildProviderQualifiedDescription(row: ProviderSelectionModelRow): string {
return row.description ? `${row.providerLabel} · ${row.description}` : row.providerLabel;
}
export function matchesModelSearch(
row: ProviderSelectionModelRow,
normalizedQuery: string,
): boolean {
return scoreModelRow(row, normalizedQuery) !== null;
}
function getModelRowSearchFields(row: ProviderSelectionModelRow): string[] {
return [row.modelLabel, row.modelId, row.providerLabel, row.description ?? ""];
}
export function scoreModelRow(row: ProviderSelectionModelRow, normalizedQuery: string) {
return scoreTextFields(normalizedQuery, getModelRowSearchFields(row));
}
export function filterAndRankModelRows(
rows: ProviderSelectionModelRow[],
normalizedQuery: string,
): ProviderSelectionModelRow[] {
if (!normalizedQuery) return rows;
const scored = rows
.map((row) => ({ row, score: scoreModelRow(row, normalizedQuery) }))
.filter(
(
entry,
): entry is { row: ProviderSelectionModelRow; score: NonNullable<typeof entry.score> } =>
Boolean(entry.score),
);
scored.sort((a, b) => {
const cmp = compareMatchScores(a.score, b.score);
if (cmp !== 0) return cmp;
return a.row.modelLabel.localeCompare(b.row.modelLabel);
});
return scored.map((entry) => entry.row);
}
export function resolveEffectiveComposerModelId(selection: ProviderSelectionState): string {
const selectedModelId = selection.modelId.trim();
if (selectedModelId) {
return selectedModelId;
}
return (
selection.availableModels.find((model) => model.isDefault)?.id ??
selection.availableModels[0]?.id ??
""
);
}
// The mode picker shows the first offered mode when the selected one isn't
// offered (e.g. a remembered mode the provider no longer advertises), so the
// submitted mode must match that display. A provider with no modes gets none.
// While the provider is still loading its modes aren't known yet, so the
// selected mode is kept.
export function resolveEffectiveComposerModeId(
selection: ProviderSelectionState,
isModelLoading: boolean,
): string {
const selectedModeId = selection.modeId.trim();
if (isModelLoading) {
return selectedModeId;
}
const modeIds = selection.modeOptions.map((mode) => mode.id);
return modeIds.includes(selectedModeId) ? selectedModeId : (modeIds[0] ?? "");
}
export function resolveEffectiveComposerThinkingOptionId(
selection: ProviderSelectionState,
effectiveModelId: string,
): string {
const selectedThinkingOptionId = selection.thinkingOptionId.trim();
if (selectedThinkingOptionId) {
return selectedThinkingOptionId;
}
const selectedModelDefinition =
selection.availableModels.find((model) => model.id === effectiveModelId) ?? null;
return selectedModelDefinition?.defaultThinkingOptionId ?? "";
}
export function buildDraftCommandTarget(input: {
selection: ProviderSelectionState;
cwd: string;
effectiveModelId: string;
effectiveThinkingOptionId: string;
featureValues?: Record<string, unknown>;
}): DraftCommandTarget {
const cwd = input.cwd.trim();
if (!cwd) {
return { status: "needs-project" };
}
if (!input.selection.provider) {
return { status: "needs-provider" };
}
const config = {
provider: input.selection.provider,
cwd,
...(input.selection.modeOptions.length > 0 && input.selection.modeId !== ""
? { modeId: input.selection.modeId }
: {}),
...(input.effectiveModelId ? { model: input.effectiveModelId } : {}),
...(input.effectiveThinkingOptionId
? { thinkingOptionId: input.effectiveThinkingOptionId }
: {}),
...(input.featureValues ? { featureValues: input.featureValues } : {}),
};
return { status: "ready", config };
}
export function resolveSubmissionReadiness(input: {
text: string;
allowsEmptyAutoSubmit: boolean;
providerCount: number;
selection: {
provider: AgentProvider | string | null;
modelId: string;
availableModels: readonly unknown[];
isModelLoading: boolean;
};
autoSubmitConfig: { provider: string; model: string | null } | null;
workspaceDirectory: string | null;
isConnected: boolean;
}): ProviderSelectionReadiness {
if (!input.allowsEmptyAutoSubmit && !input.text.trim()) {
return { ok: false, reason: i18n.t("providerSelection.readiness.initialPromptRequired") };
}
if (!input.isConnected) {
return { ok: false, reason: i18n.t("providerSelection.readiness.hostDisconnected") };
}
if (input.providerCount === 0) {
return { ok: false, reason: i18n.t("providerSelection.readiness.noProviders") };
}
if (!(input.autoSubmitConfig?.provider ?? input.selection.provider)) {
return { ok: false, reason: i18n.t("providerSelection.selectModel") };
}
if (input.selection.isModelLoading) {
return { ok: false, reason: i18n.t("providerSelection.readiness.modelDefaultsLoading") };
}
const hasSelectedModel = Boolean(input.autoSubmitConfig?.model ?? input.selection.modelId);
if (!hasSelectedModel && input.selection.availableModels.length > 0) {
return { ok: false, reason: i18n.t("providerSelection.readiness.noModelAvailable") };
}
if (!input.workspaceDirectory) {
return { ok: false, reason: i18n.t("providerSelection.readiness.workspaceDirectoryNotFound") };
}
return { ok: true };
}