refactor(buddy): align companion system with official CLI#82
Conversation
## 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>
📝 WalkthroughWalkthroughAdds a JSX-based CompanionCard UI, relocates companion reaction logic into a new trigger function, converts the buddy command to a local-jsx handler that returns JSX and drives outputs via onDone, and updates REPL integration and types to call the new reaction trigger. Changes
Sequence DiagramsequenceDiagram
participant User
participant REPL
participant CompanionReact as companionReact
participant BuddyAPI as buddy_react API
participant App as setReaction
User->>REPL: Execute query
REPL->>REPL: Process turn
REPL->>CompanionReact: triggerCompanionReaction(messages, setReaction)
CompanionReact->>CompanionReact: Check companion present & mute
CompanionReact->>CompanionReact: Detect addressed vs turn, enforce rate limit
CompanionReact->>CompanionReact: Build capped transcript & recent reactions
alt eligible to request
CompanionReact->>BuddyAPI: POST transcript + companion metadata (OAuth + org UUID)
BuddyAPI-->>CompanionReact: 200 + reaction
CompanionReact->>CompanionReact: Trim & cache reaction (max 8)
CompanionReact->>App: setReaction(trimmed_reaction)
App->>REPL: Update companionReaction state
else not eligible or error
CompanionReact-->>App: (no reaction) silent
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (6)
src/buddy/companionReact.ts (4)
8-13: Relative imports used instead ofsrc/path alias.The coding guidelines specify using the
src/path alias via tsconfig mapping instead of relative paths. All imports in this file use relative paths.♻️ Suggested refactor to use path alias
-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' +import { getCompanion } from 'src/buddy/companion.js' +import { getGlobalConfig } from 'src/utils/config.js' +import { getClaudeAIOAuthTokens } from 'src/utils/auth.js' +import { getOauthConfig } from 'src/constants/oauth.js' +import { getUserAgent } from 'src/utils/http.js' +import type { Message } from 'src/types/message.js'As per coding guidelines: "Import
src/path alias via tsconfig mapping instead of relative paths in imports".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/buddy/companionReact.ts` around lines 8 - 13, Replace the relative imports at the top of companionReact.ts with the project path-alias "src/" imports: update import statements referencing getCompanion, getGlobalConfig, getClaudeAIOAuthTokens, getOauthConfig, getUserAgent and the Message type to use the tsconfig path alias (e.g. import { getCompanion } from 'src/buddy/companion' etc.), ensuring module specifiers match existing exported module paths and maintain file extensions/types as needed so the compiler resolves via the tsconfig "src/*" mapping.
53-62: Rate-limit timestamp updated before API response is confirmed.
lastReactTimeis set at line 53 before the API call completes. If the call fails, the companion won't react again until the interval passes. This may be intentional to prevent hammering the API on failures, but it means transient errors (network blips) will suppress reactions for 45s.If failed attempts shouldn't consume the rate-limit window, move the timestamp update into the
.then()block:♻️ Alternative: update timestamp only on success
- lastReactTime = now - void callBuddyReactAPI(companion, transcript, addressed) .then(reaction => { if (!reaction) return + lastReactTime = Date.now() recentReactions.push(reaction) if (recentReactions.length > MAX_RECENT) recentReactions.shift() setReaction(reaction) }) .catch(() => {})🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/buddy/companionReact.ts` around lines 53 - 62, The rate-limit timestamp lastReactTime is being set before callBuddyReactAPI completes; move the update so failures don't consume the window by setting lastReactTime only after a successful reaction is confirmed inside the .then() callback (e.g., when reaction is truthy), leaving the existing logic that pushes to recentReactions, enforces MAX_RECENT, and calls setReaction untouched; do not update lastReactTime in the .catch() path so transient errors won't block subsequent calls.
152-155: Unhandled JSON parse error on malformed response.If the server returns
200 OKwith invalid JSON (e.g., empty body or HTML error page),resp.json()will throw. SincecallBuddyReactAPIis called with.catch(() => {})upstream, this is caught silently, but wrapping the parse is cleaner:♻️ Defensive JSON parsing
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 + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/buddy/companionReact.ts` around lines 152 - 155, The response parsing in callBuddyReactAPI can throw if resp.json() encounters invalid JSON; wrap the await resp.json() call in a try/catch inside callBuddyReactAPI (or the function containing resp) so malformed/empty bodies return null instead of propagating an exception, e.g., catch the error from resp.json(), optionally log/debug it, and return null; keep the existing check for resp.ok and preserve returning data.reaction?.trim() || null when parsing succeeds.
67-80: Static analysis ReDoS warning is a false positive, but edge case exists for non-word-character names.The
escapeRegexhelper properly escapes metacharacters, so the ReDoS concern is mitigated. However, ifcompanion.nameconsists entirely of non-word characters (e.g., emoji like "🦆"), the pattern\b\bmatches zero-width positions everywhere, causing false positives.Consider adding a guard or adjusting the pattern:
♻️ Handle edge case for non-word-character names
function isAddressed(messages: Message[], name: string): boolean { + // If name has no word characters, use simple includes check + if (!/\w/.test(name)) { + 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' && content.toLowerCase().includes(name.toLowerCase())) return true + } + return false + } const pattern = new RegExp(`\\b${escapeRegex(name)}\\b`, 'i')🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/buddy/companionReact.ts` around lines 67 - 80, The isAddressed function can false-positive when name is all non-word characters (e.g., emoji) because using \b around it makes the regex match everywhere; update isAddressed to detect that case by checking the provided name (from companion.name) for any word characters (e.g., /\w/ or Unicode-aware equivalent) and, if none exist, avoid surrounding with \b and instead perform a direct substring or indexOf check (or construct a regex without \b) against message content; keep using escapeRegex for escaping and only apply the \b-delimited regex when the name contains at least one word character so you preserve current behavior and avoid false positives.src/commands/buddy/buddy.ts (2)
120-123: Auto-unmute on/buddyview may surprise users who intentionally muted.When a user runs
/buddyto view their companion, it automatically unmutes if previously muted. This could be confusing for users who explicitly ran/buddy offand just want to check their companion's stats without re-enabling reactions.Consider either:
- Keeping this behavior but documenting it clearly, or
- Only viewing without changing mute state
💡 Alternative: view without auto-unmute
- // Auto-unmute when viewing - if (companion && getGlobalConfig().companionMuted) { - saveGlobalConfig(cfg => ({ ...cfg, companionMuted: false })) - } + // Note: viewing companion does not change mute state + // Use /buddy on to unmute🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/commands/buddy/buddy.ts` around lines 120 - 123, The current handler unconditionally flips companionMuted to false when a companion exists (see companion, getGlobalConfig(), saveGlobalConfig, companionMuted) which surprises users; change the behavior to only read and display the companion state without mutating companionMuted (remove or guard the saveGlobalConfig call), or alternatively require an explicit unmute action/flag (e.g. `/buddy on`) before calling saveGlobalConfig so viewing `/buddy` does not change the mute setting. Ensure you update the logic around companion and the saveGlobalConfig(...) invocation accordingly so viewing is non-mutating unless an explicit unmute command is provided.
1-16: Relative imports used instead ofsrc/path alias.Similar to
companionReact.ts, this file uses relative paths for imports rather than thesrc/path alias specified in the coding guidelines.♻️ Suggested refactor to use path alias
import React from 'react' import { getCompanion, rollWithSeed, generateSeed, -} from '../../buddy/companion.js' -import { type StoredCompanion, RARITY_STARS } from '../../buddy/types.js' -import { renderSprite } from '../../buddy/sprites.js' -import { CompanionCard } from '../../buddy/CompanionCard.js' -import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js' -import { triggerCompanionReaction } from '../../buddy/companionReact.js' -import type { ToolUseContext } from '../../Tool.js' +} from 'src/buddy/companion.js' +import { type StoredCompanion, RARITY_STARS } from 'src/buddy/types.js' +import { renderSprite } from 'src/buddy/sprites.js' +import { CompanionCard } from 'src/buddy/CompanionCard.js' +import { getGlobalConfig, saveGlobalConfig } from 'src/utils/config.js' +import { triggerCompanionReaction } from 'src/buddy/companionReact.js' +import type { ToolUseContext } from 'src/Tool.js' import type { LocalJSXCommandContext, LocalJSXCommandOnDone, -} from '../../types/command.js' +} from 'src/types/command.js'As per coding guidelines: "Import
src/path alias via tsconfig mapping instead of relative paths in imports".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/commands/buddy/buddy.ts` around lines 1 - 16, Replace the relative imports in buddy.ts with the project path alias (src/...) so imports like getCompanion, rollWithSeed, generateSeed (from ../../buddy/companion.js), StoredCompanion and RARITY_STARS (from ../../buddy/types.js), renderSprite (../../buddy/sprites.js), CompanionCard (../../buddy/CompanionCard.js), triggerCompanionReaction (../../buddy/companionReact.js) and getGlobalConfig/saveGlobalConfig (../../utils/config.js) use the tsconfig path mapping (e.g. import from "src/buddy/companion", "src/buddy/types", etc.); update each import statement accordingly and ensure extensions match project conventions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/buddy/CompanionCard.tsx`:
- Around line 15-16: Clamp the incoming stat value before computing the bar to
avoid negative repeat counts: in CompanionCard (where `value` is used to compute
`filled` and `bar`), replace the direct use of `value` with a clamped value
(e.g. v = Math.max(0, Math.min(100, value))) and compute `filled` = Math.round(v
/ 10) and then build `bar` from that clamped `filled`; ensure `filled` and `bar`
are the only places using the clamped variable so rendering is safe for values
outside 0..100.
- Around line 6-9: Replace the relative imports in CompanionCard.tsx with
tsconfig path-alias imports: import Box, Text and useInput from the aliased
module for ink (instead of '../ink.js'), import renderSprite from the aliased
sprites module (instead of './sprites.js'), and import RARITY_COLORS,
RARITY_STARS, STAT_NAMES and the Companion type from the aliased types module
(instead of './types.js'); update the import targets to use the project's
"src/..." path mappings so the symbols Box, Text, useInput, renderSprite,
RARITY_COLORS, RARITY_STARS, STAT_NAMES and Companion are resolved via the
tsconfig alias.
In `@src/commands/buddy/buddy.ts`:
- Around line 70-74: The function signature for call currently returns
Promise<null> but actually returns React elements; update the return type of the
call function to Promise<React.ReactNode> to match the LocalJSXCommandCall
interface and the actual return values (e.g., the
React.createElement(CompanionCard, ...) return path). Leave early returns that
return null as-is since null is a valid ReactNode; ensure any TypeScript
import/namespace references allow using React.ReactNode in the signature if not
already imported.
In `@src/commands/buddy/index.ts`:
- Around line 7-8: Update the command metadata to include the "on" argument:
change the description and argumentHint fields (description and argumentHint in
the buddy command) so they advertise the supported args including "on" (e.g.,
include "on" alongside "pet" and "off") to match the implemented `/buddy on`
flow.
- Line 2: Replace the relative import of isBuddyLive with the repo path alias:
change the import statement that currently references
'../../buddy/useBuddyNotification.js' to use the tsconfig alias starting with
'src/' (i.e., import isBuddyLive from 'src/buddy/useBuddyNotification.js' or
equivalent), so the symbol isBuddyLive is imported via the src/ alias instead of
a relative path.
In `@src/screens/REPL.tsx`:
- Line 278: The import in REPL.tsx uses a relative path for
triggerCompanionReaction; update the import to use the configured tsconfig path
alias (src/) instead of '../buddy/companionReact.js' so the module resolves via
the project's src/ mapping; locate the import statement referencing
triggerCompanionReaction in REPL.tsx and replace the relative path with the
aliased path (e.g., import { triggerCompanionReaction } from
'src/buddy/companionReact') ensuring the file extension and casing match project
conventions.
---
Nitpick comments:
In `@src/buddy/companionReact.ts`:
- Around line 8-13: Replace the relative imports at the top of companionReact.ts
with the project path-alias "src/" imports: update import statements referencing
getCompanion, getGlobalConfig, getClaudeAIOAuthTokens, getOauthConfig,
getUserAgent and the Message type to use the tsconfig path alias (e.g. import {
getCompanion } from 'src/buddy/companion' etc.), ensuring module specifiers
match existing exported module paths and maintain file extensions/types as
needed so the compiler resolves via the tsconfig "src/*" mapping.
- Around line 53-62: The rate-limit timestamp lastReactTime is being set before
callBuddyReactAPI completes; move the update so failures don't consume the
window by setting lastReactTime only after a successful reaction is confirmed
inside the .then() callback (e.g., when reaction is truthy), leaving the
existing logic that pushes to recentReactions, enforces MAX_RECENT, and calls
setReaction untouched; do not update lastReactTime in the .catch() path so
transient errors won't block subsequent calls.
- Around line 152-155: The response parsing in callBuddyReactAPI can throw if
resp.json() encounters invalid JSON; wrap the await resp.json() call in a
try/catch inside callBuddyReactAPI (or the function containing resp) so
malformed/empty bodies return null instead of propagating an exception, e.g.,
catch the error from resp.json(), optionally log/debug it, and return null; keep
the existing check for resp.ok and preserve returning data.reaction?.trim() ||
null when parsing succeeds.
- Around line 67-80: The isAddressed function can false-positive when name is
all non-word characters (e.g., emoji) because using \b around it makes the regex
match everywhere; update isAddressed to detect that case by checking the
provided name (from companion.name) for any word characters (e.g., /\w/ or
Unicode-aware equivalent) and, if none exist, avoid surrounding with \b and
instead perform a direct substring or indexOf check (or construct a regex
without \b) against message content; keep using escapeRegex for escaping and
only apply the \b-delimited regex when the name contains at least one word
character so you preserve current behavior and avoid false positives.
In `@src/commands/buddy/buddy.ts`:
- Around line 120-123: The current handler unconditionally flips companionMuted
to false when a companion exists (see companion, getGlobalConfig(),
saveGlobalConfig, companionMuted) which surprises users; change the behavior to
only read and display the companion state without mutating companionMuted
(remove or guard the saveGlobalConfig call), or alternatively require an
explicit unmute action/flag (e.g. `/buddy on`) before calling saveGlobalConfig
so viewing `/buddy` does not change the mute setting. Ensure you update the
logic around companion and the saveGlobalConfig(...) invocation accordingly so
viewing is non-mutating unless an explicit unmute command is provided.
- Around line 1-16: Replace the relative imports in buddy.ts with the project
path alias (src/...) so imports like getCompanion, rollWithSeed, generateSeed
(from ../../buddy/companion.js), StoredCompanion and RARITY_STARS (from
../../buddy/types.js), renderSprite (../../buddy/sprites.js), CompanionCard
(../../buddy/CompanionCard.js), triggerCompanionReaction
(../../buddy/companionReact.js) and getGlobalConfig/saveGlobalConfig
(../../utils/config.js) use the tsconfig path mapping (e.g. import from
"src/buddy/companion", "src/buddy/types", etc.); update each import statement
accordingly and ensure extensions match project conventions.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: a37b4afd-aa71-472e-8bd0-f06450ab251f
📒 Files selected for processing (7)
src/buddy/CompanionCard.tsxsrc/buddy/companionReact.tssrc/commands/buddy/buddy.tssrc/commands/buddy/index.tssrc/screens/REPL.tsxsrc/state/AppStateStore.tssrc/types/global.d.ts
| 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'; |
There was a problem hiding this comment.
Use src/ path aliases for new TSX imports.
Lines 6-9 introduce new relative imports; these should use src/... aliases.
Suggested fix
-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';
+import { Box, Text } from 'src/ink.js';
+import { useInput } from 'src/ink.js';
+import { renderSprite } from 'src/buddy/sprites.js';
+import { RARITY_COLORS, RARITY_STARS, STAT_NAMES, type Companion } from 'src/buddy/types.js';As per coding guidelines Import src/ path alias via tsconfig mapping instead of relative paths in imports.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| 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'; | |
| import { Box, Text } from 'src/ink.js'; | |
| import { useInput } from 'src/ink.js'; | |
| import { renderSprite } from 'src/buddy/sprites.js'; | |
| import { RARITY_COLORS, RARITY_STARS, STAT_NAMES, type Companion } from 'src/buddy/types.js'; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/buddy/CompanionCard.tsx` around lines 6 - 9, Replace the relative imports
in CompanionCard.tsx with tsconfig path-alias imports: import Box, Text and
useInput from the aliased module for ink (instead of '../ink.js'), import
renderSprite from the aliased sprites module (instead of './sprites.js'), and
import RARITY_COLORS, RARITY_STARS, STAT_NAMES and the Companion type from the
aliased types module (instead of './types.js'); update the import targets to use
the project's "src/..." path mappings so the symbols Box, Text, useInput,
renderSprite, RARITY_COLORS, RARITY_STARS, STAT_NAMES and Companion are resolved
via the tsconfig alias.
| @@ -1,10 +1,15 @@ | |||
| import type { Command } from '../../commands.js' | |||
| import { isBuddyLive } from '../../buddy/useBuddyNotification.js' | |||
There was a problem hiding this comment.
Switch this new import to the src/ alias.
Line 2 adds a relative import in a TS file; use the repo alias to stay consistent.
Suggested fix
-import { isBuddyLive } from '../../buddy/useBuddyNotification.js'
+import { isBuddyLive } from 'src/buddy/useBuddyNotification.js'As per coding guidelines Import src/ path alias via tsconfig mapping instead of relative paths in imports.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| import { isBuddyLive } from '../../buddy/useBuddyNotification.js' | |
| import { isBuddyLive } from 'src/buddy/useBuddyNotification.js' |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/commands/buddy/index.ts` at line 2, Replace the relative import of
isBuddyLive with the repo path alias: change the import statement that currently
references '../../buddy/useBuddyNotification.js' to use the tsconfig alias
starting with 'src/' (i.e., import isBuddyLive from
'src/buddy/useBuddyNotification.js' or equivalent), so the symbol isBuddyLive is
imported via the src/ alias instead of a relative path.
| description: 'Hatch a coding companion · pet, off', | ||
| argumentHint: '[pet|off]', |
There was a problem hiding this comment.
Include on in command hint/description to match supported args.
Line 8 currently advertises [pet|off], but /buddy on is part of the command flow in this PR. The hint/description should include it.
Suggested fix
- description: 'Hatch a coding companion · pet, off',
- argumentHint: '[pet|off]',
+ description: 'Hatch a coding companion · pet, on, off',
+ argumentHint: '[pet|on|off]',📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| description: 'Hatch a coding companion · pet, off', | |
| argumentHint: '[pet|off]', | |
| description: 'Hatch a coding companion · pet, on, off', | |
| argumentHint: '[pet|on|off]', |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/commands/buddy/index.ts` around lines 7 - 8, Update the command metadata
to include the "on" argument: change the description and argumentHint fields
(description and argumentHint in the buddy command) so they advertise the
supported args including "on" (e.g., include "on" alongside "pet" and "off") to
match the implemented `/buddy on` flow.
| import { IssueFlagBanner } from '../components/PromptInput/IssueFlagBanner.js'; | ||
| import { useIssueFlagBanner } from '../hooks/useIssueFlagBanner.js'; | ||
| import { CompanionSprite, CompanionFloatingBubble, MIN_COLS_FOR_FULL_SPRITE } from '../buddy/CompanionSprite.js'; | ||
| import { triggerCompanionReaction } from '../buddy/companionReact.js'; |
There was a problem hiding this comment.
Use src/ alias for the new companion import.
Line 278 introduces a new relative import in a TSX file; this should use the configured src/ path alias.
Suggested fix
-import { triggerCompanionReaction } from '../buddy/companionReact.js';
+import { triggerCompanionReaction } from 'src/buddy/companionReact.js';As per coding guidelines Import src/ path alias via tsconfig mapping instead of relative paths in imports.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| import { triggerCompanionReaction } from '../buddy/companionReact.js'; | |
| import { triggerCompanionReaction } from 'src/buddy/companionReact.js'; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/screens/REPL.tsx` at line 278, The import in REPL.tsx uses a relative
path for triggerCompanionReaction; update the import to use the configured
tsconfig path alias (src/) instead of '../buddy/companionReact.js' so the module
resolves via the project's src/ mapping; locate the import statement referencing
triggerCompanionReaction in REPL.tsx and replace the relative path with the
aliased path (e.g., import { triggerCompanionReaction } from
'src/buddy/companionReact') ensuring the file extension and casing match project
conventions.
- 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>
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
src/commands/buddy/buddy.ts (1)
1-16: Relative imports violate path alias convention.The coding guidelines require using the
src/path alias via tsconfig mapping instead of relative paths. All imports here use relative paths (e.g.,../../buddy/companion.js).♻️ Suggested refactor to use path aliases
import React from 'react' import { getCompanion, rollWithSeed, generateSeed, -} from '../../buddy/companion.js' -import { type StoredCompanion, RARITY_STARS } from '../../buddy/types.js' -import { renderSprite } from '../../buddy/sprites.js' -import { CompanionCard } from '../../buddy/CompanionCard.js' -import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js' -import { triggerCompanionReaction } from '../../buddy/companionReact.js' -import type { ToolUseContext } from '../../Tool.js' +} from 'src/buddy/companion.js' +import { type StoredCompanion, RARITY_STARS } from 'src/buddy/types.js' +import { renderSprite } from 'src/buddy/sprites.js' +import { CompanionCard } from 'src/buddy/CompanionCard.js' +import { getGlobalConfig, saveGlobalConfig } from 'src/utils/config.js' +import { triggerCompanionReaction } from 'src/buddy/companionReact.js' +import type { ToolUseContext } from 'src/Tool.js' import type { LocalJSXCommandContext, LocalJSXCommandOnDone, -} from '../../types/command.js' +} from 'src/types/command.js'As per coding guidelines: "Import
src/path alias via tsconfig mapping instead of relative paths in imports".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/commands/buddy/buddy.ts` around lines 1 - 16, Replace all relative imports in this file with the project's tsconfig path alias (src/). Specifically update the imports that bring in getCompanion, rollWithSeed, generateSeed from ../../buddy/companion.js; StoredCompanion and RARITY_STARS from ../../buddy/types.js; renderSprite from ../../buddy/sprites.js; CompanionCard from ../../buddy/CompanionCard.js; getGlobalConfig and saveGlobalConfig from ../../utils/config.js; and triggerCompanionReaction from ../../buddy/companionReact.js to use the src/ alias (e.g., import { getCompanion } from 'src/buddy/companion') so they conform to the codebase import convention.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/commands/buddy/buddy.ts`:
- Around line 104-111: The code is using an unsafe cast (context as
any).messages when calling triggerCompanionReaction in buddy.ts; fix by adding
messages: Message[] to the ToolUseContext (or LocalJSXCommandContext) type
declaration in src/types/command.ts (or to the Tool.ts context shape) so the
runtime messages property is represented in types, then remove the (context as
any) cast in buddy.ts and any other places (e.g., feedback.tsx, copy.tsx) so
they use context.messages typed as Message[]; alternatively, if messages is
intentionally runtime-only, add a clear comment in the type file documenting
that and keep a narrow explicit accessor function to read it rather than casting
to any.
---
Nitpick comments:
In `@src/commands/buddy/buddy.ts`:
- Around line 1-16: Replace all relative imports in this file with the project's
tsconfig path alias (src/). Specifically update the imports that bring in
getCompanion, rollWithSeed, generateSeed from ../../buddy/companion.js;
StoredCompanion and RARITY_STARS from ../../buddy/types.js; renderSprite from
../../buddy/sprites.js; CompanionCard from ../../buddy/CompanionCard.js;
getGlobalConfig and saveGlobalConfig from ../../utils/config.js; and
triggerCompanionReaction from ../../buddy/companionReact.js to use the src/
alias (e.g., import { getCompanion } from 'src/buddy/companion') so they conform
to the codebase import convention.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: c51d61fb-803b-4a76-8e73-d5e6ed24e600
📒 Files selected for processing (2)
src/buddy/CompanionCard.tsxsrc/commands/buddy/buddy.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- src/buddy/CompanionCard.tsx
| // Trigger a post-pet reaction | ||
| triggerCompanionReaction((context as any).messages ?? [], reaction => | ||
| setState?.(prev => | ||
| prev.companionReaction === reaction | ||
| ? prev | ||
| : { ...prev, companionReaction: reaction }, | ||
| ), | ||
| ) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check if messages is passed to local-jsx command contexts at runtime
rg -n "messages" src/commands --type ts -C3 | head -60
echo "---"
# Check how local-jsx commands receive their context
ast-grep --pattern 'call($_, $_, $_) {
$$$
}'Repository: claude-code-best/claude-code
Length of output: 4392
🏁 Script executed:
# First, let's examine the type definitions for LocalJSXCommandContext and ToolUseContext
cat -n src/types/command.ts | head -100Repository: claude-code-best/claude-code
Length of output: 4150
🏁 Script executed:
# Check what properties are actually available on the context types
rg "interface LocalJSXCommandContext|interface ToolUseContext" -A 20 src/types/command.tsRepository: claude-code-best/claude-code
Length of output: 54
🏁 Script executed:
# Look at buddy.ts to understand what context parameter is passed to the call function
cat -n src/commands/buddy/buddy.ts | sed -n '1,110p'Repository: claude-code-best/claude-code
Length of output: 4862
🏁 Script executed:
# Check how buddy command is invoked to understand what context type is passed
rg "buddy.*call|call.*buddy" -B 5 -A 5 src/commandsRepository: claude-code-best/claude-code
Length of output: 54
🏁 Script executed:
# Check the ToolUseContext type definition
rg "interface ToolUseContext|type ToolUseContext|export.*ToolUseContext" -A 30 src/Tool.ts | head -60Repository: claude-code-best/claude-code
Length of output: 1346
🏁 Script executed:
# Check if other commands use similar patterns to access messages
rg "context.*messages" src/commands --type ts -B 2 -A 2Repository: claude-code-best/claude-code
Length of output: 4137
🏁 Script executed:
# Search for any type augmentation or re-declaration of ToolUseContext or LocalJSXCommandContext
rg "ToolUseContext|LocalJSXCommandContext" src -g "*.d.ts" -A 5Repository: claude-code-best/claude-code
Length of output: 54
🏁 Script executed:
# Check the exact signatures of commands using context.messages to see their context type
rg "export.*function call|export const call" src/commands/feedback/feedback.tsx -A 1
rg "export.*function call|export const call" src/commands/copy/copy.tsx -A 1
rg "export.*function call|export const call" src/commands/export/export.tsx -A 1Repository: claude-code-best/claude-code
Length of output: 547
🏁 Script executed:
# Check if there's a global declaration or type file that adds messages to context
fd ".*\.d\.ts$" src | xargs grep -l "messages" 2>/dev/nullRepository: claude-code-best/claude-code
Length of output: 56
context.messages is untyped and accessed via unsafe any cast.
The messages property is not declared on ToolUseContext or LocalJSXCommandContext (per src/types/command.ts lines 80–98 and src/Tool.ts), yet the code accesses it via (context as any).messages ?? [] at line 105. While the fallback to [] prevents crashes, the companion reaction silently loses conversation context without it.
This pattern exists inconsistently across the codebase—other commands like feedback.tsx and copy.tsx access context.messages without any cast, suggesting the types are out of sync with the runtime implementation.
Add messages: Message[] to ToolUseContext or document why this is intentionally a runtime-only property.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/commands/buddy/buddy.ts` around lines 104 - 111, The code is using an
unsafe cast (context as any).messages when calling triggerCompanionReaction in
buddy.ts; fix by adding messages: Message[] to the ToolUseContext (or
LocalJSXCommandContext) type declaration in src/types/command.ts (or to the
Tool.ts context shape) so the runtime messages property is represented in types,
then remove the (context as any) cast in buddy.ts and any other places (e.g.,
feedback.tsx, copy.tsx) so they use context.messages typed as Message[];
alternatively, if messages is intentionally runtime-only, add a clear comment in
the type file documenting that and keep a narrow explicit accessor function to
read it rather than casting to any.
- 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>
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/buddy/companionReact.ts`:
- Around line 8-13: The imports in companionReact.ts use relative paths; replace
them with the tsconfig path-alias imports that start with "src/" to follow repo
conventions—for example update imports referencing getCompanion,
getGlobalConfig, getClaudeAIOAuthTokens, getOauthConfig, getUserAgent and the
Message type to use "src/..." alias paths (e.g., import { getCompanion } from
'src/buddy/companion') so the module resolver and linting conform to the
project's tsconfig mapping.
- Around line 68-77: The current detection regex (pattern built with
escapeRegex(name)) matches bare names like "Bob" instead of explicit `@-mentions`;
update the pattern construction in companionReact.ts to require an '@' before
the name (for example use a regex like `(^|\\s)@${escapeRegex(name)}\\b` with
case-insensitive flag) so the loop over messages (messages, pattern,
escapeRegex, name) only returns true when the user was explicitly mentioned with
'@'.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 6607521e-05aa-42f6-ba27-fc103381631b
📒 Files selected for processing (2)
src/buddy/companionReact.tssrc/commands/buddy/buddy.ts
✅ Files skipped from review due to trivial changes (1)
- src/commands/buddy/buddy.ts
| 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' |
There was a problem hiding this comment.
Use src/ path alias imports instead of relative imports
These imports currently violate the repository import-path convention.
Proposed fix
-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'
+import { getCompanion } from 'src/buddy/companion.js'
+import { getGlobalConfig } from 'src/utils/config.js'
+import { getClaudeAIOAuthTokens } from 'src/utils/auth.js'
+import { getOauthConfig } from 'src/constants/oauth.js'
+import { getUserAgent } from 'src/utils/http.js'
+import type { Message } from 'src/types/message.js'As per coding guidelines, Import src/ path alias via tsconfig mapping instead of relative paths in imports.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| 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' | |
| import { getCompanion } from 'src/buddy/companion.js' | |
| import { getGlobalConfig } from 'src/utils/config.js' | |
| import { getClaudeAIOAuthTokens } from 'src/utils/auth.js' | |
| import { getOauthConfig } from 'src/constants/oauth.js' | |
| import { getUserAgent } from 'src/utils/http.js' | |
| import type { Message } from 'src/types/message.js' |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/buddy/companionReact.ts` around lines 8 - 13, The imports in
companionReact.ts use relative paths; replace them with the tsconfig path-alias
imports that start with "src/" to follow repo conventions—for example update
imports referencing getCompanion, getGlobalConfig, getClaudeAIOAuthTokens,
getOauthConfig, getUserAgent and the Message type to use "src/..." alias paths
(e.g., import { getCompanion } from 'src/buddy/companion') so the module
resolver and linting conform to the project's tsconfig mapping.
| 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 |
There was a problem hiding this comment.
@-mention detection is not actually enforced
This regex matches bare names (Bob) rather than explicit mentions (@Bob), so addressed can be true when the user did not mention with @.
Proposed fix
function isAddressed(messages: Message[], name: string): boolean {
- const pattern = new RegExp(`\\b${escapeRegex(name)}\\b`, 'i')
+ const pattern = new RegExp(`(?:^|\\s)@${escapeRegex(name)}(?=\\b|\\s|$)`, 'i')
for (
let i = messages.length - 1;
i >= Math.max(0, messages.length - 3);
i--
) {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| 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 | |
| const pattern = new RegExp(`(?:^|\\s)@${escapeRegex(name)}(?=\\b|\\s|$)`, '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 |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/buddy/companionReact.ts` around lines 68 - 77, The current detection
regex (pattern built with escapeRegex(name)) matches bare names like "Bob"
instead of explicit `@-mentions`; update the pattern construction in
companionReact.ts to require an '@' before the name (for example use a regex
like `(^|\\s)@${escapeRegex(name)}\\b` with case-insensitive flag) so the loop
over messages (messages, pattern, escapeRegex, name) only returns true when the
user was explicitly mentioned with '@'.
…e-best#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>


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— JSX bordered card matching officialvc8: rarity header, colored sprite, name, personality, 10-bar stats, last reaction in nested border box. Press any key to dismiss.src/buddy/companionReact.ts— Reaction system matching officialZUK+Dc8: 45s rate limiting, @-mention detection, transcript builder (12 msgs, 5000 chars), POSTbuddy_reactAPI.Modified
src/commands/buddy/index.ts—type: local→local-jsx, addeddescription/argumentHint/immediate/isHidden.src/commands/buddy/buddy.ts—LocalCommandCall→LocalJSXCommandCallsignature. Removedmute/unmute/rehatch(official usesoff/ononly)./buddyshow returnsCompanionCardJSX. Pet auto-unmutes.companionMutedwritesglobalConfig(matches UI read source).src/screens/REPL.tsx(line 2808) —globalThis.fireCompanionObserver→import { triggerCompanionReaction }.src/state/AppStateStore.ts— Comment fix:observer.ts→companionReact.ts.src/types/global.d.ts— RemovedfireCompanionObserverdeclaration.Data flow audit (verified consistent)
companionMuted:saveGlobalConfig()↔getGlobalConfig()(all 6 read sites)companionReaction:setAppState()↔useAppState()(all 4 sites)companionPetAt:setAppState()↔useAppState()(all 2 sites)Test plan
/buddy off→ companion muted, sprite hidden/buddy on→ companion unmuted, sprite visible/buddy pet→ heart animation, auto-unmute if muted/buddy(no args) → bordered card with stats, last reaction🤖 Generated with Claude Code
Summary by CodeRabbit