@openchambery/web 2.1.9-beta.2 → 2.1.9-beta.3

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 (43) hide show
  1. package/dist/assets/{AssistantView-CeP8V0X-.js → AssistantView-3Lz_SADb.js} +3 -3
  2. package/dist/assets/{DiagramView-DRCsjVx9.js → DiagramView-BZP5Aj4S.js} +1 -1
  3. package/dist/assets/{MarkdownRenderer-BvKaN2KX.js → MarkdownRenderer-DpB0hJ_E.js} +2 -2
  4. package/dist/assets/{MarkdownRendererImpl-D6iUJhtT.js → MarkdownRendererImpl-Hg1bbRlM.js} +1 -1
  5. package/dist/assets/{MarkstreamRendererImpl-B14yVm7-.js → MarkstreamRendererImpl-BfUZU3WC.js} +1 -1
  6. package/dist/assets/{MobileDetailNavigation-e4vO0lXT.js → MobileDetailNavigation-DqGKTFzT.js} +1 -1
  7. package/dist/assets/{MobileShareBridge-BnA02Mps.js → MobileShareBridge-DHHw00TT.js} +1 -1
  8. package/dist/assets/{MobileTabPageHeader-nApqJ-1X.js → MobileTabPageHeader-jFeAz_Z2.js} +1 -1
  9. package/dist/assets/{MultiRunWindow-C7Vgvpoz.js → MultiRunWindow-C92A1Qkc.js} +1 -1
  10. package/dist/assets/{SettingsView-Be7_mr8t.js → SettingsView-C2-Tug3M.js} +1 -1
  11. package/dist/assets/{SettingsWindow-ZnWK6Itb.js → SettingsWindow-D_j-lLiY.js} +1 -1
  12. package/dist/assets/{TerminalView-BluEtXH8.js → TerminalView-Veaa8xms.js} +1 -1
  13. package/dist/assets/{ToolOutputDialog-6gouT_GS.js → ToolOutputDialog-CI32t1nx.js} +1 -1
  14. package/dist/assets/{appThemeRegistry-D59Upe3t.js → appThemeRegistry-BhVqv80m.js} +1 -1
  15. package/dist/assets/{fileMentionTouchSelection-DbICTM0g.js → fileMentionTouchSelection-ytHOa8vD.js} +1 -1
  16. package/dist/assets/{gitApi-Bq2BWLAz.js → gitApi-DZSpp5_u.js} +1 -1
  17. package/dist/assets/{insertionBoundaries-n7dVxiuI.js → insertionBoundaries-B3F2vgJn.js} +1 -1
  18. package/dist/assets/{main-BlZ-gMwg.js → main-HsqXFi70.js} +2 -2
  19. package/dist/assets/{main-DRGmBqpD.js → main-mdIF-V3Y.js} +4 -4
  20. package/dist/assets/{mcpQueries-BxICKLst.js → mcpQueries-BAU9FYFF.js} +1 -1
  21. package/dist/assets/{miniChat-PxCQIMUm.js → miniChat-B7C-IBFC.js} +2 -2
  22. package/dist/assets/{mobile-Bs5d4s3_.js → mobile-CqWR9PBo.js} +2 -2
  23. package/dist/assets/{multirun-BxUSBUb_.js → multirun-D3iS9HjC.js} +1 -1
  24. package/dist/assets/{projectMeta-dZJ-hOil.js → projectMeta-DlHKE_8C.js} +1 -1
  25. package/dist/assets/{renderElectronMiniChatApp-CGWfxiDc.js → renderElectronMiniChatApp-CVT0WyUz.js} +2 -2
  26. package/dist/assets/{renderMobileApp-DJmWaXgS.js → renderMobileApp-hUJNFMKe.js} +4 -4
  27. package/dist/assets/{runtimeConfig-B-2tboUz.js → runtimeConfig-DAxmpV5p.js} +1 -1
  28. package/dist/assets/{runtimeEndpointReset-BbJRibgL.js → runtimeEndpointReset-BCf2ORY3.js} +1 -1
  29. package/dist/assets/{sessionChangeSummary-DTeJLaNT.js → sessionChangeSummary-BYoAdclB.js} +1 -1
  30. package/dist/assets/{sessionLookup-_U9nCuBN.js → sessionLookup-DxWtYNX-.js} +1 -1
  31. package/dist/assets/{speechSettingsQueries-CdlOc_sI.js → speechSettingsQueries-CBrLvvue.js} +1 -1
  32. package/dist/assets/{useAppFontEffects-YhyFj8z3.js → useAppFontEffects-BvJWL9Tq.js} +5 -5
  33. package/dist/assets/{useDesktopWindowControlsLayout-jdIXqoD8.js → useDesktopWindowControlsLayout-6B2M3hOM.js} +1 -1
  34. package/dist/assets/{useEffectiveDirectory-COgvBW0Q.js → useEffectiveDirectory-fANUMttq.js} +1 -1
  35. package/dist/assets/{useMobileNavigationStore-CuOiHjP1.js → useMobileNavigationStore-DRpR2Oyd.js} +10 -10
  36. package/dist/assets/{useWorkerHighlightedLines-DGH-O6ZJ.js → useWorkerHighlightedLines-D1io7Q4t.js} +1 -1
  37. package/dist/index.html +2 -2
  38. package/dist/mini-chat.html +2 -2
  39. package/dist/mobile.html +2 -2
  40. package/package.json +1 -1
  41. package/server/lib/session-index/DOCUMENTATION.md +17 -0
  42. package/server/lib/session-index/sync-runtime.js +34 -14
  43. package/server/lib/session-index/sync-runtime.test.js +121 -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-BvKaN2KX.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-DpB0hJ_E.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-BlZ-gMwg.js"></script>
555
+ <script type="module" crossorigin src="/assets/main-HsqXFi70.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-B-2tboUz.js">
575
+ <link rel="modulepreload" crossorigin href="/assets/runtimeConfig-DAxmpV5p.js">
576
576
  <link rel="stylesheet" crossorigin href="/assets/src-CdGit3gf.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-PxCQIMUm.js"></script>
77
+ <script type="module" crossorigin src="/assets/miniChat-B7C-IBFC.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-B-2tboUz.js">
97
+ <link rel="modulepreload" crossorigin href="/assets/runtimeConfig-DAxmpV5p.js">
98
98
  <link rel="stylesheet" crossorigin href="/assets/src-CdGit3gf.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-Bs5d4s3_.js"></script>
66
+ <script type="module" crossorigin src="/assets/mobile-CqWR9PBo.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-B-2tboUz.js">
86
+ <link rel="modulepreload" crossorigin href="/assets/runtimeConfig-DAxmpV5p.js">
87
87
  <link rel="stylesheet" crossorigin href="/assets/src-CdGit3gf.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.2",
3
+ "version": "2.1.9-beta.3",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "./server/index.js",
@@ -121,6 +121,23 @@ observer active after startup refresh work becomes idle, and successful
121
121
  event-driven index writes with a semantic snapshot change publish a new
122
122
  revision tip immediately.
123
123
 
124
+ Background directory and child-enrichment progress share a fixed 500ms trailing
125
+ tip window: the first pending change arms the injected timer, later changes do
126
+ not reset it, and delivery reads the latest revision and sync flags. New-batch
127
+ enqueue, idle (`active=false` and `enriching=false`), `publishChange()` (including
128
+ pin/rename and event-driven writes), and stop bypass the window and cancel its
129
+ pending delivery. These immediate edges are exceptions to the 500ms rate bound.
130
+ Stop publishes idle with interrupted directories marked failed; late worker
131
+ cleanup cannot emit another revision or leave a pending tip timer. Internal
132
+ revision increments and `waitForChange` delivery remain immediate, including the
133
+ final state. Full snapshots are materialized for waiting readers, not for each
134
+ tip. No persisted revision, failure, or runtime-isolation semantics change.
135
+
136
+ The shared renderer independently throttles busy tips to a fixed 1000ms window
137
+ for compatibility with older Hosts. Idle/legacy tips use 100ms debounce and
138
+ shorten a pending busy deadline without extending it. Stream-ready and safety
139
+ timeout repair retain their existing behavior; GET remains authoritative.
140
+
124
141
  The OpenCode proxy calls `noteInteractiveRequest()` for selected-session reads
125
142
  and mutations. That aborts the current background list, yields for one second,
126
143
  then resumes the same directory. Consequently a startup index refresh cannot
@@ -7,6 +7,7 @@ const FULL_RECONCILE_INTERVAL_MS = 24 * 60 * 60 * 1000;
7
7
  const FETCH_TIMEOUT_MS = 6_000;
8
8
  const INTERACTIVE_YIELD_MS = 1_000;
9
9
  const LONG_POLL_MAX_MS = 25_000;
10
+ const REVISION_TIP_THROTTLE_MS = 500;
10
11
 
11
12
  const normalizeDirectory = (value) => {
12
13
  if (typeof value !== 'string') return null;
@@ -64,7 +65,7 @@ export const createSessionIndexSyncRuntime = ({
64
65
  buildOpenCodeUrl,
65
66
  getOpenCodeAuthHeaders,
66
67
  waitForOpenCodeReady,
67
- /** Optional tip sink: ({ revision, sync }) after each revision bump. */
68
+ /** Optional tip sink: ({ revision, sync }); background progress is coalesced. */
68
69
  onRevisionTip = null,
69
70
  /**
70
71
  * Optional Host authority projection applied to each upstream list page
@@ -96,10 +97,11 @@ export const createSessionIndexSyncRuntime = ({
96
97
  const failedDirectories = new Set();
97
98
  const waiters = new Set();
98
99
  let batchDirectories = new Set();
100
+ let revisionTipTimer;
99
101
 
100
102
  const syncState = () => ({
101
- active: running || queue.length > 0,
102
- enriching: enrichingChildFlags || childFlagQueue.length > 0,
103
+ active: !stopped && (running || queue.length > 0),
104
+ enriching: !stopped && (enrichingChildFlags || childFlagQueue.length > 0),
103
105
  completed: completedDirectories.size,
104
106
  total: batchDirectories.size,
105
107
  pendingDirectories: [...queuedDirectories],
@@ -113,17 +115,17 @@ export const createSessionIndexSyncRuntime = ({
113
115
  ...sessionIndexService.snapshot(),
114
116
  });
115
117
 
116
- const notify = () => {
117
- revision += 1;
118
- const value = snapshot();
119
- for (const resolve of [...waiters]) resolve(value);
118
+ const broadcastRevisionTip = () => {
119
+ if (revisionTipTimer !== undefined) clearTimer(revisionTipTimer);
120
+ revisionTipTimer = undefined;
121
+ const sync = syncState();
120
122
  if (typeof onRevisionTip === 'function') {
121
123
  try {
122
124
  onRevisionTip({
123
- revision: value.revision,
125
+ revision,
124
126
  sync: {
125
- active: value.sync.active === true,
126
- enriching: value.sync.enriching === true,
127
+ active: sync.active,
128
+ enriching: sync.enriching,
127
129
  },
128
130
  occurredAt: now(),
129
131
  });
@@ -133,6 +135,21 @@ export const createSessionIndexSyncRuntime = ({
133
135
  }
134
136
  };
135
137
 
138
+ const notify = (immediate = false) => {
139
+ if (stopped && !immediate) return;
140
+ revision += 1;
141
+ if (waiters.size > 0) {
142
+ const value = snapshot();
143
+ for (const resolve of [...waiters]) resolve(value);
144
+ }
145
+ const sync = syncState();
146
+ if (immediate || (!sync.active && !sync.enriching)) {
147
+ broadcastRevisionTip();
148
+ } else if (revisionTipTimer === undefined && typeof onRevisionTip === 'function') {
149
+ revisionTipTimer = setTimer(broadcastRevisionTip, REVISION_TIP_THROTTLE_MS);
150
+ }
151
+ };
152
+
136
153
  const delay = (ms, signal) => new Promise((resolve) => {
137
154
  if (ms <= 0 || signal?.aborted) {
138
155
  resolve();
@@ -414,6 +431,7 @@ export const createSessionIndexSyncRuntime = ({
414
431
  };
415
432
 
416
433
  const enqueue = (directories) => {
434
+ if (stopped) return snapshot();
417
435
  const nextRuntimeKey = sessionIndexService.getRuntimeKey();
418
436
  if (nextRuntimeKey !== runtimeKey) {
419
437
  runtimeKey = nextRuntimeKey;
@@ -427,7 +445,8 @@ export const createSessionIndexSyncRuntime = ({
427
445
  failedDirectories.clear();
428
446
  batchDirectories = new Set();
429
447
  }
430
- if (!running && queue.length === 0) {
448
+ const newBatch = !running && queue.length === 0;
449
+ if (newBatch) {
431
450
  completedDirectories.clear();
432
451
  failedDirectories.clear();
433
452
  batchDirectories = new Set();
@@ -443,7 +462,7 @@ export const createSessionIndexSyncRuntime = ({
443
462
  currentWasPreempted = true;
444
463
  currentController.abort();
445
464
  }
446
- notify();
465
+ notify(newBatch);
447
466
  void run();
448
467
  return snapshot();
449
468
  };
@@ -458,7 +477,7 @@ export const createSessionIndexSyncRuntime = ({
458
477
 
459
478
  const publishChange = () => {
460
479
  if (stopped) return;
461
- notify();
480
+ notify(true);
462
481
  };
463
482
 
464
483
  const waitForChange = (since, { signal, timeoutMs = LONG_POLL_MAX_MS } = {}) => {
@@ -481,6 +500,7 @@ export const createSessionIndexSyncRuntime = ({
481
500
  };
482
501
 
483
502
  const stop = () => {
503
+ if (stopped) return;
484
504
  stopped = true;
485
505
  currentController?.abort();
486
506
  const interruptedDirectories = [...queuedDirectories];
@@ -489,7 +509,7 @@ export const createSessionIndexSyncRuntime = ({
489
509
  queuedDirectories.clear();
490
510
  queuedChildFlagKeys.clear();
491
511
  for (const directory of interruptedDirectories) failedDirectories.add(directory);
492
- notify();
512
+ notify(true);
493
513
  };
494
514
 
495
515
  return { enqueue, noteInteractiveRequest, publishChange, snapshot, waitForChange, stop };
@@ -41,6 +41,127 @@ const isFullyIdle = (runtime) => (
41
41
  );
42
42
 
43
43
  describe('session index background sync runtime', () => {
44
+ it('coalesces hundreds of directory/child checks and immediately publishes the final revision', async () => {
45
+ vi.useFakeTimers();
46
+ try {
47
+ const service = createService();
48
+ const revisionsAtDelivery = [];
49
+ const tips = vi.fn(() => { revisionsAtDelivery.push(runtime.snapshot().revision); });
50
+ const runtime = createSessionIndexSyncRuntime({
51
+ sessionIndexService: service,
52
+ buildOpenCodeUrl: (route) => `http://opencode.test${route}`,
53
+ getOpenCodeAuthHeaders: () => ({}),
54
+ onRevisionTip: tips,
55
+ setTimer: setTimeout,
56
+ clearTimer: clearTimeout,
57
+ fetchFn: async (url) => {
58
+ await new Promise((resolve) => setTimeout(resolve, 10));
59
+ const directory = url.searchParams.get('directory');
60
+ return new Response(JSON.stringify({ data: url.searchParams.has('parentID') ? []
61
+ : Array.from({ length: 20 }, (_, i) => session(`${directory}-${i}`, 100 - i, directory)) }));
62
+ },
63
+ });
64
+ runtime.enqueue(Array.from({ length: 15 }, (_, i) => `/repo/${i}`));
65
+ expect(tips).toHaveBeenCalledTimes(1);
66
+ expect(tips.mock.calls[0][0]).toMatchObject({ revision: 1, sync: { active: true } });
67
+ await vi.advanceTimersByTimeAsync(499);
68
+ expect(tips).toHaveBeenCalledTimes(1);
69
+ await vi.advanceTimersByTimeAsync(1);
70
+ expect(tips).toHaveBeenCalledTimes(2);
71
+ const progress = runtime.waitForChange(runtime.snapshot().revision);
72
+ await vi.advanceTimersByTimeAsync(10);
73
+ expect((await progress).revision).toBe(runtime.snapshot().revision);
74
+ const final = (async () => {
75
+ let value = runtime.snapshot();
76
+ while (value.sync.active || value.sync.enriching) value = await runtime.waitForChange(value.revision);
77
+ return value;
78
+ })();
79
+ await vi.advanceTimersByTimeAsync(2640);
80
+ expect(service.replaceDirectory).toHaveBeenCalledTimes(15);
81
+ expect(service.replaceChildSessions).toHaveBeenCalledTimes(300);
82
+ expect(isFullyIdle(runtime)).toBe(true);
83
+ expect(tips).toHaveBeenCalledTimes(8);
84
+ expect(tips.mock.calls.map(([tip]) => tip.revision)).toEqual(revisionsAtDelivery);
85
+ expect(tips.mock.lastCall[0]).toMatchObject({ revision: runtime.snapshot().revision, sync: { active: false, enriching: false } });
86
+ expect((await final).revision).toBe(runtime.snapshot().revision);
87
+ expect(vi.getTimerCount()).toBe(0);
88
+ await vi.advanceTimersByTimeAsync(500);
89
+ expect(tips).toHaveBeenCalledTimes(8);
90
+ runtime.stop();
91
+ } finally {
92
+ vi.useRealTimers();
93
+ }
94
+ });
95
+
96
+ it('publishes external changes immediately while busy and stop cancels the pending tip', async () => {
97
+ vi.useFakeTimers();
98
+ try {
99
+ const tips = vi.fn();
100
+ let releaseReady;
101
+ const runtime = createSessionIndexSyncRuntime({
102
+ sessionIndexService: createService(),
103
+ onRevisionTip: tips,
104
+ waitForOpenCodeReady: () => new Promise((resolve) => { releaseReady = resolve; }),
105
+ setTimer: setTimeout,
106
+ clearTimer: clearTimeout,
107
+ });
108
+ runtime.enqueue(['/repo']);
109
+ expect(vi.getTimerCount()).toBe(1);
110
+ runtime.publishChange();
111
+ expect(tips).toHaveBeenCalledTimes(2);
112
+ expect(tips.mock.lastCall[0].revision).toBe(runtime.snapshot().revision);
113
+ expect(vi.getTimerCount()).toBe(0);
114
+ runtime.enqueue(['/repo/second']);
115
+ expect(vi.getTimerCount()).toBe(1);
116
+ const final = runtime.waitForChange(runtime.snapshot().revision);
117
+ runtime.stop();
118
+ const stoppedRevision = runtime.snapshot().revision;
119
+ expect(tips).toHaveBeenCalledTimes(3);
120
+ expect(tips.mock.lastCall[0]).toMatchObject({ revision: stoppedRevision, sync: { active: false, enriching: false } });
121
+ await expect(final).resolves.toMatchObject({ revision: stoppedRevision, sync: { active: false, failedDirectories: ['/repo', '/repo/second'] } });
122
+ expect(vi.getTimerCount()).toBe(0);
123
+ releaseReady(true);
124
+ await vi.advanceTimersByTimeAsync(1000);
125
+ expect(runtime.snapshot().revision).toBe(stoppedRevision);
126
+ expect(tips).toHaveBeenCalledTimes(3);
127
+ } finally {
128
+ vi.useRealTimers();
129
+ }
130
+ });
131
+
132
+ it('stopping during enrichment flushes idle and fences late worker notifications', async () => {
133
+ vi.useFakeTimers();
134
+ try {
135
+ const tips = vi.fn();
136
+ const runtime = createSessionIndexSyncRuntime({
137
+ sessionIndexService: createService(),
138
+ buildOpenCodeUrl: (route) => `http://opencode.test${route}`,
139
+ getOpenCodeAuthHeaders: () => ({}),
140
+ onRevisionTip: tips,
141
+ fetchFn: async (url, { signal }) => {
142
+ if (!url.searchParams.has('parentID')) return new Response(JSON.stringify([session('root', 1)]));
143
+ return new Promise((_resolve, reject) => {
144
+ signal.addEventListener('abort', () => reject(new DOMException('aborted', 'AbortError')), { once: true });
145
+ });
146
+ },
147
+ });
148
+ runtime.enqueue(['/repo']);
149
+ await vi.advanceTimersByTimeAsync(0);
150
+ expect(runtime.snapshot().sync.enriching).toBe(true);
151
+ const final = runtime.waitForChange(runtime.snapshot().revision);
152
+ runtime.stop();
153
+ const revision = runtime.snapshot().revision;
154
+ await expect(final).resolves.toMatchObject({ revision, sync: { active: false, enriching: false } });
155
+ await vi.advanceTimersByTimeAsync(1000);
156
+ expect(tips).toHaveBeenCalledTimes(2);
157
+ expect(tips.mock.lastCall[0]).toMatchObject({ revision, sync: { active: false, enriching: false } });
158
+ expect(runtime.snapshot().revision).toBe(revision);
159
+ expect(vi.getTimerCount()).toBe(0);
160
+ } finally {
161
+ vi.useRealTimers();
162
+ }
163
+ });
164
+
44
165
  it.each([1, 20, 21])('only advertises more when terminal pages contain over 20 visible roots (%i roots)', async (rootCount) => {
45
166
  const service = createService();
46
167
  const rows = Array.from({ length: Math.max(20, rootCount) }, (_, i) => ({