From 24da30d0b2d3a94a6b5ef027e2cf1a5d9cd445ef Mon Sep 17 00:00:00 2001 From: chenjingping Date: Wed, 17 Jun 2026 17:14:13 +0800 Subject: [PATCH 1/3] =?UTF-8?q?fix:=20=E5=AE=8C=E5=96=84=20Agent=20Compani?= =?UTF-8?q?on=20=E5=AE=8C=E6=88=90=E6=B0=94=E6=B3=A15s=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E5=85=B3=E9=97=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/dev.cjs | 2 +- src/apps/desktop/Cargo.toml | 2 +- .../assembly/core/src/service/config/types.rs | 4 +- .../agent-companion-pets/bitfun/pet.json | 2 +- src/web-ui/src/app/App.tsx | 97 +++++++++++++++++++ .../AgentCompanionDesktopPet.tsx | 37 ++++++- .../flow-chat-manager/EventHandlerModule.ts | 8 +- 7 files changed, 142 insertions(+), 10 deletions(-) diff --git a/scripts/dev.cjs b/scripts/dev.cjs index 6237dfd3b..9b2e7fdcc 100644 --- a/scripts/dev.cjs +++ b/scripts/dev.cjs @@ -67,7 +67,7 @@ function isDesktopMode(mode) { function getDesktopBinaryPath() { const suffix = process.platform === 'win32' ? '.exe' : ''; - const binaryName = `bitfun-desktop${suffix}`; + const binaryName = `BitFun${suffix}`; if (process.platform === 'darwin') { return path.join(ROOT_DIR, 'target', 'debug', 'BitFun.app', 'Contents', 'MacOS', 'BitFun'); diff --git a/src/apps/desktop/Cargo.toml b/src/apps/desktop/Cargo.toml index 53209d9f2..01141397a 100644 --- a/src/apps/desktop/Cargo.toml +++ b/src/apps/desktop/Cargo.toml @@ -10,7 +10,7 @@ name = "bitfun_desktop_lib" crate-type = ["staticlib", "cdylib", "rlib"] [[bin]] -name = "bitfun-desktop" +name = "BitFun" path = "src/main.rs" [build-dependencies] diff --git a/src/crates/assembly/core/src/service/config/types.rs b/src/crates/assembly/core/src/service/config/types.rs index 9e5ab803f..0d062c72a 100644 --- a/src/crates/assembly/core/src/service/config/types.rs +++ b/src/crates/assembly/core/src/service/config/types.rs @@ -216,7 +216,7 @@ pub struct AgentCompanionPetSelection { fn default_agent_companion_pet() -> Option { Some(AgentCompanionPetSelection { id: "bitfun".to_string(), - display_name: "Bitfun".to_string(), + display_name: "BitFun".to_string(), description: Some( "BitFun's mascot — Bifang, a figure from Chinese mythology said to live on Mount Zhang'e. In the Classic of Mountains and Seas (Shan Hai Jing · Western Mountains), Bifang is described as crane-like with one foot, blue feathers marked with red, and a white beak.".to_string(), ), @@ -1899,7 +1899,7 @@ mod tests { .as_ref() .expect("default companion pet should be present"); assert_eq!(pet.id, "bitfun"); - assert_eq!(pet.display_name, "Bitfun"); + assert_eq!(pet.display_name, "BitFun"); assert_eq!(pet.package_path, "/agent-companion-pets/bitfun"); assert_eq!( pet.spritesheet_path, diff --git a/src/web-ui/public/agent-companion-pets/bitfun/pet.json b/src/web-ui/public/agent-companion-pets/bitfun/pet.json index ba1fff112..52922c8c7 100644 --- a/src/web-ui/public/agent-companion-pets/bitfun/pet.json +++ b/src/web-ui/public/agent-companion-pets/bitfun/pet.json @@ -1,6 +1,6 @@ { "id": "bitfun", - "displayName": "Bitfun", + "displayName": "BitFun", "description": "BitFun's mascot — Bifang (毕方), a figure from Chinese mythology said to live on Mount Zhang'e. In the Classic of Mountains and Seas (Shan Hai Jing · Western Mountains), Bifang is described as crane-like with one foot, blue feathers marked with red, and a white beak.", "spritesheetPath": "spritesheet.webp" } diff --git a/src/web-ui/src/app/App.tsx b/src/web-ui/src/app/App.tsx index 3b2bf47d2..ec4f5f20a 100644 --- a/src/web-ui/src/app/App.tsx +++ b/src/web-ui/src/app/App.tsx @@ -25,6 +25,7 @@ import { isStartupOverlayPresent, } from './startup/startupOverlay'; import { ToolbarModeProvider } from '../flow_chat/components/toolbar-mode/ToolbarModeProvider'; +import { FlowChatStore } from '@/flow_chat/store/FlowChatStore'; const log = createLogger('App'); @@ -510,6 +511,102 @@ function App() { }; }, []); + // Listen for the Agent Companion pet window requesting completion dismissal. + // The pet window cannot call FlowChatStore directly because it runs in a + // separate WebView with its own JavaScript context (separate singleton). + useEffect(() => { + let unlisten: (() => void) | null = null; + void import('@tauri-apps/api/event') + .then(({ listen }) => listen<{ sessionId?: string }>( + 'agent-companion://dismiss-completion', + event => { + const sessionId = event.payload?.sessionId; + if (!sessionId) return; + FlowChatStore.getInstance().clearSessionUnreadCompletion(sessionId); + }, + )) + .then(removeListener => { + unlisten = removeListener; + }) + .catch(error => { + log.warn('Failed to listen for Agent companion dismiss-completion events', error); + }); + + return () => { + unlisten?.(); + }; + }, []); + + // Auto-dismiss Agent Companion completion bubbles after 5 seconds. + // Subscribes directly to FlowChatStore (not the activity bridge) to avoid + // any timing issues in the buildAgentCompanionActivity → emit chain. + // Timers live in the main window so they aren't throttled by the WebView + // engine (the pet window is a small unfocused overlay window). + useEffect(() => { + const AUTO_DISMISS_DELAY_MS = 5000; + const store = FlowChatStore.getInstance(); + const timers = new Map>(); + const scheduled = new Set(); + + const checkAndSchedule = () => { + const state = store.getState(); + const completionKinds: ReadonlySet = new Set([ + 'completed', + 'error', + 'interrupted', + ]); + + state.sessions.forEach(session => { + const sessionId = session.sessionId; + const hasCompletion = completionKinds.has(session.hasUnreadCompletion); + + if (hasCompletion && !scheduled.has(sessionId)) { + scheduled.add(sessionId); + log.info('[AgentCompanion auto-dismiss] Scheduling timer', { sessionId, completionKind: session.hasUnreadCompletion }); + const timerId = setTimeout(() => { + log.info('[AgentCompanion auto-dismiss] Timer fired, clearing unread completion', { sessionId }); + store.clearSessionUnreadCompletion(sessionId); + timers.delete(sessionId); + scheduled.delete(sessionId); + }, AUTO_DISMISS_DELAY_MS); + timers.set(sessionId, timerId); + } else if (!hasCompletion && scheduled.has(sessionId)) { + const timerId = timers.get(sessionId); + if (timerId) { + clearTimeout(timerId); + timers.delete(sessionId); + } + scheduled.delete(sessionId); + log.info('[AgentCompanion auto-dismiss] Cancelled timer (completion cleared)', { sessionId }); + } + }); + + // Clean up timers for sessions that no longer exist. + const existingIds = new Set( + Array.from(state.sessions.values()).map(s => s.sessionId), + ); + timers.forEach((timerId, sessionId) => { + if (!existingIds.has(sessionId)) { + clearTimeout(timerId); + timers.delete(sessionId); + scheduled.delete(sessionId); + log.info('[AgentCompanion auto-dismiss] Cancelled timer (session removed)', { sessionId }); + } + }); + }; + + log.info('[AgentCompanion auto-dismiss] Subscribing to FlowChatStore'); + const unsubscribe = store.subscribe(checkAndSchedule); + // Run immediately for any pre-existing completions (e.g. restored sessions). + checkAndSchedule(); + + return () => { + log.info('[AgentCompanion auto-dismiss] Unsubscribing, clearing timers'); + unsubscribe(); + timers.forEach(timerId => clearTimeout(timerId)); + }; + }, []); + // Block browser-native Ctrl+F (find bar) and Ctrl+R (hard reload). // On macOS the equivalent modifiers are Cmd+F / Cmd+R. useEffect(() => { diff --git a/src/web-ui/src/app/components/AgentCompanionDesktopPet/AgentCompanionDesktopPet.tsx b/src/web-ui/src/app/components/AgentCompanionDesktopPet/AgentCompanionDesktopPet.tsx index b92bb5c2c..91717e95e 100644 --- a/src/web-ui/src/app/components/AgentCompanionDesktopPet/AgentCompanionDesktopPet.tsx +++ b/src/web-ui/src/app/components/AgentCompanionDesktopPet/AgentCompanionDesktopPet.tsx @@ -1,11 +1,11 @@ import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { listen } from '@tauri-apps/api/event'; +import { listen, emit } from '@tauri-apps/api/event'; import { cursorPosition, getCurrentWindow } from '@tauri-apps/api/window'; import { aiExperienceConfigService, type AgentCompanionPetSelection, type AIExperienceSettings } from '@/infrastructure/config/services/AIExperienceConfigService'; import { ChatInputPixelPet, type ChatInputPixelPetMood } from '@/flow_chat/components/ChatInputPixelPet'; import type { ChatInputPetMood } from '@/flow_chat/utils/chatInputPetMood'; -import type { AgentCompanionActivityPayload, AgentCompanionTaskStatus } from '@/flow_chat/utils/agentCompanionActivity'; +import type { AgentCompanionActivityPayload, AgentCompanionTaskState, AgentCompanionTaskStatus } from '@/flow_chat/utils/agentCompanionActivity'; import { createLogger } from '@/shared/utils/logger'; import './AgentCompanionDesktopPet.scss'; @@ -25,6 +25,12 @@ const POINTER_HOVER_POLL_INTERVAL_MS = 120; /** Clicks shorter/smaller than this use `show_main_window`; beyond it we start a native drag. */ const PET_DRAG_THRESHOLD_PX = 8; const IS_WINDOWS_WEBVIEW = /\bWindows\b/i.test(window.navigator.userAgent); +/** Tasks in these states are completion bubbles that can be dismissed. */ +const COMPLETION_TASK_STATES: ReadonlySet = new Set([ + 'completed', + 'error', + 'interrupted', +]); interface TypewriterOutputState { target: string; @@ -343,6 +349,28 @@ export const AgentCompanionDesktopPet: React.FC = () => { } }, []); + /** Dismiss all completed/error/interrupted task bubbles immediately + * by emitting a cross-window Tauri event. The main window listens + * for this event and clears the unread-completion flag in its own + * FlowChatStore, which triggers a store subscription that re-emits + * the activity payload (now without the completion task). */ + const dismissCompletedTasks = useCallback(async () => { + const completionSessionIds = tasks + .filter(t => COMPLETION_TASK_STATES.has(t.state)) + .map(t => t.sessionId); + + if (completionSessionIds.length === 0) return; + + try { + await Promise.all(completionSessionIds.map(sessionId => + emit('agent-companion://dismiss-completion', { sessionId }), + )); + log.info('Dismissed completed task bubbles via user click'); + } catch (error) { + log.warn('Failed to dismiss completion tasks via cross-window event', error); + } + }, [tasks]); + const onContextMenu = useCallback((event: React.MouseEvent) => { event.preventDefault(); }, []); @@ -407,6 +435,11 @@ export const AgentCompanionDesktopPet: React.FC = () => { const shouldShowMain = !session.dragStarted; clearPetPointerSession(event.currentTarget, event.pointerId); if (shouldShowMain) { + // Dismiss completed task bubbles when user clicks the pet. + const hasCompletedTasks = tasks.some(t => COMPLETION_TASK_STATES.has(t.state)); + if (hasCompletedTasks) { + void dismissCompletedTasks(); + } void showMainWindowFromPet(); } }; diff --git a/src/web-ui/src/flow_chat/services/flow-chat-manager/EventHandlerModule.ts b/src/web-ui/src/flow_chat/services/flow-chat-manager/EventHandlerModule.ts index f09a048b0..c50cf76cf 100644 --- a/src/web-ui/src/flow_chat/services/flow-chat-manager/EventHandlerModule.ts +++ b/src/web-ui/src/flow_chat/services/flow-chat-manager/EventHandlerModule.ts @@ -149,9 +149,11 @@ export const __test_only__ = { mergeParamsPartialEventData, }; -function shouldMarkUnreadCompletion(sessionId: string): boolean { - const activeSessionId = FlowChatStore.getInstance().getState().activeSessionId; - return sessionId !== activeSessionId || !isAppWindowFocused(); +function shouldMarkUnreadCompletion(_sessionId: string): boolean { + // Always mark completions so the Agent Companion pet can show a completion + // bubble. The main window auto-dismisses the unread flag after 5 seconds, + // so there is no risk of stale indicators. + return true; } function logDroppedDataEvent( From 9ac87fafca002c9e6b0775ec1d1af03288adca44 Mon Sep 17 00:00:00 2001 From: chenjingping Date: Thu, 18 Jun 2026 15:06:09 +0800 Subject: [PATCH 2/3] =?UTF-8?q?fix:=20address=20review=20feedback=20?= =?UTF-8?q?=E2=80=94=20revert=20binary=20rename,=20refine=20auto-dismiss,?= =?UTF-8?q?=20add=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Revert bitfun-desktop → BitFun binary rename in Cargo.toml and dev.cjs - Keep shouldMarkUnreadCompletion always true but skip auto-dismiss for active focused sessions (user sees completion directly in chat UI) - Change AgentCompanion auto-dismiss logs from info to debug level - Add shouldMarkUnreadCompletion to __test_only__ with 3 focused tests --- scripts/dev.cjs | 4 ++-- src/apps/desktop/Cargo.toml | 2 +- src/web-ui/src/app/App.tsx | 21 ++++++++++++----- .../EventHandlerModule.test.ts | 23 +++++++++++++++++++ .../flow-chat-manager/EventHandlerModule.ts | 8 +++++-- 5 files changed, 47 insertions(+), 11 deletions(-) diff --git a/scripts/dev.cjs b/scripts/dev.cjs index 9b2e7fdcc..001f556aa 100644 --- a/scripts/dev.cjs +++ b/scripts/dev.cjs @@ -67,10 +67,10 @@ function isDesktopMode(mode) { function getDesktopBinaryPath() { const suffix = process.platform === 'win32' ? '.exe' : ''; - const binaryName = `BitFun${suffix}`; + const binaryName = `bitfun-desktop${suffix}`; if (process.platform === 'darwin') { - return path.join(ROOT_DIR, 'target', 'debug', 'BitFun.app', 'Contents', 'MacOS', 'BitFun'); + return path.join(ROOT_DIR, 'target', 'debug', 'BitFun.app', 'Contents', 'MacOS', 'bitfun-desktop'); } return path.join(ROOT_DIR, 'target', 'debug', binaryName); diff --git a/src/apps/desktop/Cargo.toml b/src/apps/desktop/Cargo.toml index 01141397a..53209d9f2 100644 --- a/src/apps/desktop/Cargo.toml +++ b/src/apps/desktop/Cargo.toml @@ -10,7 +10,7 @@ name = "bitfun_desktop_lib" crate-type = ["staticlib", "cdylib", "rlib"] [[bin]] -name = "BitFun" +name = "bitfun-desktop" path = "src/main.rs" [build-dependencies] diff --git a/src/web-ui/src/app/App.tsx b/src/web-ui/src/app/App.tsx index ec4f5f20a..5bde92559 100644 --- a/src/web-ui/src/app/App.tsx +++ b/src/web-ui/src/app/App.tsx @@ -26,6 +26,7 @@ import { } from './startup/startupOverlay'; import { ToolbarModeProvider } from '../flow_chat/components/toolbar-mode/ToolbarModeProvider'; import { FlowChatStore } from '@/flow_chat/store/FlowChatStore'; +import { isAppWindowFocused } from '@/flow_chat/services/flow-chat-manager/EventHandlerModule'; const log = createLogger('App'); @@ -555,16 +556,24 @@ function App() { 'error', 'interrupted', ]); + const activeSessionId = state.activeSessionId; + const windowFocused = isAppWindowFocused(); state.sessions.forEach(session => { const sessionId = session.sessionId; const hasCompletion = completionKinds.has(session.hasUnreadCompletion); if (hasCompletion && !scheduled.has(sessionId)) { + // Skip auto-dismiss for the active focused session — the user + // sees the completion directly in the chat UI. + if (sessionId === activeSessionId && windowFocused) { + store.clearSessionUnreadCompletion(sessionId); + return; + } scheduled.add(sessionId); - log.info('[AgentCompanion auto-dismiss] Scheduling timer', { sessionId, completionKind: session.hasUnreadCompletion }); + log.debug('[AgentCompanion auto-dismiss] Scheduling timer', { sessionId, completionKind: session.hasUnreadCompletion }); const timerId = setTimeout(() => { - log.info('[AgentCompanion auto-dismiss] Timer fired, clearing unread completion', { sessionId }); + log.debug('[AgentCompanion auto-dismiss] Timer fired, clearing unread completion', { sessionId }); store.clearSessionUnreadCompletion(sessionId); timers.delete(sessionId); scheduled.delete(sessionId); @@ -577,7 +586,7 @@ function App() { timers.delete(sessionId); } scheduled.delete(sessionId); - log.info('[AgentCompanion auto-dismiss] Cancelled timer (completion cleared)', { sessionId }); + log.debug('[AgentCompanion auto-dismiss] Cancelled timer (completion cleared)', { sessionId }); } }); @@ -590,18 +599,18 @@ function App() { clearTimeout(timerId); timers.delete(sessionId); scheduled.delete(sessionId); - log.info('[AgentCompanion auto-dismiss] Cancelled timer (session removed)', { sessionId }); + log.debug('[AgentCompanion auto-dismiss] Cancelled timer (session removed)', { sessionId }); } }); }; - log.info('[AgentCompanion auto-dismiss] Subscribing to FlowChatStore'); + log.debug('[AgentCompanion auto-dismiss] Subscribing to FlowChatStore'); const unsubscribe = store.subscribe(checkAndSchedule); // Run immediately for any pre-existing completions (e.g. restored sessions). checkAndSchedule(); return () => { - log.info('[AgentCompanion auto-dismiss] Unsubscribing, clearing timers'); + log.debug('[AgentCompanion auto-dismiss] Unsubscribing, clearing timers'); unsubscribe(); timers.forEach(timerId => clearTimeout(timerId)); }; diff --git a/src/web-ui/src/flow_chat/services/flow-chat-manager/EventHandlerModule.test.ts b/src/web-ui/src/flow_chat/services/flow-chat-manager/EventHandlerModule.test.ts index 8511f62fa..ab9c6b964 100644 --- a/src/web-ui/src/flow_chat/services/flow-chat-manager/EventHandlerModule.test.ts +++ b/src/web-ui/src/flow_chat/services/flow-chat-manager/EventHandlerModule.test.ts @@ -628,3 +628,26 @@ describe('handleDialogTurnComplete', () => { expect(turn?.hasFinalResponse).toBe(false); }); }); + +describe('shouldMarkUnreadCompletion', () => { + const { shouldMarkUnreadCompletion } = __test_only__; + + it('always returns true regardless of session id', () => { + expect(shouldMarkUnreadCompletion('active-session')).toBe(true); + expect(shouldMarkUnreadCompletion('background-session')).toBe(true); + expect(shouldMarkUnreadCompletion('')).toBe(true); + }); + + it('returns true for any string input — all completions are marked so the Agent Companion pet can show completion bubbles', () => { + const sessions = ['session-1', 'session-2', 'session-3']; + sessions.forEach(sessionId => { + expect(shouldMarkUnreadCompletion(sessionId)).toBe(true); + }); + }); + + it('returns true even when the input is undefined or null (defensive)', () => { + // Parameter is typed as `string` but callers may pass unexpected values. + expect(shouldMarkUnreadCompletion(undefined as unknown as string)).toBe(true); + expect(shouldMarkUnreadCompletion(null as unknown as string)).toBe(true); + }); +}); diff --git a/src/web-ui/src/flow_chat/services/flow-chat-manager/EventHandlerModule.ts b/src/web-ui/src/flow_chat/services/flow-chat-manager/EventHandlerModule.ts index c50cf76cf..5f49fd0d0 100644 --- a/src/web-ui/src/flow_chat/services/flow-chat-manager/EventHandlerModule.ts +++ b/src/web-ui/src/flow_chat/services/flow-chat-manager/EventHandlerModule.ts @@ -147,12 +147,16 @@ function mergeParamsPartialEventData( export const __test_only__ = { resolveDialogTurnDisplayContent, mergeParamsPartialEventData, + shouldMarkUnreadCompletion, }; function shouldMarkUnreadCompletion(_sessionId: string): boolean { // Always mark completions so the Agent Companion pet can show a completion - // bubble. The main window auto-dismisses the unread flag after 5 seconds, - // so there is no risk of stale indicators. + // bubble regardless of which session is active. The main window auto-dismisses + // the unread flag after 5 seconds (except for the active focused session, + // where the user sees the completion directly in the chat UI), so there is + // no risk of stale indicators. This trades an extra persistence write per + // completion for reliable cross-window pet notifications. return true; } From 084caa6dcc84d5372891f21c0ce91be4632d453e Mon Sep 17 00:00:00 2001 From: chenjingping Date: Thu, 18 Jun 2026 15:14:41 +0800 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20revert=20active-focused=20early-clea?= =?UTF-8?q?r=20=E2=80=94=20all=20completion=20bubbles=20show=20for=205s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The checkAndSchedule active-focused guard was clearing unreadCompletion immediately, which prevented the Agent Companion pet from ever showing completion bubbles for the active session. Reverted to always schedule the 5 s auto-dismiss timer for every session. --- src/web-ui/src/app/App.tsx | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/web-ui/src/app/App.tsx b/src/web-ui/src/app/App.tsx index 5bde92559..9d181bb66 100644 --- a/src/web-ui/src/app/App.tsx +++ b/src/web-ui/src/app/App.tsx @@ -26,7 +26,6 @@ import { } from './startup/startupOverlay'; import { ToolbarModeProvider } from '../flow_chat/components/toolbar-mode/ToolbarModeProvider'; import { FlowChatStore } from '@/flow_chat/store/FlowChatStore'; -import { isAppWindowFocused } from '@/flow_chat/services/flow-chat-manager/EventHandlerModule'; const log = createLogger('App'); @@ -556,20 +555,12 @@ function App() { 'error', 'interrupted', ]); - const activeSessionId = state.activeSessionId; - const windowFocused = isAppWindowFocused(); state.sessions.forEach(session => { const sessionId = session.sessionId; const hasCompletion = completionKinds.has(session.hasUnreadCompletion); if (hasCompletion && !scheduled.has(sessionId)) { - // Skip auto-dismiss for the active focused session — the user - // sees the completion directly in the chat UI. - if (sessionId === activeSessionId && windowFocused) { - store.clearSessionUnreadCompletion(sessionId); - return; - } scheduled.add(sessionId); log.debug('[AgentCompanion auto-dismiss] Scheduling timer', { sessionId, completionKind: session.hasUnreadCompletion }); const timerId = setTimeout(() => {