@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.
Files changed (49) hide show
  1. package/dist/assets/{AssistantView-BMvmzMFI.js → AssistantView-3Rfvw4sl.js} +3 -3
  2. package/dist/assets/{DiagramView-BO6rDoTN.js → DiagramView-CgmWfIrQ.js} +1 -1
  3. package/dist/assets/{MarkdownRenderer-D_ZS58Ur.js → MarkdownRenderer-CptQecYA.js} +2 -2
  4. package/dist/assets/{MarkdownRendererImpl-gBtduBHO.js → MarkdownRendererImpl-BsOrNJni.js} +1 -1
  5. package/dist/assets/{MarkstreamRendererImpl-CKilBBOc.js → MarkstreamRendererImpl-0EuyxKix.js} +1 -1
  6. package/dist/assets/{MobileDetailNavigation-Dgqdqrmy.js → MobileDetailNavigation-CI00mBRX.js} +1 -1
  7. package/dist/assets/{MobileShareBridge-G2wjyn4g.js → MobileShareBridge-BSRHQmvf.js} +1 -1
  8. package/dist/assets/{MobileTabPageHeader-Cipo7N8C.js → MobileTabPageHeader-CdgleYWQ.js} +1 -1
  9. package/dist/assets/{MultiRunWindow-C_ow_lbm.js → MultiRunWindow-D6IK1Zkp.js} +1 -1
  10. package/dist/assets/{SettingsView-BaLgORC_.js → SettingsView-D_lZOSSF.js} +1 -1
  11. package/dist/assets/{SettingsWindow-DfSPTDHb.js → SettingsWindow-ByLmdF2a.js} +1 -1
  12. package/dist/assets/{TerminalView-DhUynfus.js → TerminalView-Th-9Zt_f.js} +1 -1
  13. package/dist/assets/{ToolOutputDialog-DaKPwmer.js → ToolOutputDialog-D_fxjrzt.js} +1 -1
  14. package/dist/assets/{appThemeRegistry-zPqSQBFx.js → appThemeRegistry-wX31Kn4M.js} +1 -1
  15. package/dist/assets/{fileMentionTouchSelection-C8mKDaBe.js → fileMentionTouchSelection-BbnpDd2Y.js} +1 -1
  16. package/dist/assets/{gitApi-BCPykg93.js → gitApi-sk4tCimN.js} +1 -1
  17. package/dist/assets/{insertionBoundaries-h0pySzQJ.js → insertionBoundaries-Dup8RNq4.js} +1 -1
  18. package/dist/assets/{main-4JVZhTAZ.js → main-BVHlGGZs.js} +2 -2
  19. package/dist/assets/{main-C8RDC4kz.js → main-D7knwcbE.js} +4 -4
  20. package/dist/assets/{mcpQueries-Cm1VNAmy.js → mcpQueries-DLcJMbJ8.js} +1 -1
  21. package/dist/assets/{miniChat-_SvWYZuQ.js → miniChat-C71dZf-x.js} +2 -2
  22. package/dist/assets/{mobile-Cil7_kYn.js → mobile-BVDtJrmi.js} +2 -2
  23. package/dist/assets/{multirun-BzeInZt6.js → multirun-BaN9JOfb.js} +1 -1
  24. package/dist/assets/{projectMeta-CUAgRGwW.js → projectMeta-WqSqDsLK.js} +1 -1
  25. package/dist/assets/{renderElectronMiniChatApp-BrAIRgb-.js → renderElectronMiniChatApp-BZ3mBJuQ.js} +2 -2
  26. package/dist/assets/{renderMobileApp-DxTazPkB.js → renderMobileApp-D5oYObXh.js} +4 -4
  27. package/dist/assets/{runtimeConfig-Bljmgd6r.js → runtimeConfig-K7gJK2wK.js} +1 -1
  28. package/dist/assets/{runtimeEndpointReset-BYsDk8ux.js → runtimeEndpointReset-C9YZ_XHW.js} +1 -1
  29. package/dist/assets/{sessionChangeSummary-BslBzYAm.js → sessionChangeSummary-Db70oATx.js} +1 -1
  30. package/dist/assets/{sessionLookup-5MrKwBXP.js → sessionLookup-oPdIysdc.js} +1 -1
  31. package/dist/assets/{speechSettingsQueries-otzPynPJ.js → speechSettingsQueries-Z5qhFY5C.js} +1 -1
  32. package/dist/assets/{useAppFontEffects-DHJ_Qc3g.js → useAppFontEffects-C5__ugup.js} +5 -5
  33. package/dist/assets/{useDesktopWindowControlsLayout-BnMDHglb.js → useDesktopWindowControlsLayout-MTlcH4Zm.js} +1 -1
  34. package/dist/assets/{useEffectiveDirectory-D0qR3aRK.js → useEffectiveDirectory-Ty8RNv4U.js} +1 -1
  35. package/dist/assets/{useMobileNavigationStore-BZ7oo4Ir.js → useMobileNavigationStore-SqU7Bc3B.js} +3 -3
  36. package/dist/assets/{useWorkerHighlightedLines-B86utfqe.js → useWorkerHighlightedLines-5ZbHo-Qd.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/relay/DOCUMENTATION.md +4 -0
  42. package/server/lib/relay/tunnel-host.js +3 -0
  43. package/server/lib/relay/tunnel-host.test.js +27 -0
  44. package/server/lib/session-index/DOCUMENTATION.md +16 -0
  45. package/server/lib/session-index/host-authority-snapshot.js +31 -0
  46. package/server/lib/session-index/routes.host-archive.test.js +118 -0
  47. package/server/lib/session-index/routes.js +7 -2
  48. package/server/lib/session-index/sync-runtime.js +10 -0
  49. package/server/lib/session-index/sync-runtime.test.js +6 -0
@@ -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,`&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-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,`&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-4JVZhTAZ.js"></script>
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-Bljmgd6r.js">
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">
@@ -74,7 +74,7 @@
74
74
  }
75
75
  </style>
76
76
 
77
- <script type="module" crossorigin src="/assets/miniChat-_SvWYZuQ.js"></script>
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-Bljmgd6r.js">
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-Cil7_kYn.js"></script>
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-Bljmgd6r.js">
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openchambery/web",
3
- "version": "2.1.9-beta.5",
3
+ "version": "2.1.9-beta.6",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "./server/index.js",
@@ -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
- res.status(202).json(sessionIndexSyncRuntime.enqueue(directories));
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');