fix(quick-pane): polish spotlight lifecycle
This commit is contained in:
@@ -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, /invoke\("respond_to_extension"/);
|
||||||
assert.match(source, /requestId: extension\.id/);
|
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/);
|
||||||
|
});
|
||||||
|
|||||||
+22
-5
@@ -49,15 +49,30 @@ fn requested_quick_pane(args: &[String]) -> bool {
|
|||||||
args.iter().any(|arg| arg == "--quick")
|
args.iter().any(|arg| arg == "--quick")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn show_quick_pane(app: &AppHandle) {
|
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") {
|
||||||
|
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") {
|
if let Some(main) = app.get_webview_window("main") {
|
||||||
let _ = main.hide();
|
let _ = main.hide();
|
||||||
}
|
}
|
||||||
if let Some(window) = app.get_webview_window("quick-pane") {
|
|
||||||
let _ = window.show();
|
let _ = window.show();
|
||||||
let _ = window.unminimize();
|
let _ = window.unminimize();
|
||||||
let _ = window.set_focus();
|
let _ = window.set_focus();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn requested_new_worktree_argument(args: &[String]) -> Option<String> {
|
fn requested_new_worktree_argument(args: &[String]) -> Option<String> {
|
||||||
@@ -388,7 +403,7 @@ pub fn run() {
|
|||||||
})
|
})
|
||||||
.plugin(tauri_plugin_single_instance::init(|app, args, _cwd| {
|
.plugin(tauri_plugin_single_instance::init(|app, args, _cwd| {
|
||||||
if requested_quick_pane(&args) {
|
if requested_quick_pane(&args) {
|
||||||
show_quick_pane(app);
|
toggle_quick_pane(app);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
launch_new_session(app.clone(), &args);
|
launch_new_session(app.clone(), &args);
|
||||||
@@ -406,7 +421,7 @@ pub fn run() {
|
|||||||
}))
|
}))
|
||||||
.setup(move |app| {
|
.setup(move |app| {
|
||||||
if requested_quick_pane(&launch_args) {
|
if requested_quick_pane(&launch_args) {
|
||||||
show_quick_pane(app.handle());
|
toggle_quick_pane(app.handle());
|
||||||
} else {
|
} else {
|
||||||
launch_new_session(app.handle().clone(), &launch_args);
|
launch_new_session(app.handle().clone(), &launch_args);
|
||||||
}
|
}
|
||||||
@@ -482,9 +497,11 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[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(&["--quick".to_owned()]));
|
||||||
assert!(!requested_quick_pane(&["--toggle".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]
|
#[test]
|
||||||
|
|||||||
@@ -28,13 +28,14 @@
|
|||||||
{
|
{
|
||||||
"label": "quick-pane",
|
"label": "quick-pane",
|
||||||
"title": "Pi Quick Pane",
|
"title": "Pi Quick Pane",
|
||||||
"width": 520,
|
"width": 640,
|
||||||
"height": 360,
|
"height": 440,
|
||||||
"minWidth": 400,
|
"minWidth": 400,
|
||||||
"minHeight": 260,
|
"minHeight": 260,
|
||||||
"center": true,
|
"center": true,
|
||||||
"visible": false,
|
"visible": false,
|
||||||
"decorations": false,
|
"decorations": false,
|
||||||
|
"transparent": true,
|
||||||
"alwaysOnTop": true,
|
"alwaysOnTop": true,
|
||||||
"skipTaskbar": true,
|
"skipTaskbar": true,
|
||||||
"shadow": true
|
"shadow": true
|
||||||
|
|||||||
@@ -77,30 +77,39 @@ export default function QuickPane() {
|
|||||||
const [extension, setExtension] = useState<Extension>();
|
const [extension, setExtension] = useState<Extension>();
|
||||||
const runtimeRef = useRef<Runtime | undefined>(undefined);
|
const runtimeRef = useRef<Runtime | undefined>(undefined);
|
||||||
const requestRef = useRef(request);
|
const requestRef = useRef(request);
|
||||||
|
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||||
const phaseRef = useRef<"idle" | "starting" | "running">("idle");
|
const phaseRef = useRef<"idle" | "starting" | "running">("idle");
|
||||||
const invalid = useMemo(() => settingsError(settings), [settings]);
|
const invalid = useMemo(() => settingsError(settings), [settings]);
|
||||||
|
|
||||||
const dismissPane = () => getCurrentWindow().hide();
|
const dismissPane = () => getCurrentWindow().hide();
|
||||||
|
const focusInput = () => inputRef.current?.focus();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let active = true;
|
let active = true;
|
||||||
invoke<UiState>("load_ui_state")
|
void (async () => {
|
||||||
.then((state) => {
|
let nextSettings: QuickPaneSettings;
|
||||||
|
try {
|
||||||
|
const state = await invoke<UiState>("load_ui_state");
|
||||||
if (!active) return;
|
if (!active) return;
|
||||||
const nextSettings = { ...defaults, ...state.quickPane };
|
nextSettings = { ...defaults, ...state.quickPane };
|
||||||
|
} catch (reason) {
|
||||||
|
if (active)
|
||||||
|
setError(`Could not load settings: ${String(reason)}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setSettings(nextSettings);
|
setSettings(nextSettings);
|
||||||
setSettingsLoaded(true);
|
setSettingsLoaded(true);
|
||||||
setSelectedModel(`${nextSettings.provider}/${nextSettings.model}`);
|
setSelectedModel(`${nextSettings.provider}/${nextSettings.model}`);
|
||||||
setStatus("");
|
setStatus("");
|
||||||
return invoke<{ models?: Model[] }>("get_model_catalog");
|
try {
|
||||||
})
|
const catalog = await invoke<{ models?: Model[] }>("get_model_catalog");
|
||||||
.then((catalog) => {
|
|
||||||
if (active) setModels(catalog?.models ?? []);
|
if (active) setModels(catalog?.models ?? []);
|
||||||
})
|
} catch (reason) {
|
||||||
.catch(
|
if (active)
|
||||||
(reason) =>
|
setError(`Could not load model catalog: ${String(reason)}`);
|
||||||
active && setError(`Could not load settings: ${String(reason)}`),
|
}
|
||||||
);
|
})();
|
||||||
return () => {
|
return () => {
|
||||||
active = false;
|
active = false;
|
||||||
const current = runtimeRef.current;
|
const current = runtimeRef.current;
|
||||||
@@ -111,6 +120,10 @@ export default function QuickPane() {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (settingsLoaded) focusInput();
|
||||||
|
}, [settingsLoaded]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const paneWindow = getCurrentWindow();
|
const paneWindow = getCurrentWindow();
|
||||||
const settingsListener = listen<QuickPaneSettings>(
|
const settingsListener = listen<QuickPaneSettings>(
|
||||||
@@ -126,12 +139,11 @@ export default function QuickPane() {
|
|||||||
};
|
};
|
||||||
window.addEventListener("keydown", onKeyDown);
|
window.addEventListener("keydown", onKeyDown);
|
||||||
const focusListener = paneWindow.onFocusChanged((event) => {
|
const focusListener = paneWindow.onFocusChanged((event) => {
|
||||||
if (
|
if (event.payload) {
|
||||||
!event.payload &&
|
focusInput();
|
||||||
phaseRef.current === "idle" &&
|
} else if (phaseRef.current === "idle" && !requestRef.current.trim()) {
|
||||||
!requestRef.current.trim()
|
|
||||||
)
|
|
||||||
void dismissPane();
|
void dismissPane();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
const closeListener = paneWindow.onCloseRequested((event) => {
|
const closeListener = paneWindow.onCloseRequested((event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -299,11 +311,22 @@ export default function QuickPane() {
|
|||||||
</header>
|
</header>
|
||||||
<textarea
|
<textarea
|
||||||
autoFocus
|
autoFocus
|
||||||
|
ref={inputRef}
|
||||||
value={request}
|
value={request}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
requestRef.current = e.target.value;
|
requestRef.current = e.target.value;
|
||||||
setRequest(e.target.value);
|
setRequest(e.target.value);
|
||||||
}}
|
}}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (
|
||||||
|
event.key === "Enter" &&
|
||||||
|
!event.shiftKey &&
|
||||||
|
!event.nativeEvent.isComposing
|
||||||
|
) {
|
||||||
|
event.preventDefault();
|
||||||
|
void run();
|
||||||
|
}
|
||||||
|
}}
|
||||||
placeholder="Ask a quick question or give a quick instruction"
|
placeholder="Ask a quick question or give a quick instruction"
|
||||||
style={{ width: "100%", minHeight: 90, marginTop: 14 }}
|
style={{ width: "100%", minHeight: 90, marginTop: 14 }}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,20 +1,110 @@
|
|||||||
:root { color-scheme: dark; }
|
:root {
|
||||||
body { margin: 0; background: transparent; }
|
color-scheme: dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
margin: 0;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
.quick-pane {
|
.quick-pane {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
padding: 18px;
|
overflow-y: auto;
|
||||||
color: #f4f2ee;
|
padding: 12px;
|
||||||
font: 14px/1.4 system-ui, sans-serif;
|
color: #e6e3df;
|
||||||
background: radial-gradient(circle at top, #46494f, #23252a 62%);
|
font: 14px/1.4 Inter, ui-sans-serif, system-ui, sans-serif;
|
||||||
border: 1px solid rgba(255,255,255,.16);
|
background: rgba(34, 34, 34, 0.98);
|
||||||
border-radius: 14px;
|
border: 1px solid #4b4b4b;
|
||||||
box-shadow: 0 22px 70px rgba(0,0,0,.55);
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 22px 70px rgba(0, 0, 0, 0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-pane header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
color: #f0a347;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 900;
|
||||||
|
letter-spacing: 0.12em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-pane button,
|
||||||
|
.quick-pane select {
|
||||||
|
border: 0;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 6px 9px;
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-pane button {
|
||||||
|
margin-right: 6px;
|
||||||
|
background: #f0a347;
|
||||||
|
color: #25180b;
|
||||||
|
font-weight: 750;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-pane button:hover {
|
||||||
|
background: #ffc170;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-pane button:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: wait;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-pane select {
|
||||||
|
color: #e6e3df;
|
||||||
|
background: #353535;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-pane textarea {
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 92px;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid #484848;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #e6e3df;
|
||||||
|
background: #292929;
|
||||||
|
font: inherit;
|
||||||
|
font-size: 16px;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-pane textarea:focus,
|
||||||
|
.quick-pane button:focus-visible,
|
||||||
|
.quick-pane select:focus-visible {
|
||||||
|
outline: 3px solid #f0a347;
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-pane > label {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 7px;
|
||||||
|
margin: 10px 8px 10px 0;
|
||||||
|
color: #bbb7b0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-pane > pre,
|
||||||
|
.quick-pane > p {
|
||||||
|
margin: 10px 0 0;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid #484848;
|
||||||
|
border-radius: 6px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
background: #292929;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-pane [role="alert"] {
|
||||||
|
border-color: #a65b67;
|
||||||
|
color: #ffd7db;
|
||||||
|
background: #3d252b;
|
||||||
}
|
}
|
||||||
.quick-pane header { display:flex; align-items:center; justify-content:space-between; margin-bottom:12px; color:#c9c7c2; font-size:12px; letter-spacing:.08em; text-transform:uppercase; }
|
|
||||||
.quick-pane button, .quick-pane select { border:0; border-radius:7px; padding:7px 10px; color:inherit; background:#45484e; }
|
|
||||||
.quick-pane textarea { box-sizing:border-box; width:100%; min-height:92px; padding:14px; border:0; border-radius:10px; outline:1px solid rgba(255,255,255,.14); color:#fff; background:rgba(0,0,0,.24); font:inherit; font-size:16px; resize:none; }
|
|
||||||
.quick-pane textarea:focus { outline:2px solid #8aa9d6; background:rgba(0,0,0,.38); }
|
|
||||||
.quick-pane > label { display:inline-flex; align-items:center; gap:7px; margin:10px 8px 10px 0; color:#c9c7c2; }
|
|
||||||
.quick-pane > pre, .quick-pane > p { margin:10px 0 0; padding:10px 12px; border-radius:9px; white-space:pre-wrap; background:rgba(0,0,0,.22); }
|
|
||||||
.quick-pane [role="alert"] { color:#ffb8bd; background:rgba(119,42,52,.42); }
|
|
||||||
|
|||||||
Reference in New Issue
Block a user