Quality of life enhancements for Kagi Assistant
// ==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();
})();