diff --git a/package.json b/package.json index 95341c3d..4e921a7f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "plotlink", - "version": "0.1.16", + "version": "0.1.17", "private": true, "workspaces": [ "packages/*" diff --git a/src/app/agents/page.tsx b/src/app/agents/page.tsx index 44c1bff6..da71d09b 100644 --- a/src/app/agents/page.tsx +++ b/src/app/agents/page.tsx @@ -6,7 +6,7 @@ import { useAccount, useReadContract } from "wagmi"; import { useQuery } from "@tanstack/react-query"; import { ConnectWallet } from "../../components/ConnectWallet"; import { AgentRegister } from "../../components/AgentRegister"; -import { AgentManage } from "../../components/AgentManage"; +import { AgentManageAll } from "../../components/AgentManage"; import { AgentBuild } from "../../components/AgentBuild"; import { AgentDashboard } from "../../components/AgentDashboard"; import { erc8004Abi } from "../../../lib/contracts/erc8004"; @@ -63,12 +63,13 @@ function AgentsPageInner() { query: { enabled: needsRpcFallback }, }); + // Always check balanceOf to detect owned agent NFTs (even for known users without agent_id) const { data: rpcBalance, isLoading: rpcBalanceLoading } = useReadContract({ address: ERC8004_REGISTRY, abi: erc8004Abi, functionName: "balanceOf", args: address ? [address] : undefined, - query: { enabled: needsRpcFallback }, + query: { enabled: !!address && !dbDetected }, }); const rpcHasNft = rpcBalance !== undefined && rpcBalance > BigInt(0); @@ -98,8 +99,8 @@ function AgentsPageInner() { detectedRole = "agentWallet"; } - const hasExistingAgent = detectedAgentId !== undefined && detectedRole !== undefined; - const detectLoading = dbLoading || (!dbDetected && userExistsLoading) || (needsRpcFallback && (rpcWalletLoading || rpcBalanceLoading || (rpcHasNft && rpcTokenLoading))); + const hasExistingAgent = (detectedAgentId !== undefined && detectedRole !== undefined) || rpcHasNft; + const detectLoading = dbLoading || (!dbDetected && rpcBalanceLoading) || (needsRpcFallback && (rpcWalletLoading || (rpcHasNft && rpcTokenLoading))); // Auto-cache: when RPC fallback detects an agent not in DB, persist it const cachedRef = useRef(false); @@ -194,7 +195,7 @@ npx plotlink-ows # start writing`}

Detecting agent status...

) : hasExistingAgent ? ( - + ) : ( ) diff --git a/src/components/AgentManage.tsx b/src/components/AgentManage.tsx index b87918ce..2ed5e452 100644 --- a/src/components/AgentManage.tsx +++ b/src/components/AgentManage.tsx @@ -1,7 +1,7 @@ "use client"; import { useState, useEffect, useMemo } from "react"; -import { useAccount, useWriteContract, useReadContract, useSignTypedData } from "wagmi"; +import { useAccount, useWriteContract, useReadContract, useReadContracts, useSignTypedData } from "wagmi"; import { type Hex, type Address, zeroAddress } from "viem"; import { browserClient as publicClient } from "../../lib/rpc"; import { erc8004Abi, resolveAgentURI, type AgentMetadata } from "../../lib/contracts/erc8004"; @@ -37,9 +37,10 @@ const SET_WALLET_TYPES = { interface AgentManageProps { agentId: bigint; role: "owner" | "agentWallet"; + source?: "ows" | "direct"; } -export function AgentManage({ agentId, role }: AgentManageProps) { +export function AgentManage({ agentId, role, source }: AgentManageProps) { const { address } = useAccount(); const { writeContractAsync } = useWriteContract(); const { signTypedDataAsync } = useSignTypedData(); @@ -67,6 +68,10 @@ export function AgentManage({ agentId, role }: AgentManageProps) { const [signingWallet, setSigningWallet] = useState(false); const [submittingWallet, setSubmittingWallet] = useState(false); + // OWS wallet change (paste-based flow) + const [owsPastedSig, setOwsPastedSig] = useState(""); + const [owsPastedDeadline, setOwsPastedDeadline] = useState(""); + // Unset wallet state const [unsettingWallet, setUnsettingWallet] = useState(false); @@ -316,9 +321,18 @@ export function AgentManage({ agentId, role }: AgentManageProps) {
-

- {metadata?.name ?? "Agent"} #{agentId.toString()} -

+
+

+ {metadata?.name ?? "Agent"} #{agentId.toString()} +

+ {source && ( + + {source === "ows" ? "OWS Writer" : "Direct"} + + )} +

{role === "owner" ? "You own this agent" : "Your wallet is bound to this agent"}

@@ -460,7 +474,64 @@ export function AgentManage({ agentId, role }: AgentManageProps) { )}
+ ) : source === "ows" ? ( + /* OWS agents: paste-based flow (signature generated on local OWS app) */ +
+
+ + setNewWalletAddr(e.target.value)} placeholder="0x..." + className="border-border bg-surface text-foreground placeholder:text-muted w-full rounded border px-3 py-2 text-sm font-mono focus:border-accent focus:outline-none" /> +
+

+ Go to your OWS app → Settings → enter Agent ID {agentId.toString()} → click "Generate Wallet Bind Code". Paste the signature and deadline below. +

+
+ + setOwsPastedSig(e.target.value)} placeholder="0x..." + className="border-border bg-surface text-foreground placeholder:text-muted w-full rounded border px-3 py-2 text-sm font-mono focus:border-accent focus:outline-none" /> +
+
+ + setOwsPastedDeadline(e.target.value)} placeholder="e.g. 1712345678" + className="border-border bg-surface text-foreground placeholder:text-muted w-full rounded border px-3 py-2 text-sm font-mono focus:border-accent focus:outline-none" /> +
+
+ + +
+
) : ( + /* Direct agents: browser-based sign flow (existing) */
{walletStep === "enter" && ( <> @@ -574,3 +645,119 @@ export function AgentManage({ agentId, role }: AgentManageProps) {
); } + +const ZERO_ADDR = "0x0000000000000000000000000000000000000000"; + +/** Wrapper that enumerates all agents owned by the connected wallet */ +export function AgentManageAll() { + const { address } = useAccount(); + + const { data: balance, isLoading: balanceLoading } = useReadContract({ + address: ERC8004_REGISTRY, + abi: erc8004Abi, + functionName: "balanceOf", + args: address ? [address] : undefined, + query: { enabled: !!address }, + }); + + const agentCount = balance !== undefined ? Number(balance) : 0; + + const tokenIndexCalls = useMemo(() => { + if (!address || agentCount === 0) return []; + return Array.from({ length: agentCount }, (_, i) => ({ + address: ERC8004_REGISTRY, + abi: erc8004Abi, + functionName: "tokenOfOwnerByIndex" as const, + args: [address, BigInt(i)] as const, + })); + }, [address, agentCount]); + + const { data: tokenResults, isLoading: tokensLoading } = useReadContracts({ + contracts: tokenIndexCalls, + query: { enabled: tokenIndexCalls.length > 0 }, + }); + + const agentIds = useMemo(() => { + if (!tokenResults) return []; + return tokenResults + .filter((r) => r.status === "success" && r.result !== undefined) + .map((r) => r.result as bigint); + }, [tokenResults]); + + // Fetch agentURI + getAgentWallet for each agent to determine source + const metaCalls = useMemo(() => { + if (agentIds.length === 0) return []; + return agentIds.flatMap((id) => [ + { address: ERC8004_REGISTRY, abi: erc8004Abi, functionName: "agentURI" as const, args: [id] as const }, + { address: ERC8004_REGISTRY, abi: erc8004Abi, functionName: "getAgentWallet" as const, args: [id] as const }, + ]); + }, [agentIds]); + + const { data: metaResults, isLoading: metaLoading } = useReadContracts({ + contracts: metaCalls, + query: { enabled: metaCalls.length > 0 }, + }); + + const agents = useMemo(() => { + if (agentIds.length === 0 || !metaResults) return []; + return agentIds.map((id, i) => { + const uriResult = metaResults[i * 2]; + const walletResult = metaResults[i * 2 + 1]; + let source: "ows" | "direct" = "direct"; + if (uriResult?.status === "success" && uriResult.result) { + try { + const meta = JSON.parse(uriResult.result as string); + if (meta.type === "ows-writer" || meta.owsWallet) source = "ows"; + } catch { /* not JSON */ } + } + const walletAddr = walletResult?.status === "success" ? (walletResult.result as string) : undefined; + if (walletAddr && walletAddr !== ZERO_ADDR) source = "ows"; + return { agentId: id, source }; + }); + }, [agentIds, metaResults]); + + // Also check if connected wallet is an agent wallet + const { data: selfAgentId } = useReadContract({ + address: ERC8004_REGISTRY, + abi: erc8004Abi, + functionName: "agentIdByWallet", + args: address ? [address] : undefined, + query: { enabled: !!address }, + }); + const isSelfAgent = selfAgentId !== undefined && selfAgentId > BigInt(0); + const selfInList = agents.some((a) => a.agentId === selfAgentId); + + const isLoading = balanceLoading || tokensLoading || metaLoading; + + if (isLoading) { + return ( +
+

Loading agents...

+
+ ); + } + + const hasAgents = agents.length > 0 || (isSelfAgent && !selfInList); + + if (!hasAgents) { + return ( +
+

You have no AI agents registered.

+

+ Switch to the Register tab to register an agent. +

+
+ ); + } + + return ( +
+ {agents.map((agent) => ( + + ))} + {isSelfAgent && !selfInList && ( + + )} +
+ ); +}