Compare commits

...

12 Commits

Author SHA1 Message Date
Lluis Agusti
104854a008 chore: review suggestions (2) 2026-01-14 19:44:04 +07:00
Lluis Agusti
e4b3d2d4ef chore: review suggestions (1) 2026-01-14 19:32:45 +07:00
Lluis Agusti
dc35acb4e6 chore: prettier 2026-01-14 19:23:26 +07:00
Lluis Agusti
2c70f11ec7 chore: cleanup 2026-01-14 19:03:18 +07:00
Lluis Agusti
be7e1b8635 Merge remote-tracking branch 'origin/dev' into fix/credentials-hell 2026-01-14 18:41:09 +07:00
Zamil Majdy
61efee4139 fix(frontend): Remove hardcoded bypass of billing feature flag (#11762)
## Summary

Fixes a critical security issue where the billing button in the settings
sidebar was always visible to all users, bypassing the
`ENABLE_PLATFORM_PAYMENT` feature flag.

## Changes 🏗️

- Removed hardcoded `|| true` condition in
`frontend/src/app/(platform)/profile/(user)/layout.tsx:32` that was
bypassing the feature flag check
- The billing button is now properly gated by the
`ENABLE_PLATFORM_PAYMENT` feature flag as intended

## Root Cause

The `|| true` was accidentally left in commit
3dbc03e488 (PR #11617 - OAuth API & Single
Sign-On) from December 19, 2025. It was likely added temporarily during
development/testing to always show the billing button, but was not
removed before merging.

## Test Plan

1. Verify feature flag is set to disabled in LaunchDarkly
2. Navigate to settings page (`/profile/settings`)
3. Confirm billing button is NOT visible in the sidebar
4. Enable feature flag in LaunchDarkly
5. Refresh page and confirm billing button IS now visible
6. Verify billing page (`/profile/credits`) is still accessible via
direct URL when feature flag is disabled

## Checklist 📋

### For code changes:
- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [x] I have tested my changes according to the test plan

Fixes SECRT-1791

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* The Billing link in the profile sidebar now respects the payment
feature flag configuration and will only display when payment
functionality is enabled.

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-01-14 03:28:36 +00:00
Lluis Agusti
302f16c472 chore: wip 2026-01-13 23:26:25 +07:00
Lluis Agusti
4fede09fce chore: fixes 2026-01-13 21:27:03 +07:00
Lluis Agusti
4ce617d7e1 chore: wip 2026-01-13 20:07:32 +07:00
Bently
e539280e98 fix(blocks): set User-Agent header and URL-encode topic in GetWikipediaSummaryBlock (#11754)
The GetWikipediaSummaryBlock was returning HTTP 403 errors from
Wikipedia's API because it wasn't explicitly setting a User-Agent header
that complies with https://wikitech.wikimedia.org/wiki/Robot_policy.
Additionally, topics with spaces or special characters would cause
malformed URLs.

Fixes: OPEN-2889

Changes 🏗️

- URL-encode the topic parameter using urllib.parse.quote() to handle
spaces and special characters
- Explicitly set required headers per Wikimedia robot policy:
- User-Agent: Platform default user agent (includes app name, URL, and
contact email)
- Accept-Encoding: gzip, deflate: Recommended by Wikimedia to reduce
bandwidth
- Updated test mock to match the new function signature

Checklist 📋

For code changes:

- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [x] I have tested my changes according to the test plan:
  - [x] Verify code passes syntax check
  - [x] Verify code passes ruff linting
- [x] Create an agent using GetWikipediaSummaryBlock with a topic
containing spaces (e.g., "Artificial Intelligence")
  - [x] Verify the block returns a Wikipedia summary without 403 errors

For configuration changes:

- .env.default is updated or already compatible with my changes
- docker-compose.yml is updated or already compatible with my changes
- I have included a list of my configuration changes in the PR
description (under Changes)
.
N/A - No configuration changes required.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Improved Wikipedia API requests by adding compatible request headers
(including a proper user agent and encoding acceptance) for more
reliable responses.
* Enhanced handling of search topics by URL-encoding terms so queries
with spaces or special characters return correct results.

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-01-13 12:24:51 +00:00
Lluis Agusti
8957ecb099 Merge remote-tracking branch 'origin/dev' into fix/run-modal-layout-fixes 2026-01-13 15:37:53 +07:00
Lluis Agusti
d2305d047d chore: wip 2026-01-13 15:37:38 +07:00
55 changed files with 1856 additions and 708 deletions

View File

@@ -175,6 +175,7 @@ async def callback(
f"Successfully processed OAuth callback for user {user_id} "
f"and provider {provider.value}"
)
return CredentialsMetaResponse(
id=credentials.id,
provider=credentials.provider,
@@ -193,6 +194,7 @@ async def list_credentials(
user_id: Annotated[str, Security(get_user_id)],
) -> list[CredentialsMetaResponse]:
credentials = await creds_manager.store.get_all_creds(user_id)
return [
CredentialsMetaResponse(
id=cred.id,
@@ -215,6 +217,7 @@ async def list_credentials_by_provider(
user_id: Annotated[str, Security(get_user_id)],
) -> list[CredentialsMetaResponse]:
credentials = await creds_manager.store.get_creds_by_provider(user_id, provider)
return [
CredentialsMetaResponse(
id=cred.id,
@@ -831,6 +834,18 @@ async def list_providers() -> List[str]:
return all_providers
@router.get("/providers/system", response_model=List[str])
async def list_system_providers() -> List[str]:
"""
Get a list of providers that have platform credits (system credentials) available.
These providers can be used without the user providing their own API keys.
"""
from backend.integrations.credentials_store import SYSTEM_PROVIDERS
return list(SYSTEM_PROVIDERS)
@router.get("/providers/names", response_model=ProviderNamesResponse)
async def get_provider_names() -> ProviderNamesResponse:
"""

View File

@@ -18,6 +18,7 @@ from backend.data.model import (
SchemaField,
)
from backend.integrations.providers import ProviderName
from backend.util.request import DEFAULT_USER_AGENT
class GetWikipediaSummaryBlock(Block, GetRequest):
@@ -39,17 +40,27 @@ class GetWikipediaSummaryBlock(Block, GetRequest):
output_schema=GetWikipediaSummaryBlock.Output,
test_input={"topic": "Artificial Intelligence"},
test_output=("summary", "summary content"),
test_mock={"get_request": lambda url, json: {"extract": "summary content"}},
test_mock={
"get_request": lambda url, headers, json: {"extract": "summary content"}
},
)
async def run(self, input_data: Input, **kwargs) -> BlockOutput:
topic = input_data.topic
url = f"https://en.wikipedia.org/api/rest_v1/page/summary/{topic}"
# URL-encode the topic to handle spaces and special characters
encoded_topic = quote(topic, safe="")
url = f"https://en.wikipedia.org/api/rest_v1/page/summary/{encoded_topic}"
# Set headers per Wikimedia robot policy (https://w.wiki/4wJS)
# - User-Agent: Required, must identify the bot
# - Accept-Encoding: gzip recommended to reduce bandwidth
headers = {
"User-Agent": DEFAULT_USER_AGENT,
"Accept-Encoding": "gzip, deflate",
}
# Note: User-Agent is now automatically set by the request library
# to comply with Wikimedia's robot policy (https://w.wiki/4wJS)
try:
response = await self.get_request(url, json=True)
response = await self.get_request(url, headers=headers, json=True)
if "extract" not in response:
raise ValueError(f"Unable to parse Wikipedia response: {response}")
yield "summary", response["extract"]

View File

@@ -245,6 +245,21 @@ DEFAULT_CREDENTIALS = [
webshare_proxy_credentials,
]
SYSTEM_CREDENTIAL_IDS = {cred.id for cred in DEFAULT_CREDENTIALS}
# Set of providers that have system credentials available
SYSTEM_PROVIDERS = {cred.provider for cred in DEFAULT_CREDENTIALS}
def is_system_credential(credential_id: str) -> bool:
"""Check if a credential ID belongs to a system-managed credential."""
return credential_id in SYSTEM_CREDENTIAL_IDS
def is_system_provider(provider: str) -> bool:
"""Check if a provider has system-managed credentials available."""
return provider in SYSTEM_PROVIDERS
class IntegrationCredentialsStore:
def __init__(self):

View File

@@ -3,6 +3,13 @@ import { withSentryConfig } from "@sentry/nextjs";
/** @type {import('next').NextConfig} */
const nextConfig = {
productionBrowserSourceMaps: true,
// Externalize OpenTelemetry packages to fix Turbopack HMR issues
serverExternalPackages: [
"@opentelemetry/instrumentation",
"@opentelemetry/sdk-node",
"import-in-the-middle",
"require-in-the-middle",
],
experimental: {
serverActions: {
bodySizeLimit: "256mb",

View File

@@ -32,6 +32,7 @@
"@hookform/resolvers": "5.2.2",
"@next/third-parties": "15.4.6",
"@phosphor-icons/react": "2.1.10",
"@radix-ui/react-accordion": "1.2.12",
"@radix-ui/react-alert-dialog": "1.1.15",
"@radix-ui/react-avatar": "1.1.10",
"@radix-ui/react-checkbox": "1.3.3",
@@ -117,6 +118,7 @@
},
"devDependencies": {
"@chromatic-com/storybook": "4.1.2",
"@opentelemetry/instrumentation": "0.209.0",
"@playwright/test": "1.56.1",
"@storybook/addon-a11y": "9.1.5",
"@storybook/addon-docs": "9.1.5",
@@ -140,6 +142,7 @@
"eslint": "8.57.1",
"eslint-config-next": "15.5.7",
"eslint-plugin-storybook": "9.1.5",
"import-in-the-middle": "2.0.2",
"msw": "2.11.6",
"msw-storybook-addon": "2.0.6",
"orval": "7.13.0",
@@ -147,7 +150,7 @@
"postcss": "8.5.6",
"prettier": "3.6.2",
"prettier-plugin-tailwindcss": "0.7.1",
"require-in-the-middle": "7.5.2",
"require-in-the-middle": "8.0.1",
"storybook": "9.1.5",
"tailwindcss": "3.4.17",
"typescript": "5.9.3"
@@ -157,5 +160,10 @@
"public"
]
},
"pnpm": {
"overrides": {
"@opentelemetry/instrumentation": "0.209.0"
}
},
"packageManager": "pnpm@10.20.0+sha512.cf9998222162dd85864d0a8102e7892e7ba4ceadebbf5a31f9c2fce48dfce317a9c53b9f6464d1ef9042cba2e02ae02a9f7c143a2b438cd93c91840f0192b9dd"
}

View File

@@ -4,6 +4,9 @@ settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
overrides:
'@opentelemetry/instrumentation': 0.209.0
importers:
.:
@@ -20,6 +23,9 @@ importers:
'@phosphor-icons/react':
specifier: 2.1.10
version: 2.1.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-accordion':
specifier: 1.2.12
version: 1.2.12(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-alert-dialog':
specifier: 1.1.15
version: 1.1.15(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -270,6 +276,9 @@ importers:
'@chromatic-com/storybook':
specifier: 4.1.2
version: 4.1.2(storybook@9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2))
'@opentelemetry/instrumentation':
specifier: 0.209.0
version: 0.209.0(@opentelemetry/api@1.9.0)
'@playwright/test':
specifier: 1.56.1
version: 1.56.1
@@ -339,6 +348,9 @@ importers:
eslint-plugin-storybook:
specifier: 9.1.5
version: 9.1.5(eslint@8.57.1)(storybook@9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2))(typescript@5.9.3)
import-in-the-middle:
specifier: 2.0.2
version: 2.0.2
msw:
specifier: 2.11.6
version: 2.11.6(@types/node@24.10.0)(typescript@5.9.3)
@@ -361,8 +373,8 @@ importers:
specifier: 0.7.1
version: 0.7.1(prettier@3.6.2)
require-in-the-middle:
specifier: 7.5.2
version: 7.5.2
specifier: 8.0.1
version: 8.0.1
storybook:
specifier: 9.1.5
version: 9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2)
@@ -1543,8 +1555,8 @@ packages:
'@open-draft/until@2.1.0':
resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==}
'@opentelemetry/api-logs@0.208.0':
resolution: {integrity: sha512-CjruKY9V6NMssL/T1kAFgzosF1v9o6oeN+aX5JB/C/xPNtmgIJqcXHG7fA82Ou1zCpWGl4lROQUKwUNE1pMCyg==}
'@opentelemetry/api-logs@0.209.0':
resolution: {integrity: sha512-xomnUNi7TiAGtOgs0tb54LyrjRZLu9shJGGwkcN7NgtiPYOpNnKLkRJtzZvTjD/w6knSZH9sFZcUSUovYOPg6A==}
engines: {node: '>=8.0.0'}
'@opentelemetry/api@1.9.0':
@@ -1695,8 +1707,8 @@ packages:
peerDependencies:
'@opentelemetry/api': ^1.7.0
'@opentelemetry/instrumentation@0.208.0':
resolution: {integrity: sha512-Eju0L4qWcQS+oXxi6pgh7zvE2byogAkcsVv0OjHF/97iOz1N/aKE6etSGowYkie+YA1uo6DNwdSxaaNnLvcRlA==}
'@opentelemetry/instrumentation@0.209.0':
resolution: {integrity: sha512-Cwe863ojTCnFlxVuuhG7s6ODkAOzKsAEthKAcI4MDRYz1OmGWYnmSl4X2pbyS+hBxVTdvfZePfoEA01IjqcEyw==}
engines: {node: ^18.19.0 || >=20.6.0}
peerDependencies:
'@opentelemetry/api': ^1.3.0
@@ -1810,6 +1822,19 @@ packages:
'@radix-ui/primitive@1.1.3':
resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==}
'@radix-ui/react-accordion@1.2.12':
resolution: {integrity: sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-alert-dialog@1.1.15':
resolution: {integrity: sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==}
peerDependencies:
@@ -2631,7 +2656,7 @@ packages:
'@opentelemetry/api': ^1.9.0
'@opentelemetry/context-async-hooks': ^1.30.1 || ^2.1.0 || ^2.2.0
'@opentelemetry/core': ^1.30.1 || ^2.1.0 || ^2.2.0
'@opentelemetry/instrumentation': '>=0.57.1 <1'
'@opentelemetry/instrumentation': 0.209.0
'@opentelemetry/resources': ^1.30.1 || ^2.1.0 || ^2.2.0
'@opentelemetry/sdk-trace-base': ^1.30.1 || ^2.1.0 || ^2.2.0
'@opentelemetry/semantic-conventions': ^1.37.0
@@ -4957,8 +4982,8 @@ packages:
resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
engines: {node: '>=6'}
import-in-the-middle@2.0.1:
resolution: {integrity: sha512-bruMpJ7xz+9jwGzrwEhWgvRrlKRYCRDBrfU+ur3FcasYXLJDxTruJ//8g2Noj+QFyRBeqbpj8Bhn4Fbw6HjvhA==}
import-in-the-middle@2.0.2:
resolution: {integrity: sha512-qet/hkGt3EbNGVtbDfPu0BM+tCqBS8wT1SYrstPaDKoWtshsC6licOemz7DVtpBEyvDNzo8UTBf9/GwWuSDZ9w==}
imurmurhash@0.1.4:
resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
@@ -6502,10 +6527,6 @@ packages:
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
engines: {node: '>=0.10.0'}
require-in-the-middle@7.5.2:
resolution: {integrity: sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ==}
engines: {node: '>=8.6.0'}
require-in-the-middle@8.0.1:
resolution: {integrity: sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==}
engines: {node: '>=9.3.0 || >=8.10.0 <9.0.0'}
@@ -8716,7 +8737,7 @@ snapshots:
'@open-draft/until@2.1.0': {}
'@opentelemetry/api-logs@0.208.0':
'@opentelemetry/api-logs@0.209.0':
dependencies:
'@opentelemetry/api': 1.9.0
@@ -8735,7 +8756,7 @@ snapshots:
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0)
'@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0)
'@opentelemetry/instrumentation': 0.209.0(@opentelemetry/api@1.9.0)
transitivePeerDependencies:
- supports-color
@@ -8743,7 +8764,7 @@ snapshots:
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0)
'@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0)
'@opentelemetry/instrumentation': 0.209.0(@opentelemetry/api@1.9.0)
'@opentelemetry/semantic-conventions': 1.38.0
'@types/connect': 3.4.38
transitivePeerDependencies:
@@ -8752,7 +8773,7 @@ snapshots:
'@opentelemetry/instrumentation-dataloader@0.26.0(@opentelemetry/api@1.9.0)':
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0)
'@opentelemetry/instrumentation': 0.209.0(@opentelemetry/api@1.9.0)
transitivePeerDependencies:
- supports-color
@@ -8760,7 +8781,7 @@ snapshots:
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0)
'@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0)
'@opentelemetry/instrumentation': 0.209.0(@opentelemetry/api@1.9.0)
'@opentelemetry/semantic-conventions': 1.38.0
transitivePeerDependencies:
- supports-color
@@ -8769,21 +8790,21 @@ snapshots:
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0)
'@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0)
'@opentelemetry/instrumentation': 0.209.0(@opentelemetry/api@1.9.0)
transitivePeerDependencies:
- supports-color
'@opentelemetry/instrumentation-generic-pool@0.52.0(@opentelemetry/api@1.9.0)':
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0)
'@opentelemetry/instrumentation': 0.209.0(@opentelemetry/api@1.9.0)
transitivePeerDependencies:
- supports-color
'@opentelemetry/instrumentation-graphql@0.56.0(@opentelemetry/api@1.9.0)':
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0)
'@opentelemetry/instrumentation': 0.209.0(@opentelemetry/api@1.9.0)
transitivePeerDependencies:
- supports-color
@@ -8791,7 +8812,7 @@ snapshots:
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0)
'@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0)
'@opentelemetry/instrumentation': 0.209.0(@opentelemetry/api@1.9.0)
'@opentelemetry/semantic-conventions': 1.38.0
transitivePeerDependencies:
- supports-color
@@ -8800,7 +8821,7 @@ snapshots:
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0)
'@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0)
'@opentelemetry/instrumentation': 0.209.0(@opentelemetry/api@1.9.0)
'@opentelemetry/semantic-conventions': 1.38.0
forwarded-parse: 2.1.2
transitivePeerDependencies:
@@ -8809,7 +8830,7 @@ snapshots:
'@opentelemetry/instrumentation-ioredis@0.56.0(@opentelemetry/api@1.9.0)':
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0)
'@opentelemetry/instrumentation': 0.209.0(@opentelemetry/api@1.9.0)
'@opentelemetry/redis-common': 0.38.2
transitivePeerDependencies:
- supports-color
@@ -8817,7 +8838,7 @@ snapshots:
'@opentelemetry/instrumentation-kafkajs@0.18.0(@opentelemetry/api@1.9.0)':
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0)
'@opentelemetry/instrumentation': 0.209.0(@opentelemetry/api@1.9.0)
'@opentelemetry/semantic-conventions': 1.38.0
transitivePeerDependencies:
- supports-color
@@ -8825,7 +8846,7 @@ snapshots:
'@opentelemetry/instrumentation-knex@0.53.0(@opentelemetry/api@1.9.0)':
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0)
'@opentelemetry/instrumentation': 0.209.0(@opentelemetry/api@1.9.0)
'@opentelemetry/semantic-conventions': 1.38.0
transitivePeerDependencies:
- supports-color
@@ -8834,7 +8855,7 @@ snapshots:
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0)
'@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0)
'@opentelemetry/instrumentation': 0.209.0(@opentelemetry/api@1.9.0)
'@opentelemetry/semantic-conventions': 1.38.0
transitivePeerDependencies:
- supports-color
@@ -8842,14 +8863,14 @@ snapshots:
'@opentelemetry/instrumentation-lru-memoizer@0.53.0(@opentelemetry/api@1.9.0)':
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0)
'@opentelemetry/instrumentation': 0.209.0(@opentelemetry/api@1.9.0)
transitivePeerDependencies:
- supports-color
'@opentelemetry/instrumentation-mongodb@0.61.0(@opentelemetry/api@1.9.0)':
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0)
'@opentelemetry/instrumentation': 0.209.0(@opentelemetry/api@1.9.0)
transitivePeerDependencies:
- supports-color
@@ -8857,14 +8878,14 @@ snapshots:
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0)
'@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0)
'@opentelemetry/instrumentation': 0.209.0(@opentelemetry/api@1.9.0)
transitivePeerDependencies:
- supports-color
'@opentelemetry/instrumentation-mysql2@0.55.0(@opentelemetry/api@1.9.0)':
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0)
'@opentelemetry/instrumentation': 0.209.0(@opentelemetry/api@1.9.0)
'@opentelemetry/semantic-conventions': 1.38.0
'@opentelemetry/sql-common': 0.41.2(@opentelemetry/api@1.9.0)
transitivePeerDependencies:
@@ -8873,7 +8894,7 @@ snapshots:
'@opentelemetry/instrumentation-mysql@0.54.0(@opentelemetry/api@1.9.0)':
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0)
'@opentelemetry/instrumentation': 0.209.0(@opentelemetry/api@1.9.0)
'@types/mysql': 2.15.27
transitivePeerDependencies:
- supports-color
@@ -8882,7 +8903,7 @@ snapshots:
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0)
'@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0)
'@opentelemetry/instrumentation': 0.209.0(@opentelemetry/api@1.9.0)
'@opentelemetry/semantic-conventions': 1.38.0
'@opentelemetry/sql-common': 0.41.2(@opentelemetry/api@1.9.0)
'@types/pg': 8.15.6
@@ -8893,7 +8914,7 @@ snapshots:
'@opentelemetry/instrumentation-redis@0.57.0(@opentelemetry/api@1.9.0)':
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0)
'@opentelemetry/instrumentation': 0.209.0(@opentelemetry/api@1.9.0)
'@opentelemetry/redis-common': 0.38.2
'@opentelemetry/semantic-conventions': 1.38.0
transitivePeerDependencies:
@@ -8902,7 +8923,7 @@ snapshots:
'@opentelemetry/instrumentation-tedious@0.27.0(@opentelemetry/api@1.9.0)':
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0)
'@opentelemetry/instrumentation': 0.209.0(@opentelemetry/api@1.9.0)
'@types/tedious': 4.0.14
transitivePeerDependencies:
- supports-color
@@ -8911,16 +8932,16 @@ snapshots:
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0)
'@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0)
'@opentelemetry/instrumentation': 0.209.0(@opentelemetry/api@1.9.0)
'@opentelemetry/semantic-conventions': 1.38.0
transitivePeerDependencies:
- supports-color
'@opentelemetry/instrumentation@0.208.0(@opentelemetry/api@1.9.0)':
'@opentelemetry/instrumentation@0.209.0(@opentelemetry/api@1.9.0)':
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/api-logs': 0.208.0
import-in-the-middle: 2.0.1
'@opentelemetry/api-logs': 0.209.0
import-in-the-middle: 2.0.2
require-in-the-middle: 8.0.1
transitivePeerDependencies:
- supports-color
@@ -9100,7 +9121,7 @@ snapshots:
'@prisma/instrumentation@6.19.0(@opentelemetry/api@1.9.0)':
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0)
'@opentelemetry/instrumentation': 0.209.0(@opentelemetry/api@1.9.0)
transitivePeerDependencies:
- supports-color
@@ -9108,6 +9129,23 @@ snapshots:
'@radix-ui/primitive@1.1.3': {}
'@radix-ui/react-accordion@1.2.12(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/primitive': 1.1.3
'@radix-ui/react-collapsible': 1.1.12(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.17)(react@18.3.1)
'@radix-ui/react-context': 1.1.2(@types/react@18.3.17)(react@18.3.1)
'@radix-ui/react-direction': 1.1.1(@types/react@18.3.17)(react@18.3.1)
'@radix-ui/react-id': 1.1.1(@types/react@18.3.17)(react@18.3.1)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.17)(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
optionalDependencies:
'@types/react': 18.3.17
'@types/react-dom': 18.3.5(@types/react@18.3.17)
'@radix-ui/react-alert-dialog@1.1.15(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/primitive': 1.1.3
@@ -9932,19 +9970,19 @@ snapshots:
- supports-color
- webpack
'@sentry/node-core@10.27.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.208.0(@opentelemetry/api@1.9.0))(@opentelemetry/resources@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.38.0)':
'@sentry/node-core@10.27.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.209.0(@opentelemetry/api@1.9.0))(@opentelemetry/resources@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.38.0)':
dependencies:
'@apm-js-collab/tracing-hooks': 0.3.1
'@opentelemetry/api': 1.9.0
'@opentelemetry/context-async-hooks': 2.2.0(@opentelemetry/api@1.9.0)
'@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0)
'@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0)
'@opentelemetry/instrumentation': 0.209.0(@opentelemetry/api@1.9.0)
'@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0)
'@opentelemetry/sdk-trace-base': 2.2.0(@opentelemetry/api@1.9.0)
'@opentelemetry/semantic-conventions': 1.38.0
'@sentry/core': 10.27.0
'@sentry/opentelemetry': 10.27.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.38.0)
import-in-the-middle: 2.0.1
import-in-the-middle: 2.0.2
transitivePeerDependencies:
- supports-color
@@ -9953,7 +9991,7 @@ snapshots:
'@opentelemetry/api': 1.9.0
'@opentelemetry/context-async-hooks': 2.2.0(@opentelemetry/api@1.9.0)
'@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0)
'@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0)
'@opentelemetry/instrumentation': 0.209.0(@opentelemetry/api@1.9.0)
'@opentelemetry/instrumentation-amqplib': 0.55.0(@opentelemetry/api@1.9.0)
'@opentelemetry/instrumentation-connect': 0.52.0(@opentelemetry/api@1.9.0)
'@opentelemetry/instrumentation-dataloader': 0.26.0(@opentelemetry/api@1.9.0)
@@ -9981,9 +10019,9 @@ snapshots:
'@opentelemetry/semantic-conventions': 1.38.0
'@prisma/instrumentation': 6.19.0(@opentelemetry/api@1.9.0)
'@sentry/core': 10.27.0
'@sentry/node-core': 10.27.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.208.0(@opentelemetry/api@1.9.0))(@opentelemetry/resources@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.38.0)
'@sentry/node-core': 10.27.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.209.0(@opentelemetry/api@1.9.0))(@opentelemetry/resources@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.38.0)
'@sentry/opentelemetry': 10.27.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.38.0)
import-in-the-middle: 2.0.1
import-in-the-middle: 2.0.2
minimatch: 9.0.5
transitivePeerDependencies:
- supports-color
@@ -12792,7 +12830,7 @@ snapshots:
parent-module: 1.0.1
resolve-from: 4.0.0
import-in-the-middle@2.0.1:
import-in-the-middle@2.0.2:
dependencies:
acorn: 8.15.0
acorn-import-attributes: 1.9.5(acorn@8.15.0)
@@ -14631,14 +14669,6 @@ snapshots:
require-from-string@2.0.2: {}
require-in-the-middle@7.5.2:
dependencies:
debug: 4.4.3
module-details-from-path: 1.0.4
resolve: 1.22.11
transitivePeerDependencies:
- supports-color
require-in-the-middle@8.0.1:
dependencies:
debug: 4.4.3

View File

@@ -1,4 +1,4 @@
import { CredentialsInput } from "@/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/CredentialsInputs/CredentialsInputs";
import { CredentialsInput } from "@/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/CredentialsInputs/CredentialsInput";
import { CredentialsMetaInput } from "@/app/api/__generated__/models/credentialsMetaInput";
import { GraphMeta } from "@/app/api/__generated__/models/graphMeta";
import { useState } from "react";

View File

@@ -1,22 +1,22 @@
"use client";
import Image from "next/image";
import Link from "next/link";
import { useSearchParams } from "next/navigation";
import { useState, useMemo, useRef } from "react";
import { AuthCard } from "@/components/auth/AuthCard";
import { Text } from "@/components/atoms/Text/Text";
import { CredentialsInput } from "@/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/CredentialsInputs/CredentialsInput";
import { useGetOauthGetOauthAppInfo } from "@/app/api/__generated__/endpoints/oauth/oauth";
import { okData } from "@/app/api/helpers";
import { Button } from "@/components/atoms/Button/Button";
import { Text } from "@/components/atoms/Text/Text";
import { AuthCard } from "@/components/auth/AuthCard";
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
import { CredentialsInput } from "@/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/CredentialsInputs/CredentialsInputs";
import type {
BlockIOCredentialsSubSchema,
CredentialsMetaInput,
CredentialsType,
} from "@/lib/autogpt-server-api";
import { CheckIcon, CircleIcon } from "@phosphor-icons/react";
import { useGetOauthGetOauthAppInfo } from "@/app/api/__generated__/endpoints/oauth/oauth";
import { okData } from "@/app/api/helpers";
import Image from "next/image";
import Link from "next/link";
import { useSearchParams } from "next/navigation";
import { useMemo, useRef, useState } from "react";
// All credential types - we accept any type of credential
const ALL_CREDENTIAL_TYPES: CredentialsType[] = [

View File

@@ -3,7 +3,7 @@ import {
CustomNodeData,
} from "@/app/(platform)/build/components/legacy-builder/CustomNode/CustomNode";
import { NodeTableInput } from "@/app/(platform)/build/components/legacy-builder/NodeTableInput";
import { CredentialsInput } from "@/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/CredentialsInputs/CredentialsInputs";
import { CredentialsInput } from "@/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/CredentialsInputs/CredentialsInput";
import { Button } from "@/components/__legacy__/ui/button";
import { Calendar } from "@/components/__legacy__/ui/calendar";
import { LocalValuedInput } from "@/components/__legacy__/ui/input";

View File

@@ -1,4 +1,4 @@
import { CredentialsInput } from "@/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/CredentialsInputs/CredentialsInputs";
import { CredentialsInput } from "@/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/CredentialsInputs/CredentialsInput";
import { Card } from "@/components/atoms/Card/Card";
import { Text } from "@/components/atoms/Text/Text";
import type { BlockIOCredentialsSubSchema } from "@/lib/autogpt-server-api";

View File

@@ -1,32 +1,31 @@
"use client";
import { Button } from "@/components/atoms/Button/Button";
import { PublishAgentModal } from "@/components/contextual/PublishAgentModal/PublishAgentModal";
import { Breadcrumbs } from "@/components/molecules/Breadcrumbs/Breadcrumbs";
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
import { cn } from "@/lib/utils";
import { PlusIcon } from "@phosphor-icons/react";
import { useEffect, useState } from "react";
import { RunAgentModal } from "./components/modals/RunAgentModal/RunAgentModal";
import { useMarketplaceUpdate } from "./hooks/useMarketplaceUpdate";
import { AgentVersionChangelog } from "./components/AgentVersionChangelog";
import { MarketplaceBanners } from "@/components/contextual/MarketplaceBanners/MarketplaceBanners";
import { PublishAgentModal } from "@/components/contextual/PublishAgentModal/PublishAgentModal";
import { AgentSettingsButton } from "./components/other/AgentSettingsButton";
import { AgentSettingsModal } from "./components/modals/AgentSettingsModal/AgentSettingsModal";
import { RunAgentModal } from "./components/modals/RunAgentModal/RunAgentModal";
import { AgentRunsLoading } from "./components/other/AgentRunsLoading";
import { EmptySchedules } from "./components/other/EmptySchedules";
import { EmptyTasks } from "./components/other/EmptyTasks";
import { EmptyTemplates } from "./components/other/EmptyTemplates";
import { EmptyTriggers } from "./components/other/EmptyTriggers";
import { MarketplaceBanners } from "./components/other/MarketplaceBanners";
import { SectionWrap } from "./components/other/SectionWrap";
import { LoadingSelectedContent } from "./components/selected-views/LoadingSelectedContent";
import { SelectedRunView } from "./components/selected-views/SelectedRunView/SelectedRunView";
import { SelectedScheduleView } from "./components/selected-views/SelectedScheduleView/SelectedScheduleView";
import { SelectedSettingsView } from "./components/selected-views/SelectedSettingsView/SelectedSettingsView";
import { SelectedTemplateView } from "./components/selected-views/SelectedTemplateView/SelectedTemplateView";
import { SelectedTriggerView } from "./components/selected-views/SelectedTriggerView/SelectedTriggerView";
import { SelectedViewLayout } from "./components/selected-views/SelectedViewLayout";
import { SidebarRunsList } from "./components/sidebar/SidebarRunsList/SidebarRunsList";
import { AGENT_LIBRARY_SECTION_PADDING_X } from "./helpers";
import { useMarketplaceUpdate } from "./hooks/useMarketplaceUpdate";
import { useNewAgentLibraryView } from "./useNewAgentLibraryView";
export function NewAgentLibraryView() {
@@ -45,7 +44,6 @@ export function NewAgentLibraryView() {
handleSelectRun,
handleCountsChange,
handleClearSelectedRun,
handleSelectSettings,
onRunInitiated,
onTriggerSetup,
onScheduleCreated,
@@ -137,13 +135,16 @@ export function NewAgentLibraryView() {
return (
<>
<div className="flex h-full flex-col">
<div className="mx-6 pt-4">
<Breadcrumbs
items={[
{ name: "My Library", link: "/library" },
{ name: agent.name, link: `/library/agents/${agentId}` },
]}
/>
<div className="mx-6 flex flex-col gap-4 pt-4">
<div className="flex items-center justify-between">
<Breadcrumbs
items={[
{ name: "My Library", link: "/library" },
{ name: agent.name, link: `/library/agents/${agentId}` },
]}
/>
<AgentSettingsModal agent={agent} />
</div>
</div>
<div className="flex min-h-0 flex-1">
<EmptyTasks
@@ -170,31 +171,24 @@ export function NewAgentLibraryView() {
AGENT_LIBRARY_SECTION_PADDING_X,
)}
>
<div className="flex items-center gap-2">
<RunAgentModal
triggerSlot={
<Button
variant="primary"
size="large"
className="flex-1"
disabled={isTemplateLoading && activeTab === "templates"}
>
<PlusIcon size={20} /> New task
</Button>
}
agent={agent}
onRunCreated={onRunInitiated}
onScheduleCreated={onScheduleCreated}
onTriggerSetup={onTriggerSetup}
initialInputValues={activeTemplate?.inputs}
initialInputCredentials={activeTemplate?.credentials}
/>
<AgentSettingsButton
agent={agent}
onSelectSettings={handleSelectSettings}
selected={activeItem === "settings"}
/>
</div>
<RunAgentModal
triggerSlot={
<Button
variant="primary"
size="large"
className="w-full"
disabled={isTemplateLoading && activeTab === "templates"}
>
<PlusIcon size={20} /> New task
</Button>
}
agent={agent}
onRunCreated={onRunInitiated}
onScheduleCreated={onScheduleCreated}
onTriggerSetup={onTriggerSetup}
initialInputValues={activeTemplate?.inputs}
initialInputCredentials={activeTemplate?.credentials}
/>
</div>
<SidebarRunsList
@@ -208,12 +202,7 @@ export function NewAgentLibraryView() {
</SectionWrap>
{activeItem ? (
activeItem === "settings" ? (
<SelectedSettingsView
agent={agent}
onClearSelectedRun={handleClearSelectedRun}
/>
) : activeTab === "scheduled" ? (
activeTab === "scheduled" ? (
<SelectedScheduleView
agent={agent}
scheduleId={activeItem}
@@ -246,8 +235,6 @@ export function NewAgentLibraryView() {
onSelectRun={handleSelectRun}
onClearSelectedRun={handleClearSelectedRun}
banner={renderMarketplaceUpdateBanner()}
onSelectSettings={handleSelectSettings}
selectedSettings={activeItem === "settings"}
/>
)
) : sidebarLoading ? (

View File

@@ -3,7 +3,8 @@
import type { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { Text } from "@/components/atoms/Text/Text";
import type { CredentialsMetaInput } from "@/lib/autogpt-server-api/types";
import { CredentialsInput } from "../CredentialsInputs/CredentialsInputs";
import { CredentialsInput } from "../CredentialsInputs/CredentialsInput";
import { isSystemCredential } from "../CredentialsInputs/helpers";
import { RunAgentInputs } from "../RunAgentInputs/RunAgentInputs";
import { getAgentCredentialsFields, getAgentInputFields } from "./helpers";
@@ -71,6 +72,7 @@ export function AgentInputsReadOnly({
{credentialFieldEntries.map(([key, inputSubSchema]) => {
const credential = credentialInputs![key];
if (!credential) return null;
if (isSystemCredential(credential)) return null;
return (
<CredentialsInput

View File

@@ -0,0 +1,81 @@
"use client";
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { Button } from "@/components/atoms/Button/Button";
import { Switch } from "@/components/atoms/Switch/Switch";
import { Text } from "@/components/atoms/Text/Text";
import { Dialog } from "@/components/molecules/Dialog/Dialog";
import { useAgentSafeMode } from "@/hooks/useAgentSafeMode";
import { GearIcon } from "@phosphor-icons/react";
import { useState } from "react";
interface Props {
agent: LibraryAgent;
controlledOpen?: boolean;
onOpenChange?: (open: boolean) => void;
}
export function AgentSettingsModal({
agent,
controlledOpen,
onOpenChange,
}: Props) {
const [internalIsOpen, setInternalIsOpen] = useState(false);
const isOpen = controlledOpen !== undefined ? controlledOpen : internalIsOpen;
function setIsOpen(open: boolean) {
if (onOpenChange) {
onOpenChange(open);
} else {
setInternalIsOpen(open);
}
}
const { currentSafeMode, isPending, hasHITLBlocks, handleToggle } =
useAgentSafeMode(agent);
if (!hasHITLBlocks) return null;
return (
<Dialog
controlled={{ isOpen, set: setIsOpen }}
styling={{ maxWidth: "600px", maxHeight: "90vh" }}
title="Agent Settings"
>
{controlledOpen === undefined && (
<Dialog.Trigger>
<Button
variant="ghost"
size="small"
className="m-0 min-w-0 rounded-full p-0 px-1"
aria-label="Agent Settings"
>
<GearIcon size={18} className="text-zinc-600" />
<Text variant="small">Agent Settings</Text>
</Button>
</Dialog.Trigger>
)}
<Dialog.Content>
<div className="space-y-6">
<div className="flex w-full flex-col items-start gap-4 rounded-xl border border-zinc-100 bg-white p-6">
<div className="flex w-full items-start justify-between gap-4">
<div className="flex-1">
<Text variant="large-semibold">Require human approval</Text>
<Text variant="large" className="mt-1 text-zinc-900">
The agent will pause and wait for your review before
continuing
</Text>
</div>
<Switch
checked={currentSafeMode || false}
onCheckedChange={handleToggle}
disabled={isPending}
className="mt-1"
/>
</div>
</div>
</div>
</Dialog.Content>
</Dialog>
);
}

View File

@@ -1,6 +1,4 @@
import { Button } from "@/components/atoms/Button/Button";
import { Text } from "@/components/atoms/Text/Text";
import { InformationTooltip } from "@/components/molecules/InformationTooltip/InformationTooltip";
import {
BlockIOCredentialsSubSchema,
CredentialsMetaInput,
@@ -8,13 +6,11 @@ import {
import { cn } from "@/lib/utils";
import { toDisplayName } from "@/providers/agent-credentials/helper";
import { APIKeyCredentialsModal } from "./components/APIKeyCredentialsModal/APIKeyCredentialsModal";
import { CredentialRow } from "./components/CredentialRow/CredentialRow";
import { CredentialsSelect } from "./components/CredentialsSelect/CredentialsSelect";
import { DeleteConfirmationModal } from "./components/DeleteConfirmationModal/DeleteConfirmationModal";
import { CredentialsFlatView } from "./components/CredentialsFlatView/CredentialsFlatView";
import { HostScopedCredentialsModal } from "./components/HotScopedCredentialsModal/HotScopedCredentialsModal";
import { OAuthFlowWaitingModal } from "./components/OAuthWaitingModal/OAuthWaitingModal";
import { PasswordCredentialsModal } from "./components/PasswordCredentialsModal/PasswordCredentialsModal";
import { getCredentialDisplayName } from "./helpers";
import { isSystemCredential } from "./helpers";
import {
CredentialsInputState,
useCredentialsInput,
@@ -72,115 +68,53 @@ export function CredentialsInput({
supportsOAuth2,
supportsUserPassword,
supportsHostScoped,
credentialsToShow,
userCredentials,
systemCredentials,
oAuthError,
isAPICredentialsModalOpen,
isUserPasswordCredentialsModalOpen,
isHostScopedCredentialsModalOpen,
isOAuth2FlowInProgress,
oAuthPopupController,
credentialToDelete,
deleteCredentialsMutation,
actionButtonText,
setAPICredentialsModalOpen,
setUserPasswordCredentialsModalOpen,
setHostScopedCredentialsModalOpen,
setCredentialToDelete,
handleActionButtonClick,
handleCredentialSelect,
handleDeleteCredential,
handleDeleteConfirm,
} = hookData;
const displayName = toDisplayName(provider);
const hasCredentialsToShow = credentialsToShow.length > 0;
const selectedCredentialIsSystem =
selectedCredential && isSystemCredential(selectedCredential);
const allCredentials = [...userCredentials, ...systemCredentials];
if (readOnly && selectedCredentialIsSystem) {
return null;
}
return (
<div className={cn("mb-6", className)}>
{showTitle && (
<div className="mb-2 flex items-center gap-2">
<Text variant="large-medium">
{displayName} credentials
{isOptional && (
<span className="ml-1 text-sm font-normal text-gray-500">
(optional)
</span>
)}
</Text>
{schema.description && (
<InformationTooltip description={schema.description} />
)}
</div>
)}
{hasCredentialsToShow ? (
<>
{(credentialsToShow.length > 1 || isOptional) && !readOnly ? (
<CredentialsSelect
credentials={credentialsToShow}
provider={provider}
displayName={displayName}
selectedCredentials={selectedCredential}
onSelectCredential={handleCredentialSelect}
onClearCredential={() => onSelectCredential(undefined)}
readOnly={readOnly}
allowNone={isOptional}
variant={variant}
/>
) : (
<div className="mb-4 space-y-2">
{credentialsToShow.map((credential) => {
return (
<CredentialRow
key={credential.id}
credential={credential}
provider={provider}
displayName={displayName}
onSelect={() => handleCredentialSelect(credential.id)}
onDelete={() =>
handleDeleteCredential({
id: credential.id,
title: getCredentialDisplayName(
credential,
displayName,
),
})
}
readOnly={readOnly}
/>
);
})}
</div>
)}
{!readOnly && (
<Button
variant="secondary"
size="small"
onClick={handleActionButtonClick}
className="w-fit"
type="button"
>
{actionButtonText}
</Button>
)}
</>
) : (
!readOnly && (
<Button
variant="secondary"
size="small"
onClick={handleActionButtonClick}
className="w-fit"
type="button"
>
{actionButtonText}
</Button>
)
)}
<CredentialsFlatView
schema={schema}
provider={provider}
displayName={displayName}
credentials={allCredentials}
selectedCredential={selectedCredential}
onSelectCredential={handleCredentialSelect}
onClearCredential={() => onSelectCredential(undefined)}
onAddCredential={handleActionButtonClick}
actionButtonText={actionButtonText}
isOptional={isOptional}
showTitle={showTitle}
readOnly={readOnly}
variant={variant}
/>
{!readOnly && (
<>
{supportsApiKey ? (
{supportsApiKey && (
<APIKeyCredentialsModal
schema={schema}
open={isAPICredentialsModalOpen}
@@ -191,15 +125,15 @@ export function CredentialsInput({
}}
siblingInputs={siblingInputs}
/>
) : null}
{supportsOAuth2 ? (
)}
{supportsOAuth2 && (
<OAuthFlowWaitingModal
open={isOAuth2FlowInProgress}
onClose={() => oAuthPopupController?.abort("canceled")}
providerName={providerName}
/>
) : null}
{supportsUserPassword ? (
)}
{supportsUserPassword && (
<PasswordCredentialsModal
schema={schema}
open={isUserPasswordCredentialsModalOpen}
@@ -210,8 +144,8 @@ export function CredentialsInput({
}}
siblingInputs={siblingInputs}
/>
) : null}
{supportsHostScoped ? (
)}
{supportsHostScoped && (
<HostScopedCredentialsModal
schema={schema}
open={isHostScopedCredentialsModalOpen}
@@ -222,20 +156,13 @@ export function CredentialsInput({
}}
siblingInputs={siblingInputs}
/>
) : null}
)}
{oAuthError ? (
{oAuthError && (
<Text variant="body" className="mt-2 text-red-500">
Error: {oAuthError}
</Text>
) : null}
<DeleteConfirmationModal
credentialToDelete={credentialToDelete}
isDeleting={deleteCredentialsMutation.isPending}
onClose={() => setCredentialToDelete(null)}
onConfirm={handleDeleteConfirm}
/>
)}
</>
)}
</div>

View File

@@ -1,11 +1,11 @@
import { Input } from "@/components/atoms/Input/Input";
import { Button } from "@/components/atoms/Button/Button";
import { Dialog } from "@/components/molecules/Dialog/Dialog";
import {
Form,
FormDescription,
FormField,
} from "@/components/__legacy__/ui/form";
import { Button } from "@/components/atoms/Button/Button";
import { Input } from "@/components/atoms/Input/Input";
import { Dialog } from "@/components/molecules/Dialog/Dialog";
import {
BlockIOCredentialsSubSchema,
CredentialsMetaInput,
@@ -60,7 +60,23 @@ export function APIKeyCredentialsModal({
)}
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-2">
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-2 px-2"
>
<FormField
control={form.control}
name="title"
render={({ field }) => (
<Input
id="title"
label="Name"
type="text"
placeholder="Enter a name for this API Key..."
{...field}
/>
)}
/>
<FormField
control={form.control}
name="apiKey"
@@ -70,8 +86,7 @@ export function APIKeyCredentialsModal({
id="apiKey"
label="API Key"
type="password"
placeholder="Enter API key..."
size="small"
placeholder="Enter API Key..."
hint={
schema.credentials_scopes ? (
<FormDescription>
@@ -90,20 +105,7 @@ export function APIKeyCredentialsModal({
</>
)}
/>
<FormField
control={form.control}
name="title"
render={({ field }) => (
<Input
id="title"
label="Name"
type="text"
placeholder="Enter a name for this API key..."
size="small"
{...field}
/>
)}
/>
<FormField
control={form.control}
name="expiresAt"
@@ -113,13 +115,31 @@ export function APIKeyCredentialsModal({
label="Expiration Date"
type="datetime-local"
placeholder="Select expiration date..."
size="small"
{...field}
value={field.value}
onChange={(e) => {
const value = e.target.value;
if (value) {
const dateTime = new Date(value);
dateTime.setHours(0, 0, 0, 0);
const year = dateTime.getFullYear();
const month = String(dateTime.getMonth() + 1).padStart(
2,
"0",
);
const day = String(dateTime.getDate()).padStart(2, "0");
const normalizedValue = `${year}-${month}-${day}T00:00`;
field.onChange(normalizedValue);
} else {
field.onChange(value);
}
}}
onBlur={field.onBlur}
name={field.name}
/>
)}
/>
<Button type="submit" size="small" className="min-w-68">
Save & use this API key
<Button type="submit" className="min-w-68">
Add API Key
</Button>
</form>
</Form>

View File

@@ -1,11 +1,11 @@
import { z } from "zod";
import { useForm, type UseFormReturn } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import useCredentials from "@/hooks/useCredentials";
import {
BlockIOCredentialsSubSchema,
CredentialsMetaInput,
} from "@/lib/autogpt-server-api/types";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm, type UseFormReturn } from "react-hook-form";
import { z } from "zod";
export type APIKeyFormValues = {
apiKey: string;
@@ -40,12 +40,24 @@ export function useAPIKeyCredentialsModal({
expiresAt: z.string().optional(),
});
function getDefaultExpirationDate(): string {
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
tomorrow.setHours(0, 0, 0, 0);
const year = tomorrow.getFullYear();
const month = String(tomorrow.getMonth() + 1).padStart(2, "0");
const day = String(tomorrow.getDate()).padStart(2, "0");
const hours = String(tomorrow.getHours()).padStart(2, "0");
const minutes = String(tomorrow.getMinutes()).padStart(2, "0");
return `${year}-${month}-${day}T${hours}:${minutes}`;
}
const form = useForm<APIKeyFormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
apiKey: "",
title: "",
expiresAt: "",
expiresAt: getDefaultExpirationDate(),
},
});

View File

@@ -7,7 +7,8 @@ import {
DropdownMenuTrigger,
} from "@/components/molecules/DropdownMenu/DropdownMenu";
import { cn } from "@/lib/utils";
import { CaretDown, DotsThreeVertical } from "@phosphor-icons/react";
import { CaretDownIcon, DotsThreeVertical } from "@phosphor-icons/react";
import { useEffect, useRef, useState } from "react";
import {
fallbackIcon,
getCredentialDisplayName,
@@ -26,7 +27,7 @@ type CredentialRowProps = {
provider: string;
displayName: string;
onSelect: () => void;
onDelete: () => void;
onDelete?: () => void;
readOnly?: boolean;
showCaret?: boolean;
asSelectTrigger?: boolean;
@@ -47,11 +48,32 @@ export function CredentialRow({
}: CredentialRowProps) {
const ProviderIcon = providerIcons[provider] || fallbackIcon;
const isNodeVariant = variant === "node";
const containerRef = useRef<HTMLDivElement>(null);
const [showMaskedKey, setShowMaskedKey] = useState(true);
useEffect(() => {
const container = containerRef.current;
if (!container) return;
const resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
const width = entry.contentRect.width;
setShowMaskedKey(width >= 360);
}
});
resizeObserver.observe(container);
return () => {
resizeObserver.disconnect();
};
}, []);
return (
<div
ref={containerRef}
className={cn(
"flex items-center gap-3 rounded-medium border border-zinc-200 bg-white p-3 transition-colors",
"flex min-w-[20rem] items-center gap-3 rounded-medium border border-zinc-200 bg-white p-3 transition-colors",
asSelectTrigger && isNodeVariant
? "min-w-0 flex-1 overflow-hidden border-0 bg-transparent"
: asSelectTrigger
@@ -73,34 +95,35 @@ export function CredentialRow({
<IconKey className="h-5 w-5 shrink-0 text-zinc-800" />
<div
className={cn(
"flex min-w-0 flex-1 flex-nowrap items-center gap-4",
"relative flex min-w-0 flex-1 flex-nowrap items-center gap-4",
isNodeVariant && "overflow-hidden",
)}
>
<Text
variant="body"
className={cn(
"tracking-tight",
isNodeVariant
? "truncate"
: "line-clamp-1 flex-[0_0_50%] text-ellipsis",
"min-w-0 flex-1 tracking-tight",
isNodeVariant ? "truncate" : "line-clamp-1 text-ellipsis",
)}
>
{getCredentialDisplayName(credential, displayName)}
</Text>
{!(asSelectTrigger && isNodeVariant) && (
{!(asSelectTrigger && isNodeVariant) && showMaskedKey && (
<Text
variant="large"
className="relative top-1 hidden overflow-hidden whitespace-nowrap font-mono tracking-tight md:block"
className={cn(
"absolute top-[65%] -translate-y-1/2 overflow-hidden whitespace-nowrap font-mono tracking-tight",
asSelectTrigger ? "right-0" : "right-6",
)}
>
{"*".repeat(MASKED_KEY_LENGTH)}
</Text>
)}
</div>
{showCaret && !asSelectTrigger && (
<CaretDown className="h-4 w-4 shrink-0 text-gray-400" />
{(showCaret || (asSelectTrigger && !readOnly)) && (
<CaretDownIcon className="h-4 w-4 shrink-0 text-gray-400" />
)}
{!readOnly && !showCaret && !asSelectTrigger && (
{!readOnly && !showCaret && !asSelectTrigger && onDelete && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button

View File

@@ -0,0 +1,203 @@
import { Button } from "@/components/atoms/Button/Button";
import { Text } from "@/components/atoms/Text/Text";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/molecules/Accordion/Accordion";
import { InformationTooltip } from "@/components/molecules/InformationTooltip/InformationTooltip";
import {
BlockIOCredentialsSubSchema,
CredentialsMetaInput,
} from "@/lib/autogpt-server-api/types";
import { SlidersHorizontalIcon } from "lucide-react";
import { CredentialRow } from "../CredentialRow/CredentialRow";
import { CredentialsSelect } from "../CredentialsSelect/CredentialsSelect";
type Credential = {
id: string;
title?: string;
username?: string;
type: string;
provider: string;
};
type Props = {
schema: BlockIOCredentialsSubSchema;
provider: string;
displayName: string;
userCredentials: Credential[];
systemCredentials: Credential[];
isSystemProvider: boolean;
selectedCredential?: CredentialsMetaInput;
onSelectCredential: (credentialId: string) => void;
onClearCredential: () => void;
onAddCredential: () => void;
actionButtonText: string;
isOptional: boolean;
showTitle: boolean;
variant: "default" | "node";
};
export function CredentialsAccordionView({
schema,
provider,
displayName,
userCredentials,
systemCredentials,
isSystemProvider,
selectedCredential,
onSelectCredential,
onClearCredential,
onAddCredential,
actionButtonText,
isOptional,
showTitle,
variant,
}: Props) {
const allCredentials = [...userCredentials, ...systemCredentials];
const hasSystemCredentials = systemCredentials.length > 0;
const hasUserCredentials = userCredentials.length > 0;
const credentialsInAccordion = isSystemProvider
? allCredentials
: systemCredentials;
const shouldOpenAccordionByDefault =
hasSystemCredentials && !isOptional && !selectedCredential;
const showUserCredentialsOutsideAccordion =
!isSystemProvider && hasUserCredentials;
return (
<>
{showTitle && showUserCredentialsOutsideAccordion && (
<div className="mb-2 flex items-center gap-2">
<Text variant="large-medium">
{displayName} credentials
{isOptional && (
<span className="ml-1 text-sm font-normal text-gray-500">
(optional)
</span>
)}
</Text>
{schema.description && (
<InformationTooltip description={schema.description} />
)}
</div>
)}
{showUserCredentialsOutsideAccordion && (
<>
{userCredentials.length > 1 || isOptional ? (
<CredentialsSelect
credentials={userCredentials}
provider={provider}
displayName={displayName}
selectedCredentials={selectedCredential}
onSelectCredential={onSelectCredential}
onClearCredential={onClearCredential}
allowNone={isOptional}
variant={variant}
/>
) : (
<div className="mb-4 space-y-2">
{userCredentials.map((credential) => (
<CredentialRow
key={credential.id}
credential={credential}
provider={provider}
displayName={displayName}
onSelect={() => onSelectCredential(credential.id)}
/>
))}
</div>
)}
<Button
variant="secondary"
size="small"
onClick={onAddCredential}
className="w-fit"
type="button"
>
{actionButtonText}
</Button>
</>
)}
{hasSystemCredentials && (
<Accordion
type="single"
collapsible
defaultValue={
shouldOpenAccordionByDefault ? "system-credentials" : undefined
}
className={showUserCredentialsOutsideAccordion ? "mt-4" : ""}
>
<AccordionItem value="system-credentials" className="border-none">
<AccordionTrigger className="py-2 text-sm text-muted-foreground hover:no-underline">
<div className="flex items-center gap-1">
<SlidersHorizontalIcon className="size-4" /> System credentials
</div>
</AccordionTrigger>
<AccordionContent>
<div className="space-y-4 px-1 pt-2">
{showTitle && (
<div className="flex items-center gap-2">
<Text variant="large-medium">
{displayName} credentials
{isOptional && (
<span className="ml-1 text-sm font-normal text-gray-500">
(optional)
</span>
)}
</Text>
{schema.description && (
<InformationTooltip description={schema.description} />
)}
</div>
)}
{credentialsInAccordion.length > 0 && (
<CredentialsSelect
credentials={credentialsInAccordion}
provider={provider}
displayName={displayName}
selectedCredentials={selectedCredential}
onSelectCredential={onSelectCredential}
onClearCredential={onClearCredential}
allowNone={isOptional}
variant={variant}
/>
)}
{isSystemProvider && (
<Button
variant="secondary"
size="small"
onClick={onAddCredential}
className="w-fit"
type="button"
>
{actionButtonText}
</Button>
)}
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
)}
{!showUserCredentialsOutsideAccordion && !isSystemProvider && (
<Button
variant="secondary"
size="small"
onClick={onAddCredential}
className="w-fit"
type="button"
>
{actionButtonText}
</Button>
)}
</>
);
}

View File

@@ -0,0 +1,134 @@
import { Button } from "@/components/atoms/Button/Button";
import { Text } from "@/components/atoms/Text/Text";
import { InformationTooltip } from "@/components/molecules/InformationTooltip/InformationTooltip";
import {
BlockIOCredentialsSubSchema,
CredentialsMetaInput,
} from "@/lib/autogpt-server-api/types";
import { ExclamationTriangleIcon } from "@radix-ui/react-icons";
import { CredentialRow } from "../CredentialRow/CredentialRow";
import { CredentialsSelect } from "../CredentialsSelect/CredentialsSelect";
type Credential = {
id: string;
title?: string;
username?: string;
type: string;
provider: string;
};
type Props = {
schema: BlockIOCredentialsSubSchema;
provider: string;
displayName: string;
credentials: Credential[];
selectedCredential?: CredentialsMetaInput;
actionButtonText: string;
isOptional: boolean;
showTitle: boolean;
readOnly: boolean;
variant: "default" | "node";
onSelectCredential: (credentialId: string) => void;
onClearCredential: () => void;
onAddCredential: () => void;
};
export function CredentialsFlatView({
schema,
provider,
displayName,
credentials,
selectedCredential,
actionButtonText,
isOptional,
showTitle,
readOnly,
variant,
onSelectCredential,
onClearCredential,
onAddCredential,
}: Props) {
const hasCredentials = credentials.length > 0;
return (
<>
{showTitle && (
<div className="mb-2 flex items-center gap-2">
<Text variant="large-medium" className="flex items-center gap-2">
<span className="inline-flex items-center gap-1">
{displayName} credentials
{isOptional && (
<span className="text-sm font-normal text-gray-500">
(optional)
</span>
)}
{!isOptional && !selectedCredential && (
<span className="inline-flex items-center gap-1 text-red-600">
<ExclamationTriangleIcon className="size-4" />
<span className="text-sm font-normal">required</span>
</span>
)}
</span>
</Text>
{schema.description && (
<InformationTooltip description={schema.description} />
)}
</div>
)}
{hasCredentials ? (
<>
{(credentials.length > 1 || isOptional) && !readOnly ? (
<CredentialsSelect
credentials={credentials}
provider={provider}
displayName={displayName}
selectedCredentials={selectedCredential}
onSelectCredential={onSelectCredential}
onClearCredential={onClearCredential}
readOnly={readOnly}
allowNone={isOptional}
variant={variant}
/>
) : (
<div className="mb-4 space-y-2">
{credentials.map((credential) => (
<CredentialRow
key={credential.id}
credential={credential}
provider={provider}
displayName={displayName}
onSelect={() => onSelectCredential(credential.id)}
readOnly={readOnly}
/>
))}
</div>
)}
{!readOnly && (
<Button
variant="secondary"
size="small"
onClick={onAddCredential}
className="w-fit"
type="button"
>
{actionButtonText}
</Button>
)}
</>
) : (
!readOnly && (
<Button
variant="secondary"
size="small"
onClick={onAddCredential}
className="w-fit"
type="button"
>
{actionButtonText}
</Button>
)
)}
</>
);
}

View File

@@ -1,14 +1,4 @@
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/__legacy__/ui/select";
import { Text } from "@/components/atoms/Text/Text";
import { CredentialsMetaInput } from "@/lib/autogpt-server-api/types";
import { cn } from "@/lib/utils";
import { useEffect } from "react";
import { CredentialsMetaInput } from "@/app/api/__generated__/models/credentialsMetaInput";
import { getCredentialDisplayName } from "../../helpers";
import { CredentialRow } from "../CredentialRow/CredentialRow";
@@ -42,76 +32,77 @@ export function CredentialsSelect({
allowNone = true,
variant = "default",
}: Props) {
// Auto-select first credential if none is selected (only if allowNone is false)
useEffect(() => {
if (!allowNone && !selectedCredentials && credentials.length > 0) {
onSelectCredential(credentials[0].id);
}
}, [allowNone, selectedCredentials, credentials, onSelectCredential]);
const handleValueChange = (value: string) => {
function handleValueChange(e: React.ChangeEvent<HTMLSelectElement>) {
const value = e.target.value;
if (value === "__none__") {
onClearCredential?.();
} else {
onSelectCredential(value);
}
};
}
const selectedCredential = selectedCredentials
? credentials.find((c) => c.id === selectedCredentials.id)
: null;
const displayCredential = selectedCredential
? {
id: selectedCredential.id,
title: selectedCredential.title,
username: selectedCredential.username,
type: selectedCredential.type,
provider: selectedCredential.provider,
}
: allowNone
? {
id: "__none__",
title: "None (skip this credential)",
type: "none",
provider: provider,
}
: {
id: "__placeholder__",
title: "Select credential",
type: "placeholder",
provider: provider,
};
return (
<div className="mb-4 w-full">
<Select
value={selectedCredentials?.id || (allowNone ? "__none__" : "")}
onValueChange={handleValueChange}
>
<SelectTrigger
className={cn(
"h-auto min-h-12 w-full rounded-medium border-zinc-200 p-0 pr-4 shadow-none",
variant === "node" && "overflow-hidden",
)}
<div className="relative">
<select
value={selectedCredentials?.id ?? "__none__"}
onChange={handleValueChange}
disabled={readOnly}
className="absolute inset-0 z-10 cursor-pointer opacity-0"
aria-label={`Select ${displayName} credential`}
>
{selectedCredentials ? (
<SelectValue key={selectedCredentials.id} asChild>
<CredentialRow
credential={{
id: selectedCredentials.id,
title: selectedCredentials.title || undefined,
type: selectedCredentials.type,
provider: selectedCredentials.provider,
}}
provider={provider}
displayName={displayName}
onSelect={() => {}}
onDelete={() => {}}
readOnly={readOnly}
asSelectTrigger={true}
variant={variant}
/>
</SelectValue>
{allowNone ? (
<option value="__none__">None (skip this credential)</option>
) : (
<SelectValue key="placeholder" placeholder="Select credential" />
)}
</SelectTrigger>
<SelectContent>
{allowNone && (
<SelectItem key="__none__" value="__none__">
<div className="flex items-center gap-2">
<Text variant="body" className="tracking-tight text-gray-500">
None (skip this credential)
</Text>
</div>
</SelectItem>
<option value="__none__" disabled hidden>
Select a credential
</option>
)}
{credentials.map((credential) => (
<SelectItem key={credential.id} value={credential.id}>
<div className="flex items-center gap-2">
<Text variant="body" className="tracking-tight">
{getCredentialDisplayName(credential, displayName)}
</Text>
</div>
</SelectItem>
<option key={credential.id} value={credential.id}>
{getCredentialDisplayName(credential, displayName)}
</option>
))}
</SelectContent>
</Select>
</select>
<div className="rounded-medium border border-zinc-200 bg-white">
<CredentialRow
credential={displayCredential}
provider={provider}
displayName={displayName}
onSelect={() => {}}
onDelete={() => {}}
readOnly={readOnly}
asSelectTrigger={true}
variant={variant}
/>
</div>
</div>
</div>
);
}

View File

@@ -99,4 +99,30 @@ export function getCredentialDisplayName(
}
export const OAUTH_TIMEOUT_MS = 5 * 60 * 1000;
export const MASKED_KEY_LENGTH = 30;
export const MASKED_KEY_LENGTH = 15;
export function isSystemCredential(credential: {
title?: string | null;
is_system?: boolean;
}): boolean {
if (credential.is_system === true) return true;
if (!credential.title) return false;
const titleLower = credential.title.toLowerCase();
return (
titleLower.includes("system") ||
titleLower.startsWith("use credits for") ||
titleLower.includes("use credits")
);
}
export function filterSystemCredentials<
T extends { title?: string; is_system?: boolean },
>(credentials: T[]): T[] {
return credentials.filter((cred) => !isSystemCredential(cred));
}
export function getSystemCredentials<
T extends { title?: string; is_system?: boolean },
>(credentials: T[]): T[] {
return credentials.filter((cred) => isSystemCredential(cred));
}

View File

@@ -6,9 +6,11 @@ import {
CredentialsMetaInput,
} from "@/lib/autogpt-server-api/types";
import { useQueryClient } from "@tanstack/react-query";
import { useEffect, useMemo, useState } from "react";
import { useEffect, useRef, useState } from "react";
import {
filterSystemCredentials,
getActionButtonText,
getSystemCredentials,
OAUTH_TIMEOUT_MS,
OAuthPopupResultMessage,
} from "./helpers";
@@ -54,6 +56,7 @@ export function useCredentialsInput({
const api = useBackendAPI();
const queryClient = useQueryClient();
const credentials = useCredentials(schema, siblingInputs);
const hasAttemptedAutoSelect = useRef(false);
const deleteCredentialsMutation = useDeleteV1DeleteCredentials({
mutation: {
@@ -82,38 +85,51 @@ export function useCredentialsInput({
useEffect(() => {
if (readOnly) return;
if (!credentials || !("savedCredentials" in credentials)) return;
const availableCreds = credentials.savedCredentials;
if (
selectedCredential &&
!credentials.savedCredentials.some((c) => c.id === selectedCredential.id)
!availableCreds.some((c) => c.id === selectedCredential.id)
) {
onSelectCredential(undefined);
// Reset auto-selection flag so it can run again after unsetting invalid credential
hasAttemptedAutoSelect.current = false;
}
}, [credentials, selectedCredential, onSelectCredential, readOnly]);
// The available credential, if there is only one
const singleCredential = useMemo(() => {
if (!credentials || !("savedCredentials" in credentials)) {
return null;
}
return credentials.savedCredentials.length === 1
? credentials.savedCredentials[0]
: null;
}, [credentials]);
// Auto-select the one available credential (only if not optional)
// Auto-select the first available credential on initial mount
// Once a user has made a selection, we don't override it
useEffect(() => {
if (readOnly) return;
if (isOptional) return; // Don't auto-select when credential is optional
if (singleCredential && !selectedCredential) {
onSelectCredential(singleCredential);
if (!credentials || !("savedCredentials" in credentials)) return;
// If already selected, don't auto-select
if (selectedCredential?.id) return;
// Only attempt auto-selection once
if (hasAttemptedAutoSelect.current) return;
hasAttemptedAutoSelect.current = true;
// If optional, don't auto-select (user can choose "None")
if (isOptional) return;
const savedCreds = credentials.savedCredentials;
// Auto-select the first credential if any are available
if (savedCreds.length > 0) {
const cred = savedCreds[0];
onSelectCredential({
id: cred.id,
type: cred.type,
provider: credentials.provider,
title: (cred as any).title,
});
}
}, [
singleCredential,
selectedCredential,
onSelectCredential,
credentials,
selectedCredential?.id,
readOnly,
isOptional,
onSelectCredential,
]);
if (
@@ -135,8 +151,13 @@ export function useCredentialsInput({
supportsHostScoped,
savedCredentials,
oAuthCallback,
isSystemProvider,
} = credentials;
// Split credentials into user and system
const userCredentials = filterSystemCredentials(savedCredentials);
const systemCredentials = getSystemCredentials(savedCredentials);
async function handleOAuthLogin() {
setOAuthError(null);
const { login_url, state_token } = await api.oAuthLogin(
@@ -291,7 +312,10 @@ export function useCredentialsInput({
supportsOAuth2,
supportsUserPassword,
supportsHostScoped,
credentialsToShow: savedCredentials,
isSystemProvider,
userCredentials,
systemCredentials,
allCredentials: savedCredentials,
selectedCredential,
oAuthError,
isAPICredentialsModalOpen,
@@ -306,7 +330,7 @@ export function useCredentialsInput({
supportsApiKey,
supportsUserPassword,
supportsHostScoped,
savedCredentials.length > 0,
userCredentials.length > 0,
),
setAPICredentialsModalOpen,
setUserPasswordCredentialsModalOpen,

View File

@@ -12,7 +12,7 @@ import {
TooltipTrigger,
} from "@/components/atoms/Tooltip/BaseTooltip";
import { Dialog } from "@/components/molecules/Dialog/Dialog";
import { useState } from "react";
import { useEffect, useRef, useState } from "react";
import { ScheduleAgentModal } from "../ScheduleAgentModal/ScheduleAgentModal";
import { ModalHeader } from "./components/ModalHeader/ModalHeader";
import { ModalRunSection } from "./components/ModalRunSection/ModalRunSection";
@@ -82,6 +82,8 @@ export function RunAgentModal({
});
const [isScheduleModalOpen, setIsScheduleModalOpen] = useState(false);
const [hasOverflow, setHasOverflow] = useState(false);
const contentRef = useRef<HTMLDivElement>(null);
const hasAnySetupFields =
Object.keys(agentInputFields || {}).length > 0 ||
@@ -89,6 +91,43 @@ export function RunAgentModal({
const isTriggerRunType = defaultRunType.includes("trigger");
useEffect(() => {
if (!isOpen) return;
function checkOverflow() {
if (!contentRef.current) return;
const scrollableParent = contentRef.current
.closest("[data-dialog-content]")
?.querySelector('[class*="overflow-y-auto"]');
if (scrollableParent) {
setHasOverflow(
scrollableParent.scrollHeight > scrollableParent.clientHeight,
);
}
}
const timeoutId = setTimeout(checkOverflow, 100);
const resizeObserver = new ResizeObserver(checkOverflow);
if (contentRef.current) {
const scrollableParent = contentRef.current
.closest("[data-dialog-content]")
?.querySelector('[class*="overflow-y-auto"]');
if (scrollableParent) {
resizeObserver.observe(scrollableParent);
}
}
return () => {
clearTimeout(timeoutId);
resizeObserver.disconnect();
};
}, [
isOpen,
hasAnySetupFields,
agentInputFields,
agentCredentialsInputFields,
]);
function handleInputChange(key: string, value: string) {
setInputValues((prev) => ({
...prev,
@@ -134,91 +173,97 @@ export function RunAgentModal({
>
<Dialog.Trigger>{triggerSlot}</Dialog.Trigger>
<Dialog.Content>
{/* Header */}
<ModalHeader agent={agent} />
<div ref={contentRef} className="flex min-h-full flex-col">
<div className="flex-1">
{/* Header */}
<ModalHeader agent={agent} />
{/* Content */}
{hasAnySetupFields ? (
<div className="mt-10">
<RunAgentModalContextProvider
value={{
agent,
defaultRunType,
presetName,
setPresetName,
presetDescription,
setPresetDescription,
inputValues,
setInputValue: handleInputChange,
agentInputFields,
inputCredentials,
setInputCredentialsValue: handleCredentialsChange,
agentCredentialsInputFields,
}}
>
<ModalRunSection />
</RunAgentModalContextProvider>
{/* Content */}
{hasAnySetupFields ? (
<div className="mt-10 pb-32">
<RunAgentModalContextProvider
value={{
agent,
defaultRunType,
presetName,
setPresetName,
presetDescription,
setPresetDescription,
inputValues,
setInputValue: handleInputChange,
agentInputFields,
inputCredentials,
setInputCredentialsValue: handleCredentialsChange,
agentCredentialsInputFields,
}}
>
<ModalRunSection />
</RunAgentModalContextProvider>
</div>
) : null}
</div>
) : null}
<Dialog.Footer className="mt-6 bg-white pt-4">
<div className="flex items-center justify-end gap-3">
{isTriggerRunType ? null : !allRequiredInputsAreSet ? (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span>
<Button
variant="secondary"
onClick={handleOpenScheduleModal}
disabled={
isExecuting ||
isSettingUpTrigger ||
!allRequiredInputsAreSet
}
>
Schedule Task
</Button>
</span>
</TooltipTrigger>
<TooltipContent>
<p>
Please set up all required inputs and credentials before
scheduling
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
) : (
<Button
variant="secondary"
onClick={handleOpenScheduleModal}
disabled={
isExecuting ||
isSettingUpTrigger ||
!allRequiredInputsAreSet
}
>
Schedule Task
</Button>
)}
<RunActions
defaultRunType={defaultRunType}
onRun={handleRun}
isExecuting={isExecuting}
isSettingUpTrigger={isSettingUpTrigger}
isRunReady={allRequiredInputsAreSet}
<Dialog.Footer
className={`sticky bottom-0 z-10 bg-white pt-4 ${
hasOverflow
? "border-t border-neutral-100 shadow-[0_-2px_8px_rgba(0,0,0,0.04)]"
: ""
}`}
>
<div className="flex items-center justify-end gap-3">
{isTriggerRunType ? null : !allRequiredInputsAreSet ? (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span>
<Button
variant="secondary"
onClick={handleOpenScheduleModal}
disabled={
isExecuting ||
isSettingUpTrigger ||
!allRequiredInputsAreSet
}
>
Schedule Task
</Button>
</span>
</TooltipTrigger>
<TooltipContent>
<p>
Please set up all required inputs and credentials
before scheduling
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
) : (
<Button
variant="secondary"
onClick={handleOpenScheduleModal}
disabled={isExecuting || isSettingUpTrigger}
>
Schedule Task
</Button>
)}
<RunActions
defaultRunType={defaultRunType}
onRun={handleRun}
isExecuting={isExecuting}
isSettingUpTrigger={isSettingUpTrigger}
isRunReady={allRequiredInputsAreSet}
/>
</div>
<ScheduleAgentModal
isOpen={isScheduleModalOpen}
onClose={handleCloseScheduleModal}
agent={agent}
inputValues={inputValues}
inputCredentials={inputCredentials}
onScheduleCreated={handleScheduleCreated}
/>
</div>
<ScheduleAgentModal
isOpen={isScheduleModalOpen}
onClose={handleCloseScheduleModal}
agent={agent}
inputValues={inputValues}
inputCredentials={inputCredentials}
onScheduleCreated={handleScheduleCreated}
/>
</Dialog.Footer>
</Dialog.Footer>
</div>
</Dialog.Content>
</Dialog>
</>

View File

@@ -0,0 +1,169 @@
import { CredentialsInput } from "@/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/CredentialsInputs/CredentialsInput";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/molecules/Accordion/Accordion";
import { CredentialsProvidersContext } from "@/providers/agent-credentials/credentials-provider";
import { SlidersHorizontal } from "@phosphor-icons/react";
import { useContext, useEffect, useMemo, useRef } from "react";
import { useRunAgentModalContext } from "../../context";
import {
CredentialField,
findSavedCredentialByProviderAndType,
hasMissingRequiredSystemCredentials,
splitCredentialFieldsBySystem,
} from "../helpers";
type Props = {
credentialFields: CredentialField[];
requiredCredentials: Set<string>;
};
export function CredentialsGroupedView({
credentialFields,
requiredCredentials,
}: Props) {
const allProviders = useContext(CredentialsProvidersContext);
const { inputCredentials, setInputCredentialsValue, inputValues } =
useRunAgentModalContext();
const { userCredentialFields, systemCredentialFields } = useMemo(
() =>
splitCredentialFieldsBySystem(
credentialFields,
allProviders,
inputCredentials,
),
[credentialFields, allProviders, inputCredentials],
);
const hasSystemCredentials = systemCredentialFields.length > 0;
const hasUserCredentials = userCredentialFields.length > 0;
const hasAttemptedAutoSelect = useRef(false);
const hasMissingSystemCredentials = useMemo(
() =>
hasMissingRequiredSystemCredentials(
systemCredentialFields,
requiredCredentials,
inputCredentials,
),
[systemCredentialFields, requiredCredentials, inputCredentials],
);
useEffect(() => {
if (hasAttemptedAutoSelect.current) return;
if (!hasSystemCredentials) return;
let appliedSelection = false;
for (const [key, schema] of systemCredentialFields) {
const alreadySelected = inputCredentials?.[key];
const isRequired = requiredCredentials.has(key);
if (alreadySelected || !isRequired) continue;
const providerNames = schema.credentials_provider || [];
const credentialTypes = schema.credentials_types || [];
const savedCredential = findSavedCredentialByProviderAndType(
providerNames,
credentialTypes,
allProviders,
);
if (savedCredential) {
appliedSelection = true;
setInputCredentialsValue(key, {
id: savedCredential.id,
provider: savedCredential.provider,
type: savedCredential.type,
title: (savedCredential as { title?: string }).title,
});
}
}
if (appliedSelection) {
hasAttemptedAutoSelect.current = true;
}
}, [
allProviders,
hasSystemCredentials,
systemCredentialFields,
requiredCredentials,
inputCredentials,
setInputCredentialsValue,
]);
return (
<div className="space-y-6">
{hasUserCredentials && (
<>
{userCredentialFields.map(
([key, inputSubSchema]: CredentialField) => {
const selectedCred = inputCredentials?.[key];
return (
<CredentialsInput
key={key}
schema={
{ ...inputSubSchema, discriminator: undefined } as any
}
selectedCredentials={selectedCred}
onSelectCredentials={(value) => {
setInputCredentialsValue(key, value);
}}
siblingInputs={inputValues}
isOptional={!requiredCredentials.has(key)}
/>
);
},
)}
</>
)}
{hasSystemCredentials && (
<Accordion
type="single"
collapsible
className={hasUserCredentials ? "mt-4" : ""}
>
<AccordionItem value="system-credentials" className="border-none">
<AccordionTrigger className="py-2 text-sm text-muted-foreground hover:no-underline">
<div className="flex items-center gap-1">
<SlidersHorizontal size={16} weight="bold" /> System credentials
{hasMissingSystemCredentials && (
<span className="text-destructive">(missing)</span>
)}
</div>
</AccordionTrigger>
<AccordionContent>
<div className="space-y-6 px-1 pt-2">
{systemCredentialFields.map(
([key, inputSubSchema]: CredentialField) => {
const selectedCred = inputCredentials?.[key];
return (
<CredentialsInput
key={key}
schema={
{ ...inputSubSchema, discriminator: undefined } as any
}
selectedCredentials={selectedCred}
onSelectCredentials={(value) => {
setInputCredentialsValue(key, value);
}}
siblingInputs={inputValues}
isOptional={!requiredCredentials.has(key)}
/>
);
},
)}
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
)}
</div>
);
}

View File

@@ -1,8 +1,9 @@
import { CredentialsInput } from "@/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/CredentialsInputs/CredentialsInputs";
import { Input } from "@/components/atoms/Input/Input";
import { InformationTooltip } from "@/components/molecules/InformationTooltip/InformationTooltip";
import { useMemo } from "react";
import { RunAgentInputs } from "../../../RunAgentInputs/RunAgentInputs";
import { useRunAgentModalContext } from "../../context";
import { CredentialsGroupedView } from "../CredentialsGroupedView/CredentialsGroupedView";
import { ModalSection } from "../ModalSection/ModalSection";
import { WebhookTriggerBanner } from "../WebhookTriggerBanner/WebhookTriggerBanner";
@@ -17,15 +18,16 @@ export function ModalRunSection() {
inputValues,
setInputValue,
agentInputFields,
inputCredentials,
setInputCredentialsValue,
agentCredentialsInputFields,
} = useRunAgentModalContext();
const inputFields = Object.entries(agentInputFields || {});
const credentialFields = Object.entries(agentCredentialsInputFields || {});
// Get the list of required credentials from the schema
const credentialFields = useMemo(() => {
if (!agentCredentialsInputFields) return [];
return Object.entries(agentCredentialsInputFields);
}, [agentCredentialsInputFields]);
const requiredCredentials = new Set(
(agent.credentials_input_schema?.required as string[]) || [],
);
@@ -97,24 +99,10 @@ export function ModalRunSection() {
title="Task Credentials"
subtitle="These are the credentials the agent will use to perform this task"
>
<div className="space-y-6">
{Object.entries(agentCredentialsInputFields || {}).map(
([key, inputSubSchema]) => (
<CredentialsInput
key={key}
schema={
{ ...inputSubSchema, discriminator: undefined } as any
}
selectedCredentials={inputCredentials?.[key]}
onSelectCredentials={(value) =>
setInputCredentialsValue(key, value)
}
siblingInputs={inputValues}
isOptional={!requiredCredentials.has(key)}
/>
),
)}
</div>
<CredentialsGroupedView
credentialFields={credentialFields}
requiredCredentials={requiredCredentials}
/>
</ModalSection>
) : null}
</div>

View File

@@ -0,0 +1,93 @@
import { CredentialsProvidersContextType } from "@/providers/agent-credentials/credentials-provider";
export type CredentialField = [string, any];
type SavedCredential = {
id: string;
provider: string;
type: string;
title?: string | null;
};
export function splitCredentialFieldsBySystem(
credentialFields: CredentialField[],
allProviders: CredentialsProvidersContextType | null,
inputCredentials?: Record<string, unknown>,
) {
if (!allProviders || credentialFields.length === 0) {
return {
userCredentialFields: [] as CredentialField[],
systemCredentialFields: [] as CredentialField[],
};
}
const userFields: CredentialField[] = [];
const systemFields: CredentialField[] = [];
for (const [key, schema] of credentialFields) {
const providerNames = schema.credentials_provider || [];
const isSystemField = providerNames.some((providerName: string) => {
const providerData = allProviders[providerName];
return providerData?.isSystemProvider === true;
});
if (isSystemField) {
systemFields.push([key, schema]);
} else {
userFields.push([key, schema]);
}
}
const sortByUnsetFirst = (a: CredentialField, b: CredentialField) => {
const aIsSet = Boolean(inputCredentials?.[a[0]]);
const bIsSet = Boolean(inputCredentials?.[b[0]]);
if (aIsSet === bIsSet) return 0;
return aIsSet ? 1 : -1;
};
return {
userCredentialFields: userFields.sort(sortByUnsetFirst),
systemCredentialFields: systemFields.sort(sortByUnsetFirst),
};
}
export function hasMissingRequiredSystemCredentials(
systemCredentialFields: CredentialField[],
requiredCredentials: Set<string>,
inputCredentials?: Record<string, unknown>,
) {
if (systemCredentialFields.length === 0) return false;
return systemCredentialFields.some(([key]) => {
const isRequired = requiredCredentials.has(key);
const selectedCred = inputCredentials?.[key];
return isRequired && !selectedCred;
});
}
export function findSavedCredentialByProviderAndType(
providerNames: string[],
credentialTypes: string[],
allProviders: CredentialsProvidersContextType | null,
): SavedCredential | undefined {
for (const providerName of providerNames) {
const providerData = allProviders?.[providerName];
if (!providerData) continue;
const matchingCredential = providerData.savedCredentials.find(
(credential) => {
if (credentialTypes.length > 0) {
return credentialTypes.includes(credential.type);
}
return true;
},
);
if (matchingCredential) {
return matchingCredential as SavedCredential;
}
}
return undefined;
}

View File

@@ -11,9 +11,18 @@ import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { LibraryAgentPreset } from "@/app/api/__generated__/models/libraryAgentPreset";
import { useToast } from "@/components/molecules/Toast/use-toast";
import { isEmpty } from "@/lib/utils";
import { CredentialsProvidersContext } from "@/providers/agent-credentials/credentials-provider";
import { analytics } from "@/services/analytics";
import { useQueryClient } from "@tanstack/react-query";
import { useCallback, useEffect, useMemo, useState } from "react";
import {
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { getSystemCredentials } from "../CredentialsInputs/helpers";
import { showExecutionErrorToast } from "./errorHelpers";
export type RunVariant =
@@ -42,8 +51,10 @@ export function useAgentRunModal(
const [inputCredentials, setInputCredentials] = useState<Record<string, any>>(
callbacks?.initialInputCredentials || {},
);
const [presetName, setPresetName] = useState<string>("");
const [presetDescription, setPresetDescription] = useState<string>("");
const hasInitializedSystemCreds = useRef(false);
// Determine the default run type based on agent capabilities
const defaultRunType: RunVariant = agent.trigger_setup_info
@@ -58,6 +69,91 @@ export function useAgentRunModal(
setInputCredentials(callbacks?.initialInputCredentials || {});
}, [callbacks?.initialInputValues, callbacks?.initialInputCredentials]);
const allProviders = useContext(CredentialsProvidersContext);
// Initialize credentials with default system credentials
useEffect(() => {
if (!allProviders || !agent.credentials_input_schema?.properties) return;
if (callbacks?.initialInputCredentials) {
hasInitializedSystemCreds.current = true;
return;
}
if (hasInitializedSystemCreds.current) return;
const properties = agent.credentials_input_schema.properties as Record<
string,
any
>;
setInputCredentials((currentCreds) => {
const credsToAdd: Record<string, any> = {};
for (const [key, schema] of Object.entries(properties)) {
if (currentCreds[key]) continue;
const providerNames = schema.credentials_provider || [];
const supportedTypes = schema.credentials_types || [];
const requiredScopes = schema.credentials_scopes;
for (const providerName of providerNames) {
const providerData = allProviders[providerName];
if (!providerData) continue;
const systemCreds = getSystemCredentials(
providerData.savedCredentials ?? [],
);
const matchingSystemCreds = systemCreds.filter((cred) => {
if (!supportedTypes.includes(cred.type)) return false;
if (
cred.type === "oauth2" &&
requiredScopes &&
requiredScopes.length > 0
) {
const grantedScopes = new Set(cred.scopes || []);
const hasAllRequiredScopes = requiredScopes.every(
(scope: string) => grantedScopes.has(scope),
);
if (!hasAllRequiredScopes) return false;
}
return true;
});
if (matchingSystemCreds.length === 1) {
const systemCred = matchingSystemCreds[0];
credsToAdd[key] = {
id: systemCred.id,
type: systemCred.type,
provider: providerName,
title: systemCred.title,
};
break;
}
}
}
if (Object.keys(credsToAdd).length > 0) {
hasInitializedSystemCreds.current = true;
return {
...currentCreds,
...credsToAdd,
};
}
return currentCreds;
});
}, [
allProviders,
agent.credentials_input_schema,
callbacks?.initialInputCredentials,
]);
// Reset initialization flag when modal closes/opens or agent changes
useEffect(() => {
hasInitializedSystemCreds.current = false;
}, [isOpen, agent.graph_id]);
// API mutations
const executeGraphMutation = usePostV1ExecuteGraphAgent({
mutation: {
@@ -66,7 +162,6 @@ export function useAgentRunModal(
toast({
title: "Agent execution started",
});
// Invalidate runs list for this graph
queryClient.invalidateQueries({
queryKey: getGetV1ListGraphExecutionsQueryKey(agent.graph_id),
});
@@ -163,14 +258,10 @@ export function useAgentRunModal(
}, [agentInputSchema.required, inputValues]);
const [allCredentialsAreSet, missingCredentials] = useMemo(() => {
// Only check required credentials from schema, not all properties
// Credentials marked as optional in node metadata won't be in the required array
const requiredCredentials = new Set(
(agent.credentials_input_schema?.required as string[]) || [],
);
// Check if required credentials have valid id (not just key existence)
// A credential is valid only if it has an id field set
const missing = [...requiredCredentials].filter((key) => {
const cred = inputCredentials[key];
return !cred || !cred.id;
@@ -184,7 +275,6 @@ export function useAgentRunModal(
[agentCredentialsInputFields],
);
// Final readiness flag combining inputs + credentials when credentials are shown
const allRequiredInputsAreSet = useMemo(
() =>
allRequiredInputsAreSetRaw &&
@@ -223,7 +313,6 @@ export function useAgentRunModal(
defaultRunType === "automatic-trigger" ||
defaultRunType === "manual-trigger"
) {
// Setup trigger
if (!presetName.trim()) {
toast({
title: "⚠️ Trigger name required",
@@ -244,9 +333,6 @@ export function useAgentRunModal(
},
});
} else {
// Manual execution
// Filter out incomplete credentials (optional ones not selected)
// Only send credentials that have a valid id field
const validCredentials = Object.fromEntries(
Object.entries(inputCredentials).filter(([_, cred]) => cred && cred.id),
);
@@ -280,41 +366,24 @@ export function useAgentRunModal(
}, [agentInputFields]);
return {
// UI state
isOpen,
setIsOpen,
// Run mode
defaultRunType: defaultRunType as RunVariant,
// Form: regular inputs
inputValues,
setInputValues,
// Form: credentials
inputCredentials,
setInputCredentials,
// Preset/trigger labels
presetName,
presetDescription,
setPresetName,
setPresetDescription,
// Validation/readiness
allRequiredInputsAreSet,
missingInputs,
// Schemas for rendering
agentInputFields,
agentCredentialsInputFields,
hasInputFields,
// Async states
isExecuting: executeGraphMutation.isPending,
isSettingUpTrigger: setupTriggerMutation.isPending,
// Actions
handleRun,
};
}

View File

@@ -1,37 +1,17 @@
import { Button } from "@/components/atoms/Button/Button";
import { Text } from "@/components/atoms/Text/Text";
import { GearIcon } from "@phosphor-icons/react";
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { useAgentSafeMode } from "@/hooks/useAgentSafeMode";
interface Props {
agent: LibraryAgent;
onSelectSettings: () => void;
selected?: boolean;
}
export function AgentSettingsButton({
agent,
onSelectSettings,
selected,
}: Props) {
const { hasHITLBlocks } = useAgentSafeMode(agent);
if (!hasHITLBlocks) {
return null;
}
export function AgentSettingsButton() {
return (
<Button
variant={selected ? "secondary" : "ghost"}
variant="ghost"
size="small"
className="m-0 min-w-0 rounded-full p-0 px-1"
onClick={onSelectSettings}
aria-label="Agent Settings"
>
<GearIcon
size={18}
className={selected ? "text-zinc-900" : "text-zinc-600"}
/>
<GearIcon size={18} className="text-zinc-600" />
<Text variant="small">Agent Settings</Text>
</Button>
);
}

View File

@@ -1,3 +1,5 @@
"use client";
import { Text } from "@/components/atoms/Text/Text";
export function EmptySchedules() {

View File

@@ -1,3 +1,5 @@
"use client";
import { Text } from "@/components/atoms/Text/Text";
export function EmptyTemplates() {

View File

@@ -1,3 +1,5 @@
"use client";
import { Text } from "@/components/atoms/Text/Text";
export function EmptyTriggers() {

View File

@@ -3,7 +3,7 @@
import { Button } from "@/components/atoms/Button/Button";
import { Text } from "@/components/atoms/Text/Text";
interface MarketplaceBannersProps {
interface Props {
hasUpdate?: boolean;
latestVersion?: number;
hasUnpublishedChanges?: boolean;
@@ -21,7 +21,7 @@ export function MarketplaceBanners({
isUpdating,
onUpdate,
onPublish,
}: MarketplaceBannersProps) {
}: Props) {
const renderUpdateBanner = () => {
if (hasUpdate && latestVersion) {
return (

View File

@@ -1,3 +1,5 @@
"use client";
import { cn } from "@/lib/utils";
type Props = {

View File

@@ -1,22 +1,16 @@
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { Skeleton } from "@/components/__legacy__/ui/skeleton";
import { cn } from "@/lib/utils";
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { AGENT_LIBRARY_SECTION_PADDING_X } from "../../helpers";
import { SelectedViewLayout } from "./SelectedViewLayout";
interface Props {
agent: LibraryAgent;
onSelectSettings?: () => void;
selectedSettings?: boolean;
}
export function LoadingSelectedContent(props: Props) {
return (
<SelectedViewLayout
agent={props.agent}
onSelectSettings={props.onSelectSettings}
selectedSettings={props.selectedSettings}
>
<SelectedViewLayout agent={props.agent}>
<div
className={cn("flex flex-col gap-4", AGENT_LIBRARY_SECTION_PADDING_X)}
>

View File

@@ -33,8 +33,6 @@ interface Props {
onSelectRun?: (id: string) => void;
onClearSelectedRun?: () => void;
banner?: React.ReactNode;
onSelectSettings?: () => void;
selectedSettings?: boolean;
}
export function SelectedRunView({
@@ -43,8 +41,6 @@ export function SelectedRunView({
onSelectRun,
onClearSelectedRun,
banner,
onSelectSettings,
selectedSettings,
}: Props) {
const { run, preset, isLoading, responseError, httpError } =
useSelectedRunView(agent.graph_id, runId);
@@ -84,12 +80,7 @@ export function SelectedRunView({
return (
<div className="flex h-full w-full gap-4">
<div className="flex min-h-0 min-w-0 flex-1 flex-col">
<SelectedViewLayout
agent={agent}
banner={banner}
onSelectSettings={onSelectSettings}
selectedSettings={selectedSettings}
>
<SelectedViewLayout agent={agent} banner={banner}>
<div className="flex flex-col gap-4">
<RunDetailHeader agent={agent} run={run} />

View File

@@ -21,8 +21,6 @@ interface Props {
scheduleId: string;
onClearSelectedRun?: () => void;
banner?: React.ReactNode;
onSelectSettings?: () => void;
selectedSettings?: boolean;
}
export function SelectedScheduleView({
@@ -30,8 +28,6 @@ export function SelectedScheduleView({
scheduleId,
onClearSelectedRun,
banner,
onSelectSettings,
selectedSettings,
}: Props) {
const { schedule, isLoading, error } = useSelectedScheduleView(
agent.graph_id,
@@ -76,12 +72,7 @@ export function SelectedScheduleView({
return (
<div className="flex h-full w-full gap-4">
<div className="flex min-h-0 min-w-0 flex-1 flex-col">
<SelectedViewLayout
agent={agent}
banner={banner}
onSelectSettings={onSelectSettings}
selectedSettings={selectedSettings}
>
<SelectedViewLayout agent={agent} banner={banner}>
<div className="flex flex-col gap-4">
<div className="flex w-full flex-col gap-0">
<RunDetailHeader

View File

@@ -1,11 +1,11 @@
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { Text } from "@/components/atoms/Text/Text";
import { Switch } from "@/components/atoms/Switch/Switch";
import { Button } from "@/components/atoms/Button/Button";
import { ArrowLeftIcon } from "@phosphor-icons/react";
import { Switch } from "@/components/atoms/Switch/Switch";
import { Text } from "@/components/atoms/Text/Text";
import { useAgentSafeMode } from "@/hooks/useAgentSafeMode";
import { SelectedViewLayout } from "../SelectedViewLayout";
import { ArrowLeftIcon } from "@phosphor-icons/react";
import { AGENT_LIBRARY_SECTION_PADDING_X } from "../../../helpers";
import { SelectedViewLayout } from "../SelectedViewLayout";
interface Props {
agent: LibraryAgent;
@@ -17,7 +17,7 @@ export function SelectedSettingsView({ agent, onClearSelectedRun }: Props) {
useAgentSafeMode(agent);
return (
<SelectedViewLayout agent={agent} onSelectSettings={() => {}}>
<SelectedViewLayout agent={agent}>
<div className="flex flex-col gap-4">
<div
className={`${AGENT_LIBRARY_SECTION_PADDING_X} mb-8 flex items-center gap-3`}
@@ -33,15 +33,8 @@ export function SelectedSettingsView({ agent, onClearSelectedRun }: Props) {
<Text variant="h2">Agent Settings</Text>
</div>
<div className={AGENT_LIBRARY_SECTION_PADDING_X}>
{!hasHITLBlocks ? (
<div className="rounded-xl border border-zinc-100 bg-white p-6">
<Text variant="body" className="text-muted-foreground">
This agent doesn&apos;t have any human-in-the-loop blocks, so
there are no settings to configure.
</Text>
</div>
) : (
<div className={`${AGENT_LIBRARY_SECTION_PADDING_X} space-y-6`}>
{hasHITLBlocks ? (
<div className="flex w-full max-w-2xl flex-col items-start gap-4 rounded-xl border border-zinc-100 bg-white p-6">
<div className="flex w-full items-start justify-between gap-4">
<div className="flex-1">
@@ -59,6 +52,12 @@ export function SelectedSettingsView({ agent, onClearSelectedRun }: Props) {
/>
</div>
</div>
) : (
<div className="rounded-xl border border-zinc-100 bg-white p-6">
<Text variant="body" className="text-muted-foreground">
This agent doesn&apos;t have any configurable settings.
</Text>
</div>
)}
</div>
</div>

View File

@@ -8,7 +8,7 @@ import {
getAgentCredentialsFields,
getAgentInputFields,
} from "../../modals/AgentInputsReadOnly/helpers";
import { CredentialsInput } from "../../modals/CredentialsInputs/CredentialsInputs";
import { CredentialsInput } from "../../modals/CredentialsInputs/CredentialsInput";
import { RunAgentInputs } from "../../modals/RunAgentInputs/RunAgentInputs";
import { LoadingSelectedContent } from "../LoadingSelectedContent";
import { RunDetailCard } from "../RunDetailCard/RunDetailCard";

View File

@@ -7,7 +7,7 @@ import {
getAgentCredentialsFields,
getAgentInputFields,
} from "../../modals/AgentInputsReadOnly/helpers";
import { CredentialsInput } from "../../modals/CredentialsInputs/CredentialsInputs";
import { CredentialsInput } from "../../modals/CredentialsInputs/CredentialsInput";
import { RunAgentInputs } from "../../modals/RunAgentInputs/RunAgentInputs";
import { LoadingSelectedContent } from "../LoadingSelectedContent";
import { RunDetailCard } from "../RunDetailCard/RunDetailCard";

View File

@@ -1,7 +1,7 @@
import { Breadcrumbs } from "@/components/molecules/Breadcrumbs/Breadcrumbs";
import { AgentSettingsButton } from "@/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/other/AgentSettingsButton";
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { Breadcrumbs } from "@/components/molecules/Breadcrumbs/Breadcrumbs";
import { AGENT_LIBRARY_SECTION_PADDING_X } from "../../helpers";
import { AgentSettingsModal } from "../modals/AgentSettingsModal/AgentSettingsModal";
import { SectionWrap } from "../other/SectionWrap";
interface Props {
@@ -9,8 +9,6 @@ interface Props {
children: React.ReactNode;
banner?: React.ReactNode;
additionalBreadcrumb?: { name: string; link?: string };
onSelectSettings?: () => void;
selectedSettings?: boolean;
}
export function SelectedViewLayout(props: Props) {
@@ -19,8 +17,8 @@ export function SelectedViewLayout(props: Props) {
<div
className={`${AGENT_LIBRARY_SECTION_PADDING_X} flex-shrink-0 border-b border-zinc-100 pb-0 lg:pb-4`}
>
{props.banner && <div className="mb-4">{props.banner}</div>}
<div className="relative flex w-fit items-center gap-2">
{props.banner}
<div className="relative flex w-full items-center justify-between">
<Breadcrumbs
items={[
{ name: "My Library", link: "/library" },
@@ -33,15 +31,9 @@ export function SelectedViewLayout(props: Props) {
: []),
]}
/>
{props.agent && props.onSelectSettings && (
<div className="absolute -right-8">
<AgentSettingsButton
agent={props.agent}
onSelectSettings={props.onSelectSettings}
selected={props.selectedSettings}
/>
</div>
)}
<div className="absolute right-0">
<AgentSettingsModal agent={props.agent} />
</div>
</div>
</div>
<div className="flex min-h-0 flex-1 flex-col overflow-y-auto overflow-x-visible">

View File

@@ -12,7 +12,7 @@ import {
} from "@/lib/autogpt-server-api";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
import { CredentialsInput } from "@/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/CredentialsInputs/CredentialsInputs";
import { CredentialsInput } from "@/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/CredentialsInputs/CredentialsInput";
import { RunAgentInputs } from "@/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/RunAgentInputs/RunAgentInputs";
import { ScheduleTaskDialog } from "@/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/cron-scheduler-dialog";
import ActionButtonGroup from "@/components/__legacy__/action-button-group";

View File

@@ -1,14 +1,7 @@
"use client";
import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag";
import { NewAgentLibraryView } from "./components/NewAgentLibraryView/NewAgentLibraryView";
import { OldAgentLibraryView } from "./components/OldAgentLibraryView/OldAgentLibraryView";
export default function AgentLibraryPage() {
const isNewLibraryPageEnabled = useGetFlag(Flag.NEW_AGENT_RUNS);
return isNewLibraryPageEnabled ? (
<NewAgentLibraryView />
) : (
<OldAgentLibraryView />
);
return <NewAgentLibraryView />;
}

View File

@@ -29,7 +29,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
href: "/profile/dashboard",
icon: <StorefrontIcon className="size-5" />,
},
...(isPaymentEnabled || true
...(isPaymentEnabled
? [
{
text: "Billing",

View File

@@ -2775,6 +2775,28 @@
}
}
},
"/api/integrations/providers/system": {
"get": {
"tags": ["v1", "integrations"],
"summary": "List System Providers",
"description": "Get a list of providers that have platform credits (system credentials) available.\n\nThese providers can be used without the user providing their own API keys.",
"operationId": "getV1ListSystemProviders",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"items": { "type": "string" },
"type": "array",
"title": "Response Getv1Listsystemproviders"
}
}
}
}
}
}
},
"/api/integrations/webhooks/{webhook_id}/ping": {
"post": {
"tags": ["v1", "integrations"],

View File

@@ -20,6 +20,7 @@ export function Button(props: ButtonProps) {
rightIcon,
children,
as = "button",
asChild: _asChild, // Destructure to prevent passing to DOM
...restProps
} = props;

View File

@@ -1,6 +1,6 @@
"use client";
import { CredentialsInput } from "@/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/CredentialsInputs/CredentialsInputs";
import { CredentialsInput } from "@/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/CredentialsInputs/CredentialsInput";
import { Button } from "@/components/atoms/Button/Button";
import { CircleNotchIcon, FolderOpenIcon } from "@phosphor-icons/react";
import {

View File

@@ -0,0 +1,203 @@
import type { Meta } from "@storybook/nextjs";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "./Accordion";
const meta: Meta<typeof Accordion> = {
title: "Molecules/Accordion",
component: Accordion,
parameters: {
layout: "centered",
docs: {
description: {
component: `
## Accordion Component
A vertically stacked set of interactive headings that each reveal an associated section of content.
### ✨ Features
- **Built on Radix UI** - Uses @radix-ui/react-accordion for accessibility and functionality
- **Single or multiple** - Supports single or multiple items open at once
- **Smooth animations** - Built-in expand/collapse animations
- **Accessible** - Full keyboard navigation and screen reader support
- **Customizable** - Style with Tailwind CSS classes
### 🎯 Usage
\`\`\`tsx
<Accordion type="single" collapsible>
<AccordionItem value="item-1">
<AccordionTrigger>Is it accessible?</AccordionTrigger>
<AccordionContent>
Yes. It adheres to the WAI-ARIA design pattern.
</AccordionContent>
</AccordionItem>
</Accordion>
\`\`\`
### Props
**Accordion**:
- **type**: "single" | "multiple" - Whether one or multiple items can be open
- **collapsible**: boolean - When type is "single", allows closing all items
- **defaultValue**: string | string[] - Default open item(s)
- **value**: string | string[] - Controlled open item(s)
- **onValueChange**: (value) => void - Callback when value changes
**AccordionItem**:
- **value**: string - Unique identifier for the item
- **disabled**: boolean - Whether the item is disabled
**AccordionTrigger**:
- Standard button props
**AccordionContent**:
- Standard div props
`,
},
},
},
tags: ["autodocs"],
argTypes: {
type: {
control: "radio",
options: ["single", "multiple"],
description: "Whether one or multiple items can be open at the same time",
table: {
defaultValue: { summary: "single" },
},
},
collapsible: {
control: "boolean",
description:
'When type is "single", allows closing content when clicking on open trigger',
table: {
defaultValue: { summary: "false" },
},
},
},
};
export default meta;
export function Default() {
return (
<Accordion type="single" collapsible className="w-96">
<AccordionItem value="item-1">
<AccordionTrigger>Is it accessible?</AccordionTrigger>
<AccordionContent>
Yes. It adheres to the WAI-ARIA design pattern.
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-2">
<AccordionTrigger>Is it styled?</AccordionTrigger>
<AccordionContent>
Yes. It comes with default styles that match your design system.
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-3">
<AccordionTrigger>Is it animated?</AccordionTrigger>
<AccordionContent>
Yes. It&apos;s animated by default with smooth expand/collapse
transitions.
</AccordionContent>
</AccordionItem>
</Accordion>
);
}
export function Multiple() {
return (
<Accordion type="multiple" className="w-96">
<AccordionItem value="item-1">
<AccordionTrigger>First section</AccordionTrigger>
<AccordionContent>
Multiple items can be open at the same time when type is set to
&quot;multiple&quot;.
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-2">
<AccordionTrigger>Second section</AccordionTrigger>
<AccordionContent>
Try opening this one while the first is still open.
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-3">
<AccordionTrigger>Third section</AccordionTrigger>
<AccordionContent>
All three can be open simultaneously.
</AccordionContent>
</AccordionItem>
</Accordion>
);
}
export function DefaultOpen() {
return (
<Accordion type="single" collapsible defaultValue="item-2" className="w-96">
<AccordionItem value="item-1">
<AccordionTrigger>Closed by default</AccordionTrigger>
<AccordionContent>This item starts closed.</AccordionContent>
</AccordionItem>
<AccordionItem value="item-2">
<AccordionTrigger>Open by default</AccordionTrigger>
<AccordionContent>
This item starts open because defaultValue is set to
&quot;item-2&quot;.
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-3">
<AccordionTrigger>Also closed</AccordionTrigger>
<AccordionContent>This item also starts closed.</AccordionContent>
</AccordionItem>
</Accordion>
);
}
export function WithDisabledItem() {
return (
<Accordion type="single" collapsible className="w-96">
<AccordionItem value="item-1">
<AccordionTrigger>Available item</AccordionTrigger>
<AccordionContent>This item can be toggled.</AccordionContent>
</AccordionItem>
<AccordionItem value="item-2" disabled>
<AccordionTrigger>Disabled item</AccordionTrigger>
<AccordionContent>
This content cannot be accessed because the item is disabled.
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-3">
<AccordionTrigger>Another available item</AccordionTrigger>
<AccordionContent>This item can also be toggled.</AccordionContent>
</AccordionItem>
</Accordion>
);
}
export function CustomStyled() {
return (
<Accordion type="single" collapsible className="w-96">
<AccordionItem value="item-1" className="border-none">
<AccordionTrigger className="rounded-lg bg-zinc-100 px-4 hover:bg-zinc-200 hover:no-underline">
Custom styled trigger
</AccordionTrigger>
<AccordionContent className="px-4 pt-2">
You can customize the styling using className props.
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-2" className="mt-2 border-none">
<AccordionTrigger className="rounded-lg bg-blue-50 px-4 text-blue-700 hover:bg-blue-100 hover:no-underline">
Blue themed
</AccordionTrigger>
<AccordionContent className="px-4 pt-2 text-blue-600">
Each item can have different styles.
</AccordionContent>
</AccordionItem>
</Accordion>
);
}

View File

@@ -0,0 +1,8 @@
"use client";
export {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";

View File

@@ -22,6 +22,9 @@ export function DrawerWrap({
handleClose,
isForceOpen,
}: Props) {
const accessibleTitle = title ?? "Dialog";
const hasVisibleTitle = Boolean(title);
const closeBtn = (
<Button
variant="link"
@@ -44,15 +47,19 @@ export function DrawerWrap({
>
<div
className={`flex w-full shrink-0 items-center justify-between ${
title ? "pb-6" : "pb-0"
hasVisibleTitle ? "pb-6" : "pb-0"
}`}
>
{title ? (
<Drawer.Title className={drawerStyles.title}>{title}</Drawer.Title>
) : null}
{hasVisibleTitle ? (
<Drawer.Title className={drawerStyles.title}>
{accessibleTitle}
</Drawer.Title>
) : (
<Drawer.Title className="sr-only">{accessibleTitle}</Drawer.Title>
)}
{!isForceOpen ? (
title ? (
hasVisibleTitle ? (
closeBtn
) : (
<div

View File

@@ -1,14 +1,14 @@
import React, { useMemo } from "react";
import { FieldProps, getUiOptions } from "@rjsf/utils";
import { useNodeStore } from "@/app/(platform)/build/stores/nodeStore";
import { CredentialsInput } from "@/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/CredentialsInputs/CredentialsInput";
import { Switch } from "@/components/atoms/Switch/Switch";
import {
BlockIOCredentialsSubSchema,
CredentialsMetaInput,
} from "@/lib/autogpt-server-api";
import { CredentialsInput } from "@/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/CredentialsInputs/CredentialsInputs";
import { useNodeStore } from "@/app/(platform)/build/stores/nodeStore";
import { FieldProps, getUiOptions } from "@rjsf/utils";
import { useMemo } from "react";
import { useShallow } from "zustand/react/shallow";
import { CredentialFieldTitle } from "./components/CredentialFieldTitle";
import { Switch } from "@/components/atoms/Switch/Switch";
export const CredentialsField = (props: FieldProps) => {
const { formData, onChange, schema, registry, fieldPathId, required } = props;

View File

@@ -0,0 +1,57 @@
"use client";
import * as AccordionPrimitive from "@radix-ui/react-accordion";
import { ChevronDown } from "lucide-react";
import * as React from "react";
import { cn } from "@/lib/utils";
const Accordion = AccordionPrimitive.Root;
const AccordionItem = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
>(({ className, ...props }, ref) => (
<AccordionPrimitive.Item
ref={ref}
className={cn("border-b", className)}
{...props}
/>
));
AccordionItem.displayName = "AccordionItem";
const AccordionTrigger = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
"flex flex-1 items-center justify-between py-4 text-left text-sm font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
className,
)}
{...props}
>
{children}
<ChevronDown className="h-4 w-4 shrink-0 text-neutral-500 transition-transform duration-200 dark:text-neutral-400" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
));
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
const AccordionContent = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
{...props}
>
<div className={cn("pb-4 pt-0", className)}>{children}</div>
</AccordionPrimitive.Content>
));
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
export { Accordion, AccordionContent, AccordionItem, AccordionTrigger };

View File

@@ -352,6 +352,10 @@ export default class BackendAPI {
return this._get("/integrations/providers");
}
listSystemProviders(): Promise<string[]> {
return this._get("/integrations/providers/system");
}
listCredentials(provider?: string): Promise<CredentialsMetaResponse[]> {
return this._get(
provider

View File

@@ -593,6 +593,7 @@ export type CredentialsMetaResponse = {
scopes?: Array<string>;
username?: string;
host?: string;
is_system?: boolean;
};
/* Mirror of backend/server/integrations/router.py:CredentialsDeletionResponse */

View File

@@ -1,5 +1,4 @@
import { createContext, useCallback, useEffect, useState } from "react";
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
import { useToastOnFail } from "@/components/molecules/Toast/use-toast";
import {
APIKeyCredentials,
CredentialsDeleteNeedConfirmationResponse,
@@ -10,8 +9,9 @@ import {
UserPasswordCredentials,
} from "@/lib/autogpt-server-api";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
import { useToastOnFail } from "@/components/molecules/Toast/use-toast";
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
import { toDisplayName } from "@/providers/agent-credentials/helper";
import { createContext, useCallback, useEffect, useState } from "react";
type APIKeyCredentialsCreatable = Omit<
APIKeyCredentials,
@@ -32,6 +32,8 @@ export type CredentialsProviderData = {
provider: CredentialsProviderName;
providerName: string;
savedCredentials: CredentialsMetaResponse[];
/** Whether this provider has platform credits available (system credentials) */
isSystemProvider: boolean;
oAuthCallback: (
code: string,
state_token: string,
@@ -68,6 +70,9 @@ export default function CredentialsProvider({
const [providers, setProviders] =
useState<CredentialsProvidersContextType | null>(null);
const [providerNames, setProviderNames] = useState<string[]>([]);
const [systemProviders, setSystemProviders] = useState<Set<string>>(
new Set(),
);
const { isLoggedIn } = useSupabase();
const api = useBackendAPI();
const onFailToast = useToastOnFail();
@@ -218,17 +223,7 @@ export default function CredentialsProvider({
[api, onFailToast],
);
// Fetch provider names on mount
useEffect(() => {
api
.listProviders()
.then((names) => {
setProviderNames(names);
})
.catch(onFailToast("load provider names"));
}, [api, onFailToast]);
useEffect(() => {
const loadCredentials = useCallback(() => {
if (!isLoggedIn || providerNames.length === 0) {
if (isLoggedIn == false) setProviders({});
return;
@@ -251,27 +246,32 @@ export default function CredentialsProvider({
setProviders((prev) => ({
...prev,
...Object.fromEntries(
providerNames.map((provider) => [
provider,
{
providerNames.map((provider) => {
const providerCredentials = credentialsByProvider[provider] ?? [];
return [
provider,
providerName: toDisplayName(provider as string),
savedCredentials: credentialsByProvider[provider] ?? [],
oAuthCallback: (code: string, state_token: string) =>
oAuthCallback(provider, code, state_token),
createAPIKeyCredentials: (
credentials: APIKeyCredentialsCreatable,
) => createAPIKeyCredentials(provider, credentials),
createUserPasswordCredentials: (
credentials: UserPasswordCredentialsCreatable,
) => createUserPasswordCredentials(provider, credentials),
createHostScopedCredentials: (
credentials: HostScopedCredentialsCreatable,
) => createHostScopedCredentials(provider, credentials),
deleteCredentials: (id: string, force: boolean = false) =>
deleteCredentials(provider, id, force),
} satisfies CredentialsProviderData,
]),
{
provider,
providerName: toDisplayName(provider as string),
savedCredentials: providerCredentials,
isSystemProvider: systemProviders.has(provider),
oAuthCallback: (code: string, state_token: string) =>
oAuthCallback(provider, code, state_token),
createAPIKeyCredentials: (
credentials: APIKeyCredentialsCreatable,
) => createAPIKeyCredentials(provider, credentials),
createUserPasswordCredentials: (
credentials: UserPasswordCredentialsCreatable,
) => createUserPasswordCredentials(provider, credentials),
createHostScopedCredentials: (
credentials: HostScopedCredentialsCreatable,
) => createHostScopedCredentials(provider, credentials),
deleteCredentials: (id: string, force: boolean = false) =>
deleteCredentials(provider, id, force),
} satisfies CredentialsProviderData,
];
}),
),
}));
})
@@ -280,6 +280,7 @@ export default function CredentialsProvider({
api,
isLoggedIn,
providerNames,
systemProviders,
createAPIKeyCredentials,
createUserPasswordCredentials,
createHostScopedCredentials,
@@ -288,6 +289,20 @@ export default function CredentialsProvider({
onFailToast,
]);
// Fetch provider names and system providers on mount
useEffect(() => {
Promise.all([api.listProviders(), api.listSystemProviders()])
.then(([names, systemList]) => {
setProviderNames(names);
setSystemProviders(new Set(systemList));
})
.catch(onFailToast("Load provider names"));
}, [api, onFailToast]);
useEffect(() => {
loadCredentials();
}, [loadCredentials]);
return (
<CredentialsProvidersContext.Provider value={providers}>
{children}

View File

@@ -22,13 +22,7 @@ const config = {
poppins: ["var(--font-poppins)"],
},
colors: {
// *** APPROVED DESIGN SYSTEM COLORS ***
// These are the ONLY colors that should be used in our app
...colors,
// Legacy colors - DO NOT USE THESE IN NEW CODE
// These are kept only to prevent breaking existing styles
// Use the approved design system colors above instead
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
@@ -63,70 +57,66 @@ const config = {
foreground: "hsl(var(--card-foreground))",
},
customGray: {
100: "#d9d9d9",
200: "#a8a8a8",
300: "#878787",
400: "#646464",
500: "#474747",
600: "#282828",
700: "#272727",
"100": "#d9d9d9",
"200": "#a8a8a8",
"300": "#878787",
"400": "#646464",
"500": "#474747",
"600": "#282828",
"700": "#272727",
},
},
spacing: {
// Tailwind spacing + custom sizes
0: "0rem", // 0px
0.5: "0.125rem", // 2px
1: "0.25rem", // 4px
1.5: "0.375rem", // 6px
2: "0.5rem", // 8px
2.5: "0.625rem", // 10px
3: "0.75rem", // 12px
3.5: "0.875rem", // 14px
4: "1rem", // 16px
5: "1.25rem", // 20px
6: "1.5rem", // 24px
7: "1.75rem", // 28px
7.5: "1.875rem", // 30px
8: "2rem", // 32px
8.5: "2.125rem", // 34px
9: "2.25rem", // 36px
10: "2.5rem", // 40px
11: "2.75rem", // 44px
12: "3rem", // 48px
14: "3.5rem", // 56px
16: "4rem", // 64px
18: "4.5rem", // 72px
20: "5rem", // 80px
24: "6rem", // 96px
28: "7rem", // 112px
32: "8rem", // 128px
36: "9rem", // 144px
40: "10rem", // 160px
44: "11rem", // 176px
48: "12rem", // 192px
52: "13rem", // 208px
56: "14rem", // 224px
60: "15rem", // 240px
64: "16rem", // 256px
68: "17rem", // 272px
70: "17.5rem", // 280px
71: "17.75rem", // 284px
72: "18rem", // 288px
76: "19rem", // 304px
80: "20rem", // 320px
96: "24rem", // 384px
"0": "0rem",
"1": "0.25rem",
"2": "0.5rem",
"3": "0.75rem",
"4": "1rem",
"5": "1.25rem",
"6": "1.5rem",
"7": "1.75rem",
"8": "2rem",
"9": "2.25rem",
"10": "2.5rem",
"11": "2.75rem",
"12": "3rem",
"14": "3.5rem",
"16": "4rem",
"18": "4.5rem",
"20": "5rem",
"24": "6rem",
"28": "7rem",
"32": "8rem",
"36": "9rem",
"40": "10rem",
"44": "11rem",
"48": "12rem",
"52": "13rem",
"56": "14rem",
"60": "15rem",
"64": "16rem",
"68": "17rem",
"70": "17.5rem",
"71": "17.75rem",
"72": "18rem",
"76": "19rem",
"80": "20rem",
"96": "24rem",
"0.5": "0.125rem",
"1.5": "0.375rem",
"2.5": "0.625rem",
"3.5": "0.875rem",
"7.5": "1.875rem",
"8.5": "2.125rem",
},
borderRadius: {
// Design system border radius tokens from Figma
xsmall: "0.25rem", // 4px
small: "0.5rem", // 8px
medium: "0.75rem", // 12px
large: "1rem", // 16px
xlarge: "1.25rem", // 20px
"2xlarge": "1.5rem", // 24px
full: "9999px", // For pill buttons
// Legacy values - kept for backward compatibility
xsmall: "0.25rem",
small: "0.5rem",
medium: "0.75rem",
large: "1rem",
xlarge: "1.25rem",
"2xlarge": "1.5rem",
full: "9999px",
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
@@ -136,16 +126,28 @@ const config = {
},
keyframes: {
"accordion-down": {
from: { height: "0" },
to: { height: "var(--radix-accordion-content-height)" },
from: {
height: "0",
},
to: {
height: "var(--radix-accordion-content-height)",
},
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: "0" },
from: {
height: "var(--radix-accordion-content-height)",
},
to: {
height: "0",
},
},
"fade-in": {
"0%": { opacity: "0" }, // Start with opacity 0
"100%": { opacity: "1" }, // End with opacity 1
"0%": {
opacity: "0",
},
"100%": {
opacity: "1",
},
},
},
animation: {