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

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 (46) hide show
  1. package/dist/assets/{AssistantView-CeP8V0X-.js → AssistantView-Bn-ie4vz.js} +3 -3
  2. package/dist/assets/{DiagramView-DRCsjVx9.js → DiagramView-tawWR3qT.js} +1 -1
  3. package/dist/assets/{MarkdownRenderer-BvKaN2KX.js → MarkdownRenderer-BtVmyJy4.js} +2 -2
  4. package/dist/assets/{MarkdownRendererImpl-D6iUJhtT.js → MarkdownRendererImpl-D2OCbzEp.js} +1 -1
  5. package/dist/assets/{MarkstreamRendererImpl-B14yVm7-.js → MarkstreamRendererImpl-wLbde29-.js} +1 -1
  6. package/dist/assets/{MobileDetailNavigation-e4vO0lXT.js → MobileDetailNavigation-DgCfvpng.js} +1 -1
  7. package/dist/assets/{MobileShareBridge-BnA02Mps.js → MobileShareBridge-D9esHh6J.js} +1 -1
  8. package/dist/assets/{MobileTabPageHeader-nApqJ-1X.js → MobileTabPageHeader-DzTewIJq.js} +1 -1
  9. package/dist/assets/{MultiRunWindow-C7Vgvpoz.js → MultiRunWindow-CrcHT1DW.js} +1 -1
  10. package/dist/assets/{SettingsView-Be7_mr8t.js → SettingsView-ChHvQPP-.js} +2 -2
  11. package/dist/assets/{SettingsWindow-ZnWK6Itb.js → SettingsWindow-BLmp9C90.js} +1 -1
  12. package/dist/assets/{TerminalView-BluEtXH8.js → TerminalView-D9TwFDxt.js} +1 -1
  13. package/dist/assets/{ToolOutputDialog-6gouT_GS.js → ToolOutputDialog-0S7fJLLw.js} +1 -1
  14. package/dist/assets/{appThemeRegistry-D59Upe3t.js → appThemeRegistry-C0lox0ME.js} +1 -1
  15. package/dist/assets/{fileMentionTouchSelection-DbICTM0g.js → fileMentionTouchSelection-CWtqJ0pV.js} +1 -1
  16. package/dist/assets/{gitApi-Bq2BWLAz.js → gitApi-D4E7MHPI.js} +1 -1
  17. package/dist/assets/{insertionBoundaries-n7dVxiuI.js → insertionBoundaries-fvEQLEE_.js} +1 -1
  18. package/dist/assets/{main-DRGmBqpD.js → main-6QL82M1U.js} +4 -4
  19. package/dist/assets/{main-BlZ-gMwg.js → main-DTaOcMED.js} +2 -2
  20. package/dist/assets/{mcpQueries-BxICKLst.js → mcpQueries-D-rsCnTq.js} +1 -1
  21. package/dist/assets/{miniChat-PxCQIMUm.js → miniChat-LDIwR98X.js} +2 -2
  22. package/dist/assets/{mobile-Bs5d4s3_.js → mobile-CTXp1iXI.js} +2 -2
  23. package/dist/assets/{multirun-BxUSBUb_.js → multirun-BV5qdnL3.js} +1 -1
  24. package/dist/assets/{projectMeta-dZJ-hOil.js → projectMeta-Cp7A0mPx.js} +1 -1
  25. package/dist/assets/{renderElectronMiniChatApp-CGWfxiDc.js → renderElectronMiniChatApp-DrhDj1Jh.js} +2 -2
  26. package/dist/assets/{renderMobileApp-DJmWaXgS.js → renderMobileApp-DrG8-J7P.js} +4 -4
  27. package/dist/assets/{runtimeConfig-B-2tboUz.js → runtimeConfig-B1lpYz9l.js} +1 -1
  28. package/dist/assets/{runtimeEndpointReset-BbJRibgL.js → runtimeEndpointReset-Dp74zZAo.js} +1 -1
  29. package/dist/assets/{sessionChangeSummary-DTeJLaNT.js → sessionChangeSummary-BH28eSaw.js} +1 -1
  30. package/dist/assets/{sessionLookup-_U9nCuBN.js → sessionLookup-Ca1342QR.js} +1 -1
  31. package/dist/assets/{speechSettingsQueries-CdlOc_sI.js → speechSettingsQueries-CG9u1h1Z.js} +1 -1
  32. package/dist/assets/src-CcT-GitE.css +2 -0
  33. package/dist/assets/{useAppFontEffects-YhyFj8z3.js → useAppFontEffects-Brve5y_3.js} +5 -5
  34. package/dist/assets/{useDesktopWindowControlsLayout-jdIXqoD8.js → useDesktopWindowControlsLayout-DpEWNKaQ.js} +1 -1
  35. package/dist/assets/{useEffectiveDirectory-COgvBW0Q.js → useEffectiveDirectory-CFr8k_MH.js} +1 -1
  36. package/dist/assets/useMobileNavigationStore-CSnG4CUz.js +417 -0
  37. package/dist/assets/{useWorkerHighlightedLines-DGH-O6ZJ.js → useWorkerHighlightedLines-uJlySvgF.js} +1 -1
  38. package/dist/index.html +3 -3
  39. package/dist/mini-chat.html +3 -3
  40. package/dist/mobile.html +3 -3
  41. package/package.json +1 -1
  42. package/server/lib/session-index/DOCUMENTATION.md +23 -0
  43. package/server/lib/session-index/sync-runtime.js +35 -14
  44. package/server/lib/session-index/sync-runtime.test.js +125 -0
  45. package/dist/assets/src-CdGit3gf.css +0 -2
  46. package/dist/assets/useMobileNavigationStore-CuOiHjP1.js +0 -417
@@ -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-BtVmyJy4.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-DTaOcMED.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,8 +572,8 @@
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">
576
- <link rel="stylesheet" crossorigin href="/assets/src-CdGit3gf.css">
575
+ <link rel="modulepreload" crossorigin href="/assets/runtimeConfig-B1lpYz9l.js">
576
+ <link rel="stylesheet" crossorigin href="/assets/src-CcT-GitE.css">
577
577
  </head>
578
578
  <body class="h-full bg-background text-foreground">
579
579
  <!-- Keep the startup layer outside #root. React replaces every child of
@@ -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-LDIwR98X.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,8 +94,8 @@
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">
98
- <link rel="stylesheet" crossorigin href="/assets/src-CdGit3gf.css">
97
+ <link rel="modulepreload" crossorigin href="/assets/runtimeConfig-B1lpYz9l.js">
98
+ <link rel="stylesheet" crossorigin href="/assets/src-CcT-GitE.css">
99
99
  </head>
100
100
  <body class="h-full bg-background text-foreground">
101
101
  <div id="root" class="h-full">
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-CTXp1iXI.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,8 +83,8 @@
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">
87
- <link rel="stylesheet" crossorigin href="/assets/src-CdGit3gf.css">
86
+ <link rel="modulepreload" crossorigin href="/assets/runtimeConfig-B1lpYz9l.js">
87
+ <link rel="stylesheet" crossorigin href="/assets/src-CcT-GitE.css">
88
88
  </head>
89
89
  <body class="h-full bg-background text-foreground">
90
90
  <div id="root" class="h-full"></div>
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.4",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "./server/index.js",
@@ -121,6 +121,29 @@ 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
+ Only a `publishChange()` broadcast carries `cause: 'change'`; it replaces any
131
+ pending progress tip and the event-stream sink forwards it unchanged. Progress,
132
+ enqueue, idle and stop tips omit cause, so the marker cannot leak into later tips.
133
+ Stop publishes idle with interrupted directories marked failed; late worker
134
+ cleanup cannot emit another revision or leave a pending tip timer. Internal
135
+ revision increments and `waitForChange` delivery remain immediate, including the
136
+ final state. Full snapshots are materialized for waiting readers, not for each
137
+ tip. No persisted revision, failure, or runtime-isolation semantics change.
138
+
139
+ The shared renderer independently throttles busy tips to a fixed 1000ms window
140
+ for compatibility with older Hosts. Idle/legacy tips use 100ms debounce and
141
+ shorten a pending busy deadline without extending it. Stream-ready and safety
142
+ timeout repair retain their existing behavior; GET remains authoritative.
143
+ `cause: 'change'` also selects that 100ms/earlier-deadline path while busy, keeping
144
+ external writes responsive. Unknown causes are ignored by the client parser;
145
+ Hosts without cause retain the existing progress-throttling behavior.
146
+
124
147
  The OpenCode proxy calls `noteInteractiveRequest()` for selected-session reads
125
148
  and mutations. That aborts the current background list, yields for one second,
126
149
  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,19 +115,20 @@ 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 = (cause) => {
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(),
131
+ ...(cause === 'change' ? { cause } : {}),
129
132
  });
130
133
  } catch (error) {
131
134
  console.warn('[session-index] onRevisionTip failed:', error);
@@ -133,6 +136,21 @@ export const createSessionIndexSyncRuntime = ({
133
136
  }
134
137
  };
135
138
 
139
+ const notify = (immediate = false, cause) => {
140
+ if (stopped && !immediate) return;
141
+ revision += 1;
142
+ if (waiters.size > 0) {
143
+ const value = snapshot();
144
+ for (const resolve of [...waiters]) resolve(value);
145
+ }
146
+ const sync = syncState();
147
+ if (immediate || (!sync.active && !sync.enriching)) {
148
+ broadcastRevisionTip(cause);
149
+ } else if (revisionTipTimer === undefined && typeof onRevisionTip === 'function') {
150
+ revisionTipTimer = setTimer(broadcastRevisionTip, REVISION_TIP_THROTTLE_MS);
151
+ }
152
+ };
153
+
136
154
  const delay = (ms, signal) => new Promise((resolve) => {
137
155
  if (ms <= 0 || signal?.aborted) {
138
156
  resolve();
@@ -414,6 +432,7 @@ export const createSessionIndexSyncRuntime = ({
414
432
  };
415
433
 
416
434
  const enqueue = (directories) => {
435
+ if (stopped) return snapshot();
417
436
  const nextRuntimeKey = sessionIndexService.getRuntimeKey();
418
437
  if (nextRuntimeKey !== runtimeKey) {
419
438
  runtimeKey = nextRuntimeKey;
@@ -427,7 +446,8 @@ export const createSessionIndexSyncRuntime = ({
427
446
  failedDirectories.clear();
428
447
  batchDirectories = new Set();
429
448
  }
430
- if (!running && queue.length === 0) {
449
+ const newBatch = !running && queue.length === 0;
450
+ if (newBatch) {
431
451
  completedDirectories.clear();
432
452
  failedDirectories.clear();
433
453
  batchDirectories = new Set();
@@ -443,7 +463,7 @@ export const createSessionIndexSyncRuntime = ({
443
463
  currentWasPreempted = true;
444
464
  currentController.abort();
445
465
  }
446
- notify();
466
+ notify(newBatch);
447
467
  void run();
448
468
  return snapshot();
449
469
  };
@@ -458,7 +478,7 @@ export const createSessionIndexSyncRuntime = ({
458
478
 
459
479
  const publishChange = () => {
460
480
  if (stopped) return;
461
- notify();
481
+ notify(true, 'change');
462
482
  };
463
483
 
464
484
  const waitForChange = (since, { signal, timeoutMs = LONG_POLL_MAX_MS } = {}) => {
@@ -481,6 +501,7 @@ export const createSessionIndexSyncRuntime = ({
481
501
  };
482
502
 
483
503
  const stop = () => {
504
+ if (stopped) return;
484
505
  stopped = true;
485
506
  currentController?.abort();
486
507
  const interruptedDirectories = [...queuedDirectories];
@@ -489,7 +510,7 @@ export const createSessionIndexSyncRuntime = ({
489
510
  queuedDirectories.clear();
490
511
  queuedChildFlagKeys.clear();
491
512
  for (const directory of interruptedDirectories) failedDirectories.add(directory);
492
- notify();
513
+ notify(true);
493
514
  };
494
515
 
495
516
  return { enqueue, noteInteractiveRequest, publishChange, snapshot, waitForChange, stop };
@@ -41,6 +41,131 @@ 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
+ for (const [tip] of tips.mock.calls) expect(tip).not.toHaveProperty('cause');
86
+ expect(tips.mock.lastCall[0]).toMatchObject({ revision: runtime.snapshot().revision, sync: { active: false, enriching: false } });
87
+ expect((await final).revision).toBe(runtime.snapshot().revision);
88
+ expect(vi.getTimerCount()).toBe(0);
89
+ await vi.advanceTimersByTimeAsync(500);
90
+ expect(tips).toHaveBeenCalledTimes(8);
91
+ runtime.stop();
92
+ } finally {
93
+ vi.useRealTimers();
94
+ }
95
+ });
96
+
97
+ it('publishes external changes immediately while busy and stop cancels the pending tip', async () => {
98
+ vi.useFakeTimers();
99
+ try {
100
+ const tips = vi.fn();
101
+ let releaseReady;
102
+ const runtime = createSessionIndexSyncRuntime({
103
+ sessionIndexService: createService(),
104
+ onRevisionTip: tips,
105
+ waitForOpenCodeReady: () => new Promise((resolve) => { releaseReady = resolve; }),
106
+ setTimer: setTimeout,
107
+ clearTimer: clearTimeout,
108
+ });
109
+ runtime.enqueue(['/repo']);
110
+ expect(vi.getTimerCount()).toBe(1);
111
+ expect(tips.mock.lastCall[0]).not.toHaveProperty('cause');
112
+ runtime.publishChange();
113
+ expect(tips).toHaveBeenCalledTimes(2);
114
+ expect(tips.mock.lastCall[0]).toMatchObject({ cause: 'change', sync: { active: true } });
115
+ expect(tips.mock.lastCall[0].revision).toBe(runtime.snapshot().revision);
116
+ expect(vi.getTimerCount()).toBe(0);
117
+ runtime.enqueue(['/repo/second']);
118
+ expect(vi.getTimerCount()).toBe(1);
119
+ const final = runtime.waitForChange(runtime.snapshot().revision);
120
+ runtime.stop();
121
+ const stoppedRevision = runtime.snapshot().revision;
122
+ expect(tips).toHaveBeenCalledTimes(3);
123
+ expect(tips.mock.lastCall[0]).toMatchObject({ revision: stoppedRevision, sync: { active: false, enriching: false } });
124
+ expect(tips.mock.lastCall[0]).not.toHaveProperty('cause');
125
+ await expect(final).resolves.toMatchObject({ revision: stoppedRevision, sync: { active: false, failedDirectories: ['/repo', '/repo/second'] } });
126
+ expect(vi.getTimerCount()).toBe(0);
127
+ releaseReady(true);
128
+ await vi.advanceTimersByTimeAsync(1000);
129
+ expect(runtime.snapshot().revision).toBe(stoppedRevision);
130
+ expect(tips).toHaveBeenCalledTimes(3);
131
+ } finally {
132
+ vi.useRealTimers();
133
+ }
134
+ });
135
+
136
+ it('stopping during enrichment flushes idle and fences late worker notifications', async () => {
137
+ vi.useFakeTimers();
138
+ try {
139
+ const tips = vi.fn();
140
+ const runtime = createSessionIndexSyncRuntime({
141
+ sessionIndexService: createService(),
142
+ buildOpenCodeUrl: (route) => `http://opencode.test${route}`,
143
+ getOpenCodeAuthHeaders: () => ({}),
144
+ onRevisionTip: tips,
145
+ fetchFn: async (url, { signal }) => {
146
+ if (!url.searchParams.has('parentID')) return new Response(JSON.stringify([session('root', 1)]));
147
+ return new Promise((_resolve, reject) => {
148
+ signal.addEventListener('abort', () => reject(new DOMException('aborted', 'AbortError')), { once: true });
149
+ });
150
+ },
151
+ });
152
+ runtime.enqueue(['/repo']);
153
+ await vi.advanceTimersByTimeAsync(0);
154
+ expect(runtime.snapshot().sync.enriching).toBe(true);
155
+ const final = runtime.waitForChange(runtime.snapshot().revision);
156
+ runtime.stop();
157
+ const revision = runtime.snapshot().revision;
158
+ await expect(final).resolves.toMatchObject({ revision, sync: { active: false, enriching: false } });
159
+ await vi.advanceTimersByTimeAsync(1000);
160
+ expect(tips).toHaveBeenCalledTimes(2);
161
+ expect(tips.mock.lastCall[0]).toMatchObject({ revision, sync: { active: false, enriching: false } });
162
+ expect(runtime.snapshot().revision).toBe(revision);
163
+ expect(vi.getTimerCount()).toBe(0);
164
+ } finally {
165
+ vi.useRealTimers();
166
+ }
167
+ });
168
+
44
169
  it.each([1, 20, 21])('only advertises more when terminal pages contain over 20 visible roots (%i roots)', async (rootCount) => {
45
170
  const service = createService();
46
171
  const rows = Array.from({ length: Math.max(20, rootCount) }, (_, i) => ({