diff --git a/ui-tui/src/__tests__/submissionCore.test.ts b/ui-tui/src/__tests__/submissionCore.test.ts index 83b89a088c8..23b83e87468 100644 --- a/ui-tui/src/__tests__/submissionCore.test.ts +++ b/ui-tui/src/__tests__/submissionCore.test.ts @@ -38,7 +38,6 @@ function makeDeps(gw: GatewayClient, over: Partial = {}): Subm enqueue: vi.fn(), expand: (t: string) => t, gw, - maybeGoodVibes: vi.fn(), setLastUserMsg: vi.fn(), sys: vi.fn(), ...over diff --git a/ui-tui/src/app/createGatewayEventHandler.ts b/ui-tui/src/app/createGatewayEventHandler.ts index d9cbf30663e..5bb819790d4 100644 --- a/ui-tui/src/app/createGatewayEventHandler.ts +++ b/ui-tui/src/app/createGatewayEventHandler.ts @@ -20,7 +20,7 @@ import type { Msg, SubagentProgress, SubagentStatus } from '../types.js' import { applyDelegationStatus, getDelegationState } from './delegationStore.js' import type { GatewayEventHandlerContext } from './interfaces.js' import { getOverlayState, patchOverlayState } from './overlayStore.js' -import { flashPet } from './petFlashStore.js' +import { flashGoodVibes, flashPet } from './petFlashStore.js' import { turnController } from './turnController.js' import { getTurnState } from './turnStore.js' import { getUiState, patchUiState } from './uiStore.js' @@ -717,6 +717,14 @@ export function createGatewayEventHandler(ctx: GatewayEventHandlerContext): (ev: return + case 'reaction': + // Core-detected affection (ily / <3 / good bot): flash the ♥ and let the + // pet celebrate. Same signal drives the desktop's floating hearts. + flashGoodVibes() + flashPet('jump') + + return + case 'tool.start': turnController.recordTodos(ev.payload.todos) turnController.recordToolStart( diff --git a/ui-tui/src/app/petFlashStore.ts b/ui-tui/src/app/petFlashStore.ts index b1ecf97e995..48841a3c4be 100644 --- a/ui-tui/src/app/petFlashStore.ts +++ b/ui-tui/src/app/petFlashStore.ts @@ -14,6 +14,13 @@ export const $petFlash = atom(null) export const flashPet = (state: PetState, ms = 1600) => $petFlash.set({ state, until: Date.now() + ms }) +// Affection-heart beat: a monotonic tick the status-bar ♥ flashes on. Bumped by +// the gateway `reaction` event (core-detected ily / <3 / good bot) — the TUI's +// share of the same signal that plays the desktop's floating hearts. +export const $goodVibesTick = atom(0) + +export const flashGoodVibes = () => $goodVibesTick.set($goodVibesTick.get() + 1) + // The floating pet's footprint, or null when no pet is shown. The transcript // keeps its text clear of the pet responsively: on wide terminals it reserves a // right gutter (`width`) so lines wrap to the pet's LEFT; on narrow terminals it diff --git a/ui-tui/src/app/submissionCore.ts b/ui-tui/src/app/submissionCore.ts index 7c561b745ba..0ec3921bb3f 100644 --- a/ui-tui/src/app/submissionCore.ts +++ b/ui-tui/src/app/submissionCore.ts @@ -15,7 +15,6 @@ export interface SubmitPromptDeps { enqueue: (text: string) => void expand: (text: string) => string gw: GatewayClient - maybeGoodVibes: (text: string) => void setLastUserMsg: (value: string) => void sys: (text: string) => void } @@ -61,7 +60,6 @@ export function submitPrompt(text: string, deps: SubmitPromptDeps, showUserMessa } turnController.clearStatusTimer() - deps.maybeGoodVibes(submitText) deps.setLastUserMsg(text) if (show) { diff --git a/ui-tui/src/app/useMainApp.ts b/ui-tui/src/app/useMainApp.ts index 33ab9b2f3e3..50a94c9d45a 100644 --- a/ui-tui/src/app/useMainApp.ts +++ b/ui-tui/src/app/useMainApp.ts @@ -37,6 +37,7 @@ import { planGatewayRecovery } from './gatewayRecovery.js' import { getInputSelection } from './inputSelectionStore.js' import { type GatewayRpc, type TranscriptRow } from './interfaces.js' import { $overlayState, patchOverlayState } from './overlayStore.js' +import { $goodVibesTick } from './petFlashStore.js' import { scrollWithSelectionBy } from './scroll.js' import { turnController } from './turnController.js' import { patchTurnState, useTurnSelector } from './turnStore.js' @@ -48,7 +49,6 @@ import { useLongRunToolCharms } from './useLongRunToolCharms.js' import { useSessionLifecycle } from './useSessionLifecycle.js' import { useSubmission } from './useSubmission.js' -const GOOD_VIBES_RE = /\b(good bot|thanks|thank you|thx|ty|ily|love you)\b/i const BRACKET_PASTE_ON = '\x1b[?2004h' const BRACKET_PASTE_OFF = '\x1b[?2004l' const MAX_HEIGHT_CACHE_BUCKETS = 12 @@ -185,7 +185,8 @@ export function useMainApp(gw: GatewayClient) { const [sessionStartedAt, setSessionStartedAt] = useState(() => Date.now()) const [turnStartedAt, setTurnStartedAt] = useState(null) const [lastTurnEndedAt, setLastTurnEndedAt] = useState(null) - const [goodVibesTick, setGoodVibesTick] = useState(0) + // Bumped by the gateway `reaction` event (core-detected affection). + const goodVibesTick = useStore($goodVibesTick) const [bellOnComplete, setBellOnComplete] = useState(false) const ui = useStore($uiState) @@ -445,12 +446,6 @@ export function useMainApp(gw: GatewayClient) { [sys] ) - const maybeGoodVibes = useCallback((text: string) => { - if (GOOD_VIBES_RE.test(text)) { - setGoodVibesTick(v => v + 1) - } - }, []) - const rpc: GatewayRpc = useCallback( async = Record>( method: string, @@ -690,7 +685,6 @@ export function useMainApp(gw: GatewayClient) { composerRefs, composerState, gw, - maybeGoodVibes, setLastUserMsg, slashRef, submitRef, diff --git a/ui-tui/src/app/useSubmission.ts b/ui-tui/src/app/useSubmission.ts index 6ece0bf6412..6e02d349a1d 100644 --- a/ui-tui/src/app/useSubmission.ts +++ b/ui-tui/src/app/useSubmission.ts @@ -37,7 +37,6 @@ export function useSubmission(opts: UseSubmissionOptions) { composerRefs, composerState, gw, - maybeGoodVibes, setLastUserMsg, slashRef, submitRef, @@ -87,14 +86,13 @@ export function useSubmission(opts: UseSubmissionOptions) { enqueue: composerActions.enqueue, expand, gw, - maybeGoodVibes, setLastUserMsg, sys }, showUserMessage ) }, - [appendMessage, composerActions, composerState.pasteSnips, gw, maybeGoodVibes, setLastUserMsg, sys] + [appendMessage, composerActions, composerState.pasteSnips, gw, setLastUserMsg, sys] ) const shellExec = useCallback( @@ -362,7 +360,6 @@ export interface UseSubmissionOptions { composerRefs: ComposerRefs composerState: ComposerState gw: GatewayClient - maybeGoodVibes: (text: string) => void setLastUserMsg: (value: string) => void slashRef: MutableRefObject<(cmd: string) => boolean> submitRef: MutableRefObject<(value: string) => void> diff --git a/ui-tui/src/gatewayTypes.ts b/ui-tui/src/gatewayTypes.ts index ee6b8d78c45..6eb0317fc2f 100644 --- a/ui-tui/src/gatewayTypes.ts +++ b/ui-tui/src/gatewayTypes.ts @@ -618,6 +618,7 @@ export type GatewayEvent = | { payload?: GatewaySkin; session_id?: string; type: 'skin.changed' } | { payload: SessionInfo; session_id?: string; type: 'session.info' } | { payload?: { text?: string }; session_id?: string; type: 'thinking.delta' } + | { payload?: { kind?: string }; session_id?: string; type: 'reaction' } | { payload?: undefined; session_id?: string; type: 'message.start' } | { payload?: { kind?: string; text?: string }; session_id?: string; type: 'status.update' } | {