Files
NetMesh/application/state/usePluginVaultImporter.ts
zhaolei 3c72efcb7f
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
[Init] Initial commit - NetMesh terminal manager
2026-09-13 18:24:01 +08:00

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,
};
}