@clawpump/claw-agent 0.1.8 → 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 +180 -2
- package/agent/plugins/model-providers/usepod/__init__.py +7 -1
- package/agent/scripts/release.py +1 -0
- 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 +23 -0
- package/package.json +1 -1
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { useQuery } from '@tanstack/react-query'
|
|
2
|
+
import { useState } from 'react'
|
|
3
|
+
import { useNavigate } from 'react-router-dom'
|
|
4
|
+
|
|
5
|
+
import { getPodWallets, transferWallet, type PodWallet } from '@/hermes'
|
|
6
|
+
import { Badge } from '@/components/ui/badge'
|
|
7
|
+
import { Button } from '@/components/ui/button'
|
|
8
|
+
import { writeClipboardText } from '@/components/ui/copy-button'
|
|
9
|
+
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
|
10
|
+
import { Input } from '@/components/ui/input'
|
|
11
|
+
import { InlineNotice } from '@/components/notifications'
|
|
12
|
+
import { Check, Copy, ExternalLink, Loader2, RefreshCw, Send } from '@/lib/icons'
|
|
13
|
+
import { $pendingChatPrompt } from '@/store/composer'
|
|
14
|
+
import { notify } from '@/store/notifications'
|
|
15
|
+
|
|
16
|
+
import { NEW_CHAT_ROUTE } from '../routes'
|
|
17
|
+
import type { SetStatusbarItemGroup } from '../shell/statusbar-controls'
|
|
18
|
+
|
|
19
|
+
const shortAddr = (a: string | null): string => (a ? `${a.slice(0, 4)}…${a.slice(-4)}` : '—')
|
|
20
|
+
const fmt = (n: number | null | undefined, dp: number): string => (n == null ? '0' : n.toFixed(dp))
|
|
21
|
+
|
|
22
|
+
function AddressChip({ address }: { address: string }) {
|
|
23
|
+
const [copied, setCopied] = useState(false)
|
|
24
|
+
return (
|
|
25
|
+
<button
|
|
26
|
+
className="inline-flex items-center gap-1.5 rounded-md border border-border bg-background px-2 py-1 font-mono text-xs text-muted-foreground transition-colors hover:text-foreground"
|
|
27
|
+
onClick={async () => {
|
|
28
|
+
await writeClipboardText(address)
|
|
29
|
+
setCopied(true)
|
|
30
|
+
window.setTimeout(() => setCopied(false), 1200)
|
|
31
|
+
}}
|
|
32
|
+
title={address}
|
|
33
|
+
type="button"
|
|
34
|
+
>
|
|
35
|
+
{shortAddr(address)}
|
|
36
|
+
{copied ? <Check className="size-3" /> : <Copy className="size-3" />}
|
|
37
|
+
</button>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function TransferDialog({ wallet, onClose }: { wallet: PodWallet; onClose: () => void }) {
|
|
42
|
+
const [token, setToken] = useState<'USDC' | 'SOL'>('USDC')
|
|
43
|
+
const [to, setTo] = useState('')
|
|
44
|
+
const [amount, setAmount] = useState('')
|
|
45
|
+
const [busy, setBusy] = useState(false)
|
|
46
|
+
const [error, setError] = useState<string | null>(null)
|
|
47
|
+
const [needsWhitelist, setNeedsWhitelist] = useState(false)
|
|
48
|
+
|
|
49
|
+
const amountNum = Number(amount)
|
|
50
|
+
const valid = to.trim().length >= 32 && Number.isFinite(amountNum) && amountNum > 0
|
|
51
|
+
|
|
52
|
+
const send = async (addToWhitelist: boolean) => {
|
|
53
|
+
if (!valid || busy) {
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
setBusy(true)
|
|
57
|
+
setError(null)
|
|
58
|
+
try {
|
|
59
|
+
const res = await transferWallet({
|
|
60
|
+
add_to_whitelist: addToWhitelist || undefined,
|
|
61
|
+
agent_id: wallet.agent_id,
|
|
62
|
+
amount: amountNum,
|
|
63
|
+
to: to.trim(),
|
|
64
|
+
token
|
|
65
|
+
})
|
|
66
|
+
if (!res.ok) {
|
|
67
|
+
if (res.code === 'destination_not_whitelisted') {
|
|
68
|
+
setNeedsWhitelist(true)
|
|
69
|
+
setError('That address isn’t whitelisted yet. Whitelist it and send?')
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
setError(res.error || 'Transfer failed.')
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
notify({ durationMs: 4_000, kind: 'success', title: 'Sent', message: `${amountNum} ${token} sent.` })
|
|
76
|
+
onClose()
|
|
77
|
+
} catch (err) {
|
|
78
|
+
setError(err instanceof Error ? err.message : 'Transfer failed.')
|
|
79
|
+
} finally {
|
|
80
|
+
setBusy(false)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<Dialog onOpenChange={busy ? undefined : onClose} open>
|
|
86
|
+
<DialogContent className="max-w-md gap-0 p-0">
|
|
87
|
+
<DialogHeader className="border-b border-border px-4 py-3">
|
|
88
|
+
<DialogTitle>Send from {wallet.name || shortAddr(wallet.agent_id)}</DialogTitle>
|
|
89
|
+
</DialogHeader>
|
|
90
|
+
<div className="flex flex-col gap-3 p-4">
|
|
91
|
+
<div className="flex gap-2">
|
|
92
|
+
{(['USDC', 'SOL'] as const).map(tk => (
|
|
93
|
+
<Button
|
|
94
|
+
key={tk}
|
|
95
|
+
onClick={() => setToken(tk)}
|
|
96
|
+
size="sm"
|
|
97
|
+
variant={token === tk ? 'default' : 'outline'}
|
|
98
|
+
>
|
|
99
|
+
{tk}
|
|
100
|
+
</Button>
|
|
101
|
+
))}
|
|
102
|
+
</div>
|
|
103
|
+
<label className="flex flex-col gap-1.5 text-sm">
|
|
104
|
+
<span className="font-medium">Destination address</span>
|
|
105
|
+
<Input onChange={e => setTo(e.target.value)} placeholder="Solana address" value={to} />
|
|
106
|
+
</label>
|
|
107
|
+
<label className="flex flex-col gap-1.5 text-sm">
|
|
108
|
+
<span className="font-medium">Amount ({token})</span>
|
|
109
|
+
<Input inputMode="decimal" onChange={e => setAmount(e.target.value)} type="number" value={amount} />
|
|
110
|
+
</label>
|
|
111
|
+
{error && <InlineNotice kind={needsWhitelist ? 'warning' : 'error'}>{error}</InlineNotice>}
|
|
112
|
+
</div>
|
|
113
|
+
<DialogFooter className="flex-row items-center justify-end gap-2 bg-card p-3">
|
|
114
|
+
<Button disabled={busy} onClick={onClose} variant="outline">
|
|
115
|
+
Cancel
|
|
116
|
+
</Button>
|
|
117
|
+
{needsWhitelist ? (
|
|
118
|
+
<Button disabled={busy} onClick={() => void send(true)}>
|
|
119
|
+
{busy ? <Loader2 className="size-4 animate-spin" /> : 'Whitelist & send'}
|
|
120
|
+
</Button>
|
|
121
|
+
) : (
|
|
122
|
+
<Button disabled={!valid || busy} onClick={() => void send(false)}>
|
|
123
|
+
{busy ? <Loader2 className="size-4 animate-spin" /> : `Send ${token}`}
|
|
124
|
+
</Button>
|
|
125
|
+
)}
|
|
126
|
+
</DialogFooter>
|
|
127
|
+
</DialogContent>
|
|
128
|
+
</Dialog>
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
interface WalletViewProps extends React.ComponentProps<'section'> {
|
|
133
|
+
setStatusbarItemGroup?: SetStatusbarItemGroup
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function WalletView({ setStatusbarItemGroup: _setStatusbarItemGroup, ...props }: WalletViewProps) {
|
|
137
|
+
const navigate = useNavigate()
|
|
138
|
+
const [transfer, setTransfer] = useState<PodWallet | null>(null)
|
|
139
|
+
const wallets = useQuery({ queryKey: ['pod-wallets'], queryFn: getPodWallets, staleTime: 15_000 })
|
|
140
|
+
const rows = wallets.data?.wallets ?? []
|
|
141
|
+
|
|
142
|
+
const tokenize = (w: PodWallet) => {
|
|
143
|
+
$pendingChatPrompt.set(
|
|
144
|
+
`Launch a ClawPump token for my agent "${w.name || w.agent_id}" (agent_id ${w.agent_id}). Ask me for the ticker/symbol and any details you need, then launch it.`
|
|
145
|
+
)
|
|
146
|
+
navigate(NEW_CHAT_ROUTE)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
<section {...props} className="flex h-full min-h-0 flex-col">
|
|
151
|
+
<div className="min-h-0 flex-1 overflow-y-auto">
|
|
152
|
+
<div className="mx-auto max-w-3xl space-y-4 px-5 py-4">
|
|
153
|
+
<header className="flex items-center justify-between gap-2">
|
|
154
|
+
<h1 className="text-lg font-semibold">Agent Wallets</h1>
|
|
155
|
+
<Button onClick={() => void wallets.refetch()} size="icon" variant="ghost">
|
|
156
|
+
<RefreshCw className={wallets.isFetching ? 'size-4 animate-spin' : 'size-4'} />
|
|
157
|
+
</Button>
|
|
158
|
+
</header>
|
|
159
|
+
|
|
160
|
+
{wallets.isLoading ? (
|
|
161
|
+
<div className="flex items-center gap-2 py-10 text-sm text-muted-foreground">
|
|
162
|
+
<Loader2 className="size-4 animate-spin" /> Loading wallets…
|
|
163
|
+
</div>
|
|
164
|
+
) : rows.length === 0 ? (
|
|
165
|
+
<InlineNotice kind="warning">
|
|
166
|
+
{wallets.data?.error || 'No ClawPump agent wallets found. Create one in the ClawPump dashboard first.'}
|
|
167
|
+
</InlineNotice>
|
|
168
|
+
) : (
|
|
169
|
+
<div className="grid gap-2 sm:grid-cols-2">
|
|
170
|
+
{rows.map(w => (
|
|
171
|
+
<div className="flex flex-col gap-2 rounded-lg border border-border bg-card p-3" key={w.agent_id}>
|
|
172
|
+
<div className="flex items-center gap-2">
|
|
173
|
+
<img
|
|
174
|
+
alt=""
|
|
175
|
+
className="size-7 shrink-0 rounded-full border border-border bg-background object-cover"
|
|
176
|
+
src={w.avatar_url || '/claw-mark.png'}
|
|
177
|
+
/>
|
|
178
|
+
<span className="min-w-0 flex-1 truncate text-sm font-semibold">
|
|
179
|
+
{w.name || shortAddr(w.agent_id)}
|
|
180
|
+
</span>
|
|
181
|
+
{w.token_mint && (
|
|
182
|
+
<a
|
|
183
|
+
href={`https://solscan.io/token/${w.token_mint}`}
|
|
184
|
+
rel="noreferrer"
|
|
185
|
+
target="_blank"
|
|
186
|
+
title={w.token_mint}
|
|
187
|
+
>
|
|
188
|
+
<Badge variant="default">tokenized</Badge>
|
|
189
|
+
</a>
|
|
190
|
+
)}
|
|
191
|
+
</div>
|
|
192
|
+
|
|
193
|
+
{w.wallet_address && (
|
|
194
|
+
<div className="flex items-center justify-between gap-2">
|
|
195
|
+
<AddressChip address={w.wallet_address} />
|
|
196
|
+
<a
|
|
197
|
+
className="text-muted-foreground transition-colors hover:text-foreground"
|
|
198
|
+
href={`https://solscan.io/account/${w.wallet_address}`}
|
|
199
|
+
rel="noreferrer"
|
|
200
|
+
target="_blank"
|
|
201
|
+
>
|
|
202
|
+
<ExternalLink className="size-3.5" />
|
|
203
|
+
</a>
|
|
204
|
+
</div>
|
|
205
|
+
)}
|
|
206
|
+
|
|
207
|
+
<div className="grid grid-cols-2 gap-2 text-sm">
|
|
208
|
+
<div className="rounded-md bg-background px-2 py-1.5">
|
|
209
|
+
<div className="text-[0.62rem] uppercase tracking-wide text-muted-foreground">SOL</div>
|
|
210
|
+
<div className="font-mono">{fmt(w.sol_balance, 4)}</div>
|
|
211
|
+
</div>
|
|
212
|
+
<div className="rounded-md bg-background px-2 py-1.5">
|
|
213
|
+
<div className="text-[0.62rem] uppercase tracking-wide text-muted-foreground">USDC</div>
|
|
214
|
+
<div className="font-mono">${fmt(w.usdc_balance, 2)}</div>
|
|
215
|
+
</div>
|
|
216
|
+
</div>
|
|
217
|
+
|
|
218
|
+
<div className="flex gap-2">
|
|
219
|
+
<Button
|
|
220
|
+
className="flex-1"
|
|
221
|
+
disabled={!w.wallet_address}
|
|
222
|
+
onClick={() => setTransfer(w)}
|
|
223
|
+
size="sm"
|
|
224
|
+
variant="outline"
|
|
225
|
+
>
|
|
226
|
+
<Send className="size-3.5" /> Send
|
|
227
|
+
</Button>
|
|
228
|
+
{!w.token_mint && (
|
|
229
|
+
<Button className="flex-1" onClick={() => tokenize(w)} size="sm" variant="outline">
|
|
230
|
+
Tokenize
|
|
231
|
+
</Button>
|
|
232
|
+
)}
|
|
233
|
+
</div>
|
|
234
|
+
</div>
|
|
235
|
+
))}
|
|
236
|
+
</div>
|
|
237
|
+
)}
|
|
238
|
+
</div>
|
|
239
|
+
</div>
|
|
240
|
+
|
|
241
|
+
{transfer && <TransferDialog onClose={() => setTransfer(null)} wallet={transfer} />}
|
|
242
|
+
</section>
|
|
243
|
+
)
|
|
244
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { useNavigate } from 'react-router-dom'
|
|
3
|
+
|
|
4
|
+
import { searchX402, type X402Result } from '@/hermes'
|
|
5
|
+
import { Badge } from '@/components/ui/badge'
|
|
6
|
+
import { Button } from '@/components/ui/button'
|
|
7
|
+
import { writeClipboardText } from '@/components/ui/copy-button'
|
|
8
|
+
import { Input } from '@/components/ui/input'
|
|
9
|
+
import { Check, Copy, ExternalLink, Loader2, Search, Zap } from '@/lib/icons'
|
|
10
|
+
import { $pendingChatPrompt } from '@/store/composer'
|
|
11
|
+
import { notifyError } from '@/store/notifications'
|
|
12
|
+
|
|
13
|
+
import { NEW_CHAT_ROUTE } from '../routes'
|
|
14
|
+
import type { SetStatusbarItemGroup } from '../shell/statusbar-controls'
|
|
15
|
+
|
|
16
|
+
const bestPrice = (r: X402Result): string => r.pricing?.find(p => p.priceLabel)?.priceLabel ?? ''
|
|
17
|
+
|
|
18
|
+
const buildPrompt = (r: X402Result): string => {
|
|
19
|
+
const label = r.name || r.host || 'this service'
|
|
20
|
+
const price = bestPrice(r)
|
|
21
|
+
return `Use this x402 API and pay it with my ClawPump wallet: ${r.resourceUrl} (${label}${price ? `, ${price}` : ''}). First check what inputs it needs, then call it.`
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface X402ViewProps extends React.ComponentProps<'section'> {
|
|
25
|
+
setStatusbarItemGroup?: SetStatusbarItemGroup
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function X402View({ setStatusbarItemGroup: _setStatusbarItemGroup, ...props }: X402ViewProps) {
|
|
29
|
+
const navigate = useNavigate()
|
|
30
|
+
const [query, setQuery] = useState('')
|
|
31
|
+
const [results, setResults] = useState<X402Result[] | null>(null)
|
|
32
|
+
const [loading, setLoading] = useState(false)
|
|
33
|
+
const [copied, setCopied] = useState<string | null>(null)
|
|
34
|
+
|
|
35
|
+
const run = async () => {
|
|
36
|
+
const q = query.trim()
|
|
37
|
+
if (!q || loading) {
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
setLoading(true)
|
|
41
|
+
try {
|
|
42
|
+
const res = await searchX402(q)
|
|
43
|
+
setResults(res.results ?? [])
|
|
44
|
+
} catch (err) {
|
|
45
|
+
notifyError(err, 'x402 search failed')
|
|
46
|
+
setResults([])
|
|
47
|
+
} finally {
|
|
48
|
+
setLoading(false)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const useInChat = (r: X402Result) => {
|
|
53
|
+
$pendingChatPrompt.set(buildPrompt(r))
|
|
54
|
+
navigate(NEW_CHAT_ROUTE)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const copy = async (url: string) => {
|
|
58
|
+
await writeClipboardText(url)
|
|
59
|
+
setCopied(url)
|
|
60
|
+
window.setTimeout(() => setCopied(c => (c === url ? null : c)), 1200)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<section {...props} className="flex h-full min-h-0 flex-col">
|
|
65
|
+
<div className="min-h-0 flex-1 overflow-y-auto">
|
|
66
|
+
<div className="mx-auto max-w-3xl space-y-4 px-5 py-4">
|
|
67
|
+
<header className="flex items-center gap-2">
|
|
68
|
+
<Zap className="size-5 text-primary" />
|
|
69
|
+
<h1 className="text-lg font-semibold">x402 Marketplace</h1>
|
|
70
|
+
</header>
|
|
71
|
+
<p className="text-sm text-muted-foreground">
|
|
72
|
+
Search the Dexter x402 bazaar (via the ClawPump MCP). Any endpoint is pay-per-call from your agent wallet —
|
|
73
|
+
send one to chat and the agent calls + pays it for you.
|
|
74
|
+
</p>
|
|
75
|
+
|
|
76
|
+
<div className="flex gap-2">
|
|
77
|
+
<div className="relative flex-1">
|
|
78
|
+
<Search className="pointer-events-none absolute left-2.5 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
|
|
79
|
+
<Input
|
|
80
|
+
className="pl-8"
|
|
81
|
+
onChange={e => setQuery(e.target.value)}
|
|
82
|
+
onKeyDown={e => {
|
|
83
|
+
if (e.key === 'Enter') {
|
|
84
|
+
void run()
|
|
85
|
+
}
|
|
86
|
+
}}
|
|
87
|
+
placeholder="e.g. image generation, ETH price, weather…"
|
|
88
|
+
value={query}
|
|
89
|
+
/>
|
|
90
|
+
</div>
|
|
91
|
+
<Button disabled={loading || !query.trim()} onClick={() => void run()}>
|
|
92
|
+
{loading ? <Loader2 className="size-4 animate-spin" /> : 'Search'}
|
|
93
|
+
</Button>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
{results && results.length === 0 && !loading && (
|
|
97
|
+
<div className="py-10 text-center text-sm text-muted-foreground">No results — try a different query.</div>
|
|
98
|
+
)}
|
|
99
|
+
|
|
100
|
+
<div className="grid gap-2">
|
|
101
|
+
{(results ?? []).map((r, i) => (
|
|
102
|
+
<div className="rounded-lg border border-border bg-card p-3" key={r.resourceUrl ?? `${r.name}-${i}`}>
|
|
103
|
+
<div className="flex items-start justify-between gap-3">
|
|
104
|
+
<div className="min-w-0">
|
|
105
|
+
<div className="truncate font-medium">{r.name || r.host || 'Untitled'}</div>
|
|
106
|
+
{r.description && (
|
|
107
|
+
<div className="mt-0.5 line-clamp-2 text-xs text-muted-foreground">{r.description}</div>
|
|
108
|
+
)}
|
|
109
|
+
</div>
|
|
110
|
+
<div className="flex shrink-0 flex-col items-end gap-1">
|
|
111
|
+
{bestPrice(r) && <span className="font-mono text-xs text-primary">{bestPrice(r)}</span>}
|
|
112
|
+
{r.verified && <Badge variant="default">verified</Badge>}
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
<div className="mt-2 flex flex-wrap items-center gap-1.5">
|
|
117
|
+
{r.category && <Badge variant="muted">{r.category}</Badge>}
|
|
118
|
+
{r.method && (
|
|
119
|
+
<span className="rounded bg-muted px-1.5 py-0.5 font-mono text-[0.62rem] uppercase text-muted-foreground">
|
|
120
|
+
{r.method}
|
|
121
|
+
</span>
|
|
122
|
+
)}
|
|
123
|
+
{typeof r.qualityScore === 'number' && (
|
|
124
|
+
<span className="text-[0.62rem] text-muted-foreground">quality {r.qualityScore}</span>
|
|
125
|
+
)}
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
{r.resourceUrl && (
|
|
129
|
+
<div className="mt-2 flex items-center gap-1.5">
|
|
130
|
+
<span className="min-w-0 flex-1 truncate font-mono text-xs text-muted-foreground">
|
|
131
|
+
{r.resourceUrl}
|
|
132
|
+
</span>
|
|
133
|
+
<button
|
|
134
|
+
className="shrink-0 text-muted-foreground transition-colors hover:text-foreground"
|
|
135
|
+
onClick={() => void copy(r.resourceUrl!)}
|
|
136
|
+
title="Copy URL"
|
|
137
|
+
type="button"
|
|
138
|
+
>
|
|
139
|
+
{copied === r.resourceUrl ? <Check className="size-3.5" /> : <Copy className="size-3.5" />}
|
|
140
|
+
</button>
|
|
141
|
+
<a
|
|
142
|
+
className="shrink-0 text-muted-foreground transition-colors hover:text-foreground"
|
|
143
|
+
href={r.resourceUrl}
|
|
144
|
+
rel="noreferrer"
|
|
145
|
+
target="_blank"
|
|
146
|
+
>
|
|
147
|
+
<ExternalLink className="size-3.5" />
|
|
148
|
+
</a>
|
|
149
|
+
</div>
|
|
150
|
+
)}
|
|
151
|
+
|
|
152
|
+
<Button className="mt-2 w-full" onClick={() => useInChat(r)} size="sm" variant="outline">
|
|
153
|
+
Use in chat
|
|
154
|
+
</Button>
|
|
155
|
+
</div>
|
|
156
|
+
))}
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
</section>
|
|
161
|
+
)
|
|
162
|
+
}
|
|
@@ -426,7 +426,7 @@ const StreamStallIndicator: FC = () => {
|
|
|
426
426
|
<StatusRow
|
|
427
427
|
className="mt-1.5"
|
|
428
428
|
data-slot="aui_stream-stall"
|
|
429
|
-
label={compacting ? COMPACTION_LABEL : '
|
|
429
|
+
label={compacting ? COMPACTION_LABEL : 'Claw Agent is thinking'}
|
|
430
430
|
>
|
|
431
431
|
<span aria-hidden="true" className="dither inline-block size-3 rounded-[2px] text-midground/80 animate-pulse" />
|
|
432
432
|
{compacting && <CompactionHint />}
|
|
@@ -2,7 +2,7 @@ import { cn } from '@/lib/utils'
|
|
|
2
2
|
|
|
3
3
|
const assetPath = (path: string) => `${import.meta.env.BASE_URL}${path.replace(/^\/+/, '')}`
|
|
4
4
|
|
|
5
|
-
// Brand badge:
|
|
5
|
+
// Brand badge: ClawPump claw mark on a white tile, identical in light/dark.
|
|
6
6
|
// Fills the tile (softly rounded); size via className (default size-14).
|
|
7
7
|
export function BrandMark({ className, ...props }: React.ComponentProps<'span'>) {
|
|
8
8
|
return (
|
|
@@ -13,7 +13,7 @@ export function BrandMark({ className, ...props }: React.ComponentProps<'span'>)
|
|
|
13
13
|
)}
|
|
14
14
|
{...props}
|
|
15
15
|
>
|
|
16
|
-
<img alt="" className="size-full object-contain" src={assetPath('
|
|
16
|
+
<img alt="" className="size-full object-contain" src={assetPath('claw-mark.png')} />
|
|
17
17
|
</span>
|
|
18
18
|
)
|
|
19
19
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
{"personality":"helpful","headline":"How can I help today?","body":"Point me at a file, paste an error, or describe what you're building. I'll take it from there."}
|
|
3
3
|
{"personality":"helpful","headline":"Let's get started","body":"Try: review my diff, run the test suite, or explain this function. Ask anything about your code."}
|
|
4
4
|
{"personality":"helpful","headline":"Tell me what you need","body":"I can edit files, run commands, search the web, and walk you through tricky bugs. Just describe the task."}
|
|
5
|
-
{"personality":"helpful","headline":"Hi,
|
|
5
|
+
{"personality":"helpful","headline":"Hi, Claw Agent here","body":"Share a repo path or a question to start. I keep replies clear and link back to the files I touch."}
|
|
6
6
|
{"personality":"concise","headline":"Ready.","body":"Describe the task. I'll do it."}
|
|
7
7
|
{"personality":"concise","headline":"Waiting for input","body":"Paste code, errors, or a goal. Short answers, fast edits."}
|
|
8
8
|
{"personality":"concise","headline":"Go.","body":"Ask. I'll read files, run tests, ship patches. No filler."}
|
|
@@ -34,12 +34,12 @@
|
|
|
34
34
|
{"personality":"catgirl","headline":"tail up, claws sheathed","body":"paste an error or a plan. i debug like i hunt: quietly, thoroughly, with the occasional zoomie."}
|
|
35
35
|
{"personality":"catgirl","headline":"nyaaa~ hermes reporting","body":"say the word and i'll read your files, run your tests, and curl up in your branch with a tidy commit."}
|
|
36
36
|
{"personality":"pirate","headline":"Ahoy! Ready to sail the repo","body":"Name yer quarry - a bug, a feature, a cursed test - and I'll chase it down, matey. Diffs for plunder."}
|
|
37
|
-
{"personality":"pirate","headline":"
|
|
37
|
+
{"personality":"pirate","headline":"Claw Agent at the helm, arrr","body":"Point me at the charts (the code) and I'll patch the hull, fire the cannons (tests), hoist a clean PR."}
|
|
38
38
|
{"personality":"pirate","headline":"What be the task, cap'n?","body":"Paste an error or a plan, ye scurvy dog. I'll navigate the stack trace and bring back treasure: green tests."}
|
|
39
39
|
{"personality":"pirate","headline":"Anchors aweigh, keyboard ready","body":"Tell me where X marks the spot. I read, edit, and commit with the discipline of a proper crew, arrr."}
|
|
40
40
|
{"personality":"pirate","headline":"Yo ho! Awaitin' orders","body":"Throw me a bug, a repo path, or a wild idea. I'll plunder the docs and return with workin' code."}
|
|
41
41
|
{"personality":"shakespeare","headline":"Pray, what task dost thou bring?","body":"Speak thy bug, thy file, thy weary test, and I shall mend it with a scholar's hand and honest diff."}
|
|
42
|
-
{"personality":"shakespeare","headline":"Hark!
|
|
42
|
+
{"personality":"shakespeare","headline":"Hark! Claw Agent standeth ready","body":"Name the code that vexeth thee. I shall read, revise, and render a patch most fair and clean."}
|
|
43
43
|
{"personality":"shakespeare","headline":"What news from thy repository?","body":"Present thy stack trace or thy dream. I'll traverse files, run tests, and report in plainest verse."}
|
|
44
44
|
{"personality":"shakespeare","headline":"The stage is set, the cursor blinks","body":"Describe thy aim, good sir or madam. Thy branches shall be trimmed, thy bugs cast from the realm."}
|
|
45
45
|
{"personality":"shakespeare","headline":"Speak, and I shall act","body":"A line of intent sufficeth. I read, I edit, I commit - and leave thy history unblemished."}
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
{"personality":"surfer","headline":"Tide's up, cursor's blinking","body":"Name the task and we're off. I read, edit, test, and leave a commit smoother than a dawn patrol."}
|
|
51
51
|
{"personality":"noir","headline":"Another repo, another rainy night","body":"Tell me what's broken. I'll read the files, dust for prints, and leave a diff on the desk by morning."}
|
|
52
52
|
{"personality":"noir","headline":"The cursor blinks. So do I.","body":"You've got a bug. I've got patience and a terminal. Name the case and I'll work it till it talks."}
|
|
53
|
-
{"personality":"noir","headline":"
|
|
53
|
+
{"personality":"noir","headline":"Claw Agent. Code investigator.","body":"Paste the stack trace, the suspect file, the alibi. I read between the lines and return with the truth."}
|
|
54
54
|
{"personality":"noir","headline":"Quiet night, open prompt","body":"Every bug leaves a trail. Give me the repo and a lead - I'll follow it, patch it, and close the file."}
|
|
55
55
|
{"personality":"noir","headline":"No case too small","body":"A typo, a segfault, a whole rotten architecture - hand me the keys. I'll bring back clean tests."}
|
|
56
56
|
{"personality":"uwu","headline":"uwu ready to hewp!","body":"paste a buggy fiwe or a goaw~ i'll wead, patch, and test, aww with tiny pawprints on the diff owo"}
|
|
@@ -64,11 +64,11 @@
|
|
|
64
64
|
{"personality":"philosopher","headline":"Consider the code, then speak","body":"Describe the end you seek. I pursue it through files, tests, and docs, and report what I found on the way."}
|
|
65
65
|
{"personality":"philosopher","headline":"The unexamined repo is not worth running","body":"Share a path, a puzzle, or a principle. I'll trace the logic, propose a change, and justify each edit."}
|
|
66
66
|
{"personality":"hype","headline":"LET'S GOOOO! READY TO SHIP!","body":"Paste that bug, that repo, that wild feature idea - I AM LOCKED IN. Clean diffs. Green tests. RIGHT NOW."}
|
|
67
|
-
{"personality":"hype","headline":"
|
|
67
|
+
{"personality":"hype","headline":"CLAW AGENT ONLINE. LFG.","body":"Drop your task and watch me cook. Files read, tests run, PRs opened - we are NOT losing today, friend."}
|
|
68
68
|
{"personality":"hype","headline":"New session, infinite W's","body":"Bring the gnarliest bug you've got. I'll read, patch, test, commit like my life depends on it. LET'S GO."}
|
|
69
69
|
{"personality":"hype","headline":"ABSOLUTELY DIALED IN","body":"Describe the task. I'll blitz through files, crush failing tests, and leave a commit that SLAPS. Go go go."}
|
|
70
70
|
{"personality":"hype","headline":"Ready. So ready. Too ready.","body":"Tiny typo or huge refactor - doesn't matter. I'm shipping clean code today. Name the task and let's WORK."}
|
|
71
|
-
{"personality":"none","headline":"
|
|
71
|
+
{"personality":"none","headline":"Claw Agent is ready.","body":"Ask a question, paste an error, or point me at a repo. I can read code, run tools, and help you ship."}
|
|
72
72
|
{"personality":"none","headline":"What are we building today?","body":"Describe the task in your own words. I'll pick the right tools, explain my plan, and check in before risky steps."}
|
|
73
73
|
{"personality":"none","headline":"Start anywhere.","body":"Drop a file path, a traceback, or a rough idea. I'll investigate, suggest next steps, and keep things reversible."}
|
|
74
74
|
{"personality":"none","headline":"Your workspace, one prompt away.","body":"Search the repo, edit files, run tests, open PRs. Tell me the goal and I'll handle the mechanical parts."}
|
|
@@ -28,7 +28,7 @@ const FALLBACK_COPY: IntroCopy[] = [
|
|
|
28
28
|
body: "Bring the code, question, or stuck part. I'll read the room before making changes."
|
|
29
29
|
},
|
|
30
30
|
{
|
|
31
|
-
headline: 'What should
|
|
31
|
+
headline: 'What should Claw Agent look at?',
|
|
32
32
|
body: "Send the task, failing path, or half-formed plan. I'll help turn it into action."
|
|
33
33
|
},
|
|
34
34
|
{
|
|
@@ -120,7 +120,7 @@ function fallbackCopyForPersonality(personalityKey: string): IntroCopy[] {
|
|
|
120
120
|
body: "Send the task, file, or rough idea. I'll use your configured voice and keep the work grounded in this repo."
|
|
121
121
|
},
|
|
122
122
|
{
|
|
123
|
-
headline: `What does ${label}
|
|
123
|
+
headline: `What does ${label} Claw Agent need to see?`,
|
|
124
124
|
body: "Bring the context or the stuck part. I'll adapt to your configured personality."
|
|
125
125
|
},
|
|
126
126
|
{
|
|
@@ -128,7 +128,7 @@ function fallbackCopyForPersonality(personalityKey: string): IntroCopy[] {
|
|
|
128
128
|
body: "Send the problem, file, or idea. I'll follow the personality you've configured."
|
|
129
129
|
},
|
|
130
130
|
{
|
|
131
|
-
headline: `What should ${label}
|
|
131
|
+
headline: `What should ${label} Claw Agent tackle?`,
|
|
132
132
|
body: "Drop the task here. I'll keep the work grounded in the repo."
|
|
133
133
|
},
|
|
134
134
|
{
|
|
@@ -142,7 +142,7 @@ function pickCopy(copies: IntroCopy[], seed = 0): IntroCopy {
|
|
|
142
142
|
return copies[Math.abs(seed) % copies.length] || FALLBACK_COPY[0]
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
-
const WORDMARK = '
|
|
145
|
+
const WORDMARK = 'CLAW AGENT'
|
|
146
146
|
|
|
147
147
|
function resolveCopy(personality?: string, seed?: number): IntroCopy {
|
|
148
148
|
const personalityKey = normalizeKey(personality)
|
|
@@ -6,11 +6,12 @@ import { currentPickerSelection } from '@/lib/model-status-label'
|
|
|
6
6
|
import type { ModelOptionProvider, ModelOptionsResponse, ModelPricing } from '@/types/hermes'
|
|
7
7
|
|
|
8
8
|
import type { HermesGateway } from '../hermes'
|
|
9
|
-
import { getGlobalModelOptions } from '../hermes'
|
|
9
|
+
import { getGlobalModelOptions, getPodStatus } from '../hermes'
|
|
10
10
|
import { cn } from '../lib/utils'
|
|
11
11
|
import { startManualOnboarding } from '../store/onboarding'
|
|
12
12
|
|
|
13
13
|
import { InlineNotice } from './notifications'
|
|
14
|
+
import { PodSetupDialog } from './pod-setup-dialog'
|
|
14
15
|
import { Button } from './ui/button'
|
|
15
16
|
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from './ui/command'
|
|
16
17
|
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from './ui/dialog'
|
|
@@ -96,6 +97,12 @@ export function ModelPickerDialog({
|
|
|
96
97
|
onOpenChange(false)
|
|
97
98
|
}
|
|
98
99
|
|
|
100
|
+
// ClawPump: "Pod" — fund a private inference Pod from an agent wallet and use
|
|
101
|
+
// it as the provider. One dialog, one confirm (the on-chain spend). On
|
|
102
|
+
// success the backend has already set provider=usepod; re-select so the
|
|
103
|
+
// session adopts it (model-applied + reload prompt) like any model switch.
|
|
104
|
+
const [podOpen, setPodOpen] = useState(false)
|
|
105
|
+
|
|
99
106
|
return (
|
|
100
107
|
<Dialog onOpenChange={onOpenChange} open={open}>
|
|
101
108
|
<DialogContent className={cn('max-h-[85vh] max-w-2xl gap-0 overflow-hidden p-0', contentClassName)}>
|
|
@@ -122,6 +129,7 @@ export function ModelPickerDialog({
|
|
|
122
129
|
error={error}
|
|
123
130
|
loading={loading}
|
|
124
131
|
onSelectModel={selectModel}
|
|
132
|
+
onSetUpPod={() => setPodOpen(true)}
|
|
125
133
|
providers={providers}
|
|
126
134
|
search={search}
|
|
127
135
|
/>
|
|
@@ -137,6 +145,16 @@ export function ModelPickerDialog({
|
|
|
137
145
|
</Button>
|
|
138
146
|
</DialogFooter>
|
|
139
147
|
</DialogContent>
|
|
148
|
+
|
|
149
|
+
<PodSetupDialog
|
|
150
|
+
onOpenChange={setPodOpen}
|
|
151
|
+
onProvisioned={model => {
|
|
152
|
+
// Switch the live session onto Pod; keep the picker mounted so the
|
|
153
|
+
// dialog's "Pod ready" view shows (its Done button closes it).
|
|
154
|
+
onSelect({ provider: 'usepod', model })
|
|
155
|
+
}}
|
|
156
|
+
open={podOpen}
|
|
157
|
+
/>
|
|
140
158
|
</Dialog>
|
|
141
159
|
)
|
|
142
160
|
}
|
|
@@ -148,6 +166,7 @@ function ModelResults({
|
|
|
148
166
|
currentModel,
|
|
149
167
|
currentProvider,
|
|
150
168
|
onSelectModel,
|
|
169
|
+
onSetUpPod,
|
|
151
170
|
search
|
|
152
171
|
}: {
|
|
153
172
|
loading: boolean
|
|
@@ -156,10 +175,14 @@ function ModelResults({
|
|
|
156
175
|
currentModel: string
|
|
157
176
|
currentProvider: string
|
|
158
177
|
onSelectModel: (provider: ModelOptionProvider, model: string) => void
|
|
178
|
+
onSetUpPod: () => void
|
|
159
179
|
search: string
|
|
160
180
|
}) {
|
|
161
181
|
const { t } = useI18n()
|
|
162
182
|
const copy = t.modelPicker
|
|
183
|
+
const podStatus = useQuery({ queryKey: ['pod-status'], queryFn: getPodStatus, staleTime: 30_000 })
|
|
184
|
+
const podConnected = podStatus.data?.connected ?? false
|
|
185
|
+
const podBalance = podStatus.data?.balance_usdc
|
|
163
186
|
|
|
164
187
|
if (loading) {
|
|
165
188
|
return <LoadingResults />
|
|
@@ -175,12 +198,48 @@ function ModelResults({
|
|
|
175
198
|
)
|
|
176
199
|
}
|
|
177
200
|
|
|
201
|
+
const q = search.trim().toLowerCase()
|
|
202
|
+
|
|
203
|
+
// ClawPump: promote "Pod" at the top (mirrors the CLI's promoted Pod entry).
|
|
204
|
+
// Selecting it opens the one-confirm setup dialog (fund from a wallet) rather
|
|
205
|
+
// than switching models directly — Pod has no model until it's provisioned.
|
|
206
|
+
const podVisible = !q || 'pod usepod pay-as-you-go wallet clawpump'.includes(q)
|
|
207
|
+
const podRow = podVisible ? (
|
|
208
|
+
<CommandGroup heading="ClawPump" key="clawpump-pod">
|
|
209
|
+
<CommandItem
|
|
210
|
+
className="flex items-center gap-2 data-[selected=true]:bg-primary/15"
|
|
211
|
+
onSelect={onSetUpPod}
|
|
212
|
+
value="usepod pod pay-as-you-go clawpump wallet"
|
|
213
|
+
>
|
|
214
|
+
<img alt="" className="size-4 shrink-0 rounded-sm" src="/claw-mark.png" />
|
|
215
|
+
<span className="flex min-w-0 flex-1 flex-col">
|
|
216
|
+
<span className="font-medium">Pod</span>
|
|
217
|
+
<span className="truncate text-xs text-muted-foreground">
|
|
218
|
+
{podConnected
|
|
219
|
+
? podBalance != null
|
|
220
|
+
? `Connected · $${podBalance.toFixed(2)} USDC left`
|
|
221
|
+
: 'Connected — your inference provider'
|
|
222
|
+
: 'Pay-as-you-go inference — fund from your ClawPump wallet'}
|
|
223
|
+
</span>
|
|
224
|
+
</span>
|
|
225
|
+
{podConnected ? (
|
|
226
|
+
<span className="shrink-0 text-[0.62rem] uppercase tracking-wide text-emerald-400">✓ Connected</span>
|
|
227
|
+
) : (
|
|
228
|
+
<span className="shrink-0 text-[0.62rem] uppercase tracking-wide text-primary">Set up</span>
|
|
229
|
+
)}
|
|
230
|
+
</CommandItem>
|
|
231
|
+
</CommandGroup>
|
|
232
|
+
) : null
|
|
233
|
+
|
|
178
234
|
if (providers.length === 0) {
|
|
179
|
-
return
|
|
235
|
+
return (
|
|
236
|
+
<>
|
|
237
|
+
{podRow}
|
|
238
|
+
<div className="px-4 py-6 text-sm text-muted-foreground">{copy.noAuthenticatedProviders}</div>
|
|
239
|
+
</>
|
|
240
|
+
)
|
|
180
241
|
}
|
|
181
242
|
|
|
182
|
-
const q = search.trim().toLowerCase()
|
|
183
|
-
|
|
184
243
|
const matches = (provider: ModelOptionProvider, model: string) =>
|
|
185
244
|
!q ||
|
|
186
245
|
model.toLowerCase().includes(q) ||
|
|
@@ -194,6 +253,7 @@ function ModelResults({
|
|
|
194
253
|
|
|
195
254
|
return (
|
|
196
255
|
<>
|
|
256
|
+
{podRow}
|
|
197
257
|
{configured.map(provider => {
|
|
198
258
|
// Preserve the backend's curated order — filter in place, no re-sort.
|
|
199
259
|
const models = (provider.models ?? []).filter(m => matches(provider, m))
|