mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-01-21 04:57:58 -05:00
Compare commits
3 Commits
testing-cl
...
fix/run-mo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
383e22da19 | ||
|
|
8957ecb099 | ||
|
|
d2305d047d |
@@ -108,6 +108,9 @@ class CredentialsMetaResponse(BaseModel):
|
|||||||
host: str | None = Field(
|
host: str | None = Field(
|
||||||
default=None, description="Host pattern for host-scoped credentials"
|
default=None, description="Host pattern for host-scoped credentials"
|
||||||
)
|
)
|
||||||
|
is_system: bool = Field(
|
||||||
|
default=False, description="Whether this is a system-managed credential"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{provider}/callback", summary="Exchange OAuth code for tokens")
|
@router.post("/{provider}/callback", summary="Exchange OAuth code for tokens")
|
||||||
@@ -175,6 +178,8 @@ async def callback(
|
|||||||
f"Successfully processed OAuth callback for user {user_id} "
|
f"Successfully processed OAuth callback for user {user_id} "
|
||||||
f"and provider {provider.value}"
|
f"and provider {provider.value}"
|
||||||
)
|
)
|
||||||
|
from backend.integrations.credentials_store import is_system_credential
|
||||||
|
|
||||||
return CredentialsMetaResponse(
|
return CredentialsMetaResponse(
|
||||||
id=credentials.id,
|
id=credentials.id,
|
||||||
provider=credentials.provider,
|
provider=credentials.provider,
|
||||||
@@ -185,6 +190,7 @@ async def callback(
|
|||||||
host=(
|
host=(
|
||||||
credentials.host if isinstance(credentials, HostScopedCredentials) else None
|
credentials.host if isinstance(credentials, HostScopedCredentials) else None
|
||||||
),
|
),
|
||||||
|
is_system=is_system_credential(credentials.id),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -192,6 +198,8 @@ async def callback(
|
|||||||
async def list_credentials(
|
async def list_credentials(
|
||||||
user_id: Annotated[str, Security(get_user_id)],
|
user_id: Annotated[str, Security(get_user_id)],
|
||||||
) -> list[CredentialsMetaResponse]:
|
) -> list[CredentialsMetaResponse]:
|
||||||
|
from backend.integrations.credentials_store import is_system_credential
|
||||||
|
|
||||||
credentials = await creds_manager.store.get_all_creds(user_id)
|
credentials = await creds_manager.store.get_all_creds(user_id)
|
||||||
return [
|
return [
|
||||||
CredentialsMetaResponse(
|
CredentialsMetaResponse(
|
||||||
@@ -202,6 +210,7 @@ async def list_credentials(
|
|||||||
scopes=cred.scopes if isinstance(cred, OAuth2Credentials) else None,
|
scopes=cred.scopes if isinstance(cred, OAuth2Credentials) else None,
|
||||||
username=cred.username if isinstance(cred, OAuth2Credentials) else None,
|
username=cred.username if isinstance(cred, OAuth2Credentials) else None,
|
||||||
host=cred.host if isinstance(cred, HostScopedCredentials) else None,
|
host=cred.host if isinstance(cred, HostScopedCredentials) else None,
|
||||||
|
is_system=is_system_credential(cred.id),
|
||||||
)
|
)
|
||||||
for cred in credentials
|
for cred in credentials
|
||||||
]
|
]
|
||||||
@@ -214,6 +223,8 @@ async def list_credentials_by_provider(
|
|||||||
],
|
],
|
||||||
user_id: Annotated[str, Security(get_user_id)],
|
user_id: Annotated[str, Security(get_user_id)],
|
||||||
) -> list[CredentialsMetaResponse]:
|
) -> list[CredentialsMetaResponse]:
|
||||||
|
from backend.integrations.credentials_store import is_system_credential
|
||||||
|
|
||||||
credentials = await creds_manager.store.get_creds_by_provider(user_id, provider)
|
credentials = await creds_manager.store.get_creds_by_provider(user_id, provider)
|
||||||
return [
|
return [
|
||||||
CredentialsMetaResponse(
|
CredentialsMetaResponse(
|
||||||
@@ -224,6 +235,7 @@ async def list_credentials_by_provider(
|
|||||||
scopes=cred.scopes if isinstance(cred, OAuth2Credentials) else None,
|
scopes=cred.scopes if isinstance(cred, OAuth2Credentials) else None,
|
||||||
username=cred.username if isinstance(cred, OAuth2Credentials) else None,
|
username=cred.username if isinstance(cred, OAuth2Credentials) else None,
|
||||||
host=cred.host if isinstance(cred, HostScopedCredentials) else None,
|
host=cred.host if isinstance(cred, HostScopedCredentials) else None,
|
||||||
|
is_system=is_system_credential(cred.id),
|
||||||
)
|
)
|
||||||
for cred in credentials
|
for cred in credentials
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -245,6 +245,13 @@ DEFAULT_CREDENTIALS = [
|
|||||||
webshare_proxy_credentials,
|
webshare_proxy_credentials,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
SYSTEM_CREDENTIAL_IDS = {cred.id 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
|
||||||
|
|
||||||
|
|
||||||
class IntegrationCredentialsStore:
|
class IntegrationCredentialsStore:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
|||||||
@@ -3,6 +3,13 @@ import { withSentryConfig } from "@sentry/nextjs";
|
|||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
productionBrowserSourceMaps: true,
|
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: {
|
experimental: {
|
||||||
serverActions: {
|
serverActions: {
|
||||||
bodySizeLimit: "256mb",
|
bodySizeLimit: "256mb",
|
||||||
|
|||||||
@@ -117,6 +117,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@chromatic-com/storybook": "4.1.2",
|
"@chromatic-com/storybook": "4.1.2",
|
||||||
|
"@opentelemetry/instrumentation": "0.209.0",
|
||||||
"@playwright/test": "1.56.1",
|
"@playwright/test": "1.56.1",
|
||||||
"@storybook/addon-a11y": "9.1.5",
|
"@storybook/addon-a11y": "9.1.5",
|
||||||
"@storybook/addon-docs": "9.1.5",
|
"@storybook/addon-docs": "9.1.5",
|
||||||
@@ -140,6 +141,7 @@
|
|||||||
"eslint": "8.57.1",
|
"eslint": "8.57.1",
|
||||||
"eslint-config-next": "15.5.7",
|
"eslint-config-next": "15.5.7",
|
||||||
"eslint-plugin-storybook": "9.1.5",
|
"eslint-plugin-storybook": "9.1.5",
|
||||||
|
"import-in-the-middle": "2.0.2",
|
||||||
"msw": "2.11.6",
|
"msw": "2.11.6",
|
||||||
"msw-storybook-addon": "2.0.6",
|
"msw-storybook-addon": "2.0.6",
|
||||||
"orval": "7.13.0",
|
"orval": "7.13.0",
|
||||||
@@ -147,7 +149,7 @@
|
|||||||
"postcss": "8.5.6",
|
"postcss": "8.5.6",
|
||||||
"prettier": "3.6.2",
|
"prettier": "3.6.2",
|
||||||
"prettier-plugin-tailwindcss": "0.7.1",
|
"prettier-plugin-tailwindcss": "0.7.1",
|
||||||
"require-in-the-middle": "7.5.2",
|
"require-in-the-middle": "8.0.1",
|
||||||
"storybook": "9.1.5",
|
"storybook": "9.1.5",
|
||||||
"tailwindcss": "3.4.17",
|
"tailwindcss": "3.4.17",
|
||||||
"typescript": "5.9.3"
|
"typescript": "5.9.3"
|
||||||
|
|||||||
59
autogpt_platform/frontend/pnpm-lock.yaml
generated
59
autogpt_platform/frontend/pnpm-lock.yaml
generated
@@ -270,6 +270,9 @@ importers:
|
|||||||
'@chromatic-com/storybook':
|
'@chromatic-com/storybook':
|
||||||
specifier: 4.1.2
|
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))
|
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':
|
'@playwright/test':
|
||||||
specifier: 1.56.1
|
specifier: 1.56.1
|
||||||
version: 1.56.1
|
version: 1.56.1
|
||||||
@@ -339,6 +342,9 @@ importers:
|
|||||||
eslint-plugin-storybook:
|
eslint-plugin-storybook:
|
||||||
specifier: 9.1.5
|
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)
|
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:
|
msw:
|
||||||
specifier: 2.11.6
|
specifier: 2.11.6
|
||||||
version: 2.11.6(@types/node@24.10.0)(typescript@5.9.3)
|
version: 2.11.6(@types/node@24.10.0)(typescript@5.9.3)
|
||||||
@@ -361,8 +367,8 @@ importers:
|
|||||||
specifier: 0.7.1
|
specifier: 0.7.1
|
||||||
version: 0.7.1(prettier@3.6.2)
|
version: 0.7.1(prettier@3.6.2)
|
||||||
require-in-the-middle:
|
require-in-the-middle:
|
||||||
specifier: 7.5.2
|
specifier: 8.0.1
|
||||||
version: 7.5.2
|
version: 8.0.1
|
||||||
storybook:
|
storybook:
|
||||||
specifier: 9.1.5
|
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)
|
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)
|
||||||
@@ -1547,6 +1553,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-CjruKY9V6NMssL/T1kAFgzosF1v9o6oeN+aX5JB/C/xPNtmgIJqcXHG7fA82Ou1zCpWGl4lROQUKwUNE1pMCyg==}
|
resolution: {integrity: sha512-CjruKY9V6NMssL/T1kAFgzosF1v9o6oeN+aX5JB/C/xPNtmgIJqcXHG7fA82Ou1zCpWGl4lROQUKwUNE1pMCyg==}
|
||||||
engines: {node: '>=8.0.0'}
|
engines: {node: '>=8.0.0'}
|
||||||
|
|
||||||
|
'@opentelemetry/api-logs@0.209.0':
|
||||||
|
resolution: {integrity: sha512-xomnUNi7TiAGtOgs0tb54LyrjRZLu9shJGGwkcN7NgtiPYOpNnKLkRJtzZvTjD/w6knSZH9sFZcUSUovYOPg6A==}
|
||||||
|
engines: {node: '>=8.0.0'}
|
||||||
|
|
||||||
'@opentelemetry/api@1.9.0':
|
'@opentelemetry/api@1.9.0':
|
||||||
resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==}
|
resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==}
|
||||||
engines: {node: '>=8.0.0'}
|
engines: {node: '>=8.0.0'}
|
||||||
@@ -1701,6 +1711,12 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@opentelemetry/api': ^1.3.0
|
'@opentelemetry/api': ^1.3.0
|
||||||
|
|
||||||
|
'@opentelemetry/instrumentation@0.209.0':
|
||||||
|
resolution: {integrity: sha512-Cwe863ojTCnFlxVuuhG7s6ODkAOzKsAEthKAcI4MDRYz1OmGWYnmSl4X2pbyS+hBxVTdvfZePfoEA01IjqcEyw==}
|
||||||
|
engines: {node: ^18.19.0 || >=20.6.0}
|
||||||
|
peerDependencies:
|
||||||
|
'@opentelemetry/api': ^1.3.0
|
||||||
|
|
||||||
'@opentelemetry/redis-common@0.38.2':
|
'@opentelemetry/redis-common@0.38.2':
|
||||||
resolution: {integrity: sha512-1BCcU93iwSRZvDAgwUxC/DV4T/406SkMfxGqu5ojc3AvNI+I9GhV7v0J1HljsczuuhcnFLYqD5VmwVXfCGHzxA==}
|
resolution: {integrity: sha512-1BCcU93iwSRZvDAgwUxC/DV4T/406SkMfxGqu5ojc3AvNI+I9GhV7v0J1HljsczuuhcnFLYqD5VmwVXfCGHzxA==}
|
||||||
engines: {node: ^18.19.0 || >=20.6.0}
|
engines: {node: ^18.19.0 || >=20.6.0}
|
||||||
@@ -4957,8 +4973,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
|
resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
import-in-the-middle@2.0.1:
|
import-in-the-middle@2.0.2:
|
||||||
resolution: {integrity: sha512-bruMpJ7xz+9jwGzrwEhWgvRrlKRYCRDBrfU+ur3FcasYXLJDxTruJ//8g2Noj+QFyRBeqbpj8Bhn4Fbw6HjvhA==}
|
resolution: {integrity: sha512-qet/hkGt3EbNGVtbDfPu0BM+tCqBS8wT1SYrstPaDKoWtshsC6licOemz7DVtpBEyvDNzo8UTBf9/GwWuSDZ9w==}
|
||||||
|
|
||||||
imurmurhash@0.1.4:
|
imurmurhash@0.1.4:
|
||||||
resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
|
resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
|
||||||
@@ -6502,10 +6518,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
|
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
|
||||||
engines: {node: '>=0.10.0'}
|
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:
|
require-in-the-middle@8.0.1:
|
||||||
resolution: {integrity: sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==}
|
resolution: {integrity: sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==}
|
||||||
engines: {node: '>=9.3.0 || >=8.10.0 <9.0.0'}
|
engines: {node: '>=9.3.0 || >=8.10.0 <9.0.0'}
|
||||||
@@ -8720,6 +8732,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@opentelemetry/api': 1.9.0
|
'@opentelemetry/api': 1.9.0
|
||||||
|
|
||||||
|
'@opentelemetry/api-logs@0.209.0':
|
||||||
|
dependencies:
|
||||||
|
'@opentelemetry/api': 1.9.0
|
||||||
|
|
||||||
'@opentelemetry/api@1.9.0': {}
|
'@opentelemetry/api@1.9.0': {}
|
||||||
|
|
||||||
'@opentelemetry/context-async-hooks@2.2.0(@opentelemetry/api@1.9.0)':
|
'@opentelemetry/context-async-hooks@2.2.0(@opentelemetry/api@1.9.0)':
|
||||||
@@ -8920,7 +8936,16 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@opentelemetry/api': 1.9.0
|
'@opentelemetry/api': 1.9.0
|
||||||
'@opentelemetry/api-logs': 0.208.0
|
'@opentelemetry/api-logs': 0.208.0
|
||||||
import-in-the-middle: 2.0.1
|
import-in-the-middle: 2.0.2
|
||||||
|
require-in-the-middle: 8.0.1
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
'@opentelemetry/instrumentation@0.209.0(@opentelemetry/api@1.9.0)':
|
||||||
|
dependencies:
|
||||||
|
'@opentelemetry/api': 1.9.0
|
||||||
|
'@opentelemetry/api-logs': 0.209.0
|
||||||
|
import-in-the-middle: 2.0.2
|
||||||
require-in-the-middle: 8.0.1
|
require-in-the-middle: 8.0.1
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -9100,7 +9125,7 @@ snapshots:
|
|||||||
'@prisma/instrumentation@6.19.0(@opentelemetry/api@1.9.0)':
|
'@prisma/instrumentation@6.19.0(@opentelemetry/api@1.9.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@opentelemetry/api': 1.9.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:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -9944,7 +9969,7 @@ snapshots:
|
|||||||
'@opentelemetry/semantic-conventions': 1.38.0
|
'@opentelemetry/semantic-conventions': 1.38.0
|
||||||
'@sentry/core': 10.27.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)
|
'@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:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -9983,7 +10008,7 @@ snapshots:
|
|||||||
'@sentry/core': 10.27.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.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/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)
|
'@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
|
minimatch: 9.0.5
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -12792,7 +12817,7 @@ snapshots:
|
|||||||
parent-module: 1.0.1
|
parent-module: 1.0.1
|
||||||
resolve-from: 4.0.0
|
resolve-from: 4.0.0
|
||||||
|
|
||||||
import-in-the-middle@2.0.1:
|
import-in-the-middle@2.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
acorn: 8.15.0
|
acorn: 8.15.0
|
||||||
acorn-import-attributes: 1.9.5(acorn@8.15.0)
|
acorn-import-attributes: 1.9.5(acorn@8.15.0)
|
||||||
@@ -14631,14 +14656,6 @@ snapshots:
|
|||||||
|
|
||||||
require-from-string@2.0.2: {}
|
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:
|
require-in-the-middle@8.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
|
|||||||
@@ -1,32 +1,38 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Button } from "@/components/atoms/Button/Button";
|
import { Button } from "@/components/atoms/Button/Button";
|
||||||
|
import { Text } from "@/components/atoms/Text/Text";
|
||||||
|
import { PublishAgentModal } from "@/components/contextual/PublishAgentModal/PublishAgentModal";
|
||||||
|
import {
|
||||||
|
Alert,
|
||||||
|
AlertDescription,
|
||||||
|
AlertTitle,
|
||||||
|
} from "@/components/molecules/Alert/Alert";
|
||||||
import { Breadcrumbs } from "@/components/molecules/Breadcrumbs/Breadcrumbs";
|
import { Breadcrumbs } from "@/components/molecules/Breadcrumbs/Breadcrumbs";
|
||||||
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
|
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { PlusIcon } from "@phosphor-icons/react";
|
import { PlusIcon } from "@phosphor-icons/react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { RunAgentModal } from "./components/modals/RunAgentModal/RunAgentModal";
|
|
||||||
import { useMarketplaceUpdate } from "./hooks/useMarketplaceUpdate";
|
|
||||||
import { AgentVersionChangelog } from "./components/AgentVersionChangelog";
|
import { AgentVersionChangelog } from "./components/AgentVersionChangelog";
|
||||||
import { MarketplaceBanners } from "@/components/contextual/MarketplaceBanners/MarketplaceBanners";
|
import { AgentSettingsModal } from "./components/modals/AgentSettingsModal/AgentSettingsModal";
|
||||||
import { PublishAgentModal } from "@/components/contextual/PublishAgentModal/PublishAgentModal";
|
import { RunAgentModal } from "./components/modals/RunAgentModal/RunAgentModal";
|
||||||
import { AgentSettingsButton } from "./components/other/AgentSettingsButton";
|
|
||||||
import { AgentRunsLoading } from "./components/other/AgentRunsLoading";
|
import { AgentRunsLoading } from "./components/other/AgentRunsLoading";
|
||||||
import { EmptySchedules } from "./components/other/EmptySchedules";
|
import { EmptySchedules } from "./components/other/EmptySchedules";
|
||||||
import { EmptyTasks } from "./components/other/EmptyTasks";
|
import { EmptyTasks } from "./components/other/EmptyTasks";
|
||||||
import { EmptyTemplates } from "./components/other/EmptyTemplates";
|
import { EmptyTemplates } from "./components/other/EmptyTemplates";
|
||||||
import { EmptyTriggers } from "./components/other/EmptyTriggers";
|
import { EmptyTriggers } from "./components/other/EmptyTriggers";
|
||||||
|
import { MarketplaceBanners } from "./components/other/MarketplaceBanners";
|
||||||
import { SectionWrap } from "./components/other/SectionWrap";
|
import { SectionWrap } from "./components/other/SectionWrap";
|
||||||
import { LoadingSelectedContent } from "./components/selected-views/LoadingSelectedContent";
|
import { LoadingSelectedContent } from "./components/selected-views/LoadingSelectedContent";
|
||||||
import { SelectedRunView } from "./components/selected-views/SelectedRunView/SelectedRunView";
|
import { SelectedRunView } from "./components/selected-views/SelectedRunView/SelectedRunView";
|
||||||
import { SelectedScheduleView } from "./components/selected-views/SelectedScheduleView/SelectedScheduleView";
|
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 { SelectedTemplateView } from "./components/selected-views/SelectedTemplateView/SelectedTemplateView";
|
||||||
import { SelectedTriggerView } from "./components/selected-views/SelectedTriggerView/SelectedTriggerView";
|
import { SelectedTriggerView } from "./components/selected-views/SelectedTriggerView/SelectedTriggerView";
|
||||||
import { SelectedViewLayout } from "./components/selected-views/SelectedViewLayout";
|
import { SelectedViewLayout } from "./components/selected-views/SelectedViewLayout";
|
||||||
import { SidebarRunsList } from "./components/sidebar/SidebarRunsList/SidebarRunsList";
|
import { SidebarRunsList } from "./components/sidebar/SidebarRunsList/SidebarRunsList";
|
||||||
import { AGENT_LIBRARY_SECTION_PADDING_X } from "./helpers";
|
import { AGENT_LIBRARY_SECTION_PADDING_X } from "./helpers";
|
||||||
|
import { useAgentMissingCredentials } from "./hooks/useAgentMissingCredentials";
|
||||||
|
import { useMarketplaceUpdate } from "./hooks/useMarketplaceUpdate";
|
||||||
import { useNewAgentLibraryView } from "./useNewAgentLibraryView";
|
import { useNewAgentLibraryView } from "./useNewAgentLibraryView";
|
||||||
|
|
||||||
export function NewAgentLibraryView() {
|
export function NewAgentLibraryView() {
|
||||||
@@ -45,7 +51,6 @@ export function NewAgentLibraryView() {
|
|||||||
handleSelectRun,
|
handleSelectRun,
|
||||||
handleCountsChange,
|
handleCountsChange,
|
||||||
handleClearSelectedRun,
|
handleClearSelectedRun,
|
||||||
handleSelectSettings,
|
|
||||||
onRunInitiated,
|
onRunInitiated,
|
||||||
onTriggerSetup,
|
onTriggerSetup,
|
||||||
onScheduleCreated,
|
onScheduleCreated,
|
||||||
@@ -63,6 +68,10 @@ export function NewAgentLibraryView() {
|
|||||||
} = useMarketplaceUpdate({ agent });
|
} = useMarketplaceUpdate({ agent });
|
||||||
|
|
||||||
const [changelogOpen, setChangelogOpen] = useState(false);
|
const [changelogOpen, setChangelogOpen] = useState(false);
|
||||||
|
const [settingsModalOpen, setSettingsModalOpen] = useState(false);
|
||||||
|
|
||||||
|
const { hasMissingCredentials, isLoading: isLoadingCredentials } =
|
||||||
|
useAgentMissingCredentials(agent);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (agent) {
|
if (agent) {
|
||||||
@@ -137,13 +146,33 @@ export function NewAgentLibraryView() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex h-full flex-col">
|
<div className="flex h-full flex-col">
|
||||||
<div className="mx-6 pt-4">
|
<div className="mx-6 flex flex-col gap-4 pt-4">
|
||||||
<Breadcrumbs
|
<div className="flex items-center justify-between">
|
||||||
items={[
|
<Breadcrumbs
|
||||||
{ name: "My Library", link: "/library" },
|
items={[
|
||||||
{ name: agent.name, link: `/library/agents/${agentId}` },
|
{ name: "My Library", link: "/library" },
|
||||||
]}
|
{ name: agent.name, link: `/library/agents/${agentId}` },
|
||||||
/>
|
]}
|
||||||
|
/>
|
||||||
|
<AgentSettingsModal agent={agent} />
|
||||||
|
</div>
|
||||||
|
{hasMissingCredentials && !isLoadingCredentials && (
|
||||||
|
<Alert variant="warning">
|
||||||
|
<AlertTitle>Missing credentials</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
<Text variant="small" className="text-zinc-800">
|
||||||
|
This agent requires credentials that are not configured.{" "}
|
||||||
|
<button
|
||||||
|
onClick={() => setSettingsModalOpen(true)}
|
||||||
|
className="font-medium underline hover:no-underline"
|
||||||
|
>
|
||||||
|
Configure credentials
|
||||||
|
</button>{" "}
|
||||||
|
to run tasks.
|
||||||
|
</Text>
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex min-h-0 flex-1">
|
<div className="flex min-h-0 flex-1">
|
||||||
<EmptyTasks
|
<EmptyTasks
|
||||||
@@ -154,6 +183,13 @@ export function NewAgentLibraryView() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{agent && (
|
||||||
|
<AgentSettingsModal
|
||||||
|
agent={agent}
|
||||||
|
controlledOpen={settingsModalOpen}
|
||||||
|
onOpenChange={setSettingsModalOpen}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{renderPublishAgentModal()}
|
{renderPublishAgentModal()}
|
||||||
{renderVersionChangelog()}
|
{renderVersionChangelog()}
|
||||||
</>
|
</>
|
||||||
@@ -164,37 +200,49 @@ export function NewAgentLibraryView() {
|
|||||||
<>
|
<>
|
||||||
<div className="mx-4 grid h-full grid-cols-1 gap-0 pt-3 md:ml-4 md:mr-0 md:gap-4 lg:grid-cols-[25%_70%]">
|
<div className="mx-4 grid h-full grid-cols-1 gap-0 pt-3 md:ml-4 md:mr-0 md:gap-4 lg:grid-cols-[25%_70%]">
|
||||||
<SectionWrap className="mb-3 block">
|
<SectionWrap className="mb-3 block">
|
||||||
|
{hasMissingCredentials && !isLoadingCredentials && (
|
||||||
|
<div className={cn("mb-4", AGENT_LIBRARY_SECTION_PADDING_X)}>
|
||||||
|
<Alert variant="warning">
|
||||||
|
<AlertTitle>Missing credentials</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
<Text variant="small" className="text-zinc-800">
|
||||||
|
This agent requires credentials that are not configured.{" "}
|
||||||
|
<button
|
||||||
|
onClick={() => setSettingsModalOpen(true)}
|
||||||
|
className="font-medium underline hover:no-underline"
|
||||||
|
>
|
||||||
|
Configure credentials
|
||||||
|
</button>{" "}
|
||||||
|
to run tasks.
|
||||||
|
</Text>
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"border-b border-zinc-100 pb-5",
|
"border-b border-zinc-100 pb-5",
|
||||||
AGENT_LIBRARY_SECTION_PADDING_X,
|
AGENT_LIBRARY_SECTION_PADDING_X,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<RunAgentModal
|
||||||
<RunAgentModal
|
triggerSlot={
|
||||||
triggerSlot={
|
<Button
|
||||||
<Button
|
variant="primary"
|
||||||
variant="primary"
|
size="large"
|
||||||
size="large"
|
className="w-full"
|
||||||
className="flex-1"
|
disabled={isTemplateLoading && activeTab === "templates"}
|
||||||
disabled={isTemplateLoading && activeTab === "templates"}
|
>
|
||||||
>
|
<PlusIcon size={20} /> New task
|
||||||
<PlusIcon size={20} /> New task
|
</Button>
|
||||||
</Button>
|
}
|
||||||
}
|
agent={agent}
|
||||||
agent={agent}
|
onRunCreated={onRunInitiated}
|
||||||
onRunCreated={onRunInitiated}
|
onScheduleCreated={onScheduleCreated}
|
||||||
onScheduleCreated={onScheduleCreated}
|
onTriggerSetup={onTriggerSetup}
|
||||||
onTriggerSetup={onTriggerSetup}
|
initialInputValues={activeTemplate?.inputs}
|
||||||
initialInputValues={activeTemplate?.inputs}
|
initialInputCredentials={activeTemplate?.credentials}
|
||||||
initialInputCredentials={activeTemplate?.credentials}
|
/>
|
||||||
/>
|
|
||||||
<AgentSettingsButton
|
|
||||||
agent={agent}
|
|
||||||
onSelectSettings={handleSelectSettings}
|
|
||||||
selected={activeItem === "settings"}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SidebarRunsList
|
<SidebarRunsList
|
||||||
@@ -208,12 +256,7 @@ export function NewAgentLibraryView() {
|
|||||||
</SectionWrap>
|
</SectionWrap>
|
||||||
|
|
||||||
{activeItem ? (
|
{activeItem ? (
|
||||||
activeItem === "settings" ? (
|
activeTab === "scheduled" ? (
|
||||||
<SelectedSettingsView
|
|
||||||
agent={agent}
|
|
||||||
onClearSelectedRun={handleClearSelectedRun}
|
|
||||||
/>
|
|
||||||
) : activeTab === "scheduled" ? (
|
|
||||||
<SelectedScheduleView
|
<SelectedScheduleView
|
||||||
agent={agent}
|
agent={agent}
|
||||||
scheduleId={activeItem}
|
scheduleId={activeItem}
|
||||||
@@ -246,8 +289,6 @@ export function NewAgentLibraryView() {
|
|||||||
onSelectRun={handleSelectRun}
|
onSelectRun={handleSelectRun}
|
||||||
onClearSelectedRun={handleClearSelectedRun}
|
onClearSelectedRun={handleClearSelectedRun}
|
||||||
banner={renderMarketplaceUpdateBanner()}
|
banner={renderMarketplaceUpdateBanner()}
|
||||||
onSelectSettings={handleSelectSettings}
|
|
||||||
selectedSettings={activeItem === "settings"}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
) : sidebarLoading ? (
|
) : sidebarLoading ? (
|
||||||
@@ -287,6 +328,13 @@ export function NewAgentLibraryView() {
|
|||||||
</SelectedViewLayout>
|
</SelectedViewLayout>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{agent && (
|
||||||
|
<AgentSettingsModal
|
||||||
|
agent={agent}
|
||||||
|
controlledOpen={settingsModalOpen}
|
||||||
|
onOpenChange={setSettingsModalOpen}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{renderPublishAgentModal()}
|
{renderPublishAgentModal()}
|
||||||
{renderVersionChangelog()}
|
{renderVersionChangelog()}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
|
|||||||
import { Text } from "@/components/atoms/Text/Text";
|
import { Text } from "@/components/atoms/Text/Text";
|
||||||
import type { CredentialsMetaInput } from "@/lib/autogpt-server-api/types";
|
import type { CredentialsMetaInput } from "@/lib/autogpt-server-api/types";
|
||||||
import { CredentialsInput } from "../CredentialsInputs/CredentialsInputs";
|
import { CredentialsInput } from "../CredentialsInputs/CredentialsInputs";
|
||||||
|
import { isSystemCredential } from "../CredentialsInputs/helpers";
|
||||||
import { RunAgentInputs } from "../RunAgentInputs/RunAgentInputs";
|
import { RunAgentInputs } from "../RunAgentInputs/RunAgentInputs";
|
||||||
import { getAgentCredentialsFields, getAgentInputFields } from "./helpers";
|
import { getAgentCredentialsFields, getAgentInputFields } from "./helpers";
|
||||||
|
|
||||||
@@ -71,6 +72,7 @@ export function AgentInputsReadOnly({
|
|||||||
{credentialFieldEntries.map(([key, inputSubSchema]) => {
|
{credentialFieldEntries.map(([key, inputSubSchema]) => {
|
||||||
const credential = credentialInputs![key];
|
const credential = credentialInputs![key];
|
||||||
if (!credential) return null;
|
if (!credential) return null;
|
||||||
|
if (isSystemCredential(credential)) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CredentialsInput
|
<CredentialsInput
|
||||||
|
|||||||
@@ -0,0 +1,131 @@
|
|||||||
|
"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 { useMemo, useState } from "react";
|
||||||
|
import { useAgentSystemCredentials } from "../../../hooks/useAgentSystemCredentials";
|
||||||
|
import { SystemCredentialRow } from "../../selected-views/SelectedSettingsView/components/SystemCredentialRow";
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
const { hasSystemCredentials, systemCredentials } =
|
||||||
|
useAgentSystemCredentials(agent);
|
||||||
|
|
||||||
|
// Only show credential fields that have system credentials
|
||||||
|
const credentialFieldsWithSystemCreds = useMemo(() => {
|
||||||
|
return systemCredentials.map((item) => ({
|
||||||
|
fieldKey: item.key,
|
||||||
|
schema: item.schema,
|
||||||
|
systemCredential: item.credential,
|
||||||
|
}));
|
||||||
|
}, [systemCredentials]);
|
||||||
|
|
||||||
|
const hasAnySettings = hasHITLBlocks || hasSystemCredentials;
|
||||||
|
|
||||||
|
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">
|
||||||
|
{hasHITLBlocks && (
|
||||||
|
<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>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{hasSystemCredentials && (
|
||||||
|
<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>
|
||||||
|
<Text variant="large-semibold">System Credentials</Text>
|
||||||
|
<Text variant="body" className="mt-1 text-muted-foreground">
|
||||||
|
These credentials are managed by AutoGPT and used by the agent
|
||||||
|
to access various services. You can switch to your own
|
||||||
|
credentials if preferred.
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<div className="w-full space-y-4">
|
||||||
|
{credentialFieldsWithSystemCreds.map(
|
||||||
|
({ fieldKey, schema, systemCredential }) => (
|
||||||
|
<SystemCredentialRow
|
||||||
|
key={fieldKey}
|
||||||
|
credentialKey={fieldKey}
|
||||||
|
agentId={agent.id.toString()}
|
||||||
|
schema={schema}
|
||||||
|
systemCredential={systemCredential}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!hasAnySettings && (
|
||||||
|
<div className="py-6">
|
||||||
|
<Text variant="body" className="text-muted-foreground">
|
||||||
|
This agent doesn't have any configurable settings.
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Dialog.Content>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -10,11 +10,10 @@ import { toDisplayName } from "@/providers/agent-credentials/helper";
|
|||||||
import { APIKeyCredentialsModal } from "./components/APIKeyCredentialsModal/APIKeyCredentialsModal";
|
import { APIKeyCredentialsModal } from "./components/APIKeyCredentialsModal/APIKeyCredentialsModal";
|
||||||
import { CredentialRow } from "./components/CredentialRow/CredentialRow";
|
import { CredentialRow } from "./components/CredentialRow/CredentialRow";
|
||||||
import { CredentialsSelect } from "./components/CredentialsSelect/CredentialsSelect";
|
import { CredentialsSelect } from "./components/CredentialsSelect/CredentialsSelect";
|
||||||
import { DeleteConfirmationModal } from "./components/DeleteConfirmationModal/DeleteConfirmationModal";
|
|
||||||
import { HostScopedCredentialsModal } from "./components/HotScopedCredentialsModal/HotScopedCredentialsModal";
|
import { HostScopedCredentialsModal } from "./components/HotScopedCredentialsModal/HotScopedCredentialsModal";
|
||||||
import { OAuthFlowWaitingModal } from "./components/OAuthWaitingModal/OAuthWaitingModal";
|
import { OAuthFlowWaitingModal } from "./components/OAuthWaitingModal/OAuthWaitingModal";
|
||||||
import { PasswordCredentialsModal } from "./components/PasswordCredentialsModal/PasswordCredentialsModal";
|
import { PasswordCredentialsModal } from "./components/PasswordCredentialsModal/PasswordCredentialsModal";
|
||||||
import { getCredentialDisplayName } from "./helpers";
|
import { isSystemCredential } from "./helpers";
|
||||||
import {
|
import {
|
||||||
CredentialsInputState,
|
CredentialsInputState,
|
||||||
useCredentialsInput,
|
useCredentialsInput,
|
||||||
@@ -37,6 +36,7 @@ type Props = {
|
|||||||
isOptional?: boolean;
|
isOptional?: boolean;
|
||||||
showTitle?: boolean;
|
showTitle?: boolean;
|
||||||
variant?: "default" | "node";
|
variant?: "default" | "node";
|
||||||
|
allowSystemCredentials?: boolean; // Allow system credentials (for settings only)
|
||||||
};
|
};
|
||||||
|
|
||||||
export function CredentialsInput({
|
export function CredentialsInput({
|
||||||
@@ -50,6 +50,7 @@ export function CredentialsInput({
|
|||||||
isOptional = false,
|
isOptional = false,
|
||||||
showTitle = true,
|
showTitle = true,
|
||||||
variant = "default",
|
variant = "default",
|
||||||
|
allowSystemCredentials = false,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const hookData = useCredentialsInput({
|
const hookData = useCredentialsInput({
|
||||||
schema,
|
schema,
|
||||||
@@ -59,6 +60,7 @@ export function CredentialsInput({
|
|||||||
onLoaded,
|
onLoaded,
|
||||||
readOnly,
|
readOnly,
|
||||||
isOptional,
|
isOptional,
|
||||||
|
allowSystemCredentials,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!isLoaded(hookData)) {
|
if (!isLoaded(hookData)) {
|
||||||
@@ -79,21 +81,22 @@ export function CredentialsInput({
|
|||||||
isHostScopedCredentialsModalOpen,
|
isHostScopedCredentialsModalOpen,
|
||||||
isOAuth2FlowInProgress,
|
isOAuth2FlowInProgress,
|
||||||
oAuthPopupController,
|
oAuthPopupController,
|
||||||
credentialToDelete,
|
|
||||||
deleteCredentialsMutation,
|
|
||||||
actionButtonText,
|
actionButtonText,
|
||||||
setAPICredentialsModalOpen,
|
setAPICredentialsModalOpen,
|
||||||
setUserPasswordCredentialsModalOpen,
|
setUserPasswordCredentialsModalOpen,
|
||||||
setHostScopedCredentialsModalOpen,
|
setHostScopedCredentialsModalOpen,
|
||||||
setCredentialToDelete,
|
|
||||||
handleActionButtonClick,
|
handleActionButtonClick,
|
||||||
handleCredentialSelect,
|
handleCredentialSelect,
|
||||||
handleDeleteCredential,
|
|
||||||
handleDeleteConfirm,
|
|
||||||
} = hookData;
|
} = hookData;
|
||||||
|
|
||||||
const displayName = toDisplayName(provider);
|
const displayName = toDisplayName(provider);
|
||||||
const hasCredentialsToShow = credentialsToShow.length > 0;
|
const hasCredentialsToShow = credentialsToShow.length > 0;
|
||||||
|
const selectedCredentialIsSystem =
|
||||||
|
selectedCredential && isSystemCredential(selectedCredential);
|
||||||
|
|
||||||
|
if (readOnly && selectedCredentialIsSystem) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("mb-6", className)}>
|
<div className={cn("mb-6", className)}>
|
||||||
@@ -137,15 +140,6 @@ export function CredentialsInput({
|
|||||||
provider={provider}
|
provider={provider}
|
||||||
displayName={displayName}
|
displayName={displayName}
|
||||||
onSelect={() => handleCredentialSelect(credential.id)}
|
onSelect={() => handleCredentialSelect(credential.id)}
|
||||||
onDelete={() =>
|
|
||||||
handleDeleteCredential({
|
|
||||||
id: credential.id,
|
|
||||||
title: getCredentialDisplayName(
|
|
||||||
credential,
|
|
||||||
displayName,
|
|
||||||
),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -229,13 +223,6 @@ export function CredentialsInput({
|
|||||||
Error: {oAuthError}
|
Error: {oAuthError}
|
||||||
</Text>
|
</Text>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<DeleteConfirmationModal
|
|
||||||
credentialToDelete={credentialToDelete}
|
|
||||||
isDeleting={deleteCredentialsMutation.isPending}
|
|
||||||
onClose={() => setCredentialToDelete(null)}
|
|
||||||
onConfirm={handleDeleteConfirm}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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 {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormDescription,
|
FormDescription,
|
||||||
FormField,
|
FormField,
|
||||||
} from "@/components/__legacy__/ui/form";
|
} 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 {
|
import {
|
||||||
BlockIOCredentialsSubSchema,
|
BlockIOCredentialsSubSchema,
|
||||||
CredentialsMetaInput,
|
CredentialsMetaInput,
|
||||||
@@ -60,7 +60,10 @@ export function APIKeyCredentialsModal({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-2">
|
<form
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
className="space-y-2 px-2"
|
||||||
|
>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="apiKey"
|
name="apiKey"
|
||||||
@@ -70,8 +73,7 @@ export function APIKeyCredentialsModal({
|
|||||||
id="apiKey"
|
id="apiKey"
|
||||||
label="API Key"
|
label="API Key"
|
||||||
type="password"
|
type="password"
|
||||||
placeholder="Enter API key..."
|
placeholder="Enter API Key..."
|
||||||
size="small"
|
|
||||||
hint={
|
hint={
|
||||||
schema.credentials_scopes ? (
|
schema.credentials_scopes ? (
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
@@ -98,8 +100,7 @@ export function APIKeyCredentialsModal({
|
|||||||
id="title"
|
id="title"
|
||||||
label="Name"
|
label="Name"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Enter a name for this API key..."
|
placeholder="Enter a name for this API Key..."
|
||||||
size="small"
|
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -113,13 +114,12 @@ export function APIKeyCredentialsModal({
|
|||||||
label="Expiration Date"
|
label="Expiration Date"
|
||||||
type="datetime-local"
|
type="datetime-local"
|
||||||
placeholder="Select expiration date..."
|
placeholder="Select expiration date..."
|
||||||
size="small"
|
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Button type="submit" size="small" className="min-w-68">
|
<Button type="submit" className="min-w-68">
|
||||||
Save & use this API key
|
Add API Key
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ type CredentialRowProps = {
|
|||||||
provider: string;
|
provider: string;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
onSelect: () => void;
|
onSelect: () => void;
|
||||||
onDelete: () => void;
|
onDelete?: () => void;
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
showCaret?: boolean;
|
showCaret?: boolean;
|
||||||
asSelectTrigger?: boolean;
|
asSelectTrigger?: boolean;
|
||||||
@@ -100,7 +100,7 @@ export function CredentialRow({
|
|||||||
{showCaret && !asSelectTrigger && (
|
{showCaret && !asSelectTrigger && (
|
||||||
<CaretDown className="h-4 w-4 shrink-0 text-gray-400" />
|
<CaretDown className="h-4 w-4 shrink-0 text-gray-400" />
|
||||||
)}
|
)}
|
||||||
{!readOnly && !showCaret && !asSelectTrigger && (
|
{!readOnly && !showCaret && !asSelectTrigger && onDelete && (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ export function CredentialsSelect({
|
|||||||
>
|
>
|
||||||
<SelectTrigger
|
<SelectTrigger
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-auto min-h-12 w-full rounded-medium border-zinc-200 p-0 pr-4 shadow-none",
|
"h-auto min-h-12 w-full rounded-medium p-0 pr-4 shadow-none",
|
||||||
variant === "node" && "overflow-hidden",
|
variant === "node" && "overflow-hidden",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -87,6 +87,39 @@ export function CredentialsSelect({
|
|||||||
variant={variant}
|
variant={variant}
|
||||||
/>
|
/>
|
||||||
</SelectValue>
|
</SelectValue>
|
||||||
|
) : allowNone ? (
|
||||||
|
<SelectValue key="__none__" asChild>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-3 rounded-medium border border-zinc-200 bg-white p-3 transition-colors",
|
||||||
|
variant === "node"
|
||||||
|
? "min-w-0 flex-1 overflow-hidden border-0 bg-transparent"
|
||||||
|
: "border-0 bg-transparent",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-zinc-200">
|
||||||
|
<Text
|
||||||
|
variant="body"
|
||||||
|
className="text-xs font-medium text-zinc-500"
|
||||||
|
>
|
||||||
|
—
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex min-w-0 flex-1 flex-nowrap items-center gap-4",
|
||||||
|
variant === "node" && "overflow-hidden",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
variant="body"
|
||||||
|
className={cn("tracking-tight text-zinc-500")}
|
||||||
|
>
|
||||||
|
None (skip this credential)
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SelectValue>
|
||||||
) : (
|
) : (
|
||||||
<SelectValue key="placeholder" placeholder="Select credential" />
|
<SelectValue key="placeholder" placeholder="Select credential" />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -100,3 +100,29 @@ export function getCredentialDisplayName(
|
|||||||
|
|
||||||
export const OAUTH_TIMEOUT_MS = 5 * 60 * 1000;
|
export const OAUTH_TIMEOUT_MS = 5 * 60 * 1000;
|
||||||
export const MASKED_KEY_LENGTH = 30;
|
export const MASKED_KEY_LENGTH = 30;
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,9 +6,11 @@ import {
|
|||||||
CredentialsMetaInput,
|
CredentialsMetaInput,
|
||||||
} from "@/lib/autogpt-server-api/types";
|
} from "@/lib/autogpt-server-api/types";
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
|
filterSystemCredentials,
|
||||||
getActionButtonText,
|
getActionButtonText,
|
||||||
|
getSystemCredentials,
|
||||||
OAUTH_TIMEOUT_MS,
|
OAUTH_TIMEOUT_MS,
|
||||||
OAuthPopupResultMessage,
|
OAuthPopupResultMessage,
|
||||||
} from "./helpers";
|
} from "./helpers";
|
||||||
@@ -23,6 +25,7 @@ type Params = {
|
|||||||
onLoaded?: (loaded: boolean) => void;
|
onLoaded?: (loaded: boolean) => void;
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
isOptional?: boolean;
|
isOptional?: boolean;
|
||||||
|
allowSystemCredentials?: boolean; // Allow system credentials (for settings only)
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useCredentialsInput({
|
export function useCredentialsInput({
|
||||||
@@ -33,6 +36,7 @@ export function useCredentialsInput({
|
|||||||
onLoaded,
|
onLoaded,
|
||||||
readOnly = false,
|
readOnly = false,
|
||||||
isOptional = false,
|
isOptional = false,
|
||||||
|
allowSystemCredentials = false,
|
||||||
}: Params) {
|
}: Params) {
|
||||||
const [isAPICredentialsModalOpen, setAPICredentialsModalOpen] =
|
const [isAPICredentialsModalOpen, setAPICredentialsModalOpen] =
|
||||||
useState(false);
|
useState(false);
|
||||||
@@ -54,6 +58,7 @@ export function useCredentialsInput({
|
|||||||
const api = useBackendAPI();
|
const api = useBackendAPI();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const credentials = useCredentials(schema, siblingInputs);
|
const credentials = useCredentials(schema, siblingInputs);
|
||||||
|
const hasAttemptedAutoSelect = useRef(false);
|
||||||
|
|
||||||
const deleteCredentialsMutation = useDeleteV1DeleteCredentials({
|
const deleteCredentialsMutation = useDeleteV1DeleteCredentials({
|
||||||
mutation: {
|
mutation: {
|
||||||
@@ -82,13 +87,22 @@ export function useCredentialsInput({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (readOnly) return;
|
if (readOnly) return;
|
||||||
if (!credentials || !("savedCredentials" in credentials)) return;
|
if (!credentials || !("savedCredentials" in credentials)) return;
|
||||||
|
const availableCreds = allowSystemCredentials
|
||||||
|
? credentials.savedCredentials
|
||||||
|
: filterSystemCredentials(credentials.savedCredentials);
|
||||||
if (
|
if (
|
||||||
selectedCredential &&
|
selectedCredential &&
|
||||||
!credentials.savedCredentials.some((c) => c.id === selectedCredential.id)
|
!availableCreds.some((c) => c.id === selectedCredential.id)
|
||||||
) {
|
) {
|
||||||
onSelectCredential(undefined);
|
onSelectCredential(undefined);
|
||||||
}
|
}
|
||||||
}, [credentials, selectedCredential, onSelectCredential, readOnly]);
|
}, [
|
||||||
|
credentials,
|
||||||
|
selectedCredential,
|
||||||
|
onSelectCredential,
|
||||||
|
readOnly,
|
||||||
|
allowSystemCredentials,
|
||||||
|
]);
|
||||||
|
|
||||||
// The available credential, if there is only one
|
// The available credential, if there is only one
|
||||||
const singleCredential = useMemo(() => {
|
const singleCredential = useMemo(() => {
|
||||||
@@ -96,24 +110,111 @@ export function useCredentialsInput({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return credentials.savedCredentials.length === 1
|
const credsToUse = allowSystemCredentials
|
||||||
? credentials.savedCredentials[0]
|
? credentials.savedCredentials
|
||||||
: null;
|
: filterSystemCredentials(credentials.savedCredentials);
|
||||||
}, [credentials]);
|
return credsToUse.length === 1 ? credsToUse[0] : null;
|
||||||
|
}, [credentials, allowSystemCredentials]);
|
||||||
|
|
||||||
// Auto-select the one available credential (only if not optional)
|
// Auto-select the one available credential
|
||||||
|
// Prioritize system credentials if available
|
||||||
|
// For system credentials, always auto-select even if optional (they should be used by default)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (readOnly) return;
|
if (readOnly) return;
|
||||||
if (isOptional) return; // Don't auto-select when credential is optional
|
if (!credentials || !("savedCredentials" in credentials)) return;
|
||||||
if (singleCredential && !selectedCredential) {
|
|
||||||
|
// Early return if already selected to prevent infinite loops
|
||||||
|
const currentSelectedId = selectedCredential?.id;
|
||||||
|
if (currentSelectedId) {
|
||||||
|
hasAttemptedAutoSelect.current = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If selectedCredential is explicitly undefined and isOptional is true,
|
||||||
|
// don't auto-select - this could mean "None" was explicitly selected
|
||||||
|
// The parent component should handle setting the initial value
|
||||||
|
if (selectedCredential === undefined && isOptional) {
|
||||||
|
// Mark as attempted to prevent auto-selection when "None" is a valid choice
|
||||||
|
hasAttemptedAutoSelect.current = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only attempt auto-selection once per credential load
|
||||||
|
if (hasAttemptedAutoSelect.current) return;
|
||||||
|
|
||||||
|
const supportedTypes = schema.credentials_types || [];
|
||||||
|
const requiredScopes = schema.credentials_scopes;
|
||||||
|
const savedCreds = credentials.savedCredentials;
|
||||||
|
const systemCreds = getSystemCredentials(savedCreds);
|
||||||
|
|
||||||
|
// Filter system credentials by type and scopes (same logic as useCredentials)
|
||||||
|
const matchingSystemCreds = systemCreds.filter((cred) => {
|
||||||
|
// Check type match
|
||||||
|
if (!supportedTypes.includes(cred.type)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For OAuth2 credentials, check scopes
|
||||||
|
if (
|
||||||
|
cred.type === "oauth2" &&
|
||||||
|
requiredScopes &&
|
||||||
|
requiredScopes.length > 0
|
||||||
|
) {
|
||||||
|
const grantedScopes = new Set(cred.scopes || []);
|
||||||
|
const hasAllRequiredScopes = new Set(requiredScopes).isSubsetOf(
|
||||||
|
grantedScopes,
|
||||||
|
);
|
||||||
|
if (!hasAllRequiredScopes) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// First, try to auto-select system credential if available
|
||||||
|
if (matchingSystemCreds.length === 1) {
|
||||||
|
const systemCred = matchingSystemCreds[0];
|
||||||
|
const credProvider = credentials.provider;
|
||||||
|
hasAttemptedAutoSelect.current = true;
|
||||||
|
onSelectCredential({
|
||||||
|
id: systemCred.id,
|
||||||
|
type: systemCred.type,
|
||||||
|
provider: credProvider,
|
||||||
|
title: (systemCred as any).title,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, auto-select single credential if there's only one (and not optional)
|
||||||
|
if (!isOptional && singleCredential) {
|
||||||
|
hasAttemptedAutoSelect.current = true;
|
||||||
onSelectCredential(singleCredential);
|
onSelectCredential(singleCredential);
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
singleCredential,
|
singleCredential?.id, // Only depend on the ID, not the whole object
|
||||||
selectedCredential,
|
selectedCredential?.id, // Only depend on the ID, not the whole object
|
||||||
onSelectCredential,
|
|
||||||
readOnly,
|
readOnly,
|
||||||
isOptional,
|
isOptional,
|
||||||
|
credentials,
|
||||||
|
schema.credentials_types,
|
||||||
|
schema.credentials_scopes,
|
||||||
|
// Note: onSelectCredential removed from deps to prevent infinite loops
|
||||||
|
// It should be stable, but if it's not, the ref will prevent multiple calls
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Reset the ref when credentials change significantly
|
||||||
|
useEffect(() => {
|
||||||
|
if (credentials && "savedCredentials" in credentials) {
|
||||||
|
hasAttemptedAutoSelect.current = false;
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
credentials && "savedCredentials" in credentials
|
||||||
|
? credentials.savedCredentials.length
|
||||||
|
: 0,
|
||||||
|
credentials && "savedCredentials" in credentials
|
||||||
|
? credentials.provider
|
||||||
|
: null,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -137,6 +238,11 @@ export function useCredentialsInput({
|
|||||||
oAuthCallback,
|
oAuthCallback,
|
||||||
} = credentials;
|
} = credentials;
|
||||||
|
|
||||||
|
// Filter system credentials unless explicitly allowed (for settings)
|
||||||
|
const filteredCredentials = allowSystemCredentials
|
||||||
|
? savedCredentials
|
||||||
|
: filterSystemCredentials(savedCredentials);
|
||||||
|
|
||||||
async function handleOAuthLogin() {
|
async function handleOAuthLogin() {
|
||||||
setOAuthError(null);
|
setOAuthError(null);
|
||||||
const { login_url, state_token } = await api.oAuthLogin(
|
const { login_url, state_token } = await api.oAuthLogin(
|
||||||
@@ -291,7 +397,7 @@ export function useCredentialsInput({
|
|||||||
supportsOAuth2,
|
supportsOAuth2,
|
||||||
supportsUserPassword,
|
supportsUserPassword,
|
||||||
supportsHostScoped,
|
supportsHostScoped,
|
||||||
credentialsToShow: savedCredentials,
|
credentialsToShow: filteredCredentials,
|
||||||
selectedCredential,
|
selectedCredential,
|
||||||
oAuthError,
|
oAuthError,
|
||||||
isAPICredentialsModalOpen,
|
isAPICredentialsModalOpen,
|
||||||
@@ -306,7 +412,7 @@ export function useCredentialsInput({
|
|||||||
supportsApiKey,
|
supportsApiKey,
|
||||||
supportsUserPassword,
|
supportsUserPassword,
|
||||||
supportsHostScoped,
|
supportsHostScoped,
|
||||||
savedCredentials.length > 0,
|
filteredCredentials.length > 0,
|
||||||
),
|
),
|
||||||
setAPICredentialsModalOpen,
|
setAPICredentialsModalOpen,
|
||||||
setUserPasswordCredentialsModalOpen,
|
setUserPasswordCredentialsModalOpen,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/atoms/Tooltip/BaseTooltip";
|
} from "@/components/atoms/Tooltip/BaseTooltip";
|
||||||
import { Dialog } from "@/components/molecules/Dialog/Dialog";
|
import { Dialog } from "@/components/molecules/Dialog/Dialog";
|
||||||
import { useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { ScheduleAgentModal } from "../ScheduleAgentModal/ScheduleAgentModal";
|
import { ScheduleAgentModal } from "../ScheduleAgentModal/ScheduleAgentModal";
|
||||||
import { ModalHeader } from "./components/ModalHeader/ModalHeader";
|
import { ModalHeader } from "./components/ModalHeader/ModalHeader";
|
||||||
import { ModalRunSection } from "./components/ModalRunSection/ModalRunSection";
|
import { ModalRunSection } from "./components/ModalRunSection/ModalRunSection";
|
||||||
@@ -82,6 +82,8 @@ export function RunAgentModal({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const [isScheduleModalOpen, setIsScheduleModalOpen] = useState(false);
|
const [isScheduleModalOpen, setIsScheduleModalOpen] = useState(false);
|
||||||
|
const [hasOverflow, setHasOverflow] = useState(false);
|
||||||
|
const contentRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const hasAnySetupFields =
|
const hasAnySetupFields =
|
||||||
Object.keys(agentInputFields || {}).length > 0 ||
|
Object.keys(agentInputFields || {}).length > 0 ||
|
||||||
@@ -89,6 +91,43 @@ export function RunAgentModal({
|
|||||||
|
|
||||||
const isTriggerRunType = defaultRunType.includes("trigger");
|
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) {
|
function handleInputChange(key: string, value: string) {
|
||||||
setInputValues((prev) => ({
|
setInputValues((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
@@ -134,91 +173,97 @@ export function RunAgentModal({
|
|||||||
>
|
>
|
||||||
<Dialog.Trigger>{triggerSlot}</Dialog.Trigger>
|
<Dialog.Trigger>{triggerSlot}</Dialog.Trigger>
|
||||||
<Dialog.Content>
|
<Dialog.Content>
|
||||||
{/* Header */}
|
<div ref={contentRef} className="flex min-h-full flex-col">
|
||||||
<ModalHeader agent={agent} />
|
<div className="flex-1">
|
||||||
|
{/* Header */}
|
||||||
|
<ModalHeader agent={agent} />
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
{hasAnySetupFields ? (
|
{hasAnySetupFields ? (
|
||||||
<div className="mt-10">
|
<div className="mt-10 pb-32">
|
||||||
<RunAgentModalContextProvider
|
<RunAgentModalContextProvider
|
||||||
value={{
|
value={{
|
||||||
agent,
|
agent,
|
||||||
defaultRunType,
|
defaultRunType,
|
||||||
presetName,
|
presetName,
|
||||||
setPresetName,
|
setPresetName,
|
||||||
presetDescription,
|
presetDescription,
|
||||||
setPresetDescription,
|
setPresetDescription,
|
||||||
inputValues,
|
inputValues,
|
||||||
setInputValue: handleInputChange,
|
setInputValue: handleInputChange,
|
||||||
agentInputFields,
|
agentInputFields,
|
||||||
inputCredentials,
|
inputCredentials,
|
||||||
setInputCredentialsValue: handleCredentialsChange,
|
setInputCredentialsValue: handleCredentialsChange,
|
||||||
agentCredentialsInputFields,
|
agentCredentialsInputFields,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ModalRunSection />
|
<ModalRunSection />
|
||||||
</RunAgentModalContextProvider>
|
</RunAgentModalContextProvider>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
|
||||||
|
|
||||||
<Dialog.Footer className="mt-6 bg-white pt-4">
|
<Dialog.Footer
|
||||||
<div className="flex items-center justify-end gap-3">
|
className={`sticky bottom-0 z-10 bg-white pt-4 ${
|
||||||
{isTriggerRunType ? null : !allRequiredInputsAreSet ? (
|
hasOverflow
|
||||||
<TooltipProvider>
|
? "border-t border-neutral-100 shadow-[0_-2px_8px_rgba(0,0,0,0.04)]"
|
||||||
<Tooltip>
|
: ""
|
||||||
<TooltipTrigger asChild>
|
}`}
|
||||||
<span>
|
>
|
||||||
<Button
|
<div className="flex items-center justify-end gap-3">
|
||||||
variant="secondary"
|
{isTriggerRunType ? null : !allRequiredInputsAreSet ? (
|
||||||
onClick={handleOpenScheduleModal}
|
<TooltipProvider>
|
||||||
disabled={
|
<Tooltip>
|
||||||
isExecuting ||
|
<TooltipTrigger asChild>
|
||||||
isSettingUpTrigger ||
|
<span>
|
||||||
!allRequiredInputsAreSet
|
<Button
|
||||||
}
|
variant="secondary"
|
||||||
>
|
onClick={handleOpenScheduleModal}
|
||||||
Schedule Task
|
disabled={
|
||||||
</Button>
|
isExecuting ||
|
||||||
</span>
|
isSettingUpTrigger ||
|
||||||
</TooltipTrigger>
|
!allRequiredInputsAreSet
|
||||||
<TooltipContent>
|
}
|
||||||
<p>
|
>
|
||||||
Please set up all required inputs and credentials before
|
Schedule Task
|
||||||
scheduling
|
</Button>
|
||||||
</p>
|
</span>
|
||||||
</TooltipContent>
|
</TooltipTrigger>
|
||||||
</Tooltip>
|
<TooltipContent>
|
||||||
</TooltipProvider>
|
<p>
|
||||||
) : (
|
Please set up all required inputs and credentials
|
||||||
<Button
|
before scheduling
|
||||||
variant="secondary"
|
</p>
|
||||||
onClick={handleOpenScheduleModal}
|
</TooltipContent>
|
||||||
disabled={
|
</Tooltip>
|
||||||
isExecuting ||
|
</TooltipProvider>
|
||||||
isSettingUpTrigger ||
|
) : (
|
||||||
!allRequiredInputsAreSet
|
<Button
|
||||||
}
|
variant="secondary"
|
||||||
>
|
onClick={handleOpenScheduleModal}
|
||||||
Schedule Task
|
disabled={isExecuting || isSettingUpTrigger}
|
||||||
</Button>
|
>
|
||||||
)}
|
Schedule Task
|
||||||
<RunActions
|
</Button>
|
||||||
defaultRunType={defaultRunType}
|
)}
|
||||||
onRun={handleRun}
|
<RunActions
|
||||||
isExecuting={isExecuting}
|
defaultRunType={defaultRunType}
|
||||||
isSettingUpTrigger={isSettingUpTrigger}
|
onRun={handleRun}
|
||||||
isRunReady={allRequiredInputsAreSet}
|
isExecuting={isExecuting}
|
||||||
|
isSettingUpTrigger={isSettingUpTrigger}
|
||||||
|
isRunReady={allRequiredInputsAreSet}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<ScheduleAgentModal
|
||||||
|
isOpen={isScheduleModalOpen}
|
||||||
|
onClose={handleCloseScheduleModal}
|
||||||
|
agent={agent}
|
||||||
|
inputValues={inputValues}
|
||||||
|
inputCredentials={inputCredentials}
|
||||||
|
onScheduleCreated={handleScheduleCreated}
|
||||||
/>
|
/>
|
||||||
</div>
|
</Dialog.Footer>
|
||||||
<ScheduleAgentModal
|
</div>
|
||||||
isOpen={isScheduleModalOpen}
|
|
||||||
onClose={handleCloseScheduleModal}
|
|
||||||
agent={agent}
|
|
||||||
inputValues={inputValues}
|
|
||||||
inputCredentials={inputCredentials}
|
|
||||||
onScheduleCreated={handleScheduleCreated}
|
|
||||||
/>
|
|
||||||
</Dialog.Footer>
|
|
||||||
</Dialog.Content>
|
</Dialog.Content>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,6 +1,16 @@
|
|||||||
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/CredentialsInputs";
|
||||||
import { Input } from "@/components/atoms/Input/Input";
|
import { Input } from "@/components/atoms/Input/Input";
|
||||||
import { InformationTooltip } from "@/components/molecules/InformationTooltip/InformationTooltip";
|
import { InformationTooltip } from "@/components/molecules/InformationTooltip/InformationTooltip";
|
||||||
|
import { CredentialsProvidersContext } from "@/providers/agent-credentials/credentials-provider";
|
||||||
|
import { useContext, useMemo } from "react";
|
||||||
|
import {
|
||||||
|
NONE_CREDENTIAL_MARKER,
|
||||||
|
useAgentCredentialPreferencesStore,
|
||||||
|
} from "../../../../../stores/agentCredentialPreferencesStore";
|
||||||
|
import {
|
||||||
|
filterSystemCredentials,
|
||||||
|
isSystemCredential,
|
||||||
|
} from "../../../CredentialsInputs/helpers";
|
||||||
import { RunAgentInputs } from "../../../RunAgentInputs/RunAgentInputs";
|
import { RunAgentInputs } from "../../../RunAgentInputs/RunAgentInputs";
|
||||||
import { useRunAgentModalContext } from "../../context";
|
import { useRunAgentModalContext } from "../../context";
|
||||||
import { ModalSection } from "../ModalSection/ModalSection";
|
import { ModalSection } from "../ModalSection/ModalSection";
|
||||||
@@ -22,8 +32,44 @@ export function ModalRunSection() {
|
|||||||
agentCredentialsInputFields,
|
agentCredentialsInputFields,
|
||||||
} = useRunAgentModalContext();
|
} = useRunAgentModalContext();
|
||||||
|
|
||||||
|
const allProviders = useContext(CredentialsProvidersContext);
|
||||||
|
const store = useAgentCredentialPreferencesStore();
|
||||||
|
|
||||||
const inputFields = Object.entries(agentInputFields || {});
|
const inputFields = Object.entries(agentInputFields || {});
|
||||||
const credentialFields = Object.entries(agentCredentialsInputFields || {});
|
|
||||||
|
// Only show credential fields that have user credentials (NOT system credentials)
|
||||||
|
// System credentials should only be shown in settings, not in run modal
|
||||||
|
const credentialFields = useMemo(() => {
|
||||||
|
if (!allProviders || !agentCredentialsInputFields) return [];
|
||||||
|
|
||||||
|
return Object.entries(agentCredentialsInputFields).filter(
|
||||||
|
([_key, schema]) => {
|
||||||
|
const providerNames = schema.credentials_provider || [];
|
||||||
|
const supportedTypes = schema.credentials_types || [];
|
||||||
|
|
||||||
|
// Check if any provider has user credentials (NOT system credentials)
|
||||||
|
for (const providerName of providerNames) {
|
||||||
|
const providerData = allProviders[providerName];
|
||||||
|
if (!providerData) continue;
|
||||||
|
|
||||||
|
const userCreds = filterSystemCredentials(
|
||||||
|
providerData.savedCredentials,
|
||||||
|
);
|
||||||
|
const matchingUserCreds = userCreds.filter((cred: { type: string }) =>
|
||||||
|
supportedTypes.includes(cred.type),
|
||||||
|
);
|
||||||
|
|
||||||
|
// If there are user credentials available, show this field
|
||||||
|
if (matchingUserCreds.length > 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide the field if only system credentials exist (or no credentials at all)
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}, [agentCredentialsInputFields, allProviders]);
|
||||||
|
|
||||||
// Get the list of required credentials from the schema
|
// Get the list of required credentials from the schema
|
||||||
const requiredCredentials = new Set(
|
const requiredCredentials = new Set(
|
||||||
@@ -98,22 +144,113 @@ export function ModalRunSection() {
|
|||||||
subtitle="These are the credentials the agent will use to perform this task"
|
subtitle="These are the credentials the agent will use to perform this task"
|
||||||
>
|
>
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{Object.entries(agentCredentialsInputFields || {}).map(
|
{credentialFields
|
||||||
([key, inputSubSchema]) => (
|
.map(([key, inputSubSchema]) => {
|
||||||
<CredentialsInput
|
const selectedCred = inputCredentials?.[key];
|
||||||
key={key}
|
|
||||||
schema={
|
// Check if the selected credential is a system credential
|
||||||
{ ...inputSubSchema, discriminator: undefined } as any
|
// First check the credential object itself, then look it up in providers
|
||||||
|
let isSystemCredSelected = false;
|
||||||
|
if (selectedCred) {
|
||||||
|
// Check if credential object has is_system or title indicates system credential
|
||||||
|
isSystemCredSelected = isSystemCredential(
|
||||||
|
selectedCred as { title?: string; is_system?: boolean },
|
||||||
|
);
|
||||||
|
|
||||||
|
// If not detected by title/is_system, check by looking up in providers
|
||||||
|
if (
|
||||||
|
!isSystemCredSelected &&
|
||||||
|
selectedCred.id &&
|
||||||
|
allProviders
|
||||||
|
) {
|
||||||
|
const providerNames =
|
||||||
|
inputSubSchema.credentials_provider || [];
|
||||||
|
for (const providerName of providerNames) {
|
||||||
|
const providerData = allProviders[providerName];
|
||||||
|
if (!providerData) continue;
|
||||||
|
const systemCreds = providerData.savedCredentials.filter(
|
||||||
|
(cred: any) => cred.is_system === true,
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
systemCreds.some(
|
||||||
|
(cred: any) => cred.id === selectedCred.id,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
isSystemCredSelected = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
selectedCredentials={inputCredentials?.[key]}
|
}
|
||||||
onSelectCredentials={(value) =>
|
|
||||||
setInputCredentialsValue(key, value)
|
// If a system credential is selected, check if there are user credentials available
|
||||||
|
// If not, hide this field entirely (it will still be used for execution)
|
||||||
|
if (isSystemCredSelected) {
|
||||||
|
const providerNames =
|
||||||
|
inputSubSchema.credentials_provider || [];
|
||||||
|
const supportedTypes = inputSubSchema.credentials_types || [];
|
||||||
|
const hasUserCreds = providerNames.some(
|
||||||
|
(providerName: string) => {
|
||||||
|
const providerData = allProviders?.[providerName];
|
||||||
|
if (!providerData) return false;
|
||||||
|
const userCreds = filterSystemCredentials(
|
||||||
|
providerData.savedCredentials,
|
||||||
|
);
|
||||||
|
return userCreds.some((cred: { type: string }) =>
|
||||||
|
supportedTypes.includes(cred.type),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// If no user credentials available, hide the field completely
|
||||||
|
if (!hasUserCreds) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
siblingInputs={inputValues}
|
}
|
||||||
isOptional={!requiredCredentials.has(key)}
|
|
||||||
/>
|
// If a system credential is selected but user creds exist, don't show it in the UI
|
||||||
),
|
// (it will still be used for execution, but user can select a user credential instead)
|
||||||
)}
|
const credToDisplay = isSystemCredSelected
|
||||||
|
? undefined
|
||||||
|
: selectedCred;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CredentialsInput
|
||||||
|
key={key}
|
||||||
|
schema={
|
||||||
|
{ ...inputSubSchema, discriminator: undefined } as any
|
||||||
|
}
|
||||||
|
selectedCredentials={credToDisplay}
|
||||||
|
onSelectCredentials={(value) => {
|
||||||
|
// When user selects a credential, update the state and save to preferences
|
||||||
|
setInputCredentialsValue(key, value);
|
||||||
|
// Save to preferences store
|
||||||
|
if (value === undefined) {
|
||||||
|
store.setCredentialPreference(
|
||||||
|
agent.id.toString(),
|
||||||
|
key,
|
||||||
|
NONE_CREDENTIAL_MARKER,
|
||||||
|
);
|
||||||
|
} else if (value === null) {
|
||||||
|
store.setCredentialPreference(
|
||||||
|
agent.id.toString(),
|
||||||
|
key,
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
store.setCredentialPreference(
|
||||||
|
agent.id.toString(),
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
siblingInputs={inputValues}
|
||||||
|
isOptional={!requiredCredentials.has(key)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.filter(Boolean)}
|
||||||
</div>
|
</div>
|
||||||
</ModalSection>
|
</ModalSection>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -11,9 +11,25 @@ import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
|
|||||||
import { LibraryAgentPreset } from "@/app/api/__generated__/models/libraryAgentPreset";
|
import { LibraryAgentPreset } from "@/app/api/__generated__/models/libraryAgentPreset";
|
||||||
import { useToast } from "@/components/molecules/Toast/use-toast";
|
import { useToast } from "@/components/molecules/Toast/use-toast";
|
||||||
import { isEmpty } from "@/lib/utils";
|
import { isEmpty } from "@/lib/utils";
|
||||||
|
import { CredentialsProvidersContext } from "@/providers/agent-credentials/credentials-provider";
|
||||||
import { analytics } from "@/services/analytics";
|
import { analytics } from "@/services/analytics";
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import {
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
import {
|
||||||
|
NONE_CREDENTIAL_MARKER,
|
||||||
|
useAgentCredentialPreferencesStore,
|
||||||
|
} from "../../../stores/agentCredentialPreferencesStore";
|
||||||
|
import {
|
||||||
|
filterSystemCredentials,
|
||||||
|
getSystemCredentials,
|
||||||
|
} from "../CredentialsInputs/helpers";
|
||||||
import { showExecutionErrorToast } from "./errorHelpers";
|
import { showExecutionErrorToast } from "./errorHelpers";
|
||||||
|
|
||||||
export type RunVariant =
|
export type RunVariant =
|
||||||
@@ -42,8 +58,10 @@ export function useAgentRunModal(
|
|||||||
const [inputCredentials, setInputCredentials] = useState<Record<string, any>>(
|
const [inputCredentials, setInputCredentials] = useState<Record<string, any>>(
|
||||||
callbacks?.initialInputCredentials || {},
|
callbacks?.initialInputCredentials || {},
|
||||||
);
|
);
|
||||||
|
|
||||||
const [presetName, setPresetName] = useState<string>("");
|
const [presetName, setPresetName] = useState<string>("");
|
||||||
const [presetDescription, setPresetDescription] = useState<string>("");
|
const [presetDescription, setPresetDescription] = useState<string>("");
|
||||||
|
const hasInitializedSystemCreds = useRef(false);
|
||||||
|
|
||||||
// Determine the default run type based on agent capabilities
|
// Determine the default run type based on agent capabilities
|
||||||
const defaultRunType: RunVariant = agent.trigger_setup_info
|
const defaultRunType: RunVariant = agent.trigger_setup_info
|
||||||
@@ -58,6 +76,198 @@ export function useAgentRunModal(
|
|||||||
setInputCredentials(callbacks?.initialInputCredentials || {});
|
setInputCredentials(callbacks?.initialInputCredentials || {});
|
||||||
}, [callbacks?.initialInputValues, callbacks?.initialInputCredentials]);
|
}, [callbacks?.initialInputValues, callbacks?.initialInputCredentials]);
|
||||||
|
|
||||||
|
const allProviders = useContext(CredentialsProvidersContext);
|
||||||
|
const store = useAgentCredentialPreferencesStore();
|
||||||
|
|
||||||
|
// Initialize credentials from saved preferences or default system credentials
|
||||||
|
// This ensures credentials are used even when the field is not displayed
|
||||||
|
useEffect(() => {
|
||||||
|
if (!allProviders || !agent.credentials_input_schema?.properties) return;
|
||||||
|
if (callbacks?.initialInputCredentials) {
|
||||||
|
hasInitializedSystemCreds.current = true;
|
||||||
|
return; // Don't override if initial credentials provided
|
||||||
|
}
|
||||||
|
if (hasInitializedSystemCreds.current) return; // Already initialized
|
||||||
|
|
||||||
|
const properties = agent.credentials_input_schema.properties as Record<
|
||||||
|
string,
|
||||||
|
any
|
||||||
|
>;
|
||||||
|
|
||||||
|
// Use functional update to get current state and avoid stale closures
|
||||||
|
setInputCredentials((currentCreds) => {
|
||||||
|
const credsToAdd: Record<string, any> = {};
|
||||||
|
|
||||||
|
for (const [key, schema] of Object.entries(properties)) {
|
||||||
|
// Skip if already set
|
||||||
|
if (currentCreds[key]) continue;
|
||||||
|
|
||||||
|
// First, check if user has a saved preference
|
||||||
|
const savedPreference = store.getCredentialPreference(
|
||||||
|
agent.id.toString(),
|
||||||
|
key,
|
||||||
|
);
|
||||||
|
// Check if "None" was explicitly selected (special marker)
|
||||||
|
if (savedPreference === NONE_CREDENTIAL_MARKER) {
|
||||||
|
// User explicitly selected "None" - don't add any credential
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (savedPreference) {
|
||||||
|
credsToAdd[key] = savedPreference;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, find default system credentials for this field
|
||||||
|
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;
|
||||||
|
|
||||||
|
// For OAuth2 credentials, check scopes
|
||||||
|
if (
|
||||||
|
cred.type === "oauth2" &&
|
||||||
|
requiredScopes &&
|
||||||
|
requiredScopes.length > 0
|
||||||
|
) {
|
||||||
|
const grantedScopes = new Set(cred.scopes || []);
|
||||||
|
const hasAllRequiredScopes = new Set(requiredScopes).isSubsetOf(
|
||||||
|
grantedScopes,
|
||||||
|
);
|
||||||
|
if (!hasAllRequiredScopes) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// If there's exactly one system credential, use it as default
|
||||||
|
if (matchingSystemCreds.length === 1) {
|
||||||
|
const systemCred = matchingSystemCreds[0];
|
||||||
|
credsToAdd[key] = {
|
||||||
|
id: systemCred.id,
|
||||||
|
type: systemCred.type,
|
||||||
|
provider: providerName,
|
||||||
|
title: systemCred.title,
|
||||||
|
};
|
||||||
|
break; // Use first matching provider
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only update if we found credentials to add
|
||||||
|
if (Object.keys(credsToAdd).length > 0) {
|
||||||
|
hasInitializedSystemCreds.current = true;
|
||||||
|
return {
|
||||||
|
...currentCreds,
|
||||||
|
...credsToAdd,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentCreds; // No changes
|
||||||
|
});
|
||||||
|
}, [
|
||||||
|
allProviders,
|
||||||
|
agent.credentials_input_schema,
|
||||||
|
agent.id,
|
||||||
|
store,
|
||||||
|
callbacks?.initialInputCredentials,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Sync credentials with preferences store when modal opens
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen || !allProviders || !agent.credentials_input_schema?.properties)
|
||||||
|
return;
|
||||||
|
if (callbacks?.initialInputCredentials) return; // Don't override if initial credentials provided
|
||||||
|
|
||||||
|
const properties = agent.credentials_input_schema.properties as Record<
|
||||||
|
string,
|
||||||
|
any
|
||||||
|
>;
|
||||||
|
|
||||||
|
setInputCredentials((currentCreds) => {
|
||||||
|
const updatedCreds: Record<string, any> = { ...currentCreds };
|
||||||
|
|
||||||
|
for (const [key, schema] of Object.entries(properties)) {
|
||||||
|
const savedPreference = store.getCredentialPreference(
|
||||||
|
agent.id.toString(),
|
||||||
|
key,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (savedPreference === NONE_CREDENTIAL_MARKER) {
|
||||||
|
// User explicitly selected "None" - remove from credentials
|
||||||
|
delete updatedCreds[key];
|
||||||
|
} else if (savedPreference) {
|
||||||
|
// User has a saved preference - use it
|
||||||
|
updatedCreds[key] = savedPreference;
|
||||||
|
} else if (!updatedCreds[key]) {
|
||||||
|
// No preference and no current credential - try to find default system credential
|
||||||
|
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 = new Set(requiredScopes).isSubsetOf(
|
||||||
|
grantedScopes,
|
||||||
|
);
|
||||||
|
if (!hasAllRequiredScopes) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (matchingSystemCreds.length === 1) {
|
||||||
|
const systemCred = matchingSystemCreds[0];
|
||||||
|
updatedCreds[key] = {
|
||||||
|
id: systemCred.id,
|
||||||
|
type: systemCred.type,
|
||||||
|
provider: providerName,
|
||||||
|
title: systemCred.title,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedCreds;
|
||||||
|
});
|
||||||
|
}, [
|
||||||
|
isOpen,
|
||||||
|
agent.id,
|
||||||
|
agent.credentials_input_schema,
|
||||||
|
allProviders,
|
||||||
|
store,
|
||||||
|
callbacks?.initialInputCredentials,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Reset initialization flag when modal closes/opens or agent changes
|
||||||
|
useEffect(() => {
|
||||||
|
hasInitializedSystemCreds.current = false;
|
||||||
|
}, [isOpen, agent.graph_id]);
|
||||||
|
|
||||||
// API mutations
|
// API mutations
|
||||||
const executeGraphMutation = usePostV1ExecuteGraphAgent({
|
const executeGraphMutation = usePostV1ExecuteGraphAgent({
|
||||||
mutation: {
|
mutation: {
|
||||||
@@ -169,15 +379,70 @@ export function useAgentRunModal(
|
|||||||
(agent.credentials_input_schema?.required as string[]) || [],
|
(agent.credentials_input_schema?.required as string[]) || [],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Filter out credential fields that only have system credentials available
|
||||||
|
// System credentials should not be required in the run modal
|
||||||
|
// Also check if user has a saved preference (including NONE_MARKER)
|
||||||
|
const requiredCredentialsToCheck = [...requiredCredentials].filter(
|
||||||
|
(key) => {
|
||||||
|
// Check if user has a saved preference first
|
||||||
|
const savedPreference = store.getCredentialPreference(
|
||||||
|
agent.id.toString(),
|
||||||
|
key,
|
||||||
|
);
|
||||||
|
// If "None" was explicitly selected, don't require it
|
||||||
|
if (savedPreference === NONE_CREDENTIAL_MARKER) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// If user has a saved preference, it should be checked
|
||||||
|
if (savedPreference) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const schema = agentCredentialsInputFields[key];
|
||||||
|
if (!schema || !allProviders) return true; // If we can't check, include it
|
||||||
|
|
||||||
|
const providerNames = schema.credentials_provider || [];
|
||||||
|
const supportedTypes = schema.credentials_types || [];
|
||||||
|
|
||||||
|
// Check if any provider has non-system credentials available
|
||||||
|
for (const providerName of providerNames) {
|
||||||
|
const providerData = allProviders[providerName];
|
||||||
|
if (!providerData) continue;
|
||||||
|
|
||||||
|
const userCreds = filterSystemCredentials(
|
||||||
|
providerData.savedCredentials,
|
||||||
|
);
|
||||||
|
const matchingUserCreds = userCreds.filter((cred) =>
|
||||||
|
supportedTypes.includes(cred.type),
|
||||||
|
);
|
||||||
|
|
||||||
|
// If there are user credentials available, this field should be checked
|
||||||
|
if (matchingUserCreds.length > 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If only system credentials are available, exclude from required check
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// Check if required credentials have valid id (not just key existence)
|
// Check if required credentials have valid id (not just key existence)
|
||||||
// A credential is valid only if it has an id field set
|
// A credential is valid only if it has an id field set
|
||||||
const missing = [...requiredCredentials].filter((key) => {
|
const missing = requiredCredentialsToCheck.filter((key) => {
|
||||||
const cred = inputCredentials[key];
|
const cred = inputCredentials[key];
|
||||||
return !cred || !cred.id;
|
return !cred || !cred.id;
|
||||||
});
|
});
|
||||||
|
|
||||||
return [missing.length === 0, missing];
|
return [missing.length === 0, missing];
|
||||||
}, [agent.credentials_input_schema, inputCredentials]);
|
}, [
|
||||||
|
agent.credentials_input_schema,
|
||||||
|
agentCredentialsInputFields,
|
||||||
|
inputCredentials,
|
||||||
|
allProviders,
|
||||||
|
agent.id,
|
||||||
|
store,
|
||||||
|
]);
|
||||||
|
|
||||||
const credentialsRequired = useMemo(
|
const credentialsRequired = useMemo(
|
||||||
() => Object.keys(agentCredentialsInputFields || {}).length > 0,
|
() => Object.keys(agentCredentialsInputFields || {}).length > 0,
|
||||||
|
|||||||
@@ -1,37 +1,17 @@
|
|||||||
import { Button } from "@/components/atoms/Button/Button";
|
import { Button } from "@/components/atoms/Button/Button";
|
||||||
|
import { Text } from "@/components/atoms/Text/Text";
|
||||||
import { GearIcon } from "@phosphor-icons/react";
|
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 (
|
return (
|
||||||
<Button
|
<Button
|
||||||
variant={selected ? "secondary" : "ghost"}
|
variant="ghost"
|
||||||
size="small"
|
size="small"
|
||||||
className="m-0 min-w-0 rounded-full p-0 px-1"
|
className="m-0 min-w-0 rounded-full p-0 px-1"
|
||||||
onClick={onSelectSettings}
|
|
||||||
aria-label="Agent Settings"
|
aria-label="Agent Settings"
|
||||||
>
|
>
|
||||||
<GearIcon
|
<GearIcon size={18} className="text-zinc-600" />
|
||||||
size={18}
|
<Text variant="small">Agent Settings</Text>
|
||||||
className={selected ? "text-zinc-900" : "text-zinc-600"}
|
|
||||||
/>
|
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
import { Text } from "@/components/atoms/Text/Text";
|
import { Text } from "@/components/atoms/Text/Text";
|
||||||
|
|
||||||
export function EmptySchedules() {
|
export function EmptySchedules() {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { useQueryClient } from "@tanstack/react-query";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { useAgentMissingCredentials } from "../../hooks/useAgentMissingCredentials";
|
||||||
import { RunAgentModal } from "../modals/RunAgentModal/RunAgentModal";
|
import { RunAgentModal } from "../modals/RunAgentModal/RunAgentModal";
|
||||||
import { RunDetailCard } from "../selected-views/RunDetailCard/RunDetailCard";
|
import { RunDetailCard } from "../selected-views/RunDetailCard/RunDetailCard";
|
||||||
import { EmptyTasksIllustration } from "./EmptyTasksIllustration";
|
import { EmptyTasksIllustration } from "./EmptyTasksIllustration";
|
||||||
@@ -44,6 +45,7 @@ export function EmptyTasks({
|
|||||||
const [isDeletingAgent, setIsDeletingAgent] = useState(false);
|
const [isDeletingAgent, setIsDeletingAgent] = useState(false);
|
||||||
|
|
||||||
const { mutateAsync: deleteAgent } = useDeleteV2DeleteLibraryAgent();
|
const { mutateAsync: deleteAgent } = useDeleteV2DeleteLibraryAgent();
|
||||||
|
const { hasMissingCredentials } = useAgentMissingCredentials(agent);
|
||||||
|
|
||||||
async function handleDeleteAgent() {
|
async function handleDeleteAgent() {
|
||||||
if (!agent.id) return;
|
if (!agent.id) return;
|
||||||
@@ -124,6 +126,7 @@ export function EmptyTasks({
|
|||||||
variant="primary"
|
variant="primary"
|
||||||
size="large"
|
size="large"
|
||||||
className="inline-flex w-[19.75rem]"
|
className="inline-flex w-[19.75rem]"
|
||||||
|
disabled={hasMissingCredentials}
|
||||||
>
|
>
|
||||||
Setup your task
|
Setup your task
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
import { Text } from "@/components/atoms/Text/Text";
|
import { Text } from "@/components/atoms/Text/Text";
|
||||||
|
|
||||||
export function EmptyTemplates() {
|
export function EmptyTemplates() {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
import { Text } from "@/components/atoms/Text/Text";
|
import { Text } from "@/components/atoms/Text/Text";
|
||||||
|
|
||||||
export function EmptyTriggers() {
|
export function EmptyTriggers() {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { Button } from "@/components/atoms/Button/Button";
|
import { Button } from "@/components/atoms/Button/Button";
|
||||||
import { Text } from "@/components/atoms/Text/Text";
|
import { Text } from "@/components/atoms/Text/Text";
|
||||||
|
|
||||||
interface MarketplaceBannersProps {
|
interface Props {
|
||||||
hasUpdate?: boolean;
|
hasUpdate?: boolean;
|
||||||
latestVersion?: number;
|
latestVersion?: number;
|
||||||
hasUnpublishedChanges?: boolean;
|
hasUnpublishedChanges?: boolean;
|
||||||
@@ -21,7 +21,7 @@ export function MarketplaceBanners({
|
|||||||
isUpdating,
|
isUpdating,
|
||||||
onUpdate,
|
onUpdate,
|
||||||
onPublish,
|
onPublish,
|
||||||
}: MarketplaceBannersProps) {
|
}: Props) {
|
||||||
const renderUpdateBanner = () => {
|
const renderUpdateBanner = () => {
|
||||||
if (hasUpdate && latestVersion) {
|
if (hasUpdate && latestVersion) {
|
||||||
return (
|
return (
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|||||||
@@ -1,22 +1,16 @@
|
|||||||
|
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
|
||||||
import { Skeleton } from "@/components/__legacy__/ui/skeleton";
|
import { Skeleton } from "@/components/__legacy__/ui/skeleton";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
|
|
||||||
import { AGENT_LIBRARY_SECTION_PADDING_X } from "../../helpers";
|
import { AGENT_LIBRARY_SECTION_PADDING_X } from "../../helpers";
|
||||||
import { SelectedViewLayout } from "./SelectedViewLayout";
|
import { SelectedViewLayout } from "./SelectedViewLayout";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
agent: LibraryAgent;
|
agent: LibraryAgent;
|
||||||
onSelectSettings?: () => void;
|
|
||||||
selectedSettings?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LoadingSelectedContent(props: Props) {
|
export function LoadingSelectedContent(props: Props) {
|
||||||
return (
|
return (
|
||||||
<SelectedViewLayout
|
<SelectedViewLayout agent={props.agent}>
|
||||||
agent={props.agent}
|
|
||||||
onSelectSettings={props.onSelectSettings}
|
|
||||||
selectedSettings={props.selectedSettings}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
className={cn("flex flex-col gap-4", AGENT_LIBRARY_SECTION_PADDING_X)}
|
className={cn("flex flex-col gap-4", AGENT_LIBRARY_SECTION_PADDING_X)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -33,8 +33,6 @@ interface Props {
|
|||||||
onSelectRun?: (id: string) => void;
|
onSelectRun?: (id: string) => void;
|
||||||
onClearSelectedRun?: () => void;
|
onClearSelectedRun?: () => void;
|
||||||
banner?: React.ReactNode;
|
banner?: React.ReactNode;
|
||||||
onSelectSettings?: () => void;
|
|
||||||
selectedSettings?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SelectedRunView({
|
export function SelectedRunView({
|
||||||
@@ -43,8 +41,6 @@ export function SelectedRunView({
|
|||||||
onSelectRun,
|
onSelectRun,
|
||||||
onClearSelectedRun,
|
onClearSelectedRun,
|
||||||
banner,
|
banner,
|
||||||
onSelectSettings,
|
|
||||||
selectedSettings,
|
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const { run, preset, isLoading, responseError, httpError } =
|
const { run, preset, isLoading, responseError, httpError } =
|
||||||
useSelectedRunView(agent.graph_id, runId);
|
useSelectedRunView(agent.graph_id, runId);
|
||||||
@@ -84,12 +80,7 @@ export function SelectedRunView({
|
|||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full gap-4">
|
<div className="flex h-full w-full gap-4">
|
||||||
<div className="flex min-h-0 min-w-0 flex-1 flex-col">
|
<div className="flex min-h-0 min-w-0 flex-1 flex-col">
|
||||||
<SelectedViewLayout
|
<SelectedViewLayout agent={agent} banner={banner}>
|
||||||
agent={agent}
|
|
||||||
banner={banner}
|
|
||||||
onSelectSettings={onSelectSettings}
|
|
||||||
selectedSettings={selectedSettings}
|
|
||||||
>
|
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<RunDetailHeader agent={agent} run={run} />
|
<RunDetailHeader agent={agent} run={run} />
|
||||||
|
|
||||||
|
|||||||
@@ -21,8 +21,6 @@ interface Props {
|
|||||||
scheduleId: string;
|
scheduleId: string;
|
||||||
onClearSelectedRun?: () => void;
|
onClearSelectedRun?: () => void;
|
||||||
banner?: React.ReactNode;
|
banner?: React.ReactNode;
|
||||||
onSelectSettings?: () => void;
|
|
||||||
selectedSettings?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SelectedScheduleView({
|
export function SelectedScheduleView({
|
||||||
@@ -30,8 +28,6 @@ export function SelectedScheduleView({
|
|||||||
scheduleId,
|
scheduleId,
|
||||||
onClearSelectedRun,
|
onClearSelectedRun,
|
||||||
banner,
|
banner,
|
||||||
onSelectSettings,
|
|
||||||
selectedSettings,
|
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const { schedule, isLoading, error } = useSelectedScheduleView(
|
const { schedule, isLoading, error } = useSelectedScheduleView(
|
||||||
agent.graph_id,
|
agent.graph_id,
|
||||||
@@ -76,12 +72,7 @@ export function SelectedScheduleView({
|
|||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full gap-4">
|
<div className="flex h-full w-full gap-4">
|
||||||
<div className="flex min-h-0 min-w-0 flex-1 flex-col">
|
<div className="flex min-h-0 min-w-0 flex-1 flex-col">
|
||||||
<SelectedViewLayout
|
<SelectedViewLayout agent={agent} banner={banner}>
|
||||||
agent={agent}
|
|
||||||
banner={banner}
|
|
||||||
onSelectSettings={onSelectSettings}
|
|
||||||
selectedSettings={selectedSettings}
|
|
||||||
>
|
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div className="flex w-full flex-col gap-0">
|
<div className="flex w-full flex-col gap-0">
|
||||||
<RunDetailHeader
|
<RunDetailHeader
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
|
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 { 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 { useAgentSafeMode } from "@/hooks/useAgentSafeMode";
|
||||||
import { SelectedViewLayout } from "../SelectedViewLayout";
|
import { ArrowLeftIcon } from "@phosphor-icons/react";
|
||||||
import { AGENT_LIBRARY_SECTION_PADDING_X } from "../../../helpers";
|
import { AGENT_LIBRARY_SECTION_PADDING_X } from "../../../helpers";
|
||||||
|
import { SelectedViewLayout } from "../SelectedViewLayout";
|
||||||
|
import { SystemCredentialsSection } from "./components/SystemCredentialsSection";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
agent: LibraryAgent;
|
agent: LibraryAgent;
|
||||||
@@ -16,8 +17,12 @@ export function SelectedSettingsView({ agent, onClearSelectedRun }: Props) {
|
|||||||
const { currentSafeMode, isPending, hasHITLBlocks, handleToggle } =
|
const { currentSafeMode, isPending, hasHITLBlocks, handleToggle } =
|
||||||
useAgentSafeMode(agent);
|
useAgentSafeMode(agent);
|
||||||
|
|
||||||
|
const hasCredentialsSchema =
|
||||||
|
agent.credentials_input_schema &&
|
||||||
|
Object.keys(agent.credentials_input_schema.properties || {}).length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SelectedViewLayout agent={agent} onSelectSettings={() => {}}>
|
<SelectedViewLayout agent={agent}>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div
|
<div
|
||||||
className={`${AGENT_LIBRARY_SECTION_PADDING_X} mb-8 flex items-center gap-3`}
|
className={`${AGENT_LIBRARY_SECTION_PADDING_X} mb-8 flex items-center gap-3`}
|
||||||
@@ -33,15 +38,8 @@ export function SelectedSettingsView({ agent, onClearSelectedRun }: Props) {
|
|||||||
<Text variant="h2">Agent Settings</Text>
|
<Text variant="h2">Agent Settings</Text>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={AGENT_LIBRARY_SECTION_PADDING_X}>
|
<div className={`${AGENT_LIBRARY_SECTION_PADDING_X} space-y-6`}>
|
||||||
{!hasHITLBlocks ? (
|
{hasHITLBlocks && (
|
||||||
<div className="rounded-xl border border-zinc-100 bg-white p-6">
|
|
||||||
<Text variant="body" className="text-muted-foreground">
|
|
||||||
This agent doesn't have any human-in-the-loop blocks, so
|
|
||||||
there are no settings to configure.
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<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 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 w-full items-start justify-between gap-4">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
@@ -60,6 +58,16 @@ export function SelectedSettingsView({ agent, onClearSelectedRun }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{hasCredentialsSchema && <SystemCredentialsSection agent={agent} />}
|
||||||
|
|
||||||
|
{!hasHITLBlocks && !hasCredentialsSchema && (
|
||||||
|
<div className="rounded-xl border border-zinc-100 bg-white p-6">
|
||||||
|
<Text variant="body" className="text-muted-foreground">
|
||||||
|
This agent doesn't have any configurable settings.
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</SelectedViewLayout>
|
</SelectedViewLayout>
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Text } from "@/components/atoms/Text/Text";
|
||||||
|
import { CredentialsMetaResponse } from "@/lib/autogpt-server-api/types";
|
||||||
|
import { toDisplayName } from "@/providers/agent-credentials/helper";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { CredentialsInput } from "../../../../components/modals/CredentialsInputs/CredentialsInputs";
|
||||||
|
import {
|
||||||
|
NONE_CREDENTIAL_MARKER,
|
||||||
|
useAgentCredentialPreferencesStore,
|
||||||
|
} from "../../../../stores/agentCredentialPreferencesStore";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
credentialKey: string;
|
||||||
|
agentId: string;
|
||||||
|
schema: any;
|
||||||
|
systemCredential: CredentialsMetaResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SystemCredentialRow({
|
||||||
|
credentialKey,
|
||||||
|
agentId,
|
||||||
|
schema,
|
||||||
|
systemCredential,
|
||||||
|
}: Props) {
|
||||||
|
const store = useAgentCredentialPreferencesStore();
|
||||||
|
|
||||||
|
// Initialize with saved preference or default to system credential
|
||||||
|
const savedPreference = store.getCredentialPreference(agentId, credentialKey);
|
||||||
|
const defaultCredential = {
|
||||||
|
id: systemCredential.id,
|
||||||
|
type: systemCredential.type,
|
||||||
|
provider: systemCredential.provider,
|
||||||
|
title: systemCredential.title,
|
||||||
|
};
|
||||||
|
|
||||||
|
// If saved preference is the NONE marker, use undefined (which CredentialsInput interprets as "None")
|
||||||
|
// Otherwise use saved preference or default
|
||||||
|
const [selectedCredential, setSelectedCredential] = useState<any>(
|
||||||
|
savedPreference === NONE_CREDENTIAL_MARKER
|
||||||
|
? undefined
|
||||||
|
: savedPreference || defaultCredential,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update when preference changes externally
|
||||||
|
useEffect(() => {
|
||||||
|
const preference = store.getCredentialPreference(agentId, credentialKey);
|
||||||
|
if (preference === NONE_CREDENTIAL_MARKER) {
|
||||||
|
setSelectedCredential(undefined);
|
||||||
|
} else if (preference) {
|
||||||
|
setSelectedCredential(preference);
|
||||||
|
} else {
|
||||||
|
setSelectedCredential(defaultCredential);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [credentialKey, agentId]);
|
||||||
|
|
||||||
|
const providerName = schema.credentials_provider?.[0] || "";
|
||||||
|
const displayName = toDisplayName(providerName);
|
||||||
|
|
||||||
|
function handleSelectCredentials(value: any) {
|
||||||
|
setSelectedCredential(value);
|
||||||
|
// Save preference:
|
||||||
|
// - undefined = explicitly selected "None" (save NONE_CREDENTIAL_MARKER)
|
||||||
|
// - null = use default system credential (fallback behavior, save null)
|
||||||
|
// - credential object = use this specific credential
|
||||||
|
if (value === undefined) {
|
||||||
|
// User explicitly selected "None" - save special marker
|
||||||
|
store.setCredentialPreference(
|
||||||
|
agentId,
|
||||||
|
credentialKey,
|
||||||
|
NONE_CREDENTIAL_MARKER,
|
||||||
|
);
|
||||||
|
} else if (value === null) {
|
||||||
|
// User cleared selection - use default system credential
|
||||||
|
store.setCredentialPreference(agentId, credentialKey, null);
|
||||||
|
} else {
|
||||||
|
// User selected a credential
|
||||||
|
store.setCredentialPreference(agentId, credentialKey, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="rounded-lg border border-zinc-100 bg-zinc-50/50 px-4 pb-2 pt-4">
|
||||||
|
<Text variant="body-medium" className="mb-2 ml-2">
|
||||||
|
{displayName}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<CredentialsInput
|
||||||
|
schema={{ ...schema, discriminator: undefined }}
|
||||||
|
selectedCredentials={selectedCredential}
|
||||||
|
onSelectCredentials={handleSelectCredentials}
|
||||||
|
showTitle={false}
|
||||||
|
isOptional
|
||||||
|
allowSystemCredentials={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
|
||||||
|
import { Text } from "@/components/atoms/Text/Text";
|
||||||
|
import { useAgentSystemCredentials } from "../../../../hooks/useAgentSystemCredentials";
|
||||||
|
import { SystemCredentialRow } from "./SystemCredentialRow";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
agent: LibraryAgent;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SystemCredentialsSection({ agent }: Props) {
|
||||||
|
const { hasSystemCredentials, systemCredentials, isLoading } =
|
||||||
|
useAgentSystemCredentials(agent);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="flex w-full max-w-2xl flex-col items-start gap-4 rounded-xl border border-zinc-100 bg-white p-6">
|
||||||
|
<Text variant="large-semibold">System Credentials</Text>
|
||||||
|
<Text variant="body" className="text-muted-foreground">
|
||||||
|
Loading credentials...
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasSystemCredentials) return null;
|
||||||
|
|
||||||
|
// Group by credential field key (from schema) to show one row per field
|
||||||
|
const credentialsByField = systemCredentials.reduce(
|
||||||
|
(acc, item) => {
|
||||||
|
if (!acc[item.key]) {
|
||||||
|
acc[item.key] = item;
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, (typeof systemCredentials)[0]>,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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>
|
||||||
|
<Text variant="large-semibold">System Credentials</Text>
|
||||||
|
<Text variant="body" className="mt-1 text-muted-foreground">
|
||||||
|
These credentials are managed by AutoGPT and used by the agent to
|
||||||
|
access various services. You can switch to your own credentials if
|
||||||
|
preferred.
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<div className="w-full space-y-4">
|
||||||
|
{Object.entries(credentialsByField).map(([fieldKey, item]) => (
|
||||||
|
<SystemCredentialRow
|
||||||
|
key={fieldKey}
|
||||||
|
credentialKey={fieldKey}
|
||||||
|
agentId={agent.id.toString()}
|
||||||
|
schema={item.schema}
|
||||||
|
systemCredential={item.credential}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
|
||||||
|
import { Breadcrumbs } from "@/components/molecules/Breadcrumbs/Breadcrumbs";
|
||||||
import { AGENT_LIBRARY_SECTION_PADDING_X } from "../../helpers";
|
import { AGENT_LIBRARY_SECTION_PADDING_X } from "../../helpers";
|
||||||
|
import { AgentSettingsModal } from "../modals/AgentSettingsModal/AgentSettingsModal";
|
||||||
import { SectionWrap } from "../other/SectionWrap";
|
import { SectionWrap } from "../other/SectionWrap";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -9,8 +9,6 @@ interface Props {
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
banner?: React.ReactNode;
|
banner?: React.ReactNode;
|
||||||
additionalBreadcrumb?: { name: string; link?: string };
|
additionalBreadcrumb?: { name: string; link?: string };
|
||||||
onSelectSettings?: () => void;
|
|
||||||
selectedSettings?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SelectedViewLayout(props: Props) {
|
export function SelectedViewLayout(props: Props) {
|
||||||
@@ -19,8 +17,8 @@ export function SelectedViewLayout(props: Props) {
|
|||||||
<div
|
<div
|
||||||
className={`${AGENT_LIBRARY_SECTION_PADDING_X} flex-shrink-0 border-b border-zinc-100 pb-0 lg:pb-4`}
|
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>}
|
{props.banner}
|
||||||
<div className="relative flex w-fit items-center gap-2">
|
<div className="relative flex w-full items-center justify-between">
|
||||||
<Breadcrumbs
|
<Breadcrumbs
|
||||||
items={[
|
items={[
|
||||||
{ name: "My Library", link: "/library" },
|
{ name: "My Library", link: "/library" },
|
||||||
@@ -33,15 +31,9 @@ export function SelectedViewLayout(props: Props) {
|
|||||||
: []),
|
: []),
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
{props.agent && props.onSelectSettings && (
|
<div className="absolute right-0">
|
||||||
<div className="absolute -right-8">
|
<AgentSettingsModal agent={props.agent} />
|
||||||
<AgentSettingsButton
|
</div>
|
||||||
agent={props.agent}
|
|
||||||
onSelectSettings={props.onSelectSettings}
|
|
||||||
selected={props.selectedSettings}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex min-h-0 flex-1 flex-col overflow-y-auto overflow-x-visible">
|
<div className="flex min-h-0 flex-1 flex-col overflow-y-auto overflow-x-visible">
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { CredentialsMetaInput } from "@/lib/autogpt-server-api/types";
|
||||||
|
import { storage } from "@/services/storage/local-storage";
|
||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
// Special marker to indicate "None" was explicitly selected
|
||||||
|
export const NONE_CREDENTIAL_MARKER = { __none__: true } as const;
|
||||||
|
|
||||||
|
type AgentCredentialPreferences = Record<
|
||||||
|
string,
|
||||||
|
CredentialsMetaInput | null | typeof NONE_CREDENTIAL_MARKER
|
||||||
|
>;
|
||||||
|
|
||||||
|
const STORAGE_KEY_PREFIX = "agent_credential_prefs_";
|
||||||
|
|
||||||
|
function getStorageKey(agentId: string): string {
|
||||||
|
return `${STORAGE_KEY_PREFIX}${agentId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadPreferences(agentId: string): AgentCredentialPreferences {
|
||||||
|
const key = getStorageKey(agentId);
|
||||||
|
const stored = storage.get(key as any);
|
||||||
|
if (!stored) return {};
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(stored);
|
||||||
|
// Convert serialized NONE markers back to the constant
|
||||||
|
const result: AgentCredentialPreferences = {};
|
||||||
|
for (const [key, value] of Object.entries(parsed)) {
|
||||||
|
if (
|
||||||
|
value &&
|
||||||
|
typeof value === "object" &&
|
||||||
|
"__none__" in value &&
|
||||||
|
(value as any).__none__ === true
|
||||||
|
) {
|
||||||
|
result[key] = NONE_CREDENTIAL_MARKER;
|
||||||
|
} else {
|
||||||
|
result[key] = value as CredentialsMetaInput | null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
} catch {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function savePreferences(
|
||||||
|
agentId: string,
|
||||||
|
preferences: AgentCredentialPreferences,
|
||||||
|
): void {
|
||||||
|
const key = getStorageKey(agentId);
|
||||||
|
storage.set(key as any, JSON.stringify(preferences));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAgentCredentialPreferences(agentId: string) {
|
||||||
|
const [preferences, setPreferences] = useState<AgentCredentialPreferences>(
|
||||||
|
() => loadPreferences(agentId),
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const loaded = loadPreferences(agentId);
|
||||||
|
setPreferences(loaded);
|
||||||
|
}, [agentId]);
|
||||||
|
|
||||||
|
const setCredentialPreference = useCallback(
|
||||||
|
(
|
||||||
|
credentialKey: string,
|
||||||
|
credential: CredentialsMetaInput | null | typeof NONE_CREDENTIAL_MARKER,
|
||||||
|
) => {
|
||||||
|
setPreferences((prev) => {
|
||||||
|
const updated = {
|
||||||
|
...prev,
|
||||||
|
[credentialKey]: credential,
|
||||||
|
};
|
||||||
|
savePreferences(agentId, updated);
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[agentId],
|
||||||
|
);
|
||||||
|
|
||||||
|
const getCredentialPreference = useCallback(
|
||||||
|
(
|
||||||
|
credentialKey: string,
|
||||||
|
): CredentialsMetaInput | null | typeof NONE_CREDENTIAL_MARKER => {
|
||||||
|
return preferences[credentialKey] ?? null;
|
||||||
|
},
|
||||||
|
[preferences],
|
||||||
|
);
|
||||||
|
|
||||||
|
const clearPreference = useCallback(
|
||||||
|
(credentialKey: string) => {
|
||||||
|
setPreferences((prev) => {
|
||||||
|
const updated = { ...prev };
|
||||||
|
delete updated[credentialKey];
|
||||||
|
savePreferences(agentId, updated);
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[agentId],
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
preferences,
|
||||||
|
setCredentialPreference,
|
||||||
|
getCredentialPreference,
|
||||||
|
clearPreference,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
|
||||||
|
import { CredentialsProvidersContext } from "@/providers/agent-credentials/credentials-provider";
|
||||||
|
import { toDisplayName } from "@/providers/agent-credentials/helper";
|
||||||
|
import { useContext, useMemo } from "react";
|
||||||
|
import { getSystemCredentials } from "../components/modals/CredentialsInputs/helpers";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook to check if an agent is missing required SYSTEM credentials.
|
||||||
|
* This is only used to block "New Task" buttons.
|
||||||
|
* User credential validation is handled separately in RunAgentModal.
|
||||||
|
*/
|
||||||
|
export function useAgentMissingCredentials(
|
||||||
|
agent: LibraryAgent | null | undefined,
|
||||||
|
) {
|
||||||
|
const allProviders = useContext(CredentialsProvidersContext);
|
||||||
|
|
||||||
|
const result = useMemo(() => {
|
||||||
|
if (
|
||||||
|
!agent ||
|
||||||
|
!agent.id ||
|
||||||
|
!allProviders ||
|
||||||
|
!agent.credentials_input_schema?.properties
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
hasMissingCredentials: false,
|
||||||
|
missingCredentials: [],
|
||||||
|
isLoading: !allProviders || !agent,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const properties = agent.credentials_input_schema.properties as Record<
|
||||||
|
string,
|
||||||
|
any
|
||||||
|
>;
|
||||||
|
const requiredCredentials = new Set(
|
||||||
|
(agent.credentials_input_schema.required as string[]) || [],
|
||||||
|
);
|
||||||
|
|
||||||
|
const missingCredentials: Array<{
|
||||||
|
key: string;
|
||||||
|
providerDisplayName: string;
|
||||||
|
}> = [];
|
||||||
|
|
||||||
|
for (const [key, schema] of Object.entries(properties)) {
|
||||||
|
const isRequired = requiredCredentials.has(key);
|
||||||
|
if (!isRequired) continue; // Only check required credentials
|
||||||
|
|
||||||
|
const providerNames = schema.credentials_provider || [];
|
||||||
|
const supportedTypes = schema.credentials_types || [];
|
||||||
|
const requiredScopes = schema.credentials_scopes;
|
||||||
|
|
||||||
|
let hasSystemCredential = false;
|
||||||
|
|
||||||
|
// Check if any provider has a system credential available
|
||||||
|
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 = new Set(requiredScopes).isSubsetOf(
|
||||||
|
grantedScopes,
|
||||||
|
);
|
||||||
|
if (!hasAllRequiredScopes) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// If there's a system credential available, it's not missing
|
||||||
|
if (matchingSystemCreds.length > 0) {
|
||||||
|
hasSystemCredential = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no system credential available, mark as missing
|
||||||
|
if (!hasSystemCredential) {
|
||||||
|
const providerName = providerNames[0] || "";
|
||||||
|
missingCredentials.push({
|
||||||
|
key,
|
||||||
|
providerDisplayName: toDisplayName(providerName),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
hasMissingCredentials: missingCredentials.length > 0,
|
||||||
|
missingCredentials,
|
||||||
|
isLoading: false,
|
||||||
|
};
|
||||||
|
}, [allProviders, agent?.credentials_input_schema, agent?.id]);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
|
||||||
|
import { CredentialsMetaResponse } from "@/lib/autogpt-server-api/types";
|
||||||
|
import {
|
||||||
|
CredentialsProviderData,
|
||||||
|
CredentialsProvidersContext,
|
||||||
|
} from "@/providers/agent-credentials/credentials-provider";
|
||||||
|
import { toDisplayName } from "@/providers/agent-credentials/helper";
|
||||||
|
import { useContext, useMemo } from "react";
|
||||||
|
import {
|
||||||
|
filterSystemCredentials,
|
||||||
|
getSystemCredentials,
|
||||||
|
} from "../components/modals/CredentialsInputs/helpers";
|
||||||
|
|
||||||
|
interface SystemCredentialInfo {
|
||||||
|
key: string;
|
||||||
|
provider: string;
|
||||||
|
schema: any;
|
||||||
|
credential: CredentialsMetaResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MissingCredentialInfo {
|
||||||
|
key: string;
|
||||||
|
provider: string;
|
||||||
|
providerDisplayName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UseAgentSystemCredentialsResult {
|
||||||
|
hasSystemCredentials: boolean;
|
||||||
|
systemCredentials: SystemCredentialInfo[];
|
||||||
|
hasMissingSystemCredentials: boolean;
|
||||||
|
missingSystemCredentials: MissingCredentialInfo[];
|
||||||
|
isLoading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAgentSystemCredentials(
|
||||||
|
agent: LibraryAgent,
|
||||||
|
): UseAgentSystemCredentialsResult {
|
||||||
|
const allProviders = useContext(CredentialsProvidersContext);
|
||||||
|
|
||||||
|
const result = useMemo(() => {
|
||||||
|
const empty = {
|
||||||
|
hasSystemCredentials: false,
|
||||||
|
systemCredentials: [],
|
||||||
|
hasMissingSystemCredentials: false,
|
||||||
|
missingSystemCredentials: [],
|
||||||
|
isLoading: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!agent.credentials_input_schema?.properties) return empty;
|
||||||
|
|
||||||
|
if (!allProviders) return { ...empty, isLoading: true };
|
||||||
|
|
||||||
|
const properties = agent.credentials_input_schema.properties as Record<
|
||||||
|
string,
|
||||||
|
any
|
||||||
|
>;
|
||||||
|
const requiredCredentials = new Set(
|
||||||
|
(agent.credentials_input_schema.required as string[]) || [],
|
||||||
|
);
|
||||||
|
const systemCredentials: SystemCredentialInfo[] = [];
|
||||||
|
const missingSystemCredentials: MissingCredentialInfo[] = [];
|
||||||
|
|
||||||
|
for (const [key, schema] of Object.entries(properties)) {
|
||||||
|
const providerNames = schema.credentials_provider || [];
|
||||||
|
const isRequired = requiredCredentials.has(key);
|
||||||
|
const supportedTypes = schema.credentials_types || [];
|
||||||
|
|
||||||
|
for (const providerName of providerNames) {
|
||||||
|
const providerData: CredentialsProviderData | undefined =
|
||||||
|
allProviders[providerName];
|
||||||
|
|
||||||
|
if (!providerData) {
|
||||||
|
// Provider not loaded yet - don't mark as missing, wait for load
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for system credentials - now backend always returns them with is_system: true
|
||||||
|
const systemCreds = getSystemCredentials(providerData.savedCredentials);
|
||||||
|
const userCreds = filterSystemCredentials(
|
||||||
|
providerData.savedCredentials,
|
||||||
|
);
|
||||||
|
|
||||||
|
const matchingSystemCreds = systemCreds.filter((cred) =>
|
||||||
|
supportedTypes.includes(cred.type),
|
||||||
|
);
|
||||||
|
const matchingUserCreds = userCreds.filter((cred) =>
|
||||||
|
supportedTypes.includes(cred.type),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add system credentials if they exist (even if not configured, backend returns them)
|
||||||
|
for (const cred of matchingSystemCreds) {
|
||||||
|
systemCredentials.push({
|
||||||
|
key,
|
||||||
|
provider: providerName,
|
||||||
|
schema,
|
||||||
|
credential: cred,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only mark as missing if it's required AND there are NO credentials available
|
||||||
|
// (neither system nor user). This is a true "missing" state.
|
||||||
|
// Note: We don't block based on this anymore since the run modal
|
||||||
|
// has its own validation (allRequiredInputsAreSet)
|
||||||
|
if (
|
||||||
|
isRequired &&
|
||||||
|
matchingSystemCreds.length === 0 &&
|
||||||
|
matchingUserCreds.length === 0
|
||||||
|
) {
|
||||||
|
missingSystemCredentials.push({
|
||||||
|
key,
|
||||||
|
provider: providerName,
|
||||||
|
providerDisplayName: toDisplayName(providerName),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
hasSystemCredentials: systemCredentials.length > 0,
|
||||||
|
systemCredentials,
|
||||||
|
hasMissingSystemCredentials: missingSystemCredentials.length > 0,
|
||||||
|
missingSystemCredentials,
|
||||||
|
isLoading: false,
|
||||||
|
};
|
||||||
|
}, [agent.credentials_input_schema, allProviders]);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
import { CredentialsMetaInput } from "@/lib/autogpt-server-api/types";
|
||||||
|
import { storage } from "@/services/storage/local-storage";
|
||||||
|
import { create } from "zustand";
|
||||||
|
import { createJSONStorage, persist } from "zustand/middleware";
|
||||||
|
|
||||||
|
// Special marker to indicate "None" was explicitly selected
|
||||||
|
export const NONE_CREDENTIAL_MARKER = { __none__: true } as const;
|
||||||
|
|
||||||
|
type CredentialPreference =
|
||||||
|
| CredentialsMetaInput
|
||||||
|
| null
|
||||||
|
| typeof NONE_CREDENTIAL_MARKER;
|
||||||
|
|
||||||
|
type AgentCredentialPreferences = Record<string, CredentialPreference>;
|
||||||
|
|
||||||
|
interface AgentCredentialPreferencesStore {
|
||||||
|
preferences: Record<string, AgentCredentialPreferences>; // agentId -> preferences
|
||||||
|
setCredentialPreference: (
|
||||||
|
agentId: string,
|
||||||
|
credentialKey: string,
|
||||||
|
credential: CredentialPreference,
|
||||||
|
) => void;
|
||||||
|
getCredentialPreference: (
|
||||||
|
agentId: string,
|
||||||
|
credentialKey: string,
|
||||||
|
) => CredentialPreference;
|
||||||
|
clearPreference: (agentId: string, credentialKey: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const STORAGE_KEY = "agent_credential_preferences";
|
||||||
|
|
||||||
|
// Custom storage adapter for localStorage
|
||||||
|
const customStorage = {
|
||||||
|
getItem: (name: string): string | null => {
|
||||||
|
return storage.get(name as any) || null;
|
||||||
|
},
|
||||||
|
setItem: (name: string, value: string): void => {
|
||||||
|
storage.set(name as any, value);
|
||||||
|
},
|
||||||
|
removeItem: (name: string): void => {
|
||||||
|
storage.clean(name as any);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useAgentCredentialPreferencesStore =
|
||||||
|
create<AgentCredentialPreferencesStore>()(
|
||||||
|
persist(
|
||||||
|
(set, get) => ({
|
||||||
|
preferences: {},
|
||||||
|
|
||||||
|
setCredentialPreference: (agentId, credentialKey, credential) => {
|
||||||
|
set((state) => {
|
||||||
|
const agentPrefs = state.preferences[agentId] || {};
|
||||||
|
const updated = {
|
||||||
|
...state.preferences,
|
||||||
|
[agentId]: {
|
||||||
|
...agentPrefs,
|
||||||
|
[credentialKey]: credential,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return { preferences: updated };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
getCredentialPreference: (agentId, credentialKey) => {
|
||||||
|
const state = get();
|
||||||
|
const pref = state.preferences[agentId]?.[credentialKey];
|
||||||
|
// Convert serialized NONE marker back to constant
|
||||||
|
if (
|
||||||
|
pref &&
|
||||||
|
typeof pref === "object" &&
|
||||||
|
"__none__" in pref &&
|
||||||
|
(pref as any).__none__ === true &&
|
||||||
|
pref !== NONE_CREDENTIAL_MARKER
|
||||||
|
) {
|
||||||
|
return NONE_CREDENTIAL_MARKER;
|
||||||
|
}
|
||||||
|
return pref ?? null;
|
||||||
|
},
|
||||||
|
|
||||||
|
clearPreference: (agentId, credentialKey) => {
|
||||||
|
set((state) => {
|
||||||
|
const agentPrefs = state.preferences[agentId] || {};
|
||||||
|
const updated = { ...agentPrefs };
|
||||||
|
delete updated[credentialKey];
|
||||||
|
return {
|
||||||
|
preferences: {
|
||||||
|
...state.preferences,
|
||||||
|
[agentId]: updated,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: STORAGE_KEY,
|
||||||
|
storage: createJSONStorage(() => customStorage),
|
||||||
|
// Transform on rehydrate to convert NONE markers
|
||||||
|
onRehydrateStorage: () => (state, error) => {
|
||||||
|
if (error || !state) {
|
||||||
|
console.error("Failed to rehydrate credential preferences:", error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Convert serialized NONE markers back to constant
|
||||||
|
const converted: Record<string, AgentCredentialPreferences> = {};
|
||||||
|
for (const [agentId, prefs] of Object.entries(
|
||||||
|
state.preferences || {},
|
||||||
|
)) {
|
||||||
|
const convertedPrefs: AgentCredentialPreferences = {};
|
||||||
|
for (const [key, value] of Object.entries(prefs)) {
|
||||||
|
if (
|
||||||
|
value &&
|
||||||
|
typeof value === "object" &&
|
||||||
|
"__none__" in value &&
|
||||||
|
(value as any).__none__ === true &&
|
||||||
|
value !== NONE_CREDENTIAL_MARKER
|
||||||
|
) {
|
||||||
|
convertedPrefs[key] = NONE_CREDENTIAL_MARKER;
|
||||||
|
} else {
|
||||||
|
convertedPrefs[key] = value as CredentialPreference;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
converted[agentId] = convertedPrefs;
|
||||||
|
}
|
||||||
|
// Update state with converted preferences
|
||||||
|
if (
|
||||||
|
Object.keys(converted).length > 0 ||
|
||||||
|
Object.keys(state.preferences || {}).length > 0
|
||||||
|
) {
|
||||||
|
state.preferences = converted;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
@@ -6792,6 +6792,12 @@
|
|||||||
"anyOf": [{ "type": "string" }, { "type": "null" }],
|
"anyOf": [{ "type": "string" }, { "type": "null" }],
|
||||||
"title": "Host",
|
"title": "Host",
|
||||||
"description": "Host pattern for host-scoped credentials"
|
"description": "Host pattern for host-scoped credentials"
|
||||||
|
},
|
||||||
|
"is_system": {
|
||||||
|
"type": "boolean",
|
||||||
|
"title": "Is System",
|
||||||
|
"description": "Whether this is a system-managed credential",
|
||||||
|
"default": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export function Button(props: ButtonProps) {
|
|||||||
rightIcon,
|
rightIcon,
|
||||||
children,
|
children,
|
||||||
as = "button",
|
as = "button",
|
||||||
|
asChild: _asChild, // Destructure to prevent passing to DOM
|
||||||
...restProps
|
...restProps
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,12 @@ export function DrawerWrap({
|
|||||||
>
|
>
|
||||||
{title ? (
|
{title ? (
|
||||||
<Drawer.Title className={drawerStyles.title}>{title}</Drawer.Title>
|
<Drawer.Title className={drawerStyles.title}>{title}</Drawer.Title>
|
||||||
) : null}
|
) : (
|
||||||
|
<span className="sr-only">
|
||||||
|
{/* Title is required for a11y compliance even if not displayed so screen readers can announce it */}
|
||||||
|
<Drawer.Title>{title}</Drawer.Title>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
{!isForceOpen ? (
|
{!isForceOpen ? (
|
||||||
title ? (
|
title ? (
|
||||||
|
|||||||
@@ -593,6 +593,7 @@ export type CredentialsMetaResponse = {
|
|||||||
scopes?: Array<string>;
|
scopes?: Array<string>;
|
||||||
username?: string;
|
username?: string;
|
||||||
host?: string;
|
host?: string;
|
||||||
|
is_system?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
/* Mirror of backend/server/integrations/router.py:CredentialsDeletionResponse */
|
/* Mirror of backend/server/integrations/router.py:CredentialsDeletionResponse */
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { createContext, useCallback, useEffect, useState } from "react";
|
import { useToastOnFail } from "@/components/molecules/Toast/use-toast";
|
||||||
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
|
|
||||||
import {
|
import {
|
||||||
APIKeyCredentials,
|
APIKeyCredentials,
|
||||||
CredentialsDeleteNeedConfirmationResponse,
|
CredentialsDeleteNeedConfirmationResponse,
|
||||||
@@ -10,8 +9,9 @@ import {
|
|||||||
UserPasswordCredentials,
|
UserPasswordCredentials,
|
||||||
} from "@/lib/autogpt-server-api";
|
} from "@/lib/autogpt-server-api";
|
||||||
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
|
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 { toDisplayName } from "@/providers/agent-credentials/helper";
|
||||||
|
import { createContext, useCallback, useEffect, useState } from "react";
|
||||||
|
|
||||||
type APIKeyCredentialsCreatable = Omit<
|
type APIKeyCredentialsCreatable = Omit<
|
||||||
APIKeyCredentials,
|
APIKeyCredentials,
|
||||||
@@ -72,6 +72,8 @@ export default function CredentialsProvider({
|
|||||||
const api = useBackendAPI();
|
const api = useBackendAPI();
|
||||||
const onFailToast = useToastOnFail();
|
const onFailToast = useToastOnFail();
|
||||||
|
|
||||||
|
console.log("providers", providers);
|
||||||
|
|
||||||
const addCredentials = useCallback(
|
const addCredentials = useCallback(
|
||||||
(
|
(
|
||||||
provider: CredentialsProviderName,
|
provider: CredentialsProviderName,
|
||||||
@@ -218,17 +220,7 @@ export default function CredentialsProvider({
|
|||||||
[api, onFailToast],
|
[api, onFailToast],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Fetch provider names on mount
|
const loadCredentials = useCallback(() => {
|
||||||
useEffect(() => {
|
|
||||||
api
|
|
||||||
.listProviders()
|
|
||||||
.then((names) => {
|
|
||||||
setProviderNames(names);
|
|
||||||
})
|
|
||||||
.catch(onFailToast("load provider names"));
|
|
||||||
}, [api, onFailToast]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isLoggedIn || providerNames.length === 0) {
|
if (!isLoggedIn || providerNames.length === 0) {
|
||||||
if (isLoggedIn == false) setProviders({});
|
if (isLoggedIn == false) setProviders({});
|
||||||
return;
|
return;
|
||||||
@@ -288,6 +280,20 @@ export default function CredentialsProvider({
|
|||||||
onFailToast,
|
onFailToast,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Fetch provider names on mount
|
||||||
|
useEffect(() => {
|
||||||
|
api
|
||||||
|
.listProviders()
|
||||||
|
.then((names) => {
|
||||||
|
setProviderNames(names);
|
||||||
|
})
|
||||||
|
.catch(onFailToast("Load provider names"));
|
||||||
|
}, [api, onFailToast]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadCredentials();
|
||||||
|
}, [loadCredentials]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CredentialsProvidersContext.Provider value={providers}>
|
<CredentialsProvidersContext.Provider value={providers}>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
Reference in New Issue
Block a user