@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.
- package/dist/assets/{AssistantView-BMvmzMFI.js → AssistantView-CuT6bDtM.js} +3 -3
- package/dist/assets/{DiagramView-BO6rDoTN.js → DiagramView-D8kZ5sgP.js} +1 -1
- package/dist/assets/{MarkdownRenderer-D_ZS58Ur.js → MarkdownRenderer-3Cm5B3ni.js} +2 -2
- package/dist/assets/{MarkdownRendererImpl-gBtduBHO.js → MarkdownRendererImpl-BSYMaqFX.js} +1 -1
- package/dist/assets/{MarkstreamRendererImpl-CKilBBOc.js → MarkstreamRendererImpl-B9VRdRju.js} +1 -1
- package/dist/assets/{MobileDetailNavigation-Dgqdqrmy.js → MobileDetailNavigation-ZFfIEYTU.js} +1 -1
- package/dist/assets/{MobileShareBridge-G2wjyn4g.js → MobileShareBridge-DpuXJdlq.js} +1 -1
- package/dist/assets/{MobileTabPageHeader-Cipo7N8C.js → MobileTabPageHeader-DYRGHein.js} +1 -1
- package/dist/assets/{MultiRunWindow-C_ow_lbm.js → MultiRunWindow-BaJgbZP9.js} +1 -1
- package/dist/assets/{SettingsView-BaLgORC_.js → SettingsView-C18d1an5.js} +1 -1
- package/dist/assets/{SettingsWindow-DfSPTDHb.js → SettingsWindow-B07X5mru.js} +1 -1
- package/dist/assets/{TerminalView-DhUynfus.js → TerminalView-BbPFQ22Z.js} +1 -1
- package/dist/assets/{ToolOutputDialog-DaKPwmer.js → ToolOutputDialog-Bt3ou350.js} +1 -1
- package/dist/assets/{appThemeRegistry-zPqSQBFx.js → appThemeRegistry-BNncg3Lg.js} +1 -1
- package/dist/assets/{fileMentionTouchSelection-C8mKDaBe.js → fileMentionTouchSelection-BF0RYuUw.js} +1 -1
- package/dist/assets/{gitApi-BCPykg93.js → gitApi-KzJZseRG.js} +1 -1
- package/dist/assets/{insertionBoundaries-h0pySzQJ.js → insertionBoundaries-BoN2D2CB.js} +1 -1
- package/dist/assets/{main-C8RDC4kz.js → main-C2COAbeT.js} +4 -4
- package/dist/assets/{main-4JVZhTAZ.js → main-DBLCcpJe.js} +2 -2
- package/dist/assets/{mcpQueries-Cm1VNAmy.js → mcpQueries-BF9h43FH.js} +1 -1
- package/dist/assets/{miniChat-_SvWYZuQ.js → miniChat-Bv5ORJmD.js} +2 -2
- package/dist/assets/{mobile-Cil7_kYn.js → mobile-BzKeTq52.js} +2 -2
- package/dist/assets/{multirun-BzeInZt6.js → multirun-BHANiphy.js} +1 -1
- package/dist/assets/{projectMeta-CUAgRGwW.js → projectMeta-CuIJ2wzX.js} +1 -1
- package/dist/assets/{renderElectronMiniChatApp-BrAIRgb-.js → renderElectronMiniChatApp-B4NSy0ql.js} +2 -2
- package/dist/assets/{renderMobileApp-DxTazPkB.js → renderMobileApp-BnIawAK6.js} +4 -4
- package/dist/assets/{runtimeConfig-Bljmgd6r.js → runtimeConfig-BR335S7R.js} +1 -1
- package/dist/assets/{runtimeEndpointReset-BYsDk8ux.js → runtimeEndpointReset-grACS-Iv.js} +1 -1
- package/dist/assets/{sessionChangeSummary-BslBzYAm.js → sessionChangeSummary-Y6g4Hmn3.js} +1 -1
- package/dist/assets/{sessionLookup-5MrKwBXP.js → sessionLookup-Ntvtx26F.js} +1 -1
- package/dist/assets/{speechSettingsQueries-otzPynPJ.js → speechSettingsQueries-BS7COcv4.js} +1 -1
- package/dist/assets/{useAppFontEffects-DHJ_Qc3g.js → useAppFontEffects-DsaIbgdU.js} +5 -5
- package/dist/assets/{useDesktopWindowControlsLayout-BnMDHglb.js → useDesktopWindowControlsLayout-DFuraQa1.js} +1 -1
- package/dist/assets/{useEffectiveDirectory-D0qR3aRK.js → useEffectiveDirectory-Bw6EMInE.js} +1 -1
- package/dist/assets/{useMobileNavigationStore-BZ7oo4Ir.js → useMobileNavigationStore-BN866WTh.js} +3 -3
- package/dist/assets/{useWorkerHighlightedLines-B86utfqe.js → useWorkerHighlightedLines-DsyP6A_g.js} +1 -1
- package/dist/index.html +2 -2
- package/dist/mini-chat.html +2 -2
- package/dist/mobile.html +2 -2
- package/package.json +1 -1
- package/server/index.js +1 -0
- package/server/lib/opencode/session-directory-runtime.js +14 -1
- package/server/lib/opencode/session-directory-runtime.test.js +62 -0
- package/server/lib/relay/DOCUMENTATION.md +4 -0
- package/server/lib/relay/tunnel-host.js +3 -0
- package/server/lib/relay/tunnel-host.test.js +27 -0
- package/server/lib/session-index/DOCUMENTATION.md +34 -0
- package/server/lib/session-index/host-authority-snapshot.js +31 -0
- package/server/lib/session-index/routes.host-archive.test.js +118 -0
- package/server/lib/session-index/routes.js +7 -2
- package/server/lib/session-index/service.js +9 -0
- package/server/lib/session-index/sync-runtime.js +44 -0
- package/server/lib/session-index/sync-runtime.test.js +33 -0
package/dist/assets/{useWorkerHighlightedLines-B86utfqe.js → useWorkerHighlightedLines-DsyP6A_g.js}
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{r as e}from"./rolldown-runtime-hePW80VL.js";import{n as t}from"./runtime-fetch-WgH0pROs.js";import{Mn as n,ln as r}from"./persistence-BKszKaoQ.js";import{it as i,rt as a}from"./vendor-base-ui-CdzJ6My_.js";import{n as o}from"./platform-CgwxwfA4.js";import{v as s}from"./useUIStore-CEqU9Ujv.js";import{n as c}from"./vendor-react-BP2cRVqX.js";import{a as l,n as u}from"./vendor-aparajita-capacitor-secure-storage-xol_6BsB.js";import{f as d,r as f,t as p}from"./button-CWSs9Baz.js";import{n as m}from"./useThemeSystem-8jhE4Zyk.js";import{n as h}from"./Icon-BCFeHDWx.js";import{i as g,n as _,t as v}from"./tooltip-CV6dvLBp.js";import{a as y,i as b,o as x,t as S}from"./image-loading-Bj11k4gM.js";import{n as C}from"./MarkdownRenderer-D_ZS58Ur.js";import{n as w,o as T,s as E,t as D}from"./markdown-worker-DFNZ3KJ_.js";import{n as ee}from"./markdownTheme-CV7_0QAL.js";import{t as te}from"./vendor-tanstack-react-virtual-B_EO--9t.js";var O=e(i(),1),k=c(),A=a(),ne=`w-[min(30rem,calc(100cqw-1rem))] min-w-[280px] max-w-full overflow-hidden rounded-xl shadow-[inset_0_1px_0_0_rgba(255,255,255,0.8),inset_0_0_0_1px_rgba(0,0,0,0.04),0_0_0_1px_rgba(0,0,0,0.10),0_1px_2px_-0.5px_rgba(0,0,0,0.08),0_4px_8px_-2px_rgba(0,0,0,0.08),0_12px_20px_-4px_rgba(0,0,0,0.08)] dark:shadow-[inset_0_1px_0_0_rgba(255,255,255,0.12),inset_0_0_0_1px_rgba(255,255,255,0.08),0_0_0_1px_rgba(0,0,0,0.36),0_1px_1px_-0.5px_rgba(0,0,0,0.22),0_3px_3px_-1.5px_rgba(0,0,0,0.20),0_6px_6px_-3px_rgba(0,0,0,0.16)]`,j={maxWidth:`min(28rem, calc(100cqw - 4ch))`,backgroundColor:`var(--surface-elevated)`,color:`var(--surface-elevated-foreground)`},M=`flex items-center justify-between gap-3 border-b border-[var(--surface-subtle)] px-3 py-2.5`,N=`text-xs font-medium leading-4 text-foreground md:text-[0.8125rem] md:leading-5`,re=`text-xs tabular-nums leading-4 text-muted-foreground md:text-[0.8125rem] md:leading-5`,P=`m-0 list-none divide-y divide-[var(--surface-subtle)] p-0`,F=`${P} max-h-[min(22rem,50vh)] overflow-y-auto`,ie=`${P} w-full`,ae={className:`p-0 rounded-none`,maxHeightClass:`max-h-[46vh]`,disableHorizontal:!0},I=`px-2.5 py-1.5 text-xs leading-4 md:py-2 md:text-[0.8125rem] md:leading-5`,oe=`flex min-w-0 items-center gap-1 text-xs leading-4 text-muted-foreground md:gap-1.5 md:text-[0.8125rem] md:leading-5`,se=`min-w-0 truncate text-xs leading-4 text-foreground md:text-[0.8125rem] md:leading-5`,ce=`inline-flex shrink-0 items-center gap-1 text-xs leading-4 tabular-nums text-muted-foreground md:text-[0.8125rem] md:leading-5`,le=`h-3 w-3 shrink-0 md:h-3.5 md:w-3.5`,ue={in_progress:`text-foreground`,pending:`text-foreground`,completed:`text-muted-foreground line-through`,cancelled:`text-muted-foreground line-through`},L={in_progress:`chat.statusRow.todo.status.inProgress`,pending:`chat.statusRow.todo.status.pending`,completed:`chat.statusRow.todo.status.completed`,cancelled:`chat.statusRow.todo.status.cancelled`},R=e=>{let t=(0,k.c)(26),{todo:n,isActive:r,activeRef:i}=e,{t:a}=d(),o=z.has(n.status)?n.status:`pending`,s=L[o],c;t[0]!==a||t[1]!==s?(c=a(s),t[0]=a,t[1]=s,t[2]=c):c=t[2];let l=c,u;t[3]===o?u=t[4]:(u=o===`in_progress`?(0,A.jsx)(h,{name:`record-circle`,className:`h-3.5 w-3.5 text-[var(--status-info)]`,"aria-hidden":`true`}):o===`completed`?(0,A.jsx)(h,{name:`checkbox-circle`,className:`h-3.5 w-3.5 text-[var(--status-success)]`,"aria-hidden":`true`}):(0,A.jsx)(h,{name:`time`,className:`h-3.5 w-3.5 text-muted-foreground`,"aria-hidden":`true`}),t[3]=o,t[4]=u);let p=u,m=r?`step`:void 0,y=r&&`bg-[var(--surface-muted)] before:absolute before:inset-y-2 before:left-0 before:w-0.5 before:rounded-full before:bg-[var(--status-info)]`,b;t[5]===y?b=t[6]:(b=f(`group relative grid min-w-0 grid-cols-[1rem_minmax(0,1fr)] items-start gap-2.5`,I,y),t[5]=y,t[6]=b);let x;t[7]!==p||t[8]!==l?(x=(0,A.jsx)(g,{asChild:!0,children:(0,A.jsx)(`span`,{className:`flex size-4 items-center justify-center`,"aria-label":l,children:p})}),t[7]=p,t[8]=l,t[9]=x):x=t[9];let S;t[10]===l?S=t[11]:(S=(0,A.jsx)(_,{side:`left`,sideOffset:6,children:l}),t[10]=l,t[11]=S);let C;t[12]!==x||t[13]!==S?(C=(0,A.jsxs)(v,{children:[x,S]}),t[12]=x,t[13]=S,t[14]=C):C=t[14];let w=ue[o],T;t[15]===w?T=t[16]:(T=f(`block whitespace-normal break-words`,w),t[15]=w,t[16]=T);let E;t[17]!==T||t[18]!==n.content?(E=(0,A.jsx)(`span`,{className:`min-w-0 flex-1`,children:(0,A.jsx)(`span`,{className:T,children:n.content})}),t[17]=T,t[18]=n.content,t[19]=E):E=t[19];let D;return t[20]!==i||t[21]!==E||t[22]!==m||t[23]!==b||t[24]!==C?(D=(0,A.jsxs)(`li`,{ref:i,"aria-current":m,className:b,children:[C,E]}),t[20]=i,t[21]=E,t[22]=m,t[23]=b,t[24]=C,t[25]=D):D=t[25],D},z=new Set([`in_progress`,`pending`,`completed`,`cancelled`]),B=`image/png`,V=l(`OpenChamberMedia`),H=e=>{let t=e.toLowerCase().split(`;`)[0]?.trim()||B;if(t===`image/jpeg`||t===`image/jpg`)return`jpg`;if(t===`image/webp`)return`webp`;if(t===`image/gif`)return`gif`;if(t===`image/svg+xml`)return`svg`;if(t===`image/png`)return`png`;let n=t.split(`/`)[1];return n&&/^[a-z0-9.+-]+$/i.test(n)?n:`png`},U=(e,t)=>{let n=`image.${H(t)}`,r=(((e||``).trim()||n).split(/[\\/]/).pop()||n).replace(/[^\w.\- ()[\]]+/g,`_`).replace(/^\.+/,``);return r?/\.[a-z0-9]{1,8}$/i.test(r)?r:`${r}.${H(t)}`:n},W=e=>new Promise((t,n)=>{let r=new FileReader;r.onload=()=>{if(typeof r.result==`string`){t(r.result);return}n(Error(`Failed to encode image`))},r.onerror=()=>n(r.error??Error(`Failed to encode image`)),r.readAsDataURL(e)}),de=async e=>{let t=await W(e),n=t.indexOf(`,`);return n>=0?t.slice(n+1):t},fe=(e,t)=>{let n=URL.createObjectURL(e),r=document.createElement(`a`);r.href=n,r.download=t,document.body.appendChild(r),r.click(),document.body.removeChild(r),window.setTimeout(()=>URL.revokeObjectURL(n),1e3)},G=async(e,t)=>{let n=await fetch(e,{signal:t,cache:`no-store`});if(!n.ok)throw Error(`Image fetch failed (${n.status})`);return n.blob()},pe=async(e,t=B)=>{if(!e.naturalWidth||!e.naturalHeight)return null;let n=e.currentSrc||e.src;if(n&&(/^(?:blob:|data:|openchamber-asset:)/i.test(n)||/^https?:/i.test(n)||/^\/api(?=\/|[?#]|$)/.test(n)))try{let e=await G(n);if(e.size>0)return e}catch{}try{let n=document.createElement(`canvas`);n.width=e.naturalWidth,n.height=e.naturalHeight;let r=n.getContext(`2d`);if(!r)return null;r.drawImage(e,0,0);let i=t.startsWith(`image/`)?t:B,a=await new Promise(e=>{n.toBlob(t=>e(t),i===`image/jpg`?`image/jpeg`:i)});return a&&a.size>0?a:null}catch{return null}},me=async(e,t)=>{let n=``;try{return n=await S(e,t??new AbortController().signal),await G(n,t)}finally{n&&y(n)}},he=async(e,n)=>{let r=e.effectiveDirectory??``,i=null;e.prefetchedBlob&&e.prefetchedBlob.size>0&&(i=e.prefetchedBlob);let a=e.displayUrl?.trim();if(!i&&a)try{i=await G(a,n)}catch{i=null}if(!i){let a=e.sourceUrl.trim();if(!a)throw Error(`Image source is empty`);if(/^(?:data|blob):/i.test(a)||/^https?:/i.test(a)||/^\/api(?=\/|[?#]|$)/.test(a))i=await G(a,n);else{let e=x(a,r);if(b(e))try{i=await me(e.path,n)}catch{let r=await t(`/api/fs/raw`,{method:`GET`,cache:`no-store`,query:{path:e.path},signal:n});if(!r.ok)throw Error(`Image source request failed with status ${r.status}`);i=await r.blob()}else i=await G(e.source,n)}}if(!i||i.size<=0)throw Error(`Image is empty`);let o=(e.mimeType||(i.type&&i.type.startsWith(`image/`)?i.type:``)||B).split(`;`)[0].trim()||B;return{blob:i,filename:U(e.filename,o),mimeType:o}},ge=()=>!o()||u===void 0?!1:u.isPluginAvailable(`OpenChamberMedia`),_e=async(e,t)=>{let{blob:i,filename:a,mimeType:o}=await he(e,t);if(ge()){let e=await de(i);return await V.saveImage({dataBase64:e,mimeType:o,filename:a}),`saved`}if(r()){let e=n()?.vscode;if(e?.saveImage){let t=await W(i);return await e.saveImage({fileName:a,dataUrl:t}),`saved`}}return fe(i,a),`downloaded`},ve=e=>e.replace(/&/g,`&`).replace(/</g,`<`).replace(/>/g,`>`).replace(/"/g,`"`).replace(/'/g,`'`),ye=e=>e?Object.entries(e).map(([e,t])=>t==null?``:`${e.startsWith(`--`)?e:e.replace(/[A-Z]/g,e=>`-${e.toLowerCase()}`)}:${typeof t==`number`?`${t}px`:t};`).join(``):``,K=(e,t,n)=>{let r=e.querySelector(`pre`);r&&(r.style.margin=`0`,r.style.background=`transparent`,r.style.whiteSpace=t?`pre-wrap`:`pre`,t&&(r.style.wordBreak=`break-word`,r.style.overflowWrap=`break-word`));let i=e.querySelector(`code`);if(i){let e=ye(n);e&&i.setAttribute(`style`,`${i.getAttribute(`style`)??``}${e}`)}},be=e=>`<pre><code>${ve(e)}</code></pre>`,xe=e=>{let t=(0,k.c)(21),{code:n,language:r,className:i,style:a,codeStyle:o,wrap:s}=e,c=s!==void 0&&s,{currentTheme:l}=m(),u=O.useRef(null),d;t[0]===l?d=t[1]:(d=ee(l),t[0]=l,t[1]=d);let p=d,h,g;t[2]!==n||t[3]!==o||t[4]!==c?(h=()=>{let e=u.current;e&&(e.innerHTML=be(n),K(e,c,o))},g=[n,c,o],t[2]=n,t[3]=o,t[4]=c,t[5]=h,t[6]=g):(h=t[5],g=t[6]),O.useLayoutEffect(h,g);let _,v;t[7]!==n||t[8]!==o||t[9]!==r||t[10]!==c?(_=()=>{let e=u.current;if(!e)return;let t=!0,i=new AbortController;return D(n,(r||`text`).toLowerCase(),{signal:i.signal,priority:`visible`}).then(n=>{!t||!e||!n||(e.innerHTML=n,K(e,c,o))}),()=>{t=!1,i.abort()}},v=[n,r,c,o],t[7]=n,t[8]=o,t[9]=r,t[10]=c,t[11]=_,t[12]=v):(_=t[11],v=t[12]),O.useEffect(_,v);let y;t[13]===i?y=t[14]:(y=f(`typography-code`,i),t[13]=i,t[14]=y);let b=p,x;t[15]!==a||t[16]!==b?(x={...b,...a},t[15]=a,t[16]=b,t[17]=x):x=t[17];let S;return t[18]!==y||t[19]!==x?(S=(0,A.jsx)(`div`,{ref:u,className:y,style:x}),t[18]=y,t[19]=x,t[20]=S):S=t[20],S},Se=e=>{let t=e.replace(/^<file>\s*\n?/,``).replace(/\n?<\/file>\s*$/,``);return t=t.replace(/^\s*\d{5}\|\s?/gm,``),t.trim()},Ce=(e,t=``)=>{if(typeof e==`string`)return e;if(e==null)return t;if(typeof e==`number`||typeof e==`boolean`||typeof e==`bigint`)return String(e);try{return JSON.stringify(e)}catch{return t}},we=e=>e?e.includes(`<diagnostics`)||e.includes(`<file_diagnostics>`)||e.includes(`LSP errors detected`)||e.includes(`This file has errors`):!1,Te=e=>e?e.replace(/\n{0,2}LSP errors detected[\s\S]*?<diagnostics[^>]*>[\s\S]*?<\/diagnostics>/g,``).replace(/\n{0,2}This file has errors[\s\S]*?<\/file_diagnostics>/g,``).replace(/<diagnostics[^>]*>[\s\S]*?<\/diagnostics>/g,``).replace(/<file_diagnostics>[\s\S]*?<\/file_diagnostics>/g,``).trim():``,Ee=(e,t)=>!e||typeof e!=`object`?String(e):E(e,t||``),q=e=>{if(typeof e==`string`){let t=e.trim();return t.length>0?t:void 0}if(e&&typeof e==`object`){let t=e.patch;if(typeof t==`string`){let e=t.trim();return e.length>0?e:void 0}}},De=e=>{if(!e||typeof e!=`object`)return;let t=q(e.patch)??q(e.diff);if(t)return t;let n=Array.isArray(e.files)?e.files:[];for(let e of n){if(!e||typeof e!=`object`)continue;let t=q(e.patch)??q(e.diff);if(t)return t}},Oe=e=>{if(!e||typeof e!=`string`)return{data:null,isJson:!1};let t=e.trim();if(!t.startsWith(`{`)&&!t.startsWith(`[`)||!t.endsWith(`}`)&&!t.endsWith(`]`)||t.length<2)return{data:null,isJson:!1};try{let e=JSON.parse(t);return typeof e==`object`&&e?{data:e,isJson:!0}:{data:null,isJson:!1}}catch{return{data:null,isJson:!1}}},ke=(e,t,n)=>{let r=Se(e);if((t===`edit`||t===`multiedit`||t===`write`||t===`apply_patch`)&&we(r)&&(r=Te(r)),(t===`edit`||t===`multiedit`||t===`apply_patch`)&&r.trim().length===0){let e=De(n);if(e)return e}return r},Ae=e=>{let t=e.match(/<type>(file|directory)<\/type>/i)?.[1]?.toLowerCase()??`unknown`,n=(e.match(/<content>([\s\S]*?)<\/content>/i)?.[1]??e).replace(/\r\n/g,`
|
|
1
|
+
import{r as e}from"./rolldown-runtime-hePW80VL.js";import{n as t}from"./runtime-fetch-WgH0pROs.js";import{Mn as n,ln as r}from"./persistence-BKszKaoQ.js";import{it as i,rt as a}from"./vendor-base-ui-CdzJ6My_.js";import{n as o}from"./platform-CgwxwfA4.js";import{v as s}from"./useUIStore-CEqU9Ujv.js";import{n as c}from"./vendor-react-BP2cRVqX.js";import{a as l,n as u}from"./vendor-aparajita-capacitor-secure-storage-xol_6BsB.js";import{f as d,r as f,t as p}from"./button-CWSs9Baz.js";import{n as m}from"./useThemeSystem-8jhE4Zyk.js";import{n as h}from"./Icon-BCFeHDWx.js";import{i as g,n as _,t as v}from"./tooltip-CV6dvLBp.js";import{a as y,i as b,o as x,t as S}from"./image-loading-Bj11k4gM.js";import{n as C}from"./MarkdownRenderer-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,`&`).replace(/</g,`<`).replace(/>/g,`>`).replace(/"/g,`"`).replace(/'/g,`'`),ye=e=>e?Object.entries(e).map(([e,t])=>t==null?``:`${e.startsWith(`--`)?e:e.replace(/[A-Z]/g,e=>`-${e.toLowerCase()}`)}:${typeof t==`number`?`${t}px`:t};`).join(``):``,K=(e,t,n)=>{let r=e.querySelector(`pre`);r&&(r.style.margin=`0`,r.style.background=`transparent`,r.style.whiteSpace=t?`pre-wrap`:`pre`,t&&(r.style.wordBreak=`break-word`,r.style.overflowWrap=`break-word`));let i=e.querySelector(`code`);if(i){let e=ye(n);e&&i.setAttribute(`style`,`${i.getAttribute(`style`)??``}${e}`)}},be=e=>`<pre><code>${ve(e)}</code></pre>`,xe=e=>{let t=(0,k.c)(21),{code:n,language:r,className:i,style:a,codeStyle:o,wrap:s}=e,c=s!==void 0&&s,{currentTheme:l}=m(),u=O.useRef(null),d;t[0]===l?d=t[1]:(d=ee(l),t[0]=l,t[1]=d);let p=d,h,g;t[2]!==n||t[3]!==o||t[4]!==c?(h=()=>{let e=u.current;e&&(e.innerHTML=be(n),K(e,c,o))},g=[n,c,o],t[2]=n,t[3]=o,t[4]=c,t[5]=h,t[6]=g):(h=t[5],g=t[6]),O.useLayoutEffect(h,g);let _,v;t[7]!==n||t[8]!==o||t[9]!==r||t[10]!==c?(_=()=>{let e=u.current;if(!e)return;let t=!0,i=new AbortController;return D(n,(r||`text`).toLowerCase(),{signal:i.signal,priority:`visible`}).then(n=>{!t||!e||!n||(e.innerHTML=n,K(e,c,o))}),()=>{t=!1,i.abort()}},v=[n,r,c,o],t[7]=n,t[8]=o,t[9]=r,t[10]=c,t[11]=_,t[12]=v):(_=t[11],v=t[12]),O.useEffect(_,v);let y;t[13]===i?y=t[14]:(y=f(`typography-code`,i),t[13]=i,t[14]=y);let b=p,x;t[15]!==a||t[16]!==b?(x={...b,...a},t[15]=a,t[16]=b,t[17]=x):x=t[17];let S;return t[18]!==y||t[19]!==x?(S=(0,A.jsx)(`div`,{ref:u,className:y,style:x}),t[18]=y,t[19]=x,t[20]=S):S=t[20],S},Se=e=>{let t=e.replace(/^<file>\s*\n?/,``).replace(/\n?<\/file>\s*$/,``);return t=t.replace(/^\s*\d{5}\|\s?/gm,``),t.trim()},Ce=(e,t=``)=>{if(typeof e==`string`)return e;if(e==null)return t;if(typeof e==`number`||typeof e==`boolean`||typeof e==`bigint`)return String(e);try{return JSON.stringify(e)}catch{return t}},we=e=>e?e.includes(`<diagnostics`)||e.includes(`<file_diagnostics>`)||e.includes(`LSP errors detected`)||e.includes(`This file has errors`):!1,Te=e=>e?e.replace(/\n{0,2}LSP errors detected[\s\S]*?<diagnostics[^>]*>[\s\S]*?<\/diagnostics>/g,``).replace(/\n{0,2}This file has errors[\s\S]*?<\/file_diagnostics>/g,``).replace(/<diagnostics[^>]*>[\s\S]*?<\/diagnostics>/g,``).replace(/<file_diagnostics>[\s\S]*?<\/file_diagnostics>/g,``).trim():``,Ee=(e,t)=>!e||typeof e!=`object`?String(e):E(e,t||``),q=e=>{if(typeof e==`string`){let t=e.trim();return t.length>0?t:void 0}if(e&&typeof e==`object`){let t=e.patch;if(typeof t==`string`){let e=t.trim();return e.length>0?e:void 0}}},De=e=>{if(!e||typeof e!=`object`)return;let t=q(e.patch)??q(e.diff);if(t)return t;let n=Array.isArray(e.files)?e.files:[];for(let e of n){if(!e||typeof e!=`object`)continue;let t=q(e.patch)??q(e.diff);if(t)return t}},Oe=e=>{if(!e||typeof e!=`string`)return{data:null,isJson:!1};let t=e.trim();if(!t.startsWith(`{`)&&!t.startsWith(`[`)||!t.endsWith(`}`)&&!t.endsWith(`]`)||t.length<2)return{data:null,isJson:!1};try{let e=JSON.parse(t);return typeof e==`object`&&e?{data:e,isJson:!0}:{data:null,isJson:!1}}catch{return{data:null,isJson:!1}}},ke=(e,t,n)=>{let r=Se(e);if((t===`edit`||t===`multiedit`||t===`write`||t===`apply_patch`)&&we(r)&&(r=Te(r)),(t===`edit`||t===`multiedit`||t===`apply_patch`)&&r.trim().length===0){let e=De(n);if(e)return e}return r},Ae=e=>{let t=e.match(/<type>(file|directory)<\/type>/i)?.[1]?.toLowerCase()??`unknown`,n=(e.match(/<content>([\s\S]*?)<\/content>/i)?.[1]??e).replace(/\r\n/g,`
|
|
2
2
|
`).split(`
|
|
3
3
|
`),r=e=>/\(\s*File has more lines\..*offset.*\)/i.test(e.trim());return{type:t,lines:n.map(e=>{let n=e.trim(),i=n.startsWith(`(`)&&n.endsWith(`)`)||r(n);if(t!==`directory`){let t=e.match(/^(\d+):\s?(.*)$/);if(t){let e=t[2],n=e.trim(),i=n.startsWith(`(`)&&n.endsWith(`)`)||r(n);return{lineNumber:i?null:Number(t[1]),text:e,isInfo:i}}}return{lineNumber:null,text:e,isInfo:i}}).filter((e,t,n)=>{if(e.text.trim().length>0)return!0;let r=n[t-1],i=n[t+1],a=!!(r?.isInfo||i?.isInfo),o=e.lineNumber!==null;return!(a&&o)})}},je=(e,t)=>{try{let n=e.trim().split(`
|
|
4
4
|
`).filter(Boolean);if(n.length===0)return null;let r=[];return n.forEach(e=>{let t=e.match(/^(\s*)(.+)$/);if(t){let[,e,n]=t,i=Math.floor(e.length/2),a=!n.endsWith(`/`);r.push({name:n.replace(/\/$/,``),depth:i,isFile:a})}}),(0,A.jsx)(`div`,{className:f(`w-full min-w-0 font-mono space-y-0.5`,t?.unstyled?null:`p-3 bg-muted/20 rounded-xl border border-border/30`),style:s.tool.popup,children:r.map((e,t)=>(0,A.jsx)(`div`,{className:`min-w-0`,style:{paddingLeft:`${e.depth*20}px`},children:e.isFile?(0,A.jsx)(`span`,{className:`text-foreground/90 block truncate`,children:e.name}):(0,A.jsxs)(`span`,{className:`font-semibold text-foreground block truncate`,children:[e.name,`/`]})},t))})}catch{return null}},Me={backgroundColor:`var(--status-info)`,opacity:.6},Ne=(e,t,n)=>{try{let r=e.trim().split(`
|
package/dist/index.html
CHANGED
|
@@ -552,7 +552,7 @@
|
|
|
552
552
|
opacity: 0.72;
|
|
553
553
|
}
|
|
554
554
|
</style>
|
|
555
|
-
<script type="module" crossorigin src="/assets/main-
|
|
555
|
+
<script type="module" crossorigin src="/assets/main-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-
|
|
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">
|
package/dist/mini-chat.html
CHANGED
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
}
|
|
75
75
|
</style>
|
|
76
76
|
|
|
77
|
-
<script type="module" crossorigin src="/assets/miniChat-
|
|
77
|
+
<script type="module" crossorigin src="/assets/miniChat-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-
|
|
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-
|
|
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-
|
|
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
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
|
-
|
|
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,
|