Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
161 lines
6.1 KiB
TypeScript
161 lines
6.1 KiB
TypeScript
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
import type { VaultImportDestination } from "../../domain/vaultImport";
|
|
import { pluginExtensionBridge } from "./pluginExtensionBridge";
|
|
|
|
type Translation = (key: string, params?: Record<string, unknown>) => string;
|
|
|
|
type PluginVaultImporterOptions = {
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
onPluginPreviewCommit: (
|
|
preview: NetcattyPluginImporterPreview,
|
|
destination?: VaultImportDestination,
|
|
) => Promise<void> | void;
|
|
destination?: VaultImportDestination | null;
|
|
t: Translation;
|
|
};
|
|
|
|
const localizeProviderLabel = (
|
|
provider: NetcattyExtensionProviderContribution,
|
|
locale = typeof navigator === "undefined" ? "en" : navigator.language,
|
|
): string => {
|
|
const label = provider.provider.label;
|
|
if (typeof label === "string") return label;
|
|
return label[locale] ?? label[locale.split("-")[0]] ?? label.en ?? provider.provider.id;
|
|
};
|
|
|
|
const summarizePluginImporterPreview = (preview: NetcattyPluginImporterPreview | null): string | null => {
|
|
if (!preview) return null;
|
|
const drafts = preview.records.filter((record) => record.type === "draft");
|
|
const byKind = drafts.reduce<Record<string, number>>((counts, record) => {
|
|
if (record.type === "draft") counts[record.draft.kind] = (counts[record.draft.kind] ?? 0) + 1;
|
|
return counts;
|
|
}, {});
|
|
return Object.entries(byKind).map(([kind, count]) => `${kind}: ${count}`).join(" · ");
|
|
};
|
|
|
|
export function usePluginVaultImporter({
|
|
open,
|
|
onOpenChange,
|
|
onPluginPreviewCommit,
|
|
destination,
|
|
t,
|
|
}: PluginVaultImporterOptions) {
|
|
const activePluginImportRequestRef = useRef<string | null>(null);
|
|
const pluginImportGenerationRef = useRef(0);
|
|
const [pluginProviders, setPluginProviders] = useState<ReadonlyArray<NetcattyExtensionProviderContribution>>([]);
|
|
const [pluginPreview, setPluginPreview] = useState<NetcattyPluginImporterPreview | null>(null);
|
|
const [pluginBusy, setPluginBusy] = useState(false);
|
|
const [pluginError, setPluginError] = useState<string | null>(null);
|
|
const [pluginProgress, setPluginProgress] = useState<NetcattyPluginImporterProgressEvent["progress"] | null>(null);
|
|
|
|
useEffect(() => pluginExtensionBridge.onImporterProgress((event) => {
|
|
if (event.requestId === activePluginImportRequestRef.current) setPluginProgress(event.progress);
|
|
}), []);
|
|
|
|
useEffect(() => {
|
|
if (!open) return;
|
|
let cancelled = false;
|
|
void pluginExtensionBridge.listProviders("importer").then((providers) => {
|
|
if (!cancelled) setPluginProviders(providers);
|
|
}).catch(() => {
|
|
if (!cancelled) setPluginProviders([]);
|
|
});
|
|
return () => { cancelled = true; };
|
|
}, [open]);
|
|
|
|
const resetPluginImporterState = useCallback(() => {
|
|
pluginImportGenerationRef.current += 1;
|
|
const requestId = activePluginImportRequestRef.current;
|
|
activePluginImportRequestRef.current = null;
|
|
if (requestId) void pluginExtensionBridge.cancelRequest(requestId).catch(() => false);
|
|
setPluginPreview(null);
|
|
setPluginError(null);
|
|
setPluginProgress(null);
|
|
setPluginBusy(false);
|
|
}, []);
|
|
|
|
const handleOpenChange = useCallback((newOpen: boolean) => {
|
|
if (!newOpen) resetPluginImporterState();
|
|
onOpenChange(newOpen);
|
|
}, [onOpenChange, resetPluginImporterState]);
|
|
|
|
const pickPluginFile = useCallback((provider: NetcattyExtensionProviderContribution) => {
|
|
if (pluginBusy) return;
|
|
const generation = ++pluginImportGenerationRef.current;
|
|
const isCurrent = () => pluginImportGenerationRef.current === generation;
|
|
setPluginBusy(true);
|
|
setPluginError(null);
|
|
setPluginProgress(null);
|
|
void (async () => {
|
|
let selection: Awaited<ReturnType<typeof pluginExtensionBridge.selectImporterFile>> = null;
|
|
let consumed = false;
|
|
let requestId: string | null = null;
|
|
try {
|
|
selection = await pluginExtensionBridge.selectImporterFile();
|
|
if (!selection || !isCurrent()) return;
|
|
requestId = crypto.randomUUID();
|
|
activePluginImportRequestRef.current = requestId;
|
|
const detection = await pluginExtensionBridge.detectImporter({
|
|
requestId,
|
|
providerId: provider.provider.id,
|
|
sample: selection.sample,
|
|
fileName: selection.fileName,
|
|
});
|
|
if (!isCurrent()) return;
|
|
if (detection && detection.confidence <= 0) {
|
|
throw new Error(detection.reason || t("vault.import.plugins.notRecognized"));
|
|
}
|
|
const preview = await pluginExtensionBridge.parseImporterFile({
|
|
requestId,
|
|
providerId: provider.provider.id,
|
|
selectionToken: selection.selectionToken,
|
|
});
|
|
consumed = true;
|
|
if (isCurrent()) setPluginPreview(preview);
|
|
} catch (error) {
|
|
if (isCurrent()) setPluginError(error instanceof Error ? error.message : t("common.unknownError"));
|
|
} finally {
|
|
if (activePluginImportRequestRef.current === requestId) activePluginImportRequestRef.current = null;
|
|
if (selection && !consumed) {
|
|
await pluginExtensionBridge.releaseImporterFile(selection.selectionToken).catch(() => false);
|
|
}
|
|
if (isCurrent()) {
|
|
setPluginBusy(false);
|
|
setPluginProgress(null);
|
|
}
|
|
}
|
|
})();
|
|
}, [pluginBusy, t]);
|
|
|
|
const commitPluginPreview = useCallback(async () => {
|
|
if (!pluginPreview || pluginBusy) return;
|
|
setPluginBusy(true);
|
|
setPluginError(null);
|
|
try {
|
|
await onPluginPreviewCommit(pluginPreview, destination ?? undefined);
|
|
handleOpenChange(false);
|
|
} catch (error) {
|
|
setPluginError(error instanceof Error ? error.message : t("common.unknownError"));
|
|
} finally {
|
|
setPluginBusy(false);
|
|
}
|
|
}, [destination, handleOpenChange, onPluginPreviewCommit, pluginBusy, pluginPreview, t]);
|
|
|
|
const previewSummary = useMemo(() => summarizePluginImporterPreview(pluginPreview), [pluginPreview]);
|
|
|
|
return {
|
|
pluginProviders,
|
|
pluginPreview,
|
|
pluginBusy,
|
|
pluginError,
|
|
pluginProgress,
|
|
previewSummary,
|
|
pickPluginFile,
|
|
commitPluginPreview,
|
|
clearPluginPreview: () => setPluginPreview(null),
|
|
handleOpenChange,
|
|
localizeProviderLabel,
|
|
};
|
|
}
|