Automation on a logged-in account.


Human-assisted YouTube engagement — fast, safe, and in your control Engage smarter on YouTube. YouTube Assist helps you scan channels, queue videos, find relevant comments by keyword or tag, and insert replies using templates — all in safe assist mode. No auto-posting, no bots, just faster human engagement.

I had a question for A.I. and lead me to building a tool. You can purchase it for 9.99, or build it yourself by following this post instructions.

If I am logged into youtube accout, why cant I put a link like https://www.youtube.com/@cracked and jave a browser extension, or webpage scan the page , click each link and add comment that auto filled in a panel , semi automated in a sense

Because once you try to “scan a channel page → open each video → post a prefilled comment,” you’re no longer doing normal browsing — you’re doing automation on a logged-in account, and YouTube (and browsers) put a lot of friction in the way on purpose.

Here are the big blockers:

1) YouTube actively detects and throttles automated commenting

Even if you’re logged in, bulk/comment automation looks like spam behavior. YouTube has anti-abuse systems that may:

  • require extra verification (captcha / “are you a bot?”)
  • shadow-hide comments
  • rate limit or temporarily block commenting
  • flag the account for spam-like activity
2) A normal webpage cannot “click around” other pages while logged in

A regular website you visit cannot:

  • read the DOM of youtube.com pages (cross-origin security)
  • access your logged-in YouTube session cookies
  • click buttons on other sites

That’s by design: otherwise any random site could hijack your logged-in accounts.

3) A browser extension can interact with YouTube — but it has strict constraints

Extensions can inject scripts into pages you’re visiting, but:

  • it usually only works on the active tab (unless you build background scripting + tab control)
  • YouTube uses a dynamic app shell (SPA), so scraping/element selectors break often
  • extension stores still reject “spammy automation” behavior, and YouTube can still detect it
4) “Auto-commenting” often violates platform rules

Even “semi automated” can cross the line if it’s doing repetitive posting at scale. If it’s mass engagement, it’s risky.


Safer ways to get most of what you want (without tripping alarms)
Option A: “Assist mode” (recommended)

Build an extension that:

  • collects video links from a channel page
  • shows them in a side panel
  • lets you click Open next video
  • pre-fills a comment box template for you to submit manually

This keeps you in control, looks like normal user behavior, and is far less likely to trigger spam systems.

Option B: Use the official YouTube Data API (best for compliance)

If you own the channel / have API access:

  • you can fetch uploads via API
  • you can manage comments via API within allowed quotas
  • you’ll use OAuth, not cookie hacking

But even with the API, mass commenting can still be considered spam depending on behavior.

Option C: Template + clipboard workflow

Fast and simple:

  • extension shows a “Copy comment” button with variable placeholders
  • you paste + submit manually

If you tell me which you mean:

  1. Commenting on your own videos (like pinning a standard message), or
  2. Commenting on someone else’s channel/videos for marketing,

…I’ll point you to the safest architecture for that goal (assist-mode vs API), and I can draft a clean extension UI plan (panel, queue, “next link”, comment templates, delay timers, duplicate detection).

option A and Commenting on someone else’s channel/videos for marketing,

Got it — Option A “assist mode” is the right lane for this, because it can speed you up without turning into “bot commenting” (which is where accounts get throttled/flagged).

Here’s a clean, practical way to build it so it’s useful for marketing but still “human-in-the-loop”:

What the extension should do (and NOT do)

✅ Do

  • Extract video links from a channel’s pages (Videos tab, Shorts tab).
  • Build a “queue” in a side panel (with title + thumbnail + timestamp).
  • Let you pick a comment template and auto-generate a comment (with variables).
  • Add a one-click paste into the YouTube comment box.
  • Track what you already processed (skip duplicates).
  • Add “cooldown reminders” so you don’t rapid-fire.

❌ Don’t

  • Auto-submit comments.
  • Post the same comment repeatedly across many videos.
  • Try to bypass rate limits, captchas, or moderation.
  • Run in the background commenting on tabs you aren’t actively viewing.

That’s the difference between “assistant tool” and “spam bot.”


The workflow you want (fast + safe)
  1. You open youtube.com/@cracked/videos
  2. You click “Scan Page” in the extension side panel
  3. The panel shows a queue: 50/100 video links found
  4. You click Open Next
  5. On the video page, the panel shows:
    • video title / channel name
    • comment template dropdown
    • generated comment box (editable)
    • buttons: Insert, Copy, Mark Done, Skip
  6. You press Insert, it fills the comment field
  7. You manually hit Comment (you’re the one submitting)

This stays “normal user behavior,” but still saves you tons of time.


Comment templates that won’t get you flagged

If you do marketing, the #1 anti-spam strategy is variation + relevance.

Template idea (variable + personalized)
  • Pull from the page:
    • video title
    • first 1–2 hashtags (if visible)
    • channel name

Example generator:

  • “That part about {keyword from title} was wild — especially at {timestamp}. Quick question: {question}

Then your templates include:

  • a question
  • a genuine observation
  • optional soft mention of your thing without links, or very occasional link

Hard truth: dropping links in every comment is a fast way to get shadowed.


Extension architecture (simple & reliable)
Manifest (MV3)
  • permissions: storage, tabs, scripting, activeTab
  • host_permissions: https://www.youtube.com/*
  • content_scripts: runs on YouTube pages to:
    • scrape links on channel pages
    • insert comments on watch pages
  • sidePanel (Chrome) or popup fallback:
    • Side panel is ideal for this use-case
Content script responsibilities

On channel pages:

  • detect video grid items
  • extract URLs (/watch?v=... and /shorts/...)
  • collect titles if available
  • send results to background/service worker

On watch pages:

  • find the comment box reliably
  • wait for YouTube to render it (SPA)
  • insert text safely (dispatch input events)
Storage
  • chrome.storage.local:
    • queue list
    • “done” set keyed by videoId
    • template library
    • per-channel notes

Anti-break tactics (YouTube changes a lot)
  • Don’t rely on one brittle selector.
  • Use a “waitForElement” helper with retries + MutationObserver.
  • Detect SPA navigation (yt-navigate-finish event) and re-run logic.

Guardrails that protect your account

Add these features into the panel:

  • Cooldown timer (like “Wait 45–90s before next comment”)
  • Duplicate detector (warn if comment is too similar to last 10)
  • Daily cap counter (“You did 18 today”)
  • Link usage limiter (e.g., “only allow link every 10 comments”)

These aren’t just “nice.” They’re what keeps you from getting cooked.


If you want, I can generate the actual code (MV3 + side panel)

I can write you a working starter extension with:

  • Side panel UI (queue + templates + notes)
  • Scan channel page → build queue
  • Open next video button
  • Insert comment button (fills comment field, doesn’t submit)
  • Storage for done/skip/templates

Just tell me: Chrome only, or Chrome + Edge (same code basically).

can i target a replay from a certain tag i may see , add that option, because I normally scan the comments and reply to certain tags, often the same comment may need to go to three people they made similar comments

Yes — in assist mode you can absolutely add a “targeted reply” feature where the panel helps you find comments matching certain tags/keywords and then lets you reply to selected people (with the same reply or slightly varied replies), while you still click Reply/Submit yourself.

Here’s how I’d design it.

Feature: Target replies by tag/keyword
What “tag” can mean (and how to target it)

On YouTube comments, the most common “tags” you’ll want to target are:

  1. Mentions: @username (people tagging others, or you tagging them)
  2. Keywords/phrases: “where can I…”, “link?”, “what mic?”, “how do you…”
  3. Hashtags: #something (less common in comments but possible)
  4. Sentiment / intent (simple heuristics): question marks, “anyone know”, “help”, “what is”, etc.
Panel controls (Reply Finder)

Add a “Reply Finder” section in the side panel:

  • Match type:
    • Contains keyword(s)
    • Matches any of these words
    • Matches all of these words
    • Regex (advanced)
  • Search scope:
    • Top-level comments only
    • Replies included too
  • Quick toggles:
    • Only comments with ?
    • Only comments containing @
    • Only comments with ≥ N likes (optional)
  • Results list:
    • Checkbox per matched comment
    • Shows: author name, snippet, likes, “Reply” button
Feature: One reply to multiple people

This is perfect for your use case (“same reply may need to go to three people”).

Safer approach (recommended)
  • You select 3 matched comments in the panel
  • The panel generates 3 slightly varied replies from one template (to reduce repetition)
  • You click Reply → Insert for each one
  • You manually click Send/Reply each time
Template example (with light variation)

Template:

“@{author} good point about {topic}. My take: {shortAnswer}. What part are you stuck on?”

Variation knobs:

  • rotate openers: “Good point / You’re right / I noticed that too”
  • rotate closers: “What do you think? / Curious your thoughts / Hope that helps”

This avoids copy-paste identical replies that can get devalued.

How the extension finds the “right” reply box

On a matched comment, the content script can:

  1. Scroll that comment into view
  2. Click the comment’s Reply button (or you do it)
  3. Wait for the reply editor to appear
  4. Insert your text into the editor (dispatch input events)
  5. Stop there (no auto-submit)
Practical limitations you should expect
  • YouTube loads comments dynamically. The extension needs a “Load more comments” helper (or you scroll manually).
  • If you try to reply to a lot of people fast, YouTube may rate-limit you anyway.
  • Repeating the same promotional pitch to many users (even as replies) can still get filtered. Variation + relevance matters.
Suggested UI layout for your panel

Tab 1: Queue

  • Scan channel / videos list
  • Open next video

Tab 2: Reply Finder

  • Filters (keywords, @mentions, questions)
  • Results list + checkboxes
  • Reply composer (template dropdown + editable text)
  • Buttons:
    • Insert reply to selected (one-by-one)
    • Insert next
    • Mark replied / Skip

Tab 3: Templates

  • Reply templates
  • Variation settings
  • “No-link mode” toggle (recommended most of the time)

If you want, I can write the full MV3 extension starter that includes this “Reply Finder” flow (scan comments → filter → select → insert reply). It’ll work on the page you’re currently viewing and won’t auto-submit.

yes full mv3

Thought for 11s

Below is a full working Chrome MV3 extension (assist-mode) that:

  • ✅ Scans a channel “Videos”/“Shorts” page and builds a queue
  • ✅ Opens Next video from the queue
  • ✅ On a video page, lets you Insert a prepared top-level comment (you still click Comment)
  • ✅ On a video page, scans comments for keywords/@mentions/? and lists matches
  • ✅ Lets you select multiple matched comments and open Reply + insert reply text one-by-one (you still click Reply/Send)
  • ✅ Saves queue + “done” + templates in chrome.storage.local

It does not auto-submit anything.


1) Folder tree

Create a folder like: yt-assist-mv3/

yt-assist-mv3/
  manifest.json
  service_worker.js
  content_script.js
  sidepanel.html
  sidepanel.js
  sidepanel.css

2) manifest.json

{
  "name": "YT Assist (Queue + Reply Finder) - MV3",
  "description": "Assist-mode helper for scanning YouTube pages, queueing videos, and inserting comments/replies (no auto-submit).",
  "version": "1.0.0",
  "manifest_version": 3,

  "permissions": ["storage", "tabs", "scripting", "activeTab"],
  "host_permissions": ["https://www.youtube.com/*"],

  "background": {
    "service_worker": "service_worker.js"
  },

  "content_scripts": [
    {
      "matches": ["https://www.youtube.com/*"],
      "js": ["content_script.js"],
      "run_at": "document_idle"
    }
  ],

  "side_panel": {
    "default_path": "sidepanel.html"
  },

  "action": {
    "default_title": "YT Assist"
  }
}

3) service_worker.js
const STORAGE_KEYS = {
  queue: "ytassist_queue",
  done: "ytassist_done",
  templates: "ytassist_templates",
  settings: "ytassist_settings"
};

const DEFAULT_TEMPLATES = {
  comments: [
    "This part about “{title}” was crazy — especially around {timestamp}. Quick question: {question}",
    "Solid video, {channel}. The point on “{title}” hit. What made you start doing this style?"
  ],
  replies: [
    "@{author} Good point. I noticed that too — especially in “{title}”. What’s your take on {question}?",
    "@{author} That’s fair. If you mean {topic}, I’d say: {shortAnswer}. Curious what you think."
  ]
};

const DEFAULT_SETTINGS = {
  cooldownSeconds: 60,
  linkEveryN: 10
};

async function getStore(keys) {
  return await chrome.storage.local.get(keys);
}

async function setStore(obj) {
  return await chrome.storage.local.set(obj);
}

async function ensureDefaults() {
  const st = await getStore([STORAGE_KEYS.templates, STORAGE_KEYS.settings, STORAGE_KEYS.queue, STORAGE_KEYS.done]);
  const out = {};
  if (!st[STORAGE_KEYS.templates]) out[STORAGE_KEYS.templates] = DEFAULT_TEMPLATES;
  if (!st[STORAGE_KEYS.settings]) out[STORAGE_KEYS.settings] = DEFAULT_SETTINGS;
  if (!st[STORAGE_KEYS.queue]) out[STORAGE_KEYS.queue] = [];
  if (!st[STORAGE_KEYS.done]) out[STORAGE_KEYS.done] = {};
  if (Object.keys(out).length) await setStore(out);
}

chrome.runtime.onInstalled.addListener(() => {
  ensureDefaults();
});

chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
  (async () => {
    await ensureDefaults();

    if (msg?.type === "STORE_QUEUE_APPEND") {
      const { items = [] } = msg;
      const st = await getStore([STORAGE_KEYS.queue]);
      const q = st[STORAGE_KEYS.queue] || [];

      // De-dupe by url
      const have = new Set(q.map(x => x.url));
      const merged = [...q];
      for (const it of items) {
        if (!it?.url) continue;
        if (!have.has(it.url)) {
          have.add(it.url);
          merged.push(it);
        }
      }
      await setStore({ [STORAGE_KEYS.queue]: merged });
      sendResponse({ ok: true, count: merged.length });
      return;
    }

    if (msg?.type === "GET_STATE") {
      const st = await getStore([STORAGE_KEYS.queue, STORAGE_KEYS.done, STORAGE_KEYS.templates, STORAGE_KEYS.settings]);
      sendResponse({ ok: true, state: st });
      return;
    }

    if (msg?.type === "SET_STATE") {
      const { patch = {} } = msg;
      await setStore(patch);
      sendResponse({ ok: true });
      return;
    }

    if (msg?.type === "QUEUE_POP_NEXT") {
      const st = await getStore([STORAGE_KEYS.queue, STORAGE_KEYS.done]);
      const q = st[STORAGE_KEYS.queue] || [];
      const done = st[STORAGE_KEYS.done] || {};

      // find first not done
      let idx = q.findIndex(x => x?.url && !done[x.url]);
      if (idx === -1) idx = 0;

      const next = q[idx] || null;
      sendResponse({ ok: true, next });
      return;
    }

    if (msg?.type === "MARK_DONE") {
      const { url } = msg;
      if (!url) return sendResponse({ ok: false, error: "Missing url" });

      const st = await getStore([STORAGE_KEYS.done]);
      const done = st[STORAGE_KEYS.done] || {};
      done[url] = { at: Date.now() };
      await setStore({ [STORAGE_KEYS.done]: done });
      sendResponse({ ok: true });
      return;
    }

    sendResponse({ ok: false, error: "Unknown message type" });
  })();

  return true; // async
});

4) sidepanel.html
<!doctype html>
<html>
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width,initial-scale=1" />
  <title>YT Assist</title>
  <link rel="stylesheet" href="sidepanel.css" />
</head>
<body>
  <header class="top">
    <div class="brand">
      <div class="dot"></div>
      <div>
        <div class="title">YT Assist</div>
        <div class="sub">Queue + Reply Finder (no auto-submit)</div>
      </div>
    </div>
  </header>

  <nav class="tabs">
    <button class="tab active" data-tab="queue">Queue</button>
    <button class="tab" data-tab="reply">Reply Finder</button>
    <button class="tab" data-tab="templates">Templates</button>
  </nav>

  <main>
    <!-- QUEUE -->
    <section class="panel active" id="panel-queue">
      <div class="row">
        <button id="btnScanVideos" class="btn">Scan this page for videos</button>
        <button id="btnRefreshState" class="btn ghost">Refresh</button>
      </div>

      <div class="card">
        <div class="row">
          <button id="btnOpenNext" class="btn">Open Next</button>
          <button id="btnMarkDone" class="btn ghost">Mark Current Done</button>
        </div>

        <div class="row">
          <label>Cooldown:</label>
          <input id="cooldown" type="number" min="10" step="5" class="input small" />
          <span class="muted">sec</span>
          <button id="btnStartCooldown" class="btn ghost">Start</button>
        </div>

        <div id="cooldownStatus" class="muted small"></div>
      </div>

      <div class="card">
        <div class="row between">
          <div>
            <div class="h">Queue</div>
            <div class="muted small" id="queueMeta"></div>
          </div>
          <button id="btnClearDone" class="btn ghost danger">Clear Done</button>
        </div>
        <div id="queueList" class="list"></div>
      </div>

      <div class="card">
        <div class="h">Top-level comment helper</div>
        <textarea id="commentBox" class="textarea" placeholder="Write / generate a comment here (you will still press Comment)."></textarea>
        <div class="row">
          <button id="btnInsertComment" class="btn">Insert into YouTube comment box</button>
          <button id="btnCopyComment" class="btn ghost">Copy</button>
        </div>
      </div>
    </section>

    <!-- REPLY FINDER -->
    <section class="panel" id="panel-reply">
      <div class="card">
        <div class="h">Find comments to reply to (current video page)</div>

        <div class="grid2">
          <div>
            <label class="lbl">Keywords (comma separated)</label>
            <input id="rfKeywords" class="input" placeholder="e.g. link, where, how, mic, @, #"
            />
          </div>
          <div>
            <label class="lbl">Match mode</label>
            <select id="rfMode" class="input">
              <option value="any">Any keyword</option>
              <option value="all">All keywords</option>
              <option value="contains">Contains phrase (exact)</option>
            </select>
          </div>
        </div>

        <div class="row wrap">
          <label class="chk"><input id="rfQOnly" type="checkbox" /> Only “?” questions</label>
          <label class="chk"><input id="rfMentions" type="checkbox" /> Contains “@”</label>
          <label class="chk"><input id="rfHashtags" type="checkbox" /> Contains “#”</label>
        </div>

        <div class="row">
          <button id="btnScanComments" class="btn">Scan comments</button>
          <button id="btnSelectAll" class="btn ghost">Select all</button>
          <button id="btnSelectNone" class="btn ghost">Select none</button>
        </div>

        <div class="muted small" id="rfMeta"></div>
      </div>

      <div class="card">
        <div class="h">Reply composer</div>
        <textarea id="replyBox" class="textarea" placeholder="Write your reply. Use @name manually if you want."></textarea>
        <div class="row">
          <button id="btnInsertReplyNext" class="btn">Reply + Insert (next selected)</button>
          <button id="btnCopyReply" class="btn ghost">Copy</button>
        </div>
        <div class="muted small">
          Tip: the tool will open the Reply box and insert text. You still click “Reply”.
        </div>
      </div>

      <div class="card">
        <div class="h">Matches</div>
        <div id="matchList" class="list"></div>
      </div>
    </section>

    <!-- TEMPLATES -->
    <section class="panel" id="panel-templates">
      <div class="card">
        <div class="h">Templates</div>
        <div class="muted small">Saved locally in your browser (chrome.storage.local).</div>
      </div>

      <div class="card">
        <div class="h">Comment templates (one per line)</div>
        <textarea id="tplComments" class="textarea"></textarea>
      </div>

      <div class="card">
        <div class="h">Reply templates (one per line)</div>
        <textarea id="tplReplies" class="textarea"></textarea>
      </div>

      <div class="row">
        <button id="btnSaveTemplates" class="btn">Save Templates</button>
        <button id="btnLoadTemplates" class="btn ghost">Reload</button>
      </div>

      <div class="card">
        <div class="h">Template variables</div>
        <div class="muted small">
          {title}, {channel}, {timestamp}, {question}, {topic}, {shortAnswer}, {author}
        </div>
      </div>
    </section>
  </main>

  <script src="sidepanel.js"></script>
</body>
</html>

5) sidepanel.css
:root{
  --bg:#0b0c10;
  --card:#11131a;
  --text:#eaeef7;
  --muted:#9aa3b2;
  --line:#23273a;
  --accent:#7c5cff;
  --danger:#ff5c7c;
}

*{box-sizing:border-box}
body{
  margin:0;
  font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Arial;
  background:var(--bg);
  color:var(--text);
}
.top{
  padding:12px 12px 8px;
  border-bottom:1px solid var(--line);
}
.brand{
  display:flex;
  gap:10px;
  align-items:center;
}
.dot{
  width:12px;height:12px;border-radius:99px;
  background:var(--accent);
  box-shadow:0 0 18px rgba(124,92,255,.65);
}
.title{font-weight:700}
.sub{font-size:12px;color:var(--muted)}

.tabs{
  display:flex;
  gap:6px;
  padding:8px 10px;
  border-bottom:1px solid var(--line);
}
.tab{
  border:1px solid var(--line);
  background:transparent;
  color:var(--text);
  padding:8px 10px;
  border-radius:12px;
  cursor:pointer;
}
.tab.active{
  background:rgba(124,92,255,.15);
  border-color:rgba(124,92,255,.45);
}

main{padding:10px}
.panel{display:none}
.panel.active{display:block}

.card{
  background:var(--card);
  border:1px solid var(--line);
  border-radius:16px;
  padding:10px;
  margin-bottom:10px;
}
.h{font-weight:700;margin-bottom:6px}
.small{font-size:12px}
.muted{color:var(--muted)}

.row{display:flex; gap:8px; align-items:center; margin:8px 0}
.row.between{justify-content:space-between}
.row.wrap{flex-wrap:wrap}
.grid2{display:grid; grid-template-columns:1fr 1fr; gap:10px}

.btn{
  background:var(--accent);
  color:#fff;
  border:none;
  padding:10px 12px;
  border-radius:12px;
  cursor:pointer;
  font-weight:600;
}
.btn.ghost{
  background:transparent;
  border:1px solid var(--line);
}
.btn.danger{
  border-color:rgba(255,92,124,.5);
  color:var(--danger);
}
.input, select.input{
  width:100%;
  background:#0e1017;
  border:1px solid var(--line);
  color:var(--text);
  padding:10px;
  border-radius:12px;
}
.input.small{width:80px}
.lbl{display:block;font-size:12px;color:var(--muted);margin-bottom:6px}
.textarea{
  width:100%;
  min-height:90px;
  background:#0e1017;
  border:1px solid var(--line);
  color:var(--text);
  padding:10px;
  border-radius:12px;
  resize:vertical;
}

.list{display:flex; flex-direction:column; gap:8px}
.item{
  border:1px solid var(--line);
  border-radius:14px;
  padding:8px;
  background:#0e1017;
}
.item .t{font-weight:650}
.item .s{color:var(--muted); font-size:12px; margin-top:4px}
.item .actions{display:flex; gap:8px; margin-top:8px; flex-wrap:wrap}
.badge{
  font-size:11px;
  padding:2px 8px;
  border:1px solid var(--line);
  border-radius:999px;
  color:var(--muted);
}
.chk{font-size:13px;color:var(--text)}

6) sidepanel.js
const K = {
  queue: "ytassist_queue",
  done: "ytassist_done",
  templates: "ytassist_templates",
  settings: "ytassist_settings"
};

const $ = (sel) => document.querySelector(sel);
const $$ = (sel) => Array.from(document.querySelectorAll(sel));

function sleep(ms){ return new Promise(r=>setTimeout(r,ms)); }

async function getActiveTab() {
  const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
  return tab;
}

async function sendToActiveTab(payload) {
  const tab = await getActiveTab();
  if (!tab?.id) return { ok:false, error:"No active tab" };
  return await chrome.tabs.sendMessage(tab.id, payload);
}

async function bg(msg) {
  return await chrome.runtime.sendMessage(msg);
}

function setTab(name){
  $$(".tab").forEach(b => b.classList.toggle("active", b.dataset.tab === name));
  $$(".panel").forEach(p => p.classList.toggle("active", p.id === `panel-${name}`));
}

$$(".tab").forEach(b => b.addEventListener("click", () => setTab(b.dataset.tab)));

let cooldownTimer = null;
let cooldownRemaining = 0;

function startCooldown(seconds){
  clearInterval(cooldownTimer);
  cooldownRemaining = seconds;
  $("#cooldownStatus").textContent = `Cooldown: ${cooldownRemaining}s`;
  cooldownTimer = setInterval(() => {
    cooldownRemaining--;
    if (cooldownRemaining <= 0) {
      clearInterval(cooldownTimer);
      $("#cooldownStatus").textContent = "Cooldown finished.";
      return;
    }
    $("#cooldownStatus").textContent = `Cooldown: ${cooldownRemaining}s`;
  }, 1000);
}

function escapeHtml(s=""){
  return s.replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
}

function renderQueue(queue, done){
  const total = queue.length;
  const doneCount = queue.filter(x => done?.[x.url]).length;
  $("#queueMeta").textContent = `${total} items • ${doneCount} done`;

  const wrap = $("#queueList");
  wrap.innerHTML = "";

  queue.slice(0, 200).forEach((it, idx) => {
    const isDone = !!done?.[it.url];
    const el = document.createElement("div");
    el.className = "item";
    el.innerHTML = `
      <div class="t">${escapeHtml(it.title || it.url)}</div>
      <div class="s">${escapeHtml(it.url)}</div>
      <div class="actions">
        <span class="badge">${isDone ? "DONE" : "PENDING"}</span>
        <button class="btn ghost" data-open="${escapeHtml(it.url)}">Open</button>
        <button class="btn ghost" data-done="${escapeHtml(it.url)}">Mark done</button>
      </div>
    `;
    wrap.appendChild(el);
  });

  wrap.querySelectorAll("[data-open]").forEach(btn => btn.addEventListener("click", async () => {
    const url = btn.getAttribute("data-open");
    const tab = await getActiveTab();
    await chrome.tabs.update(tab.id, { url });
  }));

  wrap.querySelectorAll("[data-done]").forEach(btn => btn.addEventListener("click", async () => {
    const url = btn.getAttribute("data-done");
    await bg({ type:"MARK_DONE", url });
    await refresh();
  }));
}

function renderMatches(matches){
  $("#rfMeta").textContent = `${matches.length} matches found (select some, then Reply + Insert).`;

  const wrap = $("#matchList");
  wrap.innerHTML = "";

  matches.forEach((m) => {
    const el = document.createElement("div");
    el.className = "item";
    el.innerHTML = `
      <label class="chk">
        <input type="checkbox" class="mchk" data-id="${escapeHtml(m.assistId)}" checked />
        <span class="t">${escapeHtml(m.author || "Unknown")}</span>
      </label>
      <div class="s">${escapeHtml(m.text || "")}</div>
      <div class="actions">
        <button class="btn ghost" data-scroll="${escapeHtml(m.assistId)}">Scroll</button>
      </div>
    `;
    wrap.appendChild(el);
  });

  wrap.querySelectorAll("[data-scroll]").forEach(btn => btn.addEventListener("click", async () => {
    const id = btn.getAttribute("data-scroll");
    await sendToActiveTab({ type:"SCROLL_TO_MATCH", assistId:id });
  }));
}

async function refresh(){
  const res = await bg({ type:"GET_STATE" });
  if (!res?.ok) return;

  const st = res.state;
  const queue = st[K.queue] || [];
  const done  = st[K.done]  || {};
  const templates = st[K.templates] || { comments:[], replies:[] };
  const settings  = st[K.settings] || { cooldownSeconds:60 };

  $("#cooldown").value = settings.cooldownSeconds ?? 60;

  renderQueue(queue, done);
  $("#tplComments").value = (templates.comments || []).join("\n");
  $("#tplReplies").value  = (templates.replies  || []).join("\n");
}

$("#btnRefreshState").addEventListener("click", refresh);

$("#btnScanVideos").addEventListener("click", async () => {
  const r = await sendToActiveTab({ type:"SCAN_VIDEOS_FROM_PAGE" });
  if (!r?.ok) {
    alert(r?.error || "Scan failed. Make sure you're on a YouTube channel Videos/Shorts page.");
    return;
  }
  await bg({ type:"STORE_QUEUE_APPEND", items: r.items });
  await refresh();
});

$("#btnOpenNext").addEventListener("click", async () => {
  const r = await bg({ type:"QUEUE_POP_NEXT" });
  if (!r?.ok || !r.next?.url) return alert("No next item.");
  const tab = await getActiveTab();
  await chrome.tabs.update(tab.id, { url: r.next.url });
});

$("#btnMarkDone").addEventListener("click", async () => {
  const tab = await getActiveTab();
  if (!tab?.url) return;
  await bg({ type:"MARK_DONE", url: tab.url });
  await refresh();
});

$("#btnClearDone").addEventListener("click", async () => {
  await bg({ type:"SET_STATE", patch: { [K.done]: {} }});
  await refresh();
});

$("#btnStartCooldown").addEventListener("click", async () => {
  const seconds = parseInt($("#cooldown").value || "60", 10);
  startCooldown(Math.max(10, seconds));
  await bg({ type:"SET_STATE", patch: { [K.settings]: { cooldownSeconds: Math.max(10, seconds) } }});
});

$("#btnCopyComment").addEventListener("click", async () => {
  await navigator.clipboard.writeText($("#commentBox").value || "");
});

$("#btnInsertComment").addEventListener("click", async () => {
  const text = $("#commentBox").value || "";
  const r = await sendToActiveTab({ type:"INSERT_TOP_LEVEL_COMMENT_TEXT", text });
  if (!r?.ok) alert(r?.error || "Insert failed.");
});

$("#btnScanComments").addEventListener("click", async () => {
  const keywords = ($("#rfKeywords").value || "").split(",").map(s=>s.trim()).filter(Boolean);
  const mode = $("#rfMode").value;
  const qOnly = $("#rfQOnly").checked;
  const mentions = $("#rfMentions").checked;
  const hashtags = $("#rfHashtags").checked;

  const r = await sendToActiveTab({
    type:"SCAN_COMMENTS_FOR_MATCHES",
    filters: { keywords, mode, qOnly, mentions, hashtags }
  });

  if (!r?.ok) {
    alert(r?.error || "Comment scan failed. Make sure you're on a video watch page.");
    return;
  }
  renderMatches(r.matches || []);
});

$("#btnSelectAll").addEventListener("click", () => {
  $$("#matchList .mchk").forEach(c => c.checked = true);
});
$("#btnSelectNone").addEventListener("click", () => {
  $$("#matchList .mchk").forEach(c => c.checked = false);
});

$("#btnCopyReply").addEventListener("click", async () => {
  await navigator.clipboard.writeText($("#replyBox").value || "");
});

$("#btnInsertReplyNext").addEventListener("click", async () => {
  const text = $("#replyBox").value || "";
  const selected = $$("#matchList .mchk").filter(c => c.checked).map(c => c.dataset.id);
  if (!selected.length) return alert("Select at least one match.");

  // take first selected, then uncheck it
  const id = selected[0];
  const r = await sendToActiveTab({ type:"REPLY_AND_INSERT_TEXT", assistId:id, text });
  if (!r?.ok) return alert(r?.error || "Reply insert failed.");

  const chk = $(`#matchList .mchk[data-id="${CSS.escape(id)}"]`);
  if (chk) chk.checked = false;
});

$("#btnSaveTemplates").addEventListener("click", async () => {
  const comments = ($("#tplComments").value || "").split("\n").map(s=>s.trim()).filter(Boolean);
  const replies  = ($("#tplReplies").value  || "").split("\n").map(s=>s.trim()).filter(Boolean);

  await bg({ type:"SET_STATE", patch: { [K.templates]: { comments, replies } }});
  alert("Templates saved.");
});

$("#btnLoadTemplates").addEventListener("click", refresh);

// init
refresh();

7) content_script.js
// YT Assist content script (MV3)
// - scans channel pages for videos
// - scans watch pages for comments matching filters
// - inserts comment/reply text (no submit)

const log = (...a) => console.log("[YTAssist]", ...a);

function sleep(ms){ return new Promise(r=>setTimeout(r,ms)); }

async function waitFor(fn, { timeout=12000, interval=200 } = {}) {
  const start = Date.now();
  while (Date.now() - start < timeout) {
    const v = fn();
    if (v) return v;
    await sleep(interval);
  }
  return null;
}

function isWatchPage() {
  return location.pathname === "/watch" || location.pathname.startsWith("/shorts/");
}

function isChannelVideoGridPage() {
  // covers /@handle/videos, /channel/.../videos, /@handle/shorts
  return /\/(videos|shorts)$/.test(location.pathname) || location.href.includes("/videos") || location.href.includes("/shorts");
}

function uniqBy(arr, keyFn) {
  const seen = new Set();
  const out = [];
  for (const x of arr) {
    const k = keyFn(x);
    if (!k || seen.has(k)) continue;
    seen.add(k);
    out.push(x);
  }
  return out;
}

function getVideoIdFromUrl(url) {
  try {
    const u = new URL(url);
    if (u.pathname.startsWith("/watch")) return u.searchParams.get("v") || "";
    if (u.pathname.startsWith("/shorts/")) return u.pathname.split("/shorts/")[1]?.split(/[?&#]/)[0] || "";
    return "";
  } catch { return ""; }
}

function normalizeUrl(href) {
  if (!href) return null;
  if (href.startsWith("http")) return href;
  if (href.startsWith("/")) return "https://www.youtube.com" + href;
  return null;
}

function genId() {
  return "m_" + Math.random().toString(36).slice(2) + Date.now().toString(36);
}

function setEditableText(el, text) {
  // YouTube uses contenteditable; this pattern triggers React-ish listeners
  el.focus();
  document.execCommand("selectAll", false, null);
  document.execCommand("insertText", false, text);

  el.dispatchEvent(new InputEvent("input", { bubbles: true, data: text, inputType: "insertText" }));
  el.dispatchEvent(new Event("change", { bubbles: true }));
}

async function ensureCommentsLoaded() {
  // scroll to comments area
  window.scrollTo({ top: 900, behavior: "smooth" });
  await sleep(500);

  // wait for comments container
  const comments = await waitFor(() =>
    document.querySelector("ytd-comments#comments") ||
    document.querySelector("ytd-item-section-renderer#sections")
  , { timeout: 12000 });

  if (!comments) return false;

  // Light autoload: scroll a bit to trigger initial render
  for (let i=0;i<3;i++){
    window.scrollBy({ top: 800, behavior: "smooth" });
    await sleep(450);
  }
  return true;
}

function findTopLevelCommentBox() {
  // Several layouts exist; try common patterns
  // 1) Standard watch page comment box:
  const a = document.querySelector("ytd-comment-simplebox-renderer #contenteditable-root");
  if (a) return a;

  // 2) Alternate:
  const b = document.querySelector("ytd-commentbox #contenteditable-root");
  if (b) return b;

  // 3) Shorts sometimes:
  const c = document.querySelector("ytd-comment-simplebox-renderer [contenteditable='true']");
  if (c) return c;

  return null;
}

async function openTopLevelCommentBoxIfNeeded() {
  // Sometimes needs click on placeholder
  const placeholder =
    document.querySelector("ytd-comment-simplebox-renderer #placeholder-area") ||
    document.querySelector("ytd-comment-simplebox-renderer tp-yt-paper-input") ||
    document.querySelector("ytd-comment-simplebox-renderer");

  const box = findTopLevelCommentBox();
  if (box) return box;

  if (placeholder) placeholder.click();
  await sleep(250);

  return findTopLevelCommentBox();
}

function collectVideoLinksFromGrid() {
  // Channel “Videos/Shorts” grid usually uses ytd-rich-grid-media
  const anchors = Array.from(document.querySelectorAll("a#thumbnail[href], a#video-title-link[href]"));
  const items = anchors
    .map(a => {
      const url = normalizeUrl(a.getAttribute("href"));
      if (!url) return null;

      const vid = getVideoIdFromUrl(url);
      if (!vid) return null;

      // title can be near; try closest rich item
      const card = a.closest("ytd-rich-grid-media, ytd-grid-video-renderer, ytd-rich-item-renderer");
      let title = "";
      if (card) {
        const t =
          card.querySelector("#video-title") ||
          card.querySelector("#video-title-link") ||
          card.querySelector("a#video-title-link") ||
          card.querySelector("yt-formatted-string#video-title");
        title = (t?.textContent || "").trim();
      }
      return { url, title };
    })
    .filter(Boolean);

  return uniqBy(items, x => x.url);
}

function getCommentElements() {
  // Top-level comments:
  const nodes = Array.from(document.querySelectorAll("ytd-comment-thread-renderer"));
  return nodes;
}

function extractCommentText(threadEl) {
  const content = threadEl.querySelector("#content-text");
  const author  = threadEl.querySelector("#author-text span") || threadEl.querySelector("#author-text");
  const txt = (content?.textContent || "").trim();
  const who = (author?.textContent || "").trim();
  return { txt, who };
}

function passesFilters(text, filters) {
  const t = (text || "").toLowerCase();
  const {
    keywords = [],
    mode = "any",
    qOnly = false,
    mentions = false,
    hashtags = false
  } = filters || {};

  if (qOnly && !t.includes("?")) return false;
  if (mentions && !t.includes("@")) return false;
  if (hashtags && !t.includes("#")) return false;

  if (!keywords.length) return true;

  const ks = keywords.map(k => k.toLowerCase());

  if (mode === "contains") {
    // treat keywords joined as a phrase if multiple; else single
    const phrase = ks.join(" ").trim();
    return phrase ? t.includes(phrase) : true;
  }

  if (mode === "all") {
    return ks.every(k => t.includes(k));
  }

  // any
  return ks.some(k => t.includes(k));
}

async function scrollToAssistId(assistId) {
  const el = document.querySelector(`[data-ytassist-id="${CSS.escape(assistId)}"]`);
  if (!el) return false;
  el.scrollIntoView({ behavior: "smooth", block: "center" });
  return true;
}

async function clickReplyOnThread(threadEl) {
  // reply button inside comment action bar
  const replyBtn =
    threadEl.querySelector("#reply-button-end #button") ||
    threadEl.querySelector("ytd-button-renderer#reply-button-end button") ||
    threadEl.querySelector("tp-yt-paper-button#reply-button-end");

  if (replyBtn) replyBtn.click();
  else {
    // fallback: find button with aria-label
    const btn = Array.from(threadEl.querySelectorAll("button")).find(b => /reply/i.test(b.textContent || b.getAttribute("aria-label") || ""));
    if (btn) btn.click();
  }

  // wait for reply editor
  const editor = await waitFor(() =>
    threadEl.querySelector("ytd-commentbox #contenteditable-root") ||
    threadEl.querySelector("ytd-commentbox [contenteditable='true']") ||
    threadEl.querySelector("ytd-commentbox")
  , { timeout: 8000 });

  // Try to return actual contenteditable root
  const root =
    threadEl.querySelector("ytd-commentbox #contenteditable-root") ||
    threadEl.querySelector("ytd-commentbox [contenteditable='true']");
  return root || null;
}

chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
  (async () => {
    try {
      if (msg?.type === "SCAN_VIDEOS_FROM_PAGE") {
        if (!isChannelVideoGridPage()) {
          sendResponse({ ok:false, error:"Go to a channel's /videos or /shorts page first." });
          return;
        }

        // give the grid a moment to populate
        await sleep(400);

        // Encourage scroll to load more before scan (light)
        for (let i=0;i<3;i++){
          window.scrollBy({ top: 1200, behavior:"smooth" });
          await sleep(350);
        }

        const items = collectVideoLinksFromGrid();
        sendResponse({ ok:true, items });
        return;
      }

      if (msg?.type === "INSERT_TOP_LEVEL_COMMENT_TEXT") {
        if (!isWatchPage()) {
          sendResponse({ ok:false, error:"Open a YouTube video (/watch or /shorts) first." });
          return;
        }

        const text = (msg.text || "").trim();
        if (!text) {
          sendResponse({ ok:false, error:"No text provided." });
          return;
        }

        await ensureCommentsLoaded();

        const box = await openTopLevelCommentBoxIfNeeded();
        if (!box) {
          sendResponse({ ok:false, error:"Couldn't find the top-level comment box. Scroll to comments and try again." });
          return;
        }

        setEditableText(box, text);
        sendResponse({ ok:true });
        return;
      }

      if (msg?.type === "SCAN_COMMENTS_FOR_MATCHES") {
        if (!isWatchPage()) {
          sendResponse({ ok:false, error:"Open a YouTube video (/watch or /shorts) first." });
          return;
        }

        const ok = await ensureCommentsLoaded();
        if (!ok) {
          sendResponse({ ok:false, error:"Couldn't load comments container. Try scrolling down to comments first." });
          return;
        }

        // Attempt to load more a bit
        for (let i=0;i<4;i++){
          window.scrollBy({ top: 1200, behavior:"smooth" });
          await sleep(350);
        }

        const threads = getCommentElements();
        const matches = [];

        for (const th of threads) {
          const { txt, who } = extractCommentText(th);
          if (!txt) continue;

          if (!passesFilters(txt, msg.filters)) continue;

          // tag element with temporary id for later targeting
          let id = th.getAttribute("data-ytassist-id");
          if (!id) {
            id = genId();
            th.setAttribute("data-ytassist-id", id);
          }

          matches.push({
            assistId: id,
            author: who,
            text: txt.slice(0, 240)
          });

          // keep list manageable
          if (matches.length >= 80) break;
        }

        sendResponse({ ok:true, matches });
        return;
      }

      if (msg?.type === "SCROLL_TO_MATCH") {
        const ok = await scrollToAssistId(msg.assistId);
        sendResponse({ ok });
        return;
      }

      if (msg?.type === "REPLY_AND_INSERT_TEXT") {
        if (!isWatchPage()) {
          sendResponse({ ok:false, error:"Open a YouTube video first." });
          return;
        }

        const text = (msg.text || "").trim();
        if (!text) return sendResponse({ ok:false, error:"No reply text provided." });

        const th = document.querySelector(`[data-ytassist-id="${CSS.escape(msg.assistId)}"]`);
        if (!th) return sendResponse({ ok:false, error:"That matched comment is no longer available. Re-scan comments." });

        th.scrollIntoView({ behavior:"smooth", block:"center" });
        await sleep(300);

        const editor = await clickReplyOnThread(th);
        if (!editor) return sendResponse({ ok:false, error:"Couldn't open reply editor. Try clicking Reply manually once, then press again." });

        setEditableText(editor, text);
        sendResponse({ ok:true });
        return;
      }

      sendResponse({ ok:false, error:"Unknown message type." });
    } catch (e) {
      sendResponse({ ok:false, error: String(e?.message || e) });
    }
  })();

  return true;
});

8) Load it in Chrome

  1. Go to chrome://extensions
  2. Enable Developer mode
  3. Click Load unpacked
  4. Select the yt-assist-mv3/ folder
  5. Open YouTube, then open the Side panel (Chrome side panel button), choose YT Assist

How to use (your exact use case)

Queue mode (channel scanning)

  • Go to: https://www.youtube.com/@cracked/videos (or /shorts)
  • In the side panel → QueueScan this page for videos
  • Then Open Next

Reply Finder mode (target certain tags/keywords)

  • While on a video page:
  • Side panel → Reply Finder
  • Put keywords like: link, where, how, @, #
  • Toggle Only “?” if you want question-type leads
  • Scan comments
  • Select multiple matches
  • Write your reply
  • Click Reply + Insert (next selected) (repeat)

Important guardrail (keeps you from getting cooked)

Even in assist mode, avoid blasting identical replies. Keep it:

  • relevant to what they said
  • slightly varied
  • not link-heavy

If you want, I can extend this build with:

  • “Auto-variation” (rotating openers/closers)
  • A per-video “already replied” tracker
  • A “link every N replies” limiter
  • A “load more comments” helper button (scroll + wait + re-scan)
I got an error, here’s how I worked through it.
the extension installed but side panel doesnt work . tried it on web at .. https://studio1live.com/YoutubeXXXengage/yt-assist-mv3/sidepanel.html looks nice but neither buttons work local folder on extension , or on web . somethings missing

Yep — two separate things are happening:

  1. The web version at https://studio1live.com/.../sidepanel.html will never work as-is, because it calls Chrome Extension APIs like chrome.tabs, chrome.runtime, chrome.storage. Those only exist inside an installed extension page (chrome-extension://…), not on a normal website.
  2. In the installed extension, your buttons can look dead if chrome.tabs.sendMessage() throws (common when you’re not on a YouTube tab, or the content script didn’t inject). In MV3, an unhandled promise rejection can stop the click handler flow.

So what’s “missing” is mainly:

  • a fallback UI launcher (popup) so you can open it reliably
  • error handling so you see why it’s failing instead of “nothing happens”
  • plus a quick sanity checklist (YouTube tab active, content script present, etc.)

Below is the fix that makes it work as an extension, and makes failures show a clear message.


A) Why the web copy won’t work

Your hosted URL can show the UI, but no buttons can use chrome.* because a website cannot access your browser’s extension APIs or your YouTube session. That’s browser security by design.

If you want a web-hosted UI, it must talk to an extension via native messaging / external connect, which is a whole different build.


B) Make the extension UI open reliably (add popup fallback)
1) Update manifest.json

Add default_popup so clicking the extension icon opens the same UI even if side panel isn’t enabled/visible:

{
  "name": "YT Assist (Queue + Reply Finder) - MV3",
  "description": "Assist-mode helper for scanning YouTube pages, queueing videos, and inserting comments/replies (no auto-submit).",
  "version": "1.0.1",
  "manifest_version": 3,

  "permissions": ["storage", "tabs", "scripting", "activeTab"],
  "host_permissions": ["https://www.youtube.com/*"],

  "background": { "service_worker": "service_worker.js" },

  "content_scripts": [
    {
      "matches": ["https://www.youtube.com/*"],
      "js": ["content_script.js"],
      "run_at": "document_idle"
    }
  ],

  "side_panel": { "default_path": "sidepanel.html" },

  "action": {
    "default_title": "YT Assist",
    "default_popup": "sidepanel.html"
  }
}

Then go to chrome://extensions → hit Reload on the extension.

Now you can open the UI by clicking the extension icon (works everywhere).


C) Fix “buttons do nothing” (add hard error handling)

Replace your sidepanel.js with this drop-in patched version (same features, but it alerts you when the content script isn’t reachable):

const K = {
  queue: "ytassist_queue",
  done: "ytassist_done",
  templates: "ytassist_templates",
  settings: "ytassist_settings"
};

const $ = (sel) => document.querySelector(sel);
const $$ = (sel) => Array.from(document.querySelectorAll(sel));

function sleep(ms){ return new Promise(r=>setTimeout(r,ms)); }

function showErr(msg){
  console.error("[YTAssist UI]", msg);
  alert(msg);
}

async function getActiveTab() {
  const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
  return tabs?.[0] || null;
}

async function sendToActiveTab(payload) {
  try {
    const tab = await getActiveTab();
    if (!tab?.id) return { ok:false, error:"No active tab found." };

    // If you're not on youtube.com, the content script won't exist
    if (!String(tab.url || "").startsWith("https://www.youtube.com/")) {
      return { ok:false, error:"Open a YouTube tab first (youtube.com), then try again." };
    }

    // If no receiver exists, Chrome throws. Catch it and show a friendly error.
    const res = await chrome.tabs.sendMessage(tab.id, payload);
    return res || { ok:false, error:"No response from content script." };
  } catch (e) {
    return {
      ok:false,
      error:
        "Could not reach the YouTube helper script.\n\n" +
        "Fix checklist:\n" +
        "1) Make sure the ACTIVE tab is a youtube.com page.\n" +
        "2) Reload the YouTube tab (Ctrl+R) after installing/reloading the extension.\n" +
        "3) On chrome://extensions → Inspect views → Service worker / errors.\n\n" +
        "Technical: " + (e?.message || String(e))
    };
  }
}

async function bg(msg) {
  try {
    return await chrome.runtime.sendMessage(msg);
  } catch (e) {
    return { ok:false, error: "Background not responding: " + (e?.message || String(e)) };
  }
}

function setTab(name){
  $$(".tab").forEach(b => b.classList.toggle("active", b.dataset.tab === name));
  $$(".panel").forEach(p => p.classList.toggle("active", p.id === `panel-${name}`));
}

$$(".tab").forEach(b => b.addEventListener("click", () => setTab(b.dataset.tab)));

let cooldownTimer = null;
let cooldownRemaining = 0;

function startCooldown(seconds){
  clearInterval(cooldownTimer);
  cooldownRemaining = seconds;
  $("#cooldownStatus").textContent = `Cooldown: ${cooldownRemaining}s`;
  cooldownTimer = setInterval(() => {
    cooldownRemaining--;
    if (cooldownRemaining <= 0) {
      clearInterval(cooldownTimer);
      $("#cooldownStatus").textContent = "Cooldown finished.";
      return;
    }
    $("#cooldownStatus").textContent = `Cooldown: ${cooldownRemaining}s`;
  }, 1000);
}

function escapeHtml(s=""){
  return s.replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
}

function renderQueue(queue, done){
  const total = queue.length;
  const doneCount = queue.filter(x => done?.[x.url]).length;
  $("#queueMeta").textContent = `${total} items • ${doneCount} done`;

  const wrap = $("#queueList");
  wrap.innerHTML = "";

  queue.slice(0, 200).forEach((it) => {
    const isDone = !!done?.[it.url];
    const el = document.createElement("div");
    el.className = "item";
    el.innerHTML = `
      <div class="t">${escapeHtml(it.title || it.url)}</div>
      <div class="s">${escapeHtml(it.url)}</div>
      <div class="actions">
        <span class="badge">${isDone ? "DONE" : "PENDING"}</span>
        <button class="btn ghost" data-open="${escapeHtml(it.url)}">Open</button>
        <button class="btn ghost" data-done="${escapeHtml(it.url)}">Mark done</button>
      </div>
    `;
    wrap.appendChild(el);
  });

  wrap.querySelectorAll("[data-open]").forEach(btn => btn.addEventListener("click", async () => {
    const url = btn.getAttribute("data-open");
    const tab = await getActiveTab();
    if (!tab?.id) return showErr("No active tab.");
    await chrome.tabs.update(tab.id, { url });
  }));

  wrap.querySelectorAll("[data-done]").forEach(btn => btn.addEventListener("click", async () => {
    const url = btn.getAttribute("data-done");
    const r = await bg({ type:"MARK_DONE", url });
    if (!r?.ok) return showErr(r?.error || "Failed to mark done.");
    await refresh();
  }));
}

function renderMatches(matches){
  $("#rfMeta").textContent = `${matches.length} matches found. Select some, then Reply + Insert.`;

  const wrap = $("#matchList");
  wrap.innerHTML = "";

  matches.forEach((m) => {
    const el = document.createElement("div");
    el.className = "item";
    el.innerHTML = `
      <label class="chk">
        <input type="checkbox" class="mchk" data-id="${escapeHtml(m.assistId)}" checked />
        <span class="t">${escapeHtml(m.author || "Unknown")}</span>
      </label>
      <div class="s">${escapeHtml(m.text || "")}</div>
      <div class="actions">
        <button class="btn ghost" data-scroll="${escapeHtml(m.assistId)}">Scroll</button>
      </div>
    `;
    wrap.appendChild(el);
  });

  wrap.querySelectorAll("[data-scroll]").forEach(btn => btn.addEventListener("click", async () => {
    const id = btn.getAttribute("data-scroll");
    const r = await sendToActiveTab({ type:"SCROLL_TO_MATCH", assistId:id });
    if (!r?.ok) showErr(r?.error || "Scroll failed.");
  }));
}

async function refresh(){
  const res = await bg({ type:"GET_STATE" });
  if (!res?.ok) return showErr(res?.error || "Failed to load state.");

  const st = res.state;
  const queue = st[K.queue] || [];
  const done  = st[K.done]  || {};
  const templates = st[K.templates] || { comments:[], replies:[] };
  const settings  = st[K.settings] || { cooldownSeconds:60 };

  $("#cooldown").value = settings.cooldownSeconds ?? 60;

  renderQueue(queue, done);
  $("#tplComments").value = (templates.comments || []).join("\n");
  $("#tplReplies").value  = (templates.replies  || []).join("\n");
}

$("#btnRefreshState").addEventListener("click", refresh);

$("#btnScanVideos").addEventListener("click", async () => {
  const r = await sendToActiveTab({ type:"SCAN_VIDEOS_FROM_PAGE" });
  if (!r?.ok) return showErr(r?.error || "Scan failed.");

  const store = await bg({ type:"STORE_QUEUE_APPEND", items: r.items });
  if (!store?.ok) return showErr(store?.error || "Could not store queue.");
  await refresh();
});

$("#btnOpenNext").addEventListener("click", async () => {
  const r = await bg({ type:"QUEUE_POP_NEXT" });
  if (!r?.ok || !r.next?.url) return showErr("No next item found.");

  const tab = await getActiveTab();
  if (!tab?.id) return showErr("No active tab.");
  await chrome.tabs.update(tab.id, { url: r.next.url });
});

$("#btnMarkDone").addEventListener("click", async () => {
  const tab = await getActiveTab();
  if (!tab?.url) return showErr("No active tab URL.");
  const r = await bg({ type:"MARK_DONE", url: tab.url });
  if (!r?.ok) return showErr(r?.error || "Failed to mark done.");
  await refresh();
});

$("#btnClearDone").addEventListener("click", async () => {
  const r = await bg({ type:"SET_STATE", patch: { [K.done]: {} }});
  if (!r?.ok) return showErr(r?.error || "Failed to clear done.");
  await refresh();
});

$("#btnStartCooldown").addEventListener("click", async () => {
  const seconds = parseInt($("#cooldown").value || "60", 10);
  startCooldown(Math.max(10, seconds));
  const r = await bg({ type:"SET_STATE", patch: { [K.settings]: { cooldownSeconds: Math.max(10, seconds) } }});
  if (!r?.ok) showErr(r?.error || "Failed to save settings.");
});

$("#btnCopyComment").addEventListener("click", async () => {
  try { await navigator.clipboard.writeText($("#commentBox").value || ""); }
  catch { showErr("Clipboard blocked. Try copying manually."); }
});

$("#btnInsertComment").addEventListener("click", async () => {
  const text = ($("#commentBox").value || "").trim();
  if (!text) return showErr("Comment box is empty.");
  const r = await sendToActiveTab({ type:"INSERT_TOP_LEVEL_COMMENT_TEXT", text });
  if (!r?.ok) showErr(r?.error || "Insert failed.");
});

$("#btnScanComments").addEventListener("click", async () => {
  const keywords = ($("#rfKeywords").value || "").split(",").map(s=>s.trim()).filter(Boolean);
  const mode = $("#rfMode").value;
  const qOnly = $("#rfQOnly").checked;
  const mentions = $("#rfMentions").checked;
  const hashtags = $("#rfHashtags").checked;

  const r = await sendToActiveTab({
    type:"SCAN_COMMENTS_FOR_MATCHES",
    filters: { keywords, mode, qOnly, mentions, hashtags }
  });

  if (!r?.ok) return showErr(r?.error || "Comment scan failed.");
  renderMatches(r.matches || []);
});

$("#btnSelectAll").addEventListener("click", () => {
  $$("#matchList .mchk").forEach(c => c.checked = true);
});
$("#btnSelectNone").addEventListener("click", () => {
  $$("#matchList .mchk").forEach(c => c.checked = false);
});

$("#btnCopyReply").addEventListener("click", async () => {
  try { await navigator.clipboard.writeText($("#replyBox").value || ""); }
  catch { showErr("Clipboard blocked. Try copying manually."); }
});

$("#btnInsertReplyNext").addEventListener("click", async () => {
  const text = ($("#replyBox").value || "").trim();
  if (!text) return showErr("Reply box is empty.");

  const selected = $$("#matchList .mchk").filter(c => c.checked).map(c => c.dataset.id);
  if (!selected.length) return showErr("Select at least one match.");

  const id = selected[0];
  const r = await sendToActiveTab({ type:"REPLY_AND_INSERT_TEXT", assistId:id, text });
  if (!r?.ok) return showErr(r?.error || "Reply insert failed.");

  const chk = $(`#matchList .mchk[data-id="${CSS.escape(id)}"]`);
  if (chk) chk.checked = false;
});

$("#btnSaveTemplates").addEventListener("click", async () => {
  const comments = ($("#tplComments").value || "").split("\n").map(s=>s.trim()).filter(Boolean);
  const replies  = ($("#tplReplies").value  || "").split("\n").map(s=>s.trim()).filter(Boolean);

  const r = await bg({ type:"SET_STATE", patch: { [K.templates]: { comments, replies } }});
  if (!r?.ok) return showErr(r?.error || "Failed to save templates.");
  alert("Templates saved.");
});

$("#btnLoadTemplates").addEventListener("click", refresh);

refresh();

Reload extension after replacing.


D) Quick checklist to test (takes 30 seconds)
  1. Open a YouTube tab:
    • https://www.youtube.com/@cracked/videos
  2. Reload that tab (Ctrl+R) after reloading the extension
  3. Click your extension icon → UI opens (popup)
  4. Press Scan this page for videos
    • If it fails, you’ll now get a clear alert telling you why.

E) Where to see the real error (if it still fails)
  • chrome://extensions → your extension → Inspect views
    • “service worker” (background)
    • (and if visible) the panel/popup page

Also:

  • On YouTube tab → F12 → Console → look for [YTAssist]

If you paste me the exact alert message you get after this patch (even 1 line), I’ll pinpoint the exact cause (usually: not on youtube tab, content script not injected, or YouTube page type mismatch like “Home” vs “Videos”).

write a quickmet description for youtube assit ext tool

Here are a few quick, clean description options you can use (pick the tone you want). These are sized to work for a Chrome extension listing or internal project page.


Option 1 — Clean & Professional

YouTube Assist is a human-in-the-loop engagement tool that helps creators and marketers efficiently interact with YouTube content. It scans channel pages and video comments, builds a smart queue, and assists with inserting comments or replies — while keeping the user in full control. No auto-posting. No bots. Just faster, smarter engagement.


Option 2 — Marketing-Focused

YouTube Assist helps you engage faster on YouTube without triggering spam filters. Scan channels, queue videos, find comments by keyword or tag, and reply with pre-written templates — all in assist mode so you decide when to post. Designed for safe, organic outreach and audience growth.


Option 3 — Developer / Power-User Tone

YouTube Assist is an MV3 Chrome extension that provides assisted YouTube engagement. Features include channel scanning, video queues, keyword-based comment discovery, and comment/reply insertion — all without automated submission. Built for creators, marketers, and researchers who want speed without violating platform behavior norms.


Option 4 — Short Store-Ready Blurb (Recommended)

Engage smarter on YouTube. YouTube Assist helps you scan channels, queue videos, find relevant comments by keyword or tag, and insert replies using templates — all in safe assist mode. No auto-posting, no bots, just faster human engagement.


Ultra-Short (Tagline)

Human-assisted YouTube engagement — fast, safe, and in your control.


If you want, I can also:

  • Write the Chrome Web Store long description
  • Generate feature bullets
  • Add a “Not a bot / Assist Mode” compliance disclaimer
  • Brand it under Studio1Live / YouTubeXXXengage

Author of this page give reader authority to take this code and make their own, modify as you like, rebrand and sale as you like, you have everything you need.

#CFB #ConferenceChampionship #CFP #Miami #ACC #SEC #Big12 #CollegeFootballPlayoff

Shoutouts: @CFBPlayoff, @ESPNCFB, @CollegeGameDay, #CFB, #ConferenceChampionship, #CollegeFootballPlayoff

Follow: @herudaguru for more unfiltered college football breakdowns, rants, and picks.

Support the Studio1Live Sports Family

Tags: #herudaguru #soccergod #studio1live #MiamiHurricanes #CFP #CollegeFootball #CFPCommittee #SportsPicks #SportsBettingCommunity Shoutouts & YouTube Tags:

@AngelSerrano, @MrOvadose101, @Hurricane357, @Normal Guy, @Slug Bruce, @Black Panther JackieBrown, @Darvis Baylock, @Aniyunwiya_Nike903, @FOH, @Angel Serrano, @Timothy Brown:Tru-Shu: Shu, @KillSwitch R6, @Dre Light, @Saint_Louis, @Kosmon Cal tv, @Crisjamie Mac, @Nonya Bidness, @Shaquile Oatmeal, @TassCo The C.E.O, @JJ The Enlightened One, @Jeramiah Myers, @Sunshine, @inthehighest1, @Siemon The atheistkilla, @James Smith, @SunofdeBeach, @Vitamin C, @Mohamed Hussain, @Anthony Manzano, @B Correa, @818CaliCane, @Jorge Mayorga, @Rich Andrews, @El_rico88, @Scuba Steve, @Ryder Boy, @jrgodfatha88, @ChiTown Cane, @The Dulci Effect, @Live and Learn, @IcebergSlim38, @SipAmazin, @Yanik- Alahim D’orsey Bey, @KARMACOMING2C0113CT, @Zac Pac – Dizzies Be Dizzy, @Bailioso, @Beast Of Burden, @MoneyLineRay, @KDUB’S WORLD, @blackwhileindian, @SHAWN P DIDDY, @BIG CHIEF TOILET PAPER, @LACONIC, @Master of Self, @Big Chief All American, @Michelle R, @I Am Scorpion, @PatrickGeneLeBlancHardy, @Black Hole Sun, @Motivated Dedicated, @EL Gumbo, @Migo_critt, @Coinlito’s Way, @David Williams, @KMS_929, @Talking Trash With the Steelers Gang!, @Daniel Berri Sports Highlights, @Shard, @Billy DeLuLu, @BangkokVice


Leave a Reply

Your email address will not be published. Required fields are marked *