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
1 change: 1 addition & 0 deletions src/apps/desktop/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,7 @@ pub async fn run() {
update_app_status,
theme::show_agent_companion_desktop_pet,
theme::hide_agent_companion_desktop_pet,
theme::resize_agent_companion_desktop_pet,
list_agent_companion_pets,
import_agent_companion_pet_package,
delete_agent_companion_pet_package,
Expand Down
117 changes: 100 additions & 17 deletions src/apps/desktop/src/theme.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ use log::{debug, error, warn};
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_MIN_SIZE: f64 = 96.0;
const AGENT_COMPANION_WINDOW_MAX_WIDTH: f64 = 360.0;
const AGENT_COMPANION_WINDOW_MAX_HEIGHT: f64 = 240.0;
const AGENT_COMPANION_WINDOW_MARGIN: i32 = 64;

#[derive(Debug, Clone)]
Expand Down Expand Up @@ -321,31 +322,104 @@ fn app_url(path: &str) -> WebviewUrl {
}
}

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

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

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 window_size = window
.outer_size()
.ok()
.map(|size| size.to_logical::<f64>(scale_factor));
let window_width = window_size
.as_ref()
.map(|size| size.width)
.unwrap_or(AGENT_COMPANION_WINDOW_MIN_SIZE);
let window_height = window_size
.as_ref()
.map(|size| size.height)
.unwrap_or(AGENT_COMPANION_WINDOW_MIN_SIZE);
let x = area_position.x + area_size.width
- AGENT_COMPANION_WINDOW_WIDTH
- window_width
- f64::from(AGENT_COMPANION_WINDOW_MARGIN);
let y = area_position.y + area_size.height
- AGENT_COMPANION_WINDOW_HEIGHT
- window_height
- f64::from(AGENT_COMPANION_WINDOW_MARGIN);

if let Err(e) = window.set_position(tauri::LogicalPosition::new(
Some(tauri::LogicalPosition::new(
x.max(area_position.x),
y.max(area_position.y),
))
}

fn position_agent_companion_window(app: &tauri::AppHandle, window: &tauri::WebviewWindow) {
let Some(position) = agent_companion_default_position(app, window) else {
return;
};

if let Err(e) = window.set_position(position) {
warn!("Failed to position Agent companion window: {}", e);
}
}

fn resize_agent_companion_window(
app: &tauri::AppHandle,
window: &tauri::WebviewWindow,
width: f64,
height: f64,
) {
let monitor: Option<tauri::Monitor> = window
.current_monitor()
.ok()
.flatten()
.or_else(|| app.primary_monitor().ok().flatten());
let scale_factor = monitor.as_ref().map(|monitor| monitor.scale_factor());
let old_size = scale_factor.and_then(|scale_factor| {
window
.outer_size()
.ok()
.map(|size| size.to_logical::<f64>(scale_factor))
});
let old_position = scale_factor.and_then(|scale_factor| {
window
.outer_position()
.ok()
.map(|position| position.to_logical::<f64>(scale_factor))
});

if let Err(e) = window.set_size(tauri::LogicalSize::new(width, height)) {
warn!("Failed to resize Agent companion window: {}", e);
return;
}

let next_position = old_position
.zip(old_size)
.map(|(position, size)| {
tauri::LogicalPosition::new(
position.x + size.width - width,
position.y + size.height - height,
)
})
.or_else(|| agent_companion_default_position(app, window));

let Some(position) = next_position else {
return;
};

if let Err(e) = window.set_position(tauri::LogicalPosition::new(
position.x,
position.y,
)) {
warn!("Failed to position Agent companion window: {}", e);
}
Expand All @@ -366,17 +440,14 @@ pub async fn show_agent_companion_desktop_pet(app: tauri::AppHandle) -> Result<(
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,
AGENT_COMPANION_WINDOW_MIN_SIZE,
AGENT_COMPANION_WINDOW_MIN_SIZE,
)
.max_inner_size(
AGENT_COMPANION_WINDOW_WIDTH,
AGENT_COMPANION_WINDOW_HEIGHT,
)
.min_inner_size(
AGENT_COMPANION_WINDOW_WIDTH,
AGENT_COMPANION_WINDOW_HEIGHT,
AGENT_COMPANION_WINDOW_MAX_WIDTH,
AGENT_COMPANION_WINDOW_MAX_HEIGHT,
)
.min_inner_size(1.0, 1.0)
.resizable(false)
.decorations(false)
.transparent(true)
Expand Down Expand Up @@ -404,6 +475,18 @@ pub async fn show_agent_companion_desktop_pet(app: tauri::AppHandle) -> Result<(
Ok(())
}

#[tauri::command]
pub async fn resize_agent_companion_desktop_pet(
app: tauri::AppHandle,
width: f64,
height: f64,
) -> Result<(), String> {
if let Some(window) = app.get_webview_window(AGENT_COMPANION_WINDOW_LABEL) {
resize_agent_companion_window(&app, &window, width, height);
}
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) {
Expand Down
38 changes: 38 additions & 0 deletions src/web-ui/src/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { aiExperienceConfigService } from '@/infrastructure/config/services/AIEx
import { syncAgentCompanionDesktopWindow } from '@/infrastructure/config/services/AgentCompanionWindowService';
import { buildAgentCompanionActivity, subscribeAgentCompanionActivity } from '@/flow_chat/utils/agentCompanionActivity';
import { emitAgentCompanionActivity } from '@/flow_chat/services/AgentCompanionActivityBridge';
import { FlowChatStore } from '@/flow_chat/store/FlowChatStore';
import { useWorkspaceContext } from '../infrastructure/contexts/WorkspaceContext';
import SplashScreen from './components/SplashScreen/SplashScreen';
import { useGlobalSceneShortcuts } from './hooks/useGlobalSceneShortcuts';
Expand Down Expand Up @@ -185,6 +186,43 @@ function App() {
void emitAgentCompanionActivity(activity);
}), []);

useEffect(() => {
let unlisten: (() => void) | null = null;
void import('@tauri-apps/api/event')
.then(({ listen }) => listen<{ sessionId?: string }>(
'agent-companion://open-session',
async event => {
const sessionId = event.payload?.sessionId;
if (!sessionId) return;

const flowChatStore = FlowChatStore.getInstance();
if (flowChatStore.getState().sessions.has(sessionId)) {
flowChatStore.switchSession(sessionId);
}

try {
const { invoke } = await import('@tauri-apps/api/core');
await invoke('show_main_window');
} catch (error) {
log.warn('Failed to show main window from Agent companion bubble', {
sessionId,
error,
});
}
},
))
.then(removeListener => {
unlisten = removeListener;
})
.catch(error => {
log.warn('Failed to listen for Agent companion session open events', error);
});

return () => {
unlisten?.();
};
}, []);

// Observe AI initialization state
useEffect(() => {
if (aiError) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,8 @@
height: 100vh;
position: relative;
background: transparent;
cursor: grab;
user-select: none;

&:active {
cursor: grabbing;
}

&__pet {
width: min(96px, 100vw);
height: min(96px, 100vh);
Expand Down Expand Up @@ -52,18 +47,34 @@

&__bubbles {
position: absolute;
right: 88px;
bottom: 24px;
width: min(252px, calc(100vw - 104px));
max-height: calc(100vh - 16px);
right: calc(var(--bitfun-agent-companion-pet-size, 96px) - var(--bitfun-agent-companion-gap, 8px));
bottom: 0;
width: min(252px, calc(100vw - var(--bitfun-agent-companion-pet-size, 96px) - var(--bitfun-agent-companion-gap, 8px)));
max-height: 100vh;
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 6px;
pointer-events: none;
overflow-x: hidden;
overflow-y: auto;
overscroll-behavior: contain;
pointer-events: auto;
scrollbar-width: thin;

&::-webkit-scrollbar {
width: 5px;
}

&::-webkit-scrollbar-thumb {
border-radius: 999px;
background: rgba(15, 23, 42, 0.18);
}
}

&__bubble {
appearance: none;
text-align: left;
font: inherit;
max-width: 100%;
min-width: 132px;
padding: 7px 10px;
Expand All @@ -73,6 +84,17 @@
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.14);
color: #1f2937;
backdrop-filter: blur(10px);
cursor: pointer;

&:hover {
transform: translateY(-1px);
box-shadow: 0 12px 26px rgba(15, 23, 42, 0.18);
}

&:focus-visible {
outline: 2px solid rgba(59, 130, 246, 0.7);
outline-offset: 2px;
}
}

&__bubble-title,
Expand Down
Loading
Loading