opencode-webui 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -34,7 +34,7 @@ Options:
34
34
  | `WEBUI_PASSWORD` | generated on first boot | Log in with this passphrase. Set it to pin your own instead of the generated one. |
35
35
  | `WEBUI_HOST` | `127.0.0.1` | Bind address. A wildcard (`0.0.0.0` / `::`) is refused without a password. |
36
36
  | `WEBUI_PROXY_PORT` | `4097` | Port for the UI and `/api/*`. |
37
- | `WEBUI_EXTENSION_DIR` | the global + project dirs | Replace both with ONE directory (the sandbox does this to keep WIP isolated). |
37
+ | `WEBUI_EXTENSION_DIR` | the global + project dirs | Adds a higher-precedence source shadowing both (the sandbox uses this to keep WIP isolated; shipped extensions still load underneath). |
38
38
 
39
39
  Sessions are shared with the `opencode` TUI — open a session in the TUI, continue it in the browser.
40
40
 
@@ -0,0 +1 @@
1
+ import{a as e,n as t,t as n}from"./jsx-runtime-B-hcVAMW.js";import{f as r,n as i}from"./index-Bs1cKqOa.js";var a=e(t(),1),o=n(),s=`brother-agent`,c=0,l=new Set;function u(){c++;for(let e of l)e()}function d(e){return l.add(e),()=>{l.delete(e)}}function f(){return(0,a.useSyncExternalStore)(d,()=>c)}var p=new Set,m=new Map;async function h(){try{let e=await fetch(`/api/webui/ext/brother-agent/brothers`);if(!e.ok)return;let t=await e.json();if(!Array.isArray(t.brothers))return;let n=new Set(t.brothers.filter(e=>typeof e==`string`)),r=n.size===p.size;if(r){for(let e of n)if(!p.has(e)){r=!1;break}}if(!r){p.clear();for(let e of n)p.add(e);u()}}catch{}}function g(e){return m.get(e)===`brother-agent`||p.has(e)}var _=window;_.__brotherAgentStarted||(_.__brotherAgentStarted=!0,h(),window.setInterval(()=>{h()},3e4)),r({kind:`hook`,id:`brother-agent`,event:`session.adopted`,handler:(e,t)=>{t();let n=e.sessionID;if(typeof n!=`string`||n===``){h();return}h(),i.getSession(n).then(e=>{let t=e.data.metadata?.origin;typeof t==`string`&&t!==``?m.get(n)!==t&&(m.set(n,t),u()):m.delete(n)&&u()}).catch(()=>{})}}),r({kind:`wrap`,id:`brother-agent-row`,target:`sidebar.sessionRow`,render:(e,t)=>{f();let n=t(),r=e.sessionID;return typeof r!=`string`||!g(r)?n:(0,o.jsxs)(`span`,{className:`relative block min-w-0`,children:[n,(0,o.jsx)(`span`,{title:`Brother agent session`,className:`pointer-events-none absolute top-1.5 right-2 rounded border border-[var(--border-weak-base)] px-1 text-[10px] leading-4 text-[var(--text-weaker)]`,children:`brother`})]})}}),r({kind:`wrap`,id:`brother-agent-header`,target:`conversation.header`,render:(e,t)=>{f();let n=t(),r=e.sessionID;return typeof r!=`string`||!g(r)?n:(0,o.jsxs)(o.Fragment,{children:[n,(0,o.jsx)(`div`,{className:`border-b border-[var(--border-base)] px-4 py-1 text-xs text-[var(--text-weaker)]`,children:`brother agent session — launched via brother_agent · finish notices arrive here automatically`})]})}});export{s as id};
@@ -0,0 +1 @@
1
+ import{a as e,n as t,t as n}from"./jsx-runtime-B-hcVAMW.js";import{a as r,c as i,h as a,i as o,l as s,m as c,o as l,p as u,r as d,s as f,t as p,u as m}from"./index-Bs1cKqOa.js";var h=a(`key-round`,[[`path`,{d:`M2.586 17.414A2 2 0 0 0 2 18.828V21a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h1a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h.172a2 2 0 0 0 1.414-.586l.814-.814a6.5 6.5 0 1 0-4-4z`,key:`1s6t7t`}],[`circle`,{cx:`16.5`,cy:`7.5`,r:`.5`,fill:`currentColor`,key:`w0ekpg`}]]),g=a(`mic`,[[`path`,{d:`M12 19v3`,key:`npa21l`}],[`path`,{d:`M19 10v2a7 7 0 0 1-14 0v-2`,key:`1vc78b`}],[`rect`,{x:`9`,y:`2`,width:`6`,height:`13`,rx:`3`,key:`s6n7sd`}]]),_=e(t(),1),v=n(),y=`groq-voice`,b=p(),x=`groq-voice.apiKey`,S=`https://api.groq.com/openai/v1/audio/transcriptions`,C=`https://api.groq.com/openai/v1/models`,w=`whisper-large-v3-turbo`,T=12e4,E=6e3,D=`webui.drafts`;function O(){try{return window.localStorage.getItem(x)??``}catch{return``}}function k(e){try{e?window.localStorage.setItem(x,e):window.localStorage.removeItem(x)}catch{}}var A=new Set,j=null,M=null;function N(e){j=e;for(let t of Array.from(A))try{t(e)}catch{}}function P(e){return A.add(e),()=>{A.delete(e)}}function F(){return j}function I(e,t=E){N(e),M!==null&&clearTimeout(M),M=setTimeout(()=>{N(null)},t)}function L(){M!==null&&(clearTimeout(M),M=null),N(null)}function R(){let e=[`audio/webm;codecs=opus`,`audio/webm`,`audio/mp4`,`audio/ogg;codecs=opus`];if(window.MediaRecorder===void 0)return null;for(let t of e)try{if(window.MediaRecorder.isTypeSupported(t))return t}catch{}return null}function z(e){if(e)try{for(let t of e.getTracks())t.stop()}catch{}}function B(e){let t=e instanceof DOMException?e.name:``;return t===`NotAllowedError`||t===`PermissionDeniedError`||t===`SecurityError`?`Microphone permission denied — allow mic access in the browser and try again`:t===`NotFoundError`||t===`DevicesNotFoundError`?`No microphone found — plug one in and try again`:t===`NotReadableError`||t===`TrackStartError`?`Microphone is busy or unreadable — close other apps using it and try again`:`Could not start the microphone: ${e instanceof Error?e.message:String(e)}`}function V(e){let t=e instanceof Error?e.message:String(e);if(t===`missing-key`)return`Groq API key missing — open the key settings (key button next to the mic)`;if(t===`empty-transcript`)return`Groq returned an empty transcript — try speaking a little longer`;let n=/^http (\d{3})/.exec(t);if(n!==null)switch(Number(n[1])){case 401:return`Groq rejected the API key (401) — check it in the key settings`;case 403:return`Groq denied access (403) — check the API key and account status`;case 404:return`Groq endpoint not found (404) — the transcription API may have changed`;case 413:return`Recording too large for Groq (413) — keep messages under ~25 MB`;case 429:return`Groq rate limit hit (429) — wait a moment and try again`;default:{let e=t.indexOf(`: `);return e>=0?`Groq error: ${t.slice(e+2)}`:`Groq error (HTTP ${n[1]})`}}return t===`Failed to fetch`||t.startsWith(`NetworkError`)||t.startsWith(`network`)?`Network error reaching Groq — check your connection`:`Transcription failed: ${t}`}async function H(e,t){let n=O().trim();if(n===``)throw Error(`missing-key`);let r=new FormData,i=t.includes(`mp4`)?`m4a`:`webm`;r.append(`file`,new File([e],`voice-${Date.now()}.${i}`,{type:t||`audio/webm`})),r.append(`model`,w);let a;try{a=await fetch(S,{method:`POST`,headers:{Authorization:`Bearer ${n}`},body:r})}catch(e){throw Error(e instanceof Error?e.message:`network error`)}if(!a.ok){let e=``;try{let t=await a.json();t?.error&&typeof t.error.message==`string`&&(e=t.error.message)}catch{}throw Error(e===``?`http ${a.status}`:`http ${a.status}: ${e}`)}let o=await a.json(),s=typeof o.text==`string`?o.text.trim():``;if(s===``)throw Error(`empty-transcript`);return s}async function U(){let e=O().trim();if(e===``)return{ok:!1,error:`missing-key`};try{let t=await fetch(C,{headers:{Authorization:`Bearer ${e}`}});if(t.ok)return{ok:!0,status:t.status};let n=``;try{let e=await t.json();e?.error&&typeof e.error.message==`string`&&(n=e.error.message)}catch{}return{ok:!1,status:t.status,error:n===``?`http ${t.status}`:n}}catch(e){return{ok:!1,error:e instanceof Error?e.message:String(e)}}}function W(e,t){try{let n=window.localStorage.getItem(D),r=n?JSON.parse(n):{},i=(typeof r[e]?.text==`string`?r[e]?.text:``).replace(/\s+$/,``);r[e]={text:i===``?t:`${i} ${t}`,at:Date.now()},window.localStorage.setItem(D,JSON.stringify(r));let a=document.querySelector(`[data-oc-composer-input]`);return a instanceof HTMLTextAreaElement&&a.focus(),!0}catch{return!1}}function G({open:e,onOpenChange:t}){let[n,a]=(0,_.useState)(O),[c,u]=(0,_.useState)(!1),[p,h]=(0,_.useState)(!1),[g,y]=(0,_.useState)(null);(0,_.useEffect)(()=>{e&&(a(O()),y(null),u(!1))},[e]),(0,_.useEffect)(()=>{if(!c)return;let e=setTimeout(()=>u(!1),1500);return()=>clearTimeout(e)},[c]);function b(){k(n.trim()),u(!0)}function x(){if(n.trim()===``){y({ok:!1,error:`missing-key`});return}h(!0),y(null),k(n.trim()),U().then(e=>{h(!1),y(e)})}let S,C=`text-[color:var(--text-weaker)]`;return g===null?S=`The key stays in this browser (localStorage) and is only ever sent to api.groq.com`:g.ok?(S=`Key works — Groq accepted it (${g.status})`,C=`text-green-600 dark:text-green-400`):g.error===`missing-key`?(S=`Enter a key first`,C=`text-red-600 dark:text-red-400`):(S=`Key rejected: ${g.error??`http ${g.status}`}`,C=`text-red-600 dark:text-red-400`),(0,v.jsx)(o,{open:e,onOpenChange:t,children:(0,v.jsxs)(r,{children:[(0,v.jsxs)(i,{children:[(0,v.jsx)(s,{children:`Groq voice input`}),(0,v.jsxs)(l,{children:[`The mic button in the chat composer records your voice and transcribes it with Groq`,` `,w,`. Get a free API key at console.groq.com (Keys).`]})]}),(0,v.jsxs)(`label`,{className:`flex flex-col gap-1.5 text-sm`,children:[(0,v.jsx)(`span`,{className:`font-medium`,children:`Groq API key`}),(0,v.jsx)(d,{type:`password`,value:n,placeholder:`gsk_…`,autoComplete:`off`,spellCheck:!1,onChange:e=>a(e.target.value)})]}),(0,v.jsx)(`div`,{className:`min-h-5 text-xs ${C}`,role:`status`,children:S}),(0,v.jsxs)(f,{children:[c&&(0,v.jsx)(`span`,{className:`mr-auto self-center text-xs text-green-600 dark:text-green-400`,children:`Saved`}),(0,v.jsx)(m,{type:`button`,variant:`outline`,size:`sm`,onClick:b,children:`Save`}),(0,v.jsx)(m,{type:`button`,size:`sm`,disabled:p||n.trim()===``,onClick:x,children:p?`Testing…`:`Test key`})]})]})})}function K({sessionID:e,appendDraft:t}){let[n,r]=(0,_.useState)(`idle`),[i,a]=(0,_.useState)(0),[o,s]=(0,_.useState)(F),[l,d]=(0,_.useState)(!1),f=(0,_.useRef)(null),p=(0,_.useRef)(null),y=(0,_.useRef)([]),b=(0,_.useRef)(0),x=(0,_.useRef)(null),S=(0,_.useRef)(!1),C=(0,_.useRef)(!0);(0,_.useEffect)(()=>{C.current=!0;let e=P(s);return()=>{C.current=!1,e(),x.current!==null&&(clearInterval(x.current),x.current=null);let t=f.current;if(t&&t.state===`recording`)try{t.onstop=null,t.stop()}catch{}z(p.current),p.current=null,f.current=null,L()}},[]);function w(){x.current!==null&&(clearInterval(x.current),x.current=null),z(p.current),p.current=null,f.current=null,y.current=[],S.current=!1}function E(n){return t?(t(n),!0):W(e,n)}function D(){if(typeof navigator>`u`||!navigator.mediaDevices||typeof navigator.mediaDevices.getUserMedia!=`function`){I({kind:`error`,text:`This browser does not support microphone recording`});return}if(O().trim()===``){I({kind:`error`,text:`Groq API key missing — open the key settings (key button next to the mic)`});return}r(`recording`);let e;try{e=navigator.mediaDevices.getUserMedia({audio:!0})}catch(e){r(`idle`),I({kind:`error`,text:B(e)});return}e.then(e=>{if(!C.current||S.current){z(e);return}p.current=e;let t=R(),n;try{n=t===null?new MediaRecorder(e):new MediaRecorder(e,{mimeType:t})}catch{try{n=new MediaRecorder(e)}catch{if(!C.current){z(e);return}r(`idle`),w(),I({kind:`error`,text:`This browser cannot record audio (MediaRecorder unavailable)`});return}}f.current=n,y.current=[],n.ondataavailable=e=>{e.data&&e.data.size>0&&y.current.push(e.data)},n.onerror=()=>{S.current||!C.current||(r(`idle`),w(),I({kind:`error`,text:`Recording failed — the microphone may be unavailable`}))},n.onstop=()=>{let e=new Blob(y.current,{type:n.mimeType||`audio/webm`}),t=n.mimeType||`audio/webm`,i=C.current;if(w(),e.size===0){i&&(r(`idle`),I({kind:`error`,text:`Recording captured no audio — check the microphone and try again`}));return}N({kind:`working`,text:`Transcribing voice message…`}),H(e,t).then(e=>{let t=E(e);i&&r(`idle`),t?I({kind:`ok`,text:`Transcribed and inserted`},4e3):I({kind:`error`,text:`Transcribed, but could not insert it into the input`})},e=>{i&&r(`idle`),I({kind:`error`,text:V(e)})})};try{n.start(),b.current=Date.now(),a(0),N({kind:`working`,text:`Recording…`}),x.current=setInterval(()=>{if(!C.current)return;let e=Date.now()-b.current,t=Math.round(e/1e3);a(t),N({kind:`working`,text:`Recording… ${t}s`}),e>=T&&(I({kind:`working`,text:`Reached the 2-minute recording limit — sending to Groq…`},4e3),A.current())},500)}catch{r(`idle`),w(),I({kind:`error`,text:`Could not start recording`})}},e=>{S.current||!C.current||(r(`idle`),w(),I({kind:`error`,text:B(e)}))})}function k(){let e=f.current;if(!(!e||e.state!==`recording`)){S.current=!0,x.current!==null&&(clearInterval(x.current),x.current=null),r(`transcribing`);try{e.stop()}catch{w(),r(`idle`),I({kind:`error`,text:`Could not stop the recording`})}}}let A=(0,_.useRef)(k);A.current=k;let j=n===`recording`?`Stop recording`:n===`transcribing`?`Transcribing…`:`Record a voice message`,M=n===`recording`?`Stop recording (${i}s)`:j;return(0,v.jsxs)(`span`,{className:`relative mb-0.5 flex items-center`,"data-groq-voice":!0,children:[n===`recording`?(0,v.jsxs)(m,{type:`button`,variant:`destructive`,size:`sm`,"data-groq-voice-mic":`recording`,"aria-label":j,title:M,onMouseDown:e=>e.preventDefault(),onClick:()=>A.current(),className:`animate-pulse tabular-nums`,children:[(0,v.jsx)(u,{}),i,`s`]}):(0,v.jsx)(m,{type:`button`,variant:`ghost`,size:`icon-sm`,"data-groq-voice-mic":`idle`,"aria-label":j,title:M,disabled:n===`transcribing`,onMouseDown:e=>e.preventDefault(),onClick:D,children:n===`transcribing`?(0,v.jsx)(c,{className:`animate-spin`}):(0,v.jsx)(g,{})}),(0,v.jsx)(m,{type:`button`,variant:`ghost`,size:`icon-sm`,"data-groq-voice-settings":!0,"aria-label":`Groq voice settings`,title:`Groq voice settings — API key`,onMouseDown:e=>e.preventDefault(),onClick:()=>d(!0),children:(0,v.jsx)(h,{})}),o!==null&&(0,v.jsx)(`span`,{role:`status`,"data-groq-voice-status":``,title:o.text,className:`absolute right-0 bottom-full z-30 mb-2 w-max max-w-64 truncate rounded-md border border-[color:var(--border-weak-base)] bg-[color:var(--surface-float-base)] px-2 py-1 text-xs shadow-lg ${o.kind===`error`?`text-red-600 dark:text-red-400`:o.kind===`ok`?`text-green-600 dark:text-green-400`:`text-[color:var(--text-weaker)]`}`,children:o.text}),(0,v.jsx)(G,{open:l,onOpenChange:d})]})}b.register({kind:`wrap`,id:`groq-voice`,target:`composer.sendActions`,render:(e,t)=>{let n=typeof e.sessionID==`string`?e.sessionID:``,r=e.appendDraft,i=typeof r==`function`?r:void 0;return(0,v.jsxs)(v.Fragment,{children:[t(),(0,v.jsx)(K,{sessionID:n,appendDraft:i})]})}});export{y as id};