diff --git a/apps/desktop/scripts/run-tests.mjs b/apps/desktop/scripts/run-tests.mjs index 08294e21..41692520 100644 --- a/apps/desktop/scripts/run-tests.mjs +++ b/apps/desktop/scripts/run-tests.mjs @@ -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", diff --git a/apps/desktop/src/check-packaging-contract.ts b/apps/desktop/src/check-packaging-contract.ts index f0a4f076..52f0ece3 100644 --- a/apps/desktop/src/check-packaging-contract.ts +++ b/apps/desktop/src/check-packaging-contract.ts @@ -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."); diff --git a/apps/desktop/src/renderer/src/control-center/codemap.md b/apps/desktop/src/renderer/src/control-center/codemap.md index 7505a5c3..60957507 100644 --- a/apps/desktop/src/renderer/src/control-center/codemap.md +++ b/apps/desktop/src/renderer/src/control-center/codemap.md @@ -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, diff --git a/apps/desktop/src/renderer/src/control-center/integrations-view-agent-sections.tsx b/apps/desktop/src/renderer/src/control-center/integrations-view-agent-sections.tsx new file mode 100644 index 00000000..3dc14ed7 --- /dev/null +++ b/apps/desktop/src/renderer/src/control-center/integrations-view-agent-sections.tsx @@ -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
+
{t("integrations.commandSource")}{t("integrations.cliMode")}
+

{t("integrations.linkToCentralPanel")}

+

{t(commandModeLabelKeys[snapshot.commandMode])}

+
; +} + +export function ClaudeIntegrationSections({ + snapshot, + isBusy, + onRunIntegrationAction, + onUpdatePath, + onReloadIntegrations, +}: { + snapshot: AgentSetupSnapshot; + isBusy: boolean; + onRunIntegrationAction: (label: string, action: AgentSetupAction) => Promise; + onUpdatePath: (key: keyof AgentSetupCommandPaths, value: string) => Promise; + onReloadIntegrations: () => Promise; +}) { + const { t } = useI18n(); + return <> +
+
{t("integrations.connection")}{t("integrations.statusRouting")}
+
+
+ {snapshot.status.label} + {snapshot.status.details} +
+ {snapshot.status.state} +
+
+ +
+
{t("integrations.configuration")}{t("integrations.commandPaths")}
+
+ void onUpdatePath("claude", value)} disabled={isBusy} /> +
+
+ +
+
+
{t("integrations.optional")}{t("integrations.claudeHooks")}
+
+ {snapshot.hookStatus.status} +
+
+ + +
+
+
+
{t("integrations.included")}{t("integrations.instructions")}
+
+ {snapshot.memoryStatus.state} +
+ +
+
+ +
+
{t("integrations.actions")}{t("integrations.management")}
+
+ {snapshot.status.canConfigure && } + {snapshot.status.canReplace && } + {snapshot.status.canRemove && } + +
+
+ ; +} + +export function OpenCodeIntegrationSections({ + snapshot, + isBusy, + onRunIntegrationAction, + onUpdatePath, + onReloadIntegrations, +}: { + snapshot: AgentSetupSnapshot; + isBusy: boolean; + onRunIntegrationAction: (label: string, action: AgentSetupAction) => Promise; + onUpdatePath: (key: keyof AgentSetupCommandPaths, value: string) => Promise; + onReloadIntegrations: () => Promise; +}) { + const { t } = useI18n(); + return <> +
+
{t("integrations.connection")}{t("integrations.globalSetup")}
+
+
+ {snapshot.opencodeStatus.label} + {snapshot.opencodeStatus.details} +
+ {snapshot.opencodeStatus.state} +
+
+ +
+
{t("integrations.configuration")}{t("integrations.commandPaths")}
+
+ void onUpdatePath("opencode", value)} disabled={isBusy} /> +
+
+ +
+
{t("integrations.actions")}{t("integrations.management")}
+
+ {snapshot.opencodeStatus.canInstall && } + {snapshot.opencodeStatus.canRemove && } + +
+
+ +
+ +
{t("integrations.advanced")}{t("integrations.configPreview")}
+ +
+
+        {JSON.stringify(snapshot.opencodePreview.configPreview, null, 2)}
+      
+
+ ; +} + +export function CursorIntegrationSections({ + snapshot, + isBusy, + onRunIntegrationAction, + onReloadIntegrations, +}: { + snapshot: AgentSetupSnapshot; + isBusy: boolean; + onRunIntegrationAction: (label: string, action: AgentSetupAction) => Promise; + onReloadIntegrations: () => Promise; +}) { + const { t } = useI18n(); + return <> +
+
{t("integrations.connection")}{t("integrations.globalMcp")}
+
+
+ {snapshot.cursorStatus.label} + {snapshot.cursorStatus.details} +
+ {snapshot.cursorStatus.state} +
+
+ +
+
{t("integrations.actions")}{t("integrations.management")}
+
+ {snapshot.cursorStatus.canInstall && } + {snapshot.cursorStatus.canReplace && } + {snapshot.cursorStatus.canRemove && } + +
+
+ +
+ +
{t("integrations.advanced")}{t("integrations.rulesPreview")}
+ +
+

{snapshot.cursorPreview.rulesPath}

+
+        {snapshot.cursorPreview.rulesContent}
+      
+
+ ; +} diff --git a/apps/desktop/src/renderer/src/control-center/integrations-view-detail-dialog.tsx b/apps/desktop/src/renderer/src/control-center/integrations-view-detail-dialog.tsx index f12fa1cc..0d35a52c 100644 --- a/apps/desktop/src/renderer/src/control-center/integrations-view-detail-dialog.tsx +++ b/apps/desktop/src/renderer/src/control-center/integrations-view-detail-dialog.tsx @@ -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") && ( -
-
{t("integrations.commandSource")}{t("integrations.cliMode")}
-

{t("integrations.linkToCentralPanel")}

-

{t(commandModeLabelKeys[snapshot.commandMode])}

-
- )} + {selectedId === "mcp-tool-servers" && ( {children} ; } - -function ClaudeIntegrationSections({ - snapshot, - isBusy, - onRunIntegrationAction, - onUpdatePath, - onReloadIntegrations, -}: { - snapshot: AgentSetupSnapshot; - isBusy: boolean; - onRunIntegrationAction: (label: string, action: AgentSetupAction) => Promise; - onUpdatePath: (key: keyof AgentSetupCommandPaths, value: string) => Promise; - onReloadIntegrations: () => Promise; -}) { - const { t } = useI18n(); - return <> -
-
{t("integrations.connection")}{t("integrations.statusRouting")}
-
-
- {snapshot.status.label} - {snapshot.status.details} -
- {snapshot.status.state} -
-
- -
-
{t("integrations.configuration")}{t("integrations.commandPaths")}
-
- void onUpdatePath("claude", value)} disabled={isBusy} /> -
-
- -
-
-
{t("integrations.optional")}{t("integrations.claudeHooks")}
-
- {snapshot.hookStatus.status} -
-
- - -
-
-
-
{t("integrations.included")}{t("integrations.instructions")}
-
- {snapshot.memoryStatus.state} -
- -
-
- -
-
{t("integrations.actions")}{t("integrations.management")}
-
- {snapshot.status.canConfigure && } - {snapshot.status.canReplace && } - {snapshot.status.canRemove && } - -
-
- ; -} - -function OpenCodeIntegrationSections({ - snapshot, - isBusy, - onRunIntegrationAction, - onUpdatePath, - onReloadIntegrations, -}: { - snapshot: AgentSetupSnapshot; - isBusy: boolean; - onRunIntegrationAction: (label: string, action: AgentSetupAction) => Promise; - onUpdatePath: (key: keyof AgentSetupCommandPaths, value: string) => Promise; - onReloadIntegrations: () => Promise; -}) { - const { t } = useI18n(); - return <> -
-
{t("integrations.connection")}{t("integrations.globalSetup")}
-
-
- {snapshot.opencodeStatus.label} - {snapshot.opencodeStatus.details} -
- {snapshot.opencodeStatus.state} -
-
- -
-
{t("integrations.configuration")}{t("integrations.commandPaths")}
-
- void onUpdatePath("opencode", value)} disabled={isBusy} /> -
-
- -
-
{t("integrations.actions")}{t("integrations.management")}
-
- {snapshot.opencodeStatus.canInstall && } - {snapshot.opencodeStatus.canRemove && } - -
-
- -
- -
{t("integrations.advanced")}{t("integrations.configPreview")}
- -
-
-        {JSON.stringify(snapshot.opencodePreview.configPreview, null, 2)}
-      
-
- ; -} - -function CursorIntegrationSections({ - snapshot, - isBusy, - onRunIntegrationAction, - onReloadIntegrations, -}: { - snapshot: AgentSetupSnapshot; - isBusy: boolean; - onRunIntegrationAction: (label: string, action: AgentSetupAction) => Promise; - onReloadIntegrations: () => Promise; -}) { - const { t } = useI18n(); - return <> -
-
{t("integrations.connection")}{t("integrations.globalMcp")}
-
-
- {snapshot.cursorStatus.label} - {snapshot.cursorStatus.details} -
- {snapshot.cursorStatus.state} -
-
- -
-
{t("integrations.actions")}{t("integrations.management")}
-
- {snapshot.cursorStatus.canInstall && } - {snapshot.cursorStatus.canReplace && } - {snapshot.cursorStatus.canRemove && } - -
-
- -
- -
{t("integrations.advanced")}{t("integrations.rulesPreview")}
- -
-

{snapshot.cursorPreview.rulesPath}

-
-        {snapshot.cursorPreview.rulesContent}
-      
-
- ; -} diff --git a/apps/desktop/tests/integrations-view-agent-sections.test.ts b/apps/desktop/tests/integrations-view-agent-sections.test.ts new file mode 100644 index 00000000..f8efe1b8 --- /dev/null +++ b/apps/desktop/tests/integrations-view-agent-sections.test.ts @@ -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."); diff --git a/apps/desktop/tests/integrations-view-detail-dialog.test.ts b/apps/desktop/tests/integrations-view-detail-dialog.test.ts index 4f6cb279..e722dafe 100644 --- a/apps/desktop/tests/integrations-view-detail-dialog.test.ts +++ b/apps/desktop/tests/integrations-view-detail-dialog.test.ts @@ -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, /