Extract integrations agent sections seam
This commit is contained in:
parent
af8521801f
commit
4949f5f2e8
8 changed files with 250 additions and 191 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-agent-sections.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",
|
||||
|
|
|
|||
|
|
@ -131,6 +131,7 @@ const controlCenterFamiliarsHelpersSource = readFileSync(join(appDir, "src", "re
|
|||
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 controlCenterIntegrationsAgentSectionsSource = readFileSync(join(appDir, "src", "renderer", "src", "control-center", "integrations-view-agent-sections.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");
|
||||
|
|
@ -237,6 +238,7 @@ const agentSetupCombinedSource = `${agentSetupSource}\n${agentSetupActionsSource
|
|||
const controlCenterIntegrationsCombinedSource = [
|
||||
controlCenterIntegrationsSource,
|
||||
controlCenterIntegrationsDetailDialogSource,
|
||||
controlCenterIntegrationsAgentSectionsSource,
|
||||
controlCenterIntegrationsSharedSource,
|
||||
controlCenterIntegrationsToolingSectionsSource,
|
||||
controlCenterIntegrationsMcpToolkitSectionSource,
|
||||
|
|
@ -608,6 +610,8 @@ assert.match(controlCenterIntegrationsSource, /from "\.\/integrations-view-detai
|
|||
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(controlCenterIntegrationsDetailDialogSource, /export function IntegrationsDetailDialog/, "Control Center integrations detail dialog seam must export the selected-integration overlay.");
|
||||
assert.match(controlCenterIntegrationsDetailDialogSource, /from "\.\/integrations-view-agent-sections"/, "Control Center integrations detail dialog seam must import the extracted agent sections.");
|
||||
assert.match(controlCenterIntegrationsAgentSectionsSource, /export function IntegrationCommandModeSection/, "Control Center integrations agent seam must export the command-mode notice.");
|
||||
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.");
|
||||
|
|
|
|||
|
|
@ -25,8 +25,9 @@ surface they depend on.
|
|||
- `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.
|
||||
the tooling/Pi/toolkit dialog composition, and the overlay shell.
|
||||
- `integrations-view-agent-sections.tsx` owns the Claude/OpenCode/Cursor detail
|
||||
sections plus the shared command-mode notice.
|
||||
- `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,211 @@
|
|||
import { useI18n } from "../i18n";
|
||||
import {
|
||||
PathField,
|
||||
claudeStatusTone,
|
||||
cursorStatusTone,
|
||||
opencodeStatusTone,
|
||||
} from "./integrations-view-shared";
|
||||
import * as Shared from "./shared";
|
||||
import type {
|
||||
AgentSetupAction,
|
||||
AgentSetupCommandPaths,
|
||||
AgentSetupSnapshot,
|
||||
} from "./shared";
|
||||
|
||||
const {
|
||||
Button,
|
||||
HookIcon,
|
||||
InstallIcon,
|
||||
MemoryIcon,
|
||||
NextIcon,
|
||||
RefreshIcon,
|
||||
RemoveIcon,
|
||||
ReplaceIcon,
|
||||
StatusPill,
|
||||
commandModeLabelKeys,
|
||||
} = Shared;
|
||||
|
||||
export function IntegrationCommandModeSection({
|
||||
snapshot,
|
||||
selectedId,
|
||||
}: {
|
||||
snapshot: AgentSetupSnapshot;
|
||||
selectedId: string;
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
if (selectedId !== "claude" && selectedId !== "opencode" && selectedId !== "cursor") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <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>;
|
||||
}
|
||||
|
||||
export 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>
|
||||
</>;
|
||||
}
|
||||
|
||||
export 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>
|
||||
</>;
|
||||
}
|
||||
|
||||
export 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,12 +1,12 @@
|
|||
import type { ReactNode } from "react";
|
||||
import { useI18n } from "../i18n";
|
||||
import {
|
||||
IntegrationIcon,
|
||||
PathField,
|
||||
claudeStatusTone,
|
||||
cursorStatusTone,
|
||||
opencodeStatusTone,
|
||||
} from "./integrations-view-shared";
|
||||
ClaudeIntegrationSections,
|
||||
CursorIntegrationSections,
|
||||
IntegrationCommandModeSection,
|
||||
OpenCodeIntegrationSections,
|
||||
} from "./integrations-view-agent-sections";
|
||||
import { IntegrationIcon } from "./integrations-view-shared";
|
||||
import {
|
||||
FamiliarOSMcpServerSections,
|
||||
McpToolServersSection,
|
||||
|
|
@ -28,15 +28,6 @@ import type {
|
|||
const {
|
||||
Button,
|
||||
CloseIcon,
|
||||
HookIcon,
|
||||
InstallIcon,
|
||||
MemoryIcon,
|
||||
NextIcon,
|
||||
RefreshIcon,
|
||||
RemoveIcon,
|
||||
ReplaceIcon,
|
||||
StatusPill,
|
||||
commandModeLabelKeys,
|
||||
} = Shared;
|
||||
|
||||
type IntegrationsDetailDialogProps = {
|
||||
|
|
@ -109,13 +100,7 @@ export function IntegrationsDetailDialog({
|
|||
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>
|
||||
)}
|
||||
<IntegrationCommandModeSection snapshot={snapshot} selectedId={selectedId} />
|
||||
|
||||
{selectedId === "mcp-tool-servers" && (
|
||||
<McpToolServersSection
|
||||
|
|
@ -219,169 +204,3 @@ function GlassDialog({
|
|||
<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>
|
||||
</>;
|
||||
}
|
||||
|
|
|
|||
17
apps/desktop/tests/integrations-view-agent-sections.test.ts
Normal file
17
apps/desktop/tests/integrations-view-agent-sections.test.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
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 integrationsViewDetailDialogSource = readFileSync(resolve(desktopRoot, "src/renderer/src/control-center/integrations-view-detail-dialog.tsx"), "utf8");
|
||||
const integrationsViewAgentSectionsSource = readFileSync(resolve(desktopRoot, "src/renderer/src/control-center/integrations-view-agent-sections.tsx"), "utf8");
|
||||
|
||||
assert.match(integrationsViewDetailDialogSource, /from "\.\/integrations-view-agent-sections"/, "Integrations detail dialog must import the extracted agent sections seam.");
|
||||
assert.match(integrationsViewAgentSectionsSource, /export function IntegrationCommandModeSection/, "Integrations agent seam must export the command-mode notice.");
|
||||
assert.match(integrationsViewAgentSectionsSource, /export function ClaudeIntegrationSections/, "Integrations agent seam must export the Claude detail sections.");
|
||||
assert.match(integrationsViewAgentSectionsSource, /export function OpenCodeIntegrationSections/, "Integrations agent seam must export the OpenCode detail sections.");
|
||||
assert.match(integrationsViewAgentSectionsSource, /export function CursorIntegrationSections/, "Integrations agent seam must export the Cursor detail sections.");
|
||||
assert.match(integrationsViewAgentSectionsSource, /integrations\.rulesPreview/, "Integrations agent seam must retain the Cursor rules preview.");
|
||||
|
||||
console.error("Integrations agent sections seam validation passed.");
|
||||
|
|
@ -6,11 +6,13 @@ 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 integrationsViewAgentSectionsSource = readFileSync(resolve(desktopRoot, "src/renderer/src/control-center/integrations-view-agent-sections.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.");
|
||||
assert.match(integrationsViewDetailDialogSource, /from "\.\/integrations-view-agent-sections"/, "Integrations detail dialog seam must import the extracted agent sections.");
|
||||
assert.match(integrationsViewAgentSectionsSource, /integrations\.rulesPreview/, "Integrations agent seam must retain the Cursor rules preview.");
|
||||
|
||||
console.error("Integrations detail dialog seam validation passed.");
|
||||
|
|
|
|||
|
|
@ -6,19 +6,23 @@ 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 integrationsViewAgentSectionsSource = readFileSync(resolve(desktopRoot, "src/renderer/src/control-center/integrations-view-agent-sections.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,
|
||||
integrationsViewAgentSectionsSource,
|
||||
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(integrationsViewDetailDialogSource, /from "\.\/integrations-view-agent-sections"/, "Integrations detail dialog seam must import the extracted agent sections.");
|
||||
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(integrationsViewAgentSectionsSource, /export function IntegrationCommandModeSection/, "Integrations agent seam must export the command-mode notice.");
|
||||
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.");
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue