How to Add a Quick Input Panel (Ask Mode) in a Tauri App

You can add a quick input panel to a Tauri app by creating a child webview positioned at the bottom of the window, implementing a React component that invokes Rust commands for sync and send operations, and dynamically resizing the view at runtime via platform-specific layout logic.

The lencx/ChatGPT repository demonstrates this pattern through its "Ask mode" feature—a compact input panel that slides up from the bottom of the window without disrupting the main view. This implementation combines a React frontend for capturing user input with Rust backend commands that manage the webview lifecycle and window positioning. Understanding this architecture allows you to build similar quick-access interfaces in any Tauri application.

Creating the Frontend Input Component

The quick input panel starts as a dedicated React component that handles text capture, debounced state synchronization, and message submission. In src/view/Ask.tsx, the component uses Tauri's invoke API to communicate with the Rust backend while managing local UI state.

Handling Input with Debounced Sync

The component debounces every keystroke to prevent excessive cross-process communication. After a 300ms delay, it calls the ask_sync command to keep the Rust side updated with the current message content:

// src/view/Ask.tsx
import { useState, useEffect, useRef } from 'react';
import { invoke } from '@tauri-apps/api/core';
import debounce from 'lodash/debounce';
import useInfo from '~hooks/useInfo';

export default function ChatInput() {
  const inputRef = useRef<HTMLTextAreaElement>(null);
  const [message, setMessage] = useState('');
  
  // Debounced sync to Rust backend
  useEffect(() => {
    const syncMessage = debounce(async () => {
      await invoke('ask_sync', { message: JSON.stringify(message) });
    }, 300);
    syncMessage();
    return () => syncMessage.cancel();
  }, [message]);
  
  const handleInput = (e) => setMessage(e.target.value);
  
  // Render textarea...
}

Submitting Messages via Keyboard Shortcuts

The panel listens for ⌘+Enter (macOS) or Ctrl+Enter (Windows/Linux) to trigger the ask_send command. After submission, the input clears and refocuses automatically:

// src/view/Ask.tsx (continued)
import { useHotkeys } from 'react-hotkeys-hook';

const { isMac } = useInfo();

useHotkeys(isMac ? 'meta+enter' : 'ctrl+enter', async (e) => {
  e.preventDefault();
  await handleSend();
}, { enableOnFormTags: true }, [message]);

const handleSend = async () => {
  if (!message) return;
  await invoke('ask_send', { message: JSON.stringify(message) });
  setMessage('');
  if (inputRef.current) {
    inputRef.current.value = '';
    inputRef.current.focus();
  }
};

Implementing Tauri Rust Commands

The Rust backend in src-tauri/src/core/cmd.rs exposes two core commands that bridge the frontend input with the application's business logic. These commands execute JavaScript inside the main webview to simulate user actions on the underlying page.

The ask_sync and ask_send Commands

The ask_sync command updates the internal state of the ChatGPT interface, while ask_send triggers the submission and manages focus return:

// src-tauri/src/core/cmd.rs
use tauri::{command, AppHandle};

#[command]
pub fn ask_sync(app: AppHandle, message: String) {
    app.get_window("core")
        .unwrap()
        .get_webview("main")
        .unwrap()
        .eval(&format!("ChatAsk.sync({})", message))
        .unwrap();
}

#[command]
pub fn ask_send(app: AppHandle) {
    let win = app.get_window("core").unwrap();
    win.get_webview("main")
        .unwrap()
        .eval(
            r#"
            ChatAsk.submit();
            setTimeout(() => {
                __TAURI__.webview.Webview.getByLabel('ask')?.setFocus();
            }, 500);
            "#,
        )
        .unwrap();
}

Initializing the Child Webview

During application startup, the setup.rs file creates three child webviews attached to the main window: main (the primary content), titlebar (custom controls), and ask (the quick input panel). The Ask webview loads the same index.html entry point but renders a different component based on its label:

// src-tauri/src/core/setup.rs
use tauri::{WebviewBuilder, WebviewUrl, LogicalPosition, PhysicalSize};

let ask_view = WebviewBuilder::new("ask", WebviewUrl::App("index.html".into()))
    .auto_resize();

let ask_mode_height = if conf.ask_mode { ASK_HEIGHT } else { 0.0 };
let ask_height = (scale_factor * ask_mode_height).round() as u32;

// Position at bottom of window
win.add_child(
    ask_view,
    LogicalPosition::new(
        0.0,
        (win_size.height as f64 / scale_factor) - ask_mode_height,
    ),
    PhysicalSize::new(win_size.width, ask_height),
)?;

The ASK_HEIGHT constant (set to 120.0 in src-tauri/src/core/constant.rs) defines the default panel height, which scales according to the monitor's DPI factor.

Positioning and Toggling the Quick Input Panel

To create a slide-up effect or hide the panel entirely, the application recalculates webview geometries at runtime based on the ask_mode configuration flag.

Platform-Specific Layout Calculations

While the basic positioning logic remains consistent across platforms, the coordinate system accounts for scale factors to ensure crisp rendering on high-DPI displays. When the panel is disabled, the height collapses to 0.0, effectively hiding the webview while keeping it in the window hierarchy.

Runtime Toggle Implementation

The set_view_ask command in cmd.rs handles dynamic showing and hiding. It persists the user's preference to disk, recalculates dimensions using the current scale factor, and repositions all three child webviews to accommodate the new layout:

// src-tauri/src/core/cmd.rs
#[command]
pub fn set_view_ask(app: AppHandle, enabled: bool) {
    // Persist configuration
    let conf = AppConf::load(&app).unwrap();
    conf.amend(serde_json::json!({"ask_mode": enabled}))
        .unwrap()
        .save(&app)
        .unwrap();

    // Calculate new dimensions
    let ask_mode_height = if enabled { ASK_HEIGHT } else { 0.0 };
    let scale_factor = core_window.scale_factor().unwrap();
    let ask_height = (scale_factor * ask_mode_height).round() as u32;
    
    // Resize and reposition webviews (implementation continues...)
}

The title bar component in src/view/Titlebar.tsx triggers this toggle via a simple button handler:

const handleAsk = () => {
  setEnableAsk(!enableAsk);
  invoke('set_view_ask', { enabled: !enableAsk });
};

Wiring Webview Labels to React Components

Because all child webviews load the same index.html, the application uses the webview label to determine which React component to render. In src/App.tsx, a mapping dictionary routes each label to its corresponding view:

// src/App.tsx
import { getCurrentWebview } from '@tauri-apps/api/webview';
import Titlebar from '~view/Titlebar';
import Ask from '~view/Ask';
import Settings from '~view/Settings';

const viewMap = {
  titlebar: <Titlebar />,
  ask: <Ask />,
  settings: <Settings />,
};

export default function App() {
  const webview = getCurrentWebview();
  return viewMap[webview.label as keyof typeof viewMap] || null;
}

When the Ask webview initializes with the label "ask", it renders the quick input panel component instead of the main ChatGPT interface.

Summary

  • Child webviews enable isolated UI panels within a single Tauri window, each loading specific React components based on their label.
  • Debounced invoke calls (ask_sync) minimize overhead when synchronizing frontend input state with the Rust backend.
  • Platform-aware positioning using LogicalPosition and PhysicalSize ensures the panel renders correctly across different DPI settings.
  • Dynamic resizing via set_view_ask allows runtime toggling of the panel without recreating the window.
  • JavaScript evaluation via webview.eval() bridges React frontend actions with existing web content in the main view.

Frequently Asked Questions

How do I change the height of the quick input panel in a Tauri app?

Modify the ASK_HEIGHT constant defined in src-tauri/src/core/constant.rs. The default value is 120.0 pixels, which the application scales according to the monitor's DPI factor during window layout calculations in setup.rs.

Can I use this pattern for panels other than chat input?

Yes. Create a new React component in src/view/, add a corresponding entry to the viewMap in src/App.tsx, and initialize a new child webview in setup.rs with a unique label (e.g., "notes" or "search"). Expose new commands in cmd.rs following the ask_sync/ask_send pattern to handle your specific data flow.

Why does the quick input panel use debouncing for the sync command?

The 300ms debounce in Ask.tsx prevents the frontend from invoking the Rust command on every keystroke, which would create excessive cross-process traffic. This optimization ensures smooth typing performance while keeping the backend state synchronized.

How does the panel return focus after sending a message?

The ask_send command evaluates JavaScript that calls setTimeout to refocus the Ask webview after 500ms. This delay ensures the submission completes in the main webview before returning keyboard focus to the input panel.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →