Compare commits

..

1 Commits

Author SHA1 Message Date
Reinier van der Leer
3b09a94e3f feat(frontend/builder): Add sub-graph update UX (#11631)
[OPEN-2743: Ability to Update Sub-Agents in Graph (Without
Re-Adding)](https://linear.app/autogpt/issue/OPEN-2743/ability-to-update-sub-agents-in-graph-without-re-adding)

Updating sub-graphs is a cumbersome experience at the moment, this
should help. :)

Demo in Builder v2:


https://github.com/user-attachments/assets/df564f32-4d1d-432c-bb91-fe9065068360


https://github.com/user-attachments/assets/f169471a-1f22-46e9-a958-ddb72d3f65af


### Changes 🏗️

- Add sub-graph update banner with I/O incompatibility notification and
resolution mode
  - Red visual indicators for broken inputs/outputs and edges
  - Update bars and tooltips show compatibility details
- Sub-agent update UI with compatibility checks, incompatibility dialog,
and guided resolution workflow
- Resolution mode banner guiding users to remove incompatible
connections
- Visual controls to stage/apply updates and auto-apply when broken
connections are fixed
  
  Technical:
- Builder v1: Add `CustomNode` > `IncompatibilityDialog` +
`SubAgentUpdateBar` sub-components
- Builder v2: Add `SubAgentUpdateFeature` + `ResolutionModeBar` +
`IncompatibleUpdateDialog` + `useSubAgentUpdateState` sub-components
  - Add `useSubAgentUpdate` hook

- Related fixes in Builder v1:
  - Fix static edges not rendering as such
  - Fix edge styling not applying
- Related fixes in Builder v2:
  - Fix excess spacing for nested node input fields

Other:
- "Retry" button in error view now reloads the page instead of
navigating to `/marketplace`

### Checklist 📋

#### For code changes:
- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [x] I have tested my changes according to the test plan:
  - CI for existing frontend UX flows
- [x] Updating to a new sub-agent version with compatibility issues: UX
flow works
- [x] Updating to a new sub-agent version with *no* compatibility
issues: works
  - [x] Designer approves of the look

---------

Co-authored-by: abhi1992002 <abhimanyu1992002@gmail.com>
Co-authored-by: Abhimanyu Yadav <122007096+Abhi1992002@users.noreply.github.com>
2026-01-14 13:25:20 +00:00
93 changed files with 3101 additions and 2009 deletions

View File

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

View File

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

View File

@@ -3,13 +3,6 @@ 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",

View File

@@ -32,7 +32,6 @@
"@hookform/resolvers": "5.2.2", "@hookform/resolvers": "5.2.2",
"@next/third-parties": "15.4.6", "@next/third-parties": "15.4.6",
"@phosphor-icons/react": "2.1.10", "@phosphor-icons/react": "2.1.10",
"@radix-ui/react-accordion": "1.2.12",
"@radix-ui/react-alert-dialog": "1.1.15", "@radix-ui/react-alert-dialog": "1.1.15",
"@radix-ui/react-avatar": "1.1.10", "@radix-ui/react-avatar": "1.1.10",
"@radix-ui/react-checkbox": "1.3.3", "@radix-ui/react-checkbox": "1.3.3",
@@ -118,7 +117,6 @@
}, },
"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",
@@ -142,7 +140,6 @@
"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",
@@ -150,7 +147,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": "8.0.1", "require-in-the-middle": "7.5.2",
"storybook": "9.1.5", "storybook": "9.1.5",
"tailwindcss": "3.4.17", "tailwindcss": "3.4.17",
"typescript": "5.9.3" "typescript": "5.9.3"
@@ -160,10 +157,5 @@
"public" "public"
] ]
}, },
"pnpm": {
"overrides": {
"@opentelemetry/instrumentation": "0.209.0"
}
},
"packageManager": "pnpm@10.20.0+sha512.cf9998222162dd85864d0a8102e7892e7ba4ceadebbf5a31f9c2fce48dfce317a9c53b9f6464d1ef9042cba2e02ae02a9f7c143a2b438cd93c91840f0192b9dd" "packageManager": "pnpm@10.20.0+sha512.cf9998222162dd85864d0a8102e7892e7ba4ceadebbf5a31f9c2fce48dfce317a9c53b9f6464d1ef9042cba2e02ae02a9f7c143a2b438cd93c91840f0192b9dd"
} }

View File

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

View File

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

View File

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

View File

@@ -81,18 +81,16 @@ export const RunInputDialog = ({
Inputs Inputs
</Text> </Text>
</div> </div>
<div className="px-2"> <FormRenderer
<FormRenderer jsonSchema={inputSchema as RJSFSchema}
jsonSchema={inputSchema as RJSFSchema} handleChange={(v) => handleInputChange(v.formData)}
handleChange={(v) => handleInputChange(v.formData)} uiSchema={uiSchema}
uiSchema={uiSchema} initialValues={{}}
initialValues={{}} formContext={{
formContext={{ showHandles: false,
showHandles: false, size: "large",
size: "large", }}
}} />
/>
</div>
</div> </div>
)} )}

View File

@@ -3,6 +3,7 @@ import { useGetV2GetSpecificBlocks } from "@/app/api/__generated__/endpoints/def
import { import {
useGetV1GetExecutionDetails, useGetV1GetExecutionDetails,
useGetV1GetSpecificGraph, useGetV1GetSpecificGraph,
useGetV1ListUserGraphs,
} from "@/app/api/__generated__/endpoints/graphs/graphs"; } from "@/app/api/__generated__/endpoints/graphs/graphs";
import { BlockInfo } from "@/app/api/__generated__/models/blockInfo"; import { BlockInfo } from "@/app/api/__generated__/models/blockInfo";
import { GraphModel } from "@/app/api/__generated__/models/graphModel"; import { GraphModel } from "@/app/api/__generated__/models/graphModel";
@@ -17,6 +18,7 @@ import { useReactFlow } from "@xyflow/react";
import { useControlPanelStore } from "../../../stores/controlPanelStore"; import { useControlPanelStore } from "../../../stores/controlPanelStore";
import { useHistoryStore } from "../../../stores/historyStore"; import { useHistoryStore } from "../../../stores/historyStore";
import { AgentExecutionStatus } from "@/app/api/__generated__/models/agentExecutionStatus"; import { AgentExecutionStatus } from "@/app/api/__generated__/models/agentExecutionStatus";
import { okData } from "@/app/api/helpers";
export const useFlow = () => { export const useFlow = () => {
const [isLocked, setIsLocked] = useState(false); const [isLocked, setIsLocked] = useState(false);
@@ -36,6 +38,9 @@ export const useFlow = () => {
const setGraphExecutionStatus = useGraphStore( const setGraphExecutionStatus = useGraphStore(
useShallow((state) => state.setGraphExecutionStatus), useShallow((state) => state.setGraphExecutionStatus),
); );
const setAvailableSubGraphs = useGraphStore(
useShallow((state) => state.setAvailableSubGraphs),
);
const updateEdgeBeads = useEdgeStore( const updateEdgeBeads = useEdgeStore(
useShallow((state) => state.updateEdgeBeads), useShallow((state) => state.updateEdgeBeads),
); );
@@ -62,6 +67,11 @@ export const useFlow = () => {
}, },
); );
// Fetch all available graphs for sub-agent update detection
const { data: availableGraphs } = useGetV1ListUserGraphs({
query: { select: okData },
});
const { data: graph, isLoading: isGraphLoading } = useGetV1GetSpecificGraph( const { data: graph, isLoading: isGraphLoading } = useGetV1GetSpecificGraph(
flowID ?? "", flowID ?? "",
flowVersion !== null ? { version: flowVersion } : {}, flowVersion !== null ? { version: flowVersion } : {},
@@ -116,10 +126,18 @@ export const useFlow = () => {
} }
}, [graph]); }, [graph]);
// Update available sub-graphs in store for sub-agent update detection
useEffect(() => {
if (availableGraphs) {
setAvailableSubGraphs(availableGraphs);
}
}, [availableGraphs, setAvailableSubGraphs]);
// adding nodes // adding nodes
useEffect(() => { useEffect(() => {
if (customNodes.length > 0) { if (customNodes.length > 0) {
useNodeStore.getState().setNodes([]); useNodeStore.getState().setNodes([]);
useNodeStore.getState().clearResolutionState();
addNodes(customNodes); addNodes(customNodes);
// Sync hardcoded values with handle IDs. // Sync hardcoded values with handle IDs.
@@ -203,6 +221,7 @@ export const useFlow = () => {
useEffect(() => { useEffect(() => {
return () => { return () => {
useNodeStore.getState().setNodes([]); useNodeStore.getState().setNodes([]);
useNodeStore.getState().clearResolutionState();
useEdgeStore.getState().setEdges([]); useEdgeStore.getState().setEdges([]);
useGraphStore.getState().reset(); useGraphStore.getState().reset();
useEdgeStore.getState().resetEdgeBeads(); useEdgeStore.getState().resetEdgeBeads();

View File

@@ -8,6 +8,7 @@ import {
getBezierPath, getBezierPath,
} from "@xyflow/react"; } from "@xyflow/react";
import { useEdgeStore } from "@/app/(platform)/build/stores/edgeStore"; import { useEdgeStore } from "@/app/(platform)/build/stores/edgeStore";
import { useNodeStore } from "@/app/(platform)/build/stores/nodeStore";
import { XIcon } from "@phosphor-icons/react"; import { XIcon } from "@phosphor-icons/react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { NodeExecutionResult } from "@/lib/autogpt-server-api"; import { NodeExecutionResult } from "@/lib/autogpt-server-api";
@@ -35,6 +36,8 @@ const CustomEdge = ({
selected, selected,
}: EdgeProps<CustomEdge>) => { }: EdgeProps<CustomEdge>) => {
const removeConnection = useEdgeStore((state) => state.removeEdge); const removeConnection = useEdgeStore((state) => state.removeEdge);
// Subscribe to the brokenEdgeIDs map and check if this edge is broken across any node
const isBroken = useNodeStore((state) => state.isEdgeBroken(id));
const [isHovered, setIsHovered] = useState(false); const [isHovered, setIsHovered] = useState(false);
const [edgePath, labelX, labelY] = getBezierPath({ const [edgePath, labelX, labelY] = getBezierPath({
@@ -50,6 +53,12 @@ const CustomEdge = ({
const beadUp = data?.beadUp ?? 0; const beadUp = data?.beadUp ?? 0;
const beadDown = data?.beadDown ?? 0; const beadDown = data?.beadDown ?? 0;
const handleRemoveEdge = () => {
removeConnection(id);
// Note: broken edge tracking is cleaned up automatically by useSubAgentUpdateState
// when it detects the edge no longer exists
};
return ( return (
<> <>
<BaseEdge <BaseEdge
@@ -57,9 +66,11 @@ const CustomEdge = ({
markerEnd={markerEnd} markerEnd={markerEnd}
className={cn( className={cn(
isStatic && "!stroke-[1.5px] [stroke-dasharray:6]", isStatic && "!stroke-[1.5px] [stroke-dasharray:6]",
selected isBroken
? "stroke-zinc-800" ? "!stroke-red-500 !stroke-[2px] [stroke-dasharray:4]"
: "stroke-zinc-500/50 hover:stroke-zinc-500", : selected
? "stroke-zinc-800"
: "stroke-zinc-500/50 hover:stroke-zinc-500",
)} )}
/> />
<JSBeads <JSBeads
@@ -70,12 +81,16 @@ const CustomEdge = ({
/> />
<EdgeLabelRenderer> <EdgeLabelRenderer>
<Button <Button
onClick={() => removeConnection(id)} onClick={handleRemoveEdge}
className={cn( className={cn(
"absolute h-fit min-w-0 p-1 transition-opacity", "absolute h-fit min-w-0 p-1 transition-opacity",
isHovered ? "opacity-100" : "opacity-0", isBroken
? "bg-red-500 opacity-100 hover:bg-red-600"
: isHovered
? "opacity-100"
: "opacity-0",
)} )}
variant="secondary" variant={isBroken ? "primary" : "secondary"}
style={{ style={{
transform: `translate(-50%, -50%) translate(${labelX}px, ${labelY}px)`, transform: `translate(-50%, -50%) translate(${labelX}px, ${labelY}px)`,
pointerEvents: "all", pointerEvents: "all",

View File

@@ -3,6 +3,7 @@ import { Handle, Position } from "@xyflow/react";
import { useEdgeStore } from "../../../stores/edgeStore"; import { useEdgeStore } from "../../../stores/edgeStore";
import { cleanUpHandleId } from "@/components/renderers/InputRenderer/helpers"; import { cleanUpHandleId } from "@/components/renderers/InputRenderer/helpers";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { useNodeStore } from "../../../stores/nodeStore";
const InputNodeHandle = ({ const InputNodeHandle = ({
handleId, handleId,
@@ -15,6 +16,9 @@ const InputNodeHandle = ({
const isInputConnected = useEdgeStore((state) => const isInputConnected = useEdgeStore((state) =>
state.isInputConnected(nodeId ?? "", cleanedHandleId), state.isInputConnected(nodeId ?? "", cleanedHandleId),
); );
const isInputBroken = useNodeStore((state) =>
state.isInputBroken(nodeId, cleanedHandleId),
);
return ( return (
<Handle <Handle
@@ -27,7 +31,10 @@ const InputNodeHandle = ({
<CircleIcon <CircleIcon
size={16} size={16}
weight={isInputConnected ? "fill" : "duotone"} weight={isInputConnected ? "fill" : "duotone"}
className={"text-gray-400 opacity-100"} className={cn(
"text-gray-400 opacity-100",
isInputBroken && "text-red-500",
)}
/> />
</div> </div>
</Handle> </Handle>
@@ -38,14 +45,17 @@ const OutputNodeHandle = ({
field_name, field_name,
nodeId, nodeId,
hexColor, hexColor,
isBroken,
}: { }: {
field_name: string; field_name: string;
nodeId: string; nodeId: string;
hexColor: string; hexColor: string;
isBroken: boolean;
}) => { }) => {
const isOutputConnected = useEdgeStore((state) => const isOutputConnected = useEdgeStore((state) =>
state.isOutputConnected(nodeId, field_name), state.isOutputConnected(nodeId, field_name),
); );
return ( return (
<Handle <Handle
type={"source"} type={"source"}
@@ -58,7 +68,10 @@ const OutputNodeHandle = ({
size={16} size={16}
weight={"duotone"} weight={"duotone"}
color={isOutputConnected ? hexColor : "gray"} color={isOutputConnected ? hexColor : "gray"}
className={cn("text-gray-400 opacity-100")} className={cn(
"text-gray-400 opacity-100",
isBroken && "text-red-500",
)}
/> />
</div> </div>
</Handle> </Handle>

View File

@@ -20,6 +20,8 @@ import { NodeDataRenderer } from "./components/NodeOutput/NodeOutput";
import { NodeRightClickMenu } from "./components/NodeRightClickMenu"; import { NodeRightClickMenu } from "./components/NodeRightClickMenu";
import { StickyNoteBlock } from "./components/StickyNoteBlock"; import { StickyNoteBlock } from "./components/StickyNoteBlock";
import { WebhookDisclaimer } from "./components/WebhookDisclaimer"; import { WebhookDisclaimer } from "./components/WebhookDisclaimer";
import { SubAgentUpdateFeature } from "./components/SubAgentUpdate/SubAgentUpdateFeature";
import { useCustomNode } from "./useCustomNode";
export type CustomNodeData = { export type CustomNodeData = {
hardcodedValues: { hardcodedValues: {
@@ -45,6 +47,10 @@ export type CustomNode = XYNode<CustomNodeData, "custom">;
export const CustomNode: React.FC<NodeProps<CustomNode>> = React.memo( export const CustomNode: React.FC<NodeProps<CustomNode>> = React.memo(
({ data, id: nodeId, selected }) => { ({ data, id: nodeId, selected }) => {
const { inputSchema, outputSchema } = useCustomNode({ data, nodeId });
const isAgent = data.uiType === BlockUIType.AGENT;
if (data.uiType === BlockUIType.NOTE) { if (data.uiType === BlockUIType.NOTE) {
return ( return (
<StickyNoteBlock data={data} selected={selected} nodeId={nodeId} /> <StickyNoteBlock data={data} selected={selected} nodeId={nodeId} />
@@ -63,16 +69,6 @@ export const CustomNode: React.FC<NodeProps<CustomNode>> = React.memo(
const isAyrshare = data.uiType === BlockUIType.AYRSHARE; const isAyrshare = data.uiType === BlockUIType.AYRSHARE;
const inputSchema =
data.uiType === BlockUIType.AGENT
? (data.hardcodedValues.input_schema ?? {})
: data.inputSchema;
const outputSchema =
data.uiType === BlockUIType.AGENT
? (data.hardcodedValues.output_schema ?? {})
: data.outputSchema;
const hasConfigErrors = const hasConfigErrors =
data.errors && data.errors &&
Object.values(data.errors).some( Object.values(data.errors).some(
@@ -87,12 +83,11 @@ export const CustomNode: React.FC<NodeProps<CustomNode>> = React.memo(
const hasErrors = hasConfigErrors || hasOutputError; const hasErrors = hasConfigErrors || hasOutputError;
// Currently all blockTypes design are similar - that's why i am using the same component for all of them
// If in future - if we need some drastic change in some blockTypes design - we can create separate components for them
const node = ( const node = (
<NodeContainer selected={selected} nodeId={nodeId} hasErrors={hasErrors}> <NodeContainer selected={selected} nodeId={nodeId} hasErrors={hasErrors}>
<div className="rounded-xlarge bg-white"> <div className="rounded-xlarge bg-white">
<NodeHeader data={data} nodeId={nodeId} /> <NodeHeader data={data} nodeId={nodeId} />
{isAgent && <SubAgentUpdateFeature nodeID={nodeId} nodeData={data} />}
{isWebhook && <WebhookDisclaimer nodeId={nodeId} />} {isWebhook && <WebhookDisclaimer nodeId={nodeId} />}
{isAyrshare && <AyrshareConnectButton />} {isAyrshare && <AyrshareConnectButton />}
<FormCreator <FormCreator

View File

@@ -0,0 +1,118 @@
import React from "react";
import { ArrowUpIcon, WarningIcon } from "@phosphor-icons/react";
import { Button } from "@/components/atoms/Button/Button";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/atoms/Tooltip/BaseTooltip";
import { cn, beautifyString } from "@/lib/utils";
import { CustomNodeData } from "../../CustomNode";
import { useSubAgentUpdateState } from "./useSubAgentUpdateState";
import { IncompatibleUpdateDialog } from "./components/IncompatibleUpdateDialog";
import { ResolutionModeBar } from "./components/ResolutionModeBar";
/**
* Inline component for the update bar that can be placed after the header.
* Use this inside the node content where you want the bar to appear.
*/
type SubAgentUpdateFeatureProps = {
nodeID: string;
nodeData: CustomNodeData;
};
export function SubAgentUpdateFeature({
nodeID,
nodeData,
}: SubAgentUpdateFeatureProps) {
const {
updateInfo,
isInResolutionMode,
handleUpdateClick,
showIncompatibilityDialog,
setShowIncompatibilityDialog,
handleConfirmIncompatibleUpdate,
} = useSubAgentUpdateState({ nodeID: nodeID, nodeData: nodeData });
const agentName = nodeData.title || "Agent";
if (!updateInfo.hasUpdate && !isInResolutionMode) {
return null;
}
return (
<>
{isInResolutionMode ? (
<ResolutionModeBar incompatibilities={updateInfo.incompatibilities} />
) : (
<SubAgentUpdateAvailableBar
currentVersion={updateInfo.currentVersion}
latestVersion={updateInfo.latestVersion}
isCompatible={updateInfo.isCompatible}
onUpdate={handleUpdateClick}
/>
)}
{/* Incompatibility dialog - rendered here since this component owns the state */}
{updateInfo.incompatibilities && (
<IncompatibleUpdateDialog
isOpen={showIncompatibilityDialog}
onClose={() => setShowIncompatibilityDialog(false)}
onConfirm={handleConfirmIncompatibleUpdate}
currentVersion={updateInfo.currentVersion}
latestVersion={updateInfo.latestVersion}
agentName={beautifyString(agentName)}
incompatibilities={updateInfo.incompatibilities}
/>
)}
</>
);
}
type SubAgentUpdateAvailableBarProps = {
currentVersion: number;
latestVersion: number;
isCompatible: boolean;
onUpdate: () => void;
};
function SubAgentUpdateAvailableBar({
currentVersion,
latestVersion,
isCompatible,
onUpdate,
}: SubAgentUpdateAvailableBarProps): React.ReactElement {
return (
<div className="flex items-center justify-between gap-2 rounded-t-xl bg-blue-50 px-3 py-2 dark:bg-blue-900/30">
<div className="flex items-center gap-2">
<ArrowUpIcon className="h-4 w-4 text-blue-600 dark:text-blue-400" />
<span className="text-sm text-blue-700 dark:text-blue-300">
Update available (v{currentVersion} v{latestVersion})
</span>
{!isCompatible && (
<Tooltip>
<TooltipTrigger asChild>
<WarningIcon className="h-4 w-4 text-amber-500" />
</TooltipTrigger>
<TooltipContent className="max-w-xs">
<p className="font-medium">Incompatible changes detected</p>
<p className="text-xs text-gray-400">
Click Update to see details
</p>
</TooltipContent>
</Tooltip>
)}
</div>
<Button
size="small"
variant={isCompatible ? "primary" : "outline"}
onClick={onUpdate}
className={cn(
"h-7 text-xs",
!isCompatible && "border-amber-500 text-amber-600 hover:bg-amber-50",
)}
>
Update
</Button>
</div>
);
}

View File

@@ -0,0 +1,274 @@
import React from "react";
import {
WarningIcon,
XCircleIcon,
PlusCircleIcon,
} from "@phosphor-icons/react";
import { Button } from "@/components/atoms/Button/Button";
import { Alert, AlertDescription } from "@/components/molecules/Alert/Alert";
import { Dialog } from "@/components/molecules/Dialog/Dialog";
import { beautifyString } from "@/lib/utils";
import { IncompatibilityInfo } from "@/app/(platform)/build/hooks/useSubAgentUpdate/types";
type IncompatibleUpdateDialogProps = {
isOpen: boolean;
onClose: () => void;
onConfirm: () => void;
currentVersion: number;
latestVersion: number;
agentName: string;
incompatibilities: IncompatibilityInfo;
};
export function IncompatibleUpdateDialog({
isOpen,
onClose,
onConfirm,
currentVersion,
latestVersion,
agentName,
incompatibilities,
}: IncompatibleUpdateDialogProps) {
const hasMissingInputs = incompatibilities.missingInputs.length > 0;
const hasMissingOutputs = incompatibilities.missingOutputs.length > 0;
const hasNewInputs = incompatibilities.newInputs.length > 0;
const hasNewOutputs = incompatibilities.newOutputs.length > 0;
const hasNewRequired = incompatibilities.newRequiredInputs.length > 0;
const hasTypeMismatches = incompatibilities.inputTypeMismatches.length > 0;
const hasInputChanges = hasMissingInputs || hasNewInputs;
const hasOutputChanges = hasMissingOutputs || hasNewOutputs;
return (
<Dialog
title={
<div className="flex items-center gap-2">
<WarningIcon className="h-5 w-5 text-amber-500" weight="fill" />
Incompatible Update
</div>
}
controlled={{
isOpen,
set: async (open) => {
if (!open) onClose();
},
}}
onClose={onClose}
styling={{ maxWidth: "32rem" }}
>
<Dialog.Content>
<div className="space-y-4">
<p className="text-sm text-gray-600 dark:text-gray-400">
Updating <strong>{beautifyString(agentName)}</strong> from v
{currentVersion} to v{latestVersion} will break some connections.
</p>
{/* Input changes - two column layout */}
{hasInputChanges && (
<TwoColumnSection
title="Input Changes"
leftIcon={
<XCircleIcon className="h-4 w-4 text-red-500" weight="fill" />
}
leftTitle="Removed"
leftItems={incompatibilities.missingInputs}
rightIcon={
<PlusCircleIcon
className="h-4 w-4 text-green-500"
weight="fill"
/>
}
rightTitle="Added"
rightItems={incompatibilities.newInputs}
/>
)}
{/* Output changes - two column layout */}
{hasOutputChanges && (
<TwoColumnSection
title="Output Changes"
leftIcon={
<XCircleIcon className="h-4 w-4 text-red-500" weight="fill" />
}
leftTitle="Removed"
leftItems={incompatibilities.missingOutputs}
rightIcon={
<PlusCircleIcon
className="h-4 w-4 text-green-500"
weight="fill"
/>
}
rightTitle="Added"
rightItems={incompatibilities.newOutputs}
/>
)}
{hasTypeMismatches && (
<SingleColumnSection
icon={
<XCircleIcon className="h-4 w-4 text-red-500" weight="fill" />
}
title="Type Changed"
description="These connected inputs have a different type:"
items={incompatibilities.inputTypeMismatches.map(
(m) => `${m.name} (${m.oldType}${m.newType})`,
)}
/>
)}
{hasNewRequired && (
<SingleColumnSection
icon={
<PlusCircleIcon
className="h-4 w-4 text-amber-500"
weight="fill"
/>
}
title="New Required Inputs"
description="These inputs are now required:"
items={incompatibilities.newRequiredInputs}
/>
)}
<Alert variant="warning">
<AlertDescription>
If you proceed, you&apos;ll need to remove the broken connections
before you can save or run your agent.
</AlertDescription>
</Alert>
<Dialog.Footer>
<Button variant="ghost" size="small" onClick={onClose}>
Cancel
</Button>
<Button
variant="primary"
size="small"
onClick={onConfirm}
className="border-amber-700 bg-amber-600 hover:bg-amber-700"
>
Update Anyway
</Button>
</Dialog.Footer>
</div>
</Dialog.Content>
</Dialog>
);
}
type TwoColumnSectionProps = {
title: string;
leftIcon: React.ReactNode;
leftTitle: string;
leftItems: string[];
rightIcon: React.ReactNode;
rightTitle: string;
rightItems: string[];
};
function TwoColumnSection({
title,
leftIcon,
leftTitle,
leftItems,
rightIcon,
rightTitle,
rightItems,
}: TwoColumnSectionProps) {
return (
<div className="rounded-md border border-gray-200 p-3 dark:border-gray-700">
<span className="font-medium">{title}</span>
<div className="mt-2 grid grid-cols-2 items-start gap-4">
{/* Left column - Breaking changes */}
<div className="min-w-0">
<div className="flex items-center gap-1.5 text-sm text-gray-500 dark:text-gray-400">
{leftIcon}
<span>{leftTitle}</span>
</div>
<ul className="mt-1.5 space-y-1">
{leftItems.length > 0 ? (
leftItems.map((item) => (
<li
key={item}
className="text-sm text-gray-700 dark:text-gray-300"
>
<code className="rounded bg-red-50 px-1 py-0.5 font-mono text-xs text-red-700 dark:bg-red-900/30 dark:text-red-300">
{item}
</code>
</li>
))
) : (
<li className="text-sm italic text-gray-400 dark:text-gray-500">
None
</li>
)}
</ul>
</div>
{/* Right column - Possible solutions */}
<div className="min-w-0">
<div className="flex items-center gap-1.5 text-sm text-gray-500 dark:text-gray-400">
{rightIcon}
<span>{rightTitle}</span>
</div>
<ul className="mt-1.5 space-y-1">
{rightItems.length > 0 ? (
rightItems.map((item) => (
<li
key={item}
className="text-sm text-gray-700 dark:text-gray-300"
>
<code className="rounded bg-green-50 px-1 py-0.5 font-mono text-xs text-green-700 dark:bg-green-900/30 dark:text-green-300">
{item}
</code>
</li>
))
) : (
<li className="text-sm italic text-gray-400 dark:text-gray-500">
None
</li>
)}
</ul>
</div>
</div>
</div>
);
}
type SingleColumnSectionProps = {
icon: React.ReactNode;
title: string;
description: string;
items: string[];
};
function SingleColumnSection({
icon,
title,
description,
items,
}: SingleColumnSectionProps) {
return (
<div className="rounded-md border border-gray-200 p-3 dark:border-gray-700">
<div className="flex items-center gap-2">
{icon}
<span className="font-medium">{title}</span>
</div>
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
{description}
</p>
<ul className="mt-2 space-y-1">
{items.map((item) => (
<li
key={item}
className="ml-4 list-disc text-sm text-gray-700 dark:text-gray-300"
>
<code className="rounded bg-gray-100 px-1 py-0.5 font-mono text-xs dark:bg-gray-800">
{item}
</code>
</li>
))}
</ul>
</div>
);
}

View File

@@ -0,0 +1,107 @@
import React from "react";
import { InfoIcon, WarningIcon } from "@phosphor-icons/react";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/atoms/Tooltip/BaseTooltip";
import { IncompatibilityInfo } from "@/app/(platform)/build/hooks/useSubAgentUpdate/types";
type ResolutionModeBarProps = {
incompatibilities: IncompatibilityInfo | null;
};
export function ResolutionModeBar({
incompatibilities,
}: ResolutionModeBarProps): React.ReactElement {
const renderIncompatibilities = () => {
if (!incompatibilities) return <span>No incompatibilities</span>;
const sections: React.ReactNode[] = [];
if (incompatibilities.missingInputs.length > 0) {
sections.push(
<div key="missing-inputs" className="mb-1">
<span className="font-semibold">Missing inputs: </span>
{incompatibilities.missingInputs.map((name, i) => (
<React.Fragment key={name}>
<code className="font-mono">{name}</code>
{i < incompatibilities.missingInputs.length - 1 && ", "}
</React.Fragment>
))}
</div>,
);
}
if (incompatibilities.missingOutputs.length > 0) {
sections.push(
<div key="missing-outputs" className="mb-1">
<span className="font-semibold">Missing outputs: </span>
{incompatibilities.missingOutputs.map((name, i) => (
<React.Fragment key={name}>
<code className="font-mono">{name}</code>
{i < incompatibilities.missingOutputs.length - 1 && ", "}
</React.Fragment>
))}
</div>,
);
}
if (incompatibilities.newRequiredInputs.length > 0) {
sections.push(
<div key="new-required" className="mb-1">
<span className="font-semibold">New required inputs: </span>
{incompatibilities.newRequiredInputs.map((name, i) => (
<React.Fragment key={name}>
<code className="font-mono">{name}</code>
{i < incompatibilities.newRequiredInputs.length - 1 && ", "}
</React.Fragment>
))}
</div>,
);
}
if (incompatibilities.inputTypeMismatches.length > 0) {
sections.push(
<div key="type-mismatches" className="mb-1">
<span className="font-semibold">Type changed: </span>
{incompatibilities.inputTypeMismatches.map((m, i) => (
<React.Fragment key={m.name}>
<code className="font-mono">{m.name}</code>
<span className="text-gray-400">
{" "}
({m.oldType} {m.newType})
</span>
{i < incompatibilities.inputTypeMismatches.length - 1 && ", "}
</React.Fragment>
))}
</div>,
);
}
return <>{sections}</>;
};
return (
<div className="flex items-center justify-between gap-2 rounded-t-xl bg-amber-50 px-3 py-2 dark:bg-amber-900/30">
<div className="flex items-center gap-2">
<WarningIcon className="h-4 w-4 text-amber-600 dark:text-amber-400" />
<span className="text-sm text-amber-700 dark:text-amber-300">
Remove incompatible connections
</span>
<Tooltip>
<TooltipTrigger asChild>
<InfoIcon className="h-4 w-4 cursor-help text-amber-500" />
</TooltipTrigger>
<TooltipContent className="max-w-sm">
<p className="mb-2 font-semibold">Incompatible changes:</p>
<div className="text-xs">{renderIncompatibilities()}</div>
<p className="mt-2 text-xs text-gray-400">
{(incompatibilities?.newRequiredInputs.length ?? 0) > 0
? "Replace / delete"
: "Delete"}{" "}
the red connections to continue
</p>
</TooltipContent>
</Tooltip>
</div>
</div>
);
}

View File

@@ -0,0 +1,194 @@
import { useState, useCallback, useEffect } from "react";
import { useShallow } from "zustand/react/shallow";
import { useGraphStore } from "@/app/(platform)/build/stores/graphStore";
import {
useNodeStore,
NodeResolutionData,
} from "@/app/(platform)/build/stores/nodeStore";
import { useEdgeStore } from "@/app/(platform)/build/stores/edgeStore";
import {
useSubAgentUpdate,
createUpdatedAgentNodeInputs,
getBrokenEdgeIDs,
} from "@/app/(platform)/build/hooks/useSubAgentUpdate";
import { GraphInputSchema, GraphOutputSchema } from "@/lib/autogpt-server-api";
import { CustomNodeData } from "../../CustomNode";
// Stable empty set to avoid creating new references in selectors
const EMPTY_SET: Set<string> = new Set();
type UseSubAgentUpdateParams = {
nodeID: string;
nodeData: CustomNodeData;
};
export function useSubAgentUpdateState({
nodeID,
nodeData,
}: UseSubAgentUpdateParams) {
const [showIncompatibilityDialog, setShowIncompatibilityDialog] =
useState(false);
// Get store actions
const updateNodeData = useNodeStore(
useShallow((state) => state.updateNodeData),
);
const setNodeResolutionMode = useNodeStore(
useShallow((state) => state.setNodeResolutionMode),
);
const isNodeInResolutionMode = useNodeStore(
useShallow((state) => state.isNodeInResolutionMode),
);
const setBrokenEdgeIDs = useNodeStore(
useShallow((state) => state.setBrokenEdgeIDs),
);
// Get this node's broken edge IDs from the per-node map
// Use EMPTY_SET as fallback to maintain referential stability
const brokenEdgeIDs = useNodeStore(
(state) => state.brokenEdgeIDs.get(nodeID) || EMPTY_SET,
);
const getNodeResolutionData = useNodeStore(
useShallow((state) => state.getNodeResolutionData),
);
const connectedEdges = useEdgeStore(
useShallow((state) => state.getNodeEdges(nodeID)),
);
const availableSubGraphs = useGraphStore(
useShallow((state) => state.availableSubGraphs),
);
// Extract agent-specific data
const graphID = nodeData.hardcodedValues?.graph_id as string | undefined;
const graphVersion = nodeData.hardcodedValues?.graph_version as
| number
| undefined;
const currentInputSchema = nodeData.hardcodedValues?.input_schema as
| GraphInputSchema
| undefined;
const currentOutputSchema = nodeData.hardcodedValues?.output_schema as
| GraphOutputSchema
| undefined;
// Use the sub-agent update hook
const updateInfo = useSubAgentUpdate(
nodeID,
graphID,
graphVersion,
currentInputSchema,
currentOutputSchema,
connectedEdges,
availableSubGraphs,
);
const isInResolutionMode = isNodeInResolutionMode(nodeID);
// Handle update button click
const handleUpdateClick = useCallback(() => {
if (!updateInfo.hasUpdate || !updateInfo.latestGraph) return;
if (updateInfo.isCompatible) {
// Compatible update - apply directly
const newHardcodedValues = createUpdatedAgentNodeInputs(
nodeData.hardcodedValues,
updateInfo.latestGraph,
);
updateNodeData(nodeID, { hardcodedValues: newHardcodedValues });
} else {
// Incompatible update - show dialog
setShowIncompatibilityDialog(true);
}
}, [
updateInfo.hasUpdate,
updateInfo.latestGraph,
updateInfo.isCompatible,
nodeData.hardcodedValues,
updateNodeData,
nodeID,
]);
// Handle confirming an incompatible update
function handleConfirmIncompatibleUpdate() {
if (!updateInfo.latestGraph || !updateInfo.incompatibilities) return;
const latestGraph = updateInfo.latestGraph;
// Get the new schemas from the latest graph version
const newInputSchema =
(latestGraph.input_schema as Record<string, unknown>) || {};
const newOutputSchema =
(latestGraph.output_schema as Record<string, unknown>) || {};
// Create the updated hardcoded values but DON'T apply them yet
// We'll apply them when resolution is complete
const pendingHardcodedValues = createUpdatedAgentNodeInputs(
nodeData.hardcodedValues,
latestGraph,
);
// Get broken edge IDs and store them for this node
const brokenIds = getBrokenEdgeIDs(
connectedEdges,
updateInfo.incompatibilities,
nodeID,
);
setBrokenEdgeIDs(nodeID, brokenIds);
// Enter resolution mode with both old and new schemas
// DON'T apply the update yet - keep old schema so connections remain visible
const resolutionData: NodeResolutionData = {
incompatibilities: updateInfo.incompatibilities,
pendingUpdate: {
input_schema: newInputSchema,
output_schema: newOutputSchema,
},
currentSchema: {
input_schema: (currentInputSchema as Record<string, unknown>) || {},
output_schema: (currentOutputSchema as Record<string, unknown>) || {},
},
pendingHardcodedValues,
};
setNodeResolutionMode(nodeID, true, resolutionData);
setShowIncompatibilityDialog(false);
}
// Check if resolution is complete (all broken edges removed)
const resolutionData = getNodeResolutionData(nodeID);
// Auto-check resolution on edge changes
useEffect(() => {
if (!isInResolutionMode) return;
// Check if any broken edges still exist
const remainingBroken = Array.from(brokenEdgeIDs).filter((edgeId) =>
connectedEdges.some((e) => e.id === edgeId),
);
if (remainingBroken.length === 0) {
// Resolution complete - now apply the pending update
if (resolutionData?.pendingHardcodedValues) {
updateNodeData(nodeID, {
hardcodedValues: resolutionData.pendingHardcodedValues,
});
}
// setNodeResolutionMode will clean up this node's broken edges automatically
setNodeResolutionMode(nodeID, false);
}
}, [
isInResolutionMode,
brokenEdgeIDs,
connectedEdges,
resolutionData,
nodeID,
]);
return {
updateInfo,
isInResolutionMode,
resolutionData,
showIncompatibilityDialog,
setShowIncompatibilityDialog,
handleUpdateClick,
handleConfirmIncompatibleUpdate,
};
}

View File

@@ -1,4 +1,6 @@
import { AgentExecutionStatus } from "@/app/api/__generated__/models/agentExecutionStatus"; import { AgentExecutionStatus } from "@/app/api/__generated__/models/agentExecutionStatus";
import { NodeResolutionData } from "@/app/(platform)/build/stores/nodeStore";
import { RJSFSchema } from "@rjsf/utils";
export const nodeStyleBasedOnStatus: Record<AgentExecutionStatus, string> = { export const nodeStyleBasedOnStatus: Record<AgentExecutionStatus, string> = {
INCOMPLETE: "ring-slate-300 bg-slate-300", INCOMPLETE: "ring-slate-300 bg-slate-300",
@@ -9,3 +11,48 @@ export const nodeStyleBasedOnStatus: Record<AgentExecutionStatus, string> = {
TERMINATED: "ring-orange-300 bg-orange-300 ", TERMINATED: "ring-orange-300 bg-orange-300 ",
FAILED: "ring-red-300 bg-red-300", FAILED: "ring-red-300 bg-red-300",
}; };
/**
* Merges schemas during resolution mode to include removed inputs/outputs
* that still have connections, so users can see and delete them.
*/
export function mergeSchemaForResolution(
currentSchema: Record<string, unknown>,
newSchema: Record<string, unknown>,
resolutionData: NodeResolutionData,
type: "input" | "output",
): Record<string, unknown> {
const newProps = (newSchema.properties as RJSFSchema) || {};
const currentProps = (currentSchema.properties as RJSFSchema) || {};
const mergedProps = { ...newProps };
const incomp = resolutionData.incompatibilities;
if (type === "input") {
// Add back missing inputs that have connections
incomp.missingInputs.forEach((inputName: string) => {
if (currentProps[inputName]) {
mergedProps[inputName] = currentProps[inputName];
}
});
// Add back inputs with type mismatches (keep old type so connection works visually)
incomp.inputTypeMismatches.forEach(
(mismatch: { name: string; oldType: string; newType: string }) => {
if (currentProps[mismatch.name]) {
mergedProps[mismatch.name] = currentProps[mismatch.name];
}
},
);
} else {
// Add back missing outputs that have connections
incomp.missingOutputs.forEach((outputName: string) => {
if (currentProps[outputName]) {
mergedProps[outputName] = currentProps[outputName];
}
});
}
return {
...newSchema,
properties: mergedProps,
};
}

View File

@@ -0,0 +1,58 @@
import { useNodeStore } from "@/app/(platform)/build/stores/nodeStore";
import { CustomNodeData } from "./CustomNode";
import { BlockUIType } from "../../../types";
import { useMemo } from "react";
import { mergeSchemaForResolution } from "./helpers";
export const useCustomNode = ({
data,
nodeId,
}: {
data: CustomNodeData;
nodeId: string;
}) => {
const isInResolutionMode = useNodeStore((state) =>
state.nodesInResolutionMode.has(nodeId),
);
const resolutionData = useNodeStore((state) =>
state.nodeResolutionData.get(nodeId),
);
const isAgent = data.uiType === BlockUIType.AGENT;
const currentInputSchema = isAgent
? (data.hardcodedValues.input_schema ?? {})
: data.inputSchema;
const currentOutputSchema = isAgent
? (data.hardcodedValues.output_schema ?? {})
: data.outputSchema;
const inputSchema = useMemo(() => {
if (isAgent && isInResolutionMode && resolutionData) {
return mergeSchemaForResolution(
resolutionData.currentSchema.input_schema,
resolutionData.pendingUpdate.input_schema,
resolutionData,
"input",
);
}
return currentInputSchema;
}, [isAgent, isInResolutionMode, resolutionData, currentInputSchema]);
const outputSchema = useMemo(() => {
if (isAgent && isInResolutionMode && resolutionData) {
return mergeSchemaForResolution(
resolutionData.currentSchema.output_schema,
resolutionData.pendingUpdate.output_schema,
resolutionData,
"output",
);
}
return currentOutputSchema;
}, [isAgent, isInResolutionMode, resolutionData, currentOutputSchema]);
return {
inputSchema,
outputSchema,
};
};

View File

@@ -5,20 +5,16 @@ import { useNodeStore } from "../../../stores/nodeStore";
import { BlockUIType } from "../../types"; import { BlockUIType } from "../../types";
import { FormRenderer } from "@/components/renderers/InputRenderer/FormRenderer"; import { FormRenderer } from "@/components/renderers/InputRenderer/FormRenderer";
export const FormCreator = React.memo( interface FormCreatorProps {
({ jsonSchema: RJSFSchema;
jsonSchema, nodeId: string;
nodeId, uiType: BlockUIType;
uiType, showHandles?: boolean;
showHandles = true, className?: string;
className, }
}: {
jsonSchema: RJSFSchema; export const FormCreator: React.FC<FormCreatorProps> = React.memo(
nodeId: string; ({ jsonSchema, nodeId, uiType, showHandles = true, className }) => {
uiType: BlockUIType;
showHandles?: boolean;
className?: string;
}) => {
const updateNodeData = useNodeStore((state) => state.updateNodeData); const updateNodeData = useNodeStore((state) => state.updateNodeData);
const getHardCodedValues = useNodeStore( const getHardCodedValues = useNodeStore(

View File

@@ -14,6 +14,8 @@ import {
import { useEdgeStore } from "@/app/(platform)/build/stores/edgeStore"; import { useEdgeStore } from "@/app/(platform)/build/stores/edgeStore";
import { getTypeDisplayInfo } from "./helpers"; import { getTypeDisplayInfo } from "./helpers";
import { BlockUIType } from "../../types"; import { BlockUIType } from "../../types";
import { cn } from "@/lib/utils";
import { useBrokenOutputs } from "./useBrokenOutputs";
export const OutputHandler = ({ export const OutputHandler = ({
outputSchema, outputSchema,
@@ -27,6 +29,9 @@ export const OutputHandler = ({
const { isOutputConnected } = useEdgeStore(); const { isOutputConnected } = useEdgeStore();
const properties = outputSchema?.properties || {}; const properties = outputSchema?.properties || {};
const [isOutputVisible, setIsOutputVisible] = useState(true); const [isOutputVisible, setIsOutputVisible] = useState(true);
const brokenOutputs = useBrokenOutputs(nodeId);
console.log("brokenOutputs", brokenOutputs);
const showHandles = uiType !== BlockUIType.OUTPUT; const showHandles = uiType !== BlockUIType.OUTPUT;
@@ -44,6 +49,7 @@ export const OutputHandler = ({
const shouldShow = isConnected || isOutputVisible; const shouldShow = isConnected || isOutputVisible;
const { displayType, colorClass, hexColor } = const { displayType, colorClass, hexColor } =
getTypeDisplayInfo(fieldSchema); getTypeDisplayInfo(fieldSchema);
const isBroken = brokenOutputs.has(fullKey);
return shouldShow ? ( return shouldShow ? (
<div key={fullKey} className="flex flex-col items-end gap-2"> <div key={fullKey} className="flex flex-col items-end gap-2">
@@ -64,15 +70,29 @@ export const OutputHandler = ({
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>
)} )}
<Text variant="body" className="text-slate-700"> <Text
variant="body"
className={cn(
"text-slate-700",
isBroken && "text-red-500 line-through",
)}
>
{fieldTitle} {fieldTitle}
</Text> </Text>
<Text variant="small" as="span" className={colorClass}> <Text
variant="small"
as="span"
className={cn(
colorClass,
isBroken && "!text-red-500 line-through",
)}
>
({displayType}) ({displayType})
</Text> </Text>
{showHandles && ( {showHandles && (
<OutputNodeHandle <OutputNodeHandle
isBroken={isBroken}
field_name={fullKey} field_name={fullKey}
nodeId={nodeId} nodeId={nodeId}
hexColor={hexColor} hexColor={hexColor}

View File

@@ -0,0 +1,23 @@
import { useMemo } from "react";
import { useNodeStore } from "@/app/(platform)/build/stores/nodeStore";
/**
* Hook to get the set of broken output names for a node in resolution mode.
*/
export function useBrokenOutputs(nodeID: string): Set<string> {
// Subscribe to the actual state values, not just methods
const isInResolution = useNodeStore((state) =>
state.nodesInResolutionMode.has(nodeID),
);
const resolutionData = useNodeStore((state) =>
state.nodeResolutionData.get(nodeID),
);
return useMemo(() => {
if (!isInResolution || !resolutionData) {
return new Set<string>();
}
return new Set(resolutionData.incompatibilities.missingOutputs);
}, [isInResolution, resolutionData]);
}

View File

@@ -25,7 +25,7 @@ export const RightSidebar = () => {
> >
<div className="mb-4"> <div className="mb-4">
<h2 className="text-lg font-semibold text-slate-800 dark:text-slate-200"> <h2 className="text-lg font-semibold text-slate-800 dark:text-slate-200">
Flow Debug Panel Graph Debug Panel
</h2> </h2>
</div> </div>
@@ -65,7 +65,7 @@ export const RightSidebar = () => {
{l.source_id}[{l.source_name}] {l.sink_id}[{l.sink_name}] {l.source_id}[{l.source_name}] {l.sink_id}[{l.sink_name}]
</div> </div>
<div className="mt-1 text-slate-500 dark:text-slate-400"> <div className="mt-1 text-slate-500 dark:text-slate-400">
edge_id: {l.id} edge.id: {l.id}
</div> </div>
</div> </div>
))} ))}

View File

@@ -12,7 +12,14 @@ import {
PopoverContent, PopoverContent,
PopoverTrigger, PopoverTrigger,
} from "@/components/__legacy__/ui/popover"; } from "@/components/__legacy__/ui/popover";
import { Block, BlockUIType, SpecialBlockID } from "@/lib/autogpt-server-api"; import {
Block,
BlockIORootSchema,
BlockUIType,
GraphInputSchema,
GraphOutputSchema,
SpecialBlockID,
} from "@/lib/autogpt-server-api";
import { MagnifyingGlassIcon, PlusIcon } from "@radix-ui/react-icons"; import { MagnifyingGlassIcon, PlusIcon } from "@radix-ui/react-icons";
import { IconToyBrick } from "@/components/__legacy__/ui/icons"; import { IconToyBrick } from "@/components/__legacy__/ui/icons";
import { getPrimaryCategoryColor } from "@/lib/utils"; import { getPrimaryCategoryColor } from "@/lib/utils";
@@ -24,8 +31,10 @@ import {
import { GraphMeta } from "@/lib/autogpt-server-api"; import { GraphMeta } from "@/lib/autogpt-server-api";
import jaro from "jaro-winkler"; import jaro from "jaro-winkler";
type _Block = Block & { type _Block = Omit<Block, "inputSchema" | "outputSchema"> & {
uiKey?: string; uiKey?: string;
inputSchema: BlockIORootSchema | GraphInputSchema;
outputSchema: BlockIORootSchema | GraphOutputSchema;
hardcodedValues?: Record<string, any>; hardcodedValues?: Record<string, any>;
_cached?: { _cached?: {
blockName: string; blockName: string;

View File

@@ -2,7 +2,7 @@ import React from "react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { Button } from "@/components/__legacy__/ui/button"; import { Button } from "@/components/__legacy__/ui/button";
import { LogOut } from "lucide-react"; import { LogOut } from "lucide-react";
import { ClockIcon } from "@phosphor-icons/react"; import { ClockIcon, WarningIcon } from "@phosphor-icons/react";
import { IconPlay, IconSquare } from "@/components/__legacy__/ui/icons"; import { IconPlay, IconSquare } from "@/components/__legacy__/ui/icons";
interface Props { interface Props {
@@ -13,6 +13,7 @@ interface Props {
isRunning: boolean; isRunning: boolean;
isDisabled: boolean; isDisabled: boolean;
className?: string; className?: string;
resolutionModeActive?: boolean;
} }
export const BuildActionBar: React.FC<Props> = ({ export const BuildActionBar: React.FC<Props> = ({
@@ -23,9 +24,30 @@ export const BuildActionBar: React.FC<Props> = ({
isRunning, isRunning,
isDisabled, isDisabled,
className, className,
resolutionModeActive = false,
}) => { }) => {
const buttonClasses = const buttonClasses =
"flex items-center gap-2 text-sm font-medium md:text-lg"; "flex items-center gap-2 text-sm font-medium md:text-lg";
// Show resolution mode message instead of action buttons
if (resolutionModeActive) {
return (
<div
className={cn(
"flex w-fit select-none items-center justify-center p-4",
className,
)}
>
<div className="flex items-center gap-3 rounded-lg border border-amber-300 bg-amber-50 px-4 py-3 dark:border-amber-700 dark:bg-amber-900/30">
<WarningIcon className="size-5 text-amber-600 dark:text-amber-400" />
<span className="text-sm font-medium text-amber-800 dark:text-amber-200">
Remove incompatible connections to continue
</span>
</div>
</div>
);
}
return ( return (
<div <div
className={cn( className={cn(

View File

@@ -60,10 +60,16 @@ export function CustomEdge({
targetY - 5, targetY - 5,
); );
const { deleteElements } = useReactFlow<Node, CustomEdge>(); const { deleteElements } = useReactFlow<Node, CustomEdge>();
const { visualizeBeads } = useContext(BuilderContext) ?? { const builderContext = useContext(BuilderContext);
const { visualizeBeads } = builderContext ?? {
visualizeBeads: "no", visualizeBeads: "no",
}; };
// Check if this edge is broken (during resolution mode)
const isBroken =
builderContext?.resolutionMode?.active &&
builderContext?.resolutionMode?.brokenEdgeIds?.includes(id);
const onEdgeRemoveClick = () => { const onEdgeRemoveClick = () => {
deleteElements({ edges: [{ id }] }); deleteElements({ edges: [{ id }] });
}; };
@@ -171,12 +177,27 @@ export function CustomEdge({
const middle = getPointForT(0.5); const middle = getPointForT(0.5);
// Determine edge color - red for broken edges
const baseColor = data?.edgeColor ?? "#555555";
const edgeColor = isBroken ? "#ef4444" : baseColor;
// Add opacity to hex color (99 = 60% opacity, 80 = 50% opacity)
const strokeColor = isBroken
? `${edgeColor}99`
: selected
? edgeColor
: `${edgeColor}80`;
return ( return (
<> <>
<BaseEdge <BaseEdge
path={svgPath} path={svgPath}
markerEnd={markerEnd} markerEnd={markerEnd}
className={`data-sentry-unmask transition-all duration-200 ${data?.isStatic ? "[stroke-dasharray:5_3]" : "[stroke-dasharray:0]"} [stroke-width:${data?.isStatic ? 2.5 : 2}px] hover:[stroke-width:${data?.isStatic ? 3.5 : 3}px] ${selected ? `[stroke:${data?.edgeColor ?? "#555555"}]` : `[stroke:${data?.edgeColor ?? "#555555"}80] hover:[stroke:${data?.edgeColor ?? "#555555"}]`}`} style={{
stroke: strokeColor,
strokeWidth: data?.isStatic ? 2.5 : 2,
strokeDasharray: data?.isStatic ? "5 3" : undefined,
}}
className="data-sentry-unmask transition-all duration-200"
/> />
<path <path
d={svgPath} d={svgPath}

View File

@@ -18,6 +18,8 @@ import {
BlockIOSubSchema, BlockIOSubSchema,
BlockUIType, BlockUIType,
Category, Category,
GraphInputSchema,
GraphOutputSchema,
NodeExecutionResult, NodeExecutionResult,
} from "@/lib/autogpt-server-api"; } from "@/lib/autogpt-server-api";
import { import {
@@ -62,14 +64,21 @@ import { NodeGenericInputField, NodeTextBoxInput } from "../NodeInputs";
import NodeOutputs from "../NodeOutputs"; import NodeOutputs from "../NodeOutputs";
import OutputModalComponent from "../OutputModalComponent"; import OutputModalComponent from "../OutputModalComponent";
import "./customnode.css"; import "./customnode.css";
import { SubAgentUpdateBar } from "./SubAgentUpdateBar";
import { IncompatibilityDialog } from "./IncompatibilityDialog";
import {
useSubAgentUpdate,
createUpdatedAgentNodeInputs,
getBrokenEdgeIDs,
} from "../../../hooks/useSubAgentUpdate";
export type ConnectionData = Array<{ export type ConnectedEdge = {
edge_id: string; id: string;
source: string; source: string;
sourceHandle: string; sourceHandle: string;
target: string; target: string;
targetHandle: string; targetHandle: string;
}>; };
export type CustomNodeData = { export type CustomNodeData = {
blockType: string; blockType: string;
@@ -80,7 +89,7 @@ export type CustomNodeData = {
inputSchema: BlockIORootSchema; inputSchema: BlockIORootSchema;
outputSchema: BlockIORootSchema; outputSchema: BlockIORootSchema;
hardcodedValues: { [key: string]: any }; hardcodedValues: { [key: string]: any };
connections: ConnectionData; connections: ConnectedEdge[];
isOutputOpen: boolean; isOutputOpen: boolean;
status?: NodeExecutionResult["status"]; status?: NodeExecutionResult["status"];
/** executionResults contains outputs across multiple executions /** executionResults contains outputs across multiple executions
@@ -127,20 +136,199 @@ export const CustomNode = React.memo(
let subGraphID = ""; let subGraphID = "";
if (data.uiType === BlockUIType.AGENT) {
// Display the graph's schema instead AgentExecutorBlock's schema.
data.inputSchema = data.hardcodedValues?.input_schema || {};
data.outputSchema = data.hardcodedValues?.output_schema || {};
subGraphID = data.hardcodedValues?.graph_id || subGraphID;
}
if (!builderContext) { if (!builderContext) {
throw new Error( throw new Error(
"BuilderContext consumer must be inside FlowEditor component", "BuilderContext consumer must be inside FlowEditor component",
); );
} }
const { libraryAgent, setIsAnyModalOpen, getNextNodeId } = builderContext; const {
libraryAgent,
setIsAnyModalOpen,
getNextNodeId,
availableFlows,
resolutionMode,
enterResolutionMode,
} = builderContext;
// Check if this node is in resolution mode (moved up for schema merge logic)
const isInResolutionMode =
resolutionMode.active && resolutionMode.nodeId === id;
if (data.uiType === BlockUIType.AGENT) {
// Display the graph's schema instead AgentExecutorBlock's schema.
const currentInputSchema = data.hardcodedValues?.input_schema || {};
const currentOutputSchema = data.hardcodedValues?.output_schema || {};
subGraphID = data.hardcodedValues?.graph_id || subGraphID;
// During resolution mode, merge old connected inputs/outputs with new schema
if (isInResolutionMode && resolutionMode.pendingUpdate) {
const newInputSchema =
(resolutionMode.pendingUpdate.input_schema as BlockIORootSchema) ||
{};
const newOutputSchema =
(resolutionMode.pendingUpdate.output_schema as BlockIORootSchema) ||
{};
// Merge input schemas: start with new schema, add old connected inputs that are missing
const mergedInputProps = { ...newInputSchema.properties };
const incomp = resolutionMode.incompatibilities;
if (incomp && currentInputSchema.properties) {
// Add back missing inputs that have connections (so user can see/delete them)
incomp.missingInputs.forEach((inputName) => {
if (currentInputSchema.properties[inputName]) {
mergedInputProps[inputName] =
currentInputSchema.properties[inputName];
}
});
// Add back inputs with type mismatches (keep old type so connection still works visually)
incomp.inputTypeMismatches.forEach((mismatch) => {
if (currentInputSchema.properties[mismatch.name]) {
mergedInputProps[mismatch.name] =
currentInputSchema.properties[mismatch.name];
}
});
}
// Merge output schemas: start with new schema, add old connected outputs that are missing
const mergedOutputProps = { ...newOutputSchema.properties };
if (incomp && currentOutputSchema.properties) {
incomp.missingOutputs.forEach((outputName) => {
if (currentOutputSchema.properties[outputName]) {
mergedOutputProps[outputName] =
currentOutputSchema.properties[outputName];
}
});
}
data.inputSchema = {
...newInputSchema,
properties: mergedInputProps,
};
data.outputSchema = {
...newOutputSchema,
properties: mergedOutputProps,
};
} else {
data.inputSchema = currentInputSchema;
data.outputSchema = currentOutputSchema;
}
}
const setHardcodedValues = useCallback(
(values: any) => {
updateNodeData(id, { hardcodedValues: values });
},
[id, updateNodeData],
);
// Sub-agent update detection
const isAgentBlock = data.uiType === BlockUIType.AGENT;
const graphId = isAgentBlock ? data.hardcodedValues?.graph_id : undefined;
const graphVersion = isAgentBlock
? data.hardcodedValues?.graph_version
: undefined;
const subAgentUpdate = useSubAgentUpdate(
id,
graphId,
graphVersion,
isAgentBlock
? (data.hardcodedValues?.input_schema as GraphInputSchema)
: undefined,
isAgentBlock
? (data.hardcodedValues?.output_schema as GraphOutputSchema)
: undefined,
data.connections,
availableFlows,
);
const [showIncompatibilityDialog, setShowIncompatibilityDialog] =
useState(false);
// Helper to check if a handle is broken (for resolution mode)
const isInputHandleBroken = useCallback(
(handleName: string): boolean => {
if (!isInResolutionMode || !resolutionMode.incompatibilities) {
return false;
}
const incomp = resolutionMode.incompatibilities;
return (
incomp.missingInputs.includes(handleName) ||
incomp.inputTypeMismatches.some((m) => m.name === handleName)
);
},
[isInResolutionMode, resolutionMode.incompatibilities],
);
const isOutputHandleBroken = useCallback(
(handleName: string): boolean => {
if (!isInResolutionMode || !resolutionMode.incompatibilities) {
return false;
}
return resolutionMode.incompatibilities.missingOutputs.includes(
handleName,
);
},
[isInResolutionMode, resolutionMode.incompatibilities],
);
// Handle update button click
const handleUpdateClick = useCallback(() => {
if (!subAgentUpdate.latestGraph) return;
if (subAgentUpdate.isCompatible) {
// Compatible update - directly apply
const updatedValues = createUpdatedAgentNodeInputs(
data.hardcodedValues,
subAgentUpdate.latestGraph,
);
setHardcodedValues(updatedValues);
toast({
title: "Agent updated",
description: `Updated to version ${subAgentUpdate.latestVersion}`,
});
} else {
// Incompatible update - show dialog
setShowIncompatibilityDialog(true);
}
}, [subAgentUpdate, data.hardcodedValues, setHardcodedValues]);
// Handle confirm incompatible update
const handleConfirmIncompatibleUpdate = useCallback(() => {
if (!subAgentUpdate.latestGraph || !subAgentUpdate.incompatibilities) {
return;
}
// Create the updated values but DON'T apply them yet
const updatedValues = createUpdatedAgentNodeInputs(
data.hardcodedValues,
subAgentUpdate.latestGraph,
);
// Get broken edge IDs
const brokenEdgeIds = getBrokenEdgeIDs(
data.connections,
subAgentUpdate.incompatibilities,
id,
);
// Enter resolution mode with pending update (don't apply schema yet)
enterResolutionMode(
id,
subAgentUpdate.incompatibilities,
brokenEdgeIds,
updatedValues,
);
setShowIncompatibilityDialog(false);
}, [
subAgentUpdate,
data.hardcodedValues,
data.connections,
id,
enterResolutionMode,
]);
useEffect(() => { useEffect(() => {
if (data.executionResults || data.status) { if (data.executionResults || data.status) {
@@ -156,13 +344,6 @@ export const CustomNode = React.memo(
setIsAnyModalOpen?.(isModalOpen || isOutputModalOpen); setIsAnyModalOpen?.(isModalOpen || isOutputModalOpen);
}, [isModalOpen, isOutputModalOpen, data, setIsAnyModalOpen]); }, [isModalOpen, isOutputModalOpen, data, setIsAnyModalOpen]);
const setHardcodedValues = useCallback(
(values: any) => {
updateNodeData(id, { hardcodedValues: values });
},
[id, updateNodeData],
);
const handleTitleEdit = useCallback(() => { const handleTitleEdit = useCallback(() => {
setIsEditingTitle(true); setIsEditingTitle(true);
setTimeout(() => { setTimeout(() => {
@@ -255,6 +436,7 @@ export const CustomNode = React.memo(
isConnected={isOutputHandleConnected(propKey)} isConnected={isOutputHandleConnected(propKey)}
schema={fieldSchema} schema={fieldSchema}
side="right" side="right"
isBroken={isOutputHandleBroken(propKey)}
/> />
{"properties" in fieldSchema && {"properties" in fieldSchema &&
renderHandles( renderHandles(
@@ -385,6 +567,7 @@ export const CustomNode = React.memo(
isRequired={isRequired} isRequired={isRequired}
schema={propSchema} schema={propSchema}
side="left" side="left"
isBroken={isInputHandleBroken(propKey)}
/> />
) : ( ) : (
propKey !== "credentials" && propKey !== "credentials" &&
@@ -873,6 +1056,22 @@ export const CustomNode = React.memo(
<ContextMenuContent /> <ContextMenuContent />
</div> </div>
{/* Sub-agent Update Bar - shown below header */}
{isAgentBlock && (subAgentUpdate.hasUpdate || isInResolutionMode) && (
<SubAgentUpdateBar
currentVersion={subAgentUpdate.currentVersion}
latestVersion={subAgentUpdate.latestVersion}
isCompatible={subAgentUpdate.isCompatible}
incompatibilities={
isInResolutionMode
? resolutionMode.incompatibilities
: subAgentUpdate.incompatibilities
}
onUpdate={handleUpdateClick}
isInResolutionMode={isInResolutionMode}
/>
)}
{/* Body */} {/* Body */}
<div className="mx-5 my-6 rounded-b-xl"> <div className="mx-5 my-6 rounded-b-xl">
{/* Input Handles */} {/* Input Handles */}
@@ -1044,9 +1243,24 @@ export const CustomNode = React.memo(
); );
return ( return (
<ContextMenu.Root> <>
<ContextMenu.Trigger>{nodeContent()}</ContextMenu.Trigger> <ContextMenu.Root>
</ContextMenu.Root> <ContextMenu.Trigger>{nodeContent()}</ContextMenu.Trigger>
</ContextMenu.Root>
{/* Incompatibility Dialog for sub-agent updates */}
{isAgentBlock && subAgentUpdate.incompatibilities && (
<IncompatibilityDialog
isOpen={showIncompatibilityDialog}
onClose={() => setShowIncompatibilityDialog(false)}
onConfirm={handleConfirmIncompatibleUpdate}
currentVersion={subAgentUpdate.currentVersion}
latestVersion={subAgentUpdate.latestVersion}
agentName={data.blockType || "Agent"}
incompatibilities={subAgentUpdate.incompatibilities}
/>
)}
</>
); );
}, },
(prevProps, nextProps) => { (prevProps, nextProps) => {

View File

@@ -0,0 +1,244 @@
import React from "react";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/__legacy__/ui/dialog";
import { Button } from "@/components/__legacy__/ui/button";
import { AlertTriangle, XCircle, PlusCircle } from "lucide-react";
import { IncompatibilityInfo } from "../../../hooks/useSubAgentUpdate/types";
import { beautifyString } from "@/lib/utils";
import { Alert, AlertDescription } from "@/components/molecules/Alert/Alert";
interface IncompatibilityDialogProps {
isOpen: boolean;
onClose: () => void;
onConfirm: () => void;
currentVersion: number;
latestVersion: number;
agentName: string;
incompatibilities: IncompatibilityInfo;
}
export const IncompatibilityDialog: React.FC<IncompatibilityDialogProps> = ({
isOpen,
onClose,
onConfirm,
currentVersion,
latestVersion,
agentName,
incompatibilities,
}) => {
const hasMissingInputs = incompatibilities.missingInputs.length > 0;
const hasMissingOutputs = incompatibilities.missingOutputs.length > 0;
const hasNewInputs = incompatibilities.newInputs.length > 0;
const hasNewOutputs = incompatibilities.newOutputs.length > 0;
const hasNewRequired = incompatibilities.newRequiredInputs.length > 0;
const hasTypeMismatches = incompatibilities.inputTypeMismatches.length > 0;
const hasInputChanges = hasMissingInputs || hasNewInputs;
const hasOutputChanges = hasMissingOutputs || hasNewOutputs;
return (
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
<DialogContent className="max-w-lg">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<AlertTriangle className="h-5 w-5 text-amber-500" />
Incompatible Update
</DialogTitle>
<DialogDescription>
Updating <strong>{beautifyString(agentName)}</strong> from v
{currentVersion} to v{latestVersion} will break some connections.
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-2">
{/* Input changes - two column layout */}
{hasInputChanges && (
<TwoColumnSection
title="Input Changes"
leftIcon={<XCircle className="h-4 w-4 text-red-500" />}
leftTitle="Removed"
leftItems={incompatibilities.missingInputs}
rightIcon={<PlusCircle className="h-4 w-4 text-green-500" />}
rightTitle="Added"
rightItems={incompatibilities.newInputs}
/>
)}
{/* Output changes - two column layout */}
{hasOutputChanges && (
<TwoColumnSection
title="Output Changes"
leftIcon={<XCircle className="h-4 w-4 text-red-500" />}
leftTitle="Removed"
leftItems={incompatibilities.missingOutputs}
rightIcon={<PlusCircle className="h-4 w-4 text-green-500" />}
rightTitle="Added"
rightItems={incompatibilities.newOutputs}
/>
)}
{hasTypeMismatches && (
<SingleColumnSection
icon={<XCircle className="h-4 w-4 text-red-500" />}
title="Type Changed"
description="These connected inputs have a different type:"
items={incompatibilities.inputTypeMismatches.map(
(m) => `${m.name} (${m.oldType}${m.newType})`,
)}
/>
)}
{hasNewRequired && (
<SingleColumnSection
icon={<PlusCircle className="h-4 w-4 text-amber-500" />}
title="New Required Inputs"
description="These inputs are now required:"
items={incompatibilities.newRequiredInputs}
/>
)}
</div>
<Alert variant="warning">
<AlertDescription>
If you proceed, you&apos;ll need to remove the broken connections
before you can save or run your agent.
</AlertDescription>
</Alert>
<DialogFooter className="gap-2 sm:gap-0">
<Button variant="outline" onClick={onClose}>
Cancel
</Button>
<Button
variant="destructive"
onClick={onConfirm}
className="bg-amber-600 hover:bg-amber-700"
>
Update Anyway
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
interface TwoColumnSectionProps {
title: string;
leftIcon: React.ReactNode;
leftTitle: string;
leftItems: string[];
rightIcon: React.ReactNode;
rightTitle: string;
rightItems: string[];
}
const TwoColumnSection: React.FC<TwoColumnSectionProps> = ({
title,
leftIcon,
leftTitle,
leftItems,
rightIcon,
rightTitle,
rightItems,
}) => (
<div className="rounded-md border border-gray-200 p-3 dark:border-gray-700">
<span className="font-medium">{title}</span>
<div className="mt-2 grid grid-cols-2 items-start gap-4">
{/* Left column - Breaking changes */}
<div className="min-w-0">
<div className="flex items-center gap-1.5 text-sm text-gray-500 dark:text-gray-400">
{leftIcon}
<span>{leftTitle}</span>
</div>
<ul className="mt-1.5 space-y-1">
{leftItems.length > 0 ? (
leftItems.map((item) => (
<li
key={item}
className="text-sm text-gray-700 dark:text-gray-300"
>
<code className="rounded bg-red-50 px-1 py-0.5 font-mono text-xs text-red-700 dark:bg-red-900/30 dark:text-red-300">
{item}
</code>
</li>
))
) : (
<li className="text-sm italic text-gray-400 dark:text-gray-500">
None
</li>
)}
</ul>
</div>
{/* Right column - Possible solutions */}
<div className="min-w-0">
<div className="flex items-center gap-1.5 text-sm text-gray-500 dark:text-gray-400">
{rightIcon}
<span>{rightTitle}</span>
</div>
<ul className="mt-1.5 space-y-1">
{rightItems.length > 0 ? (
rightItems.map((item) => (
<li
key={item}
className="text-sm text-gray-700 dark:text-gray-300"
>
<code className="rounded bg-green-50 px-1 py-0.5 font-mono text-xs text-green-700 dark:bg-green-900/30 dark:text-green-300">
{item}
</code>
</li>
))
) : (
<li className="text-sm italic text-gray-400 dark:text-gray-500">
None
</li>
)}
</ul>
</div>
</div>
</div>
);
interface SingleColumnSectionProps {
icon: React.ReactNode;
title: string;
description: string;
items: string[];
}
const SingleColumnSection: React.FC<SingleColumnSectionProps> = ({
icon,
title,
description,
items,
}) => (
<div className="rounded-md border border-gray-200 p-3 dark:border-gray-700">
<div className="flex items-center gap-2">
{icon}
<span className="font-medium">{title}</span>
</div>
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
{description}
</p>
<ul className="mt-2 space-y-1">
{items.map((item) => (
<li
key={item}
className="ml-4 list-disc text-sm text-gray-700 dark:text-gray-300"
>
<code className="rounded bg-gray-100 px-1 py-0.5 font-mono text-xs dark:bg-gray-800">
{item}
</code>
</li>
))}
</ul>
</div>
);
export default IncompatibilityDialog;

View File

@@ -0,0 +1,130 @@
import React from "react";
import { Button } from "@/components/__legacy__/ui/button";
import { ArrowUp, AlertTriangle, Info } from "lucide-react";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/atoms/Tooltip/BaseTooltip";
import { IncompatibilityInfo } from "../../../hooks/useSubAgentUpdate/types";
import { cn } from "@/lib/utils";
interface SubAgentUpdateBarProps {
currentVersion: number;
latestVersion: number;
isCompatible: boolean;
incompatibilities: IncompatibilityInfo | null;
onUpdate: () => void;
isInResolutionMode?: boolean;
}
export const SubAgentUpdateBar: React.FC<SubAgentUpdateBarProps> = ({
currentVersion,
latestVersion,
isCompatible,
incompatibilities,
onUpdate,
isInResolutionMode = false,
}) => {
if (isInResolutionMode) {
return <ResolutionModeBar incompatibilities={incompatibilities} />;
}
return (
<div className="flex items-center justify-between gap-2 rounded-t-lg bg-blue-50 px-3 py-2 dark:bg-blue-900/30">
<div className="flex items-center gap-2">
<ArrowUp className="h-4 w-4 text-blue-600 dark:text-blue-400" />
<span className="text-sm text-blue-700 dark:text-blue-300">
Update available (v{currentVersion} v{latestVersion})
</span>
{!isCompatible && (
<Tooltip>
<TooltipTrigger asChild>
<AlertTriangle className="h-4 w-4 text-amber-500" />
</TooltipTrigger>
<TooltipContent className="max-w-xs">
<p className="font-medium">Incompatible changes detected</p>
<p className="text-xs text-gray-400">
Click Update to see details
</p>
</TooltipContent>
</Tooltip>
)}
</div>
<Button
size="sm"
variant={isCompatible ? "default" : "outline"}
onClick={onUpdate}
className={cn(
"h-7 text-xs",
!isCompatible && "border-amber-500 text-amber-600 hover:bg-amber-50",
)}
>
Update
</Button>
</div>
);
};
interface ResolutionModeBarProps {
incompatibilities: IncompatibilityInfo | null;
}
const ResolutionModeBar: React.FC<ResolutionModeBarProps> = ({
incompatibilities,
}) => {
const formatIncompatibilities = () => {
if (!incompatibilities) return "No incompatibilities";
const items: string[] = [];
if (incompatibilities.missingInputs.length > 0) {
items.push(
`Missing inputs: ${incompatibilities.missingInputs.join(", ")}`,
);
}
if (incompatibilities.missingOutputs.length > 0) {
items.push(
`Missing outputs: ${incompatibilities.missingOutputs.join(", ")}`,
);
}
if (incompatibilities.newRequiredInputs.length > 0) {
items.push(
`New required inputs: ${incompatibilities.newRequiredInputs.join(", ")}`,
);
}
if (incompatibilities.inputTypeMismatches.length > 0) {
const mismatches = incompatibilities.inputTypeMismatches
.map((m) => `${m.name} (${m.oldType}${m.newType})`)
.join(", ");
items.push(`Type changed: ${mismatches}`);
}
return items.join("\n");
};
return (
<div className="flex items-center justify-between gap-2 rounded-t-lg bg-amber-50 px-3 py-2 dark:bg-amber-900/30">
<div className="flex items-center gap-2">
<AlertTriangle className="h-4 w-4 text-amber-600 dark:text-amber-400" />
<span className="text-sm text-amber-700 dark:text-amber-300">
Remove incompatible connections
</span>
<Tooltip>
<TooltipTrigger asChild>
<Info className="h-4 w-4 cursor-help text-amber-500" />
</TooltipTrigger>
<TooltipContent className="max-w-sm whitespace-pre-line">
<p className="font-medium">Incompatible changes:</p>
<p className="mt-1 text-xs">{formatIncompatibilities()}</p>
<p className="mt-2 text-xs text-gray-400">
Delete the red connections to continue
</p>
</TooltipContent>
</Tooltip>
</div>
</div>
);
};
export default SubAgentUpdateBar;

View File

@@ -26,15 +26,17 @@ import {
applyNodeChanges, applyNodeChanges,
} from "@xyflow/react"; } from "@xyflow/react";
import "@xyflow/react/dist/style.css"; import "@xyflow/react/dist/style.css";
import { CustomNode } from "../CustomNode/CustomNode"; import { ConnectedEdge, CustomNode } from "../CustomNode/CustomNode";
import "./flow.css"; import "./flow.css";
import { import {
BlockUIType, BlockUIType,
formatEdgeID, formatEdgeID,
GraphExecutionID, GraphExecutionID,
GraphID, GraphID,
GraphMeta,
LibraryAgent, LibraryAgent,
} from "@/lib/autogpt-server-api"; } from "@/lib/autogpt-server-api";
import { IncompatibilityInfo } from "../../../hooks/useSubAgentUpdate/types";
import { Key, storage } from "@/services/storage/local-storage"; import { Key, storage } from "@/services/storage/local-storage";
import { findNewlyAddedBlockCoordinates, getTypeColor } from "@/lib/utils"; import { findNewlyAddedBlockCoordinates, getTypeColor } from "@/lib/utils";
import { history } from "../history"; import { history } from "../history";
@@ -72,12 +74,30 @@ import { FloatingSafeModeToggle } from "../../FloatingSafeModeToogle";
// It helps to prevent spamming the history with small movements especially when pressing on a input in a block // It helps to prevent spamming the history with small movements especially when pressing on a input in a block
const MINIMUM_MOVE_BEFORE_LOG = 50; const MINIMUM_MOVE_BEFORE_LOG = 50;
export type ResolutionModeState = {
active: boolean;
nodeId: string | null;
incompatibilities: IncompatibilityInfo | null;
brokenEdgeIds: string[];
pendingUpdate: Record<string, unknown> | null; // The hardcoded values to apply after resolution
};
type BuilderContextType = { type BuilderContextType = {
libraryAgent: LibraryAgent | null; libraryAgent: LibraryAgent | null;
visualizeBeads: "no" | "static" | "animate"; visualizeBeads: "no" | "static" | "animate";
setIsAnyModalOpen: (isOpen: boolean) => void; setIsAnyModalOpen: (isOpen: boolean) => void;
getNextNodeId: () => string; getNextNodeId: () => string;
getNodeTitle: (nodeID: string) => string | null; getNodeTitle: (nodeID: string) => string | null;
availableFlows: GraphMeta[];
resolutionMode: ResolutionModeState;
enterResolutionMode: (
nodeId: string,
incompatibilities: IncompatibilityInfo,
brokenEdgeIds: string[],
pendingUpdate: Record<string, unknown>,
) => void;
exitResolutionMode: () => void;
applyPendingUpdate: () => void;
}; };
export type NodeDimension = { export type NodeDimension = {
@@ -172,6 +192,92 @@ const FlowEditor: React.FC<{
// It stores the dimension of all nodes with position as well // It stores the dimension of all nodes with position as well
const [nodeDimensions, setNodeDimensions] = useState<NodeDimension>({}); const [nodeDimensions, setNodeDimensions] = useState<NodeDimension>({});
// Resolution mode state for sub-agent incompatible updates
const [resolutionMode, setResolutionMode] = useState<ResolutionModeState>({
active: false,
nodeId: null,
incompatibilities: null,
brokenEdgeIds: [],
pendingUpdate: null,
});
const enterResolutionMode = useCallback(
(
nodeId: string,
incompatibilities: IncompatibilityInfo,
brokenEdgeIds: string[],
pendingUpdate: Record<string, unknown>,
) => {
setResolutionMode({
active: true,
nodeId,
incompatibilities,
brokenEdgeIds,
pendingUpdate,
});
},
[],
);
const exitResolutionMode = useCallback(() => {
setResolutionMode({
active: false,
nodeId: null,
incompatibilities: null,
brokenEdgeIds: [],
pendingUpdate: null,
});
}, []);
// Apply pending update after resolution mode completes
const applyPendingUpdate = useCallback(() => {
if (!resolutionMode.nodeId || !resolutionMode.pendingUpdate) return;
const node = nodes.find((n) => n.id === resolutionMode.nodeId);
if (node) {
const pendingUpdate = resolutionMode.pendingUpdate as {
[key: string]: any;
};
setNodes((nds) =>
nds.map((n) =>
n.id === resolutionMode.nodeId
? { ...n, data: { ...n.data, hardcodedValues: pendingUpdate } }
: n,
),
);
}
exitResolutionMode();
toast({
title: "Update complete",
description: "Agent has been updated to the new version.",
});
}, [resolutionMode, nodes, setNodes, exitResolutionMode, toast]);
// Check if all broken edges have been removed and auto-apply pending update
useEffect(() => {
if (!resolutionMode.active || resolutionMode.brokenEdgeIds.length === 0) {
return;
}
const currentEdgeIds = new Set(edges.map((e) => e.id));
const remainingBrokenEdges = resolutionMode.brokenEdgeIds.filter((id) =>
currentEdgeIds.has(id),
);
if (remainingBrokenEdges.length === 0) {
// All broken edges have been removed, apply pending update
applyPendingUpdate();
} else if (
remainingBrokenEdges.length !== resolutionMode.brokenEdgeIds.length
) {
// Update the list of broken edges
setResolutionMode((prev) => ({
...prev,
brokenEdgeIds: remainingBrokenEdges,
}));
}
}, [edges, resolutionMode, applyPendingUpdate]);
// Set page title with or without graph name // Set page title with or without graph name
useEffect(() => { useEffect(() => {
document.title = savedAgent document.title = savedAgent
@@ -431,17 +537,19 @@ const FlowEditor: React.FC<{
...node.data.connections.filter( ...node.data.connections.filter(
(conn) => (conn) =>
!removedEdges.some( !removedEdges.some(
(removedEdge) => removedEdge.id === conn.edge_id, (removedEdge) => removedEdge.id === conn.id,
), ),
), ),
// Add node connections for added edges // Add node connections for added edges
...addedEdges.map((addedEdge) => ({ ...addedEdges.map(
edge_id: addedEdge.item.id, (addedEdge): ConnectedEdge => ({
source: addedEdge.item.source, id: addedEdge.item.id,
target: addedEdge.item.target, source: addedEdge.item.source,
sourceHandle: addedEdge.item.sourceHandle!, target: addedEdge.item.target,
targetHandle: addedEdge.item.targetHandle!, sourceHandle: addedEdge.item.sourceHandle!,
})), targetHandle: addedEdge.item.targetHandle!,
}),
),
], ],
}, },
})); }));
@@ -467,13 +575,15 @@ const FlowEditor: React.FC<{
data: { data: {
...node.data, ...node.data,
connections: [ connections: [
...replaceEdges.map((replaceEdge) => ({ ...replaceEdges.map(
edge_id: replaceEdge.item.id, (replaceEdge): ConnectedEdge => ({
source: replaceEdge.item.source, id: replaceEdge.item.id,
target: replaceEdge.item.target, source: replaceEdge.item.source,
sourceHandle: replaceEdge.item.sourceHandle!, target: replaceEdge.item.target,
targetHandle: replaceEdge.item.targetHandle!, sourceHandle: replaceEdge.item.sourceHandle!,
})), targetHandle: replaceEdge.item.targetHandle!,
}),
),
], ],
}, },
})), })),
@@ -890,8 +1000,23 @@ const FlowEditor: React.FC<{
setIsAnyModalOpen, setIsAnyModalOpen,
getNextNodeId, getNextNodeId,
getNodeTitle, getNodeTitle,
availableFlows,
resolutionMode,
enterResolutionMode,
exitResolutionMode,
applyPendingUpdate,
}), }),
[libraryAgent, visualizeBeads, getNextNodeId, getNodeTitle], [
libraryAgent,
visualizeBeads,
getNextNodeId,
getNodeTitle,
availableFlows,
resolutionMode,
enterResolutionMode,
applyPendingUpdate,
exitResolutionMode,
],
); );
return ( return (
@@ -991,6 +1116,7 @@ const FlowEditor: React.FC<{
onClickScheduleButton={handleScheduleButton} onClickScheduleButton={handleScheduleButton}
isDisabled={!savedAgent} isDisabled={!savedAgent}
isRunning={isRunning} isRunning={isRunning}
resolutionModeActive={resolutionMode.active}
/> />
) : ( ) : (
<Alert className="absolute bottom-4 left-1/2 z-20 w-auto -translate-x-1/2 select-none"> <Alert className="absolute bottom-4 left-1/2 z-20 w-auto -translate-x-1/2 select-none">

View File

@@ -1,6 +1,11 @@
import { BlockIOSubSchema } from "@/lib/autogpt-server-api/types"; import { BlockIOSubSchema } from "@/lib/autogpt-server-api/types";
import { cn } from "@/lib/utils"; import {
import { beautifyString, getTypeBgColor, getTypeTextColor } from "@/lib/utils"; cn,
beautifyString,
getTypeBgColor,
getTypeTextColor,
getEffectiveType,
} from "@/lib/utils";
import { FC, memo, useCallback } from "react"; import { FC, memo, useCallback } from "react";
import { Handle, Position } from "@xyflow/react"; import { Handle, Position } from "@xyflow/react";
import { InformationTooltip } from "@/components/molecules/InformationTooltip/InformationTooltip"; import { InformationTooltip } from "@/components/molecules/InformationTooltip/InformationTooltip";
@@ -13,6 +18,7 @@ type HandleProps = {
side: "left" | "right"; side: "left" | "right";
title?: string; title?: string;
className?: string; className?: string;
isBroken?: boolean;
}; };
// Move the constant out of the component to avoid re-creation on every render. // Move the constant out of the component to avoid re-creation on every render.
@@ -27,18 +33,23 @@ const TYPE_NAME: Record<string, string> = {
}; };
// Extract and memoize the Dot component so that it doesn't re-render unnecessarily. // Extract and memoize the Dot component so that it doesn't re-render unnecessarily.
const Dot: FC<{ isConnected: boolean; type?: string }> = memo( const Dot: FC<{ isConnected: boolean; type?: string; isBroken?: boolean }> =
({ isConnected, type }) => { memo(({ isConnected, type, isBroken }) => {
const color = isConnected const color = isBroken
? getTypeBgColor(type || "any") ? "border-red-500 bg-red-100 dark:bg-red-900/30"
: "border-gray-300 dark:border-gray-600"; : isConnected
? getTypeBgColor(type || "any")
: "border-gray-300 dark:border-gray-600";
return ( return (
<div <div
className={`${color} m-1 h-4 w-4 rounded-full border-2 bg-white transition-colors duration-100 group-hover:bg-gray-300 dark:bg-slate-800 dark:group-hover:bg-gray-700`} className={cn(
"m-1 h-4 w-4 rounded-full border-2 bg-white transition-colors duration-100 group-hover:bg-gray-300 dark:bg-slate-800 dark:group-hover:bg-gray-700",
color,
isBroken && "opacity-50",
)}
/> />
); );
}, });
);
Dot.displayName = "Dot"; Dot.displayName = "Dot";
const NodeHandle: FC<HandleProps> = ({ const NodeHandle: FC<HandleProps> = ({
@@ -49,24 +60,34 @@ const NodeHandle: FC<HandleProps> = ({
side, side,
title, title,
className, className,
isBroken = false,
}) => { }) => {
const typeClass = `text-sm ${getTypeTextColor(schema.type || "any")} ${ // Extract effective type from schema (handles anyOf/oneOf/allOf wrappers)
const effectiveType = getEffectiveType(schema);
const typeClass = `text-sm ${getTypeTextColor(effectiveType || "any")} ${
side === "left" ? "text-left" : "text-right" side === "left" ? "text-left" : "text-right"
}`; }`;
const label = ( const label = (
<div className="flex flex-grow flex-row"> <div className={cn("flex flex-grow flex-row", isBroken && "opacity-50")}>
<span <span
className={cn( className={cn(
"data-sentry-unmask text-m green flex items-end pr-2 text-gray-900 dark:text-gray-100", "data-sentry-unmask text-m green flex items-end pr-2 text-gray-900 dark:text-gray-100",
className, className,
isBroken && "text-red-500 line-through",
)} )}
> >
{title || schema.title || beautifyString(keyName.toLowerCase())} {title || schema.title || beautifyString(keyName.toLowerCase())}
{isRequired ? "*" : ""} {isRequired ? "*" : ""}
</span> </span>
<span className={`${typeClass} data-sentry-unmask flex items-end`}> <span
({TYPE_NAME[schema.type as keyof typeof TYPE_NAME] || "any"}) className={cn(
`${typeClass} data-sentry-unmask flex items-end`,
isBroken && "text-red-400",
)}
>
({TYPE_NAME[effectiveType as keyof typeof TYPE_NAME] || "any"})
</span> </span>
</div> </div>
); );
@@ -84,7 +105,7 @@ const NodeHandle: FC<HandleProps> = ({
return ( return (
<div <div
key={keyName} key={keyName}
className="handle-container" className={cn("handle-container", isBroken && "pointer-events-none")}
onContextMenu={handleContextMenu} onContextMenu={handleContextMenu}
> >
<Handle <Handle
@@ -92,10 +113,15 @@ const NodeHandle: FC<HandleProps> = ({
data-testid={`input-handle-${keyName}`} data-testid={`input-handle-${keyName}`}
position={Position.Left} position={Position.Left}
id={keyName} id={keyName}
className="group -ml-[38px]" className={cn("group -ml-[38px]", isBroken && "cursor-not-allowed")}
isConnectable={!isBroken}
> >
<div className="pointer-events-none flex items-center"> <div className="pointer-events-none flex items-center">
<Dot isConnected={isConnected} type={schema.type} /> <Dot
isConnected={isConnected}
type={effectiveType}
isBroken={isBroken}
/>
{label} {label}
</div> </div>
</Handle> </Handle>
@@ -106,7 +132,10 @@ const NodeHandle: FC<HandleProps> = ({
return ( return (
<div <div
key={keyName} key={keyName}
className="handle-container justify-end" className={cn(
"handle-container justify-end",
isBroken && "pointer-events-none",
)}
onContextMenu={handleContextMenu} onContextMenu={handleContextMenu}
> >
<Handle <Handle
@@ -114,11 +143,16 @@ const NodeHandle: FC<HandleProps> = ({
data-testid={`output-handle-${keyName}`} data-testid={`output-handle-${keyName}`}
position={Position.Right} position={Position.Right}
id={keyName} id={keyName}
className="group -mr-[38px]" className={cn("group -mr-[38px]", isBroken && "cursor-not-allowed")}
isConnectable={!isBroken}
> >
<div className="pointer-events-none flex items-center"> <div className="pointer-events-none flex items-center">
{label} {label}
<Dot isConnected={isConnected} type={schema.type} /> <Dot
isConnected={isConnected}
type={effectiveType}
isBroken={isBroken}
/>
</div> </div>
</Handle> </Handle>
</div> </div>

View File

@@ -1,9 +1,9 @@
import { import {
ConnectionData, ConnectedEdge,
CustomNodeData, CustomNodeData,
} from "@/app/(platform)/build/components/legacy-builder/CustomNode/CustomNode"; } from "@/app/(platform)/build/components/legacy-builder/CustomNode/CustomNode";
import { NodeTableInput } from "@/app/(platform)/build/components/legacy-builder/NodeTableInput"; import { NodeTableInput } from "@/app/(platform)/build/components/legacy-builder/NodeTableInput";
import { CredentialsInput } from "@/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/CredentialsInputs/CredentialsInput"; import { CredentialsInput } from "@/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/CredentialsInputs/CredentialsInputs";
import { Button } from "@/components/__legacy__/ui/button"; import { Button } from "@/components/__legacy__/ui/button";
import { Calendar } from "@/components/__legacy__/ui/calendar"; import { Calendar } from "@/components/__legacy__/ui/calendar";
import { LocalValuedInput } from "@/components/__legacy__/ui/input"; import { LocalValuedInput } from "@/components/__legacy__/ui/input";
@@ -65,7 +65,7 @@ type NodeObjectInputTreeProps = {
selfKey?: string; selfKey?: string;
schema: BlockIORootSchema | BlockIOObjectSubSchema; schema: BlockIORootSchema | BlockIOObjectSubSchema;
object?: { [key: string]: any }; object?: { [key: string]: any };
connections: ConnectionData; connections: ConnectedEdge[];
handleInputClick: (key: string) => void; handleInputClick: (key: string) => void;
handleInputChange: (key: string, value: any) => void; handleInputChange: (key: string, value: any) => void;
errors: { [key: string]: string | undefined }; errors: { [key: string]: string | undefined };
@@ -585,7 +585,7 @@ const NodeOneOfDiscriminatorField: FC<{
currentValue?: any; currentValue?: any;
defaultValue?: any; defaultValue?: any;
errors: { [key: string]: string | undefined }; errors: { [key: string]: string | undefined };
connections: ConnectionData; connections: ConnectedEdge[];
handleInputChange: (key: string, value: any) => void; handleInputChange: (key: string, value: any) => void;
handleInputClick: (key: string) => void; handleInputClick: (key: string) => void;
className?: string; className?: string;

View File

@@ -1,15 +1,16 @@
import { FC, useCallback, useEffect, useState } from "react"; import { FC, useCallback, useEffect, useState } from "react";
import NodeHandle from "@/app/(platform)/build/components/legacy-builder/NodeHandle"; import NodeHandle from "@/app/(platform)/build/components/legacy-builder/NodeHandle";
import { import type {
BlockIOTableSubSchema, BlockIOTableSubSchema,
TableCellValue, TableCellValue,
TableRow, TableRow,
} from "@/lib/autogpt-server-api/types"; } from "@/lib/autogpt-server-api/types";
import type { ConnectedEdge } from "./CustomNode/CustomNode";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { PlusIcon, XIcon } from "@phosphor-icons/react"; import { PlusIcon, XIcon } from "@phosphor-icons/react";
import { Button } from "../../../../../components/atoms/Button/Button"; import { Button } from "@/components/atoms/Button/Button";
import { Input } from "../../../../../components/atoms/Input/Input"; import { Input } from "@/components/atoms/Input/Input";
interface NodeTableInputProps { interface NodeTableInputProps {
/** Unique identifier for the node in the builder graph */ /** Unique identifier for the node in the builder graph */
@@ -25,13 +26,7 @@ interface NodeTableInputProps {
/** Validation errors mapped by field key */ /** Validation errors mapped by field key */
errors: { [key: string]: string | undefined }; errors: { [key: string]: string | undefined };
/** Graph connections between nodes in the builder */ /** Graph connections between nodes in the builder */
connections: { connections: ConnectedEdge[];
edge_id: string;
source: string;
sourceHandle: string;
target: string;
targetHandle: string;
}[];
/** Callback when table data changes */ /** Callback when table data changes */
handleInputChange: (key: string, value: TableRow[]) => void; handleInputChange: (key: string, value: TableRow[]) => void;
/** Callback when input field is clicked (for builder selection) */ /** Callback when input field is clicked (for builder selection) */

View File

@@ -1,6 +1,7 @@
import { useCallback } from "react"; import { useCallback } from "react";
import { Node, Edge, useReactFlow } from "@xyflow/react"; import { Node, Edge, useReactFlow } from "@xyflow/react";
import { Key, storage } from "@/services/storage/local-storage"; import { Key, storage } from "@/services/storage/local-storage";
import { ConnectedEdge } from "./CustomNode/CustomNode";
interface CopyableData { interface CopyableData {
nodes: Node[]; nodes: Node[];
@@ -111,13 +112,15 @@ export function useCopyPaste(getNextNodeId: () => string) {
(edge: Edge) => (edge: Edge) =>
edge.source === node.id || edge.target === node.id, edge.source === node.id || edge.target === node.id,
) )
.map((edge: Edge) => ({ .map(
edge_id: edge.id, (edge: Edge): ConnectedEdge => ({
source: edge.source, id: edge.id,
target: edge.target, source: edge.source,
sourceHandle: edge.sourceHandle, target: edge.target,
targetHandle: edge.targetHandle, sourceHandle: edge.sourceHandle!,
})); targetHandle: edge.targetHandle!,
}),
);
return { return {
...node, ...node,

View File

@@ -0,0 +1,104 @@
import { GraphInputSchema } from "@/lib/autogpt-server-api";
import { GraphMetaLike, IncompatibilityInfo } from "./types";
// Helper type for schema properties - the generated types are too loose
type SchemaProperties = Record<string, GraphInputSchema["properties"][string]>;
type SchemaRequired = string[];
// Helper to safely extract schema properties
export function getSchemaProperties(schema: unknown): SchemaProperties {
if (
schema &&
typeof schema === "object" &&
"properties" in schema &&
typeof schema.properties === "object" &&
schema.properties !== null
) {
return schema.properties as SchemaProperties;
}
return {};
}
export function getSchemaRequired(schema: unknown): SchemaRequired {
if (
schema &&
typeof schema === "object" &&
"required" in schema &&
Array.isArray(schema.required)
) {
return schema.required as SchemaRequired;
}
return [];
}
/**
* Creates the updated agent node inputs for a sub-agent node
*/
export function createUpdatedAgentNodeInputs(
currentInputs: Record<string, unknown>,
latestSubGraphVersion: GraphMetaLike,
): Record<string, unknown> {
return {
...currentInputs,
graph_version: latestSubGraphVersion.version,
input_schema: latestSubGraphVersion.input_schema,
output_schema: latestSubGraphVersion.output_schema,
};
}
/** Generic edge type that works with both builders:
* - New builder uses CustomEdge with (formally) optional handles
* - Legacy builder uses ConnectedEdge type with required handles */
export type EdgeLike = {
id: string;
source: string;
target: string;
sourceHandle?: string | null;
targetHandle?: string | null;
};
/**
* Determines which edges are broken after an incompatible update.
* Works with both legacy ConnectedEdge and new CustomEdge.
*/
export function getBrokenEdgeIDs(
connections: EdgeLike[],
incompatibilities: IncompatibilityInfo,
nodeID: string,
): string[] {
const brokenEdgeIDs: string[] = [];
const typeMismatchInputNames = new Set(
incompatibilities.inputTypeMismatches.map((m) => m.name),
);
connections.forEach((conn) => {
// Check if this connection uses a missing input (node is target)
if (
conn.target === nodeID &&
conn.targetHandle &&
incompatibilities.missingInputs.includes(conn.targetHandle)
) {
brokenEdgeIDs.push(conn.id);
}
// Check if this connection uses an input with a type mismatch (node is target)
if (
conn.target === nodeID &&
conn.targetHandle &&
typeMismatchInputNames.has(conn.targetHandle)
) {
brokenEdgeIDs.push(conn.id);
}
// Check if this connection uses a missing output (node is source)
if (
conn.source === nodeID &&
conn.sourceHandle &&
incompatibilities.missingOutputs.includes(conn.sourceHandle)
) {
brokenEdgeIDs.push(conn.id);
}
});
return brokenEdgeIDs;
}

View File

@@ -0,0 +1,2 @@
export { useSubAgentUpdate } from "./useSubAgentUpdate";
export { createUpdatedAgentNodeInputs, getBrokenEdgeIDs } from "./helpers";

View File

@@ -0,0 +1,27 @@
import type { GraphMeta as LegacyGraphMeta } from "@/lib/autogpt-server-api";
import type { GraphMeta as GeneratedGraphMeta } from "@/app/api/__generated__/models/graphMeta";
export type SubAgentUpdateInfo<T extends GraphMetaLike = GraphMetaLike> = {
hasUpdate: boolean;
currentVersion: number;
latestVersion: number;
latestGraph: T | null;
isCompatible: boolean;
incompatibilities: IncompatibilityInfo | null;
};
// Union type for GraphMeta that works with both legacy and new builder
export type GraphMetaLike = LegacyGraphMeta | GeneratedGraphMeta;
export type IncompatibilityInfo = {
missingInputs: string[]; // Connected inputs that no longer exist
missingOutputs: string[]; // Connected outputs that no longer exist
newInputs: string[]; // Inputs that exist in new version but not in current
newOutputs: string[]; // Outputs that exist in new version but not in current
newRequiredInputs: string[]; // New required inputs not in current version or not required
inputTypeMismatches: Array<{
name: string;
oldType: string;
newType: string;
}>; // Connected inputs where the type has changed
};

View File

@@ -0,0 +1,160 @@
import { useMemo } from "react";
import { GraphInputSchema, GraphOutputSchema } from "@/lib/autogpt-server-api";
import { getEffectiveType } from "@/lib/utils";
import { EdgeLike, getSchemaProperties, getSchemaRequired } from "./helpers";
import {
GraphMetaLike,
IncompatibilityInfo,
SubAgentUpdateInfo,
} from "./types";
/**
* Checks if a newer version of a sub-agent is available and determines compatibility
*/
export function useSubAgentUpdate<T extends GraphMetaLike>(
nodeID: string,
graphID: string | undefined,
graphVersion: number | undefined,
currentInputSchema: GraphInputSchema | undefined,
currentOutputSchema: GraphOutputSchema | undefined,
connections: EdgeLike[],
availableGraphs: T[],
): SubAgentUpdateInfo<T> {
// Find the latest version of the same graph
const latestGraph = useMemo(() => {
if (!graphID) return null;
return availableGraphs.find((graph) => graph.id === graphID) || null;
}, [graphID, availableGraphs]);
// Check if there's an update available
const hasUpdate = useMemo(() => {
if (!latestGraph || graphVersion === undefined) return false;
return latestGraph.version! > graphVersion;
}, [latestGraph, graphVersion]);
// Get connected input and output handles for this specific node
const connectedHandles = useMemo(() => {
const inputHandles = new Set<string>();
const outputHandles = new Set<string>();
connections.forEach((conn) => {
// If this node is the target, the targetHandle is an input on this node
if (conn.target === nodeID && conn.targetHandle) {
inputHandles.add(conn.targetHandle);
}
// If this node is the source, the sourceHandle is an output on this node
if (conn.source === nodeID && conn.sourceHandle) {
outputHandles.add(conn.sourceHandle);
}
});
return { inputHandles, outputHandles };
}, [connections, nodeID]);
// Check schema compatibility
const compatibilityResult = useMemo((): {
isCompatible: boolean;
incompatibilities: IncompatibilityInfo | null;
} => {
if (!hasUpdate || !latestGraph) {
return { isCompatible: true, incompatibilities: null };
}
const newInputProps = getSchemaProperties(latestGraph.input_schema);
const newOutputProps = getSchemaProperties(latestGraph.output_schema);
const newRequiredInputs = getSchemaRequired(latestGraph.input_schema);
const currentInputProps = getSchemaProperties(currentInputSchema);
const currentOutputProps = getSchemaProperties(currentOutputSchema);
const currentRequiredInputs = getSchemaRequired(currentInputSchema);
const incompatibilities: IncompatibilityInfo = {
missingInputs: [],
missingOutputs: [],
newInputs: [],
newOutputs: [],
newRequiredInputs: [],
inputTypeMismatches: [],
};
// Check for missing connected inputs and type mismatches
connectedHandles.inputHandles.forEach((inputHandle) => {
if (!(inputHandle in newInputProps)) {
incompatibilities.missingInputs.push(inputHandle);
} else {
// Check for type mismatch on connected inputs
const currentProp = currentInputProps[inputHandle];
const newProp = newInputProps[inputHandle];
const currentType = getEffectiveType(currentProp);
const newType = getEffectiveType(newProp);
if (currentType && newType && currentType !== newType) {
incompatibilities.inputTypeMismatches.push({
name: inputHandle,
oldType: currentType,
newType: newType,
});
}
}
});
// Check for missing connected outputs
connectedHandles.outputHandles.forEach((outputHandle) => {
if (!(outputHandle in newOutputProps)) {
incompatibilities.missingOutputs.push(outputHandle);
}
});
// Check for new required inputs that didn't exist or weren't required before
newRequiredInputs.forEach((requiredInput) => {
const existedBefore = requiredInput in currentInputProps;
const wasRequiredBefore = currentRequiredInputs.includes(
requiredInput as string,
);
if (!existedBefore || !wasRequiredBefore) {
incompatibilities.newRequiredInputs.push(requiredInput as string);
}
});
// Check for new inputs that don't exist in the current version
Object.keys(newInputProps).forEach((inputName) => {
if (!(inputName in currentInputProps)) {
incompatibilities.newInputs.push(inputName);
}
});
// Check for new outputs that don't exist in the current version
Object.keys(newOutputProps).forEach((outputName) => {
if (!(outputName in currentOutputProps)) {
incompatibilities.newOutputs.push(outputName);
}
});
const hasIncompatibilities =
incompatibilities.missingInputs.length > 0 ||
incompatibilities.missingOutputs.length > 0 ||
incompatibilities.newRequiredInputs.length > 0 ||
incompatibilities.inputTypeMismatches.length > 0;
return {
isCompatible: !hasIncompatibilities,
incompatibilities: hasIncompatibilities ? incompatibilities : null,
};
}, [
hasUpdate,
latestGraph,
currentInputSchema,
currentOutputSchema,
connectedHandles,
]);
return {
hasUpdate,
currentVersion: graphVersion || 0,
latestVersion: latestGraph?.version || 0,
latestGraph,
isCompatible: compatibilityResult.isCompatible,
incompatibilities: compatibilityResult.incompatibilities,
};
}

View File

@@ -1,5 +1,6 @@
import { create } from "zustand"; import { create } from "zustand";
import { AgentExecutionStatus } from "@/app/api/__generated__/models/agentExecutionStatus"; import { AgentExecutionStatus } from "@/app/api/__generated__/models/agentExecutionStatus";
import { GraphMeta } from "@/app/api/__generated__/models/graphMeta";
interface GraphStore { interface GraphStore {
graphExecutionStatus: AgentExecutionStatus | undefined; graphExecutionStatus: AgentExecutionStatus | undefined;
@@ -17,6 +18,10 @@ interface GraphStore {
outputSchema: Record<string, any> | null, outputSchema: Record<string, any> | null,
) => void; ) => void;
// Available graphs; used for sub-graph updates
availableSubGraphs: GraphMeta[];
setAvailableSubGraphs: (graphs: GraphMeta[]) => void;
hasInputs: () => boolean; hasInputs: () => boolean;
hasCredentials: () => boolean; hasCredentials: () => boolean;
hasOutputs: () => boolean; hasOutputs: () => boolean;
@@ -29,6 +34,7 @@ export const useGraphStore = create<GraphStore>((set, get) => ({
inputSchema: null, inputSchema: null,
credentialsInputSchema: null, credentialsInputSchema: null,
outputSchema: null, outputSchema: null,
availableSubGraphs: [],
setGraphExecutionStatus: (status: AgentExecutionStatus | undefined) => { setGraphExecutionStatus: (status: AgentExecutionStatus | undefined) => {
set({ set({
@@ -46,6 +52,8 @@ export const useGraphStore = create<GraphStore>((set, get) => ({
setGraphSchemas: (inputSchema, credentialsInputSchema, outputSchema) => setGraphSchemas: (inputSchema, credentialsInputSchema, outputSchema) =>
set({ inputSchema, credentialsInputSchema, outputSchema }), set({ inputSchema, credentialsInputSchema, outputSchema }),
setAvailableSubGraphs: (graphs) => set({ availableSubGraphs: graphs }),
hasOutputs: () => { hasOutputs: () => {
const { outputSchema } = get(); const { outputSchema } = get();
return Object.keys(outputSchema?.properties ?? {}).length > 0; return Object.keys(outputSchema?.properties ?? {}).length > 0;

View File

@@ -17,6 +17,25 @@ import {
ensurePathExists, ensurePathExists,
parseHandleIdToPath, parseHandleIdToPath,
} from "@/components/renderers/InputRenderer/helpers"; } from "@/components/renderers/InputRenderer/helpers";
import { IncompatibilityInfo } from "../hooks/useSubAgentUpdate/types";
// Resolution mode data stored per node
export type NodeResolutionData = {
incompatibilities: IncompatibilityInfo;
// The NEW schema from the update (what we're updating TO)
pendingUpdate: {
input_schema: Record<string, unknown>;
output_schema: Record<string, unknown>;
};
// The OLD schema before the update (what we're updating FROM)
// Needed to merge and show removed inputs during resolution
currentSchema: {
input_schema: Record<string, unknown>;
output_schema: Record<string, unknown>;
};
// The full updated hardcoded values to apply when resolution completes
pendingHardcodedValues: Record<string, unknown>;
};
// Minimum movement (in pixels) required before logging position change to history // Minimum movement (in pixels) required before logging position change to history
// Prevents spamming history with small movements when clicking on inputs inside blocks // Prevents spamming history with small movements when clicking on inputs inside blocks
@@ -65,12 +84,32 @@ type NodeStore = {
backendId: string, backendId: string,
errors: { [key: string]: string }, errors: { [key: string]: string },
) => void; ) => void;
clearAllNodeErrors: () => void; // Add this
syncHardcodedValuesWithHandleIds: (nodeId: string) => void; syncHardcodedValuesWithHandleIds: (nodeId: string) => void;
// Credentials optional helpers
setCredentialsOptional: (nodeId: string, optional: boolean) => void; setCredentialsOptional: (nodeId: string, optional: boolean) => void;
clearAllNodeErrors: () => void;
nodesInResolutionMode: Set<string>;
brokenEdgeIDs: Map<string, Set<string>>;
nodeResolutionData: Map<string, NodeResolutionData>;
setNodeResolutionMode: (
nodeID: string,
inResolution: boolean,
resolutionData?: NodeResolutionData,
) => void;
isNodeInResolutionMode: (nodeID: string) => boolean;
getNodeResolutionData: (nodeID: string) => NodeResolutionData | undefined;
setBrokenEdgeIDs: (nodeID: string, edgeIDs: string[]) => void;
removeBrokenEdgeID: (nodeID: string, edgeID: string) => void;
isEdgeBroken: (edgeID: string) => boolean;
clearResolutionState: () => void;
isInputBroken: (nodeID: string, handleID: string) => boolean;
getInputTypeMismatch: (
nodeID: string,
handleID: string,
) => string | undefined;
}; };
export const useNodeStore = create<NodeStore>((set, get) => ({ export const useNodeStore = create<NodeStore>((set, get) => ({
@@ -374,4 +413,99 @@ export const useNodeStore = create<NodeStore>((set, get) => ({
useHistoryStore.getState().pushState(newState); useHistoryStore.getState().pushState(newState);
}, },
// Sub-agent resolution mode state
nodesInResolutionMode: new Set<string>(),
brokenEdgeIDs: new Map<string, Set<string>>(),
nodeResolutionData: new Map<string, NodeResolutionData>(),
setNodeResolutionMode: (
nodeID: string,
inResolution: boolean,
resolutionData?: NodeResolutionData,
) => {
set((state) => {
const newNodesSet = new Set(state.nodesInResolutionMode);
const newResolutionDataMap = new Map(state.nodeResolutionData);
const newBrokenEdgeIDs = new Map(state.brokenEdgeIDs);
if (inResolution) {
newNodesSet.add(nodeID);
if (resolutionData) {
newResolutionDataMap.set(nodeID, resolutionData);
}
} else {
newNodesSet.delete(nodeID);
newResolutionDataMap.delete(nodeID);
newBrokenEdgeIDs.delete(nodeID); // Clean up broken edges when exiting resolution mode
}
return {
nodesInResolutionMode: newNodesSet,
nodeResolutionData: newResolutionDataMap,
brokenEdgeIDs: newBrokenEdgeIDs,
};
});
},
isNodeInResolutionMode: (nodeID: string) => {
return get().nodesInResolutionMode.has(nodeID);
},
getNodeResolutionData: (nodeID: string) => {
return get().nodeResolutionData.get(nodeID);
},
setBrokenEdgeIDs: (nodeID: string, edgeIDs: string[]) => {
set((state) => {
const newMap = new Map(state.brokenEdgeIDs);
newMap.set(nodeID, new Set(edgeIDs));
return { brokenEdgeIDs: newMap };
});
},
removeBrokenEdgeID: (nodeID: string, edgeID: string) => {
set((state) => {
const newMap = new Map(state.brokenEdgeIDs);
const nodeSet = new Set(newMap.get(nodeID) || []);
nodeSet.delete(edgeID);
newMap.set(nodeID, nodeSet);
return { brokenEdgeIDs: newMap };
});
},
isEdgeBroken: (edgeID: string) => {
// Check across all nodes
const brokenEdgeIDs = get().brokenEdgeIDs;
for (const edgeSet of brokenEdgeIDs.values()) {
if (edgeSet.has(edgeID)) {
return true;
}
}
return false;
},
clearResolutionState: () => {
set({
nodesInResolutionMode: new Set<string>(),
brokenEdgeIDs: new Map<string, Set<string>>(),
nodeResolutionData: new Map<string, NodeResolutionData>(),
});
},
// Helper functions for input renderers
isInputBroken: (nodeID: string, handleID: string) => {
const resolutionData = get().nodeResolutionData.get(nodeID);
if (!resolutionData) return false;
return resolutionData.incompatibilities.missingInputs.includes(handleID);
},
getInputTypeMismatch: (nodeID: string, handleID: string) => {
const resolutionData = get().nodeResolutionData.get(nodeID);
if (!resolutionData) return undefined;
const mismatch = resolutionData.incompatibilities.inputTypeMismatches.find(
(m) => m.name === handleID,
);
return mismatch?.newType;
},
})); }));

View File

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

View File

@@ -18,7 +18,7 @@ function ErrorPageContent() {
) { ) {
window.location.href = "/login"; window.location.href = "/login";
} else { } else {
window.location.href = "/marketplace"; window.document.location.reload();
} }
} }

View File

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

View File

@@ -3,8 +3,7 @@
import type { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent"; 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/CredentialsInput"; 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";
@@ -72,7 +71,6 @@ 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -99,30 +99,4 @@ export function getCredentialDisplayName(
} }
export const OAUTH_TIMEOUT_MS = 5 * 60 * 1000; export const OAUTH_TIMEOUT_MS = 5 * 60 * 1000;
export const MASKED_KEY_LENGTH = 15; 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));
}

View File

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

View File

@@ -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 { useEffect, useRef, useState } from "react"; import { 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,8 +82,6 @@ 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 ||
@@ -91,43 +89,6 @@ 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,
@@ -173,97 +134,91 @@ export function RunAgentModal({
> >
<Dialog.Trigger>{triggerSlot}</Dialog.Trigger> <Dialog.Trigger>{triggerSlot}</Dialog.Trigger>
<Dialog.Content> <Dialog.Content>
<div ref={contentRef} className="flex min-h-full flex-col"> {/* Header */}
<div className="flex-1"> <ModalHeader agent={agent} />
{/* Header */}
<ModalHeader agent={agent} />
{/* Content */} {/* Content */}
{hasAnySetupFields ? ( {hasAnySetupFields ? (
<div className="mt-10 pb-32"> <div className="mt-10">
<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 <Dialog.Footer className="mt-6 bg-white pt-4">
className={`sticky bottom-0 z-10 bg-white pt-4 ${ <div className="flex items-center justify-end gap-3">
hasOverflow {isTriggerRunType ? null : !allRequiredInputsAreSet ? (
? "border-t border-neutral-100 shadow-[0_-2px_8px_rgba(0,0,0,0.04)]" <TooltipProvider>
: "" <Tooltip>
}`} <TooltipTrigger asChild>
> <span>
<div className="flex items-center justify-end gap-3"> <Button
{isTriggerRunType ? null : !allRequiredInputsAreSet ? ( variant="secondary"
<TooltipProvider> onClick={handleOpenScheduleModal}
<Tooltip> disabled={
<TooltipTrigger asChild> isExecuting ||
<span> isSettingUpTrigger ||
<Button !allRequiredInputsAreSet
variant="secondary" }
onClick={handleOpenScheduleModal} >
disabled={ Schedule Task
isExecuting || </Button>
isSettingUpTrigger || </span>
!allRequiredInputsAreSet </TooltipTrigger>
} <TooltipContent>
> <p>
Schedule Task Please set up all required inputs and credentials before
</Button> scheduling
</span> </p>
</TooltipTrigger> </TooltipContent>
<TooltipContent> </Tooltip>
<p> </TooltipProvider>
Please set up all required inputs and credentials ) : (
before scheduling <Button
</p> variant="secondary"
</TooltipContent> onClick={handleOpenScheduleModal}
</Tooltip> disabled={
</TooltipProvider> isExecuting ||
) : ( isSettingUpTrigger ||
<Button !allRequiredInputsAreSet
variant="secondary" }
onClick={handleOpenScheduleModal} >
disabled={isExecuting || isSettingUpTrigger} Schedule Task
> </Button>
Schedule Task )}
</Button> <RunActions
)} defaultRunType={defaultRunType}
<RunActions onRun={handleRun}
defaultRunType={defaultRunType} isExecuting={isExecuting}
onRun={handleRun} isSettingUpTrigger={isSettingUpTrigger}
isExecuting={isExecuting} isRunReady={allRequiredInputsAreSet}
isSettingUpTrigger={isSettingUpTrigger}
isRunReady={allRequiredInputsAreSet}
/>
</div>
<ScheduleAgentModal
isOpen={isScheduleModalOpen}
onClose={handleCloseScheduleModal}
agent={agent}
inputValues={inputValues}
inputCredentials={inputCredentials}
onScheduleCreated={handleScheduleCreated}
/> />
</Dialog.Footer> </div>
</div> <ScheduleAgentModal
isOpen={isScheduleModalOpen}
onClose={handleCloseScheduleModal}
agent={agent}
inputValues={inputValues}
inputCredentials={inputCredentials}
onScheduleCreated={handleScheduleCreated}
/>
</Dialog.Footer>
</Dialog.Content> </Dialog.Content>
</Dialog> </Dialog>
</> </>

View File

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

View File

@@ -1,9 +1,8 @@
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 { useMemo } from "react";
import { RunAgentInputs } from "../../../RunAgentInputs/RunAgentInputs"; import { RunAgentInputs } from "../../../RunAgentInputs/RunAgentInputs";
import { useRunAgentModalContext } from "../../context"; import { useRunAgentModalContext } from "../../context";
import { CredentialsGroupedView } from "../CredentialsGroupedView/CredentialsGroupedView";
import { ModalSection } from "../ModalSection/ModalSection"; import { ModalSection } from "../ModalSection/ModalSection";
import { WebhookTriggerBanner } from "../WebhookTriggerBanner/WebhookTriggerBanner"; import { WebhookTriggerBanner } from "../WebhookTriggerBanner/WebhookTriggerBanner";
@@ -18,16 +17,15 @@ export function ModalRunSection() {
inputValues, inputValues,
setInputValue, setInputValue,
agentInputFields, agentInputFields,
inputCredentials,
setInputCredentialsValue,
agentCredentialsInputFields, agentCredentialsInputFields,
} = useRunAgentModalContext(); } = useRunAgentModalContext();
const inputFields = Object.entries(agentInputFields || {}); const inputFields = Object.entries(agentInputFields || {});
const credentialFields = Object.entries(agentCredentialsInputFields || {});
const credentialFields = useMemo(() => { // Get the list of required credentials from the schema
if (!agentCredentialsInputFields) return [];
return Object.entries(agentCredentialsInputFields);
}, [agentCredentialsInputFields]);
const requiredCredentials = new Set( const requiredCredentials = new Set(
(agent.credentials_input_schema?.required as string[]) || [], (agent.credentials_input_schema?.required as string[]) || [],
); );
@@ -99,10 +97,24 @@ export function ModalRunSection() {
title="Task Credentials" title="Task Credentials"
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"
> >
<CredentialsGroupedView <div className="space-y-6">
credentialFields={credentialFields} {Object.entries(agentCredentialsInputFields || {}).map(
requiredCredentials={requiredCredentials} ([key, inputSubSchema]) => (
/> <CredentialsInput
key={key}
schema={
{ ...inputSubSchema, discriminator: undefined } as any
}
selectedCredentials={inputCredentials?.[key]}
onSelectCredentials={(value) =>
setInputCredentialsValue(key, value)
}
siblingInputs={inputValues}
isOptional={!requiredCredentials.has(key)}
/>
),
)}
</div>
</ModalSection> </ModalSection>
) : null} ) : null}
</div> </div>

View File

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

View File

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

View File

@@ -1,17 +1,37 @@
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="ghost" variant={selected ? "secondary" : "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 size={18} className="text-zinc-600" /> <GearIcon
<Text variant="small">Agent Settings</Text> size={18}
className={selected ? "text-zinc-900" : "text-zinc-600"}
/>
</Button> </Button>
); );
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,16 +1,22 @@
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 agent={props.agent}> <SelectedViewLayout
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)}
> >

View File

@@ -33,6 +33,8 @@ 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({
@@ -41,6 +43,8 @@ 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);
@@ -80,7 +84,12 @@ 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 agent={agent} banner={banner}> <SelectedViewLayout
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} />

View File

@@ -21,6 +21,8 @@ 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({
@@ -28,6 +30,8 @@ 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,
@@ -72,7 +76,12 @@ 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 agent={agent} banner={banner}> <SelectedViewLayout
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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { Breadcrumbs } from "@/components/molecules/Breadcrumbs/Breadcrumbs"; 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 { 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,6 +9,8 @@ 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) {
@@ -17,8 +19,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} {props.banner && <div className="mb-4">{props.banner}</div>}
<div className="relative flex w-full items-center justify-between"> <div className="relative flex w-fit items-center gap-2">
<Breadcrumbs <Breadcrumbs
items={[ items={[
{ name: "My Library", link: "/library" }, { name: "My Library", link: "/library" },
@@ -31,9 +33,15 @@ export function SelectedViewLayout(props: Props) {
: []), : []),
]} ]}
/> />
<div className="absolute right-0"> {props.agent && props.onSelectSettings && (
<AgentSettingsModal agent={props.agent} /> <div className="absolute -right-8">
</div> <AgentSettingsButton
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">

View File

@@ -12,7 +12,7 @@ import {
} 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 { CredentialsInput } from "@/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/CredentialsInputs/CredentialsInput"; import { CredentialsInput } from "@/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/CredentialsInputs/CredentialsInputs";
import { RunAgentInputs } from "@/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/RunAgentInputs/RunAgentInputs"; import { RunAgentInputs } from "@/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/RunAgentInputs/RunAgentInputs";
import { ScheduleTaskDialog } from "@/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/cron-scheduler-dialog"; import { ScheduleTaskDialog } from "@/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/cron-scheduler-dialog";
import ActionButtonGroup from "@/components/__legacy__/action-button-group"; import ActionButtonGroup from "@/components/__legacy__/action-button-group";

View File

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

View File

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

View File

@@ -20,7 +20,6 @@ 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;

View File

@@ -32,7 +32,7 @@ export const extendedButtonVariants = cva(
), ),
}, },
size: { size: {
small: "px-3 py-2 text-sm gap-1.5 h-[2.25rem]", small: "px-3 py-2 text-sm gap-1.5 h-[2.25rem] min-w-[5.5rem]",
large: "px-4 py-3 text-sm gap-2 h-[3.25rem]", large: "px-4 py-3 text-sm gap-2 h-[3.25rem]",
icon: "p-3 !min-w-0", icon: "p-3 !min-w-0",
}, },

View File

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

View File

@@ -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 Props { interface MarketplaceBannersProps {
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,
}: Props) { }: MarketplaceBannersProps) {
const renderUpdateBanner = () => { const renderUpdateBanner = () => {
if (hasUpdate && latestVersion) { if (hasUpdate && latestVersion) {
return ( return (

View File

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

View File

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

View File

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

View File

@@ -30,8 +30,6 @@ export const FormRenderer = ({
return generateUiSchemaForCustomFields(preprocessedSchema, uiSchema); return generateUiSchemaForCustomFields(preprocessedSchema, uiSchema);
}, [preprocessedSchema, uiSchema]); }, [preprocessedSchema, uiSchema]);
console.log("preprocessedSchema", preprocessedSchema);
return ( return (
<div className={"mb-6 mt-4"}> <div className={"mb-6 mt-4"}>
<Form <Form

View File

@@ -2,10 +2,12 @@ import { FieldProps, getUiOptions, getWidget } from "@rjsf/utils";
import { AnyOfFieldTitle } from "./components/AnyOfFieldTitle"; import { AnyOfFieldTitle } from "./components/AnyOfFieldTitle";
import { isEmpty } from "lodash"; import { isEmpty } from "lodash";
import { useAnyOfField } from "./useAnyOfField"; import { useAnyOfField } from "./useAnyOfField";
import { getHandleId, updateUiOption } from "../../helpers"; import { cleanUpHandleId, getHandleId, updateUiOption } from "../../helpers";
import { useEdgeStore } from "@/app/(platform)/build/stores/edgeStore"; import { useEdgeStore } from "@/app/(platform)/build/stores/edgeStore";
import { ANY_OF_FLAG } from "../../constants"; import { ANY_OF_FLAG } from "../../constants";
import { findCustomFieldId } from "../../registry"; import { findCustomFieldId } from "../../registry";
import { useNodeStore } from "@/app/(platform)/build/stores/nodeStore";
import { cn } from "@/lib/utils";
export const AnyOfField = (props: FieldProps) => { export const AnyOfField = (props: FieldProps) => {
const { registry, schema } = props; const { registry, schema } = props;
@@ -21,6 +23,8 @@ export const AnyOfField = (props: FieldProps) => {
field_id, field_id,
} = useAnyOfField(props); } = useAnyOfField(props);
const isInputBroken = useNodeStore((state) => state.isInputBroken);
const parentCustomFieldId = findCustomFieldId(schema); const parentCustomFieldId = findCustomFieldId(schema);
if (parentCustomFieldId) { if (parentCustomFieldId) {
return null; return null;
@@ -43,6 +47,7 @@ export const AnyOfField = (props: FieldProps) => {
}); });
const isHandleConnected = isInputConnected(nodeId, handleId); const isHandleConnected = isInputConnected(nodeId, handleId);
const isAnyOfInputBroken = isInputBroken(nodeId, cleanUpHandleId(handleId));
// Now anyOf can render - custom fields if the option schema matches a custom field // Now anyOf can render - custom fields if the option schema matches a custom field
const optionCustomFieldId = optionSchema const optionCustomFieldId = optionSchema
@@ -78,7 +83,11 @@ export const AnyOfField = (props: FieldProps) => {
registry={registry} registry={registry}
placeholder={props.placeholder} placeholder={props.placeholder}
autocomplete={props.autocomplete} autocomplete={props.autocomplete}
className="-ml-1 h-[22px] w-fit gap-1 px-1 pl-2 text-xs font-medium" className={cn(
"-ml-1 h-[22px] w-fit gap-1 px-1 pl-2 text-xs font-medium",
isAnyOfInputBroken &&
"border-red-500 bg-red-100 text-red-600 line-through",
)}
autofocus={props.autofocus} autofocus={props.autofocus}
label="" label=""
hideLabel={true} hideLabel={true}
@@ -93,7 +102,7 @@ export const AnyOfField = (props: FieldProps) => {
selector={selector} selector={selector}
uiSchema={updatedUiSchema} uiSchema={updatedUiSchema}
/> />
{!isHandleConnected && optionsSchemaField} {!isHandleConnected && !isAnyOfInputBroken && optionsSchemaField}
</div> </div>
); );
}; };

View File

@@ -13,6 +13,7 @@ import { Text } from "@/components/atoms/Text/Text";
import { isOptionalType } from "../../../utils/schema-utils"; import { isOptionalType } from "../../../utils/schema-utils";
import { getTypeDisplayInfo } from "@/app/(platform)/build/components/FlowEditor/nodes/helpers"; import { getTypeDisplayInfo } from "@/app/(platform)/build/components/FlowEditor/nodes/helpers";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { useNodeStore } from "@/app/(platform)/build/stores/nodeStore";
interface customFieldProps extends FieldProps { interface customFieldProps extends FieldProps {
selector: JSX.Element; selector: JSX.Element;
@@ -51,6 +52,13 @@ export const AnyOfFieldTitle = (props: customFieldProps) => {
shouldShowTypeSelector(schema) && !isArrayItem && !isHandleConnected; shouldShowTypeSelector(schema) && !isArrayItem && !isHandleConnected;
const shoudlShowType = isHandleConnected || (isOptional && type); const shoudlShowType = isHandleConnected || (isOptional && type);
const isInputBroken = useNodeStore((state) =>
state.isInputBroken(nodeId, cleanUpHandleId(uiOptions.handleId)),
);
const inputMismatch = useNodeStore((state) =>
state.getInputTypeMismatch(nodeId, cleanUpHandleId(uiOptions.handleId)),
);
return ( return (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<TitleFieldTemplate <TitleFieldTemplate
@@ -62,8 +70,16 @@ export const AnyOfFieldTitle = (props: customFieldProps) => {
uiSchema={uiSchema} uiSchema={uiSchema}
/> />
{shoudlShowType && ( {shoudlShowType && (
<Text variant="small" className={cn("text-zinc-700", colorClass)}> <Text
{isOptional ? `(${displayType})` : "(any)"} variant="small"
className={cn(
"text-zinc-700",
isInputBroken && "line-through",
colorClass,
inputMismatch && "rounded-md bg-red-100 px-1 !text-red-500",
)}
>
{isOptional ? `(${inputMismatch || displayType})` : "(any)"}
</Text> </Text>
)} )}
{shouldShowSelector && selector} {shouldShowSelector && selector}

View File

@@ -9,8 +9,9 @@ import { Text } from "@/components/atoms/Text/Text";
import { getTypeDisplayInfo } from "@/app/(platform)/build/components/FlowEditor/nodes/helpers"; import { getTypeDisplayInfo } from "@/app/(platform)/build/components/FlowEditor/nodes/helpers";
import { isAnyOfSchema } from "../../utils/schema-utils"; import { isAnyOfSchema } from "../../utils/schema-utils";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { isArrayItem } from "../../helpers"; import { cleanUpHandleId, isArrayItem } from "../../helpers";
import { InputNodeHandle } from "@/app/(platform)/build/components/FlowEditor/handlers/NodeHandle"; import { InputNodeHandle } from "@/app/(platform)/build/components/FlowEditor/handlers/NodeHandle";
import { useNodeStore } from "@/app/(platform)/build/stores/nodeStore";
export default function TitleField(props: TitleFieldProps) { export default function TitleField(props: TitleFieldProps) {
const { id, title, required, schema, registry, uiSchema } = props; const { id, title, required, schema, registry, uiSchema } = props;
@@ -26,6 +27,11 @@ export default function TitleField(props: TitleFieldProps) {
const smallText = isArrayItemFlag || additional; const smallText = isArrayItemFlag || additional;
const showHandle = uiOptions.showHandles ?? showHandles; const showHandle = uiOptions.showHandles ?? showHandles;
const isInputBroken = useNodeStore((state) =>
state.isInputBroken(nodeId, cleanUpHandleId(uiOptions.handleId)),
);
return ( return (
<div className="flex items-center"> <div className="flex items-center">
{showHandle !== false && ( {showHandle !== false && (
@@ -34,7 +40,11 @@ export default function TitleField(props: TitleFieldProps) {
<Text <Text
variant={isArrayItemFlag ? "small" : "body"} variant={isArrayItemFlag ? "small" : "body"}
id={id} id={id}
className={cn("line-clamp-1", smallText && "text-sm text-zinc-700")} className={cn(
"line-clamp-1",
smallText && "text-sm text-zinc-700",
isInputBroken && "text-red-500 line-through",
)}
> >
{title} {title}
</Text> </Text>
@@ -44,7 +54,7 @@ export default function TitleField(props: TitleFieldProps) {
{!isAnyOf && ( {!isAnyOf && (
<Text <Text
variant="small" variant="small"
className={cn("ml-2", colorClass)} className={cn("ml-2", isInputBroken && "line-through", colorClass)}
id={description_id} id={description_id}
> >
({displayType}) ({displayType})

View File

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

View File

@@ -30,6 +30,8 @@ export function updateUiOption<T extends Record<string, any>>(
} }
export const cleanUpHandleId = (handleId: string) => { export const cleanUpHandleId = (handleId: string) => {
if (!handleId) return "";
let newHandleId = handleId; let newHandleId = handleId;
if (handleId.includes(ANY_OF_FLAG)) { if (handleId.includes(ANY_OF_FLAG)) {
newHandleId = newHandleId.replace(ANY_OF_FLAG, ""); newHandleId = newHandleId.replace(ANY_OF_FLAG, "");

View File

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

View File

@@ -233,13 +233,14 @@ export default function useAgentGraph(
title: `${block.name} ${node.id}`, title: `${block.name} ${node.id}`,
inputSchema: block.inputSchema, inputSchema: block.inputSchema,
outputSchema: block.outputSchema, outputSchema: block.outputSchema,
isOutputStatic: block.staticOutput,
hardcodedValues: node.input_default, hardcodedValues: node.input_default,
uiType: block.uiType, uiType: block.uiType,
metadata: metadata, metadata: metadata,
connections: graph.links connections: graph.links
.filter((l) => [l.source_id, l.sink_id].includes(node.id)) .filter((l) => [l.source_id, l.sink_id].includes(node.id))
.map((link) => ({ .map((link) => ({
edge_id: formatEdgeID(link), id: formatEdgeID(link),
source: link.source_id, source: link.source_id,
sourceHandle: link.source_name, sourceHandle: link.source_name,
target: link.sink_id, target: link.sink_id,

View File

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

View File

@@ -245,8 +245,8 @@ export type BlockIONullSubSchema = BlockIOSubSchemaMeta & {
// At the time of writing, combined schemas only occur on the first nested level in a // At the time of writing, combined schemas only occur on the first nested level in a
// block schema. It is typed this way to make the use of these objects less tedious. // block schema. It is typed this way to make the use of these objects less tedious.
type BlockIOCombinedTypeSubSchema = BlockIOSubSchemaMeta & { type BlockIOCombinedTypeSubSchema = BlockIOSubSchemaMeta & {
type: never; type?: never;
const: never; const?: never;
} & ( } & (
| { | {
allOf: [BlockIOSimpleTypeSubSchema]; allOf: [BlockIOSimpleTypeSubSchema];
@@ -368,8 +368,8 @@ export type GraphMeta = {
recommended_schedule_cron: string | null; recommended_schedule_cron: string | null;
forked_from_id?: GraphID | null; forked_from_id?: GraphID | null;
forked_from_version?: number | null; forked_from_version?: number | null;
input_schema: GraphIOSchema; input_schema: GraphInputSchema;
output_schema: GraphIOSchema; output_schema: GraphOutputSchema;
credentials_input_schema: CredentialsInputSchema; credentials_input_schema: CredentialsInputSchema;
} & ( } & (
| { | {
@@ -385,19 +385,51 @@ export type GraphMeta = {
export type GraphID = Brand<string, "GraphID">; export type GraphID = Brand<string, "GraphID">;
/* Derived from backend/data/graph.py:Graph._generate_schema() */ /* Derived from backend/data/graph.py:Graph._generate_schema() */
export type GraphIOSchema = { export type GraphInputSchema = {
type: "object"; type: "object";
properties: Record<string, GraphIOSubSchema>; properties: Record<string, GraphInputSubSchema>;
required: (keyof BlockIORootSchema["properties"])[]; required: (keyof GraphInputSchema["properties"])[];
}; };
export type GraphIOSubSchema = Omit< export type GraphInputSubSchema = GraphOutputSubSchema &
BlockIOSubSchemaMeta, (
"placeholder" | "depends_on" | "hidden" | { type?: never; default: any | null } // AgentInputBlock (generic Any type)
> & { | { type: "string"; format: "short-text"; default: string | null } // AgentShortTextInputBlock
type: never; // bodge to avoid type checking hell; doesn't exist at runtime | { type: "string"; format: "long-text"; default: string | null } // AgentLongTextInputBlock
default?: string; | { type: "integer"; default: number | null } // AgentNumberInputBlock
| { type: "string"; format: "date"; default: string | null } // AgentDateInputBlock
| { type: "string"; format: "time"; default: string | null } // AgentTimeInputBlock
| { type: "string"; format: "file"; default: string | null } // AgentFileInputBlock
| { type: "string"; enum: string[]; default: string | null } // AgentDropdownInputBlock
| { type: "boolean"; default: boolean } // AgentToggleInputBlock
| {
// AgentTableInputBlock
type: "array";
format: "table";
items: {
type: "object";
properties: Record<string, { type: "string" }>;
};
default: Array<Record<string, string>> | null;
}
| {
// AgentGoogleDriveFileInputBlock
type: "object";
format: "google-drive-picker";
google_drive_picker_config?: GoogleDrivePickerConfig;
default: GoogleDriveFile | null;
}
);
export type GraphOutputSchema = {
type: "object";
properties: Record<string, GraphOutputSubSchema>;
required: (keyof GraphOutputSchema["properties"])[];
};
export type GraphOutputSubSchema = {
// TODO: typed outputs based on the incoming edges?
title: string;
description?: string;
advanced: boolean;
secret: boolean; secret: boolean;
metadata?: any;
}; };
export type CredentialsInputSchema = { export type CredentialsInputSchema = {
@@ -440,8 +472,8 @@ export type GraphUpdateable = Omit<
is_active?: boolean; is_active?: boolean;
nodes: NodeCreatable[]; nodes: NodeCreatable[];
links: LinkCreatable[]; links: LinkCreatable[];
input_schema?: GraphIOSchema; input_schema?: GraphInputSchema;
output_schema?: GraphIOSchema; output_schema?: GraphOutputSchema;
}; };
export type GraphCreatable = _GraphCreatableInner & { export type GraphCreatable = _GraphCreatableInner & {
@@ -497,8 +529,8 @@ export type LibraryAgent = {
name: string; name: string;
description: string; description: string;
instructions?: string | null; instructions?: string | null;
input_schema: GraphIOSchema; input_schema: GraphInputSchema;
output_schema: GraphIOSchema; output_schema: GraphOutputSchema;
credentials_input_schema: CredentialsInputSchema; credentials_input_schema: CredentialsInputSchema;
new_output: boolean; new_output: boolean;
can_access_graph: boolean; can_access_graph: boolean;
@@ -593,7 +625,6 @@ 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 */

View File

@@ -6,7 +6,10 @@ import { NodeDimension } from "@/app/(platform)/build/components/legacy-builder/
import { import {
BlockIOObjectSubSchema, BlockIOObjectSubSchema,
BlockIORootSchema, BlockIORootSchema,
BlockIOSubSchema,
Category, Category,
GraphInputSubSchema,
GraphOutputSubSchema,
} from "@/lib/autogpt-server-api/types"; } from "@/lib/autogpt-server-api/types";
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
@@ -76,8 +79,8 @@ export function getTypeBgColor(type: string | null): string {
); );
} }
export function getTypeColor(type: string | null): string { export function getTypeColor(type: string | undefined): string {
if (type === null) return "#6b7280"; if (!type) return "#6b7280";
return ( return (
{ {
string: "#22c55e", string: "#22c55e",
@@ -88,11 +91,59 @@ export function getTypeColor(type: string | null): string {
array: "#6366f1", array: "#6366f1",
null: "#6b7280", null: "#6b7280",
any: "#6b7280", any: "#6b7280",
"": "#6b7280",
}[type] || "#6b7280" }[type] || "#6b7280"
); );
} }
/**
* Extracts the effective type from a JSON schema, handling anyOf/oneOf/allOf wrappers.
* Returns the first non-null type found in the schema structure.
*/
export function getEffectiveType(
schema:
| BlockIOSubSchema
| GraphInputSubSchema
| GraphOutputSubSchema
| null
| undefined,
): string | undefined {
if (!schema) return undefined;
// Direct type property
if ("type" in schema && schema.type) {
return String(schema.type);
}
// Handle allOf - typically a single-item wrapper
if (
"allOf" in schema &&
Array.isArray(schema.allOf) &&
schema.allOf.length > 0
) {
return getEffectiveType(schema.allOf[0]);
}
// Handle anyOf - e.g. [{ type: "string" }, { type: "null" }]
if ("anyOf" in schema && Array.isArray(schema.anyOf)) {
for (const item of schema.anyOf) {
if ("type" in item && item.type !== "null") {
return String(item.type);
}
}
}
// Handle oneOf
if ("oneOf" in schema && Array.isArray(schema.oneOf)) {
for (const item of schema.oneOf) {
if ("type" in item && item.type !== "null") {
return String(item.type);
}
}
}
return undefined;
}
export function beautifyString(name: string): string { export function beautifyString(name: string): string {
// Regular expression to identify places to split, considering acronyms // Regular expression to identify places to split, considering acronyms
const result = name const result = name

View File

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

View File

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