app
Chip Fetcher (cf)
Public Made by Adomby adom
Crawls the web on your behalf to gather everything about the chips you care about: datasheets, symbols, footprints, 3D models, stock and pricing, straight from the manufacturer first, into your EDA tool. Say "cf" to your AI.
← Commit history
1.3.9: focus-aware clipboard routing on every copy path (hasFocus check, 800 ms race, in-gesture execCommand fallback); scrollbar block verified against the codified skill
3 files changed
+30−16
Cargo.lock+1−1@@ -4,7 +4,7 @@ version = 4 [[package]] name = "adom-chip-fetcher"-version = "1.3.8"+version = "1.3.9" dependencies = [ "anyhow", "clap",
Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "adom-chip-fetcher"-version = "1.3.8"+version = "1.3.9" edition = "2021" description = "Crawls the web on your behalf to gather everything about the chips you care about (datasheet, symbol, footprint, 3D, stock) into one library, by driving Chrome via pup."
src/ui/index.html+28−14@@ -704,16 +704,30 @@ function escapeHtml(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, m // Relative paths so the page works behind /proxy/<port>/ and inside a webview. const API_BASE = location.pathname.replace(/index\.html$/, '').replace(/\/$/, ''); const TRY_IT_PARKED = true; // ship-night posture: no /ai-threads calls; Try it copies instead (John's ruling, 2026-08-23)+function copyViaTextarea(t) {+ // Must run synchronously inside the click gesture: execCommand('copy')+ // only works during a user gesture.+ try {+ const ta = document.createElement('textarea');+ ta.value = t; ta.style.position = 'fixed'; ta.style.opacity = '0';+ document.body.appendChild(ta); ta.select();+ const ok = document.execCommand('copy'); ta.remove(); return ok;+ } catch (e) { return false; }+} async function copyText(t) {- try { await navigator.clipboard.writeText(t); return true; }- catch (e) {- try {- const ta = document.createElement('textarea');- ta.value = t; ta.style.position = 'fixed'; ta.style.opacity = '0';- document.body.appendChild(ta); ta.select();- const ok = document.execCommand('copy'); ta.remove(); return ok;- } catch (e2) { return false; }- }+ // Unfocused windows (a background pup tab, a webview panel) make+ // navigator.clipboard.writeText HANG with "document is not focused"+ // (parts-search 1.1.2 finding). Route synchronously by focus, and race+ // the async path at 800 ms so a hung promise never eats the gesture.+ if (!document.hasFocus()) return copyViaTextarea(t);+ try {+ const ok = await Promise.race([+ navigator.clipboard.writeText(t).then(() => true),+ new Promise(res => setTimeout(() => res('timeout'), 800)),+ ]);+ if (ok === true) return true;+ } catch (e) {}+ return copyViaTextarea(t); } const DASH = '\u2014'; // the data sentinel inventory.rs emits for "unknown"; never shown to a person const isPhone = () => matchMedia('(hover: none)').matches || window.innerWidth < 720;@@ -1051,7 +1065,7 @@ async function buildSay() { const b = document.createElement('button'); b.className = 'say-phrase'; b.type = 'button'; b.setAttribute('data-tooltip', 'Copy this phrase to your clipboard, then paste it to your AI.'); b.innerHTML = '“' + escapeHtml(p).replace(/chip ?fetcher|\bcf\b/gi, m => '<em>' + m + '</em>') + '”';- b.onclick = async () => { try { await navigator.clipboard.writeText(p); cfToast('Copied: ' + p, { type: 'ok' }); } catch (e) { cfToast('Could not copy: ' + e.message, { type: 'err' }); } };+ b.onclick = async () => { if (await copyText(p)) cfToast('Copied: ' + p, { type: 'ok' }); else cfToast('Could not copy; select the phrase and copy it by hand.', { type: 'err' }); }; const t = document.createElement('button'); t.className = 'say-try'; t.type = 'button'; t.textContent = 'Try it'; if (TRY_IT_PARKED) { // John's ruling (2026-08-23): no live AI-thread staging for now, but the@@ -1070,7 +1084,7 @@ async function buildSay() { try { const d = await (await postJson('/api/say/try', { phrase: p })).json(); if (d.staged) cfToast('Staged in your AI thread' + (d.where ? ' (' + d.where + ')' : '') + ': ' + p + '. Press Enter there to send.', { type: 'ok' });- else { try { await navigator.clipboard.writeText(p); } catch (e) {} cfToast('Copied instead: ' + (d.reason || 'could not reach your AI thread') + '. Paste it into your AI.'); }+ else { await copyText(p); cfToast('Copied instead: ' + (d.reason || 'could not reach your AI thread') + '. Paste it into your AI.'); } } catch (e) { cfToast('Try failed: ' + e.message, { type: 'err' }); } finally { t.disabled = false; } };@@ -1255,7 +1269,7 @@ function render() { const phrase = 'fetch ' + q.toUpperCase() + ' with cf'; cards.innerHTML = '<div class="empty-state">Nothing in your library matches "' + escapeHtml(q) + '".<br>Not fetched yet? Ask your AI: <button class="btn" id="btn-fetch-q">Copy: "' + escapeHtml(phrase) + '"</button></div>'; const b = $id('btn-fetch-q');- if (b) b.onclick = async () => { try { await navigator.clipboard.writeText(phrase); cfToast('Copied: ' + phrase + '. Paste it to your AI.', { type: 'ok' }); } catch (e) {} };+ if (b) b.onclick = async () => { if (await copyText(phrase)) cfToast('Copied: ' + phrase + '. Paste it to your AI.', { type: 'ok' }); }; } else { cards.innerHTML = `<div class="empty-state">${LIB.chips.length ? 'No chips match these filters.' : 'No parts in the library yet.'}</div>`; }@@ -1769,8 +1783,8 @@ $id('btn-welcome-hide').onclick = async () => { }; $id('btn-welcome-example').onclick = async () => { const p = 'fetch STM32H743VIT6 with cf';- try { await navigator.clipboard.writeText(p); cfToast('Copied: ' + p + '. Paste it to your AI.', { type: 'ok' }); }- catch (e) { cfToast('Could not copy: ' + e.message, { type: 'err' }); }+ if (await copyText(p)) cfToast('Copied: ' + p + '. Paste it to your AI.', { type: 'ok' });+ else cfToast('Could not copy; type it into your AI instead.', { type: 'err' }); }; $id('btn-help').onclick = async () => { WELCOME_PINNED = !WELCOME_PINNED;