Skip to content

Instantly share code, notes, and snippets.

@catgarret
Created July 17, 2026 16:58
Show Gist options
  • Select an option

  • Save catgarret/93849470ec0420b5f464cd79d2f18238 to your computer and use it in GitHub Desktop.

Select an option

Save catgarret/93849470ec0420b5f464cd79d2f18238 to your computer and use it in GitHub Desktop.
Gemini, Copilot, Claude, Meta AI chat history delete script

AI Chat History Delete Script

Gemini, Copilot, Claude, Meta AI의 대화 기록을 브라우저 콘솔에서 정리하기 위한 JavaScript 스크립트입니다.

지원 서비스

  • Gemini
  • Copilot
  • Claude
  • Meta AI

사용 방법

  1. 삭제하려는 AI 서비스의 대화 목록 페이지를 엽니다.
  2. 브라우저 개발자 도구를 엽니다.
  3. Console 탭에 스크립트를 붙여넣습니다.
  4. 처음에는 적은 개수로 테스트합니다.
  5. 정상 동작을 확인한 뒤 필요한 만큼 실행합니다.

주의사항

  • 서비스의 화면 구조가 바뀌면 동작하지 않을 수 있습니다.
  • 중요한 대화까지 삭제될 수 있으니 반드시 확인 후 사용하세요.
  • 처음에는 3~5개 정도만 테스트하는 것을 권장합니다.
  • 이 스크립트는 각 서비스의 공식 기능이 아닙니다.

관련 글

사용 방법과 주의사항은 아래 글에서 자세히 확인할 수 있습니다.

Gemini·Copilot·Claude·Meta AI 대화 기록 한 번에 삭제하기

(async () => {
/**
* AI Chat History Cleaner - Gemini / Copilot / Claude / Meta AI
*
* Supported:
* - Google Gemini: gemini.google.com
* - Microsoft Copilot: copilot.microsoft.com
* - Claude: claude.ai
* - Meta AI: meta.ai
*
* Stop:
* window.__aiChatHistoryCleanerStop = true;
*/
const CONFIG = {
defaultLimit: 20,
maxLimit: 500,
delayAfterMenuOpen: 850,
delayAfterDeleteClick: 1000,
delayAfterConfirmClick: 2600,
delayAfterNoConfirmDelete: 2200,
requireFinalConfirmation: true,
enableLazyPreload: true,
preloadExtraBuffer: 10,
preloadScrollDelay: 1200,
preloadMaxRounds: 90,
preloadStableLimit: 5,
lowWatermarkRows: 5,
retryLoadMoreRounds: 12,
};
window.__aiChatHistoryCleanerStop = false;
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const unique = (array) =>
array.filter((item, index, arr) => item && arr.indexOf(item) === index);
const getText = (element) => {
return (
(
element?.innerText ||
element?.textContent ||
element?.getAttribute?.("aria-label") ||
element?.getAttribute?.("title") ||
""
) + ""
)
.replace(/\s+/g, " ")
.trim();
};
const isVisible = (element) => {
if (!element) return false;
const rect = element.getBoundingClientRect();
const style = getComputedStyle(element);
return (
rect.width > 0 &&
rect.height > 0 &&
rect.bottom > 0 &&
rect.top < window.innerHeight &&
rect.right > 0 &&
rect.left < window.innerWidth &&
style.display !== "none" &&
style.visibility !== "hidden" &&
style.opacity !== "0"
);
};
const isUsableElement = (element) => {
if (!element) return false;
const style = getComputedStyle(element);
return style.display !== "none" && style.visibility !== "hidden";
};
const getClickableElement = (element) => {
if (!element) return null;
return (
element.closest?.(
[
"button",
'[role="button"]',
'[role="menuitem"]',
'[data-slot="dropdown-menu-item"]',
'[data-testid="delete-chat-trigger"]',
".mat-mdc-menu-item",
"a",
].join(", ")
) || element
);
};
const dispatchHover = (element) => {
if (!element) return;
const rect = element.getBoundingClientRect();
const options = {
bubbles: true,
cancelable: true,
composed: true,
view: window,
clientX: rect.right - 20,
clientY: rect.top + rect.height / 2,
};
[
"pointerover",
"pointerenter",
"pointermove",
"mouseover",
"mouseenter",
"mousemove",
].forEach((eventName) => {
try {
const EventClass =
eventName.startsWith("pointer") && window.PointerEvent
? PointerEvent
: MouseEvent;
element.dispatchEvent(new EventClass(eventName, options));
} catch (_) {
try {
element.dispatchEvent(new MouseEvent(eventName, options));
} catch (_) {}
}
});
};
const clickElement = async (rawElement) => {
const element = getClickableElement(rawElement);
if (!element) return false;
element.scrollIntoView({ block: "center", inline: "center" });
await sleep(150);
try {
element.focus?.({ preventScroll: true });
} catch (_) {
try {
element.focus?.();
} catch (_) {}
}
const rect = element.getBoundingClientRect();
const options = {
bubbles: true,
cancelable: true,
composed: true,
view: window,
clientX: rect.left + rect.width / 2,
clientY: rect.top + rect.height / 2,
};
const events = [
"pointerover",
"pointerenter",
"pointermove",
"mouseover",
"mouseenter",
"mousemove",
"pointerdown",
"mousedown",
"pointerup",
"mouseup",
"click",
];
for (const eventName of events) {
try {
const EventClass =
eventName.startsWith("pointer") && window.PointerEvent
? PointerEvent
: MouseEvent;
element.dispatchEvent(new EventClass(eventName, options));
} catch (_) {
try {
element.dispatchEvent(new MouseEvent(eventName, options));
} catch (_) {}
}
}
return true;
};
const dispatchScroll = (element) => {
if (!element) return;
try {
element.dispatchEvent(new Event("scroll", { bubbles: true }));
} catch (_) {}
try {
element.dispatchEvent(
new WheelEvent("wheel", {
bubbles: true,
cancelable: true,
deltaY: 1200,
})
);
} catch (_) {
try {
element.dispatchEvent(new Event("wheel", { bubbles: true }));
} catch (_) {}
}
};
const forceScrollBottom = (element) => {
if (!element) return;
try {
element.scrollTop = element.scrollHeight;
dispatchScroll(element);
} catch (_) {}
try {
element.scrollTo({
top: element.scrollHeight,
behavior: "instant",
});
dispatchScroll(element);
} catch (_) {}
};
const forceScrollTop = (element) => {
if (!element) return;
try {
element.scrollTop = 0;
dispatchScroll(element);
} catch (_) {}
try {
element.scrollTo({
top: 0,
behavior: "instant",
});
dispatchScroll(element);
} catch (_) {}
};
const findBestScrollContainer = (root) => {
const candidates = [];
let node = root;
while (node && node !== document.documentElement) {
candidates.push(node);
node = node.parentElement;
}
if (root?.querySelectorAll) {
candidates.push(...root.querySelectorAll("*"));
}
candidates.push(
document.querySelector("aside"),
document.querySelector("nav"),
document.scrollingElement,
document.documentElement,
document.body
);
const scored = unique(candidates)
.filter(Boolean)
.map((el) => {
const rect = el.getBoundingClientRect?.() || {
left: 0,
top: 0,
width: window.innerWidth,
height: window.innerHeight,
};
const style = getComputedStyle(el);
const scrollableAmount = (el.scrollHeight || 0) - (el.clientHeight || 0);
let score = 0;
if (scrollableAmount > 40) score += 100;
if (/auto|scroll/i.test(style.overflowY)) score += 45;
if (rect.left < 700) score += 35;
if (rect.width > 160 && rect.width < 750) score += 35;
if (rect.height > 250) score += 20;
if (el.matches?.("aside, nav")) score += 20;
score += Math.min(120, scrollableAmount / 20);
return {
el,
score,
scrollableAmount,
rect,
overflowY: style.overflowY,
};
})
.filter((item) => item.scrollableAmount > 40)
.sort((a, b) => b.score - a.score);
return scored[0]?.el || document.scrollingElement || document.documentElement;
};
const getMenuRoots = () => {
return [
...document.querySelectorAll('[role="menu"]'),
...document.querySelectorAll('[data-cds="Menu"]'),
...document.querySelectorAll('[data-base-ui-focusable][role="menu"]'),
...document.querySelectorAll('[data-floating-ui-focusable][role="menu"]'),
...document.querySelectorAll('[data-radix-popper-content-wrapper]'),
...document.querySelectorAll('[data-radix-menu-content]'),
...document.querySelectorAll('[data-slot*="dropdown"]'),
...document.querySelectorAll('[data-sidebar*="menu"]'),
...document.querySelectorAll(".cdk-overlay-container"),
...document.querySelectorAll("[data-testid*='menu']"),
...document.querySelectorAll("[id^='radix-']"),
...document.querySelectorAll("[id^='_r_']"),
document.body,
].filter(Boolean);
};
const findDeleteMenuItem = async (serviceName) => {
for (let attempt = 0; attempt < 20; attempt++) {
const roots = getMenuRoots();
for (const root of roots) {
const candidates = [
...root.querySelectorAll(
[
// Claude 확정 셀렉터
'[data-testid="delete-chat-trigger"]',
// Meta 확정 계열
'[data-radix-popper-content-wrapper] [role="menuitem"][data-slot="dropdown-menu-item"]',
'[data-radix-menu-content] [role="menuitem"][data-slot="dropdown-menu-item"]',
// 범용 메뉴 항목
'button[role="menuitem"]',
'[role="menuitem"]',
'[data-slot="dropdown-menu-item"]',
'[data-radix-collection-item]',
// 텍스트/속성 기반
'button[title*="대화 삭제"]',
'button[aria-label*="대화 삭제"]',
'button[title*="채팅 삭제"]',
'button[aria-label*="채팅 삭제"]',
'button[title*="삭제"]',
'button[aria-label*="삭제"]',
'button[title*="Delete"]',
'button[aria-label*="Delete"]',
'button[title*="Remove"]',
'button[aria-label*="Remove"]',
".mat-mdc-menu-item",
"button",
"div",
"span",
].join(", ")
),
].filter(isVisible);
const scored = candidates
.map((element) => {
const clickable = getClickableElement(element);
const text = getText(element);
const clickableText = getText(clickable);
const title =
element.getAttribute?.("title") ||
clickable?.getAttribute?.("title") ||
"";
const aria =
element.getAttribute?.("aria-label") ||
clickable?.getAttribute?.("aria-label") ||
"";
const testId =
element.getAttribute?.("data-testid") ||
clickable?.getAttribute?.("data-testid") ||
"";
const slot =
element.getAttribute?.("data-slot") ||
clickable?.getAttribute?.("data-slot") ||
"";
const className =
element.getAttribute?.("class") ||
clickable?.getAttribute?.("class") ||
"";
const combined = `${text} ${clickableText} ${title} ${aria} ${testId} ${slot} ${className}`
.replace(/\s+/g, " ")
.trim();
let score = 0;
// Claude 확정
if (/delete-chat-trigger/i.test(testId)) {
score += 280;
}
// Meta 확정: Radix dropdown menu item + destructive class + 삭제
if (
serviceName === "Meta AI" &&
/dropdown-menu-item/i.test(slot) &&
/text-text-destructive/i.test(className) &&
/삭제/.test(combined)
) {
score += 300;
}
if (
serviceName === "Meta AI" &&
element.closest?.("[data-radix-popper-content-wrapper]") &&
/text-text-destructive/i.test(className) &&
/삭제/.test(combined)
) {
score += 300;
}
// 한국어 삭제 문구
if (/대화 삭제|채팅 삭제|삭제.*대화|삭제.*채팅/i.test(combined)) {
score += 160;
}
// 영어 삭제 문구
if (
/Delete conversation|Delete chat|Delete prompt|Remove conversation|Remove chat/i.test(
combined
)
) {
score += 160;
}
// 단독 삭제 텍스트
if (/^삭제$|^삭제하기$|^Delete$|^Remove$/i.test(text)) {
score += 130;
}
if (/^삭제$|^삭제하기$|^Delete$|^Remove$/i.test(clickableText)) {
score += 130;
}
if (/delete|trash|remove/i.test(combined)) {
score += 70;
}
// 위험/파괴 클래스 가산점
if (/danger|destructive|red|text-text-destructive|text-danger|group-menu-danger/i.test(className)) {
score += 80;
}
// 역할/태그 가산점
if (clickable?.matches?.('button[role="menuitem"]')) score += 50;
if (clickable?.matches?.('[role="menuitem"]')) score += 45;
if (clickable?.matches?.('[data-slot="dropdown-menu-item"]')) score += 45;
if (clickable?.matches?.(".mat-mdc-menu-item")) score += 35;
if (clickable?.tagName?.toLowerCase() === "button") score += 25;
// 삭제가 아닌 항목 감점
if (
/즐겨찾기|고정|Pin|Star|이름 변경|이름 바꾸기|Rename|초대|Invite|공유|Share|복사|Copy|요약|Summary|프로젝트|Project|보고|Report|Archive|보관|Export|내보내기|Move|이동|추가|Add/i.test(
combined
)
) {
score -= 220;
}
return {
rawElement: element,
element: clickable,
score,
text,
clickableText,
title,
aria,
testId,
slot,
className,
combined,
};
})
.filter((item) => item.element && item.score >= 110)
.sort((a, b) => b.score - a.score);
if (scored.length > 0) {
const picked = scored[0];
console.log("[AI Chat Cleaner] 삭제 메뉴 감지:", {
service: serviceName,
text: picked.text,
clickableText: picked.clickableText,
title: picked.title,
aria: picked.aria,
testId: picked.testId,
slot: picked.slot,
score: picked.score,
});
return picked.element;
}
}
await sleep(200);
}
return null;
};
const findConfirmDeleteButton = async (serviceName) => {
for (let attempt = 0; attempt < 24; attempt++) {
const roots = [
...document.querySelectorAll('[role="dialog"]'),
...document.querySelectorAll('[role="alertdialog"]'),
...document.querySelectorAll("mat-dialog-container"),
...document.querySelectorAll('[data-testid*="dialog"]'),
...document.querySelectorAll('[data-slot="dialog-content"]'),
...document.querySelectorAll('[data-floating-ui-focusable]'),
...document.querySelectorAll('[data-radix-popper-content-wrapper]'),
...document.querySelectorAll('[data-radix-dialog-content]'),
...document.querySelectorAll(".cdk-overlay-container"),
document.body,
].filter(Boolean);
for (const root of roots) {
const rootText = getText(root);
const buttons = [
...root.querySelectorAll('button, [role="button"], [data-slot="button"]'),
].filter(isVisible);
const scored = buttons
.map((button) => {
const text = getText(button);
const title = button.getAttribute?.("title") || "";
const aria = button.getAttribute?.("aria-label") || "";
const className = button.getAttribute?.("class") || "";
const combined = `${text} ${title} ${aria} ${rootText} ${className}`
.replace(/\s+/g, " ")
.trim();
let score = 0;
// Claude 확인창: "대화 삭제" + "삭제"
if (/대화 삭제/i.test(rootText) && /^삭제$/i.test(text)) {
score += 280;
}
// Meta 확인창: "채팅을 삭제할까요?" + "삭제"
if (/채팅을 삭제할까요/i.test(rootText) && /^삭제$/i.test(text)) {
score += 280;
}
// 범용 확인창
if (/^삭제$|^삭제하기$|^Delete$|^Remove$/i.test(text)) score += 150;
if (/대화 삭제|채팅 삭제|삭제.*대화|삭제.*채팅/i.test(combined)) score += 120;
if (
/Delete conversation|Delete chat|Delete prompt|Remove conversation|Remove chat/i.test(
combined
)
) {
score += 120;
}
if (/확인|Confirm|OK/i.test(text)) score += 25;
// 위험 버튼 클래스 가산점
if (
/danger|red|accent|destructive|text-on-danger|text-text-on-accent|gradient-red|from-gradient-red/i.test(
className
)
) {
score += 60;
}
// 취소/닫기 감점
if (/취소|Cancel|닫기|Close/i.test(text)) score -= 450;
if (/닫기/i.test(aria)) score -= 450;
return {
button,
score,
text,
title,
aria,
combined,
};
})
.filter((item) => item.score >= 120)
.sort((a, b) => b.score - a.score);
if (scored.length > 0) {
const picked = scored[0];
console.log("[AI Chat Cleaner] 삭제 확인 버튼 감지:", {
service: serviceName,
text: picked.text,
title: picked.title,
aria: picked.aria,
score: picked.score,
});
return picked.button;
}
}
await sleep(200);
}
return null;
};
const waitForRowCountChange = async (
cleaner,
beforeCount,
targetKey,
timeoutMs = 5000
) => {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
const rows = cleaner.getRows();
const currentCount = rows.length;
const stillExists = targetKey
? rows.some((row) => cleaner.getKey(row) === targetKey)
: false;
if (currentCount < beforeCount || !stillExists) {
return true;
}
await sleep(250);
}
return false;
};
const preloadRowsForCleaner = async (cleaner, targetCount) => {
if (!CONFIG.enableLazyPreload) return;
if (!cleaner.supportsLazyLoad) return;
const desiredCount = Math.min(
targetCount + CONFIG.preloadExtraBuffer,
CONFIG.maxLimit + CONFIG.preloadExtraBuffer
);
const scroller = cleaner.getScrollContainer?.();
if (!scroller) {
console.warn(`[AI Chat Cleaner] ${cleaner.name} 스크롤 컨테이너를 찾지 못했습니다.`);
return;
}
let previousCount = cleaner.getRows().length;
let stableRounds = 0;
console.log(`[AI Chat Cleaner] ${cleaner.name} 대화 목록 사전 로딩 시작:`, {
현재로드수: previousCount,
목표로드수: desiredCount,
scrollHeight: scroller.scrollHeight,
clientHeight: scroller.clientHeight,
});
for (let round = 1; round <= CONFIG.preloadMaxRounds; round++) {
if (window.__aiChatHistoryCleanerStop) {
console.warn("[AI Chat Cleaner] 중지 요청으로 사전 로딩을 멈췄습니다.");
break;
}
const currentCount = cleaner.getRows().length;
if (currentCount >= desiredCount) {
console.log(`[AI Chat Cleaner] ${cleaner.name} 사전 로딩 목표 도달:`, {
로드수: currentCount,
목표: desiredCount,
});
break;
}
forceScrollBottom(scroller);
await sleep(CONFIG.preloadScrollDelay);
const newCount = cleaner.getRows().length;
console.log(`[AI Chat Cleaner] ${cleaner.name} 사전 로딩 진행:`, {
round,
이전로드수: currentCount,
현재로드수: newCount,
목표로드수: desiredCount,
});
if (newCount <= previousCount) {
stableRounds += 1;
} else {
stableRounds = 0;
previousCount = newCount;
}
if (stableRounds >= CONFIG.preloadStableLimit) {
console.log(
`[AI Chat Cleaner] ${cleaner.name} 추가 로드 증가가 멈춰 사전 로딩을 종료합니다.`,
{
로드수: newCount,
stableRounds,
}
);
break;
}
}
forceScrollTop(scroller);
await sleep(700);
console.log(`[AI Chat Cleaner] ${cleaner.name} 대화 목록 사전 로딩 종료:`, {
최종로드수: cleaner.getRows().length,
});
};
const loadMoreIfNeeded = async (cleaner, remainingTarget) => {
if (!cleaner.supportsLazyLoad) return;
const rows = cleaner.getRows();
if (rows.length > CONFIG.lowWatermarkRows) return;
const scroller = cleaner.getScrollContainer?.();
if (!scroller) {
console.warn(`[AI Chat Cleaner] ${cleaner.name} 스크롤 컨테이너를 찾지 못했습니다.`);
return;
}
let previousCount = rows.length;
let stableRounds = 0;
console.log(`[AI Chat Cleaner] ${cleaner.name} 로드된 대화가 부족해 추가 로딩을 시도합니다.`, {
현재로드수: previousCount,
남은삭제목표: remainingTarget,
});
for (let round = 1; round <= CONFIG.retryLoadMoreRounds; round++) {
if (window.__aiChatHistoryCleanerStop) break;
forceScrollBottom(scroller);
await sleep(CONFIG.preloadScrollDelay);
const newCount = cleaner.getRows().length;
console.log(`[AI Chat Cleaner] ${cleaner.name} 추가 로딩 진행:`, {
round,
이전로드수: previousCount,
현재로드수: newCount,
});
if (newCount > CONFIG.lowWatermarkRows) break;
if (newCount <= previousCount) {
stableRounds += 1;
} else {
stableRounds = 0;
previousCount = newCount;
}
if (stableRounds >= 3) break;
}
forceScrollTop(scroller);
await sleep(500);
};
const GeminiCleaner = {
name: "Google Gemini",
supportsLazyLoad: true,
detect() {
return (
location.hostname.includes("gemini.google.com") ||
!!document.querySelector('[data-test-id="all-conversations"]') ||
!!document.querySelector(
'[data-test-id="conversation"] [data-test-id="actions-menu-button"]'
)
);
},
getRoot() {
return (
document.querySelector('[data-test-id="all-conversations"]') ||
document.querySelector("#sidenav-section-content-chats") ||
document.querySelector(".chat-history-list") ||
document.querySelector(".chat-history") ||
document.body
);
},
getScrollContainer() {
return findBestScrollContainer(this.getRoot());
},
forceShowActionButtons() {
if (!document.getElementById("__ai_cleaner_gemini_css")) {
const style = document.createElement("style");
style.id = "__ai_cleaner_gemini_css";
style.textContent = `
[data-test-id="conversation"] .hovered-trailing-content,
[data-test-id="conversation"] [data-test-id="actions-menu-button"],
[data-test-id="conversation"] [data-test-id="actions-menu-button"] button {
display: flex !important;
visibility: visible !important;
opacity: 1 !important;
pointer-events: auto !important;
}
[data-test-id="conversation"] .trailing-content {
opacity: 0 !important;
pointer-events: none !important;
}
`;
document.head.appendChild(style);
}
},
getRows() {
const root = this.getRoot();
return [...root.querySelectorAll('[data-test-id="conversation"]')]
.filter(isUsableElement)
.filter((row) => row.querySelector('a[href^="/app/"]'))
.sort(
(a, b) =>
a.getBoundingClientRect().top - b.getBoundingClientRect().top
);
},
getTitle(row) {
const titleElement = row.querySelector(".title-text");
const linkElement = row.querySelector("a[aria-label]");
return (
getText(titleElement) ||
linkElement?.getAttribute("aria-label") ||
getText(row) ||
"(제목 없음)"
);
},
getKey(row) {
const linkElement = row.querySelector('a[href^="/app/"]');
return linkElement?.getAttribute("href") || this.getTitle(row);
},
async getMoreButton(row) {
this.forceShowActionButtons();
row.scrollIntoView({ block: "center" });
dispatchHover(row);
await sleep(300);
return (
row.querySelector('[data-test-id="actions-menu-button"] button') ||
row.querySelector('[data-test-id="actions-menu-button"]') ||
row.querySelector('button[aria-label*="더보기"]') ||
row.querySelector('button[aria-label*="옵션"]') ||
row.querySelector('mat-icon[fonticon="more_vert"]')?.closest("button") ||
null
);
},
};
const CopilotCleaner = {
name: "Microsoft Copilot",
supportsLazyLoad: true,
detect() {
return (
location.hostname.includes("copilot.microsoft.com") ||
!!document.querySelector('[data-testid="sidebar-expanded-content"]') ||
!!document.querySelector('button[id^="conversation-options-"]')
);
},
getRoot() {
return (
document.querySelector('[data-testid="sidebar-expanded-content"]') ||
document.querySelector('[role="list"]') ||
document.body
);
},
getScrollContainer() {
return findBestScrollContainer(this.getRoot());
},
forceShowActionButtons() {
if (!document.getElementById("__ai_cleaner_copilot_css")) {
const style = document.createElement("style");
style.id = "__ai_cleaner_copilot_css";
style.textContent = `
button[id^="conversation-options-"] {
display: flex !important;
visibility: visible !important;
opacity: 1 !important;
pointer-events: auto !important;
position: static !important;
width: 24px !important;
height: 24px !important;
min-width: 24px !important;
min-height: 24px !important;
clip: auto !important;
clip-path: none !important;
overflow: visible !important;
transform: none !important;
}
span:has(> button[id^="conversation-options-"]) {
display: inline-flex !important;
visibility: visible !important;
opacity: 1 !important;
pointer-events: auto !important;
position: static !important;
width: auto !important;
height: auto !important;
clip: auto !important;
clip-path: none !important;
overflow: visible !important;
}
`;
document.head.appendChild(style);
}
document.querySelectorAll('button[id^="conversation-options-"]').forEach((button) => {
const parent = button.parentElement;
button.style.setProperty("display", "flex", "important");
button.style.setProperty("visibility", "visible", "important");
button.style.setProperty("opacity", "1", "important");
button.style.setProperty("pointer-events", "auto", "important");
button.style.setProperty("position", "static", "important");
button.style.setProperty("width", "24px", "important");
button.style.setProperty("height", "24px", "important");
button.style.setProperty("clip", "auto", "important");
button.style.setProperty("clip-path", "none", "important");
button.style.setProperty("overflow", "visible", "important");
if (parent) {
parent.classList.remove("sr-only");
parent.style.setProperty("display", "inline-flex", "important");
parent.style.setProperty("visibility", "visible", "important");
parent.style.setProperty("opacity", "1", "important");
parent.style.setProperty("pointer-events", "auto", "important");
parent.style.setProperty("position", "static", "important");
parent.style.setProperty("width", "auto", "important");
parent.style.setProperty("height", "auto", "important");
parent.style.setProperty("clip", "auto", "important");
parent.style.setProperty("clip-path", "none", "important");
parent.style.setProperty("overflow", "visible", "important");
}
});
},
getRows() {
this.forceShowActionButtons();
const root = this.getRoot();
return [...root.querySelectorAll('[role="link"][aria-label]')]
.filter(isUsableElement)
.filter((row) => row.querySelector('button[id^="conversation-options-"]'))
.filter((row) => {
const title = this.getTitle(row);
return !/새 대화|New chat|New conversation/i.test(title);
})
.sort(
(a, b) =>
a.getBoundingClientRect().top - b.getBoundingClientRect().top
);
},
getTitle(row) {
const titleElement = row.querySelector("p[title]");
return (
titleElement?.getAttribute("title") ||
row.getAttribute("aria-label") ||
getText(row) ||
"(제목 없음)"
);
},
getKey(row) {
const button = row.querySelector('button[id^="conversation-options-"]');
return button?.id || this.getTitle(row);
},
async getMoreButton(row) {
this.forceShowActionButtons();
row.scrollIntoView({ block: "center" });
dispatchHover(row);
await sleep(300);
return (
row.querySelector('button[id^="conversation-options-"]') ||
row.querySelector('button[aria-label*="보기 옵션"]') ||
row.querySelector('button[title*="보기 옵션"]') ||
row.querySelector('button[aria-label*="options" i]') ||
row.querySelector('button[title*="options" i]') ||
null
);
},
};
const ClaudeCleaner = {
name: "Claude",
supportsLazyLoad: true,
detect() {
return (
location.hostname.includes("claude.ai") ||
!!document.querySelector('a[data-dd-action-name="sidebar-chat-item"][href^="/chat/"]') ||
!!document.querySelector('button[aria-label*="더 많은 옵션"]')
);
},
getRoot() {
const firstChatLink = document.querySelector(
'a[data-dd-action-name="sidebar-chat-item"][href^="/chat/"]'
);
return (
firstChatLink?.closest("ul") ||
firstChatLink?.closest("nav") ||
firstChatLink?.closest("aside") ||
document.querySelector("nav") ||
document.querySelector("aside") ||
document.body
);
},
getScrollContainer() {
return findBestScrollContainer(this.getRoot());
},
forceShowActionButtons() {
if (!document.getElementById("__ai_cleaner_claude_css")) {
const style = document.createElement("style");
style.id = "__ai_cleaner_claude_css";
style.textContent = `
a[data-dd-action-name="sidebar-chat-item"][href^="/chat/"] ~ div button[aria-haspopup="menu"],
a[data-dd-action-name="sidebar-chat-item"][href^="/chat/"] + div button[aria-haspopup="menu"],
button[aria-label*="더 많은 옵션"],
button[aria-label*="more options" i],
button[aria-label*="options" i] {
display: inline-flex !important;
visibility: visible !important;
opacity: 1 !important;
pointer-events: auto !important;
}
div:has(> button[aria-label*="더 많은 옵션"]),
div:has(> button[aria-label*="more options" i]),
div:has(> button[aria-label*="options" i]) {
display: flex !important;
visibility: visible !important;
opacity: 1 !important;
pointer-events: auto !important;
}
`;
document.head.appendChild(style);
}
document
.querySelectorAll(
'button[aria-label*="더 많은 옵션"], button[aria-label*="more options" i], button[aria-label*="options" i]'
)
.forEach((button) => {
const parent = button.parentElement;
button.style.setProperty("display", "inline-flex", "important");
button.style.setProperty("visibility", "visible", "important");
button.style.setProperty("opacity", "1", "important");
button.style.setProperty("pointer-events", "auto", "important");
if (parent) {
parent.style.setProperty("display", "flex", "important");
parent.style.setProperty("visibility", "visible", "important");
parent.style.setProperty("opacity", "1", "important");
parent.style.setProperty("pointer-events", "auto", "important");
}
});
},
getRows() {
this.forceShowActionButtons();
const root = this.getRoot();
return [...root.querySelectorAll('a[data-dd-action-name="sidebar-chat-item"][href^="/chat/"]')]
.map((link) => link.closest("li") || link.closest("div.relative.group") || link.parentElement)
.filter(Boolean)
.filter((row, index, arr) => arr.indexOf(row) === index)
.filter(isUsableElement)
.sort(
(a, b) =>
a.getBoundingClientRect().top - b.getBoundingClientRect().top
);
},
getTitle(row) {
const link = row.querySelector('a[data-dd-action-name="sidebar-chat-item"][href^="/chat/"]');
const srOnlyTitle = row.querySelector("[data-find-omitted]");
const visibleTitle = row.querySelector('span[aria-hidden="true"], span.truncate, .truncate');
return (
getText(srOnlyTitle) ||
getText(visibleTitle) ||
getText(link) ||
link?.getAttribute("href") ||
"(제목 없음)"
);
},
getKey(row) {
const link = row.querySelector('a[data-dd-action-name="sidebar-chat-item"][href^="/chat/"]');
return link?.getAttribute("href") || this.getTitle(row);
},
async getMoreButton(row) {
this.forceShowActionButtons();
row.scrollIntoView({ block: "center" });
dispatchHover(row);
await sleep(350);
return (
row.querySelector('button[aria-label*="더 많은 옵션"]') ||
row.querySelector('button[aria-label*="more options" i]') ||
row.querySelector('button[aria-label*="options" i]') ||
row.querySelector('button[aria-haspopup="menu"]') ||
null
);
},
};
const MetaCleaner = {
name: "Meta AI",
supportsLazyLoad: true,
detect() {
return (
location.hostname.includes("meta.ai") ||
!!document.querySelector('[data-testid="conversation-item"] a[href^="/prompt/"]') ||
!!document.querySelector('[data-sidebar="menu-action"][aria-label*="옵션"]')
);
},
getRoot() {
const firstConversation = document.querySelector('[data-testid="conversation-item"]');
return (
firstConversation?.closest('[data-sidebar="menu"]') ||
firstConversation?.closest("ul") ||
firstConversation?.closest("nav") ||
firstConversation?.closest("aside") ||
document.querySelector('[data-sidebar="sidebar"]') ||
document.querySelector("nav") ||
document.querySelector("aside") ||
document.body
);
},
getScrollContainer() {
return findBestScrollContainer(this.getRoot());
},
forceShowActionButtons() {
if (!document.getElementById("__ai_cleaner_meta_css")) {
const style = document.createElement("style");
style.id = "__ai_cleaner_meta_css";
style.textContent = `
[data-testid="conversation-item"] [data-sidebar="menu-action"],
[data-testid="conversation-item"] [data-slot="dropdown-menu-trigger"],
[data-testid="conversation-item"] button[aria-label*="옵션"],
[data-testid="conversation-item"] button[aria-label*="options" i] {
display: inline-flex !important;
visibility: visible !important;
opacity: 1 !important;
pointer-events: auto !important;
}
[data-testid="conversation-item"] .contents,
[data-testid="conversation-item"] div:has(> [data-sidebar="menu-action"]) {
display: contents !important;
visibility: visible !important;
opacity: 1 !important;
pointer-events: auto !important;
}
`;
document.head.appendChild(style);
}
document
.querySelectorAll(
'[data-testid="conversation-item"] [data-sidebar="menu-action"], [data-testid="conversation-item"] [data-slot="dropdown-menu-trigger"], [data-testid="conversation-item"] button[aria-label*="옵션"], [data-testid="conversation-item"] button[aria-label*="options" i]'
)
.forEach((button) => {
button.style.setProperty("display", "inline-flex", "important");
button.style.setProperty("visibility", "visible", "important");
button.style.setProperty("opacity", "1", "important");
button.style.setProperty("pointer-events", "auto", "important");
});
},
getRows() {
this.forceShowActionButtons();
const root = this.getRoot();
return [...root.querySelectorAll('[data-testid="conversation-item"]')]
.filter(isUsableElement)
.filter((row) => row.querySelector('a[href^="/prompt/"]'))
.sort(
(a, b) =>
a.getBoundingClientRect().top - b.getBoundingClientRect().top
);
},
getTitle(row) {
const link = row.querySelector('a[href^="/prompt/"]');
const titleElement =
row.querySelector('[data-sidebar="menu-button"] .truncate') ||
row.querySelector('a[href^="/prompt/"] span.truncate') ||
row.querySelector(".truncate");
return (
getText(titleElement) ||
getText(link) ||
link?.getAttribute("href") ||
"(제목 없음)"
);
},
getKey(row) {
const link = row.querySelector('a[href^="/prompt/"]');
return link?.getAttribute("href") || this.getTitle(row);
},
async getMoreButton(row) {
this.forceShowActionButtons();
row.scrollIntoView({ block: "center" });
dispatchHover(row);
await sleep(350);
return (
row.querySelector('[data-sidebar="menu-action"]') ||
row.querySelector('[data-slot="dropdown-menu-trigger"]') ||
row.querySelector('button[aria-label*="옵션"]') ||
row.querySelector('button[aria-label*="options" i]') ||
row.querySelector('button[aria-haspopup="menu"]') ||
null
);
},
};
const cleaners = [
GeminiCleaner,
CopilotCleaner,
ClaudeCleaner,
MetaCleaner,
];
const activeCleaner = cleaners.find((cleaner) => cleaner.detect());
if (!activeCleaner) {
alert(
"지원되는 AI 채팅 서비스 페이지를 감지하지 못했습니다.\n\n" +
`현재 도메인: ${location.hostname}\n\n` +
"지원: Google Gemini, Microsoft Copilot, Claude, Meta AI"
);
return;
}
const askDeleteLimit = () => {
const input = prompt(
`[${activeCleaner.name}] 삭제할 최근 대화 개수를 입력하세요. 최대 ${CONFIG.maxLimit}개까지 실행됩니다.`,
String(CONFIG.defaultLimit)
);
const parsed = parseInt(input || "0", 10);
if (!Number.isFinite(parsed) || parsed <= 0) {
return 0;
}
return Math.min(parsed, CONFIG.maxLimit);
};
const limit = askDeleteLimit();
if (!limit) {
console.log("[AI Chat Cleaner] 실행이 취소되었습니다.");
return;
}
if (CONFIG.requireFinalConfirmation) {
const confirmed = confirm(
`[${activeCleaner.name}] 최근 대화 ${limit}개를 순차적으로 삭제합니다.\n\n` +
"이 작업은 되돌리기 어려울 수 있습니다.\n" +
"계속 진행하시겠습니까?"
);
if (!confirmed) {
console.log("[AI Chat Cleaner] 사용자가 실행을 취소했습니다.");
return;
}
}
activeCleaner.forceShowActionButtons();
console.log(`[AI Chat Cleaner] 감지된 서비스: ${activeCleaner.name}`);
if (activeCleaner.supportsLazyLoad) {
await preloadRowsForCleaner(activeCleaner, limit);
}
let deletedCount = 0;
const deletedKeys = new Set();
for (let index = 0; index < limit; index++) {
if (window.__aiChatHistoryCleanerStop) {
console.warn("[AI Chat Cleaner] 중지 요청으로 작업을 멈췄습니다.");
break;
}
activeCleaner.forceShowActionButtons();
await loadMoreIfNeeded(activeCleaner, limit - deletedCount);
let rowsBefore = activeCleaner
.getRows()
.filter((row) => !deletedKeys.has(activeCleaner.getKey(row)));
if (rowsBefore.length === 0 && activeCleaner.supportsLazyLoad) {
console.warn(
`[AI Chat Cleaner] 로드된 대화가 없어 ${activeCleaner.name} 추가 로딩을 한 번 더 시도합니다.`
);
await preloadRowsForCleaner(activeCleaner, limit - deletedCount);
rowsBefore = activeCleaner
.getRows()
.filter((row) => !deletedKeys.has(activeCleaner.getKey(row)));
}
const beforeCount = rowsBefore.length;
if (rowsBefore.length === 0) {
console.warn("[AI Chat Cleaner] 삭제 가능한 대화 행을 찾지 못했습니다.");
break;
}
const targetRow = rowsBefore[0];
const targetTitle = activeCleaner.getTitle(targetRow);
const targetKey = activeCleaner.getKey(targetRow);
console.log(
`[AI Chat Cleaner] ${deletedCount + 1}/${limit} 삭제 대상: ${targetTitle}`
);
const moreButton = await activeCleaner.getMoreButton(targetRow);
if (!moreButton) {
console.warn(
`[AI Chat Cleaner] 옵션 버튼을 찾지 못했습니다: ${targetTitle}`
);
break;
}
await clickElement(moreButton);
await sleep(CONFIG.delayAfterMenuOpen);
const deleteMenuItem = await findDeleteMenuItem(activeCleaner.name);
if (!deleteMenuItem) {
console.warn(
`[AI Chat Cleaner] 삭제 메뉴를 찾지 못했습니다: ${targetTitle}`
);
const visibleMenuItems = [
...document.querySelectorAll(
[
'[role="menu"] button',
'[role="menu"] [role="menuitem"]',
'[data-cds="Menu"] [role="menuitem"]',
'[data-radix-popper-content-wrapper] button',
'[data-radix-popper-content-wrapper] [role="menuitem"]',
'[data-radix-popper-content-wrapper] [data-slot="dropdown-menu-item"]',
'[data-radix-menu-content] [data-slot="dropdown-menu-item"]',
'[data-slot*="dropdown"] button',
'[data-slot*="dropdown"] [role="menuitem"]',
'[data-testid="delete-chat-trigger"]',
].join(", ")
),
]
.filter(isVisible)
.map((el) => ({
text: getText(el),
title: el.getAttribute?.("title"),
aria: el.getAttribute?.("aria-label"),
testId: el.getAttribute?.("data-testid"),
slot: el.getAttribute?.("data-slot"),
className: el.getAttribute?.("class"),
}));
console.table(visibleMenuItems);
break;
}
await clickElement(deleteMenuItem);
await sleep(CONFIG.delayAfterDeleteClick);
const confirmButton = await findConfirmDeleteButton(activeCleaner.name);
if (confirmButton) {
await clickElement(confirmButton);
await sleep(CONFIG.delayAfterConfirmClick);
deletedCount += 1;
deletedKeys.add(targetKey);
console.log(`[AI Chat Cleaner] 삭제 완료: ${deletedCount}개`);
continue;
}
const changed = await waitForRowCountChange(
activeCleaner,
beforeCount,
targetKey,
CONFIG.delayAfterNoConfirmDelete + 3000
);
if (changed) {
deletedCount += 1;
deletedKeys.add(targetKey);
console.log(`[AI Chat Cleaner] 삭제 완료: ${deletedCount}개`);
await sleep(CONFIG.delayAfterNoConfirmDelete);
continue;
}
console.warn(
`[AI Chat Cleaner] 삭제 확인 버튼을 찾지 못했고, 목록 변화도 확인되지 않았습니다: ${targetTitle}`
);
break;
}
alert(`[${activeCleaner.name}] 최근 대화 삭제 작업 완료: ${deletedCount}개`);
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment