Skip to content

feat: add vault and adapter identity display#524

Merged
antoncoding merged 13 commits into
masterfrom
feat/vault-and-adapter
May 14, 2026
Merged

feat: add vault and adapter identity display#524
antoncoding merged 13 commits into
masterfrom
feat/vault-and-adapter

Conversation

@antoncoding
Copy link
Copy Markdown
Owner

@antoncoding antoncoding commented May 14, 2026

Summary by CodeRabbit

  • New Features

    • Vault identity resolution and a hook to detect vault/account kinds
    • Vault metadata images with graceful fallbacks; richer identity chips, badges, tooltips and explicit linked‑vault links
    • New vault info section (asset, network, curator) and managed‑exposures view for adapter positions
    • Helper to format vault adapter type labels
  • Behavior

    • Positions page gating and data fetches now respect vault identity; account identity accepts optional chainId and button disabled/loading handling improved
  • Documentation

    • Expanded UI guidance for identity/relationship metadata, vault positioning, and compact headers

Review Change Stack

@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented May 14, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
monarch Ready Ready Preview, Comment May 14, 2026 4:23pm

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 14, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a vault account identity API and hook, surfaces vault metadata in AccountIdentity, gates positions queries for vault pages, adds vault-specific UI components (AccountVaultInfo, VaultManagedExposures), and appends validation docs.

Changes

Vault Identity & Positions UI Integration

Layer / File(s) Summary
Vault identity types & resolver
src/contexts/VaultRegistryContext.tsx, src/utils/vaults.ts
Adds VaultAccountKind/VaultAccountIdentity, normalizes addresses, maps Morpho vaults and Monarch adapter-aliases, implements getVaultAccountIdentity, and updates provider API.
useVaultAccountIdentity & address label hookup
src/hooks/useVaultAccountIdentity.ts, src/hooks/useAddressLabel.ts
Adds useVaultAccountIdentity hook and updates useAddressLabel to return vaultIdentity, vaultName, and shortAddress derived from the registry.
AccountIdentity UI & Button changes
src/components/shared/account-identity.tsx, src/components/ui/button.tsx
AccountIdentity prefers vault metadata images with fallback, computes mainTag/entityBadge/linked-vault links, and uses resolved chainId when available; Button explicitly computes isDisabled and conditionally renders Slot or native <button>.
Vault info & managed exposures
src/features/positions/components/account-vault-info.tsx, src/features/positions/components/adapter-managed-exposure.tsx
New AccountVaultInfo routes rendering by identity kind; VaultV2Info fetches vault data for VaultInfoRow; VaultInfoRow shows network/asset/curator chips and Morpho external link; VaultManagedExposures shows per-adapter exposure tables with loading/empty states.
Positions view gating & integration
src/features/positions/positions-view.tsx
Positions derives accountVaultIdentity, computes showNativeAccountSections to gate native account UI/data, renders AccountVaultInfo in header, and conditionally renders VaultManagedExposures for vault-v2.
Position summary hook: enabled gating
src/hooks/useUserPositionsSummaryData.ts
Hook adds options?: { enabled?: boolean }, derives activeUser when enabled, gates transactions/snapshots/current blocks by activeUser, and short-circuits refetch when inactive.
Utils & Validation docs
src/utils/vaults.ts, docs/VALIDATIONS.md
Adds formatVaultAdapterType(adapterType?), and inserts validation rules for identity header composition, relationship metadata linking, neutral entity styling, adapter-held vault rendering constraints, and compact header density guidelines.

Sequence Diagram

sequenceDiagram
  participant User
  participant PositionsView
  participant useVaultAccountIdentity as Hook
  participant VaultRegistry
  participant AccountIdentity
  participant AccountVaultInfo
  User->>PositionsView: open account positions
  PositionsView->>Hook: useVaultAccountIdentity(account)
  Hook->>VaultRegistry: getVaultAccountIdentity(address, chainId?)
  VaultRegistry->>VaultRegistry: normalize & classify address
  VaultRegistry->>Hook: VaultAccountIdentity | undefined
  PositionsView->>AccountIdentity: render with vaultIdentity
  AccountIdentity->>AccountIdentity: prefer metadata image, show tags/links
  PositionsView->>AccountVaultInfo: render vault-specific info (if any)
  PositionsView->>PositionsView: gate native sections by identity.kind
Loading

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested labels

feature request, ui

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add vault and adapter identity display' directly matches the main changes in the PR, which introduce vault/adapter identity components, types, and display logic across multiple files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/vault-and-adapter

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a comprehensive identity system for vaults and adapters, including new UI components like AccountVaultInfo and VaultManagedExposures. The VaultRegistryContext has been expanded to support detailed VaultAccountIdentity metadata, and the Positions view now conditionally renders sections based on whether the account is a V2 vault. A critical issue was identified in the Button component where the disabled prop is not correctly passed to the Radix Slot when asChild is true, which would prevent underlying elements from receiving the disabled state.

Comment on lines +94 to +105
if (asChild) {
return (
<Slot
className={buttonClassName}
ref={ref}
{...props}
aria-disabled={isDisabled}
>
{children}
</Slot>
);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

When asChild is true, the disabled prop is not being passed to the Slot component. Since disabled was destructured from the component arguments, it is no longer part of the props object spread onto the Slot. This results in the underlying child element (like a native button) not receiving the disabled state, even if isLoading is true or the disabled prop was explicitly provided. While the new validation rule 96 in VALIDATIONS.md correctly advises against appending siblings like spinners to a slotted child, the functional disabled state must still be synchronized.

Suggested change
if (asChild) {
return (
<Slot
className={buttonClassName}
ref={ref}
{...props}
aria-disabled={isDisabled}
>
{children}
</Slot>
);
}
if (asChild) {
return (
<Slot
className={buttonClassName}
ref={ref}
{...props}
disabled={isDisabled}
aria-disabled={isDisabled}
>
{children}
</Slot>
);
}

@coderabbitai coderabbitai Bot added feature request Specific feature ready to be implemented ui User interface labels May 14, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/features/positions/positions-view.tsx (1)

157-165: ⚡ Quick win

Render managed exposures for all vault-v2 pages.

VaultManagedExposures already resolves adapters from vault data. Gating on adapterAddress can hide exposures when fallback adapter info is absent.

Proposed change
-        {accountVaultIdentity?.kind === 'vault-v2' && accountVaultIdentity.adapterAddress && (
+        {accountVaultIdentity?.kind === 'vault-v2' && (
           <VaultManagedExposures
             vaultAddress={account as Address}
             fallbackAdapterAddress={accountVaultIdentity.adapterAddress}
             fallbackAdapterType={accountVaultIdentity.adapterType}
             chainId={accountVaultIdentity.chainId}
             period={period}
           />
         )}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/positions/positions-view.tsx` around lines 157 - 165, The
current render is gating VaultManagedExposures on
accountVaultIdentity.adapterAddress which hides managed exposures when fallback
adapter info is missing; change the condition to render VaultManagedExposures
whenever accountVaultIdentity?.kind === 'vault-v2' (i.e., remove the
adapterAddress truthiness check) so the component can resolve adapters from
vault data itself; keep passing vaultAddress (account as Address) and the
optional fallback props (fallbackAdapterAddress, fallbackAdapterType, chainId,
period) so VaultManagedExposures can use fallbacks when available.
src/hooks/useUserPositionsSummaryData.ts (1)

76-93: ⚡ Quick win

Guard transaction cache operations behind activeUser.

When the hook is disabled, this still runs merge/reconcile with userAddress: undefined. That can create noisy cache writes on the disabled path. Short-circuit both paths when no active user.

Proposed change
   const mergedTransactions = useMemo(
-    () =>
-      mergeUserTransactionsWithRecentCache({
-        userAddress: activeUser,
-        chainIds: uniqueChainIds,
-        apiTransactions: txData?.items ?? [],
-      }),
+    () =>
+      activeUser
+        ? mergeUserTransactionsWithRecentCache({
+            userAddress: activeUser,
+            chainIds: uniqueChainIds,
+            apiTransactions: txData?.items ?? [],
+          })
+        : [],
     [activeUser, uniqueChainIds, txData?.items],
   );

   useEffect(() => {
+    if (!activeUser) return;
     reconcileUserTransactionHistoryCache({
       userAddress: activeUser,
       chainIds: uniqueChainIds,
       apiTransactions: txData?.items ?? [],
     });
   }, [activeUser, uniqueChainIds, txData?.items]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hooks/useUserPositionsSummaryData.ts` around lines 76 - 93, The merge and
reconcile calls run even when activeUser is undefined; guard both by
short-circuiting when activeUser is falsy: in the useMemo that computes
mergedTransactions, return an empty array (or appropriate default) immediately
if activeUser is not set instead of calling
mergeUserTransactionsWithRecentCache; similarly, in the useEffect that calls
reconcileUserTransactionHistoryCache, early-return (do nothing) when activeUser
is falsy so no cache writes occur; keep uniqueChainIds and txData?.items in the
dependency arrays and reference mergeUserTransactionsWithRecentCache and
reconcileUserTransactionHistoryCache to locate the two places to change.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/ui/button.tsx`:
- Around line 94-105: When asChild is true, the Slot rendering uses
aria-disabled but not the disabled attribute so native button children remain
interactive; update the Slot invocation inside the asChild branch to forward the
disabled attribute (disabled={isDisabled}) so it reaches native button elements,
and ensure the resolved className (buttonClassName) also conditionally includes
pointer-events-none when isDisabled is true to cover non-button children that
rely on CSS for disabling; reference the asChild branch where Slot is rendered
and the isDisabled and buttonClassName symbols.

---

Nitpick comments:
In `@src/features/positions/positions-view.tsx`:
- Around line 157-165: The current render is gating VaultManagedExposures on
accountVaultIdentity.adapterAddress which hides managed exposures when fallback
adapter info is missing; change the condition to render VaultManagedExposures
whenever accountVaultIdentity?.kind === 'vault-v2' (i.e., remove the
adapterAddress truthiness check) so the component can resolve adapters from
vault data itself; keep passing vaultAddress (account as Address) and the
optional fallback props (fallbackAdapterAddress, fallbackAdapterType, chainId,
period) so VaultManagedExposures can use fallbacks when available.

In `@src/hooks/useUserPositionsSummaryData.ts`:
- Around line 76-93: The merge and reconcile calls run even when activeUser is
undefined; guard both by short-circuiting when activeUser is falsy: in the
useMemo that computes mergedTransactions, return an empty array (or appropriate
default) immediately if activeUser is not set instead of calling
mergeUserTransactionsWithRecentCache; similarly, in the useEffect that calls
reconcileUserTransactionHistoryCache, early-return (do nothing) when activeUser
is falsy so no cache writes occur; keep uniqueChainIds and txData?.items in the
dependency arrays and reference mergeUserTransactionsWithRecentCache and
reconcileUserTransactionHistoryCache to locate the two places to change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: a6c34478-a38a-45d3-98e8-1e1995780ecd

📥 Commits

Reviewing files that changed from the base of the PR and between 7ff7ca2 and e2f30e0.

📒 Files selected for processing (11)
  • docs/VALIDATIONS.md
  • src/components/shared/account-identity.tsx
  • src/components/ui/button.tsx
  • src/contexts/VaultRegistryContext.tsx
  • src/features/positions/components/account-vault-info.tsx
  • src/features/positions/components/adapter-managed-exposure.tsx
  • src/features/positions/positions-view.tsx
  • src/hooks/useAddressLabel.ts
  • src/hooks/useUserPositionsSummaryData.ts
  • src/hooks/useVaultAccountIdentity.ts
  • src/utils/vaults.ts

Comment thread src/components/ui/button.tsx
@antoncoding
Copy link
Copy Markdown
Owner Author

/gemini review

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a comprehensive identity system for vaults and adapters, enhancing the AccountIdentity component with metadata images, tooltips, and specific tags for different entity types. It adds new components like AccountVaultInfo and VaultManagedExposures to the positions view, allowing for a more detailed display of vault-related data and managed exposures. Additionally, the Button component was refactored for better Radix Slot integration, and the VaultRegistryContext was expanded to support the new identity structure. A performance improvement was suggested in the VaultRegistryContext to use .find() instead of .filter().map() when searching for aliases to avoid unnecessary array iterations.

Comment on lines +157 to 172
const adapterAlias = chainId
? adapterAliasesByScopedAddress.get(getAddressKey(normalizedAddress, chainId))
: adapterAliases.filter((candidate) => candidate.address.toLowerCase() === normalizedAddress).map(toAdapterAddressAlias)[0];

if (adapterAlias) {
return adapterAliasToAdapterIdentity(adapterAlias, getVaultByAddress(toAddress(adapterAlias.vaultAddress), adapterAlias.chainId));
}

if (!chainId) {
return undefined;
const vaultAliases = chainId
? (adapterAliasesByVaultScopedAddress.get(getAddressKey(normalizedAddress, chainId)) ?? [])
: adapterAliases.filter((candidate) => candidate.vaultAddress.toLowerCase() === normalizedAddress).map(toAdapterAddressAlias);

const vaultAlias = vaultAliases[0];
if (vaultAlias) {
return adapterAliasToVaultIdentity(vaultAlias, getVaultByAddress(address, vaultAlias.chainId));
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current implementation uses .filter().map()[0] and .filter().map() which iterates over the entire adapterAliases array even when only the first match is needed. Using .find() is more efficient as it stops iteration once a match is found. Additionally, since only the first element of vaultAliases is used, the logic can be simplified to directly retrieve the first matching alias from the map or the array.

      const adapterAlias = chainId
        ? adapterAliasesByScopedAddress.get(getAddressKey(normalizedAddress, chainId))
        : (() => {
            const match = adapterAliases.find((c) => c.address.toLowerCase() === normalizedAddress);
            return match ? toAdapterAddressAlias(match) : undefined;
          })();

      if (adapterAlias) {
        return adapterAliasToAdapterIdentity(adapterAlias, getVaultByAddress(toAddress(adapterAlias.vaultAddress), adapterAlias.chainId));
      }

      const vaultAlias = chainId
        ? adapterAliasesByVaultScopedAddress.get(getAddressKey(normalizedAddress, chainId))?.[0]
        : (() => {
            const match = adapterAliases.find((c) => c.vaultAddress.toLowerCase() === normalizedAddress);
            return match ? toAdapterAddressAlias(match) : undefined;
          })();

      if (vaultAlias) {
        return adapterAliasToVaultIdentity(vaultAlias, getVaultByAddress(address, vaultAlias.chainId));
      }

@antoncoding antoncoding changed the title feat: vault and adatper display feat: add vault and adapter identity display May 14, 2026
Copy link
Copy Markdown
Collaborator

@starksama starksama left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Request changes. The Button/Slot issue is addressed and the remaining Gemini .find() note is non-blocking, but vault-v2 page gating currently treats unresolved vault metadata as a native account. That can render/fetch the native account sections on cold load before adapter aliases resolve, which is exactly the state this PR is trying to avoid.

const { addVisitedAddress, toggleAddressBookmark, isAddressBookmarked } = usePortfolioBookmarks();
const accountVaultIdentity = useVaultAccountIdentity(account);
const isV2VaultPage = accountVaultIdentity?.kind === 'vault-v2';
const showNativeAccountSections = !isV2VaultPage;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes an unresolved identity equivalent to a non-vault account. On a direct/cold load, useVaultAccountIdentity(account) can be undefined while the vault registry/adapter aliases are still loading, so showNativeAccountSections becomes true and the page starts fetching/rendering native vault-address positions/vault tables before later flipping to the vault-v2 view. That violates the new validation rule about metadata-backed display guards not treating missing metadata as a negative match, and it can briefly show the exact native-account sections this PR is meant to suppress. The guard needs a readiness state from VaultRegistryContext/useVaultAccountIdentity and should hold the native sections in loading/neutral state until vault identity can actually be evaluated.

Copy link
Copy Markdown
Collaborator

@starksama starksama left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved after follow-up fix. Native account sections now stay behind vault identity readiness, so vault-v2 pages no longer treat unresolved metadata as a negative match during cold load.

@starksama
Copy link
Copy Markdown
Collaborator

/gemini review

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a robust identity system for vaults and adapters, adding the VaultAccountIdentity type and updating the VaultRegistryContext to support detailed metadata resolution. Key UI enhancements include new components for vault information and adapter-managed exposures, alongside a refactored AccountIdentity component. Review feedback suggests optimizing performance in the registry context by using pre-calculated maps for address lookups and simplifying the Button component's asChild logic by passing the disabled prop directly to the Radix Slot instead of using cloneElement.

Comment on lines +156 to +183
const getVaultAccountIdentity = useCallback(
(address: Address, chainId?: number): VaultAccountIdentity | undefined => {
const normalizedAddress = address.toLowerCase();

const adapterAlias = chainId
? adapterAliasesByScopedAddress.get(getAddressKey(normalizedAddress, chainId))
: toOptionalAdapterAddressAlias(adapterAliases.find((candidate) => candidate.address.toLowerCase() === normalizedAddress));

if (adapterAlias) {
return adapterAliasToAdapterIdentity(adapterAlias, getVaultByAddress(toAddress(adapterAlias.vaultAddress), adapterAlias.chainId));
}

if (!chainId) {
return undefined;
const vaultAlias = chainId
? adapterAliasesByVaultScopedAddress.get(getAddressKey(normalizedAddress, chainId))?.[0]
: toOptionalAdapterAddressAlias(adapterAliases.find((candidate) => candidate.vaultAddress.toLowerCase() === normalizedAddress));
if (vaultAlias) {
return adapterAliasToVaultIdentity(vaultAlias, getVaultByAddress(address, vaultAlias.chainId));
}

const adapterAlias = adapterAliasesByScopedAddress.get(getAddressKey(address, chainId));
if (!adapterAlias) {
const vault = getVaultByAddress(address, chainId);
if (vault) {
return morphoVaultToIdentity(vault);
}

return undefined;
},
[adapterAliases, adapterAliasesByScopedAddress, adapterAliasesByVaultScopedAddress, getVaultByAddress],
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The getVaultAccountIdentity function performs O(N) lookups using adapterAliases.find() when chainId is not provided (lines 162 and 170). Since this registry is a central context used for identity resolution across the app, these linear searches could impact performance as the number of adapters grows. Consider pre-calculating maps keyed by address and vaultAddress (ignoring chainId) within useMemo to ensure O(1) lookups in all cases.

Comment on lines +97 to +113
if (asChild) {
const slottedChildren =
isDisabled && isValidElement<{ disabled?: boolean }>(children)
? cloneElement(children, { disabled: true })
: children;

return (
<Slot
className={buttonClassName}
ref={ref}
{...props}
aria-disabled={isDisabled}
>
{slottedChildren}
</Slot>
);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The asChild implementation uses cloneElement to manually inject the disabled prop. It's more idiomatic and cleaner to pass the disabled prop directly to the Slot component. Radix UI's Slot will handle the prop merging onto the child element automatically. This also simplifies the code by removing the need for manual element validation and cloning.

    if (asChild) {
      return (
        <Slot
          className={buttonClassName}
          ref={ref}
          {...props}
          disabled={isDisabled}
          aria-disabled={isDisabled}
        >
          {children}
        </Slot>
      );
    }

@antoncoding
Copy link
Copy Markdown
Owner Author

/gemini review

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a comprehensive system for identifying and displaying vault and adapter account identities. Key changes include updating the AccountIdentity component to support vault-specific metadata and images, refactoring the Button component for better asChild support, and expanding the VaultRegistryContext to manage account identities. New components like AccountVaultInfo and VaultManagedExposures were added to the positions view to provide detailed vault information and exposure data. Review feedback suggests improving type safety for asset addresses, enhancing accessibility for interactive elements by using semantic buttons, and considering performance optimizations for rendering multiple adapter exposures.

Comment thread src/contexts/VaultRegistryContext.tsx Outdated
vaultAddress: Address;
adapterAddress?: Address;
adapterType?: string;
assetAddress?: string;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For consistency and type safety, assetAddress should use the Address type instead of string. This ensures that downstream components, such as TokenIcon in AccountVaultInfo, receive the correctly formatted address type expected by their props.

Suggested change
assetAddress?: string;
assetAddress?: Address;

Comment on lines +42 to +47
<span
className="inline-flex cursor-pointer items-center rounded-sm bg-hovered px-2 py-1 text-xs text-secondary transition-colors hover:text-primary"
aria-label={`Open actions for ${label}`}
>
{label}
</span>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The span element used as a trigger for the AccountActionsPopover is interactive but lacks proper accessibility attributes. It should be focusable and indicate its role to assistive technologies. Using a <button> element is more idiomatic and provides these features natively.

      <button
        type="button"
        className="inline-flex cursor-pointer items-center rounded-sm bg-hovered px-2 py-1 text-xs text-secondary transition-colors hover:text-primary"
        aria-label={`Open actions for ${label}`}
      >
        {label}
      </button>

Comment on lines +163 to +172
{adapters.map((adapter) => (
<AdapterExposure
key={`${chainId}:${adapter.address}`}
adapterAddress={adapter.address}
adapterType={adapter.adapterType}
chainId={chainId}
period={period}
/>
))}
</div>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Rendering multiple AdapterExposure components in a loop, each invoking the useUserPositionsSummaryData hook, can lead to a significant number of concurrent network requests and performance degradation if a vault has many adapters. Each hook instance triggers multiple queries (positions, blocks, transactions, snapshots). Consider if there's a way to batch these data fetches or if the UI should only load them on demand (e.g., via an accordion or lazy loading) to improve scalability.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not changing this in this PR. The current vault adapter list comes from the vault metadata and is expected to be small; the UX requirement here is to show each adapter exposure inline when a vault has several adapters. Adding lazy loading or an accordion now would add state and hide the relationship we are trying to make explicit. If we see vaults with many adapters in production data, batching/lazy loading should be handled as a dedicated follow-up.

@antoncoding
Copy link
Copy Markdown
Owner Author

/gemini review

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a robust identity system for vaults and adapters, enabling the application to differentiate between native accounts, Morpho vaults, and vault adapters. Key enhancements include the definition of VaultAccountIdentity within the VaultRegistryContext, new UI components like AccountVaultInfo and VaultManagedExposures for displaying vault-specific metadata, and logic in the Positions view to conditionally render content based on the account type. Review feedback highlights opportunities to optimize identity lookups by avoiding linear searches and suggests refining the loading states to prevent layout shifts and jarring transitions when navigating to vault-specific pages.


const adapterAlias = chainId
? adapterAliasesByScopedAddress.get(getAddressKey(normalizedAddress, chainId))
: toOptionalAdapterAddressAlias(adapterAliases.find((candidate) => candidate.address.toLowerCase() === normalizedAddress));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

When chainId is not provided, getVaultAccountIdentity performs a linear search over adapterAliases. While likely fine for small datasets, this could become a performance bottleneck if the registry grows or if this function is called frequently (e.g., in a list). Consider maintaining an additional Map keyed by address only to allow O(1) lookups even when chainId is missing.

} = usePortfolioValue(marketPositions, vaults);

const loading = isMarketsLoading || isPositionsLoading;
const loading = !canEvaluateVaultIdentity || (showNativeAccountSections && (isMarketsLoading || isPositionsLoading));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The loading state logic for V2 vault pages might lead to a jarring user experience. Currently, loading becomes false as soon as the vault registry is evaluated (line 40), which causes the global LoadingScreen to disappear. However, the VaultManagedExposures component then performs its own asynchronous data fetch (useVaultV2Data). During this time, the page will appear empty before the exposure data suddenly pops in. Consider including the loading state of the vault-specific data in this top-level loading variable to ensure a continuous loading state until the primary content is ready.

Comment on lines +149 to +151
if (adapters.length === 0) {
return null;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Returning null when adapters.length === 0 while the data is still loading (i.e., vaultDataQuery.isLoading) contributes to the layout shift mentioned in the Positions view. It would be better to return a loading skeleton or at least preserve the section header to indicate that content is being fetched.

@antoncoding
Copy link
Copy Markdown
Owner Author

/gemini review

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements a comprehensive vault identity system to distinguish between Morpho vaults, V2 vaults, and adapters. Key changes include the introduction of the VaultAccountIdentity type in the registry context, new components for displaying vault-specific metadata and managed exposures, and enhancements to the AccountIdentity component to support metadata images and specialized badges. The Button component was also updated to improve disabled state handling when used with Radix Slot. Feedback suggests a more idiomatic approach for the Button component by leveraging Radix Slot's automatic prop merging instead of using cloneElement to pass the disabled attribute.

Comment on lines +97 to +112
if (asChild) {
const slottedChildren =
isDisabled && isValidElement<{ disabled?: boolean }>(children)
? cloneElement(children, { disabled: true })
: children;

return (
<Slot
className={buttonClassName}
ref={ref}
{...props}
aria-disabled={isDisabled}
>
{slottedChildren}
</Slot>
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

When using Radix Slot with asChild, it is more idiomatic to pass props directly to the Slot component rather than using cloneElement. Radix Slot handles prop merging automatically, ensuring that the child element receives the disabled and aria-disabled attributes correctly. This approach is cleaner and avoids potential issues with cloneElement on complex children. Additionally, passing disabled to the Slot ensures that if the child is a native button, it correctly receives the HTML disabled attribute.

    if (asChild) {
      return (
        <Slot
          className={buttonClassName}
          ref={ref}
          {...props}
          disabled={isDisabled}
          aria-disabled={isDisabled}
        >
          {children}
        </Slot>
      );
    }

@antoncoding
Copy link
Copy Markdown
Owner Author

/gemini review

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a comprehensive identity system for accounts, specifically distinguishing between standard accounts, Morpho vaults, and vault adapters. Key updates include an expanded VaultRegistryContext for identity resolution, a refactored AccountIdentity component with support for metadata images and entity-specific badges, and new components for displaying vault-specific information and managed exposures. The Positions view was also updated to conditionally render these new sections based on the account type. Feedback was provided regarding the duplication of loading and empty state UI blocks in the VaultManagedExposures component, suggesting the use of shared components for consistency.

chainId={chainId}
/>

{isPositionsLoading && <div className="rounded bg-surface px-4 py-6 text-sm text-secondary shadow-sm">Loading...</div>}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The loading and empty state UI blocks are repeated multiple times with identical styling in this file (lines 73, 97, 167). To adhere to the project's validation rules (Universal Validation rule 19 and UI rule 91), please consider using the established LoadingScreen and EmptyScreen components or extracting a shared component for these inline status messages to avoid duplication and ensure UI consistency.

References
  1. The implementation uses established UI components, util functions, and project patterns where possible. Do not declare ad hoc functions, always try to expand existing utilities or shared components first.
  2. Avoid repeated large UI blocks; extract or reuse only when it reduces real duplication.

@antoncoding
Copy link
Copy Markdown
Owner Author

/gemini review

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a comprehensive identity resolution system for vaults and adapters, enhancing the account view with specific metadata, icons, and relationship links. Key changes include the addition of VaultRegistryContext to manage vault identities, a new AccountVaultInfo component for displaying vault-specific details, and the VaultManagedExposures feature to show positions held by linked adapters. Feedback focuses on refining the Button component's handling of disabled states and asChild props to avoid redundant classes and invalid HTML. Additionally, there are concerns regarding the non-deterministic nature of address-to-identity lookups when a chain ID is missing, and the potential performance impact of parallel data fetching for multiple adapters within the exposure view.

Comment on lines +92 to +95
const buttonClassName = cn(
buttonVariants({ variant, size, radius, fullWidth, isLoading, className }),
isDisabled && 'pointer-events-none cursor-not-allowed opacity-50',
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The manual application of pointer-events-none cursor-not-allowed opacity-50 is redundant for native <button> elements as these are already handled by the disabled: pseudo-class in buttonVariants (line 9). These classes should only be applied when asChild is true. Additionally, note that pointer-events-none prevents the cursor-not-allowed from appearing, which might be confusing for users expecting a visual cue for the disabled state.

Suggested change
const buttonClassName = cn(
buttonVariants({ variant, size, radius, fullWidth, isLoading, className }),
isDisabled && 'pointer-events-none cursor-not-allowed opacity-50',
);
const buttonClassName = cn(
buttonVariants({ variant, size, radius, fullWidth, isLoading, className }),
asChild && isDisabled && 'pointer-events-none cursor-not-allowed opacity-50',
);

Comment thread src/components/ui/button.tsx Outdated
Comment on lines +100 to +101
disabled: isDisabled,
'aria-disabled': isDisabled,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

When asChild is true and the child is a Link (which renders an <a> tag), passing the disabled prop results in invalid HTML. While aria-disabled is correct for accessibility on non-form elements, the disabled attribute should ideally only be passed to elements that support it (like <button>).

Comment thread src/contexts/VaultRegistryContext.tsx Outdated
Comment on lines +130 to +150
const adapterAliasesByAddress = useMemo(() => {
const lookup = new Map<string, AdapterAddressAlias>();
for (const adapterAlias of adapterAliases) {
const key = adapterAlias.address.toLowerCase();
if (!lookup.has(key)) {
lookup.set(key, toAdapterAddressAlias(adapterAlias));
}
}
return lookup;
}, [adapterAliases]);

const adapterAliasesByVaultAddress = useMemo(() => {
const lookup = new Map<string, AdapterAddressAlias>();
for (const adapterAlias of adapterAliases) {
const key = adapterAlias.vaultAddress.toLowerCase();
if (!lookup.has(key)) {
lookup.set(key, toAdapterAddressAlias(adapterAlias));
}
}
return lookup;
}, [adapterAliases]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Both adapterAliasesByAddress and adapterAliasesByVaultAddress maps only store the first alias encountered for a given address, ignoring potential duplicates on different chains. This makes identity resolution non-deterministic and potentially incorrect when chainId is not provided. If an address is used on multiple chains with different metadata, the registry will always return the metadata of the first one found in the list. Consider storing a list of aliases per address or ensuring that lookups without a chainId are handled more robustly.

Comment on lines +60 to +61
const { isPositionsLoading, positions, refetch, isRefetching, isEarningsLoading, actualBlockData, transactions, snapshotsByChain } =
useUserPositionsSummaryData(adapterAddress, period, chainIds);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Calling useUserPositionsSummaryData inside a loop for each adapter can lead to a significant number of parallel network requests and heavy client-side processing if a vault has many adapters. Consider if the underlying data fetching can be batched or if a more optimized approach for multi-account position summaries is available to improve performance and reduce API load.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved the actionable review items in e122944. For this adapter-fetching concern: there is not currently a shared multi-account positions-summary query to reuse, and changing that data contract would be larger than this PR. The component already deduplicates adapter addresses and preserves the requested inline exposure view. If production data shows vaults with many adapters, that should be handled with a dedicated batched positions query rather than adding lazy/accordion behavior here.

@antoncoding
Copy link
Copy Markdown
Owner Author

/gemini review

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a comprehensive "Vault Identity" system to differentiate between standard accounts, Morpho vaults, and vault adapters. Key changes include a refactored AccountIdentity component with metadata image support, new context logic for resolving account identities, and specialized UI components like AccountVaultInfo and VaultManagedExposures for detailed vault/adapter views. Additionally, the Button component was updated for better Radix Slot compatibility. Feedback was provided regarding performance optimizations in VaultRegistryContext.tsx, specifically recommending the use of .push() instead of spread syntax within loops to avoid $O(N^2)$ complexity from repeated array allocations.

Comment thread src/contexts/VaultRegistryContext.tsx Outdated
const lookup = new Map<string, AdapterAddressAlias[]>();
for (const adapterAlias of adapterAliases) {
const key = adapterAlias.address.toLowerCase();
lookup.set(key, [...(lookup.get(key) ?? []), toAdapterAddressAlias(adapterAlias)]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Avoid using spread syntax in accumulators inside loops as it leads to $O(N^2)$ complexity due to repeated array allocations. Use .push() instead, which is more efficient and consistent with the implementation on line 155.

      const existing = lookup.get(key) ?? [];
      existing.push(toAdapterAddressAlias(adapterAlias));
      lookup.set(key, existing);
References
  1. Avoid spread syntax in accumulators inside loops to maintain performance and adhere to the project's validation rules.

Comment thread src/contexts/VaultRegistryContext.tsx Outdated
const lookup = new Map<string, AdapterAddressAlias[]>();
for (const adapterAlias of adapterAliases) {
const key = adapterAlias.vaultAddress.toLowerCase();
lookup.set(key, [...(lookup.get(key) ?? []), toAdapterAddressAlias(adapterAlias)]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Avoid using spread syntax in accumulators inside loops. Using .push() is more efficient and follows the pattern established elsewhere in this file.

      const existing = lookup.get(key) ?? [];
      existing.push(toAdapterAddressAlias(adapterAlias));
      lookup.set(key, existing);
References
  1. Avoid spread syntax in accumulators inside loops.

@antoncoding
Copy link
Copy Markdown
Owner Author

/gemini review

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a robust identity system for vaults and adapters, enhancing account displays with metadata images, specialized badges, and contextual links. Key changes include the addition of the VaultAccountIdentity type, new components for vault-specific information and managed exposures, and improvements to the Button component's asChild behavior to correctly handle disabled states. Feedback was provided to optimize the VaultRegistryContext by consolidating multiple memoized lookup maps into a single iteration to improve performance and code cleanliness.

Comment on lines 124 to 163
const adapterAliasesByScopedAddress = useMemo(() => {
const lookup = new Map<string, AdapterAddressAlias>();
for (const adapterAlias of adapterAliases) {
lookup.set(getAddressKey(adapterAlias.address, adapterAlias.chainId), {
vaultAddress: adapterAlias.vaultAddress,
vaultName: adapterAlias.vaultName,
adapterType: adapterAlias.adapterType,
});
lookup.set(getAddressKey(adapterAlias.address, adapterAlias.chainId), toAdapterAddressAlias(adapterAlias));
}
return lookup;
}, [adapterAliases]);

const adapterAliasesByAddress = useMemo(() => {
const lookup = new Map<string, AdapterAddressAlias[]>();
for (const adapterAlias of adapterAliases) {
const key = adapterAlias.address.toLowerCase();
const existing = lookup.get(key) ?? [];
existing.push(toAdapterAddressAlias(adapterAlias));
lookup.set(key, existing);
}
return lookup;
}, [adapterAliases]);

const adapterAliasesByVaultAddress = useMemo(() => {
const lookup = new Map<string, AdapterAddressAlias[]>();
for (const adapterAlias of adapterAliases) {
const key = adapterAlias.vaultAddress.toLowerCase();
const existing = lookup.get(key) ?? [];
existing.push(toAdapterAddressAlias(adapterAlias));
lookup.set(key, existing);
}
return lookup;
}, [adapterAliases]);

const adapterAliasesByVaultScopedAddress = useMemo(() => {
const lookup = new Map<string, AdapterAddressAlias[]>();
for (const adapterAlias of adapterAliases) {
const key = getAddressKey(adapterAlias.vaultAddress, adapterAlias.chainId);
const existing = lookup.get(key) ?? [];
existing.push(toAdapterAddressAlias(adapterAlias));
lookup.set(key, existing);
}
return lookup;
}, [adapterAliases]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The multiple useMemo hooks for indexing adapterAliases can be consolidated into a single useMemo block. This reduces the number of iterations over the adapterAliases array from four to one, improving efficiency and making the code cleaner.

  const { 
    adapterAliasesByScopedAddress, 
    adapterAliasesByAddress, 
    adapterAliasesByVaultAddress, 
    adapterAliasesByVaultScopedAddress 
  } = useMemo(() => {
    const scoped = new Map<string, AdapterAddressAlias>();
    const byAddress = new Map<string, AdapterAddressAlias[]>();
    const byVault = new Map<string, AdapterAddressAlias[]>();
    const byVaultScoped = new Map<string, AdapterAddressAlias[]>();

    for (const adapterAlias of adapterAliases) {
      const alias = toAdapterAddressAlias(adapterAlias);
      const addressKey = getAddressKey(adapterAlias.address, adapterAlias.chainId);
      const vaultKey = getAddressKey(adapterAlias.vaultAddress, adapterAlias.chainId);
      const lowerAddress = adapterAlias.address.toLowerCase();
      const lowerVault = adapterAlias.vaultAddress.toLowerCase();

      scoped.set(addressKey, alias);

      const existingAddr = byAddress.get(lowerAddress) ?? [];
      existingAddr.push(alias);
      byAddress.set(lowerAddress, existingAddr);

      const existingVault = byVault.get(lowerVault) ?? [];
      existingVault.push(alias);
      byVault.set(lowerVault, existingVault);

      const existingVaultScoped = byVaultScoped.get(vaultKey) ?? [];
      existingVaultScoped.push(alias);
      byVaultScoped.set(vaultKey, existingVaultScoped);
    }

    return {
      adapterAliasesByScopedAddress: scoped,
      adapterAliasesByAddress: byAddress,
      adapterAliasesByVaultAddress: byVault,
      adapterAliasesByVaultScopedAddress: byVaultScoped,
    };
  }, [adapterAliases]);

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving this one unchanged. This is a cleanup/performance suggestion rather than a correctness issue, and consolidating the existing maps into one larger memo block would add churn after the review fixes. The current code is deterministic for unscoped lookups, avoids O(N^2) array spread, and keeps each lookup map explicit.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/features/positions/components/adapter-managed-exposure.tsx`:
- Around line 153-157: The current early return treats failed vault fetches the
same as legitimately empty adapter lists; update the conditional in
adapter-managed-exposure.tsx to explicitly handle vaultDataQuery.isError (or
vaultDataQuery.error) before returning null: if vaultDataQuery.isError render an
error state/UI (and optionally log the error), if !showLoading and
adapters.length === 0 render an explicit “no adapters” / empty state instead of
null; keep the existing showLoading behavior for vaultDataQuery.isLoading.
Reference symbols: showLoading, vaultDataQuery, adapters.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: a1838f2f-8d70-42e0-a9c8-6308468fddae

📥 Commits

Reviewing files that changed from the base of the PR and between ecb6d04 and 525b93f.

📒 Files selected for processing (3)
  • src/components/ui/button.tsx
  • src/contexts/VaultRegistryContext.tsx
  • src/features/positions/components/adapter-managed-exposure.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/contexts/VaultRegistryContext.tsx

Comment on lines +153 to +157
const showLoading = vaultDataQuery.isLoading && adapters.length === 0;

if (adapters.length === 0 && !showLoading) {
return null;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle vault query errors explicitly instead of returning null.

At Line 155, adapters.length === 0 && !showLoading returns null for both empty data and failed fetches. That hides a broken state as if nothing exists.

Suggested fix
-  const showLoading = vaultDataQuery.isLoading && adapters.length === 0;
+  const showLoading = vaultDataQuery.isLoading && adapters.length === 0;
+  const showError = vaultDataQuery.isError && adapters.length === 0;

-  if (adapters.length === 0 && !showLoading) {
+  if (showError) {
+    return <ExposureStatus message="Failed to load exposures." />;
+  }
+
+  if (adapters.length === 0 && !showLoading) {
     return null;
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/positions/components/adapter-managed-exposure.tsx` around lines
153 - 157, The current early return treats failed vault fetches the same as
legitimately empty adapter lists; update the conditional in
adapter-managed-exposure.tsx to explicitly handle vaultDataQuery.isError (or
vaultDataQuery.error) before returning null: if vaultDataQuery.isError render an
error state/UI (and optionally log the error), if !showLoading and
adapters.length === 0 render an explicit “no adapters” / empty state instead of
null; keep the existing showLoading behavior for vaultDataQuery.isLoading.
Reference symbols: showLoading, vaultDataQuery, adapters.

@antoncoding antoncoding merged commit cbc5b13 into master May 14, 2026
4 checks passed
@antoncoding antoncoding deleted the feat/vault-and-adapter branch May 14, 2026 16:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature request Specific feature ready to be implemented ui User interface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants