@clawpump/claw-agent 0.1.7 → 0.1.9
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/.mailmap +4 -0
- package/agent/apps/desktop/README.md +3 -3
- package/agent/apps/desktop/assets/icon.icns +0 -0
- package/agent/apps/desktop/assets/icon.ico +0 -0
- package/agent/apps/desktop/assets/icon.png +0 -0
- package/agent/apps/desktop/electron/backend-ready.cjs +2 -2
- package/agent/apps/desktop/electron/dashboard-token.cjs +3 -3
- package/agent/apps/desktop/electron/hardening.cjs +1 -1
- package/agent/apps/desktop/electron/main.cjs +65 -65
- package/agent/apps/desktop/index.html +1 -1
- package/agent/apps/desktop/package.json +11 -11
- package/agent/apps/desktop/public/apple-touch-icon.png +0 -0
- package/agent/apps/desktop/public/claw-mark.png +0 -0
- package/agent/apps/desktop/scripts/set-exe-identity.cjs +2 -2
- package/agent/apps/desktop/src/app/chat/composer/controls.tsx +2 -0
- package/agent/apps/desktop/src/app/chat/composer/index.tsx +10 -0
- package/agent/apps/desktop/src/app/chat/composer/pod-credits.tsx +49 -0
- package/agent/apps/desktop/src/app/chat/index.tsx +1 -1
- package/agent/apps/desktop/src/app/chat/sidebar/index.tsx +4 -2
- package/agent/apps/desktop/src/app/desktop-controller.tsx +18 -0
- package/agent/apps/desktop/src/app/gateway/hooks/use-gateway-request.ts +1 -1
- package/agent/apps/desktop/src/app/messaging/index.tsx +5 -5
- package/agent/apps/desktop/src/app/routes.ts +9 -1
- package/agent/apps/desktop/src/app/session/hooks/use-message-stream.ts +3 -3
- package/agent/apps/desktop/src/app/settings/constants.ts +5 -5
- package/agent/apps/desktop/src/app/settings/model-settings.tsx +1 -1
- package/agent/apps/desktop/src/app/settings/providers-settings.tsx +46 -1
- package/agent/apps/desktop/src/app/settings/uninstall-section.tsx +5 -5
- package/agent/apps/desktop/src/app/types.ts +9 -1
- package/agent/apps/desktop/src/app/wallet/index.tsx +244 -0
- package/agent/apps/desktop/src/app/x402/index.tsx +162 -0
- package/agent/apps/desktop/src/components/assistant-ui/thread.tsx +1 -1
- package/agent/apps/desktop/src/components/brand-mark.tsx +2 -2
- package/agent/apps/desktop/src/components/chat/intro-copy.jsonl +6 -6
- package/agent/apps/desktop/src/components/chat/intro.tsx +4 -4
- package/agent/apps/desktop/src/components/model-picker.tsx +64 -4
- package/agent/apps/desktop/src/components/pod-setup-dialog.tsx +227 -0
- package/agent/apps/desktop/src/hermes.ts +109 -3
- package/agent/apps/desktop/src/i18n/en.ts +80 -78
- package/agent/apps/desktop/src/i18n/ja.ts +82 -82
- package/agent/apps/desktop/src/i18n/runtime.test.ts +2 -2
- package/agent/apps/desktop/src/i18n/zh-hant.ts +82 -82
- package/agent/apps/desktop/src/i18n/zh.ts +87 -87
- package/agent/apps/desktop/src/lib/desktop-fs.ts +1 -1
- package/agent/apps/desktop/src/lib/desktop-slash-commands.ts +4 -4
- package/agent/apps/desktop/src/store/composer.ts +7 -0
- package/agent/apps/desktop/src/store/onboarding.ts +5 -5
- package/agent/apps/desktop/src/themes/presets.ts +54 -54
- package/agent/cli.py +184 -10
- package/agent/hermes_cli/distribution.py +188 -8
- package/agent/hermes_cli/providers.py +29 -0
- package/agent/hermes_cli/web_server.py +403 -34
- package/agent/plugins/model-providers/usepod/__init__.py +7 -1
- package/agent/scripts/release.py +1 -0
- package/agent/web/public/claw-logo.png +0 -0
- package/agent/web/src/App.tsx +6 -4
- package/agent/web/src/components/ChatSidebar.tsx +5 -0
- package/agent/web/src/components/ModelPickerDialog.tsx +28 -1
- package/agent/web/src/components/PodCredits.tsx +57 -0
- package/agent/web/src/components/PodSetupDialog.tsx +240 -0
- package/agent/web/src/lib/api.ts +135 -0
- package/agent/web/src/pages/AgentMailPage.tsx +684 -0
- package/agent/web/src/pages/WalletPage.tsx +53 -5
- package/package.json +1 -1
|
@@ -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
|
+
}
|