diff --git a/src/components/settings/integrations-panel.tsx b/src/components/settings/integrations-panel.tsx index f97c181..855c326 100644 --- a/src/components/settings/integrations-panel.tsx +++ b/src/components/settings/integrations-panel.tsx @@ -49,12 +49,7 @@ import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { ErrorState } from "@/components/ui/error-state"; import { Skeleton } from "@/components/ui/skeleton"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogTitle, -} from "@/components/ui/dialog"; +import { Dialog, DialogContent, DialogDescription, DialogTitle } from "@/components/ui/dialog"; import { PUBLISHERS_QUERY_KEY } from "@/lib/publishers"; import { SOCIAL_QUERY_KEY } from "@/lib/social"; function XMarkIcon({ className }: { className?: string }) { @@ -128,19 +123,19 @@ export function IntegrationsPanel() {

Add-ons

- ClippyOS is an OS. These integrations are add-ons. Core AI is required. Daytona is - the browser runtime for Social Computer Use. Keys stay on the server. + ClippyOS is an OS. These integrations are add-ons. Core AI is required. Daytona is the + browser runtime for Social Computer Use. Keys stay on the server.

{query.data.role === "member" && inheritWorkspaceApis ? (

- You’re using the owner’s workspace APIs. Ask an owner to turn that off if you - need to connect your own keys. + You’re using the owner’s workspace APIs. Ask an owner to turn that off if you need to + connect your own keys.

) : null} {query.data.role === "member" && !inheritWorkspaceApis ? (

- These keys are yours. They don’t use the owner’s APIs unless an owner shares - workspace APIs with this login. + These keys are yours. They don’t use the owner’s APIs unless an owner shares workspace + APIs with this login.

) : null}
@@ -150,11 +145,7 @@ export function IntegrationsPanel() { key={id} id={id} isAdmin={Boolean(isAdmin)} - canEdit={ - id === "whop" - ? Boolean(isAdmin) - : canEditIntegrations - } + canEdit={id === "whop" ? Boolean(isAdmin) : canEditIntegrations} last4={query.data.items[id].last4} health={query.data.items[id].health} lastTestedAt={query.data.items[id].lastTestedAt} @@ -173,7 +164,8 @@ export function IntegrationsPanel() {

ClippyOS MCP

- Remote MCP URL plus scoped connector tokens for Grok Bot and Cursor. Publish still honors Approvals. + Remote MCP URL plus scoped connector tokens for Grok Bot and Cursor. Publish still + honors Approvals.

@@ -215,12 +207,20 @@ function IntegrationCard({ const [pendingDisconnect, setPendingDisconnect] = useState(false); const [fields, setFields] = useState>({}); const configured = health !== "not_configured"; + // Test always checks the credential already stored on the server, never the value + // typed above — Save first, or a correct key you just pasted comes back "rejected". + const hasUnsavedFields = Object.values(fields).some((value) => value.trim().length > 0); useEffect(() => { if (id !== "x") return; function onMessage(event: MessageEvent) { if (event.origin !== window.location.origin) return; - const data = event.data as { source?: string; ok?: boolean; provider?: string; error?: string }; + const data = event.data as { + source?: string; + ok?: boolean; + provider?: string; + error?: string; + }; if (!data || data.source !== "clippy-social-oauth") return; if (data.provider && data.provider !== "x") return; void queryClient.invalidateQueries({ queryKey: INTEGRATIONS_QUERY_KEY }); @@ -345,13 +345,9 @@ function IntegrationCard({ {copy.required ? Required : null}

{ADDON_META[id].requiredFor}

-

- Used by: {ADDON_META[id].usedBy.join(" · ")} -

+

Used by: {ADDON_META[id].usedBy.join(" · ")}

- {lastTestedAt - ? `Last tested ${formatRelativeTime(lastTestedAt)}` - : "Not tested yet"} + {lastTestedAt ? `Last tested ${formatRelativeTime(lastTestedAt)}` : "Not tested yet"} {lastError ? ` · ${lastError}` : null}

{id === "x" ? ( @@ -377,7 +373,11 @@ function IntegrationCard({

) : null} - {id === "ai" && canEdit ?
: null} + {id === "ai" && canEdit ? ( +
+ +
+ ) : null} {canEdit ? (
@@ -387,7 +387,9 @@ function IntegrationCard({ label="API key" value={fields.key ?? ""} onChange={(value) => setFields({ key: value })} - placeholder={configured ? "•••• stored on the server" : "Paste the xAI / Grok API key"} + placeholder={ + configured ? "•••• stored on the server" : "Paste the xAI / Grok API key" + } /> ) : null} {id === "crayo" ? ( @@ -401,7 +403,8 @@ function IntegrationCard({ />

From crayo.ai → Developer API. A key saved here is used ahead of the deploy’s - CRAYO_API_KEY, so you can rotate it without a redeploy. Test calls GET /v1/account (free). + CRAYO_API_KEY, so you can rotate it without a redeploy. Test calls GET /v1/account + (free).

) : null} @@ -460,8 +463,8 @@ function IntegrationCard({ placeholder={configured ? "•••• stored on the server" : "lin_api_…"} />

- Personal API key or OAuth. Map team, project, and Kanban columns in the Linear section - below. Test never creates an issue. + Personal API key or OAuth. Map team, project, and Kanban columns in the Linear + section below. Test never creates an issue.

) : null} @@ -547,11 +550,11 @@ function IntegrationCard({ />

Social Machine defaults to the Linux container snapshot daytona-medium (Windows - snapshots need a Daytona plan that includes them). Hibernate auto-stops the - sandbox after idle minutes (container class has no hot pause; the filesystem - persists across stop). Clock is Australia/Sydney. Start auto-provisions a - free country-matched HTTP proxy; paste a paid residential URL if you have one. - Test Connection and Test proxy never start a sandbox. + snapshots need a Daytona plan that includes them). Hibernate auto-stops the sandbox + after idle minutes (container class has no hot pause; the filesystem persists across + stop). Clock is Australia/Sydney. Start auto-provisions a free country-matched HTTP + proxy; paste a paid residential URL if you have one. Test Connection and Test proxy + never start a sandbox.

) : null} @@ -735,15 +738,16 @@ function IntegrationCard({

- User-context OAuth only. Tokens stay on the server. Test Connection never posts. - X has no draft API — draft jobs stay local until Publish. + User-context OAuth only. Tokens stay on the server. Test Connection never posts. X + has no draft API — draft jobs stay local until Publish.

) : null} {configured ? (

- A key is already stored{last4 ? ` (ends …${last4})` : ""}. Paste a new one above and Save to - replace it; fields you leave blank keep their current values. Disconnect removes it. + A key is already stored{last4 ? ` (ends …${last4})` : ""}. Paste a new one above and + Save to replace it; fields you leave blank keep their current values. Disconnect + removes it.

) : null} @@ -327,7 +349,9 @@ function AgentPage() { crayoReady={crayoReady} starting={start.isPending} onChange={(draft) => - setCards((current) => current.map((row) => (row.id === card.id ? { ...row, draft } : row))) + setCards((current) => + current.map((row) => (row.id === card.id ? { ...row, draft } : row)), + ) } onDismiss={() => setCards((current) => current.filter((row) => row.id !== card.id))} onRun={(input) => { @@ -341,8 +365,8 @@ function AgentPage() {

Hermes Agent

- General clipping operator with Crayo specialities. Type /short, /voice, /image, or /autoclip — a - specialty card pops in. Free text uses the planner. + General clipping operator with Crayo specialities. Type /short, /voice, /image, or + /autoclip — a specialty card pops in. Free text uses the planner.

@@ -371,7 +395,9 @@ function AgentPage() { {moreOpen ? (
- {AGENT_PRESETS.filter((id) => !(CRAYO_AGENT_PRESETS as readonly string[]).includes(id)).map((id) => ( + {AGENT_PRESETS.filter( + (id) => !(CRAYO_AGENT_PRESETS as readonly string[]).includes(id), + ).map((id) => (