diff --git a/llm-picker.html b/llm-picker.html
index 97f655cd..c1f56955 100644
--- a/llm-picker.html
+++ b/llm-picker.html
@@ -87,7 +87,11 @@
}
document.getElementById("settingsRow").addEventListener("click", () => api.settings());
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.ready();
// Report rendered size so the main process can size the WebContentsView
diff --git a/main.js b/main.js
index 777f6c20..370b0beb 100644
--- a/main.js
+++ b/main.js
@@ -172,7 +172,7 @@ const SEARCH_ENGINES = {
// 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
// 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
// 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
@@ -5962,6 +5962,7 @@ function createWindow() {
closeOnClickAway(enginePicker, () => epVisible, () => showEnginePicker(false));
closeOnClickAway(langPicker, () => lpVisible, () => showLangPicker(false));
closeOnClickAway(downloadsPop, () => dlVisible, () => showDownloads(false));
+ // llmPickerPop joins this contract below, where it is created.
// Floating address-bar suggestions dropdown.
addressPicker = new WebContentsView({ webPreferences: { preload: path.join(__dirname, "address-picker-preload.js") } });
try { addressPicker.setBackgroundColor("#00000000"); } catch {}
@@ -6069,6 +6070,11 @@ function createWindow() {
raise(llmPickerPop);
deferOverlayLoad(llmPickerPop, "llm-picker.html");
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.
jsDialogPop = new WebContentsView({ webPreferences: { preload: path.join(__dirname, "js-dialog-preload.js") } });
try { jsDialogPop.setBackgroundColor("#00000000"); } catch {}
@@ -7876,6 +7882,7 @@ ipcMain.handle("jsdialog-answer", (e, reqId, ok, value) => {
let approvalPop = null;
let unlockPop = null; // vault unlock prompt (unlock.html), see requestVaultUnlock
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
const approvalQueue = [];
let approvalCurrent = null; // { reqId, resolve }
@@ -9955,8 +9962,9 @@ ipcMain.handle("quicklinks-llm-menu", (e, rect) => {
try {
llmPickerPop.webContents.send("llm-picker-state", llmPickerState());
llmPickerPop.setVisible(true);
- raise(llmPickerPop, primaryW?.win);
- llmPickerPop.webContents.focus();
+ llmPickerVisible = true;
+ raise(llmPickerPop, primaryW);
+ if (primaryW?.win?.isFocused()) try { llmPickerPop.webContents.focus(); } catch {}
} catch {}
});
return true;
@@ -9964,6 +9972,7 @@ ipcMain.handle("quicklinks-llm-menu", (e, rect) => {
function hideLlmPicker() {
if (!llmPickerPop) return;
try { llmPickerPop.setVisible(false); } catch {}
+ llmPickerVisible = false;
}
ipcMain.handle("llm-picker-ready", (e) => {
if (!llmPickerPop || e.sender !== llmPickerPop.webContents) return false;
diff --git a/package.json b/package.json
index 466e1fc3..ab5e9490 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "theseus-navigator",
- "version": "0.3.119",
+ "version": "0.3.120",
"license": "MPL-2.0",
"description": "Theseus Navigator — a browser that follows the thread. By Silent Mode, a Deviant project.",
"author": "Silent Mode",