AI 서비스를 자주 쓰다 보면 대화 목록이 생각보다 빠르게 쌓입니다.
시간이 지나면 대부분 다시 열어보지 않는 테스트 대화, 임시 프롬프트, 작업 중간에 만든 초안, 실패한 질문들로 남습니다. 특히 Gemini나 Copilot처럼 왼쪽 사이드바에 최근 대화가 계속 쌓이는 서비스는 한 번 정리하려고 하면 생각보다 손이 많이 갑니다.
물론 각 서비스에서 대화를 하나씩 삭제할 수는 있습니다. 문제는 “하나씩”이라는 점입니다.
대화 하나를 선택하고, 더보기 버튼을 누르고, 삭제를 누르고, 확인 버튼을 누른 뒤, 다시 다음 대화로 이동하는 과정을 반복해야 합니다. 몇 개 정도면 괜찮지만, 수십 개가 넘어가면 귀찮음이 먼저 옵니다.
ChatGPT와 DeepSeek는 설정에서 모든 채팅을 일괄 삭제할 수 있지만, Gemini와 Copilot은 개인 계정과 Google Workspace 계정 모두 사용자가 대화를 일일이 삭제해야 합니다.
맨 처음에는 ChatGPT Atlas로 해볼까 했지만, 보안 정책 때문인지 제대로 동작하지 않았습니다. 그래서 바이브 코딩으로 Gemini, Copilot, Claude, Meta AI의 최근 대화 목록을 순차적으로 삭제하는 스크립트를 만들어보았습니다.
거창한 프로그램은 아니고, 브라우저 콘솔에서 실행하는 JavaScript 스크립트입니다.
사용 방법
각 AI 서비스에 접속한 뒤, 브라우저 개발자 도구의 콘솔에 스크립트를 붙여넣고 실행하면 됩니다. 스크립트가 현재 접속한 서비스가 Gemini, Copilot, Claude, Meta AI 중 어디인지 감지하고, 삭제할 최근 대화 개수를 입력받은 뒤 순차적으로 삭제를 진행합니다.
삭제 작업은 되돌리기 어려울 수 있기 때문에 바로 실행되지 않도록 했습니다. 먼저 삭제할 개수를 입력받고, 다시 한 번 확인창을 띄운 뒤 작업을 시작합니다. 실행 중 문제가 생기거나 멈추고 싶을 때는 콘솔에서 중지 플래그를 입력해 작업을 중단할 수도 있습니다.
핵심은 “삭제 버튼을 직접 찾아 반복 클릭하는 자동화”입니다. 공식 API를 사용하는 방식은 아니고, 사용자가 화면에서 직접 하는 삭제 과정을 스크립트가 대신 수행하는 구조입니다.






실행 코드
(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}개`);
})();이 스크립트는 각 서비스의 현재 화면 구조를 기준으로 동작합니다. 따라서 Gemini, Copilot, Claude, Meta AI의 UI나 버튼 구조가 바뀌면 정상적으로 동작하지 않을 수 있습니다.
처음 사용할 때는 많은 개수를 한 번에 실행하기보다, 3개에서 5개 정도로 먼저 테스트해보는 것을 권장합니다.
또한 삭제 대상에는 중요하다고 생각하는 대화까지 포함될 수 있습니다. 실행 전 대화 목록을 한 번 확인하고, 필요한 기록이 있다면 별도로 저장한 뒤 사용하는 것이 좋습니다.


