app
Adom Library
Public Made by Adomby adom
adom-lbr — the EDA library translator. Bring a component in from any supported EDA tool and convert it to any other (KiCad ⇄ Altium ⇄ EAGLE/Fusion) through one canonical adom-lbr JSON — symbol + footprint + 3D + pin↔pad map. Altium is written natively (no Altium needed). Includes the adom-lbr Manager: browse wiki components and view symbol/footprint/3D + the per-EDA layer stackup.
← Commit history
2.37.0: AI-progress status route (1.0.187+); one send path
10 files changed
+116−72
Cargo.lock+1−1@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "adom-lbr"-version = "2.36.1"+version = "2.37.0" dependencies = [ "altium-codec", "anyhow",
Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "adom-lbr"-version = "2.36.1"+version = "2.37.0" edition = "2021" description = "adom-lbr generator / EDA translator: the canonical adom-lbr JSON is the hub, with native converters to Altium (.SchLib/.PcbLib/.IntLib), EAGLE/Fusion (.lbr), and KiCad. Altium output is written natively — no Altium needed at conversion time."
VERSION+1−1@@ -1 +1 @@-2.36.1+2.37.0
bin/adom-lbr⋯ 1 unchanged line ⋯
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "adom-lbr",- "version": "2.36.1",+ "version": "2.37.0", "type": "app", "description": "adom-lbr \u2014 the EDA library translator. Bring a component in from any supported EDA tool and convert it to any other (KiCad \u21c4 Altium \u21c4 EAGLE/Fusion) through one canonical adom-lbr JSON \u2014 symbol + footprint + 3D + pin\u2194pad map. Altium is written natively (no Altium needed). Includes the adom-lbr Manager: browse wiki components and view symbol/footprint/3D + the per-EDA layer stackup.", "dependencies": {},
page.json+2−2@@ -1,6 +1,6 @@ { "slug": "adom-lbr",- "version": "2.36.1",+ "version": "2.37.0", "type": "app", "description": "adom-lbr — the EDA library translator. Bring a component in from any supported EDA tool and convert it to any other (KiCad ⇄ Altium ⇄ EAGLE/Fusion) through one canonical adom-lbr JSON — symbol + footprint + 3D + pin↔pad map. Altium is written natively (no Altium needed). Includes the adom-lbr Manager: browse wiki components and view symbol/footprint/3D + the per-EDA layer stackup.", "dependencies": {},@@ -88,4 +88,4 @@ "label": "Browse parts" } ]-}+}
skills/adom-lbr-dockbar/SKILL.md+17−4@@ -100,10 +100,23 @@ escalates to "stuck?" past 15 minutes), and newbie-graduation (localStorage key `adom-lbr:dash-graduation`, namespaced because the cloud proxy serves every port under one origin; dismissal / 3 green launches / first AI-driven action fold the scaffolding, the say strip last at 5).-"Send to AI" goes through `POST /api/say-send`: the backend opens a NEW-thread via the panel's AI-threads API (tag `dock:adom-lbr`) and submits --new threads only, resume stays parked; failure answers {ok:false, why}-and the page falls back to copying.+"Send to AI" belongs to the SHARED newbie-panel module, not to this app. The+module owns the one send implementation in the fleet; this server only proxies+three routes to the Hydrogen control API, same-origin, bytes unchanged in both+directions:++ GET /updater/status (the version gate)+ POST /ai-threads/run-prompt (120s: the tab create alone can take 60)+ GET /ai-threads/run-prompt/status (query string INTACT, polled once a second)++The third is new for Hydrogen 1.0.187+, which returns a `job` id in under a+second and never fills `delivered_to` again; all progress lives on the status+route. An app that forwards only the path asks about no job at all, and every+poll answers about nothing while still returning 200.++`POST /api/say-send` is GONE (2.37.0). It was a second, server-side send that+verified by `delivered_to`, so on 1.0.187+ it would have called every successful+send a failure. One send path, in the module. ## Say it, and Try it
src/dock.rs−48@@ -248,54 +248,6 @@ pub fn control_proxy_raw(path: &str, post_body: Option<&str>, timeout_s: u32) -> Ok(ProxyReply { status, content_type, body }) } -pub fn say_send(phrase: &str) -> Value {- if phrase.trim().is_empty() {- return json!({"ok": false, "why": "no phrase given"});- }- let base = control_api_base();-- // Gate NUMERICALLY on the updater's current version (a lexical compare- // passes 1.0.99 as newer than 1.0.142). The crash fix ships in 1.0.142;- // an older shell gets an honest refusal and the page's copy fallback.- if let Some(v) = curl_json(&format!("{base}/updater/status"), None, 4)- .and_then(|j| j.pointer("/updater/current_version").or_else(|| j.pointer("/current_version"))- .and_then(|x| x.as_str()).map(String::from))- {- let nums: Vec<u32> = v.trim_start_matches('v').split('.').filter_map(|p| p.parse().ok()).collect();- if nums.len() >= 3 && (nums[0], nums[1], nums[2]) < (1u32, 0u32, 142u32) {- return json!({"ok": false, "shellTooOld": true, "shellVersion": v,- "why": format!("This Hydrogen ({v}) predates the fix for a crash in the AI panel's send path; update to 1.0.142 or newer. Copied instead; paste it into any AI thread.")});- }- }-- // ONE call: /ai-threads/new never returns an id, so the create and the- // prompt ride together. 120 s, because the tab create alone can take 60.- let body = json!({- "new": true,- "prompt": phrase,- "submit": true,- "name": "Adom LBR - say it",- "tags": ["dock:adom-lbr"]- });- let Some(r) = curl_json(&format!("{base}/ai-threads/run-prompt"), Some(&body), 120) else {- // NEVER retry blind on a create timeout: a second call can land a- // second thread. Surface the failure and let the page copy instead.- return json!({"ok": false, "why": "the AI panel did not answer within 120 s; nothing was sent. Copied instead."});- };- // Verify by delivered_to, never by the thread's name: a correctly-named tab- // that delivered nothing, and an unnamed tab that delivered, both happened.- let delivered = r.get("delivered_to").and_then(|d| d.as_str()).filter(|d| !d.is_empty());- match delivered {- Some(d) => json!({"ok": true, "delivered_to": d}),- None => {- let why = r.get("_hints").and_then(|h| h.as_str())- .or_else(|| r.get("error").and_then(|e| e.as_str()))- .unwrap_or("the AI panel accepted the call but confirmed no delivery");- json!({"ok": false, "why": format!("{why}. Copied instead.")})- }- }-}- /// "Try it": stage the phrase, unsubmitted, into this app's own AI thread via /// the Hydrogen control API. Census first (open threads), history second /// (closed ones, resumed first), else tell the page to copy. Reachable from a
src/manage.rs+9−7@@ -132,7 +132,8 @@ fn path_known(path: &str) -> bool { matches!(path, "/" | "/health" | "/api/apply" | "/api/apply-progress" | "/api/status" | "/newbie-panel.js" | "/updater/status" | "/ai-threads/run-prompt"- | "/api/say" | "/api/say-send" | "/api/try" | "/api/settings"+ | "/ai-threads/run-prompt/status"+ | "/api/say" | "/api/try" | "/api/settings" | "/api/settings/reset" | "/api/claim-webview" | "/api/components") || (path.starts_with("/tour/step-") && path.ends_with(".mp3")) || path.ends_with("/favicon.svg") || path.ends_with("/favicon.ico")@@ -211,15 +212,16 @@ fn route_inner(method: &Method, url: &str, path: &str, body: &str) -> Response<C // create alone can take 60, and a blind retry lands a SECOND thread. (Method::Get, "/updater/status") => proxy_resp(dock::control_proxy_raw("/updater/status", None, 5)), (Method::Post, "/ai-threads/run-prompt") => proxy_resp(dock::control_proxy_raw("/ai-threads/run-prompt", Some(body), 120)),+ // Hydrogen 1.0.187+ splits the send in two: the POST returns a job id in well under+ // a second and NEVER fills delivered_to any more, so all progress lives here and is+ // polled once a second. FORWARD THE QUERY STRING: this is called as ?job=<uuid>, and+ // a proxy that forwards only the path answers about no job at all. Without the route+ // every poll 404s and the user gets no progress reporting; the module fails fast and+ // names it, but that is a diagnostic, not a substitute for wiring it.+ (Method::Get, "/ai-threads/run-prompt/status") => proxy_resp(dock::control_proxy_raw(url, None, 5)), (Method::Get, "/api/say") => json_resp(&json!({"status":"ok","say":dock::say_phrases(),"noun":"lbr"}).to_string()), // say-strip contract: Send to AI opens a NEW thread and submits (new // threads only; the resume path stays parked in /api/try).- (Method::Post, "/api/say-send") => {- let phrase = serde_json::from_str::<Value>(body).ok()- .and_then(|v| v.get("phrase").and_then(|p| p.as_str()).map(String::from))- .unwrap_or_default();- json_resp(&dock::say_send(&phrase).to_string())- } (Method::Post, "/api/try") => { let phrase = serde_json::from_str::<Value>(body).ok() .and_then(|v| v.get("phrase").and_then(|p| p.as_str()).map(String::from))
src/newbie-panel.js+84−7@@ -112,6 +112,23 @@ var TWO_COL_MIN = 700, PHRASE_BUDGET = 336; var MIN_API = [1, 0, 142]; + /* Hydrogen 1.0.187+ answers a send with a JOB and reports progress on+ * GET /ai-threads/run-prompt/status?job=<uuid>. Measured on the VM: tab visible ~0.1s,+ * session id ~2.5s, sent by 3-5s, a trivial answer done ~8s. One poll a second is the+ * right cadence for that shape, and it is an in-memory read on the other end. */+ var PHASE = {+ opening: 'Opening a new AI chat…',+ prefilled: 'Prompt ready…',+ submitted: 'Sent, waiting for the AI…',+ working: 'The AI is working…',+ needs_you: 'The AI needs you. Open the tab.',+ done: 'Done.',+ submit_failed: 'The prompt is in the tab. Press Enter there to send it.',+ submitted_unidentified:'Sent, but the AI never checked in.',+ submit_unconfirmed: 'Sent, but delivery was not confirmed.',+ still_working: 'The AI is still working. Open the tab.'+ };+ function css() { if (document.getElementById('anp-css')) return; var s = document.createElement('style');@@ -174,6 +191,9 @@ if (!toastHost) { toastHost = el('div', 'anp-toasts'); document.body.appendChild(toastHost); } var t = el('div', 'anp-toast ' + (kind || 'info'), esc(msg)); toastHost.appendChild(t);+ /* retextable, so a long send can narrate its phases in ONE toast instead of+ * stacking six of them on top of each other. */+ t.setText = function (next) { t.innerHTML = esc(next); return t; }; setTimeout(function () { t.remove(); }, ms || (kind === 'err' ? 7000 : 4000)); return t; }@@ -224,7 +244,7 @@ }) .catch(function () { return { ok: true, version: null, open: true }; }); }- function send(base, phrase, appName, slug) {+ function send(base, phrase, appName, slug, onProgress) { return gate(base).then(function (g) { if (!g.ok) throw new Error('Adom needs updating to send prompts (found ' + g.version + ', needs 1.0.142). Run: adom update'); var ctl = new AbortController(), timer = setTimeout(function () { ctl.abort(); }, 120000);@@ -241,12 +261,55 @@ .then(function (res) { clearTimeout(timer); var j = res.j || {};- /* THREE states, never two. delivered_to is an ARRAY; a thread that opened but did- * not confirm delivery is NOT a failure, and retrying it double-sends. */+ if (!(res.status >= 200 && res.status < 300))+ throw new Error((j._hints && j._hints.note) || j.error || ('AI threads returned ' + res.status));+ /* PRE-1.0.187 SHAPE, kept because older Hydrogen builds still answer this way.+ * delivered_to is an ARRAY; a thread that opened but did not confirm delivery is+ * NOT a failure, and retrying it double-sends. */ var dt = j.delivered_to, arr = Array.isArray(dt) ? dt : (dt ? [dt] : []); if (arr.length || (j.inject && j.inject.delivered_in_conversation)) return { state: 'delivered' };- if (res.status >= 200 && res.status < 300) return { state: 'opened', hint: j._hints && j._hints.note };- throw new Error((j._hints && j._hints.note) || j.error || ('AI threads returned ' + res.status));+ /* 1.0.187+ returns in well under a second with a JOB, not a delivery. ok:true means+ * the job STARTED. delivered_to is never filled on this path any more, so without+ * the poll below every send would report "delivery not confirmed" forever. */+ if (!j.job) return { state: 'opened', hint: j._hints && j._hints.note };+ var started = Date.now(), missing = false;+ return new Promise(function (resolve) {+ (function tick() {+ /* the deadline is checked in BOTH paths. Checking it only after a successful+ * poll leaves a dashboard whose server stops answering polling forever. */+ function again() {+ if (Date.now() - started > 120000)+ return resolve({ state: 'opened', hint: 'no confirmation within 120s' });+ setTimeout(tick, 1000);+ }+ fetch(base + '/ai-threads/run-prompt/status?job=' + encodeURIComponent(j.job))+ .then(function (r) {+ /* THE STATUS ROUTE IS A THIRD PROXY ROUTE. Dashboards proxy /updater/status+ * and POST /ai-threads/run-prompt only. If this one was never added, every+ * poll 404s and the user watches "Opening…" for the full 120s before a+ * hedge, which is worse than the behaviour this replaced. Fail FAST and say+ * exactly which route is missing instead of hanging. */+ if (r.status === 404) { missing = true; return null; }+ return r.json();+ })+ .then(function (st) {+ if (missing) return resolve({ state: 'opened', phase: 'no_status_route',+ hint: 'Your AI chat opened. This app does not proxy GET ' ++ '/ai-threads/run-prompt/status yet, so progress cannot be shown.' });+ var state = (st.job && st.job.state) || '';+ if (typeof onProgress === 'function') { try { onProgress(state, st.job); } catch (e) {} }+ /* working or later IS sent, for the user-facing toast. */+ if (state === 'working' || state === 'done' || state === 'needs_you' ||+ state === 'still_working') return resolve({ state: 'delivered', phase: state });+ if (state === 'submit_failed' || state === 'submitted_unidentified' ||+ state === 'submit_unconfirmed')+ return resolve({ state: 'opened', phase: state,+ hint: (st.job && (st.job.error || st.job.note)) || state });+ again();+ })+ .catch(again);+ })();+ }); }) .catch(function (e) { clearTimeout(timer);@@ -316,7 +379,10 @@ if (sd.disabled) return; sd.disabled = true; var pending = toast('Opening a new AI thread…', 'info', 120000);- send(base, phrase, appName, slug).then(function (r) {+ send(base, phrase, appName, slug, function (state, job) {+ if (PHASE[state]) pending.setText(PHASE[state]);+ if (typeof cfg.onProgress === 'function') { try { cfg.onProgress(state, job); } catch (e) {} }+ }).then(function (r) { pending.remove(); if (r.state === 'delivered') toast('Sent. Your AI thread has the prompt.', 'ok'); else toast('Thread opened, delivery not confirmed. Check your AI threads before sending again.', 'info', 8000);@@ -605,10 +671,21 @@ real !== 'ERR' && real !== 404 && real !== fake); }); })+ .then(function () {+ /* THE STATUS ROUTE, added for Hydrogen 1.0.187+. It is a THIRD proxy route and no+ * dashboard had it before 2.22.0, so it is the row most likely to be red. A GET with+ * a job id that cannot exist must still be ANSWERED (any non-404), because a 404 here+ * means the route was never proxied and progress can never be reported. */+ return fetch(base + '/ai-threads/run-prompt/status?job=__anp_probe__')+ .then(function (r) { return r.status; }, function () { return 'ERR'; })+ .then(function (st) {+ add('progress route wired', 'answers (not 404)', String(st), st !== 'ERR' && st !== 404);+ });+ }) .then(function () { return {ok: ok, rows: rows}; }); } root.AdomNewbiePanel = { mount: mount, toast: toast, selfCheck: selfCheck, copyText: copyText,- checkSend: checkSend, version: '2.21.0',+ checkSend: checkSend, version: '2.22.0', TWO_COL_MIN: TWO_COL_MIN, PHRASE_BUDGET: PHRASE_BUDGET }; })(typeof window !== 'undefined' ? window : this);