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

> Learn how to add a quick input panel ask mode to your Tauri app. Implement a React component with Rust commands and dynamic resizing for a seamless user experience.

- Repository: [lencx/ChatGPT](https://github.com/lencx/ChatGPT)
- Tags: how-to-guide
- Published: 2026-03-06

---

**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`](https://github.com/lencx/ChatGPT/blob/main/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:

```tsx
// 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:

```tsx
// 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`](https://github.com/lencx/ChatGPT/blob/main/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:

```rust
// 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`](https://github.com/lencx/ChatGPT/blob/main/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`](https://github.com/lencx/ChatGPT/blob/main/index.html) entry point but renders a different component based on its label:

```rust
// 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`](https://github.com/lencx/ChatGPT/blob/main/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`](https://github.com/lencx/ChatGPT/blob/main/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:

```rust
// 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`](https://github.com/lencx/ChatGPT/blob/main/src/view/Titlebar.tsx) triggers this toggle via a simple button handler:

```tsx
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`](https://github.com/lencx/ChatGPT/blob/main/index.html), the application uses the webview label to determine which React component to render. In [`src/App.tsx`](https://github.com/lencx/ChatGPT/blob/main/src/App.tsx), a mapping dictionary routes each label to its corresponding view:

```tsx
// 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`](https://github.com/lencx/ChatGPT/blob/main/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`](https://github.com/lencx/ChatGPT/blob/main/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`](https://github.com/lencx/ChatGPT/blob/main/src/App.tsx), and initialize a new child webview in [`setup.rs`](https://github.com/lencx/ChatGPT/blob/main/setup.rs) with a unique label (e.g., `"notes"` or `"search"`). Expose new commands in [`cmd.rs`](https://github.com/lencx/ChatGPT/blob/main/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`](https://github.com/lencx/ChatGPT/blob/main/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.