fix(web): harden agent runner marketplace flows

This commit is contained in:
Hyu
2026-09-01 12:46:16 +08:00
parent e5e62c8fe9
commit 982d660236
9 changed files with 426 additions and 30 deletions
@@ -1,5 +1,6 @@
import { httpClient } from '@/app/infra/http/HttpClient';
import { getCloudServiceClient } from '@/app/infra/http';
import { getActiveWorkspaceUuid } from '@/app/infra/http/workspaceContext';
import type { IDynamicFormItemOption } from '@/app/infra/entities/form/dynamic';
import type { PipelineConfigTab } from '@/app/infra/entities/pipeline';
import type { PluginV4 } from '@/app/infra/entities/plugin';
@@ -9,6 +10,8 @@ export const RUNNER_COMPONENT_FILTER = 'AgentRunner';
const RUNNER_CATALOG_PAGE_SIZE = 100;
const RUNNER_INSTALL_TIMEOUT_MS = 120_000;
const RUNNER_REGISTRATION_TIMEOUT_MS = 60_000;
const RUNNER_INSTALL_INTENT_KEY_PREFIX = 'langbot-agent-runner-install';
const RUNNER_INSTALL_INTENT_EVENT = 'langbot-agent-runner-install-change';
export type AgentRunnerMarketplaceErrorCode =
| 'version-unavailable'
@@ -32,6 +35,21 @@ export interface InstalledAgentRunner {
runner: IDynamicFormItemOption;
}
export interface PendingAgentRunnerInstall {
taskId: number;
pluginId: string;
pluginAuthor: string;
pluginName: string;
pluginLabel: string;
scope: string;
startedAt: number;
}
interface InstallAgentRunnerOptions {
scope: string;
onTaskCreated?: (taskId: number) => void;
}
function wait(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
@@ -52,6 +70,74 @@ export function runnerPluginPrefix(plugin: Pick<PluginV4, 'author' | 'name'>) {
return `plugin:${plugin.author}/${plugin.name}/`;
}
function installIntentStorageKey(scope: string) {
return `${RUNNER_INSTALL_INTENT_KEY_PREFIX}:${getActiveWorkspaceUuid() || 'default'}:${scope}`;
}
function emitInstallIntentChange(scope: string) {
if (typeof window === 'undefined') return;
window.dispatchEvent(
new CustomEvent(RUNNER_INSTALL_INTENT_EVENT, { detail: { scope } }),
);
}
export function readPendingAgentRunnerInstall(
scope: string,
): PendingAgentRunnerInstall | null {
if (typeof window === 'undefined') return null;
try {
const raw = sessionStorage.getItem(installIntentStorageKey(scope));
if (!raw) return null;
const value = JSON.parse(raw) as Partial<PendingAgentRunnerInstall>;
if (
value.scope !== scope ||
typeof value.taskId !== 'number' ||
typeof value.pluginId !== 'string' ||
typeof value.pluginAuthor !== 'string' ||
typeof value.pluginName !== 'string' ||
typeof value.pluginLabel !== 'string' ||
typeof value.startedAt !== 'number'
) {
sessionStorage.removeItem(installIntentStorageKey(scope));
return null;
}
return value as PendingAgentRunnerInstall;
} catch {
return null;
}
}
function writePendingAgentRunnerInstall(intent: PendingAgentRunnerInstall) {
if (typeof window === 'undefined') return;
sessionStorage.setItem(
installIntentStorageKey(intent.scope),
JSON.stringify(intent),
);
emitInstallIntentChange(intent.scope);
}
export function clearPendingAgentRunnerInstall(scope: string, taskId?: number) {
if (typeof window === 'undefined') return;
const current = readPendingAgentRunnerInstall(scope);
if (taskId !== undefined && current?.taskId !== taskId) return;
sessionStorage.removeItem(installIntentStorageKey(scope));
emitInstallIntentChange(scope);
}
export function subscribePendingAgentRunnerInstall(
scope: string,
listener: () => void,
) {
if (typeof window === 'undefined') return () => undefined;
const handleChange = (event: Event) => {
const detail = (event as CustomEvent<{ scope?: string }>).detail;
if (detail?.scope === scope) listener();
};
window.addEventListener(RUNNER_INSTALL_INTENT_EVENT, handleChange);
return () =>
window.removeEventListener(RUNNER_INSTALL_INTENT_EVENT, handleChange);
}
export async function loadAgentRunnerCatalog(): Promise<AgentRunnerCatalog> {
const cloudClient = await getCloudServiceClient();
const [firstSearchResult, recommendationResult, installedResult] =
@@ -124,6 +210,7 @@ export async function loadAgentRunnerCatalog(): Promise<AgentRunnerCatalog> {
export async function installMarketplaceAgentRunner(
plugin: PluginV4,
options: InstallAgentRunnerOptions,
): Promise<InstalledAgentRunner> {
if (!plugin.latest_version) {
throw new AgentRunnerMarketplaceError('version-unavailable');
@@ -134,17 +221,51 @@ export async function installMarketplaceAgentRunner(
plugin.name,
plugin.latest_version,
);
const pending: PendingAgentRunnerInstall = {
taskId,
pluginId: marketplacePluginId(plugin),
pluginAuthor: plugin.author,
pluginName: plugin.name,
pluginLabel: extractPluginLabel(plugin),
scope: options.scope,
startedAt: Date.now(),
};
writePendingAgentRunnerInstall(pending);
options.onTaskCreated?.(taskId);
return finishAgentRunnerInstall(pending);
}
function extractPluginLabel(plugin: PluginV4) {
const label = plugin.label;
if (typeof label === 'string') return label || plugin.name;
if (label && typeof label === 'object') {
const localized = Object.values(label).find(
(value): value is string => typeof value === 'string' && value.length > 0,
);
if (localized) return localized;
}
return plugin.name;
}
async function finishAgentRunnerInstall(
pending: PendingAgentRunnerInstall,
): Promise<InstalledAgentRunner> {
// A refreshed page receives a fresh observation window. The backend task is
// authoritative; `startedAt` is display metadata, not a reason to abandon a
// still-running installation immediately after recovery.
const installDeadline = Date.now() + RUNNER_INSTALL_TIMEOUT_MS;
let installCompleted = false;
while (Date.now() < installDeadline) {
const task = await httpClient.getAsyncTask(taskId);
while (true) {
const task = await httpClient.getAsyncTask(pending.taskId);
if (task.runtime.done) {
if (task.runtime.exception) {
clearPendingAgentRunnerInstall(pending.scope, pending.taskId);
throw new Error(task.runtime.exception);
}
installCompleted = true;
break;
}
if (Date.now() >= installDeadline) break;
await wait(1000);
}
if (!installCompleted) {
@@ -152,7 +273,10 @@ export async function installMarketplaceAgentRunner(
}
const registrationDeadline = Date.now() + RUNNER_REGISTRATION_TIMEOUT_MS;
const prefix = runnerPluginPrefix(plugin);
const prefix = runnerPluginPrefix({
author: pending.pluginAuthor,
name: pending.pluginName,
});
while (Date.now() < registrationDeadline) {
const metadata = await httpClient.getGeneralPipelineMetadata();
const configTab = metadata.configs.find((config) => config.name === 'ai');
@@ -169,10 +293,20 @@ export async function installMarketplaceAgentRunner(
pluginRunnerOptions[0];
if (configTab && runner) {
clearPendingAgentRunnerInstall(pending.scope, pending.taskId);
return { configTab, runner };
}
await wait(1000);
}
clearPendingAgentRunnerInstall(pending.scope, pending.taskId);
throw new AgentRunnerMarketplaceError('registration-timeout');
}
export async function resumePendingAgentRunnerInstall(
scope: string,
): Promise<InstalledAgentRunner | null> {
const pending = readPendingAgentRunnerInstall(scope);
if (!pending) return null;
return finishAgentRunnerInstall(pending);
}
@@ -13,7 +13,7 @@ import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { useTranslation } from 'react-i18next';
import { toast } from 'sonner';
import { Bot, SlidersHorizontal, Zap } from 'lucide-react';
import { Bot, Loader2, SlidersHorizontal, Zap } from 'lucide-react';
import { httpClient } from '@/app/infra/http/HttpClient';
import { Agent, ApiRespPluginSystemStatus } from '@/app/infra/entities/api';
import {
@@ -21,6 +21,13 @@ import {
PipelineConfigTab,
} from '@/app/infra/entities/pipeline';
import DynamicFormComponent from '@/app/home/components/dynamic-form/DynamicFormComponent';
import { getDefaultValues } from '@/app/home/components/dynamic-form/DynamicFormItemConfig';
import {
getErrorMessage,
readPendingAgentRunnerInstall,
resumePendingAgentRunnerInstall,
type InstalledAgentRunner,
} from '@/app/home/agents/agent-runner-marketplace';
import { extractI18nObject } from '@/i18n/I18nProvider';
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
import {
@@ -117,6 +124,8 @@ function AgentFormComponent(
useState<ApiRespPluginSystemStatus | null>(null);
const [pluginStatusLoading, setPluginStatusLoading] = useState(true);
const [pluginStatusError, setPluginStatusError] = useState(false);
const [initialDataLoaded, setInitialDataLoaded] = useState(false);
const [runnerInstallRecovering, setRunnerInstallRecovering] = useState(false);
const [activeSection, setActiveSection] =
useState<AgentConfigSection>('runner');
const isSavingRef = useRef(false);
@@ -147,6 +156,35 @@ function AgentFormComponent(
supported_event_patterns: ['*'],
},
});
const runnerInstallScope = `agent:${agentId}`;
const applyInstalledRunner = useCallback(
(installed: InstalledAgentRunner) => {
setRunnerConfigSchema(installed.configTab);
const currentRunner = form.getValues('runner') || {};
const currentConfigs = form.getValues('runner_config') || {};
const runnerName = installed.runner.name;
const runnerStage = installed.configTab.stages.find(
(stage) => stage.name === runnerName,
);
form.setValue(
'runner',
{ ...currentRunner, id: runnerName },
{ shouldDirty: true },
);
if (!(runnerName in currentConfigs) && runnerStage) {
form.setValue(
'runner_config',
{
...currentConfigs,
[runnerName]: getDefaultValues(runnerStage.config),
},
{ shouldDirty: true },
);
}
},
[form],
);
const savedSnapshotRef = useRef('');
const initializedStagesRef = useRef<Set<string>>(new Set());
@@ -189,6 +227,7 @@ function AgentFormComponent(
form.reset(loadedValues);
savedSnapshotRef.current = JSON.stringify(loadedValues);
initializedStagesRef.current.clear();
setInitialDataLoaded(true);
})
.catch((err) => {
toast.error(t('agents.loadError') + err.msg);
@@ -198,6 +237,40 @@ function AgentFormComponent(
};
}, [agentId, form, t]);
useEffect(() => {
if (
!initialDataLoaded ||
!readPendingAgentRunnerInstall(runnerInstallScope)
) {
return;
}
let cancelled = false;
setRunnerInstallRecovering(true);
void resumePendingAgentRunnerInstall(runnerInstallScope)
.then((installed) => {
if (cancelled || !installed) return;
applyInstalledRunner(installed);
toast.success(
t('wizard.aiEngine.installSuccess', {
runner: extractI18nObject(installed.runner.label),
}),
);
})
.catch((error) => {
if (!cancelled) {
toast.error(
getErrorMessage(error) || t('wizard.aiEngine.installFailed'),
);
}
})
.finally(() => {
if (!cancelled) setRunnerInstallRecovering(false);
});
return () => {
cancelled = true;
};
}, [applyInstalledRunner, initialDataLoaded, runnerInstallScope, t]);
const loadPluginSystemStatus = useCallback(async () => {
setPluginStatusLoading(true);
setPluginStatusError(false);
@@ -425,7 +498,8 @@ function AgentFormComponent(
label={extractI18nObject(config.label)}
value={String(field.value ?? '')}
onValueChange={field.onChange}
onMetadataRefresh={setRunnerConfigSchema}
installScope={runnerInstallScope}
onInstalled={applyInstalledRunner}
/>
) : undefined
: undefined
@@ -576,7 +650,17 @@ function AgentFormComponent(
{activeSection === 'runner_config' && (
<div className="space-y-6">
{activeRunnerStage ? (
{runnerInstallRecovering ? (
<Card>
<CardHeader>
<CardTitle>{t('agents.runnerSettings')}</CardTitle>
<CardDescription className="flex items-center gap-2">
<Loader2 className="size-4 animate-spin" />
{t('agents.restoringRunnerInstall')}
</CardDescription>
</CardHeader>
</Card>
) : activeRunnerStage ? (
renderDynamicStage(activeRunnerStage)
) : (
<Card>
@@ -1,11 +1,10 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { Bot, Download, Loader2, Store } from 'lucide-react';
import { Bot, Download, ExternalLink, Loader2, Store } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { toast } from 'sonner';
import { getCloudServiceClientSync, httpClient } from '@/app/infra/http';
import type { IDynamicFormItemOption } from '@/app/infra/entities/form/dynamic';
import type { PipelineConfigTab } from '@/app/infra/entities/pipeline';
import type { PluginV4 } from '@/app/infra/entities/plugin';
import {
AgentRunnerMarketplaceError,
@@ -14,7 +13,11 @@ import {
loadAgentRunnerCatalog,
marketplacePluginId,
runnerPluginPrefix,
readPendingAgentRunnerInstall,
subscribePendingAgentRunnerInstall,
type InstalledAgentRunner,
} from '@/app/home/agents/agent-runner-marketplace';
import { usePluginInstallTasks } from '@/app/home/plugins/components/plugin-install-task';
import { extractI18nObject } from '@/i18n/I18nProvider';
import {
Select,
@@ -109,21 +112,24 @@ export default function AgentRunnerSelect({
label,
value,
onValueChange,
onMetadataRefresh,
installScope,
onInstalled,
}: {
options: IDynamicFormItemOption[];
label: string;
value: string;
onValueChange: (value: string) => void;
onMetadataRefresh: (configTab: PipelineConfigTab) => void;
installScope: string;
onInstalled: (installed: InstalledAgentRunner) => void;
}) {
const { t } = useTranslation();
const { addTask } = usePluginInstallTasks();
const [marketplaceRunners, setMarketplaceRunners] = useState<PluginV4[]>([]);
const [installedPluginIds, setInstalledPluginIds] = useState<string[]>([]);
const [catalogLoading, setCatalogLoading] = useState(true);
const [catalogError, setCatalogError] = useState(false);
const [installingPlugin, setInstallingPlugin] = useState<PluginV4 | null>(
null,
const [pendingInstall, setPendingInstall] = useState(() =>
readPendingAgentRunnerInstall(installScope),
);
const [installError, setInstallError] = useState<string | null>(null);
@@ -146,6 +152,13 @@ export default function AgentRunnerSelect({
void loadCatalog();
}, [loadCatalog]);
useEffect(() => {
const syncPendingInstall = () =>
setPendingInstall(readPendingAgentRunnerInstall(installScope));
syncPendingInstall();
return subscribePendingAgentRunnerInstall(installScope, syncPendingInstall);
}, [installScope]);
const marketplaceOptions = useMemo(
() =>
marketplaceRunners.filter((plugin) => {
@@ -159,6 +172,11 @@ export default function AgentRunnerSelect({
);
const selectedOption = options.find((option) => option.name === value);
const installingPlugin = pendingInstall
? (marketplaceRunners.find(
(plugin) => marketplacePluginId(plugin) === pendingInstall.pluginId,
) ?? null)
: null;
const handleValueChange = useCallback(
async (nextValue: string) => {
@@ -172,14 +190,21 @@ export default function AgentRunnerSelect({
const plugin = marketplaceRunners.find(
(candidate) => marketplacePluginId(candidate) === pluginId,
);
if (!plugin || installingPlugin) return;
if (!plugin || pendingInstall) return;
setInstallingPlugin(plugin);
setInstallError(null);
try {
const installed = await installMarketplaceAgentRunner(plugin);
onMetadataRefresh(installed.configTab);
onValueChange(installed.runner.name);
const installed = await installMarketplaceAgentRunner(plugin, {
scope: installScope,
onTaskCreated: (taskId) =>
addTask({
taskId,
pluginName: marketplacePluginId(plugin),
source: 'marketplace',
extensionType: 'plugin',
}),
});
onInstalled(installed);
await loadCatalog();
toast.success(
t('wizard.aiEngine.installSuccess', {
@@ -191,15 +216,17 @@ export default function AgentRunnerSelect({
setInstallError(message);
toast.error(message);
} finally {
setInstallingPlugin(null);
setPendingInstall(readPendingAgentRunnerInstall(installScope));
}
},
[
installingPlugin,
addTask,
installScope,
loadCatalog,
marketplaceRunners,
onMetadataRefresh,
onInstalled,
onValueChange,
pendingInstall,
t,
],
);
@@ -208,7 +235,7 @@ export default function AgentRunnerSelect({
<div className="w-full max-w-[22rem] space-y-2">
<Select
value={value}
disabled={installingPlugin !== null}
disabled={pendingInstall !== null}
onValueChange={(nextValue) => void handleValueChange(nextValue)}
onOpenChange={(open) => {
if (open && catalogError && !catalogLoading) void loadCatalog();
@@ -264,11 +291,22 @@ export default function AgentRunnerSelect({
<SelectSeparator />
<SelectGroup>
<SelectLabel className="px-2 py-1 text-[11px] font-medium">
<span className="inline-flex items-center gap-1.5">
<SelectLabel className="flex items-center justify-between gap-2 px-2 py-1 text-[11px] font-medium">
<span className="inline-flex min-w-0 items-center gap-1.5">
<Store className="size-3.5" />
{t('agents.marketplaceRunners')}
</span>
<a
href="https://space.langbot.app/market?type=plugin&component=AgentRunner"
target="_blank"
rel="noreferrer"
className="inline-flex shrink-0 items-center gap-1 rounded border px-1.5 py-0.5 text-[10px] font-medium text-foreground hover:bg-accent"
onPointerDown={(event) => event.stopPropagation()}
onClick={(event) => event.stopPropagation()}
>
{t('agents.viewMarketplace')}
<ExternalLink className="size-3" />
</a>
</SelectLabel>
{catalogLoading && marketplaceOptions.length === 0 ? (
<div className="flex items-center gap-2 px-2 py-1.5 text-xs text-muted-foreground">
@@ -1,5 +1,6 @@
import {
forwardRef,
useCallback,
useEffect,
useImperativeHandle,
useMemo,
@@ -58,6 +59,12 @@ import {
} from 'lucide-react';
import PipelineExtension from '@/app/home/pipelines/components/pipeline-extensions/PipelineExtension';
import AgentRunnerSelect from '@/app/home/agents/components/AgentRunnerSelect';
import {
getErrorMessage,
readPendingAgentRunnerInstall,
resumePendingAgentRunnerInstall,
type InstalledAgentRunner,
} from '@/app/home/agents/agent-runner-marketplace';
interface PipelineFormComponentProps {
pipelineId?: string;
@@ -204,6 +211,8 @@ const PipelineFormComponent = forwardRef<
useState<PipelineConfigTab>();
const [outputConfigTabSchema, setOutputConfigTabSchema] =
useState<PipelineConfigTab>();
const [metadataLoaded, setMetadataLoaded] = useState(false);
const [pipelineLoaded, setPipelineLoaded] = useState(!isEditMode);
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
@@ -219,6 +228,36 @@ const PipelineFormComponent = forwardRef<
output: {},
},
});
const runnerInstallScope = `pipeline:${pipelineId || 'new'}`;
const applyInstalledRunner = useCallback(
(installed: InstalledAgentRunner) => {
setAIConfigTabSchema(installed.configTab);
const currentAI = (form.getValues('ai') || {}) as Record<string, any>;
const currentRunner =
currentAI.runner && typeof currentAI.runner === 'object'
? currentAI.runner
: {};
const currentConfigs =
currentAI.runner_config && typeof currentAI.runner_config === 'object'
? currentAI.runner_config
: {};
const runnerName = installed.runner.name;
const runnerStage = installed.configTab.stages.find(
(stage) => stage.name === runnerName,
);
form.setValue('ai', {
...currentAI,
runner: { ...currentRunner, id: runnerName },
runner_config: {
...currentConfigs,
...(runnerStage && !(runnerName in currentConfigs)
? { [runnerName]: getDefaultValues(runnerStage.config) }
: {}),
},
});
},
[form],
);
const dynamicFormSystemContext = useMemo(
() => ({ pipeline_id: pipelineId }),
[pipelineId],
@@ -244,6 +283,8 @@ const PipelineFormComponent = forwardRef<
}, [hasUnsavedChanges, onDirtyChange]);
useEffect(() => {
setMetadataLoaded(false);
setPipelineLoaded(!isEditMode);
// get config schema from metadata
httpClient.getGeneralPipelineMetadata().then((resp) => {
for (const config of resp.configs) {
@@ -257,6 +298,7 @@ const PipelineFormComponent = forwardRef<
setOutputConfigTabSchema(config);
}
}
setMetadataLoaded(true);
});
if (isEditMode) {
@@ -279,10 +321,48 @@ const PipelineFormComponent = forwardRef<
form.reset(loadedValues);
savedSnapshotRef.current = JSON.stringify(loadedValues);
initializedStagesRef.current.clear();
setPipelineLoaded(true);
});
}
}, [form, isEditMode, pipelineId]);
useEffect(() => {
if (
!metadataLoaded ||
!pipelineLoaded ||
!readPendingAgentRunnerInstall(runnerInstallScope)
) {
return;
}
let cancelled = false;
void resumePendingAgentRunnerInstall(runnerInstallScope)
.then((installed) => {
if (cancelled || !installed) return;
applyInstalledRunner(installed);
toast.success(
t('wizard.aiEngine.installSuccess', {
runner: extractI18nObject(installed.runner.label),
}),
);
})
.catch((error) => {
if (!cancelled) {
toast.error(
getErrorMessage(error) || t('wizard.aiEngine.installFailed'),
);
}
});
return () => {
cancelled = true;
};
}, [
applyInstalledRunner,
metadataLoaded,
pipelineLoaded,
runnerInstallScope,
t,
]);
useEffect(() => {
if (!isEditMode) {
form.reset({
@@ -523,7 +603,8 @@ const PipelineFormComponent = forwardRef<
label={extractI18nObject(config.label)}
value={String(field.value ?? '')}
onValueChange={field.onChange}
onMetadataRefresh={setAIConfigTabSchema}
installScope={runnerInstallScope}
onInstalled={applyInstalledRunner}
/>
) : undefined
}
+46 -1
View File
@@ -70,6 +70,8 @@ import {
installMarketplaceAgentRunner,
loadAgentRunnerCatalog,
marketplacePluginId,
readPendingAgentRunnerInstall,
resumePendingAgentRunnerInstall,
runnerPluginPrefix,
} from '@/app/home/agents/agent-runner-marketplace';
import {
@@ -104,6 +106,7 @@ import {
// ---------------------------------------------------------------------------
const TOTAL_STEPS = 4;
const WIZARD_RUNNER_INSTALL_SCOPE = 'wizard';
type WizardScenarioId =
| 'message_reply'
@@ -497,7 +500,9 @@ export default function WizardPage() {
setRunnerInstallError(null);
try {
const installed = await installMarketplaceAgentRunner(plugin);
const installed = await installMarketplaceAgentRunner(plugin, {
scope: WIZARD_RUNNER_INSTALL_SCOPE,
});
setAiConfigTab(installed.configTab);
setInstalledPluginIds((current) =>
current.includes(pluginId) ? current : [...current, pluginId],
@@ -529,6 +534,46 @@ export default function WizardPage() {
[handleSelectRunner, t],
);
useEffect(() => {
if (isLoading) return;
const pending = readPendingAgentRunnerInstall(WIZARD_RUNNER_INSTALL_SCOPE);
if (!pending) return;
let cancelled = false;
setInstallingRunnerPluginId(pending.pluginId);
setRunnerInstallError(null);
void resumePendingAgentRunnerInstall(WIZARD_RUNNER_INSTALL_SCOPE)
.then((installed) => {
if (cancelled || !installed) return;
setAiConfigTab(installed.configTab);
setInstalledPluginIds((current) =>
current.includes(pending.pluginId)
? current
: [...current, pending.pluginId],
);
handleSelectRunner(installed.runner.name, installed.configTab);
toast.success(
t('wizard.aiEngine.installSuccess', {
runner: pending.pluginLabel,
}),
);
})
.catch((error) => {
if (cancelled) return;
const message =
getErrorMessage(error) || t('wizard.aiEngine.installFailed');
setRunnerInstallError(message);
toast.error(message);
})
.finally(() => {
if (!cancelled) setInstallingRunnerPluginId(null);
});
return () => {
cancelled = true;
};
}, [handleSelectRunner, isLoading, t]);
// ---- Navigation helpers ----
const canProceed = useCallback((): boolean => {
+4 -1
View File
@@ -771,7 +771,10 @@ const enUS = {
noRunnersAvailableDescription:
'Install and enable an AgentRunner extension before configuring this Agent.',
installedRunners: 'Installed AgentRunners',
marketplaceRunners: 'AgentRunner Marketplace',
marketplaceRunners: 'AgentRunner plugins in Marketplace',
viewMarketplace: 'View market',
restoringRunnerInstall:
'Restoring the AgentRunner plugin installation and waiting for the runner…',
noInstalledRunners: 'No AgentRunner extension is installed yet.',
installingRunner: 'Installing {{runner}}...',
selectedRunnerUnavailable: 'Selected runner is unavailable',
+4 -1
View File
@@ -783,7 +783,10 @@ const jaJP = {
noRunnersAvailableDescription:
'この Agent を設定する前に AgentRunner 拡張機能をインストールして有効にしてください。',
installedRunners: 'インストール済み AgentRunner',
marketplaceRunners: 'AgentRunner マーケットプレイス',
marketplaceRunners: 'マーケットプレイスの AgentRunner プラグイン',
viewMarketplace: '市場を見る',
restoringRunnerInstall:
'AgentRunner プラグインのインストールを復元し、ランナーを待機しています…',
noInstalledRunners:
'AgentRunner 拡張機能はまだインストールされていません。',
installingRunner: '{{runner}} をインストールしています...',
+3 -1
View File
@@ -739,7 +739,9 @@ const zhHans = {
noRunnersAvailableDescription:
'请先安装并启用 AgentRunner 扩展,再配置此 Agent。',
installedRunners: '已安装的 AgentRunner',
marketplaceRunners: 'AgentRunner 插件市场',
marketplaceRunners: '插件市场中的 AgentRunner 插件',
viewMarketplace: '查看市场',
restoringRunnerInstall: '正在恢复 AgentRunner 插件安装并等待运行器就绪…',
noInstalledRunners: '尚未安装任何 AgentRunner 扩展。',
installingRunner: '正在安装 {{runner}}...',
selectedRunnerUnavailable: '所选运行器不可用',
+8 -2
View File
@@ -68,14 +68,20 @@ test('binds every message-reply bot to its provisional pipeline before verificat
test('keeps the 4.11 AgentRunner marketplace installation flow', () => {
assert.match(wizardSource, /loadAgentRunnerCatalog\(\)/);
assert.match(wizardSource, /installMarketplaceAgentRunner\(plugin\)/);
assert.match(
wizardSource,
/installMarketplaceAgentRunner\(plugin, \{[\s\S]*?scope: WIZARD_RUNNER_INSTALL_SCOPE/,
);
assert.match(wizardSource, /resumePendingAgentRunnerInstall\(/);
assert.match(
runnerMarketplaceSource,
/RUNNER_COMPONENT_FILTER = 'AgentRunner'/,
);
assert.match(runnerMarketplaceSource, /installPluginFromMarketplace\(/);
assert.match(runnerMarketplaceSource, /runnerPluginPrefix\(plugin\)/);
assert.match(runnerMarketplaceSource, /const prefix = runnerPluginPrefix\(\{/);
assert.match(runnerMarketplaceSource, /option\.name\.startsWith\(prefix\)/);
assert.match(runnerMarketplaceSource, /registrationDeadline/);
assert.match(runnerMarketplaceSource, /sessionStorage\.setItem\(/);
});
test('requires the selected AgentRunner mandatory configuration before finishing', () => {