feat: activate buddy system in open build (#346)

This commit is contained in:
Kevin Codex
2026-04-05 05:39:00 +08:00
committed by GitHub
parent ba1b9913aa
commit d1a2df2f69
12 changed files with 299 additions and 29 deletions

View File

@@ -1,5 +1,4 @@
import { c as _c } from "react-compiler-runtime";
import { feature } from 'bun:bundle';
import figures from 'figures';
import React, { useEffect, useRef, useState } from 'react';
import { useTerminalSize } from '../hooks/useTerminalSize.js';
@@ -11,6 +10,7 @@ import { getGlobalConfig } from '../utils/config.js';
import { isFullscreenActive } from '../utils/fullscreen.js';
import type { Theme } from '../utils/theme.js';
import { getCompanion } from './companion.js';
import { isBuddyEnabled } from './feature.js';
import { renderFace, renderSprite, spriteFrameCount } from './sprites.js';
import { RARITY_COLORS } from './types.js';
const TICK_MS = 500;
@@ -165,7 +165,7 @@ function spriteColWidth(nameWidth: number): number {
// Narrow terminals: 0 — REPL.tsx stacks the one-liner on its own row
// (above input in fullscreen, below in scrollback), so no reservation.
export function companionReservedColumns(terminalColumns: number, speaking: boolean): number {
if (!feature('BUDDY')) return 0;
if (!isBuddyEnabled()) return 0;
const companion = getCompanion();
if (!companion || getGlobalConfig().companionMuted) return 0;
if (terminalColumns < MIN_COLS_FOR_FULL_SPRITE) return 0;
@@ -212,7 +212,7 @@ export function CompanionSprite(): React.ReactNode {
return () => clearTimeout(timer);
// eslint-disable-next-line react-hooks/exhaustive-deps -- tick intentionally captured at reaction-change, not tracked
}, [reaction, setAppState]);
if (!feature('BUDDY')) return null;
if (!isBuddyEnabled()) return null;
const companion = getCompanion();
if (!companion || getGlobalConfig().companionMuted) return null;
const color = RARITY_COLORS[companion.rarity];
@@ -337,7 +337,7 @@ export function CompanionFloatingBubble() {
t3 = $[4];
}
useEffect(t2, t3);
if (!feature("BUDDY") || !reaction) {
if (!isBuddyEnabled() || !reaction) {
return null;
}
const companion = getCompanion();

3
src/buddy/feature.ts Normal file
View File

@@ -0,0 +1,3 @@
export function isBuddyEnabled(): boolean {
return true
}

65
src/buddy/observer.ts Normal file
View File

@@ -0,0 +1,65 @@
import type { Message } from '../types/message.js'
import { getGlobalConfig } from '../utils/config.js'
import { getUserMessageText } from '../utils/messages.js'
import { getCompanion } from './companion.js'
const DIRECT_REPLIES = [
'I am observing.',
'I am helping from the corner.',
'I saw that.',
'Still here.',
'Watching closely.',
] as const
const PET_REPLIES = [
'happy chirp',
'tiny victory dance',
'quietly approves',
'wiggles with joy',
'looks pleased',
] as const
function hashString(s: string): number {
let h = 2166136261
for (let i = 0; i < s.length; i++) {
h ^= s.charCodeAt(i)
h = Math.imul(h, 16777619)
}
return h >>> 0
}
function pickDeterministic<T>(items: readonly T[], seed: string): T {
return items[hashString(seed) % items.length]!
}
export async function fireCompanionObserver(
messages: Message[],
onReaction: (reaction: string | undefined) => void,
): Promise<void> {
const companion = getCompanion()
if (!companion || getGlobalConfig().companionMuted) return
const lastUser = [...messages].reverse().find(msg => msg.type === 'user')
if (!lastUser) return
const text = getUserMessageText(lastUser)?.trim()
if (!text) return
const lower = text.toLowerCase()
const companionName = companion.name.toLowerCase()
if (lower.includes('/buddy')) {
onReaction(pickDeterministic(PET_REPLIES, text + companion.name))
return
}
if (
lower.includes(companionName) ||
lower.includes('buddy') ||
lower.includes('companion')
) {
onReaction(
`${companion.name}: ${pickDeterministic(DIRECT_REPLIES, text + companion.personality)}`,
)
}
}

View File

@@ -1,8 +1,8 @@
import { feature } from 'bun:bundle'
import type { Message } from '../types/message.js'
import type { Attachment } from '../utils/attachments.js'
import { getGlobalConfig } from '../utils/config.js'
import { getCompanion } from './companion.js'
import { isBuddyEnabled } from './feature.js'
export function companionIntroText(name: string, species: string): string {
return `# Companion
@@ -15,7 +15,7 @@ When the user addresses ${name} directly (by name), its bubble will answer. Your
export function getCompanionIntroAttachment(
messages: Message[] | undefined,
): Attachment[] {
if (!feature('BUDDY')) return []
if (!isBuddyEnabled()) return []
const companion = getCompanion()
if (!companion || getGlobalConfig().companionMuted) return []

View File

@@ -1,10 +1,10 @@
import { c as _c } from "react-compiler-runtime";
import { feature } from 'bun:bundle';
import React, { useEffect } from 'react';
import { useNotifications } from '../context/notifications.js';
import { Text } from '../ink.js';
import { getGlobalConfig } from '../utils/config.js';
import { getRainbowColor } from '../utils/thinking.js';
import { isBuddyEnabled } from './feature.js';
// Local date, not UTC — 24h rolling wave across timezones. Sustained Twitter
// buzz instead of a single UTC-midnight spike, gentler on soul-gen load.
@@ -50,7 +50,7 @@ export function useBuddyNotification() {
let t1;
if ($[0] !== addNotification || $[1] !== removeNotification) {
t0 = () => {
if (!feature("BUDDY")) {
if (!isBuddyEnabled()) {
return;
}
const config = getGlobalConfig();
@@ -80,7 +80,7 @@ export function findBuddyTriggerPositions(text: string): Array<{
start: number;
end: number;
}> {
if (!feature('BUDDY')) return [];
if (!isBuddyEnabled()) return [];
const triggers: Array<{
start: number;
end: number;