@openchambery/web 2.1.9-beta.5 → 2.1.9-beta.6
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/dist/assets/{AssistantView-BMvmzMFI.js → AssistantView-3Rfvw4sl.js} +3 -3
- package/dist/assets/{DiagramView-BO6rDoTN.js → DiagramView-CgmWfIrQ.js} +1 -1
- package/dist/assets/{MarkdownRenderer-D_ZS58Ur.js → MarkdownRenderer-CptQecYA.js} +2 -2
- package/dist/assets/{MarkdownRendererImpl-gBtduBHO.js → MarkdownRendererImpl-BsOrNJni.js} +1 -1
- package/dist/assets/{MarkstreamRendererImpl-CKilBBOc.js → MarkstreamRendererImpl-0EuyxKix.js} +1 -1
- package/dist/assets/{MobileDetailNavigation-Dgqdqrmy.js → MobileDetailNavigation-CI00mBRX.js} +1 -1
- package/dist/assets/{MobileShareBridge-G2wjyn4g.js → MobileShareBridge-BSRHQmvf.js} +1 -1
- package/dist/assets/{MobileTabPageHeader-Cipo7N8C.js → MobileTabPageHeader-CdgleYWQ.js} +1 -1
- package/dist/assets/{MultiRunWindow-C_ow_lbm.js → MultiRunWindow-D6IK1Zkp.js} +1 -1
- package/dist/assets/{SettingsView-BaLgORC_.js → SettingsView-D_lZOSSF.js} +1 -1
- package/dist/assets/{SettingsWindow-DfSPTDHb.js → SettingsWindow-ByLmdF2a.js} +1 -1
- package/dist/assets/{TerminalView-DhUynfus.js → TerminalView-Th-9Zt_f.js} +1 -1
- package/dist/assets/{ToolOutputDialog-DaKPwmer.js → ToolOutputDialog-D_fxjrzt.js} +1 -1
- package/dist/assets/{appThemeRegistry-zPqSQBFx.js → appThemeRegistry-wX31Kn4M.js} +1 -1
- package/dist/assets/{fileMentionTouchSelection-C8mKDaBe.js → fileMentionTouchSelection-BbnpDd2Y.js} +1 -1
- package/dist/assets/{gitApi-BCPykg93.js → gitApi-sk4tCimN.js} +1 -1
- package/dist/assets/{insertionBoundaries-h0pySzQJ.js → insertionBoundaries-Dup8RNq4.js} +1 -1
- package/dist/assets/{main-4JVZhTAZ.js → main-BVHlGGZs.js} +2 -2
- package/dist/assets/{main-C8RDC4kz.js → main-D7knwcbE.js} +4 -4
- package/dist/assets/{mcpQueries-Cm1VNAmy.js → mcpQueries-DLcJMbJ8.js} +1 -1
- package/dist/assets/{miniChat-_SvWYZuQ.js → miniChat-C71dZf-x.js} +2 -2
- package/dist/assets/{mobile-Cil7_kYn.js → mobile-BVDtJrmi.js} +2 -2
- package/dist/assets/{multirun-BzeInZt6.js → multirun-BaN9JOfb.js} +1 -1
- package/dist/assets/{projectMeta-CUAgRGwW.js → projectMeta-WqSqDsLK.js} +1 -1
- package/dist/assets/{renderElectronMiniChatApp-BrAIRgb-.js → renderElectronMiniChatApp-BZ3mBJuQ.js} +2 -2
- package/dist/assets/{renderMobileApp-DxTazPkB.js → renderMobileApp-D5oYObXh.js} +4 -4
- package/dist/assets/{runtimeConfig-Bljmgd6r.js → runtimeConfig-K7gJK2wK.js} +1 -1
- package/dist/assets/{runtimeEndpointReset-BYsDk8ux.js → runtimeEndpointReset-C9YZ_XHW.js} +1 -1
- package/dist/assets/{sessionChangeSummary-BslBzYAm.js → sessionChangeSummary-Db70oATx.js} +1 -1
- package/dist/assets/{sessionLookup-5MrKwBXP.js → sessionLookup-oPdIysdc.js} +1 -1
- package/dist/assets/{speechSettingsQueries-otzPynPJ.js → speechSettingsQueries-Z5qhFY5C.js} +1 -1
- package/dist/assets/{useAppFontEffects-DHJ_Qc3g.js → useAppFontEffects-C5__ugup.js} +5 -5
- package/dist/assets/{useDesktopWindowControlsLayout-BnMDHglb.js → useDesktopWindowControlsLayout-MTlcH4Zm.js} +1 -1
- package/dist/assets/{useEffectiveDirectory-D0qR3aRK.js → useEffectiveDirectory-Ty8RNv4U.js} +1 -1
- package/dist/assets/{useMobileNavigationStore-BZ7oo4Ir.js → useMobileNavigationStore-SqU7Bc3B.js} +3 -3
- package/dist/assets/{useWorkerHighlightedLines-B86utfqe.js → useWorkerHighlightedLines-5ZbHo-Qd.js} +1 -1
- package/dist/index.html +2 -2
- package/dist/mini-chat.html +2 -2
- package/dist/mobile.html +2 -2
- package/package.json +1 -1
- package/server/lib/relay/DOCUMENTATION.md +4 -0
- package/server/lib/relay/tunnel-host.js +3 -0
- package/server/lib/relay/tunnel-host.test.js +27 -0
- package/server/lib/session-index/DOCUMENTATION.md +16 -0
- package/server/lib/session-index/host-authority-snapshot.js +31 -0
- package/server/lib/session-index/routes.host-archive.test.js +118 -0
- package/server/lib/session-index/routes.js +7 -2
- package/server/lib/session-index/sync-runtime.js +10 -0
- package/server/lib/session-index/sync-runtime.test.js +6 -0
package/dist/assets/{useWorkerHighlightedLines-B86utfqe.js → useWorkerHighlightedLines-5ZbHo-Qd.js}
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{r as e}from"./rolldown-runtime-hePW80VL.js";import{n as t}from"./runtime-fetch-WgH0pROs.js";import{Mn as n,ln as r}from"./persistence-BKszKaoQ.js";import{it as i,rt as a}from"./vendor-base-ui-CdzJ6My_.js";import{n as o}from"./platform-CgwxwfA4.js";import{v as s}from"./useUIStore-CEqU9Ujv.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-CWSs9Baz.js";import{n as m}from"./useThemeSystem-8jhE4Zyk.js";import{n as h}from"./Icon-BCFeHDWx.js";import{i as g,n as _,t as v}from"./tooltip-CV6dvLBp.js";import{a as y,i as b,o as x,t as S}from"./image-loading-Bj11k4gM.js";import{n as C}from"./MarkdownRenderer-D_ZS58Ur.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,`&`).replace(/</g,`<`).replace(/>/g,`>`).replace(/"/g,`"`).replace(/'/g,`'`),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-WgH0pROs.js";import{Mn as n,ln as r}from"./persistence-BKszKaoQ.js";import{it as i,rt as a}from"./vendor-base-ui-CdzJ6My_.js";import{n as o}from"./platform-CgwxwfA4.js";import{v as s}from"./useUIStore-CEqU9Ujv.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-CWSs9Baz.js";import{n as m}from"./useThemeSystem-8jhE4Zyk.js";import{n as h}from"./Icon-BCFeHDWx.js";import{i as g,n as _,t as v}from"./tooltip-CV6dvLBp.js";import{a as y,i as b,o as x,t as S}from"./image-loading-Bj11k4gM.js";import{n as C}from"./MarkdownRenderer-CptQecYA.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,`&`).replace(/</g,`<`).replace(/>/g,`>`).replace(/"/g,`"`).replace(/'/g,`'`),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-
|
|
555
|
+
<script type="module" crossorigin src="/assets/main-BVHlGGZs.js"></script>
|
|
556
556
|
<link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-hePW80VL.js">
|
|
557
557
|
<link rel="modulepreload" crossorigin href="/assets/runtime-auth-BpCQ-dPr.js">
|
|
558
558
|
<link rel="modulepreload" crossorigin href="/assets/runtime-switch-CMiMO5Yj.js">
|
|
@@ -572,7 +572,7 @@
|
|
|
572
572
|
<link rel="modulepreload" crossorigin href="/assets/terminalApi-rxsMWL0o.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-
|
|
575
|
+
<link rel="modulepreload" crossorigin href="/assets/runtimeConfig-K7gJK2wK.js">
|
|
576
576
|
<link rel="stylesheet" crossorigin href="/assets/src-Cq3Axl32.css">
|
|
577
577
|
</head>
|
|
578
578
|
<body class="h-full bg-background text-foreground">
|
package/dist/mini-chat.html
CHANGED
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
}
|
|
75
75
|
</style>
|
|
76
76
|
|
|
77
|
-
<script type="module" crossorigin src="/assets/miniChat-
|
|
77
|
+
<script type="module" crossorigin src="/assets/miniChat-C71dZf-x.js"></script>
|
|
78
78
|
<link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-hePW80VL.js">
|
|
79
79
|
<link rel="modulepreload" crossorigin href="/assets/runtime-auth-BpCQ-dPr.js">
|
|
80
80
|
<link rel="modulepreload" crossorigin href="/assets/runtime-switch-CMiMO5Yj.js">
|
|
@@ -94,7 +94,7 @@
|
|
|
94
94
|
<link rel="modulepreload" crossorigin href="/assets/terminalApi-rxsMWL0o.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-
|
|
97
|
+
<link rel="modulepreload" crossorigin href="/assets/runtimeConfig-K7gJK2wK.js">
|
|
98
98
|
<link rel="stylesheet" crossorigin href="/assets/src-Cq3Axl32.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-
|
|
66
|
+
<script type="module" crossorigin src="/assets/mobile-BVDtJrmi.js"></script>
|
|
67
67
|
<link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-hePW80VL.js">
|
|
68
68
|
<link rel="modulepreload" crossorigin href="/assets/runtime-auth-BpCQ-dPr.js">
|
|
69
69
|
<link rel="modulepreload" crossorigin href="/assets/runtime-switch-CMiMO5Yj.js">
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
<link rel="modulepreload" crossorigin href="/assets/terminalApi-rxsMWL0o.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-
|
|
86
|
+
<link rel="modulepreload" crossorigin href="/assets/runtimeConfig-K7gJK2wK.js">
|
|
87
87
|
<link rel="stylesheet" crossorigin href="/assets/src-Cq3Axl32.css">
|
|
88
88
|
</head>
|
|
89
89
|
<body class="h-full bg-background text-foreground">
|
package/package.json
CHANGED
|
@@ -88,6 +88,10 @@ notifications) omit it and receive uncompressed bodies.
|
|
|
88
88
|
|
|
89
89
|
Only application/json and application/*+json bodies qualify. Known decoded bodies
|
|
90
90
|
under 1 KiB, HEAD/no-body responses, SSE and binary assets are not compressed.
|
|
91
|
+
SSE stays uncompressed so each event flushes immediately (a host test pushes two
|
|
92
|
+
events through a gzip-accepting request and requires both before stream end);
|
|
93
|
+
compression stays before encryption and behind the per-request opt-in. Measured
|
|
94
|
+
mobile cold starts carried ~23-28% of decoded JSON bytes on the wire with gzip.
|
|
91
95
|
Unknown-length JSON is streamed rather than buffered in full; loopback fetch's
|
|
92
96
|
automatic decompression is accounted for before checking length. Cancellation and
|
|
93
97
|
errors propagate through the stream pipeline. Host tests cover negotiation and
|
|
@@ -263,6 +263,9 @@ export const createTunnelHost = ({
|
|
|
263
263
|
const contentLength = response.headers.has('content-encoding') ? null : response.headers.get('content-length');
|
|
264
264
|
// Compress finite JSON responses before encryption, never SSE or binary assets.
|
|
265
265
|
// Old clients omit acceptEncoding and old hosts ignore it, preserving v1 framing.
|
|
266
|
+
// Do not widen to text/event-stream: SSE frames must flush per event, and gzip
|
|
267
|
+
// buffers them. Compression must also stay before encryption (ciphertext is
|
|
268
|
+
// incompressible), and only when the request explicitly opted in.
|
|
266
269
|
const gzip = request.acceptEncoding === 'gzip' && typeof CompressionStream === 'function'
|
|
267
270
|
&& method !== 'HEAD' && response.body !== null
|
|
268
271
|
&& (contentType === 'application/json' || /^application\/[^/]+\+json$/.test(contentType))
|
|
@@ -102,6 +102,33 @@ describe('negotiated JSON response compression', () => {
|
|
|
102
102
|
}
|
|
103
103
|
} finally { host.close(); fetchMock.mockRestore(); }
|
|
104
104
|
});
|
|
105
|
+
|
|
106
|
+
it('streams each SSE event as it arrives even when the client accepts gzip', async () => {
|
|
107
|
+
// Live activity rides SSE through the tunnel. Compressing it would buffer events
|
|
108
|
+
// until the gzip block fills, stalling session status/permissions on Relay.
|
|
109
|
+
let push;
|
|
110
|
+
const source = new ReadableStream({ start(controller) { push = controller; } });
|
|
111
|
+
const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response(source, {
|
|
112
|
+
headers: { 'content-type': 'text/event-stream; charset=utf-8' },
|
|
113
|
+
}));
|
|
114
|
+
const frames = [];
|
|
115
|
+
const host = createTunnelHost({ connectionId: 'sse-gzip-test', getLocalPort: () => 9,
|
|
116
|
+
sendFrame: (frame) => { frames.push(decodeTunnelFrame(frame)); }, getBufferedAmount: () => 0 });
|
|
117
|
+
const bodies = () => frames.filter((frame) => frame.frameType === TunnelFrameType.HttpBody).map((frame) => textDecoder.decode(frame.payload));
|
|
118
|
+
try {
|
|
119
|
+
await host.handleFrame(encodeTunnelFrame(TunnelFrameType.HttpRequest, 1, encodeJsonPayload({
|
|
120
|
+
method: 'GET', path: '/api/event', query: '', headers: {}, acceptEncoding: 'gzip',
|
|
121
|
+
})));
|
|
122
|
+
await waitFor(() => frames.some((frame) => frame.frameType === TunnelFrameType.HttpResponse));
|
|
123
|
+
const head = JSON.parse(textDecoder.decode(frames.find((frame) => frame.frameType === TunnelFrameType.HttpResponse).payload));
|
|
124
|
+
expect(head.encoding).toBeUndefined();
|
|
125
|
+
push.enqueue(textEncoder.encode('data: {"n":1}\n\n'));
|
|
126
|
+
await waitFor(() => bodies().join('').includes('"n":1'));
|
|
127
|
+
push.enqueue(textEncoder.encode('data: {"n":2}\n\n'));
|
|
128
|
+
await waitFor(() => bodies().join('').includes('"n":2'));
|
|
129
|
+
expect(frames.some((frame) => frame.frameType === TunnelFrameType.StreamEnd)).toBe(false);
|
|
130
|
+
} finally { push.close(); host.close(); fetchMock.mockRestore(); }
|
|
131
|
+
});
|
|
105
132
|
});
|
|
106
133
|
|
|
107
134
|
describe('isTunnelWsPathAllowed', () => {
|
|
@@ -77,6 +77,15 @@ Pin and unpin use `POST` / `DELETE`
|
|
|
77
77
|
Snapshots include `pinnedSessionIds` plus `time.pinned` on any matching
|
|
78
78
|
summary row.
|
|
79
79
|
|
|
80
|
+
List snapshots (`GET /api/openchamber/session-index` and the `POST /sync`
|
|
81
|
+
response body) share one Host projection: committed Host metadata is merged,
|
|
82
|
+
Host-archived rows and pins are dropped, and metadata no list consumer reads is
|
|
83
|
+
omitted (`openchamber.titleAuthority`, `openchamber.scheduledTaskSkill`,
|
|
84
|
+
`titleRefresh.forMessageID` / `generatedAt` / `forkFirstSendSessionID`). Unknown
|
|
85
|
+
keys pass through. Writers and the title runtime read the omitted keys from the
|
|
86
|
+
session record, never from the index list. The deep-link lookup keeps the full
|
|
87
|
+
projection. Both list routes return 503 when Host metadata is unavailable.
|
|
88
|
+
|
|
80
89
|
The server-side global OpenCode event subscriber writes session summary events
|
|
81
90
|
directly into this index. User `message.updated` events and `session.idle`
|
|
82
91
|
completion events advance the separate `activity_updated_at` ordering field,
|
|
@@ -136,6 +145,13 @@ revision increments and `waitForChange` delivery remain immediate, including the
|
|
|
136
145
|
final state. Full snapshots are materialized for waiting readers, not for each
|
|
137
146
|
tip. No persisted revision, failure, or runtime-isolation semantics change.
|
|
138
147
|
|
|
148
|
+
Regression budget: one batch bumps the revision per directory and per child-flag
|
|
149
|
+
check (~300+ on a cold start). Before coalescing, each bump became an SSE tip and
|
|
150
|
+
every Relay client re-fetched the full snapshot per burst. `sync-runtime.test.js`
|
|
151
|
+
asserts that a 15-directory/300-child batch with 300+ revisions emits at most
|
|
152
|
+
one tip per 500ms window plus immediate edges. Progress `notify()` calls must not
|
|
153
|
+
pass `immediate`.
|
|
154
|
+
|
|
139
155
|
The shared renderer independently throttles busy tips to a fixed 1000ms window
|
|
140
156
|
for compatibility with older Hosts. Idle/legacy tips use 100ms debounce and
|
|
141
157
|
shorten a pending busy deadline without extending it. Stream-ready and safety
|
|
@@ -23,9 +23,39 @@ export const projectIndexSessionWithHost = (session, storedById) => {
|
|
|
23
23
|
return projectSessionWithHostMetadata(session, host);
|
|
24
24
|
};
|
|
25
25
|
|
|
26
|
+
// Host metadata keys that no session-list consumer reads. Writers and the title
|
|
27
|
+
// runtime read them from the session record itself, never from the index list.
|
|
28
|
+
// Unknown keys pass through, so new metadata stays visible by default.
|
|
29
|
+
const LIST_OMITTED_OPENCHAMBER_KEYS = ['titleAuthority', 'scheduledTaskSkill'];
|
|
30
|
+
const LIST_OMITTED_TITLE_REFRESH_KEYS = ['forMessageID', 'generatedAt', 'forkFirstSendSessionID'];
|
|
31
|
+
|
|
32
|
+
const omitKeys = (record, keys) => {
|
|
33
|
+
if (!keys.some((key) => Object.prototype.hasOwnProperty.call(record, key))) return record;
|
|
34
|
+
const next = { ...record };
|
|
35
|
+
for (const key of keys) delete next[key];
|
|
36
|
+
return next;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* List projection: drop Host metadata the session list never reads (~20% of a
|
|
41
|
+
* snapshot). Lookup hits and session detail keep the full projection.
|
|
42
|
+
*/
|
|
43
|
+
const toIndexListSession = (session) => {
|
|
44
|
+
const metadata = session?.metadata;
|
|
45
|
+
if (!isPlainObject(metadata) || !isPlainObject(metadata.openchamber)) return session;
|
|
46
|
+
let openchamber = omitKeys(metadata.openchamber, LIST_OMITTED_OPENCHAMBER_KEYS);
|
|
47
|
+
if (isPlainObject(openchamber.titleRefresh)) {
|
|
48
|
+
const titleRefresh = omitKeys(openchamber.titleRefresh, LIST_OMITTED_TITLE_REFRESH_KEYS);
|
|
49
|
+
if (titleRefresh !== openchamber.titleRefresh) openchamber = { ...openchamber, titleRefresh };
|
|
50
|
+
}
|
|
51
|
+
if (openchamber === metadata.openchamber) return session;
|
|
52
|
+
return { ...session, metadata: { ...metadata, openchamber } };
|
|
53
|
+
};
|
|
54
|
+
|
|
26
55
|
/**
|
|
27
56
|
* Filter a full index snapshot through Host archive authority.
|
|
28
57
|
* Archived sessions drop from directory lists; pins for Host-archived ids drop.
|
|
58
|
+
* Surviving sessions use the list projection (`toIndexListSession`).
|
|
29
59
|
*
|
|
30
60
|
* @param {object} snapshot `{ directories, pinnedSessionIds, ... }`
|
|
31
61
|
* @param {Record<string, object>} storedById
|
|
@@ -41,6 +71,7 @@ export const applyHostArchiveToIndexSnapshot = (snapshot, storedById) => {
|
|
|
41
71
|
? directory.sessions
|
|
42
72
|
.map((session) => projectIndexSessionWithHost(session, stored))
|
|
43
73
|
.filter((session) => session && !isSessionArchived(session))
|
|
74
|
+
.map(toIndexListSession)
|
|
44
75
|
: [];
|
|
45
76
|
return { ...directory, sessions };
|
|
46
77
|
})
|
|
@@ -198,4 +198,122 @@ describe('session-index routes + Host archive (production path)', () => {
|
|
|
198
198
|
expect(res.body.directories[0].sessions.map((s) => s.id)).toEqual(['ses_y']);
|
|
199
199
|
index.close();
|
|
200
200
|
});
|
|
201
|
+
|
|
202
|
+
it('list snapshots drop unread Host metadata; lookup keeps the full projection', async () => {
|
|
203
|
+
const dataDir = makeDir();
|
|
204
|
+
const store = createSessionMetadataStore({ dataDir });
|
|
205
|
+
const index = createSessionIndexService({
|
|
206
|
+
dbPath: path.join(dataDir, 'index.sqlite'),
|
|
207
|
+
getRuntimeConfig: () => ({ apiBaseUrl: 'http://runtime-test' }),
|
|
208
|
+
});
|
|
209
|
+
index.replaceDirectory({
|
|
210
|
+
directory: '/repo',
|
|
211
|
+
sessions: [{ id: 'ses_a', title: 'A', directory: '/repo', time: { created: 1, updated: 10 } }],
|
|
212
|
+
cursor: null,
|
|
213
|
+
hasMore: false,
|
|
214
|
+
});
|
|
215
|
+
await store.setSessionMetadata('ses_a', {
|
|
216
|
+
openchamber: {
|
|
217
|
+
titleAuthority: { sessionID: 'ses_a', title: 'A', revision: 'rev-1', source: 'manual' },
|
|
218
|
+
scheduledTaskSkill: { hostOrigin: 'http://127.0.0.1:1' },
|
|
219
|
+
scheduledTask: { taskID: 'task-1' },
|
|
220
|
+
goal: { objective: 'ship' },
|
|
221
|
+
titleRefresh: {
|
|
222
|
+
activityUpdatedAt: 20,
|
|
223
|
+
isGenerating: true,
|
|
224
|
+
lastError: 'boom',
|
|
225
|
+
failedAt: 30,
|
|
226
|
+
lastAutoTitle: 'Auto',
|
|
227
|
+
requestedAt: 40,
|
|
228
|
+
forMessageID: 'msg_1',
|
|
229
|
+
generatedAt: 50,
|
|
230
|
+
forkFirstSendSessionID: 'ses_parent',
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const { app, route } = registry();
|
|
236
|
+
registerSessionIndexRoutes(app, {
|
|
237
|
+
sessionIndexService: index,
|
|
238
|
+
getCommittedHostMetadata: () => store.getAll(),
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
const snapRes = response();
|
|
242
|
+
await route('GET', '/api/openchamber/session-index')({}, snapRes);
|
|
243
|
+
const listed = snapRes.body.directories[0].sessions[0].metadata.openchamber;
|
|
244
|
+
expect(listed.titleAuthority).toBeUndefined();
|
|
245
|
+
expect(listed.scheduledTaskSkill).toBeUndefined();
|
|
246
|
+
expect(listed.scheduledTask).toEqual({ taskID: 'task-1' });
|
|
247
|
+
expect(listed.goal).toEqual({ objective: 'ship' });
|
|
248
|
+
expect(listed.titleRefresh).toEqual({
|
|
249
|
+
activityUpdatedAt: 20,
|
|
250
|
+
isGenerating: true,
|
|
251
|
+
lastError: 'boom',
|
|
252
|
+
failedAt: 30,
|
|
253
|
+
lastAutoTitle: 'Auto',
|
|
254
|
+
requestedAt: 40,
|
|
255
|
+
});
|
|
256
|
+
expect(listed.sessionStatus).toMatchObject({ type: 'idle' });
|
|
257
|
+
|
|
258
|
+
const lookupRes = response();
|
|
259
|
+
await route('GET', '/api/openchamber/session-index/session/:sessionId')(
|
|
260
|
+
{ params: { sessionId: 'ses_a' } },
|
|
261
|
+
lookupRes,
|
|
262
|
+
);
|
|
263
|
+
expect(lookupRes.body.session.metadata.openchamber.titleAuthority).toMatchObject({ revision: 'rev-1' });
|
|
264
|
+
expect(lookupRes.body.session.metadata.openchamber.titleRefresh.forMessageID).toBe('msg_1');
|
|
265
|
+
index.close();
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('POST /sync applies the same Host archive gate as GET', async () => {
|
|
269
|
+
const dataDir = makeDir();
|
|
270
|
+
const store = createSessionMetadataStore({ dataDir });
|
|
271
|
+
const index = createSessionIndexService({
|
|
272
|
+
dbPath: path.join(dataDir, 'index.sqlite'),
|
|
273
|
+
getRuntimeConfig: () => ({ apiBaseUrl: 'http://runtime-test' }),
|
|
274
|
+
});
|
|
275
|
+
index.replaceDirectory({
|
|
276
|
+
directory: '/repo',
|
|
277
|
+
sessions: [
|
|
278
|
+
{ id: 'ses_live', title: 'Live', directory: '/repo', time: { created: 1, updated: 10 } },
|
|
279
|
+
{ id: 'ses_arch', title: 'Arch', directory: '/repo', time: { created: 1, updated: 9 } },
|
|
280
|
+
],
|
|
281
|
+
cursor: null,
|
|
282
|
+
hasMore: false,
|
|
283
|
+
});
|
|
284
|
+
index.setPinned('ses_arch', 50);
|
|
285
|
+
await store.setSessionMetadata('ses_arch', {
|
|
286
|
+
openchamber: { archive: { archivedAt: 99 } },
|
|
287
|
+
}, { allowArchive: true });
|
|
288
|
+
const enqueue = vi.fn(() => ({ revision: 3, sync: { active: true }, ...index.snapshot() }));
|
|
289
|
+
const syncRuntime = { enqueue, snapshot: () => ({ revision: 3, ...index.snapshot() }) };
|
|
290
|
+
|
|
291
|
+
const { app, route } = registry();
|
|
292
|
+
registerSessionIndexRoutes(app, {
|
|
293
|
+
sessionIndexService: index,
|
|
294
|
+
sessionIndexSyncRuntime: syncRuntime,
|
|
295
|
+
getCommittedHostMetadata: () => store.getAll(),
|
|
296
|
+
});
|
|
297
|
+
const res = response();
|
|
298
|
+
await route('POST', '/api/openchamber/session-index/sync')({ body: { directories: ['/repo'] } }, res);
|
|
299
|
+
expect(res.statusCode).toBe(202);
|
|
300
|
+
expect(enqueue).toHaveBeenCalledWith(['/repo']);
|
|
301
|
+
expect(res.body).toMatchObject({ revision: 3, sync: { active: true } });
|
|
302
|
+
expect(res.body.directories[0].sessions.map((s) => s.id)).toEqual(['ses_live']);
|
|
303
|
+
expect(res.body.pinnedSessionIds).toEqual([]);
|
|
304
|
+
|
|
305
|
+
const { app: app2, route: route2 } = registry();
|
|
306
|
+
registerSessionIndexRoutes(app2, {
|
|
307
|
+
sessionIndexService: index,
|
|
308
|
+
sessionIndexSyncRuntime: syncRuntime,
|
|
309
|
+
getCommittedHostMetadata: async () => {
|
|
310
|
+
throw new Error('session metadata is unavailable: EACCES');
|
|
311
|
+
},
|
|
312
|
+
});
|
|
313
|
+
const failRes = response();
|
|
314
|
+
await route2('POST', '/api/openchamber/session-index/sync')({ body: { directories: ['/repo'] } }, failRes);
|
|
315
|
+
expect(failRes.statusCode).toBe(503);
|
|
316
|
+
expect(failRes.body).toMatchObject({ retryable: true, code: 'session_metadata_unavailable' });
|
|
317
|
+
index.close();
|
|
318
|
+
});
|
|
201
319
|
});
|
|
@@ -52,10 +52,15 @@ export const registerSessionIndexRoutes = (app, {
|
|
|
52
52
|
return res.json({ available: true, ...filtered });
|
|
53
53
|
});
|
|
54
54
|
|
|
55
|
-
app.post('/api/openchamber/session-index/sync', (req, res) => {
|
|
55
|
+
app.post('/api/openchamber/session-index/sync', async (req, res) => {
|
|
56
56
|
if (!sessionIndexSyncRuntime) return unsupported(res);
|
|
57
57
|
const directories = Array.isArray(req.body?.directories) ? req.body.directories : [];
|
|
58
|
-
|
|
58
|
+
const base = sessionIndexSyncRuntime.enqueue(directories);
|
|
59
|
+
if (!hostStoreWired) return res.status(202).json(base);
|
|
60
|
+
// Clients apply this body as a snapshot: same Host archive gate as GET.
|
|
61
|
+
const host = await readHostMap();
|
|
62
|
+
if (host.status === 'unavailable') return metadataUnavailable(res, host.error);
|
|
63
|
+
return res.status(202).json(applyHostArchiveToIndexSnapshot(base, host.stored));
|
|
59
64
|
});
|
|
60
65
|
|
|
61
66
|
app.put('/api/openchamber/session-index/directory', (req, res) => {
|
|
@@ -7,6 +7,13 @@ const FULL_RECONCILE_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
|
7
7
|
const FETCH_TIMEOUT_MS = 6_000;
|
|
8
8
|
const INTERACTIVE_YIELD_MS = 1_000;
|
|
9
9
|
const LONG_POLL_MAX_MS = 25_000;
|
|
10
|
+
/**
|
|
11
|
+
* Fixed trailing window for background progress tips. Each SSE tip makes every
|
|
12
|
+
* connected client GET the full snapshot, and one sync batch bumps the revision
|
|
13
|
+
* once per directory and once per child-flag check (~300+ per cold start). Keep
|
|
14
|
+
* progress `notify()` calls on the throttled path; only new batch, idle, stop and
|
|
15
|
+
* `publishChange()` may broadcast immediately. sync-runtime.test.js bounds this.
|
|
16
|
+
*/
|
|
10
17
|
const REVISION_TIP_THROTTLE_MS = 500;
|
|
11
18
|
|
|
12
19
|
const normalizeDirectory = (value) => {
|
|
@@ -136,6 +143,9 @@ export const createSessionIndexSyncRuntime = ({
|
|
|
136
143
|
}
|
|
137
144
|
};
|
|
138
145
|
|
|
146
|
+
// `immediate` is reserved for edges a client must see without delay (new batch,
|
|
147
|
+
// idle, stop, external change). Progress callers must pass nothing: an immediate
|
|
148
|
+
// progress tip reintroduces one full client snapshot GET per directory/child.
|
|
139
149
|
const notify = (immediate = false, cause) => {
|
|
140
150
|
if (stopped && !immediate) return;
|
|
141
151
|
revision += 1;
|
|
@@ -80,6 +80,12 @@ describe('session index background sync runtime', () => {
|
|
|
80
80
|
expect(service.replaceDirectory).toHaveBeenCalledTimes(15);
|
|
81
81
|
expect(service.replaceChildSessions).toHaveBeenCalledTimes(300);
|
|
82
82
|
expect(isFullyIdle(runtime)).toBe(true);
|
|
83
|
+
// Relay cold-start budget: 315 internal revisions must reach SSE as a handful
|
|
84
|
+
// of tips (one per 500ms window + immediate edges), never one per directory or
|
|
85
|
+
// child check. Each tip costs every Relay client a full snapshot GET; the
|
|
86
|
+
// uncoalesced Host measured ~316 tips per mobile cold start.
|
|
87
|
+
expect(runtime.snapshot().revision).toBeGreaterThan(300);
|
|
88
|
+
expect(tips.mock.calls.length).toBeLessThanOrEqual(Math.ceil(2650 / 500) + 3);
|
|
83
89
|
expect(tips).toHaveBeenCalledTimes(8);
|
|
84
90
|
expect(tips.mock.calls.map(([tip]) => tip.revision)).toEqual(revisionsAtDelivery);
|
|
85
91
|
for (const [tip] of tips.mock.calls) expect(tip).not.toHaveProperty('cause');
|