Skip to content

Instantly share code, notes, and snippets.

@yuuki7
Last active December 3, 2025 20:35
  • Select an option

Select an option

Revisions

  1. 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);
  2. 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
    }
    };