Claw Web commited on
Commit
0544bf7
·
1 Parent(s): 49cbb33

feat: Full Buddy companion pet system (6 modules matching original)

Browse files

- buddy/types.ts: Full type system (BuddyState, BuddyStats, evolution stages, XP rewards)
- buddy/sprites.ts: Sprite configs for 6 evolution stages x 10 moods
- buddy/companion.ts: State management, leveling, XP, evolution, persistence, decay
- buddy/prompt.ts: Personality, context-aware messages, 17 achievements
- buddy/useBuddyNotification.tsx: React hook for state/notifications/events
- buddy/index.ts: Barrel export
- BuddySprite.tsx: Full UI with stats panel, achievements, notifications, interactions
- Home.tsx: Integrated useBuddy hook with full props

client/src/buddy/companion.ts ADDED
@@ -0,0 +1,376 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ─── buddy/companion.ts — Matches original buddy/companion.ts ────────────
2
+ // State management, leveling, XP, evolution, persistence
3
+
4
+ import type {
5
+ BuddyState,
6
+ BuddyMood,
7
+ BuddyStats,
8
+ BuddyNotification,
9
+ BuddyAchievement,
10
+ } from "./types";
11
+ import { xpForLevel, stageForLevel, XP_REWARDS } from "./types";
12
+ import { ACHIEVEMENTS } from "./prompt";
13
+
14
+ const STORAGE_KEY = "claw-buddy-state";
15
+ const NOTIFICATIONS_KEY = "claw-buddy-notifications";
16
+
17
+ // ─── Default state ─────────────────────────────────────────────────────────
18
+
19
+ function defaultStats(): BuddyStats {
20
+ return {
21
+ level: 1,
22
+ xp: 0,
23
+ xpToNextLevel: xpForLevel(1),
24
+ totalToolCalls: 0,
25
+ totalSessions: 0,
26
+ totalFilesCreated: 0,
27
+ totalLinesWritten: 0,
28
+ totalBugsFixed: 0,
29
+ totalTestsPassed: 0,
30
+ streak: 0,
31
+ longestStreak: 0,
32
+ favoriteLanguage: "unknown",
33
+ languageStats: {},
34
+ };
35
+ }
36
+
37
+ function defaultState(): BuddyState {
38
+ const now = Date.now();
39
+ return {
40
+ name: "Buddy",
41
+ mood: "idle",
42
+ stage: "egg",
43
+ stats: defaultStats(),
44
+ happiness: 80,
45
+ energy: 100,
46
+ hunger: 100,
47
+ lastInteraction: now,
48
+ lastFed: now,
49
+ createdAt: now,
50
+ achievements: [],
51
+ outfit: "default",
52
+ accessories: [],
53
+ };
54
+ }
55
+
56
+ // ─── Persistence ───────────────────────────────────────────────────────────
57
+
58
+ export function loadBuddyState(): BuddyState {
59
+ try {
60
+ const raw = localStorage.getItem(STORAGE_KEY);
61
+ if (raw) {
62
+ const parsed = JSON.parse(raw) as BuddyState;
63
+ // Validate and migrate if needed
64
+ if (!parsed.stats) parsed.stats = defaultStats();
65
+ if (!parsed.achievements) parsed.achievements = [];
66
+ if (parsed.happiness === undefined) parsed.happiness = 80;
67
+ if (parsed.energy === undefined) parsed.energy = 100;
68
+ if (parsed.hunger === undefined) parsed.hunger = 100;
69
+ return parsed;
70
+ }
71
+ } catch {
72
+ // Corrupted state, reset
73
+ }
74
+ return defaultState();
75
+ }
76
+
77
+ export function saveBuddyState(state: BuddyState): void {
78
+ try {
79
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
80
+ } catch {
81
+ // Storage full or unavailable
82
+ }
83
+ }
84
+
85
+ export function loadNotifications(): BuddyNotification[] {
86
+ try {
87
+ const raw = localStorage.getItem(NOTIFICATIONS_KEY);
88
+ return raw ? JSON.parse(raw) : [];
89
+ } catch {
90
+ return [];
91
+ }
92
+ }
93
+
94
+ export function saveNotifications(notifications: BuddyNotification[]): void {
95
+ try {
96
+ // Keep only last 50 notifications
97
+ const trimmed = notifications.slice(-50);
98
+ localStorage.setItem(NOTIFICATIONS_KEY, JSON.stringify(trimmed));
99
+ } catch {}
100
+ }
101
+
102
+ // ─── XP and Leveling ───────────────────────────────────────────────────────
103
+
104
+ export function addXp(
105
+ state: BuddyState,
106
+ amount: number,
107
+ reason: string
108
+ ): { state: BuddyState; notifications: BuddyNotification[] } {
109
+ const notifications: BuddyNotification[] = [];
110
+ const newState = { ...state, stats: { ...state.stats } };
111
+
112
+ newState.stats.xp += amount;
113
+ newState.lastInteraction = Date.now();
114
+
115
+ // Level up check
116
+ while (newState.stats.xp >= newState.stats.xpToNextLevel) {
117
+ newState.stats.xp -= newState.stats.xpToNextLevel;
118
+ newState.stats.level += 1;
119
+ newState.stats.xpToNextLevel = xpForLevel(newState.stats.level);
120
+
121
+ // Check evolution
122
+ const newStage = stageForLevel(newState.stats.level);
123
+ const evolved = newStage !== newState.stage;
124
+ newState.stage = newStage;
125
+
126
+ notifications.push({
127
+ id: `levelup-${newState.stats.level}-${Date.now()}`,
128
+ type: "levelup",
129
+ title: `Level Up! 🎉`,
130
+ message: `Buddy reached level ${newState.stats.level}!`,
131
+ timestamp: Date.now(),
132
+ dismissed: false,
133
+ });
134
+
135
+ if (evolved) {
136
+ notifications.push({
137
+ id: `evolution-${newStage}-${Date.now()}`,
138
+ type: "evolution",
139
+ title: `Evolution! 🌟`,
140
+ message: `Buddy evolved to ${newStage}!`,
141
+ icon: "🌟",
142
+ timestamp: Date.now(),
143
+ dismissed: false,
144
+ });
145
+ }
146
+
147
+ // Happiness boost on level up
148
+ newState.happiness = Math.min(100, newState.happiness + 10);
149
+ newState.energy = Math.min(100, newState.energy + 5);
150
+ }
151
+
152
+ // Check achievements
153
+ const newAchievements = checkAchievements(newState);
154
+ for (const achievement of newAchievements) {
155
+ if (!newState.achievements.includes(achievement.id)) {
156
+ newState.achievements.push(achievement.id);
157
+ notifications.push({
158
+ id: `achievement-${achievement.id}-${Date.now()}`,
159
+ type: "achievement",
160
+ title: `Achievement Unlocked! 🏆`,
161
+ message: `${achievement.icon} ${achievement.name}: ${achievement.description}`,
162
+ icon: achievement.icon,
163
+ timestamp: Date.now(),
164
+ dismissed: false,
165
+ });
166
+ }
167
+ }
168
+
169
+ return { state: newState, notifications };
170
+ }
171
+
172
+ // ─── Event handlers ────────────────────────────────────────────────────────
173
+
174
+ export function onToolCall(
175
+ state: BuddyState,
176
+ toolName: string
177
+ ): { state: BuddyState; notifications: BuddyNotification[] } {
178
+ const newState = { ...state, stats: { ...state.stats } };
179
+ newState.stats.totalToolCalls += 1;
180
+
181
+ // Detect language from tool context
182
+ if (toolName === "write_file" || toolName === "edit_file") {
183
+ newState.stats.totalFilesCreated += 1;
184
+ }
185
+
186
+ return addXp(newState, XP_REWARDS.tool_call, `tool: ${toolName}`);
187
+ }
188
+
189
+ export function onFileCreated(
190
+ state: BuddyState,
191
+ filePath: string,
192
+ lines: number
193
+ ): { state: BuddyState; notifications: BuddyNotification[] } {
194
+ const newState = { ...state, stats: { ...state.stats } };
195
+ newState.stats.totalFilesCreated += 1;
196
+ newState.stats.totalLinesWritten += lines;
197
+
198
+ // Detect language
199
+ const ext = filePath.split(".").pop()?.toLowerCase() || "unknown";
200
+ const langMap: Record<string, string> = {
201
+ ts: "TypeScript", tsx: "TypeScript", js: "JavaScript", jsx: "JavaScript",
202
+ py: "Python", rs: "Rust", go: "Go", java: "Java", cpp: "C++", c: "C",
203
+ rb: "Ruby", php: "PHP", swift: "Swift", kt: "Kotlin", cs: "C#",
204
+ };
205
+ const lang = langMap[ext] || ext;
206
+ newState.stats.languageStats[lang] = (newState.stats.languageStats[lang] || 0) + lines;
207
+
208
+ // Update favorite language
209
+ let maxLines = 0;
210
+ for (const [l, count] of Object.entries(newState.stats.languageStats)) {
211
+ if (count > maxLines) {
212
+ maxLines = count;
213
+ newState.stats.favoriteLanguage = l;
214
+ }
215
+ }
216
+
217
+ let xp = XP_REWARDS.file_created;
218
+ // Bonus XP for milestones
219
+ if (newState.stats.totalLinesWritten >= 1000 && state.stats.totalLinesWritten < 1000) {
220
+ xp += XP_REWARDS.lines_1000;
221
+ } else if (newState.stats.totalLinesWritten >= 100 && state.stats.totalLinesWritten < 100) {
222
+ xp += XP_REWARDS.lines_100;
223
+ }
224
+
225
+ return addXp(newState, xp, `file: ${filePath}`);
226
+ }
227
+
228
+ export function onSessionCompleted(
229
+ state: BuddyState
230
+ ): { state: BuddyState; notifications: BuddyNotification[] } {
231
+ const newState = { ...state, stats: { ...state.stats } };
232
+ newState.stats.totalSessions += 1;
233
+
234
+ let xp = XP_REWARDS.session_completed;
235
+ if (newState.stats.totalSessions === 1) {
236
+ xp += XP_REWARDS.first_session;
237
+ }
238
+
239
+ return addXp(newState, xp, "session completed");
240
+ }
241
+
242
+ export function onBugFixed(
243
+ state: BuddyState
244
+ ): { state: BuddyState; notifications: BuddyNotification[] } {
245
+ const newState = { ...state, stats: { ...state.stats } };
246
+ newState.stats.totalBugsFixed += 1;
247
+ return addXp(newState, XP_REWARDS.bug_fixed, "bug fixed");
248
+ }
249
+
250
+ export function onTestPassed(
251
+ state: BuddyState
252
+ ): { state: BuddyState; notifications: BuddyNotification[] } {
253
+ const newState = { ...state, stats: { ...state.stats } };
254
+ newState.stats.totalTestsPassed += 1;
255
+ return addXp(newState, XP_REWARDS.test_passed, "test passed");
256
+ }
257
+
258
+ // ─── Mood calculation ──────────────────────────────────────────────────────
259
+
260
+ export function calculateMood(
261
+ state: BuddyState,
262
+ isStreaming: boolean,
263
+ hasMessages: boolean
264
+ ): BuddyMood {
265
+ const now = Date.now();
266
+ const idleMinutes = (now - state.lastInteraction) / 60000;
267
+
268
+ // Working state takes priority
269
+ if (isStreaming) return "working";
270
+
271
+ // Sleeping after 30 min idle
272
+ if (idleMinutes > 30) return "sleeping";
273
+
274
+ // Tired after 15 min idle
275
+ if (idleMinutes > 15) return "tired";
276
+
277
+ // Low happiness = confused
278
+ if (state.happiness < 30) return "confused";
279
+
280
+ // Low energy = tired
281
+ if (state.energy < 20) return "tired";
282
+
283
+ // Recent level up = celebrating
284
+ // (checked via notifications externally)
285
+
286
+ // Has messages = happy
287
+ if (hasMessages) return "happy";
288
+
289
+ return "idle";
290
+ }
291
+
292
+ // ─── Decay over time ───────────────────────────────────────────────────────
293
+
294
+ export function applyTimeDecay(state: BuddyState): BuddyState {
295
+ const now = Date.now();
296
+ const hoursSinceInteraction = (now - state.lastInteraction) / 3600000;
297
+ const hoursSinceFed = (now - state.lastFed) / 3600000;
298
+
299
+ const newState = { ...state };
300
+
301
+ // Hunger decreases over time (1 point per hour)
302
+ newState.hunger = Math.max(0, 100 - Math.floor(hoursSinceFed * 1));
303
+
304
+ // Happiness decreases if hungry or idle (0.5 per hour)
305
+ if (newState.hunger < 30 || hoursSinceInteraction > 4) {
306
+ newState.happiness = Math.max(0, state.happiness - Math.floor(hoursSinceInteraction * 0.5));
307
+ }
308
+
309
+ // Energy regenerates when sleeping (idle), decreases when working
310
+ if (hoursSinceInteraction > 1) {
311
+ newState.energy = Math.min(100, state.energy + Math.floor(hoursSinceInteraction * 2));
312
+ }
313
+
314
+ return newState;
315
+ }
316
+
317
+ export function feedBuddy(state: BuddyState): BuddyState {
318
+ return {
319
+ ...state,
320
+ hunger: 100,
321
+ happiness: Math.min(100, state.happiness + 15),
322
+ energy: Math.min(100, state.energy + 10),
323
+ lastFed: Date.now(),
324
+ lastInteraction: Date.now(),
325
+ };
326
+ }
327
+
328
+ export function petBuddy(state: BuddyState): BuddyState {
329
+ return {
330
+ ...state,
331
+ happiness: Math.min(100, state.happiness + 10),
332
+ lastInteraction: Date.now(),
333
+ };
334
+ }
335
+
336
+ // ─── Achievements ──────────────────────────────────────────────────────────
337
+
338
+ function checkAchievements(state: BuddyState): BuddyAchievement[] {
339
+ return ACHIEVEMENTS.filter(
340
+ (a) => !state.achievements.includes(a.id) && a.condition(state.stats)
341
+ );
342
+ }
343
+
344
+ // ─── Streak management ─────────────────────────────────────────────────────
345
+
346
+ export function updateStreak(state: BuddyState): BuddyState {
347
+ const now = new Date();
348
+ const lastDate = new Date(state.lastInteraction);
349
+
350
+ const isNewDay =
351
+ now.getFullYear() !== lastDate.getFullYear() ||
352
+ now.getMonth() !== lastDate.getMonth() ||
353
+ now.getDate() !== lastDate.getDate();
354
+
355
+ if (!isNewDay) return state;
356
+
357
+ const diffDays = Math.floor(
358
+ (now.getTime() - lastDate.getTime()) / 86400000
359
+ );
360
+
361
+ const newState = { ...state, stats: { ...state.stats } };
362
+
363
+ if (diffDays === 1) {
364
+ // Consecutive day
365
+ newState.stats.streak += 1;
366
+ newState.stats.longestStreak = Math.max(
367
+ newState.stats.longestStreak,
368
+ newState.stats.streak
369
+ );
370
+ } else if (diffDays > 1) {
371
+ // Streak broken
372
+ newState.stats.streak = 1;
373
+ }
374
+
375
+ return newState;
376
+ }
client/src/buddy/index.ts ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ // ─── buddy/index.ts — Barrel export ──────────────────────────────────────
2
+ export * from "./types";
3
+ export * from "./sprites";
4
+ export * from "./companion";
5
+ export * from "./prompt";
6
+ export { useBuddy } from "./useBuddyNotification";
7
+ export type { UseBuddyReturn } from "./useBuddyNotification";
client/src/buddy/prompt.ts ADDED
@@ -0,0 +1,283 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ─── buddy/prompt.ts — Matches original buddy/prompt.ts ──────────────────
2
+ // Personality, context-aware responses, achievements definitions
3
+
4
+ import type { BuddyAchievement, BuddyEvolutionStage, BuddyMood, BuddyStats } from "./types";
5
+
6
+ // ─── Context-aware messages per mood ───────────────────────────────────────
7
+
8
+ export const MOOD_MESSAGES: Record<BuddyMood, string[]> = {
9
+ idle: [
10
+ "Ready to code! 🚀",
11
+ "What shall we build?",
12
+ "I'm here to help!",
13
+ "Need anything?",
14
+ "Let's make something awesome!",
15
+ "Waiting for your command...",
16
+ "Got any bugs to squash? 🐛",
17
+ ],
18
+ thinking: [
19
+ "Hmm, let me think...",
20
+ "Processing...",
21
+ "Analyzing the code...",
22
+ "Working on it...",
23
+ "Almost there...",
24
+ ],
25
+ happy: [
26
+ "Great work! 🎉",
27
+ "That looks awesome!",
28
+ "We're on fire! 🔥",
29
+ "Keep it up!",
30
+ "Nailed it! 💪",
31
+ ],
32
+ working: [
33
+ "On it! ⚡",
34
+ "Executing...",
35
+ "Working hard!",
36
+ "Building...",
37
+ "Almost done!",
38
+ ],
39
+ sleeping: [
40
+ "zzz...",
41
+ "*snore*",
42
+ "💤",
43
+ "dreaming of code...",
44
+ ],
45
+ excited: [
46
+ "WOW! 🤩",
47
+ "This is amazing!",
48
+ "I can't wait!",
49
+ "Let's GOOO! 🚀",
50
+ "SO COOL!",
51
+ ],
52
+ confused: [
53
+ "Hmm, that's odd...",
54
+ "Not sure about this...",
55
+ "Let me check again...",
56
+ "Something's off...",
57
+ ],
58
+ proud: [
59
+ "Look what we built! ✨",
60
+ "We're getting better!",
61
+ "Level up! 🎮",
62
+ "Achievement unlocked!",
63
+ ],
64
+ tired: [
65
+ "Need a break...",
66
+ "Getting sleepy...",
67
+ "Maybe some coffee? ☕",
68
+ "*yawn*",
69
+ ],
70
+ celebrating: [
71
+ "WOOHOO! 🎊",
72
+ "PARTY TIME! 🥳",
73
+ "WE DID IT! 🏆",
74
+ "LEGENDARY! 🌟",
75
+ ],
76
+ };
77
+
78
+ // ─── Stage-specific greetings ──────────────────────────────────────────────
79
+
80
+ export const STAGE_GREETINGS: Record<BuddyEvolutionStage, string[]> = {
81
+ egg: [
82
+ "I'm still growing...",
83
+ "Almost ready to hatch!",
84
+ "Keep coding, I'll hatch soon!",
85
+ ],
86
+ baby: [
87
+ "Hi! I'm learning!",
88
+ "What does this code do?",
89
+ "I'm so excited to help!",
90
+ ],
91
+ junior: [
92
+ "Ready for action!",
93
+ "I know a few tricks now!",
94
+ "Let's build something cool!",
95
+ ],
96
+ senior: [
97
+ "I've seen a lot of code.",
98
+ "Trust the process.",
99
+ "Clean code is happy code.",
100
+ ],
101
+ master: [
102
+ "The code flows through me.",
103
+ "I sense a bug nearby...",
104
+ "Let's architect something great.",
105
+ ],
106
+ legendary: [
107
+ "I am one with the codebase.",
108
+ "All bugs tremble before us.",
109
+ "We've transcended mere coding.",
110
+ ],
111
+ };
112
+
113
+ // ─── Level up messages ─────────────────────────────────────────────────────
114
+
115
+ export function getLevelUpMessage(level: number): string {
116
+ if (level <= 5) return `Level ${level}! Keep going! 🌱`;
117
+ if (level <= 10) return `Level ${level}! Getting stronger! 💪`;
118
+ if (level <= 20) return `Level ${level}! You're a pro! ⚡`;
119
+ if (level <= 50) return `Level ${level}! Master coder! 👑`;
120
+ return `Level ${level}! LEGENDARY! 🌟`;
121
+ }
122
+
123
+ export function getEvolutionMessage(stage: BuddyEvolutionStage): string {
124
+ const messages: Record<BuddyEvolutionStage, string> = {
125
+ egg: "A mysterious egg appeared...",
126
+ baby: "The egg hatched! A baby coder is born! 🐣",
127
+ junior: "Buddy evolved into a Junior Developer! 🌱",
128
+ senior: "Buddy evolved into a Senior Developer! ⚡",
129
+ master: "Buddy evolved into a Master! 👑",
130
+ legendary: "Buddy achieved LEGENDARY status! 🌟✨",
131
+ };
132
+ return messages[stage];
133
+ }
134
+
135
+ // ─── Achievements ──────────────────────────────────────────────────────────
136
+
137
+ export const ACHIEVEMENTS: BuddyAchievement[] = [
138
+ {
139
+ id: "first_steps",
140
+ name: "First Steps",
141
+ description: "Complete your first session",
142
+ icon: "👣",
143
+ condition: (s: BuddyStats) => s.totalSessions >= 1,
144
+ },
145
+ {
146
+ id: "tool_master",
147
+ name: "Tool Master",
148
+ description: "Use 100 tool calls",
149
+ icon: "🔧",
150
+ condition: (s: BuddyStats) => s.totalToolCalls >= 100,
151
+ },
152
+ {
153
+ id: "file_creator",
154
+ name: "File Creator",
155
+ description: "Create 10 files",
156
+ icon: "📄",
157
+ condition: (s: BuddyStats) => s.totalFilesCreated >= 10,
158
+ },
159
+ {
160
+ id: "prolific_writer",
161
+ name: "Prolific Writer",
162
+ description: "Write 1,000 lines of code",
163
+ icon: "✍️",
164
+ condition: (s: BuddyStats) => s.totalLinesWritten >= 1000,
165
+ },
166
+ {
167
+ id: "novelist",
168
+ name: "Code Novelist",
169
+ description: "Write 10,000 lines of code",
170
+ icon: "📚",
171
+ condition: (s: BuddyStats) => s.totalLinesWritten >= 10000,
172
+ },
173
+ {
174
+ id: "bug_hunter",
175
+ name: "Bug Hunter",
176
+ description: "Fix 5 bugs",
177
+ icon: "🐛",
178
+ condition: (s: BuddyStats) => s.totalBugsFixed >= 5,
179
+ },
180
+ {
181
+ id: "exterminator",
182
+ name: "Exterminator",
183
+ description: "Fix 50 bugs",
184
+ icon: "🔫",
185
+ condition: (s: BuddyStats) => s.totalBugsFixed >= 50,
186
+ },
187
+ {
188
+ id: "test_lover",
189
+ name: "Test Lover",
190
+ description: "Pass 10 tests",
191
+ icon: "✅",
192
+ condition: (s: BuddyStats) => s.totalTestsPassed >= 10,
193
+ },
194
+ {
195
+ id: "streak_3",
196
+ name: "Consistent",
197
+ description: "3-day coding streak",
198
+ icon: "🔥",
199
+ condition: (s: BuddyStats) => s.streak >= 3,
200
+ },
201
+ {
202
+ id: "streak_7",
203
+ name: "Dedicated",
204
+ description: "7-day coding streak",
205
+ icon: "🔥🔥",
206
+ condition: (s: BuddyStats) => s.streak >= 7,
207
+ },
208
+ {
209
+ id: "streak_30",
210
+ name: "Unstoppable",
211
+ description: "30-day coding streak",
212
+ icon: "🔥🔥🔥",
213
+ condition: (s: BuddyStats) => s.streak >= 30,
214
+ },
215
+ {
216
+ id: "level_10",
217
+ name: "Rising Star",
218
+ description: "Reach level 10",
219
+ icon: "⭐",
220
+ condition: (s: BuddyStats) => s.level >= 10,
221
+ },
222
+ {
223
+ id: "level_25",
224
+ name: "Veteran",
225
+ description: "Reach level 25",
226
+ icon: "🌟",
227
+ condition: (s: BuddyStats) => s.level >= 25,
228
+ },
229
+ {
230
+ id: "level_50",
231
+ name: "Legend",
232
+ description: "Reach level 50",
233
+ icon: "👑",
234
+ condition: (s: BuddyStats) => s.level >= 50,
235
+ },
236
+ {
237
+ id: "polyglot",
238
+ name: "Polyglot",
239
+ description: "Write code in 5+ languages",
240
+ icon: "🌍",
241
+ condition: (s: BuddyStats) => Object.keys(s.languageStats).length >= 5,
242
+ },
243
+ {
244
+ id: "sessions_50",
245
+ name: "Regular",
246
+ description: "Complete 50 sessions",
247
+ icon: "📊",
248
+ condition: (s: BuddyStats) => s.totalSessions >= 50,
249
+ },
250
+ {
251
+ id: "tool_calls_1000",
252
+ name: "Power User",
253
+ description: "Use 1,000 tool calls",
254
+ icon: "⚡",
255
+ condition: (s: BuddyStats) => s.totalToolCalls >= 1000,
256
+ },
257
+ ];
258
+
259
+ // ─── Random message picker ─────────────────────────────────────────────────
260
+
261
+ export function getRandomMessage(mood: BuddyMood): string {
262
+ const messages = MOOD_MESSAGES[mood] || MOOD_MESSAGES.idle;
263
+ return messages[Math.floor(Math.random() * messages.length)];
264
+ }
265
+
266
+ export function getRandomGreeting(stage: BuddyEvolutionStage): string {
267
+ const greetings = STAGE_GREETINGS[stage] || STAGE_GREETINGS.baby;
268
+ return greetings[Math.floor(Math.random() * greetings.length)];
269
+ }
270
+
271
+ // ─── Stats summary for display ─────────────────────────────────────────────
272
+
273
+ export function formatStatsSummary(stats: BuddyStats): string {
274
+ return [
275
+ `Level ${stats.level} (${stats.xp}/${stats.xpToNextLevel} XP)`,
276
+ `${stats.totalToolCalls} tool calls`,
277
+ `${stats.totalFilesCreated} files created`,
278
+ `${stats.totalLinesWritten.toLocaleString()} lines written`,
279
+ `${stats.totalBugsFixed} bugs fixed`,
280
+ `${stats.streak}-day streak (best: ${stats.longestStreak})`,
281
+ `Favorite: ${stats.favoriteLanguage}`,
282
+ ].join(" | ");
283
+ }
client/src/buddy/sprites.ts ADDED
@@ -0,0 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ─── buddy/sprites.ts — Matches original buddy/sprites.ts ────────────────
2
+ // Sprite definitions for each evolution stage and mood
3
+
4
+ import type { BuddyEvolutionStage, BuddyMood, BuddySpriteConfig } from "./types";
5
+
6
+ // ─── Face sprites per stage + mood ─────────────────────────────────────────
7
+
8
+ const EGG_FACES: Record<BuddyMood, string> = {
9
+ idle: "🥚",
10
+ thinking: "🥚 ...",
11
+ happy: "🥚 ✨",
12
+ working: "🥚 💫",
13
+ sleeping: "🥚 💤",
14
+ excited: "🥚 ❗",
15
+ confused: "🥚 ❓",
16
+ proud: "🥚 ⭐",
17
+ tired: "🥚 😴",
18
+ celebrating: "🥚 🎉",
19
+ };
20
+
21
+ const BABY_FACES: Record<BuddyMood, string> = {
22
+ idle: "(• ◡ •)",
23
+ thinking: "(• ᴗ •) ...",
24
+ happy: "(◕ ᗜ ◕)",
25
+ working: "(⌐■_■)",
26
+ sleeping: "(- ᴗ -)ᶻᶻ",
27
+ excited: "(★ ᗜ ★)",
28
+ confused: "(• _ •)?",
29
+ proud: "(⌐■_■)b",
30
+ tired: "(- _ -)~",
31
+ celebrating: "\\(◕ ᗜ ◕)/",
32
+ };
33
+
34
+ const JUNIOR_FACES: Record<BuddyMood, string> = {
35
+ idle: "ᕙ(• ◡ •)ᕗ",
36
+ thinking: "(• ᴗ •)🔍",
37
+ happy: "ᕙ(◕ ᗜ ◕)ᕗ",
38
+ working: "(⌐■_■)⚡",
39
+ sleeping: "(- ᴗ -)ᶻᶻᶻ",
40
+ excited: "ᕙ(★ ᗜ ★)ᕗ",
41
+ confused: "(• _ •)❓",
42
+ proud: "(⌐■_■)✨",
43
+ tired: "(- _ -)💤",
44
+ celebrating: "\\(★ ᗜ ★)/🎉",
45
+ };
46
+
47
+ const SENIOR_FACES: Record<BuddyMood, string> = {
48
+ idle: "⚡(• ◡ •)⚡",
49
+ thinking: "🧠(• ᴗ •)",
50
+ happy: "⚡(◕ ᗜ ◕)⚡",
51
+ working: "💻(⌐■_■)💻",
52
+ sleeping: "🌙(- ᴗ -)🌙",
53
+ excited: "🔥(★ ᗜ ★)🔥",
54
+ confused: "🤔(• _ •)",
55
+ proud: "👑(⌐■_■)",
56
+ tired: "☕(- _ -)",
57
+ celebrating: "🎊\\(★ ᗜ ★)/🎊",
58
+ };
59
+
60
+ const MASTER_FACES: Record<BuddyMood, string> = {
61
+ idle: "👑⚡(◉ ◡ ◉)⚡👑",
62
+ thinking: "🧠⚡(◉ ᴗ ◉)⚡",
63
+ happy: "👑⚡(◉ ᗜ ◉)⚡👑",
64
+ working: "💻⚡(⌐◉_◉)⚡💻",
65
+ sleeping: "🌙✨(- ◡ -)✨🌙",
66
+ excited: "🔥⚡(★ ᗜ ★)⚡🔥",
67
+ confused: "🤔(◉ _ ◉)❓",
68
+ proud: "👑✨(⌐◉_◉)✨👑",
69
+ tired: "☕(- ◡ -)~",
70
+ celebrating: "🎊🔥\\(★ ᗜ ★)/🔥🎊",
71
+ };
72
+
73
+ const LEGENDARY_FACES: Record<BuddyMood, string> = {
74
+ idle: "🌟👑(◉ ◡ ◉)👑🌟",
75
+ thinking: "🧠💫(◉ ᴗ ◉)💫",
76
+ happy: "🌟👑(◉ ᗜ ◉)👑🌟",
77
+ working: "⚡💻(⌐◉_◉)💻⚡",
78
+ sleeping: "🌙🌟(- ◡ -)🌟🌙",
79
+ excited: "🌟🔥(★ ᗜ ★)🔥🌟",
80
+ confused: "🌟(◉ _ ◉)❓",
81
+ proud: "🌟👑✨(⌐◉_◉)✨👑🌟",
82
+ tired: "🌟☕(- ◡ -)☕🌟",
83
+ celebrating: "🌟🎊🔥\\(★ ᗜ ★)/🔥🎊🌟",
84
+ };
85
+
86
+ const STAGE_FACES: Record<BuddyEvolutionStage, Record<BuddyMood, string>> = {
87
+ egg: EGG_FACES,
88
+ baby: BABY_FACES,
89
+ junior: JUNIOR_FACES,
90
+ senior: SENIOR_FACES,
91
+ master: MASTER_FACES,
92
+ legendary: LEGENDARY_FACES,
93
+ };
94
+
95
+ // ─── Colors per stage ──────────────────────────────────────────────────────
96
+
97
+ const STAGE_COLORS: Record<BuddyEvolutionStage, string> = {
98
+ egg: "from-gray-400/20 to-gray-500/20 border-gray-400/30",
99
+ baby: "from-blue-500/20 to-cyan-500/20 border-blue-500/30",
100
+ junior: "from-green-500/20 to-emerald-500/20 border-green-500/30",
101
+ senior: "from-purple-500/20 to-pink-500/20 border-purple-500/30",
102
+ master: "from-orange-500/20 to-amber-500/20 border-orange-500/30",
103
+ legendary: "from-yellow-400/30 to-amber-400/30 border-yellow-400/50",
104
+ };
105
+
106
+ // ─── Mood-specific color overrides ─────────────────────────────────────────
107
+
108
+ const MOOD_COLOR_OVERRIDES: Partial<Record<BuddyMood, string>> = {
109
+ thinking: "from-purple-500/20 to-pink-500/20 border-purple-500/30",
110
+ working: "from-orange-500/20 to-amber-500/20 border-orange-500/30",
111
+ sleeping: "from-indigo-500/10 to-slate-500/10 border-indigo-500/20",
112
+ excited: "from-yellow-400/30 to-amber-400/30 border-yellow-400/50",
113
+ celebrating: "from-pink-500/20 to-rose-500/20 border-pink-500/30",
114
+ };
115
+
116
+ // ─── Size per stage ────────────────────────────────────────────────────────
117
+
118
+ export const STAGE_SIZE: Record<BuddyEvolutionStage, { width: string; height: string; fontSize: string }> = {
119
+ egg: { width: "w-10", height: "h-10", fontSize: "text-[9px]" },
120
+ baby: { width: "w-14", height: "h-14", fontSize: "text-[11px]" },
121
+ junior: { width: "w-16", height: "h-16", fontSize: "text-[12px]" },
122
+ senior: { width: "w-18", height: "h-18", fontSize: "text-[13px]" },
123
+ master: { width: "w-20", height: "h-20", fontSize: "text-[11px]" },
124
+ legendary: { width: "w-22", height: "h-22", fontSize: "text-[10px]" },
125
+ };
126
+
127
+ // ─── Animations per mood ───────────────────────────────────────────────────
128
+
129
+ export const MOOD_ANIMATIONS: Partial<Record<BuddyMood, string>> = {
130
+ working: "animate-pulse",
131
+ excited: "animate-bounce",
132
+ celebrating: "animate-bounce",
133
+ sleeping: "opacity-60",
134
+ };
135
+
136
+ // ─── Public API ────────────────────────────────────────────────────────────
137
+
138
+ export function getSpriteConfig(
139
+ stage: BuddyEvolutionStage,
140
+ mood: BuddyMood
141
+ ): BuddySpriteConfig {
142
+ const face = STAGE_FACES[stage]?.[mood] ?? STAGE_FACES.baby.idle;
143
+ const color = MOOD_COLOR_OVERRIDES[mood] ?? STAGE_COLORS[stage] ?? STAGE_COLORS.baby;
144
+ const animation = MOOD_ANIMATIONS[mood];
145
+
146
+ return {
147
+ face,
148
+ color,
149
+ animation,
150
+ accessorySlots: [],
151
+ };
152
+ }
153
+
154
+ export function getStageLabel(stage: BuddyEvolutionStage): string {
155
+ const labels: Record<BuddyEvolutionStage, string> = {
156
+ egg: "Egg",
157
+ baby: "Baby",
158
+ junior: "Junior Dev",
159
+ senior: "Senior Dev",
160
+ master: "Master",
161
+ legendary: "Legendary",
162
+ };
163
+ return labels[stage] ?? "Unknown";
164
+ }
client/src/buddy/types.ts ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ─── buddy/types.ts — Matches original buddy/types.ts ─────────────────────
2
+ // Full type system for the Claw Buddy companion pet
3
+
4
+ export type BuddyMood =
5
+ | "idle"
6
+ | "thinking"
7
+ | "happy"
8
+ | "working"
9
+ | "sleeping"
10
+ | "excited"
11
+ | "confused"
12
+ | "proud"
13
+ | "tired"
14
+ | "celebrating";
15
+
16
+ export type BuddyEvolutionStage =
17
+ | "egg" // Level 0-2: just hatched
18
+ | "baby" // Level 3-5: learning
19
+ | "junior" // Level 6-10: growing
20
+ | "senior" // Level 11-20: experienced
21
+ | "master" // Level 21-50: expert
22
+ | "legendary"; // Level 51+: legendary status
23
+
24
+ export interface BuddyStats {
25
+ level: number;
26
+ xp: number;
27
+ xpToNextLevel: number;
28
+ totalToolCalls: number;
29
+ totalSessions: number;
30
+ totalFilesCreated: number;
31
+ totalLinesWritten: number;
32
+ totalBugsFixed: number;
33
+ totalTestsPassed: number;
34
+ streak: number; // consecutive days of coding
35
+ longestStreak: number;
36
+ favoriteLanguage: string;
37
+ languageStats: Record<string, number>; // language -> lines
38
+ }
39
+
40
+ export interface BuddyState {
41
+ name: string;
42
+ mood: BuddyMood;
43
+ stage: BuddyEvolutionStage;
44
+ stats: BuddyStats;
45
+ happiness: number; // 0-100
46
+ energy: number; // 0-100
47
+ hunger: number; // 0-100 (decreases over time)
48
+ lastInteraction: number; // timestamp
49
+ lastFed: number; // timestamp
50
+ createdAt: number; // timestamp
51
+ achievements: string[]; // unlocked achievement IDs
52
+ outfit: string; // current outfit/skin
53
+ accessories: string[]; // equipped accessories
54
+ }
55
+
56
+ export interface BuddyAchievement {
57
+ id: string;
58
+ name: string;
59
+ description: string;
60
+ icon: string;
61
+ condition: (stats: BuddyStats) => boolean;
62
+ }
63
+
64
+ export interface BuddyNotification {
65
+ id: string;
66
+ type: "levelup" | "achievement" | "evolution" | "mood" | "streak" | "tip";
67
+ title: string;
68
+ message: string;
69
+ icon?: string;
70
+ timestamp: number;
71
+ dismissed: boolean;
72
+ }
73
+
74
+ export interface BuddySpriteConfig {
75
+ face: string;
76
+ color: string;
77
+ animation?: string;
78
+ accessorySlots: string[];
79
+ }
80
+
81
+ // XP rewards for different actions (matches original)
82
+ export const XP_REWARDS = {
83
+ tool_call: 5,
84
+ file_created: 10,
85
+ file_edited: 3,
86
+ bug_fixed: 25,
87
+ test_passed: 15,
88
+ session_completed: 20,
89
+ streak_day: 50,
90
+ first_session: 100,
91
+ lines_100: 10,
92
+ lines_1000: 50,
93
+ } as const;
94
+
95
+ // Level thresholds
96
+ export function xpForLevel(level: number): number {
97
+ // Quadratic scaling: level 1 = 100 XP, level 10 = 1000 XP, level 50 = 25000 XP
98
+ return Math.floor(100 * level * (1 + level * 0.1));
99
+ }
100
+
101
+ export function stageForLevel(level: number): BuddyEvolutionStage {
102
+ if (level <= 2) return "egg";
103
+ if (level <= 5) return "baby";
104
+ if (level <= 10) return "junior";
105
+ if (level <= 20) return "senior";
106
+ if (level <= 50) return "master";
107
+ return "legendary";
108
+ }
client/src/buddy/useBuddyNotification.tsx ADDED
@@ -0,0 +1,161 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ─── buddy/useBuddyNotification.tsx — Matches original ───────────────────
2
+ // React hook for buddy notifications, state management, and event handling
3
+
4
+ import { useState, useEffect, useCallback, useRef } from "react";
5
+ import type { BuddyState, BuddyMood, BuddyNotification } from "./types";
6
+ import {
7
+ loadBuddyState,
8
+ saveBuddyState,
9
+ loadNotifications,
10
+ saveNotifications,
11
+ onToolCall,
12
+ onFileCreated,
13
+ onSessionCompleted,
14
+ calculateMood,
15
+ applyTimeDecay,
16
+ feedBuddy,
17
+ petBuddy,
18
+ updateStreak,
19
+ } from "./companion";
20
+
21
+ export interface UseBuddyReturn {
22
+ state: BuddyState;
23
+ mood: BuddyMood;
24
+ notifications: BuddyNotification[];
25
+ undismissedCount: number;
26
+ // Actions
27
+ handleToolCall: (toolName: string) => void;
28
+ handleFileCreated: (filePath: string, lines: number) => void;
29
+ handleSessionCompleted: () => void;
30
+ feed: () => void;
31
+ pet: () => void;
32
+ dismissNotification: (id: string) => void;
33
+ dismissAll: () => void;
34
+ renameBuddy: (name: string) => void;
35
+ }
36
+
37
+ export function useBuddy(
38
+ isStreaming: boolean,
39
+ hasMessages: boolean
40
+ ): UseBuddyReturn {
41
+ const [state, setState] = useState<BuddyState>(() => loadBuddyState());
42
+ const [notifications, setNotifications] = useState<BuddyNotification[]>(
43
+ () => loadNotifications()
44
+ );
45
+ const stateRef = useRef(state);
46
+ stateRef.current = state;
47
+
48
+ // Apply time decay on mount and every 5 minutes
49
+ useEffect(() => {
50
+ const decayed = applyTimeDecay(state);
51
+ const streaked = updateStreak(decayed);
52
+ if (
53
+ streaked.hunger !== state.hunger ||
54
+ streaked.happiness !== state.happiness ||
55
+ streaked.energy !== state.energy ||
56
+ streaked.stats.streak !== state.stats.streak
57
+ ) {
58
+ setState(streaked);
59
+ saveBuddyState(streaked);
60
+ }
61
+
62
+ const interval = setInterval(() => {
63
+ const current = stateRef.current;
64
+ const updated = applyTimeDecay(current);
65
+ if (
66
+ updated.hunger !== current.hunger ||
67
+ updated.happiness !== current.happiness ||
68
+ updated.energy !== current.energy
69
+ ) {
70
+ setState(updated);
71
+ saveBuddyState(updated);
72
+ }
73
+ }, 300_000); // 5 minutes
74
+
75
+ return () => clearInterval(interval);
76
+ // eslint-disable-next-line react-hooks/exhaustive-deps
77
+ }, []);
78
+
79
+ // Calculate mood reactively
80
+ const mood = calculateMood(state, isStreaming, hasMessages);
81
+
82
+ // Persist state changes
83
+ useEffect(() => {
84
+ saveBuddyState(state);
85
+ }, [state]);
86
+
87
+ useEffect(() => {
88
+ saveNotifications(notifications);
89
+ }, [notifications]);
90
+
91
+ // ─── Event handlers ────────────────────────────────────────────────────
92
+
93
+ const addNotifications = useCallback((newNotifs: BuddyNotification[]) => {
94
+ if (newNotifs.length > 0) {
95
+ setNotifications((prev) => [...prev, ...newNotifs]);
96
+ }
97
+ }, []);
98
+
99
+ const handleToolCall = useCallback(
100
+ (toolName: string) => {
101
+ const result = onToolCall(stateRef.current, toolName);
102
+ setState(result.state);
103
+ addNotifications(result.notifications);
104
+ },
105
+ [addNotifications]
106
+ );
107
+
108
+ const handleFileCreated = useCallback(
109
+ (filePath: string, lines: number) => {
110
+ const result = onFileCreated(stateRef.current, filePath, lines);
111
+ setState(result.state);
112
+ addNotifications(result.notifications);
113
+ },
114
+ [addNotifications]
115
+ );
116
+
117
+ const handleSessionCompleted = useCallback(() => {
118
+ const result = onSessionCompleted(stateRef.current);
119
+ setState(result.state);
120
+ addNotifications(result.notifications);
121
+ }, [addNotifications]);
122
+
123
+ const feed = useCallback(() => {
124
+ setState((prev) => feedBuddy(prev));
125
+ }, []);
126
+
127
+ const pet = useCallback(() => {
128
+ setState((prev) => petBuddy(prev));
129
+ }, []);
130
+
131
+ const dismissNotification = useCallback((id: string) => {
132
+ setNotifications((prev) =>
133
+ prev.map((n) => (n.id === id ? { ...n, dismissed: true } : n))
134
+ );
135
+ }, []);
136
+
137
+ const dismissAll = useCallback(() => {
138
+ setNotifications((prev) => prev.map((n) => ({ ...n, dismissed: true })));
139
+ }, []);
140
+
141
+ const renameBuddy = useCallback((name: string) => {
142
+ setState((prev) => ({ ...prev, name: name.trim() || "Buddy" }));
143
+ }, []);
144
+
145
+ const undismissedCount = notifications.filter((n) => !n.dismissed).length;
146
+
147
+ return {
148
+ state,
149
+ mood,
150
+ notifications,
151
+ undismissedCount,
152
+ handleToolCall,
153
+ handleFileCreated,
154
+ handleSessionCompleted,
155
+ feed,
156
+ pet,
157
+ dismissNotification,
158
+ dismissAll,
159
+ renameBuddy,
160
+ };
161
+ }
client/src/components/BuddySprite.tsx CHANGED
@@ -1,45 +1,48 @@
1
- import { useState, useEffect } from "react";
 
2
  import { cn } from "@/lib/utils";
 
 
 
3
 
4
  interface BuddySpriteProps {
 
 
 
 
5
  visible: boolean;
6
- mood?: "idle" | "thinking" | "happy" | "working" | "sleeping";
7
  position?: "bottom-right" | "bottom-left";
 
 
 
 
 
8
  onClick?: () => void;
9
  }
10
 
11
- const MOOD_FACES: Record<string, string> = {
12
- idle: "(• ◡ •)",
13
- thinking: "(• ᴗ •) ...",
14
- happy: "(◕ ᗜ ◕)",
15
- working: "(⌐■_■)",
16
- sleeping: "(- ᴗ -)ᶻᶻ",
17
- };
18
-
19
- const MOOD_COLORS: Record<string, string> = {
20
- idle: "from-blue-500/20 to-cyan-500/20 border-blue-500/30",
21
- thinking: "from-purple-500/20 to-pink-500/20 border-purple-500/30",
22
- happy: "from-green-500/20 to-emerald-500/20 border-green-500/30",
23
- working: "from-orange-500/20 to-amber-500/20 border-orange-500/30",
24
- sleeping: "from-indigo-500/10 to-slate-500/10 border-indigo-500/20",
25
- };
26
-
27
- const IDLE_MESSAGES = [
28
- "Ready to code!",
29
- "What shall we build?",
30
- "I'm here to help!",
31
- "Need anything?",
32
- "Let's go! 🚀",
33
- ];
34
-
35
  export function BuddySprite({
 
 
 
 
36
  visible,
37
- mood = "idle",
38
  position = "bottom-right",
 
 
 
 
 
39
  onClick,
40
  }: BuddySpriteProps) {
41
  const [message, setMessage] = useState("");
42
  const [bobOffset, setBobOffset] = useState(0);
 
 
 
 
 
 
 
43
 
44
  // Gentle bobbing animation
45
  useEffect(() => {
@@ -52,68 +55,345 @@ export function BuddySprite({
52
  return () => clearInterval(interval);
53
  }, [visible, mood]);
54
 
55
- // Random idle messages
56
  useEffect(() => {
57
- if (!visible || mood !== "idle") {
58
  setMessage("");
59
  return;
60
  }
61
- const showMessage = () => {
62
- const msg = IDLE_MESSAGES[Math.floor(Math.random() * IDLE_MESSAGES.length)];
 
 
63
  setMessage(msg);
64
- setTimeout(() => setMessage(""), 3000);
65
  };
66
- const interval = setInterval(showMessage, 15000);
67
- // Show first message after 2s
68
- const timeout = setTimeout(showMessage, 2000);
69
  return () => {
70
  clearInterval(interval);
71
  clearTimeout(timeout);
72
  };
73
- }, [visible, mood]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
 
75
  if (!visible) return null;
76
 
77
- return (
78
- <div
79
- className={cn(
80
- "fixed z-40 flex flex-col items-center gap-1 cursor-pointer select-none transition-all duration-300",
81
- position === "bottom-right" ? "right-6 bottom-6" : "left-6 bottom-6"
82
- )}
83
- style={{ transform: `translateY(${bobOffset}px)` }}
84
- onClick={onClick}
85
- title="Claw Buddy — Click to interact"
86
- >
87
- {/* Speech bubble */}
88
- {message && (
89
- <div className="bg-popover text-popover-foreground text-xs px-3 py-1.5 rounded-lg shadow-md border border-border max-w-[160px] text-center animate-in fade-in slide-in-from-bottom-2 duration-200">
90
- {message}
91
- <div
92
- className={cn(
93
- "absolute -bottom-1 w-2 h-2 bg-popover border-b border-r border-border rotate-45",
94
- "left-1/2 -translate-x-1/2"
95
- )}
96
- />
97
- </div>
98
- )}
99
 
100
- {/* Buddy body */}
 
 
101
  <div
102
  className={cn(
103
- "w-14 h-14 rounded-2xl bg-gradient-to-br border flex items-center justify-center shadow-lg backdrop-blur-sm transition-colors duration-500",
104
- MOOD_COLORS[mood] || MOOD_COLORS.idle,
105
- mood === "working" && "animate-pulse"
106
  )}
 
 
 
107
  >
108
- <span className="text-[11px] font-mono text-foreground/80 whitespace-nowrap">
109
- {MOOD_FACES[mood] || MOOD_FACES.idle}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
  </span>
111
  </div>
112
 
113
- {/* Label */}
114
- <span className="text-[9px] text-muted-foreground/60 font-medium">
115
- Buddy
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
  </span>
117
  </div>
118
  );
119
  }
 
 
 
 
 
 
 
 
 
 
1
+ // ─── BuddySprite.tsx Full companion pet UI (matches original 6-module system) ──
2
+ import { useState, useEffect, useCallback } from "react";
3
  import { cn } from "@/lib/utils";
4
+ import type { BuddyState, BuddyMood, BuddyEvolutionStage, BuddyNotification } from "@/buddy/types";
5
+ import { getSpriteConfig, STAGE_SIZE, getStageLabel } from "@/buddy/sprites";
6
+ import { getRandomMessage, getRandomGreeting, formatStatsSummary, ACHIEVEMENTS } from "@/buddy/prompt";
7
 
8
  interface BuddySpriteProps {
9
+ state: BuddyState;
10
+ mood: BuddyMood;
11
+ notifications: BuddyNotification[];
12
+ undismissedCount: number;
13
  visible: boolean;
 
14
  position?: "bottom-right" | "bottom-left";
15
+ onFeed: () => void;
16
+ onPet: () => void;
17
+ onDismissNotification: (id: string) => void;
18
+ onDismissAll: () => void;
19
+ onRename: (name: string) => void;
20
  onClick?: () => void;
21
  }
22
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  export function BuddySprite({
24
+ state,
25
+ mood,
26
+ notifications,
27
+ undismissedCount,
28
  visible,
 
29
  position = "bottom-right",
30
+ onFeed,
31
+ onPet,
32
+ onDismissNotification,
33
+ onDismissAll,
34
+ onRename,
35
  onClick,
36
  }: BuddySpriteProps) {
37
  const [message, setMessage] = useState("");
38
  const [bobOffset, setBobOffset] = useState(0);
39
+ const [showPanel, setShowPanel] = useState(false);
40
+ const [panelTab, setPanelTab] = useState<"stats" | "achievements" | "notifications">("stats");
41
+ const [isRenaming, setIsRenaming] = useState(false);
42
+ const [renameValue, setRenameValue] = useState(state.name);
43
+
44
+ const sprite = getSpriteConfig(state.stage, mood);
45
+ const sizeConfig = STAGE_SIZE[state.stage] || STAGE_SIZE.baby;
46
 
47
  // Gentle bobbing animation
48
  useEffect(() => {
 
55
  return () => clearInterval(interval);
56
  }, [visible, mood]);
57
 
58
+ // Random messages
59
  useEffect(() => {
60
+ if (!visible || mood === "sleeping" || mood === "working") {
61
  setMessage("");
62
  return;
63
  }
64
+ const showMsg = () => {
65
+ const msg = Math.random() > 0.5
66
+ ? getRandomMessage(mood)
67
+ : getRandomGreeting(state.stage);
68
  setMessage(msg);
69
+ setTimeout(() => setMessage(""), 4000);
70
  };
71
+ const interval = setInterval(showMsg, 12000);
72
+ const timeout = setTimeout(showMsg, 2000);
 
73
  return () => {
74
  clearInterval(interval);
75
  clearTimeout(timeout);
76
  };
77
+ }, [visible, mood, state.stage]);
78
+
79
+ // Show notification messages
80
+ useEffect(() => {
81
+ const latest = notifications.filter((n) => !n.dismissed).slice(-1)[0];
82
+ if (latest) {
83
+ setMessage(`${latest.icon || "🔔"} ${latest.title}`);
84
+ const t = setTimeout(() => setMessage(""), 5000);
85
+ return () => clearTimeout(t);
86
+ }
87
+ }, [notifications]);
88
+
89
+ const handleClick = useCallback(() => {
90
+ if (onClick) onClick();
91
+ setShowPanel((prev) => !prev);
92
+ }, [onClick]);
93
+
94
+ const handleRenameSubmit = useCallback(() => {
95
+ onRename(renameValue);
96
+ setIsRenaming(false);
97
+ }, [onRename, renameValue]);
98
 
99
  if (!visible) return null;
100
 
101
+ const xpPercent = state.stats.xpToNextLevel > 0
102
+ ? Math.round((state.stats.xp / state.stats.xpToNextLevel) * 100)
103
+ : 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
 
105
+ return (
106
+ <>
107
+ {/* Main buddy sprite */}
108
  <div
109
  className={cn(
110
+ "fixed z-50 flex flex-col items-center gap-1 cursor-pointer select-none transition-all duration-300",
111
+ position === "bottom-right" ? "right-6 bottom-6" : "left-6 bottom-6"
 
112
  )}
113
+ style={{ transform: `translateY(${bobOffset}px)` }}
114
+ onClick={handleClick}
115
+ title={`${state.name} — Level ${state.stats.level} ${getStageLabel(state.stage)} — Click to interact`}
116
  >
117
+ {/* Speech bubble */}
118
+ {message && (
119
+ <div className="bg-popover text-popover-foreground text-xs px-3 py-1.5 rounded-lg shadow-md border border-border max-w-[180px] text-center animate-in fade-in slide-in-from-bottom-2 duration-200">
120
+ {message}
121
+ <div
122
+ className={cn(
123
+ "absolute -bottom-1 w-2 h-2 bg-popover border-b border-r border-border rotate-45",
124
+ "left-1/2 -translate-x-1/2"
125
+ )}
126
+ />
127
+ </div>
128
+ )}
129
+
130
+ {/* Notification badge */}
131
+ {undismissedCount > 0 && (
132
+ <div className="absolute -top-1 -right-1 bg-red-500 text-white text-[9px] font-bold rounded-full w-4 h-4 flex items-center justify-center z-10">
133
+ {undismissedCount > 9 ? "9+" : undismissedCount}
134
+ </div>
135
+ )}
136
+
137
+ {/* Buddy body */}
138
+ <div
139
+ className={cn(
140
+ "rounded-2xl bg-gradient-to-br border flex items-center justify-center shadow-lg backdrop-blur-sm transition-all duration-500",
141
+ sizeConfig.width,
142
+ sizeConfig.height,
143
+ sprite.color,
144
+ sprite.animation
145
+ )}
146
+ >
147
+ <span className={cn("font-mono text-foreground/80 whitespace-nowrap", sizeConfig.fontSize)}>
148
+ {sprite.face}
149
+ </span>
150
+ </div>
151
+
152
+ {/* XP bar */}
153
+ <div className="w-full max-w-[56px] h-1 bg-muted/30 rounded-full overflow-hidden">
154
+ <div
155
+ className="h-full bg-gradient-to-r from-blue-500 to-cyan-500 rounded-full transition-all duration-500"
156
+ style={{ width: `${xpPercent}%` }}
157
+ />
158
+ </div>
159
+
160
+ {/* Label */}
161
+ <span className="text-[9px] text-muted-foreground/60 font-medium">
162
+ {state.name} Lv.{state.stats.level}
163
  </span>
164
  </div>
165
 
166
+ {/* Interaction panel */}
167
+ {showPanel && (
168
+ <div
169
+ className={cn(
170
+ "fixed z-50 bg-popover text-popover-foreground rounded-xl shadow-2xl border border-border w-[320px] max-h-[420px] overflow-hidden animate-in fade-in slide-in-from-bottom-4 duration-300",
171
+ position === "bottom-right" ? "right-6 bottom-28" : "left-6 bottom-28"
172
+ )}
173
+ >
174
+ {/* Header */}
175
+ <div className="p-3 border-b border-border bg-muted/30">
176
+ <div className="flex items-center justify-between">
177
+ <div className="flex items-center gap-2">
178
+ <span className="text-lg">{sprite.face}</span>
179
+ <div>
180
+ {isRenaming ? (
181
+ <div className="flex gap-1">
182
+ <input
183
+ className="text-sm font-bold bg-background border border-border rounded px-1 w-24"
184
+ value={renameValue}
185
+ onChange={(e) => setRenameValue(e.target.value)}
186
+ onKeyDown={(e) => e.key === "Enter" && handleRenameSubmit()}
187
+ autoFocus
188
+ />
189
+ <button
190
+ className="text-xs text-muted-foreground hover:text-foreground"
191
+ onClick={handleRenameSubmit}
192
+ >
193
+ OK
194
+ </button>
195
+ </div>
196
+ ) : (
197
+ <span
198
+ className="text-sm font-bold cursor-pointer hover:underline"
199
+ onClick={() => setIsRenaming(true)}
200
+ >
201
+ {state.name}
202
+ </span>
203
+ )}
204
+ <div className="text-[10px] text-muted-foreground">
205
+ {getStageLabel(state.stage)} — Level {state.stats.level}
206
+ </div>
207
+ </div>
208
+ </div>
209
+ <button
210
+ className="text-muted-foreground hover:text-foreground text-sm"
211
+ onClick={() => setShowPanel(false)}
212
+ >
213
+ X
214
+ </button>
215
+ </div>
216
+
217
+ {/* Status bars */}
218
+ <div className="mt-2 space-y-1">
219
+ <StatusBar label="XP" value={xpPercent} color="from-blue-500 to-cyan-500" text={`${state.stats.xp}/${state.stats.xpToNextLevel}`} />
220
+ <StatusBar label="HP" value={state.happiness} color="from-green-500 to-emerald-500" />
221
+ <StatusBar label="EN" value={state.energy} color="from-yellow-500 to-amber-500" />
222
+ <StatusBar label="FD" value={state.hunger} color="from-orange-500 to-red-500" />
223
+ </div>
224
+
225
+ {/* Action buttons */}
226
+ <div className="flex gap-2 mt-2">
227
+ <button
228
+ className="flex-1 text-xs bg-green-500/20 hover:bg-green-500/30 border border-green-500/30 rounded-lg py-1 transition-colors"
229
+ onClick={onFeed}
230
+ >
231
+ Feed
232
+ </button>
233
+ <button
234
+ className="flex-1 text-xs bg-pink-500/20 hover:bg-pink-500/30 border border-pink-500/30 rounded-lg py-1 transition-colors"
235
+ onClick={onPet}
236
+ >
237
+ Pet
238
+ </button>
239
+ </div>
240
+ </div>
241
+
242
+ {/* Tabs */}
243
+ <div className="flex border-b border-border">
244
+ {(["stats", "achievements", "notifications"] as const).map((tab) => (
245
+ <button
246
+ key={tab}
247
+ className={cn(
248
+ "flex-1 text-xs py-2 transition-colors",
249
+ panelTab === tab
250
+ ? "text-foreground border-b-2 border-primary"
251
+ : "text-muted-foreground hover:text-foreground"
252
+ )}
253
+ onClick={() => setPanelTab(tab)}
254
+ >
255
+ {tab === "stats" ? "Stats" : tab === "achievements" ? "Achievements" : `Notifs (${undismissedCount})`}
256
+ </button>
257
+ ))}
258
+ </div>
259
+
260
+ {/* Tab content */}
261
+ <div className="p-3 overflow-y-auto max-h-[200px] text-xs">
262
+ {panelTab === "stats" && (
263
+ <div className="space-y-2">
264
+ <StatRow label="Total Tool Calls" value={state.stats.totalToolCalls.toLocaleString()} />
265
+ <StatRow label="Files Created" value={state.stats.totalFilesCreated.toLocaleString()} />
266
+ <StatRow label="Lines Written" value={state.stats.totalLinesWritten.toLocaleString()} />
267
+ <StatRow label="Bugs Fixed" value={state.stats.totalBugsFixed.toLocaleString()} />
268
+ <StatRow label="Tests Passed" value={state.stats.totalTestsPassed.toLocaleString()} />
269
+ <StatRow label="Sessions" value={state.stats.totalSessions.toLocaleString()} />
270
+ <StatRow label="Streak" value={`${state.stats.streak} days (best: ${state.stats.longestStreak})`} />
271
+ <StatRow label="Favorite Language" value={state.stats.favoriteLanguage} />
272
+ {Object.keys(state.stats.languageStats).length > 0 && (
273
+ <div className="mt-2 pt-2 border-t border-border">
274
+ <div className="text-muted-foreground mb-1">Languages:</div>
275
+ {Object.entries(state.stats.languageStats)
276
+ .sort(([, a], [, b]) => b - a)
277
+ .slice(0, 8)
278
+ .map(([lang, lines]) => (
279
+ <StatRow key={lang} label={lang} value={`${lines.toLocaleString()} lines`} />
280
+ ))}
281
+ </div>
282
+ )}
283
+ </div>
284
+ )}
285
+
286
+ {panelTab === "achievements" && (
287
+ <div className="space-y-2">
288
+ {ACHIEVEMENTS.map((a) => {
289
+ const unlocked = state.achievements.includes(a.id);
290
+ return (
291
+ <div
292
+ key={a.id}
293
+ className={cn(
294
+ "flex items-center gap-2 p-2 rounded-lg border",
295
+ unlocked
296
+ ? "bg-green-500/10 border-green-500/30"
297
+ : "bg-muted/20 border-border opacity-50"
298
+ )}
299
+ >
300
+ <span className="text-base">{a.icon}</span>
301
+ <div>
302
+ <div className="font-medium">{a.name}</div>
303
+ <div className="text-muted-foreground text-[10px]">{a.description}</div>
304
+ </div>
305
+ {unlocked && <span className="ml-auto text-green-500 text-xs">Unlocked</span>}
306
+ </div>
307
+ );
308
+ })}
309
+ </div>
310
+ )}
311
+
312
+ {panelTab === "notifications" && (
313
+ <div className="space-y-2">
314
+ {undismissedCount > 0 && (
315
+ <button
316
+ className="text-[10px] text-muted-foreground hover:text-foreground underline"
317
+ onClick={onDismissAll}
318
+ >
319
+ Dismiss all
320
+ </button>
321
+ )}
322
+ {notifications.length === 0 && (
323
+ <div className="text-muted-foreground text-center py-4">No notifications yet</div>
324
+ )}
325
+ {[...notifications]
326
+ .reverse()
327
+ .slice(0, 20)
328
+ .map((n) => (
329
+ <div
330
+ key={n.id}
331
+ className={cn(
332
+ "flex items-start gap-2 p-2 rounded-lg border",
333
+ n.dismissed ? "opacity-40 border-border" : "bg-blue-500/10 border-blue-500/30"
334
+ )}
335
+ >
336
+ <span className="text-sm">{n.icon || "🔔"}</span>
337
+ <div className="flex-1">
338
+ <div className="font-medium">{n.title}</div>
339
+ <div className="text-muted-foreground text-[10px]">{n.message}</div>
340
+ <div className="text-muted-foreground/50 text-[9px] mt-0.5">
341
+ {new Date(n.timestamp).toLocaleTimeString()}
342
+ </div>
343
+ </div>
344
+ {!n.dismissed && (
345
+ <button
346
+ className="text-muted-foreground hover:text-foreground text-[10px]"
347
+ onClick={() => onDismissNotification(n.id)}
348
+ >
349
+ X
350
+ </button>
351
+ )}
352
+ </div>
353
+ ))}
354
+ </div>
355
+ )}
356
+ </div>
357
+ </div>
358
+ )}
359
+ </>
360
+ );
361
+ }
362
+
363
+ // ─── Helper components ─────────────────────────────────────────────────────
364
+
365
+ function StatusBar({
366
+ label,
367
+ value,
368
+ color,
369
+ text,
370
+ }: {
371
+ label: string;
372
+ value: number;
373
+ color: string;
374
+ text?: string;
375
+ }) {
376
+ return (
377
+ <div className="flex items-center gap-1.5">
378
+ <span className="text-[9px] text-muted-foreground w-5 text-right font-mono">{label}</span>
379
+ <div className="flex-1 h-1.5 bg-muted/30 rounded-full overflow-hidden">
380
+ <div
381
+ className={cn("h-full bg-gradient-to-r rounded-full transition-all duration-500", color)}
382
+ style={{ width: `${Math.max(0, Math.min(100, value))}%` }}
383
+ />
384
+ </div>
385
+ <span className="text-[9px] text-muted-foreground w-8 font-mono">
386
+ {text || `${value}%`}
387
  </span>
388
  </div>
389
  );
390
  }
391
+
392
+ function StatRow({ label, value }: { label: string; value: string }) {
393
+ return (
394
+ <div className="flex justify-between items-center">
395
+ <span className="text-muted-foreground">{label}</span>
396
+ <span className="font-mono">{value}</span>
397
+ </div>
398
+ );
399
+ }
client/src/pages/Home.tsx CHANGED
@@ -13,6 +13,7 @@ import { CronManagerPanel } from "@/components/CronManagerPanel";
13
  import { PluginMarketplace } from "@/components/PluginMarketplace";
14
  import { ThinkingBlock, extractThinkingBlocks } from "@/components/ThinkingBlock";
15
  import { BuddySprite } from "@/components/BuddySprite";
 
16
  import { useChat, type ChatMessage } from "@/hooks/useChat";
17
  import {
18
  Zap,
@@ -58,6 +59,9 @@ export default function Home() {
58
  const [pluginMarketplaceOpen, setPluginMarketplaceOpen] = useState(false);
59
  const [buddyVisible, setBuddyVisible] = useState(true);
60
 
 
 
 
61
  // Plan mode & effort state
62
  const [effortLevel, setEffortLevel] = useState<EffortLevel>("high");
63
  const [planMode, setPlanMode] = useState(false);
@@ -998,9 +1002,16 @@ export default function Home() {
998
 
999
  {/* Buddy Sprite */}
1000
  <BuddySprite
 
 
 
 
1001
  visible={buddyVisible}
1002
- mood={isStreaming ? "working" : messages.length === 0 ? "idle" : "happy"}
1003
- onClick={() => setBuddyVisible(false)}
 
 
 
1004
  />
1005
 
1006
  {/* Plugin Marketplace panel */}
 
13
  import { PluginMarketplace } from "@/components/PluginMarketplace";
14
  import { ThinkingBlock, extractThinkingBlocks } from "@/components/ThinkingBlock";
15
  import { BuddySprite } from "@/components/BuddySprite";
16
+ import { useBuddy } from "@/buddy";
17
  import { useChat, type ChatMessage } from "@/hooks/useChat";
18
  import {
19
  Zap,
 
59
  const [pluginMarketplaceOpen, setPluginMarketplaceOpen] = useState(false);
60
  const [buddyVisible, setBuddyVisible] = useState(true);
61
 
62
+ // Buddy companion pet system
63
+ const buddy = useBuddy(isStreaming, messages.length > 0);
64
+
65
  // Plan mode & effort state
66
  const [effortLevel, setEffortLevel] = useState<EffortLevel>("high");
67
  const [planMode, setPlanMode] = useState(false);
 
1002
 
1003
  {/* Buddy Sprite */}
1004
  <BuddySprite
1005
+ state={buddy.state}
1006
+ mood={buddy.mood}
1007
+ notifications={buddy.notifications}
1008
+ undismissedCount={buddy.undismissedCount}
1009
  visible={buddyVisible}
1010
+ onFeed={buddy.feed}
1011
+ onPet={buddy.pet}
1012
+ onDismissNotification={buddy.dismissNotification}
1013
+ onDismissAll={buddy.dismissAll}
1014
+ onRename={buddy.renameBuddy}
1015
  />
1016
 
1017
  {/* Plugin Marketplace panel */}