@clawpump/claw-agent 0.1.6 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agent/hermes_cli/web_server.py +232 -37
- package/agent/skills/clawpump/SKILL.md +1 -0
- package/agent/web/public/claw-logo.png +0 -0
- package/agent/web/src/App.tsx +6 -4
- package/agent/web/src/lib/api.ts +112 -0
- package/agent/web/src/pages/AgentMailPage.tsx +684 -0
- package/agent/web/src/pages/WalletPage.tsx +53 -5
- package/package.json +1 -1
|
@@ -12081,46 +12081,105 @@ def _parse_mcp_json(text: str) -> Any:
|
|
|
12081
12081
|
return data
|
|
12082
12082
|
|
|
12083
12083
|
|
|
12084
|
+
# ── Warm ClawPump MCP session (shared by the dashboard's MCP routes) ──────
|
|
12085
|
+
# Keep ONE MCP session alive and reuse it — the same thing the agent runtime
|
|
12086
|
+
# does, which is why chat kept working while the dashboard didn't. Opening a
|
|
12087
|
+
# fresh connection per request stalls the shared MCP event loop under OAuth
|
|
12088
|
+
# token churn and wedges every page until a restart. One warm session plus a
|
|
12089
|
+
# single reconnect-on-failure stays fast and self-heals. The lock serializes
|
|
12090
|
+
# calls (one JSON-RPC stream per session); the short timeout means a stall
|
|
12091
|
+
# fails fast instead of hanging forever.
|
|
12092
|
+
_warm_mcp_lock = threading.Lock()
|
|
12093
|
+
_warm_mcp_server: Any = None # live MCPServerTask, kept warm by its keepalive ping
|
|
12094
|
+
|
|
12095
|
+
|
|
12096
|
+
class _McpToolError(RuntimeError):
|
|
12097
|
+
"""The tool ran but returned an error — the session itself is healthy."""
|
|
12098
|
+
|
|
12099
|
+
|
|
12100
|
+
def _clawpump_call(tool: str, arguments: Optional[dict] = None, *, timeout: float = 20) -> Any:
|
|
12101
|
+
"""Call a ClawPump MCP tool over the dashboard's warm, reused session.
|
|
12102
|
+
|
|
12103
|
+
Reuses one long-lived connection and reconnects only if the session has
|
|
12104
|
+
died, so the dashboard behaves like the agent's persistent connection
|
|
12105
|
+
instead of reconnecting (and stalling) per request. Returns parsed JSON;
|
|
12106
|
+
raises ``RuntimeError`` when the MCP is unconfigured or the tool errors.
|
|
12107
|
+
"""
|
|
12108
|
+
global _warm_mcp_server
|
|
12109
|
+
|
|
12110
|
+
name, cfg = _clawpump_mcp()
|
|
12111
|
+
if not name:
|
|
12112
|
+
raise RuntimeError("ClawPump MCP is not configured. Run `hermes clawpump setup`.")
|
|
12113
|
+
|
|
12114
|
+
from tools.mcp_tool import _ensure_mcp_loop, _run_on_mcp_loop, _connect_server
|
|
12115
|
+
from hermes_cli.mcp_config import _resolve_mcp_server_config
|
|
12116
|
+
|
|
12117
|
+
cfg = _resolve_mcp_server_config(cfg)
|
|
12118
|
+
_ensure_mcp_loop()
|
|
12119
|
+
|
|
12120
|
+
def _invoke(server: Any) -> Any:
|
|
12121
|
+
async def _coro():
|
|
12122
|
+
result = await server.session.call_tool(tool, arguments=arguments or {})
|
|
12123
|
+
text = "".join(b.text for b in (result.content or []) if hasattr(b, "text"))
|
|
12124
|
+
if getattr(result, "isError", False):
|
|
12125
|
+
raise _McpToolError(text or "MCP tool returned an error")
|
|
12126
|
+
return text
|
|
12127
|
+
|
|
12128
|
+
data = _parse_mcp_json(_run_on_mcp_loop(_coro, timeout=timeout))
|
|
12129
|
+
if isinstance(data, dict) and isinstance(data.get("error"), str) and data["error"]:
|
|
12130
|
+
raise _McpToolError(data["error"])
|
|
12131
|
+
return data
|
|
12132
|
+
|
|
12133
|
+
with _warm_mcp_lock:
|
|
12134
|
+
server = _warm_mcp_server
|
|
12135
|
+
if server is not None:
|
|
12136
|
+
try:
|
|
12137
|
+
return _invoke(server)
|
|
12138
|
+
except _McpToolError:
|
|
12139
|
+
raise # the session is fine — the tool itself returned an error
|
|
12140
|
+
except Exception:
|
|
12141
|
+
_warm_mcp_server = None # transport died — drop it and reconnect
|
|
12142
|
+
try:
|
|
12143
|
+
_run_on_mcp_loop(lambda: server.shutdown(), timeout=5)
|
|
12144
|
+
except Exception:
|
|
12145
|
+
pass
|
|
12146
|
+
server = _run_on_mcp_loop(lambda: _connect_server(name, cfg), timeout=timeout)
|
|
12147
|
+
_warm_mcp_server = server
|
|
12148
|
+
return _invoke(server)
|
|
12149
|
+
|
|
12150
|
+
|
|
12084
12151
|
@app.get("/api/wallet/balances")
|
|
12085
12152
|
def get_wallet_balances():
|
|
12086
12153
|
"""Agent wallet balances (name + address + SOL + USDC) via the ClawPump MCP.
|
|
12087
12154
|
|
|
12088
12155
|
Calls ``get_wallet_summaries`` (and ``list_agents`` for display names) over
|
|
12089
|
-
|
|
12090
|
-
Read-only. Sync def so FastAPI runs it in a threadpool — MCP call blocks.
|
|
12156
|
+
the dashboard's warm MCP session. Read-only. Sync def — MCP call blocks.
|
|
12091
12157
|
"""
|
|
12092
|
-
from hermes_cli.mcp_config import _call_single_tool
|
|
12093
|
-
|
|
12094
|
-
srv_name, srv_cfg = _clawpump_mcp()
|
|
12095
|
-
if not srv_name:
|
|
12096
|
-
return {
|
|
12097
|
-
"ok": False,
|
|
12098
|
-
"error": "ClawPump MCP is not configured. Run `hermes clawpump setup`.",
|
|
12099
|
-
"wallets": [],
|
|
12100
|
-
}
|
|
12101
|
-
|
|
12102
12158
|
try:
|
|
12103
|
-
|
|
12159
|
+
data = _clawpump_call("get_wallet_summaries", {})
|
|
12104
12160
|
except Exception as exc: # surface any connection / tool error to the UI
|
|
12105
12161
|
return {"ok": False, "error": str(exc), "wallets": []}
|
|
12106
12162
|
|
|
12107
|
-
data = _parse_mcp_json(text)
|
|
12108
12163
|
wallets = data if isinstance(data, list) else []
|
|
12109
12164
|
|
|
12110
12165
|
# Enrich with the agent's display name — the summaries only carry the
|
|
12111
12166
|
# agent_id UUID. Best-effort: a failure here just leaves ``name`` unset.
|
|
12112
12167
|
try:
|
|
12113
|
-
agents =
|
|
12168
|
+
agents = _clawpump_call("list_agents", {})
|
|
12114
12169
|
if isinstance(agents, dict):
|
|
12115
12170
|
agents = agents.get("agents")
|
|
12116
|
-
|
|
12117
|
-
a["id"]: a
|
|
12171
|
+
by_id = {
|
|
12172
|
+
a["id"]: a
|
|
12118
12173
|
for a in (agents or [])
|
|
12119
12174
|
if isinstance(a, dict) and a.get("id")
|
|
12120
12175
|
}
|
|
12121
12176
|
for w in wallets:
|
|
12122
12177
|
if isinstance(w, dict):
|
|
12123
|
-
|
|
12178
|
+
meta = by_id.get(w.get("agent_id")) or {}
|
|
12179
|
+
w["name"] = meta.get("name")
|
|
12180
|
+
w["token_mint"] = meta.get("token_mint")
|
|
12181
|
+
# avatar_url is fetched lazily via /api/agent/avatar — list_agents
|
|
12182
|
+
# nulls it; only the single get_agent returns it.
|
|
12124
12183
|
except Exception:
|
|
12125
12184
|
pass
|
|
12126
12185
|
|
|
@@ -12188,42 +12247,178 @@ def post_wallet_transfer(body: WalletTransferBody):
|
|
|
12188
12247
|
|
|
12189
12248
|
|
|
12190
12249
|
@app.get("/api/x402/search")
|
|
12191
|
-
def x402_search(q: str = ""):
|
|
12250
|
+
def x402_search(q: str = "", network: str = "solana"):
|
|
12192
12251
|
"""Search the x402 marketplace via the ClawPump MCP (``dexter_search``).
|
|
12193
12252
|
|
|
12194
|
-
|
|
12195
|
-
|
|
12253
|
+
Discovery (Dexter + PayAI aggregation) and ``network`` filtering live in the
|
|
12254
|
+
MCP tool — the dashboard just forwards ``query`` + ``network`` and renders.
|
|
12255
|
+
The ClawPump wallet settles x402 in USDC on Solana, so the default is
|
|
12256
|
+
``solana``; pass ``network=all`` to see every chain. Sync def — MCP blocks.
|
|
12196
12257
|
"""
|
|
12197
|
-
from hermes_cli.mcp_config import _call_single_tool
|
|
12198
|
-
|
|
12199
12258
|
query = (q or "").strip()
|
|
12200
12259
|
if not query:
|
|
12201
|
-
return {"ok": True, "query": "", "results": []}
|
|
12202
|
-
|
|
12203
|
-
srv_name, srv_cfg = _clawpump_mcp()
|
|
12204
|
-
if not srv_name:
|
|
12205
|
-
return {
|
|
12206
|
-
"ok": False,
|
|
12207
|
-
"error": "ClawPump MCP is not configured. Run `hermes clawpump setup`.",
|
|
12208
|
-
"results": [],
|
|
12209
|
-
}
|
|
12260
|
+
return {"ok": True, "query": "", "network": network, "results": []}
|
|
12210
12261
|
|
|
12211
12262
|
try:
|
|
12212
|
-
|
|
12263
|
+
data = _clawpump_call("dexter_search", {"query": query, "network": network})
|
|
12213
12264
|
except Exception as exc: # surface any connection / tool error to the UI
|
|
12214
12265
|
return {"ok": False, "error": str(exc), "results": []}
|
|
12215
12266
|
|
|
12216
|
-
data = _parse_mcp_json(text)
|
|
12217
12267
|
if isinstance(data, dict):
|
|
12218
|
-
if isinstance(data.get("error"), str) and data["error"]:
|
|
12219
|
-
return {"ok": False, "error": data["error"], "results": []}
|
|
12220
12268
|
results = data.get("results")
|
|
12221
12269
|
results = results if isinstance(results, list) else []
|
|
12222
12270
|
elif isinstance(data, list):
|
|
12223
12271
|
results = data
|
|
12224
12272
|
else:
|
|
12225
12273
|
results = []
|
|
12226
|
-
return {"ok": True, "query": query, "results": results}
|
|
12274
|
+
return {"ok": True, "query": query, "network": network, "results": results}
|
|
12275
|
+
|
|
12276
|
+
|
|
12277
|
+
# ---------------------------------------------------------------------------
|
|
12278
|
+
# Agent Mail (AgentMail) — agent email inbox via the ClawPump MCP
|
|
12279
|
+
# ---------------------------------------------------------------------------
|
|
12280
|
+
# Thin proxies over the ``agent_mail_*`` MCP tools, mirroring the x402 route
|
|
12281
|
+
# above. The two spending/outward actions (provision an inbox, send an email)
|
|
12282
|
+
# require an explicit ``confirm`` from the UI — the dashboard never auto-spends.
|
|
12283
|
+
|
|
12284
|
+
|
|
12285
|
+
def _agent_args(agent_id: Optional[str]) -> Dict[str, Any]:
|
|
12286
|
+
"""Base args for a ClawPump MCP call. Omit ``agent_id`` entirely when the
|
|
12287
|
+
dashboard hasn't picked one so the MCP resolves the default/only agent."""
|
|
12288
|
+
aid = (agent_id or "").strip()
|
|
12289
|
+
return {"agent_id": aid} if aid else {}
|
|
12290
|
+
|
|
12291
|
+
|
|
12292
|
+
def _clawpump_call_once(tool: str, arguments: Optional[dict] = None, *, timeout: float = 30) -> Any:
|
|
12293
|
+
"""Call a ClawPump MCP tool over a fresh, one-shot connection.
|
|
12294
|
+
|
|
12295
|
+
For rare, irreversible actions (wallet transfers, email sends, paid inbox
|
|
12296
|
+
provisioning): a fresh connect means current auth and — crucially — exactly
|
|
12297
|
+
one attempt, so a dropped response is never silently re-sent. Frequent reads
|
|
12298
|
+
use the warm ``_clawpump_call`` instead.
|
|
12299
|
+
"""
|
|
12300
|
+
from hermes_cli.mcp_config import _call_single_tool
|
|
12301
|
+
|
|
12302
|
+
name, cfg = _clawpump_mcp()
|
|
12303
|
+
if not name:
|
|
12304
|
+
raise RuntimeError("ClawPump MCP is not configured. Run `hermes clawpump setup`.")
|
|
12305
|
+
data = _parse_mcp_json(_call_single_tool(name, cfg, tool, arguments or {}, connect_timeout=timeout))
|
|
12306
|
+
if isinstance(data, dict) and isinstance(data.get("error"), str) and data["error"]:
|
|
12307
|
+
raise RuntimeError(data["error"])
|
|
12308
|
+
return data
|
|
12309
|
+
|
|
12310
|
+
|
|
12311
|
+
class MailCreateBody(BaseModel):
|
|
12312
|
+
agent_id: Optional[str] = None
|
|
12313
|
+
username: Optional[str] = None
|
|
12314
|
+
confirm: bool = False
|
|
12315
|
+
|
|
12316
|
+
|
|
12317
|
+
class MailSendBody(BaseModel):
|
|
12318
|
+
agent_id: Optional[str] = None
|
|
12319
|
+
to: Any = None # str | list[str] — the MCP tool accepts either
|
|
12320
|
+
subject: str
|
|
12321
|
+
text: Optional[str] = None
|
|
12322
|
+
html: Optional[str] = None
|
|
12323
|
+
cc: Optional[List[str]] = None
|
|
12324
|
+
bcc: Optional[List[str]] = None
|
|
12325
|
+
reply_to: Optional[str] = None
|
|
12326
|
+
confirm: bool = False
|
|
12327
|
+
|
|
12328
|
+
|
|
12329
|
+
@app.get("/api/mail/address")
|
|
12330
|
+
def mail_address(agent_id: str = ""):
|
|
12331
|
+
"""The agent's email address + inbox status via ``agent_mail_get_address``."""
|
|
12332
|
+
try:
|
|
12333
|
+
data = _clawpump_call("agent_mail_get_address", _agent_args(agent_id))
|
|
12334
|
+
except Exception as exc:
|
|
12335
|
+
return {"ok": False, "error": str(exc), "has_inbox": False, "inbox": None}
|
|
12336
|
+
inbox = data.get("inbox") if isinstance(data, dict) else None
|
|
12337
|
+
return {"ok": True, "has_inbox": bool(inbox), "inbox": inbox}
|
|
12338
|
+
|
|
12339
|
+
|
|
12340
|
+
@app.get("/api/mail/messages")
|
|
12341
|
+
def mail_messages(agent_id: str = "", direction: str = "", limit: int = 50):
|
|
12342
|
+
"""List the agent's emails (synced inbound + sent) via ``agent_mail_list``."""
|
|
12343
|
+
args = _agent_args(agent_id)
|
|
12344
|
+
if direction in ("inbound", "outbound"):
|
|
12345
|
+
args["direction"] = direction
|
|
12346
|
+
args["limit"] = max(1, min(int(limit or 50), 200))
|
|
12347
|
+
try:
|
|
12348
|
+
data = _clawpump_call("agent_mail_list", args)
|
|
12349
|
+
except Exception as exc:
|
|
12350
|
+
return {"ok": False, "error": str(exc), "messages": []}
|
|
12351
|
+
messages = data.get("messages") if isinstance(data, dict) else None
|
|
12352
|
+
return {"ok": True, "messages": messages if isinstance(messages, list) else []}
|
|
12353
|
+
|
|
12354
|
+
|
|
12355
|
+
@app.get("/api/mail/message")
|
|
12356
|
+
def mail_message(message_id: str, agent_id: str = ""):
|
|
12357
|
+
"""Read one email (full body) via ``agent_mail_read``."""
|
|
12358
|
+
args = _agent_args(agent_id)
|
|
12359
|
+
args["message_id"] = message_id
|
|
12360
|
+
try:
|
|
12361
|
+
data = _clawpump_call("agent_mail_read", args)
|
|
12362
|
+
except Exception as exc:
|
|
12363
|
+
return {"ok": False, "error": str(exc), "message": None}
|
|
12364
|
+
message = data.get("message") if isinstance(data, dict) else None
|
|
12365
|
+
return {"ok": True, "message": message}
|
|
12366
|
+
|
|
12367
|
+
|
|
12368
|
+
@app.post("/api/mail/create")
|
|
12369
|
+
def mail_create(body: MailCreateBody):
|
|
12370
|
+
"""Provision the agent's inbox (~$2 USDC) via ``agent_mail_create``."""
|
|
12371
|
+
if not body.confirm:
|
|
12372
|
+
return {
|
|
12373
|
+
"ok": False,
|
|
12374
|
+
"error": "Provisioning an inbox costs ~$2 USDC from the agent wallet — confirm to proceed.",
|
|
12375
|
+
}
|
|
12376
|
+
args = _agent_args(body.agent_id)
|
|
12377
|
+
args["confirm_provision"] = True
|
|
12378
|
+
if body.username and body.username.strip():
|
|
12379
|
+
args["username"] = body.username.strip()
|
|
12380
|
+
try:
|
|
12381
|
+
data = _clawpump_call_once("agent_mail_create", args)
|
|
12382
|
+
except Exception as exc:
|
|
12383
|
+
return {"ok": False, "error": str(exc)}
|
|
12384
|
+
if not isinstance(data, dict):
|
|
12385
|
+
data = {}
|
|
12386
|
+
return {
|
|
12387
|
+
"ok": True,
|
|
12388
|
+
"inbox": data.get("inbox"),
|
|
12389
|
+
"alreadyExisted": bool(data.get("alreadyExisted")),
|
|
12390
|
+
"note": data.get("note"),
|
|
12391
|
+
}
|
|
12392
|
+
|
|
12393
|
+
|
|
12394
|
+
@app.post("/api/mail/send")
|
|
12395
|
+
def mail_send(body: MailSendBody):
|
|
12396
|
+
"""Send an email from the agent's inbox via ``agent_mail_send`` (x402)."""
|
|
12397
|
+
if not body.confirm:
|
|
12398
|
+
return {"ok": False, "error": "Sending email is outward-facing — confirm to proceed."}
|
|
12399
|
+
if not (body.text and body.text.strip()) and not (body.html and body.html.strip()):
|
|
12400
|
+
return {"ok": False, "error": "Provide a message body."}
|
|
12401
|
+
if not body.to:
|
|
12402
|
+
return {"ok": False, "error": "At least one recipient is required."}
|
|
12403
|
+
args = _agent_args(body.agent_id)
|
|
12404
|
+
args["confirm_send"] = True
|
|
12405
|
+
args["to"] = body.to
|
|
12406
|
+
args["subject"] = body.subject
|
|
12407
|
+
if body.text:
|
|
12408
|
+
args["text"] = body.text
|
|
12409
|
+
if body.html:
|
|
12410
|
+
args["html"] = body.html
|
|
12411
|
+
if body.cc:
|
|
12412
|
+
args["cc"] = body.cc
|
|
12413
|
+
if body.bcc:
|
|
12414
|
+
args["bcc"] = body.bcc
|
|
12415
|
+
if body.reply_to and body.reply_to.strip():
|
|
12416
|
+
args["reply_to"] = body.reply_to.strip()
|
|
12417
|
+
try:
|
|
12418
|
+
data = _clawpump_call_once("agent_mail_send", args)
|
|
12419
|
+
except Exception as exc:
|
|
12420
|
+
return {"ok": False, "error": str(exc)}
|
|
12421
|
+
return {"ok": True, "result": data}
|
|
12227
12422
|
|
|
12228
12423
|
|
|
12229
12424
|
_FONT_DEFAULT_ID = "space-grotesk"
|
|
@@ -144,6 +144,7 @@ Common patterns:
|
|
|
144
144
|
- **Paid x402 API call (agent wallet pays):** `pay_sh_search` → `pay_sh_provider_details` (price) → `pay_sh_prepare_call` → (confirm price with user) → `pay_sh_execute_approved`. The agent needs the `x402` skill (`update_agent` with `enabled_skills`).
|
|
145
145
|
- **Discover x402 APIs on Dexter:** `dexter_search` (free, by capability — e.g. "ETH price", "image generation") returns matching resources with their payable `resourceUrl`, HTTP method, per-call USDC price, and input schema. No separate Dexter wallet.
|
|
146
146
|
- **Pay ANY x402 URL (open, not just a catalog):** `x402_pay_check` (free — fetch the URL's 402 challenge → price + input schema) → quote the price to the user → `x402_pay` (pays from the agent's ClawPump wallet, `confirm_payment: true`, `max_amount_usd` = the cap the user approved). Works on any x402 endpoint, including a `resourceUrl` from `dexter_search`. The wallet signs server-side; spend is hard-capped. Needs the `x402` skill (`update_agent` with `enabled_skills`).
|
|
147
|
+
- **Payment funds:** check with `get_wallet_summaries` (`usdc_balance`).
|
|
147
148
|
- **Pay your Hermes inference from the ClawPump wallet (UsePod):** if the user runs Hermes on their own UsePod pod (`USEPOD_API_KEY` in `~/.hermes/.env`), they can top that pod up straight from their ClawPump wallet instead of funding it manually at usepod.ai. Flow: `get_balance` (confirm the agent wallet holds enough USDC) → ask for the amount + their 16-hex `deposit_code` (from UsePod `/v1/register`) → quote both back → (confirm) `usepod_deposit` with `confirm_deposit: true`. The full amount goes on-chain into that pod (no fee); UsePod then draws inference cost from it. This is **separate** from ClawPump's own hosted chat/credits — it just pays the user's own pod from their wallet.
|
|
148
149
|
|
|
149
150
|
## Setting up Pod (UsePod) — auto flow
|
|
Binary file
|
package/agent/web/src/App.tsx
CHANGED
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
Globe,
|
|
34
34
|
Heart,
|
|
35
35
|
KeyRound,
|
|
36
|
+
Mail,
|
|
36
37
|
Menu,
|
|
37
38
|
MessageSquare,
|
|
38
39
|
Package,
|
|
@@ -76,6 +77,7 @@ import type { SystemAction } from "@/contexts/system-actions-context";
|
|
|
76
77
|
import ConfigPage from "@/pages/ConfigPage";
|
|
77
78
|
import WalletPage from "@/pages/WalletPage";
|
|
78
79
|
import X402Page from "@/pages/X402Page";
|
|
80
|
+
import AgentMailPage from "@/pages/AgentMailPage";
|
|
79
81
|
import DocsPage from "@/pages/DocsPage";
|
|
80
82
|
import EnvPage from "@/pages/EnvPage";
|
|
81
83
|
import FilesPage from "@/pages/FilesPage";
|
|
@@ -137,6 +139,7 @@ const BUILTIN_ROUTES_CORE: Record<string, ComponentType> = {
|
|
|
137
139
|
"/": RootRedirect,
|
|
138
140
|
"/wallet": WalletPage,
|
|
139
141
|
"/x402": X402Page,
|
|
142
|
+
"/mail": AgentMailPage,
|
|
140
143
|
"/sessions": SessionsPage,
|
|
141
144
|
"/files": FilesPage,
|
|
142
145
|
"/analytics": AnalyticsPage,
|
|
@@ -168,6 +171,7 @@ function ChatRouteSink() {
|
|
|
168
171
|
const BUILTIN_NAV_REST: NavItem[] = [
|
|
169
172
|
{ path: "/wallet", label: "Wallet", icon: Wallet },
|
|
170
173
|
{ path: "/x402", label: "x402", icon: Zap },
|
|
174
|
+
{ path: "/mail", label: "Mail", icon: Mail },
|
|
171
175
|
{
|
|
172
176
|
path: "/sessions",
|
|
173
177
|
labelKey: "sessions",
|
|
@@ -522,10 +526,9 @@ export default function App() {
|
|
|
522
526
|
</Button>
|
|
523
527
|
|
|
524
528
|
<img
|
|
525
|
-
src="/claw-logo.
|
|
529
|
+
src="/claw-logo.png"
|
|
526
530
|
alt=""
|
|
527
531
|
className="h-7 w-7 shrink-0"
|
|
528
|
-
style={{ mixBlendMode: "screen" }}
|
|
529
532
|
/>
|
|
530
533
|
<Typography
|
|
531
534
|
className="font-bold text-[0.95rem] leading-[0.95] tracking-[0.05em] text-midground"
|
|
@@ -587,10 +590,9 @@ export default function App() {
|
|
|
587
590
|
<PluginSlot name="header-left" />
|
|
588
591
|
|
|
589
592
|
<img
|
|
590
|
-
src="/claw-logo.
|
|
593
|
+
src="/claw-logo.png"
|
|
591
594
|
alt="Claw Agent"
|
|
592
595
|
className="h-8 w-8 shrink-0"
|
|
593
|
-
style={{ mixBlendMode: "screen" }}
|
|
594
596
|
/>
|
|
595
597
|
<Typography
|
|
596
598
|
className="font-bold text-[1.125rem] leading-[0.95] tracking-[0.0525rem] text-midground uppercase"
|
package/agent/web/src/lib/api.ts
CHANGED
|
@@ -313,6 +313,8 @@ function appendProfileParam(url: string, profile?: string): string {
|
|
|
313
313
|
export interface AgentWalletBalance {
|
|
314
314
|
agent_id: string;
|
|
315
315
|
name?: string | null;
|
|
316
|
+
avatar_url?: string | null;
|
|
317
|
+
token_mint?: string | null;
|
|
316
318
|
wallet_address: string | null;
|
|
317
319
|
sol_balance: number | null;
|
|
318
320
|
usdc_balance: number | null;
|
|
@@ -369,6 +371,86 @@ export interface X402SearchResponse {
|
|
|
369
371
|
results: X402Result[];
|
|
370
372
|
}
|
|
371
373
|
|
|
374
|
+
// ── Agent Mail (AgentMail, via the ClawPump MCP) ───────────────────────
|
|
375
|
+
export interface MailInbox {
|
|
376
|
+
id: string;
|
|
377
|
+
agentId: string;
|
|
378
|
+
provider: string;
|
|
379
|
+
inboxId: string;
|
|
380
|
+
emailAddress: string;
|
|
381
|
+
username: string;
|
|
382
|
+
domain: string;
|
|
383
|
+
webhookId: string | null;
|
|
384
|
+
verified: boolean;
|
|
385
|
+
status: string;
|
|
386
|
+
createdAt: string;
|
|
387
|
+
updatedAt: string;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
export interface MailMessage {
|
|
391
|
+
id: string;
|
|
392
|
+
agentId: string;
|
|
393
|
+
inboxId: string;
|
|
394
|
+
messageId: string;
|
|
395
|
+
threadId: string | null;
|
|
396
|
+
direction: "inbound" | "outbound";
|
|
397
|
+
fromAddress: string | null;
|
|
398
|
+
toAddresses: string[];
|
|
399
|
+
ccAddresses: string[];
|
|
400
|
+
subject: string | null;
|
|
401
|
+
textBody: string | null;
|
|
402
|
+
htmlBody: string | null;
|
|
403
|
+
preview: string | null;
|
|
404
|
+
read: boolean;
|
|
405
|
+
agentmailCreatedAt: string | null;
|
|
406
|
+
createdAt: string;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
export interface MailAddressResponse {
|
|
410
|
+
ok: boolean;
|
|
411
|
+
error?: string;
|
|
412
|
+
has_inbox: boolean;
|
|
413
|
+
inbox: MailInbox | null;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
export interface MailMessagesResponse {
|
|
417
|
+
ok: boolean;
|
|
418
|
+
error?: string;
|
|
419
|
+
messages: MailMessage[];
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
export interface MailMessageResponse {
|
|
423
|
+
ok: boolean;
|
|
424
|
+
error?: string;
|
|
425
|
+
message: MailMessage | null;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
export interface MailCreateResponse {
|
|
429
|
+
ok: boolean;
|
|
430
|
+
error?: string;
|
|
431
|
+
inbox?: MailInbox | null;
|
|
432
|
+
alreadyExisted?: boolean;
|
|
433
|
+
note?: string | null;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
export interface MailSendBody {
|
|
437
|
+
agent_id: string;
|
|
438
|
+
to: string[];
|
|
439
|
+
subject: string;
|
|
440
|
+
text?: string;
|
|
441
|
+
html?: string;
|
|
442
|
+
cc?: string[];
|
|
443
|
+
bcc?: string[];
|
|
444
|
+
reply_to?: string;
|
|
445
|
+
confirm: boolean;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
export interface MailSendResponse {
|
|
449
|
+
ok: boolean;
|
|
450
|
+
error?: string;
|
|
451
|
+
result?: unknown;
|
|
452
|
+
}
|
|
453
|
+
|
|
372
454
|
export const api = {
|
|
373
455
|
getStatus: () => fetchJSON<StatusResponse>("/api/status"),
|
|
374
456
|
getWalletBalances: () =>
|
|
@@ -381,6 +463,36 @@ export const api = {
|
|
|
381
463
|
headers: { "Content-Type": "application/json" },
|
|
382
464
|
body: JSON.stringify(body),
|
|
383
465
|
}),
|
|
466
|
+
|
|
467
|
+
// ── Agent Mail (AgentMail) ─────────────────────────────────────────
|
|
468
|
+
// Every call carries an explicit agent_id — the MCP requires it once the
|
|
469
|
+
// account has more than one agent.
|
|
470
|
+
getMailAddress: (agentId: string) =>
|
|
471
|
+
fetchJSON<MailAddressResponse>(
|
|
472
|
+
`/api/mail/address?agent_id=${encodeURIComponent(agentId)}`,
|
|
473
|
+
),
|
|
474
|
+
listMail: (opts: { agentId: string; direction?: "inbound" | "outbound"; limit?: number }) => {
|
|
475
|
+
const qs = new URLSearchParams({ agent_id: opts.agentId });
|
|
476
|
+
if (opts.direction) qs.set("direction", opts.direction);
|
|
477
|
+
if (opts.limit) qs.set("limit", String(opts.limit));
|
|
478
|
+
return fetchJSON<MailMessagesResponse>(`/api/mail/messages?${qs.toString()}`);
|
|
479
|
+
},
|
|
480
|
+
readMail: (messageId: string, agentId: string) =>
|
|
481
|
+
fetchJSON<MailMessageResponse>(
|
|
482
|
+
`/api/mail/message?message_id=${encodeURIComponent(messageId)}&agent_id=${encodeURIComponent(agentId)}`,
|
|
483
|
+
),
|
|
484
|
+
createInbox: (body: { agent_id: string; username?: string; confirm: boolean }) =>
|
|
485
|
+
fetchJSON<MailCreateResponse>("/api/mail/create", {
|
|
486
|
+
method: "POST",
|
|
487
|
+
headers: { "Content-Type": "application/json" },
|
|
488
|
+
body: JSON.stringify(body),
|
|
489
|
+
}),
|
|
490
|
+
sendMail: (body: MailSendBody) =>
|
|
491
|
+
fetchJSON<MailSendResponse>("/api/mail/send", {
|
|
492
|
+
method: "POST",
|
|
493
|
+
headers: { "Content-Type": "application/json" },
|
|
494
|
+
body: JSON.stringify(body),
|
|
495
|
+
}),
|
|
384
496
|
/**
|
|
385
497
|
* Identity probe for the dashboard auth gate (Phase 7).
|
|
386
498
|
*
|
|
@@ -0,0 +1,684 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
ArrowLeft,
|
|
4
|
+
Check,
|
|
5
|
+
Copy,
|
|
6
|
+
Inbox,
|
|
7
|
+
Mail,
|
|
8
|
+
PenSquare,
|
|
9
|
+
RefreshCw,
|
|
10
|
+
Send,
|
|
11
|
+
ShieldCheck,
|
|
12
|
+
} from "lucide-react";
|
|
13
|
+
import { api } from "@/lib/api";
|
|
14
|
+
import type { AgentWalletBalance, MailInbox, MailMessage } from "@/lib/api";
|
|
15
|
+
import { Button } from "@nous-research/ui/ui/components/button";
|
|
16
|
+
import { Badge } from "@nous-research/ui/ui/components/badge";
|
|
17
|
+
import { Spinner } from "@nous-research/ui/ui/components/spinner";
|
|
18
|
+
import {
|
|
19
|
+
Card,
|
|
20
|
+
CardContent,
|
|
21
|
+
CardHeader,
|
|
22
|
+
CardTitle,
|
|
23
|
+
} from "@nous-research/ui/ui/components/card";
|
|
24
|
+
|
|
25
|
+
type View = "list" | "read" | "compose";
|
|
26
|
+
type Filter = "all" | "inbound" | "outbound";
|
|
27
|
+
|
|
28
|
+
const EMAIL_RE = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;
|
|
29
|
+
|
|
30
|
+
function parseRecipients(raw: string): string[] {
|
|
31
|
+
return raw
|
|
32
|
+
.split(/[\s,;]+/)
|
|
33
|
+
.map((s) => s.trim())
|
|
34
|
+
.filter(Boolean);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function formatDate(iso: string | null): string {
|
|
38
|
+
if (!iso) return "";
|
|
39
|
+
const d = new Date(iso);
|
|
40
|
+
if (Number.isNaN(d.getTime())) return "";
|
|
41
|
+
return d.toLocaleString(undefined, {
|
|
42
|
+
month: "short",
|
|
43
|
+
day: "numeric",
|
|
44
|
+
hour: "2-digit",
|
|
45
|
+
minute: "2-digit",
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const inputCls =
|
|
50
|
+
"w-full rounded-md border border-border bg-background px-3 py-2 text-sm outline-none focus:border-primary";
|
|
51
|
+
|
|
52
|
+
function CopyButton({ value }: { value: string }) {
|
|
53
|
+
const [copied, setCopied] = useState(false);
|
|
54
|
+
const onCopy = useCallback(() => {
|
|
55
|
+
navigator.clipboard
|
|
56
|
+
.writeText(value)
|
|
57
|
+
.then(() => {
|
|
58
|
+
setCopied(true);
|
|
59
|
+
setTimeout(() => setCopied(false), 1500);
|
|
60
|
+
})
|
|
61
|
+
.catch(() => {});
|
|
62
|
+
}, [value]);
|
|
63
|
+
return (
|
|
64
|
+
<button
|
|
65
|
+
type="button"
|
|
66
|
+
onClick={onCopy}
|
|
67
|
+
title="Copy email address"
|
|
68
|
+
aria-label="Copy email address"
|
|
69
|
+
className="inline-flex h-7 w-7 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
|
70
|
+
>
|
|
71
|
+
{copied ? <Check className="h-4 w-4 text-green-500" /> : <Copy className="h-4 w-4" />}
|
|
72
|
+
</button>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export default function AgentMailPage() {
|
|
77
|
+
// ── Agent selection (the MCP requires an explicit agent_id) ────────
|
|
78
|
+
const [agents, setAgents] = useState<AgentWalletBalance[]>([]);
|
|
79
|
+
const [agentId, setAgentId] = useState("");
|
|
80
|
+
|
|
81
|
+
// ── Inbox state ────────────────────────────────────────────────────
|
|
82
|
+
const [inbox, setInbox] = useState<MailInbox | null>(null);
|
|
83
|
+
const [hasInbox, setHasInbox] = useState(false);
|
|
84
|
+
const [inboxLoading, setInboxLoading] = useState(true);
|
|
85
|
+
const [inboxError, setInboxError] = useState<string | null>(null);
|
|
86
|
+
|
|
87
|
+
// ── Provisioning ───────────────────────────────────────────────────
|
|
88
|
+
const [username, setUsername] = useState("");
|
|
89
|
+
const [creating, setCreating] = useState(false);
|
|
90
|
+
const [createArmed, setCreateArmed] = useState(false);
|
|
91
|
+
const [createError, setCreateError] = useState<string | null>(null);
|
|
92
|
+
|
|
93
|
+
// ── Messages ───────────────────────────────────────────────────────
|
|
94
|
+
const [view, setView] = useState<View>("list");
|
|
95
|
+
const [filter, setFilter] = useState<Filter>("all");
|
|
96
|
+
const [messages, setMessages] = useState<MailMessage[]>([]);
|
|
97
|
+
const [messagesLoading, setMessagesLoading] = useState(false);
|
|
98
|
+
const [messagesError, setMessagesError] = useState<string | null>(null);
|
|
99
|
+
const [selected, setSelected] = useState<MailMessage | null>(null);
|
|
100
|
+
const [selectedLoading, setSelectedLoading] = useState(false);
|
|
101
|
+
|
|
102
|
+
// ── Compose ────────────────────────────────────────────────────────
|
|
103
|
+
const [to, setTo] = useState("");
|
|
104
|
+
const [cc, setCc] = useState("");
|
|
105
|
+
const [bcc, setBcc] = useState("");
|
|
106
|
+
const [subject, setSubject] = useState("");
|
|
107
|
+
const [bodyText, setBodyText] = useState("");
|
|
108
|
+
const [replyTo, setReplyTo] = useState("");
|
|
109
|
+
const [sending, setSending] = useState(false);
|
|
110
|
+
const [sendArmed, setSendArmed] = useState(false);
|
|
111
|
+
const [sendError, setSendError] = useState<string | null>(null);
|
|
112
|
+
|
|
113
|
+
const loadInbox = useCallback(() => {
|
|
114
|
+
if (!agentId) return;
|
|
115
|
+
setInboxLoading(true);
|
|
116
|
+
setInboxError(null);
|
|
117
|
+
api
|
|
118
|
+
.getMailAddress(agentId)
|
|
119
|
+
.then((resp) => {
|
|
120
|
+
if (resp.ok) {
|
|
121
|
+
setInbox(resp.inbox);
|
|
122
|
+
setHasInbox(resp.has_inbox);
|
|
123
|
+
} else {
|
|
124
|
+
setInbox(null);
|
|
125
|
+
setHasInbox(false);
|
|
126
|
+
setInboxError(resp.error ?? "Could not load inbox");
|
|
127
|
+
}
|
|
128
|
+
})
|
|
129
|
+
.catch((e) => setInboxError(e instanceof Error ? e.message : String(e)))
|
|
130
|
+
.finally(() => setInboxLoading(false));
|
|
131
|
+
}, [agentId]);
|
|
132
|
+
|
|
133
|
+
const loadMessages = useCallback(() => {
|
|
134
|
+
if (!agentId) return;
|
|
135
|
+
setMessagesLoading(true);
|
|
136
|
+
setMessagesError(null);
|
|
137
|
+
api
|
|
138
|
+
.listMail({ agentId, direction: filter === "all" ? undefined : filter, limit: 100 })
|
|
139
|
+
.then((resp) => {
|
|
140
|
+
if (resp.ok) {
|
|
141
|
+
setMessages(resp.messages ?? []);
|
|
142
|
+
} else {
|
|
143
|
+
setMessagesError(resp.error ?? "Could not load messages");
|
|
144
|
+
setMessages([]);
|
|
145
|
+
}
|
|
146
|
+
})
|
|
147
|
+
.catch((e) => setMessagesError(e instanceof Error ? e.message : String(e)))
|
|
148
|
+
.finally(() => setMessagesLoading(false));
|
|
149
|
+
}, [agentId, filter]);
|
|
150
|
+
|
|
151
|
+
// Load the agent list once; default to the first agent.
|
|
152
|
+
useEffect(() => {
|
|
153
|
+
api
|
|
154
|
+
.getWalletBalances()
|
|
155
|
+
.then((r) => {
|
|
156
|
+
if (r.ok && r.wallets.length) {
|
|
157
|
+
setAgents(r.wallets);
|
|
158
|
+
setAgentId((cur) => cur || r.wallets[0].agent_id);
|
|
159
|
+
}
|
|
160
|
+
})
|
|
161
|
+
.catch(() => {});
|
|
162
|
+
}, []);
|
|
163
|
+
|
|
164
|
+
useEffect(() => {
|
|
165
|
+
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
166
|
+
loadInbox();
|
|
167
|
+
}, [loadInbox]);
|
|
168
|
+
|
|
169
|
+
useEffect(() => {
|
|
170
|
+
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
171
|
+
if (hasInbox) loadMessages();
|
|
172
|
+
}, [hasInbox, loadMessages]);
|
|
173
|
+
|
|
174
|
+
const createInbox = useCallback(() => {
|
|
175
|
+
setCreating(true);
|
|
176
|
+
setCreateError(null);
|
|
177
|
+
api
|
|
178
|
+
.createInbox({ agent_id: agentId, username: username.trim() || undefined, confirm: true })
|
|
179
|
+
.then((resp) => {
|
|
180
|
+
if (resp.ok) {
|
|
181
|
+
setCreateArmed(false);
|
|
182
|
+
if (resp.inbox) {
|
|
183
|
+
setInbox(resp.inbox);
|
|
184
|
+
setHasInbox(true);
|
|
185
|
+
} else {
|
|
186
|
+
loadInbox();
|
|
187
|
+
}
|
|
188
|
+
} else {
|
|
189
|
+
setCreateError(resp.error ?? "Could not create inbox");
|
|
190
|
+
}
|
|
191
|
+
})
|
|
192
|
+
.catch((e) => setCreateError(e instanceof Error ? e.message : String(e)))
|
|
193
|
+
.finally(() => setCreating(false));
|
|
194
|
+
}, [agentId, username, loadInbox]);
|
|
195
|
+
|
|
196
|
+
const openMessage = useCallback(
|
|
197
|
+
(m: MailMessage) => {
|
|
198
|
+
setSelected(m);
|
|
199
|
+
setView("read");
|
|
200
|
+
setSelectedLoading(true);
|
|
201
|
+
api
|
|
202
|
+
.readMail(m.messageId, agentId)
|
|
203
|
+
.then((resp) => {
|
|
204
|
+
if (resp.ok && resp.message) setSelected(resp.message);
|
|
205
|
+
})
|
|
206
|
+
.catch(() => {})
|
|
207
|
+
.finally(() => setSelectedLoading(false));
|
|
208
|
+
},
|
|
209
|
+
[agentId],
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
const recipients = useMemo(() => parseRecipients(to), [to]);
|
|
213
|
+
const selectedWallet = useMemo(
|
|
214
|
+
() => agents.find((a) => a.agent_id === agentId) ?? null,
|
|
215
|
+
[agents, agentId],
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
const startCompose = useCallback(() => {
|
|
219
|
+
setSendError(null);
|
|
220
|
+
setSendArmed(false);
|
|
221
|
+
setView("compose");
|
|
222
|
+
}, []);
|
|
223
|
+
|
|
224
|
+
const sendMail = useCallback(() => {
|
|
225
|
+
const toList = parseRecipients(to);
|
|
226
|
+
const ccList = parseRecipients(cc);
|
|
227
|
+
const bccList = parseRecipients(bcc);
|
|
228
|
+
const bad = [...toList, ...ccList, ...bccList].find((a) => !EMAIL_RE.test(a));
|
|
229
|
+
if (toList.length === 0) {
|
|
230
|
+
setSendError("Add at least one recipient.");
|
|
231
|
+
setSendArmed(false);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (bad) {
|
|
235
|
+
setSendError(`Not a valid email: ${bad}`);
|
|
236
|
+
setSendArmed(false);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (!subject.trim()) {
|
|
240
|
+
setSendError("Add a subject.");
|
|
241
|
+
setSendArmed(false);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
if (!bodyText.trim()) {
|
|
245
|
+
setSendError("Write a message.");
|
|
246
|
+
setSendArmed(false);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
setSending(true);
|
|
250
|
+
setSendError(null);
|
|
251
|
+
api
|
|
252
|
+
.sendMail({
|
|
253
|
+
agent_id: agentId,
|
|
254
|
+
to: toList,
|
|
255
|
+
cc: ccList.length ? ccList : undefined,
|
|
256
|
+
bcc: bccList.length ? bccList : undefined,
|
|
257
|
+
subject: subject.trim(),
|
|
258
|
+
text: bodyText,
|
|
259
|
+
reply_to: replyTo.trim() || undefined,
|
|
260
|
+
confirm: true,
|
|
261
|
+
})
|
|
262
|
+
.then((resp) => {
|
|
263
|
+
if (resp.ok) {
|
|
264
|
+
setTo("");
|
|
265
|
+
setCc("");
|
|
266
|
+
setBcc("");
|
|
267
|
+
setSubject("");
|
|
268
|
+
setBodyText("");
|
|
269
|
+
setReplyTo("");
|
|
270
|
+
setSendArmed(false);
|
|
271
|
+
setView("list");
|
|
272
|
+
loadMessages();
|
|
273
|
+
} else {
|
|
274
|
+
setSendError(resp.error ?? "Send failed");
|
|
275
|
+
setSendArmed(false);
|
|
276
|
+
}
|
|
277
|
+
})
|
|
278
|
+
.catch((e) => {
|
|
279
|
+
setSendError(e instanceof Error ? e.message : String(e));
|
|
280
|
+
setSendArmed(false);
|
|
281
|
+
})
|
|
282
|
+
.finally(() => setSending(false));
|
|
283
|
+
}, [agentId, to, cc, bcc, subject, bodyText, replyTo, loadMessages]);
|
|
284
|
+
|
|
285
|
+
// ── Render ─────────────────────────────────────────────────────────
|
|
286
|
+
return (
|
|
287
|
+
<div className="mx-auto max-w-3xl space-y-4 p-4">
|
|
288
|
+
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
289
|
+
<div className="flex items-center gap-2">
|
|
290
|
+
<Mail className="h-5 w-5 text-muted-foreground" />
|
|
291
|
+
<h1 className="text-lg font-semibold">Agent Mail</h1>
|
|
292
|
+
{agents.length > 0 && (
|
|
293
|
+
<select
|
|
294
|
+
value={agentId}
|
|
295
|
+
onChange={(e) => {
|
|
296
|
+
setAgentId(e.target.value);
|
|
297
|
+
setView("list");
|
|
298
|
+
setSelected(null);
|
|
299
|
+
}}
|
|
300
|
+
title="Select agent"
|
|
301
|
+
className="ml-1 max-w-[180px] rounded-md border border-border bg-background px-2 py-1 text-sm outline-none focus:border-primary"
|
|
302
|
+
>
|
|
303
|
+
{agents.map((a) => (
|
|
304
|
+
<option key={a.agent_id} value={a.agent_id}>
|
|
305
|
+
{a.name || a.agent_id.slice(0, 8)}
|
|
306
|
+
</option>
|
|
307
|
+
))}
|
|
308
|
+
</select>
|
|
309
|
+
)}
|
|
310
|
+
</div>
|
|
311
|
+
{hasInbox && view === "list" && (
|
|
312
|
+
<div className="flex items-center gap-2">
|
|
313
|
+
<button
|
|
314
|
+
type="button"
|
|
315
|
+
onClick={loadMessages}
|
|
316
|
+
title="Refresh"
|
|
317
|
+
aria-label="Refresh"
|
|
318
|
+
className="inline-flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
|
319
|
+
>
|
|
320
|
+
<RefreshCw className={`h-4 w-4 ${messagesLoading ? "animate-spin" : ""}`} />
|
|
321
|
+
</button>
|
|
322
|
+
<Button size="sm" prefix={<PenSquare className="h-4 w-4" />} onClick={startCompose}>
|
|
323
|
+
Compose
|
|
324
|
+
</Button>
|
|
325
|
+
</div>
|
|
326
|
+
)}
|
|
327
|
+
</div>
|
|
328
|
+
|
|
329
|
+
{/* Inbox address bar */}
|
|
330
|
+
{hasInbox && inbox && (
|
|
331
|
+
<Card>
|
|
332
|
+
<CardContent className="flex flex-wrap items-center justify-between gap-2 py-3">
|
|
333
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
334
|
+
<Inbox className="h-4 w-4 shrink-0 text-muted-foreground" />
|
|
335
|
+
<span className="truncate font-mono text-sm text-emerald-300">
|
|
336
|
+
{inbox.emailAddress}
|
|
337
|
+
</span>
|
|
338
|
+
<CopyButton value={inbox.emailAddress} />
|
|
339
|
+
</div>
|
|
340
|
+
<div className="flex items-center gap-1.5">
|
|
341
|
+
{inbox.verified && (
|
|
342
|
+
<Badge tone="success" className="shrink-0">
|
|
343
|
+
<ShieldCheck className="mr-1 h-3 w-3" /> verified
|
|
344
|
+
</Badge>
|
|
345
|
+
)}
|
|
346
|
+
{inbox.status && inbox.status !== "active" && (
|
|
347
|
+
<Badge tone="secondary">{inbox.status}</Badge>
|
|
348
|
+
)}
|
|
349
|
+
</div>
|
|
350
|
+
</CardContent>
|
|
351
|
+
</Card>
|
|
352
|
+
)}
|
|
353
|
+
|
|
354
|
+
{inboxError && (
|
|
355
|
+
<Card className="border-destructive/40">
|
|
356
|
+
<CardContent className="py-3 text-sm text-destructive">{inboxError}</CardContent>
|
|
357
|
+
</Card>
|
|
358
|
+
)}
|
|
359
|
+
|
|
360
|
+
{inboxLoading ? (
|
|
361
|
+
<div className="flex justify-center py-12">
|
|
362
|
+
<Spinner />
|
|
363
|
+
</div>
|
|
364
|
+
) : !hasInbox ? (
|
|
365
|
+
/* ── Provision an inbox ─────────────────────────────────────── */
|
|
366
|
+
<Card>
|
|
367
|
+
<CardHeader className="pb-2">
|
|
368
|
+
<CardTitle className="text-sm font-semibold">No inbox yet</CardTitle>
|
|
369
|
+
</CardHeader>
|
|
370
|
+
<CardContent className="space-y-3">
|
|
371
|
+
<p className="text-sm text-muted-foreground">
|
|
372
|
+
Give this agent a real email address (e.g.{" "}
|
|
373
|
+
<span className="font-mono text-foreground">name@agentmail.to</span>) so it can
|
|
374
|
+
send and receive mail. Provisioning is a one-time{" "}
|
|
375
|
+
<span className="font-semibold text-foreground">~$2 USDC</span> payment from the
|
|
376
|
+
agent's own wallet over x402.
|
|
377
|
+
</p>
|
|
378
|
+
{selectedWallet && (
|
|
379
|
+
<div className="flex items-center justify-between rounded-md border border-border bg-background px-3 py-2">
|
|
380
|
+
<span className="text-sm text-muted-foreground">This agent's USDC balance</span>
|
|
381
|
+
<span
|
|
382
|
+
className={`font-mono text-sm font-semibold ${
|
|
383
|
+
(selectedWallet.usdc_balance ?? 0) >= 2 ? "text-emerald-300" : "text-amber-300"
|
|
384
|
+
}`}
|
|
385
|
+
>
|
|
386
|
+
{selectedWallet.usdc_balance != null
|
|
387
|
+
? `$${selectedWallet.usdc_balance.toFixed(2)}`
|
|
388
|
+
: "—"}
|
|
389
|
+
</span>
|
|
390
|
+
</div>
|
|
391
|
+
)}
|
|
392
|
+
{selectedWallet && (selectedWallet.usdc_balance ?? 0) < 2 && (
|
|
393
|
+
<p className="text-xs text-amber-300">
|
|
394
|
+
Not enough USDC for the ~$2 fee — add USDC to this agent's wallet (or swap
|
|
395
|
+
SOL → USDC) before creating the inbox.
|
|
396
|
+
</p>
|
|
397
|
+
)}
|
|
398
|
+
<div className="flex flex-col gap-2 sm:flex-row">
|
|
399
|
+
<input
|
|
400
|
+
value={username}
|
|
401
|
+
onChange={(e) => setUsername(e.target.value)}
|
|
402
|
+
placeholder="optional username (a-z, 0-9, dot, dash) — omit to auto-generate"
|
|
403
|
+
className={inputCls + " sm:flex-1"}
|
|
404
|
+
/>
|
|
405
|
+
</div>
|
|
406
|
+
{createError && <p className="text-sm text-destructive">{createError}</p>}
|
|
407
|
+
{createArmed ? (
|
|
408
|
+
<div className="flex flex-wrap items-center gap-2 rounded-md border border-amber-500/40 bg-amber-500/5 p-3">
|
|
409
|
+
<span className="text-sm text-amber-200">
|
|
410
|
+
This pays <span className="font-semibold">~$2 USDC</span> from the agent wallet.
|
|
411
|
+
Continue?
|
|
412
|
+
</span>
|
|
413
|
+
<div className="ml-auto flex gap-2">
|
|
414
|
+
<button
|
|
415
|
+
type="button"
|
|
416
|
+
onClick={() => setCreateArmed(false)}
|
|
417
|
+
className="rounded-md px-3 py-1.5 text-sm text-muted-foreground hover:bg-muted hover:text-foreground"
|
|
418
|
+
>
|
|
419
|
+
Cancel
|
|
420
|
+
</button>
|
|
421
|
+
<Button size="sm" onClick={createInbox} disabled={creating}>
|
|
422
|
+
{creating ? "Creating…" : "Confirm & pay"}
|
|
423
|
+
</Button>
|
|
424
|
+
</div>
|
|
425
|
+
</div>
|
|
426
|
+
) : (
|
|
427
|
+
<Button onClick={() => setCreateArmed(true)} disabled={creating}>
|
|
428
|
+
Create inbox
|
|
429
|
+
</Button>
|
|
430
|
+
)}
|
|
431
|
+
</CardContent>
|
|
432
|
+
</Card>
|
|
433
|
+
) : view === "compose" ? (
|
|
434
|
+
/* ── Compose ────────────────────────────────────────────────── */
|
|
435
|
+
<Card>
|
|
436
|
+
<CardHeader className="flex flex-row items-center justify-between gap-2 pb-2">
|
|
437
|
+
<CardTitle className="text-sm font-semibold">New email</CardTitle>
|
|
438
|
+
<button
|
|
439
|
+
type="button"
|
|
440
|
+
onClick={() => setView("list")}
|
|
441
|
+
className="inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground"
|
|
442
|
+
>
|
|
443
|
+
<ArrowLeft className="h-4 w-4" /> Back
|
|
444
|
+
</button>
|
|
445
|
+
</CardHeader>
|
|
446
|
+
<CardContent className="space-y-3">
|
|
447
|
+
<div className="space-y-1">
|
|
448
|
+
<label className="text-xs text-muted-foreground">To</label>
|
|
449
|
+
<input
|
|
450
|
+
value={to}
|
|
451
|
+
onChange={(e) => setTo(e.target.value)}
|
|
452
|
+
placeholder="alice@example.com, bob@example.com"
|
|
453
|
+
className={inputCls}
|
|
454
|
+
/>
|
|
455
|
+
</div>
|
|
456
|
+
<div className="grid gap-3 sm:grid-cols-2">
|
|
457
|
+
<div className="space-y-1">
|
|
458
|
+
<label className="text-xs text-muted-foreground">Cc (optional)</label>
|
|
459
|
+
<input value={cc} onChange={(e) => setCc(e.target.value)} className={inputCls} />
|
|
460
|
+
</div>
|
|
461
|
+
<div className="space-y-1">
|
|
462
|
+
<label className="text-xs text-muted-foreground">Bcc (optional)</label>
|
|
463
|
+
<input value={bcc} onChange={(e) => setBcc(e.target.value)} className={inputCls} />
|
|
464
|
+
</div>
|
|
465
|
+
</div>
|
|
466
|
+
<div className="space-y-1">
|
|
467
|
+
<label className="text-xs text-muted-foreground">Subject</label>
|
|
468
|
+
<input
|
|
469
|
+
value={subject}
|
|
470
|
+
onChange={(e) => setSubject(e.target.value)}
|
|
471
|
+
className={inputCls}
|
|
472
|
+
/>
|
|
473
|
+
</div>
|
|
474
|
+
<div className="space-y-1">
|
|
475
|
+
<label className="text-xs text-muted-foreground">Message</label>
|
|
476
|
+
<textarea
|
|
477
|
+
value={bodyText}
|
|
478
|
+
onChange={(e) => setBodyText(e.target.value)}
|
|
479
|
+
rows={10}
|
|
480
|
+
className={inputCls + " resize-y font-sans"}
|
|
481
|
+
/>
|
|
482
|
+
</div>
|
|
483
|
+
<div className="space-y-1">
|
|
484
|
+
<label className="text-xs text-muted-foreground">Reply-To (optional)</label>
|
|
485
|
+
<input
|
|
486
|
+
value={replyTo}
|
|
487
|
+
onChange={(e) => setReplyTo(e.target.value)}
|
|
488
|
+
className={inputCls}
|
|
489
|
+
/>
|
|
490
|
+
</div>
|
|
491
|
+
|
|
492
|
+
{sendError && <p className="text-sm text-destructive">{sendError}</p>}
|
|
493
|
+
|
|
494
|
+
{sendArmed ? (
|
|
495
|
+
<div className="flex flex-wrap items-center gap-2 rounded-md border border-amber-500/40 bg-amber-500/5 p-3">
|
|
496
|
+
<span className="text-sm text-amber-200">
|
|
497
|
+
Send a real email from{" "}
|
|
498
|
+
<span className="font-mono">{inbox?.emailAddress}</span> to{" "}
|
|
499
|
+
{recipients.length} recipient{recipients.length === 1 ? "" : "s"}? Any per-send
|
|
500
|
+
fee is paid in USDC from the agent wallet.
|
|
501
|
+
</span>
|
|
502
|
+
<div className="ml-auto flex gap-2">
|
|
503
|
+
<button
|
|
504
|
+
type="button"
|
|
505
|
+
onClick={() => setSendArmed(false)}
|
|
506
|
+
className="rounded-md px-3 py-1.5 text-sm text-muted-foreground hover:bg-muted hover:text-foreground"
|
|
507
|
+
>
|
|
508
|
+
Cancel
|
|
509
|
+
</button>
|
|
510
|
+
<Button
|
|
511
|
+
size="sm"
|
|
512
|
+
prefix={<Send className="h-4 w-4" />}
|
|
513
|
+
onClick={sendMail}
|
|
514
|
+
disabled={sending}
|
|
515
|
+
>
|
|
516
|
+
{sending ? "Sending…" : "Send now"}
|
|
517
|
+
</Button>
|
|
518
|
+
</div>
|
|
519
|
+
</div>
|
|
520
|
+
) : (
|
|
521
|
+
<Button
|
|
522
|
+
prefix={<Send className="h-4 w-4" />}
|
|
523
|
+
onClick={() => {
|
|
524
|
+
setSendError(null);
|
|
525
|
+
setSendArmed(true);
|
|
526
|
+
}}
|
|
527
|
+
disabled={sending}
|
|
528
|
+
>
|
|
529
|
+
Send
|
|
530
|
+
</Button>
|
|
531
|
+
)}
|
|
532
|
+
</CardContent>
|
|
533
|
+
</Card>
|
|
534
|
+
) : view === "read" && selected ? (
|
|
535
|
+
/* ── Read one message ───────────────────────────────────────── */
|
|
536
|
+
<Card>
|
|
537
|
+
<CardHeader className="space-y-2 pb-2">
|
|
538
|
+
<button
|
|
539
|
+
type="button"
|
|
540
|
+
onClick={() => setView("list")}
|
|
541
|
+
className="inline-flex w-fit items-center gap-1 text-sm text-muted-foreground hover:text-foreground"
|
|
542
|
+
>
|
|
543
|
+
<ArrowLeft className="h-4 w-4" /> Back to inbox
|
|
544
|
+
</button>
|
|
545
|
+
<CardTitle className="text-base font-semibold">
|
|
546
|
+
{selected.subject || "(no subject)"}
|
|
547
|
+
</CardTitle>
|
|
548
|
+
<div className="space-y-0.5 text-xs text-muted-foreground">
|
|
549
|
+
<div>
|
|
550
|
+
<Badge
|
|
551
|
+
tone={selected.direction === "inbound" ? "secondary" : "success"}
|
|
552
|
+
className="mr-2"
|
|
553
|
+
>
|
|
554
|
+
{selected.direction === "inbound" ? "received" : "sent"}
|
|
555
|
+
</Badge>
|
|
556
|
+
{formatDate(selected.agentmailCreatedAt || selected.createdAt)}
|
|
557
|
+
</div>
|
|
558
|
+
{selected.fromAddress && (
|
|
559
|
+
<div>
|
|
560
|
+
<span className="text-foreground/70">From:</span> {selected.fromAddress}
|
|
561
|
+
</div>
|
|
562
|
+
)}
|
|
563
|
+
{selected.toAddresses?.length > 0 && (
|
|
564
|
+
<div>
|
|
565
|
+
<span className="text-foreground/70">To:</span> {selected.toAddresses.join(", ")}
|
|
566
|
+
</div>
|
|
567
|
+
)}
|
|
568
|
+
{selected.ccAddresses?.length > 0 && (
|
|
569
|
+
<div>
|
|
570
|
+
<span className="text-foreground/70">Cc:</span> {selected.ccAddresses.join(", ")}
|
|
571
|
+
</div>
|
|
572
|
+
)}
|
|
573
|
+
</div>
|
|
574
|
+
</CardHeader>
|
|
575
|
+
<CardContent>
|
|
576
|
+
{selectedLoading ? (
|
|
577
|
+
<div className="flex justify-center py-8">
|
|
578
|
+
<Spinner />
|
|
579
|
+
</div>
|
|
580
|
+
) : selected.textBody ? (
|
|
581
|
+
<pre className="whitespace-pre-wrap break-words font-sans text-sm text-foreground/90">
|
|
582
|
+
{selected.textBody}
|
|
583
|
+
</pre>
|
|
584
|
+
) : selected.htmlBody ? (
|
|
585
|
+
<iframe
|
|
586
|
+
title="email body"
|
|
587
|
+
sandbox=""
|
|
588
|
+
srcDoc={selected.htmlBody}
|
|
589
|
+
className="h-[60vh] w-full rounded-md border border-border bg-white"
|
|
590
|
+
/>
|
|
591
|
+
) : (
|
|
592
|
+
<p className="text-sm text-muted-foreground">{selected.preview || "(empty message)"}</p>
|
|
593
|
+
)}
|
|
594
|
+
</CardContent>
|
|
595
|
+
</Card>
|
|
596
|
+
) : (
|
|
597
|
+
/* ── Inbox list ─────────────────────────────────────────────── */
|
|
598
|
+
<div className="space-y-3">
|
|
599
|
+
<div className="flex gap-1">
|
|
600
|
+
{(["all", "inbound", "outbound"] as Filter[]).map((f) => (
|
|
601
|
+
<button
|
|
602
|
+
key={f}
|
|
603
|
+
type="button"
|
|
604
|
+
onClick={() => setFilter(f)}
|
|
605
|
+
className={`rounded-md px-3 py-1.5 text-sm transition-colors ${
|
|
606
|
+
filter === f
|
|
607
|
+
? "bg-muted font-medium text-foreground"
|
|
608
|
+
: "text-muted-foreground hover:bg-muted/50 hover:text-foreground"
|
|
609
|
+
}`}
|
|
610
|
+
>
|
|
611
|
+
{f === "all" ? "All" : f === "inbound" ? "Inbox" : "Sent"}
|
|
612
|
+
</button>
|
|
613
|
+
))}
|
|
614
|
+
</div>
|
|
615
|
+
|
|
616
|
+
{messagesError && (
|
|
617
|
+
<Card className="border-destructive/40">
|
|
618
|
+
<CardContent className="py-3 text-sm text-destructive">{messagesError}</CardContent>
|
|
619
|
+
</Card>
|
|
620
|
+
)}
|
|
621
|
+
|
|
622
|
+
{messagesLoading ? (
|
|
623
|
+
<div className="flex justify-center py-12">
|
|
624
|
+
<Spinner />
|
|
625
|
+
</div>
|
|
626
|
+
) : messages.length === 0 ? (
|
|
627
|
+
<Card>
|
|
628
|
+
<CardContent className="py-8 text-center text-sm text-muted-foreground">
|
|
629
|
+
No messages yet.
|
|
630
|
+
</CardContent>
|
|
631
|
+
</Card>
|
|
632
|
+
) : (
|
|
633
|
+
<div className="space-y-2">
|
|
634
|
+
{messages.map((m) => {
|
|
635
|
+
const who =
|
|
636
|
+
m.direction === "inbound"
|
|
637
|
+
? m.fromAddress || "unknown sender"
|
|
638
|
+
: `To: ${m.toAddresses?.join(", ") || "—"}`;
|
|
639
|
+
return (
|
|
640
|
+
<button
|
|
641
|
+
key={m.id}
|
|
642
|
+
type="button"
|
|
643
|
+
onClick={() => openMessage(m)}
|
|
644
|
+
className="w-full rounded-lg border border-border bg-background p-3 text-left transition-colors hover:border-primary/50 hover:bg-muted/30"
|
|
645
|
+
>
|
|
646
|
+
<div className="flex items-center justify-between gap-2">
|
|
647
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
648
|
+
{m.direction === "inbound" ? (
|
|
649
|
+
<Inbox className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
|
650
|
+
) : (
|
|
651
|
+
<Send className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
|
652
|
+
)}
|
|
653
|
+
<span
|
|
654
|
+
className={`truncate text-sm ${
|
|
655
|
+
m.direction === "inbound" && !m.read
|
|
656
|
+
? "font-semibold text-foreground"
|
|
657
|
+
: "text-foreground/80"
|
|
658
|
+
}`}
|
|
659
|
+
>
|
|
660
|
+
{who}
|
|
661
|
+
</span>
|
|
662
|
+
</div>
|
|
663
|
+
<span className="shrink-0 text-xs text-muted-foreground">
|
|
664
|
+
{formatDate(m.agentmailCreatedAt || m.createdAt)}
|
|
665
|
+
</span>
|
|
666
|
+
</div>
|
|
667
|
+
<div className="mt-1 truncate text-sm text-foreground/90">
|
|
668
|
+
{m.subject || "(no subject)"}
|
|
669
|
+
</div>
|
|
670
|
+
{m.preview && (
|
|
671
|
+
<div className="mt-0.5 truncate text-xs text-muted-foreground">
|
|
672
|
+
{m.preview}
|
|
673
|
+
</div>
|
|
674
|
+
)}
|
|
675
|
+
</button>
|
|
676
|
+
);
|
|
677
|
+
})}
|
|
678
|
+
</div>
|
|
679
|
+
)}
|
|
680
|
+
</div>
|
|
681
|
+
)}
|
|
682
|
+
</div>
|
|
683
|
+
);
|
|
684
|
+
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { useCallback, useEffect, useState } from "react";
|
|
2
|
-
import {
|
|
2
|
+
import { useNavigate } from "react-router-dom";
|
|
3
|
+
import { Check, Coins, Copy, RefreshCw, Send, Wallet, X } from "lucide-react";
|
|
3
4
|
import { api } from "@/lib/api";
|
|
4
5
|
import type { AgentWalletBalance } from "@/lib/api";
|
|
5
6
|
import { Button } from "@nous-research/ui/ui/components/button";
|
|
7
|
+
import { Badge } from "@nous-research/ui/ui/components/badge";
|
|
6
8
|
import { Spinner } from "@nous-research/ui/ui/components/spinner";
|
|
7
9
|
import {
|
|
8
10
|
Card,
|
|
@@ -11,6 +13,23 @@ import {
|
|
|
11
13
|
CardTitle,
|
|
12
14
|
} from "@nous-research/ui/ui/components/card";
|
|
13
15
|
|
|
16
|
+
function AgentAvatar({ url }: { url?: string | null }) {
|
|
17
|
+
// ``url`` is a signed avatar URL when the agent has one, else null → fall
|
|
18
|
+
// back to the ClawPump claw so every card shows a real logo.
|
|
19
|
+
return (
|
|
20
|
+
<img
|
|
21
|
+
src={url || "/claw-logo.png"}
|
|
22
|
+
alt=""
|
|
23
|
+
className="h-7 w-7 shrink-0 rounded-full border border-border bg-background object-cover"
|
|
24
|
+
/>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function tokenizePrompt(w: AgentWalletBalance): string {
|
|
29
|
+
const name = w.name || w.agent_id;
|
|
30
|
+
return `Launch a ClawPump token for my agent "${name}" (agent_id ${w.agent_id}). Ask me for the ticker/symbol and any details you need, then launch it.`;
|
|
31
|
+
}
|
|
32
|
+
|
|
14
33
|
/* ── Token logos (inline SVG, no network) ─────────────────────────────── */
|
|
15
34
|
|
|
16
35
|
function SolLogo({ className = "h-5 w-5" }: { className?: string }) {
|
|
@@ -277,6 +296,7 @@ export default function WalletPage() {
|
|
|
277
296
|
const [loading, setLoading] = useState(true);
|
|
278
297
|
const [error, setError] = useState<string | null>(null);
|
|
279
298
|
const [transfer, setTransfer] = useState<AgentWalletBalance | null>(null);
|
|
299
|
+
const navigate = useNavigate();
|
|
280
300
|
|
|
281
301
|
const load = useCallback(() => {
|
|
282
302
|
setLoading(true);
|
|
@@ -296,6 +316,7 @@ export default function WalletPage() {
|
|
|
296
316
|
}, []);
|
|
297
317
|
|
|
298
318
|
useEffect(() => {
|
|
319
|
+
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
299
320
|
load();
|
|
300
321
|
}, [load]);
|
|
301
322
|
|
|
@@ -334,13 +355,27 @@ export default function WalletPage() {
|
|
|
334
355
|
</CardContent>
|
|
335
356
|
</Card>
|
|
336
357
|
) : (
|
|
337
|
-
<div className="
|
|
358
|
+
<div className="grid gap-3 sm:grid-cols-2">
|
|
338
359
|
{wallets.map((w) => (
|
|
339
360
|
<Card key={w.agent_id}>
|
|
340
361
|
<CardHeader className="flex flex-row items-center justify-between gap-2 pb-2">
|
|
341
|
-
<
|
|
342
|
-
{w.
|
|
343
|
-
|
|
362
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
363
|
+
<AgentAvatar url={w.avatar_url} />
|
|
364
|
+
<CardTitle className="truncate text-sm font-semibold">
|
|
365
|
+
{w.name || shortAddress(w.agent_id)}
|
|
366
|
+
</CardTitle>
|
|
367
|
+
{w.token_mint && (
|
|
368
|
+
<a
|
|
369
|
+
href={`https://solscan.io/token/${w.token_mint}`}
|
|
370
|
+
target="_blank"
|
|
371
|
+
rel="noreferrer"
|
|
372
|
+
title={w.token_mint}
|
|
373
|
+
className="shrink-0"
|
|
374
|
+
>
|
|
375
|
+
<Badge tone="success">tokenized</Badge>
|
|
376
|
+
</a>
|
|
377
|
+
)}
|
|
378
|
+
</div>
|
|
344
379
|
{w.wallet_address && (
|
|
345
380
|
<Button
|
|
346
381
|
outlined
|
|
@@ -383,6 +418,19 @@ export default function WalletPage() {
|
|
|
383
418
|
</div>
|
|
384
419
|
</div>
|
|
385
420
|
</div>
|
|
421
|
+
{!w.token_mint && (
|
|
422
|
+
<Button
|
|
423
|
+
outlined
|
|
424
|
+
size="sm"
|
|
425
|
+
className="w-full"
|
|
426
|
+
onClick={() =>
|
|
427
|
+
navigate(`/chat?prompt=${encodeURIComponent(tokenizePrompt(w))}`)
|
|
428
|
+
}
|
|
429
|
+
prefix={<Coins className="h-4 w-4" />}
|
|
430
|
+
>
|
|
431
|
+
Tokenize this agent
|
|
432
|
+
</Button>
|
|
433
|
+
)}
|
|
386
434
|
</CardContent>
|
|
387
435
|
</Card>
|
|
388
436
|
))}
|