(* I'm sorry that this is an discussion rather than an issue. *)
For those who are tired of granting permissions again and again, here is a workaround for you:
Install Greasemonkey (or other similar extensions), create a new script with the following content and you are good to go:
// ==UserScript==
// @name ChatGPT CatDesk approval allowlist
// @match https://chatgpt.com/*
// @match https://chat.openai.com/*
// @run-at document-idle
// @grant none
// ==/UserScript==
(() => {
"use strict";
const cfg = {
armed: false,
dryRun: false,
connector: /^CatDesk$/,
allowedBody: /[\s\S]/,
allowButtonText: /^(許可する|Allow)$/i,
blockedSharedDataKeys: /^$/,
cooldownMs: 2000,
};
let lastClickAt = 0;
let ui = null;
const norm = (s) => (s || "").replace(/\s+/g, " ").trim();
function isVisible(el) {
const r = el.getBoundingClientRect();
const s = getComputedStyle(el);
return r.width > 0 && r.height > 0 && s.display !== "none" && s.visibility !== "hidden";
}
function cardText(card) {
return norm(card.innerText || card.textContent);
}
function getConnectorName(card) {
const img = card.querySelector("img[alt]");
if (img?.alt) return norm(img.alt);
const candidates = [...card.querySelectorAll("div, span")]
.map((el) => norm(el.textContent))
.filter(Boolean);
return candidates.find((t) => cfg.connector.test(t)) || "";
}
function getTitle(card) {
return norm(card.querySelector("h2")?.textContent);
}
function getSharedDataKeys(card) {
return [...card.querySelectorAll("dt")]
.map((dt) => norm(dt.textContent))
.filter(Boolean);
}
function isAllowedCard(card) {
const connector = getConnectorName(card);
const body = cardText(card);
const sharedKeys = getSharedDataKeys(card);
const blockedKey = sharedKeys.find((k) => cfg.blockedSharedDataKeys.test(k));
const result = {
connector,
title: getTitle(card),
sharedKeys,
matchedConnector: cfg.connector.test(connector),
matchedBody: cfg.allowedBody.test(body),
blockedKey,
};
console.debug("[CatDesk approval] inspected card:", result);
return result.matchedConnector && result.matchedBody && !result.blockedKey;
}
function findAllowButton(card) {
const buttonArea = card.querySelector('[data-testid="tool-action-buttons"]') || card;
return [...buttonArea.querySelectorAll("button")]
.filter(isVisible)
.find((btn) => cfg.allowButtonText.test(norm(btn.textContent)));
}
function updateFloatingStatus(extraText = "") {
if (!ui) return;
const armed = cfg.armed;
const dry = cfg.dryRun;
ui.dot.style.background = armed ? "#22c55e" : "#ef4444";
ui.label.textContent = armed ? "CatDesk armed" : "CatDesk disarmed";
ui.mode.textContent = dry ? "dry-run" : "live";
ui.extra.textContent = extraText;
ui.root.title = `CatDesk approval: ${armed ? "armed" : "disarmed"}, ${dry ? "dry-run" : "live"}`;
}
function createFloatingStatus() {
const host = document.createElement("div");
host.id = "catdesk-approval-status-host";
const shadow = host.attachShadow({ mode: "open" });
const style = document.createElement("style");
style.textContent = `
:host {
all: initial;
}
button {
position: fixed;
right: 18px;
bottom: 18px;
z-index: 2147483647;
display: flex;
align-items: center;
gap: 8px;
border: 1px solid rgba(255, 255, 255, 0.18);
border-radius: 999px;
padding: 8px 11px;
background: rgba(20, 20, 20, 0.88);
color: white;
font: 12px/1.2 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.28);
cursor: pointer;
user-select: none;
backdrop-filter: blur(8px);
}
button:hover {
background: rgba(32, 32, 32, 0.94);
}
.dot {
width: 10px;
height: 10px;
border-radius: 999px;
flex: 0 0 auto;
}
.text {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 2px;
}
.label {
font-weight: 650;
}
.sub {
display: flex;
gap: 6px;
opacity: 0.75;
}
.extra:empty {
display: none;
}
`;
const button = document.createElement("button");
button.type = "button";
button.setAttribute("aria-label", "Toggle CatDesk approval auto-allow");
const dot = document.createElement("span");
dot.className = "dot";
const text = document.createElement("span");
text.className = "text";
const label = document.createElement("span");
label.className = "label";
const sub = document.createElement("span");
sub.className = "sub";
const mode = document.createElement("span");
mode.className = "mode";
const extra = document.createElement("span");
extra.className = "extra";
sub.append(mode, extra);
text.append(label, sub);
button.append(dot, text);
shadow.append(style, button);
button.addEventListener("click", () => {
toggleArmed();
});
document.documentElement.append(host);
ui = { root: button, dot, label, mode, extra };
updateFloatingStatus();
}
function flashStatus(message) {
updateFloatingStatus(message);
setTimeout(() => updateFloatingStatus(), 1600);
}
function toggleArmed() {
cfg.armed = !cfg.armed;
console.info(`[CatDesk approval] ${cfg.armed ? "armed" : "disarmed"}; dryRun=${cfg.dryRun}`);
updateFloatingStatus();
scan();
}
function scan() {
if (!cfg.armed) return;
if (Date.now() - lastClickAt < cfg.cooldownMs) return;
const cards = [...document.querySelectorAll('[data-testid="tool-approval-card"]')]
.filter(isVisible);
for (const card of cards) {
if (!isAllowedCard(card)) continue;
const allowButton = findAllowButton(card);
if (!allowButton) continue;
console.info("[CatDesk approval] matched card:", {
title: getTitle(card),
sharedKeys: getSharedDataKeys(card),
dryRun: cfg.dryRun,
});
flashStatus(cfg.dryRun ? "matched" : "clicked");
if (!cfg.dryRun) {
lastClickAt = Date.now();
allowButton.click();
}
return;
}
}
createFloatingStatus();
new MutationObserver(scan).observe(document.documentElement, {
childList: true,
subtree: true,
characterData: true,
});
setInterval(scan, 1000);
window.addEventListener("keydown", (ev) => {
if (ev.ctrlKey && ev.altKey && ev.key.toLowerCase() === "y") {
toggleArmed();
}
});
console.info("[CatDesk approval] loaded. Click the floating pill or press Ctrl+Alt+Y to toggle.");
})();
The switch button will be at the bottom right.
(* I'm sorry that this is an discussion rather than an issue. *)
For those who are tired of granting permissions again and again, here is a workaround for you:
Install Greasemonkey (or other similar extensions), create a new script with the following content and you are good to go:
The switch button will be at the bottom right.