← Commit history

1.3.7: real taskbar screenshots in Settings, cf shorthand everywhere (header, strip, glossary), self-healing doctor --fix for broken sister installers, AI skills facet covers all 3 skills + 8 vendor site guides, status probes cached 5 min (live only while Settings is open), tuckable getting-started card with the STM32H743 example, honest library filter

John Lauer ·9e2cb39fcd ·1mo ago ·parent 17402ae
7 files changed +184−39
Cargo.lock+1−1
@@ -4,7 +4,7 @@ version = 4  [[package]] name = "adom-chip-fetcher"-version = "1.3.6"+version = "1.3.7" dependencies = [  "anyhow",  "clap",
Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "adom-chip-fetcher"-version = "1.3.6"+version = "1.3.7" 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/cli.rs+59−6
@@ -2209,7 +2209,13 @@ fn doctor_cmd(fix: bool, port: u16) -> Result<()> {         if state != "ok" { bad += 1; }         let detail = if key == "dashboard" {             match crate::dock::running_version(port) {-                Some(v) => format!("v{v} serving on {}", crate::dock::direct_url(port)),+                Some(v) if v == crate::dock::VERSION => format!("v{v} serving on {}", crate::dock::direct_url(port)),+                Some(v) => {+                    bad += 1;+                    mark = "WARN";+                    hints::print_hint(&format!("an older server (v{v}) still owns port {port}; `adom-chip-fetcher serve --print-url` retires and replaces it"));+                    format!("v{v} serving, but this binary is v{}", crate::dock::VERSION)+                }                 None => { bad += 1; mark = "WARN"; "not running".to_string() }             }         } else {@@ -2238,11 +2244,32 @@ fn doctor_cmd(fix: bool, port: u16) -> Result<()> {     }     if fix {         if !f.missing_tools.is_empty() {-            println!("\n== installing the missing sister tools via the wiki");-            let st = std::process::Command::new("sh")-                .args(["-c", "export PATH=\"$HOME/.local/bin:$PATH\"; adom-wiki pkg install adom/adom-chip-fetcher"])-                .status()?;-            println!("{}", if st.success() { "OK: wiki install finished" } else { "ERROR: wiki install failed" });+            println!("\n== installing the missing sister tools");+            for tool in &f.missing_tools {+                println!("-- adom-wiki pkg install adom/{tool}");+                let _ = std::process::Command::new("sh")+                    .args(["-c", &format!("export PATH=\"$HOME/.local/bin:$PATH\"; adom-wiki pkg install adom/{tool} 2>&1 | tail -2")])+                    .status();+                if on_path_bin(tool) { println!("OK: {tool} installed"); continue; }+                // Self-heal: some sister packages ship the binary in the tarball+                // but their install.sh forgets to install it (adom-step), or+                // installs with sudo to /usr/local (adom-concur). Take the+                // binary straight from the extracted payload; issues are filed+                // on those pages so the workaround can retire.+                if let Some(payload) = payload_binary(tool) {+                    let home = std::env::var("HOME").unwrap_or_default();+                    let dest = std::path::PathBuf::from(&home).join(".local/bin").join(tool);+                    match std::fs::copy(&payload, &dest) {+                        Ok(_) => {+                            let _ = std::process::Command::new("chmod").arg("0755").arg(&dest).status();+                            println!("OK: {tool} self-healed from the package payload ({} -> {})", payload.display(), dest.display());+                        }+                        Err(e) => println!("ERROR: {tool}: payload copy failed: {e}"),+                    }+                } else {+                    println!("ERROR: {tool} is still missing and its package ships no binary in the payload. Its page has an open issue; nothing chip-fetcher can do locally.");+                }+            }         }         let home = std::env::var("HOME").unwrap_or_default();         if !std::path::Path::new(&home).join(".claude/skills/adom-chip-fetcher/SKILL.md").is_file() {@@ -2326,3 +2353,29 @@ fn settings_cmd(op: SettingsOp) -> Result<()> {         SettingsOp::Reset => { crate::settings::reset()?; println!("OK: settings restored to defaults"); Ok(()) }     } }+++fn on_path_bin(bin: &str) -> bool {+    let home = std::env::var("HOME").unwrap_or_default();+    std::path::Path::new(&home).join(".local/bin").join(bin).is_file()+        || std::env::var("PATH").unwrap_or_default().split(':').any(|d| std::path::Path::new(d).join(bin).is_file())+}++/// A binary shipped inside a sister package's extracted payload+/// (`~/project/adom_modules/adom/<tool>/{,bin/}<tool>`), for self-healing+/// installers that forgot to install it.+fn payload_binary(tool: &str) -> Option<std::path::PathBuf> {+    let home = std::env::var("HOME").unwrap_or_default();+    let base = std::path::PathBuf::from(&home).join("project/adom_modules/adom").join(tool);+    for cand in [base.join(tool), base.join("bin").join(tool)] {+        if cand.is_file() {+            // ELF magic so a directory README named like the tool never lands on PATH.+            if let Ok(bytes) = std::fs::read(&cand) {+                if bytes.starts_with(b"\x7fELF") || bytes.starts_with(b"#!") {+                    return Some(cand);+                }+            }+        }+    }+    None+}
src/dock.rs+44−12
@@ -399,7 +399,30 @@ fn rank(state: &str) -> u8 { /// Whether a desktop is connected through Adom Bridge. `adom-bridge targets` /// is the authoritative probe; it answers in well under a second when the /// relay is up and the 2s timeout bounds the dead case.-fn bridge_targets() -> Option<usize> {+fn bridge_targets(force: bool) -> Option<usize> {+    // The one non-trivial probe (spawns a process, talks to the relay).+    // Cached 5 minutes: a desktop appearing or leaving is a slow event, and+    // the dock/dashboard polling must not turn into a background process+    // treadmill (John, 2026-08-23). `?force=1` (the settings panel, doctor)+    // bypasses the cache.+    static CACHE: Mutex<Option<(Instant, Option<usize>)>> = Mutex::new(None);+    if !force {+        if let Ok(c) = CACHE.lock() {+            if let Some((t, v)) = c.as_ref() {+                if t.elapsed() < Duration::from_secs(300) {+                    return *v;+                }+            }+        }+    }+    let v = bridge_targets_probe();+    if let Ok(mut c) = CACHE.lock() {+        *c = Some((Instant::now(), v));+    }+    v+}++fn bridge_targets_probe() -> Option<usize> {     let bin = if on_path("adom-bridge") { "adom-bridge" } else { "adom-bridge-cli" };     let out = std::process::Command::new("timeout").args(["2", bin, "targets"]).output().ok()?;     let text = String::from_utf8_lossy(&out.stdout);@@ -409,6 +432,10 @@ fn bridge_targets() -> Option<usize> {     v.get("count").and_then(|c| c.as_u64()).map(|c| c as usize) } +pub fn build_facets() -> Facets {+    build_facets_forced(true)+}+ pub struct Facets {     pub leds: serde_json::Map<String, Value>,     pub state: String,@@ -417,7 +444,7 @@ pub struct Facets {  /// Build every facet. Cheap checks only (PATH stats, one bounded bridge /// probe, the library listing), cached by `status_json`.-pub fn build_facets() -> Facets {+pub fn build_facets_forced(force: bool) -> Facets {     let mut leds = serde_json::Map::new();      // 1. the dashboard itself: answering means ok.@@ -426,15 +453,20 @@ pub fn build_facets() -> Facets {         led("Dashboard", "ok", format!("v{VERSION} serving"), "The dashboard answers /api/status; the dock polls it every 15s."),     ); -    // 2. the chip-fetcher skill on this container.+    // 2. the AI skills on this container: the main skill (+playbooks), the+    // dock skill, and the community-extensible per-vendor site pack.     let home = std::env::var("HOME").unwrap_or_default();-    let skill = std::path::Path::new(&home).join(".claude/skills/adom-chip-fetcher/SKILL.md");+    let sk = |n: &str| std::path::Path::new(&home).join(".claude/skills").join(n).join("SKILL.md").is_file();+    let sites_dir = std::path::Path::new(&home).join(".claude/skills/chip-fetcher-sites/sites");+    let site_count = std::fs::read_dir(&sites_dir).map(|rd| rd.flatten().filter(|e| e.path().is_dir()).count()).unwrap_or(0);+    let have = [sk("adom-chip-fetcher"), sk("adom-chip-fetcher-dock"), sk("chip-fetcher-sites")];+    let n_have = have.iter().filter(|b| **b).count();     leds.insert(         "skill".into(),-        if skill.is_file() {-            led("AI skill", "ok", "chip-fetcher skill installed".into(), "The AI reads ~/.claude/skills/adom-chip-fetcher/SKILL.md to know how to drive this app.")+        if n_have == 3 {+            led("AI skills", "ok", format!("3 skills, {site_count} vendor site guides"), "adom-chip-fetcher (the sourcing ladder + 14 playbooks), adom-chip-fetcher-dock (the Dock Bar), and chip-fetcher-sites (one crawl guide per vendor website; anyone can contribute a new site).")         } else {-            led("AI skill", "warn", "skill not installed".into(), "Run `adom-chip-fetcher install` so your AI learns the sourcing ladder and the rules.")+            led("AI skills", "warn", format!("{n_have}/3 skills installed"), "Run `adom-chip-fetcher install` (or `adom-wiki pkg install adom/adom-chip-fetcher`) so your AI has the sourcing ladder, the dock guide and the per-site crawl playbooks.")         },     ); @@ -444,13 +476,13 @@ pub fn build_facets() -> Facets {     leds.insert(         "tools".into(),         if missing.is_empty() {-            led("Sister tools", "ok", format!("{total}/{total} present"), "Symbol, footprint, 3D, Fusion export, pin-1 sign-off, thumbnails and the distributor backends are all on PATH.")+            led("Sister tools", "ok", format!("{total}/{total} installed automatically"), "The wiki installs these as dependencies of chip fetcher; nothing to manage. This light only exists to catch a broken install.")         } else {             led(                 "Sister tools",                 "warn",-                format!("{}/{} present, missing {}", total - missing.len(), total, missing.join(", ")),-                "Run `adom-wiki pkg install adom/adom-chip-fetcher` (it installs every dependency), or `adom-chip-fetcher doctor --fix`.",+                format!("{}/{} landed, missing {}", total - missing.len(), total, missing.join(", ")),+                "These install automatically with chip fetcher; a missing one means its own package misfired. `adom-chip-fetcher doctor --fix` re-installs each missing tool and self-heals from the downloaded payload.",             )         },     );@@ -458,7 +490,7 @@ pub fn build_facets() -> Facets {     // 4. the desktop bridge (pup lives there; no desktop means no scraping).     leds.insert(         "bridge".into(),-        match bridge_targets() {+        match bridge_targets(force) {             Some(n) if n > 0 => led("Desktop", "ok", format!("{n} box(es) on Adom Bridge"), "pup drives the chip-fetcher Chrome profile on a connected desktop."),             Some(_) => led("Desktop", "warn", "no desktop connected".into(), "Open Adom Hydrogen on your desktop; chip-fetcher scrapes vendor sites through pup there. The library view works without it."),             None => led("Desktop", "warn", "adom-bridge not answering".into(), "Install the bridge CLI (`adom-wiki pkg install adom/adom-bridge`) and open Adom Hydrogen on your desktop."),@@ -528,7 +560,7 @@ pub fn status_json(force: bool) -> String {             }         }     }-    let f = build_facets();+    let f = build_facets_forced(force);     let body = json!({         "ok": true,         "app": APP,
src/jobs.rs+2−2
@@ -37,9 +37,9 @@ fn registry() -> std::sync::MutexGuard<'static, Option<HashMap<u64, Arc<Mutex<Jo pub fn command_for(action: &str, body: &Value) -> Option<(String, Vec<String>)> {     let sh = |script: &str| Some(("sh".to_string(), vec!["-c".to_string(), format!("export PATH=\"$HOME/.local/bin:$PATH\"; {script}")]));     match action {-        "install_tools" => sh("adom-wiki pkg install adom/adom-chip-fetcher 2>&1; echo; adom-chip-fetcher doctor 2>&1"),+        "install_tools" => sh("adom-chip-fetcher doctor --fix 2>&1"),         "install_skill" => sh("adom-chip-fetcher install 2>&1"),-        "refresh_status" => sh("adom-chip-fetcher doctor 2>&1"),+        "refresh_status" => sh("adom-chip-fetcher doctor 2>&1; true"),         "import_logins" => {             let hosts = body                 .get("hostFilter")
src/settings.rs+1
@@ -21,6 +21,7 @@ pub const SCHEMA: &[(&str, &str, &[&str], &str)] = &[     ("auto_zoom", "false", &[], "Auto-zoom the active card"),     ("auto_select_active", "true", &[], "Follow the active lookup"),     ("show_stock_badges", "true", &[], "Stock and lifecycle badges on cards"),+    ("show_welcome", "true", &[], "Getting-started card on the dashboard"),     ("activity_stale_seconds", "90", &[], "Heartbeat considered stale after (seconds)"), ]; 
src/ui/index.html+76−17
@@ -56,6 +56,7 @@ code{font-family:var(--mono);font-size:.92em} header{flex:0 0 44px;height:44px;display:flex;align-items:center;gap:12px;padding:0 12px 0 16px;border-bottom:1px solid var(--border);background:var(--surface);position:relative;z-index:var(--z-toolbar);font-family:var(--brand-heading)} .brand{display:flex;align-items:center;gap:8px;color:var(--accent);font-size:13px;font-weight:600;letter-spacing:.01em;cursor:default;flex-shrink:0;white-space:nowrap} .brand svg.logo{width:16px;height:16px;flex-shrink:0;color:var(--text)}+.app-cf{color:var(--text-2);font-weight:500} .brand .app-subtitle{color:var(--text-2);font-weight:500} .brand .sep{color:var(--text-3);font-weight:400} .vsep{width:1px;height:18px;background:var(--border);flex-shrink:0}@@ -137,7 +138,8 @@ header{flex:0 0 44px;height:44px;display:flex;align-items:center;gap:12px;paddin  /* ---- Welcome (first run) ---- */ .welcome{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:18px 20px;margin-bottom:16px;max-width:760px}-.welcome h2{font:600 18px var(--brand-heading);color:var(--text);margin-bottom:6px}+.welcome h2{font:600 18px var(--brand-heading);color:var(--text)}+.welcome-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:6px} .welcome p{color:var(--text-2);margin-bottom:12px;max-width:62ch} .welcome ol{list-style:none;display:flex;flex-direction:column;gap:10px} .welcome li{display:flex;gap:12px;align-items:flex-start;line-height:1.5}@@ -417,7 +419,9 @@ dialog.settings{width:min(680px,92vw);overflow:hidden} .choice-set{border:none;margin:0;padding:0;display:flex;flex-wrap:wrap;gap:.75rem} .choice{flex:1 1 240px;display:flex;flex-direction:column;gap:.35rem;padding:.6rem;cursor:pointer;position:relative;background:var(--surface);border:1px solid var(--border);border-radius:8px} .choice input{position:absolute;opacity:0;pointer-events:none}-.choice-shot{display:flex;align-items:center;justify-content:center;height:72px;border:1px dashed var(--border);border-radius:6px;background:var(--bg);color:var(--text-2);font:11.5px var(--brand-body)}+.choice-shot{display:flex;align-items:center;justify-content:center;min-height:72px;border:1px dashed var(--border);border-radius:6px;background:var(--bg);color:var(--text-2);font:11.5px var(--brand-body);overflow:hidden;padding:6px}+.choice-shot img{display:block;max-width:100%;height:auto;border-radius:4px}+.choice-shot.loaded{border-style:solid} .choice-name{font:600 .875rem var(--brand-heading);color:var(--text)} .choice-desc{font:.75rem/1.5 var(--brand-body);color:var(--text-2)} .choice:hover{border-color:var(--accent);background:var(--elevated)}@@ -508,7 +512,7 @@ dialog.settings{width:min(680px,92vw);overflow:hidden} <header>   <div class="brand" id="brand" tabindex="0" aria-describedby="app-info">     <svg class="logo" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="7" y="7" width="10" height="10" rx="1.5"/><path d="M9 7V4M12 7V4M15 7V4M9 20v-3M12 20v-3M15 20v-3M7 9H4M7 12H4M7 15H4M20 9h-3M20 12h-3M20 15h-3"/></svg>-    <span class="app-name">Chip Fetcher</span>+    <span class="app-name">Chip Fetcher <span class="app-cf">(cf)</span></span>     <span class="sep">&middot;</span>     <span class="app-subtitle">manufacturer CAD bundles</span>   </div>@@ -528,6 +532,7 @@ dialog.settings{width:min(680px,92vw);overflow:hidden}     <button class="iconbtn" id="btn-sources" aria-pressed="false" aria-label="Sourcing order" data-tooltip="Sourcing order: the exact priority chip fetcher uses to search the internet for electronics data. Manufacturer site first, then the Adom data tools (parts-search, Mouser, DigiKey, JLCPCB), then the manual CAD-crawl skills (SnapMagic, CSE, Ultra Librarian), with LCSC last. Click again to return to the cards."><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 6h16M4 12h16M4 18h10"/></svg></button>     <button class="iconbtn" id="btn-arch" aria-label="Architecture map" data-tooltip="Architecture map: every tool chip fetcher depends on. Local CLIs that run in your container, native Rust libraries, remote service containers called over HTTPS, and the sub-apps you may also need to modify. Open this before editing chip fetcher."><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M9 3L3 6v15l6-3 6 3 6-3V3l-6 3-6-3z"/><path d="M9 3v15M15 6v15"/></svg></button>     <button class="iconbtn" id="btn-reveal-lib" aria-label="Library folder" data-tooltip="Reveal the chip fetcher library folder in the VS Code Explorer sidebar. Opens ~/project/chip-fetcher/library/ so you can see every chip directory."><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7.5a2 2 0 0 1 2-2h4l2 2.5h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7.5z"/></svg></button>+    <button class="iconbtn" id="btn-help" aria-pressed="false" aria-label="Getting started" data-tooltip="Show the getting-started card: the three steps, the example cf phrase, and where the component lights live."><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M9.5 9a2.5 2.5 0 1 1 3.4 2.33c-.7.28-1.4.84-1.4 1.67v.5"/><path d="M12 17h.01"/></svg></button>     <button class="iconbtn" id="btn-settings" aria-label="Settings" data-tooltip="Settings: components and their install buttons, the taskbar progress mode, dashboard behaviour, and vendor logins."><!-- the shared Adom cog (assets/icons/settings-cog.svg), inlined so it follows currentColor --><svg viewBox="0 0 24 24" aria-hidden="true"><path fill="currentColor" d="M12 15.5A3.5 3.5 0 0 1 8.5 12A3.5 3.5 0 0 1 12 8.5a3.5 3.5 0 0 1 3.5 3.5a3.5 3.5 0 0 1-3.5 3.5m7.43-2.53c.04-.32.07-.64.07-.97s-.03-.66-.07-1l2.11-1.63c.19-.15.24-.42.12-.64l-2-3.46c-.12-.22-.39-.31-.61-.22l-2.49 1c-.52-.39-1.06-.73-1.69-.98l-.37-2.65A.506.506 0 0 0 14 2h-4c-.25 0-.46.18-.5.42l-.37 2.65c-.63.25-1.17.59-1.69.98l-2.49-1c-.22-.09-.49 0-.61.22l-2 3.46c-.13.22-.07.49.12.64L4.57 11c-.04.34-.07.67-.07 1s.03.65.07.97l-2.11 1.66c-.19.15-.25.42-.12.64l2 3.46c.12.22.39.3.61.22l2.49-1.01c.52.4 1.06.74 1.69.99l.37 2.65c.04.24.25.42.5.42h4c.25 0 .46-.18.5-.42l.37-2.65c.63-.26 1.17-.59 1.69-.99l2.49 1.01c.22.08.49 0 .61-.22l2-3.46c.12-.22.07-.49-.12-.64z"/></svg></button>   </div>   <div class="hover-popover" id="app-info" role="dialog" aria-label="App information">@@ -544,7 +549,7 @@ dialog.settings{width:min(680px,92vw);overflow:hidden} </header>  <div class="filterbar" role="toolbar" aria-label="Library filters">-  <input type="search" id="q" placeholder="Search part number, vendor or package" data-tooltip="Filter the cards by part number, vendor or package. Typing narrows the grid as you go.">+  <input type="search" id="q" placeholder="Filter your library: part number, vendor, package" data-tooltip="Filters the cards already in your library. Fetching a NEW part happens through your AI, not here: copy a phrase from the strip above.">   <select id="vendor" data-tooltip="Show only one vendor's parts."><option value="">All vendors</option></select>   <select id="status" data-tooltip="Show only full bundles, only stubs, or cards missing a specific file.">     <option value="">All status</option>@@ -561,17 +566,20 @@ dialog.settings{width:min(680px,92vw);overflow:hidden}     <section class="say" id="say" aria-labelledby="sayTitle">       <div class="say-head">         <h2 id="sayTitle">Say it to your AI</h2>-        <span class="say-sub">You do not drive this dashboard by hand. You ask your AI, using the words <b>chip fetcher</b>. Click a phrase to copy it, then paste it to your AI.</span>+        <span class="say-sub">You do not drive this dashboard by hand. You ask your AI, using the words <b>chip fetcher</b>, or just <b>cf</b> for short. Click a phrase to copy it, then paste it to your AI.</span>       </div>       <div class="say-list" id="sayList"></div>     </section>     <div class="leds" id="leds" aria-label="Component status"></div>     <section class="welcome" id="welcome" hidden>-      <h2>Welcome to Chip Fetcher</h2>-      <p>Your library is empty, so this page has nothing to show yet. Three steps get the first card on screen.</p>+      <div class="welcome-head">+        <h2>Getting started</h2>+        <button class="iconbtn" id="btn-welcome-hide" aria-label="Tuck this away" data-tooltip="Tuck the getting-started card away. Bring it back any time with the ? button in the header."><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M18 6L6 18M6 6l12 12"/></svg></button>+      </div>+      <p>Three steps get the first card on screen. Once cf feels familiar, tuck this card away with the X; the ? in the header brings it back.</p>       <ol>         <li><span class="num">1</span><div>Check the component lights above and fix anything that is not green. The Components card in Settings has an Install button for each one.<br><button class="btn" id="btn-welcome-settings">Open Settings to Components</button></div></li>-        <li><span class="num">2</span><div>Ask your AI one of the phrases above, for example "fetch STM32F103C8T6 with chip fetcher". The AI drives pup on your desktop to find the manufacturer's files.</div></li>+        <li><span class="num">2</span><div>Ask your AI for a chip by part number, using the word <b>cf</b>. Try it right now with a serious one, the STM32H743 (a 480 MHz Cortex-M7):<br><button class="btn btn-primary" id="btn-welcome-example">Copy: "fetch STM32H743VIT6 with cf"</button></div></li>         <li><span class="num">3</span><div>Cards appear here as parts are fetched. Each card carries the datasheet, the KiCad symbol, the footprint and the 3D model, with one-click open and send-to-EDA buttons.</div></li>       </ol>     </section>@@ -592,8 +600,8 @@ dialog.settings{width:min(680px,92vw);overflow:hidden}      <section class="settings-card" id="card-components">       <h3 class="card-title">Components <span class="card-status" id="stComponents"></span></h3>-      <p class="card-desc-s">Chip fetcher is five pieces working together, and each light below is recomputed from the box every fifteen seconds. The <b>Dashboard</b> is this page and its server. The <b>AI skill</b> is the file at <code>~/.claude/skills/adom-chip-fetcher/SKILL.md</code> that teaches your AI how to drive the app. The <b>Sister tools</b> are thirteen command-line programs that chip fetcher calls: adom-symbol, adom-footprint, adom-step, adom-lbr, adom-chipsmith, adom-chip-thumbnailer, adom-concur, adom-ds2sf, adom-sfconvert, adom-parts-search, adom-mouser, adom-digikey and adom-jlcpcb. The <b>Desktop</b> is a connected Adom Bridge box where pup runs Chrome to fetch files. The <b>Library</b> is the folder of fetched parts behind the cards.</p>-      <p class="card-desc-s">Install runs the real command in the background and streams its output into a console under the row, line by line, ending with the exit code. Sister tools install with <code>adom-wiki pkg install adom/adom-chip-fetcher</code>; the skill installs with <code>adom-chip-fetcher install</code>. Nothing here is silent.</p>+      <p class="card-desc-s">Chip fetcher (cf) is five pieces working together. The <b>Dashboard</b> is this page and its server. The <b>AI skills</b> are what your AI reads to drive the app: the main skill with its 14 sourcing playbooks, the Dock Bar skill, and <b>chip-fetcher-sites</b>, a pack of per-vendor crawl guides (SnapEDA, Mouser, DigiKey, Ultra Librarian and more) that any Adom user can extend with a site they have figured out. The <b>Sister tools</b> are thirteen command-line programs the wiki installs automatically as cf's dependencies (adom-symbol, adom-footprint, adom-step, adom-lbr, adom-chipsmith, adom-chip-thumbnailer, adom-concur, adom-ds2sf, adom-sfconvert, adom-parts-search, adom-mouser, adom-digikey, adom-jlcpcb); nothing to manage, the light only exists to catch a broken install, and it shows how much machinery one cf prompt drives. The <b>Desktop</b> is a connected Adom Bridge box where pup runs Chrome to fetch files. The <b>Library</b> is the folder of fetched parts behind the cards.</p>+      <p class="card-desc-s">These lights are cheap cached readings, not a background loop: file checks answer from disk, the desktop probe runs at most every five minutes, and only while this panel is open does the page re-read everything live (every fifteen seconds, so an Install you just ran turns green in front of you). Install buttons run the real command in the background and stream its output into a console under the row, ending with the exit code. Nothing here is silent.</p>       <div id="compRows"></div>     </section> @@ -606,13 +614,13 @@ dialog.settings{width:min(680px,92vw);overflow:hidden}           <legend id="taskbarLabel" class="set-check-label" style="padding:0;margin-bottom:.35rem">What the dashboard's taskbar button does while a lookup is running.</legend>           <label class="choice" data-mode="indeterminate" data-selected="false">             <input type="radio" name="taskbar_progress" value="indeterminate">-            <span class="choice-shot">screenshot pending</span>+            <span class="choice-shot"><img src="" data-shot="taskbar-indeterminate.png" alt="The dashboard's taskbar button with a moving indeterminate progress marquee under the icon"></span>             <span class="choice-name">Indeterminate bar</span>             <span class="choice-desc">A moving marquee under the icon for as long as the lookup runs. Says "working", never a made-up percentage.</span>           </label>           <label class="choice" data-mode="off" data-selected="false">             <input type="radio" name="taskbar_progress" value="off">-            <span class="choice-shot">screenshot pending</span>+            <span class="choice-shot"><img src="" data-shot="taskbar-off.png" alt="The same taskbar button with no progress indication of any kind"></span>             <span class="choice-name">Off</span>             <span class="choice-desc">The taskbar button never changes. You rely on the header pill and the card ring on this page.</span>           </label>@@ -924,7 +932,7 @@ async function loadLibrary() {   const vendors = [...new Set(LIB.chips.map(c => c.vendor).filter(v => v && v !== '?' && v !== DASH))].sort();   vsel.innerHTML = '<option value="">All vendors</option>' + vendors.map(v => `<option value="${escapeHtml(v)}">${escapeHtml(v)}</option>`).join('');   vsel.value = keep;-  $id('welcome').hidden = LIB.chips.length > 0;+  $id('welcome').hidden = !(WELCOME_PINNED || (LIB.chips.length === 0 && SETTINGS.show_welcome !== false));   updateSubject(); render(); renderProjectTabs();   if (firstLoad) loadProjects();   if (ACTIVE_MPN) { scrollActiveCardIntoView(); autoZoomIfEnabled(ACTIVE_MPN); }@@ -1031,7 +1039,7 @@ async function buildSay() {     const item = document.createElement('div'); item.className = 'say-item';     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 = '&ldquo;' + escapeHtml(p).replace(/chip ?fetcher/gi, m => '<em>' + m + '</em>') + '&rdquo;';+    b.innerHTML = '&ldquo;' + escapeHtml(p).replace(/chip ?fetcher|\bcf\b/gi, m => '<em>' + m + '</em>') + '&rdquo;';     b.onclick = async () => { try { await navigator.clipboard.writeText(p); cfToast('Copied: ' + p, { type: 'ok' }); } catch (e) { cfToast('Could not copy: ' + e.message, { type: 'err' }); } };     // TRY_IT_PARKED (ship night 2026-08-23, dock-bar thread): the Try it path     // POSTs /ai-threads/run-prompt, and tonight ah parks its AI Threads backend@@ -1225,7 +1233,18 @@ function render() {     if (s === 'no-pdf' && c.has_pdf) return false;     return true;   });-  if (!filtered.length) { cards.innerHTML = `<div class="empty-state">${LIB.chips.length ? 'No chips match these filters.' : 'No parts in the library yet.'}</div>`; return; }+  if (!filtered.length) {+    const q = ($id('q').value || '').trim();+    if (LIB.chips.length && q) {+      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) {} };+    } else {+      cards.innerHTML = `<div class="empty-state">${LIB.chips.length ? 'No chips match these filters.' : 'No parts in the library yet.'}</div>`;+    }+    return;+  }   if (ACTIVE_MPN && !filtered.find(c => c.mpn === ACTIVE_MPN)) {     const activeChip = chipByMpn(ACTIVE_MPN);     if (activeChip) {@@ -1651,6 +1670,22 @@ async function renderSources() {   host.innerHTML = `<div class="srcwrap"><div class="srctitle">Sourcing order: how chip fetcher searches the internet</div><div class="srcsub">The real, code-backed order, served from <code>/api/sources</code> so it can never drift from behaviour. Three concerns, each searched top to bottom. <b>Manufacturer first for CAD; the Adom data tools (keys baked in, always work) for metadata; the manual-crawl skills are the fallback; LCSC is last.</b></div>${concerns}</div>`; } +// Taskbar choice-card screenshots: real captures of the dashboard's taskbar+// button on a Windows desktop (settings-layout: never fabricate). The PNGs are+// native device pixels from a 150%-scaled display; render at+// naturalWidth / captureDpr so they sit at the physical size of the real+// taskbar. If a capture is missing, the dashed "screenshot pending" box stays.+const SHOT_CAPTURE_DPR = 1.5; // the display scale the captures were taken at+function loadChoiceShots() {+  document.querySelectorAll('.choice-shot img[data-shot]').forEach(img => {+    if (img.dataset.wired) return; img.dataset.wired = '1';+    img.onload = () => { img.style.width = (img.naturalWidth / SHOT_CAPTURE_DPR) + 'px'; img.closest('.choice-shot').classList.add('loaded'); };+    img.onerror = () => { img.closest('.choice-shot').textContent = 'screenshot pending'; };+    img.src = API_BASE + '/assets/shots/' + img.dataset.shot + '?v=' + (SERVER_VERSION || '');+    if (img.complete && img.naturalWidth) img.onload();+  });+}+ // ---- Settings modal (settings-layout skill): native dialog, four dismissals, optimistic bounded saves. ---- const dlg = $id('dlgSettings'); const ENUM_LABEL = { taskbar_progress: { indeterminate: 'Indeterminate bar', off: 'Off' } };@@ -1699,7 +1734,7 @@ async function saveSetting(key, value) {   } finally { clearTimeout(timer); } } function openSettings(cardId) {-  renderSettings(); renderComponents(); loadCredentials();+  renderSettings(); renderComponents(); loadCredentials(); loadChoiceShots(); loadStatus(true);   dlg.style.margin = ''; dlg.style.left = ''; dlg.style.top = '';   if (!dlg.open) dlg.showModal();   dlg.focus();@@ -1709,6 +1744,25 @@ function openSettings(cardId) { } $id('btn-settings').onclick = () => openSettings(); $id('btn-welcome-settings').onclick = () => openSettings('card-components');+let WELCOME_PINNED = false; // the ? button shows the card even with a full library+$id('btn-welcome-hide').onclick = async () => {+  WELCOME_PINNED = false;+  $id('welcome').hidden = true;+  try { await postJson('/api/settings', { show_welcome: false }); SETTINGS.show_welcome = false; } catch (e) {}+  cfToast('Tucked away. The ? button in the header brings it back.', { type: 'ok' });+};+$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' }); }+};+$id('btn-help').onclick = async () => {+  WELCOME_PINNED = !WELCOME_PINNED;+  if (WELCOME_PINNED && SETTINGS.show_welcome === false) { try { await postJson('/api/settings', { show_welcome: true }); SETTINGS.show_welcome = true; } catch (e) {} }+  $id('welcome').hidden = !WELCOME_PINNED && !(LIB && LIB.chips.length === 0 && SETTINGS.show_welcome !== false);+  if (!$id('welcome').hidden) $id('welcome').scrollIntoView({ block: 'nearest' });+  $id('btn-help').setAttribute('aria-pressed', String(WELCOME_PINNED));+}; $id('btnCloseSettings').onclick = () => dlg.close(); dlg.addEventListener('close', reportUi); dlg.addEventListener('click', e => {@@ -1846,7 +1900,12 @@ loadSettings(); loadLibrary().catch(e => { $id('cards').innerHTML = '<div class="empty-state">Could not load the library: ' + escapeHtml(e.message) + '</div>'; }); api('/state').then(s => { if (s && s.version) { SERVER_VERSION = s.version; $id('popVersion').textContent = 'v' + s.version; $id('settingsVersion').textContent = 'v' + s.version; } }).catch(() => {}); pollActivity(); activityTimer = setInterval(pollActivity, 1000);-setInterval(() => loadStatus(), 15000);+// Ambient: a cached /api/status read every 60 s (the server holds its own+// 5-minute cache on the one expensive probe). Live recompute (?force=1)+// only while the settings panel is open, every 15 s, so Install results+// turn green in front of the user without a background process treadmill.+setInterval(() => { if (dlg.open) loadStatus(true); }, 15000);+setInterval(() => { if (!dlg.open) loadStatus(); }, 60000); reportUi(); </script> </body>