From 08a2a5fe8e10df55483c22be3605647930fba948 Mon Sep 17 00:00:00 2001 From: Salem Sayed Abdel Gawad Date: Tue, 15 Sep 2026 11:15:19 +0300 Subject: [PATCH] Fix the Advisor switch on iOS and release 0.1.3 bb shows plugin composer actions only while the mobile composer is expanded, and collapses it once the editor loses focus. Tapping the switch with the keyboard open blurred the editor, so the composer collapsed and unmounted the switch before the tap registered. Presses no longer take focus from the editor, and a touch toggles on pointerup like bb's own submit button, ignoring the click that follows. The retry button shares the same handling. Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 5 +++ app.test.tsx | 53 ++++++++++++++++++++++++++++++ app.tsx | 75 ++++++++++++++++++++++++++++++++++++++++--- dist/app.js | 2 +- dist/app.meta.json | 2 +- dist/server.meta.json | 2 +- package-lock.json | 4 +-- package.json | 2 +- 8 files changed, 134 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 059dda5..21af1a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 0.1.3 + +- Make the Advisor switch respond to taps on iPhone and in the iOS home-screen app. Tapping it with the keyboard open used to dismiss the keyboard and minimize the composer without changing the switch. +- Keep focus in the editor when the switch or its retry button is pressed, so the keyboard stays open and the switch stays in view after it changes. + ## 0.1.2 - Bound model discovery calls to five seconds and keep healthy provider results when another provider fails or stalls. diff --git a/app.test.tsx b/app.test.tsx index 9249def..62157c4 100644 --- a/app.test.tsx +++ b/app.test.tsx @@ -140,6 +140,59 @@ describe("advisor thread switch", () => { ]); }); + function flippableSwitch() { + let enabled = true; + return renderSlot(toggleSlot, {}, { + composer: threadComposer, + rpc: { + threadToggle: () => ({ enabled, override: enabled, globalEnabled: true }), + setThreadToggle: (input) => { + enabled = z.object({ enabled: z.boolean() }).parse(input).enabled; + return { enabled, override: enabled, globalEnabled: true }; + }, + }, + }); + } + const touch = { pointerType: "touch", pointerId: 1, isPrimary: true }; + + // On a phone the host collapses the composer, unmounting this switch, as soon + // as the editor loses focus — before iOS gets around to delivering the click. + it("toggles on a touch tap without taking focus from the editor", async () => { + const slot = flippableSwitch(); + const control = await q(slot).findByRole("switch") as HTMLButtonElement; + + // A cancelled pointerdown or mousedown is what leaves focus in the editor. + expect(fireEvent.pointerDown(control, touch)).toBe(false); + expect(fireEvent.mouseDown(control)).toBe(false); + fireEvent.pointerUp(control, touch); + await waitFor(() => expect(control.getAttribute("aria-checked")).toBe("false")); + await waitFor(() => expect(control.disabled).toBe(false)); + + // The click the browser synthesizes after the tap must not flip it back. + fireEvent.click(control, { detail: 1 }); + expect( + slot.inspection.rpcCalls.filter((call) => call.method === "setThreadToggle"), + ).toEqual([ + { method: "setThreadToggle", input: { threadId: "t1", enabled: false } }, + ]); + }); + + it("ignores a touch that drags off the switch but not a later mouse click", async () => { + const slot = flippableSwitch(); + const control = await q(slot).findByRole("switch") as HTMLButtonElement; + + fireEvent.pointerDown(control, touch); + fireEvent.pointerMove(control, { ...touch, clientX: 40 }); + fireEvent.pointerUp(control, { ...touch, clientX: 40 }); + fireEvent.click(control, { detail: 1 }); + expect(slot.inspection.rpcCalls.filter((call) => call.method === "setThreadToggle")) + .toEqual([]); + + fireEvent.pointerDown(control, { pointerType: "mouse", pointerId: 2, isPrimary: true }); + fireEvent.click(control, { detail: 1 }); + await waitFor(() => expect(control.getAttribute("aria-checked")).toBe("false")); + }); + it("snaps back instead of claiming a switch the server rejected", async () => { const slot = renderSlot(toggleSlot, {}, { composer: threadComposer, diff --git a/app.tsx b/app.tsx index 0fc7c71..27c30c7 100644 --- a/app.tsx +++ b/app.tsx @@ -433,6 +433,69 @@ function badgeStanding( }; } +/** Finger travel beyond which a touch is a scroll or drag rather than a tap. */ +const TAP_SLOP_PX = 10; + +function travel(start: { x: number; y: number }, event: React.PointerEvent) { + return Math.hypot(event.clientX - start.x, event.clientY - start.y); +} + +/** + * Press handling for a button in the composer's action row. On a phone the host + * only renders plugin actions while the composer is expanded, and collapses it + * once the editor loses focus: a tap that moves focus dismisses the keyboard + * and unmounts the button before iOS delivers the click. So a press never takes + * focus, and a touch acts on `pointerup` like the host's own submit button, + * dropping the click that trails it. Keyboard activation clicks with `detail` + * 0 and still acts. + */ +function useComposerActionPress(onPress: () => void) { + const tap = useRef<{ pointerId: number; x: number; y: number } | null>(null); + const touchPress = useRef(false); + + return { + onPointerDown: (event: React.PointerEvent) => { + tap.current = null; + touchPress.current = event.pointerType === "touch"; + if (!touchPress.current || !event.isPrimary || event.button !== 0) return; + event.preventDefault(); + tap.current = { pointerId: event.pointerId, x: event.clientX, y: event.clientY }; + }, + onPointerMove: (event: React.PointerEvent) => { + const start = tap.current; + if (start?.pointerId === event.pointerId && travel(start, event) > TAP_SLOP_PX) { + tap.current = null; + } + }, + onPointerCancel: () => { + // The browser took the gesture, usually to scroll; no click follows. + tap.current = null; + touchPress.current = false; + }, + onPointerUp: (event: React.PointerEvent) => { + const start = tap.current; + tap.current = null; + if (start?.pointerId !== event.pointerId) return; + const box = event.currentTarget.getBoundingClientRect(); + const inside = + event.clientX >= box.left && + event.clientX <= box.right && + event.clientY >= box.top && + event.clientY <= box.bottom; + if (inside && travel(start, event) <= TAP_SLOP_PX) onPress(); + }, + onMouseDown: (event: React.MouseEvent) => { + event.preventDefault(); + }, + onClick: (event: React.MouseEvent) => { + const afterTouch = touchPress.current; + touchPress.current = false; + if (afterTouch && event.detail > 0) return; + onPress(); + }, + }; +} + /** Keep state scoped to the thread even when the host reuses a composer. */ function AdvisorThreadToggle() { const { scope } = useComposer(); @@ -469,6 +532,11 @@ function ThreadAdvisorSwitch({ threadId }: { threadId: string }) { setSaving(false); } }, [rpc, threadId, state, saving, reload]); + const togglePress = useComposerActionPress(() => void toggle()); + const retryPress = useComposerActionPress(() => { + setWriteError(null); + void reload(); + }); const problem = writeError ?? error; if (problem) { @@ -477,10 +545,7 @@ function ThreadAdvisorSwitch({ threadId }: { threadId: string }) { type="button" title={`Advisor switch unavailable (${problem}). Click to retry.`} aria-label="Advisor switch unavailable, click to retry" - onClick={() => { - setWriteError(null); - void reload(); - }} + {...retryPress} className="inline-flex h-7 shrink-0 items-center rounded-md border border-dashed border-border px-1.5 text-subtle-foreground hover:bg-state-hover" > @@ -503,7 +568,7 @@ function ThreadAdvisorSwitch({ threadId }: { threadId: string }) { aria-checked={enabled} aria-label={hint} disabled={saving} - onClick={() => void toggle()} + {...togglePress} className={`inline-flex h-7 shrink-0 items-center gap-1.5 rounded-md border px-1.5 hover:bg-state-hover ${ enabled ? "border-border text-foreground" diff --git a/dist/app.js b/dist/app.js index ed4f504..7f1714e 100644 --- a/dist/app.js +++ b/dist/app.js @@ -1 +1 @@ -var z=globalThis.__bbPluginRuntime;if(z==null||z.react==null)throw new Error('Cannot load "react": this bundle must be loaded by the BB app, which provides the shared plugin runtime (globalThis.__bbPluginRuntime).');var L=z.react,Se="default"in L?L.default:L,{Activity:Te,Children:Ie,Component:Ee,Fragment:Pe,Profiler:_e,PureComponent:$e,StrictMode:Le,Suspense:Me,act:De,cache:Be,cacheSignal:qe,captureOwnerStack:Fe,cloneElement:He,createContext:ze,createElement:Ve,createRef:Ye,forwardRef:We,isValidElement:je,lazy:Oe,memo:Ue,startTransition:Ge,unstable_useCacheRefresh:Ke,use:Qe,useActionState:Je,useCallback:A,useContext:Xe,useDebugValue:Ze,useDeferredValue:et,useEffect:_,useEffectEvent:tt,useId:nt,useImperativeHandle:rt,useInsertionEffect:ot,useLayoutEffect:st,useMemo:M,useOptimistic:at,useReducer:it,useRef:ne,useState:b,useSyncExternalStore:lt,useTransition:dt,version:ut}=L;async function V(e,t,s,u){let i=new AbortController,c=u?AbortSignal.any([i.signal,u]):i.signal,m,p=new Promise((h,v)=>{m=()=>v(c.reason),c.aborted?m():c.addEventListener("abort",m,{once:!0})}),w=setTimeout(()=>i.abort(new Error(s)),t);try{return await Promise.race([Promise.resolve().then(()=>(c.throwIfAborted(),e(c))),p])}finally{clearTimeout(w),c.removeEventListener("abort",m)}}var Y=globalThis.__bbPluginRuntime;if(Y==null||Y.pluginSdkApp==null)throw new Error('Cannot load "@get-bb/plugin-sdk/app": this bundle must be loaded by the BB app, which provides the shared plugin runtime (globalThis.__bbPluginRuntime).');var D=Y.pluginSdkApp,mt="default"in D?D.default:D,{Markdown:re,ThreadChat:oe,UrlLink:gt,definePluginApp:se,experimental_BranchPicker:bt,experimental_Diff:vt,experimental_FileLink:ft,experimental_Icon:ht,experimental_NewThreadComposer:xt,experimental_PermissionModePicker:yt,experimental_ProviderIcon:wt,experimental_ProviderModelPicker:Nt,experimental_SourceCode:kt,experimental_useAppPanel:Ct,experimental_useBranches:Rt,experimental_useCheckoutState:At,experimental_useCodeTheme:St,experimental_useFixedTabTarget:Tt,experimental_useProviders:It,experimental_useSidebarThreadActions:Et,experimental_useSidebarThreadPullRequest:Pt,experimental_useSidebarThreadSplit:_t,experimental_useSidebarThreads:$t,useBbContext:Lt,useBbNavigate:W,useComposer:j,useComposerView:Mt,useRealtime:O,useRealtimeConnectionState:ae,useRpc:S,useSettings:Dt}=D;var U=globalThis.__bbPluginRuntime;if(U==null||U.jsxRuntime==null)throw new Error('Cannot load "react/jsx-runtime": this bundle must be loaded by the BB app, which provides the shared plugin runtime (globalThis.__bbPluginRuntime).');var B=U.jsxRuntime,qt="default"in B?B.default:B,{Fragment:E,jsx:n,jsxs:a}=B;var X="reviews",pe={unreviewed:"Not reviewed",waiting:"Waiting for completed turn",pending:"Review pending",approved:"Approved","changes-requested":"Changes requested",decided:"User decided",unavailable:"Review unavailable"},me={fixed:"You marked it fixed","not-an-issue":"You said it isn't an issue","wont-fix":"You decided not to fix it"};function ge(e,t){return e.resolvedAt!==null?{text:`${e.decision===""?"You dismissed it":me[e.decision]} ${P(e.resolvedAt,t)}`,tone:"text-subtle-foreground"}:e.closedAt!==null?{text:`Advisor re-checked and closed it${e.closedSeq===null?"":` at turn ${e.closedSeq}`}`,tone:"text-success-foreground"}:e.continuedAt!==null?{text:`Corrective follow-up started ${P(e.continuedAt,t)}`,tone:"text-subtle-foreground"}:e.repeatCount>1?{text:`Re-raised ${e.repeatCount}\xD7 \u2014 not addressed`,tone:"text-destructive-text"}:e.sentAt!==null?{text:`Sent to the agent ${P(e.sentAt,t)}`,tone:"text-subtle-foreground"}:{text:"Queued \u2014 goes to the agent with your next message",tone:"text-subtle-foreground"}}var ie={pass:0,nit:1,concern:2,blocker:3},be={blocker:"text-destructive-text border-surface-destructive-border bg-surface-destructive",concern:"text-warning-text border-warning/30 bg-warning/10",nit:"text-readback-foreground border-border bg-surface-recessed",pass:"text-success-foreground border-success/30 bg-success/10"},Z="inline-flex items-center gap-1.5 rounded-full border px-1.5 py-px text-2xs font-semibold uppercase tracking-wide whitespace-nowrap",I={blocker:"Must fix",concern:"Should fix",nit:"Minor",pass:"No issues",unavailable:"Didn't run",reviewing:"Review pending",waiting:"Waiting",decided:"Decided"},T={blocker:"The advisor found something it thinks must be fixed before this work is finished.",concern:"The advisor found a real problem worth fixing, but not a blocking one.",nit:"A small suggestion. Safe to ignore.",pass:"The advisor reviewed this turn and found nothing actionable.",unavailable:"The review did not run, so nothing was checked. This is not an approval.",reviewing:"A review is running right now. Nothing has been checked yet.",waiting:"Advisor will review when the current agent turn has a completed answer.",decided:"The user overruled or accepted the latest finding. Advisor did not verify a fix."};function P(e,t){let s=Math.max(0,Math.round((t-e)/1e3));if(s<60)return"just now";let u=Math.round(s/60);if(u<60)return`${u}m ago`;let i=Math.round(u/60);if(i<24)return`${i}h ago`;let c=Math.round(i/24);return c<7?`${c}d ago`:new Date(e).toLocaleDateString()}function ve(e=6e4){let[t,s]=b(()=>Date.now());return _(()=>{let u=setInterval(()=>s(Date.now()),e);return()=>clearInterval(u)},[e]),t}function de({severity:e,muted:t,children:s}){return a("span",{title:T[e],className:`${Z} ${be[e]} ${t?"opacity-60":""}`,children:[n("span",{className:"size-1.5 shrink-0 rounded-full bg-current"}),s??I[e]]})}function fe({children:e}){return a("span",{title:T.unavailable,className:`${Z} border-dashed border-border bg-transparent text-subtle-foreground`,children:[n("span",{className:"size-1.5 shrink-0 rounded-full bg-current"}),e??I.unavailable]})}var he={blocker:"M8 2h8l6 6v8l-6 6H8l-6-6V8zM12 8v5M12 16.5h.01",concern:"M12 3 22 20H2zM12 10v4M12 17.5h.01",nit:"M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18M12 8v5M12 16.5h.01",pass:"M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10zm-3-10 2 2 4-4",unavailable:"M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18M6 6l12 12",reviewing:"M11 4a7 7 0 1 0 0 14 7 7 0 0 0 0-14M16 16l4 4",waiting:"M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18M12 7v5l3 2",decided:"M5 12l4 4L19 6M12 3a9 9 0 1 0 0 18"};function J({standing:e,className:t}){return n("svg",{className:t,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round","aria-hidden":"true",children:n("path",{d:he[e]})})}function q(e,t){let s=S(),[u,i]=b(null),[c,m]=b(null),p=A(async()=>{if(e!==null)try{m(null),i(await s.call(t,{threadId:e}))}catch(f){m(f instanceof Error?f.message:String(f))}},[s,t,e]);_(()=>{p()},[p]),O("thread-changed",A(f=>{(typeof f=="object"&&f!==null?f.threadId:void 0)===e&&p()},[e,p]));let w=ae(),[h,v]=b(w==="connected");return _(()=>{if(w!=="connected"){v(!1);return}h||(v(!0),p())},[w,h,p]),{data:u,error:c,reload:p}}function xe({threadId:e,isCompactViewport:t}){let s=W(),{data:u}=q(e,"threadBadge");if(!u)return null;let i=ye(u);return i?a("button",{type:"button",title:i.label,"aria-label":i.label,onClick:()=>s.openThreadPanel({actionId:X}),className:`inline-flex h-7 items-center gap-1.5 rounded-md border px-2 text-xs hover:bg-state-hover ${i.tone}`,children:[n(J,{standing:i.kind,className:"size-3.5 shrink-0"}),t?null:n("span",{children:I[i.kind]}),i.repeatCount>1?a("span",{className:"text-2xs font-semibold tabular-nums opacity-85",children:["\xD7",i.repeatCount]}):null]}):null}var G="text-muted-foreground border-border bg-surface-recessed",K="border-dashed border-border bg-transparent text-subtle-foreground",le="text-destructive-text border-surface-destructive-border bg-surface-destructive";function ye(e){if(e.lifecycle==="waiting")return{kind:"waiting",label:T.waiting,tone:K,repeatCount:0};if(e.lifecycle==="pending"||e.reviewing)return{kind:"reviewing",label:T.reviewing,tone:K,repeatCount:0};if(e.lifecycle==="decided")return{kind:"decided",label:T.decided,tone:G,repeatCount:0};if(e.latestIsUnavailable)return{kind:"unavailable",label:`Advisor did not run: ${e.latestUnavailableReason??"the review failed"}. This is not an approval.`,tone:K,repeatCount:0};let t=e.open;if(t){let u=t.repeatCount>1?` Flagged ${t.repeatCount} times and still unresolved.`:"";return{kind:t.severity,label:`Advisor: ${I[t.severity]}. ${t.summary}${u} ${T[t.severity]}`,tone:t.severity==="blocker"?le:G,repeatCount:t.repeatCount}}let s=e.latest;return s?{kind:s.severity,label:`Advisor: ${I[s.severity]}. ${s.summary} ${T[s.severity]}`,tone:s.severity==="blocker"?le:G,repeatCount:s.repeatCount}:null}function we(){let{scope:e}=j();return e.kind!=="thread"?null:n(Ne,{threadId:e.threadId},e.threadId)}function Ne({threadId:e}){let t=S(),{data:s,error:u,reload:i}=q(e,"threadToggle"),[c,m]=b(null),[p,w]=b(!1),[h,v]=b(null);O("advisor-settings-changed",A(()=>{i()},[i]));let f=A(async()=>{if(s===null||p)return;let x=!s.enabled;m(x),w(!0),v(null);try{await t.call("setThreadToggle",{threadId:e,enabled:x})}catch(C){v(C instanceof Error?C.message:String(C))}finally{await i(),m(null),w(!1)}},[t,e,s,p,i]),o=h??u;if(o)return n("button",{type:"button",title:`Advisor switch unavailable (${o}). Click to retry.`,"aria-label":"Advisor switch unavailable, click to retry",onClick:()=>{v(null),i()},className:"inline-flex h-7 shrink-0 items-center rounded-md border border-dashed border-border px-1.5 text-subtle-foreground hover:bg-state-hover",children:n(J,{standing:"unavailable",className:"size-3.5 shrink-0"})});if(s===null)return null;let d=c??s.enabled,k=d?"Advisor on":"Advisor off";return a("span",{className:"group relative inline-flex shrink-0",children:[a("button",{type:"button",role:"switch","aria-checked":d,"aria-label":k,disabled:p,onClick:()=>{f()},className:`inline-flex h-7 shrink-0 items-center gap-1.5 rounded-md border px-1.5 hover:bg-state-hover ${d?"border-border text-foreground":"border-dashed border-border text-subtle-foreground"}`,children:[n(J,{standing:"pass",className:"size-3.5 shrink-0"}),n("span",{"aria-hidden":"true",className:`relative h-4 w-7 shrink-0 rounded-full border transition-colors ${d?"border-success bg-success":"border-border bg-surface-recessed"}`,children:n("span",{className:`absolute left-px top-px size-3 rounded-full bg-background transition-transform ${d?"translate-x-3":"translate-x-0"}`})})]}),n("span",{role:"tooltip",className:"pointer-events-none absolute bottom-full left-1/2 z-50 mb-1.5 -translate-x-1/2 whitespace-nowrap rounded-md border border-border bg-card px-2 py-1 text-2xs text-foreground opacity-0 shadow-md transition-opacity group-hover:opacity-100 group-focus-within:opacity-100",children:k})]})}function ke(){let e=j(),t=W(),s=S(),u=e.scope,i=u.kind==="thread"?u.threadId:null,{data:c,error:m,reload:p}=q(i,"pendingAdvice"),[w,h]=b(!1),[v,f]=b(!1),[o,d]=b(null);if(!i)return null;let k=o??m;if(k)return a("div",{className:"rounded-lg border border-border bg-card px-4 py-3 text-xs",children:[a("p",{className:"text-destructive-text",children:["Advisor status unavailable \u2014 any pending review will still be sent to the agent. (",k,")"]}),n("div",{className:"mt-2 flex justify-end",children:n("button",{type:"button",onClick:()=>{d(null),p()},className:"h-7 rounded-md border border-border bg-card px-2.5 text-xs text-foreground hover:bg-state-hover",children:"Retry"})})]});let x=c?.advice??null;if(!x)return null;let C=x.repeatCount>1;return a("div",{className:"rounded-lg border border-border bg-card px-4 py-3 text-xs text-muted-foreground",children:[a("div",{className:"flex flex-wrap items-center gap-2",children:[n(de,{severity:x.severity,children:C?`${I[x.severity]} \xB7 ${x.repeatCount}\xD7`:I[x.severity]}),n("h3",{className:"min-w-0 flex-1 text-sm font-semibold text-foreground",children:"Advisor reviewed your last turn"})]}),a("p",{className:"mt-2",children:[n("span",{className:"font-medium text-foreground",children:x.summary}),C?a("span",{className:"text-destructive-text",children:[" ","The agent was told this before and did not resolve it."]}):null]}),n("p",{className:"mt-1 text-2xs text-subtle-foreground",children:"Start a corrective turn now, or it will be shared with the agent on your next message."}),a("div",{className:"mt-3 flex flex-wrap items-center justify-end gap-2",children:[n("button",{type:"button",disabled:w,onClick:()=>{h(!0),d(null),s.call("dismissAdvice",{threadId:i,adviceId:x.id}).then(p).catch(y=>{d(y instanceof Error?y.message:String(y))}).finally(()=>h(!1))},className:"h-7 rounded-md border border-transparent px-2.5 text-xs text-muted-foreground hover:bg-state-hover disabled:opacity-50",children:"Dismiss"}),n("button",{type:"button",onClick:()=>t.openThreadPanel({actionId:X}),className:"h-7 rounded-md border border-border bg-card px-2.5 text-xs text-foreground hover:bg-state-hover",children:"View review"}),n("button",{type:"button",disabled:v,onClick:()=>{f(!0),d(null),s.call("continueFinding",{threadId:i,reviewId:x.id}).then(y=>{if(!y.started&&y.reason!=="already-started")throw new Error("This finding is no longer open.");return p()}).catch(y=>{d(y instanceof Error?y.message:String(y))}).finally(()=>f(!1))},className:"h-7 rounded-md border border-surface-destructive-border bg-surface-destructive px-2.5 text-xs font-medium text-destructive-text hover:opacity-90 disabled:opacity-50",children:v?"Starting\u2026":"Fix in new turn"})]})]})}var $="h-7 rounded-md border border-border bg-card px-2.5 text-xs text-foreground hover:bg-state-hover disabled:cursor-not-allowed disabled:opacity-50";function Q({threadId:e,review:t,expanded:s,now:u,onToggle:i,onResolved:c}){let m=S(),[p,w]=b(!1),[h,v]=b(null),[f,o]=b(""),[d,k]=b(!1),[x,C]=b(!1),[y,g]=b(null),r=t.advisorThreadId,l=t.repeatCount>1,R=t.resolvedAt!==null||t.closedAt!==null,ee=ge(t,u),ue=A(()=>{w(N=>!N),!(h!==null||!r)&&m.call("reviewerThreadAvailable",{threadId:e,advisorThreadId:r}).then(N=>v(N.available)).catch(()=>v(!1))},[m,e,r,h]),te=A((N,F)=>{k(!0),g(null),m.call("resolveFinding",{threadId:e,chainId:t.chainId,resolved:N,reason:N?f.trim():"",decision:F}).then(()=>{o(""),c()}).catch(H=>{g(H instanceof Error?H.message:String(H))}).finally(()=>k(!1))},[m,e,t.chainId,f,c]),ce=A(()=>{C(!0),g(null),m.call("continueFinding",{threadId:e,reviewId:t.id}).then(N=>{if(!N.started&&N.reason!=="already-started")throw new Error("This finding is no longer open.");c()}).catch(N=>{g(N instanceof Error?N.message:String(N))}).finally(()=>C(!1))},[m,e,t.id,c]);return a("div",{className:`border-b border-border last:border-b-0 ${s?"bg-surface-recessed":""}`,children:[a("button",{type:"button",onClick:i,"aria-expanded":s,className:`relative w-full px-3.5 py-3 text-left hover:bg-state-hover ${l&&!R?"pl-5":""}`,children:[l&&!R?n("span",{"aria-hidden":"true",className:"absolute inset-y-1.5 left-0 w-0.5 rounded-sm bg-destructive/60"}):null,a("span",{className:"flex flex-wrap items-center gap-2",children:[n(de,{severity:t.severity,muted:R}),n("span",{className:`min-w-0 text-sm font-medium ${R?"text-muted-foreground line-through decoration-1":"text-foreground"}`,children:t.summary}),l?a("span",{title:`Raised ${t.repeatCount} times`,className:`${Z} border-border bg-surface-recessed normal-case text-muted-foreground`,children:[t.repeatCount,"\xD7"]}):null]}),a("span",{className:"mt-1.5 flex flex-wrap gap-x-2.5 gap-y-1 text-2xs text-subtle-foreground",children:[n("span",{className:ee.tone,title:t.resolvedReason||void 0,children:ee.text}),n("span",{"aria-hidden":"true",children:"\xB7"}),l?a("span",{title:`First raised at timeline sequence ${t.firstSourceSeq}`,children:["first flagged ",P(t.firstCreatedAt,u)]}):n("span",{title:`Timeline sequence ${t.sourceSeq}`,children:P(t.createdAt,u)}),t.model?a(E,{children:[n("span",{"aria-hidden":"true",children:"\xB7"}),a("span",{children:[t.model,t.reasoningLevel&&t.reasoningLevel!=="default"?` \xB7 ${t.reasoningLevel}`:""]})]}):null]})]}),s?a("div",{className:"px-3.5 pb-3",children:[t.details.trim().length>0?n("div",{className:"text-xs text-muted-foreground",children:n(re,{content:t.details})}):n("p",{className:"text-xs text-subtle-foreground",children:"No further detail was given."}),t.resolvedAt!==null&&t.resolvedReason?a("p",{className:"mt-2 text-2xs text-subtle-foreground",children:["Your note: ",t.resolvedReason]}):null,t.severity!=="pass"?n("div",{className:"mt-2.5 flex flex-wrap items-center gap-2",children:R?n("button",{type:"button",disabled:d,onClick:()=>te(!1,"not-an-issue"),className:$,children:"Reopen"}):a(E,{children:[n("button",{type:"button",disabled:x||t.continuedAt!==null,onClick:ce,className:"h-7 rounded-md border border-surface-destructive-border bg-surface-destructive px-2.5 text-xs font-medium text-destructive-text hover:opacity-90 disabled:opacity-50",children:t.continuedAt!==null?"Follow-up started":x?"Starting\u2026":"Fix in new turn"}),n("input",{value:f,onChange:N=>o(N.currentTarget.value),maxLength:500,placeholder:"Note (optional)","aria-label":`Note about ${t.summary}`,className:"h-7 min-w-0 flex-1 rounded-md border border-input bg-background px-2 text-xs text-foreground placeholder:text-subtle-foreground"}),[["not-an-issue","Not an issue"],["wont-fix","Won't fix"]].map(([N,F])=>n("button",{type:"button",disabled:d,onClick:()=>te(!0,N),className:$,children:F},N))]})}):null,y?n("p",{className:"mt-1.5 text-2xs text-destructive-text",children:y}):null,r?a(E,{children:[n("button",{type:"button",onClick:ue,"aria-expanded":p,className:`mt-2 ${$}`,children:p?"Hide the reviewer's work":"Show the reviewer's work"}),p?h===!1?n("p",{className:"mt-2 text-2xs text-subtle-foreground",children:"The reviewer thread for this review is no longer available."}):h===null?n("p",{className:"mt-2 text-2xs text-subtle-foreground",children:"Loading the reviewer's work\u2026"}):n("div",{className:"mt-2 overflow-hidden rounded-lg border border-border",children:n(oe,{threadId:r,variant:"timeline",layout:"document"})}):null]}):null]}):null]})}function Ce({incident:e,now:t}){return a("div",{className:"border-b border-border px-3.5 py-3 last:border-b-0",children:[a("div",{className:"flex flex-wrap items-center gap-2",children:[n(fe,{}),n("span",{className:"min-w-0 flex-1 text-xs text-muted-foreground",children:e.reason})]}),a("p",{className:"mt-1 text-2xs text-subtle-foreground",children:["No review ran ",P(e.createdAt,t)," \u2014 nothing was checked, so this is not an approval."]})]})}function Re({threadId:e}){let t=S(),{data:s,error:u,reload:i}=q(e,"threadReviews"),[c,m]=b(null),[p,w]=b(!1),[h,v]=b(null),f=ve(),o=M(()=>(s?.reviews??[]).filter(r=>r.severity!=="pass"&&r.resolvedAt===null&&r.closedAt===null).sort((r,l)=>ie[l.severity]-ie[r.severity]||l.sourceSeq-r.sourceSeq||l.id-r.id),[s?.reviews]),d=M(()=>(s?.reviews??[]).filter(r=>r.severity!=="pass"&&(r.resolvedAt!==null||r.closedAt!==null)).sort((r,l)=>(l.resolvedAt??l.closedAt??0)-(r.resolvedAt??r.closedAt??0)),[s?.reviews]),k=M(()=>s?[...s.reviews.filter(l=>l.severity==="pass").map(l=>({kind:"review",sortSeq:l.sourceSeq,review:l})),...s.incidents.map(l=>({kind:"incident",sortSeq:l.sourceSeq,incident:l}))].sort((l,R)=>R.sortSeq-l.sortSeq||R.kind.localeCompare(l.kind)):[],[s]),x=A(()=>{w(!0),v(null),t.call("requestReview",{threadId:e}).then(r=>(r.waiting?v("Advisor will review when this agent turn completes."):r.started||v("A review is already running."),i())).catch(r=>{v(r instanceof Error?r.message:String(r))}).finally(()=>w(!1))},[t,e,i]);if(u)return a("div",{className:"p-4",children:[n("p",{className:"text-xs text-destructive-text",children:u}),n("button",{type:"button",onClick:()=>{i()},className:`mt-2 ${$}`,children:"Retry"})]});if(!s)return n("p",{className:"p-4 text-xs text-subtle-foreground",children:"Loading reviews\u2026"});let C=s.reviewing,y=s.lifecycle==="waiting",g=s.reviews.length+s.incidents.length;return a("div",{className:"flex h-full min-h-0 flex-col overflow-y-auto",children:[a("div",{className:"sticky top-0 z-10 border-b border-border bg-background px-3.5 py-2",children:[a("div",{className:"flex flex-wrap items-center gap-x-2 gap-y-1 text-2xs text-subtle-foreground",children:[n("span",{className:"font-medium text-muted-foreground",children:o.length===1?"1 open":`${o.length} open`}),n("span",{"aria-hidden":"true",children:"\xB7"}),n("span",{children:pe[s.lifecycle]}),d.length>0?a(E,{children:[n("span",{"aria-hidden":"true",children:"\xB7"}),a("span",{children:[d.length," decided"]})]}):null,s.incidents.length>0?a(E,{children:[n("span",{"aria-hidden":"true",children:"\xB7"}),n("span",{children:s.incidents.length===1?"1 review never ran":`${s.incidents.length} reviews never ran`})]}):null,n("span",{className:"flex-1"}),n("button",{type:"button",disabled:C||y||p,onClick:x,className:$,children:y?"Waiting\u2026":C?"Reviewing\u2026":"Review now"})]}),h?n("p",{className:"mt-1 text-2xs text-subtle-foreground",children:h}):null]}),o.length===0?n("p",{className:"px-3.5 py-6 text-center text-xs text-subtle-foreground",children:g===0?"No advisor reviews on this thread yet.":"Nothing outstanding. Earlier reviews are in the log below."}):o.map(r=>n(Q,{threadId:e,review:r,now:f,expanded:c===r.chainId,onToggle:()=>m(l=>l===r.chainId?null:r.chainId),onResolved:()=>{i()}},`r${r.chainId}`)),d.length>0?a("details",{className:"border-t border-border",open:o.length===0,children:[a("summary",{className:"cursor-pointer px-3.5 py-2 text-2xs text-subtle-foreground hover:bg-state-hover",children:["Decided (",d.length,")"]}),d.map(r=>n(Q,{threadId:e,review:r,now:f,expanded:c===r.chainId,onToggle:()=>m(l=>l===r.chainId?null:r.chainId),onResolved:()=>{i()}},`d${r.chainId}`))]}):null,k.length>0?a("details",{className:"border-t border-border",children:[a("summary",{className:"cursor-pointer px-3.5 py-2 text-2xs text-subtle-foreground hover:bg-state-hover",children:["Review log (",k.length,")"]}),k.map(r=>r.kind==="review"?n(Q,{threadId:e,review:r.review,now:f,expanded:c===r.review.chainId,onToggle:()=>m(l=>l===r.review.chainId?null:r.review.chainId),onResolved:()=>{i()}},`r${r.review.chainId}`):n(Ce,{incident:r.incident,now:f},`i${r.incident.id}`))]}):null]})}function Ae(){let e=S(),[t,s]=b(null),[u,i]=b(null),[c,m]=b(null),[p,w]=b(!0),h=ne(null),v=A(async()=>{h.current?.abort();let o=new AbortController;h.current=o,w(!0),i(null);try{let d=await V(()=>e.call("modelConfiguration",null),2e4,"Loading machine models timed out. Try refreshing the models.",o.signal);o.signal.aborted||s(d)}catch(d){o.signal.aborted||i(d instanceof Error?d.message:String(d))}finally{o.signal.aborted||w(!1)}},[e]);_(()=>(v(),()=>h.current?.abort()),[v]);let f=n("button",{type:"button",className:"rounded-md border border-input px-3 py-1.5 text-sm disabled:opacity-50",disabled:p||c!==null,onClick:()=>{v()},children:p?"Refreshing models\u2026":"Refresh models"});return!t&&p?n("p",{className:"text-sm text-muted-foreground",children:"Loading machine models\u2026"}):a("div",{className:"space-y-4",children:[u?n("p",{role:"alert",className:"text-sm text-destructive",children:u}):null,f,n("p",{className:"text-sm text-muted-foreground",children:"Each review runs on the primary thread's machine. Choose an advisor model and reasoning level independently for each machine; machines without a selection follow the primary thread's provider and model."}),t?.hosts.map(o=>{let d=o.selection?o.options.findIndex(g=>g.providerId===o.selection?.providerId&&g.model===o.selection.model):-1,k=d>=0?o.options[d]:void 0,x=!o.selection||o.selection.reasoningLevel==="default"||k?.supportedReasoningLevels.includes(o.selection.reasoningLevel)===!0,C=d>=0?String(d):o.selection?"unavailable":"follow",y=g=>{i(null),m(o.hostId),V(()=>e.call("setHostModel",{hostId:o.hostId,selection:g}),15e3,"Saving the model timed out. Refresh models to check the saved selection.").then(()=>s(r=>r&&{hosts:r.hosts.map(l=>l.hostId===o.hostId?{...l,selection:g}:l)})).catch(r=>{i(r instanceof Error?r.message:String(r))}).finally(()=>m(null))};return a("div",{className:"rounded-lg border border-border p-3",children:[a("div",{className:"mb-2 flex items-center justify-between gap-3",children:[a("div",{children:[n("div",{className:"text-sm font-medium text-foreground",children:o.hostName}),n("div",{className:"text-xs text-muted-foreground",children:o.connected?"Connected":"Disconnected"})]}),c===o.hostId?n("span",{className:"text-xs text-muted-foreground",children:"Saving\u2026"}):null]}),a("select",{"aria-label":`Advisor model for ${o.hostName}`,className:"h-9 w-full rounded-md border border-input bg-background px-3 text-sm text-foreground disabled:cursor-not-allowed disabled:opacity-50",disabled:!o.connected||c!==null||p,value:C,onChange:g=>{let r=g.currentTarget.value,l=r==="follow"?null:(()=>{let R=o.options[Number(r)];return R?{providerId:R.providerId,model:R.model,reasoningLevel:"default"}:null})();y(l)},children:[n("option",{value:"follow",children:"Follow primary thread"}),o.selection&&d<0?a("option",{value:"unavailable",disabled:!0,children:["Saved: ",o.selection.providerId," \xB7 ",o.selection.model," (not loaded)"]}):null,o.options.map((g,r)=>a("option",{value:String(r),children:[g.providerName," \xB7 ",g.modelName,g.isDefault?" (default)":""]},`${g.providerId}:${g.model}`))]}),o.selection&&k?a("select",{"aria-label":`Advisor reasoning for ${o.hostName}`,className:"mt-2 h-9 w-full rounded-md border border-input bg-background px-3 text-sm text-foreground disabled:cursor-not-allowed disabled:opacity-50",disabled:!o.connected||c!==null||p,value:o.selection.reasoningLevel,onChange:g=>y({providerId:o.selection.providerId,model:o.selection.model,reasoningLevel:g.currentTarget.value}),children:[a("option",{value:"default",children:["Model default (",k.defaultReasoningLevel,")"]}),k.supportedReasoningLevels.map(g=>n("option",{value:g,children:g},g))]}):null,o.selection&&(d<0||!x)?a("p",{className:"mt-2 text-xs text-destructive",children:["Could not verify saved configuration ",o.selection.providerId,"/",o.selection.model,"@",o.selection.reasoningLevel,". Refresh models to retry; reviews may fall back to the primary model."]}):null,o.error?n("p",{className:"mt-2 text-xs text-muted-foreground",children:o.error}):null]},o.hostId)}),t?.hosts.length===0?n("p",{className:"text-sm text-muted-foreground",children:"No enrolled machines found."}):null]})}var Yt=se(e=>{e.slots.settingsSection({id:"models",title:"Advisor model by machine",description:"Live model choices from each enrolled bb machine.",component:Ae}),e.slots.threadPanelAction({id:X,title:"Advisor",icon:"ShieldCheck",layout:"flush",component:Re}),e.slots.experimental_threadHeaderAction({id:"verdict",title:"Advisor verdict",component:xe}),e.composer.customize({id:"pending-advice",scopes:["thread"],banners:[{id:"pending-advice",chrome:"bare",component:ke}]}),e.composer.customize({id:"advisor-switch",scopes:["thread"],actions:[{id:"thread-toggle",component:we}]})});export{Yt as default}; +var z=globalThis.__bbPluginRuntime;if(z==null||z.react==null)throw new Error('Cannot load "react": this bundle must be loaded by the BB app, which provides the shared plugin runtime (globalThis.__bbPluginRuntime).');var $=z.react,Pe="default"in $?$.default:$,{Activity:Ie,Children:_e,Component:Le,Fragment:$e,Profiler:Me,PureComponent:De,StrictMode:Be,Suspense:qe,act:He,cache:Ye,cacheSignal:Fe,captureOwnerStack:ze,cloneElement:Ve,createContext:We,createElement:je,createRef:Oe,forwardRef:Ue,isValidElement:Xe,lazy:Ge,memo:Ke,startTransition:Qe,unstable_useCacheRefresh:Je,use:Ze,useActionState:et,useCallback:A,useContext:tt,useDebugValue:nt,useDeferredValue:rt,useEffect:_,useEffectEvent:ot,useId:st,useImperativeHandle:at,useInsertionEffect:it,useLayoutEffect:lt,useMemo:M,useOptimistic:dt,useReducer:ut,useRef:D,useState:b,useSyncExternalStore:ct,useTransition:pt,version:mt}=$;async function V(e,t,o,a){let i=new AbortController,d=a?AbortSignal.any([i.signal,a]):i.signal,g,p=new Promise((y,f)=>{g=()=>f(d.reason),d.aborted?g():d.addEventListener("abort",g,{once:!0})}),w=setTimeout(()=>i.abort(new Error(o)),t);try{return await Promise.race([Promise.resolve().then(()=>(d.throwIfAborted(),e(d))),p])}finally{clearTimeout(w),d.removeEventListener("abort",g)}}var W=globalThis.__bbPluginRuntime;if(W==null||W.pluginSdkApp==null)throw new Error('Cannot load "@get-bb/plugin-sdk/app": this bundle must be loaded by the BB app, which provides the shared plugin runtime (globalThis.__bbPluginRuntime).');var B=W.pluginSdkApp,ft="default"in B?B.default:B,{Markdown:re,ThreadChat:oe,UrlLink:vt,definePluginApp:se,experimental_BranchPicker:ht,experimental_Diff:xt,experimental_FileLink:yt,experimental_Icon:wt,experimental_NewThreadComposer:Nt,experimental_PermissionModePicker:kt,experimental_ProviderIcon:Ct,experimental_ProviderModelPicker:Rt,experimental_SourceCode:At,experimental_useAppPanel:St,experimental_useBranches:Tt,experimental_useCheckoutState:Et,experimental_useCodeTheme:Pt,experimental_useFixedTabTarget:It,experimental_useProviders:_t,experimental_useSidebarThreadActions:Lt,experimental_useSidebarThreadPullRequest:$t,experimental_useSidebarThreadSplit:Mt,experimental_useSidebarThreads:Dt,useBbContext:Bt,useBbNavigate:j,useComposer:O,useComposerView:qt,useRealtime:U,useRealtimeConnectionState:ae,useRpc:S,useSettings:Ht}=B;var X=globalThis.__bbPluginRuntime;if(X==null||X.jsxRuntime==null)throw new Error('Cannot load "react/jsx-runtime": this bundle must be loaded by the BB app, which provides the shared plugin runtime (globalThis.__bbPluginRuntime).');var q=X.jsxRuntime,Ft="default"in q?q.default:q,{Fragment:P,jsx:n,jsxs:l}=q;var Z="reviews",be={unreviewed:"Not reviewed",waiting:"Waiting for completed turn",pending:"Review pending",approved:"Approved","changes-requested":"Changes requested",decided:"User decided",unavailable:"Review unavailable"},fe={fixed:"You marked it fixed","not-an-issue":"You said it isn't an issue","wont-fix":"You decided not to fix it"};function ve(e,t){return e.resolvedAt!==null?{text:`${e.decision===""?"You dismissed it":fe[e.decision]} ${I(e.resolvedAt,t)}`,tone:"text-subtle-foreground"}:e.closedAt!==null?{text:`Advisor re-checked and closed it${e.closedSeq===null?"":` at turn ${e.closedSeq}`}`,tone:"text-success-foreground"}:e.continuedAt!==null?{text:`Corrective follow-up started ${I(e.continuedAt,t)}`,tone:"text-subtle-foreground"}:e.repeatCount>1?{text:`Re-raised ${e.repeatCount}\xD7 \u2014 not addressed`,tone:"text-destructive-text"}:e.sentAt!==null?{text:`Sent to the agent ${I(e.sentAt,t)}`,tone:"text-subtle-foreground"}:{text:"Queued \u2014 goes to the agent with your next message",tone:"text-subtle-foreground"}}var ie={pass:0,nit:1,concern:2,blocker:3},he={blocker:"text-destructive-text border-surface-destructive-border bg-surface-destructive",concern:"text-warning-text border-warning/30 bg-warning/10",nit:"text-readback-foreground border-border bg-surface-recessed",pass:"text-success-foreground border-success/30 bg-success/10"},ee="inline-flex items-center gap-1.5 rounded-full border px-1.5 py-px text-2xs font-semibold uppercase tracking-wide whitespace-nowrap",E={blocker:"Must fix",concern:"Should fix",nit:"Minor",pass:"No issues",unavailable:"Didn't run",reviewing:"Review pending",waiting:"Waiting",decided:"Decided"},T={blocker:"The advisor found something it thinks must be fixed before this work is finished.",concern:"The advisor found a real problem worth fixing, but not a blocking one.",nit:"A small suggestion. Safe to ignore.",pass:"The advisor reviewed this turn and found nothing actionable.",unavailable:"The review did not run, so nothing was checked. This is not an approval.",reviewing:"A review is running right now. Nothing has been checked yet.",waiting:"Advisor will review when the current agent turn has a completed answer.",decided:"The user overruled or accepted the latest finding. Advisor did not verify a fix."};function I(e,t){let o=Math.max(0,Math.round((t-e)/1e3));if(o<60)return"just now";let a=Math.round(o/60);if(a<60)return`${a}m ago`;let i=Math.round(a/60);if(i<24)return`${i}h ago`;let d=Math.round(i/24);return d<7?`${d}d ago`:new Date(e).toLocaleDateString()}function xe(e=6e4){let[t,o]=b(()=>Date.now());return _(()=>{let a=setInterval(()=>o(Date.now()),e);return()=>clearInterval(a)},[e]),t}function pe({severity:e,muted:t,children:o}){return l("span",{title:T[e],className:`${ee} ${he[e]} ${t?"opacity-60":""}`,children:[n("span",{className:"size-1.5 shrink-0 rounded-full bg-current"}),o??E[e]]})}function ye({children:e}){return l("span",{title:T.unavailable,className:`${ee} border-dashed border-border bg-transparent text-subtle-foreground`,children:[n("span",{className:"size-1.5 shrink-0 rounded-full bg-current"}),e??E.unavailable]})}var we={blocker:"M8 2h8l6 6v8l-6 6H8l-6-6V8zM12 8v5M12 16.5h.01",concern:"M12 3 22 20H2zM12 10v4M12 17.5h.01",nit:"M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18M12 8v5M12 16.5h.01",pass:"M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10zm-3-10 2 2 4-4",unavailable:"M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18M6 6l12 12",reviewing:"M11 4a7 7 0 1 0 0 14 7 7 0 0 0 0-14M16 16l4 4",waiting:"M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18M12 7v5l3 2",decided:"M5 12l4 4L19 6M12 3a9 9 0 1 0 0 18"};function J({standing:e,className:t}){return n("svg",{className:t,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round","aria-hidden":"true",children:n("path",{d:we[e]})})}function H(e,t){let o=S(),[a,i]=b(null),[d,g]=b(null),p=A(async()=>{if(e!==null)try{g(null),i(await o.call(t,{threadId:e}))}catch(v){g(v instanceof Error?v.message:String(v))}},[o,t,e]);_(()=>{p()},[p]),U("thread-changed",A(v=>{(typeof v=="object"&&v!==null?v.threadId:void 0)===e&&p()},[e,p]));let w=ae(),[y,f]=b(w==="connected");return _(()=>{if(w!=="connected"){f(!1);return}y||(f(!0),p())},[w,y,p]),{data:a,error:d,reload:p}}function Ne({threadId:e,isCompactViewport:t}){let o=j(),{data:a}=H(e,"threadBadge");if(!a)return null;let i=ke(a);return i?l("button",{type:"button",title:i.label,"aria-label":i.label,onClick:()=>o.openThreadPanel({actionId:Z}),className:`inline-flex h-7 items-center gap-1.5 rounded-md border px-2 text-xs hover:bg-state-hover ${i.tone}`,children:[n(J,{standing:i.kind,className:"size-3.5 shrink-0"}),t?null:n("span",{children:E[i.kind]}),i.repeatCount>1?l("span",{className:"text-2xs font-semibold tabular-nums opacity-85",children:["\xD7",i.repeatCount]}):null]}):null}var G="text-muted-foreground border-border bg-surface-recessed",K="border-dashed border-border bg-transparent text-subtle-foreground",le="text-destructive-text border-surface-destructive-border bg-surface-destructive";function ke(e){if(e.lifecycle==="waiting")return{kind:"waiting",label:T.waiting,tone:K,repeatCount:0};if(e.lifecycle==="pending"||e.reviewing)return{kind:"reviewing",label:T.reviewing,tone:K,repeatCount:0};if(e.lifecycle==="decided")return{kind:"decided",label:T.decided,tone:G,repeatCount:0};if(e.latestIsUnavailable)return{kind:"unavailable",label:`Advisor did not run: ${e.latestUnavailableReason??"the review failed"}. This is not an approval.`,tone:K,repeatCount:0};let t=e.open;if(t){let a=t.repeatCount>1?` Flagged ${t.repeatCount} times and still unresolved.`:"";return{kind:t.severity,label:`Advisor: ${E[t.severity]}. ${t.summary}${a} ${T[t.severity]}`,tone:t.severity==="blocker"?le:G,repeatCount:t.repeatCount}}let o=e.latest;return o?{kind:o.severity,label:`Advisor: ${E[o.severity]}. ${o.summary} ${T[o.severity]}`,tone:o.severity==="blocker"?le:G,repeatCount:o.repeatCount}:null}var de=10;function ue(e,t){return Math.hypot(t.clientX-e.x,t.clientY-e.y)}function ce(e){let t=D(null),o=D(!1);return{onPointerDown:a=>{t.current=null,o.current=a.pointerType==="touch",!(!o.current||!a.isPrimary||a.button!==0)&&(a.preventDefault(),t.current={pointerId:a.pointerId,x:a.clientX,y:a.clientY})},onPointerMove:a=>{let i=t.current;i?.pointerId===a.pointerId&&ue(i,a)>de&&(t.current=null)},onPointerCancel:()=>{t.current=null,o.current=!1},onPointerUp:a=>{let i=t.current;if(t.current=null,i?.pointerId!==a.pointerId)return;let d=a.currentTarget.getBoundingClientRect();a.clientX>=d.left&&a.clientX<=d.right&&a.clientY>=d.top&&a.clientY<=d.bottom&&ue(i,a)<=de&&e()},onMouseDown:a=>{a.preventDefault()},onClick:a=>{let i=o.current;o.current=!1,!(i&&a.detail>0)&&e()}}}function Ce(){let{scope:e}=O();return e.kind!=="thread"?null:n(Re,{threadId:e.threadId},e.threadId)}function Re({threadId:e}){let t=S(),{data:o,error:a,reload:i}=H(e,"threadToggle"),[d,g]=b(null),[p,w]=b(!1),[y,f]=b(null);U("advisor-settings-changed",A(()=>{i()},[i]));let v=A(async()=>{if(o===null||p)return;let x=!o.enabled;g(x),w(!0),f(null);try{await t.call("setThreadToggle",{threadId:e,enabled:x})}catch(c){f(c instanceof Error?c.message:String(c))}finally{await i(),g(null),w(!1)}},[t,e,o,p,i]),s=ce(()=>{v()}),m=ce(()=>{f(null),i()}),k=y??a;if(k)return n("button",{type:"button",title:`Advisor switch unavailable (${k}). Click to retry.`,"aria-label":"Advisor switch unavailable, click to retry",...m,className:"inline-flex h-7 shrink-0 items-center rounded-md border border-dashed border-border px-1.5 text-subtle-foreground hover:bg-state-hover",children:n(J,{standing:"unavailable",className:"size-3.5 shrink-0"})});if(o===null)return null;let h=d??o.enabled,C=h?"Advisor on":"Advisor off";return l("span",{className:"group relative inline-flex shrink-0",children:[l("button",{type:"button",role:"switch","aria-checked":h,"aria-label":C,disabled:p,...s,className:`inline-flex h-7 shrink-0 items-center gap-1.5 rounded-md border px-1.5 hover:bg-state-hover ${h?"border-border text-foreground":"border-dashed border-border text-subtle-foreground"}`,children:[n(J,{standing:"pass",className:"size-3.5 shrink-0"}),n("span",{"aria-hidden":"true",className:`relative h-4 w-7 shrink-0 rounded-full border transition-colors ${h?"border-success bg-success":"border-border bg-surface-recessed"}`,children:n("span",{className:`absolute left-px top-px size-3 rounded-full bg-background transition-transform ${h?"translate-x-3":"translate-x-0"}`})})]}),n("span",{role:"tooltip",className:"pointer-events-none absolute bottom-full left-1/2 z-50 mb-1.5 -translate-x-1/2 whitespace-nowrap rounded-md border border-border bg-card px-2 py-1 text-2xs text-foreground opacity-0 shadow-md transition-opacity group-hover:opacity-100 group-focus-within:opacity-100",children:C})]})}function Ae(){let e=O(),t=j(),o=S(),a=e.scope,i=a.kind==="thread"?a.threadId:null,{data:d,error:g,reload:p}=H(i,"pendingAdvice"),[w,y]=b(!1),[f,v]=b(!1),[s,m]=b(null);if(!i)return null;let k=s??g;if(k)return l("div",{className:"rounded-lg border border-border bg-card px-4 py-3 text-xs",children:[l("p",{className:"text-destructive-text",children:["Advisor status unavailable \u2014 any pending review will still be sent to the agent. (",k,")"]}),n("div",{className:"mt-2 flex justify-end",children:n("button",{type:"button",onClick:()=>{m(null),p()},className:"h-7 rounded-md border border-border bg-card px-2.5 text-xs text-foreground hover:bg-state-hover",children:"Retry"})})]});let h=d?.advice??null;if(!h)return null;let C=h.repeatCount>1;return l("div",{className:"rounded-lg border border-border bg-card px-4 py-3 text-xs text-muted-foreground",children:[l("div",{className:"flex flex-wrap items-center gap-2",children:[n(pe,{severity:h.severity,children:C?`${E[h.severity]} \xB7 ${h.repeatCount}\xD7`:E[h.severity]}),n("h3",{className:"min-w-0 flex-1 text-sm font-semibold text-foreground",children:"Advisor reviewed your last turn"})]}),l("p",{className:"mt-2",children:[n("span",{className:"font-medium text-foreground",children:h.summary}),C?l("span",{className:"text-destructive-text",children:[" ","The agent was told this before and did not resolve it."]}):null]}),n("p",{className:"mt-1 text-2xs text-subtle-foreground",children:"Start a corrective turn now, or it will be shared with the agent on your next message."}),l("div",{className:"mt-3 flex flex-wrap items-center justify-end gap-2",children:[n("button",{type:"button",disabled:w,onClick:()=>{y(!0),m(null),o.call("dismissAdvice",{threadId:i,adviceId:h.id}).then(p).catch(x=>{m(x instanceof Error?x.message:String(x))}).finally(()=>y(!1))},className:"h-7 rounded-md border border-transparent px-2.5 text-xs text-muted-foreground hover:bg-state-hover disabled:opacity-50",children:"Dismiss"}),n("button",{type:"button",onClick:()=>t.openThreadPanel({actionId:Z}),className:"h-7 rounded-md border border-border bg-card px-2.5 text-xs text-foreground hover:bg-state-hover",children:"View review"}),n("button",{type:"button",disabled:f,onClick:()=>{v(!0),m(null),o.call("continueFinding",{threadId:i,reviewId:h.id}).then(x=>{if(!x.started&&x.reason!=="already-started")throw new Error("This finding is no longer open.");return p()}).catch(x=>{m(x instanceof Error?x.message:String(x))}).finally(()=>v(!1))},className:"h-7 rounded-md border border-surface-destructive-border bg-surface-destructive px-2.5 text-xs font-medium text-destructive-text hover:opacity-90 disabled:opacity-50",children:f?"Starting\u2026":"Fix in new turn"})]})]})}var L="h-7 rounded-md border border-border bg-card px-2.5 text-xs text-foreground hover:bg-state-hover disabled:cursor-not-allowed disabled:opacity-50";function Q({threadId:e,review:t,expanded:o,now:a,onToggle:i,onResolved:d}){let g=S(),[p,w]=b(!1),[y,f]=b(null),[v,s]=b(""),[m,k]=b(!1),[h,C]=b(!1),[x,c]=b(null),r=t.advisorThreadId,u=t.repeatCount>1,R=t.resolvedAt!==null||t.closedAt!==null,te=ve(t,a),me=A(()=>{w(N=>!N),!(y!==null||!r)&&g.call("reviewerThreadAvailable",{threadId:e,advisorThreadId:r}).then(N=>f(N.available)).catch(()=>f(!1))},[g,e,r,y]),ne=A((N,Y)=>{k(!0),c(null),g.call("resolveFinding",{threadId:e,chainId:t.chainId,resolved:N,reason:N?v.trim():"",decision:Y}).then(()=>{s(""),d()}).catch(F=>{c(F instanceof Error?F.message:String(F))}).finally(()=>k(!1))},[g,e,t.chainId,v,d]),ge=A(()=>{C(!0),c(null),g.call("continueFinding",{threadId:e,reviewId:t.id}).then(N=>{if(!N.started&&N.reason!=="already-started")throw new Error("This finding is no longer open.");d()}).catch(N=>{c(N instanceof Error?N.message:String(N))}).finally(()=>C(!1))},[g,e,t.id,d]);return l("div",{className:`border-b border-border last:border-b-0 ${o?"bg-surface-recessed":""}`,children:[l("button",{type:"button",onClick:i,"aria-expanded":o,className:`relative w-full px-3.5 py-3 text-left hover:bg-state-hover ${u&&!R?"pl-5":""}`,children:[u&&!R?n("span",{"aria-hidden":"true",className:"absolute inset-y-1.5 left-0 w-0.5 rounded-sm bg-destructive/60"}):null,l("span",{className:"flex flex-wrap items-center gap-2",children:[n(pe,{severity:t.severity,muted:R}),n("span",{className:`min-w-0 text-sm font-medium ${R?"text-muted-foreground line-through decoration-1":"text-foreground"}`,children:t.summary}),u?l("span",{title:`Raised ${t.repeatCount} times`,className:`${ee} border-border bg-surface-recessed normal-case text-muted-foreground`,children:[t.repeatCount,"\xD7"]}):null]}),l("span",{className:"mt-1.5 flex flex-wrap gap-x-2.5 gap-y-1 text-2xs text-subtle-foreground",children:[n("span",{className:te.tone,title:t.resolvedReason||void 0,children:te.text}),n("span",{"aria-hidden":"true",children:"\xB7"}),u?l("span",{title:`First raised at timeline sequence ${t.firstSourceSeq}`,children:["first flagged ",I(t.firstCreatedAt,a)]}):n("span",{title:`Timeline sequence ${t.sourceSeq}`,children:I(t.createdAt,a)}),t.model?l(P,{children:[n("span",{"aria-hidden":"true",children:"\xB7"}),l("span",{children:[t.model,t.reasoningLevel&&t.reasoningLevel!=="default"?` \xB7 ${t.reasoningLevel}`:""]})]}):null]})]}),o?l("div",{className:"px-3.5 pb-3",children:[t.details.trim().length>0?n("div",{className:"text-xs text-muted-foreground",children:n(re,{content:t.details})}):n("p",{className:"text-xs text-subtle-foreground",children:"No further detail was given."}),t.resolvedAt!==null&&t.resolvedReason?l("p",{className:"mt-2 text-2xs text-subtle-foreground",children:["Your note: ",t.resolvedReason]}):null,t.severity!=="pass"?n("div",{className:"mt-2.5 flex flex-wrap items-center gap-2",children:R?n("button",{type:"button",disabled:m,onClick:()=>ne(!1,"not-an-issue"),className:L,children:"Reopen"}):l(P,{children:[n("button",{type:"button",disabled:h||t.continuedAt!==null,onClick:ge,className:"h-7 rounded-md border border-surface-destructive-border bg-surface-destructive px-2.5 text-xs font-medium text-destructive-text hover:opacity-90 disabled:opacity-50",children:t.continuedAt!==null?"Follow-up started":h?"Starting\u2026":"Fix in new turn"}),n("input",{value:v,onChange:N=>s(N.currentTarget.value),maxLength:500,placeholder:"Note (optional)","aria-label":`Note about ${t.summary}`,className:"h-7 min-w-0 flex-1 rounded-md border border-input bg-background px-2 text-xs text-foreground placeholder:text-subtle-foreground"}),[["not-an-issue","Not an issue"],["wont-fix","Won't fix"]].map(([N,Y])=>n("button",{type:"button",disabled:m,onClick:()=>ne(!0,N),className:L,children:Y},N))]})}):null,x?n("p",{className:"mt-1.5 text-2xs text-destructive-text",children:x}):null,r?l(P,{children:[n("button",{type:"button",onClick:me,"aria-expanded":p,className:`mt-2 ${L}`,children:p?"Hide the reviewer's work":"Show the reviewer's work"}),p?y===!1?n("p",{className:"mt-2 text-2xs text-subtle-foreground",children:"The reviewer thread for this review is no longer available."}):y===null?n("p",{className:"mt-2 text-2xs text-subtle-foreground",children:"Loading the reviewer's work\u2026"}):n("div",{className:"mt-2 overflow-hidden rounded-lg border border-border",children:n(oe,{threadId:r,variant:"timeline",layout:"document"})}):null]}):null]}):null]})}function Se({incident:e,now:t}){return l("div",{className:"border-b border-border px-3.5 py-3 last:border-b-0",children:[l("div",{className:"flex flex-wrap items-center gap-2",children:[n(ye,{}),n("span",{className:"min-w-0 flex-1 text-xs text-muted-foreground",children:e.reason})]}),l("p",{className:"mt-1 text-2xs text-subtle-foreground",children:["No review ran ",I(e.createdAt,t)," \u2014 nothing was checked, so this is not an approval."]})]})}function Te({threadId:e}){let t=S(),{data:o,error:a,reload:i}=H(e,"threadReviews"),[d,g]=b(null),[p,w]=b(!1),[y,f]=b(null),v=xe(),s=M(()=>(o?.reviews??[]).filter(r=>r.severity!=="pass"&&r.resolvedAt===null&&r.closedAt===null).sort((r,u)=>ie[u.severity]-ie[r.severity]||u.sourceSeq-r.sourceSeq||u.id-r.id),[o?.reviews]),m=M(()=>(o?.reviews??[]).filter(r=>r.severity!=="pass"&&(r.resolvedAt!==null||r.closedAt!==null)).sort((r,u)=>(u.resolvedAt??u.closedAt??0)-(r.resolvedAt??r.closedAt??0)),[o?.reviews]),k=M(()=>o?[...o.reviews.filter(u=>u.severity==="pass").map(u=>({kind:"review",sortSeq:u.sourceSeq,review:u})),...o.incidents.map(u=>({kind:"incident",sortSeq:u.sourceSeq,incident:u}))].sort((u,R)=>R.sortSeq-u.sortSeq||R.kind.localeCompare(u.kind)):[],[o]),h=A(()=>{w(!0),f(null),t.call("requestReview",{threadId:e}).then(r=>(r.waiting?f("Advisor will review when this agent turn completes."):r.started||f("A review is already running."),i())).catch(r=>{f(r instanceof Error?r.message:String(r))}).finally(()=>w(!1))},[t,e,i]);if(a)return l("div",{className:"p-4",children:[n("p",{className:"text-xs text-destructive-text",children:a}),n("button",{type:"button",onClick:()=>{i()},className:`mt-2 ${L}`,children:"Retry"})]});if(!o)return n("p",{className:"p-4 text-xs text-subtle-foreground",children:"Loading reviews\u2026"});let C=o.reviewing,x=o.lifecycle==="waiting",c=o.reviews.length+o.incidents.length;return l("div",{className:"flex h-full min-h-0 flex-col overflow-y-auto",children:[l("div",{className:"sticky top-0 z-10 border-b border-border bg-background px-3.5 py-2",children:[l("div",{className:"flex flex-wrap items-center gap-x-2 gap-y-1 text-2xs text-subtle-foreground",children:[n("span",{className:"font-medium text-muted-foreground",children:s.length===1?"1 open":`${s.length} open`}),n("span",{"aria-hidden":"true",children:"\xB7"}),n("span",{children:be[o.lifecycle]}),m.length>0?l(P,{children:[n("span",{"aria-hidden":"true",children:"\xB7"}),l("span",{children:[m.length," decided"]})]}):null,o.incidents.length>0?l(P,{children:[n("span",{"aria-hidden":"true",children:"\xB7"}),n("span",{children:o.incidents.length===1?"1 review never ran":`${o.incidents.length} reviews never ran`})]}):null,n("span",{className:"flex-1"}),n("button",{type:"button",disabled:C||x||p,onClick:h,className:L,children:x?"Waiting\u2026":C?"Reviewing\u2026":"Review now"})]}),y?n("p",{className:"mt-1 text-2xs text-subtle-foreground",children:y}):null]}),s.length===0?n("p",{className:"px-3.5 py-6 text-center text-xs text-subtle-foreground",children:c===0?"No advisor reviews on this thread yet.":"Nothing outstanding. Earlier reviews are in the log below."}):s.map(r=>n(Q,{threadId:e,review:r,now:v,expanded:d===r.chainId,onToggle:()=>g(u=>u===r.chainId?null:r.chainId),onResolved:()=>{i()}},`r${r.chainId}`)),m.length>0?l("details",{className:"border-t border-border",open:s.length===0,children:[l("summary",{className:"cursor-pointer px-3.5 py-2 text-2xs text-subtle-foreground hover:bg-state-hover",children:["Decided (",m.length,")"]}),m.map(r=>n(Q,{threadId:e,review:r,now:v,expanded:d===r.chainId,onToggle:()=>g(u=>u===r.chainId?null:r.chainId),onResolved:()=>{i()}},`d${r.chainId}`))]}):null,k.length>0?l("details",{className:"border-t border-border",children:[l("summary",{className:"cursor-pointer px-3.5 py-2 text-2xs text-subtle-foreground hover:bg-state-hover",children:["Review log (",k.length,")"]}),k.map(r=>r.kind==="review"?n(Q,{threadId:e,review:r.review,now:v,expanded:d===r.review.chainId,onToggle:()=>g(u=>u===r.review.chainId?null:r.review.chainId),onResolved:()=>{i()}},`r${r.review.chainId}`):n(Se,{incident:r.incident,now:v},`i${r.incident.id}`))]}):null]})}function Ee(){let e=S(),[t,o]=b(null),[a,i]=b(null),[d,g]=b(null),[p,w]=b(!0),y=D(null),f=A(async()=>{y.current?.abort();let s=new AbortController;y.current=s,w(!0),i(null);try{let m=await V(()=>e.call("modelConfiguration",null),2e4,"Loading machine models timed out. Try refreshing the models.",s.signal);s.signal.aborted||o(m)}catch(m){s.signal.aborted||i(m instanceof Error?m.message:String(m))}finally{s.signal.aborted||w(!1)}},[e]);_(()=>(f(),()=>y.current?.abort()),[f]);let v=n("button",{type:"button",className:"rounded-md border border-input px-3 py-1.5 text-sm disabled:opacity-50",disabled:p||d!==null,onClick:()=>{f()},children:p?"Refreshing models\u2026":"Refresh models"});return!t&&p?n("p",{className:"text-sm text-muted-foreground",children:"Loading machine models\u2026"}):l("div",{className:"space-y-4",children:[a?n("p",{role:"alert",className:"text-sm text-destructive",children:a}):null,v,n("p",{className:"text-sm text-muted-foreground",children:"Each review runs on the primary thread's machine. Choose an advisor model and reasoning level independently for each machine; machines without a selection follow the primary thread's provider and model."}),t?.hosts.map(s=>{let m=s.selection?s.options.findIndex(c=>c.providerId===s.selection?.providerId&&c.model===s.selection.model):-1,k=m>=0?s.options[m]:void 0,h=!s.selection||s.selection.reasoningLevel==="default"||k?.supportedReasoningLevels.includes(s.selection.reasoningLevel)===!0,C=m>=0?String(m):s.selection?"unavailable":"follow",x=c=>{i(null),g(s.hostId),V(()=>e.call("setHostModel",{hostId:s.hostId,selection:c}),15e3,"Saving the model timed out. Refresh models to check the saved selection.").then(()=>o(r=>r&&{hosts:r.hosts.map(u=>u.hostId===s.hostId?{...u,selection:c}:u)})).catch(r=>{i(r instanceof Error?r.message:String(r))}).finally(()=>g(null))};return l("div",{className:"rounded-lg border border-border p-3",children:[l("div",{className:"mb-2 flex items-center justify-between gap-3",children:[l("div",{children:[n("div",{className:"text-sm font-medium text-foreground",children:s.hostName}),n("div",{className:"text-xs text-muted-foreground",children:s.connected?"Connected":"Disconnected"})]}),d===s.hostId?n("span",{className:"text-xs text-muted-foreground",children:"Saving\u2026"}):null]}),l("select",{"aria-label":`Advisor model for ${s.hostName}`,className:"h-9 w-full rounded-md border border-input bg-background px-3 text-sm text-foreground disabled:cursor-not-allowed disabled:opacity-50",disabled:!s.connected||d!==null||p,value:C,onChange:c=>{let r=c.currentTarget.value,u=r==="follow"?null:(()=>{let R=s.options[Number(r)];return R?{providerId:R.providerId,model:R.model,reasoningLevel:"default"}:null})();x(u)},children:[n("option",{value:"follow",children:"Follow primary thread"}),s.selection&&m<0?l("option",{value:"unavailable",disabled:!0,children:["Saved: ",s.selection.providerId," \xB7 ",s.selection.model," (not loaded)"]}):null,s.options.map((c,r)=>l("option",{value:String(r),children:[c.providerName," \xB7 ",c.modelName,c.isDefault?" (default)":""]},`${c.providerId}:${c.model}`))]}),s.selection&&k?l("select",{"aria-label":`Advisor reasoning for ${s.hostName}`,className:"mt-2 h-9 w-full rounded-md border border-input bg-background px-3 text-sm text-foreground disabled:cursor-not-allowed disabled:opacity-50",disabled:!s.connected||d!==null||p,value:s.selection.reasoningLevel,onChange:c=>x({providerId:s.selection.providerId,model:s.selection.model,reasoningLevel:c.currentTarget.value}),children:[l("option",{value:"default",children:["Model default (",k.defaultReasoningLevel,")"]}),k.supportedReasoningLevels.map(c=>n("option",{value:c,children:c},c))]}):null,s.selection&&(m<0||!h)?l("p",{className:"mt-2 text-xs text-destructive",children:["Could not verify saved configuration ",s.selection.providerId,"/",s.selection.model,"@",s.selection.reasoningLevel,". Refresh models to retry; reviews may fall back to the primary model."]}):null,s.error?n("p",{className:"mt-2 text-xs text-muted-foreground",children:s.error}):null]},s.hostId)}),t?.hosts.length===0?n("p",{className:"text-sm text-muted-foreground",children:"No enrolled machines found."}):null]})}var Ot=se(e=>{e.slots.settingsSection({id:"models",title:"Advisor model by machine",description:"Live model choices from each enrolled bb machine.",component:Ee}),e.slots.threadPanelAction({id:Z,title:"Advisor",icon:"ShieldCheck",layout:"flush",component:Te}),e.slots.experimental_threadHeaderAction({id:"verdict",title:"Advisor verdict",component:Ne}),e.composer.customize({id:"pending-advice",scopes:["thread"],banners:[{id:"pending-advice",chrome:"bare",component:Ae}]}),e.composer.customize({id:"advisor-switch",scopes:["thread"],actions:[{id:"thread-toggle",component:Ce}]})});export{Ot as default}; diff --git a/dist/app.meta.json b/dist/app.meta.json index 6274da4..b4fa1e8 100644 --- a/dist/app.meta.json +++ b/dist/app.meta.json @@ -3,7 +3,7 @@ "sdkVersion": "0.4.87", "artifactFormatVersion": 1, "pluginId": "advisor", - "pluginVersion": "0.1.2", + "pluginVersion": "0.1.3", "builtWith": { "bbVersion": "0.43.1", "pluginSdkVersion": "0.4.87" diff --git a/dist/server.meta.json b/dist/server.meta.json index 6274da4..b4fa1e8 100644 --- a/dist/server.meta.json +++ b/dist/server.meta.json @@ -3,7 +3,7 @@ "sdkVersion": "0.4.87", "artifactFormatVersion": 1, "pluginId": "advisor", - "pluginVersion": "0.1.2", + "pluginVersion": "0.1.3", "builtWith": { "bbVersion": "0.43.1", "pluginSdkVersion": "0.4.87" diff --git a/package-lock.json b/package-lock.json index d04e19c..71cce2b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "bb-plugin-advisor", - "version": "0.1.2", + "version": "0.1.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "bb-plugin-advisor", - "version": "0.1.2", + "version": "0.1.3", "license": "MIT", "dependencies": { "zod": "4.3.6" diff --git a/package.json b/package.json index 05e6408..27a3e28 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bb-plugin-advisor", - "version": "0.1.2", + "version": "0.1.3", "description": "A persistent independent reviewer for bb coding threads.", "license": "MIT", "author": {