@openchambery/web 2.1.9-beta.5 → 2.1.9-beta.7

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 (53) hide show
  1. package/dist/assets/{AssistantView-BMvmzMFI.js → AssistantView-CuT6bDtM.js} +3 -3
  2. package/dist/assets/{DiagramView-BO6rDoTN.js → DiagramView-D8kZ5sgP.js} +1 -1
  3. package/dist/assets/{MarkdownRenderer-D_ZS58Ur.js → MarkdownRenderer-3Cm5B3ni.js} +2 -2
  4. package/dist/assets/{MarkdownRendererImpl-gBtduBHO.js → MarkdownRendererImpl-BSYMaqFX.js} +1 -1
  5. package/dist/assets/{MarkstreamRendererImpl-CKilBBOc.js → MarkstreamRendererImpl-B9VRdRju.js} +1 -1
  6. package/dist/assets/{MobileDetailNavigation-Dgqdqrmy.js → MobileDetailNavigation-ZFfIEYTU.js} +1 -1
  7. package/dist/assets/{MobileShareBridge-G2wjyn4g.js → MobileShareBridge-DpuXJdlq.js} +1 -1
  8. package/dist/assets/{MobileTabPageHeader-Cipo7N8C.js → MobileTabPageHeader-DYRGHein.js} +1 -1
  9. package/dist/assets/{MultiRunWindow-C_ow_lbm.js → MultiRunWindow-BaJgbZP9.js} +1 -1
  10. package/dist/assets/{SettingsView-BaLgORC_.js → SettingsView-C18d1an5.js} +1 -1
  11. package/dist/assets/{SettingsWindow-DfSPTDHb.js → SettingsWindow-B07X5mru.js} +1 -1
  12. package/dist/assets/{TerminalView-DhUynfus.js → TerminalView-BbPFQ22Z.js} +1 -1
  13. package/dist/assets/{ToolOutputDialog-DaKPwmer.js → ToolOutputDialog-Bt3ou350.js} +1 -1
  14. package/dist/assets/{appThemeRegistry-zPqSQBFx.js → appThemeRegistry-BNncg3Lg.js} +1 -1
  15. package/dist/assets/{fileMentionTouchSelection-C8mKDaBe.js → fileMentionTouchSelection-BF0RYuUw.js} +1 -1
  16. package/dist/assets/{gitApi-BCPykg93.js → gitApi-KzJZseRG.js} +1 -1
  17. package/dist/assets/{insertionBoundaries-h0pySzQJ.js → insertionBoundaries-BoN2D2CB.js} +1 -1
  18. package/dist/assets/{main-C8RDC4kz.js → main-C2COAbeT.js} +4 -4
  19. package/dist/assets/{main-4JVZhTAZ.js → main-DBLCcpJe.js} +2 -2
  20. package/dist/assets/{mcpQueries-Cm1VNAmy.js → mcpQueries-BF9h43FH.js} +1 -1
  21. package/dist/assets/{miniChat-_SvWYZuQ.js → miniChat-Bv5ORJmD.js} +2 -2
  22. package/dist/assets/{mobile-Cil7_kYn.js → mobile-BzKeTq52.js} +2 -2
  23. package/dist/assets/{multirun-BzeInZt6.js → multirun-BHANiphy.js} +1 -1
  24. package/dist/assets/{projectMeta-CUAgRGwW.js → projectMeta-CuIJ2wzX.js} +1 -1
  25. package/dist/assets/{renderElectronMiniChatApp-BrAIRgb-.js → renderElectronMiniChatApp-B4NSy0ql.js} +2 -2
  26. package/dist/assets/{renderMobileApp-DxTazPkB.js → renderMobileApp-BnIawAK6.js} +4 -4
  27. package/dist/assets/{runtimeConfig-Bljmgd6r.js → runtimeConfig-BR335S7R.js} +1 -1
  28. package/dist/assets/{runtimeEndpointReset-BYsDk8ux.js → runtimeEndpointReset-grACS-Iv.js} +1 -1
  29. package/dist/assets/{sessionChangeSummary-BslBzYAm.js → sessionChangeSummary-Y6g4Hmn3.js} +1 -1
  30. package/dist/assets/{sessionLookup-5MrKwBXP.js → sessionLookup-Ntvtx26F.js} +1 -1
  31. package/dist/assets/{speechSettingsQueries-otzPynPJ.js → speechSettingsQueries-BS7COcv4.js} +1 -1
  32. package/dist/assets/{useAppFontEffects-DHJ_Qc3g.js → useAppFontEffects-DsaIbgdU.js} +5 -5
  33. package/dist/assets/{useDesktopWindowControlsLayout-BnMDHglb.js → useDesktopWindowControlsLayout-DFuraQa1.js} +1 -1
  34. package/dist/assets/{useEffectiveDirectory-D0qR3aRK.js → useEffectiveDirectory-Bw6EMInE.js} +1 -1
  35. package/dist/assets/{useMobileNavigationStore-BZ7oo4Ir.js → useMobileNavigationStore-BN866WTh.js} +3 -3
  36. package/dist/assets/{useWorkerHighlightedLines-B86utfqe.js → useWorkerHighlightedLines-DsyP6A_g.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/index.js +1 -0
  42. package/server/lib/opencode/session-directory-runtime.js +14 -1
  43. package/server/lib/opencode/session-directory-runtime.test.js +62 -0
  44. package/server/lib/relay/DOCUMENTATION.md +4 -0
  45. package/server/lib/relay/tunnel-host.js +3 -0
  46. package/server/lib/relay/tunnel-host.test.js +27 -0
  47. package/server/lib/session-index/DOCUMENTATION.md +34 -0
  48. package/server/lib/session-index/host-authority-snapshot.js +31 -0
  49. package/server/lib/session-index/routes.host-archive.test.js +118 -0
  50. package/server/lib/session-index/routes.js +7 -2
  51. package/server/lib/session-index/service.js +9 -0
  52. package/server/lib/session-index/sync-runtime.js +44 -0
  53. package/server/lib/session-index/sync-runtime.test.js +33 -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-3Cm5B3ni.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-DBLCcpJe.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-BR335S7R.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-Bv5ORJmD.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-BR335S7R.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-BzKeTq52.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-BR335S7R.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.7",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "./server/index.js",
package/server/index.js CHANGED
@@ -1638,6 +1638,7 @@ async function main(options = {}) {
1638
1638
  getOpenCodeAuthHeaders,
1639
1639
  waitForOpenCodeReady,
1640
1640
  fetchFn: indexSessionDirectories.fetchSessionList,
1641
+ isDirectoryMissing: indexSessionDirectories.isDirectoryMissing,
1641
1642
  resolveDirectory: indexSessionDirectories.resolveDirectory,
1642
1643
  projectSessions: (sessions) => {
1643
1644
  const snap = sessionMetadataStore.getSnapshotSync();
@@ -8,6 +8,7 @@ const directoryOf = (session) => session?.location?.directory ?? session?.direct
8
8
  export const createSessionDirectoryRuntime = ({
9
9
  getRuntimeUrl,
10
10
  realpath = fs.realpathSync.native,
11
+ stat = fs.promises.stat,
11
12
  fetchFn = (...args) => globalThis.fetch(...args),
12
13
  now = Date.now,
13
14
  } = {}) => {
@@ -38,6 +39,18 @@ export const createSessionDirectoryRuntime = ({
38
39
  return resolved;
39
40
  };
40
41
  const resolveDirectory = (value) => localPath(value) ?? normalize(value);
42
+ const isDirectoryMissing = async (value) => {
43
+ const input = normalize(value);
44
+ if (!input || !isLocal()) return false;
45
+ const runtime = scope;
46
+ try {
47
+ await stat(input);
48
+ return false;
49
+ } catch (error) {
50
+ // Permission and I/O failures are not proof of deletion.
51
+ return error?.code === 'ENOENT' && isLocal() && scope === runtime;
52
+ }
53
+ };
41
54
  const projectSession = (session) => {
42
55
  if (!session || typeof session !== 'object') return session;
43
56
  const directory = resolveDirectory(directoryOf(session));
@@ -106,5 +119,5 @@ export const createSessionDirectoryRuntime = ({
106
119
  headers.set('content-type', 'application/json');
107
120
  return new Response(JSON.stringify(body), { status: response.status, headers });
108
121
  };
109
- return { isLocal, resolveDirectory, projectSession, projectEvent, fetchSessionList };
122
+ return { isLocal, resolveDirectory, isDirectoryMissing, projectSession, projectEvent, fetchSessionList };
110
123
  };
@@ -24,6 +24,68 @@ const session = (id, directory, updated = 1) => ({ id, title: id, location: { di
24
24
  const json = (value, status = 200) => new Response(JSON.stringify(value), { status, headers: { 'content-type': 'application/json' } });
25
25
 
26
26
  describe('physical session directory identity', () => {
27
+ it('checks deletion only on the owning local runtime and preserves uncertain failures', async () => {
28
+ let url = 'http://127.0.0.1:4000';
29
+ const stat = vi.fn(async () => { throw Object.assign(new Error('missing'), { code: 'ENOENT' }); });
30
+ const runtime = createSessionDirectoryRuntime({ getRuntimeUrl: () => url, stat });
31
+ expect(await runtime.isDirectoryMissing('/repo')).toBe(true);
32
+ stat.mockRejectedValueOnce(Object.assign(new Error('denied'), { code: 'EACCES' }));
33
+ expect(await runtime.isDirectoryMissing('/repo')).toBe(false);
34
+ stat.mockRejectedValueOnce(Object.assign(new Error('I/O'), { code: 'EIO' }));
35
+ expect(await runtime.isDirectoryMissing('/repo')).toBe(false);
36
+ url = 'https://remote.example';
37
+ stat.mockClear();
38
+ expect(await runtime.isDirectoryMissing('/repo')).toBe(false);
39
+ expect(stat).not.toHaveBeenCalled();
40
+ url = 'http://127.0.0.1:4000';
41
+ stat.mockImplementationOnce(async () => {
42
+ url = 'https://remote.example';
43
+ throw Object.assign(new Error('missing'), { code: 'ENOENT' });
44
+ });
45
+ expect(await runtime.isDirectoryMissing('/repo')).toBe(false);
46
+ });
47
+
48
+ it('cleans missing empty directories, skips scans without losing history, and recovers restored paths', async () => {
49
+ const { real, other } = fixture();
50
+ const missing = path.join(other, 'deleted');
51
+ const history = path.join(other, 'history');
52
+ const partial = path.join(other, 'partial');
53
+ const child = path.join(other, 'child');
54
+ const directories = createSessionDirectoryRuntime();
55
+ const service = createSessionIndexService({ dbPath: ':memory:' });
56
+ for (const directory of [real, missing, history, partial, child]) {
57
+ service.replaceDirectory({ directory, sessions: directory === history ? [session('old', history)] : [], hasMore: directory === partial });
58
+ }
59
+ service.replaceChildSessions(child, 'parent', [{ id: 'child' }]);
60
+ service.setPinned('old', 1);
61
+ const fetchFn = vi.fn(async () => json([]));
62
+ const runtime = createSessionIndexSyncRuntime({
63
+ sessionIndexService: service,
64
+ isDirectoryMissing: directories.isDirectoryMissing,
65
+ buildOpenCodeUrl: (route) => `http://localhost${route}`,
66
+ getOpenCodeAuthHeaders: () => ({}),
67
+ fetchFn,
68
+ });
69
+ try {
70
+ await vi.waitFor(() => expect(service.snapshot().directories.map(x => x.directory)).not.toContain(missing));
71
+ expect(service.snapshot().directories.map(x => x.directory)).toEqual(expect.arrayContaining([real, history, partial, child]));
72
+ runtime.enqueue([missing, history, partial, child]);
73
+ await vi.waitFor(() => expect(runtime.snapshot().sync.active).toBe(false));
74
+ expect(fetchFn).not.toHaveBeenCalled();
75
+ expect(service.findBySessionId('old')).toBeTruthy();
76
+ expect(service.snapshot().pinnedSessionIds).toEqual(['old']);
77
+ expect(service.snapshot().directories.map(x => x.directory)).not.toContain(missing);
78
+ fs.mkdirSync(missing);
79
+ runtime.enqueue([missing]);
80
+ await vi.waitFor(() => expect(runtime.snapshot().sync.active).toBe(false));
81
+ expect(fetchFn).toHaveBeenCalledTimes(1);
82
+ expect(service.snapshot().directories.map(x => x.directory)).toContain(missing);
83
+ } finally {
84
+ runtime.stop();
85
+ service.close();
86
+ }
87
+ });
88
+
27
89
  it('recovers unindexed alias history across pages without admitting another worktree', async () => {
28
90
  const { real, alias, other } = fixture();
29
91
  const requests = [];
@@ -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', () => {
@@ -1,5 +1,23 @@
1
1
  # Session Index Runtime
2
2
 
3
+ ## Missing local directories
4
+
5
+ The Host probes local directory existence asynchronously before background root
6
+ and child scans. Confirmed ENOENT skips upstream loading without replacing history
7
+ with an empty result. Remote upstreams never use the Host filesystem as evidence;
8
+ permission and I/O errors are not deletion. Checks are not negatively cached, so
9
+ a recreated directory participates in the next requested sync.
10
+
11
+ At startup and every 60 seconds, the sync runtime checks empty, exhausted indexed
12
+ directories. It removes only directory shells with no summary rows, no child rows,
13
+ and no more pages, using one conditional SQLite deletion. Session history and pins
14
+ are preserved. This maintenance does not touch code files or upstream projects.
15
+ Runtime switches and shutdown discard pending checks; successful cleanup publishes
16
+ a revision tip. Snapshot GETs do not perform filesystem scans. Old project paths
17
+ may still be submitted by clients, but pre-scan checks prevent recreating empty
18
+ shells for missing paths. App or external worktree deletion is picked up by the
19
+ same maintenance pass (within 60 seconds, plus filesystem check time).
20
+
3
21
  ## Physical directory identity
4
22
 
5
23
  The Host injects `session-directory-runtime.js` into index writes, background
@@ -77,6 +95,15 @@ Pin and unpin use `POST` / `DELETE`
77
95
  Snapshots include `pinnedSessionIds` plus `time.pinned` on any matching
78
96
  summary row.
79
97
 
98
+ List snapshots (`GET /api/openchamber/session-index` and the `POST /sync`
99
+ response body) share one Host projection: committed Host metadata is merged,
100
+ Host-archived rows and pins are dropped, and metadata no list consumer reads is
101
+ omitted (`openchamber.titleAuthority`, `openchamber.scheduledTaskSkill`,
102
+ `titleRefresh.forMessageID` / `generatedAt` / `forkFirstSendSessionID`). Unknown
103
+ keys pass through. Writers and the title runtime read the omitted keys from the
104
+ session record, never from the index list. The deep-link lookup keeps the full
105
+ projection. Both list routes return 503 when Host metadata is unavailable.
106
+
80
107
  The server-side global OpenCode event subscriber writes session summary events
81
108
  directly into this index. User `message.updated` events and `session.idle`
82
109
  completion events advance the separate `activity_updated_at` ordering field,
@@ -136,6 +163,13 @@ revision increments and `waitForChange` delivery remain immediate, including the
136
163
  final state. Full snapshots are materialized for waiting readers, not for each
137
164
  tip. No persisted revision, failure, or runtime-isolation semantics change.
138
165
 
166
+ Regression budget: one batch bumps the revision per directory and per child-flag
167
+ check (~300+ on a cold start). Before coalescing, each bump became an SSE tip and
168
+ every Relay client re-fetched the full snapshot per burst. `sync-runtime.test.js`
169
+ asserts that a 15-directory/300-child batch with 300+ revisions emits at most
170
+ one tip per 500ms window plus immediate edges. Progress `notify()` calls must not
171
+ pass `immediate`.
172
+
139
173
  The shared renderer independently throttles busy tips to a fixed 1000ms window
140
174
  for compatibility with older Hosts. Idle/legacy tips use 100ms debounce and
141
175
  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) => {
@@ -679,9 +679,18 @@ export const createSessionIndexService = ({ dbPath, getRuntimeConfig = () => nul
679
679
  return result.changes > 0;
680
680
  });
681
681
 
682
+ const removeEmptyDirectory = (directory) => db.prepare(`
683
+ DELETE FROM runtime_directory WHERE runtime_key = ? AND directory = ? AND has_more = 0
684
+ AND NOT EXISTS (SELECT 1 FROM session_summary s
685
+ WHERE s.runtime_key = runtime_directory.runtime_key AND s.directory = runtime_directory.directory)
686
+ AND NOT EXISTS (SELECT 1 FROM session_child c
687
+ WHERE c.runtime_key = runtime_directory.runtime_key AND c.directory = runtime_directory.directory)
688
+ `).run(runtimeKey(), directory).changes > 0;
689
+
682
690
  try { reconcileDirectoryAliases(runtimeKey()); } catch (error) { db.close(); throw error; }
683
691
  return {
684
692
  replaceDirectory,
693
+ removeEmptyDirectory,
685
694
  replaceDirectories,
686
695
  upsert,
687
696
  upsertAndReportChange,