availsync 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.adal/skills/stripe-best-practices/SKILL.md +42 -0
- package/.adal/skills/stripe-best-practices/references/billing.md +36 -0
- package/.adal/skills/stripe-best-practices/references/connect.md +48 -0
- package/.adal/skills/stripe-best-practices/references/payments.md +79 -0
- package/.adal/skills/stripe-best-practices/references/security.md +109 -0
- package/.adal/skills/stripe-best-practices/references/treasury.md +16 -0
- package/.adal/skills/stripe-projects/SKILL.md +139 -0
- package/.adal/skills/upgrade-stripe/SKILL.md +185 -0
- package/.agents/skills/stripe-best-practices/SKILL.md +42 -0
- package/.agents/skills/stripe-best-practices/references/billing.md +36 -0
- package/.agents/skills/stripe-best-practices/references/connect.md +48 -0
- package/.agents/skills/stripe-best-practices/references/payments.md +79 -0
- package/.agents/skills/stripe-best-practices/references/security.md +109 -0
- package/.agents/skills/stripe-best-practices/references/treasury.md +16 -0
- package/.agents/skills/stripe-projects/SKILL.md +139 -0
- package/.agents/skills/upgrade-stripe/SKILL.md +185 -0
- package/.augment/skills/stripe-best-practices/SKILL.md +42 -0
- package/.augment/skills/stripe-best-practices/references/billing.md +36 -0
- package/.augment/skills/stripe-best-practices/references/connect.md +48 -0
- package/.augment/skills/stripe-best-practices/references/payments.md +79 -0
- package/.augment/skills/stripe-best-practices/references/security.md +109 -0
- package/.augment/skills/stripe-best-practices/references/treasury.md +16 -0
- package/.augment/skills/stripe-projects/SKILL.md +139 -0
- package/.augment/skills/upgrade-stripe/SKILL.md +185 -0
- package/.bob/skills/stripe-best-practices/SKILL.md +42 -0
- package/.bob/skills/stripe-best-practices/references/billing.md +36 -0
- package/.bob/skills/stripe-best-practices/references/connect.md +48 -0
- package/.bob/skills/stripe-best-practices/references/payments.md +79 -0
- package/.bob/skills/stripe-best-practices/references/security.md +109 -0
- package/.bob/skills/stripe-best-practices/references/treasury.md +16 -0
- package/.bob/skills/stripe-projects/SKILL.md +139 -0
- package/.bob/skills/upgrade-stripe/SKILL.md +185 -0
- package/.claude/settings.local.json +7 -0
- package/.claude/skills/stripe-best-practices/SKILL.md +42 -0
- package/.claude/skills/stripe-best-practices/references/billing.md +36 -0
- package/.claude/skills/stripe-best-practices/references/connect.md +48 -0
- package/.claude/skills/stripe-best-practices/references/payments.md +79 -0
- package/.claude/skills/stripe-best-practices/references/security.md +109 -0
- package/.claude/skills/stripe-best-practices/references/treasury.md +16 -0
- package/.claude/skills/stripe-projects/SKILL.md +139 -0
- package/.claude/skills/upgrade-stripe/SKILL.md +185 -0
- package/.codebuddy/skills/stripe-best-practices/SKILL.md +42 -0
- package/.codebuddy/skills/stripe-best-practices/references/billing.md +36 -0
- package/.codebuddy/skills/stripe-best-practices/references/connect.md +48 -0
- package/.codebuddy/skills/stripe-best-practices/references/payments.md +79 -0
- package/.codebuddy/skills/stripe-best-practices/references/security.md +109 -0
- package/.codebuddy/skills/stripe-best-practices/references/treasury.md +16 -0
- package/.codebuddy/skills/stripe-projects/SKILL.md +139 -0
- package/.codebuddy/skills/upgrade-stripe/SKILL.md +185 -0
- package/.commandcode/skills/stripe-best-practices/SKILL.md +42 -0
- package/.commandcode/skills/stripe-best-practices/references/billing.md +36 -0
- package/.commandcode/skills/stripe-best-practices/references/connect.md +48 -0
- package/.commandcode/skills/stripe-best-practices/references/payments.md +79 -0
- package/.commandcode/skills/stripe-best-practices/references/security.md +109 -0
- package/.commandcode/skills/stripe-best-practices/references/treasury.md +16 -0
- package/.commandcode/skills/stripe-projects/SKILL.md +139 -0
- package/.commandcode/skills/upgrade-stripe/SKILL.md +185 -0
- package/.continue/skills/stripe-best-practices/SKILL.md +42 -0
- package/.continue/skills/stripe-best-practices/references/billing.md +36 -0
- package/.continue/skills/stripe-best-practices/references/connect.md +48 -0
- package/.continue/skills/stripe-best-practices/references/payments.md +79 -0
- package/.continue/skills/stripe-best-practices/references/security.md +109 -0
- package/.continue/skills/stripe-best-practices/references/treasury.md +16 -0
- package/.continue/skills/stripe-projects/SKILL.md +139 -0
- package/.continue/skills/upgrade-stripe/SKILL.md +185 -0
- package/.cortex/skills/stripe-best-practices/SKILL.md +42 -0
- package/.cortex/skills/stripe-best-practices/references/billing.md +36 -0
- package/.cortex/skills/stripe-best-practices/references/connect.md +48 -0
- package/.cortex/skills/stripe-best-practices/references/payments.md +79 -0
- package/.cortex/skills/stripe-best-practices/references/security.md +109 -0
- package/.cortex/skills/stripe-best-practices/references/treasury.md +16 -0
- package/.cortex/skills/stripe-projects/SKILL.md +139 -0
- package/.cortex/skills/upgrade-stripe/SKILL.md +185 -0
- package/.crush/skills/stripe-best-practices/SKILL.md +42 -0
- package/.crush/skills/stripe-best-practices/references/billing.md +36 -0
- package/.crush/skills/stripe-best-practices/references/connect.md +48 -0
- package/.crush/skills/stripe-best-practices/references/payments.md +79 -0
- package/.crush/skills/stripe-best-practices/references/security.md +109 -0
- package/.crush/skills/stripe-best-practices/references/treasury.md +16 -0
- package/.crush/skills/stripe-projects/SKILL.md +139 -0
- package/.crush/skills/upgrade-stripe/SKILL.md +185 -0
- package/.env.example +20 -0
- package/.factory/skills/stripe-best-practices/SKILL.md +42 -0
- package/.factory/skills/stripe-best-practices/references/billing.md +36 -0
- package/.factory/skills/stripe-best-practices/references/connect.md +48 -0
- package/.factory/skills/stripe-best-practices/references/payments.md +79 -0
- package/.factory/skills/stripe-best-practices/references/security.md +109 -0
- package/.factory/skills/stripe-best-practices/references/treasury.md +16 -0
- package/.factory/skills/stripe-projects/SKILL.md +139 -0
- package/.factory/skills/upgrade-stripe/SKILL.md +185 -0
- package/.goose/skills/stripe-best-practices/SKILL.md +42 -0
- package/.goose/skills/stripe-best-practices/references/billing.md +36 -0
- package/.goose/skills/stripe-best-practices/references/connect.md +48 -0
- package/.goose/skills/stripe-best-practices/references/payments.md +79 -0
- package/.goose/skills/stripe-best-practices/references/security.md +109 -0
- package/.goose/skills/stripe-best-practices/references/treasury.md +16 -0
- package/.goose/skills/stripe-projects/SKILL.md +139 -0
- package/.goose/skills/upgrade-stripe/SKILL.md +185 -0
- package/.iflow/skills/stripe-best-practices/SKILL.md +42 -0
- package/.iflow/skills/stripe-best-practices/references/billing.md +36 -0
- package/.iflow/skills/stripe-best-practices/references/connect.md +48 -0
- package/.iflow/skills/stripe-best-practices/references/payments.md +79 -0
- package/.iflow/skills/stripe-best-practices/references/security.md +109 -0
- package/.iflow/skills/stripe-best-practices/references/treasury.md +16 -0
- package/.iflow/skills/stripe-projects/SKILL.md +139 -0
- package/.iflow/skills/upgrade-stripe/SKILL.md +185 -0
- package/.junie/skills/stripe-best-practices/SKILL.md +42 -0
- package/.junie/skills/stripe-best-practices/references/billing.md +36 -0
- package/.junie/skills/stripe-best-practices/references/connect.md +48 -0
- package/.junie/skills/stripe-best-practices/references/payments.md +79 -0
- package/.junie/skills/stripe-best-practices/references/security.md +109 -0
- package/.junie/skills/stripe-best-practices/references/treasury.md +16 -0
- package/.junie/skills/stripe-projects/SKILL.md +139 -0
- package/.junie/skills/upgrade-stripe/SKILL.md +185 -0
- package/.kilocode/skills/stripe-best-practices/SKILL.md +42 -0
- package/.kilocode/skills/stripe-best-practices/references/billing.md +36 -0
- package/.kilocode/skills/stripe-best-practices/references/connect.md +48 -0
- package/.kilocode/skills/stripe-best-practices/references/payments.md +79 -0
- package/.kilocode/skills/stripe-best-practices/references/security.md +109 -0
- package/.kilocode/skills/stripe-best-practices/references/treasury.md +16 -0
- package/.kilocode/skills/stripe-projects/SKILL.md +139 -0
- package/.kilocode/skills/upgrade-stripe/SKILL.md +185 -0
- package/.kiro/skills/stripe-best-practices/SKILL.md +42 -0
- package/.kiro/skills/stripe-best-practices/references/billing.md +36 -0
- package/.kiro/skills/stripe-best-practices/references/connect.md +48 -0
- package/.kiro/skills/stripe-best-practices/references/payments.md +79 -0
- package/.kiro/skills/stripe-best-practices/references/security.md +109 -0
- package/.kiro/skills/stripe-best-practices/references/treasury.md +16 -0
- package/.kiro/skills/stripe-projects/SKILL.md +139 -0
- package/.kiro/skills/upgrade-stripe/SKILL.md +185 -0
- package/.kode/skills/stripe-best-practices/SKILL.md +42 -0
- package/.kode/skills/stripe-best-practices/references/billing.md +36 -0
- package/.kode/skills/stripe-best-practices/references/connect.md +48 -0
- package/.kode/skills/stripe-best-practices/references/payments.md +79 -0
- package/.kode/skills/stripe-best-practices/references/security.md +109 -0
- package/.kode/skills/stripe-best-practices/references/treasury.md +16 -0
- package/.kode/skills/stripe-projects/SKILL.md +139 -0
- package/.kode/skills/upgrade-stripe/SKILL.md +185 -0
- package/.mcpjam/skills/stripe-best-practices/SKILL.md +42 -0
- package/.mcpjam/skills/stripe-best-practices/references/billing.md +36 -0
- package/.mcpjam/skills/stripe-best-practices/references/connect.md +48 -0
- package/.mcpjam/skills/stripe-best-practices/references/payments.md +79 -0
- package/.mcpjam/skills/stripe-best-practices/references/security.md +109 -0
- package/.mcpjam/skills/stripe-best-practices/references/treasury.md +16 -0
- package/.mcpjam/skills/stripe-projects/SKILL.md +139 -0
- package/.mcpjam/skills/upgrade-stripe/SKILL.md +185 -0
- package/.mux/skills/stripe-best-practices/SKILL.md +42 -0
- package/.mux/skills/stripe-best-practices/references/billing.md +36 -0
- package/.mux/skills/stripe-best-practices/references/connect.md +48 -0
- package/.mux/skills/stripe-best-practices/references/payments.md +79 -0
- package/.mux/skills/stripe-best-practices/references/security.md +109 -0
- package/.mux/skills/stripe-best-practices/references/treasury.md +16 -0
- package/.mux/skills/stripe-projects/SKILL.md +139 -0
- package/.mux/skills/upgrade-stripe/SKILL.md +185 -0
- package/.neovate/skills/stripe-best-practices/SKILL.md +42 -0
- package/.neovate/skills/stripe-best-practices/references/billing.md +36 -0
- package/.neovate/skills/stripe-best-practices/references/connect.md +48 -0
- package/.neovate/skills/stripe-best-practices/references/payments.md +79 -0
- package/.neovate/skills/stripe-best-practices/references/security.md +109 -0
- package/.neovate/skills/stripe-best-practices/references/treasury.md +16 -0
- package/.neovate/skills/stripe-projects/SKILL.md +139 -0
- package/.neovate/skills/upgrade-stripe/SKILL.md +185 -0
- package/.nixpacksignore +14 -0
- package/.openhands/skills/stripe-best-practices/SKILL.md +42 -0
- package/.openhands/skills/stripe-best-practices/references/billing.md +36 -0
- package/.openhands/skills/stripe-best-practices/references/connect.md +48 -0
- package/.openhands/skills/stripe-best-practices/references/payments.md +79 -0
- package/.openhands/skills/stripe-best-practices/references/security.md +109 -0
- package/.openhands/skills/stripe-best-practices/references/treasury.md +16 -0
- package/.openhands/skills/stripe-projects/SKILL.md +139 -0
- package/.openhands/skills/upgrade-stripe/SKILL.md +185 -0
- package/.pi/skills/stripe-best-practices/SKILL.md +42 -0
- package/.pi/skills/stripe-best-practices/references/billing.md +36 -0
- package/.pi/skills/stripe-best-practices/references/connect.md +48 -0
- package/.pi/skills/stripe-best-practices/references/payments.md +79 -0
- package/.pi/skills/stripe-best-practices/references/security.md +109 -0
- package/.pi/skills/stripe-best-practices/references/treasury.md +16 -0
- package/.pi/skills/stripe-projects/SKILL.md +139 -0
- package/.pi/skills/upgrade-stripe/SKILL.md +185 -0
- package/.pochi/skills/stripe-best-practices/SKILL.md +42 -0
- package/.pochi/skills/stripe-best-practices/references/billing.md +36 -0
- package/.pochi/skills/stripe-best-practices/references/connect.md +48 -0
- package/.pochi/skills/stripe-best-practices/references/payments.md +79 -0
- package/.pochi/skills/stripe-best-practices/references/security.md +109 -0
- package/.pochi/skills/stripe-best-practices/references/treasury.md +16 -0
- package/.pochi/skills/stripe-projects/SKILL.md +139 -0
- package/.pochi/skills/upgrade-stripe/SKILL.md +185 -0
- package/.qoder/skills/stripe-best-practices/SKILL.md +42 -0
- package/.qoder/skills/stripe-best-practices/references/billing.md +36 -0
- package/.qoder/skills/stripe-best-practices/references/connect.md +48 -0
- package/.qoder/skills/stripe-best-practices/references/payments.md +79 -0
- package/.qoder/skills/stripe-best-practices/references/security.md +109 -0
- package/.qoder/skills/stripe-best-practices/references/treasury.md +16 -0
- package/.qoder/skills/stripe-projects/SKILL.md +139 -0
- package/.qoder/skills/upgrade-stripe/SKILL.md +185 -0
- package/.qwen/skills/stripe-best-practices/SKILL.md +42 -0
- package/.qwen/skills/stripe-best-practices/references/billing.md +36 -0
- package/.qwen/skills/stripe-best-practices/references/connect.md +48 -0
- package/.qwen/skills/stripe-best-practices/references/payments.md +79 -0
- package/.qwen/skills/stripe-best-practices/references/security.md +109 -0
- package/.qwen/skills/stripe-best-practices/references/treasury.md +16 -0
- package/.qwen/skills/stripe-projects/SKILL.md +139 -0
- package/.qwen/skills/upgrade-stripe/SKILL.md +185 -0
- package/.roo/skills/stripe-best-practices/SKILL.md +42 -0
- package/.roo/skills/stripe-best-practices/references/billing.md +36 -0
- package/.roo/skills/stripe-best-practices/references/connect.md +48 -0
- package/.roo/skills/stripe-best-practices/references/payments.md +79 -0
- package/.roo/skills/stripe-best-practices/references/security.md +109 -0
- package/.roo/skills/stripe-best-practices/references/treasury.md +16 -0
- package/.roo/skills/stripe-projects/SKILL.md +139 -0
- package/.roo/skills/upgrade-stripe/SKILL.md +185 -0
- package/.trae/skills/stripe-best-practices/SKILL.md +42 -0
- package/.trae/skills/stripe-best-practices/references/billing.md +36 -0
- package/.trae/skills/stripe-best-practices/references/connect.md +48 -0
- package/.trae/skills/stripe-best-practices/references/payments.md +79 -0
- package/.trae/skills/stripe-best-practices/references/security.md +109 -0
- package/.trae/skills/stripe-best-practices/references/treasury.md +16 -0
- package/.trae/skills/stripe-projects/SKILL.md +139 -0
- package/.trae/skills/upgrade-stripe/SKILL.md +185 -0
- package/.vibe/skills/stripe-best-practices/SKILL.md +42 -0
- package/.vibe/skills/stripe-best-practices/references/billing.md +36 -0
- package/.vibe/skills/stripe-best-practices/references/connect.md +48 -0
- package/.vibe/skills/stripe-best-practices/references/payments.md +79 -0
- package/.vibe/skills/stripe-best-practices/references/security.md +109 -0
- package/.vibe/skills/stripe-best-practices/references/treasury.md +16 -0
- package/.vibe/skills/stripe-projects/SKILL.md +139 -0
- package/.vibe/skills/upgrade-stripe/SKILL.md +185 -0
- package/.windsurf/skills/stripe-best-practices/SKILL.md +42 -0
- package/.windsurf/skills/stripe-best-practices/references/billing.md +36 -0
- package/.windsurf/skills/stripe-best-practices/references/connect.md +48 -0
- package/.windsurf/skills/stripe-best-practices/references/payments.md +79 -0
- package/.windsurf/skills/stripe-best-practices/references/security.md +109 -0
- package/.windsurf/skills/stripe-best-practices/references/treasury.md +16 -0
- package/.windsurf/skills/stripe-projects/SKILL.md +139 -0
- package/.windsurf/skills/upgrade-stripe/SKILL.md +185 -0
- package/.zencoder/skills/stripe-best-practices/SKILL.md +42 -0
- package/.zencoder/skills/stripe-best-practices/references/billing.md +36 -0
- package/.zencoder/skills/stripe-best-practices/references/connect.md +48 -0
- package/.zencoder/skills/stripe-best-practices/references/payments.md +79 -0
- package/.zencoder/skills/stripe-best-practices/references/security.md +109 -0
- package/.zencoder/skills/stripe-best-practices/references/treasury.md +16 -0
- package/.zencoder/skills/stripe-projects/SKILL.md +139 -0
- package/.zencoder/skills/upgrade-stripe/SKILL.md +185 -0
- package/AUDIT.md +95 -0
- package/BLOCKERS.md +0 -0
- package/COOLIFY.md +51 -0
- package/MCP_SETUP.md +23 -0
- package/PRODUCTION_CHECKLIST.md +246 -0
- package/README.md +47 -0
- package/ROADMAP.md +91 -0
- package/docs/superpowers/plans/2026-05-11-availsync-frontend-sales-flow.md +2445 -0
- package/frontend/.env.example +2 -0
- package/frontend/app/admin/layout.tsx +13 -0
- package/frontend/app/admin/page.tsx +747 -0
- package/frontend/app/app/activity/page.tsx +257 -0
- package/frontend/app/app/agents/[agentId]/page.tsx +21 -0
- package/frontend/app/app/agents/page.tsx +1155 -0
- package/frontend/app/app/audit/page.tsx +225 -0
- package/frontend/app/app/availability/page.tsx +840 -0
- package/frontend/app/app/holds/page.tsx +262 -0
- package/frontend/app/app/layout.tsx +19 -0
- package/frontend/app/app/onboarding/page.tsx +10 -0
- package/frontend/app/app/onboarding/verify/page.tsx +309 -0
- package/frontend/app/app/page.tsx +508 -0
- package/frontend/app/app/settings/page.tsx +399 -0
- package/frontend/app/app/work/page.tsx +426 -0
- package/frontend/app/changelog/page.tsx +93 -0
- package/frontend/app/checkout/page.tsx +25 -0
- package/frontend/app/docs/api/page.tsx +157 -0
- package/frontend/app/docs/page.tsx +296 -0
- package/frontend/app/docs/pilot/page.tsx +127 -0
- package/frontend/app/docs/quickstart/page.tsx +318 -0
- package/frontend/app/docs/reliability/page.tsx +78 -0
- package/frontend/app/docs/sdk/node/page.tsx +166 -0
- package/frontend/app/globals.css +57 -0
- package/frontend/app/icon.png +0 -0
- package/frontend/app/layout.tsx +87 -0
- package/frontend/app/login/page.tsx +14 -0
- package/frontend/app/page.tsx +47 -0
- package/frontend/app/pricing/page.tsx +66 -0
- package/frontend/app/privacy/page.tsx +52 -0
- package/frontend/app/robots.ts +26 -0
- package/frontend/app/security/page.tsx +74 -0
- package/frontend/app/signup/page.tsx +14 -0
- package/frontend/app/sitemap.ts +14 -0
- package/frontend/app/terms/page.tsx +51 -0
- package/frontend/components/brand/AvailsyncLogo.tsx +56 -0
- package/frontend/components/checkout/CheckoutClient.tsx +100 -0
- package/frontend/components/dashboard/AgentForm.tsx +59 -0
- package/frontend/components/dashboard/AppShell.tsx +291 -0
- package/frontend/components/dashboard/AvailabilityChecker.tsx +117 -0
- package/frontend/components/dashboard/AvailabilityWindowForm.tsx +40 -0
- package/frontend/components/dashboard/HoldForm.tsx +133 -0
- package/frontend/components/dashboard/MetricCard.tsx +10 -0
- package/frontend/components/login/LoginForm.tsx +95 -0
- package/frontend/components/marketing/AgentCoordinationStory.tsx +1530 -0
- package/frontend/components/marketing/Faq.tsx +41 -0
- package/frontend/components/marketing/Hero.tsx +73 -0
- package/frontend/components/marketing/HowItWorks.tsx +28 -0
- package/frontend/components/marketing/ObserveModeTeaser.tsx +41 -0
- package/frontend/components/marketing/PricingTeaser.tsx +23 -0
- package/frontend/components/marketing/ProblemSolution.tsx +36 -0
- package/frontend/components/marketing/SiteFooter.tsx +59 -0
- package/frontend/components/marketing/SiteHeader.tsx +45 -0
- package/frontend/components/marketing/UseCases.tsx +27 -0
- package/frontend/components/onboarding/OnboardingClient.tsx +278 -0
- package/frontend/components/pricing/PricingCards.tsx +65 -0
- package/frontend/components/privacy/CookieConsent.tsx +230 -0
- package/frontend/components/privacy/CookieSettingsButton.tsx +15 -0
- package/frontend/components/seo/JsonLd.tsx +10 -0
- package/frontend/components/signup/SignupForm.tsx +55 -0
- package/frontend/components/ui/Badge.tsx +23 -0
- package/frontend/components/ui/Button.tsx +37 -0
- package/frontend/components/ui/Card.tsx +11 -0
- package/frontend/components/ui/ConfirmDialog.tsx +77 -0
- package/frontend/components/ui/EmptyState.tsx +24 -0
- package/frontend/components/ui/Input.tsx +14 -0
- package/frontend/components/ui/KeyDisplay.tsx +49 -0
- package/frontend/components/ui/Select.tsx +14 -0
- package/frontend/components/ui/Skeleton.tsx +24 -0
- package/frontend/components/ui/Tabs.tsx +19 -0
- package/frontend/components/ui/Textarea.tsx +14 -0
- package/frontend/components/ui/Toast.tsx +78 -0
- package/frontend/components/waitlist/WaitlistDialog.tsx +128 -0
- package/frontend/lib/api.ts +1282 -0
- package/frontend/lib/billing.ts +6 -0
- package/frontend/lib/cookieConsent.ts +113 -0
- package/frontend/lib/format.ts +16 -0
- package/frontend/lib/plans.ts +62 -0
- package/frontend/lib/schemas.ts +108 -0
- package/frontend/lib/seo.ts +376 -0
- package/frontend/lib/setupGuides.ts +630 -0
- package/frontend/lib/storage.ts +30 -0
- package/frontend/next-env.d.ts +6 -0
- package/frontend/next.config.mjs +13 -0
- package/frontend/package-lock.json +14409 -0
- package/frontend/package.json +41 -0
- package/frontend/playwright.config.ts +20 -0
- package/frontend/postcss.config.mjs +8 -0
- package/frontend/public/.gitkeep +0 -0
- package/frontend/public/brand/availsync-logo-board.png +0 -0
- package/frontend/public/brand/availsync-logo-dark.png +0 -0
- package/frontend/public/brand/availsync-mark-dark.png +0 -0
- package/frontend/public/brand/availsync-wordmark-dark.png +0 -0
- package/frontend/public/marketing/hero-agent-coordination.png +0 -0
- package/frontend/tailwind.config.ts +53 -0
- package/frontend/tests/smoke.spec.ts +89 -0
- package/frontend/tsconfig.json +23 -0
- package/jest.config.js +7 -0
- package/nixpacks.toml +11 -0
- package/package.json +53 -0
- package/packages/mcp/LICENSE +21 -0
- package/packages/mcp/README.md +60 -0
- package/packages/mcp/jest.config.cjs +8 -0
- package/packages/mcp/package.json +54 -0
- package/packages/mcp/src/helpers.ts +38 -0
- package/packages/mcp/src/index.test.ts +60 -0
- package/packages/mcp/src/index.ts +387 -0
- package/packages/mcp/tsconfig.json +20 -0
- package/packages/mcp/tsconfig.test.json +12 -0
- package/packages/node/LICENSE +21 -0
- package/packages/node/README.md +120 -0
- package/packages/node/jest.config.cjs +8 -0
- package/packages/node/package.json +46 -0
- package/packages/node/src/index.test.ts +360 -0
- package/packages/node/src/index.ts +402 -0
- package/packages/node/tsconfig.json +20 -0
- package/packages/node/tsconfig.test.json +12 -0
- package/plan.md +923 -0
- package/skills/stripe-best-practices/SKILL.md +42 -0
- package/skills/stripe-best-practices/references/billing.md +36 -0
- package/skills/stripe-best-practices/references/connect.md +48 -0
- package/skills/stripe-best-practices/references/payments.md +79 -0
- package/skills/stripe-best-practices/references/security.md +109 -0
- package/skills/stripe-best-practices/references/treasury.md +16 -0
- package/skills/stripe-projects/SKILL.md +139 -0
- package/skills/upgrade-stripe/SKILL.md +185 -0
- package/skills-lock.json +20 -0
- package/src/core/availability.ts +178 -0
- package/src/core/conflict.ts +209 -0
- package/src/core/work.ts +490 -0
- package/src/db/client.ts +17 -0
- package/src/db/migrations/001_init.sql +88 -0
- package/src/db/migrations/002_stripe.sql +2 -0
- package/src/db/migrations/003_workspace_auth.sql +19 -0
- package/src/db/migrations/004_agent_mcp_status.sql +2 -0
- package/src/db/migrations/005_hold_event_actor.sql +4 -0
- package/src/db/migrations/006_agent_activity.sql +35 -0
- package/src/db/migrations/007_work_coordination.sql +60 -0
- package/src/db/migrations/008_work_claim_leases.sql +20 -0
- package/src/db/migrations/009_billing_subscription_state.sql +23 -0
- package/src/db/migrations/010_agent_api_key_prefix.sql +10 -0
- package/src/db/migrations/011_org_verified_and_work_event_retention.sql +11 -0
- package/src/db/migrations/012_agent_enforcement_mode.sql +12 -0
- package/src/db/migrations/013_support_tickets.sql +21 -0
- package/src/db/migrations/014_paid_plan_waitlist.sql +23 -0
- package/src/db/migrations/015_agent_last_seen.sql +2 -0
- package/src/db/migrations.ts +164 -0
- package/src/db/run-migrations.ts +13 -0
- package/src/index.ts +183 -0
- package/src/lib/activity.ts +137 -0
- package/src/lib/apiKeys.ts +32 -0
- package/src/lib/appInfo.ts +26 -0
- package/src/lib/billingConfig.ts +3 -0
- package/src/lib/env.ts +75 -0
- package/src/lib/logger.ts +8 -0
- package/src/lib/plans.ts +204 -0
- package/src/mcp/server.js +5 -0
- package/src/mcp/server.ts +350 -0
- package/src/middleware/auth.ts +342 -0
- package/src/middleware/requestId.ts +16 -0
- package/src/routes/account.ts +168 -0
- package/src/routes/activity.ts +126 -0
- package/src/routes/admin.ts +514 -0
- package/src/routes/audit.ts +68 -0
- package/src/routes/auth.ts +203 -0
- package/src/routes/availability.ts +325 -0
- package/src/routes/billing.ts +406 -0
- package/src/routes/conflicts.ts +131 -0
- package/src/routes/holds.ts +437 -0
- package/src/routes/mcp.ts +57 -0
- package/src/routes/metrics.ts +39 -0
- package/src/routes/onboarding.ts +273 -0
- package/src/routes/orgs.ts +981 -0
- package/src/routes/preferences.ts +132 -0
- package/src/routes/session.ts +16 -0
- package/src/routes/support.ts +77 -0
- package/src/routes/value.ts +186 -0
- package/src/routes/waitlist.ts +63 -0
- package/src/routes/work.ts +1578 -0
- package/src/server.ts +36 -0
- package/src/types/index.ts +109 -0
- package/tests/integration/activity.route.test.ts +103 -0
- package/tests/integration/admin.route.test.ts +143 -0
- package/tests/integration/agent-keys.route.test.ts +237 -0
- package/tests/integration/availability.route.test.ts +125 -0
- package/tests/integration/billing.route.test.ts +393 -0
- package/tests/integration/conflicts.route.test.ts +131 -0
- package/tests/integration/flows.test.ts +154 -0
- package/tests/integration/helpers.ts +134 -0
- package/tests/integration/holds.route.test.ts +185 -0
- package/tests/integration/metrics.route.test.ts +100 -0
- package/tests/integration/onboarding.verify.route.test.ts +163 -0
- package/tests/integration/preferences.route.test.ts +53 -0
- package/tests/integration/session.route.test.ts +97 -0
- package/tests/integration/system.route.test.ts +92 -0
- package/tests/integration/value.route.test.ts +235 -0
- package/tests/integration/work.route.test.ts +745 -0
- package/tests/setup.ts +4 -0
- package/tests/smoke.sh +62 -0
- package/tests/unit/auth.test.ts +114 -0
- package/tests/unit/availability.test.ts +149 -0
- package/tests/unit/conflict.test.ts +118 -0
- package/tests/unit/env.test.ts +69 -0
- package/tests/unit/migrations.test.ts +135 -0
- package/tests/unit/request-id.test.ts +37 -0
- package/tmp-mobile-agents.png +0 -0
- package/tmp-next-mobile.err.log +10 -0
- package/tmp-next-mobile.log +5 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,1155 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState, useCallback, useRef } from 'react';
|
|
4
|
+
import {
|
|
5
|
+
listAgents,
|
|
6
|
+
createAgent,
|
|
7
|
+
updateAgent,
|
|
8
|
+
deleteAgent,
|
|
9
|
+
getValueHealth,
|
|
10
|
+
rotateAgentKey,
|
|
11
|
+
runAgentSetupTest,
|
|
12
|
+
testAgentConnection,
|
|
13
|
+
type SetupTestRun,
|
|
14
|
+
type ValueHealth,
|
|
15
|
+
} from '@/lib/api';
|
|
16
|
+
import { loadSession, type StoredSession } from '@/lib/storage';
|
|
17
|
+
import { Badge } from '@/components/ui/Badge';
|
|
18
|
+
import { EmptyState } from '@/components/ui/EmptyState';
|
|
19
|
+
import { SkeletonRow } from '@/components/ui/Skeleton';
|
|
20
|
+
import { useToast } from '@/components/ui/Toast';
|
|
21
|
+
import { Check, KeyRound, Pencil, Plug, Trash2, Users, Plus, Copy, X, Eye, EyeOff } from 'lucide-react';
|
|
22
|
+
import type { Agent } from '@/lib/schemas';
|
|
23
|
+
import {
|
|
24
|
+
automationGuardrailSnippets,
|
|
25
|
+
claudeCursorSnippet,
|
|
26
|
+
codexMultiAgentMcpSnippet,
|
|
27
|
+
codexMcpSetupSnippet,
|
|
28
|
+
codexPromptSnippet,
|
|
29
|
+
mcpEnvSnippet,
|
|
30
|
+
nodeSdkSnippets,
|
|
31
|
+
openClawSetupSnippet,
|
|
32
|
+
openClawSkillSnippet,
|
|
33
|
+
schedulingSnippets,
|
|
34
|
+
serverCronSnippet,
|
|
35
|
+
workGuardrailSnippets,
|
|
36
|
+
type SetupSnippet,
|
|
37
|
+
} from '@/lib/setupGuides';
|
|
38
|
+
import { billingUpgradesEnabled } from '@/lib/billing';
|
|
39
|
+
|
|
40
|
+
const typeLabels: Record<string, string> = {
|
|
41
|
+
external_meeting: 'External',
|
|
42
|
+
internal: 'Internal',
|
|
43
|
+
focus: 'Focus',
|
|
44
|
+
generic: 'Generic',
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const typeBadgeVariant: Record<string, 'accent' | 'success' | 'warning' | 'neutral'> = {
|
|
48
|
+
external_meeting: 'accent',
|
|
49
|
+
internal: 'success',
|
|
50
|
+
focus: 'warning',
|
|
51
|
+
generic: 'neutral',
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const healthBadge: Record<ValueHealth['agent_health'][number]['status'], { label: string; variant: 'success' | 'warning' | 'neutral' | 'error' }> = {
|
|
55
|
+
healthy: { label: 'Healthy', variant: 'success' },
|
|
56
|
+
quiet: { label: 'Quiet', variant: 'warning' },
|
|
57
|
+
never_connected: { label: 'Never connected', variant: 'neutral' },
|
|
58
|
+
error: { label: 'Error', variant: 'error' },
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const enforcementBadge: Record<Agent['enforcement_mode'], { label: string; variant: 'success' | 'neutral' }> = {
|
|
62
|
+
enforce: { label: 'Enforce', variant: 'success' },
|
|
63
|
+
observe: { label: 'Observe', variant: 'neutral' },
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
function formatAgentTimestamp(value: string | null | undefined) {
|
|
67
|
+
if (!value) return '-';
|
|
68
|
+
const date = new Date(value);
|
|
69
|
+
if (Number.isNaN(date.getTime())) return '-';
|
|
70
|
+
const pad = (number: number) => String(number).padStart(2, '0');
|
|
71
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function mcpStatus(agent: Agent): { label: string; variant: 'success' | 'warning' | 'neutral'; detail: string } {
|
|
75
|
+
if (!agent.mcp_last_seen_at) {
|
|
76
|
+
return { label: 'Offline', variant: 'neutral', detail: 'No heartbeat yet' };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const diffMs = Date.now() - new Date(agent.mcp_last_seen_at).getTime();
|
|
80
|
+
if (diffMs < 2 * 60 * 1000) {
|
|
81
|
+
return { label: 'Online', variant: 'success', detail: 'Seen just now' };
|
|
82
|
+
}
|
|
83
|
+
if (diffMs < 15 * 60 * 1000) {
|
|
84
|
+
return { label: 'Recent', variant: 'warning', detail: `${Math.floor(diffMs / 60000)}m ago` };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return { label: 'Offline', variant: 'neutral', detail: `${Math.floor(diffMs / 60000)}m ago` };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export default function AgentsPage() {
|
|
91
|
+
const { toast } = useToast();
|
|
92
|
+
const [agents, setAgents] = useState<Agent[]>([]);
|
|
93
|
+
const [valueHealth, setValueHealth] = useState<ValueHealth | null>(null);
|
|
94
|
+
const [loading, setLoading] = useState(true);
|
|
95
|
+
const [session, setSession] = useState<StoredSession | null>(null);
|
|
96
|
+
const [showNewPanel, setShowNewPanel] = useState(false);
|
|
97
|
+
const [newAgentKey, setNewAgentKey] = useState<string | null>(null);
|
|
98
|
+
const [newAgentId, setNewAgentId] = useState<string | null>(null);
|
|
99
|
+
const [newAgentKeySaved, setNewAgentKeySaved] = useState(false);
|
|
100
|
+
const [form, setForm] = useState({ name: '', agent_type: 'generic', priority: 0 });
|
|
101
|
+
const [editingAgentId, setEditingAgentId] = useState<string | null>(null);
|
|
102
|
+
const [editForm, setEditForm] = useState({ name: '', agent_type: 'generic', priority: 0, enforcement_mode: 'enforce' as Agent['enforcement_mode'] });
|
|
103
|
+
const [creating, setCreating] = useState(false);
|
|
104
|
+
const [connectAgent, setConnectAgent] = useState<Agent | null>(null);
|
|
105
|
+
const [connection, setConnection] = useState<Awaited<ReturnType<typeof testAgentConnection>> | null>(null);
|
|
106
|
+
const [connectionLoading, setConnectionLoading] = useState(false);
|
|
107
|
+
const [rotatedKey, setRotatedKey] = useState<string | null>(null);
|
|
108
|
+
const [rotating, setRotating] = useState(false);
|
|
109
|
+
const [planLimit, setPlanLimit] = useState<{ message: string; upgrade_plan: string | null } | null>(null);
|
|
110
|
+
const [connectTab, setConnectTab] = useState<'status' | 'sdk' | 'mcp' | 'automation' | 'openclaw' | 'rest' | 'codex' | 'activity'>('status');
|
|
111
|
+
const [connectResource, setConnectResource] = useState({ resource_type: 'repo' as 'project' | 'repo', resource_key: 'owner/repo', duration_minutes: 45 });
|
|
112
|
+
const [setupTest, setSetupTest] = useState<SetupTestRun | null>(null);
|
|
113
|
+
const [setupTestRunning, setSetupTestRunning] = useState(false);
|
|
114
|
+
const autoOpenedAgent = useRef<string | null>(null);
|
|
115
|
+
|
|
116
|
+
const loadAgents = useCallback(() => {
|
|
117
|
+
const s = loadSession();
|
|
118
|
+
setSession(s);
|
|
119
|
+
if (s) {
|
|
120
|
+
Promise.all([
|
|
121
|
+
listAgents(s.orgId),
|
|
122
|
+
getValueHealth().catch(() => null),
|
|
123
|
+
])
|
|
124
|
+
.then(([data, health]) => {
|
|
125
|
+
setAgents(data);
|
|
126
|
+
setValueHealth(health);
|
|
127
|
+
setLoading(false);
|
|
128
|
+
})
|
|
129
|
+
.catch(() => setLoading(false));
|
|
130
|
+
} else {
|
|
131
|
+
setLoading(false);
|
|
132
|
+
}
|
|
133
|
+
}, []);
|
|
134
|
+
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
loadAgents();
|
|
137
|
+
}, [loadAgents]);
|
|
138
|
+
|
|
139
|
+
useEffect(() => {
|
|
140
|
+
if (typeof window === 'undefined' || !session || agents.length === 0) return;
|
|
141
|
+
const agentId = new URLSearchParams(window.location.search).get('connect');
|
|
142
|
+
if (!agentId || autoOpenedAgent.current === agentId) return;
|
|
143
|
+
const agent = agents.find((row) => row.id === agentId);
|
|
144
|
+
if (!agent) return;
|
|
145
|
+
autoOpenedAgent.current = agentId;
|
|
146
|
+
openConnect(agent);
|
|
147
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
148
|
+
}, [agents, session]);
|
|
149
|
+
|
|
150
|
+
// Keyboard shortcut: N to open new agent panel
|
|
151
|
+
useEffect(() => {
|
|
152
|
+
function handleKey(e: KeyboardEvent) {
|
|
153
|
+
if (e.key === 'n' && !showNewPanel && !(e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement)) {
|
|
154
|
+
e.preventDefault();
|
|
155
|
+
setShowNewPanel(true);
|
|
156
|
+
}
|
|
157
|
+
if (e.key === 'Escape') {
|
|
158
|
+
setShowNewPanel(false);
|
|
159
|
+
setNewAgentKey(null);
|
|
160
|
+
setNewAgentId(null);
|
|
161
|
+
setNewAgentKeySaved(false);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
window.addEventListener('keydown', handleKey);
|
|
165
|
+
return () => window.removeEventListener('keydown', handleKey);
|
|
166
|
+
}, [showNewPanel]);
|
|
167
|
+
|
|
168
|
+
const handleCreate = async () => {
|
|
169
|
+
if (!session || !form.name.trim()) return;
|
|
170
|
+
setCreating(true);
|
|
171
|
+
try {
|
|
172
|
+
setPlanLimit(null);
|
|
173
|
+
const result = await createAgent({
|
|
174
|
+
orgId: session.orgId,
|
|
175
|
+
name: form.name.trim(),
|
|
176
|
+
agent_type: form.agent_type as Agent['agent_type'],
|
|
177
|
+
priority: form.priority,
|
|
178
|
+
});
|
|
179
|
+
setNewAgentKey(result.apiKey);
|
|
180
|
+
setNewAgentId(result.agent.id);
|
|
181
|
+
setNewAgentKeySaved(false);
|
|
182
|
+
setForm({ name: '', agent_type: 'generic', priority: 0 });
|
|
183
|
+
loadAgents();
|
|
184
|
+
toast('Agent created', 'success');
|
|
185
|
+
} catch (err) {
|
|
186
|
+
const body = (err as { body?: { error?: string; message?: string; upgrade_plan?: string | null } }).body;
|
|
187
|
+
if (body?.error === 'plan_limit_reached') {
|
|
188
|
+
setPlanLimit({ message: body.message || (err as Error).message, upgrade_plan: body.upgrade_plan ?? null });
|
|
189
|
+
}
|
|
190
|
+
toast((err as Error).message, 'error');
|
|
191
|
+
} finally {
|
|
192
|
+
setCreating(false);
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const startEdit = (agent: Agent) => {
|
|
197
|
+
setEditingAgentId(agent.id);
|
|
198
|
+
setEditForm({
|
|
199
|
+
name: agent.name,
|
|
200
|
+
agent_type: agent.agent_type,
|
|
201
|
+
priority: agent.priority,
|
|
202
|
+
enforcement_mode: agent.enforcement_mode,
|
|
203
|
+
});
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const handleUpdate = async () => {
|
|
207
|
+
if (!session || !editingAgentId || !editForm.name.trim()) return;
|
|
208
|
+
try {
|
|
209
|
+
await updateAgent({
|
|
210
|
+
orgId: session.orgId,
|
|
211
|
+
agentId: editingAgentId,
|
|
212
|
+
name: editForm.name.trim(),
|
|
213
|
+
agent_type: editForm.agent_type as Agent['agent_type'],
|
|
214
|
+
priority: editForm.priority,
|
|
215
|
+
enforcement_mode: editForm.enforcement_mode,
|
|
216
|
+
});
|
|
217
|
+
toast('Agent updated', 'success');
|
|
218
|
+
setEditingAgentId(null);
|
|
219
|
+
loadAgents();
|
|
220
|
+
} catch (err) {
|
|
221
|
+
toast((err as Error).message, 'error');
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const handleDelete = async (agent: Agent) => {
|
|
226
|
+
if (!session) return;
|
|
227
|
+
if (!window.confirm(`Delete ${agent.name}? Holds and windows for this agent will also be removed.`)) return;
|
|
228
|
+
try {
|
|
229
|
+
await deleteAgent(session.orgId, agent.id);
|
|
230
|
+
toast('Agent deleted', 'success');
|
|
231
|
+
loadAgents();
|
|
232
|
+
} catch (err) {
|
|
233
|
+
toast((err as Error).message, 'error');
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const openConnect = async (agent: Agent) => {
|
|
238
|
+
if (!session) return;
|
|
239
|
+
setConnectAgent(agent);
|
|
240
|
+
setConnectTab('status');
|
|
241
|
+
setConnection(null);
|
|
242
|
+
setRotatedKey(null);
|
|
243
|
+
setSetupTest(null);
|
|
244
|
+
setConnectionLoading(true);
|
|
245
|
+
try {
|
|
246
|
+
setConnection(await testAgentConnection(session.orgId, agent.id));
|
|
247
|
+
} catch (err) {
|
|
248
|
+
toast((err as Error).message, 'error');
|
|
249
|
+
} finally {
|
|
250
|
+
setConnectionLoading(false);
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const handleRotateKey = async () => {
|
|
255
|
+
if (!session || !connectAgent) return;
|
|
256
|
+
if (!window.confirm('Rotate this API key? The old key will stop working immediately.')) return;
|
|
257
|
+
setRotating(true);
|
|
258
|
+
try {
|
|
259
|
+
const result = await rotateAgentKey(session.orgId, connectAgent.id);
|
|
260
|
+
setRotatedKey(result.apiKey);
|
|
261
|
+
toast('API key rotated', 'success');
|
|
262
|
+
loadAgents();
|
|
263
|
+
} catch (err) {
|
|
264
|
+
toast((err as Error).message, 'error');
|
|
265
|
+
} finally {
|
|
266
|
+
setRotating(false);
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
const handleRunSetupTest = async () => {
|
|
271
|
+
if (!session || !connectAgent) return;
|
|
272
|
+
setSetupTestRunning(true);
|
|
273
|
+
try {
|
|
274
|
+
const result = await runAgentSetupTest(session.orgId, connectAgent.id);
|
|
275
|
+
setSetupTest(result);
|
|
276
|
+
setConnection(await testAgentConnection(session.orgId, connectAgent.id));
|
|
277
|
+
setValueHealth(await getValueHealth().catch(() => null));
|
|
278
|
+
toast(result.status === 'success' ? 'Setup test passed' : `Setup test ${result.status}`, result.status === 'error' ? 'error' : 'success');
|
|
279
|
+
} catch (err) {
|
|
280
|
+
const body = (err as { body?: SetupTestRun }).body;
|
|
281
|
+
if (body?.steps) setSetupTest(body);
|
|
282
|
+
toast((err as Error).message, 'error');
|
|
283
|
+
} finally {
|
|
284
|
+
setSetupTestRunning(false);
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
const copyText = (text: string, label = 'Copied') => {
|
|
289
|
+
navigator.clipboard.writeText(text);
|
|
290
|
+
toast(label, 'success');
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
const appUrl = typeof window !== 'undefined' ? window.location.origin : 'https://availsync.dev';
|
|
294
|
+
const guideInput = {
|
|
295
|
+
appUrl,
|
|
296
|
+
agentId: connectAgent?.id,
|
|
297
|
+
resourceType: connectResource.resource_type,
|
|
298
|
+
resourceKey: connectResource.resource_key || 'owner/repo',
|
|
299
|
+
durationMinutes: connectResource.duration_minutes || 45,
|
|
300
|
+
};
|
|
301
|
+
const mcpSnippet = mcpEnvSnippet(guideInput);
|
|
302
|
+
const sdkSnippets = nodeSdkSnippets(guideInput);
|
|
303
|
+
const cursorSnippet = claudeCursorSnippet(guideInput);
|
|
304
|
+
const codexMcpSnippet = codexMcpSetupSnippet(guideInput);
|
|
305
|
+
const codexMultiAgentSnippet = codexMultiAgentMcpSnippet(guideInput);
|
|
306
|
+
const automationSnippets = automationGuardrailSnippets(guideInput);
|
|
307
|
+
const openClawSkill = openClawSkillSnippet(guideInput);
|
|
308
|
+
const openClawSetup = openClawSetupSnippet(guideInput);
|
|
309
|
+
const workSnippets = workGuardrailSnippets(guideInput);
|
|
310
|
+
const scheduleSnippets = schedulingSnippets(guideInput);
|
|
311
|
+
const healthByAgent = new Map(valueHealth?.agent_health.map((health) => [health.agent_id, health]) ?? []);
|
|
312
|
+
const connectAgentHealth = connectAgent ? healthByAgent.get(connectAgent.id) : undefined;
|
|
313
|
+
const codexSnippet = codexPromptSnippet(guideInput);
|
|
314
|
+
const cronSnippet = serverCronSnippet(guideInput);
|
|
315
|
+
const readinessItems = connection
|
|
316
|
+
? [
|
|
317
|
+
['Agent created', connection.setup_readiness.agent_created],
|
|
318
|
+
['API key copied or rotated', Boolean(rotatedKey || connection.last_successful_api_call || connection.last_seen_at || connection.mcp_last_seen_at)],
|
|
319
|
+
['MCP heartbeat seen', connection.setup_readiness.mcp_heartbeat_seen],
|
|
320
|
+
['First availability check', connection.setup_readiness.first_availability_check],
|
|
321
|
+
['First conflict preview', connection.setup_readiness.first_conflict_preview],
|
|
322
|
+
['First work check', connection.setup_readiness.first_work_check],
|
|
323
|
+
['First work claim', connection.setup_readiness.first_work_claim],
|
|
324
|
+
['First work extend', connection.setup_readiness.first_work_extend],
|
|
325
|
+
['First work release', connection.setup_readiness.first_work_release],
|
|
326
|
+
] as Array<[string, boolean]>
|
|
327
|
+
: [];
|
|
328
|
+
const missingReadinessItems = readinessItems.filter(([, value]) => !value).map(([label]) => label);
|
|
329
|
+
|
|
330
|
+
return (
|
|
331
|
+
<div className="w-full max-w-none p-4 sm:p-6">
|
|
332
|
+
<div className="mb-6 flex flex-wrap items-center justify-between gap-3">
|
|
333
|
+
<h1 className="text-title font-semibold text-text-primary">Agents</h1>
|
|
334
|
+
<button
|
|
335
|
+
onClick={() => setShowNewPanel(true)}
|
|
336
|
+
className="flex items-center gap-1.5 rounded bg-accent text-white px-3 py-1.5 text-body font-medium hover:bg-accent-hover transition-colors active:scale-[0.98]"
|
|
337
|
+
>
|
|
338
|
+
<Plus className="h-3.5 w-3.5" />
|
|
339
|
+
New agent
|
|
340
|
+
</button>
|
|
341
|
+
</div>
|
|
342
|
+
|
|
343
|
+
{planLimit && (
|
|
344
|
+
<div className="mb-4 rounded border border-warning/40 bg-warning/10 p-3 text-body text-warning">
|
|
345
|
+
<div>{planLimit.message}</div>
|
|
346
|
+
{planLimit.upgrade_plan && (
|
|
347
|
+
<a
|
|
348
|
+
className="mt-2 inline-block text-accent hover:text-accent-hover"
|
|
349
|
+
href={billingUpgradesEnabled() ? `/checkout?plan=${planLimit.upgrade_plan}` : '/app/settings?tab=billing'}
|
|
350
|
+
>
|
|
351
|
+
{billingUpgradesEnabled() ? `Upgrade to ${planLimit.upgrade_plan}` : 'Join paid plan waitlist'}
|
|
352
|
+
</a>
|
|
353
|
+
)}
|
|
354
|
+
</div>
|
|
355
|
+
)}
|
|
356
|
+
|
|
357
|
+
{/* New agent slide-over */}
|
|
358
|
+
{showNewPanel && (
|
|
359
|
+
<div className="fixed inset-0 z-50 flex justify-end">
|
|
360
|
+
<div className="absolute inset-0 bg-black/50" onClick={() => { setShowNewPanel(false); setNewAgentKey(null); setNewAgentId(null); setNewAgentKeySaved(false); }} />
|
|
361
|
+
<div className="relative w-full max-w-sm overflow-y-auto border-l border-border bg-surface p-4 sm:p-6">
|
|
362
|
+
<div className="flex items-center justify-between mb-6">
|
|
363
|
+
<h2 className="text-heading font-medium text-text-primary">New agent</h2>
|
|
364
|
+
<button onClick={() => { setShowNewPanel(false); setNewAgentKey(null); setNewAgentId(null); setNewAgentKeySaved(false); }} className="text-text-tertiary hover:text-text-secondary">
|
|
365
|
+
<X className="h-4 w-4" />
|
|
366
|
+
</button>
|
|
367
|
+
</div>
|
|
368
|
+
|
|
369
|
+
{newAgentKey ? (
|
|
370
|
+
<div>
|
|
371
|
+
<OneTimeKeyPanel apiKey={newAgentKey} onCopy={copyText} saved={newAgentKeySaved} onSavedChange={setNewAgentKeySaved} />
|
|
372
|
+
{newAgentId && (
|
|
373
|
+
<div className="rounded border border-border bg-bg p-4 mb-4">
|
|
374
|
+
<label className="block text-label uppercase text-text-tertiary mb-1">Agent ID</label>
|
|
375
|
+
<div className="flex items-center gap-2">
|
|
376
|
+
<span className="font-mono text-[12px] text-text-secondary break-all">{newAgentId}</span>
|
|
377
|
+
<button
|
|
378
|
+
onClick={() => {
|
|
379
|
+
navigator.clipboard.writeText(newAgentId);
|
|
380
|
+
toast('Agent ID copied', 'success');
|
|
381
|
+
}}
|
|
382
|
+
className="text-text-tertiary hover:text-text-secondary"
|
|
383
|
+
title="Copy agent ID"
|
|
384
|
+
>
|
|
385
|
+
<Copy className="h-3.5 w-3.5" />
|
|
386
|
+
</button>
|
|
387
|
+
</div>
|
|
388
|
+
</div>
|
|
389
|
+
)}
|
|
390
|
+
<button
|
|
391
|
+
onClick={() => { setShowNewPanel(false); setNewAgentKey(null); setNewAgentId(null); setNewAgentKeySaved(false); }}
|
|
392
|
+
disabled={!newAgentKeySaved}
|
|
393
|
+
className="w-full rounded bg-surface-raised text-text-secondary py-2 text-body hover:bg-border transition-colors disabled:cursor-not-allowed disabled:opacity-40"
|
|
394
|
+
>
|
|
395
|
+
Done
|
|
396
|
+
</button>
|
|
397
|
+
</div>
|
|
398
|
+
) : (
|
|
399
|
+
<div className="space-y-4">
|
|
400
|
+
<div>
|
|
401
|
+
<label className="block text-label uppercase text-text-tertiary mb-1">Name</label>
|
|
402
|
+
<input
|
|
403
|
+
value={form.name}
|
|
404
|
+
onChange={(e) => setForm({ ...form, name: e.target.value })}
|
|
405
|
+
className="w-full rounded bg-bg border border-border px-3 py-2 text-body text-text-primary outline-none focus:border-border-focus"
|
|
406
|
+
placeholder="e.g. deploy_agent"
|
|
407
|
+
autoFocus
|
|
408
|
+
/>
|
|
409
|
+
</div>
|
|
410
|
+
<div>
|
|
411
|
+
<label className="block text-label uppercase text-text-tertiary mb-1">Type</label>
|
|
412
|
+
<select
|
|
413
|
+
value={form.agent_type}
|
|
414
|
+
onChange={(e) => setForm({ ...form, agent_type: e.target.value })}
|
|
415
|
+
className="w-full rounded bg-bg border border-border px-3 py-2 text-body text-text-primary outline-none focus:border-border-focus"
|
|
416
|
+
>
|
|
417
|
+
<option value="external_meeting">External meeting</option>
|
|
418
|
+
<option value="internal">Internal</option>
|
|
419
|
+
<option value="focus">Focus</option>
|
|
420
|
+
<option value="generic">Coding / generic</option>
|
|
421
|
+
</select>
|
|
422
|
+
</div>
|
|
423
|
+
<div>
|
|
424
|
+
<label className="block text-label uppercase text-text-tertiary mb-1">Priority</label>
|
|
425
|
+
<input
|
|
426
|
+
type="number"
|
|
427
|
+
value={form.priority}
|
|
428
|
+
onChange={(e) => setForm({ ...form, priority: Number(e.target.value) })}
|
|
429
|
+
className="w-full rounded bg-bg border border-border px-3 py-2 text-body text-text-primary outline-none focus:border-border-focus"
|
|
430
|
+
/>
|
|
431
|
+
</div>
|
|
432
|
+
<button
|
|
433
|
+
onClick={handleCreate}
|
|
434
|
+
disabled={creating || !form.name.trim()}
|
|
435
|
+
className="w-full rounded bg-accent text-white py-2 text-body font-medium hover:bg-accent-hover transition-colors disabled:opacity-40 active:scale-[0.98]"
|
|
436
|
+
>
|
|
437
|
+
{creating ? 'Creating...' : 'Create agent'}
|
|
438
|
+
</button>
|
|
439
|
+
</div>
|
|
440
|
+
)}
|
|
441
|
+
</div>
|
|
442
|
+
</div>
|
|
443
|
+
)}
|
|
444
|
+
|
|
445
|
+
{connectAgent && (
|
|
446
|
+
<div className="fixed inset-0 z-50 flex justify-end">
|
|
447
|
+
<div className="absolute inset-0 bg-black/50" onClick={() => setConnectAgent(null)} />
|
|
448
|
+
<div className="relative w-full max-w-2xl overflow-y-auto border-l border-border bg-surface p-4 sm:p-6">
|
|
449
|
+
<div className="mb-6 flex items-start justify-between gap-4">
|
|
450
|
+
<div>
|
|
451
|
+
<h2 className="text-heading font-medium text-text-primary">Connect {connectAgent.name}</h2>
|
|
452
|
+
<div className="mt-1 flex items-center gap-2">
|
|
453
|
+
<p className="break-all font-mono text-[12px] text-text-tertiary">{connectAgent.id}</p>
|
|
454
|
+
<button
|
|
455
|
+
onClick={() => copyText(connectAgent.id, 'Agent ID copied')}
|
|
456
|
+
className="shrink-0 text-text-tertiary hover:text-text-secondary"
|
|
457
|
+
title="Copy agent ID"
|
|
458
|
+
>
|
|
459
|
+
<Copy className="h-3.5 w-3.5" />
|
|
460
|
+
</button>
|
|
461
|
+
</div>
|
|
462
|
+
</div>
|
|
463
|
+
<button onClick={() => setConnectAgent(null)} className="text-text-tertiary hover:text-text-secondary">
|
|
464
|
+
<X className="h-4 w-4" />
|
|
465
|
+
</button>
|
|
466
|
+
</div>
|
|
467
|
+
|
|
468
|
+
<div className="mb-5 flex flex-wrap gap-2 border-b border-border pb-3">
|
|
469
|
+
{[
|
|
470
|
+
['status', 'Status'],
|
|
471
|
+
['sdk', 'Node SDK'],
|
|
472
|
+
['mcp', 'MCP'],
|
|
473
|
+
['automation', 'Automation'],
|
|
474
|
+
['openclaw', 'OpenClaw'],
|
|
475
|
+
['rest', 'REST'],
|
|
476
|
+
['codex', 'Codex/Cursor'],
|
|
477
|
+
['activity', 'Activity'],
|
|
478
|
+
].map(([id, label]) => (
|
|
479
|
+
<button
|
|
480
|
+
key={id}
|
|
481
|
+
onClick={() => setConnectTab(id as typeof connectTab)}
|
|
482
|
+
className={`rounded border px-3 py-1.5 text-body transition-colors ${
|
|
483
|
+
connectTab === id
|
|
484
|
+
? 'border-accent bg-accent/10 text-accent'
|
|
485
|
+
: 'border-border bg-bg text-text-secondary hover:text-text-primary'
|
|
486
|
+
}`}
|
|
487
|
+
>
|
|
488
|
+
{label}
|
|
489
|
+
</button>
|
|
490
|
+
))}
|
|
491
|
+
</div>
|
|
492
|
+
|
|
493
|
+
{connectTab === 'status' && (
|
|
494
|
+
<>
|
|
495
|
+
<div className="mb-5 rounded border border-border bg-bg p-4">
|
|
496
|
+
<div className="mb-3 flex items-center justify-between">
|
|
497
|
+
<h3 className="text-body font-medium text-text-primary">Connection status</h3>
|
|
498
|
+
<button
|
|
499
|
+
onClick={() => openConnect(connectAgent)}
|
|
500
|
+
className="text-body text-accent hover:text-accent-hover"
|
|
501
|
+
>
|
|
502
|
+
Test again
|
|
503
|
+
</button>
|
|
504
|
+
</div>
|
|
505
|
+
{connectionLoading ? (
|
|
506
|
+
<p className="text-body text-text-tertiary">Testing connection...</p>
|
|
507
|
+
) : connection ? (
|
|
508
|
+
<>
|
|
509
|
+
<div className="grid gap-2 sm:grid-cols-2">
|
|
510
|
+
<StatusLine label="MCP" value={connection.mcp_status} success={connection.mcp_status === 'online'} />
|
|
511
|
+
<StatusLine label="Health" value={connectAgentHealth ? healthBadge[connectAgentHealth.status].label : 'unknown'} success={connectAgentHealth?.status === 'healthy'} />
|
|
512
|
+
<StatusLine label="Enforcement" value={enforcementBadge[connectAgent.enforcement_mode].label} success={connectAgent.enforcement_mode === 'enforce'} />
|
|
513
|
+
<StatusLine label="Last seen" value={connection.last_seen_at ? new Date(connection.last_seen_at).toLocaleString() : 'none'} />
|
|
514
|
+
<StatusLine label="Last real activity" value={connectAgentHealth?.last_activity_at ? new Date(connectAgentHealth.last_activity_at).toLocaleString() : 'none'} />
|
|
515
|
+
<StatusLine label="Last call" value={connection.last_successful_api_call?.activity_type || 'none'} />
|
|
516
|
+
<StatusLine label="Last error" value={connectAgentHealth?.last_error_code || connection.last_error?.error_code || 'none'} success={!connectAgentHealth?.last_error_at && !connection.last_error} />
|
|
517
|
+
<StatusLine
|
|
518
|
+
label="Latest activity"
|
|
519
|
+
value={connection.setup_readiness.latest_activity_type || 'none'}
|
|
520
|
+
/>
|
|
521
|
+
</div>
|
|
522
|
+
{connection.mcp_status !== 'online' && (
|
|
523
|
+
<p className="mt-3 text-body text-text-secondary">
|
|
524
|
+
MCP is offline. Go to the MCP tab if this agent should be connected through Claude, Cursor, or another MCP client.
|
|
525
|
+
</p>
|
|
526
|
+
)}
|
|
527
|
+
{connectAgentHealth?.status === 'quiet' && (
|
|
528
|
+
<p className="mt-3 text-body text-warning">
|
|
529
|
+
No Availsync calls in 3 days. Check SDK/MCP env config.
|
|
530
|
+
</p>
|
|
531
|
+
)}
|
|
532
|
+
{connectAgent.enforcement_mode === 'observe' && (
|
|
533
|
+
<p className="mt-3 text-body text-text-secondary">
|
|
534
|
+
Observe-only is active. This agent will still run; Availsync records what it would have blocked.
|
|
535
|
+
</p>
|
|
536
|
+
)}
|
|
537
|
+
<div className="mt-4 rounded border border-border bg-surface px-3 py-2">
|
|
538
|
+
<p className="text-body text-text-secondary">
|
|
539
|
+
Recommended pilot path: run verification, switch this agent to Observe,
|
|
540
|
+
install <code>@availsync/node@alpha</code>, then check Activity for real traffic.
|
|
541
|
+
</p>
|
|
542
|
+
<div className="mt-2 flex flex-wrap gap-3">
|
|
543
|
+
<button onClick={() => setConnectTab('sdk')} className="text-body text-accent hover:text-accent-hover">
|
|
544
|
+
Open Node SDK setup
|
|
545
|
+
</button>
|
|
546
|
+
<a href="/docs/sdk/node" className="text-body text-accent hover:text-accent-hover">
|
|
547
|
+
Read SDK docs
|
|
548
|
+
</a>
|
|
549
|
+
</div>
|
|
550
|
+
</div>
|
|
551
|
+
</>
|
|
552
|
+
) : (
|
|
553
|
+
<p className="text-body text-text-tertiary">Run a connection test to verify setup.</p>
|
|
554
|
+
)}
|
|
555
|
+
</div>
|
|
556
|
+
|
|
557
|
+
<div className="mb-5 rounded border border-border bg-bg p-4">
|
|
558
|
+
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
|
559
|
+
<div>
|
|
560
|
+
<h3 className="text-body font-medium text-text-primary">Dashboard setup test</h3>
|
|
561
|
+
<p className="mt-1 text-body text-text-secondary">
|
|
562
|
+
Run this safe dashboard test first, then connect your real MCP client.
|
|
563
|
+
</p>
|
|
564
|
+
</div>
|
|
565
|
+
<button
|
|
566
|
+
onClick={handleRunSetupTest}
|
|
567
|
+
disabled={setupTestRunning}
|
|
568
|
+
className="rounded bg-accent px-3 py-1.5 text-body font-medium text-white hover:bg-accent-hover disabled:opacity-50"
|
|
569
|
+
>
|
|
570
|
+
{setupTestRunning ? 'Running...' : 'Run test workflow'}
|
|
571
|
+
</button>
|
|
572
|
+
</div>
|
|
573
|
+
{setupTest && (
|
|
574
|
+
<div className="mt-4 space-y-2">
|
|
575
|
+
<div className="flex items-center justify-between">
|
|
576
|
+
<span className="font-mono text-[12px] text-text-tertiary">{setupTest.run_id}</span>
|
|
577
|
+
<Badge variant={setupTest.status === 'success' ? 'success' : setupTest.status === 'warning' ? 'warning' : 'error'}>
|
|
578
|
+
{setupTest.status}
|
|
579
|
+
</Badge>
|
|
580
|
+
</div>
|
|
581
|
+
<div className="space-y-2">
|
|
582
|
+
{setupTest.steps.map((step) => (
|
|
583
|
+
<SetupTestStepRow key={`${setupTest.run_id}-${step.name}`} step={step} />
|
|
584
|
+
))}
|
|
585
|
+
</div>
|
|
586
|
+
</div>
|
|
587
|
+
)}
|
|
588
|
+
</div>
|
|
589
|
+
|
|
590
|
+
{connection && (
|
|
591
|
+
<div className="mb-5 rounded border border-border bg-bg p-4">
|
|
592
|
+
<h3 className="mb-3 text-body font-medium text-text-primary">Setup checklist</h3>
|
|
593
|
+
<div className="grid gap-2 sm:grid-cols-2">
|
|
594
|
+
{readinessItems.map(([label, value]) => (
|
|
595
|
+
<div key={label} className="flex items-center justify-between gap-2 rounded border border-border bg-surface px-3 py-2 text-body">
|
|
596
|
+
<span className="text-text-secondary">{label}</span>
|
|
597
|
+
<Badge variant={value ? 'success' : 'neutral'}>{value ? 'Done' : 'Missing'}</Badge>
|
|
598
|
+
</div>
|
|
599
|
+
))}
|
|
600
|
+
</div>
|
|
601
|
+
{missingReadinessItems.length > 0 && (
|
|
602
|
+
<div className="mt-4 rounded border border-border bg-surface px-3 py-2">
|
|
603
|
+
<p className="text-body text-text-secondary">
|
|
604
|
+
Next: {missingReadinessItems.slice(0, 3).join(', ')}
|
|
605
|
+
{missingReadinessItems.length > 3 ? ` and ${missingReadinessItems.length - 3} more` : ''}.
|
|
606
|
+
</p>
|
|
607
|
+
</div>
|
|
608
|
+
)}
|
|
609
|
+
</div>
|
|
610
|
+
)}
|
|
611
|
+
|
|
612
|
+
<div className="rounded border border-border bg-bg p-4">
|
|
613
|
+
<div className="mb-3 flex items-center gap-2">
|
|
614
|
+
<KeyRound className="h-4 w-4 text-text-tertiary" />
|
|
615
|
+
<h3 className="text-body font-medium text-text-primary">API key</h3>
|
|
616
|
+
</div>
|
|
617
|
+
{rotatedKey ? (
|
|
618
|
+
<div>
|
|
619
|
+
<OneTimeKeyPanel apiKey={rotatedKey} onCopy={copyText} />
|
|
620
|
+
</div>
|
|
621
|
+
) : (
|
|
622
|
+
<button
|
|
623
|
+
onClick={handleRotateKey}
|
|
624
|
+
disabled={rotating}
|
|
625
|
+
className="rounded border border-error/40 px-3 py-1.5 text-body font-medium text-error hover:border-error hover:bg-error/10 disabled:opacity-50"
|
|
626
|
+
>
|
|
627
|
+
{rotating ? 'Rotating...' : 'Rotate key'}
|
|
628
|
+
</button>
|
|
629
|
+
)}
|
|
630
|
+
</div>
|
|
631
|
+
</>
|
|
632
|
+
)}
|
|
633
|
+
|
|
634
|
+
{connectTab === 'sdk' && (
|
|
635
|
+
<div className="space-y-4">
|
|
636
|
+
<div className="rounded border border-border bg-bg p-4">
|
|
637
|
+
<h3 className="text-body font-medium text-text-primary">Node SDK pilot path</h3>
|
|
638
|
+
<p className="mt-1 text-body text-text-secondary">
|
|
639
|
+
Install <code>@availsync/node@alpha</code>, keep the API key in env, and wrap the repo task with withClaim. Use Observe first when you want proof without blocking real work.
|
|
640
|
+
</p>
|
|
641
|
+
<div className="mt-3 flex flex-wrap gap-3">
|
|
642
|
+
<a href="/docs/sdk/node" className="text-body text-accent hover:text-accent-hover">
|
|
643
|
+
Full SDK docs
|
|
644
|
+
</a>
|
|
645
|
+
<a href="/docs/pilot" className="text-body text-accent hover:text-accent-hover">
|
|
646
|
+
Pilot guide
|
|
647
|
+
</a>
|
|
648
|
+
</div>
|
|
649
|
+
</div>
|
|
650
|
+
{sdkSnippets.map((snippet) => <SnippetCard key={snippet.id} snippet={snippet} onCopy={copyText} />)}
|
|
651
|
+
</div>
|
|
652
|
+
)}
|
|
653
|
+
|
|
654
|
+
{connectTab === 'mcp' && (
|
|
655
|
+
<div className="space-y-4">
|
|
656
|
+
<div className="rounded border border-border bg-bg p-4">
|
|
657
|
+
<h3 className="text-body font-medium text-text-primary">Codex app setup</h3>
|
|
658
|
+
<p className="mt-1 text-body text-text-secondary">
|
|
659
|
+
Open Codex, go to Settings, then MCP servers. Add a new server and use the
|
|
660
|
+
field values below. The API key is the one-time secret from create or rotate key.
|
|
661
|
+
</p>
|
|
662
|
+
<div className="mt-3 grid gap-2 text-body text-text-secondary sm:grid-cols-2">
|
|
663
|
+
{[
|
|
664
|
+
'Save the server.',
|
|
665
|
+
'Restart Codex or reload MCP servers.',
|
|
666
|
+
'Ask Codex to list available MCP tools.',
|
|
667
|
+
'Return here and click Test again.',
|
|
668
|
+
].map((step, index) => (
|
|
669
|
+
<div className="flex gap-2 rounded border border-border bg-surface px-3 py-2" key={step}>
|
|
670
|
+
<span className="font-mono text-text-tertiary">{index + 1}</span>
|
|
671
|
+
<span>{step}</span>
|
|
672
|
+
</div>
|
|
673
|
+
))}
|
|
674
|
+
</div>
|
|
675
|
+
</div>
|
|
676
|
+
<SnippetCard snippet={codexMcpSnippet} onCopy={copyText} />
|
|
677
|
+
<div className="rounded border border-border bg-bg p-4">
|
|
678
|
+
<h3 className="text-body font-medium text-text-primary">Multiple Codex sessions</h3>
|
|
679
|
+
<p className="mt-1 text-body text-text-secondary">
|
|
680
|
+
Use one shared agent id when you only need a global Codex lock across sessions.
|
|
681
|
+
Use one Availsync agent per Codex role when you want different priorities or
|
|
682
|
+
conflict behavior.
|
|
683
|
+
</p>
|
|
684
|
+
<div className="mt-3 grid gap-2 text-body text-text-secondary sm:grid-cols-2">
|
|
685
|
+
<div className="rounded border border-border bg-surface px-3 py-2">
|
|
686
|
+
<span className="font-medium text-text-primary">Shared agent</span>
|
|
687
|
+
<p className="mt-1 text-text-tertiary">Best when all Codex sessions act as the same operator.</p>
|
|
688
|
+
</div>
|
|
689
|
+
<div className="rounded border border-border bg-surface px-3 py-2">
|
|
690
|
+
<span className="font-medium text-text-primary">Role agents</span>
|
|
691
|
+
<p className="mt-1 text-text-tertiary">Best for builder, reviewer, deploy, cleanup, or experiment priorities.</p>
|
|
692
|
+
</div>
|
|
693
|
+
</div>
|
|
694
|
+
</div>
|
|
695
|
+
<SnippetCard snippet={codexMultiAgentSnippet} onCopy={copyText} />
|
|
696
|
+
<SnippetCard snippet={mcpSnippet} onCopy={copyText} />
|
|
697
|
+
<SnippetCard snippet={cursorSnippet} onCopy={copyText} />
|
|
698
|
+
<div className="rounded border border-border bg-bg p-4">
|
|
699
|
+
<h3 className="text-body font-medium text-text-primary">MCP tool order</h3>
|
|
700
|
+
<p className="mt-1 text-body text-text-secondary">
|
|
701
|
+
heartbeat {'->'} check_work_slot {'->'} claim_work_slot {'->'} extend_work_slot while working {'->'} release_work_slot.
|
|
702
|
+
</p>
|
|
703
|
+
<p className="mt-2 text-body text-text-tertiary">
|
|
704
|
+
If Codex does not show these tools, check that Node.js and npx work locally,
|
|
705
|
+
confirm the agent id matches this agent, and rotate the key if the original API
|
|
706
|
+
key was lost.
|
|
707
|
+
</p>
|
|
708
|
+
</div>
|
|
709
|
+
</div>
|
|
710
|
+
)}
|
|
711
|
+
|
|
712
|
+
{connectTab === 'automation' && (
|
|
713
|
+
<div className="space-y-4">
|
|
714
|
+
<div className="rounded border border-border bg-bg p-4">
|
|
715
|
+
<h3 className="text-body font-medium text-text-primary">Automation guardrail</h3>
|
|
716
|
+
<p className="mt-1 text-body text-text-secondary">
|
|
717
|
+
Generate a wrapper for scheduled Codex, Claude, Cursor, cron, or server jobs. Blocked runs return skip_run and should exit cleanly.
|
|
718
|
+
</p>
|
|
719
|
+
<p className="mt-2 text-body text-text-secondary">
|
|
720
|
+
To pilot safely, switch the agent to Observe on the Agents table. Observe mode never creates a lease; it only reports what Availsync would have blocked.
|
|
721
|
+
</p>
|
|
722
|
+
<p className="mt-2 text-body text-text-tertiary">
|
|
723
|
+
Default to repo-level for maximum safety. Use project-level only for independent streams; Availsync does not yet infer dependencies between different resources.
|
|
724
|
+
</p>
|
|
725
|
+
<div className="mt-4 grid gap-3 sm:grid-cols-[140px_1fr_120px]">
|
|
726
|
+
<div>
|
|
727
|
+
<label className="mb-1 block text-label uppercase text-text-tertiary">Resource</label>
|
|
728
|
+
<select
|
|
729
|
+
value={connectResource.resource_type}
|
|
730
|
+
onChange={(event) => setConnectResource({ ...connectResource, resource_type: event.target.value as 'project' | 'repo' })}
|
|
731
|
+
className="w-full rounded border border-border bg-surface px-3 py-2 text-body text-text-primary outline-none focus:border-border-focus"
|
|
732
|
+
>
|
|
733
|
+
<option value="repo">Repo · safest</option>
|
|
734
|
+
<option value="project">Project · advanced</option>
|
|
735
|
+
</select>
|
|
736
|
+
</div>
|
|
737
|
+
<div>
|
|
738
|
+
<label className="mb-1 block text-label uppercase text-text-tertiary">Resource key</label>
|
|
739
|
+
<input
|
|
740
|
+
value={connectResource.resource_key}
|
|
741
|
+
onChange={(event) => setConnectResource({ ...connectResource, resource_key: event.target.value })}
|
|
742
|
+
className="w-full rounded border border-border bg-surface px-3 py-2 text-body text-text-primary outline-none focus:border-border-focus"
|
|
743
|
+
placeholder="owner/repo"
|
|
744
|
+
/>
|
|
745
|
+
</div>
|
|
746
|
+
<div>
|
|
747
|
+
<label className="mb-1 block text-label uppercase text-text-tertiary">Minutes</label>
|
|
748
|
+
<input
|
|
749
|
+
type="number"
|
|
750
|
+
min={1}
|
|
751
|
+
max={240}
|
|
752
|
+
value={connectResource.duration_minutes}
|
|
753
|
+
onChange={(event) => setConnectResource({ ...connectResource, duration_minutes: Number(event.target.value) || 45 })}
|
|
754
|
+
className="w-full rounded border border-border bg-surface px-3 py-2 text-body text-text-primary outline-none focus:border-border-focus"
|
|
755
|
+
/>
|
|
756
|
+
</div>
|
|
757
|
+
</div>
|
|
758
|
+
</div>
|
|
759
|
+
{automationSnippets.map((snippet) => <SnippetCard key={snippet.id} snippet={snippet} onCopy={copyText} />)}
|
|
760
|
+
</div>
|
|
761
|
+
)}
|
|
762
|
+
|
|
763
|
+
{connectTab === 'openclaw' && (
|
|
764
|
+
<div className="space-y-4">
|
|
765
|
+
<div className="rounded border border-border bg-bg p-4">
|
|
766
|
+
<h3 className="text-body font-medium text-text-primary">OpenClaw skill pack</h3>
|
|
767
|
+
<p className="mt-1 text-body text-text-secondary">
|
|
768
|
+
Install the SKILL.md in OpenClaw and keep API_KEY in env. The skill teaches OpenClaw to start, extend, and finish Availsync work runs before touching the repo.
|
|
769
|
+
</p>
|
|
770
|
+
</div>
|
|
771
|
+
<SnippetCard snippet={openClawSkill} onCopy={copyText} />
|
|
772
|
+
<SnippetCard snippet={openClawSetup} onCopy={copyText} />
|
|
773
|
+
</div>
|
|
774
|
+
)}
|
|
775
|
+
|
|
776
|
+
{connectTab === 'rest' && (
|
|
777
|
+
<div className="space-y-4">
|
|
778
|
+
<div className="rounded border border-border bg-bg p-4">
|
|
779
|
+
<h3 className="text-body font-medium text-text-primary">Work guardrail flow</h3>
|
|
780
|
+
<p className="mt-1 text-body text-text-secondary">
|
|
781
|
+
Use Availsync to stop agents from acting on the same selected resource at the same time. Repo-level is the safest default.
|
|
782
|
+
</p>
|
|
783
|
+
</div>
|
|
784
|
+
{workSnippets.map((snippet) => <SnippetCard key={snippet.id} snippet={snippet} onCopy={copyText} />)}
|
|
785
|
+
{scheduleSnippets.map((snippet) => <SnippetCard key={snippet.id} snippet={snippet} onCopy={copyText} />)}
|
|
786
|
+
</div>
|
|
787
|
+
)}
|
|
788
|
+
|
|
789
|
+
{connectTab === 'codex' && (
|
|
790
|
+
<div className="space-y-4">
|
|
791
|
+
<SnippetCard snippet={codexSnippet} onCopy={copyText} />
|
|
792
|
+
<SnippetCard snippet={cronSnippet} onCopy={copyText} />
|
|
793
|
+
<div className="rounded border border-border bg-bg p-4">
|
|
794
|
+
<h3 className="text-body font-medium text-text-primary">Plugin roadmap</h3>
|
|
795
|
+
<p className="mt-1 text-body text-text-secondary">
|
|
796
|
+
A dedicated Codex plugin is a future integration layer. For now, use the MCP server or REST flow above.
|
|
797
|
+
</p>
|
|
798
|
+
</div>
|
|
799
|
+
</div>
|
|
800
|
+
)}
|
|
801
|
+
|
|
802
|
+
{connectTab === 'activity' && (
|
|
803
|
+
<div className="space-y-4">
|
|
804
|
+
<div className="rounded border border-border bg-bg p-4">
|
|
805
|
+
<h3 className="text-body font-medium text-text-primary">Activity validation</h3>
|
|
806
|
+
<p className="mt-1 text-body text-text-secondary">
|
|
807
|
+
Activity shows MCP/API/runtime traffic. Audit stays focused on governance, work lifecycle decisions, holds, and conflicts.
|
|
808
|
+
</p>
|
|
809
|
+
<button
|
|
810
|
+
onClick={() => { window.location.href = `/app/activity?agent=${connectAgent.id}`; }}
|
|
811
|
+
className="mt-4 rounded bg-accent px-3 py-1.5 text-body font-medium text-white hover:bg-accent-hover"
|
|
812
|
+
>
|
|
813
|
+
Open Activity filtered to this agent
|
|
814
|
+
</button>
|
|
815
|
+
</div>
|
|
816
|
+
<div className="rounded border border-border bg-bg p-4">
|
|
817
|
+
<h3 className="text-body font-medium text-text-primary">Latest observed event</h3>
|
|
818
|
+
<p className="mt-1 font-mono text-[12px] text-text-tertiary">
|
|
819
|
+
{connection?.setup_readiness.latest_activity_type || 'none'} {connection?.setup_readiness.latest_activity_at || ''}
|
|
820
|
+
</p>
|
|
821
|
+
</div>
|
|
822
|
+
</div>
|
|
823
|
+
)}
|
|
824
|
+
</div>
|
|
825
|
+
</div>
|
|
826
|
+
)}
|
|
827
|
+
|
|
828
|
+
{/* Agent table */}
|
|
829
|
+
{loading ? (
|
|
830
|
+
<div className="rounded border border-border overflow-hidden">
|
|
831
|
+
{Array.from({ length: 3 }).map((_, i) => (
|
|
832
|
+
<SkeletonRow key={i} />
|
|
833
|
+
))}
|
|
834
|
+
</div>
|
|
835
|
+
) : agents.length === 0 ? (
|
|
836
|
+
<EmptyState
|
|
837
|
+
icon={Users}
|
|
838
|
+
title="No agents yet"
|
|
839
|
+
description="Create your first agent, copy its one-time API key, then install the Node SDK to send real guarded runs."
|
|
840
|
+
action={
|
|
841
|
+
<button
|
|
842
|
+
onClick={() => setShowNewPanel(true)}
|
|
843
|
+
className="rounded bg-accent text-white px-3 py-1.5 text-body font-medium hover:bg-accent-hover transition-colors"
|
|
844
|
+
>
|
|
845
|
+
Create agent
|
|
846
|
+
</button>
|
|
847
|
+
}
|
|
848
|
+
/>
|
|
849
|
+
) : (
|
|
850
|
+
<div className="overflow-x-auto rounded border border-border dark-scroll">
|
|
851
|
+
<table className="w-full min-w-[1180px]">
|
|
852
|
+
<colgroup>
|
|
853
|
+
<col className="w-[150px]" />
|
|
854
|
+
<col className="w-[130px]" />
|
|
855
|
+
<col className="w-[100px]" />
|
|
856
|
+
<col className="w-[95px]" />
|
|
857
|
+
<col className="w-[135px]" />
|
|
858
|
+
<col className="w-[150px]" />
|
|
859
|
+
<col className="w-[100px]" />
|
|
860
|
+
<col className="w-[85px]" />
|
|
861
|
+
<col className="w-[120px]" />
|
|
862
|
+
<col className="w-[115px]" />
|
|
863
|
+
</colgroup>
|
|
864
|
+
<thead>
|
|
865
|
+
<tr className="border-b border-border bg-surface text-label uppercase text-text-tertiary">
|
|
866
|
+
<th className="px-4 py-2 text-left font-medium">Name</th>
|
|
867
|
+
<th className="px-4 py-2 text-left font-medium">Agent ID</th>
|
|
868
|
+
<th className="px-4 py-2 text-left font-medium">Health</th>
|
|
869
|
+
<th className="px-4 py-2 text-left font-medium">Mode</th>
|
|
870
|
+
<th className="px-4 py-2 text-left font-medium">MCP</th>
|
|
871
|
+
<th className="px-4 py-2 text-left font-medium">Last seen</th>
|
|
872
|
+
<th className="px-4 py-2 text-left font-medium">Type</th>
|
|
873
|
+
<th className="px-4 py-2 text-left font-medium">Priority</th>
|
|
874
|
+
<th className="px-4 py-2 text-left font-medium">Created</th>
|
|
875
|
+
<th className="px-4 py-2 text-right font-medium">Actions</th>
|
|
876
|
+
</tr>
|
|
877
|
+
</thead>
|
|
878
|
+
<tbody>
|
|
879
|
+
{agents.map((agent) => {
|
|
880
|
+
const status = mcpStatus(agent);
|
|
881
|
+
const health = healthByAgent.get(agent.id);
|
|
882
|
+
return (
|
|
883
|
+
<tr
|
|
884
|
+
key={agent.id}
|
|
885
|
+
className="h-11 border-b border-border transition-colors duration-150 hover:bg-surface-raised"
|
|
886
|
+
>
|
|
887
|
+
<td className="px-4 py-2 text-body font-medium text-text-primary">
|
|
888
|
+
{editingAgentId === agent.id ? (
|
|
889
|
+
<input
|
|
890
|
+
value={editForm.name}
|
|
891
|
+
onChange={(e) => setEditForm({ ...editForm, name: e.target.value })}
|
|
892
|
+
className="w-full rounded bg-bg border border-border px-2 py-1 text-body text-text-primary outline-none focus:border-border-focus"
|
|
893
|
+
/>
|
|
894
|
+
) : (
|
|
895
|
+
<span className="block truncate" title={agent.name}>{agent.name}</span>
|
|
896
|
+
)}
|
|
897
|
+
</td>
|
|
898
|
+
<td className="px-4 py-2">
|
|
899
|
+
<div className="flex items-center gap-2 whitespace-nowrap">
|
|
900
|
+
<span className="font-mono text-[12px] text-text-secondary" title={agent.id}>
|
|
901
|
+
{agent.id.slice(0, 8)}...
|
|
902
|
+
</span>
|
|
903
|
+
<button
|
|
904
|
+
onClick={() => {
|
|
905
|
+
navigator.clipboard.writeText(agent.id);
|
|
906
|
+
toast('Agent ID copied', 'success');
|
|
907
|
+
}}
|
|
908
|
+
className="shrink-0 text-text-tertiary hover:text-text-secondary"
|
|
909
|
+
title="Copy full agent ID"
|
|
910
|
+
>
|
|
911
|
+
<Copy className="h-3.5 w-3.5" />
|
|
912
|
+
</button>
|
|
913
|
+
</div>
|
|
914
|
+
</td>
|
|
915
|
+
<td className="px-4 py-2">
|
|
916
|
+
{health ? (
|
|
917
|
+
<Badge variant={healthBadge[health.status].variant}>{healthBadge[health.status].label}</Badge>
|
|
918
|
+
) : (
|
|
919
|
+
<Badge variant="neutral">Unknown</Badge>
|
|
920
|
+
)}
|
|
921
|
+
</td>
|
|
922
|
+
<td className="px-4 py-2">
|
|
923
|
+
{editingAgentId === agent.id ? (
|
|
924
|
+
<select
|
|
925
|
+
value={editForm.enforcement_mode}
|
|
926
|
+
onChange={(e) => setEditForm({ ...editForm, enforcement_mode: e.target.value as Agent['enforcement_mode'] })}
|
|
927
|
+
className="rounded bg-bg border border-border px-2 py-1 text-body text-text-primary outline-none focus:border-border-focus"
|
|
928
|
+
>
|
|
929
|
+
<option value="enforce">Enforce</option>
|
|
930
|
+
<option value="observe">Observe</option>
|
|
931
|
+
</select>
|
|
932
|
+
) : (
|
|
933
|
+
<Badge variant={enforcementBadge[agent.enforcement_mode].variant}>{enforcementBadge[agent.enforcement_mode].label}</Badge>
|
|
934
|
+
)}
|
|
935
|
+
</td>
|
|
936
|
+
<td className="px-4 py-2">
|
|
937
|
+
<div className="flex items-center gap-2 whitespace-nowrap">
|
|
938
|
+
<Badge variant={status.variant}>{status.label}</Badge>
|
|
939
|
+
<span className="text-label text-text-tertiary">{status.detail}</span>
|
|
940
|
+
</div>
|
|
941
|
+
</td>
|
|
942
|
+
<td className="whitespace-nowrap px-4 py-2 font-mono text-body text-text-tertiary">
|
|
943
|
+
{formatAgentTimestamp(agent.last_seen_at ?? agent.last_used_at)}
|
|
944
|
+
</td>
|
|
945
|
+
<td className="px-4 py-2">
|
|
946
|
+
{editingAgentId === agent.id ? (
|
|
947
|
+
<select
|
|
948
|
+
value={editForm.agent_type}
|
|
949
|
+
onChange={(e) => setEditForm({ ...editForm, agent_type: e.target.value })}
|
|
950
|
+
className="rounded bg-bg border border-border px-2 py-1 text-body text-text-primary outline-none focus:border-border-focus"
|
|
951
|
+
>
|
|
952
|
+
<option value="external_meeting">External</option>
|
|
953
|
+
<option value="internal">Internal</option>
|
|
954
|
+
<option value="focus">Focus</option>
|
|
955
|
+
<option value="generic">Generic</option>
|
|
956
|
+
</select>
|
|
957
|
+
) : (
|
|
958
|
+
<Badge variant={typeBadgeVariant[agent.agent_type] || 'neutral'}>
|
|
959
|
+
{typeLabels[agent.agent_type] || agent.agent_type}
|
|
960
|
+
</Badge>
|
|
961
|
+
)}
|
|
962
|
+
</td>
|
|
963
|
+
<td className="whitespace-nowrap px-4 py-2 font-mono text-body text-text-secondary">
|
|
964
|
+
{editingAgentId === agent.id ? (
|
|
965
|
+
<input
|
|
966
|
+
type="number"
|
|
967
|
+
value={editForm.priority}
|
|
968
|
+
onChange={(e) => setEditForm({ ...editForm, priority: Number(e.target.value) })}
|
|
969
|
+
className="w-20 rounded bg-bg border border-border px-2 py-1 text-body text-text-primary outline-none focus:border-border-focus"
|
|
970
|
+
/>
|
|
971
|
+
) : (
|
|
972
|
+
agent.priority
|
|
973
|
+
)}
|
|
974
|
+
</td>
|
|
975
|
+
<td className="whitespace-nowrap px-4 py-2 font-mono text-body text-text-tertiary">
|
|
976
|
+
{formatAgentTimestamp(agent.created_at).slice(0, 10)}
|
|
977
|
+
</td>
|
|
978
|
+
<td className="px-4 py-2">
|
|
979
|
+
<div className="flex items-center justify-end gap-3 whitespace-nowrap">
|
|
980
|
+
{editingAgentId === agent.id ? (
|
|
981
|
+
<>
|
|
982
|
+
<button
|
|
983
|
+
onClick={handleUpdate}
|
|
984
|
+
className="shrink-0 text-success hover:text-success/80"
|
|
985
|
+
title="Save changes"
|
|
986
|
+
>
|
|
987
|
+
<Check className="h-3.5 w-3.5" />
|
|
988
|
+
</button>
|
|
989
|
+
<button
|
|
990
|
+
onClick={() => setEditingAgentId(null)}
|
|
991
|
+
className="shrink-0 text-text-tertiary hover:text-text-secondary"
|
|
992
|
+
title="Cancel"
|
|
993
|
+
>
|
|
994
|
+
<X className="h-3.5 w-3.5" />
|
|
995
|
+
</button>
|
|
996
|
+
</>
|
|
997
|
+
) : (
|
|
998
|
+
<>
|
|
999
|
+
<button
|
|
1000
|
+
onClick={() => openConnect(agent)}
|
|
1001
|
+
className="shrink-0 text-text-tertiary hover:text-text-secondary"
|
|
1002
|
+
title="Connect agent"
|
|
1003
|
+
>
|
|
1004
|
+
<Plug className="h-3.5 w-3.5" />
|
|
1005
|
+
</button>
|
|
1006
|
+
<button
|
|
1007
|
+
onClick={() => startEdit(agent)}
|
|
1008
|
+
className="shrink-0 text-text-tertiary hover:text-text-secondary"
|
|
1009
|
+
title="Edit agent"
|
|
1010
|
+
>
|
|
1011
|
+
<Pencil className="h-3.5 w-3.5" />
|
|
1012
|
+
</button>
|
|
1013
|
+
<button
|
|
1014
|
+
onClick={() => handleDelete(agent)}
|
|
1015
|
+
className="shrink-0 text-error hover:text-error/80"
|
|
1016
|
+
title="Delete agent"
|
|
1017
|
+
>
|
|
1018
|
+
<Trash2 className="h-3.5 w-3.5" />
|
|
1019
|
+
</button>
|
|
1020
|
+
</>
|
|
1021
|
+
)}
|
|
1022
|
+
</div>
|
|
1023
|
+
</td>
|
|
1024
|
+
</tr>
|
|
1025
|
+
);
|
|
1026
|
+
})}
|
|
1027
|
+
</tbody>
|
|
1028
|
+
</table>
|
|
1029
|
+
</div>
|
|
1030
|
+
)}
|
|
1031
|
+
</div>
|
|
1032
|
+
);
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
function StatusLine({ label, value, success }: { label: string; value: string; success?: boolean }) {
|
|
1036
|
+
return (
|
|
1037
|
+
<div className="rounded border border-border bg-surface p-3">
|
|
1038
|
+
<div className="text-label uppercase text-text-tertiary">{label}</div>
|
|
1039
|
+
<div className="mt-1 flex items-center justify-between gap-2">
|
|
1040
|
+
<span className="truncate text-body text-text-secondary">{value}</span>
|
|
1041
|
+
{success != null && <Badge variant={success ? 'success' : 'neutral'}>{success ? 'OK' : 'Check'}</Badge>}
|
|
1042
|
+
</div>
|
|
1043
|
+
</div>
|
|
1044
|
+
);
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
function SetupTestStepRow({ step }: { step: SetupTestRun['steps'][number] }) {
|
|
1048
|
+
const variant =
|
|
1049
|
+
step.status === 'success' ? 'success' : step.status === 'warning' ? 'warning' : step.status === 'blocked' || step.status === 'error' ? 'error' : 'neutral';
|
|
1050
|
+
|
|
1051
|
+
return (
|
|
1052
|
+
<div className="rounded border border-border bg-surface p-3">
|
|
1053
|
+
<div className="flex items-start justify-between gap-3">
|
|
1054
|
+
<div>
|
|
1055
|
+
<div className="font-mono text-[12px] text-text-primary">{step.name.replaceAll('_', ' ')}</div>
|
|
1056
|
+
<p className="mt-1 text-body text-text-secondary">{step.summary}</p>
|
|
1057
|
+
{step.next_action && (
|
|
1058
|
+
<p className="mt-1 text-body text-warning">{step.next_action}</p>
|
|
1059
|
+
)}
|
|
1060
|
+
</div>
|
|
1061
|
+
<div className="flex shrink-0 flex-col items-end gap-1">
|
|
1062
|
+
<Badge variant={variant}>{step.status}</Badge>
|
|
1063
|
+
<span className="font-mono text-[11px] text-text-tertiary">{step.latency_ms}ms</span>
|
|
1064
|
+
</div>
|
|
1065
|
+
</div>
|
|
1066
|
+
</div>
|
|
1067
|
+
);
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
function OneTimeKeyPanel({
|
|
1071
|
+
apiKey,
|
|
1072
|
+
onCopy,
|
|
1073
|
+
saved,
|
|
1074
|
+
onSavedChange,
|
|
1075
|
+
}: {
|
|
1076
|
+
apiKey: string;
|
|
1077
|
+
onCopy: (text: string, label?: string) => void;
|
|
1078
|
+
saved?: boolean;
|
|
1079
|
+
onSavedChange?: (saved: boolean) => void;
|
|
1080
|
+
}) {
|
|
1081
|
+
const [revealed, setRevealed] = useState(false);
|
|
1082
|
+
|
|
1083
|
+
return (
|
|
1084
|
+
<div className="mb-4 rounded border border-warning/30 bg-bg p-4">
|
|
1085
|
+
<p className="mb-2 text-body font-medium text-warning">Copy now. This key will not be shown again.</p>
|
|
1086
|
+
<div className="mb-3 flex items-center gap-2 rounded bg-surface p-3">
|
|
1087
|
+
<input
|
|
1088
|
+
readOnly
|
|
1089
|
+
type={revealed ? 'text' : 'password'}
|
|
1090
|
+
value={apiKey}
|
|
1091
|
+
className="min-w-0 flex-1 bg-transparent font-mono text-[12px] text-text-secondary outline-none"
|
|
1092
|
+
/>
|
|
1093
|
+
<button
|
|
1094
|
+
onClick={() => setRevealed((value) => !value)}
|
|
1095
|
+
className="shrink-0 text-text-tertiary hover:text-text-secondary"
|
|
1096
|
+
title={revealed ? 'Hide API key' : 'Reveal API key'}
|
|
1097
|
+
>
|
|
1098
|
+
{revealed ? <EyeOff className="h-3.5 w-3.5" /> : <Eye className="h-3.5 w-3.5" />}
|
|
1099
|
+
</button>
|
|
1100
|
+
</div>
|
|
1101
|
+
<div className="flex flex-wrap items-center gap-3">
|
|
1102
|
+
<button
|
|
1103
|
+
onClick={() => {
|
|
1104
|
+
onCopy(apiKey, 'API key copied');
|
|
1105
|
+
onSavedChange?.(true);
|
|
1106
|
+
}}
|
|
1107
|
+
className="flex items-center gap-1.5 text-body text-accent hover:text-accent-hover"
|
|
1108
|
+
>
|
|
1109
|
+
<Copy className="h-3.5 w-3.5" />
|
|
1110
|
+
Copy to clipboard
|
|
1111
|
+
</button>
|
|
1112
|
+
{onSavedChange && (
|
|
1113
|
+
<label className="flex items-center gap-2 text-body text-text-secondary">
|
|
1114
|
+
<input
|
|
1115
|
+
type="checkbox"
|
|
1116
|
+
checked={Boolean(saved)}
|
|
1117
|
+
onChange={(event) => onSavedChange(event.target.checked)}
|
|
1118
|
+
className="h-4 w-4 accent-accent"
|
|
1119
|
+
/>
|
|
1120
|
+
I saved this key
|
|
1121
|
+
</label>
|
|
1122
|
+
)}
|
|
1123
|
+
</div>
|
|
1124
|
+
</div>
|
|
1125
|
+
);
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
function SnippetCard({
|
|
1129
|
+
snippet,
|
|
1130
|
+
onCopy,
|
|
1131
|
+
}: {
|
|
1132
|
+
snippet: SetupSnippet;
|
|
1133
|
+
onCopy: (text: string, label?: string) => void;
|
|
1134
|
+
}) {
|
|
1135
|
+
return (
|
|
1136
|
+
<div className="rounded border border-border bg-bg p-4">
|
|
1137
|
+
<div className="mb-3 flex items-start justify-between gap-3">
|
|
1138
|
+
<div>
|
|
1139
|
+
<h3 className="text-body font-medium text-text-primary">{snippet.title}</h3>
|
|
1140
|
+
<p className="mt-1 text-body text-text-secondary">{snippet.description}</p>
|
|
1141
|
+
</div>
|
|
1142
|
+
<button
|
|
1143
|
+
onClick={() => onCopy(snippet.code, snippet.copyLabel)}
|
|
1144
|
+
className="flex shrink-0 items-center gap-1.5 rounded border border-border bg-surface px-2 py-1 text-body text-text-secondary hover:text-text-primary"
|
|
1145
|
+
>
|
|
1146
|
+
<Copy className="h-3.5 w-3.5" />
|
|
1147
|
+
Copy
|
|
1148
|
+
</button>
|
|
1149
|
+
</div>
|
|
1150
|
+
<pre className="max-h-72 overflow-auto rounded border border-border bg-surface p-3 text-[11px] text-text-secondary whitespace-pre-wrap">
|
|
1151
|
+
{snippet.code}
|
|
1152
|
+
</pre>
|
|
1153
|
+
</div>
|
|
1154
|
+
);
|
|
1155
|
+
}
|