📌Expert Workflows · Step 2: AI Chrome Extension · Edition A (Reseller) · Part 3 of 3 — final
🎯 What You’re Actually Building Here
This Chrome extension task buttons tutorial starts with a working personal prototype: it reads selected text, sends it to an AI provider, and returns a rewritten version. It also has a grey puzzle-piece icon, exactly one button, and forgets everything the moment you look away.
That gap — between “it works” and “I reach for it without thinking” — is smaller than it looks and matters more than it sounds. A tool you have to concentrate to use gets abandoned in a week. By the end of this post yours will have a proper icon in the toolbar, three buttons for the three jobs you actually do, and a memory that survives the popup closing.
We’ll also finish with the honest conversation nobody has in extension tutorials: what would genuinely need to change before you could hand this to another reseller.
Project context
the example user is a composite secondhand-clothing reseller created for this educational scenario. She represents a beginner improving a personal extension through three distinct listing tasks: rewriting a description, shortening it for a social caption, and generating title options. This is not a claim about a real person’s routine or results.
Follow the published reseller series in order. The current download below contains the complete Part 3 folder, including all four source files and the icons subfolder.
🛠️ What to Have Ready
- Parts 1 and 2 complete — a working extension that rewrites selected text
- Allow roughly 35–60 minutes, depending on debugging and image preparation
- Optionally, an image tool for the icon (details below)
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: Giving It a Real Icon
The grey puzzle piece is Chrome’s placeholder for “this extension didn’t provide an icon.” Adding the declared icon files and changes how the tool feels more than any code you’ll write today.
You need three square PNG images — 16, 48, and 128 pixels. The 16 is what sits in your toolbar; the others appear in the extensions list and elsewhere. Make one 128×128 image and resize it twice.
Where to get the image? Three realistic options. Draw something simple in any image editor — a coloured square with a letter on it is completely respectable. Use a free icon site. Or generate one, which is the option worth trying if you followed the earlier levels: theAI image generation guidecovers how to art-direct a clean, flat icon rather than accepting whatever the model offers first. A prompt like“flat vector app icon, simple clothing tag symbol, two colours, plain background, no text”is a starting prompt to evaluate, not a guaranteed result.
Create a folder callediconsinside your extension folder and save the three files asicon16.png,icon48.png, andicon128.png. Then updatemanifest.json— note both places the icons appear:
{
"manifest_version": 3,
"name": "Listing Helper",
"version": "1.2",
"description": "Rewrites, shortens and titles your listings using AI.",
"permissions": ["activeTab", "scripting", "storage"],
"host_permissions": ["https://your-provider-url/*"],
"background": { "service_worker": "background.js" },
"icons": {
"16": "icons/icon16.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
},
"action": {
"default_popup": "popup.html",
"default_title": "Listing Helper",
"default_icon": {
"16": "icons/icon16.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
}
}
}
Refresh the extension atchrome://extensionsand look at your toolbar. That’s your tool, with your mark on it. (Chrome’sextension documentationlists the icon sizes used in each surface, if you want to be thorough.)
📝 Step 2: Three Buttons, Three Jobs
One button currently performs one task. In this Troubleshooting example, the example user needs three separate listing actions, so the interface should match those distinct jobs.
Replace the button area inpopup.html:
<div style="display: flex; gap: 6px;">
<button class="task" data-task="rewrite">Rewrite</button>
<button class="task" data-task="shorten">Shorten</button>
<button class="task" data-task="title">Titles</button>
</div>
Thedata-taskattribute is the trick that keeps this simple — each button carries a label saying which job it wants, so one piece of code handles all three instead of you writing three near-identical copies.
Replace popup.js with this complete version, including configuration saving and restoration of the last completed result:
chrome.storage.local.get(["lastResult", "lastTask"]).then((saved) => {
if (saved.lastResult) {
document.getElementById("out").value = saved.lastResult;
document.getElementById("status").textContent = "Showing your last " + saved.lastTask + " result.";
}
});
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.querySelectorAll(".task").forEach((button) => {
button.addEventListener("click", () => runTask(button.dataset.task));
});
async function runTask(task) {
const status = document.getElementById("status");
const out = document.getElementById("out");
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 = "Working…"; out.value = "";
chrome.runtime.sendMessage({ type: "RUN", task, text: selected }, (reply) => {
if (chrome.runtime.lastError) { status.textContent = "Connection ended. Keep the popup open and try again."; return; }
if (reply && reply.ok) { out.value = reply.result; status.textContent = "Done — you can close this and come back."; }
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."; }
}
Replace background.js with this complete worker. It includes task selection, provider checks, a 25-second request deadline, response validation, and completed-result storage:
const GUARDRAIL =
" Keep included facts accurate; preserve material qualifiers. Short tasks may omit details. Do not invent sizes, brands, " +
"materials, measurements or condition. Do not add claims that are not in the text.";
const PROMPTS = {
rewrite: "Rewrite this as a clear, warm marketplace description that is easy to skim." + GUARDRAIL,
shorten: "Rewrite this as a single friendly sentence under 30 words, suitable for a social media caption." + GUARDRAIL,
title: "Write 3 listing title options, each under 60 characters, each on its own line. Put the most searchable words first." + GUARDRAIL,
};
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type !== "RUN") return;
handleTask(message.task, message.text)
.then((result) => sendResponse({ ok: true, result }))
.catch((error) => sendResponse({ ok: false, error: String(error) }));
return true;
});
async function handleTask(task, text) {
if (!PROMPTS[task]) throw new Error("Unknown task.");
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.");
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, messages: [{ role: "user", content: PROMPTS[task] + "\n\n" + text }] }),
});
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.");
const result = choice.message.content.trim();
await chrome.storage.local.set({ lastResult: result, lastTask: task, lastAt: Date.now() });
return result;
}
The shared prompt instruction applies to all three tasks. Short captions and titles necessarily omit details, so check that included facts and material qualifiers stay accurate. The prompt does not enforce factuality or length. Count words/characters and compare with the original before publishing.
⚠️ Troubleshooting — “Cannot read properties of null”
(Troubleshooting example — a common mistake reconstructed for learning, not a personal claim.)
🔴 BEFORE
After adding the buttons, nothing happened at all. Opening the popup’s console (right-click inside the popup → Inspect) showed:
Uncaught TypeError: Cannot read properties of null (reading 'addEventListener')
🤔 Cause
This reconstructed example leaves the old go click handler in JavaScript after replacing that HTML button with three class="task" buttons.
The old document.getElementById("go").addEventListener(...) now calls a method on null. Replace that handler with the task-button registration shown above. An output-field mismatch would fail when accessing that field, which is a different line.
Another cause is running the script before its elements exist. Keep the script after the controls, or use defer when loading it in the head.
🟢 AFTER
Use these checks for this example:
Keep the script tag at the very end of<body>, after everything it needs to find — which is howpopup.htmlhas been written since part 2. If you prefer it in the head, adddefer(<script src="popup.js" defer></script>), which tells the browser to wait until the page is built.
Match selectors to the actual HTML. Check whether the expected element exists, whether the identifier matches, and whether the script ran before the element was parsed.
To open the popup’s console, right-clickinside the open popupand choose Inspect — a separate window from the service worker console you met in part 2. Extensions have several consoles, and knowing which one holds your error is half of debugging them.
💡 Scenario takeaway: A spelling mismatch near the first event listener can stop later handlers from being registered. When several buttons fail together, check the earliest console error and compare every HTML id with the corresponding JavaScript selector.
📝 Step 3: A Memory That Survives
After a request completes, clicking outside the popup closes it and discards its displayed state. Store a completed result separately so reopening can restore it.
⚠️ Troubleshooting — The result that vanished when I looked away
(Troubleshooting example — a common mistake reconstructed for learning, not a personal claim.)
🔴 BEFORE
No error message. Click a button, look at the page while it works, click the icon again: empty box, as if nothing had happened. Sometimes the result had arrived; sometimes it hadn’t. There was no way to tell.
🤔 Cause
A popup isn’t a window that stays open — Chrome destroys it entirely the moment it loses focus, along with everything it was displaying. This is normal, expected behaviour and it isn’t going to change.
A worker may continue after the popup closes, but completion is not guaranteed: Manifest V3 workers can terminate and a request can fail. Store a result only after successful validation. This example does not resume unfinished requests. Keep the popup open while waiting; a manual retry may create another billable request. See Chrome’s worker lifecycle.
🟢 AFTER
The complete worker above already includes this storage step after a successful response. This excerpt explains it; do not paste a duplicate call into the file:
await chrome.storage.local.set({
lastResult: result,
lastTask: task,
lastAt: Date.now(),
});
The complete popup above already includes this restoration block at the top:
chrome.storage.local.get(["lastResult", "lastTask"]).then((saved) => {
if (saved.lastResult) {
document.getElementById("out").value = saved.lastResult;
document.getElementById("status").textContent =
"Showing your last " + saved.lastTask + " result.";
}
});
Now the popup can restore the last non-sensitive result after it reopens. chrome.storage.local persists data, but it is not an encrypted secret vault: extension contexts and anyone with access to the browser profile may retrieve stored values. Use this client-side build only as a personal learning prototype, restrict provider keys and spending limits, and move production requests behind a server. See the Chrome Storage API documentation.
💡 Scenario takeaway: Persisting the last non-sensitive result lets a user leave the popup and return later. Avoid storing confidential listing data longer than necessary, and provide a clear way to overwrite or remove saved results on shared devices.
⚠️ What Still Needs Doing
You now have a useful personal prototype, and the natural next thought is “my reseller group would love this.” Before you send anyone the folder, three things you need to know.
Your key travels with the folder if you’re careless.The key itself lives in browser storage, not in your files, so a copied folder doesn’t carry it — that’s exactly why part 2 set it up that way. But if you ever pasted a key into a file while experimenting, it’s still in that file. Check before sharing, and if you find one, revoke it in your provider’s dashboard and generate a new one rather than just deleting the line.
This is a personal prototype. Public distribution needs a separate review of authentication, usage limits, privacy disclosures, provider terms and store requirements. A reviewed server can hold provider credentials, but it must also prevent unauthorized use and abuse. This article does not implement that infrastructure.
Review everything before posting. Prompt instructions cannot guarantee accurate condition, measurements or claims. Compare each draft with the original, including omissions in short formats; take the time the item requires.
✅ What You’ve Built
The complete Part 3 folder provides three task buttons, toolbar icons, a provider request handler, and storage for the last completed result. The local tests below check these paths with mocked APIs; use the manual installation and provider checks before relying on the extension.
Three posts ago, “make a browser extension” was something other people did.
💡 For the Ambitious
Add a fourth button for whateveryoukeep doing manually — a “translate to plain English” button, a “list the measurements as bullet points” button, a “check this for policy-risky words” button. Keep the shared request flow: add a button with adata-taskname, add a matching line toPROMPTS, done. That’s the moment a tool becomes yours, and it’s theprompt-as-product idea from Essentials Step 6in its most practical form.
❓ Try This Yourself
Use your extension for one full listing session — ten items, start to finish — and keep a note of every moment you thought “I wish it also…”. That list is your roadmap. Build the top item as a fourth button before you touch anything else in this course; nothing teaches faster than adding a feature you personally want.
🔗 What’s Next
This Chrome-extension project is complete. Continue with the Expert Workflows roadmap for the current published project path.
Next in the course:[Step 3] Build an AI Telegram Bot— where your tool stops living in your own browser and starts answering other people, which is the first time what you’ve built becomes something withusers. The full path is mapped in The Expert Workflows Curriculum Roadmap.
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 3 evidence — 30 August 2026
Archive correction: the earlier ZIP stored icons at its root while the manifest expected an icons/ subfolder. Its recorded source checks did not establish that the packaged extension would load. Use the corrected September 12 package above; the following is retained as historical test evidence.
The Part 2 base and Part 3 additions were consolidated and checked on 30 August 2026 using Windows 10 and Node.js v24.19.0. The package contains three correctly sized placeholder icons and no real API key, account data, selected page text, or live provider configuration.
Reproduction input and observed outputs
Mock input: Blue mug, chipped rim.
- Rewrite:
Mock rewrite result - Shorten:
Mock shorten result - Titles:
Mock title result
Each request included the original input and the shared rule not to invent sizes, brands, materials, measurements, condition, or unsupported claims. The latest result and task were also written to the mocked storage layer and restored by the popup test.
Automated verification results
| Check | Result |
|---|---|
| Manifest V3 and integration files | PASS |
| Icon files and dimensions | PASS |
| JavaScript syntax | PASS |
| Three task buttons | PASS |
| Shared factual guardrail | PASS |
| Secret and placeholder boundary | PASS |
| Missing-key and unknown-task guards | PASS |
| All task prompts and mocked outputs | PASS |
| HTTP error handling | PASS |
| Popup restore and empty-selection guards | PASS |
Download the verified Part 3 tutorial files (ZIP)
SHA-256: B2121A76D367E88841F526C84D820502A28E6FD390EE1980C7C131AE99627CE7
Verification boundary: this is a deterministic mock test, not evidence that a named provider accepted a live request. The included icons are locally generated plain blue placeholder PNGs at the declared sizes, not commissioned brand artwork. Configure the provider and model using current documentation before live use.

![[Step 2-A-3/3] Making It Yours — Icons, Task Buttons, and a Tool You’ll Actually Use (Reseller Edition)](https://lifetechhack.com/wp-content/uploads/2026/08/expert-808-customize.png)