Extract integrations detail dialog seam
This commit is contained in:
parent
a714b2accc
commit
df0f98a1b7
7 changed files with 453 additions and 220 deletions
|
|
@ -73,6 +73,7 @@ const behaviorTests = [
|
|||
".test-dist/tests/mcp-toolkit-installer-seams.test.js",
|
||||
".test-dist/tests/mcp-toolkit-catalog.test.js",
|
||||
".test-dist/tests/integrations-view-toolkit-split.test.js",
|
||||
".test-dist/tests/integrations-view-detail-dialog.test.js",
|
||||
".test-dist/tests/integrations-view-state.test.js",
|
||||
".test-dist/tests/familiars-view-detail-dialog.test.js",
|
||||
".test-dist/tests/familiars-view-state.test.js",
|
||||
|
|
|
|||
|
|
@ -129,6 +129,7 @@ const controlCenterFamiliarsStateSource = readFileSync(join(appDir, "src", "rend
|
|||
const controlCenterFamiliarsHelpersSource = readFileSync(join(appDir, "src", "renderer", "src", "control-center", "familiars-view-helpers.ts"), "utf8");
|
||||
const controlCenterFamiliarsEffectsSource = readFileSync(join(appDir, "src", "renderer", "src", "control-center", "familiars-view-effects.ts"), "utf8");
|
||||
const controlCenterIntegrationsSource = readFileSync(join(appDir, "src", "renderer", "src", "control-center", "integrations-view.tsx"), "utf8");
|
||||
const controlCenterIntegrationsDetailDialogSource = readFileSync(join(appDir, "src", "renderer", "src", "control-center", "integrations-view-detail-dialog.tsx"), "utf8");
|
||||
const controlCenterIntegrationsStateSource = readFileSync(join(appDir, "src", "renderer", "src", "control-center", "integrations-view-state.ts"), "utf8");
|
||||
const controlCenterIntegrationsSharedSource = readFileSync(join(appDir, "src", "renderer", "src", "control-center", "integrations-view-shared.tsx"), "utf8");
|
||||
const controlCenterIntegrationsToolingSectionsSource = readFileSync(join(appDir, "src", "renderer", "src", "control-center", "integrations-view-tooling-sections.tsx"), "utf8");
|
||||
|
|
@ -234,6 +235,7 @@ const controlCenterInternalUiSource = [
|
|||
const agentSetupCombinedSource = `${agentSetupSource}\n${agentSetupActionsSource}\n${agentSetupEditorToolsSource}`;
|
||||
const controlCenterIntegrationsCombinedSource = [
|
||||
controlCenterIntegrationsSource,
|
||||
controlCenterIntegrationsDetailDialogSource,
|
||||
controlCenterIntegrationsSharedSource,
|
||||
controlCenterIntegrationsToolingSectionsSource,
|
||||
controlCenterIntegrationsMcpToolkitSectionSource,
|
||||
|
|
@ -599,9 +601,11 @@ assert.match(controlCenterFamiliarsEffectsSource, /export function useCatalogSea
|
|||
assert.match(controlCenterFamiliarsEffectsSource, /export function useCatalogSearchPrefetch/, "Control Center familiars effects seam must export catalog search prefetch behavior.");
|
||||
assert.match(controlCenterRouteDataLoadersSource, /getPetsState/, "Control Center shared route data loaders must bridge familiars state APIs.");
|
||||
assert.match(controlCenterIntegrationsSource, /export function IntegrationsView\(\)/, "Control Center must include the integrations page.");
|
||||
assert.match(controlCenterIntegrationsSource, /from "\.\/integrations-view-detail-dialog"/, "Control Center integrations route must import the extracted detail dialog seam.");
|
||||
assert.match(controlCenterIntegrationsSource, /from "\.\/integrations-view-state"/, "Control Center integrations route must import the extracted integrations state seam.");
|
||||
assert.match(controlCenterIntegrationsSource, /from "\.\/integrations-view-shared"/, "Control Center integrations route must import the extracted integrations shared seam.");
|
||||
assert.match(controlCenterIntegrationsSource, /from "\.\/integrations-view-tooling-sections"/, "Control Center integrations route must import the extracted integrations tooling seam.");
|
||||
assert.match(controlCenterIntegrationsDetailDialogSource, /export function IntegrationsDetailDialog/, "Control Center integrations detail dialog seam must export the selected-integration overlay.");
|
||||
assert.match(controlCenterIntegrationsDetailDialogSource, /from "\.\/integrations-view-tooling-sections"/, "Control Center integrations detail dialog seam must import the extracted integrations tooling seam.");
|
||||
assert.match(controlCenterIntegrationsStateSource, /export function useIntegrationsViewState/, "Control Center integrations state seam must export the route hook.");
|
||||
assert.match(controlCenterIntegrationsStateSource, /from "\.\/route-data-loaders"/, "Control Center integrations state seam must compose shared route data loaders.");
|
||||
assert.match(controlCenterIntegrationsStateSource, /from "\.\/integrations-view-shared"/, "Control Center integrations state seam must compose the extracted integrations shared helpers.");
|
||||
|
|
|
|||
|
|
@ -22,8 +22,11 @@ surface they depend on.
|
|||
card-level actions while composing the extracted detail overlay.
|
||||
- `familiars-view-detail-dialog.tsx` owns the selected-familiar detail overlay,
|
||||
animation previews, and detail actions.
|
||||
- `integrations-view.tsx` owns integrations-route state, action callbacks, card
|
||||
layout, and the remaining Claude/OpenCode/Cursor detail shell.
|
||||
- `integrations-view.tsx` owns the integrations-route shell, card grid, and
|
||||
message/loading presentation while composing the extracted detail overlay.
|
||||
- `integrations-view-detail-dialog.tsx` owns the selected-integration overlay,
|
||||
Claude/OpenCode/Cursor detail sections, and the remaining tooling dialog
|
||||
composition.
|
||||
- `integrations-view-shared.tsx` owns route-local integration icons,
|
||||
command-path field UI, and status-tone helpers.
|
||||
- `integrations-view-tooling-sections.tsx` owns the MCP tool-server,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,387 @@
|
|||
import type { ReactNode } from "react";
|
||||
import { useI18n } from "../i18n";
|
||||
import {
|
||||
IntegrationIcon,
|
||||
PathField,
|
||||
claudeStatusTone,
|
||||
cursorStatusTone,
|
||||
opencodeStatusTone,
|
||||
} from "./integrations-view-shared";
|
||||
import {
|
||||
FamiliarOSMcpServerSections,
|
||||
McpToolServersSection,
|
||||
McpToolkitSection,
|
||||
PiIntegrationSection,
|
||||
} from "./integrations-view-tooling-sections";
|
||||
import * as Shared from "./shared";
|
||||
import type {
|
||||
AgentSetupAction,
|
||||
AgentSetupCommandPaths,
|
||||
AgentSetupSnapshot,
|
||||
FamiliarOSMcpServerHealth,
|
||||
FamiliarOSMcpServerPreview,
|
||||
McpToolkitInstallMode,
|
||||
McpToolkitInstallResult,
|
||||
McpToolkitPersistentTarget,
|
||||
} from "./shared";
|
||||
|
||||
const {
|
||||
Button,
|
||||
CloseIcon,
|
||||
HookIcon,
|
||||
InstallIcon,
|
||||
MemoryIcon,
|
||||
NextIcon,
|
||||
RefreshIcon,
|
||||
RemoveIcon,
|
||||
ReplaceIcon,
|
||||
StatusPill,
|
||||
commandModeLabelKeys,
|
||||
} = Shared;
|
||||
|
||||
type IntegrationsDetailDialogProps = {
|
||||
snapshot: AgentSetupSnapshot;
|
||||
selectedId: string;
|
||||
selectedIntegrationName?: string;
|
||||
selectedToolkitId: string;
|
||||
onSelectedIdChange: (value: string | null) => void;
|
||||
onSelectedToolkitIdChange: (value: string) => void;
|
||||
toolkitInstallMode: McpToolkitInstallMode;
|
||||
onToolkitInstallModeChange: (value: McpToolkitInstallMode) => void;
|
||||
toolkitPersistentTarget: McpToolkitPersistentTarget;
|
||||
onToolkitPersistentTargetChange: (value: McpToolkitPersistentTarget) => void;
|
||||
toolkitLastInstall: McpToolkitInstallResult | null;
|
||||
vanillaChatTools: string[];
|
||||
vanillaChatSaving: boolean;
|
||||
familiarosMcpPreview: FamiliarOSMcpServerPreview | null;
|
||||
familiarosMcpTest: { busy: boolean; result: FamiliarOSMcpServerHealth | null };
|
||||
isBusy: boolean;
|
||||
loadIntegrations: (selectedPetId?: string, commandMode?: AgentSetupSnapshot["commandMode"]) => Promise<void>;
|
||||
runIntegrationAction: (label: string, action: AgentSetupAction) => Promise<void>;
|
||||
updatePath: (key: keyof AgentSetupCommandPaths, value: string) => Promise<void>;
|
||||
changeCommandMode: (mode: AgentSetupSnapshot["commandMode"]) => void;
|
||||
copyText: (label: string, value: string) => Promise<void>;
|
||||
openDocs: (url: string) => Promise<void>;
|
||||
testFamiliarOSMcpServer: () => Promise<void>;
|
||||
copyFamiliarOSMcpJson: () => Promise<void>;
|
||||
saveVanillaChatTools: (toolIds: string[]) => Promise<void>;
|
||||
toggleVanillaChatTool: (id: string) => void;
|
||||
installToolkitHost: () => Promise<void>;
|
||||
};
|
||||
|
||||
export function IntegrationsDetailDialog({
|
||||
snapshot,
|
||||
selectedId,
|
||||
selectedIntegrationName,
|
||||
selectedToolkitId,
|
||||
onSelectedIdChange,
|
||||
onSelectedToolkitIdChange,
|
||||
toolkitInstallMode,
|
||||
onToolkitInstallModeChange,
|
||||
toolkitPersistentTarget,
|
||||
onToolkitPersistentTargetChange,
|
||||
toolkitLastInstall,
|
||||
vanillaChatTools,
|
||||
vanillaChatSaving,
|
||||
familiarosMcpPreview,
|
||||
familiarosMcpTest,
|
||||
isBusy,
|
||||
loadIntegrations,
|
||||
runIntegrationAction,
|
||||
updatePath,
|
||||
changeCommandMode,
|
||||
copyText,
|
||||
openDocs,
|
||||
testFamiliarOSMcpServer,
|
||||
copyFamiliarOSMcpJson,
|
||||
saveVanillaChatTools,
|
||||
toggleVanillaChatTool,
|
||||
installToolkitHost,
|
||||
}: IntegrationsDetailDialogProps) {
|
||||
const { t } = useI18n();
|
||||
const integrationDialogTitleId = `integration-detail-title-${selectedId}`;
|
||||
|
||||
return <div className="plugin-config-overlay" role="dialog" aria-modal="true" aria-labelledby={integrationDialogTitleId}>
|
||||
<button className="plugin-config-backdrop" type="button" aria-label={t("integrations.closeAria")} onClick={() => onSelectedIdChange(null)} />
|
||||
<GlassDialog
|
||||
selectedId={selectedId}
|
||||
selectedIntegrationName={selectedIntegrationName}
|
||||
integrationDialogTitleId={integrationDialogTitleId}
|
||||
onClose={() => onSelectedIdChange(null)}
|
||||
>
|
||||
{(selectedId === "claude" || selectedId === "opencode" || selectedId === "cursor") && (
|
||||
<section className="plugin-section">
|
||||
<div className="plugin-section-title"><small>{t("integrations.commandSource")}</small><strong>{t("integrations.cliMode")}</strong></div>
|
||||
<p className="text-sm text-slatecopy">{t("integrations.linkToCentralPanel")}</p>
|
||||
<p className="text-xs text-slatecopy mt-2">{t(commandModeLabelKeys[snapshot.commandMode])}</p>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{selectedId === "mcp-tool-servers" && (
|
||||
<McpToolServersSection
|
||||
vanillaChatTools={vanillaChatTools}
|
||||
vanillaChatSaving={vanillaChatSaving}
|
||||
isBusy={isBusy}
|
||||
onToggleTool={toggleVanillaChatTool}
|
||||
onSaveTools={saveVanillaChatTools}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedId === "familiaros-mcp-server" && (
|
||||
<FamiliarOSMcpServerSections
|
||||
snapshot={snapshot}
|
||||
familiarosMcpTest={familiarosMcpTest}
|
||||
familiarosMcpPreview={familiarosMcpPreview}
|
||||
isBusy={isBusy}
|
||||
onChangeCommandMode={changeCommandMode}
|
||||
onSaveNodePath={(value) => updatePath("node", value)}
|
||||
onSelectPet={(petId) => void loadIntegrations(petId)}
|
||||
onTestServer={testFamiliarOSMcpServer}
|
||||
onCopyMcpJson={copyFamiliarOSMcpJson}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedId === "claude" && (
|
||||
<ClaudeIntegrationSections
|
||||
snapshot={snapshot}
|
||||
isBusy={isBusy}
|
||||
onRunIntegrationAction={runIntegrationAction}
|
||||
onUpdatePath={updatePath}
|
||||
onReloadIntegrations={loadIntegrations}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedId === "opencode" && (
|
||||
<OpenCodeIntegrationSections
|
||||
snapshot={snapshot}
|
||||
isBusy={isBusy}
|
||||
onRunIntegrationAction={runIntegrationAction}
|
||||
onUpdatePath={updatePath}
|
||||
onReloadIntegrations={loadIntegrations}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedId === "cursor" && (
|
||||
<CursorIntegrationSections
|
||||
snapshot={snapshot}
|
||||
isBusy={isBusy}
|
||||
onRunIntegrationAction={runIntegrationAction}
|
||||
onReloadIntegrations={loadIntegrations}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedId === "pi" && <PiIntegrationSection />}
|
||||
|
||||
{selectedId === "mcp-toolkit" && (
|
||||
<McpToolkitSection
|
||||
selectedToolkitId={selectedToolkitId}
|
||||
onSelectedToolkitIdChange={onSelectedToolkitIdChange}
|
||||
toolkitInstallMode={toolkitInstallMode}
|
||||
onToolkitInstallModeChange={onToolkitInstallModeChange}
|
||||
toolkitPersistentTarget={toolkitPersistentTarget}
|
||||
onToolkitPersistentTargetChange={onToolkitPersistentTargetChange}
|
||||
toolkitLastInstall={toolkitLastInstall}
|
||||
isBusy={isBusy}
|
||||
onInstallToolkitHost={installToolkitHost}
|
||||
onCopyText={copyText}
|
||||
onOpenDocs={openDocs}
|
||||
/>
|
||||
)}
|
||||
</GlassDialog>
|
||||
</div>;
|
||||
}
|
||||
|
||||
function GlassDialog({
|
||||
selectedId,
|
||||
selectedIntegrationName,
|
||||
integrationDialogTitleId,
|
||||
onClose,
|
||||
children,
|
||||
}: {
|
||||
selectedId: string;
|
||||
selectedIntegrationName?: string;
|
||||
integrationDialogTitleId: string;
|
||||
onClose: () => void;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
return <Shared.GlassCard className="plugin-inspector">
|
||||
<div className="plugin-inspector-head">
|
||||
<div className="plugin-inspector-icon">
|
||||
<IntegrationIcon id={selectedId} />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="eyebrow">{t("integrations.detail")}</p>
|
||||
<h2 id={integrationDialogTitleId}>{selectedIntegrationName}</h2>
|
||||
</div>
|
||||
<Button variant="secondary" size="compact" icon={<CloseIcon />} onClick={onClose}>{t("integrations.close")}</Button>
|
||||
</div>
|
||||
<div className="flex flex-col gap-5 mt-4">{children}</div>
|
||||
</Shared.GlassCard>;
|
||||
}
|
||||
|
||||
function ClaudeIntegrationSections({
|
||||
snapshot,
|
||||
isBusy,
|
||||
onRunIntegrationAction,
|
||||
onUpdatePath,
|
||||
onReloadIntegrations,
|
||||
}: {
|
||||
snapshot: AgentSetupSnapshot;
|
||||
isBusy: boolean;
|
||||
onRunIntegrationAction: (label: string, action: AgentSetupAction) => Promise<void>;
|
||||
onUpdatePath: (key: keyof AgentSetupCommandPaths, value: string) => Promise<void>;
|
||||
onReloadIntegrations: () => Promise<void>;
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
return <>
|
||||
<section className="plugin-section">
|
||||
<div className="plugin-section-title"><small>{t("integrations.connection")}</small><strong>{t("integrations.statusRouting")}</strong></div>
|
||||
<div className="flex items-center justify-between p-3 rounded-2xl bg-blue-50/50 border border-blue-100/50">
|
||||
<div className="flex flex-col">
|
||||
<strong className="text-sm text-navy">{snapshot.status.label}</strong>
|
||||
<small className="text-xs text-slatecopy">{snapshot.status.details}</small>
|
||||
</div>
|
||||
<StatusPill tone={claudeStatusTone(snapshot.status.state)}>{snapshot.status.state}</StatusPill>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="plugin-section">
|
||||
<div className="plugin-section-title"><small>{t("integrations.configuration")}</small><strong>{t("integrations.commandPaths")}</strong></div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<PathField label={t("integrations.claudeCommand")} value={snapshot.commandPaths.claude} placeholder="claude" onSave={(value) => void onUpdatePath("claude", value)} disabled={isBusy} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<section className="plugin-section">
|
||||
<div className="plugin-section-title"><small>{t("integrations.optional")}</small><strong>{t("integrations.claudeHooks")}</strong></div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<StatusPill tone={snapshot.hookStatus.status === "installed" ? "green" : "blue"}>{snapshot.hookStatus.status}</StatusPill>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Button variant="primary" size="compact" icon={<HookIcon />} disabled={isBusy} onClick={() => void onRunIntegrationAction(t("integrations.busy.installingHooks"), "install-hooks")}>{t("integrations.installHooks")}</Button>
|
||||
<Button variant="danger" size="compact" icon={<RemoveIcon />} disabled={isBusy || snapshot.hookStatus.status === "needs_setup"} onClick={() => void onRunIntegrationAction(t("integrations.busy.removingHooks"), "uninstall-hooks")}>{t("integrations.removeHooks")}</Button>
|
||||
</div>
|
||||
</section>
|
||||
<section className="plugin-section">
|
||||
<div className="plugin-section-title"><small>{t("integrations.included")}</small><strong>{t("integrations.instructions")}</strong></div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<StatusPill tone={snapshot.memoryStatus.state === "installed" ? "green" : "blue"}>{snapshot.memoryStatus.state}</StatusPill>
|
||||
</div>
|
||||
<Button variant="secondary" size="compact" icon={<MemoryIcon />} disabled={isBusy} onClick={() => void onRunIntegrationAction(t("integrations.busy.updatingInstructions"), "install-memory")}>{t("integrations.updateInstructions")}</Button>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section className="plugin-section">
|
||||
<div className="plugin-section-title"><small>{t("integrations.actions")}</small><strong>{t("integrations.management")}</strong></div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{snapshot.status.canConfigure && <Button variant="primary" icon={<InstallIcon />} disabled={isBusy} onClick={() => void onRunIntegrationAction(t("integrations.busy.installing"), "configure")}>{t("integrations.installMcp")}</Button>}
|
||||
{snapshot.status.canReplace && <Button variant="warning" icon={<ReplaceIcon />} disabled={isBusy} onClick={() => void onRunIntegrationAction(t("integrations.busy.replacing"), "replace")}>{t("integrations.replaceMcp")}</Button>}
|
||||
{snapshot.status.canRemove && <Button variant="danger" icon={<RemoveIcon />} disabled={isBusy} onClick={() => void onRunIntegrationAction(t("integrations.busy.removing"), "remove")}>{t("integrations.removeMcp")}</Button>}
|
||||
<Button variant="secondary" icon={<RefreshIcon />} disabled={isBusy} onClick={() => void onReloadIntegrations()}>{t("integrations.refreshStatus")}</Button>
|
||||
</div>
|
||||
</section>
|
||||
</>;
|
||||
}
|
||||
|
||||
function OpenCodeIntegrationSections({
|
||||
snapshot,
|
||||
isBusy,
|
||||
onRunIntegrationAction,
|
||||
onUpdatePath,
|
||||
onReloadIntegrations,
|
||||
}: {
|
||||
snapshot: AgentSetupSnapshot;
|
||||
isBusy: boolean;
|
||||
onRunIntegrationAction: (label: string, action: AgentSetupAction) => Promise<void>;
|
||||
onUpdatePath: (key: keyof AgentSetupCommandPaths, value: string) => Promise<void>;
|
||||
onReloadIntegrations: () => Promise<void>;
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
return <>
|
||||
<section className="plugin-section">
|
||||
<div className="plugin-section-title"><small>{t("integrations.connection")}</small><strong>{t("integrations.globalSetup")}</strong></div>
|
||||
<div className="flex items-center justify-between p-3 rounded-2xl bg-blue-50/50 border border-blue-100/50">
|
||||
<div className="flex flex-col">
|
||||
<strong className="text-sm text-navy">{snapshot.opencodeStatus.label}</strong>
|
||||
<small className="text-xs text-slatecopy">{snapshot.opencodeStatus.details}</small>
|
||||
</div>
|
||||
<StatusPill tone={opencodeStatusTone(snapshot.opencodeStatus.state)}>{snapshot.opencodeStatus.state}</StatusPill>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="plugin-section">
|
||||
<div className="plugin-section-title"><small>{t("integrations.configuration")}</small><strong>{t("integrations.commandPaths")}</strong></div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<PathField label={t("integrations.opencodeCommand")} value={snapshot.commandPaths.opencode} placeholder="opencode" onSave={(value) => void onUpdatePath("opencode", value)} disabled={isBusy} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="plugin-section">
|
||||
<div className="plugin-section-title"><small>{t("integrations.actions")}</small><strong>{t("integrations.management")}</strong></div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{snapshot.opencodeStatus.canInstall && <Button variant="primary" icon={<InstallIcon />} disabled={isBusy} onClick={() => void onRunIntegrationAction(t("integrations.busy.installing"), "opencode-install")}>{t("integrations.installGlobal")}</Button>}
|
||||
{snapshot.opencodeStatus.canRemove && <Button variant="danger" icon={<RemoveIcon />} disabled={isBusy} onClick={() => void onRunIntegrationAction(t("integrations.busy.removing"), "opencode-remove")}>{t("integrations.removeGlobal")}</Button>}
|
||||
<Button variant="secondary" icon={<RefreshIcon />} disabled={isBusy} onClick={() => void onReloadIntegrations()}>{t("integrations.refreshStatus")}</Button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<details className="plugin-section group">
|
||||
<summary className="cursor-pointer list-none flex items-center justify-between">
|
||||
<div className="plugin-section-title"><small>{t("integrations.advanced")}</small><strong>{t("integrations.configPreview")}</strong></div>
|
||||
<span className="text-brand group-open:rotate-180 transition-transform"><NextIcon /></span>
|
||||
</summary>
|
||||
<pre className="mt-3 p-3 rounded-xl bg-navy/5 text-[10px] font-mono overflow-x-auto border border-navy/5">
|
||||
{JSON.stringify(snapshot.opencodePreview.configPreview, null, 2)}
|
||||
</pre>
|
||||
</details>
|
||||
</>;
|
||||
}
|
||||
|
||||
function CursorIntegrationSections({
|
||||
snapshot,
|
||||
isBusy,
|
||||
onRunIntegrationAction,
|
||||
onReloadIntegrations,
|
||||
}: {
|
||||
snapshot: AgentSetupSnapshot;
|
||||
isBusy: boolean;
|
||||
onRunIntegrationAction: (label: string, action: AgentSetupAction) => Promise<void>;
|
||||
onReloadIntegrations: () => Promise<void>;
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
return <>
|
||||
<section className="plugin-section">
|
||||
<div className="plugin-section-title"><small>{t("integrations.connection")}</small><strong>{t("integrations.globalMcp")}</strong></div>
|
||||
<div className="flex items-center justify-between p-3 rounded-2xl bg-blue-50/50 border border-blue-100/50">
|
||||
<div className="flex flex-col">
|
||||
<strong className="text-sm text-navy">{snapshot.cursorStatus.label}</strong>
|
||||
<small className="text-xs text-slatecopy">{snapshot.cursorStatus.details}</small>
|
||||
</div>
|
||||
<StatusPill tone={cursorStatusTone(snapshot.cursorStatus.state)}>{snapshot.cursorStatus.state}</StatusPill>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="plugin-section">
|
||||
<div className="plugin-section-title"><small>{t("integrations.actions")}</small><strong>{t("integrations.management")}</strong></div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{snapshot.cursorStatus.canInstall && <Button variant="primary" icon={<InstallIcon />} disabled={isBusy} onClick={() => void onRunIntegrationAction(t("integrations.busy.installing"), "cursor-install")}>{t("integrations.installMcp")}</Button>}
|
||||
{snapshot.cursorStatus.canReplace && <Button variant="warning" icon={<ReplaceIcon />} disabled={isBusy} onClick={() => void onRunIntegrationAction(t("integrations.busy.replacing"), "cursor-replace")}>{t("integrations.replaceMcp")}</Button>}
|
||||
{snapshot.cursorStatus.canRemove && <Button variant="danger" icon={<RemoveIcon />} disabled={isBusy} onClick={() => void onRunIntegrationAction(t("integrations.busy.removing"), "cursor-remove")}>{t("integrations.removeMcp")}</Button>}
|
||||
<Button variant="secondary" icon={<RefreshIcon />} disabled={isBusy} onClick={() => void onReloadIntegrations()}>{t("integrations.refreshStatus")}</Button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<details className="plugin-section group">
|
||||
<summary className="cursor-pointer list-none flex items-center justify-between">
|
||||
<div className="plugin-section-title"><small>{t("integrations.advanced")}</small><strong>{t("integrations.rulesPreview")}</strong></div>
|
||||
<span className="text-brand group-open:rotate-180 transition-transform"><NextIcon /></span>
|
||||
</summary>
|
||||
<p className="mt-3 text-xs text-slatecopy">{snapshot.cursorPreview.rulesPath}</p>
|
||||
<pre className="mt-3 p-3 rounded-xl bg-navy/5 text-[10px] font-mono overflow-x-auto border border-navy/5">
|
||||
{snapshot.cursorPreview.rulesContent}
|
||||
</pre>
|
||||
</details>
|
||||
</>;
|
||||
}
|
||||
|
|
@ -1,31 +1,17 @@
|
|||
import { useI18n } from "../i18n";
|
||||
import {
|
||||
IntegrationIcon,
|
||||
PathField,
|
||||
claudeStatusTone,
|
||||
cursorStatusTone,
|
||||
opencodeStatusTone,
|
||||
} from "./integrations-view-shared";
|
||||
import {
|
||||
FamiliarOSMcpServerSections,
|
||||
McpToolServersSection,
|
||||
McpToolkitSection,
|
||||
PiIntegrationSection,
|
||||
} from "./integrations-view-tooling-sections";
|
||||
import { IntegrationsDetailDialog } from "./integrations-view-detail-dialog";
|
||||
import { useIntegrationsViewState } from "./integrations-view-state";
|
||||
import * as Shared from "./shared";
|
||||
|
||||
const {
|
||||
Button,
|
||||
CloseIcon,
|
||||
ConfigureIcon,
|
||||
GlassCard,
|
||||
HookIcon,
|
||||
InstallIcon,
|
||||
MemoryIcon,
|
||||
RefreshIcon,
|
||||
RemoveIcon,
|
||||
ReplaceIcon,
|
||||
Spinner,
|
||||
StatusPill,
|
||||
} = Shared;
|
||||
|
|
@ -75,7 +61,6 @@ export function IntegrationsView() {
|
|||
</GlassCard>
|
||||
);
|
||||
}
|
||||
const integrationDialogTitleId = selectedId ? `integration-detail-title-${selectedId}` : undefined;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6 h-full overflow-y-auto pr-2">
|
||||
|
|
@ -129,202 +114,35 @@ export function IntegrationsView() {
|
|||
</div>
|
||||
|
||||
{selectedId && (
|
||||
<div className="plugin-config-overlay" role="dialog" aria-modal="true" aria-labelledby={integrationDialogTitleId}>
|
||||
<button className="plugin-config-backdrop" type="button" aria-label={t("integrations.closeAria")} onClick={() => setSelectedId(null)} />
|
||||
<GlassCard className="plugin-inspector">
|
||||
<div className="plugin-inspector-head">
|
||||
<div className="plugin-inspector-icon">
|
||||
<IntegrationIcon id={selectedId} />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="eyebrow">{t("integrations.detail")}</p>
|
||||
<h2 id={integrationDialogTitleId}>{selectedIntegrationName}</h2>
|
||||
</div>
|
||||
<Button variant="secondary" size="compact" icon={<CloseIcon />} onClick={() => setSelectedId(null)}>{t("integrations.close")}</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-5 mt-4">
|
||||
{(selectedId === "claude" || selectedId === "opencode" || selectedId === "cursor") && (
|
||||
<section className="plugin-section">
|
||||
<div className="plugin-section-title"><small>{t("integrations.commandSource")}</small><strong>{t("integrations.cliMode")}</strong></div>
|
||||
<p className="text-sm text-slatecopy">{t("integrations.linkToCentralPanel")}</p>
|
||||
<p className="text-xs text-slatecopy mt-2">{t(Shared.commandModeLabelKeys[snapshot.commandMode])}</p>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{selectedId === "mcp-tool-servers" && (
|
||||
<McpToolServersSection
|
||||
vanillaChatTools={vanillaChatTools}
|
||||
vanillaChatSaving={vanillaChatSaving}
|
||||
isBusy={isBusy}
|
||||
onToggleTool={toggleVanillaChatTool}
|
||||
onSaveTools={saveVanillaChatTools}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedId === "familiaros-mcp-server" && (
|
||||
<FamiliarOSMcpServerSections
|
||||
snapshot={snapshot}
|
||||
familiarosMcpTest={familiarosMcpTest}
|
||||
familiarosMcpPreview={familiarosMcpPreview}
|
||||
isBusy={isBusy}
|
||||
onChangeCommandMode={changeCommandMode}
|
||||
onSaveNodePath={(value) => updatePath("node", value)}
|
||||
onSelectPet={(petId) => void loadIntegrations(petId)}
|
||||
onTestServer={testFamiliarOSMcpServer}
|
||||
onCopyMcpJson={copyFamiliarOSMcpJson}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedId === "claude" && (
|
||||
<>
|
||||
<section className="plugin-section">
|
||||
<div className="plugin-section-title"><small>{t("integrations.connection")}</small><strong>{t("integrations.statusRouting")}</strong></div>
|
||||
<div className="flex items-center justify-between p-3 rounded-2xl bg-blue-50/50 border border-blue-100/50">
|
||||
<div className="flex flex-col">
|
||||
<strong className="text-sm text-navy">{snapshot.status.label}</strong>
|
||||
<small className="text-xs text-slatecopy">{snapshot.status.details}</small>
|
||||
</div>
|
||||
<StatusPill tone={claudeStatusTone(snapshot.status.state)}>{snapshot.status.state}</StatusPill>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="plugin-section">
|
||||
<div className="plugin-section-title"><small>{t("integrations.configuration")}</small><strong>{t("integrations.commandPaths")}</strong></div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<PathField label={t("integrations.claudeCommand")} value={snapshot.commandPaths.claude} placeholder="claude" onSave={(value) => updatePath("claude", value)} disabled={isBusy} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<section className="plugin-section">
|
||||
<div className="plugin-section-title"><small>{t("integrations.optional")}</small><strong>{t("integrations.claudeHooks")}</strong></div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<StatusPill tone={snapshot.hookStatus.status === "installed" ? "green" : "blue"}>{snapshot.hookStatus.status}</StatusPill>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Button variant="primary" size="compact" icon={<HookIcon />} disabled={isBusy} onClick={() => runIntegrationAction(t("integrations.busy.installingHooks"), "install-hooks")}>{t("integrations.installHooks")}</Button>
|
||||
<Button variant="danger" size="compact" icon={<RemoveIcon />} disabled={isBusy || snapshot.hookStatus.status === "needs_setup"} onClick={() => runIntegrationAction(t("integrations.busy.removingHooks"), "uninstall-hooks")}>{t("integrations.removeHooks")}</Button>
|
||||
</div>
|
||||
</section>
|
||||
<section className="plugin-section">
|
||||
<div className="plugin-section-title"><small>{t("integrations.included")}</small><strong>{t("integrations.instructions")}</strong></div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<StatusPill tone={snapshot.memoryStatus.state === "installed" ? "green" : "blue"}>{snapshot.memoryStatus.state}</StatusPill>
|
||||
</div>
|
||||
<Button variant="secondary" size="compact" icon={<MemoryIcon />} disabled={isBusy} onClick={() => runIntegrationAction(t("integrations.busy.updatingInstructions"), "install-memory")}>{t("integrations.updateInstructions")}</Button>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section className="plugin-section">
|
||||
<div className="plugin-section-title"><small>{t("integrations.actions")}</small><strong>{t("integrations.management")}</strong></div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{snapshot.status.canConfigure && <Button variant="primary" icon={<InstallIcon />} disabled={isBusy} onClick={() => runIntegrationAction(t("integrations.busy.installing"), "configure")}>{t("integrations.installMcp")}</Button>}
|
||||
{snapshot.status.canReplace && <Button variant="warning" icon={<ReplaceIcon />} disabled={isBusy} onClick={() => runIntegrationAction(t("integrations.busy.replacing"), "replace")}>{t("integrations.replaceMcp")}</Button>}
|
||||
{snapshot.status.canRemove && <Button variant="danger" icon={<RemoveIcon />} disabled={isBusy} onClick={() => runIntegrationAction(t("integrations.busy.removing"), "remove")}>{t("integrations.removeMcp")}</Button>}
|
||||
<Button variant="secondary" icon={<RefreshIcon />} disabled={isBusy} onClick={() => void loadIntegrations()}>{t("integrations.refreshStatus")}</Button>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
)}
|
||||
|
||||
{selectedId === "opencode" && (
|
||||
<>
|
||||
<section className="plugin-section">
|
||||
<div className="plugin-section-title"><small>{t("integrations.connection")}</small><strong>{t("integrations.globalSetup")}</strong></div>
|
||||
<div className="flex items-center justify-between p-3 rounded-2xl bg-blue-50/50 border border-blue-100/50">
|
||||
<div className="flex flex-col">
|
||||
<strong className="text-sm text-navy">{snapshot.opencodeStatus.label}</strong>
|
||||
<small className="text-xs text-slatecopy">{snapshot.opencodeStatus.details}</small>
|
||||
</div>
|
||||
<StatusPill tone={opencodeStatusTone(snapshot.opencodeStatus.state)}>{snapshot.opencodeStatus.state}</StatusPill>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="plugin-section">
|
||||
<div className="plugin-section-title"><small>{t("integrations.configuration")}</small><strong>{t("integrations.commandPaths")}</strong></div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<PathField label={t("integrations.opencodeCommand")} value={snapshot.commandPaths.opencode} placeholder="opencode" onSave={(value) => updatePath("opencode", value)} disabled={isBusy} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="plugin-section">
|
||||
<div className="plugin-section-title"><small>{t("integrations.actions")}</small><strong>{t("integrations.management")}</strong></div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{snapshot.opencodeStatus.canInstall && <Button variant="primary" icon={<InstallIcon />} disabled={isBusy} onClick={() => runIntegrationAction(t("integrations.busy.installing"), "opencode-install")}>{t("integrations.installGlobal")}</Button>}
|
||||
{snapshot.opencodeStatus.canRemove && <Button variant="danger" icon={<RemoveIcon />} disabled={isBusy} onClick={() => runIntegrationAction(t("integrations.busy.removing"), "opencode-remove")}>{t("integrations.removeGlobal")}</Button>}
|
||||
<Button variant="secondary" icon={<RefreshIcon />} disabled={isBusy} onClick={() => void loadIntegrations()}>{t("integrations.refreshStatus")}</Button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<details className="plugin-section group">
|
||||
<summary className="cursor-pointer list-none flex items-center justify-between">
|
||||
<div className="plugin-section-title"><small>{t("integrations.advanced")}</small><strong>{t("integrations.configPreview")}</strong></div>
|
||||
<span className="text-brand group-open:rotate-180 transition-transform"><Shared.NextIcon /></span>
|
||||
</summary>
|
||||
<pre className="mt-3 p-3 rounded-xl bg-navy/5 text-[10px] font-mono overflow-x-auto border border-navy/5">
|
||||
{JSON.stringify(snapshot.opencodePreview.configPreview, null, 2)}
|
||||
</pre>
|
||||
</details>
|
||||
</>
|
||||
)}
|
||||
|
||||
{selectedId === "cursor" && (
|
||||
<>
|
||||
<section className="plugin-section">
|
||||
<div className="plugin-section-title"><small>{t("integrations.connection")}</small><strong>{t("integrations.globalMcp")}</strong></div>
|
||||
<div className="flex items-center justify-between p-3 rounded-2xl bg-blue-50/50 border border-blue-100/50">
|
||||
<div className="flex flex-col">
|
||||
<strong className="text-sm text-navy">{snapshot.cursorStatus.label}</strong>
|
||||
<small className="text-xs text-slatecopy">{snapshot.cursorStatus.details}</small>
|
||||
</div>
|
||||
<StatusPill tone={cursorStatusTone(snapshot.cursorStatus.state)}>{snapshot.cursorStatus.state}</StatusPill>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="plugin-section">
|
||||
<div className="plugin-section-title"><small>{t("integrations.actions")}</small><strong>{t("integrations.management")}</strong></div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{snapshot.cursorStatus.canInstall && <Button variant="primary" icon={<InstallIcon />} disabled={isBusy} onClick={() => runIntegrationAction(t("integrations.busy.installing"), "cursor-install")}>{t("integrations.installMcp")}</Button>}
|
||||
{snapshot.cursorStatus.canReplace && <Button variant="warning" icon={<ReplaceIcon />} disabled={isBusy} onClick={() => runIntegrationAction(t("integrations.busy.replacing"), "cursor-replace")}>{t("integrations.replaceMcp")}</Button>}
|
||||
{snapshot.cursorStatus.canRemove && <Button variant="danger" icon={<RemoveIcon />} disabled={isBusy} onClick={() => runIntegrationAction(t("integrations.busy.removing"), "cursor-remove")}>{t("integrations.removeMcp")}</Button>}
|
||||
<Button variant="secondary" icon={<RefreshIcon />} disabled={isBusy} onClick={() => void loadIntegrations()}>{t("integrations.refreshStatus")}</Button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<details className="plugin-section group">
|
||||
<summary className="cursor-pointer list-none flex items-center justify-between">
|
||||
<div className="plugin-section-title"><small>{t("integrations.advanced")}</small><strong>{t("integrations.rulesPreview")}</strong></div>
|
||||
<span className="text-brand group-open:rotate-180 transition-transform"><Shared.NextIcon /></span>
|
||||
</summary>
|
||||
<p className="mt-3 text-xs text-slatecopy">{snapshot.cursorPreview.rulesPath}</p>
|
||||
<pre className="mt-3 p-3 rounded-xl bg-navy/5 text-[10px] font-mono overflow-x-auto border border-navy/5">
|
||||
{snapshot.cursorPreview.rulesContent}
|
||||
</pre>
|
||||
</details>
|
||||
</>
|
||||
)}
|
||||
|
||||
{selectedId === "pi" && <PiIntegrationSection />}
|
||||
|
||||
{selectedId === "mcp-toolkit" && (
|
||||
<McpToolkitSection
|
||||
selectedToolkitId={selectedToolkitId}
|
||||
onSelectedToolkitIdChange={setSelectedToolkitId}
|
||||
toolkitInstallMode={toolkitInstallMode}
|
||||
onToolkitInstallModeChange={setToolkitInstallMode}
|
||||
toolkitPersistentTarget={toolkitPersistentTarget}
|
||||
onToolkitPersistentTargetChange={setToolkitPersistentTarget}
|
||||
toolkitLastInstall={toolkitLastInstall}
|
||||
isBusy={isBusy}
|
||||
onInstallToolkitHost={installToolkitHost}
|
||||
onCopyText={copyText}
|
||||
onOpenDocs={openDocs}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</GlassCard>
|
||||
</div>
|
||||
<IntegrationsDetailDialog
|
||||
snapshot={snapshot}
|
||||
selectedId={selectedId}
|
||||
selectedIntegrationName={selectedIntegrationName}
|
||||
selectedToolkitId={selectedToolkitId}
|
||||
onSelectedIdChange={setSelectedId}
|
||||
onSelectedToolkitIdChange={setSelectedToolkitId}
|
||||
toolkitInstallMode={toolkitInstallMode}
|
||||
onToolkitInstallModeChange={setToolkitInstallMode}
|
||||
toolkitPersistentTarget={toolkitPersistentTarget}
|
||||
onToolkitPersistentTargetChange={setToolkitPersistentTarget}
|
||||
toolkitLastInstall={toolkitLastInstall}
|
||||
vanillaChatTools={vanillaChatTools}
|
||||
vanillaChatSaving={vanillaChatSaving}
|
||||
familiarosMcpPreview={familiarosMcpPreview}
|
||||
familiarosMcpTest={familiarosMcpTest}
|
||||
isBusy={isBusy}
|
||||
loadIntegrations={loadIntegrations}
|
||||
runIntegrationAction={runIntegrationAction}
|
||||
updatePath={updatePath}
|
||||
changeCommandMode={changeCommandMode}
|
||||
copyText={copyText}
|
||||
openDocs={openDocs}
|
||||
testFamiliarOSMcpServer={testFamiliarOSMcpServer}
|
||||
copyFamiliarOSMcpJson={copyFamiliarOSMcpJson}
|
||||
saveVanillaChatTools={saveVanillaChatTools}
|
||||
toggleVanillaChatTool={toggleVanillaChatTool}
|
||||
installToolkitHost={installToolkitHost}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
16
apps/desktop/tests/integrations-view-detail-dialog.test.ts
Normal file
16
apps/desktop/tests/integrations-view-detail-dialog.test.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const desktopRoot = process.env.FAMILIAROS_DESKTOP_ROOT ?? resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const integrationsViewSource = readFileSync(resolve(desktopRoot, "src/renderer/src/control-center/integrations-view.tsx"), "utf8");
|
||||
const integrationsViewDetailDialogSource = readFileSync(resolve(desktopRoot, "src/renderer/src/control-center/integrations-view-detail-dialog.tsx"), "utf8");
|
||||
|
||||
assert.match(integrationsViewSource, /from "\.\/integrations-view-detail-dialog"/, "Integrations view must import the extracted detail dialog seam.");
|
||||
assert.match(integrationsViewSource, /<IntegrationsDetailDialog/, "Integrations view must render the extracted detail dialog seam.");
|
||||
assert.match(integrationsViewDetailDialogSource, /export function IntegrationsDetailDialog/, "Integrations detail dialog seam must export the selected-integration overlay.");
|
||||
assert.match(integrationsViewDetailDialogSource, /<FamiliarOSMcpServerSections/, "Integrations detail dialog seam must retain the FamiliarOS MCP server section.");
|
||||
assert.match(integrationsViewDetailDialogSource, /integrations\.rulesPreview/, "Integrations detail dialog seam must retain the Cursor rules preview.");
|
||||
|
||||
console.error("Integrations detail dialog seam validation passed.");
|
||||
|
|
@ -5,20 +5,24 @@ import { fileURLToPath } from "node:url";
|
|||
|
||||
const desktopRoot = process.env.FAMILIAROS_DESKTOP_ROOT ?? resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const integrationsViewSource = readFileSync(resolve(desktopRoot, "src/renderer/src/control-center/integrations-view.tsx"), "utf8");
|
||||
const integrationsViewDetailDialogSource = readFileSync(resolve(desktopRoot, "src/renderer/src/control-center/integrations-view-detail-dialog.tsx"), "utf8");
|
||||
const integrationsViewSharedSource = readFileSync(resolve(desktopRoot, "src/renderer/src/control-center/integrations-view-shared.tsx"), "utf8");
|
||||
const integrationsViewToolingSectionsSource = readFileSync(resolve(desktopRoot, "src/renderer/src/control-center/integrations-view-tooling-sections.tsx"), "utf8");
|
||||
const integrationsCombinedSource = [
|
||||
integrationsViewSource,
|
||||
integrationsViewDetailDialogSource,
|
||||
integrationsViewSharedSource,
|
||||
integrationsViewToolingSectionsSource,
|
||||
].join("\n");
|
||||
|
||||
assert.match(integrationsViewSource, /from "\.\/integrations-view-detail-dialog"/, "Integrations view must import the extracted detail dialog seam.");
|
||||
assert.match(integrationsViewSource, /from "\.\/integrations-view-shared"/, "Integrations view must import the extracted shared helper seam.");
|
||||
assert.match(integrationsViewSource, /from "\.\/integrations-view-tooling-sections"/, "Integrations view must import the extracted tooling section seam.");
|
||||
assert.match(integrationsViewSource, /<McpToolServersSection/, "Integrations view must render the extracted MCP tool servers section.");
|
||||
assert.match(integrationsViewSource, /<FamiliarOSMcpServerSections/, "Integrations view must render the extracted FamiliarOS MCP server section.");
|
||||
assert.match(integrationsViewSource, /<PiIntegrationSection/, "Integrations view must render the extracted Pi guidance section.");
|
||||
assert.match(integrationsViewSource, /<McpToolkitSection/, "Integrations view must render the extracted curated toolkit section.");
|
||||
assert.match(integrationsViewDetailDialogSource, /from "\.\/integrations-view-tooling-sections"/, "Integrations detail dialog seam must import the extracted tooling section seam.");
|
||||
assert.match(integrationsViewDetailDialogSource, /export function IntegrationsDetailDialog/, "Integrations detail dialog seam must export the selected-integration overlay.");
|
||||
assert.match(integrationsViewDetailDialogSource, /<McpToolServersSection/, "Integrations detail dialog seam must render the extracted MCP tool servers section.");
|
||||
assert.match(integrationsViewDetailDialogSource, /<FamiliarOSMcpServerSections/, "Integrations detail dialog seam must render the extracted FamiliarOS MCP server section.");
|
||||
assert.match(integrationsViewDetailDialogSource, /<PiIntegrationSection/, "Integrations detail dialog seam must render the extracted Pi guidance section.");
|
||||
assert.match(integrationsViewDetailDialogSource, /<McpToolkitSection/, "Integrations detail dialog seam must render the extracted curated toolkit section.");
|
||||
|
||||
assert.match(integrationsViewSharedSource, /export function PathField/, "Integrations shared seam must export the command-path field helper.");
|
||||
assert.match(integrationsViewSharedSource, /export function IntegrationIcon/, "Integrations shared seam must export the integration icon helper.");
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue