Skip to content

Instantly share code, notes, and snippets.

@yuuki7
Last active December 4, 2025 17:34
  • Select an option

Select an option

Revisions

  1. yuuki7 revised this gist Dec 4, 2025. 2 changed files with 45 additions and 0 deletions.
    16 changes: 16 additions & 0 deletions mindra-2025-12-02.log
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,16 @@
    {"ts":"2025-12-02T14:46:23.538Z","level":1,"message":"[UniversalSearch] loaded (fully unified input/send)","line":11,"sourceId":"file:///Users/yuuki/mindra-light/renderer/ai/universal-search.js"}
    {"ts":"2025-12-02T14:46:23.569Z","level":2,"message":"%cElectron Security Warning (Insecure Content-Security-Policy) font-weight: bold; This renderer process has either no Content Security\n Policy set or a policy with \"unsafe-eval\" enabled. This exposes users of\n this app to unnecessary security risks.\n\nFor more information and help, consult\nhttps://electronjs.org/docs/tutorial/security.\nThis warning will not show up\nonce the app is packaged.","line":2,"sourceId":"node:electron/js2c/sandbox_bundle"}
    {"ts":"2025-12-02T14:46:23.569Z","level":2,"message":"%cElectron Security Warning (allowpopups) font-weight: bold; A <webview> has \"allowpopups\" set to true. This exposes\n users of this app to some security risk, since popups are just\n BrowserWindows. If you do not need this feature, you should disable it.\n\n \nFor more information and help, consult\nhttps://electronjs.org/docs/tutorial/security.\nThis warning will not show up\nonce the app is packaged.","line":2,"sourceId":"node:electron/js2c/sandbox_bundle"}
    {"ts":"2025-12-02T14:51:53.486Z","level":1,"message":"[UniversalSearch] query = Hi, can you hear me?","line":698,"sourceId":"file:///Users/yuuki/mindra-light/renderer/ai/universal-search.js"}
    {"ts":"2025-12-02T14:51:53.511Z","level":1,"message":"[UniversalSearch] AI not ready: 送信したよ","line":740,"sourceId":"file:///Users/yuuki/mindra-light/renderer/ai/universal-search.js"}
    {"ts":"2025-12-02T14:52:16.525Z","level":1,"message":"[UniversalSearch] query = What's up?","line":698,"sourceId":"file:///Users/yuuki/mindra-light/renderer/ai/universal-search.js"}
    {"ts":"2025-12-02T14:52:16.545Z","level":1,"message":"[UniversalSearch] AI not ready: 送信したよ","line":740,"sourceId":"file:///Users/yuuki/mindra-light/renderer/ai/universal-search.js"}
    {"ts":"2025-12-02T14:54:24.692Z","level":1,"message":"[UniversalSearch] query = Hi, can you hear me?","line":698,"sourceId":"file:///Users/yuuki/mindra-light/renderer/ai/universal-search.js"}
    {"ts":"2025-12-02T14:54:24.719Z","level":1,"message":"[UniversalSearch] AI not ready: 送信したよ","line":740,"sourceId":"file:///Users/yuuki/mindra-light/renderer/ai/universal-search.js"}
    {"ts":"2025-12-02T14:55:26.905Z","level":1,"message":"[UniversalSearch] query = How's going?","line":698,"sourceId":"file:///Users/yuuki/mindra-light/renderer/ai/universal-search.js"}
    {"ts":"2025-12-02T14:55:26.925Z","level":1,"message":"[UniversalSearch] AI not ready: 送信したよ","line":740,"sourceId":"file:///Users/yuuki/mindra-light/renderer/ai/universal-search.js"}
    {"ts":"2025-12-02T14:56:53.365Z","level":3,"message":"Unexpected error while loading URL Error: Error invoking remote method 'GUEST_VIEW_MANAGER_CALL': Error: ERR_NAME_NOT_RESOLVED (-105) loading 'https://chat.kruti.ai/'","line":2,"sourceId":"node:electron/js2c/isolated_bundle"}
    {"ts":"2025-12-02T14:57:57.157Z","level":1,"message":"[UniversalSearch] query = Hi can you hear me?","line":698,"sourceId":"file:///Users/yuuki/mindra-light/renderer/ai/universal-search.js"}
    {"ts":"2025-12-02T14:57:57.174Z","level":1,"message":"[UniversalSearch] AI not ready: 送信したよ","line":740,"sourceId":"file:///Users/yuuki/mindra-light/renderer/ai/universal-search.js"}
    {"ts":"2025-12-02T14:58:19.024Z","level":1,"message":"[UniversalSearch] query = What's up?","line":698,"sourceId":"file:///Users/yuuki/mindra-light/renderer/ai/universal-search.js"}
    {"ts":"2025-12-02T14:58:19.043Z","level":1,"message":"[UniversalSearch] AI not ready: 送信したよ","line":740,"sourceId":"file:///Users/yuuki/mindra-light/renderer/ai/universal-search.js"}
    29 changes: 29 additions & 0 deletions mindra-2025-12-03.log
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,29 @@
    {"ts":"2025-12-03T14:34:46.454Z","level":1,"message":"[UniversalSearch] loaded (fully unified input/send)","line":11,"sourceId":"file:///Users/yuuki/mindra-light/renderer/ai/universal-search.js"}
    {"ts":"2025-12-03T14:34:46.469Z","level":2,"message":"%cElectron Security Warning (Insecure Content-Security-Policy) font-weight: bold; This renderer process has either no Content Security\n Policy set or a policy with \"unsafe-eval\" enabled. This exposes users of\n this app to unnecessary security risks.\n\nFor more information and help, consult\nhttps://electronjs.org/docs/tutorial/security.\nThis warning will not show up\nonce the app is packaged.","line":2,"sourceId":"node:electron/js2c/sandbox_bundle"}
    {"ts":"2025-12-03T14:34:46.469Z","level":2,"message":"%cElectron Security Warning (allowpopups) font-weight: bold; A <webview> has \"allowpopups\" set to true. This exposes\n users of this app to some security risk, since popups are just\n BrowserWindows. If you do not need this feature, you should disable it.\n\n \nFor more information and help, consult\nhttps://electronjs.org/docs/tutorial/security.\nThis warning will not show up\nonce the app is packaged.","line":2,"sourceId":"node:electron/js2c/sandbox_bundle"}
    {"ts":"2025-12-03T14:34:55.672Z","level":3,"message":"Unexpected error while loading URL Error: Error invoking remote method 'GUEST_VIEW_MANAGER_CALL': Error: ERR_ABORTED (-3) loading 'https://www.google.com/'","line":2,"sourceId":"node:electron/js2c/isolated_bundle"}
    {"ts":"2025-12-03T14:34:55.672Z","level":3,"message":"Unexpected error while loading URL Error: Error invoking remote method 'GUEST_VIEW_MANAGER_CALL': Error: ERR_ABORTED (-3) loading 'https://www.google.com/'","line":2,"sourceId":"node:electron/js2c/isolated_bundle"}
    {"ts":"2025-12-03T14:34:57.655Z","level":3,"message":"Unexpected error while loading URL Error: Error invoking remote method 'GUEST_VIEW_MANAGER_CALL': Error: ERR_ABORTED (-3) loading 'https://www.google.com/'","line":2,"sourceId":"node:electron/js2c/isolated_bundle"}
    {"ts":"2025-12-03T14:34:57.656Z","level":3,"message":"Unexpected error while loading URL Error: Error invoking remote method 'GUEST_VIEW_MANAGER_CALL': Error: ERR_ABORTED (-3) loading 'https://about.google/?utm_source=google-JP&utm_medium=referral&utm_campaign=hp-footer&fg=1'","line":2,"sourceId":"node:electron/js2c/isolated_bundle"}
    {"ts":"2025-12-03T14:34:59.366Z","level":3,"message":"Unexpected error while loading URL Error: Error invoking remote method 'GUEST_VIEW_MANAGER_CALL': Error: ERR_ABORTED (-3) loading 'https://www.google.com/'","line":2,"sourceId":"node:electron/js2c/isolated_bundle"}
    {"ts":"2025-12-03T14:35:02.513Z","level":3,"message":"Unexpected error while loading URL Error: Error invoking remote method 'GUEST_VIEW_MANAGER_CALL': Error: ERR_ABORTED (-3) loading 'https://www.google.com/'","line":2,"sourceId":"node:electron/js2c/isolated_bundle"}
    {"ts":"2025-12-03T14:35:02.513Z","level":3,"message":"Unexpected error while loading URL Error: Error invoking remote method 'GUEST_VIEW_MANAGER_CALL': Error: ERR_ABORTED (-3) loading 'https://www.google.com/'","line":2,"sourceId":"node:electron/js2c/isolated_bundle"}
    {"ts":"2025-12-03T14:35:03.009Z","level":3,"message":"Unexpected error while loading URL Error: Error invoking remote method 'GUEST_VIEW_MANAGER_CALL': Error: (-3) loading 'https://business.google.com/jp/google-ads/?subid=ww-ww-et-g-awa-a-g_hpafoot1_1!o2&utm_source=google.com&utm_medium=referral&utm_campaign=google_hpafooter&fg=1'","line":2,"sourceId":"node:electron/js2c/isolated_bundle"}
    {"ts":"2025-12-03T14:35:04.557Z","level":3,"message":"Unexpected error while loading URL Error: Error invoking remote method 'GUEST_VIEW_MANAGER_CALL': Error: ERR_ABORTED (-3) loading 'https://www.google.com/'","line":2,"sourceId":"node:electron/js2c/isolated_bundle"}
    {"ts":"2025-12-03T14:35:04.557Z","level":3,"message":"Unexpected error while loading URL Error: Error invoking remote method 'GUEST_VIEW_MANAGER_CALL': Error: ERR_ABORTED (-3) loading 'https://business.google.com/jp/google-ads/?subid=ww-ww-et-g-awa-a-g_hpafoot1_1!o2&utm_source=google.com&utm_medium=referral&utm_campaign=google_hpafooter&fg=1'","line":2,"sourceId":"node:electron/js2c/isolated_bundle"}
    {"ts":"2025-12-03T14:35:07.774Z","level":3,"message":"Unexpected error while loading URL Error: Error invoking remote method 'GUEST_VIEW_MANAGER_CALL': Error: ERR_ABORTED (-3) loading 'https://www.google.com/'","line":2,"sourceId":"node:electron/js2c/isolated_bundle"}
    {"ts":"2025-12-03T14:35:12.459Z","level":3,"message":"Unexpected error while loading URL Error: Error invoking remote method 'GUEST_VIEW_MANAGER_CALL': Error: ERR_ABORTED (-3) loading 'https://www.google.com/'","line":2,"sourceId":"node:electron/js2c/isolated_bundle"}
    {"ts":"2025-12-03T14:35:13.873Z","level":3,"message":"Unexpected error while loading URL Error: Error invoking remote method 'GUEST_VIEW_MANAGER_CALL': Error: ERR_ABORTED (-3) loading 'https://business.google.com/jp/business-profile/?subid=ww-ww-et-g-awa-a-g_hpbfoot1_1!o2&utm_source=google.com&utm_medium=referral&utm_campaign=google_hpbfooter&fg=1'","line":2,"sourceId":"node:electron/js2c/isolated_bundle"}
    {"ts":"2025-12-03T14:35:14.866Z","level":3,"message":"Unexpected error while loading URL Error: Error invoking remote method 'GUEST_VIEW_MANAGER_CALL': Error: ERR_ABORTED (-3) loading 'https://www.google.com/'","line":2,"sourceId":"node:electron/js2c/isolated_bundle"}
    {"ts":"2025-12-03T14:35:15.715Z","level":3,"message":"Unexpected error while loading URL Error: Error invoking remote method 'GUEST_VIEW_MANAGER_CALL': Error: ERR_ABORTED (-3) loading 'https://business.google.com/jp/business-profile/?subid=ww-ww-et-g-awa-a-g_hpbfoot1_1!o2&utm_source=google.com&utm_medium=referral&utm_campaign=google_hpbfooter&fg=1'","line":2,"sourceId":"node:electron/js2c/isolated_bundle"}
    {"ts":"2025-12-03T14:35:16.360Z","level":3,"message":"Unexpected error while loading URL Error: Error invoking remote method 'GUEST_VIEW_MANAGER_CALL': Error: ERR_ABORTED (-3) loading 'https://www.google.com/'","line":2,"sourceId":"node:electron/js2c/isolated_bundle"}
    {"ts":"2025-12-03T16:13:51.613Z","level":"WARN","message":"test","line":515,"source":"file:///Users/yuuki/mindra-light/renderer/ai/universal-search.js"}
    {"ts":"2025-12-03T16:14:13.303Z","level":"WARN","message":"test","line":657,"source":"file:///Users/yuuki/mindra-light/renderer/ai/universal-search.js"}
    {"ts":"2025-12-03T16:15:56.398Z","level":"WARN","message":"test","line":657,"source":"file:///Users/yuuki/mindra-light/renderer/ai/universal-search.js"}
    {"ts":"2025-12-03T16:16:30.130Z","level":"WARN","message":"ee","line":657,"source":"file:///Users/yuuki/mindra-light/renderer/ai/universal-search.js"}
    {"ts":"2025-12-03T16:17:39.678Z","level":"WARN","message":"test","line":657,"source":"file:///Users/yuuki/mindra-light/renderer/ai/universal-search.js"}
    {"ts":"2025-12-03T16:17:45.677Z","level":"WARN","message":"string","line":657,"source":"file:///Users/yuuki/mindra-light/renderer/ai/universal-search.js"}
    {"ts":"2025-12-03T16:18:01.980Z","level":"WARN","message":"\"test\"","line":657,"source":"file:///Users/yuuki/mindra-light/renderer/ai/universal-search.js"}
    {"ts":"2025-12-03T16:18:36.979Z","level":"WARN","message":"function","line":684,"source":"file:///Users/yuuki/mindra-light/renderer/ai/universal-search.js"}
    {"ts":"2025-12-03T16:18:36.983Z","level":"WARN","message":"chat-mscopilot-typed","line":689,"source":"file:///Users/yuuki/mindra-light/renderer/ai/universal-search.js"}
    {"ts":"2025-12-03T17:31:19.704Z","level":"WARN","message":"oslogin sdk encryptStoreToken success!","line":1,"source":"https://ernie.baidu.com/passport/pcmanage/assets/index-C2RxhhXZ.js"}
  2. yuuki7 revised this gist Dec 3, 2025. 1 changed file with 32 additions and 107 deletions.
    139 changes: 32 additions & 107 deletions universal-search.js
    Original file line number Diff line number Diff line change
    @@ -47,9 +47,9 @@ const WEB_ACTION_RULES = [
    chat: { input: "mindra-ng" },
    },
    {
    // [US] Microsoft Copilot Web(専用ルート)
    // [US] Microsoft Copilot Web
    match: /https?:\/\/copilot\.microsoft\.com\//,
    chat: { input: "mindra-ng" },
    chat: { input: "mindra-dummy" },
    },
    {
    // [US] Bing Chat / Copilot in Bing
    @@ -183,13 +183,25 @@ const WEB_ACTION_RULES = [
    },
    {
    // [CN] Zhipu GLM
    match: /https?:\/\/(chatglm\.cn|bigmodel\.cn)\//,
    chat: { input: "mindra-dummy" },
    match: /https?:\/\/(chatglm\.cn|bigmodel\.cn|chat\.z\.ai)\//,
    chat: {
    input: 'textarea#chat-input',

    // FIXME: GLM かどうか判定するためのフラグ
    is_glm: true,
    },
    },
    {
    // [CN] Kimi(専用ルート)
    // [CN] Kimi
    match: /https?:\/\/(www\.)?kimi\.com\//,
    chat: { input: "mindra-ng" },
    chat: { input: "mindra-dummy" },
    },
    {
    // [CN] MiniMax
    match: /https?:\/\/agent\.minimax\.io\//,
    chat: {
    input: 'textarea#chat-input',
    },
    },

    // ===== Japan / Korea / Asia =====
    @@ -205,8 +217,10 @@ const WEB_ACTION_RULES = [
    },
    {
    // [IN] Krutrim
    match: /https?:\/\/(www\.)?krutrim\.com\//,
    chat: { input: "mindra-dummy" },
    match: /https?:\/\/(www\.)?(krutrim\.com|kruti\.ai)\//,
    chat: {
    input: 'textarea.txt-primary',
    },
    },
    {
    // [IN] Hanooman
    @@ -381,13 +395,7 @@ function buildInjectionScript(query, rule) {
    if (!el) return false;
    el.focus();
    if ("value" in el) {
    el.value = q;
    }
    if (el.isContentEditable || el.getAttribute("contenteditable") === "true") {
    el.innerText = q;
    }
    document.execCommand('insertText', false, q);
    try {
    el.dispatchEvent(new Event("input", { bubbles: true }));
    @@ -427,12 +435,15 @@ function buildInjectionScript(query, rule) {
    }
    // さらに保険として、フォーム経由なら submit も試す
    try {
    var form = el.form;
    if (form) {
    form.submit();
    }
    } catch (e) {}
    // FIXME: GLM ではトップページに戻ってしまうので実行しない(Claude では Enter で送信できないので実行する必要がある)
    if (!cfg?.chat?.is_glm) {
    try {
    var form = el.form;
    if (form) {
    form.submit();
    }
    } catch (e) {}
    }
    }, 150); // ← ここでちょっと待つ
    // 非同期送信なので true 固定でOK
    @@ -651,32 +662,6 @@ async function runActionInWebview(wv, query) {
    return result || "none";
    }

    // ========= Microsoft Copilot 専用ルート ==========
    if (/copilot\.microsoft\.com/.test(url)) {
    const code = `(function(q) {
    var el =
    document.querySelector('textarea#userInput') ||
    document.querySelector('textarea[data-testid="composer-input"]') ||
    document.querySelector('textarea[role="textbox"]');
    if (!el) return 'none';
    el.focus();
    el.value = q;
    try {
    el.dispatchEvent(new Event('input', { bubbles: true }));
    el.dispatchEvent(new Event('change', { bubbles: true }));
    } catch (_) {}
    return 'chat-mscopilot-typed';
    })(${JSON.stringify(query)});`;

    if (typeof wv.executeJavaScript !== "function") return "error:no-exec";
    const result = await wv.executeJavaScript(code, false);
    return result || "none";
    }

    // ========= Manus 専用ルート ==========
    if (/manus\.im\/app/.test(url)) {
    const code = `(function(q) {
    @@ -716,66 +701,6 @@ async function runActionInWebview(wv, query) {
    return result || "none";
    }

    // ========= Kimi 専用ルート ==========
    if (/kimi\.com/.test(url)) {
    const code = `(function(q) {
    var el =
    document.querySelector('div[contenteditable="true"][data-lexical-editor="true"][role="textbox"]') ||
    document.querySelector('div[contenteditable="true"][role="textbox"]');
    if (!el) return 'none';
    el.focus();
    try {
    var sel = window.getSelection();
    if (sel) {
    var range = document.createRange();
    range.selectNodeContents(el);
    range.collapse(false);
    sel.removeAllRanges();
    sel.addRange(range);
    }
    } catch (_) {}
    try {
    document.execCommand('selectAll', false, null);
    document.execCommand('delete', false, null);
    var ok = false;
    try { ok = document.execCommand('insertText', false, q); } catch (_) {}
    if (!ok) el.textContent = q;
    } catch (_) {
    el.textContent = q;
    }
    try {
    el.dispatchEvent(new Event('input', { bubbles: true }));
    el.dispatchEvent(new Event('change', { bubbles: true }));
    } catch (_) {}
    setTimeout(() => {
    try {
    var evInit = {
    key: 'Enter',
    code: 'Enter',
    keyCode: 13,
    which: 13,
    bubbles: true
    };
    ['keydown', 'keypress', 'keyup'].forEach(t => {
    el.dispatchEvent(new KeyboardEvent(t, evInit));
    });
    } catch (_) {}
    }, 150);
    return 'chat-kimi';
    })(${JSON.stringify(query)});`;

    if (typeof wv.executeJavaScript !== "function") return "error:no-exec";
    const result = await wv.executeJavaScript(code, false);
    return result || "none";
    }

    // ========= ここから汎用ルート ==========
    const rule = getRuleForWebview(wv);
    const code = buildInjectionScript(query, rule);
  3. yuuki7 created this gist Dec 3, 2025.
    839 changes: 839 additions & 0 deletions universal-search.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,839 @@
    // ==========================================================
    // Universal WebAction Engine
    // - 検索 / チャットの区別を完全撤廃
    // - 「いい感じの入力欄を1つ見つけて、文字を入れて送信」だけやる
    // - WEB_ACTION_RULES = 世界AIサービス辞典(国別)
    // * input: "mindra-dummy" → 未確認
    // * input: "mindra-ok" → 汎用でOK
    // * input: "mindra-ng" → 汎用でNG(専用ルートあり)
    // ==========================================================

    // ----------------------------------------------------------
    // Pseudo-selector 判定(JS 側)
    // ----------------------------------------------------------
    function isMindraPseudoSelector(sel) {
    return sel === "mindra-dummy" || sel === "mindra-ok" || sel === "mindra-ng";
    }


    // ----------------------------------------------------------
    // 世界 AI サイト辞典(WEB_ACTION_RULES)
    // - 1件ごとに「どこの何か」をコメントで明記
    // ----------------------------------------------------------
    const WEB_ACTION_RULES = [
    // ===== Global / US =====
    {
    // [US] OpenAI ChatGPT
    match: /https?:\/\/(chatgpt\.com|chat\.openai\.com)\//,
    chat: {
    input:
    '#prompt-textarea, div#prompt-textarea, textarea[role="textbox"], div[contenteditable="true"][role="textbox"]',
    sendButton: 'button[data-testid="send-button"]',
    },
    },
    {
    // [US] OpenAI Platform / Playground
    match: /https?:\/\/platform\.openai\.com\//,
    chat: { input: "mindra-dummy" },
    },
    {
    // [US] Anthropic Claude
    match: /https?:\/\/claude\.ai\//,
    chat: { input: "mindra-dummy" },
    },
    {
    // [US] Perplexity(汎用NG → 専用ルート使用)
    match: /https?:\/\/(www\.)?perplexity\.ai\//,
    chat: { input: "mindra-ng" },
    },
    {
    // [US] Microsoft Copilot Web(専用ルート)
    match: /https?:\/\/copilot\.microsoft\.com\//,
    chat: { input: "mindra-ng" },
    },
    {
    // [US] Bing Chat / Copilot in Bing
    match: /https?:\/\/www\.bing\.com\/chat/,
    chat: { input: "mindra-dummy" },
    },
    {
    // [US] xAI Grok (grok.com)
    match: /https?:\/\/grok\.com\//,
    chat: { input: "mindra-dummy" },
    },
    {
    // [US] xAI Grok on X
    match: /https?:\/\/x\.com\/i\/grok/,
    chat: { input: "mindra-dummy" },
    },
    {
    // [US] Meta AI(Meta / Facebook / Instagram / Threads)
    match:
    /https?:\/\/(meta\.ai|www\.facebook\.com|www\.instagram\.com|www\.threads\.net)\//,
    chat: { input: "mindra-dummy" },
    },
    {
    // [US] Character.AI
    match: /https?:\/\/(www\.)?character\.ai\//,
    chat: { input: "mindra-dummy" },
    },
    {
    // [US] Poe (Quora)
    match: /https?:\/\/poe\.com\//,
    chat: { input: "mindra-dummy" },
    },
    {
    // [US] Replika
    match: /https?:\/\/replika\.com\//,
    chat: { input: "mindra-dummy" },
    },
    {
    // [US] HuggingFace Chat
    match: /https?:\/\/huggingface\.co\/chat/,
    chat: { input: "mindra-dummy" },
    },
    {
    // [US] Groq Chat / Console
    match: /https?:\/\/(groq\.com|console\.groq\.com)\//,
    chat: { input: "mindra-dummy" },
    },
    {
    // [US] GitHub Copilot Chat(専用ルート)
    match: /https?:\/\/github\.com\/copilot/,
    chat: { input: "mindra-ng" },
    },

    // ===== Google 系 =====
    {
    // [Google] Gemini
    match: /https?:\/\/gemini\.google\.com\//,
    chat: { input: "mindra-dummy" },
    },
    {
    // [Google] AI Studio
    match: /https?:\/\/aistudio\.google\.com\//,
    chat: { input: "mindra-dummy" },
    },
    {
    // [Google] 検索
    match: /https?:\/\/www\.google\./,
    search: {
    input: 'mindra-ng',
    },
    },

    // ===== Europe 系 =====
    {
    // [EU] Mistral / Chat
    match: /https?:\/\/(mistral\.ai|chat\.mistral\.ai)\//,
    chat: { input: "mindra-dummy" },
    },
    {
    // [EU] Aleph Alpha
    match: /https?:\/\/(www\.)?aleph-alpha\.com\//,
    chat: { input: "mindra-dummy" },
    },
    {
    // [EU] DeepL Chat
    match: /https?:\/\/www\.deepl\.com\/chat/,
    chat: { input: "mindra-dummy" },
    },
    {
    // [EU] Pi (heyPi)
    match: /https?:\/\/heypi\.com\//,
    chat: { input: "mindra-dummy" },
    },

    // ===== China 系 =====
    {
    // [CN] Baidu ERNIE / 文心一言
    match: /https?:\/\/(ernie\.baidu\.com|yiyan\.baidu\.com)\//,
    chat: { input: "mindra-dummy" },
    },
    {
    // [CN] Alibaba Tongyi / Qwen
    match:
    /https?:\/\/(chat\.qwen\.ai|tongyi\.aliyun\.com|qwen\.ai|gpt\.aliyun\.com)\//,
    chat: { input: "mindra-dummy" },
    },
    {
    // [CN] DeepSeek Chat
    match: /https?:\/\/chat\.deepseek\.com\//,
    chat: { input: "mindra-dummy" },
    },
    {
    // [CN] Tencent Hunyuan
    match: /https?:\/\/hunyuan\.tencent\.com\//,
    chat: { input: "mindra-dummy" },
    },
    {
    // [CN] ByteDance Doubao
    match: /https?:\/\/doubao\.com\//,
    chat: { input: "mindra-dummy" },
    },
    {
    // [CN] SenseTime 日日新
    match: /https?:\/\/ririshin\.com\//,
    chat: { input: "mindra-dummy" },
    },
    {
    // [CN] iFlytek Spark
    match: /https?:\/\/.*xfyun\.cn\//,
    chat: { input: "mindra-dummy" },
    },
    {
    // [CN] Zhipu GLM
    match: /https?:\/\/(chatglm\.cn|bigmodel\.cn)\//,
    chat: { input: "mindra-dummy" },
    },
    {
    // [CN] Kimi(専用ルート)
    match: /https?:\/\/(www\.)?kimi\.com\//,
    chat: { input: "mindra-ng" },
    },

    // ===== Japan / Korea / Asia =====
    {
    // [KR] Naver CLOVA Studio / X
    match: /https?:\/\/clovastudio\.ncloud\.com\//,
    chat: { input: "mindra-dummy" },
    },
    {
    // [KR] Kakao / KoGPT など
    match: /https?:\/\/.*kakao\.com\//,
    chat: { input: "mindra-dummy" },
    },
    {
    // [IN] Krutrim
    match: /https?:\/\/(www\.)?krutrim\.com\//,
    chat: { input: "mindra-dummy" },
    },
    {
    // [IN] Hanooman
    match: /https?:\/\/(www\.)?hanooman\.ai\//,
    chat: { input: "mindra-dummy" },
    },
    {
    // [IN] Sarvam AI
    match: /https?:\/\/(www\.)?sarvam\.ai\//,
    chat: { input: "mindra-dummy" },
    },
    {
    // [IN] Bhashini / BharatGPT
    match: /https?:\/\/bhashini\.gov\.in\//,
    chat: { input: "mindra-dummy" },
    },

    // ===== Agent / Work / Biz =====
    {
    // [US] Jasper AI
    match: /https?:\/\/(www\.)?jasper\.ai\//,
    chat: { input: "mindra-dummy" },
    },
    {
    // [US] Notion AI
    match: /https?:\/\/(www\.)?notion\.so\//,
    chat: { input: "mindra-dummy" },
    },
    {
    // [US] Slack / Slack AI
    match: /https?:\/\/.*slack\.com\//,
    chat: { input: "mindra-dummy" },
    },
    {
    // [US] Zoom / AI Companion
    match: /https?:\/\/.*zoom\.us\//,
    chat: { input: "mindra-dummy" },
    },
    {
    // [US] AgentGPT
    match: /https?:\/\/agentgpt\.reworkd\.ai\//,
    chat: { input: "mindra-dummy" },
    },
    {
    // [US] Aomni / Agent系
    match: /https?:\/\/(www\.)?aomni\.com\//,
    chat: { input: "mindra-dummy" },
    },
    ];


    // ----------------------------------------------------------
    // 最低限ステート
    // ----------------------------------------------------------
    window.UNIVERSAL_SEARCH = {
    lastQuery: "",
    lastResult: null,
    };


    // ----------------------------------------------------------
    // webview URL取得
    // ----------------------------------------------------------
    function getWebviewUrl(wv) {
    try {
    if (typeof wv.getURL === "function") return wv.getURL();
    } catch (_) {}
    try {
    if (wv.src) return wv.src;
    } catch (_) {}
    return "";
    }


    // ----------------------------------------------------------
    // URL → WEB_ACTION_RULES
    // ----------------------------------------------------------
    function getRuleForWebview(wv) {
    const url = getWebviewUrl(wv) || "";
    for (const rule of WEB_ACTION_RULES) {
    try {
    if (rule.match && rule.match.test(url)) {
    return rule;
    }
    } catch (_) {}
    }
    return null;
    }


    // ----------------------------------------------------------
    // 対象webview一覧(表示中のものだけに絞る)
    // ----------------------------------------------------------

    // webview が画面上で「見えている」か判定
    function isVisibleWebview(wv) {
    try {
    const style = window.getComputedStyle(wv);
    if (style.display === "none" || style.visibility === "hidden") {
    return false;
    }
    const rect = wv.getBoundingClientRect();
    if (rect.width <= 0 || rect.height <= 0) {
    return false;
    }
    return true;
    } catch (_) {
    // 何か取れなかった場合は、とりあえず true 扱い(安全側)
    return true;
    }
    }

    function getTargets() {
    const views = window.mindraViews || {};

    // splitview があるならまずそこから(その中でも表示されているものだけ)
    if (typeof views.getSplitWebviews === "function") {
    try {
    const split = views.getSplitWebviews();
    if (Array.isArray(split) && split.length > 0) {
    const visibleSplit = split.filter(isVisibleWebview);
    if (visibleSplit.length > 0) return visibleSplit;
    // 全部不可視だった場合はそのまま split を返さず、下のフォールバックに回す
    }
    } catch (_) {}
    }

    // 全 webview から「見えているもの」だけ
    try {
    const all = Array.from(document.querySelectorAll("webview"));
    if (all.length > 0) {
    const visible = all.filter(isVisibleWebview);
    if (visible.length > 0) return visible;
    return all; // 全部不可視扱いなら一応 all を返す
    }
    } catch (_) {}

    // 最後の保険:アクティブ webview 単体
    if (typeof views.getActiveWebview === "function") {
    try {
    const active = views.getActiveWebview();
    if (active) return [active];
    } catch (_) {}
    }

    return [];
    }


    // ----------------------------------------------------------
    // 注入スクリプト(search/chat 完全統一版)
    // - 「候補セレクタのリスト」を 1 本だけ持つ
    // ----------------------------------------------------------
    function buildInjectionScript(query, rule) {
    const q = JSON.stringify(query);
    const cfg = JSON.stringify(
    rule
    ? {
    search: rule.search || null,
    chat: rule.chat || null,
    }
    : null
    );

    return `
    (function(q, cfg) {
    function isPseudo(sel) {
    return sel === 'mindra-dummy' || sel === 'mindra-ng';
    }
    function typeAndSend(el, cfg) {
    if (!el) return false;
    el.focus();
    if ("value" in el) {
    el.value = q;
    }
    if (el.isContentEditable || el.getAttribute("contenteditable") === "true") {
    el.innerText = q;
    }
    try {
    el.dispatchEvent(new Event("input", { bubbles: true }));
    el.dispatchEvent(new Event("change", { bubbles: true }));
    } catch (e) {}
    // ここから少し待ってから送信する
    setTimeout(function () {
    var sent = false;
    // ルールに sendButton があれば優先してクリック
    var btnSel = cfg && cfg.chat && cfg.chat.sendButton;
    if (btnSel && !isPseudo(btnSel)) {
    try {
    var btn = document.querySelector(btnSel);
    if (btn) {
    btn.click();
    sent = true;
    }
    } catch (e) {}
    }
    // クリックで送れなかった場合は Enter で送信
    if (!sent) {
    try {
    var ev = {
    key: "Enter",
    code: "Enter",
    keyCode: 13,
    which: 13,
    bubbles: true,
    };
    el.dispatchEvent(new KeyboardEvent("keydown", ev));
    el.dispatchEvent(new KeyboardEvent("keypress", ev));
    el.dispatchEvent(new KeyboardEvent("keyup", ev));
    } catch (e) {}
    }
    // さらに保険として、フォーム経由なら submit も試す
    try {
    var form = el.form;
    if (form) {
    form.submit();
    }
    } catch (e) {}
    }, 150); // ← ここでちょっと待つ
    // 非同期送信なので true 固定でOK
    return true;
    }
    // ===== ルール由来の優先セレクタ =====
    var ruleSelectors = [];
    if (cfg && cfg.search && cfg.search.input && !isPseudo(cfg.search.input)) {
    ruleSelectors.push(cfg.search.input);
    }
    if (cfg && cfg.chat && cfg.chat.input && !isPseudo(cfg.chat.input)) {
    ruleSelectors.push(cfg.chat.input);
    }
    // ===== 汎用セレクタ =====
    var unifiedSelectors = [
    // 検索系
    'form[action="/search"] input[name="q"]',
    'input[type="search"]',
    'input[role="searchbox"]',
    'input[name="q"]',
    'input[placeholder*="検索"]',
    'input[placeholder*="Search"]',
    // チャット系
    'textarea[placeholder*="メッセージ"]',
    'textarea[placeholder*="message"]',
    'textarea[role="textbox"]',
    'div[contenteditable="true"][role="textbox"]',
    'div[contenteditable="true"][data-lexical-editor="true"]',
    'div[contenteditable="true"][data-placeholder]',
    'div[contenteditable="true"]'
    ];
    // ===== 候補の決定 =====
    var target = null;
    // 1) ルールで指定されているセレクタを優先
    for (var i = 0; i < ruleSelectors.length && !target; i++) {
    try {
    var elRule = document.querySelector(ruleSelectors[i]);
    if (elRule) target = elRule;
    } catch (_) {}
    }
    // 2) ルールで見つからなければ汎用セレクタから探す
    if (!target) {
    for (var j = 0; j < unifiedSelectors.length && !target; j++) {
    try {
    var elU = document.querySelector(unifiedSelectors[j]);
    if (elU) target = elU;
    } catch (_) {}
    }
    }
    // 3) 最後の保険
    if (!target) {
    target =
    document.querySelector('input[type="text"]') ||
    document.querySelector("textarea") ||
    document.querySelector('[contenteditable="true"]');
    }
    if (!target) {
    return "none";
    }
    var ok = typeAndSend(target, cfg || {});
    return ok ? "ok" : "none";
    })(${q}, ${cfg});
    `;
    }


    // ----------------------------------------------------------
    // runActionInWebview :専用ルート → 汎用ルート
    // ----------------------------------------------------------
    async function runActionInWebview(wv, query) {
    const url = getWebviewUrl(wv) || "";

    // ========= Google 検索(トップ / 結果 共通) 専用ルート ==========
    if (/https?:\/\/www\.google\./.test(url)) {
    const code = `(function(q){
    // トップページ・検索結果ページ両方に対応するため、textarea / input 両方を見る
    var input =
    document.querySelector('form[action="/search"] textarea[name="q"]') ||
    document.querySelector('form[action="/search"] input[name="q"]') ||
    document.querySelector('textarea[name="q"]') ||
    document.querySelector('input[name="q"]');
    if (!input) return "google:no-input";
    input.focus();
    if ("value" in input) {
    input.value = q;
    } else {
    input.textContent = q;
    }
    try {
    input.dispatchEvent(new Event('input', { bubbles: true }));
    input.dispatchEvent(new Event('change', { bubbles: true }));
    } catch (_) {}
    setTimeout(function () {
    try {
    // 検索実行ボタンいろいろ
    var btn =
    document.querySelector('button[aria-label="検索"]') ||
    document.querySelector('button[aria-label="Google 検索"]') ||
    document.querySelector('button[aria-label="Search"]') ||
    document.querySelector('input[name="btnK"][type="submit"]');
    if (btn) {
    btn.click();
    } else if (input.form) {
    // フォームがあれば submit
    input.form.submit();
    } else {
    // それでもダメなら Enter を投げる
    var evInit = {
    key: 'Enter',
    code: 'Enter',
    keyCode: 13,
    which: 13,
    bubbles: true
    };
    ['keydown','keypress','keyup'].forEach(function(t){
    input.dispatchEvent(new KeyboardEvent(t, evInit));
    });
    }
    } catch (_) {}
    }, 150);
    return "google:ok";
    })(${JSON.stringify(query)});`;

    try {
    if (typeof wv.executeJavaScript !== "function") return "error:no-exec";
    const result = await wv.executeJavaScript(code, false);
    return result || "none";
    } catch (_) {
    return "error:exception";
    }
    }

    // ========= Perplexity 専用ルート ==========
    if (/perplexity\.ai/.test(url)) {
    const code = `(function(q) {
    var el = document.querySelector(
    'div#ask-input[contenteditable="true"],' +
    'div[contenteditable="true"][data-test-id="user-textbox"],' +
    'div[contenteditable="true"][data-lexical-editor="true"]'
    );
    if (!el) return 'none';
    el.focus();
    try {
    var sel = window.getSelection();
    if (sel) {
    var range = document.createRange();
    range.selectNodeContents(el);
    range.collapse(false);
    sel.removeAllRanges();
    sel.addRange(range);
    }
    } catch (_) {}
    try {
    document.execCommand('selectAll', false, null);
    document.execCommand('delete', false, null);
    var ok = false;
    try { ok = document.execCommand('insertText', false, q); } catch (_) {}
    if (!ok) el.textContent = q;
    } catch (_) {
    el.textContent = q;
    }
    try {
    el.dispatchEvent(new Event('input', { bubbles: true }));
    el.dispatchEvent(new Event('change', { bubbles: true }));
    } catch (_) {}
    return 'chat-perplexity-typed';
    })(${JSON.stringify(query)});`;

    try {
    if (typeof wv.executeJavaScript !== "function") return "error:no-exec";
    const result = await wv.executeJavaScript(code, false);
    return result || "none";
    } catch (_) {
    return "error:exception";
    }
    }

    // ========= GitHub Copilot 専用ルート ==========
    if (/github\.com\/copilot/.test(url)) {
    const code = `(function(q) {
    var el = document.querySelector('textarea');
    if (!el) return 'none';
    el.focus();
    el.value = q;
    try {
    el.dispatchEvent(new Event('input', { bubbles: true }));
    el.dispatchEvent(new Event('change', { bubbles: true }));
    } catch (_) {}
    return 'chat-copilot-typed';
    })(${JSON.stringify(query)});`;

    if (typeof wv.executeJavaScript !== "function") return "error:no-exec";
    const result = await wv.executeJavaScript(code, false);
    return result || "none";
    }

    // ========= Microsoft Copilot 専用ルート ==========
    if (/copilot\.microsoft\.com/.test(url)) {
    const code = `(function(q) {
    var el =
    document.querySelector('textarea#userInput') ||
    document.querySelector('textarea[data-testid="composer-input"]') ||
    document.querySelector('textarea[role="textbox"]');
    if (!el) return 'none';
    el.focus();
    el.value = q;
    try {
    el.dispatchEvent(new Event('input', { bubbles: true }));
    el.dispatchEvent(new Event('change', { bubbles: true }));
    } catch (_) {}
    return 'chat-mscopilot-typed';
    })(${JSON.stringify(query)});`;

    if (typeof wv.executeJavaScript !== "function") return "error:no-exec";
    const result = await wv.executeJavaScript(code, false);
    return result || "none";
    }

    // ========= Manus 専用ルート ==========
    if (/manus\.im\/app/.test(url)) {
    const code = `(function(q) {
    var el =
    document.querySelector('textarea[placeholder*="タスクを割り当てて"]') ||
    document.querySelector('textarea');
    if (!el) return 'none';
    el.focus();
    el.value = q;
    try {
    el.dispatchEvent(new Event('input', { bubbles: true }));
    el.dispatchEvent(new Event('change', { bubbles: true }));
    } catch (_) {}
    setTimeout(() => {
    try {
    var evInit = {
    key: 'Enter',
    code: 'Enter',
    keyCode: 13,
    which: 13,
    bubbles: true
    };
    ['keydown', 'keypress', 'keyup'].forEach(t => {
    el.dispatchEvent(new KeyboardEvent(t, evInit));
    });
    } catch (_) {}
    }, 150);
    return 'chat-manus';
    })(${JSON.stringify(query)});`;

    if (typeof wv.executeJavaScript !== "function") return "error:no-exec";
    const result = await wv.executeJavaScript(code, false);
    return result || "none";
    }

    // ========= Kimi 専用ルート ==========
    if (/kimi\.com/.test(url)) {
    const code = `(function(q) {
    var el =
    document.querySelector('div[contenteditable="true"][data-lexical-editor="true"][role="textbox"]') ||
    document.querySelector('div[contenteditable="true"][role="textbox"]');
    if (!el) return 'none';
    el.focus();
    try {
    var sel = window.getSelection();
    if (sel) {
    var range = document.createRange();
    range.selectNodeContents(el);
    range.collapse(false);
    sel.removeAllRanges();
    sel.addRange(range);
    }
    } catch (_) {}
    try {
    document.execCommand('selectAll', false, null);
    document.execCommand('delete', false, null);
    var ok = false;
    try { ok = document.execCommand('insertText', false, q); } catch (_) {}
    if (!ok) el.textContent = q;
    } catch (_) {
    el.textContent = q;
    }
    try {
    el.dispatchEvent(new Event('input', { bubbles: true }));
    el.dispatchEvent(new Event('change', { bubbles: true }));
    } catch (_) {}
    setTimeout(() => {
    try {
    var evInit = {
    key: 'Enter',
    code: 'Enter',
    keyCode: 13,
    which: 13,
    bubbles: true
    };
    ['keydown', 'keypress', 'keyup'].forEach(t => {
    el.dispatchEvent(new KeyboardEvent(t, evInit));
    });
    } catch (_) {}
    }, 150);
    return 'chat-kimi';
    })(${JSON.stringify(query)});`;

    if (typeof wv.executeJavaScript !== "function") return "error:no-exec";
    const result = await wv.executeJavaScript(code, false);
    return result || "none";
    }

    // ========= ここから汎用ルート ==========
    const rule = getRuleForWebview(wv);
    const code = buildInjectionScript(query, rule);

    try {
    if (typeof wv.executeJavaScript !== "function") return "error:no-exec";
    const result = await wv.executeJavaScript(code, false);
    return result || "none";
    } catch (_) {
    return "error:exception";
    }
    }


    // ----------------------------------------------------------
    // エントリ(第2引数は互換のため受け取るだけで無視)
    // ----------------------------------------------------------
    window.runUniversalSearch = async function (query, _ignored) {
    const targets = getTargets();

    if (targets.length === 0) {
    const msg =
    "いい感じの入力欄が見つからなくて、操作できなかったよ。";
    window.UNIVERSAL_SEARCH.lastQuery = query;
    window.UNIVERSAL_SEARCH.lastResult = { targets: 0 };
    window.dispatchUniversalSearchResult(msg);
    return msg;
    }

    let successCount = 0;

    for (const wv of targets) {
    const status = await runActionInWebview(wv, query);
    if (status && !String(status).startsWith("error") && status !== "none") {
    successCount++;
    }
    }

    const msg =
    successCount === 0
    ? "いい感じの入力欄が見つからなくて、何もできなかったよ。"
    : "送信したよ";

    window.UNIVERSAL_SEARCH.lastQuery = query;
    window.UNIVERSAL_SEARCH.lastResult = { targets: targets.length };

    window.dispatchUniversalSearchResult(msg);
    return msg;
    };


    // ----------------------------------------------------------
    // AI サイドバーへ結果通知
    // ----------------------------------------------------------
    window.dispatchUniversalSearchResult = function (msg) {
    if (window.mindraAI?.receiveSearchResult) {
    window.mindraAI.receiveSearchResult(msg);
    } else {
    //_ignore
    }
    };