[Step 2-A-2/3] Making It Read the Page and Talk to AI (Reseller Edition)

📌Expert Workflows · Step 2: AI Chrome Extension · Edition A (Reseller) · Part 2 of 3

🎯 Why Bother With This

In Part 1 you built an extension that loads and opens a popup. Today you add selected-text capture and a provider request.

Select ordinary text in the main frame of a normal website, click the extension, and request a draft rewrite. This reader does not support browser settings, the Chrome Web Store, PDF viewers, form inputs, or nested frames. Provider latency, cost, and availability vary.

This lesson separates page-reading errors, provider configuration, and response failures so you can diagnose each one. The key stays out of source code, but browser local storage is not encrypted credential storage.

Project context

Use the complete Part 2 folder in the verified download below, or create the four files shown in this article.

🛠️ Your Setup Checklist

  • Part 1 complete: alisting-helperfolder withmanifest.jsonandpopup.html, loading successfully in Chrome
  • An API key, documented Chat Completions-compatible endpoint and model from your chosen provider. Check API pricing and data terms separately from any consumer chat subscription; a chat free tier does not imply free API access.
  • About 40 minutes

How to Use This Troubleshooting Tutorial

  • 🔴 BEFORE shows an intentionally incomplete or flawed example so readers can reproduce and understand a representative problem.
  • 🧠 CAUSE explains why the problem occurs.
  • 🟢 AFTER presents the corrected implementation for this lesson.
  • ✅ VERIFICATION explains how to confirm that the correction works.

📝 Step 1: Understanding the Two Places Your Code Can Live

Chrome extensions separate popup, background worker, and page-reading code. This tutorial puts provider requests in the worker and uses the popup to display results.

The popupis the little window that opens when you click your icon. It appears when you click and disappears the moment you click elsewhere — including everything it was holding in memory.

The background service worker handles messages without a visible window. Manifest V3 workers can terminate; they are not permanent processes. Keep the popup open while a request runs. Part 2 does not recover an unfinished request or retain its output after closing.

Extension pages, including popups, and service workers can make cross-origin requests with appropriate host permissions. Page-injected code has different restrictions. We use the worker here to keep the request logic separate. See Chrome’s network-request guidance and worker lifecycle.

If that feels abstract, the Troubleshooting notes below makes it concrete. It’s the wall almost everyone hits.

📝 Step 2: Updating the Manifest

Your manifest needs three new things: permission to see the current tab, permission to store your key, and a declaration of which outside address you’ll contact. Replacemanifest.jsonwith:

{
  "manifest_version": 3,
  "name": "Listing Helper",
  "version": "1.1",
  "description": "Rewrites and summarizes selected text using AI.",
  "permissions": ["activeTab", "scripting", "storage"],
  "host_permissions": ["https://your-provider-url/*"],
  "background": {
    "service_worker": "background.js"
  },
  "action": {
    "default_popup": "popup.html",
    "default_title": "Listing Helper"
  }
}

activeTab grants temporary access after a user gesture; scripting permits the selected-text reader, and storage remembers configuration. Replace https://your-provider-url/* with the provider’s HTTPS origin, for example https://api.example.com/*. Host permissions match scheme and host; they do not restrict access to one API path. The base URL in popup.js separately includes any required path such as /v1.

backgroundpoints at the file we’re about to write.

📝 Step 3: The Background File That Calls the AI

Createbackground.jsin the same folder:

// Listens for a request from the popup, calls the AI, sends the answer back.
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.type !== "REWRITE") return;

  handleRewrite(message.text)
    .then((result) => sendResponse({ ok: true, result }))
    .catch((error) => sendResponse({ ok: false, error: String(error) }));

  return true; // keeps the channel open for the async reply
});

async function handleRewrite(text) {
  const { apiKey, baseUrl, model } = await chrome.storage.local.get([
    "apiKey",
    "baseUrl",
    "model",
  ]);

  if (!apiKey) throw new Error("No API key saved yet. Open the popup and add one.");

  const prompt =
    "Rewrite the following product description for an online marketplace listing. " +
    "Keep every factual detail exactly as given — do not invent sizes, brands, " +
    "materials, or condition. Make it clear, warm and easy to skim. " +
    "Reply with the rewritten description only.\n\n" +
    text;

  if (!baseUrl || !model || baseUrl.includes("your-provider-url") || model === "your-model-name") throw new Error("Configure your provider URL and model in popup.js, then save again.");
  if (typeof text !== "string" || !text.trim()) throw new Error("Select some text first.");
  const response = await fetch(`${baseUrl}/chat/completions`, {
    method: "POST",
    signal: AbortSignal.timeout(25000),
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${apiKey}`,
    },
    body: JSON.stringify({
      model: model,
      messages: [{ role: "user", content: prompt }],
      
    }),
  });

  if (!response.ok) {
    throw new Error(`Service returned ${response.status}`);
  }

  const data = await response.json();
  const choice = data.choices?.[0];
  if (choice?.finish_reason !== "stop" || typeof choice?.message?.content !== "string" || !choice.message.content.trim()) throw new Error("Provider returned an empty or incomplete answer. Check its Chat Completions compatibility and try again.");
  return choice.message.content.trim();
}

Two things deserve your attention rather than a copy-paste.

The prompt asks the model to preserve factual details and avoid invented materials, sizes, brands, or condition. This is an instruction, not protection against inaccurate listings. Compare every draft with the source before posting, especially defects, measurements and qualifiers.

The request omits optional temperature settings for compatibility. No temperature guarantees factual accuracy. It aborts after 25 seconds and rejects missing, empty or incomplete answers; the provider must return the documented Chat Completions response shape with finish_reason: "stop". A timeout does not prove the request was unbilled.

📝 Step 4: The Popup That Ties It Together

Replacepopup.htmlwith:

<!DOCTYPE html>
<html>
  <body style="width: 320px; padding: 14px; font-family: sans-serif;">
    <h3 style="margin-top: 0;">Listing Helper</h3>

    <div id="setup">
      <input id="key" type="password" placeholder="Paste your API key" style="width: 100%;" />
      <button id="save" style="margin-top: 6px;">Save key</button>
    </div>

    <hr />
    <button id="go" style="width: 100%; padding: 8px;">Rewrite selected text</button>
    <p id="status" style="color: #666; font-size: 12px;"></p>
    <textarea id="out" style="width: 100%; height: 160px;"></textarea>

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

And createpopup.js:

document.getElementById("save").addEventListener("click", async () => {
  const apiKey = document.getElementById("key").value.trim();
  await chrome.storage.local.set({
    apiKey,
    baseUrl: "https://your-provider-url/v1",
    model: "your-model-name",
  });
  document.getElementById("status").textContent = "Key saved.";
  document.getElementById("key").value = "";
});

document.getElementById("go").addEventListener("click", async () => {
  const status = document.getElementById("status");
  status.textContent = "Reading page…";

  try {
  const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
  const [{ result: selected }] = await chrome.scripting.executeScript({
    target: { tabId: tab.id },
    func: () => window.getSelection().toString(),
  });

  if (!selected) {
    status.textContent = "Select some text on the page first.";
    return;
  }

  status.textContent = "Asking the AI…";
  chrome.runtime.sendMessage({ type: "REWRITE", text: selected }, (reply) => {
    if (chrome.runtime.lastError) { status.textContent = "Connection ended. Keep the popup open and try again."; return; }
    if (reply && reply.ok) {
      document.getElementById("out").value = reply.result;
      status.textContent = "Done.";
    } else {
      status.textContent = "Error: " + (reply ? reply.error : "no response");
    }
  });

  } catch { status.textContent = "Cannot read this page. Select text on a normal website and try again."; }});

Remember to put your provider’s real base URL and model name intopopup.js.

Reload the extension at chrome://extensions. On a normal website select a paragraph, open the popup, save your key, and click Rewrite selected text. Keep the popup open while waiting. After changing the configured URL or model, reload and save again.


⚠️ Troubleshooting — The request that was blocked with no explanation

(Troubleshooting example — a common mistake reconstructed for learning, not a personal claim.)

🔴 BEFORE

Educational error scenario: A fetch call is placed in page-injected code, and the request is blocked or behaves differently because page and extension contexts have different security boundaries.

Access to fetch at 'https://...' from origin 'https://www.example-marketplace.com'
has been blocked by CORS policy

🤔 Cause

CORS controls whether page-context scripts can read cross-origin responses; it does not ban every cross-origin request. In this teaching example, the destination does not allow the page origin. Moving the request to the extension worker with the required host permission uses the intended extension context.

The extension must also declare the specific external origins it needs in host_permissions. Grant only the narrowest provider origin required and review Chrome’s current extension documentation.

🟢 AFTER

The fix is the architecture above, and it’s worth stating as a rule you can reuse forever:do the network call in the background service worker, and declare the address inhost_permissions.The popup asks the service worker to do the work; the service worker calls the AI and sends the answer back.

Two practical notes while you’re debugging this class of problem. To see errors from your background code, go tochrome://extensions, find your card, and click the“service worker”link — that opens the console where those messages appear, and without it you’re guessing. And whenever you changemanifest.json, click refresh on the card; manifest changes don’t take effect until you do.

Educational error scenario — repeated key entry and hard-coding. A popup loses its in-memory value when closed, but placing the key in source code would expose it through backups or sharing.

🔴 BEFORE

The corrected personal-use prototype stores configuration in the extension’s local storage rather than source files. This is convenience storage, not a hardware-backed secret vault.

🤔 Cause

Hard-coding a key places it in the extension folder and can expose it through version control, backup, or sharing. A key stored in the browser profile can still be accessed by this extension and by anyone controlling the profile or device.

🟢 AFTER

chrome.storage.local— which is what the code above already uses. It saves the key against your browser profile, not inside your extension’s files, so it survives the popup closing but doesn’t travel with the folder.

The pattern is the one you saw in earlier steps wearing different clothes:.envon a laptop, Colab Secrets in a notebook,chrome.storage.localin an extension. Same principle every time —keep the credential out of source code, minimise its permissions, monitor usage, and rotate it if exposure is suspected.

For distribution, design and review a server that holds the provider credential, authenticates users, enforces usage limits and handles abuse. A server is not automatically secure, and the personal prototype here does not include that infrastructure.

Security takeaway: Treat an API key as a billable credential. Do not share the extension profile or folder as though local storage made the key universally safe.


⚠️ Loose Ends Worth Tying

Your tool works. Three things to know before you use it on real listings.

Read every rewrite before you post it.Your prompt tells the model not to invent details, and that instruction genuinely helps — but “helps” is not “guarantees.” A model can still smooth “small mark on the sleeve” into something softer, and on a marketplace that difference has consequences with buyers and with platform policy. Treat the output as a strong first draft written by an enthusiastic assistant who has never seen the item.

Know what you send. Selected text goes to the configured provider. Check its current retention and training terms for the API you use. Do not select buyer messages, addresses, account details or confidential text.

Watch usage. Requests can incur charges and encounter provider limits. Inspect your API account’s usage and set available spending controls.

✅ What You Have Now

You now have the four files for a personal selected-text rewriting extension. Configuration stays outside the source files after you save it in browser local storage; that storage remains accessible to the extension and anyone controlling the profile.

Highlight, request, review, then copy. Measure your own time including corrections before claiming a saving.

💡 If You Want to Push It

Change one line — the prompt inbackground.js— and your tool becomes something else entirely. Ask for “a friendly 40-word version for social media” and it writes captions. Ask for “three suggested titles under 60 characters” and it does your listing headlines. The code stays identical; the instruction is the product. That’s theprompting skill from Essentials Step 6paying off inside software you built.

❓ Try This Yourself

Rewrite three real descriptions with it, then compare each output against your original word by word. Find anything the model added, softened, or dropped. Whatever you find is exactly what your prompt needs to forbid — and editing that one sentence to fix it is how this tool becomes genuinely yours.

🔗 Next in This Series

[Step 2-A-3/3] Making It Yours — Buttons, Icons and a Tool You’ll Actually Use— where we add a proper icon so it stops looking like a placeholder, give it multiple buttons for different jobs (rewrite, shorten, write a title), remember your last result, and cover honestly what would need to change before you could share it with another person.


Personal-prototype boundary

This design is for a local personal prototype, not public distribution. Selected page text is transmitted to the configured provider. Do not select messages, addresses, account data, or confidential content. Browser local storage is not a secret vault; use a restricted key, monitor usage, and move credentials behind a reviewed server before distribution.

Series status: Part 3 is published and adds task buttons, icons and completed-result storage. Both versions remain personal prototypes.


Updated runnable package — 12 September 2026

Download the complete Part 2 and Part 3 folders, tests and recorded results. Load only the folder for the part you are following. Part 3’s icon paths now resolve inside its icons/ folder.

Actual Node.js v24.19.0 run: Part 2 10/10 checks, Part 3 10/10 checks, and 14/14 additional failure checks passed. The additional checks exercise malformed, empty and truncated provider replies, missing configuration, empty input, restricted-page failure and a lost popup message channel. Chrome APIs and provider responses were mocked; this does not demonstrate browser installation or live AI quality, speed or cost.

Run node part2/verify.mjs, node part3/verify.mjs, then node regressions.mjs from the unzipped parent folder. The README explains provider setup and manual installation checks.

Download corrected Parts 2–3 and experiment results (ZIP)
SHA-256: 4a2b6421fb74934a02f512ba76e45500b8f2d2dc120cf0b3b1c811c4380c2c80

Archived Part 2 evidence — 30 August 2026

The following records the earlier revision. Use the September 12 package above for the current implementation.

The four tutorial files were reconstructed from this article and checked on 30 August 2026 using Windows 10 and Node.js v24.19.0. The package contains no real API key, account data, selected page text, or live provider configuration.

Reproduction input and observed output

Mock input: Blue mug, chipped rim.

Observed mocked output: Clear factual listing.

The deterministic test confirmed that the input was included in the request body, the expected endpoint and POST method were used, a missing key stopped before a network call, and a simulated HTTP 429 followed the error path.

Automated verification results

Check Result Verified condition
Manifest JSON and V3 PASS The manifest parsed and declared Manifest V3.
Manifest file linkage PASS The background worker and popup point to existing files.
JavaScript syntax PASS Both JavaScript files parsed successfully.
Required popup elements PASS Key, save, rewrite, status, and output controls exist.
Secret scan PASS No credential-like token is included.
Provider boundary PASS The provider URL and model remain explicit placeholders.
Missing-key guard PASS No request occurs without an API key.
Mock request and response PASS Endpoint, method, input, and returned content were verified.
HTTP error path PASS A simulated 429 response becomes an error.
Popup guards PASS Test-only key storage and empty-selection behavior were verified.

Download the verified Part 2 tutorial files (ZIP)
SHA-256: 710D0255168547BBD7F4D05208518B482D2B2C91829DC83481F9F804B923AFD9

Verification boundary: this is a deterministic mock test, not evidence that a named provider accepted a live request. No live API call was made. Before use, replace the provider and model placeholders using the chosen provider’s current documentation and review its availability, pricing, request format, and data terms.