[Kagi] Assistant Improved

Quality of life enhancements for Kagi Assistant

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

Advertisement:

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

Advertisement:

// ==UserScript==
// @name         [Kagi] Assistant Improved
// @description  Quality of life enhancements for Kagi Assistant
// @version      0.1.1
// @license      GPL-3.0
// @author       Alex Cochran
// @namespace    cc.sector-c
// @match        https://assistant.kagi.com/chat/*
// @grant        none
// ==/UserScript==

// --->>>
// [ATTRIBUTION]
// scripts = [
//   { title  = "Kagi Assistant Ctrl+Enter to submit",
//     author = "nothingbird",
//     source = "https://greasyfork.org/en/scripts/551375-kagi-assistant-ctrl-enter-to-submit",
//     license = "GPL-3.0" },
//   { title  = "Kagi Assistant Enhancements",
//     author = "stanyslassz",
//     source = "https://greasyfork.org/en/scripts/523026-kagi-assistant-enhancements",
//     license = "GPL-3.0" },
// ]
//
// [AUTHOR]
// name  = "Alex Cochran"
// email = "[email protected]"
// date  = 2026-06-26
// <<<---

/* jshint esversion: 8 */

(function () {
    "use strict";

    // [FEATURES]
    // ==========

    /**
     * Prevents plain Enter from submitting a form or inserting a newline in
     * any `<textarea>`, while leaving all modifier-key combinations untouched.
     *
     * A capturing-phase `keydown` listener is registered on `document`. When the
     * keydown originates from a `<textarea>` the following behaviour applies:
     *
     * | Key combination            | Behavior                                            |
     * | -------------------------- | --------------------------------------------------- |
     * | Enter (no modifier)        | Default action and propagation are suppressed.      |
     * | Shift+Enter                | Native newline insertion — listener returns early.  |
     * | Ctrl+Enter                 | Native (e.g. Kagi) submit — listener returns early. |
     * | Alt+Enter / Meta+Enter     | Native behaviour — listener returns early.          |
     *
     * Composition-state keystrokes (`event.isComposing` or `keyCode === 229`)
     * are always ignored so IME input is not disrupted.
     *
     * Because the listener is attached in the **capture** phase (`true` as the
     * third argument to `addEventListener`), it runs before any
     * element-level handlers, ensuring `stopImmediatePropagation()` can reliably
     * block downstream listeners.
     *
     * @returns {void}
     */
    function ctrlEnterSubmit() {
        document.addEventListener(
            "keydown",
            function (event) {
                const target = event.target;
                if (target.tagName !== "TEXTAREA") return;
                if (event.isComposing || event.keyCode === 229) return;
                if (event.key !== "Enter") return;

                // Shift+Enter: let browser insert newline — don't touch
                if (event.shiftKey) return;

                // Ctrl+Enter: let Kagi's native submit run
                if (event.ctrlKey) return;

                // Alt/Meta+Enter: leave alone
                if (event.altKey || event.metaKey) return;

                // Plain Enter: block submit, do nothing (no newline, no submit)
                event.preventDefault();
                event.stopImmediatePropagation();
            },
            true,
        );
    }

    /**
     * Appends a copy-to-clipboard button to every `.codehilite` block in the
     * document, skipping any block that already contains one.
     *
     * For each matched block a `<button class="bottom-copy-btn relative">` is
     * created with an inline SVG copy icon and a "Copied to clipboard" tooltip
     * span. The button is marked `data-partial-update-ignore="true"` so partial
     * DOM updates leave it intact.
     *
     * Clicking the button reads the text content of the `<code>` element inside
     * the block, writes it to the system clipboard via
     * `navigator.clipboard.writeText`, then reveals the tooltip for 2 seconds
     * before hiding it again.
     *
     * @returns {void}
     */
    function addCodeCopyButton() {
        const codeBlocks = document.querySelectorAll('.codehilite');

        codeBlocks.forEach(block => {
            if (block.querySelector('.bottom-copy-btn')) return;

            const copyButton = document.createElement('button');
            copyButton.className = 'bottom-copy-btn relative';
            copyButton.title = 'Copy';
            copyButton.setAttribute('data-partial-update-ignore', 'true');
            copyButton.innerHTML = `
                <span class="_0_copied_tooltip">Copied to clipboard</span>
                <i class="icon-sm">
                    <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                        <path d="M7.5 7.5V5.25A2.25 2.25 0 019.75 3H18.75A2.25 2.25 0 0121 5.25V14.25A2.25 2.25 0 0118.75 16.5H16.5M16.5 9.75A2.25 2.25 0 0014.25 7.5H5.25A2.25 2.25 0 003 9.75V18.75A2.25 2.25 0 005.25 21H14.25A2.25 2.25 0 0016.5 18.75V9.75Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path>
                    </svg>
                </i>
            `;

            copyButton.addEventListener('click', async () => {
                const code = block.querySelector('code').textContent;
                await navigator.clipboard.writeText(code);

                const tooltip = copyButton.querySelector('._0_copied_tooltip');
                tooltip.style.display = 'block';
                setTimeout(() => {
                    tooltip.style.display = 'none';
                }, 2000);
            });

            block.appendChild(copyButton);
        });
    }

    // [CORE]
    // =======

    function init() {
        ctrlEnterSubmit();
        addCodeCopyButton();
    }

    // Observe DOM changes
    const observer = new MutationObserver((mutations) => {
        init();
    });

    observer.observe(document.body, {
        childList: true,
        subtree: true,
    });

    // Initialize features
    init();
})();