mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-21 15:55:50 +00:00
Merge branch 'pr/amDosion/82'
This commit is contained in:
110
src/buddy/CompanionCard.tsx
Normal file
110
src/buddy/CompanionCard.tsx
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
/**
|
||||||
|
* Companion display card — shown by /buddy (no args).
|
||||||
|
* Mirrors official vc8 component: bordered box with sprite, stats, last reaction.
|
||||||
|
*/
|
||||||
|
import React from 'react';
|
||||||
|
import { Box, Text } from '../ink.js';
|
||||||
|
import { useInput } from '../ink.js';
|
||||||
|
import { renderSprite } from './sprites.js';
|
||||||
|
import { RARITY_COLORS, RARITY_STARS, STAT_NAMES, type Companion } from './types.js';
|
||||||
|
|
||||||
|
const CARD_WIDTH = 40;
|
||||||
|
const CARD_PADDING_X = 2;
|
||||||
|
|
||||||
|
function StatBar({ name, value }: { name: string; value: number }) {
|
||||||
|
const clamped = Math.max(0, Math.min(100, value));
|
||||||
|
const filled = Math.round(clamped / 10);
|
||||||
|
const bar = '\u2588'.repeat(filled) + '\u2591'.repeat(10 - filled);
|
||||||
|
return (
|
||||||
|
<Text>
|
||||||
|
{name.padEnd(10)} {bar} {String(value).padStart(3)}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CompanionCard({
|
||||||
|
companion,
|
||||||
|
lastReaction,
|
||||||
|
onDone,
|
||||||
|
}: {
|
||||||
|
companion: Companion;
|
||||||
|
lastReaction?: string;
|
||||||
|
onDone?: (result?: string, options?: { display?: string }) => void;
|
||||||
|
}) {
|
||||||
|
const color = RARITY_COLORS[companion.rarity];
|
||||||
|
const stars = RARITY_STARS[companion.rarity];
|
||||||
|
const sprite = renderSprite(companion, 0);
|
||||||
|
|
||||||
|
// Press any key to dismiss
|
||||||
|
useInput(
|
||||||
|
() => {
|
||||||
|
onDone?.(undefined, { display: 'skip' });
|
||||||
|
},
|
||||||
|
{ isActive: onDone !== undefined },
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
flexDirection="column"
|
||||||
|
borderStyle="round"
|
||||||
|
borderColor={color}
|
||||||
|
paddingX={CARD_PADDING_X}
|
||||||
|
paddingY={1}
|
||||||
|
width={CARD_WIDTH}
|
||||||
|
flexShrink={0}
|
||||||
|
>
|
||||||
|
{/* Header: rarity + species */}
|
||||||
|
<Box justifyContent="space-between">
|
||||||
|
<Text bold color={color}>
|
||||||
|
{stars} {companion.rarity.toUpperCase()}
|
||||||
|
</Text>
|
||||||
|
<Text color={color}>{companion.species.toUpperCase()}</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Shiny indicator */}
|
||||||
|
{companion.shiny && (
|
||||||
|
<Text color="warning" bold>
|
||||||
|
{'\u2728'} SHINY {'\u2728'}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Sprite */}
|
||||||
|
<Box flexDirection="column" marginY={1}>
|
||||||
|
{sprite.map((line, i) => (
|
||||||
|
<Text key={i} color={color}>
|
||||||
|
{line}
|
||||||
|
</Text>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Name */}
|
||||||
|
<Text bold>{companion.name}</Text>
|
||||||
|
|
||||||
|
{/* Personality */}
|
||||||
|
<Box marginY={1}>
|
||||||
|
<Text dimColor italic>
|
||||||
|
"{companion.personality}"
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Stats */}
|
||||||
|
<Box flexDirection="column">
|
||||||
|
{STAT_NAMES.map(name => (
|
||||||
|
<StatBar key={name} name={name} value={companion.stats[name] ?? 0} />
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Last reaction */}
|
||||||
|
{lastReaction && (
|
||||||
|
<Box flexDirection="column" marginTop={1}>
|
||||||
|
<Text dimColor>last said</Text>
|
||||||
|
<Box borderStyle="round" borderColor="inactive" paddingX={1}>
|
||||||
|
<Text dimColor italic>
|
||||||
|
{lastReaction}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
156
src/buddy/companionReact.ts
Normal file
156
src/buddy/companionReact.ts
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
/**
|
||||||
|
* Companion reaction system — aligns with official ZUK + Dc8 pattern.
|
||||||
|
*
|
||||||
|
* Called from REPL.tsx after each query turn. Checks mute state, frequency
|
||||||
|
* limits, and @-mention detection, then calls the buddy_react API to
|
||||||
|
* generate a reaction shown in the CompanionSprite speech bubble.
|
||||||
|
*/
|
||||||
|
import { getCompanion } from './companion.js'
|
||||||
|
import { getGlobalConfig } from '../utils/config.js'
|
||||||
|
import { getClaudeAIOAuthTokens } from '../utils/auth.js'
|
||||||
|
import { getOauthConfig } from '../constants/oauth.js'
|
||||||
|
import { getUserAgent } from '../utils/http.js'
|
||||||
|
import type { Message } from '../types/message.js'
|
||||||
|
|
||||||
|
// ─── Rate limiting ──────────────────────────────────
|
||||||
|
|
||||||
|
let lastReactTime = 0
|
||||||
|
const MIN_INTERVAL_MS = 45_000 // official is roughly 30-60s
|
||||||
|
|
||||||
|
// ─── Recent reactions (avoid repetition) ────────────
|
||||||
|
|
||||||
|
const recentReactions: string[] = []
|
||||||
|
const MAX_RECENT = 8
|
||||||
|
|
||||||
|
// ─── Public API ─────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trigger a companion reaction after a query turn.
|
||||||
|
*
|
||||||
|
* Mirrors official `ZUK()`:
|
||||||
|
* 1. Check companion exists and is not muted
|
||||||
|
* 2. Detect if user @-mentioned companion by name
|
||||||
|
* 3. Apply rate limiting (skip if not addressed and too soon)
|
||||||
|
* 4. Build conversation transcript
|
||||||
|
* 5. Call buddy_react API
|
||||||
|
* 6. Pass reaction text to setReaction callback
|
||||||
|
*/
|
||||||
|
export function triggerCompanionReaction(
|
||||||
|
messages: Message[],
|
||||||
|
setReaction: (text: string | undefined) => void,
|
||||||
|
): void {
|
||||||
|
const companion = getCompanion()
|
||||||
|
if (!companion || getGlobalConfig().companionMuted) return
|
||||||
|
|
||||||
|
const addressed = isAddressed(messages, companion.name)
|
||||||
|
|
||||||
|
const now = Date.now()
|
||||||
|
if (!addressed && now - lastReactTime < MIN_INTERVAL_MS) return
|
||||||
|
|
||||||
|
const transcript = buildTranscript(messages)
|
||||||
|
if (!transcript.trim()) return
|
||||||
|
|
||||||
|
lastReactTime = now
|
||||||
|
|
||||||
|
void callBuddyReactAPI(companion, transcript, addressed)
|
||||||
|
.then(reaction => {
|
||||||
|
if (!reaction) return
|
||||||
|
recentReactions.push(reaction)
|
||||||
|
if (recentReactions.length > MAX_RECENT) recentReactions.shift()
|
||||||
|
setReaction(reaction)
|
||||||
|
})
|
||||||
|
.catch(() => {})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Helpers ────────────────────────────────────────
|
||||||
|
|
||||||
|
function isAddressed(messages: Message[], name: string): boolean {
|
||||||
|
const pattern = new RegExp(`\\b${escapeRegex(name)}\\b`, 'i')
|
||||||
|
for (
|
||||||
|
let i = messages.length - 1;
|
||||||
|
i >= Math.max(0, messages.length - 3);
|
||||||
|
i--
|
||||||
|
) {
|
||||||
|
const m = messages[i]
|
||||||
|
if (m?.type !== 'user') continue
|
||||||
|
const content = (m as any).message?.content
|
||||||
|
if (typeof content === 'string' && pattern.test(content)) return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeRegex(s: string): string {
|
||||||
|
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildTranscript(messages: Message[]): string {
|
||||||
|
return messages
|
||||||
|
.slice(-12)
|
||||||
|
.filter(m => m.type === 'user' || m.type === 'assistant')
|
||||||
|
.map(m => {
|
||||||
|
const role = m.type === 'user' ? 'user' : 'claude'
|
||||||
|
const content = (m as any).message?.content
|
||||||
|
const text =
|
||||||
|
typeof content === 'string'
|
||||||
|
? content.slice(0, 300)
|
||||||
|
: Array.isArray(content)
|
||||||
|
? content
|
||||||
|
.filter((b: any) => b?.type === 'text')
|
||||||
|
.map((b: any) => b.text)
|
||||||
|
.join(' ')
|
||||||
|
.slice(0, 300)
|
||||||
|
: ''
|
||||||
|
return `${role}: ${text}`
|
||||||
|
})
|
||||||
|
.join('\n')
|
||||||
|
.slice(0, 5000)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── API call ───────────────────────────────────────
|
||||||
|
|
||||||
|
async function callBuddyReactAPI(
|
||||||
|
companion: {
|
||||||
|
name: string
|
||||||
|
personality: string
|
||||||
|
species: string
|
||||||
|
rarity: string
|
||||||
|
stats: Record<string, number>
|
||||||
|
},
|
||||||
|
transcript: string,
|
||||||
|
addressed: boolean,
|
||||||
|
): Promise<string | null> {
|
||||||
|
const tokens = getClaudeAIOAuthTokens()
|
||||||
|
if (!tokens?.accessToken) return null
|
||||||
|
|
||||||
|
const orgId = getGlobalConfig().oauthAccount?.organizationUuid
|
||||||
|
if (!orgId) return null
|
||||||
|
|
||||||
|
const baseUrl = getOauthConfig().BASE_API_URL
|
||||||
|
const url = `${baseUrl}/api/organizations/${orgId}/claude_code/buddy_react`
|
||||||
|
|
||||||
|
const resp = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${tokens.accessToken}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'User-Agent': getUserAgent(),
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: companion.name.slice(0, 32),
|
||||||
|
personality: companion.personality.slice(0, 200),
|
||||||
|
species: companion.species,
|
||||||
|
rarity: companion.rarity,
|
||||||
|
stats: companion.stats,
|
||||||
|
transcript,
|
||||||
|
reason: addressed ? 'addressed' : 'turn',
|
||||||
|
recent: recentReactions.map(r => r.slice(0, 200)),
|
||||||
|
addressed,
|
||||||
|
}),
|
||||||
|
signal: AbortSignal.timeout(10_000),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!resp.ok) return null
|
||||||
|
|
||||||
|
const data = (await resp.json()) as { reaction?: string }
|
||||||
|
return data.reaction?.trim() || null
|
||||||
|
}
|
||||||
@@ -1,16 +1,19 @@
|
|||||||
|
import React from 'react'
|
||||||
import {
|
import {
|
||||||
getCompanion,
|
getCompanion,
|
||||||
rollWithSeed,
|
rollWithSeed,
|
||||||
generateSeed,
|
generateSeed,
|
||||||
} from '../../buddy/companion.js'
|
} from '../../buddy/companion.js'
|
||||||
import {
|
import { type StoredCompanion, RARITY_STARS } from '../../buddy/types.js'
|
||||||
type StoredCompanion,
|
|
||||||
RARITY_STARS,
|
|
||||||
STAT_NAMES,
|
|
||||||
} from '../../buddy/types.js'
|
|
||||||
import { renderSprite } from '../../buddy/sprites.js'
|
import { renderSprite } from '../../buddy/sprites.js'
|
||||||
|
import { CompanionCard } from '../../buddy/CompanionCard.js'
|
||||||
import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js'
|
import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js'
|
||||||
import type { LocalCommandCall } from '../../types/command.js'
|
import { triggerCompanionReaction } from '../../buddy/companionReact.js'
|
||||||
|
import type { ToolUseContext } from '../../Tool.js'
|
||||||
|
import type {
|
||||||
|
LocalJSXCommandContext,
|
||||||
|
LocalJSXCommandOnDone,
|
||||||
|
} from '../../types/command.js'
|
||||||
|
|
||||||
// Species → default name fragments for hatch (no API needed)
|
// Species → default name fragments for hatch (no API needed)
|
||||||
const SPECIES_NAMES: Record<string, string> = {
|
const SPECIES_NAMES: Record<string, string> = {
|
||||||
@@ -39,198 +42,128 @@ const SPECIES_PERSONALITY: Record<string, string> = {
|
|||||||
goose: 'Assertive and honks at bad code. Takes no prisoners in code reviews.',
|
goose: 'Assertive and honks at bad code. Takes no prisoners in code reviews.',
|
||||||
blob: 'Adaptable and goes with the flow. Sometimes splits into two when confused.',
|
blob: 'Adaptable and goes with the flow. Sometimes splits into two when confused.',
|
||||||
cat: 'Independent and judgmental. Watches you type with mild disdain.',
|
cat: 'Independent and judgmental. Watches you type with mild disdain.',
|
||||||
dragon: 'Fiery and passionate about architecture. Hoards good variable names.',
|
dragon:
|
||||||
octopus: 'Multitasker extraordinaire. Wraps tentacles around every problem at once.',
|
'Fiery and passionate about architecture. Hoards good variable names.',
|
||||||
|
octopus:
|
||||||
|
'Multitasker extraordinaire. Wraps tentacles around every problem at once.',
|
||||||
owl: 'Wise but verbose. Always says "let me think about that" for exactly 3 seconds.',
|
owl: 'Wise but verbose. Always says "let me think about that" for exactly 3 seconds.',
|
||||||
penguin: 'Cool under pressure. Slides gracefully through merge conflicts.',
|
penguin: 'Cool under pressure. Slides gracefully through merge conflicts.',
|
||||||
turtle: 'Patient and thorough. Believes slow and steady wins the deploy.',
|
turtle: 'Patient and thorough. Believes slow and steady wins the deploy.',
|
||||||
snail: 'Methodical and leaves a trail of useful comments. Never rushes.',
|
snail: 'Methodical and leaves a trail of useful comments. Never rushes.',
|
||||||
ghost: 'Ethereal and appears at the worst possible moments with spooky insights.',
|
ghost:
|
||||||
|
'Ethereal and appears at the worst possible moments with spooky insights.',
|
||||||
axolotl: 'Regenerative and cheerful. Recovers from any bug with a smile.',
|
axolotl: 'Regenerative and cheerful. Recovers from any bug with a smile.',
|
||||||
capybara: 'Zen master. Remains calm while everything around is on fire.',
|
capybara: 'Zen master. Remains calm while everything around is on fire.',
|
||||||
cactus: 'Prickly on the outside but full of good intentions. Thrives on neglect.',
|
cactus:
|
||||||
|
'Prickly on the outside but full of good intentions. Thrives on neglect.',
|
||||||
robot: 'Efficient and literal. Processes feedback in binary.',
|
robot: 'Efficient and literal. Processes feedback in binary.',
|
||||||
rabbit: 'Energetic and hops between tasks. Finishes before you start.',
|
rabbit: 'Energetic and hops between tasks. Finishes before you start.',
|
||||||
mushroom: 'Quietly insightful. Grows on you over time.',
|
mushroom: 'Quietly insightful. Grows on you over time.',
|
||||||
chonk: 'Big, warm, and takes up the whole couch. Prioritizes comfort over elegance.',
|
chonk:
|
||||||
|
'Big, warm, and takes up the whole couch. Prioritizes comfort over elegance.',
|
||||||
}
|
}
|
||||||
|
|
||||||
function speciesLabel(species: string): string {
|
function speciesLabel(species: string): string {
|
||||||
return species.charAt(0).toUpperCase() + species.slice(1)
|
return species.charAt(0).toUpperCase() + species.slice(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderStats(stats: Record<string, number>): string {
|
export async function call(
|
||||||
const lines = STAT_NAMES.map(name => {
|
onDone: LocalJSXCommandOnDone,
|
||||||
const val = stats[name] ?? 0
|
context: ToolUseContext & LocalJSXCommandContext,
|
||||||
const filled = Math.round(val / 5)
|
args: string,
|
||||||
const bar = '█'.repeat(filled) + '░'.repeat(20 - filled)
|
): Promise<React.ReactNode> {
|
||||||
return ` ${name.padEnd(10)} ${bar} ${val}`
|
const sub = args?.trim().toLowerCase() ?? ''
|
||||||
})
|
const setState = context.setAppState
|
||||||
return lines.join('\n')
|
|
||||||
}
|
|
||||||
|
|
||||||
export const call: LocalCommandCall = async (args, _context) => {
|
// ── /buddy off — mute companion ──
|
||||||
const sub = args.trim().toLowerCase()
|
if (sub === 'off') {
|
||||||
const config = getGlobalConfig()
|
saveGlobalConfig(cfg => ({ ...cfg, companionMuted: true }))
|
||||||
|
onDone('companion muted', { display: 'system' })
|
||||||
// /buddy — show current companion or hint to hatch
|
return null
|
||||||
if (sub === '') {
|
|
||||||
const companion = getCompanion()
|
|
||||||
if (!companion) {
|
|
||||||
return {
|
|
||||||
type: 'text',
|
|
||||||
value:
|
|
||||||
"You don't have a companion yet! Use /buddy hatch to get one.",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const stars = RARITY_STARS[companion.rarity]
|
|
||||||
const sprite = renderSprite(companion, 0)
|
|
||||||
const shiny = companion.shiny ? ' ✨ Shiny!' : ''
|
|
||||||
|
|
||||||
const lines = [
|
|
||||||
sprite.join('\n'),
|
|
||||||
'',
|
|
||||||
` ${companion.name} the ${speciesLabel(companion.species)}${shiny}`,
|
|
||||||
` Rarity: ${stars} (${companion.rarity})`,
|
|
||||||
` Eye: ${companion.eye} Hat: ${companion.hat}`,
|
|
||||||
companion.personality ? `\n "${companion.personality}"` : '',
|
|
||||||
'',
|
|
||||||
' Stats:',
|
|
||||||
renderStats(companion.stats),
|
|
||||||
'',
|
|
||||||
' Commands: /buddy pet /buddy mute /buddy unmute /buddy hatch /buddy rehatch',
|
|
||||||
]
|
|
||||||
return { type: 'text', value: lines.join('\n') }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// /buddy hatch — create a new companion
|
// ── /buddy on — unmute companion ──
|
||||||
if (sub === 'hatch') {
|
if (sub === 'on') {
|
||||||
if (config.companion) {
|
saveGlobalConfig(cfg => ({ ...cfg, companionMuted: false }))
|
||||||
return {
|
onDone('companion unmuted', { display: 'system' })
|
||||||
type: 'text',
|
return null
|
||||||
value: `You already have a companion! Use /buddy to see it.\n(Tip: /buddy hatch again will re-roll a new one.)`,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const seed = generateSeed()
|
|
||||||
const r = rollWithSeed(seed)
|
|
||||||
const name = SPECIES_NAMES[r.bones.species] ?? 'Buddy'
|
|
||||||
const personality =
|
|
||||||
SPECIES_PERSONALITY[r.bones.species] ?? 'Mysterious and code-savvy.'
|
|
||||||
|
|
||||||
const stored: StoredCompanion = {
|
|
||||||
name,
|
|
||||||
personality,
|
|
||||||
seed,
|
|
||||||
hatchedAt: Date.now(),
|
|
||||||
}
|
|
||||||
|
|
||||||
saveGlobalConfig(cfg => ({ ...cfg, companion: stored }))
|
|
||||||
|
|
||||||
const stars = RARITY_STARS[r.bones.rarity]
|
|
||||||
const sprite = renderSprite(r.bones, 0)
|
|
||||||
const shiny = r.bones.shiny ? ' ✨ Shiny!' : ''
|
|
||||||
|
|
||||||
const lines = [
|
|
||||||
' 🎉 A wild companion appeared!',
|
|
||||||
'',
|
|
||||||
sprite.join('\n'),
|
|
||||||
'',
|
|
||||||
` ${name} the ${speciesLabel(r.bones.species)}${shiny}`,
|
|
||||||
` Rarity: ${stars} (${r.bones.rarity})`,
|
|
||||||
` "${personality}"`,
|
|
||||||
'',
|
|
||||||
' Your companion will now appear beside your input box!',
|
|
||||||
]
|
|
||||||
return { type: 'text', value: lines.join('\n') }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// /buddy pet — trigger heart animation
|
// ── /buddy pet — trigger heart animation + auto unmute ──
|
||||||
if (sub === 'pet') {
|
if (sub === 'pet') {
|
||||||
const companion = getCompanion()
|
const companion = getCompanion()
|
||||||
if (!companion) {
|
if (!companion) {
|
||||||
return {
|
onDone('no companion yet \u00b7 run /buddy first', { display: 'system' })
|
||||||
type: 'text',
|
return null
|
||||||
value:
|
|
||||||
"You don't have a companion yet! Use /buddy hatch to get one.",
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
// Auto-unmute on pet + trigger heart animation
|
||||||
const { setAppState } = await import('../../state/AppStateStore.js')
|
|
||||||
setAppState(prev => ({
|
|
||||||
...prev,
|
|
||||||
companionPetAt: Date.now(),
|
|
||||||
}))
|
|
||||||
} catch {
|
|
||||||
// non-interactive mode — AppState not available
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: 'text',
|
|
||||||
value: ` ${renderSprite(companion, 0).join('\n')}\n\n ${companion.name} purrs happily! ♥`,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// /buddy mute
|
|
||||||
if (sub === 'mute') {
|
|
||||||
if (config.companionMuted) {
|
|
||||||
return { type: 'text', value: ' Companion is already muted.' }
|
|
||||||
}
|
|
||||||
saveGlobalConfig(cfg => ({ ...cfg, companionMuted: true }))
|
|
||||||
return { type: 'text', value: ' Companion muted. It will hide quietly. Use /buddy unmute to bring it back.' }
|
|
||||||
}
|
|
||||||
|
|
||||||
// /buddy unmute
|
|
||||||
if (sub === 'unmute') {
|
|
||||||
if (!config.companionMuted) {
|
|
||||||
return { type: 'text', value: ' Companion is not muted.' }
|
|
||||||
}
|
|
||||||
saveGlobalConfig(cfg => ({ ...cfg, companionMuted: false }))
|
saveGlobalConfig(cfg => ({ ...cfg, companionMuted: false }))
|
||||||
return { type: 'text', value: ' Companion unmuted! Welcome back.' }
|
setState?.(prev => ({ ...prev, companionPetAt: Date.now() }))
|
||||||
|
|
||||||
|
// Trigger a post-pet reaction
|
||||||
|
triggerCompanionReaction((context as any).messages ?? [], reaction =>
|
||||||
|
setState?.(prev =>
|
||||||
|
prev.companionReaction === reaction
|
||||||
|
? prev
|
||||||
|
: { ...prev, companionReaction: reaction },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
onDone(`petted ${companion.name}`, { display: 'system' })
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
// /buddy rehatch — re-roll a new companion (replaces existing)
|
// ── /buddy (no args) — show existing or hatch ──
|
||||||
if (sub === 'rehatch') {
|
const companion = getCompanion()
|
||||||
const seed = generateSeed()
|
|
||||||
const r = rollWithSeed(seed)
|
|
||||||
const name = SPECIES_NAMES[r.bones.species] ?? 'Buddy'
|
|
||||||
const personality =
|
|
||||||
SPECIES_PERSONALITY[r.bones.species] ?? 'Mysterious and code-savvy.'
|
|
||||||
|
|
||||||
const stored: StoredCompanion = {
|
// Auto-unmute when viewing
|
||||||
name,
|
if (companion && getGlobalConfig().companionMuted) {
|
||||||
personality,
|
saveGlobalConfig(cfg => ({ ...cfg, companionMuted: false }))
|
||||||
seed,
|
|
||||||
hatchedAt: Date.now(),
|
|
||||||
}
|
|
||||||
|
|
||||||
saveGlobalConfig(cfg => ({ ...cfg, companion: stored }))
|
|
||||||
|
|
||||||
const stars = RARITY_STARS[r.bones.rarity]
|
|
||||||
const sprite = renderSprite(r.bones, 0)
|
|
||||||
const shiny = r.bones.shiny ? ' ✨ Shiny!' : ''
|
|
||||||
|
|
||||||
const lines = [
|
|
||||||
' 🎉 A new companion appeared!',
|
|
||||||
'',
|
|
||||||
sprite.join('\n'),
|
|
||||||
'',
|
|
||||||
` ${name} the ${speciesLabel(r.bones.species)}${shiny}`,
|
|
||||||
` Rarity: ${stars} (${r.bones.rarity})`,
|
|
||||||
` "${personality}"`,
|
|
||||||
'',
|
|
||||||
' Your old companion has been replaced!',
|
|
||||||
]
|
|
||||||
return { type: 'text', value: lines.join('\n') }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unknown subcommand
|
if (companion) {
|
||||||
return {
|
// Return JSX card — matches official vc8 component
|
||||||
type: 'text',
|
const lastReaction = context.getAppState?.()?.companionReaction
|
||||||
value:
|
return React.createElement(CompanionCard, {
|
||||||
' Unknown command: /buddy ' +
|
companion,
|
||||||
sub +
|
lastReaction,
|
||||||
'\n Commands: /buddy (info) /buddy hatch /buddy rehatch /buddy pet /buddy mute /buddy unmute',
|
onDone,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── No companion → hatch ──
|
||||||
|
const seed = generateSeed()
|
||||||
|
const r = rollWithSeed(seed)
|
||||||
|
const name = SPECIES_NAMES[r.bones.species] ?? 'Buddy'
|
||||||
|
const personality =
|
||||||
|
SPECIES_PERSONALITY[r.bones.species] ?? 'Mysterious and code-savvy.'
|
||||||
|
|
||||||
|
const stored: StoredCompanion = {
|
||||||
|
name,
|
||||||
|
personality,
|
||||||
|
seed,
|
||||||
|
hatchedAt: Date.now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
saveGlobalConfig(cfg => ({ ...cfg, companion: stored }))
|
||||||
|
|
||||||
|
const stars = RARITY_STARS[r.bones.rarity]
|
||||||
|
const sprite = renderSprite(r.bones, 0)
|
||||||
|
const shiny = r.bones.shiny ? ' \u2728 Shiny!' : ''
|
||||||
|
|
||||||
|
const lines = [
|
||||||
|
'A wild companion appeared!',
|
||||||
|
'',
|
||||||
|
...sprite,
|
||||||
|
'',
|
||||||
|
`${name} the ${speciesLabel(r.bones.species)}${shiny}`,
|
||||||
|
`Rarity: ${stars} (${r.bones.rarity})`,
|
||||||
|
`"${personality}"`,
|
||||||
|
'',
|
||||||
|
'Your companion will now appear beside your input box!',
|
||||||
|
'Say its name to get its take \u00b7 /buddy pet \u00b7 /buddy off',
|
||||||
|
]
|
||||||
|
onDone(lines.join('\n'), { display: 'system' })
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
import type { Command } from '../../commands.js'
|
import type { Command } from '../../commands.js'
|
||||||
|
import { isBuddyLive } from '../../buddy/useBuddyNotification.js'
|
||||||
|
|
||||||
const buddy = {
|
const buddy = {
|
||||||
type: 'local',
|
type: 'local-jsx',
|
||||||
name: 'buddy',
|
name: 'buddy',
|
||||||
description: 'View and manage your companion buddy',
|
description: 'Hatch a coding companion · pet, off',
|
||||||
supportsNonInteractive: false,
|
argumentHint: '[pet|off]',
|
||||||
|
immediate: true,
|
||||||
|
get isHidden() {
|
||||||
|
return !isBuddyLive()
|
||||||
|
},
|
||||||
load: () => import('./buddy.js'),
|
load: () => import('./buddy.js'),
|
||||||
} satisfies Command
|
} satisfies Command
|
||||||
|
|
||||||
|
|||||||
@@ -275,6 +275,7 @@ const WebBrowserPanelModule = feature('WEB_BROWSER_TOOL') ? require('../tools/We
|
|||||||
import { IssueFlagBanner } from '../components/PromptInput/IssueFlagBanner.js';
|
import { IssueFlagBanner } from '../components/PromptInput/IssueFlagBanner.js';
|
||||||
import { useIssueFlagBanner } from '../hooks/useIssueFlagBanner.js';
|
import { useIssueFlagBanner } from '../hooks/useIssueFlagBanner.js';
|
||||||
import { CompanionSprite, CompanionFloatingBubble, MIN_COLS_FOR_FULL_SPRITE } from '../buddy/CompanionSprite.js';
|
import { CompanionSprite, CompanionFloatingBubble, MIN_COLS_FOR_FULL_SPRITE } from '../buddy/CompanionSprite.js';
|
||||||
|
import { triggerCompanionReaction } from '../buddy/companionReact.js';
|
||||||
import { DevBar } from '../components/DevBar.js';
|
import { DevBar } from '../components/DevBar.js';
|
||||||
// Session manager removed - using AppState now
|
// Session manager removed - using AppState now
|
||||||
import type { RemoteSessionConfig } from '../remote/RemoteSessionManager.js';
|
import type { RemoteSessionConfig } from '../remote/RemoteSessionManager.js';
|
||||||
@@ -2805,12 +2806,13 @@ export function REPL({
|
|||||||
})) {
|
})) {
|
||||||
onQueryEvent(event);
|
onQueryEvent(event);
|
||||||
}
|
}
|
||||||
// TODO: implement fireCompanionObserver — companion model reaction after each query turn
|
if (feature('BUDDY')) {
|
||||||
if (feature('BUDDY') && typeof fireCompanionObserver === 'function') {
|
triggerCompanionReaction(messagesRef.current, reaction =>
|
||||||
void fireCompanionObserver(messagesRef.current, reaction => setAppState(prev => prev.companionReaction === reaction ? prev : {
|
setAppState(prev => prev.companionReaction === reaction ? prev : {
|
||||||
...prev,
|
...prev,
|
||||||
companionReaction: reaction as string | undefined
|
companionReaction: reaction as string | undefined,
|
||||||
}));
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
queryCheckpoint('query_end');
|
queryCheckpoint('query_end');
|
||||||
|
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ export type AppState = DeepImmutable<{
|
|||||||
foregroundedTaskId?: string
|
foregroundedTaskId?: string
|
||||||
// Task ID of in-process teammate whose transcript is being viewed (undefined = leader's view)
|
// Task ID of in-process teammate whose transcript is being viewed (undefined = leader's view)
|
||||||
viewingAgentTaskId?: string
|
viewingAgentTaskId?: string
|
||||||
// Latest companion reaction from the friend observer (src/buddy/observer.ts)
|
// Latest companion reaction from buddy_react API (src/buddy/companionReact.ts)
|
||||||
companionReaction?: string
|
companionReaction?: string
|
||||||
// Timestamp of last /buddy pet — CompanionSprite renders hearts while recent
|
// Timestamp of last /buddy pet — CompanionSprite renders hearts while recent
|
||||||
companionPetAt?: number
|
companionPetAt?: number
|
||||||
|
|||||||
6
src/types/global.d.ts
vendored
6
src/types/global.d.ts
vendored
@@ -28,11 +28,7 @@ declare function getAntModelOverrideConfig(): {
|
|||||||
[key: string]: unknown
|
[key: string]: unknown
|
||||||
} | null
|
} | null
|
||||||
|
|
||||||
// Companion/buddy observer (internal)
|
// Companion reactions handled by src/buddy/companionReact.ts (direct import)
|
||||||
declare function fireCompanionObserver(
|
|
||||||
messages: unknown[],
|
|
||||||
callback: (reaction: unknown) => void,
|
|
||||||
): void
|
|
||||||
|
|
||||||
// Metrics (internal)
|
// Metrics (internal)
|
||||||
type ApiMetricEntry = { ttftMs: number; firstTokenTime: number; lastTokenTime: number; responseLengthBaseline: number; endResponseLength: number }
|
type ApiMetricEntry = { ttftMs: number; firstTokenTime: number; lastTokenTime: number; responseLengthBaseline: number; endResponseLength: number }
|
||||||
|
|||||||
Reference in New Issue
Block a user