diff --git a/test/quick-pane-ui.test.js b/test/quick-pane-ui.test.js index a93f121..d4c6ae3 100644 --- a/test/quick-pane-ui.test.js +++ b/test/quick-pane-ui.test.js @@ -54,3 +54,38 @@ test("Quick Pane routes extension responses through existing bridge command", as assert.match(source, /invoke\("respond_to_extension"/); assert.match(source, /requestId: extension\.id/); }); + +test("Quick Pane uses native quick dimensions and keyboard-first controls", async () => { + const [source, stylesheet, configSource] = await Promise.all([ + readFile( + new URL("../ui/src/quick-pane/QuickPane.tsx", import.meta.url), + "utf8", + ), + readFile( + new URL("../ui/src/quick-pane/quick-pane.css", import.meta.url), + "utf8", + ), + readFile(new URL("../ui/src-tauri/tauri.conf.json", import.meta.url), "utf8"), + ]); + const config = JSON.parse(configSource); + const quickPane = config.app.windows.find((window) => window.label === "quick-pane"); + + assert.deepEqual( + { + width: quickPane.width, + height: quickPane.height, + transparent: quickPane.transparent, + }, + { width: 640, height: 440, transparent: true }, + ); + assert.match(source, /ref=\{inputRef\}/); + assert.match(source, /focusInput\(\)/); + assert.match( + source, + /event\.key === "Enter" &&\s*!event\.shiftKey &&\s*!event\.nativeEvent\.isComposing/, + ); + assert.match(source, /event\.preventDefault\(\);\s*void run\(\);/); + assert.match(source, /Could not load model catalog:/); + assert.match(stylesheet, /#f0a347/); + assert.doesNotMatch(stylesheet, /#8aa9d6|#46494f|#23252a/); +}); diff --git a/ui/src-tauri/src/lib.rs b/ui/src-tauri/src/lib.rs index 9799bfd..cc8a98d 100644 --- a/ui/src-tauri/src/lib.rs +++ b/ui/src-tauri/src/lib.rs @@ -49,14 +49,29 @@ fn requested_quick_pane(args: &[String]) -> bool { args.iter().any(|arg| arg == "--quick") } -fn show_quick_pane(app: &AppHandle) { - if let Some(main) = app.get_webview_window("main") { - let _ = main.hide(); +fn quick_pane_action(is_visible: bool) -> WindowAction { + if is_visible { + WindowAction::Hide + } else { + WindowAction::ShowAndFocus } +} + +fn toggle_quick_pane(app: &AppHandle) { if let Some(window) = app.get_webview_window("quick-pane") { - let _ = window.show(); - let _ = window.unminimize(); - let _ = window.set_focus(); + match quick_pane_action(window.is_visible().unwrap_or(false)) { + WindowAction::Hide => { + let _ = window.hide(); + } + WindowAction::ShowAndFocus => { + if let Some(main) = app.get_webview_window("main") { + let _ = main.hide(); + } + let _ = window.show(); + let _ = window.unminimize(); + let _ = window.set_focus(); + } + } } } @@ -388,7 +403,7 @@ pub fn run() { }) .plugin(tauri_plugin_single_instance::init(|app, args, _cwd| { if requested_quick_pane(&args) { - show_quick_pane(app); + toggle_quick_pane(app); return; } launch_new_session(app.clone(), &args); @@ -406,7 +421,7 @@ pub fn run() { })) .setup(move |app| { if requested_quick_pane(&launch_args) { - show_quick_pane(app.handle()); + toggle_quick_pane(app.handle()); } else { launch_new_session(app.handle().clone(), &launch_args); } @@ -482,9 +497,11 @@ mod tests { } #[test] - fn quick_flag_is_detected_without_affecting_toggle() { + fn quick_flag_is_detected_and_toggles_its_own_window() { assert!(requested_quick_pane(&["--quick".to_owned()])); assert!(!requested_quick_pane(&["--toggle".to_owned()])); + assert_eq!(quick_pane_action(false), WindowAction::ShowAndFocus); + assert_eq!(quick_pane_action(true), WindowAction::Hide); } #[test] diff --git a/ui/src-tauri/tauri.conf.json b/ui/src-tauri/tauri.conf.json index d5c2c8f..419cacb 100644 --- a/ui/src-tauri/tauri.conf.json +++ b/ui/src-tauri/tauri.conf.json @@ -28,13 +28,14 @@ { "label": "quick-pane", "title": "Pi Quick Pane", - "width": 520, - "height": 360, + "width": 640, + "height": 440, "minWidth": 400, "minHeight": 260, "center": true, "visible": false, "decorations": false, + "transparent": true, "alwaysOnTop": true, "skipTaskbar": true, "shadow": true diff --git a/ui/src/quick-pane/QuickPane.tsx b/ui/src/quick-pane/QuickPane.tsx index fc03a2a..bc51696 100644 --- a/ui/src/quick-pane/QuickPane.tsx +++ b/ui/src/quick-pane/QuickPane.tsx @@ -77,30 +77,39 @@ export default function QuickPane() { const [extension, setExtension] = useState(); const runtimeRef = useRef(undefined); const requestRef = useRef(request); + const inputRef = useRef(null); const phaseRef = useRef<"idle" | "starting" | "running">("idle"); const invalid = useMemo(() => settingsError(settings), [settings]); const dismissPane = () => getCurrentWindow().hide(); + const focusInput = () => inputRef.current?.focus(); useEffect(() => { let active = true; - invoke("load_ui_state") - .then((state) => { + void (async () => { + let nextSettings: QuickPaneSettings; + try { + const state = await invoke("load_ui_state"); if (!active) return; - const nextSettings = { ...defaults, ...state.quickPane }; - setSettings(nextSettings); - setSettingsLoaded(true); - setSelectedModel(`${nextSettings.provider}/${nextSettings.model}`); - setStatus(""); - return invoke<{ models?: Model[] }>("get_model_catalog"); - }) - .then((catalog) => { + nextSettings = { ...defaults, ...state.quickPane }; + } catch (reason) { + if (active) + setError(`Could not load settings: ${String(reason)}`); + return; + } + + setSettings(nextSettings); + setSettingsLoaded(true); + setSelectedModel(`${nextSettings.provider}/${nextSettings.model}`); + setStatus(""); + try { + const catalog = await invoke<{ models?: Model[] }>("get_model_catalog"); if (active) setModels(catalog?.models ?? []); - }) - .catch( - (reason) => - active && setError(`Could not load settings: ${String(reason)}`), - ); + } catch (reason) { + if (active) + setError(`Could not load model catalog: ${String(reason)}`); + } + })(); return () => { active = false; const current = runtimeRef.current; @@ -111,6 +120,10 @@ export default function QuickPane() { }; }, []); + useEffect(() => { + if (settingsLoaded) focusInput(); + }, [settingsLoaded]); + useEffect(() => { const paneWindow = getCurrentWindow(); const settingsListener = listen( @@ -126,12 +139,11 @@ export default function QuickPane() { }; window.addEventListener("keydown", onKeyDown); const focusListener = paneWindow.onFocusChanged((event) => { - if ( - !event.payload && - phaseRef.current === "idle" && - !requestRef.current.trim() - ) + if (event.payload) { + focusInput(); + } else if (phaseRef.current === "idle" && !requestRef.current.trim()) { void dismissPane(); + } }); const closeListener = paneWindow.onCloseRequested((event) => { event.preventDefault(); @@ -299,11 +311,22 @@ export default function QuickPane() {