refactor(buddy): align companion system with official CLI (#82)

* 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>

* fix(buddy): address CodeRabbit review findings

- buddy.ts: return type Promise<null> → Promise<React.ReactNode>
  to match LocalJSXCommandCall interface (CompanionCard path returns
  ReactElement, not null).
- CompanionCard.tsx: clamp stat value to 0..100 before .repeat()
  to prevent negative count runtime error on out-of-range values.

Import path alias suggestions (src/ vs ../) dismissed — project
convention uses relative paths (verified against color.ts, help.ts).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(buddy): address second round CodeRabbit findings

- buddy.ts:105: remove unsafe (context as any).messages cast.
  ToolUseContext already declares messages: Message[] at Tool.ts:250,
  so context.messages is properly typed. Other commands (feedback,
  copy, export) access it the same way without cast.

- companionReact.ts:154: wrap resp.json() in try/catch for defensive
  JSON parsing. Malformed 200 responses now return null instead of
  propagating to the outer catch.

Rate-limit timing (set before API call) kept as-is — matches official
ZUK pattern: prevents retry-storm on transient failures.

src/ path alias suggestions dismissed — project uses relative paths.
Auto-unmute on /buddy view kept — matches official behavior.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: unraid <local@unraid.local>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
claude-code-best
2026-04-03 17:22:54 +08:00
committed by GitHub
2 changed files with 7 additions and 3 deletions

View File

@@ -151,6 +151,10 @@ async function callBuddyReactAPI(
if (!resp.ok) return null
const data = (await resp.json()) as { reaction?: string }
return data.reaction?.trim() || null
try {
const data = (await resp.json()) as { reaction?: string }
return data.reaction?.trim() || null
} catch {
return null
}
}

View File

@@ -102,7 +102,7 @@ export async function call(
setState?.(prev => ({ ...prev, companionPetAt: Date.now() }))
// Trigger a post-pet reaction
triggerCompanionReaction((context as any).messages ?? [], reaction =>
triggerCompanionReaction(context.messages ?? [], reaction =>
setState?.(prev =>
prev.companionReaction === reaction
? prev