Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ similar = "2.5"
urlencoding = "2.1"

# Tauri (desktop only)
tauri = { version = "2", features = ["unstable"] }
tauri = { version = "2", features = ["unstable", "macos-private-api"] }
tauri-plugin-opener = "2"
tauri-plugin-dialog = "2.6"
tauri-plugin-fs = "2"
Expand Down
2 changes: 1 addition & 1 deletion src/apps/desktop/capabilities/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "BitFun default capabilities",
"windows": ["main"],
"windows": ["main", "agent-companion-pet"],
"permissions": [
"log:default",
"autostart:default",
Expand Down
2 changes: 2 additions & 0 deletions src/apps/desktop/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,8 @@ pub async fn run() {
fix_mermaid_code,
get_app_state,
update_app_status,
theme::show_agent_companion_desktop_pet,
theme::hide_agent_companion_desktop_pet,
list_agent_companion_pets,
import_agent_companion_pet_package,
delete_agent_companion_pet_package,
Expand Down
122 changes: 119 additions & 3 deletions src/apps/desktop/src/theme.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ use bitfun_core::infrastructure::try_get_path_manager_arc;
use bitfun_core::service::config::types::GlobalConfig;
use dark_light::Mode;
use log::{debug, error, warn};
use tauri::WebviewUrl;
use tauri::{Manager, WebviewUrl};

const AGENT_COMPANION_WINDOW_LABEL: &str = "agent-companion-pet";
const AGENT_COMPANION_WINDOW_WIDTH: f64 = 360.0;
const AGENT_COMPANION_WINDOW_HEIGHT: f64 = 180.0;
const AGENT_COMPANION_WINDOW_MARGIN: i32 = 64;

#[derive(Debug, Clone)]
pub struct ThemeConfig {
Expand Down Expand Up @@ -297,10 +302,121 @@ pub fn create_main_window(app_handle: &tauri::AppHandle) {
}
}

fn app_url(path: &str) -> WebviewUrl {
if cfg!(debug_assertions) {
match format!("http://localhost:1422/{}", path).parse() {
Ok(url) => WebviewUrl::External(url),
Err(e) => {
error!("Invalid dev URL, fallback to app URL: {}", e);
WebviewUrl::App(path.into())
}
}
} else {
let app_path = if path.starts_with('?') {
format!("index.html{}", path)
} else {
path.to_string()
};
WebviewUrl::App(app_path.into())
}
}

fn position_agent_companion_window(app: &tauri::AppHandle, window: &tauri::WebviewWindow) {
let monitor: Option<tauri::Monitor> = window
.current_monitor()
.ok()
.flatten()
.or_else(|| app.primary_monitor().ok().flatten());

let Some(monitor) = monitor else {
return;
};

let scale_factor = monitor.scale_factor();
let area = monitor.work_area();
let area_position = area.position.to_logical::<f64>(scale_factor);
let area_size = area.size.to_logical::<f64>(scale_factor);
let x = area_position.x + area_size.width
- AGENT_COMPANION_WINDOW_WIDTH
- f64::from(AGENT_COMPANION_WINDOW_MARGIN);
let y = area_position.y + area_size.height
- AGENT_COMPANION_WINDOW_HEIGHT
- f64::from(AGENT_COMPANION_WINDOW_MARGIN);

if let Err(e) = window.set_position(tauri::LogicalPosition::new(
x.max(area_position.x),
y.max(area_position.y),
)) {
warn!("Failed to position Agent companion window: {}", e);
}
}

#[tauri::command]
pub async fn show_main_window(app: tauri::AppHandle) -> Result<(), String> {
use tauri::Manager;
pub async fn show_agent_companion_desktop_pet(app: tauri::AppHandle) -> Result<(), String> {
if let Some(window) = app.get_webview_window(AGENT_COMPANION_WINDOW_LABEL) {
position_agent_companion_window(&app, &window);
window.show().map_err(|e| {
error!("Failed to show Agent companion window: {}", e);
format!("Failed to show Agent companion window: {}", e)
})?;
return Ok(());
}

let url = app_url("?bitfunWindow=agent-companion");
let mut builder = tauri::WebviewWindowBuilder::new(&app, AGENT_COMPANION_WINDOW_LABEL, url)
.title("BitFun Agent Companion")
.inner_size(
AGENT_COMPANION_WINDOW_WIDTH,
AGENT_COMPANION_WINDOW_HEIGHT,
)
.max_inner_size(
AGENT_COMPANION_WINDOW_WIDTH,
AGENT_COMPANION_WINDOW_HEIGHT,
)
.min_inner_size(
AGENT_COMPANION_WINDOW_WIDTH,
AGENT_COMPANION_WINDOW_HEIGHT,
)
.resizable(false)
.decorations(false)
.transparent(true)
.always_on_top(true)
.skip_taskbar(true)
.shadow(false)
.visible(false)
.accept_first_mouse(true)
.background_color(tauri::window::Color(0, 0, 0, 0));

builder = builder.disable_drag_drop_handler();

let window = builder.build().map_err(|e| {
error!("Failed to create Agent companion window: {}", e);
format!("Failed to create Agent companion window: {}", e)
})?;

position_agent_companion_window(&app, &window);

window.show().map_err(|e| {
error!("Failed to show Agent companion window: {}", e);
format!("Failed to show Agent companion window: {}", e)
})?;

Ok(())
}

#[tauri::command]
pub async fn hide_agent_companion_desktop_pet(app: tauri::AppHandle) -> Result<(), String> {
if let Some(window) = app.get_webview_window(AGENT_COMPANION_WINDOW_LABEL) {
window.close().map_err(|e| {
error!("Failed to close Agent companion window: {}", e);
format!("Failed to close Agent companion window: {}", e)
})?;
}
Ok(())
}

#[tauri::command]
pub async fn show_main_window(app: tauri::AppHandle) -> Result<(), String> {
if let Some(main_window) = app.get_webview_window("main") {
#[cfg(target_os = "windows")]
{
Expand Down
1 change: 1 addition & 0 deletions src/apps/desktop/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"security": {
"csp": null
},
"macOSPrivateApi": true,
"withGlobalTauri": true
}
}
63 changes: 62 additions & 1 deletion src/crates/core/src/service/config/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,25 @@ pub struct AIExperienceConfig {
pub enable_visual_mode: bool,
/// Whether to show the pixel Agent companion in the collapsed chat input.
pub enable_agent_companion: bool,
/// Where to show the Agent companion: "input" or "desktop".
pub agent_companion_display_mode: String,
/// Optional Petdex-compatible companion package selected by the user.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub agent_companion_pet: Option<AgentCompanionPetSelection>,
}

/// User-selected Agent companion pet package.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AgentCompanionPetSelection {
pub id: String,
pub display_name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub source: String,
pub package_path: String,
pub spritesheet_path: String,
pub spritesheet_mime_type: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
Expand Down Expand Up @@ -1254,6 +1273,8 @@ impl Default for AIExperienceConfig {
enable_welcome_panel_ai_analysis: false,
enable_visual_mode: false,
enable_agent_companion: true,
agent_companion_display_mode: "desktop".to_string(),
agent_companion_pet: None,
}
}
}
Expand Down Expand Up @@ -1663,7 +1684,7 @@ impl AIModelConfig {

#[cfg(test)]
mod tests {
use super::{AIConfig, AIModelConfig, ReasoningMode};
use super::{AIConfig, AIExperienceConfig, AIModelConfig, ReasoningMode};

#[test]
fn deserializes_compatibility_thinking_flag_into_reasoning_mode() {
Expand All @@ -1683,6 +1704,46 @@ mod tests {
assert!(config.enable_thinking_process);
}

#[test]
fn preserves_selected_agent_companion_pet() {
let config: AIExperienceConfig = serde_json::from_value(serde_json::json!({
"enable_session_title_generation": true,
"enable_welcome_panel_ai_analysis": false,
"enable_visual_mode": false,
"enable_agent_companion": true,
"agent_companion_display_mode": "desktop",
"agent_companion_pet": {
"id": "pixel-panda",
"displayName": "Pixel Panda",
"description": "A gentle panda holding a tiny pixel brush.",
"source": "preset",
"packagePath": "/agent-companion-pets/pixel-panda",
"spritesheetPath": "/agent-companion-pets/pixel-panda/spritesheet.webp",
"spritesheetMimeType": "image/webp"
}
}))
.expect("AI experience config with selected companion pet should deserialize");

let pet = config
.agent_companion_pet
.as_ref()
.expect("selected companion pet should be retained");
assert_eq!(pet.id, "pixel-panda");
assert_eq!(pet.display_name, "Pixel Panda");
assert_eq!(pet.package_path, "/agent-companion-pets/pixel-panda");
assert_eq!(config.agent_companion_display_mode, "desktop");

let serialized = serde_json::to_value(&config).expect("config should serialize");
assert_eq!(
serialized["agent_companion_pet"]["displayName"],
"Pixel Panda"
);
assert_eq!(
serialized["agent_companion_pet"]["spritesheetPath"],
"/agent-companion-pets/pixel-panda/spritesheet.webp"
);
}

#[test]
fn deserializes_compatibility_false_thinking_flag_into_default_reasoning_mode() {
let config: AIModelConfig = serde_json::from_value(serde_json::json!({
Expand Down
26 changes: 26 additions & 0 deletions src/web-ui/src/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ import { NotificationContainer, NotificationCenter } from '../shared/notificatio
import { AnnouncementProvider } from '../shared/announcement-system';
import { ConfirmDialogRenderer } from '../component-library';
import { createLogger } from '@/shared/utils/logger';
import { aiExperienceConfigService } from '@/infrastructure/config/services/AIExperienceConfigService';
import { syncAgentCompanionDesktopWindow } from '@/infrastructure/config/services/AgentCompanionWindowService';
import { buildAgentCompanionActivity, subscribeAgentCompanionActivity } from '@/flow_chat/utils/agentCompanionActivity';
import { emitAgentCompanionActivity } from '@/flow_chat/services/AgentCompanionActivityBridge';
import { useWorkspaceContext } from '../infrastructure/contexts/WorkspaceContext';
import SplashScreen from './components/SplashScreen/SplashScreen';
import { useGlobalSceneShortcuts } from './hooks/useGlobalSceneShortcuts';
Expand Down Expand Up @@ -159,6 +163,28 @@ function App() {

}, []);

useEffect(() => {
const emitCurrentAgentCompanionActivity = () => {
void emitAgentCompanionActivity(buildAgentCompanionActivity());
};

void aiExperienceConfigService.getSettingsAsync().then(async settings => {
await syncAgentCompanionDesktopWindow(settings);
emitCurrentAgentCompanionActivity();
window.setTimeout(emitCurrentAgentCompanionActivity, 250);
});
return aiExperienceConfigService.addChangeListener(settings => {
void syncAgentCompanionDesktopWindow(settings).then(() => {
emitCurrentAgentCompanionActivity();
window.setTimeout(emitCurrentAgentCompanionActivity, 250);
});
});
}, []);

useEffect(() => subscribeAgentCompanionActivity(activity => {
void emitAgentCompanionActivity(activity);
}), []);

// Observe AI initialization state
useEffect(() => {
if (aiError) {
Expand Down
Loading
Loading