refactor(buddy): align companion system with official CLI

## Summary

Reverse-engineered the official Claude Code CLI (v2.1.91) buddy/companion
system and aligned our implementation to match.

## Changes (7 files)

### Added
- `src/buddy/CompanionCard.tsx` (+109)
  JSX bordered card matching official vc8: rarity header, colored sprite,
  name, personality, 10-bar stats, last reaction in nested border.

- `src/buddy/companionReact.ts` (+156)
  Reaction system matching official ZUK+Dc8: 45s rate limiting, @-mention
  detection, transcript builder (12 msgs, 5000 chars), POST buddy_react API.

### Modified
- `src/commands/buddy/index.ts`
  type: local -> local-jsx, description/argumentHint/immediate/isHidden.

- `src/commands/buddy/buddy.ts`
  LocalCommandCall -> LocalJSXCommandCall signature (onDone, context, args).
  Removed mute/unmute/rehatch (official uses off/on only).
  /buddy show returns CompanionCard JSX instead of plain text.
  Pet auto-unmutes. companionMuted writes globalConfig (matches UI read source).

- `src/screens/REPL.tsx` (line 2808)
  globalThis.fireCompanionObserver -> import triggerCompanionReaction.

- `src/state/AppStateStore.ts` — comment fix.
- `src/types/global.d.ts` — removed fireCompanionObserver declaration.

## Data flow (verified consistent)
- companionMuted: saveGlobalConfig() <-> getGlobalConfig() (6 read sites)
- companionReaction: setAppState() <-> useAppState() (4 sites)
- companionPetAt: setAppState() <-> useAppState() (2 sites)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
unraid
2026-04-03 16:36:22 +08:00
parent 7935bfb4b8
commit 991119491c
7 changed files with 384 additions and 183 deletions

109
src/buddy/CompanionCard.tsx Normal file
View File

@@ -0,0 +1,109 @@
/**
* 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 filled = Math.round(value / 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>
&quot;{companion.personality}&quot;
</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>
);
}