Theseus 0.3.120: Pick-AI button opens its list again; "DuckDuckGo AI" renamed to "Duck AI"

The 0.3.106 switch from a native menu to an HTML overlay for Pick AI gave the
overlay a client-side window.blur handler that called api.close(). In the
engine picker (same pattern) the close-on-click-away is driven from main
with a `win.isFocused()` guard, so a focus flicker between the strip view
and the pop does not count as a click-away; the LLM overlay missed that
guard, so Electron's own view-to-view focus flicker on open was enough to
close the picker the moment it opened. From 0.3.106 through 0.3.119 the
Pick-AI button looked like a no-op: it rendered the pop internally (the DOM
was there, 7 rows) but setVisible flipped back off before paint. Users saw
a blank click.

Fix is the same contract the engine, language, downloads and site-info
pickers use: closeOnClickAway on main, no blur handler on the client.
`llmPickerVisible` is tracked alongside setVisible so the gate can read it.
The focus() call on open now runs only when the window is actually focused
(same guard the other overlays use). The raise() call also passed
`primaryW?.win` where a window record was expected, so the view never
re-parented when a secondary window became primary — fixed to pass the
record.

Also renames DuckDuckGo AI → Duck AI in the engine catalog. duck.ai is
Duck's own branded entry for the chat UI and matches what the host lands
on; the longer name was ambiguous next to the DuckDuckGo search entry.
This commit is contained in:
Local Dev 2026-10-27 17:08:33 +02:00
parent 47a739ad72
commit fa2d2f89da
3 changed files with 18 additions and 5 deletions

View file

@ -87,7 +87,11 @@
} }
document.getElementById("settingsRow").addEventListener("click", () => api.settings()); document.getElementById("settingsRow").addEventListener("click", () => api.settings());
document.addEventListener("keydown", (e) => { if (e.key === "Escape") api.close(); }); document.addEventListener("keydown", (e) => { if (e.key === "Escape") api.close(); });
window.addEventListener("blur", () => api.close()); // Close-on-click-away is driven from main (see closeOnClickAway registration);
// relying on a client-side window.blur here closes the picker the moment it
// opens if Electron's focus flickers between the strip view and the pop,
// which is what happened in 0.3.106–0.3.119 — the pop briefly rendered then
// vanished and users saw the Pick-AI button as a no-op.
api.onState(render); api.onState(render);
api.ready(); api.ready();
// Report rendered size so the main process can size the WebContentsView // Report rendered size so the main process can size the WebContentsView

15
main.js
View file

@ -172,7 +172,7 @@ const SEARCH_ENGINES = {
// Llama 3 + Mistral behind one URL. Serves ?q= on the regular DDG host and // Llama 3 + Mistral behind one URL. Serves ?q= on the regular DDG host and
// flips into the chat UI with &ia=chat, so it also doubles as a toolbar // flips into the chat UI with &ia=chat, so it also doubles as a toolbar
// search target with pre-filled answer. // search target with pre-filled answer.
duckai: { kind: "llm", tier: "catalog", name: "DuckDuckGo AI", sym: "🦆", fav: "duck.ai", home: "https://duck.ai/", url: (q) => "https://duckduckgo.com/?q=" + encodeURIComponent(q) + "&ia=chat" }, duckai: { kind: "llm", tier: "catalog", name: "Duck AI", sym: "🦆", fav: "duck.ai", home: "https://duck.ai/", url: (q) => "https://duckduckgo.com/?q=" + encodeURIComponent(q) + "&ia=chat" },
// Yandex Alisa — Russian-language chatbot. The chat UI is on a path, so we // Yandex Alisa — Russian-language chatbot. The chat UI is on a path, so we
// route home via `home`; URL-query mode falls back to regular Yandex search // route home via `home`; URL-query mode falls back to regular Yandex search
// (ya.ru does not expose a public URL to open the chat with a pre-filled // (ya.ru does not expose a public URL to open the chat with a pre-filled
@ -5962,6 +5962,7 @@ function createWindow() {
closeOnClickAway(enginePicker, () => epVisible, () => showEnginePicker(false)); closeOnClickAway(enginePicker, () => epVisible, () => showEnginePicker(false));
closeOnClickAway(langPicker, () => lpVisible, () => showLangPicker(false)); closeOnClickAway(langPicker, () => lpVisible, () => showLangPicker(false));
closeOnClickAway(downloadsPop, () => dlVisible, () => showDownloads(false)); closeOnClickAway(downloadsPop, () => dlVisible, () => showDownloads(false));
// llmPickerPop joins this contract below, where it is created.
// Floating address-bar suggestions dropdown. // Floating address-bar suggestions dropdown.
addressPicker = new WebContentsView({ webPreferences: { preload: path.join(__dirname, "address-picker-preload.js") } }); addressPicker = new WebContentsView({ webPreferences: { preload: path.join(__dirname, "address-picker-preload.js") } });
try { addressPicker.setBackgroundColor("#00000000"); } catch {} try { addressPicker.setBackgroundColor("#00000000"); } catch {}
@ -6069,6 +6070,11 @@ function createWindow() {
raise(llmPickerPop); raise(llmPickerPop);
deferOverlayLoad(llmPickerPop, "llm-picker.html"); deferOverlayLoad(llmPickerPop, "llm-picker.html");
llmPickerPop.setVisible(false); llmPickerPop.setVisible(false);
// Same close-on-click-away contract the engine picker uses. The HTML page
// used to carry its own window.blur handler; it closed the picker the
// moment it opened because Electron's focus flickered between the strip
// view and the pop.
closeOnClickAway(llmPickerPop, () => llmPickerVisible, () => hideLlmPicker());
// Page dialogs (alert / confirm / prompt) — see the js-dialog block. // Page dialogs (alert / confirm / prompt) — see the js-dialog block.
jsDialogPop = new WebContentsView({ webPreferences: { preload: path.join(__dirname, "js-dialog-preload.js") } }); jsDialogPop = new WebContentsView({ webPreferences: { preload: path.join(__dirname, "js-dialog-preload.js") } });
try { jsDialogPop.setBackgroundColor("#00000000"); } catch {} try { jsDialogPop.setBackgroundColor("#00000000"); } catch {}
@ -7876,6 +7882,7 @@ ipcMain.handle("jsdialog-answer", (e, reqId, ok, value) => {
let approvalPop = null; let approvalPop = null;
let unlockPop = null; // vault unlock prompt (unlock.html), see requestVaultUnlock let unlockPop = null; // vault unlock prompt (unlock.html), see requestVaultUnlock
let llmPickerPop = null; // sidebar-strip AI engine picker (llm-picker.html) let llmPickerPop = null; // sidebar-strip AI engine picker (llm-picker.html)
let llmPickerVisible = false; // kept in sync with setVisible so closeOnClickAway can gate
let llmPickerSize = { w: 240, h: 160 }; // last reported pop size for layout let llmPickerSize = { w: 240, h: 160 }; // last reported pop size for layout
const approvalQueue = []; const approvalQueue = [];
let approvalCurrent = null; // { reqId, resolve } let approvalCurrent = null; // { reqId, resolve }
@ -9955,8 +9962,9 @@ ipcMain.handle("quicklinks-llm-menu", (e, rect) => {
try { try {
llmPickerPop.webContents.send("llm-picker-state", llmPickerState()); llmPickerPop.webContents.send("llm-picker-state", llmPickerState());
llmPickerPop.setVisible(true); llmPickerPop.setVisible(true);
raise(llmPickerPop, primaryW?.win); llmPickerVisible = true;
llmPickerPop.webContents.focus(); raise(llmPickerPop, primaryW);
if (primaryW?.win?.isFocused()) try { llmPickerPop.webContents.focus(); } catch {}
} catch {} } catch {}
}); });
return true; return true;
@ -9964,6 +9972,7 @@ ipcMain.handle("quicklinks-llm-menu", (e, rect) => {
function hideLlmPicker() { function hideLlmPicker() {
if (!llmPickerPop) return; if (!llmPickerPop) return;
try { llmPickerPop.setVisible(false); } catch {} try { llmPickerPop.setVisible(false); } catch {}
llmPickerVisible = false;
} }
ipcMain.handle("llm-picker-ready", (e) => { ipcMain.handle("llm-picker-ready", (e) => {
if (!llmPickerPop || e.sender !== llmPickerPop.webContents) return false; if (!llmPickerPop || e.sender !== llmPickerPop.webContents) return false;

View file

@ -1,6 +1,6 @@
{ {
"name": "theseus-navigator", "name": "theseus-navigator",
"version": "0.3.119", "version": "0.3.120",
"license": "MPL-2.0", "license": "MPL-2.0",
"description": "Theseus Navigator — a browser that follows the thread. By Silent Mode, a Deviant project.", "description": "Theseus Navigator — a browser that follows the thread. By Silent Mode, a Deviant project.",
"author": "Silent Mode", "author": "Silent Mode",