Files
cardclaws/cardclaws-mobile/app/pick-brain.tsx
T
Omar SobhandClaude Opus 4.8 9765a08a5a
CI / policy (push) Has been cancelled
CI / backend (push) Has been cancelled
CI / profile (push) Has been cancelled
CI / mobile (push) Has been cancelled
Agents: real ClawBrainHub integration (list + pull brains → trading cards)
Backend:
- brainhub.rs: BrainHubClient trait + HttpBrainHubClient (public discovery API,
  optional Bearer token) + FakeBrainHubClient. Lists the user's brains
  (CLAWBRAINHUB_OWNER, default "omar") + redclawsystems reference brains, and
  pulls a .brain JSON, parsing identity/skills/tools into card fields
  (tagline, skills, tools, capabilities). 2 parser unit tests.
- Routes GET /v1/brainhub/brains + /v1/brainhub/pull (rate-limited); wired into
  AppState + main (HttpBrainHubClient) + test harness (FakeBrainHubClient).

Mobile:
- src/api/brainhub.ts (listBrains/pullBrain), draftStore.pendingAgentBrain.
- app/pick-brain.tsx: pick a brain → pull → hand to the agent-card editor.
- Agents "+ New agent" now opens the brain picker; demo.tsx adopts the pulled
  brain (name, tagline, skill bars, tools, capabilities) for the flip side.

Verified live against clawbrainhub.com (omar's brains + reference brains list;
general-assistant pull yields real skills). Token stays server-side/env only.
Gate: mobile 28, backend 123.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-06 00:50:16 -05:00

113 lines
3.9 KiB
TypeScript

// Pick an agent brain from ClawBrainHub (your account + reference brains), pull
// it, and hand it to the agent-card editor to turn into a trading card.
import { Stack, useRouter } from "expo-router";
import { useEffect, useState } from "react";
import { ActivityIndicator, FlatList, Pressable, StyleSheet, Text, View } from "react-native";
import { BrainSummary, listBrains, pullBrain } from "../src/api/brainhub";
import { useDraftStore } from "../src/stores/draftStore";
export default function PickBrain() {
const router = useRouter();
const setPendingAgentBrain = useDraftStore((s) => s.setPendingAgentBrain);
const [brains, setBrains] = useState<BrainSummary[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [pulling, setPulling] = useState<string | null>(null);
useEffect(() => {
let alive = true;
listBrains()
.then((b) => alive && setBrains(b))
.catch(() => alive && setError("Couldn't reach ClawBrainHub. Check your connection."))
.finally(() => alive && setLoading(false));
return () => {
alive = false;
};
}, []);
const choose = async (b: BrainSummary) => {
const key = `${b.owner}/${b.name}`;
setPulling(key);
try {
const brain = await pullBrain(b.owner, b.name, b.version);
setPendingAgentBrain(brain);
router.replace({ pathname: "/demo", params: { kind: "agent" } });
} catch {
setError("Couldn't pull that brain. Try another.");
setPulling(null);
}
};
return (
<View style={styles.root}>
<Stack.Screen options={{ title: "Pick an agent brain", headerTitleAlign: "center" }} />
{loading ? (
<View style={styles.center}>
<ActivityIndicator color="#ff3b30" />
<Text style={styles.dim}>Loading brains</Text>
</View>
) : error ? (
<View style={styles.center}>
<Text style={styles.error}>{error}</Text>
</View>
) : (
<FlatList
data={brains}
keyExtractor={(b) => `${b.owner}/${b.name}`}
contentContainerStyle={styles.list}
ListHeaderComponent={
<Text style={styles.hint}>Pull a brain from ClawBrainHub to make an agent card.</Text>
}
renderItem={({ item }) => {
const key = `${item.owner}/${item.name}`;
return (
<Pressable
style={styles.row}
disabled={!!pulling}
onPress={() => choose(item)}
>
<View style={{ flex: 1 }}>
<Text style={styles.name} numberOfLines={1}>
{item.name}
</Text>
<Text style={styles.owner} numberOfLines={1}>
@{item.owner} · v{item.version}
{item.badge ? ` · ${item.badge}` : ""}
</Text>
</View>
{pulling === key ? (
<ActivityIndicator color="#ff3b30" />
) : (
<Text style={styles.pull}>Pull </Text>
)}
</Pressable>
);
}}
/>
)}
</View>
);
}
const styles = StyleSheet.create({
root: { flex: 1, backgroundColor: "#0a0a0c" },
center: { flex: 1, alignItems: "center", justifyContent: "center", gap: 12, padding: 32 },
dim: { color: "#9a9aa0" },
error: { color: "#ff453a", textAlign: "center", fontSize: 15 },
list: { padding: 16, gap: 10 },
hint: { color: "#9a9aa0", fontSize: 14, marginBottom: 8 },
row: {
flexDirection: "row",
alignItems: "center",
gap: 12,
backgroundColor: "#15151a",
borderRadius: 14,
paddingHorizontal: 16,
paddingVertical: 16,
},
name: { color: "#f5f5f7", fontSize: 16, fontWeight: "700" },
owner: { color: "#9a9aa0", fontSize: 13, marginTop: 2 },
pull: { color: "#ff3b30", fontWeight: "700", fontSize: 14 },
});