@openchambery/web 2.1.8-beta.4 → 2.1.8-beta.5

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.
Files changed (45) hide show
  1. package/dist/assets/{AssistantView-Dx-mzVKK.js → AssistantView-DOr44dum.js} +3 -3
  2. package/dist/assets/{DiagramView-CDmixuZQ.js → DiagramView-DwjEsYl2.js} +1 -1
  3. package/dist/assets/{MarkdownRenderer-CggK1I6D.js → MarkdownRenderer-BVTJzJKH.js} +2 -2
  4. package/dist/assets/{MarkdownRendererImpl-Cms-VuSl.js → MarkdownRendererImpl-DKRawvvq.js} +1 -1
  5. package/dist/assets/{MarkstreamRendererImpl-CrC7_3Y7.js → MarkstreamRendererImpl-8IdRnZ2d.js} +1 -1
  6. package/dist/assets/{MobileDetailNavigation-CunQ1OIb.js → MobileDetailNavigation-WqYrphqb.js} +1 -1
  7. package/dist/assets/{MobileShareBridge-CoRbF_fc.js → MobileShareBridge-C6AvFHjG.js} +1 -1
  8. package/dist/assets/{MobileTabPageHeader-Bj_Lu8tt.js → MobileTabPageHeader-BbDsAtKl.js} +1 -1
  9. package/dist/assets/{MultiRunWindow-BgNee8Nh.js → MultiRunWindow-19VPj6Of.js} +1 -1
  10. package/dist/assets/{SettingsView-D0Rk6ERT.js → SettingsView-DruiCw4C.js} +1 -1
  11. package/dist/assets/{SettingsWindow-8i92GNln.js → SettingsWindow-BBfF03M7.js} +1 -1
  12. package/dist/assets/{TerminalView-mLwQ0UU9.js → TerminalView-BE1wI19_.js} +1 -1
  13. package/dist/assets/{ToolOutputDialog-DH6ASkUk.js → ToolOutputDialog-DOXDmUxQ.js} +1 -1
  14. package/dist/assets/{appThemeRegistry-CHeMJdPY.js → appThemeRegistry-D4TkQDbf.js} +1 -1
  15. package/dist/assets/{fileMentionTouchSelection-CSelAE3Z.js → fileMentionTouchSelection-DuGw9C0F.js} +1 -1
  16. package/dist/assets/{gitApi-B123oHQk.js → gitApi-QxaZos8j.js} +1 -1
  17. package/dist/assets/{insertionBoundaries-cfka-ff_.js → insertionBoundaries-BFtbjSBb.js} +1 -1
  18. package/dist/assets/{main-IO37JIWf.js → main-BJckugyb.js} +2 -2
  19. package/dist/assets/{main-DJhLxkOQ.js → main-CSpFdmT4.js} +4 -4
  20. package/dist/assets/{mcpQueries-Digedyij.js → mcpQueries-WrVQC4xT.js} +1 -1
  21. package/dist/assets/{miniChat-mE8pRELI.js → miniChat-YVKGcjkV.js} +2 -2
  22. package/dist/assets/{mobile-Cia2889g.js → mobile-DtAt6Tnq.js} +2 -2
  23. package/dist/assets/{multirun-C1I9Attc.js → multirun-lQfWeDYv.js} +1 -1
  24. package/dist/assets/{projectMeta-C2WeOasQ.js → projectMeta-gv05m6Dm.js} +1 -1
  25. package/dist/assets/{renderElectronMiniChatApp-FpoAJUKI.js → renderElectronMiniChatApp-BRhwzG7e.js} +2 -2
  26. package/dist/assets/{renderMobileApp-DrT0ZwHw.js → renderMobileApp-FbnzqDL3.js} +4 -4
  27. package/dist/assets/{runtimeConfig-yW48patn.js → runtimeConfig-Bm1EPWPq.js} +1 -1
  28. package/dist/assets/{runtimeEndpointReset-CMmN7tH1.js → runtimeEndpointReset-C4gavnsn.js} +1 -1
  29. package/dist/assets/{sessionChangeSummary-D1YrkHbB.js → sessionChangeSummary-DkNmkmIq.js} +1 -1
  30. package/dist/assets/{sessionLookup-DrKLmlMv.js → sessionLookup-DWJfnR75.js} +1 -1
  31. package/dist/assets/{speechSettingsQueries-BNPsqHp5.js → speechSettingsQueries-CoNlugxH.js} +1 -1
  32. package/dist/assets/{useAppFontEffects-BUJapti0.js → useAppFontEffects-DAeZcw5Z.js} +5 -5
  33. package/dist/assets/{useDesktopWindowControlsLayout-Dlh9xNoE.js → useDesktopWindowControlsLayout-ia1w5b_c.js} +1 -1
  34. package/dist/assets/{useEffectiveDirectory-2dkaAaxr.js → useEffectiveDirectory-BWMFhdHo.js} +1 -1
  35. package/dist/assets/{useMobileNavigationStore-BRQBoMI1.js → useMobileNavigationStore-D7wof6Tl.js} +3 -3
  36. package/dist/assets/{useWorkerHighlightedLines-BRAfFOVn.js → useWorkerHighlightedLines-DdBSr8AB.js} +1 -1
  37. package/dist/index.html +2 -2
  38. package/dist/mini-chat.html +2 -2
  39. package/dist/mobile.html +2 -2
  40. package/package.json +1 -1
  41. package/server/lib/opencode/DOCUMENTATION.md +1 -0
  42. package/server/lib/opencode/config-entities-layering.test.js +2 -1
  43. package/server/lib/opencode/plugin-routes.test.js +1 -1
  44. package/server/lib/opencode/plugins.js +31 -21
  45. package/server/lib/opencode/plugins.test.js +55 -10
@@ -1,4 +1,4 @@
1
- import{r as e}from"./rolldown-runtime-hePW80VL.js";import{n as t}from"./runtime-fetch-QCWYlAx0.js";import{Mn as n,ln as r}from"./persistence-B7ORJ371.js";import{it as i,rt as a}from"./vendor-base-ui-CdzJ6My_.js";import{n as o}from"./platform-CiBU2bCp.js";import{v as s}from"./useUIStore-CDMI_Yzu.js";import{n as c}from"./vendor-react-BP2cRVqX.js";import{a as l,n as u}from"./vendor-aparajita-capacitor-secure-storage-xol_6BsB.js";import{f as d,r as f,t as p}from"./button-DOrwfkre.js";import{n as m}from"./useThemeSystem-8jhE4Zyk.js";import{n as h}from"./Icon-QASb6r3F.js";import{i as g,n as _,t as v}from"./tooltip-JgW0lJaV.js";import{a as y,i as b,o as x,t as S}from"./image-loading-qNtUCy69.js";import{n as C}from"./MarkdownRenderer-CggK1I6D.js";import{n as w,o as T,s as E,t as D}from"./markdown-worker-DFNZ3KJ_.js";import{n as ee}from"./markdownTheme-CV7_0QAL.js";import{t as te}from"./vendor-tanstack-react-virtual-B_EO--9t.js";var O=e(i(),1),k=c(),A=a(),ne=`w-[min(30rem,calc(100cqw-1rem))] min-w-[280px] max-w-full overflow-hidden rounded-xl shadow-[inset_0_1px_0_0_rgba(255,255,255,0.8),inset_0_0_0_1px_rgba(0,0,0,0.04),0_0_0_1px_rgba(0,0,0,0.10),0_1px_2px_-0.5px_rgba(0,0,0,0.08),0_4px_8px_-2px_rgba(0,0,0,0.08),0_12px_20px_-4px_rgba(0,0,0,0.08)] dark:shadow-[inset_0_1px_0_0_rgba(255,255,255,0.12),inset_0_0_0_1px_rgba(255,255,255,0.08),0_0_0_1px_rgba(0,0,0,0.36),0_1px_1px_-0.5px_rgba(0,0,0,0.22),0_3px_3px_-1.5px_rgba(0,0,0,0.20),0_6px_6px_-3px_rgba(0,0,0,0.16)]`,j={maxWidth:`min(28rem, calc(100cqw - 4ch))`,backgroundColor:`var(--surface-elevated)`,color:`var(--surface-elevated-foreground)`},M=`flex items-center justify-between gap-3 border-b border-[var(--surface-subtle)] px-3 py-2.5`,N=`text-xs font-medium leading-4 text-foreground md:text-[0.8125rem] md:leading-5`,re=`text-xs tabular-nums leading-4 text-muted-foreground md:text-[0.8125rem] md:leading-5`,P=`m-0 list-none divide-y divide-[var(--surface-subtle)] p-0`,F=`${P} max-h-[min(22rem,50vh)] overflow-y-auto`,ie=`${P} w-full`,ae={className:`p-0 rounded-none`,maxHeightClass:`max-h-[46vh]`,disableHorizontal:!0},I=`px-2.5 py-1.5 text-xs leading-4 md:py-2 md:text-[0.8125rem] md:leading-5`,oe=`flex min-w-0 items-center gap-1 text-xs leading-4 text-muted-foreground md:gap-1.5 md:text-[0.8125rem] md:leading-5`,se=`min-w-0 truncate text-xs leading-4 text-foreground md:text-[0.8125rem] md:leading-5`,ce=`inline-flex shrink-0 items-center gap-1 text-xs leading-4 tabular-nums text-muted-foreground md:text-[0.8125rem] md:leading-5`,le=`h-3 w-3 shrink-0 md:h-3.5 md:w-3.5`,ue={in_progress:`text-foreground`,pending:`text-foreground`,completed:`text-muted-foreground line-through`,cancelled:`text-muted-foreground line-through`},L={in_progress:`chat.statusRow.todo.status.inProgress`,pending:`chat.statusRow.todo.status.pending`,completed:`chat.statusRow.todo.status.completed`,cancelled:`chat.statusRow.todo.status.cancelled`},R=e=>{let t=(0,k.c)(26),{todo:n,isActive:r,activeRef:i}=e,{t:a}=d(),o=z.has(n.status)?n.status:`pending`,s=L[o],c;t[0]!==a||t[1]!==s?(c=a(s),t[0]=a,t[1]=s,t[2]=c):c=t[2];let l=c,u;t[3]===o?u=t[4]:(u=o===`in_progress`?(0,A.jsx)(h,{name:`record-circle`,className:`h-3.5 w-3.5 text-[var(--status-info)]`,"aria-hidden":`true`}):o===`completed`?(0,A.jsx)(h,{name:`checkbox-circle`,className:`h-3.5 w-3.5 text-[var(--status-success)]`,"aria-hidden":`true`}):(0,A.jsx)(h,{name:`time`,className:`h-3.5 w-3.5 text-muted-foreground`,"aria-hidden":`true`}),t[3]=o,t[4]=u);let p=u,m=r?`step`:void 0,y=r&&`bg-[var(--surface-muted)] before:absolute before:inset-y-2 before:left-0 before:w-0.5 before:rounded-full before:bg-[var(--status-info)]`,b;t[5]===y?b=t[6]:(b=f(`group relative grid min-w-0 grid-cols-[1rem_minmax(0,1fr)] items-start gap-2.5`,I,y),t[5]=y,t[6]=b);let x;t[7]!==p||t[8]!==l?(x=(0,A.jsx)(g,{asChild:!0,children:(0,A.jsx)(`span`,{className:`flex size-4 items-center justify-center`,"aria-label":l,children:p})}),t[7]=p,t[8]=l,t[9]=x):x=t[9];let S;t[10]===l?S=t[11]:(S=(0,A.jsx)(_,{side:`left`,sideOffset:6,children:l}),t[10]=l,t[11]=S);let C;t[12]!==x||t[13]!==S?(C=(0,A.jsxs)(v,{children:[x,S]}),t[12]=x,t[13]=S,t[14]=C):C=t[14];let w=ue[o],T;t[15]===w?T=t[16]:(T=f(`block whitespace-normal break-words`,w),t[15]=w,t[16]=T);let E;t[17]!==T||t[18]!==n.content?(E=(0,A.jsx)(`span`,{className:`min-w-0 flex-1`,children:(0,A.jsx)(`span`,{className:T,children:n.content})}),t[17]=T,t[18]=n.content,t[19]=E):E=t[19];let D;return t[20]!==i||t[21]!==E||t[22]!==m||t[23]!==b||t[24]!==C?(D=(0,A.jsxs)(`li`,{ref:i,"aria-current":m,className:b,children:[C,E]}),t[20]=i,t[21]=E,t[22]=m,t[23]=b,t[24]=C,t[25]=D):D=t[25],D},z=new Set([`in_progress`,`pending`,`completed`,`cancelled`]),B=`image/png`,V=l(`OpenChamberMedia`),H=e=>{let t=e.toLowerCase().split(`;`)[0]?.trim()||B;if(t===`image/jpeg`||t===`image/jpg`)return`jpg`;if(t===`image/webp`)return`webp`;if(t===`image/gif`)return`gif`;if(t===`image/svg+xml`)return`svg`;if(t===`image/png`)return`png`;let n=t.split(`/`)[1];return n&&/^[a-z0-9.+-]+$/i.test(n)?n:`png`},U=(e,t)=>{let n=`image.${H(t)}`,r=(((e||``).trim()||n).split(/[\\/]/).pop()||n).replace(/[^\w.\- ()[\]]+/g,`_`).replace(/^\.+/,``);return r?/\.[a-z0-9]{1,8}$/i.test(r)?r:`${r}.${H(t)}`:n},W=e=>new Promise((t,n)=>{let r=new FileReader;r.onload=()=>{if(typeof r.result==`string`){t(r.result);return}n(Error(`Failed to encode image`))},r.onerror=()=>n(r.error??Error(`Failed to encode image`)),r.readAsDataURL(e)}),de=async e=>{let t=await W(e),n=t.indexOf(`,`);return n>=0?t.slice(n+1):t},fe=(e,t)=>{let n=URL.createObjectURL(e),r=document.createElement(`a`);r.href=n,r.download=t,document.body.appendChild(r),r.click(),document.body.removeChild(r),window.setTimeout(()=>URL.revokeObjectURL(n),1e3)},G=async(e,t)=>{let n=await fetch(e,{signal:t,cache:`no-store`});if(!n.ok)throw Error(`Image fetch failed (${n.status})`);return n.blob()},pe=async(e,t=B)=>{if(!e.naturalWidth||!e.naturalHeight)return null;let n=e.currentSrc||e.src;if(n&&(/^(?:blob:|data:|openchamber-asset:)/i.test(n)||/^https?:/i.test(n)||/^\/api(?=\/|[?#]|$)/.test(n)))try{let e=await G(n);if(e.size>0)return e}catch{}try{let n=document.createElement(`canvas`);n.width=e.naturalWidth,n.height=e.naturalHeight;let r=n.getContext(`2d`);if(!r)return null;r.drawImage(e,0,0);let i=t.startsWith(`image/`)?t:B,a=await new Promise(e=>{n.toBlob(t=>e(t),i===`image/jpg`?`image/jpeg`:i)});return a&&a.size>0?a:null}catch{return null}},me=async(e,t)=>{let n=``;try{return n=await S(e,t??new AbortController().signal),await G(n,t)}finally{n&&y(n)}},he=async(e,n)=>{let r=e.effectiveDirectory??``,i=null;e.prefetchedBlob&&e.prefetchedBlob.size>0&&(i=e.prefetchedBlob);let a=e.displayUrl?.trim();if(!i&&a)try{i=await G(a,n)}catch{i=null}if(!i){let a=e.sourceUrl.trim();if(!a)throw Error(`Image source is empty`);if(/^(?:data|blob):/i.test(a)||/^https?:/i.test(a)||/^\/api(?=\/|[?#]|$)/.test(a))i=await G(a,n);else{let e=x(a,r);if(b(e))try{i=await me(e.path,n)}catch{let r=await t(`/api/fs/raw`,{method:`GET`,cache:`no-store`,query:{path:e.path},signal:n});if(!r.ok)throw Error(`Image source request failed with status ${r.status}`);i=await r.blob()}else i=await G(e.source,n)}}if(!i||i.size<=0)throw Error(`Image is empty`);let o=(e.mimeType||(i.type&&i.type.startsWith(`image/`)?i.type:``)||B).split(`;`)[0].trim()||B;return{blob:i,filename:U(e.filename,o),mimeType:o}},ge=()=>!o()||u===void 0?!1:u.isPluginAvailable(`OpenChamberMedia`),_e=async(e,t)=>{let{blob:i,filename:a,mimeType:o}=await he(e,t);if(ge()){let e=await de(i);return await V.saveImage({dataBase64:e,mimeType:o,filename:a}),`saved`}if(r()){let e=n()?.vscode;if(e?.saveImage){let t=await W(i);return await e.saveImage({fileName:a,dataUrl:t}),`saved`}}return fe(i,a),`downloaded`},ve=e=>e.replace(/&/g,`&amp;`).replace(/</g,`&lt;`).replace(/>/g,`&gt;`).replace(/"/g,`&quot;`).replace(/'/g,`&#39;`),ye=e=>e?Object.entries(e).map(([e,t])=>t==null?``:`${e.startsWith(`--`)?e:e.replace(/[A-Z]/g,e=>`-${e.toLowerCase()}`)}:${typeof t==`number`?`${t}px`:t};`).join(``):``,K=(e,t,n)=>{let r=e.querySelector(`pre`);r&&(r.style.margin=`0`,r.style.background=`transparent`,r.style.whiteSpace=t?`pre-wrap`:`pre`,t&&(r.style.wordBreak=`break-word`,r.style.overflowWrap=`break-word`));let i=e.querySelector(`code`);if(i){let e=ye(n);e&&i.setAttribute(`style`,`${i.getAttribute(`style`)??``}${e}`)}},be=e=>`<pre><code>${ve(e)}</code></pre>`,xe=e=>{let t=(0,k.c)(21),{code:n,language:r,className:i,style:a,codeStyle:o,wrap:s}=e,c=s!==void 0&&s,{currentTheme:l}=m(),u=O.useRef(null),d;t[0]===l?d=t[1]:(d=ee(l),t[0]=l,t[1]=d);let p=d,h,g;t[2]!==n||t[3]!==o||t[4]!==c?(h=()=>{let e=u.current;e&&(e.innerHTML=be(n),K(e,c,o))},g=[n,c,o],t[2]=n,t[3]=o,t[4]=c,t[5]=h,t[6]=g):(h=t[5],g=t[6]),O.useLayoutEffect(h,g);let _,v;t[7]!==n||t[8]!==o||t[9]!==r||t[10]!==c?(_=()=>{let e=u.current;if(!e)return;let t=!0,i=new AbortController;return D(n,(r||`text`).toLowerCase(),{signal:i.signal,priority:`visible`}).then(n=>{!t||!e||!n||(e.innerHTML=n,K(e,c,o))}),()=>{t=!1,i.abort()}},v=[n,r,c,o],t[7]=n,t[8]=o,t[9]=r,t[10]=c,t[11]=_,t[12]=v):(_=t[11],v=t[12]),O.useEffect(_,v);let y;t[13]===i?y=t[14]:(y=f(`typography-code`,i),t[13]=i,t[14]=y);let b=p,x;t[15]!==a||t[16]!==b?(x={...b,...a},t[15]=a,t[16]=b,t[17]=x):x=t[17];let S;return t[18]!==y||t[19]!==x?(S=(0,A.jsx)(`div`,{ref:u,className:y,style:x}),t[18]=y,t[19]=x,t[20]=S):S=t[20],S},Se=e=>{let t=e.replace(/^<file>\s*\n?/,``).replace(/\n?<\/file>\s*$/,``);return t=t.replace(/^\s*\d{5}\|\s?/gm,``),t.trim()},Ce=(e,t=``)=>{if(typeof e==`string`)return e;if(e==null)return t;if(typeof e==`number`||typeof e==`boolean`||typeof e==`bigint`)return String(e);try{return JSON.stringify(e)}catch{return t}},we=e=>e?e.includes(`<diagnostics`)||e.includes(`<file_diagnostics>`)||e.includes(`LSP errors detected`)||e.includes(`This file has errors`):!1,Te=e=>e?e.replace(/\n{0,2}LSP errors detected[\s\S]*?<diagnostics[^>]*>[\s\S]*?<\/diagnostics>/g,``).replace(/\n{0,2}This file has errors[\s\S]*?<\/file_diagnostics>/g,``).replace(/<diagnostics[^>]*>[\s\S]*?<\/diagnostics>/g,``).replace(/<file_diagnostics>[\s\S]*?<\/file_diagnostics>/g,``).trim():``,Ee=(e,t)=>!e||typeof e!=`object`?String(e):E(e,t||``),q=e=>{if(typeof e==`string`){let t=e.trim();return t.length>0?t:void 0}if(e&&typeof e==`object`){let t=e.patch;if(typeof t==`string`){let e=t.trim();return e.length>0?e:void 0}}},De=e=>{if(!e||typeof e!=`object`)return;let t=q(e.patch)??q(e.diff);if(t)return t;let n=Array.isArray(e.files)?e.files:[];for(let e of n){if(!e||typeof e!=`object`)continue;let t=q(e.patch)??q(e.diff);if(t)return t}},Oe=e=>{if(!e||typeof e!=`string`)return{data:null,isJson:!1};let t=e.trim();if(!t.startsWith(`{`)&&!t.startsWith(`[`)||!t.endsWith(`}`)&&!t.endsWith(`]`)||t.length<2)return{data:null,isJson:!1};try{let e=JSON.parse(t);return typeof e==`object`&&e?{data:e,isJson:!0}:{data:null,isJson:!1}}catch{return{data:null,isJson:!1}}},ke=(e,t,n)=>{let r=Se(e);if((t===`edit`||t===`multiedit`||t===`write`||t===`apply_patch`)&&we(r)&&(r=Te(r)),(t===`edit`||t===`multiedit`||t===`apply_patch`)&&r.trim().length===0){let e=De(n);if(e)return e}return r},Ae=e=>{let t=e.match(/<type>(file|directory)<\/type>/i)?.[1]?.toLowerCase()??`unknown`,n=(e.match(/<content>([\s\S]*?)<\/content>/i)?.[1]??e).replace(/\r\n/g,`
1
+ import{r as e}from"./rolldown-runtime-hePW80VL.js";import{n as t}from"./runtime-fetch-QCWYlAx0.js";import{Mn as n,ln as r}from"./persistence-B7ORJ371.js";import{it as i,rt as a}from"./vendor-base-ui-CdzJ6My_.js";import{n as o}from"./platform-CiBU2bCp.js";import{v as s}from"./useUIStore-CDMI_Yzu.js";import{n as c}from"./vendor-react-BP2cRVqX.js";import{a as l,n as u}from"./vendor-aparajita-capacitor-secure-storage-xol_6BsB.js";import{f as d,r as f,t as p}from"./button-DOrwfkre.js";import{n as m}from"./useThemeSystem-8jhE4Zyk.js";import{n as h}from"./Icon-QASb6r3F.js";import{i as g,n as _,t as v}from"./tooltip-JgW0lJaV.js";import{a as y,i as b,o as x,t as S}from"./image-loading-qNtUCy69.js";import{n as C}from"./MarkdownRenderer-BVTJzJKH.js";import{n as w,o as T,s as E,t as D}from"./markdown-worker-DFNZ3KJ_.js";import{n as ee}from"./markdownTheme-CV7_0QAL.js";import{t as te}from"./vendor-tanstack-react-virtual-B_EO--9t.js";var O=e(i(),1),k=c(),A=a(),ne=`w-[min(30rem,calc(100cqw-1rem))] min-w-[280px] max-w-full overflow-hidden rounded-xl shadow-[inset_0_1px_0_0_rgba(255,255,255,0.8),inset_0_0_0_1px_rgba(0,0,0,0.04),0_0_0_1px_rgba(0,0,0,0.10),0_1px_2px_-0.5px_rgba(0,0,0,0.08),0_4px_8px_-2px_rgba(0,0,0,0.08),0_12px_20px_-4px_rgba(0,0,0,0.08)] dark:shadow-[inset_0_1px_0_0_rgba(255,255,255,0.12),inset_0_0_0_1px_rgba(255,255,255,0.08),0_0_0_1px_rgba(0,0,0,0.36),0_1px_1px_-0.5px_rgba(0,0,0,0.22),0_3px_3px_-1.5px_rgba(0,0,0,0.20),0_6px_6px_-3px_rgba(0,0,0,0.16)]`,j={maxWidth:`min(28rem, calc(100cqw - 4ch))`,backgroundColor:`var(--surface-elevated)`,color:`var(--surface-elevated-foreground)`},M=`flex items-center justify-between gap-3 border-b border-[var(--surface-subtle)] px-3 py-2.5`,N=`text-xs font-medium leading-4 text-foreground md:text-[0.8125rem] md:leading-5`,re=`text-xs tabular-nums leading-4 text-muted-foreground md:text-[0.8125rem] md:leading-5`,P=`m-0 list-none divide-y divide-[var(--surface-subtle)] p-0`,F=`${P} max-h-[min(22rem,50vh)] overflow-y-auto`,ie=`${P} w-full`,ae={className:`p-0 rounded-none`,maxHeightClass:`max-h-[46vh]`,disableHorizontal:!0},I=`px-2.5 py-1.5 text-xs leading-4 md:py-2 md:text-[0.8125rem] md:leading-5`,oe=`flex min-w-0 items-center gap-1 text-xs leading-4 text-muted-foreground md:gap-1.5 md:text-[0.8125rem] md:leading-5`,se=`min-w-0 truncate text-xs leading-4 text-foreground md:text-[0.8125rem] md:leading-5`,ce=`inline-flex shrink-0 items-center gap-1 text-xs leading-4 tabular-nums text-muted-foreground md:text-[0.8125rem] md:leading-5`,le=`h-3 w-3 shrink-0 md:h-3.5 md:w-3.5`,ue={in_progress:`text-foreground`,pending:`text-foreground`,completed:`text-muted-foreground line-through`,cancelled:`text-muted-foreground line-through`},L={in_progress:`chat.statusRow.todo.status.inProgress`,pending:`chat.statusRow.todo.status.pending`,completed:`chat.statusRow.todo.status.completed`,cancelled:`chat.statusRow.todo.status.cancelled`},R=e=>{let t=(0,k.c)(26),{todo:n,isActive:r,activeRef:i}=e,{t:a}=d(),o=z.has(n.status)?n.status:`pending`,s=L[o],c;t[0]!==a||t[1]!==s?(c=a(s),t[0]=a,t[1]=s,t[2]=c):c=t[2];let l=c,u;t[3]===o?u=t[4]:(u=o===`in_progress`?(0,A.jsx)(h,{name:`record-circle`,className:`h-3.5 w-3.5 text-[var(--status-info)]`,"aria-hidden":`true`}):o===`completed`?(0,A.jsx)(h,{name:`checkbox-circle`,className:`h-3.5 w-3.5 text-[var(--status-success)]`,"aria-hidden":`true`}):(0,A.jsx)(h,{name:`time`,className:`h-3.5 w-3.5 text-muted-foreground`,"aria-hidden":`true`}),t[3]=o,t[4]=u);let p=u,m=r?`step`:void 0,y=r&&`bg-[var(--surface-muted)] before:absolute before:inset-y-2 before:left-0 before:w-0.5 before:rounded-full before:bg-[var(--status-info)]`,b;t[5]===y?b=t[6]:(b=f(`group relative grid min-w-0 grid-cols-[1rem_minmax(0,1fr)] items-start gap-2.5`,I,y),t[5]=y,t[6]=b);let x;t[7]!==p||t[8]!==l?(x=(0,A.jsx)(g,{asChild:!0,children:(0,A.jsx)(`span`,{className:`flex size-4 items-center justify-center`,"aria-label":l,children:p})}),t[7]=p,t[8]=l,t[9]=x):x=t[9];let S;t[10]===l?S=t[11]:(S=(0,A.jsx)(_,{side:`left`,sideOffset:6,children:l}),t[10]=l,t[11]=S);let C;t[12]!==x||t[13]!==S?(C=(0,A.jsxs)(v,{children:[x,S]}),t[12]=x,t[13]=S,t[14]=C):C=t[14];let w=ue[o],T;t[15]===w?T=t[16]:(T=f(`block whitespace-normal break-words`,w),t[15]=w,t[16]=T);let E;t[17]!==T||t[18]!==n.content?(E=(0,A.jsx)(`span`,{className:`min-w-0 flex-1`,children:(0,A.jsx)(`span`,{className:T,children:n.content})}),t[17]=T,t[18]=n.content,t[19]=E):E=t[19];let D;return t[20]!==i||t[21]!==E||t[22]!==m||t[23]!==b||t[24]!==C?(D=(0,A.jsxs)(`li`,{ref:i,"aria-current":m,className:b,children:[C,E]}),t[20]=i,t[21]=E,t[22]=m,t[23]=b,t[24]=C,t[25]=D):D=t[25],D},z=new Set([`in_progress`,`pending`,`completed`,`cancelled`]),B=`image/png`,V=l(`OpenChamberMedia`),H=e=>{let t=e.toLowerCase().split(`;`)[0]?.trim()||B;if(t===`image/jpeg`||t===`image/jpg`)return`jpg`;if(t===`image/webp`)return`webp`;if(t===`image/gif`)return`gif`;if(t===`image/svg+xml`)return`svg`;if(t===`image/png`)return`png`;let n=t.split(`/`)[1];return n&&/^[a-z0-9.+-]+$/i.test(n)?n:`png`},U=(e,t)=>{let n=`image.${H(t)}`,r=(((e||``).trim()||n).split(/[\\/]/).pop()||n).replace(/[^\w.\- ()[\]]+/g,`_`).replace(/^\.+/,``);return r?/\.[a-z0-9]{1,8}$/i.test(r)?r:`${r}.${H(t)}`:n},W=e=>new Promise((t,n)=>{let r=new FileReader;r.onload=()=>{if(typeof r.result==`string`){t(r.result);return}n(Error(`Failed to encode image`))},r.onerror=()=>n(r.error??Error(`Failed to encode image`)),r.readAsDataURL(e)}),de=async e=>{let t=await W(e),n=t.indexOf(`,`);return n>=0?t.slice(n+1):t},fe=(e,t)=>{let n=URL.createObjectURL(e),r=document.createElement(`a`);r.href=n,r.download=t,document.body.appendChild(r),r.click(),document.body.removeChild(r),window.setTimeout(()=>URL.revokeObjectURL(n),1e3)},G=async(e,t)=>{let n=await fetch(e,{signal:t,cache:`no-store`});if(!n.ok)throw Error(`Image fetch failed (${n.status})`);return n.blob()},pe=async(e,t=B)=>{if(!e.naturalWidth||!e.naturalHeight)return null;let n=e.currentSrc||e.src;if(n&&(/^(?:blob:|data:|openchamber-asset:)/i.test(n)||/^https?:/i.test(n)||/^\/api(?=\/|[?#]|$)/.test(n)))try{let e=await G(n);if(e.size>0)return e}catch{}try{let n=document.createElement(`canvas`);n.width=e.naturalWidth,n.height=e.naturalHeight;let r=n.getContext(`2d`);if(!r)return null;r.drawImage(e,0,0);let i=t.startsWith(`image/`)?t:B,a=await new Promise(e=>{n.toBlob(t=>e(t),i===`image/jpg`?`image/jpeg`:i)});return a&&a.size>0?a:null}catch{return null}},me=async(e,t)=>{let n=``;try{return n=await S(e,t??new AbortController().signal),await G(n,t)}finally{n&&y(n)}},he=async(e,n)=>{let r=e.effectiveDirectory??``,i=null;e.prefetchedBlob&&e.prefetchedBlob.size>0&&(i=e.prefetchedBlob);let a=e.displayUrl?.trim();if(!i&&a)try{i=await G(a,n)}catch{i=null}if(!i){let a=e.sourceUrl.trim();if(!a)throw Error(`Image source is empty`);if(/^(?:data|blob):/i.test(a)||/^https?:/i.test(a)||/^\/api(?=\/|[?#]|$)/.test(a))i=await G(a,n);else{let e=x(a,r);if(b(e))try{i=await me(e.path,n)}catch{let r=await t(`/api/fs/raw`,{method:`GET`,cache:`no-store`,query:{path:e.path},signal:n});if(!r.ok)throw Error(`Image source request failed with status ${r.status}`);i=await r.blob()}else i=await G(e.source,n)}}if(!i||i.size<=0)throw Error(`Image is empty`);let o=(e.mimeType||(i.type&&i.type.startsWith(`image/`)?i.type:``)||B).split(`;`)[0].trim()||B;return{blob:i,filename:U(e.filename,o),mimeType:o}},ge=()=>!o()||u===void 0?!1:u.isPluginAvailable(`OpenChamberMedia`),_e=async(e,t)=>{let{blob:i,filename:a,mimeType:o}=await he(e,t);if(ge()){let e=await de(i);return await V.saveImage({dataBase64:e,mimeType:o,filename:a}),`saved`}if(r()){let e=n()?.vscode;if(e?.saveImage){let t=await W(i);return await e.saveImage({fileName:a,dataUrl:t}),`saved`}}return fe(i,a),`downloaded`},ve=e=>e.replace(/&/g,`&amp;`).replace(/</g,`&lt;`).replace(/>/g,`&gt;`).replace(/"/g,`&quot;`).replace(/'/g,`&#39;`),ye=e=>e?Object.entries(e).map(([e,t])=>t==null?``:`${e.startsWith(`--`)?e:e.replace(/[A-Z]/g,e=>`-${e.toLowerCase()}`)}:${typeof t==`number`?`${t}px`:t};`).join(``):``,K=(e,t,n)=>{let r=e.querySelector(`pre`);r&&(r.style.margin=`0`,r.style.background=`transparent`,r.style.whiteSpace=t?`pre-wrap`:`pre`,t&&(r.style.wordBreak=`break-word`,r.style.overflowWrap=`break-word`));let i=e.querySelector(`code`);if(i){let e=ye(n);e&&i.setAttribute(`style`,`${i.getAttribute(`style`)??``}${e}`)}},be=e=>`<pre><code>${ve(e)}</code></pre>`,xe=e=>{let t=(0,k.c)(21),{code:n,language:r,className:i,style:a,codeStyle:o,wrap:s}=e,c=s!==void 0&&s,{currentTheme:l}=m(),u=O.useRef(null),d;t[0]===l?d=t[1]:(d=ee(l),t[0]=l,t[1]=d);let p=d,h,g;t[2]!==n||t[3]!==o||t[4]!==c?(h=()=>{let e=u.current;e&&(e.innerHTML=be(n),K(e,c,o))},g=[n,c,o],t[2]=n,t[3]=o,t[4]=c,t[5]=h,t[6]=g):(h=t[5],g=t[6]),O.useLayoutEffect(h,g);let _,v;t[7]!==n||t[8]!==o||t[9]!==r||t[10]!==c?(_=()=>{let e=u.current;if(!e)return;let t=!0,i=new AbortController;return D(n,(r||`text`).toLowerCase(),{signal:i.signal,priority:`visible`}).then(n=>{!t||!e||!n||(e.innerHTML=n,K(e,c,o))}),()=>{t=!1,i.abort()}},v=[n,r,c,o],t[7]=n,t[8]=o,t[9]=r,t[10]=c,t[11]=_,t[12]=v):(_=t[11],v=t[12]),O.useEffect(_,v);let y;t[13]===i?y=t[14]:(y=f(`typography-code`,i),t[13]=i,t[14]=y);let b=p,x;t[15]!==a||t[16]!==b?(x={...b,...a},t[15]=a,t[16]=b,t[17]=x):x=t[17];let S;return t[18]!==y||t[19]!==x?(S=(0,A.jsx)(`div`,{ref:u,className:y,style:x}),t[18]=y,t[19]=x,t[20]=S):S=t[20],S},Se=e=>{let t=e.replace(/^<file>\s*\n?/,``).replace(/\n?<\/file>\s*$/,``);return t=t.replace(/^\s*\d{5}\|\s?/gm,``),t.trim()},Ce=(e,t=``)=>{if(typeof e==`string`)return e;if(e==null)return t;if(typeof e==`number`||typeof e==`boolean`||typeof e==`bigint`)return String(e);try{return JSON.stringify(e)}catch{return t}},we=e=>e?e.includes(`<diagnostics`)||e.includes(`<file_diagnostics>`)||e.includes(`LSP errors detected`)||e.includes(`This file has errors`):!1,Te=e=>e?e.replace(/\n{0,2}LSP errors detected[\s\S]*?<diagnostics[^>]*>[\s\S]*?<\/diagnostics>/g,``).replace(/\n{0,2}This file has errors[\s\S]*?<\/file_diagnostics>/g,``).replace(/<diagnostics[^>]*>[\s\S]*?<\/diagnostics>/g,``).replace(/<file_diagnostics>[\s\S]*?<\/file_diagnostics>/g,``).trim():``,Ee=(e,t)=>!e||typeof e!=`object`?String(e):E(e,t||``),q=e=>{if(typeof e==`string`){let t=e.trim();return t.length>0?t:void 0}if(e&&typeof e==`object`){let t=e.patch;if(typeof t==`string`){let e=t.trim();return e.length>0?e:void 0}}},De=e=>{if(!e||typeof e!=`object`)return;let t=q(e.patch)??q(e.diff);if(t)return t;let n=Array.isArray(e.files)?e.files:[];for(let e of n){if(!e||typeof e!=`object`)continue;let t=q(e.patch)??q(e.diff);if(t)return t}},Oe=e=>{if(!e||typeof e!=`string`)return{data:null,isJson:!1};let t=e.trim();if(!t.startsWith(`{`)&&!t.startsWith(`[`)||!t.endsWith(`}`)&&!t.endsWith(`]`)||t.length<2)return{data:null,isJson:!1};try{let e=JSON.parse(t);return typeof e==`object`&&e?{data:e,isJson:!0}:{data:null,isJson:!1}}catch{return{data:null,isJson:!1}}},ke=(e,t,n)=>{let r=Se(e);if((t===`edit`||t===`multiedit`||t===`write`||t===`apply_patch`)&&we(r)&&(r=Te(r)),(t===`edit`||t===`multiedit`||t===`apply_patch`)&&r.trim().length===0){let e=De(n);if(e)return e}return r},Ae=e=>{let t=e.match(/<type>(file|directory)<\/type>/i)?.[1]?.toLowerCase()??`unknown`,n=(e.match(/<content>([\s\S]*?)<\/content>/i)?.[1]??e).replace(/\r\n/g,`
2
2
  `).split(`
3
3
  `),r=e=>/\(\s*File has more lines\..*offset.*\)/i.test(e.trim());return{type:t,lines:n.map(e=>{let n=e.trim(),i=n.startsWith(`(`)&&n.endsWith(`)`)||r(n);if(t!==`directory`){let t=e.match(/^(\d+):\s?(.*)$/);if(t){let e=t[2],n=e.trim(),i=n.startsWith(`(`)&&n.endsWith(`)`)||r(n);return{lineNumber:i?null:Number(t[1]),text:e,isInfo:i}}}return{lineNumber:null,text:e,isInfo:i}}).filter((e,t,n)=>{if(e.text.trim().length>0)return!0;let r=n[t-1],i=n[t+1],a=!!(r?.isInfo||i?.isInfo),o=e.lineNumber!==null;return!(a&&o)})}},je=(e,t)=>{try{let n=e.trim().split(`
4
4
  `).filter(Boolean);if(n.length===0)return null;let r=[];return n.forEach(e=>{let t=e.match(/^(\s*)(.+)$/);if(t){let[,e,n]=t,i=Math.floor(e.length/2),a=!n.endsWith(`/`);r.push({name:n.replace(/\/$/,``),depth:i,isFile:a})}}),(0,A.jsx)(`div`,{className:f(`w-full min-w-0 font-mono space-y-0.5`,t?.unstyled?null:`p-3 bg-muted/20 rounded-xl border border-border/30`),style:s.tool.popup,children:r.map((e,t)=>(0,A.jsx)(`div`,{className:`min-w-0`,style:{paddingLeft:`${e.depth*20}px`},children:e.isFile?(0,A.jsx)(`span`,{className:`text-foreground/90 block truncate`,children:e.name}):(0,A.jsxs)(`span`,{className:`font-semibold text-foreground block truncate`,children:[e.name,`/`]})},t))})}catch{return null}},Me={backgroundColor:`var(--status-info)`,opacity:.6},Ne=(e,t,n)=>{try{let r=e.trim().split(`
package/dist/index.html CHANGED
@@ -552,7 +552,7 @@
552
552
  opacity: 0.72;
553
553
  }
554
554
  </style>
555
- <script type="module" crossorigin src="/assets/main-IO37JIWf.js"></script>
555
+ <script type="module" crossorigin src="/assets/main-BJckugyb.js"></script>
556
556
  <link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-hePW80VL.js">
557
557
  <link rel="modulepreload" crossorigin href="/assets/runtime-auth-C8w0nlKy.js">
558
558
  <link rel="modulepreload" crossorigin href="/assets/runtime-switch-3ad3yzM7.js">
@@ -572,7 +572,7 @@
572
572
  <link rel="modulepreload" crossorigin href="/assets/terminalApi-bN5IYQau.js">
573
573
  <link rel="modulepreload" crossorigin href="/assets/types-BuiqMmh_.js">
574
574
  <link rel="modulepreload" crossorigin href="/assets/capgoAdapter-KNLHDj7G.js">
575
- <link rel="modulepreload" crossorigin href="/assets/runtimeConfig-yW48patn.js">
575
+ <link rel="modulepreload" crossorigin href="/assets/runtimeConfig-Bm1EPWPq.js">
576
576
  <link rel="stylesheet" crossorigin href="/assets/src-C9BNQK0O.css">
577
577
  </head>
578
578
  <body class="h-full bg-background text-foreground">
@@ -74,7 +74,7 @@
74
74
  }
75
75
  </style>
76
76
 
77
- <script type="module" crossorigin src="/assets/miniChat-mE8pRELI.js"></script>
77
+ <script type="module" crossorigin src="/assets/miniChat-YVKGcjkV.js"></script>
78
78
  <link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-hePW80VL.js">
79
79
  <link rel="modulepreload" crossorigin href="/assets/runtime-auth-C8w0nlKy.js">
80
80
  <link rel="modulepreload" crossorigin href="/assets/runtime-switch-3ad3yzM7.js">
@@ -94,7 +94,7 @@
94
94
  <link rel="modulepreload" crossorigin href="/assets/terminalApi-bN5IYQau.js">
95
95
  <link rel="modulepreload" crossorigin href="/assets/types-BuiqMmh_.js">
96
96
  <link rel="modulepreload" crossorigin href="/assets/capgoAdapter-KNLHDj7G.js">
97
- <link rel="modulepreload" crossorigin href="/assets/runtimeConfig-yW48patn.js">
97
+ <link rel="modulepreload" crossorigin href="/assets/runtimeConfig-Bm1EPWPq.js">
98
98
  <link rel="stylesheet" crossorigin href="/assets/src-C9BNQK0O.css">
99
99
  </head>
100
100
  <body class="h-full bg-background text-foreground">
package/dist/mobile.html CHANGED
@@ -63,7 +63,7 @@
63
63
  })();
64
64
  </script>
65
65
 
66
- <script type="module" crossorigin src="/assets/mobile-Cia2889g.js"></script>
66
+ <script type="module" crossorigin src="/assets/mobile-DtAt6Tnq.js"></script>
67
67
  <link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-hePW80VL.js">
68
68
  <link rel="modulepreload" crossorigin href="/assets/runtime-auth-C8w0nlKy.js">
69
69
  <link rel="modulepreload" crossorigin href="/assets/runtime-switch-3ad3yzM7.js">
@@ -83,7 +83,7 @@
83
83
  <link rel="modulepreload" crossorigin href="/assets/terminalApi-bN5IYQau.js">
84
84
  <link rel="modulepreload" crossorigin href="/assets/types-BuiqMmh_.js">
85
85
  <link rel="modulepreload" crossorigin href="/assets/capgoAdapter-KNLHDj7G.js">
86
- <link rel="modulepreload" crossorigin href="/assets/runtimeConfig-yW48patn.js">
86
+ <link rel="modulepreload" crossorigin href="/assets/runtimeConfig-Bm1EPWPq.js">
87
87
  <link rel="stylesheet" crossorigin href="/assets/src-C9BNQK0O.css">
88
88
  </head>
89
89
  <body class="h-full bg-background text-foreground">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openchambery/web",
3
- "version": "2.1.8-beta.4",
3
+ "version": "2.1.8-beta.5",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "./server/index.js",
@@ -19,6 +19,7 @@ This module provides OpenCode server integration utilities for the web server ru
19
19
  from the authenticated client context (null for owner UI sessions), allowing
20
20
  connection shortcuts to exclude themselves without changing management lists.
21
21
  - `plugins.js`: config entries and plugin-directory files are inventory, not compatibility checks. They do not synthesize `compatibility` or `compatible` fields; Settings uses the OpenCode runtime inventory for actual load status and errors.
22
+ - Reads native V2 `plugins` (strings or `{ package, options }`) with legacy `plugin`/tuple fallback. A native array, including an empty one, wins within each document. Entry mutations write only `plugins`, convert retained legacy tuples, and remove the shadowed legacy key in the touched source file. Options, ordering, unrelated configuration and JSONC comments are preserved through the shared atomic config writer; malformed entries or write failures do not replace the original file. Web, Electron and mobile use this host implementation; VS Code mirrors the same persistence contract.
22
23
  - `session-directory-runtime.js`: Host-side physical directory identity for session index/list/detail/live projection. Uses a runtime-scoped, bounded 256-entry cache (60s success / 5s failed resolution), cleared when the upstream URL changes. Local directory lists use the upstream project identity and physical-directory filtering to include legacy symlink sessions without mixing worktrees. One request reads one page and preserves opaque cursors; consumers must follow continuations even after filtered-empty pages. Non-loopback upstreams keep their own path namespace.
23
24
  - `packages/web/server/lib/opencode/index.js`: public entrypoint (currently baseline placeholder).
24
25
  - `packages/web/server/lib/opencode/auth.js`: host-only authoritative provider credential API reads and deletion.
@@ -66,7 +66,8 @@ describe('Web/Electron entity physical provenance', () => {
66
66
  fs.writeFileSync(jsonc, '{ // retained\n"plugin":["higher-plugin"]}');
67
67
  const entry = plugins.listPluginEntries().find((item) => item.spec === 'lower-plugin');
68
68
  plugins.updatePluginEntry(entry.id, { spec: 'updated-plugin' });
69
- expect(readConfigFile(json).plugin).toEqual(['updated-plugin']);
69
+ expect(readConfigFile(json).plugins).toEqual(['updated-plugin']);
70
+ expect(readConfigFile(json).plugin).toBeUndefined();
70
71
  expect(readConfigFile(jsonc).plugin).toEqual(['higher-plugin']);
71
72
  });
72
73
  });
@@ -316,7 +316,7 @@ describe('opencode plugin routes', () => {
316
316
 
317
317
  const after = await request(app).get('/api/config/plugins').expect(200);
318
318
  expect(after.body.entries).toEqual([]);
319
- expect(readJson(userConfigPath).plugin).toBeUndefined();
319
+ expect(readJson(userConfigPath).plugins).toBeUndefined();
320
320
  expect(refreshOpenCodeAfterConfigChange).not.toHaveBeenCalled();
321
321
  });
322
322
 
@@ -79,6 +79,25 @@ function readPluginConfigLayers(workingDirectory) {
79
79
  return readConfigLayers(workingDirectory);
80
80
  }
81
81
 
82
+ function readPluginArray(config) {
83
+ if (Array.isArray(config.plugins)) return config.plugins;
84
+ return Array.isArray(config.plugin) ? config.plugin : [];
85
+ }
86
+
87
+ function writePluginArray(config, plugin, filePath) {
88
+ const next = { ...config };
89
+ delete next.plugin;
90
+ if (plugin.length > 0) {
91
+ next.plugins = plugin.map((raw) => {
92
+ const parsed = parsePluginRaw(raw);
93
+ return Array.isArray(raw) ? serializePluginEntry(parsed) : raw;
94
+ });
95
+ } else {
96
+ delete next.plugins;
97
+ }
98
+ writeConfig(next, filePath);
99
+ }
100
+
82
101
  function validateFileName(fileName) {
83
102
  if (typeof fileName !== 'string' || !fileName) {
84
103
  throw codedError('Plugin file name is required', 'INVALID_FILENAME');
@@ -125,9 +144,8 @@ function getPluginTarget(id, workingDirectory) {
125
144
  validateScope(scope);
126
145
  const layers = readPluginConfigLayers(workingDirectory);
127
146
  const source = configSources(layers).find((candidate) => candidate.scope === scope
128
- && Array.isArray(candidate.config?.plugin)
129
- && candidate.config.plugin.some((raw) => parsePluginRaw(raw).spec === spec));
130
- const plugin = Array.isArray(source?.config?.plugin) ? source.config.plugin : [];
147
+ && readPluginArray(candidate.config).some((raw) => parsePluginRaw(raw).spec === spec));
148
+ const plugin = source ? [...readPluginArray(source.config)] : [];
131
149
  const index = plugin.findIndex((raw) => parsePluginRaw(raw).spec === spec);
132
150
  if (!source || index === -1) {
133
151
  return null;
@@ -181,13 +199,16 @@ function parsePluginRaw(raw) {
181
199
  if (Array.isArray(raw) && raw.length === 2 && isRecord(raw[1])) {
182
200
  return { spec: validatePluginSpec(raw[0]), options: { ...raw[1] } };
183
201
  }
184
- throw codedError('Plugin spec must be a string or [string, object]', 'INVALID_SPEC');
202
+ if (isRecord(raw) && (raw.options === undefined || isRecord(raw.options))) {
203
+ return { spec: validatePluginSpec(raw.package), ...(raw.options === undefined ? {} : { options: { ...raw.options } }) };
204
+ }
205
+ throw codedError('Plugin spec must be a string or { package, options } object', 'INVALID_SPEC');
185
206
  }
186
207
 
187
208
  function serializePluginEntry(entry) {
188
209
  const spec = validatePluginSpec(entry?.spec);
189
210
  if (hasOptions(entry?.options)) {
190
- return [spec, { ...entry.options }];
211
+ return { package: spec, options: { ...entry.options } };
191
212
  }
192
213
  return spec;
193
214
  }
@@ -195,10 +216,7 @@ function serializePluginEntry(entry) {
195
216
  function listPluginEntries(workingDirectory) {
196
217
  const layers = readPluginConfigLayers(workingDirectory);
197
218
  return configSources(layers).flatMap((source) => {
198
- if (!Array.isArray(source.config?.plugin)) {
199
- return [];
200
- }
201
- return source.config.plugin.map((raw) => {
219
+ return readPluginArray(source.config).map((raw) => {
202
220
  const parsed = parsePluginRaw(raw);
203
221
  return {
204
222
  id: encodePluginId('config', `${source.scope}:${parsed.spec}`),
@@ -225,8 +243,7 @@ function createPluginEntry(entry, workingDirectory) {
225
243
  const layers = readPluginConfigLayers(workingDirectory);
226
244
  const existing = configSources(layers).find((source) => (
227
245
  source.scope === scope
228
- && Array.isArray(source.config?.plugin)
229
- && source.config.plugin.some((raw) => parsePluginRaw(raw).spec === spec)
246
+ && readPluginArray(source.config).some((raw) => parsePluginRaw(raw).spec === spec)
230
247
  ));
231
248
  if (existing) {
232
249
  throw codedError(`Plugin "${spec}" already exists`, 'ENTRY_EXISTS');
@@ -242,11 +259,7 @@ function createPluginEntry(entry, workingDirectory) {
242
259
  config = getConfigForPath(layers, targetPath);
243
260
  }
244
261
 
245
- if (!Array.isArray(config.plugin)) {
246
- config.plugin = [];
247
- }
248
- config.plugin.push(serializePluginEntry({ spec, options: entry.options }));
249
- writeConfig(config, targetPath);
262
+ writePluginArray(config, [...readPluginArray(config), serializePluginEntry({ spec, options: entry.options })], targetPath);
250
263
  }
251
264
 
252
265
  function updatePluginEntry(id, updates, workingDirectory) {
@@ -258,7 +271,7 @@ function updatePluginEntry(id, updates, workingDirectory) {
258
271
  const nextSpec = updates?.spec === undefined ? existing.spec : validatePluginSpec(updates.spec);
259
272
  const nextOptions = updates?.options === undefined ? existing.options : updates.options;
260
273
  target.plugin[target.index] = serializePluginEntry({ spec: nextSpec, options: nextOptions });
261
- writeConfig(target.source.config, target.source.filePath);
274
+ writePluginArray(target.source.config, target.plugin, target.source.filePath);
262
275
  }
263
276
 
264
277
  function deletePluginEntry(id, workingDirectory) {
@@ -267,10 +280,7 @@ function deletePluginEntry(id, workingDirectory) {
267
280
  throw codedError('Plugin entry not found', 'NOT_FOUND');
268
281
  }
269
282
  target.plugin.splice(target.index, 1);
270
- if (target.plugin.length === 0) {
271
- delete target.source.config.plugin;
272
- }
273
- writeConfig(target.source.config, target.source.filePath);
283
+ writePluginArray(target.source.config, target.plugin, target.source.filePath);
274
284
  }
275
285
 
276
286
  function listPluginDirFiles(workingDirectory) {
@@ -52,6 +52,9 @@ describe('opencode plugins data layer', () => {
52
52
  expect(plugins.parsePluginRaw('foo@1.0.0')).toEqual({ spec: 'foo@1.0.0' });
53
53
  expect(plugins.parsePluginRaw(['foo', { a: 1 }])).toEqual({ spec: 'foo', options: { a: 1 } });
54
54
  expect(plugins.parsePluginRaw(['foo', {}])).toEqual({ spec: 'foo', options: {} });
55
+ expect(plugins.parsePluginRaw({ package: 'foo', options: { a: 1 } })).toEqual({ spec: 'foo', options: { a: 1 } });
56
+ expect(plugins.parsePluginRaw({ package: 'foo' })).toEqual({ spec: 'foo' });
57
+ expect(() => plugins.parsePluginRaw({ package: 'foo', options: [] })).toThrow('Plugin spec');
55
58
  expect(() => plugins.parsePluginRaw(123)).toThrow('Plugin spec');
56
59
  });
57
60
 
@@ -59,7 +62,7 @@ describe('opencode plugins data layer', () => {
59
62
  expect(plugins.serializePluginEntry({ spec: 'foo' })).toBe('foo');
60
63
  expect(plugins.serializePluginEntry({ spec: 'foo', options: undefined })).toBe('foo');
61
64
  expect(plugins.serializePluginEntry({ spec: 'foo', options: {} })).toBe('foo');
62
- expect(plugins.serializePluginEntry({ spec: 'foo', options: { a: 1 } })).toEqual(['foo', { a: 1 }]);
65
+ expect(plugins.serializePluginEntry({ spec: 'foo', options: { a: 1 } })).toEqual({ package: 'foo', options: { a: 1 } });
63
66
  });
64
67
 
65
68
  test('rejects invalid specs and file names', () => {
@@ -71,11 +74,11 @@ describe('opencode plugins data layer', () => {
71
74
  expect(() => plugins.writePluginDirFile({ fileName: 'foo.txt', content: '', scope: 'project' }, projectDir)).toThrow('Plugin file name');
72
75
  });
73
76
 
74
- test('creates string and tuple entries with duplicate rejection', () => {
77
+ test('creates V2 string and object entries with duplicate rejection', () => {
75
78
  plugins.createPluginEntry({ spec: 'plain-plugin', scope: 'user' }, projectDir);
76
79
  plugins.createPluginEntry({ spec: 'tuple-plugin', options: { apiKey: 'x' }, scope: 'user' }, projectDir);
77
80
 
78
- expect(readJson(userConfigPath).plugin).toEqual(['plain-plugin', ['tuple-plugin', { apiKey: 'x' }]]);
81
+ expect(readJson(userConfigPath).plugins).toEqual(['plain-plugin', { package: 'tuple-plugin', options: { apiKey: 'x' } }]);
79
82
  expect(() => plugins.createPluginEntry({ spec: 'plain-plugin', scope: 'user' }, projectDir)).toThrow('already exists');
80
83
  expect(thrownBy(() => plugins.createPluginEntry({ spec: 'plain-plugin', scope: 'user' }, projectDir))).toHaveProperty('code', 'ENTRY_EXISTS');
81
84
  });
@@ -84,8 +87,8 @@ describe('opencode plugins data layer', () => {
84
87
  plugins.createPluginEntry({ spec: 'user-plugin', scope: 'user' }, projectDir);
85
88
  plugins.createPluginEntry({ spec: 'project-plugin', scope: 'project' }, projectDir);
86
89
 
87
- expect(readJson(userConfigPath).plugin).toEqual(['user-plugin']);
88
- expect(readJson(path.join(projectDir, '.opencode', 'opencode.json')).plugin).toEqual(['project-plugin']);
90
+ expect(readJson(userConfigPath).plugins).toEqual(['user-plugin']);
91
+ expect(readJson(path.join(projectDir, '.opencode', 'opencode.json')).plugins).toEqual(['project-plugin']);
89
92
  });
90
93
 
91
94
  test('re-resolves custom config env between calls', () => {
@@ -100,20 +103,62 @@ describe('opencode plugins data layer', () => {
100
103
  plugins.createPluginEntry({ spec: 'second-plugin', scope: 'user' }, projectDir);
101
104
  plugins.writePluginDirFile({ fileName: 'second.js', content: 'two', scope: 'user' }, projectDir);
102
105
 
103
- expect(readJson(firstConfigPath).plugin).toEqual(['first-plugin']);
104
- expect(readJson(secondConfigPath).plugin).toEqual(['second-plugin']);
106
+ expect(readJson(firstConfigPath).plugins).toEqual(['first-plugin']);
107
+ expect(readJson(secondConfigPath).plugins).toEqual(['second-plugin']);
105
108
  expect(fs.existsSync(path.join(path.dirname(firstConfigPath), 'plugins', 'first.js'))).toBe(true);
106
109
  expect(fs.existsSync(path.join(path.dirname(secondConfigPath), 'plugins', 'second.js'))).toBe(true);
107
110
  });
108
111
 
109
- test('updates entries in place and transitions between string and tuple', () => {
112
+ test('migrates legacy entries in place and transitions between string and object', () => {
110
113
  writeJson(userConfigPath, { plugin: ['first', ['second', { a: 1 }], 'third'] });
111
114
 
112
115
  plugins.updatePluginEntry(plugins.encodePluginId('config', 'user:second'), { spec: 'second-new', options: {} }, projectDir);
113
- expect(readJson(userConfigPath).plugin).toEqual(['first', 'second-new', 'third']);
116
+ expect(readJson(userConfigPath)).toEqual({ plugins: ['first', 'second-new', 'third'] });
114
117
 
115
118
  plugins.updatePluginEntry(plugins.encodePluginId('config', 'user:first'), { spec: 'first-new', options: { b: 2 } }, projectDir);
116
- expect(readJson(userConfigPath).plugin).toEqual([['first-new', { b: 2 }], 'second-new', 'third']);
119
+ expect(readJson(userConfigPath).plugins).toEqual([{ package: 'first-new', options: { b: 2 } }, 'second-new', 'third']);
120
+ });
121
+
122
+ test('converts untouched legacy options on save and preserves unrelated JSONC', () => {
123
+ fs.writeFileSync(userConfigPath, '// retained comment\n' + JSON.stringify({ model: 'example/model', plugin: [['old', { nested: { enabled: false } }]] }));
124
+ plugins.createPluginEntry({ spec: 'new', scope: 'user' }, projectDir);
125
+ const text = fs.readFileSync(userConfigPath, 'utf8');
126
+ expect(text).toContain('// retained comment');
127
+ expect(plugins.listPluginEntries(projectDir).map(({ spec, options }) => ({ spec, options }))).toEqual([
128
+ { spec: 'old', options: { nested: { enabled: false } } }, { spec: 'new', options: undefined },
129
+ ]);
130
+ expect(text).toContain('"plugins"');
131
+ expect(text).not.toContain('"plugin"');
132
+ expect(text).toContain('example/model');
133
+ });
134
+
135
+ test('prefers native plugins over legacy entries including an empty native array', () => {
136
+ writeJson(userConfigPath, { plugin: ['shadowed'], plugins: [{ package: 'native', options: { enabled: false } }] });
137
+ expect(plugins.listPluginEntries(projectDir).map((entry) => entry.spec)).toEqual(['native']);
138
+ expect(() => plugins.createPluginEntry({ spec: 'native' }, projectDir)).toThrow('already exists');
139
+ plugins.deletePluginEntry(plugins.encodePluginId('config', 'user:native'), projectDir);
140
+ expect(readJson(userConfigPath)).toEqual({});
141
+ writeJson(userConfigPath, { plugin: ['shadowed'], plugins: [] });
142
+ expect(plugins.listPluginEntries(projectDir)).toEqual([]);
143
+ plugins.createPluginEntry({ spec: 'new' }, projectDir);
144
+ expect(readJson(userConfigPath)).toEqual({ plugins: ['new'] });
145
+ });
146
+
147
+ test('malformed legacy data and failed atomic writes leave original config intact', () => {
148
+ writeJson(userConfigPath, { plugin: [['bad', null]], model: 'example/model' });
149
+ const malformed = fs.readFileSync(userConfigPath, 'utf8');
150
+ expect(() => plugins.createPluginEntry({ spec: 'new' }, projectDir)).toThrow('Plugin spec');
151
+ expect(fs.readFileSync(userConfigPath, 'utf8')).toBe(malformed);
152
+
153
+ writeJson(userConfigPath, { plugin: [['old', { enabled: false }]] });
154
+ const original = fs.readFileSync(userConfigPath, 'utf8');
155
+ const rename = vi.spyOn(fs, 'renameSync').mockImplementation(() => { throw new Error('write failed'); });
156
+ try {
157
+ expect(() => plugins.createPluginEntry({ spec: 'new' }, projectDir)).toThrow('write failed');
158
+ expect(fs.readFileSync(userConfigPath, 'utf8')).toBe(original);
159
+ } finally {
160
+ rename.mockRestore();
161
+ }
117
162
  });
118
163
 
119
164
  test('deletes entries and prunes empty plugin key', () => {