fix(settings): harden conductor imports

This commit is contained in:
Mohamed Boudra
2026-07-17 14:48:54 +02:00
parent f030ddefb1
commit f44ef807ef
12 changed files with 216 additions and 46 deletions
+1
View File
@@ -2084,6 +2084,7 @@ export const ar: TranslationResources = {
success: "{{source}} settings imported",
errorTitle: "Couldn't import settings",
errors: {
capabilityMissing: "Update the host to use this.",
notFound: "No {{source}} project config was found.",
invalid: "{{path}} couldn't be parsed.",
staleSource: "The {{source}} config changed. Refresh the preview before importing.",
+1
View File
@@ -2098,6 +2098,7 @@ export const en = {
success: "{{source}} settings imported",
errorTitle: "Couldn't import settings",
errors: {
capabilityMissing: "Update the host to use this.",
notFound: "No {{source}} project config was found.",
invalid: "{{path}} couldn't be parsed.",
staleSource: "The {{source}} config changed. Refresh the preview before importing.",
+1
View File
@@ -2136,6 +2136,7 @@ export const es: TranslationResources = {
success: "{{source}} settings imported",
errorTitle: "Couldn't import settings",
errors: {
capabilityMissing: "Update the host to use this.",
notFound: "No {{source}} project config was found.",
invalid: "{{path}} couldn't be parsed.",
staleSource: "The {{source}} config changed. Refresh the preview before importing.",
+1
View File
@@ -2139,6 +2139,7 @@ export const fr: TranslationResources = {
success: "{{source}} settings imported",
errorTitle: "Couldn't import settings",
errors: {
capabilityMissing: "Update the host to use this.",
notFound: "No {{source}} project config was found.",
invalid: "{{path}} couldn't be parsed.",
staleSource: "The {{source}} config changed. Refresh the preview before importing.",
+1
View File
@@ -2109,6 +2109,7 @@ export const ja: TranslationResources = {
success: "{{source}} settings imported",
errorTitle: "Couldn't import settings",
errors: {
capabilityMissing: "Update the host to use this.",
notFound: "No {{source}} project config was found.",
invalid: "{{path}} couldn't be parsed.",
staleSource: "The {{source}} config changed. Refresh the preview before importing.",
+1
View File
@@ -2122,6 +2122,7 @@ export const ptBR: TranslationResources = {
success: "{{source}} settings imported",
errorTitle: "Couldn't import settings",
errors: {
capabilityMissing: "Update the host to use this.",
notFound: "No {{source}} project config was found.",
invalid: "{{path}} couldn't be parsed.",
staleSource: "The {{source}} config changed. Refresh the preview before importing.",
+1
View File
@@ -2127,6 +2127,7 @@ export const ru: TranslationResources = {
success: "{{source}} settings imported",
errorTitle: "Couldn't import settings",
errors: {
capabilityMissing: "Update the host to use this.",
notFound: "No {{source}} project config was found.",
invalid: "{{path}} couldn't be parsed.",
staleSource: "The {{source}} config changed. Refresh the preview before importing.",
+1
View File
@@ -2060,6 +2060,7 @@ export const zhCN: TranslationResources = {
success: "{{source}} settings imported",
errorTitle: "Couldn't import settings",
errors: {
capabilityMissing: "Update the host to use this.",
notFound: "No {{source}} project config was found.",
invalid: "{{path}} couldn't be parsed.",
staleSource: "The {{source}} config changed. Refresh the preview before importing.",
@@ -16,6 +16,7 @@ import type { ProjectConfigImportIntent } from "./route";
export type ProjectConfigImportVisibleError =
| ProjectConfigRpcError
| { code: "capability_missing" }
| { code: "transport"; message: string };
export type ProjectConfigImportState =
@@ -58,12 +59,7 @@ export function ProjectConfigImportSheet({
);
const preview = state.preview;
const visibleError = state.status === "error" ? state.error : null;
const needsRefresh =
visibleError?.code === "stale_source_config" ||
visibleError?.code === "stale_project_config" ||
visibleError?.code === "nothing_to_import";
const retryAction = state.status === "error" ? state.retryAction : "refresh";
const handleRetry = retryAction === "apply" ? onApply : onRefresh;
const canImport =
state.status === "ready" &&
state.preview.status === "available" &&
@@ -93,25 +89,12 @@ export function ProjectConfigImportSheet({
title={t("settings.project.import.errorTitle")}
description={projectConfigImportErrorText(visibleError, t, sourceName)}
>
{needsRefresh ? (
<Button
testID="project-config-import-refresh"
onPress={onRefresh}
variant="outline"
size="sm"
>
{t("settings.project.import.refreshPreview")}
</Button>
) : (
<Button
testID="project-config-import-retry"
onPress={handleRetry}
variant="outline"
size="sm"
>
{t("settings.project.actions.tryAgain")}
</Button>
)}
<ImportErrorRetryButton
error={visibleError}
retryAction={retryAction}
onRefresh={onRefresh}
onApply={onApply}
/>
<Button
testID="project-config-import-cancel-error"
onPress={onClose}
@@ -147,6 +130,44 @@ export function ProjectConfigImportSheet({
);
}
function ImportErrorRetryButton(input: {
error: ProjectConfigImportVisibleError;
retryAction: "refresh" | "apply";
onRefresh: () => void;
onApply: () => void;
}) {
const { t } = useTranslation();
if (input.error.code === "capability_missing") {
return null;
}
const needsRefresh =
input.error.code === "stale_source_config" ||
input.error.code === "stale_project_config" ||
input.error.code === "nothing_to_import";
if (needsRefresh) {
return (
<Button
testID="project-config-import-refresh"
onPress={input.onRefresh}
variant="outline"
size="sm"
>
{t("settings.project.import.refreshPreview")}
</Button>
);
}
return (
<Button
testID="project-config-import-retry"
onPress={input.retryAction === "apply" ? input.onApply : input.onRefresh}
variant="outline"
size="sm"
>
{t("settings.project.actions.tryAgain")}
</Button>
);
}
function PreviewBody({ preview }: { preview: ProjectConfigImportPreview }) {
const { t } = useTranslation();
const sections = [
@@ -214,6 +235,8 @@ function projectConfigImportErrorText(
switch (error.code) {
case "transport":
return error.message;
case "capability_missing":
return t("settings.project.import.errors.capabilityMissing");
case "source_config_not_found":
return t("settings.project.import.errors.notFound", { source: sourceName });
case "invalid_source_config":
@@ -50,13 +50,18 @@ export function useProjectConfigImportModel(input: {
const [applyError, setApplyError] = useState<ProjectConfigImportVisibleError | null>(null);
const [retryAction, setRetryAction] = useState<ProjectConfigImportRetryAction>("apply");
const activeSource = intent ? registry.get(intent.source) : null;
const routeIntentCapabilityMissing = isRouteIntentCapabilityMissing({
intent,
routeIntent: input.routeIntent,
sources,
});
const activePreview = useProjectConfigImportPreviewQuery({
client: input.client,
serverId: input.serverId,
repoRoot: input.repoRoot,
source: intent?.source ?? null,
protocolSource: intent?.protocolSource ?? null,
enabled: Boolean(intent && input.projectConfigLoaded),
enabled: Boolean(intent && input.projectConfigLoaded && !routeIntentCapabilityMissing),
});
const preview = activePreview.data?.ok ? activePreview.data : null;
const queryClient = useQueryClient();
@@ -159,13 +164,14 @@ export function useProjectConfigImportModel(input: {
if (!intent) {
return null;
}
const error =
applyError ??
normalizeProjectConfigImportError(
activePreview.data && !activePreview.data.ok
? activePreview.data.error
: activePreview.error,
);
const error = routeIntentCapabilityMissing
? ({ code: "capability_missing" } as const)
: (applyError ??
normalizeProjectConfigImportError(
activePreview.data && !activePreview.data.ok
? activePreview.data.error
: activePreview.error,
));
if (error) {
return {
status: "error",
@@ -190,6 +196,7 @@ export function useProjectConfigImportModel(input: {
intent,
preview,
retryAction,
routeIntentCapabilityMissing,
]);
return {
@@ -264,6 +271,20 @@ function projectConfigImportAvailabilityStatus(count: number): "none" | "one" |
return count === 1 ? "one" : "many";
}
function isRouteIntentCapabilityMissing(input: {
intent: ProjectConfigImportIntent | null;
routeIntent: ProjectConfigImportIntent | null;
sources: ProjectConfigImportSourceRegistration[];
}): boolean {
if (!input.intent || input.routeIntent?.intentId !== input.intent.intentId) {
return false;
}
const intentSourceKey = stableProjectConfigImportSourceKey(input.intent.source);
return !input.sources.some(
(source) => stableProjectConfigImportSourceKey(source.source) === intentSourceKey,
);
}
function useAdvertisedProjectConfigImportSources(
serverId: string | null | undefined,
registry: ProjectConfigImportSourceRegistry,