Skip to content

Full-Screen Mode

The assistant supports three view modes: Closed, Sidebar (400px panel), and Full-Screen (viewport overlay). Users switch between them with header buttons or keyboard shortcuts.

The AssistantViewMode enum drives all rendering decisions:

src/mods/assistant/types/assistant_view_mode.rs
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AssistantViewMode {
Closed,
Sidebar,
FullScreen,
}
impl AssistantViewMode {
pub fn toggle_to(mut signal: Signal<Self>, target: Self) {
let current = *signal.peek();
signal.set(if current == target { Self::Closed } else { target });
}
}

toggle_to acts as a toggle — pressing the same shortcut twice returns to Closed.

ShortcutAction
Cmd/Ctrl+JToggle sidebar open/closed
Cmd/Ctrl+Shift+JToggle full-screen open/closed
EscapeMinimize full-screen → sidebar

Shortcuts are registered via JS interop in AssistantProvider and communicate back to Rust through dioxus.send():

src/mods/assistant/components/assistant_provider_component.rs
fn use_keyboard_shortcuts(mut view_mode: Signal<AssistantViewMode>) {
use_hook(move || {
spawn(async move {
let mut handle = document::eval(r#"
document.addEventListener('keydown', (e) => {
const meta = e.metaKey || e.ctrlKey;
if (meta && e.shiftKey && (e.key === 'j' || e.key === 'J')) {
e.preventDefault();
dioxus.send('fullscreen');
} else if (meta && (e.key === 'j' || e.key === 'J')) {
e.preventDefault();
dioxus.send('sidebar');
} else if (e.key === 'Escape') {
dioxus.send('escape');
}
});
"#);
loop {
match handle.recv::<String>().await {
Ok("sidebar") => AssistantViewMode::toggle_to(view_mode, AssistantViewMode::Sidebar),
Ok("fullscreen") => AssistantViewMode::toggle_to(view_mode, AssistantViewMode::FullScreen),
Ok("escape") => {
if *view_mode.peek() == AssistantViewMode::FullScreen {
view_mode.set(AssistantViewMode::Sidebar);
}
}
_ => {}
}
}
});
});
}

The assistant uses a provider + shared body pattern. AssistantProvider owns all state and the WebSocket connection. Both the sidebar panel and full-screen overlay render the same AssistantChatBody component — only the wrapper differs.

AppLayout
└── AssistantProvider ← owns state, WebSocket, shortcuts
├── AppLayoutWithAssistant
│ ├── Sidebar + Header + main
│ ├── AssistantPanel ← 400px sliding sidebar
│ │ └── AssistantChatBody
│ └── AssistantFullScreen ← viewport overlay (conditional)
│ └── AssistantChatBody

The provider creates an AssistantContext shared via Dioxus context:

#[derive(Clone, Copy)]
pub struct AssistantContext {
pub view_mode: Signal<AssistantViewMode>,
pub messages: Signal<Vec<DisplayMessage>>,
pub input_value: Signal<String>,
pub is_loading: Signal<bool>,
pub current_title: Signal<Option<String>>,
pub current_conversation_id: Signal<Option<uuid::Uuid>>,
pub conversations: Signal<Vec<ConversationSummary>>,
pub show_session_picker: Signal<bool>,
pub page_ctx: Signal<PageContext>,
pub quick_actions: Signal<Option<Vec<QuickAction>>>,
}

Messages, input text, and conversation state persist when switching between sidebar and full-screen.

The sidebar uses a width transition to slide in and out:

src/mods/assistant/components/assistant_panel_component.rs
div {
class: if props.is_open {
"w-[400px] transition-all duration-300 overflow-hidden z-20"
} else {
"w-0 transition-all duration-300 overflow-hidden"
},
div {
class: "min-w-[400px] h-full bg-background border-l border-border flex flex-col",
AssistantChatBody {
on_expand: move |_| view_mode.set(AssistantViewMode::FullScreen),
on_close: move |_| view_mode.set(AssistantViewMode::Closed),
}
}
}

The outer container transitions between w-[400px] and w-0. The inner container keeps a fixed min-w-[400px] so content doesn’t reflow during animation.

The overlay renders a centered card over a blurred backdrop:

src/mods/assistant/components/assistant_full_screen_component.rs
div {
class: "fixed inset-0 z-50 flex items-center justify-center \
bg-background/80 backdrop-blur-sm",
onclick: move |_| view_mode.set(AssistantViewMode::Sidebar),
div {
class: "relative w-full max-w-3xl mx-4 \
h-[calc(100vh-6rem)] rounded-lg border \
border-border bg-card shadow-floating flex flex-col",
onclick: move |e: MouseEvent| e.stop_propagation(),
AssistantChatBody {
on_minimize: move |_| view_mode.set(AssistantViewMode::Sidebar),
}
}
}

Click the backdrop or press Escape to minimize back to the sidebar. The card itself stops click propagation.

Links in assistant messages are intercepted via event delegation. Internal links (/path) trigger SPA navigation through window.__assistantNav(), which minimizes full-screen to sidebar before navigating. External links open in a new tab.

// src/mods/assistant/helpers.rs — intercept_assistant_links()
// Attaches a delegated click handler to all #assistant-messages containers
// Internal: e.preventDefault() → __assistantNav(href)
// External: sets target="_blank" + rel="noopener noreferrer"

AssistantHeader conditionally renders expand/minimize/close buttons based on which callbacks are passed:

ButtonIconShown inAction
ExpandMaximize2SidebarSwitch to full-screen
MinimizeMinimize2Full-screenSwitch to sidebar
CloseXSidebarClose assistant
FilePurpose
types/assistant_view_mode.rsAssistantViewMode enum
components/assistant_provider_component.rsState provider, shortcuts, link handler
components/assistant_chat_body_component.rsShared chat UI
components/assistant_panel_component.rs400px sidebar wrapper
components/assistant_full_screen_component.rsViewport overlay wrapper
components/assistant_header_component.rsHeader with mode-switching buttons
helpers.rsFocus, scroll, link interception

All paths relative to src/mods/assistant/.