app
Parts Search
Public Made by Adomby adom
One search box across Mouser, DigiKey and JLCPCB: parallel queries, side-by-side product photos, and a Mouser-preferred recommendation that reasons around stock and lead time.
← Commit history
v1.3.3: Send to AI: 120s budget, delivery proven by delivered_to, honest recovery
7 files changed
+58−102
Cargo.lock+1−1@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "adom-parts-search"-version = "1.3.2"+version = "1.3.3" dependencies = [ "base64", "clap",
Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "adom-parts-search"-version = "1.3.2"+version = "1.3.3" edition = "2021" description = "Parts Search Electronics component search — CLI, backend HTTP service, and Hydrogen webview app" license = "MIT"
VERSION+1−1@@ -1 +1 @@-1.3.2+1.3.3
package.json+1−40@@ -38,44 +38,5 @@ "SKILL.md", "BUILD-SKILL.md", "CHANGELOG.md"- ],- "changelog": "Send to AI: 120s budget, delivery proven by delivered_to, honest recovery",- "discovery_triggers": [- "parts search",- "find part",- "source component",- "source part",- "compare distributors",- "compare vendors",- "Mouser or DigiKey",- "BOM sourcing",- "price this part",- "where can I get",- "check stock",- "parts availability",- "fastest delivery",- "cheapest part",- "component search",- "part lookup",- "price breaks"- ],- "discovery_pitch": "Search Mouser, DigiKey and JLCPCB in parallel with product photos; Mouser-preferred best-source pick. Say 'with parts search' to your AI.",- "sample_prompts": [- {- "label": "source an STM32F103RBT6",- "prompt": "source an STM32F103RBT6"- },- {- "label": "where can I get an ESP32-S3-WROOM cheape",- "prompt": "where can I get an ESP32-S3-WROOM cheapest and fastest"- },- {- "label": "compare Mouser, DigiKey and JLCPCB for a",- "prompt": "compare Mouser, DigiKey and JLCPCB for an LM555"- },- {- "label": "price this BOM across distributors",- "prompt": "price this BOM across distributors"- } ]-}+}
page.json+7−48@@ -36,22 +36,10 @@ "path": "docs/hero.png" }, "sample_prompts": [- {- "label": "source an STM32F103RBT6",- "prompt": "source an STM32F103RBT6"- },- {- "label": "where can I get an ESP32-S3-WROOM cheape",- "prompt": "where can I get an ESP32-S3-WROOM cheapest and fastest"- },- {- "label": "compare Mouser, DigiKey and JLCPCB for a",- "prompt": "compare Mouser, DigiKey and JLCPCB for an LM555"- },- {- "label": "price this BOM across distributors",- "prompt": "price this BOM across distributors"- }+ "source an STM32F103RBT6",+ "where can I get an ESP32-S3-WROOM cheapest and fastest",+ "compare Mouser, DigiKey and JLCPCB for an LM555",+ "price this BOM across distributors" ], "discovery_triggers": [ "parts search",@@ -66,41 +54,12 @@ "where can I get", "check stock", "parts availability",- "fastest delivery",- "cheapest part",- "component search",- "part lookup",- "price breaks"+ "fastest delivery" ],- "discovery_pitch": "Search Mouser, DigiKey and JLCPCB in parallel with product photos; Mouser-preferred best-source pick. Say 'with parts search' to your AI.",+ "discovery_pitch": "Search Mouser + DigiKey + JLCPCB in parallel from one interface, with product photos and a Mouser-preferred best-source pick.", "metadata": { "platform": "linux" }, "category": "tools",- "org": "adom",- "description": "One search box across Mouser, DigiKey and JLCPCB. Parallel queries, side-by-side product photos, and a Mouser-preferred recommendation that reasons around stock and lead time. Dock Bar ready: serve, health, settings, Say it.",- "dependencies": {- "adom/adom-mouser": "*",- "adom/adom-digikey": "*",- "adom/adom-jlcpcb": "*"- },- "needs_sudo": false,- "scripts": {- "install": "./install.sh",- "uninstall": "./uninstall.sh"- },- "files": [- "bin/**",- "skills/**",- "docs/icon.svg",- "dockbar.json",- "install.sh",- "uninstall.sh",- "package.json",- "README.md",- "SKILL.md",- "BUILD-SKILL.md",- "CHANGELOG.md"- ],- "changelog": "Send to AI: 120s budget, delivery proven by delivered_to, honest recovery"+ "org": "adom" }
publish.sh+13−3@@ -143,11 +143,21 @@ CHECK_TGZ=$(mktemp) fetch_tarball() { curl -fsSL -H "Authorization: Bearer $TOK" "$REG/api/packages/adom/adom-parts-search/$VERSION/tarball" -o "$CHECK_TGZ"; } # The registry can serve a just-superseded listing for a while after a publish # (observed on 1.1.3, 1.2.0 and 1.3.2); poll up to ~60s before believing it.-for _try in 1 2 3 4 5 6; do- fetch_tarball- tar tzf "$CHECK_TGZ" | grep -q "dockbar\.json" && break+# Observed on 1.1.3, 1.2.0, 1.3.2 and 1.3.3: the registry can serve a stale+# listing for well over a minute after a SUCCESSFUL publish. Poll up to 3+# minutes, and if it still looks wrong, say exactly what we saw rather than+# asserting a failure the publish did not actually have.+VERIFY_OK=""+for _try in $(seq 1 18); do+ if fetch_tarball && tar tzf "$CHECK_TGZ" 2>/dev/null | grep -q "dockbar\.json"; then VERIFY_OK=1; break; fi sleep 10 done+if [ -z "$VERIFY_OK" ]; then+ echo "--- registry still not serving a complete $VERSION tarball after 3 minutes."+ echo "--- last fetch: $(curl -s -o /dev/null -w 'http %{http_code}, %{size_download} bytes' -H "Authorization: Bearer $TOK" "$REG/api/packages/adom/adom-parts-search/$VERSION/tarball")"+ echo "--- members seen: $(tar tzf "$CHECK_TGZ" 2>/dev/null | tr '\n' ' ' | cut -c1-200)"+ fail "tarball verify never passed; the publish itself may still be fine, re-check with: curl -H \"Authorization: Bearer \$TOK\" $REG/api/packages/adom/adom-parts-search/$VERSION/tarball | tar tz"+fi if tar tzf "$CHECK_TGZ" | grep -q "page\.json"; then rm -f "$CHECK_TGZ"; fail "published tarball still contains page.json"; fi if ! tar tzf "$CHECK_TGZ" | grep -q "dockbar\.json"; then rm -f "$CHECK_TGZ"; fail "published tarball is missing dockbar.json"; fi rm -f "$CHECK_TGZ"
src/app/mod.rs+34−8@@ -231,10 +231,10 @@ fn hget(path: &str) -> Result<Value, String> { let agent = ureq::AgentBuilder::new().timeout(Duration::from_millis(8000)).build(); agent.get(&format!("{}{}", hydrogen_ctl(), path)).call().map_err(|e| e.to_string())?.into_json().map_err(|e| e.to_string()) }-/// Opening an AI thread drives the editor and legitimately takes 10-30s on a-/// busy workbench (every CDP touch serializes through one gate), so the-/// AI-threads calls get 45s. A short client timeout manufactures phantom-/// failures out of sends that actually worked.+/// Opening an AI thread drives the editor and the tab CREATE alone can take+/// 60s on a loaded workbench, so the AI-threads calls get 120s. A short+/// client timeout manufactures phantom failures out of sends that actually+/// worked, and worse, invites a blind retry that duplicates the thread. fn hpost_with(path: &str, body: Value, timeout_ms: u64) -> Result<(u16, Value), String> { let agent = ureq::AgentBuilder::new().timeout(Duration::from_millis(timeout_ms)).build(); match agent.post(&format!("{}{}", hydrogen_ctl(), path)).send_json(body) {@@ -258,6 +258,20 @@ fn shell_version() -> Option<(u32, u32, u32)> { /// everything earlier, including the widely-installed 1.0.141, can crash the /// user's whole shell on a send. const MIN_SHELL: (u32, u32, u32) = (1, 0, 142);+/// The panel's `_hints` teach the recovery for a late/failed open. Surface+/// that text verbatim instead of inventing wording.+fn hints_text(v: &Value) -> Option<String> {+ let h = v.get("_hints")?;+ let text = match h {+ Value::String(s) => s.clone(),+ Value::Array(a) => a.iter().filter_map(|x| x.as_str()).collect::<Vec<_>>().join(" "),+ Value::Object(o) => o.values().filter_map(|x| x.as_str()).collect::<Vec<_>>().join(" "),+ _ => return None,+ };+ let text = text.trim();+ if text.is_empty() { None } else { Some(text.chars().take(300).collect()) }+}+ /// Send to AI. ONE call: /ai-threads/run-prompt with `new: true` opens the /// thread AND delivers the prompt. The old open-then-target flow could never /// work, because /ai-threads/new returns no id (a thread has no sid until its@@ -279,12 +293,24 @@ fn say_send(phrase: &str) -> Value { "name": "Parts Search - say it", "tags": [APP_TAG, "dock:adom-parts-search"], });- match hpost_with("/ai-threads/run-prompt", body, 45_000) {- Ok((code, v)) if (200..300).contains(&code) && v.get("ok").and_then(|b| b.as_bool()) != Some(false) => {+ match hpost_with("/ai-threads/run-prompt", body, 120_000) {+ // Delivery proof is `delivered_to`, never the thread's name: the+ // composite deterministically ADOPTS an idle empty Claude tab when one+ // exists, and a staged name can lag or miss on an adopted tab.+ Ok((code, v)) if (200..300).contains(&code) && v.get("ok").and_then(|b| b.as_bool()) != Some(false) && v.get("delivered_to").map(|d| !d.is_null()).unwrap_or(false) => { json!({"ok": true, "delivered_to": v.get("delivered_to").cloned().unwrap_or(Value::Null)}) }- Ok((code, v)) => json!({"ok": false, "why": format!("The AI panel refused the send ({}): {}", code, v.get("error").and_then(|e| e.as_str()).unwrap_or("no reason given"))}),- Err(_) => json!({"ok": false, "why": "The AI panel did not answer in 45 seconds. Copied the phrase instead."}),+ // Anything else is NOT proven delivered. Never retry blind, that+ // duplicates the thread: surface the panel's own _hints text, which is+ // written to say what actually happened and how to recover.+ Ok((code, v)) => {+ let hint = hints_text(&v);+ json!({"ok": false, "why": match hint {+ Some(h) => format!("The AI panel did not confirm delivery ({}). {}", code, h),+ None => format!("The AI panel did not confirm delivery ({}): {}. Copied the phrase instead.", code, v.get("error").and_then(|e| e.as_str()).unwrap_or("no reason given")),+ }})+ }+ Err(_) => json!({"ok": false, "why": "The AI panel did not answer within 2 minutes, so the send is unconfirmed. It may still land: check your AI threads before sending again, because sending twice opens two threads. Copied the phrase instead."}), } }