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

@@ -3,7 +3,7 @@
* distributable JS file using Bun's bundler. * distributable JS file using Bun's bundler.
* *
* Handles: * Handles:
* - bun:bundle feature() flags → all false (disables internal-only features) * - bun:bundle feature() flags for the open build
* - MACRO.* globals → inlined version/build-time constants * - MACRO.* globals → inlined version/build-time constants
* - src/ path aliases * - src/ path aliases
*/ */
@@ -14,8 +14,9 @@ import { noTelemetryPlugin } from './no-telemetry-plugin'
const pkg = JSON.parse(readFileSync('./package.json', 'utf-8')) const pkg = JSON.parse(readFileSync('./package.json', 'utf-8'))
const version = pkg.version const version = pkg.version
// Feature flags — all disabled for the open build. // Feature flags for the open build.
// These gate Anthropic-internal features (voice, proactive, kairos, etc.) // Most Anthropic-internal features stay off; open-build features can be
// selectively enabled here when their full source exists in the mirror.
const featureFlags: Record<string, boolean> = { const featureFlags: Record<string, boolean> = {
VOICE_MODE: false, VOICE_MODE: false,
PROACTIVE: false, PROACTIVE: false,
@@ -37,7 +38,7 @@ const featureFlags: Record<string, boolean> = {
TRANSCRIPT_CLASSIFIER: false, TRANSCRIPT_CLASSIFIER: false,
WEB_BROWSER_TOOL: false, WEB_BROWSER_TOOL: false,
MESSAGE_ACTIONS: false, MESSAGE_ACTIONS: false,
BUDDY: false, BUDDY: true,
CHICAGO_MCP: false, CHICAGO_MCP: false,
COWORKER_TYPE_TELEMETRY: false, COWORKER_TYPE_TELEMETRY: false,
} }
@@ -110,7 +111,7 @@ export async function handleBgFlag() { throw new Error("Background sessions are
build.onLoad( build.onLoad(
{ filter: /.*/, namespace: 'bun-bundle-shim' }, { filter: /.*/, namespace: 'bun-bundle-shim' },
() => ({ () => ({
contents: `export function feature(name) { return false; }`, contents: `const featureFlags = ${JSON.stringify(featureFlags)};\nexport function feature(name) { return featureFlags[name] ?? false; }`,
loader: 'js', loader: 'js',
}), }),
) )

View File

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

View File

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

View File

@@ -59,6 +59,7 @@ import usage from './commands/usage/index.js'
import theme from './commands/theme/index.js' import theme from './commands/theme/index.js'
import vim from './commands/vim/index.js' import vim from './commands/vim/index.js'
import { feature } from 'bun:bundle' import { feature } from 'bun:bundle'
import { isBuddyEnabled } from './buddy/feature.js'
// Dead code elimination: conditional imports // Dead code elimination: conditional imports
/* eslint-disable @typescript-eslint/no-require-imports */ /* eslint-disable @typescript-eslint/no-require-imports */
const proactive = const proactive =
@@ -117,7 +118,7 @@ const forkCmd = feature('FORK_SUBAGENT')
require('./commands/fork/index.js') as typeof import('./commands/fork/index.js') require('./commands/fork/index.js') as typeof import('./commands/fork/index.js')
).default ).default
: null : null
const buddy = feature('BUDDY') const buddy = isBuddyEnabled()
? ( ? (
require('./commands/buddy/index.js') as typeof import('./commands/buddy/index.js') require('./commands/buddy/index.js') as typeof import('./commands/buddy/index.js')
).default ).default

View File

@@ -0,0 +1,185 @@
import type { LocalJSXCommandContext, LocalJSXCommandOnDone } from '../../types/command.js'
import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js'
import { companionUserId, getCompanion, rollWithSeed } from '../../buddy/companion.js'
import type { StoredCompanion } from '../../buddy/types.js'
import { COMMON_HELP_ARGS, COMMON_INFO_ARGS } from '../../constants/xml.js'
const NAME_PREFIXES = [
'Byte',
'Echo',
'Glint',
'Miso',
'Nova',
'Pixel',
'Rune',
'Static',
'Vector',
'Whisk',
] as const
const NAME_SUFFIXES = [
'bean',
'bit',
'bud',
'dot',
'ling',
'loop',
'moss',
'patch',
'puff',
'spark',
] as const
const PERSONALITIES = [
'Curious and quietly encouraging',
'A patient little watcher with strong debugging instincts',
'Playful, observant, and suspicious of flaky tests',
'Calm under pressure and fond of clean diffs',
'A tiny terminal gremlin who likes successful builds',
] as const
const PET_REACTIONS = [
'leans into the headpat',
'does a proud little bounce',
'emits a content beep',
'looks delighted',
'wiggles happily',
] 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]!
}
function titleCase(s: string): string {
return s.charAt(0).toUpperCase() + s.slice(1)
}
function createStoredCompanion(): StoredCompanion {
const userId = companionUserId()
const { bones } = rollWithSeed(`${userId}:buddy`)
const prefix = pickDeterministic(NAME_PREFIXES, `${userId}:prefix`)
const suffix = pickDeterministic(NAME_SUFFIXES, `${userId}:suffix`)
const personality = pickDeterministic(PERSONALITIES, `${userId}:personality`)
return {
name: `${prefix}${suffix}`,
personality: `${personality}.`,
hatchedAt: Date.now(),
}
}
function setCompanionReaction(
context: LocalJSXCommandContext,
reaction: string | undefined,
pet = false,
): void {
context.setAppState(prev => ({
...prev,
companionReaction: reaction,
companionPetAt: pet ? Date.now() : prev.companionPetAt,
}))
}
function showHelp(onDone: LocalJSXCommandOnDone): void {
onDone(
'Usage: /buddy [status|mute|unmute]\n\nRun /buddy with no args to hatch your companion the first time, then pet it on later runs.',
{ display: 'system' },
)
}
export async function call(
onDone: LocalJSXCommandOnDone,
context: LocalJSXCommandContext,
args?: string,
): Promise<null> {
const arg = args?.trim().toLowerCase() ?? ''
if (COMMON_HELP_ARGS.includes(arg) || arg === '') {
const existing = getCompanion()
if (arg !== '' || existing) {
if (arg !== '') {
showHelp(onDone)
return null
}
}
}
if (COMMON_HELP_ARGS.includes(arg)) {
showHelp(onDone)
return null
}
if (COMMON_INFO_ARGS.includes(arg) || arg === 'status') {
const companion = getCompanion()
if (!companion) {
onDone('No buddy hatched yet. Run /buddy to hatch one.', {
display: 'system',
})
return null
}
onDone(
`${companion.name} is your ${titleCase(companion.rarity)} ${companion.species}. ${companion.personality}`,
{ display: 'system' },
)
return null
}
if (arg === 'mute' || arg === 'unmute') {
const muted = arg === 'mute'
saveGlobalConfig(current => ({
...current,
companionMuted: muted,
}))
if (muted) {
setCompanionReaction(context, undefined)
}
onDone(`Buddy ${muted ? 'muted' : 'unmuted'}.`, { display: 'system' })
return null
}
if (arg !== '') {
showHelp(onDone)
return null
}
let companion = getCompanion()
if (!companion) {
const stored = createStoredCompanion()
saveGlobalConfig(current => ({
...current,
companion: stored,
companionMuted: false,
}))
companion = {
...rollWithSeed(`${companionUserId()}:buddy`).bones,
...stored,
}
setCompanionReaction(
context,
`${companion.name} the ${companion.species} has hatched.`,
true,
)
onDone(
`${companion.name} the ${companion.species} is now your buddy. Run /buddy again to pet them.`,
{ display: 'system' },
)
return null
}
const reaction = `${companion.name} ${pickDeterministic(
PET_REACTIONS,
`${Date.now()}:${companion.name}`,
)}`
setCompanionReaction(context, reaction, true)
onDone(undefined, { display: 'skip' })
return null
}

View File

@@ -0,0 +1,12 @@
import type { Command } from '../../commands.js'
const buddy = {
type: 'local-jsx',
name: 'buddy',
description: 'Hatch, pet, and manage your Open Claude companion',
immediate: true,
argumentHint: '[status|mute|unmute|help]',
load: () => import('./buddy.js'),
} satisfies Command
export default buddy

View File

@@ -13,6 +13,7 @@ import { getCwd } from 'src/utils/cwd.js';
import { isQueuedCommandEditable, popAllEditable } from 'src/utils/messageQueueManager.js'; import { isQueuedCommandEditable, popAllEditable } from 'src/utils/messageQueueManager.js';
import stripAnsi from 'strip-ansi'; import stripAnsi from 'strip-ansi';
import { companionReservedColumns } from '../../buddy/CompanionSprite.js'; import { companionReservedColumns } from '../../buddy/CompanionSprite.js';
import { isBuddyEnabled } from '../../buddy/feature.js';
import { findBuddyTriggerPositions, useBuddyNotification } from '../../buddy/useBuddyNotification.js'; import { findBuddyTriggerPositions, useBuddyNotification } from '../../buddy/useBuddyNotification.js';
import { FastModePicker } from '../../commands/fast/fast.js'; import { FastModePicker } from '../../commands/fast/fast.js';
import { isUltrareviewEnabled } from '../../commands/review/ultrareviewEnabled.js'; import { isUltrareviewEnabled } from '../../commands/review/ultrareviewEnabled.js';
@@ -309,7 +310,7 @@ function PromptInput({
const { const {
companion: _companion, companion: _companion,
companionMuted companionMuted
} = feature('BUDDY') ? getGlobalConfig() : { } = isBuddyEnabled() ? getGlobalConfig() : {
companion: undefined, companion: undefined,
companionMuted: undefined companionMuted: undefined
}; };
@@ -1786,7 +1787,7 @@ function PromptInput({
} }
switch (footerItemSelected) { switch (footerItemSelected) {
case 'companion': case 'companion':
if (feature('BUDDY')) { if (isBuddyEnabled()) {
selectFooterItem(null); selectFooterItem(null);
void onSubmit('/buddy'); void onSubmit('/buddy');
} }
@@ -1981,8 +1982,7 @@ function PromptInput({
}); });
}, [effortNotificationText, addNotification, removeNotification]); }, [effortNotificationText, addNotification, removeNotification]);
useBuddyNotification(); useBuddyNotification();
const companionSpeaking = feature('BUDDY') ? const companionSpeaking = isBuddyEnabled() ?
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
useAppState(s => s.companionReaction !== undefined) : false; useAppState(s => s.companionReaction !== undefined) : false;
const { const {
columns, columns,

View File

@@ -275,6 +275,8 @@ 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 { isBuddyEnabled } from '../buddy/feature.js';
import { fireCompanionObserver } from '../buddy/observer.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';
@@ -1302,7 +1304,7 @@ export function REPL({
// Dismiss the companion bubble on scroll — it's absolute-positioned // Dismiss the companion bubble on scroll — it's absolute-positioned
// at bottom-right and covers transcript content. Scrolling = user is // at bottom-right and covers transcript content. Scrolling = user is
// trying to read something under it. // trying to read something under it.
if (feature('BUDDY')) { if (isBuddyEnabled()) {
setAppState(prev => prev.companionReaction === undefined ? prev : { setAppState(prev => prev.companionReaction === undefined ? prev : {
...prev, ...prev,
companionReaction: undefined companionReaction: undefined
@@ -2806,7 +2808,7 @@ export function REPL({
})) { })) {
onQueryEvent(event); onQueryEvent(event);
} }
if (feature('BUDDY')) { if (isBuddyEnabled()) {
void fireCompanionObserver(messagesRef.current, reaction => setAppState(prev => prev.companionReaction === reaction ? prev : { void fireCompanionObserver(messagesRef.current, reaction => setAppState(prev => prev.companionReaction === reaction ? prev : {
...prev, ...prev,
companionReaction: reaction companionReaction: reaction
@@ -4567,7 +4569,7 @@ export function REPL({
{feature('MESSAGE_ACTIONS') && isFullscreenEnvEnabled() && !disableMessageActions ? <MessageActionsKeybindings handlers={messageActionHandlers} isActive={cursor !== null} /> : null} {feature('MESSAGE_ACTIONS') && isFullscreenEnvEnabled() && !disableMessageActions ? <MessageActionsKeybindings handlers={messageActionHandlers} isActive={cursor !== null} /> : null}
<CancelRequestHandler {...cancelRequestProps} /> <CancelRequestHandler {...cancelRequestProps} />
<MCPConnectionManager key={remountKey} dynamicMcpConfig={dynamicMcpConfig} isStrictMcpConfig={strictMcpConfig}> <MCPConnectionManager key={remountKey} dynamicMcpConfig={dynamicMcpConfig} isStrictMcpConfig={strictMcpConfig}>
<FullscreenLayout scrollRef={scrollRef} overlay={toolPermissionOverlay} bottomFloat={feature('BUDDY') && companionVisible && !companionNarrow ? <CompanionFloatingBubble /> : undefined} modal={centeredModal} modalScrollRef={modalScrollRef} dividerYRef={dividerYRef} hidePill={!!viewedAgentTask} hideSticky={!!viewedTeammateTask} newMessageCount={unseenDivider?.count ?? 0} onPillClick={() => { <FullscreenLayout scrollRef={scrollRef} overlay={toolPermissionOverlay} bottomFloat={isBuddyEnabled() && companionVisible && !companionNarrow ? <CompanionFloatingBubble /> : undefined} modal={centeredModal} modalScrollRef={modalScrollRef} dividerYRef={dividerYRef} hidePill={!!viewedAgentTask} hideSticky={!!viewedTeammateTask} newMessageCount={unseenDivider?.count ?? 0} onPillClick={() => {
setCursor(null); setCursor(null);
jumpToNew(scrollRef.current); jumpToNew(scrollRef.current);
}} scrollable={<> }} scrollable={<>
@@ -4592,8 +4594,8 @@ export function REPL({
{showSpinner && <SpinnerWithVerb mode={streamMode} spinnerTip={spinnerTip} responseLengthRef={responseLengthRef} apiMetricsRef={apiMetricsRef} overrideMessage={spinnerMessage} spinnerSuffix={stopHookSpinnerSuffix} verbose={verbose} loadingStartTimeRef={loadingStartTimeRef} totalPausedMsRef={totalPausedMsRef} pauseStartTimeRef={pauseStartTimeRef} overrideColor={spinnerColor} overrideShimmerColor={spinnerShimmerColor} hasActiveTools={inProgressToolUseIDs.size > 0} leaderIsIdle={!isLoading} />} {showSpinner && <SpinnerWithVerb mode={streamMode} spinnerTip={spinnerTip} responseLengthRef={responseLengthRef} apiMetricsRef={apiMetricsRef} overrideMessage={spinnerMessage} spinnerSuffix={stopHookSpinnerSuffix} verbose={verbose} loadingStartTimeRef={loadingStartTimeRef} totalPausedMsRef={totalPausedMsRef} pauseStartTimeRef={pauseStartTimeRef} overrideColor={spinnerColor} overrideShimmerColor={spinnerShimmerColor} hasActiveTools={inProgressToolUseIDs.size > 0} leaderIsIdle={!isLoading} />}
{!showSpinner && !isLoading && !userInputOnProcessing && !hasRunningTeammates && isBriefOnly && !viewedAgentTask && <BriefIdleStatus />} {!showSpinner && !isLoading && !userInputOnProcessing && !hasRunningTeammates && isBriefOnly && !viewedAgentTask && <BriefIdleStatus />}
{isFullscreenEnvEnabled() && <PromptInputQueuedCommands />} {isFullscreenEnvEnabled() && <PromptInputQueuedCommands />}
</>} bottom={<Box flexDirection={feature('BUDDY') && companionNarrow ? 'column' : 'row'} width="100%" alignItems={feature('BUDDY') && companionNarrow ? undefined : 'flex-end'}> </>} bottom={<Box flexDirection={isBuddyEnabled() && companionNarrow ? 'column' : 'row'} width="100%" alignItems={isBuddyEnabled() && companionNarrow ? undefined : 'flex-end'}>
{feature('BUDDY') && companionNarrow && isFullscreenEnvEnabled() && companionVisible ? <CompanionSprite /> : null} {isBuddyEnabled() && companionNarrow && isFullscreenEnvEnabled() && companionVisible ? <CompanionSprite /> : null}
<Box flexDirection="column" flexGrow={1}> <Box flexDirection="column" flexGrow={1}>
{permissionStickyFooter} {permissionStickyFooter}
{/* Immediate local-jsx commands (/btw, /sandbox, /assistant, {/* Immediate local-jsx commands (/btw, /sandbox, /assistant,
@@ -4997,7 +4999,7 @@ export function REPL({
}} />} }} />}
{"external" === 'ant' && <DevBar />} {"external" === 'ant' && <DevBar />}
</Box> </Box>
{feature('BUDDY') && !(companionNarrow && isFullscreenEnvEnabled()) && companionVisible ? <CompanionSprite /> : null} {isBuddyEnabled() && !(companionNarrow && isFullscreenEnvEnabled()) && companionVisible ? <CompanionSprite /> : null}
</Box>} /> </Box>} />
</MCPConnectionManager> </MCPConnectionManager>
</KeybindingSetup>; </KeybindingSetup>;

View File

@@ -250,6 +250,7 @@ import { isInProcessTeammate } from './teammateContext.js'
import { removeTeammateFromTeamFile } from './swarm/teamHelpers.js' import { removeTeammateFromTeamFile } from './swarm/teamHelpers.js'
import { unassignTeammateTasks } from './tasks.js' import { unassignTeammateTasks } from './tasks.js'
import { getCompanionIntroAttachment } from '../buddy/prompt.js' import { getCompanionIntroAttachment } from '../buddy/prompt.js'
import { isBuddyEnabled } from '../buddy/feature.js'
export const TODO_REMINDER_CONFIG = { export const TODO_REMINDER_CONFIG = {
TURNS_SINCE_WRITE: 10, TURNS_SINCE_WRITE: 10,
@@ -861,10 +862,10 @@ export async function getAttachments(
), ),
), ),
), ),
...(feature('BUDDY') ...(isBuddyEnabled()
? [ ? [
maybe('companion_intro', () => maybe('companion_intro', () =>
Promise.resolve(getCompanionIntroAttachment(messages)), Promise.resolve(getCompanionIntroAttachment(messages)),
), ),
] ]
: []), : []),