Skip to content

feat: Vulti Agent Chat Integration#3910

Draft
enriquesouza wants to merge 30 commits intomainfrom
feat/vulti-agent-chat
Draft

feat: Vulti Agent Chat Integration#3910
enriquesouza wants to merge 30 commits intomainfrom
feat/vulti-agent-chat

Conversation

@enriquesouza
Copy link
Contributor

@enriquesouza enriquesouza commented Feb 25, 2026

Vulti Agent Chat Integration

This PR integrates the Vulti AI Assistant (powered by the agent-backend) into the Vultisig iOS app as a full-featured chat experience.


Features

🤖 Agent Chat UI

  • New Agent Chat screen accessible from the vault home screen
  • SSE streaming responses with real-time status indicators (Analyzing, Planning, Simulating…)
  • Conversation history (list + individual chats)
  • Stop button to cancel AI generation mid-stream
  • Conversation starter bubbles
  • Matches Figma design with glow effects and monospace status fonts

🔐 Agent Authentication

  • EIP-191 signed payloads using the vault's ECDSA key (via FastVault keysign)
  • Auth token persisted per vault to avoid repeated signing
  • Proper Keccak256 signature handling

🛠️ iOS-side Tool Executor

The AgentToolExecutor handles all actions that require local vault access. The AI calls these directly — no manual interaction needed.

Chain & Token Management

Action Description
add_chain Adds a new blockchain to the vault (e.g. SUI, Solana)
add_coin / add_token Adds a specific token to an existing chain
remove_chain Removes a chain and all its tokens from the vault
remove_coin Removes a specific token from the vault
search_token Searches the local TokensStore by ticker/chain/contract

Vault Information

Action Description
list_vaults Lists all vaults with name, active chains, type (fast/secure)
get_addresses Returns all chain addresses for the active vault
get_balances Returns live token balances from the vault
get_portfolio Returns total portfolio fiat value
get_market_price Returns cached market price for a token

Address Book

Action Description
get_address_book Reads address book entries (filterable by chain/query)
add_address_book / address_book_add Adds a new contact
delete_address_book / address_book_remove Removes a contact

Server-side (MCP/agent-backend handled)
build_swap_tx, build_send_tx, build_custom_tx, plugin_install, create_policy, delete_policy, read_evm_contract, scan_tx, thorchain_query, get_eth_balance, get_token_balance, get_utxo_balance and more — all forwarded to the MCP server.

Pending

  • sign_tx — requires the full iOS Keysign flow (2-of-3 multi-party signing), not yet auto-executable from the agent.

🧭 MCP Session Priming

  • On each new conversation creation, the agent-backend is primed with the vault's ECDSA/EdDSA public keys and chain code via set_vault so MCP tools can resolve addresses server-side.

🔧 Robustness Improvements

  • Parameter parsing for add_chain / add_coin is resilient to LLM shape variations (nested array or flat object both accepted)
  • All known action types are explicitly handled — unknown actions return a clear error instead of silently failing

Files Changed

  • Services/Agent/AgentToolExecutor.swift — full tool executor with all iOS-side actions
  • Services/Agent/AgentModels.swift — all action parameter/result model types
  • Services/Agent/AgentBackendClient.swift — REST + SSE streaming client
  • Services/Agent/AgentAuthService.swift — vault-signed auth token management
  • Services/Agent/AgentContextBuilder.swift — vault context assembly for AI prompts
  • Views/Agent/AgentChatView.swift — main chat UI
  • Views/Agent/AgentConversationsView.swift — conversation list
  • ViewModels/Agent/AgentChatViewModel.swift — orchestrator
  • ViewModels/Agent/AgentConversationsViewModel.swift — conversation list state

Related Repositories

  • agent-backend: Updated prompt.go to add instructions for remove_chain, remove_coin, and the full Modifying Chains & Coins section
  • mcp: Logging improvements for tool execution tracing

Summary by CodeRabbit

Release Notes

  • New Features

    • Added Agent feature with conversation management and real-time chat capabilities for enhanced user interactions.
  • Improvements

    • Streamlined vault signing workflow for improved user experience.
    • Enhanced backward compatibility for blockchain data handling.
    • Integrated new Agent navigation tab alongside existing wallet and DeFi features.

@coderabbitai
Copy link

coderabbitai bot commented Feb 25, 2026

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

This pull request introduces a comprehensive Agent feature to the VultisigApp iOS application, including backend integration via REST and Server-Sent Events (SSE), MPC-based authentication, conversation management, and a complete UI layer. Additionally, the keysign flow is simplified by removing fastVaultPassword parameters throughout the codebase, and Chain model backward compatibility is enhanced.

Changes

Cohort / File(s) Summary
Agent Module Structure
VultisigApp.xcodeproj/project.pbxproj
New Agent group with backend, auth, tooling, context, models, conversation, chat, and routing files integrated into project build configuration. No runtime behavior changes; purely structural organization.
Agent Backend & API Client
Services/Agent/AgentBackendClient.swift, Services/Agent/AgentModels.swift
Introduces REST client and SSE streaming support for agent conversations with comprehensive data models (requests, contexts, actions, responses). Includes conversation CRUD, message sending with streaming, and event handling for text deltas, actions, and transactions. Total ~982 lines.
Agent Authentication Service
Services/Agent/AgentAuthService.swift
Implements MPC-based authentication via FastVault keysign ceremony, token caching with Keychain persistence, token refresh/validation, and disconnect flows. Includes EIP-191 signing, Ethereum address derivation, and verifier API integration (~431 lines).
Agent Tool Execution & Context
Services/Agent/AgentToolExecutor.swift, Services/Agent/AgentContextBuilder.swift
Provides action routing for token/chain management, vault operations, address book management, and signing. Constructs message context from vault data with optional balance information (~686 lines).
FastVault Keysign Service
Services/FastVault/FastVaultKeysignService.swift
New headless MPC ceremony orchestrator for ECDSA/EdDSA signing with relay session management, peer discovery polling, and retry logic. Six-step keysign flow with logging (~274 lines).
Agent ViewModels
View Models/Agent/AgentChatViewModel.swift, View Models/Agent/AgentConversationsViewModel.swift
Chat and conversations state management with authentication, message streaming (SSE), action auto-execution, transaction handling, and token management (~765 lines combined). Handles connection state, loading, errors, and password prompts.
Agent UI Views
Views/Agent/AgentChatView.swift, Views/Agent/AgentConversationsView.swift, Views/Agent/AgentChatMessageView.swift, Views/Agent/AgentPasswordPromptView.swift, Views/Agent/AgentThinkingIndicator.swift
Complete chat interface with message rendering, input bar, starters grid, password authentication flow, transaction proposal/status rendering, and animated thinking indicator (~942 lines combined).
Agent Navigation & Routing
Features/Agent/Navigation/AgentRoute.swift, Features/Agent/Navigation/AgentRouter.swift, Navigation/VultisigRouter.swift, ContentView.swift
Adds AgentRoute enum with conversations and chat(conversationId) cases, AgentRouter for view building, and integrates agent navigation into VultisigRouter. Wires AgentRouter into main navigation stack.
Home & Tab Integration
Features/Home/HomeScreen.swift, Features/Home/Model/HomeTab.swift, Features/Wallet/ChainDetail/ChainDetailScreenContainer.swift, Views/Components/TabBar/VultiTabBar.swift
Adds .agent tab to home screen and chain detail screens with conditional DeFi tab logic. Updates tab bar rendering to include Agent tab with purple background and "stars" icon.
Keysign Refactoring - Removal of fastVaultPassword
Views/FunctionCall/FunctionCallRoute.swift, Views/FunctionCall/FunctionCallRouteBuilder.swift, Views/FunctionCall/FunctionCallRouter.swift, Views/FunctionCall/FunctionCallPairScreen.swift, Views/FunctionCall/FunctionCallVerifyScreen.swift, Views/Send/Navigation/SendRoute.swift, Views/Send/Navigation/SendRouteBuilder.swift, Views/Send/Navigation/SendRouter.swift, Views/Send/Screens/SendPairScreen.swift, Views/Send/Screens/SendVerifyScreen.swift, Views/Keysign/KeysignDiscoveryView.swift, Views/Keysign/KeysignDiscoveryViewModel.swift, Views/Settings/SettingsCustomMessageView.swift, Views/Swap/SwapCryptoView.swift, Features/Defi/Protocols/Circle/CircleWithdrawView.swift
Removes fastVaultPassword parameter from SendRoute.pairing, FunctionCallRoute.pair, and related builder/router methods. Eliminates conditional fast-keysign UI rendering and simplifies keysign discovery/view model signatures.
Infrastructure & Compatibility
Utils/Endpoint.swift, Model/Chain.swift
Adds agent and verifier API endpoint builders (agentConversations, verifier auth flows). Implements backward-compatible Chain decoding with removedChainMigrations mapping for legacy string values and custom Codable implementation.
Formatting & Minor Updates
View Models/KeysignViewModel.swift, Views/Keysign/KeysignView.swift
Whitespace and indentation-only adjustments; no functional changes.
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.75% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: Vulti Agent Chat Integration' accurately describes the main objective of the changeset: integrating an AI agent chat feature into the Vultisig app. It is clear, specific, and follows conventional commit format.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/vulti-agent-chat

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

…, and swap flows across multiple views and navigation routes.
- Add remove_chain and remove_coin execution logic using CoinService
- Add list_vaults: returns all vaults with chains, type and date
- Add get_addresses: returns all vault chain addresses (filterable)
- Add search_token: searches local TokensStore by ticker/chain/contract
- Add address_book_add / address_book_remove as action aliases
- Add add_coin as alias for add_token (prompt uses both names)
- Expand switch to cover all 30+ known action types with proper routing:
  - Server-side MCP actions return handled_server_side
  - sign_tx returns sign_requires_keysign_flow
  - Unknown actions return unknown_action_type
- Add AgentRemoveChainParams, AgentRemoveChainResult,
  AgentRemoveTokenParams, AgentRemoveTokenResult to AgentModels.swift
- AgentBackendClient: SSE streaming improvements with error handling
- AgentContextBuilder: vault context assembly for AI prompts
- AgentChatViewModel: orchestrator with tool execution, stop generation
- AgentConversationsViewModel: conversation list and management
- AgentChatView: streaming UI with status indicators and stop button
- AgentChatMessageView: message bubble with tool call inline display
- AgentConversationsView: conversation list with starters
- Endpoint.swift: agent backend URL endpoint definitions
- Keep agent chat files (AgentToolExecutor, AgentModels, AgentContextBuilder, etc.)
- Merge push notifications feature from main (PushNotificationManager, NotificationService, etc.)
- Merge ThorchainStagenet2 and VaultNotificationToggleRow from main
- Resolve project.pbxproj conflicts by keeping all additions from both branches
@enriquesouza
Copy link
Contributor Author

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Feb 27, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Fix violations across 12 files:
- trailing_whitespace: 108 in AgentToolExecutor, ~30 across other agent files
- async_without_await: 11 in AgentToolExecutor
- unused_parameter: 5 in AgentChatViewModel, AgentConversationsViewModel
- statement_position: 1 in AgentAuthService
- vertical_whitespace: 5 in AgentConversationsView, KeysignDiscoveryViewModel, KeysignViewModel
- colon: 2 in AgentConversationsView
Copy link

@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: 14

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

🟡 Minor comments (8)
VultisigApp/VultisigApp/Views/Keysign/KeysignDiscoveryView.swift-154-154 (1)

154-154: ⚠️ Potential issue | 🟡 Minor

isFast is now hardcoded, which can hide fast-vault state.

At Line 154, forcing isFast: false removes dynamic behavior and may show incorrect discovery animation/state for fast vault flows. Please derive this from vault/view-model state instead of a constant.

Suggested fix
-            isFast: false,
+            isFast: vault.signers.count == 2,

Based on learnings: In VultisigApp iOS codebase, vault classification depends on signer count (2 signers = fast vault, 3+ signers = active vault), even when vault.isFastVault is true.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@VultisigApp/VultisigApp/Views/Keysign/KeysignDiscoveryView.swift` at line
154, Replace the hardcoded "isFast: false" with a computed value derived from
the vault/view-model state: inspect the view model or vault object used in
KeysignDiscoveryView (e.g., viewModel.vault or vault) and set isFast based on
signer count (treat 2 signers as a fast vault, 3+ as active) rather than a
constant; update the call site where "isFast: false" is passed so it uses that
computed boolean (for example, derive isFast = (vault.signers.count == 2) or
equivalent property on the view model) to ensure the discovery animation/state
reflects the actual vault classification.
VultisigApp/VultisigApp/Views/Keysign/KeysignView.swift-20-20 (1)

20-20: ⚠️ Potential issue | 🟡 Minor

Make viewModel private to clear SwiftLint.

Line 20 exposes @StateObject with non-private visibility; this triggers private_swiftui_state and can break lint gating.

🔧 Suggested fix
-        `@StateObject` var viewModel = KeysignViewModel()
+    `@StateObject` private var viewModel = KeysignViewModel()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@VultisigApp/VultisigApp/Views/Keysign/KeysignView.swift` at line 20, Make the
`@StateObject` viewModel private to satisfy SwiftLint: change the visibility of
the viewModel property in KeysignView (the `@StateObject` var viewModel =
KeysignViewModel()) to private (e.g., `@StateObject` private var viewModel =
KeysignViewModel()) and update any references inside KeysignView that access
viewModel accordingly so nobody relies on it being internal/public.
VultisigApp/VultisigApp/Views/Agent/AgentConversationsView.swift-201-205 (1)

201-205: ⚠️ Potential issue | 🟡 Minor

Persist starter before route push to avoid prefill race.

Line 202 navigates first and writes the pending starter afterward; the destination can read before the value exists.

Suggested patch
 private func navigateToChat(with starter: String?) {
-        router.navigate(to:AgentRoute.chat(conversationId: nil))
         if let starter {
             UserDefaults.standard.set(starter, forKey: "agent_pending_starter")
         }
+        router.navigate(to: AgentRoute.chat(conversationId: nil))
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@VultisigApp/VultisigApp/Views/Agent/AgentConversationsView.swift` around
lines 201 - 205, In navigateToChat(with:) move the UserDefaults write for the
"agent_pending_starter" key to happen before calling router.navigate(to:
AgentRoute.chat(conversationId: nil)) so the destination can read the persisted
starter immediately; specifically, in the navigateToChat function set
UserDefaults.standard.set(starter, forKey: "agent_pending_starter") (when
starter is non-nil) first, then call router.navigate(to:AgentRoute.chat(...)).
VultisigApp/VultisigApp/Services/Agent/AgentAuthService.swift-214-218 (1)

214-218: ⚠️ Potential issue | 🟡 Minor

Fallback address derivation may be incorrect.

The fallback when WalletCore derivation fails uses the last 40 characters of the public key, which is not a valid Ethereum address derivation. This could lead to authentication failures or mismatched addresses.

Explanation

A proper fallback would need to:

  1. Decompress the public key if compressed
  2. Take the keccak256 hash of the uncompressed key (without prefix)
  3. Use the last 20 bytes as the address

The current implementation just takes the suffix of the hex-encoded public key, which is not the same as an Ethereum address.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@VultisigApp/VultisigApp/Services/Agent/AgentAuthService.swift` around lines
214 - 218, The fallback in AgentAuthService's address derivation (when
WalletCore fails) currently returns the last 40 hex chars of vault.pubKeyECDSA;
instead, detect and decompress a compressed EC public key (vault.pubKeyECDSA),
convert to the uncompressed byte form without the 0x/0x04 prefix, compute
keccak256 over those uncompressed bytes, take the last 20 bytes of the hash, and
return "0x" + hex of those 20 bytes; update the fallback branch that constructs
normalized and the return value to perform these steps and ensure you handle hex
decoding/encoding and errors appropriately.
VultisigApp/VultisigApp/View Models/Agent/AgentConversationsViewModel.swift-52-70 (1)

52-70: ⚠️ Potential issue | 🟡 Minor

Redundant isConnected = true assignment.

isConnected is set to true at line 56 before the network call, then set again at line 70 after success. The first assignment at line 56 is premature since the connection hasn't been verified yet.

Suggested fix
     func loadConversations(vault: Vault) async {
         let token = await getValidToken(vault: vault)
         print("[AgentConvos] 📝 loadConversations: token=\(token != nil ? "present" : "none")")

-        isConnected = true
         isLoading = true
         error = nil
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@VultisigApp/VultisigApp/View` Models/Agent/AgentConversationsViewModel.swift
around lines 52 - 70, The loadConversations(vault: Vault) function sets
isConnected = true prematurely before the network call; remove the early
assignment and only set isConnected = true after a successful
backendClient.listConversations response (the existing assignment after success
can remain), ensuring any error path sets isConnected = false or leaves it
unchanged; update references to isConnected within loadConversations so the
state reflects the actual connection result from listConversations and not
before getValidToken/ backendClient.listConversations completes.
VultisigApp/VultisigApp/Services/Agent/AgentAuthService.swift-264-264 (1)

264-264: ⚠️ Potential issue | 🟡 Minor

Avoid force unwrapping URLs.

Force unwrapping URL(string:)! can crash if the URL string is malformed. This pattern appears at lines 264, 297, 312, and 324.

As per coding guidelines: "Avoid force unwrapping (!); use optional binding, guard statements, or nil-coalescing operators instead."

Suggested fix
-let url = URL(string: Endpoint.verifierAuth())!
+guard let url = URL(string: Endpoint.verifierAuth()) else {
+    throw AgentAuthError.authFailed
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@VultisigApp/VultisigApp/Services/Agent/AgentAuthService.swift` at line 264,
The code in AgentAuthService uses force-unwrapped URL(string:) (e.g., the let
url = URL(string: Endpoint.verifierAuth())! instance) which can crash on
malformed strings; update each occurrence (those in AgentAuthService around the
verifierAuth, other Endpoint.* calls) to safely unwrap the URL using guard let
(or if let) and handle the nil case (returning/throwing an error or logging and
exiting) instead of force-unwrapping; ensure all locations (the occurrences you
saw at URL(string: Endpoint.verifierAuth()), and the other Endpoint.*
invocations) follow the same pattern so no URL(string:) uses a trailing !.
VultisigApp/VultisigApp/Services/Agent/AgentToolExecutor.swift-236-238 (1)

236-238: ⚠️ Potential issue | 🟡 Minor

Silent error suppression on Storage.shared.save().

Using try? silently swallows save errors. This could hide data persistence failures. Consider logging the error at minimum.

Suggested improvement
 if anySuccess {
-    try? Storage.shared.save()
+    do {
+        try Storage.shared.save()
+    } catch {
+        print("[AgentToolExecutor] ⚠️ Failed to save: \(error.localizedDescription)")
+    }
 }

This pattern repeats at lines 313, 370, 425, and 516.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@VultisigApp/VultisigApp/Services/Agent/AgentToolExecutor.swift` around lines
236 - 238, Replace the silent try? on Storage.shared.save() with proper error
handling: wrap the call in a do/catch, call try Storage.shared.save() in the do
block and log the caught error in the catch (e.g., logger.error or
processLogger.error with "Failed to save storage: \(error)"), and apply the same
change for the other occurrences mentioned (the save calls near anySuccess and
the repeats at the other locations). This ensures Storage.shared.save() errors
are not suppressed and provides actionable logs; modify the code around the
anySuccess branch in AgentToolExecutor where Storage.shared.save() is invoked.
VultisigApp/VultisigApp/Services/Agent/AgentModels.swift-586-592 (1)

586-592: ⚠️ Potential issue | 🟡 Minor

Fragile Hashable implementation for AnyCodable.

Using String(describing: value) for equality and hashing can produce unexpected results since the string representation may vary or collide for different underlying values.

Explanation

For example, two different arrays might produce the same string representation, or floating-point formatting might differ across platforms. If AnyCodable is primarily used in dictionaries/sets where hashing matters, consider a more robust implementation or removing Hashable conformance if not strictly needed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@VultisigApp/VultisigApp/Services/Agent/AgentModels.swift` around lines 586 -
592, The current == operator and hash(into:) in AnyCodable use
String(describing:) which is fragile; replace this by first attempting to
normalize the wrapped value into a comparable/hashable form (e.g., try casting
value to AnyHashable and compare/hash that), and if that fails fall back to a
deterministic binary representation (e.g., encode value with
JSONEncoder/PropertyListEncoder and compare/hash the resulting Data) or remove
Hashable conformance entirely if sets/dictionary keys of AnyCodable are not
needed. Update the implementations in the AnyCodable type (the static func ==
and func hash(into:)) to try AnyHashable casting first, then deterministic
encoded-data fallback, and ensure identical logic is used for both equality and
hashing so equal values produce the same hash.
🧹 Nitpick comments (21)
VultisigApp/VultisigApp/View Models/KeysignDiscoveryViewModel.swift (1)

69-78: Tighten participantDiscovery lifecycle guarantees

Line 73 makes participantDiscovery required at setup, but later calls still optional-chain it. This can silently skip discovery if startDiscovery() is called before setData(...).

Suggested hard-fail guard to avoid silent no-op
 func startDiscovery() {
     self.logger.info("mediator server started")
     self.startKeysignSession()
-    self.participantDiscovery?.getParticipants(
+    guard let participantDiscovery = self.participantDiscovery else {
+        self.logger.error("participantDiscovery not configured before startDiscovery")
+        self.status = .FailToStart
+        return
+    }
+    participantDiscovery.getParticipants(
         serverAddr: self.serverAddr,
         sessionID: self.sessionID,
         localParty: self.localPartyID
     )
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@VultisigApp/VultisigApp/View` Models/KeysignDiscoveryViewModel.swift around
lines 69 - 78, The participantDiscovery is treated as required in setData(...)
but optional elsewhere, which can cause startDiscovery() to silently no-op if
called earlier; update the lifecycle so participantDiscovery cannot be nil when
startDiscovery() runs by either making participantDiscovery non-optional (set
its type to ParticipantDiscovery and assign it in setData) or adding an explicit
guard/early-fail in startDiscovery() that throws or logs a
fatal/preconditionFailure when participantDiscovery is nil; reference the
setData(...) method and the startDiscovery() call site to implement the chosen
fix so discovery always fails loudly instead of silently skipping.
VultisigApp/VultisigApp/Views/Components/TabBar/VultiTabBar.swift (1)

231-233: Use theme color tokens in the new Agent preview tab.

The new preview tab uses hardcoded colors; prefer Theme.colors to keep previews aligned with app tokens.

As per coding guidelines: "Always use the Theme enum for colors instead of hardcoding color values."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@VultisigApp/VultisigApp/Views/Components/TabBar/VultiTabBar.swift` around
lines 231 - 233, The preview tab in VultiTabBar is using hardcoded Color.purple
and should use the app color tokens; replace the Color.purple usage in the Agent
preview overlay (the view that sets
.overlay(Text("Agent")...).tag(HomeTab.agent)) with the appropriate Theme.colors
token (e.g., Theme.colors.primary or whichever Theme.colors.* token matches the
intended purple) so the Agent preview follows the Theme enum colors rather than
a literal Color.
VultisigApp/VultisigApp/Utils/Endpoint.swift (1)

1133-1139: Encode dynamic conversation IDs in URL path builders.

Use encodePathComponent(_:) for id to avoid malformed path issues if IDs ever include reserved characters.

🔧 Suggested fix
 static func agentConversation(id: String) -> String {
-    "\(agentBackendUrl)/agent/conversations/\(id)"
+    "\(agentBackendUrl)/agent/conversations/\(encodePathComponent(id))"
 }

 static func agentConversationMessages(id: String) -> String {
-    "\(agentBackendUrl)/agent/conversations/\(id)/messages"
+    "\(agentBackendUrl)/agent/conversations/\(encodePathComponent(id))/messages"
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@VultisigApp/VultisigApp/Utils/Endpoint.swift` around lines 1133 - 1139, The
URL builders agentConversation(id:) and agentConversationMessages(id:) currently
interpolate raw id values into the path; update both to encode the id using
encodePathComponent(_:) before interpolation (i.e., call encodePathComponent(id)
and use that result in the path) so reserved characters in conversation IDs are
safely escaped.
VultisigApp/VultisigApp/Views/Agent/AgentPasswordPromptView.swift (1)

25-35: Replace hardcoded fonts with Theme.fonts tokens.

Typography here is hardcoded; switch to Theme.fonts to keep UI consistency across screens.

As per coding guidelines: "Always use the Theme enum for fonts instead of hardcoding font values."

Also applies to: 57-59

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@VultisigApp/VultisigApp/Views/Agent/AgentPasswordPromptView.swift` around
lines 25 - 35, Replace hardcoded font calls with Theme.fonts tokens: change the
Image(...).font(.system(size: 48)) to use the theme icon/font token (e.g.
Theme.fonts.iconLarge), change Text("Enter Vault Password").font(.title3.bold())
to the theme title token (e.g. Theme.fonts.title3Bold), and change
Text(...).font(.subheadline) to Theme.fonts.subheadline (and the other
occurrences flagged at the later Text lines) so all typography uses Theme.fonts
instead of hardcoded .font(...) calls.
VultisigApp/VultisigApp/Services/Agent/AgentBackendClient.swift (1)

293-369: Consolidate duplicated doRequest response handling.

Both overloads repeat the same status/error/empty-body logic; extracting a shared executor will reduce drift and simplify maintenance.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@VultisigApp/VultisigApp/Services/Agent/AgentBackendClient.swift` around lines
293 - 369, Both doRequest overloads duplicate response parsing and
error/empty-body handling; extract that logic into a single helper (e.g., a
private func processResponse<T: Decodable>(data: Data, response: URLResponse)
throws -> T) that encapsulates casting to HTTPURLResponse, 401/>=400 checks
(using Self.parseErrorMessage), empty-body handling returning AgentEmptyResponse
when T == AgentEmptyResponse.self, and JSON decoding; then simplify both
doRequest(method:url:token:body: some Encodable) and
doRequest(method:url:token:body: [String: Any]) to build the URLRequest, await
URLSession.shared.data(for: request), and delegate the (data, response) pair to
processResponse to return the decoded T.
VultisigApp/VultisigApp/Views/Agent/AgentChatView.swift (4)

10-10: Screen naming convention: use Screen suffix.

Per coding guidelines, views representing whole screens should use the Screen suffix (e.g., AgentChatScreen instead of AgentChatView).

As per coding guidelines: "Always use the Screen component for views that represent a whole screen, and use the Screen suffix in struct names."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@VultisigApp/VultisigApp/Views/Agent/AgentChatView.swift` at line 10, Rename
the top-level view struct AgentChatView to follow the screen naming convention
(e.g., AgentChatScreen) and update all references/usages to the new name (struct
declaration, initializers, previews, navigation destinations, and any files
importing or instantiating AgentChatView) so the type is consistently renamed
throughout the codebase.

258-261: Notification.Name extensions may conflict with other definitions.

These notification names are defined in a file extension. Consider moving them to a centralized location (e.g., a dedicated Notifications.swift file) or namespacing them to avoid potential conflicts.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@VultisigApp/VultisigApp/Views/Agent/AgentChatView.swift` around lines 258 -
261, The Notification.Name extension defining agentDidAcceptTx and
agentDidRejectTx in AgentChatView.swift may collide with other names; move these
definitions out of the view and into a centralized notifications module (e.g.,
create a Notifications.swift) or namespace them to avoid conflicts — for
example, define them as uniquely prefixed/ scoped notification identifiers and
expose them from a single place so callers use Notification.Name.<uniqueName>
(referencing the existing symbols Notification.Name, agentDidAcceptTx and
agentDidRejectTx when relocating).

116-116: Use .foregroundStyle() instead of deprecated .foregroundColor().

Multiple occurrences throughout the file (lines 116, 200, 204, 233) use the deprecated modifier.

As per coding guidelines: "Use .foregroundStyle() modifier instead of deprecated .foregroundColor() for text color styling."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@VultisigApp/VultisigApp/Views/Agent/AgentChatView.swift` at line 116, In
AgentChatView replace deprecated .foregroundColor(...) calls with
.foregroundStyle(...) for each occurrence (e.g., change
.foregroundColor(Theme.colors.textPrimary) to
.foregroundStyle(Theme.colors.textPrimary)) so Text and other view modifiers use
the modern API; update all instances referenced in AgentChatView (the ones
around the existing .foregroundColor usages) to ensure consistent styling.

164-170: Consider using Task with try await Task.sleep instead of DispatchQueue.main.asyncAfter.

The async/await pattern is preferred for delayed execution in Swift concurrency.

Example refactor
 if let starter = UserDefaults.standard.string(forKey: "agent_pending_starter") {
     UserDefaults.standard.removeObject(forKey: "agent_pending_starter")
     inputText = starter
-    DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
-        sendMessage()
-    }
+    Task {
+        try? await Task.sleep(for: .milliseconds(500))
+        sendMessage()
+    }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@VultisigApp/VultisigApp/Views/Agent/AgentChatView.swift` around lines 164 -
170, Replace the DispatchQueue.main.asyncAfter delay with Swift concurrency: set
the pending starter into inputText on the main actor, then create a Task that
uses try await Task.sleep for 500ms and invokes sendMessage() on the main actor;
reference the existing UserDefaults key "agent_pending_starter", the inputText
variable and the sendMessage() function in AgentChatView so the delay uses Task
+ Task.sleep and all UI updates occur on MainActor.
VultisigApp/VultisigApp/View Models/Agent/AgentConversationsViewModel.swift (2)

29-30: Unused timer variable.

startersRefreshTimer is declared but never scheduled. The refresh logic uses lastStartersRefresh date instead. Either remove the unused timer or implement the scheduled refresh if intended.

Remove unused timer
-    private var startersRefreshTimer: Timer?
     private var lastStartersRefresh: Date?

And in deinit:

     deinit {
-        startersRefreshTimer?.invalidate()
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@VultisigApp/VultisigApp/View` Models/Agent/AgentConversationsViewModel.swift
around lines 29 - 30, The property startersRefreshTimer is declared but never
scheduled; either remove it and any related references (including invalidation
in deinit) or implement a scheduled Timer to drive the refresh logic that
currently relies on lastStartersRefresh. If removing: delete the
startersRefreshTimer property and any timer invalidation/cleanup calls. If
implementing: create and schedule startersRefreshTimer (e.g.,
Timer.scheduledTimer...) to call your refresh method that updates
lastStartersRefresh, and ensure you invalidate startersRefreshTimer in deinit.
Use the identifiers startersRefreshTimer, lastStartersRefresh, and deinit to
locate the relevant code to change.

91-133: Duplicate code with AgentChatViewModel.loadStarters().

This method is nearly identical to the one in AgentChatViewModel. Consider extracting the shared logic into a common service or helper to reduce duplication.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@VultisigApp/VultisigApp/View` Models/Agent/AgentConversationsViewModel.swift
around lines 91 - 133, The loadStarters implementation in
AgentConversationsViewModel duplicates AgentChatViewModel.loadStarters; extract
the shared logic into a single helper/service (e.g.,
AgentStartersService.fetchStarters or AgentStarterProvider.fetch(for:vault:))
that accepts the Vault, backendClient, token-provider (getValidToken), and
logger and returns a result object (starters array and lastRefresh /
authorization state) or throws AgentBackendClient.AgentBackendError; move the
network/request/response/fallback and error handling into that service and have
both AgentConversationsViewModel.loadStarters and
AgentChatViewModel.loadStarters call the new service and then map the returned
data to their local properties (starters, lastStartersRefresh) and update
isConnected/passwordRequired when an .unauthorized error is surfaced.
VultisigApp/VultisigApp/Views/Agent/AgentChatMessageView.swift (3)

138-175: Consider using closures instead of NotificationCenter for button actions.

Using NotificationCenter to communicate button taps from the view to the ViewModel works but creates implicit coupling. A more explicit approach would be to pass action closures as parameters to the view.

Alternative approach using closures
struct AgentChatMessageView: View {
    let message: AgentChatMessage
    var onAcceptTx: ((AgentTxReady) -> Void)?
    var onRejectTx: ((AgentTxReady) -> Void)?
    
    // Then in the button actions:
    Button {
        onRejectTx?(tx)
    } label: {
        // ...
    }
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@VultisigApp/VultisigApp/Views/Agent/AgentChatMessageView.swift` around lines
138 - 175, The view currently posts NotificationCenter events (.agentDidAcceptTx
/ .agentDidRejectTx) from AgentChatMessageView button actions which creates
implicit coupling; refactor AgentChatMessageView to accept optional action
closures (e.g., onAcceptTx: ((AgentTxReady)->Void)? and onRejectTx:
((AgentTxReady)->Void)?), replace the NotificationCenter.post calls inside the
"Yes"/"No"/"Sign Transaction" Button handlers with calls to those closures
(onAcceptTx?(tx) / onRejectTx?(tx)), and update callers to provide the
appropriate ViewModel-bound closures instead of relying on notifications.

123-126: Hardcoded estimated fee placeholder.

The comment acknowledges this is a placeholder. Consider wiring up the actual estimated fee from the transaction proposal when available.

Would you like me to help implement proper fee extraction from the AgentTxReady payload, or should I open an issue to track this?

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@VultisigApp/VultisigApp/Views/Agent/AgentChatMessageView.swift` around lines
123 - 126, Replace the hardcoded fee display in AgentChatMessageView with the
actual estimated fee from the transaction payload: read the fee value (or fee
estimate) from the AgentTxReady/transaction proposal object (e.g.,
AgentTxReady.fee, .estimatedFee, or nested proposal fields) passed into the view
(where Text("EST. FEE: 0.001 \(tx.fromSymbol)") is used), format it to the
desired precision and currency symbol (fallback to a localized "N/A" or 0.0 if
missing), and update the Text to interpolate that formatted fee with
tx.fromSymbol; ensure nil-safe access and preserve the monospaced footnote
styling and Theme.colors.textTertiary color.

34-34: Use .foregroundStyle() instead of deprecated .foregroundColor().

The .foregroundColor() modifier is deprecated. This applies throughout the file (lines 34, 64, 71, 86, 90, 94, 110, 115, 120, 125, 133, 145, 157, 169, 194, 204, 224, 229, 243, 246, 254).

As per coding guidelines: "Use .foregroundStyle() modifier instead of deprecated .foregroundColor() for text color styling."

Example fix for line 34
 Text(.init(message.content)) // Renders markdown
     .font(.body)
-    .foregroundColor(Theme.colors.textPrimary)
+    .foregroundStyle(Theme.colors.textPrimary)
     .textSelection(.enabled)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@VultisigApp/VultisigApp/Views/Agent/AgentChatMessageView.swift` at line 34,
Replace all uses of the deprecated .foregroundColor(...) with
.foregroundStyle(...) in AgentChatMessageView.swift; specifically update every
modifier calls inside the AgentChatMessageView struct (e.g., in the body,
message bubble, timestamp/metadata text, roleLabel, and any Text views rendered
by functions like messageBubbleView or metadataView) to call
.foregroundStyle(Theme.colors.textPrimary) or the equivalent Theme.colors value
currently used, preserving the same color expression and any conditional logic
so behavior is unchanged.
VultisigApp/VultisigApp/View Models/Agent/AgentChatViewModel.swift (4)

124-124: Hardcoded model identifier.

The model identifier "anthropic/claude-sonnet-4.5" is hardcoded in multiple places (lines 124, 178, 500). Consider extracting this to a constant for easier maintenance.

Suggested refactor
// Add to AgentChatViewModel or a config:
private static let defaultModel = "anthropic/claude-sonnet-4.5"

// Then use:
model: Self.defaultModel
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@VultisigApp/VultisigApp/View` Models/Agent/AgentChatViewModel.swift at line
124, Extract the hardcoded model identifier into a single constant and replace
all inline occurrences with that constant: add a private static let (e.g.
private static let defaultModel = "anthropic/claude-sonnet-4.5") to
AgentChatViewModel (or a shared config) and update every place using model:
"anthropic/claude-sonnet-4.5" to use model: Self.defaultModel (or the config
constant) so the identifier is defined once and easy to maintain.

323-325: Accessing AppViewModel.shared directly instead of via environment.

The coding guidelines recommend using @EnvironmentObject for app-level singletons. However, since this is within an SSE event handler where the vault might not be passed, consider refactoring to pass the vault through the event handling chain.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@VultisigApp/VultisigApp/View` Models/Agent/AgentChatViewModel.swift around
lines 323 - 325, The code is directly accessing AppViewModel.shared inside the
SSE handler instead of using an injected environment object; update
AgentChatViewModel (and the SSE event handling chain) to accept the vault via
dependency injection rather than referencing AppViewModel.shared: add an
`@EnvironmentObject/AppViewModel` parameter to the view or pass the selected vault
into the SSE callback so you call handleActions(actions, vault: vault) with the
provided vault value; update the SSE subscription entry point that invokes this
handler to thread the vault through (e.g., pass selectedVault from the view into
the event handler) and remove direct references to AppViewModel.shared and
selectedVault from inside the handler.

268-292: Duplicate code with AgentConversationsViewModel.loadStarters().

This method is nearly identical to the one in AgentConversationsViewModel. Consider extracting to a shared service.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@VultisigApp/VultisigApp/View` Models/Agent/AgentChatViewModel.swift around
lines 268 - 292, The loadStarters implementation is duplicated between
AgentChatViewModel.loadStarters() and
AgentConversationsViewModel.loadStarters(); extract the common logic into a
shared helper (e.g.,
AgentStarterService.fetchStarters(vault:backendClient:logger:) or a static
helper on AgentStarter) that: obtains a token via getValidToken(vault:), builds
context with AgentContextBuilder.buildContext(vault:), calls
backendClient.getStarters(request:token:), returns either the fetched starters
or the fallback (shuffled and prefixed to 4), and throws on unexpected errors;
then replace both AgentChatViewModel.loadStarters and
AgentConversationsViewModel.loadStarters with a simple call to that shared
method and assign the returned value to starters.

405-418: Redundant MainActor.run inside @MainActor class.

Since AgentChatViewModel is @MainActor, any code within it already runs on the main actor. The await MainActor.run on line 408 is unnecessary.

Simplified version
     Task {
         let result = await AgentToolExecutor.execute(action: action, vault: vault)
         
-        await MainActor.run {
-            if let idx = self.messages.firstIndex(where: { $0.id == toolCallId }) {
-                self.messages[idx].toolCall?.status = result.success ? .success : .error
-                self.messages[idx].toolCall?.resultData = result.data
-                self.messages[idx].toolCall?.error = result.error
-            }
+        if let idx = self.messages.firstIndex(where: { $0.id == toolCallId }) {
+            self.messages[idx].toolCall?.status = result.success ? .success : .error
+            self.messages[idx].toolCall?.resultData = result.data
+            self.messages[idx].toolCall?.error = result.error
         }
         
         // Stream the result back
         self.sendActionResult(result, vault: vault)
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@VultisigApp/VultisigApp/View` Models/Agent/AgentChatViewModel.swift around
lines 405 - 418, The MainActor.run wrapper is redundant inside the `@MainActor`
isolated AgentChatViewModel; remove the await MainActor.run { ... } block and
perform the messages mutation directly in the Task after awaiting
AgentToolExecutor.execute(action:vault:), i.e. update
self.messages[idx].toolCall?.status/resultData/error inline (referencing
AgentToolExecutor.execute, self.messages, toolCallId and sendActionResult) and
then call sendActionResult(result, vault: vault).
VultisigApp/VultisigApp/Services/Agent/AgentContextBuilder.swift (1)

49-49: Address book integration is pending.

The TODO indicates that address book items need to be wired up. This would provide the agent with contact information for transaction destinations.

Would you like me to help implement the address book integration, or should I open an issue to track this task?

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@VultisigApp/VultisigApp/Services/Agent/AgentContextBuilder.swift` at line 49,
AgentContextBuilder is currently passing addressBook: nil — wire up the real
address book by injecting or retrieving the app's address book and supplying it
to the AgentContext; update the AgentContextBuilder initializer or factory to
accept an AddressBook or AddressBookService (e.g., AddressBook.shared or
AddressBookService.fetchContacts()), map the stored contact model to the
AgentContext's expected address book item type, and pass that value instead of
nil (with a safe empty fallback if fetching fails). Ensure you modify
AgentContextBuilder and any callers so the address book dependency is provided
and handle errors gracefully.
VultisigApp/VultisigApp/Services/Agent/AgentToolExecutor.swift (2)

240-243: Verbose JSON round-trip for result construction.

The pattern of encoding to JSON then decoding back to [String: Any] is repeated multiple times. Consider adding a helper method to simplify this.

Helper method suggestion
private static func encodeToAnyCodableDict<T: Encodable>(_ value: T) -> [String: AnyCodable] {
    guard let data = try? JSONEncoder().encode(value),
          let dict = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
        return [:]
    }
    return dict.mapValues { AnyCodable($0) }
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@VultisigApp/VultisigApp/Services/Agent/AgentToolExecutor.swift` around lines
240 - 243, Replace the repeated JSON encode/decode round-trip with a shared
helper: add a private static method (e.g., encodeToAnyCodableDict<T:
Encodable>(_ value: T) -> [String: AnyCodable]) on the AgentToolExecutor (or
adjacent utility scope) that uses JSONEncoder and JSONSerialization to produce
[String: AnyCodable] (returning [:] on failure), then update the result
construction (where AgentActionResult(action: action.type, actionId: action.id,
success: true, data: ...)) to call encodeToAnyCodableDict(["results": results])
instead of the inline JSONEncoder/JSONSerialization code so all occurrences
reuse the helper.

183-234: Batch multiple token additions efficiently.

The loop processes tokens sequentially with individual CoinService.addIfNeeded calls. For large batches, this could be slow. Also, discovered tokens are fetched individually for each native token added.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@VultisigApp/VultisigApp/Services/Agent/AgentToolExecutor.swift` around lines
183 - 234, The loop currently awaits CoinService.addIfNeeded sequentially and
calls CoinService.addDiscoveredTokens per native token; refactor to process
tokens concurrently using a TaskGroup (or map with async let) to call
CoinService.addIfNeeded for each tokenParam so adds happen in parallel, collect
successful newCoin results and which ones are native, then after the group
completes call a single batched method to process discovered natives (either add
a new CoinService.addDiscoveredTokens(nativeTokens: [Coin], to: Vault) or call
the existing addDiscoveredTokens once in a loop but outside the per-token
tasks); update the results assembly to use the grouped outcomes and still
preserve chainAdded calculation and vault.defiChains updates.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6ed361e and 316a223.

📒 Files selected for processing (41)
  • VultisigApp/VultisigApp.xcodeproj/project.pbxproj
  • VultisigApp/VultisigApp/ContentView.swift
  • VultisigApp/VultisigApp/Features/Agent/Navigation/AgentRoute.swift
  • VultisigApp/VultisigApp/Features/Agent/Navigation/AgentRouter.swift
  • VultisigApp/VultisigApp/Features/Defi/Protocols/Circle/CircleWithdrawView.swift
  • VultisigApp/VultisigApp/Features/Home/HomeScreen.swift
  • VultisigApp/VultisigApp/Features/Home/Model/HomeTab.swift
  • VultisigApp/VultisigApp/Features/Wallet/ChainDetail/ChainDetailScreenContainer.swift
  • VultisigApp/VultisigApp/Model/Chain.swift
  • VultisigApp/VultisigApp/Navigation/VultisigRouter.swift
  • VultisigApp/VultisigApp/Services/Agent/AgentAuthService.swift
  • VultisigApp/VultisigApp/Services/Agent/AgentBackendClient.swift
  • VultisigApp/VultisigApp/Services/Agent/AgentContextBuilder.swift
  • VultisigApp/VultisigApp/Services/Agent/AgentModels.swift
  • VultisigApp/VultisigApp/Services/Agent/AgentToolExecutor.swift
  • VultisigApp/VultisigApp/Services/FastVault/FastVaultKeysignService.swift
  • VultisigApp/VultisigApp/Utils/Endpoint.swift
  • VultisigApp/VultisigApp/View Models/Agent/AgentChatViewModel.swift
  • VultisigApp/VultisigApp/View Models/Agent/AgentConversationsViewModel.swift
  • VultisigApp/VultisigApp/View Models/KeysignDiscoveryViewModel.swift
  • VultisigApp/VultisigApp/View Models/KeysignViewModel.swift
  • VultisigApp/VultisigApp/Views/Agent/AgentChatMessageView.swift
  • VultisigApp/VultisigApp/Views/Agent/AgentChatView.swift
  • VultisigApp/VultisigApp/Views/Agent/AgentConversationsView.swift
  • VultisigApp/VultisigApp/Views/Agent/AgentPasswordPromptView.swift
  • VultisigApp/VultisigApp/Views/Agent/AgentThinkingIndicator.swift
  • VultisigApp/VultisigApp/Views/Components/TabBar/VultiTabBar.swift
  • VultisigApp/VultisigApp/Views/FunctionCall/FunctionCallPairScreen.swift
  • VultisigApp/VultisigApp/Views/FunctionCall/FunctionCallRoute.swift
  • VultisigApp/VultisigApp/Views/FunctionCall/FunctionCallRouteBuilder.swift
  • VultisigApp/VultisigApp/Views/FunctionCall/FunctionCallRouter.swift
  • VultisigApp/VultisigApp/Views/FunctionCall/FunctionCallVerifyScreen.swift
  • VultisigApp/VultisigApp/Views/Keysign/KeysignDiscoveryView.swift
  • VultisigApp/VultisigApp/Views/Keysign/KeysignView.swift
  • VultisigApp/VultisigApp/Views/Send/Navigation/SendRoute.swift
  • VultisigApp/VultisigApp/Views/Send/Navigation/SendRouteBuilder.swift
  • VultisigApp/VultisigApp/Views/Send/Navigation/SendRouter.swift
  • VultisigApp/VultisigApp/Views/Send/Screens/SendPairScreen.swift
  • VultisigApp/VultisigApp/Views/Send/Screens/SendVerifyScreen.swift
  • VultisigApp/VultisigApp/Views/Settings/SettingsCustomMessageView.swift
  • VultisigApp/VultisigApp/Views/Swap/SwapCryptoView.swift
💤 Files with no reviewable changes (2)
  • VultisigApp/VultisigApp/Views/Settings/SettingsCustomMessageView.swift
  • VultisigApp/VultisigApp/Views/Swap/SwapCryptoView.swift
👮 Files not reviewed due to content moderation or server errors (1)
  • VultisigApp/VultisigApp.xcodeproj/project.pbxproj

Comment on lines +59 to +66
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let rawValue = try container.decode(String.self)
if let chain = Chain(rawValue: rawValue) {
self = chain
} else if let migrated = Chain.removedChainMigrations[rawValue] {
self = migrated
} else {
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Migration precedence bug: legacy values are decoded before remapping.

init(from:) checks Chain(rawValue:) first, so migrations for values that still exist as enum cases (e.g., "polygon", "thorChainChainnet") are never applied. This blocks the intended data migration path.

Proposed fix
 init(from decoder: Decoder) throws {
     let container = try decoder.singleValueContainer()
     let rawValue = try container.decode(String.self)
-    if let chain = Chain(rawValue: rawValue) {
-        self = chain
-    } else if let migrated = Chain.removedChainMigrations[rawValue] {
+    if let migrated = Chain.removedChainMigrations[rawValue] {
         self = migrated
+    } else if let chain = Chain(rawValue: rawValue) {
+        self = chain
     } else {
         throw DecodingError.dataCorruptedError(
             in: container,
             debugDescription: "Cannot initialize Chain from invalid String value \(rawValue)"
         )
     }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let rawValue = try container.decode(String.self)
if let chain = Chain(rawValue: rawValue) {
self = chain
} else if let migrated = Chain.removedChainMigrations[rawValue] {
self = migrated
} else {
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let rawValue = try container.decode(String.self)
if let migrated = Chain.removedChainMigrations[rawValue] {
self = migrated
} else if let chain = Chain(rawValue: rawValue) {
self = chain
} else {
throw DecodingError.dataCorruptedError(
in: container,
debugDescription: "Cannot initialize Chain from invalid String value \(rawValue)"
)
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@VultisigApp/VultisigApp/Model/Chain.swift` around lines 59 - 66, The decoder
init currently tries Chain(rawValue:) before checking
Chain.removedChainMigrations, so legacy strings that still match current enum
cases (e.g., "polygon", "thorChainChainnet") never get remapped; update
init(from decoder: Decoder) to first consult
Chain.removedChainMigrations[rawValue] and, if present, assign self = migrated,
otherwise fall back to Chain(rawValue: rawValue) and then to the error path—this
ensures migrations take precedence over direct rawValue matching.

private let keychainPrefix = "vultisig_agent_auth_"

/// In-memory token cache
private var tokens: [String: AgentAuthToken] = [:]
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Thread-safety concern with tokens dictionary.

The tokens dictionary is accessed from multiple async contexts without synchronization. Since AgentAuthService is not @MainActor, concurrent access from different tasks could cause data races.

Suggested fix: Add actor isolation or use a lock

Option 1 - Make the class an actor:

-final class AgentAuthService {
+actor AgentAuthService {

Option 2 - Make it @MainActor:

+@MainActor
 final class AgentAuthService {

Option 3 - Use a lock for the dictionary:

private let tokenLock = NSLock()
private var _tokens: [String: AgentAuthToken] = [:]
private var tokens: [String: AgentAuthToken] {
    get { tokenLock.withLock { _tokens } }
    set { tokenLock.withLock { _tokens = newValue } }
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@VultisigApp/VultisigApp/Services/Agent/AgentAuthService.swift` at line 20,
The tokens dictionary on AgentAuthService is not thread-safe (private var
tokens: [String: AgentAuthToken] = [:]) and can be accessed concurrently from
async tasks; fix by adding isolation: either convert AgentAuthService to an
actor (or mark it `@MainActor`) so tokens become actor-isolated, or keep it a
class and protect accesses with a lock (e.g., replace tokens with a locked
backing store and wrap all reads/writes to _tokens with the lock). Update all
methods that read/write tokens (references to tokens, _tokens, tokenLock, or
AgentAuthService methods manipulating tokens) to use the chosen isolation
mechanism consistently.

AsyncThrowingStream { continuation in
let task = Task {
do {
let url = URL(string: Endpoint.agentConversationMessages(id: convId))!
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

wc -l VultisigApp/VultisigApp/Services/Agent/AgentBackendClient.swift

Repository: vultisig/vultisig-ios

Length of output: 132


🏁 Script executed:

sed -n '110,130p' VultisigApp/VultisigApp/Services/Agent/AgentBackendClient.swift

Repository: vultisig/vultisig-ios

Length of output: 1257


🏁 Script executed:

sed -n '320,370p' VultisigApp/VultisigApp/Services/Agent/AgentBackendClient.swift

Repository: vultisig/vultisig-ios

Length of output: 2058


Eliminate force unwraps and force casts in network operations.

Three instances where force unwraps/casts can crash the app:

  • Line 117: URL(...)! force unwrap
  • Lines 324, 363: as! T force casts for empty response handling

Use guard statements with error throwing for URL construction, and conditional casting (as?) for type checks, as shown in your suggested patches. This aligns with the existing error handling pattern already used elsewhere in this file (e.g., line 340).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@VultisigApp/VultisigApp/Services/Agent/AgentBackendClient.swift` at line 117,
Replace the force-unwrap of URL(string: Endpoint.agentConversationMessages(id:
convId)) and the force-casts `as! T` with safe checks: in the method that builds
the URL (where `URL(string: Endpoint.agentConversationMessages(id: convId))` is
used) change to a guard-let URL construction and throw a meaningful error if
nil; for the empty-response handling sites that use `as! T` (the two occurrences
in this file) replace with conditional casts (`as? T`) and guard-let the result,
throwing the same or a consistent network/parsing error when the cast fails so
it follows the existing error-handling pattern used elsewhere (see the pattern
around the code near the other guard/throw usages).

Comment on lines +133 to +140
print("[AgentBackend] 🌊 SSE response received")

guard let httpResponse = response as? HTTPURLResponse else {
print("[AgentBackend] ❌ Not an HTTP response")
throw AgentBackendError.noBody
}
print("[AgentBackend] 🌊 SSE HTTP status: \(httpResponse.statusCode)")
print("[AgentBackend] 🌊 Content-Type: \(httpResponse.value(forHTTPHeaderField: "Content-Type") ?? "nil")")
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Remove raw SSE/body print statements from request/stream paths.

These logs include streamed payload content and error bodies, which can expose user prompts/responses and backend details in device logs.

Also applies to: 143-154, 182-188, 196-205, 210-210

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@VultisigApp/VultisigApp/Services/Agent/AgentBackendClient.swift` around lines
133 - 140, Remove all raw print(...) calls that emit SSE/body content in the
AgentBackendClient SSE/request/stream handling paths (the print lines that show
SSE response, full body, or error bodies); specifically eliminate prints in the
SSE response handler (the block that casts to HTTPURLResponse and references
AgentBackendError.noBody) and the other indicated ranges (143-154, 182-188,
196-205, 210) and replace them with non-sensitive logging: only record HTTP
statusCode, Content-Type, and/or the size or a redacted indicator (no payload
text) using the app's logger or OSLog with privacy flags. Ensure no streamed
payloads, prompts, responses, or full error bodies are written to device logs.

Comment on lines +48 to +63
func keysign(input: FastVaultKeysignInput) async throws -> FastVaultKeysignResult {
var lastError: Error?

for attempt in 1...input.maxAttempts {
do {
let result = try await keysignAttempt(input: input)
return result
} catch {
lastError = error
print("[FastVaultKeysign] ⚠️ Attempt \(attempt)/\(input.maxAttempts) failed: \(error.localizedDescription)")
if attempt < input.maxAttempts && isRetryable(error) {
try await Task.sleep(for: .seconds(2))
continue
}
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Fix retry control flow: invalid attempts can crash, and non-retryable errors still retry.

1...input.maxAttempts is unsafe for maxAttempts <= 0, and non-retryable failures currently continue to later attempts. In this flow, that can trigger unnecessary repeat keysign calls.

🛠️ Suggested fix
 func keysign(input: FastVaultKeysignInput) async throws -> FastVaultKeysignResult {
+    guard input.maxAttempts > 0 else {
+        throw FastVaultKeysignError.keysignFailed("maxAttempts must be greater than 0")
+    }
     var lastError: Error?

     for attempt in 1...input.maxAttempts {
         do {
             let result = try await keysignAttempt(input: input)
             return result
         } catch {
             lastError = error
             print("[FastVaultKeysign] ⚠️ Attempt \(attempt)/\(input.maxAttempts) failed: \(error.localizedDescription)")
-            if attempt < input.maxAttempts && isRetryable(error) {
-                try await Task.sleep(for: .seconds(2))
-                continue
-            }
+            guard attempt < input.maxAttempts, isRetryable(error) else {
+                break
+            }
+            try await Task.sleep(for: .seconds(2))
+            continue
         }
     }

     throw lastError ?? FastVaultKeysignError.keysignFailed("Unknown error")
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@VultisigApp/VultisigApp/Services/FastVault/FastVaultKeysignService.swift`
around lines 48 - 63, The loop in keysign(input:) is unsafe for
input.maxAttempts <= 0 and incorrectly allows non-retryable errors to proceed to
further attempts; guard input.maxAttempts > 0 at the start of keysign(input:),
and change the catch behaviour so that if isRetryable(error) is false you
rethrow immediately instead of continuing; otherwise only retry when attempt <
input.maxAttempts and isRetryable(error) (sleep then continue). Use the existing
symbols keysign(input:), keysignAttempt(input:), isRetryable(_:), lastError, and
input.maxAttempts, and ensure that if the loop exits without returning you throw
lastError (or a specific error) rather than letting execution fall through.

Comment on lines +59 to +62
Text("Loading conversations...")
.font(.subheadline)
.foregroundColor(Theme.colors.textTertiary)
.padding(.top, 8)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, check if the file exists and find all .foregroundColor usages with line numbers
rg -n '\.foregroundColor\s*\(' VultisigApp/VultisigApp/Views/Agent/AgentConversationsView.swift

Repository: vultisig/vultisig-ios

Length of output: 871


Switch all foregroundColor usages to foregroundStyle.

This view contains 11 instances of the deprecated .foregroundColor() modifier that must be updated to .foregroundStyle() per the coding guidelines.

Affected lines and fix pattern

Lines: 61, 81, 85, 95, 98, 124, 127, 131, 154, 159, 164

-Text("...")
-    .foregroundColor(Theme.colors.textTertiary)
+Text("...")
+    .foregroundStyle(Theme.colors.textTertiary)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Text("Loading conversations...")
.font(.subheadline)
.foregroundColor(Theme.colors.textTertiary)
.padding(.top, 8)
Text("Loading conversations...")
.font(.subheadline)
.foregroundStyle(Theme.colors.textTertiary)
.padding(.top, 8)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@VultisigApp/VultisigApp/Views/Agent/AgentConversationsView.swift` around
lines 59 - 62, In AgentConversationsView, replace every usage of the deprecated
.foregroundColor(...) with .foregroundStyle(...) for the 11 occurrences inside
the view (e.g., the Text("Loading conversations...") and other Text/Image rows);
specifically change calls like .foregroundColor(Theme.colors.textTertiary) to
.foregroundStyle(Theme.colors.textTertiary) so the Theme.colors values are
passed to the new modifier; update all instances referenced in the review (the
ones within AgentConversationsView) to use .foregroundStyle to conform with the
coding guidelines.

Comment on lines +90 to +103
Button {
navigateToChat(with: nil)
} label: {
HStack {
Image(systemName: "plus.bubble.fill")
.foregroundColor(Theme.colors.bgPrimary)
Text("New Chat")
.font(.body.bold())
.foregroundColor(Theme.colors.bgPrimary)
}
.padding()
.background(Theme.colors.turquoise)
.cornerRadius(12)
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Replace custom “New Chat” CTA buttons with PrimaryButton.

These two CTA sections currently use custom button styling instead of the shared component.

As per coding guidelines: "Use PrimaryButton component for all buttons instead of creating custom button styles."

Also applies to: 121-139

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@VultisigApp/VultisigApp/Views/Agent/AgentConversationsView.swift` around
lines 90 - 103, Replace the custom-styled Button usages in
AgentConversationsView with the shared PrimaryButton component: where the view
currently calls Button { navigateToChat(with: nil) } label: { ... } (and the
similar CTA block later), swap them to use PrimaryButton so the CTA text, icon,
action (navigateToChat(with: nil) or appropriate argument) and the Theme styling
are provided by PrimaryButton; ensure you pass the action closure that calls
navigateToChat and supply the label/title and system image to PrimaryButton (or
adapt to its initializer) so no custom padding/background/cornerRadius styling
remains.


private var connectionButton: some View {
Circle()
.fill(viewModel.isConnected ? Theme.colors.alertSuccess : Color.gray)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid hardcoded disconnected color in the connection dot.

Use a semantic Theme.colors value instead of Color.gray so disconnected state follows theme design.

As per coding guidelines: "Always use the Theme enum for colors instead of hardcoding color values."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@VultisigApp/VultisigApp/Views/Agent/AgentConversationsView.swift` at line
186, The connection dot currently hardcodes the disconnected color (Color.gray);
update the ternary in the .fill modifier so the false branch uses a semantic
Theme color (e.g., replace Color.gray with Theme.colors.alertMuted or the
appropriate disconnected color) so it reads like .fill(viewModel.isConnected ?
Theme.colors.alertSuccess : Theme.colors.alertMuted) and ensure the chosen
Theme.colors property returns a SwiftUI Color to match the .fill usage.

Comment on lines +10 to +18
struct AgentPasswordPromptView: View {
let onSubmit: (String) -> Void

@State private var password = ""
@State private var isSubmitting = false
@Environment(\.dismiss) private var dismiss

var body: some View {
NavigationView {
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Align this flow to screen conventions (Screen + *Screen.swift).

This prompt is implemented as a full screen, but it is defined as AgentPasswordPromptView in *View.swift. Please move it to the screen pattern used in this repo.

As per coding guidelines: "Always use the Screen component for views that represent a whole screen, and use the Screen suffix in struct names..." and "Name screen files with the *Screen.swift pattern..."

Also applies to: 94-95

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@VultisigApp/VultisigApp/Views/Agent/AgentPasswordPromptView.swift` around
lines 10 - 18, This view is a full-screen prompt and must follow the repo's
Screen pattern: rename the struct AgentPasswordPromptView to
AgentPasswordPromptScreen (and move the file to
AgentPasswordPromptScreen.swift), update its type signature and initializer
usage (preserve onSubmit, password, isSubmitting, and dismiss bindings), and
replace any NavigationView usage inside with the project's Screen container
component so the screen integrates with the app's layout conventions; also
update all call sites that reference AgentPasswordPromptView to use
AgentPasswordPromptScreen.

Comment on lines +39 to +45
SecureField("Password", text: $password)
.textFieldStyle(.plain)
.padding()
.background(Theme.colors.bgSurface1)
.cornerRadius(12)
.foregroundColor(Theme.colors.textPrimary)
.padding(.horizontal)
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Use shared input/button components (CommonTextField, PrimaryButton).

The current raw SecureField + custom button styling diverges from the project’s standard components for consistency and maintainability.

As per coding guidelines: "Use CommonTextField for all text input fields" and "Use PrimaryButton component for all buttons instead of creating custom button styles."

Also applies to: 47-75

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@VultisigApp/VultisigApp/Views/Agent/AgentPasswordPromptView.swift` around
lines 39 - 45, Replace the raw SecureField and the custom-styled button in
AgentPasswordPromptView with the shared CommonTextField and PrimaryButton
components: swap the SecureField("Password", text: $password) usage to use
CommonTextField bound to the same password state (preserving .secure / isSecure
input variant or configuration on CommonTextField), remove the ad-hoc
padding/background/cornerRadius/foregroundColor styling and rely on Theme via
the shared component, and replace the custom Button (the block covering lines
47-75) with PrimaryButton wired to the same action/disabled logic so behavior
(submit/cancel) and state bindings remain identical. Ensure you keep the same
accessibility labels/placeholders and validation/disabled conditions when
mapping the handlers to CommonTextField and PrimaryButton.

After auto-executing actions locally the first SSE stream is already
done (isLoading=false). The few-second wait for the backend's follow-up
response looked like a freeze. Re-enable isLoading before sendActionResult
so the spinner stays visible until the next stream's first event arrives.
Plant an empty seed message (streamingMessageId set) before calling
sendActionResult. The second SSE stream's text_delta events then
append character-by-character to the seed, giving a natural typing
effect. If the backend sends a message event instead, finalizeStreamingMessage
replaces the seed cleanly — no duplicate messages, no flicker.
- AgentContextBuilder.buildContext: reads all AddressBookItem from
  SwiftData and includes them in every request, so the AI knows
  contacts immediately without calling get_address_book first.
  (Fixes the TODO on addressBook: nil)

- AgentToolExecutor.executeGetAddressBook: chain filter now compares
  against both chain.rawValue ('solana') and chain.name ('Solana'),
  matching the pattern already used in executeGetAddresses. Prevents
  empty results when the AI sends the display name as the filter.
When resolving default decimals for a chain's native coin, the previous
code used .first(where: chain == X) which for Solana returned JUP
(decimals:6) instead of native SOL (decimals:9).

Fixed in 3 places: executeAddCoin, executeAddChain, executeAddAddressBook.
Root cause of SOL balance showing 1000x too high (142 SOL vs actual 0.1422 SOL).
…upporting FastVault and handling broadcasted transactions.
@johnnyluo
Copy link
Contributor

@enriquesouza , this doesn't work on MacOS

@enriquesouza
Copy link
Contributor Author

@enriquesouza , this doesn't work on MacOS

I will fix that

@enriquesouza
Copy link
Contributor Author

Fixed the macOS compilation issue in AgentChatMessageView.swift related to UIApplication and NSWorkspace usage. Replaced it with SwiftUI's @Environment(\.openURL) for cross-platform compatibility.

@enriquesouza
Copy link
Contributor Author

Applied and pushed all actionable CodeRabbit review suggestions, including standardizing the Agent conversational views to PrimaryButton and Screen conventions, and fixing the hardcoded isFast boolean in KeysignDiscoveryView.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants