@yuriai/web 0.1.2-beta.2 → 0.1.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 (66) hide show
  1. package/dist/assets/{AssistantView-CdO4imc6.js → AssistantView-BJyGm9Js.js} +3 -3
  2. package/dist/assets/{DiagramView-ALA0pyKm.js → DiagramView-BRK8rzVX.js} +1 -1
  3. package/dist/assets/{ImageLoadingPlaceholder-BPp8BN0a.js → ImageLoadingPlaceholder-CGHGcrEo.js} +1 -1
  4. package/dist/assets/{LoadingImage-DvtPqFRw.js → LoadingImage-BG9q4Tm_.js} +1 -1
  5. package/dist/assets/MarkdownRenderer-D4Ba9SUF.js +2 -0
  6. package/dist/assets/{MarkdownRendererImpl-XEO441ER.js → MarkdownRendererImpl-Dt0t56Yx.js} +1 -1
  7. package/dist/assets/{MarkstreamRendererImpl-C_-nyQrd.js → MarkstreamRendererImpl-rGcpWsHj.js} +1 -1
  8. package/dist/assets/{MobileDetailNavigation-nJl1j85R.js → MobileDetailNavigation-Cyp_3Hha.js} +1 -1
  9. package/dist/assets/{MobileShareBridge-DZ-a1-hd.js → MobileShareBridge-BQnwoSB3.js} +1 -1
  10. package/dist/assets/MobileTabPageHeader-CDFE4Ws4.js +2 -0
  11. package/dist/assets/{MultiRunWindow-D5HKvioh.js → MultiRunWindow-rggd5jvJ.js} +1 -1
  12. package/dist/assets/{SettingsView-ulmuNYxi.js → SettingsView-DWeiBwOG.js} +10 -10
  13. package/dist/assets/{SettingsWindow-C6iznJxw.js → SettingsWindow-1csE2lF6.js} +1 -1
  14. package/dist/assets/{TerminalView-CWZCDRxs.js → TerminalView-mF-mbAzo.js} +1 -1
  15. package/dist/assets/{ToolOutputDialog-DAwoBF6c.js → ToolOutputDialog-BvgdBVst.js} +1 -1
  16. package/dist/assets/{appThemeRegistry-Cxk6QddU.js → appThemeRegistry-CiQ61T5d.js} +1 -1
  17. package/dist/assets/{capgoAdapter-KNLHDj7G.js → capgoAdapter-DLi2tcmI.js} +1 -1
  18. package/dist/assets/{fileMentionTouchSelection-COw28yPf.js → fileMentionTouchSelection-C9jpvVUn.js} +1 -1
  19. package/dist/assets/{gitApi-D0t8KXsn.js → gitApi-nArpJO6q.js} +1 -1
  20. package/dist/assets/{image-loading-CVq5GLEY.js → image-loading-23c-W-bq.js} +1 -1
  21. package/dist/assets/{insertionBoundaries-UMFkRmkG.js → insertionBoundaries-BgHgsc1p.js} +1 -1
  22. package/dist/assets/{main-DZSusaPd.js → main-D5JkjH8k.js} +22 -22
  23. package/dist/assets/{main-B_h17HmQ.js → main-R2wbcT-3.js} +2 -2
  24. package/dist/assets/{mcpQueries-CCU0buGG.js → mcpQueries-W-LFzCOw.js} +1 -1
  25. package/dist/assets/{miniChat-B1i0B5Ml.js → miniChat-ChGISVuo.js} +2 -2
  26. package/dist/assets/{mobile-C-PKN6lA.js → mobile-Dv3wbcd9.js} +2 -2
  27. package/dist/assets/{mobileBackNavigation-CBPY3q34.js → mobileBackNavigation-BnipJtFo.js} +1 -1
  28. package/dist/assets/{multirun-BTBDMVOl.js → multirun-DvSn-icg.js} +1 -1
  29. package/dist/assets/{projectMeta---vfnScT.js → projectMeta-Cm0n6vtB.js} +1 -1
  30. package/dist/assets/renderElectronMiniChatApp-BjQyp-u8.js +2 -0
  31. package/dist/assets/{renderMobileApp-DaFhnxuq.js → renderMobileApp-BEkWDH7L.js} +6 -6
  32. package/dist/assets/runtimeConfig-D3moXtPz.js +2 -0
  33. package/dist/assets/{runtimeEndpointReset-CBcDor5H.js → runtimeEndpointReset-BYn28E2A.js} +5 -5
  34. package/dist/assets/{sessionChangeSummary-DTa1NhbK.js → sessionChangeSummary-DXpoQiGE.js} +1 -1
  35. package/dist/assets/{sessionLookup-xGVDWmBL.js → sessionLookup-BYycaYEQ.js} +1 -1
  36. package/dist/assets/{speechSettingsQueries-Chq_vO6e.js → speechSettingsQueries-Bv08BFlI.js} +1 -1
  37. package/dist/assets/src-CmermOKP.css +2 -0
  38. package/dist/assets/types-Q_utERET.js +2 -0
  39. package/dist/assets/{useAppFontEffects-CzYFu1JQ.js → useAppFontEffects-HYIeI-xf.js} +40 -40
  40. package/dist/assets/{useDesktopWindowControlsLayout-DML3uW6M.js → useDesktopWindowControlsLayout-Cc-2uuN5.js} +1 -1
  41. package/dist/assets/{useEffectiveDirectory-CYhf9ITQ.js → useEffectiveDirectory-BBb97raM.js} +1 -1
  42. package/dist/assets/{useMobileNavigationStore-DvSdigrK.js → useMobileNavigationStore-CEVJSNnR.js} +3 -3
  43. package/dist/assets/{useWorkerHighlightedLines-BThWzx-h.js → useWorkerHighlightedLines-D8JkeSuf.js} +1 -1
  44. package/dist/index.html +12 -12
  45. package/dist/mini-chat.html +6 -6
  46. package/dist/mobile.html +8 -8
  47. package/package.json +1 -1
  48. package/server/lib/content-search/DOCUMENTATION.md +5 -1
  49. package/server/lib/content-search/runtime.js +1 -0
  50. package/server/lib/content-search/runtime.test.js +43 -0
  51. package/server/lib/notifications/APNS.md +13 -0
  52. package/server/lib/notifications/DOCUMENTATION.md +4 -0
  53. package/server/lib/notifications/apns-runtime.js +45 -24
  54. package/server/lib/notifications/apns-runtime.test.js +57 -1
  55. package/server/lib/notifications/routes.js +6 -2
  56. package/server/lib/opencode/DOCUMENTATION.md +2 -0
  57. package/server/lib/opencode/pwa-manifest-routes.js +2 -2
  58. package/server/lib/opencode/settings-runtime.js +1 -1
  59. package/server/lib/opencode/settings-runtime.test.js +15 -0
  60. package/server/lib/opencode/static-routes-runtime.js +6 -6
  61. package/dist/assets/MarkdownRenderer-QKxUWu_S.js +0 -2
  62. package/dist/assets/MobileTabPageHeader-sOgdUSiO.js +0 -2
  63. package/dist/assets/renderElectronMiniChatApp-CBZ5TGG8.js +0 -2
  64. package/dist/assets/runtimeConfig-DTzs5NCT.js +0 -2
  65. package/dist/assets/src-BEVRKYT8.css +0 -2
  66. package/dist/assets/types-BuiqMmh_.js +0 -1
@@ -1,4 +1,4 @@
1
- import{r as e}from"./rolldown-runtime-hePW80VL.js";import{it as t,rt as n}from"./vendor-base-ui-CdzJ6My_.js";import{v as r}from"./useUIStore-NMQx_cnG.js";import{n as i}from"./vendor-react-BP2cRVqX.js";import{f as a,r as o,t as s}from"./button-C3292Ee6.js";import{n as c}from"./useThemeSystem-8jhE4Zyk.js";import{n as l}from"./Icon-jsQQXouE.js";import{i as u,n as d,t as f}from"./tooltip-DGUZtHif.js";import{n as p}from"./MarkdownRenderer-QKxUWu_S.js";import{n as m,o as h,s as g,t as _}from"./markdown-worker-CkO3sT4Y.js";import{t as v}from"./vendor-tanstack-react-virtual-B_EO--9t.js";import{n as y}from"./markdownTheme-DHoKvD6O.js";var b=e(t(),1),x=i(),S=n(),C=`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)]`,w={maxWidth:`min(28rem, calc(100cqw - 4ch))`,backgroundColor:`var(--surface-elevated)`,color:`var(--surface-elevated-foreground)`},T=`flex items-center justify-between gap-3 border-b border-[var(--surface-subtle)] px-3 py-2.5`,E=`text-xs font-medium leading-4 text-foreground md:text-[0.8125rem] md:leading-5`,D=`text-xs tabular-nums leading-4 text-muted-foreground md:text-[0.8125rem] md:leading-5`,O=`m-0 list-none divide-y divide-[var(--surface-subtle)] p-0`,k=`${O} max-h-[min(22rem,50vh)] overflow-y-auto`,A=`${O} w-full`,ee={className:`p-0 rounded-none`,maxHeightClass:`max-h-[46vh]`,disableHorizontal:!0},j=`px-2.5 py-1.5 text-xs leading-4 md:py-2 md:text-[0.8125rem] md:leading-5`,M=`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`,te=`min-w-0 truncate text-xs leading-4 text-foreground md:text-[0.8125rem] md:leading-5`,ne=`inline-flex shrink-0 items-center gap-1 text-xs leading-4 tabular-nums text-muted-foreground md:text-[0.8125rem] md:leading-5`,re=`h-3 w-3 shrink-0 md:h-3.5 md:w-3.5`,ie={in_progress:`text-foreground`,pending:`text-foreground`,completed:`text-muted-foreground line-through`,cancelled:`text-muted-foreground line-through`},ae={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`},N=e=>{let t=(0,x.c)(26),{todo:n,isActive:r,activeRef:i}=e,{t:s}=a(),c=oe.has(n.status)?n.status:`pending`,p=ae[c],m;t[0]!==s||t[1]!==p?(m=s(p),t[0]=s,t[1]=p,t[2]=m):m=t[2];let h=m,g;t[3]===c?g=t[4]:(g=c===`in_progress`?(0,S.jsx)(l,{name:`record-circle`,className:`h-3.5 w-3.5 text-[var(--status-info)]`,"aria-hidden":`true`}):c===`completed`?(0,S.jsx)(l,{name:`checkbox-circle`,className:`h-3.5 w-3.5 text-[var(--status-success)]`,"aria-hidden":`true`}):(0,S.jsx)(l,{name:`time`,className:`h-3.5 w-3.5 text-muted-foreground`,"aria-hidden":`true`}),t[3]=c,t[4]=g);let _=g,v=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=o(`group relative grid min-w-0 grid-cols-[1rem_minmax(0,1fr)] items-start gap-2.5`,j,y),t[5]=y,t[6]=b);let C;t[7]!==_||t[8]!==h?(C=(0,S.jsx)(u,{asChild:!0,children:(0,S.jsx)(`span`,{className:`flex size-4 items-center justify-center`,"aria-label":h,children:_})}),t[7]=_,t[8]=h,t[9]=C):C=t[9];let w;t[10]===h?w=t[11]:(w=(0,S.jsx)(d,{side:`left`,sideOffset:6,children:h}),t[10]=h,t[11]=w);let T;t[12]!==C||t[13]!==w?(T=(0,S.jsxs)(f,{children:[C,w]}),t[12]=C,t[13]=w,t[14]=T):T=t[14];let E=ie[c],D;t[15]===E?D=t[16]:(D=o(`block whitespace-normal break-words`,E),t[15]=E,t[16]=D);let O;t[17]!==D||t[18]!==n.content?(O=(0,S.jsx)(`span`,{className:`min-w-0 flex-1`,children:(0,S.jsx)(`span`,{className:D,children:n.content})}),t[17]=D,t[18]=n.content,t[19]=O):O=t[19];let k;return t[20]!==i||t[21]!==O||t[22]!==v||t[23]!==b||t[24]!==T?(k=(0,S.jsxs)(`li`,{ref:i,"aria-current":v,className:b,children:[T,O]}),t[20]=i,t[21]=O,t[22]=v,t[23]=b,t[24]=T,t[25]=k):k=t[25],k},oe=new Set([`in_progress`,`pending`,`completed`,`cancelled`]),P=e=>e.replace(/&/g,`&amp;`).replace(/</g,`&lt;`).replace(/>/g,`&gt;`).replace(/"/g,`&quot;`).replace(/'/g,`&#39;`),F=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(``):``,I=(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=F(n);e&&i.setAttribute(`style`,`${i.getAttribute(`style`)??``}${e}`)}},L=e=>`<pre><code>${P(e)}</code></pre>`,R=e=>{let t=(0,x.c)(21),{code:n,language:r,className:i,style:a,codeStyle:s,wrap:l}=e,u=l!==void 0&&l,{currentTheme:d}=c(),f=b.useRef(null),p;t[0]===d?p=t[1]:(p=y(d),t[0]=d,t[1]=p);let m=p,h,g;t[2]!==n||t[3]!==s||t[4]!==u?(h=()=>{let e=f.current;e&&(e.innerHTML=L(n),I(e,u,s))},g=[n,u,s],t[2]=n,t[3]=s,t[4]=u,t[5]=h,t[6]=g):(h=t[5],g=t[6]),b.useLayoutEffect(h,g);let v,C;t[7]!==n||t[8]!==s||t[9]!==r||t[10]!==u?(v=()=>{let e=f.current;if(!e)return;let t=!0,i=new AbortController;return _(n,(r||`text`).toLowerCase(),{signal:i.signal,priority:`visible`}).then(n=>{!t||!e||!n||(e.innerHTML=n,I(e,u,s))}),()=>{t=!1,i.abort()}},C=[n,r,u,s],t[7]=n,t[8]=s,t[9]=r,t[10]=u,t[11]=v,t[12]=C):(v=t[11],C=t[12]),b.useEffect(v,C);let w;t[13]===i?w=t[14]:(w=o(`typography-code`,i),t[13]=i,t[14]=w);let T=m,E;t[15]!==a||t[16]!==T?(E={...T,...a},t[15]=a,t[16]=T,t[17]=E):E=t[17];let D;return t[18]!==w||t[19]!==E?(D=(0,S.jsx)(`div`,{ref:f,className:w,style:E}),t[18]=w,t[19]=E,t[20]=D):D=t[20],D},z=e=>{let t=e.replace(/^<file>\s*\n?/,``).replace(/\n?<\/file>\s*$/,``);return t=t.replace(/^\s*\d{5}\|\s?/gm,``),t.trim()},B=(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}},V=e=>e?e.includes(`<diagnostics`)||e.includes(`<file_diagnostics>`)||e.includes(`LSP errors detected`)||e.includes(`This file has errors`):!1,H=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():``,U=(e,t)=>!e||typeof e!=`object`?String(e):g(e,t||``),W=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}}},G=e=>{if(!e||typeof e!=`object`)return;let t=W(e.patch)??W(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=W(e.patch)??W(e.diff);if(t)return t}},K=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}}},q=(e,t,n)=>{let r=z(e);if((t===`edit`||t===`multiedit`||t===`write`||t===`apply_patch`)&&V(r)&&(r=H(r)),(t===`edit`||t===`multiedit`||t===`apply_patch`)&&r.trim().length===0){let e=G(n);if(e)return e}return r},se=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{it as t,rt as n}from"./vendor-base-ui-CdzJ6My_.js";import{v as r}from"./useUIStore-NMQx_cnG.js";import{n as i}from"./vendor-react-BP2cRVqX.js";import{f as a,r as o,t as s}from"./button-C3292Ee6.js";import{n as c}from"./useThemeSystem-8jhE4Zyk.js";import{n as l}from"./Icon-jsQQXouE.js";import{i as u,n as d,t as f}from"./tooltip-DGUZtHif.js";import{n as p}from"./MarkdownRenderer-D4Ba9SUF.js";import{n as m,o as h,s as g,t as _}from"./markdown-worker-CkO3sT4Y.js";import{t as v}from"./vendor-tanstack-react-virtual-B_EO--9t.js";import{n as y}from"./markdownTheme-DHoKvD6O.js";var b=e(t(),1),x=i(),S=n(),C=`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)]`,w={maxWidth:`min(28rem, calc(100cqw - 4ch))`,backgroundColor:`var(--surface-elevated)`,color:`var(--surface-elevated-foreground)`},T=`flex items-center justify-between gap-3 border-b border-[var(--surface-subtle)] px-3 py-2.5`,E=`text-xs font-medium leading-4 text-foreground md:text-[0.8125rem] md:leading-5`,D=`text-xs tabular-nums leading-4 text-muted-foreground md:text-[0.8125rem] md:leading-5`,O=`m-0 list-none divide-y divide-[var(--surface-subtle)] p-0`,k=`${O} max-h-[min(22rem,50vh)] overflow-y-auto`,A=`${O} w-full`,ee={className:`p-0 rounded-none`,maxHeightClass:`max-h-[46vh]`,disableHorizontal:!0},j=`px-2.5 py-1.5 text-xs leading-4 md:py-2 md:text-[0.8125rem] md:leading-5`,M=`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`,te=`min-w-0 truncate text-xs leading-4 text-foreground md:text-[0.8125rem] md:leading-5`,ne=`inline-flex shrink-0 items-center gap-1 text-xs leading-4 tabular-nums text-muted-foreground md:text-[0.8125rem] md:leading-5`,re=`h-3 w-3 shrink-0 md:h-3.5 md:w-3.5`,ie={in_progress:`text-foreground`,pending:`text-foreground`,completed:`text-muted-foreground line-through`,cancelled:`text-muted-foreground line-through`},ae={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`},N=e=>{let t=(0,x.c)(26),{todo:n,isActive:r,activeRef:i}=e,{t:s}=a(),c=oe.has(n.status)?n.status:`pending`,p=ae[c],m;t[0]!==s||t[1]!==p?(m=s(p),t[0]=s,t[1]=p,t[2]=m):m=t[2];let h=m,g;t[3]===c?g=t[4]:(g=c===`in_progress`?(0,S.jsx)(l,{name:`record-circle`,className:`h-3.5 w-3.5 text-[var(--status-info)]`,"aria-hidden":`true`}):c===`completed`?(0,S.jsx)(l,{name:`checkbox-circle`,className:`h-3.5 w-3.5 text-[var(--status-success)]`,"aria-hidden":`true`}):(0,S.jsx)(l,{name:`time`,className:`h-3.5 w-3.5 text-muted-foreground`,"aria-hidden":`true`}),t[3]=c,t[4]=g);let _=g,v=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=o(`group relative grid min-w-0 grid-cols-[1rem_minmax(0,1fr)] items-start gap-2.5`,j,y),t[5]=y,t[6]=b);let C;t[7]!==_||t[8]!==h?(C=(0,S.jsx)(u,{asChild:!0,children:(0,S.jsx)(`span`,{className:`flex size-4 items-center justify-center`,"aria-label":h,children:_})}),t[7]=_,t[8]=h,t[9]=C):C=t[9];let w;t[10]===h?w=t[11]:(w=(0,S.jsx)(d,{side:`left`,sideOffset:6,children:h}),t[10]=h,t[11]=w);let T;t[12]!==C||t[13]!==w?(T=(0,S.jsxs)(f,{children:[C,w]}),t[12]=C,t[13]=w,t[14]=T):T=t[14];let E=ie[c],D;t[15]===E?D=t[16]:(D=o(`block whitespace-normal break-words`,E),t[15]=E,t[16]=D);let O;t[17]!==D||t[18]!==n.content?(O=(0,S.jsx)(`span`,{className:`min-w-0 flex-1`,children:(0,S.jsx)(`span`,{className:D,children:n.content})}),t[17]=D,t[18]=n.content,t[19]=O):O=t[19];let k;return t[20]!==i||t[21]!==O||t[22]!==v||t[23]!==b||t[24]!==T?(k=(0,S.jsxs)(`li`,{ref:i,"aria-current":v,className:b,children:[T,O]}),t[20]=i,t[21]=O,t[22]=v,t[23]=b,t[24]=T,t[25]=k):k=t[25],k},oe=new Set([`in_progress`,`pending`,`completed`,`cancelled`]),P=e=>e.replace(/&/g,`&amp;`).replace(/</g,`&lt;`).replace(/>/g,`&gt;`).replace(/"/g,`&quot;`).replace(/'/g,`&#39;`),F=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(``):``,I=(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=F(n);e&&i.setAttribute(`style`,`${i.getAttribute(`style`)??``}${e}`)}},L=e=>`<pre><code>${P(e)}</code></pre>`,R=e=>{let t=(0,x.c)(21),{code:n,language:r,className:i,style:a,codeStyle:s,wrap:l}=e,u=l!==void 0&&l,{currentTheme:d}=c(),f=b.useRef(null),p;t[0]===d?p=t[1]:(p=y(d),t[0]=d,t[1]=p);let m=p,h,g;t[2]!==n||t[3]!==s||t[4]!==u?(h=()=>{let e=f.current;e&&(e.innerHTML=L(n),I(e,u,s))},g=[n,u,s],t[2]=n,t[3]=s,t[4]=u,t[5]=h,t[6]=g):(h=t[5],g=t[6]),b.useLayoutEffect(h,g);let v,C;t[7]!==n||t[8]!==s||t[9]!==r||t[10]!==u?(v=()=>{let e=f.current;if(!e)return;let t=!0,i=new AbortController;return _(n,(r||`text`).toLowerCase(),{signal:i.signal,priority:`visible`}).then(n=>{!t||!e||!n||(e.innerHTML=n,I(e,u,s))}),()=>{t=!1,i.abort()}},C=[n,r,u,s],t[7]=n,t[8]=s,t[9]=r,t[10]=u,t[11]=v,t[12]=C):(v=t[11],C=t[12]),b.useEffect(v,C);let w;t[13]===i?w=t[14]:(w=o(`typography-code`,i),t[13]=i,t[14]=w);let T=m,E;t[15]!==a||t[16]!==T?(E={...T,...a},t[15]=a,t[16]=T,t[17]=E):E=t[17];let D;return t[18]!==w||t[19]!==E?(D=(0,S.jsx)(`div`,{ref:f,className:w,style:E}),t[18]=w,t[19]=E,t[20]=D):D=t[20],D},z=e=>{let t=e.replace(/^<file>\s*\n?/,``).replace(/\n?<\/file>\s*$/,``);return t=t.replace(/^\s*\d{5}\|\s?/gm,``),t.trim()},B=(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}},V=e=>e?e.includes(`<diagnostics`)||e.includes(`<file_diagnostics>`)||e.includes(`LSP errors detected`)||e.includes(`This file has errors`):!1,H=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():``,U=(e,t)=>!e||typeof e!=`object`?String(e):g(e,t||``),W=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}}},G=e=>{if(!e||typeof e!=`object`)return;let t=W(e.patch)??W(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=W(e.patch)??W(e.diff);if(t)return t}},K=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}}},q=(e,t,n)=>{let r=z(e);if((t===`edit`||t===`multiedit`||t===`write`||t===`apply_patch`)&&V(r)&&(r=H(r)),(t===`edit`||t===`multiedit`||t===`apply_patch`)&&r.trim().length===0){let e=G(n);if(e)return e}return r},se=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)})}},ce=(e,t)=>{try{let n=e.trim().split(`
4
4
  `).filter(Boolean);if(n.length===0)return null;let i=[];return n.forEach(e=>{let t=e.match(/^(\s*)(.+)$/);if(t){let[,e,n]=t,r=Math.floor(e.length/2),a=!n.endsWith(`/`);i.push({name:n.replace(/\/$/,``),depth:r,isFile:a})}}),(0,S.jsx)(`div`,{className:o(`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:r.tool.popup,children:i.map((e,t)=>(0,S.jsx)(`div`,{className:`min-w-0`,style:{paddingLeft:`${e.depth*20}px`},children:e.isFile?(0,S.jsx)(`span`,{className:`text-foreground/90 block truncate`,children:e.name}):(0,S.jsxs)(`span`,{className:`font-semibold text-foreground block truncate`,children:[e.name,`/`]})},t))})}catch{return null}},le={backgroundColor:`var(--status-info)`,opacity:.6},ue=(e,t,n)=>{try{let i=e.trim().split(`
package/dist/index.html CHANGED
@@ -8,7 +8,7 @@
8
8
  <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
9
9
  <link rel="icon" type="image/png" href="/favicon-32.png" sizes="32x32" />
10
10
  <link rel="icon" type="image/png" href="/favicon-16.png" sizes="16x16" />
11
- <link rel="mask-icon" href="/favicon.svg" color="#edb449" />
11
+ <link rel="mask-icon" href="/favicon.svg" color="#4c9df3" />
12
12
 
13
13
  <!-- Apple touch icon - PNG format required for iOS PWA support -->
14
14
  <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon-180x180.png" />
@@ -238,8 +238,8 @@
238
238
  display: 'standalone',
239
239
  display_override: ['window-controls-overlay'],
240
240
  ...(manifestOrientation ? { orientation: manifestOrientation } : {}),
241
- background_color: '#151313',
242
- theme_color: '#edb449',
241
+ background_color: '#1a1a1a',
242
+ theme_color: '#4c9df3',
243
243
  icons: [
244
244
  { src: `${baseUrl}/pwa-192.png`, sizes: '192x192', type: 'image/png', purpose: 'any' },
245
245
  { src: `${baseUrl}/pwa-512.png`, sizes: '512x512', type: 'image/png', purpose: 'any' },
@@ -422,8 +422,8 @@
422
422
  </script>
423
423
 
424
424
  <!-- Theme color - Safari iOS 26+ prioritizes CSS background-color over this, but keep as fallback -->
425
- <meta name="theme-color" content="#151313" />
426
- <meta name="theme-color" content="#151313" media="(prefers-color-scheme: dark)" />
425
+ <meta name="theme-color" content="#1a1a1a" />
426
+ <meta name="theme-color" content="#1a1a1a" media="(prefers-color-scheme: dark)" />
427
427
 
428
428
  <!-- iOS Safari / installable PWA styling -->
429
429
  <meta name="mobile-web-app-capable" content="yes" />
@@ -464,8 +464,8 @@
464
464
  }
465
465
 
466
466
  :root {
467
- --splash-background-dark: #151313;
468
- --splash-stroke-dark: #CECDC3;
467
+ --splash-background-dark: #1a1a1a;
468
+ --splash-stroke-dark: #CCCCCCdd;
469
469
  --splash-background-light: #FFFCF0;
470
470
  --splash-stroke-light: #100F0F;
471
471
 
@@ -552,7 +552,7 @@
552
552
  opacity: 0.72;
553
553
  }
554
554
  </style>
555
- <script type="module" crossorigin src="/assets/main-B_h17HmQ.js"></script>
555
+ <script type="module" crossorigin src="/assets/main-R2wbcT-3.js"></script>
556
556
  <link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-hePW80VL.js">
557
557
  <link rel="modulepreload" crossorigin href="/assets/runtime-auth-ef9W7A43.js">
558
558
  <link rel="modulepreload" crossorigin href="/assets/runtime-switch-CAZiOV2G.js">
@@ -575,10 +575,10 @@
575
575
  <link rel="modulepreload" crossorigin href="/assets/platform-7bX6qtBV.js">
576
576
  <link rel="modulepreload" crossorigin href="/assets/terminalApi-BHXvha-U.js">
577
577
  <link rel="modulepreload" crossorigin href="/assets/runtime-file-download-DAOam_oV.js">
578
- <link rel="modulepreload" crossorigin href="/assets/types-BuiqMmh_.js">
579
- <link rel="modulepreload" crossorigin href="/assets/capgoAdapter-KNLHDj7G.js">
580
- <link rel="modulepreload" crossorigin href="/assets/runtimeConfig-DTzs5NCT.js">
581
- <link rel="stylesheet" crossorigin href="/assets/src-BEVRKYT8.css">
578
+ <link rel="modulepreload" crossorigin href="/assets/types-Q_utERET.js">
579
+ <link rel="modulepreload" crossorigin href="/assets/capgoAdapter-DLi2tcmI.js">
580
+ <link rel="modulepreload" crossorigin href="/assets/runtimeConfig-D3moXtPz.js">
581
+ <link rel="stylesheet" crossorigin href="/assets/src-CmermOKP.css">
582
582
  </head>
583
583
  <body class="h-full bg-background text-foreground">
584
584
  <!-- Keep the startup layer outside #root. React replaces every child of
@@ -52,7 +52,7 @@
52
52
  briefly show a second, differently-sized one before handing off. The
53
53
  backdrop matches bg-background, so fading it out is seamless. */
54
54
  :root {
55
- --splash-background-dark: #151313;
55
+ --splash-background-dark: #1a1a1a;
56
56
  --splash-background-light: #FFFCF0;
57
57
  --splash-background: var(--splash-background-dark);
58
58
  }
@@ -74,7 +74,7 @@
74
74
  }
75
75
  </style>
76
76
 
77
- <script type="module" crossorigin src="/assets/miniChat-B1i0B5Ml.js"></script>
77
+ <script type="module" crossorigin src="/assets/miniChat-ChGISVuo.js"></script>
78
78
  <link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-hePW80VL.js">
79
79
  <link rel="modulepreload" crossorigin href="/assets/runtime-auth-ef9W7A43.js">
80
80
  <link rel="modulepreload" crossorigin href="/assets/runtime-switch-CAZiOV2G.js">
@@ -97,10 +97,10 @@
97
97
  <link rel="modulepreload" crossorigin href="/assets/platform-7bX6qtBV.js">
98
98
  <link rel="modulepreload" crossorigin href="/assets/terminalApi-BHXvha-U.js">
99
99
  <link rel="modulepreload" crossorigin href="/assets/runtime-file-download-DAOam_oV.js">
100
- <link rel="modulepreload" crossorigin href="/assets/types-BuiqMmh_.js">
101
- <link rel="modulepreload" crossorigin href="/assets/capgoAdapter-KNLHDj7G.js">
102
- <link rel="modulepreload" crossorigin href="/assets/runtimeConfig-DTzs5NCT.js">
103
- <link rel="stylesheet" crossorigin href="/assets/src-BEVRKYT8.css">
100
+ <link rel="modulepreload" crossorigin href="/assets/types-Q_utERET.js">
101
+ <link rel="modulepreload" crossorigin href="/assets/capgoAdapter-DLi2tcmI.js">
102
+ <link rel="modulepreload" crossorigin href="/assets/runtimeConfig-D3moXtPz.js">
103
+ <link rel="stylesheet" crossorigin href="/assets/src-CmermOKP.css">
104
104
  </head>
105
105
  <body class="h-full bg-background text-foreground">
106
106
  <div id="root" class="h-full">
package/dist/mobile.html CHANGED
@@ -41,16 +41,16 @@
41
41
  // splashBg* are the resolved theme surface.background colors persisted by the app
42
42
  // theme system (see ThemeSystemContext). When absent (e.g. a fresh install before
43
43
  // the theme has been persisted once), fall back to the default theme's
44
- // surface.background — carbonfox-light / vitesse-dark-dark (see lib/theme/themes),
44
+ // surface.background — carbonfox-light / cursor-dark (see lib/theme/themes),
45
45
  // which is what the app renders on first launch — so the pre-paint matches instead
46
46
  // of flashing a different colour.
47
- var bgDark = localStorage.getItem('splashBgDark') || '#121212';
47
+ var bgDark = localStorage.getItem('splashBgDark') || '#1a1a1a';
48
48
  var bgLight = localStorage.getItem('splashBgLight') || '#FFFFFF';
49
49
  var bg = isDark ? bgDark : bgLight;
50
50
 
51
51
  // Set the actual --background CSS variable, not just the element background.
52
52
  // `<body class="bg-background">` paints `var(--background)`, and design-system.css
53
- // ships a baked default (.dark { --background: ... /* #151313 */ }) that otherwise
53
+ // ships a baked default (.dark { --background: #1a1a1a }) that otherwise
54
54
  // overpaints our html background the moment the stylesheet loads — before React's
55
55
  // theme system injects the real (default/custom) vars with !important. Setting the
56
56
  // var inline on the root wins over that non-!important default, so body paints the
@@ -63,7 +63,7 @@
63
63
  })();
64
64
  </script>
65
65
 
66
- <script type="module" crossorigin src="/assets/mobile-C-PKN6lA.js"></script>
66
+ <script type="module" crossorigin src="/assets/mobile-Dv3wbcd9.js"></script>
67
67
  <link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-hePW80VL.js">
68
68
  <link rel="modulepreload" crossorigin href="/assets/runtime-auth-ef9W7A43.js">
69
69
  <link rel="modulepreload" crossorigin href="/assets/runtime-switch-CAZiOV2G.js">
@@ -86,10 +86,10 @@
86
86
  <link rel="modulepreload" crossorigin href="/assets/platform-7bX6qtBV.js">
87
87
  <link rel="modulepreload" crossorigin href="/assets/terminalApi-BHXvha-U.js">
88
88
  <link rel="modulepreload" crossorigin href="/assets/runtime-file-download-DAOam_oV.js">
89
- <link rel="modulepreload" crossorigin href="/assets/types-BuiqMmh_.js">
90
- <link rel="modulepreload" crossorigin href="/assets/capgoAdapter-KNLHDj7G.js">
91
- <link rel="modulepreload" crossorigin href="/assets/runtimeConfig-DTzs5NCT.js">
92
- <link rel="stylesheet" crossorigin href="/assets/src-BEVRKYT8.css">
89
+ <link rel="modulepreload" crossorigin href="/assets/types-Q_utERET.js">
90
+ <link rel="modulepreload" crossorigin href="/assets/capgoAdapter-DLi2tcmI.js">
91
+ <link rel="modulepreload" crossorigin href="/assets/runtimeConfig-D3moXtPz.js">
92
+ <link rel="stylesheet" crossorigin href="/assets/src-CmermOKP.css">
93
93
  </head>
94
94
  <body class="h-full bg-background text-foreground">
95
95
  <div id="root" class="h-full"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yuriai/web",
3
- "version": "0.1.2-beta.2",
3
+ "version": "0.1.3",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "./server/index.js",
@@ -4,7 +4,11 @@ The Host owns `content-search.sqlite` alongside the session index. This index is
4
4
  independent of browser transcript caches and includes all projects exposed by the
5
5
  active OpenCode runtime. `session.list` is called without a project/directory
6
6
  filter; opaque cursors are followed until exhausted. The default coverage is
7
- messages created in the last thirty days in recently updated sessions.
7
+ messages created in the last thirty days in recently updated root sessions.
8
+ Sessions with a non-empty `parentID` (subagents) are excluded before message
9
+ fetching and progress counting. Only root IDs enter the authoritative pruning
10
+ set, so a successful catalog scan also removes previously indexed subagent
11
+ messages, FTS entries and checkpoints. Failed catalog reads preserve the old index.
8
12
 
9
13
  Two workers page native `message.list` directly, normalize via the shared server
10
14
  projection, and retain only user text and settled assistant text. Synthetic,
@@ -43,6 +43,7 @@ export function createContentSearchRuntime({ service, getRuntimeKey, listSession
43
43
  if (!Array.isArray(page?.data)) throw new Error('Invalid global session page');
44
44
  for (const session of page.data) {
45
45
  if (typeof session?.id !== 'string' || !session.id || !Number.isFinite(session.time?.updated)) throw new Error('Invalid global session checkpoint');
46
+ if (typeof session.parentID === 'string' && session.parentID) continue;
46
47
  if (ids.has(session.id)) continue;
47
48
  ids.add(session.id);
48
49
  if (Number(session.time?.updated) >= state.since && !deleted.has(session.id)) sessions.push(session);
@@ -2,6 +2,49 @@ import { expect, test } from 'vitest';
2
2
  import { createContentSearchService } from './service.js';
3
3
  import { createContentSearchRuntime } from './runtime.js';
4
4
 
5
+ test('indexes only roots and prunes legacy subagents after a complete catalog, including dirty children', async () => {
6
+ const service = createContentSearchService({ dbPath: ':memory:' });
7
+ const root = { id: 'root', time: { updated: 10 } };
8
+ const child = { id: 'child', parentID: 'root', time: { updated: 10 } };
9
+ const nested = { id: 'nested', parentID: 'child', time: { updated: 10 } };
10
+ const record = { info: { id: 'old', role: 'user', time: { created: 10 } }, parts: [{ type: 'text', text: 'legacy needle' }] };
11
+ service.replaceSession('host', child, [record], 0);
12
+ service.replaceSession('other', child, [record], 0);
13
+ let fail = true;
14
+ const reads = [];
15
+ const runtime = createContentSearchRuntime({ service, getRuntimeKey: () => 'host', now: () => 100,
16
+ listSessions: async ({ cursor }) => {
17
+ if (!cursor) return { data: [child, nested], cursor: { next: 'roots' } };
18
+ if (fail) throw new Error('offline');
19
+ return { data: [root] };
20
+ },
21
+ listMessages: async ({ sessionID }) => {
22
+ reads.push(sessionID);
23
+ return { data: [{ type: 'user', id: 'new', sessionID, time: { created: 10 }, text: 'root needle' }] };
24
+ },
25
+ });
26
+ try {
27
+ await runtime.sync();
28
+ expect(runtime.status().phase).toBe('error');
29
+ expect(service.search('host', 'legacy')).toHaveLength(1);
30
+ fail = false;
31
+ runtime.observe({ payload: { type: 'session.updated', properties: { id: 'child' } } });
32
+ await runtime.sync();
33
+ expect(reads).toEqual(['root']);
34
+ expect(runtime.status()).toMatchObject({ phase: 'ready', total: 1, completed: 1, sessions: 1, initializationRequired: false });
35
+ expect(service.search('host', 'needle').map((hit) => hit.sessionID)).toEqual(['root']);
36
+ expect(service.updated('host', 'child')).toBeUndefined();
37
+ expect(service.search('other', 'legacy')).toHaveLength(1);
38
+ runtime.observe({ payload: { type: 'session.updated', properties: { id: 'nested' } } });
39
+ await runtime.sync();
40
+ expect(reads).toEqual(['root']);
41
+ expect(runtime.status().initializationRequired).toBe(false);
42
+ } finally {
43
+ await runtime.stop();
44
+ service.close();
45
+ }
46
+ });
47
+
5
48
  test('persists initialization times only after all thirty-day history is covered and resumes partial work', async () => {
6
49
  const service = createContentSearchService({ dbPath: ':memory:' });
7
50
  const day = 86400000;
@@ -7,6 +7,19 @@ registered them — so a leaked device token alone can't be used to push.
7
7
 
8
8
  ## How it works
9
9
 
10
+ Native registration now includes the installed shell's `bundleId`, obtained via
11
+ Capacitor App metadata, not the connected Host or web bundle version. The Host
12
+ validates and persists this optional field with alert and Live Activity tokens.
13
+ It appends `.${bundleId}` to the existing Relay registration signature when present,
14
+ and checks app identity in registration caches. Re-registration after restart
15
+ or Relay switching preserves it. The Relay owns the allowlist and app-to-APNs-key
16
+ mapping; the mobile client never supplies credentials or an arbitrary APNs topic.
17
+ Existing tokens without identity retain the default-app behavior; missing fields
18
+ must not overwrite an identity already stored. Upgrade Relay before Host/client.
19
+ Direct APNs remains single-app and skips explicit tokens for other apps without
20
+ deleting them. Browser/desktop/VS Code do not acquire a native app identity; Android
21
+ notification-stream behavior is unchanged.
22
+
10
23
  1. The app registers its APNs device token with **its own server** (`POST /api/push/apns-token`,
11
24
  `useNativePushRegistration`). PWA/desktop never register — only the native Capacitor app.
12
25
  2. The server **binds the token on the relay**: it POSTs `{ token, publicKeyJwk, ts, sig }` to
@@ -5,6 +5,10 @@ This module provides notification message preparation utilities for the web serv
5
5
 
6
6
  ## Entrypoints and structure
7
7
 
8
+ Native push registration accepts optional `bundleId` and carries it through Host
9
+ persistence and signed Relay binding for alert and Live Activity delivery. See
10
+ `APNS.md` for the multi-app compatibility contract and deployment ordering.
11
+
8
12
  Android notification-only subscriptions send the App locale in `Accept-Language`. `native-stream.js` localizes built-in English scenario titles per response using `apns-titles.js`, for both live delivery and replay, without mutating shared history. Custom titles, contact names and bodies are preserved. Missing/unsupported locales use the existing English fallback. The UI reconnects with its current locale when language changes; direct and encrypted relay requests carry the same header.
9
13
  - `native-stream.js`: bounded in-memory Android notification-only SSE/replay runtime, instantiated by the emitter and published by trigger fanout. `GET /api/notifications/native-stream` uses normal API/client authentication. It emits only `{type: 'notification', title, body, kind?, tag?, sessionId?, assistantID?, directory?, requireHidden}` and `{type: 'ready', gap}` with opaque SSE IDs; no session-status/transcript traffic. Initial connections receive only future events, reconnects use `Last-Event-ID`. Retention is 128 events / 15 minutes, no disk writes. Unknown/expired cursors explicitly signal a gap; backpressured clients disconnect and recover using the last processed ID. Heartbeat is 60 seconds and timers are removed on response close/error. Android owns foreground-service lifetime, native visibility and local system notification display; this channel is independent of APNs/FCM and the WebView.
10
14
  - `packages/web/server/lib/notifications/index.js`: public entrypoint imported by `packages/web/server/index.js`.
@@ -28,6 +28,11 @@ const APNS_HOST_SANDBOX = 'https://api.sandbox.push.apple.com';
28
28
  // APNs rejects auth tokens older than 1h; refresh well inside that window.
29
29
  const JWT_TTL_MS = 50 * 60 * 1000;
30
30
  const DEFAULT_BUNDLE_ID = 'com.yee94.yuri';
31
+ const parseBundleId = (value) => {
32
+ if (value === undefined) return undefined;
33
+ if (typeof value !== 'string' || value.length > 255 || !/^[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+$/.test(value)) throw new Error('Invalid push bundleId');
34
+ return value;
35
+ };
31
36
  const MAX_TOKENS_PER_SESSION = 10;
32
37
  const MAX_LIVE_ACTIVITY_TOKENS_PER_SESSION = 8;
33
38
  const MAX_LIVE_ACTIVITY_TOKEN_CHARS = 512;
@@ -187,8 +192,8 @@ export const createApnsRuntime = (deps) => {
187
192
  y: publicJwk.y,
188
193
  });
189
194
 
190
- const registrationCacheKey = (registerUrl, token, platform) =>
191
- `${registerUrl}\0${token}\0${platform}`;
195
+ const registrationCacheKey = (registerUrl, token, platform, bundleId) =>
196
+ `${registerUrl}\0${token}\0${platform}\0${bundleId ?? ''}`;
192
197
 
193
198
  const rememberRelayRegisterUrl = (registerUrl) => {
194
199
  if (registrationCacheRegisterUrl === registerUrl) return;
@@ -211,28 +216,28 @@ export const createApnsRuntime = (deps) => {
211
216
  const boundRegistrationCache = (registerUrl, entries) => {
212
217
  rememberRelayRegisterUrl(registerUrl);
213
218
  const allowed = new Set(
214
- entries.map((entry) => registrationCacheKey(registerUrl, entry.deviceToken, entry.platform)),
219
+ entries.map((entry) => registrationCacheKey(registerUrl, entry.deviceToken, entry.platform, entry.bundleId)),
215
220
  );
216
221
  for (const key of registrationSuccessCache.keys()) {
217
222
  if (!allowed.has(key)) registrationSuccessCache.delete(key);
218
223
  }
219
224
  };
220
225
 
221
- const registerTokenWithRelay = async (token, platform = 'ios', relayOverride) => {
226
+ const registerTokenWithRelay = async (token, platform = 'ios', relayOverride, bundleId) => {
222
227
  const relay = relayOverride ?? (await resolveRelayConfig());
223
228
  if (!relay) return true; // direct mode — no relay binding needed
224
229
  rememberRelayRegisterUrl(relay.registerUrl);
225
- const cacheKey = registrationCacheKey(relay.registerUrl, token, platform);
230
+ const cacheKey = registrationCacheKey(relay.registerUrl, token, platform, bundleId);
226
231
  if (registrationSuccessCache.has(cacheKey)) return true;
227
232
  try {
228
233
  const { privateKey, publicJwk } = await getOrCreateRelayKeypair();
229
234
  const ts = Date.now();
230
235
  // platform is part of the signed message so it can't be tampered en route.
231
- const sig = signRelayMessage(privateKey, `${ts}.${token}.${platform}`);
236
+ const sig = signRelayMessage(privateKey, `${ts}.${token}.${platform}${bundleId ? `.${bundleId}` : ''}`);
232
237
  const res = await fetch(relay.registerUrl, {
233
238
  method: 'POST',
234
239
  headers: { 'content-type': 'application/json' },
235
- body: JSON.stringify({ token, platform, publicKeyJwk: relayPublicJwk(publicJwk), ts, sig }),
240
+ body: JSON.stringify({ token, platform, bundleId, publicKeyJwk: relayPublicJwk(publicJwk), ts, sig }),
236
241
  });
237
242
  if (!res.ok) {
238
243
  console.warn(`[Push relay] register-token failed status=${res.status}`);
@@ -246,18 +251,18 @@ export const createApnsRuntime = (deps) => {
246
251
  }
247
252
  };
248
253
 
249
- const registerLiveActivityTokenWithRelay = async (token, relayOverride) => {
254
+ const registerLiveActivityTokenWithRelay = async (token, relayOverride, bundleId) => {
250
255
  const relay = relayOverride ?? (await resolveRelayConfig());
251
256
  if (!relay) return true;
252
257
  rememberLiveActivityRelayRegisterUrl(relay.liveActivityRegisterUrl);
253
258
  const cacheKey = liveActivityRegistrationCacheKey(relay.liveActivityRegisterUrl, token);
254
- if (liveActivityRegistrationSuccessCache.has(cacheKey)) return true;
259
+ if (liveActivityRegistrationSuccessCache.get(cacheKey) === (bundleId ?? '')) return true;
255
260
  try {
256
261
  const { privateKey, publicJwk } = await getOrCreateRelayKeypair();
257
262
  const ts = Date.now();
258
263
  const sig = signRelayMessage(
259
264
  privateKey,
260
- `${ts}.${token}.${LIVE_ACTIVITY_PLATFORM}.${LIVE_ACTIVITY_KIND}`,
265
+ `${ts}.${token}.${LIVE_ACTIVITY_PLATFORM}.${LIVE_ACTIVITY_KIND}${bundleId ? `.${bundleId}` : ''}`,
261
266
  );
262
267
  const res = await fetch(relay.liveActivityRegisterUrl, {
263
268
  method: 'POST',
@@ -266,6 +271,7 @@ export const createApnsRuntime = (deps) => {
266
271
  token,
267
272
  platform: LIVE_ACTIVITY_PLATFORM,
268
273
  kind: LIVE_ACTIVITY_KIND,
274
+ bundleId,
269
275
  publicKeyJwk: relayPublicJwk(publicJwk),
270
276
  ts,
271
277
  sig,
@@ -275,7 +281,7 @@ export const createApnsRuntime = (deps) => {
275
281
  console.warn(`[Push relay] register-live-activity-token failed status=${res.status}`);
276
282
  return false;
277
283
  }
278
- liveActivityRegistrationSuccessCache.set(cacheKey, true);
284
+ liveActivityRegistrationSuccessCache.set(cacheKey, bundleId ?? '');
279
285
  return true;
280
286
  } catch (error) {
281
287
  console.warn('[Push relay] register-live-activity-token request failed:', error?.message ?? error);
@@ -394,6 +400,7 @@ export const createApnsRuntime = (deps) => {
394
400
  if (!entry || typeof entry !== 'object') return null;
395
401
  const deviceToken = entry.deviceToken;
396
402
  if (typeof deviceToken !== 'string' || deviceToken.trim().length === 0) return null;
403
+ try { parseBundleId(entry.bundleId); } catch { return null; }
397
404
  return {
398
405
  deviceToken: deviceToken.trim(),
399
406
  createdAt: typeof entry.createdAt === 'number' ? entry.createdAt : null,
@@ -403,6 +410,7 @@ export const createApnsRuntime = (deps) => {
403
410
  platform: entry.platform === 'android' ? 'android' : 'ios',
404
411
  // App UI locale at last registration; missing → English titles (legacy tokens).
405
412
  locale: normalizeApnsLocale(entry.locale),
413
+ bundleId: entry.bundleId,
406
414
  };
407
415
  })
408
416
  .filter(Boolean);
@@ -419,11 +427,12 @@ export const createApnsRuntime = (deps) => {
419
427
  if (!isLimitedId(token, MAX_LIVE_ACTIVITY_TOKEN_CHARS)) return null;
420
428
  if (!isLimitedId(activityId, MAX_LIVE_ACTIVITY_ID_CHARS)) return null;
421
429
  if (!isLimitedId(sessionId, MAX_LIVE_ACTIVITY_ID_CHARS)) return null;
430
+ try { parseBundleId(entry.bundleId); } catch { return null; }
422
431
  const createdAt = typeof entry.createdAt === 'number' ? entry.createdAt : null;
423
432
  const lastSeenAt = typeof entry.lastSeenAt === 'number' ? entry.lastSeenAt : createdAt;
424
433
  if (typeof lastSeenAt === 'number' && now - lastSeenAt > LIVE_ACTIVITY_TOKEN_TTL_MS) return null;
425
434
  const snapshot = parseLiveActivitySnapshot(entry.snapshot);
426
- return { token, activityId, sessionId, snapshot, createdAt, lastSeenAt };
435
+ return { token, activityId, sessionId, snapshot, createdAt, lastSeenAt, bundleId: entry.bundleId };
427
436
  })
428
437
  .filter(Boolean);
429
438
  };
@@ -473,17 +482,20 @@ export const createApnsRuntime = (deps) => {
473
482
  return unique;
474
483
  };
475
484
 
476
- const addOrUpdateApnsToken = async (uiSessionToken, deviceToken, userAgent, platform, locale) => {
485
+ const addOrUpdateApnsToken = async (uiSessionToken, deviceToken, userAgent, platform, locale, bundleId) => {
486
+ parseBundleId(bundleId);
477
487
  if (!uiSessionToken || typeof deviceToken !== 'string' || deviceToken.trim().length === 0) return;
478
488
  const token = deviceToken.trim();
479
489
  const tokenPlatform = normalizePlatform(platform);
480
490
  const tokenLocale = normalizeApnsLocale(locale);
491
+ let tokenBundleId = bundleId;
481
492
  const now = Date.now();
482
493
 
483
494
  await persistTokenUpdate((current) => {
484
495
  const tokensBySession = { ...(current.tokensBySession || {}) };
485
496
  const existing = normalizeTokens(tokensBySession[uiSessionToken]);
486
497
  const prior = existing.find((entry) => entry.deviceToken === token);
498
+ tokenBundleId ??= prior?.bundleId;
487
499
  const filtered = existing.filter((entry) => entry.deviceToken !== token);
488
500
  filtered.unshift({
489
501
  deviceToken: token,
@@ -492,6 +504,7 @@ export const createApnsRuntime = (deps) => {
492
504
  userAgent: typeof userAgent === 'string' && userAgent.length > 0 ? userAgent : undefined,
493
505
  platform: tokenPlatform,
494
506
  locale: tokenLocale,
507
+ bundleId: tokenBundleId,
495
508
  });
496
509
  tokensBySession[uiSessionToken] = filtered.slice(0, MAX_TOKENS_PER_SESSION);
497
510
  return { version: APNS_TOKENS_VERSION, tokensBySession };
@@ -502,7 +515,7 @@ export const createApnsRuntime = (deps) => {
502
515
  // every time (not just for new tokens) keeps existing tokens bound after a relay/server
503
516
  // upgrade rather than silently going unbound. Platform is bound too so the relay routes
504
517
  // it to APNs vs FCM.
505
- await registerTokenWithRelay(token, tokenPlatform);
518
+ await registerTokenWithRelay(token, tokenPlatform, undefined, tokenBundleId);
506
519
  };
507
520
 
508
521
  const removeApnsToken = async (uiSessionToken, deviceToken) => {
@@ -531,7 +544,9 @@ export const createApnsRuntime = (deps) => {
531
544
  });
532
545
  };
533
546
 
534
- const addOrUpdateLiveActivityToken = async (uiSessionToken, token, activityId, sessionId, snapshot) => {
547
+ const addOrUpdateLiveActivityToken = async (uiSessionToken, token, activityId, sessionId, snapshot, bundleId) => {
548
+ parseBundleId(bundleId);
549
+ let tokenBundleId = bundleId;
535
550
  if (!uiSessionToken) return;
536
551
  const trimmedToken = typeof token === 'string' ? token.trim() : '';
537
552
  const trimmedActivityId = typeof activityId === 'string' ? activityId.trim() : '';
@@ -549,6 +564,7 @@ export const createApnsRuntime = (deps) => {
549
564
  );
550
565
  const existing = liveActivityTokensBySession[uiSessionToken] || [];
551
566
  const replaced = existing.find((entry) => entry.activityId === trimmedActivityId);
567
+ tokenBundleId ??= replaced?.bundleId;
552
568
  if (replaced && replaced.token !== trimmedToken) replacedToken = replaced.token;
553
569
  const filtered = existing.filter(
554
570
  (entry) => entry.activityId !== trimmedActivityId && entry.token !== trimmedToken,
@@ -559,6 +575,7 @@ export const createApnsRuntime = (deps) => {
559
575
  activityId: trimmedActivityId,
560
576
  sessionId: trimmedSessionId,
561
577
  snapshot: nextSnapshot,
578
+ bundleId: tokenBundleId,
562
579
  createdAt: typeof replaced?.createdAt === 'number' ? replaced.createdAt : now,
563
580
  lastSeenAt: now,
564
581
  });
@@ -566,7 +583,7 @@ export const createApnsRuntime = (deps) => {
566
583
  return { ...current, liveActivityTokensBySession };
567
584
  });
568
585
 
569
- await registerLiveActivityTokenWithRelay(trimmedToken);
586
+ await registerLiveActivityTokenWithRelay(trimmedToken, undefined, tokenBundleId);
570
587
  if (replacedToken) {
571
588
  const store = await readTokensFromDisk();
572
589
  if (countLiveActivityTokenCopies(store.liveActivityTokensBySession, replacedToken) === 0) {
@@ -1122,7 +1139,7 @@ export const createApnsRuntime = (deps) => {
1122
1139
 
1123
1140
  // Shared Live Activity delivery path (relay register-then-send, or direct APNs).
1124
1141
  // Returns the tokens APNs accepted; callers decide what an accepted `end` means.
1125
- const deliverLiveActivityPayload = async (tokens, payload) => {
1142
+ const deliverLiveActivityPayload = async (tokens, payload, bundleIds) => {
1126
1143
  const accepted = [];
1127
1144
  const relay = await resolveRelayConfig();
1128
1145
  if (relay) {
@@ -1131,7 +1148,7 @@ export const createApnsRuntime = (deps) => {
1131
1148
  tokens,
1132
1149
  RELAY_REGISTER_CONCURRENCY,
1133
1150
  async (token) => {
1134
- const registered = await registerLiveActivityTokenWithRelay(token, relay);
1151
+ const registered = await registerLiveActivityTokenWithRelay(token, relay, bundleIds.get(token));
1135
1152
  return registered ? token : null;
1136
1153
  },
1137
1154
  );
@@ -1140,7 +1157,9 @@ export const createApnsRuntime = (deps) => {
1140
1157
  accepted.push(...await sendLiveActivityViaRelay(readyTokens, payload, relay));
1141
1158
  }
1142
1159
  } else {
1143
- accepted.push(...await sendLiveActivityViaDirectApns(tokens, payload));
1160
+ const config = await resolveApnsConfig();
1161
+ const compatible = tokens.filter((token) => !bundleIds.get(token) || bundleIds.get(token) === config?.bundleId);
1162
+ accepted.push(...await sendLiveActivityViaDirectApns(compatible, payload));
1144
1163
  }
1145
1164
  return accepted;
1146
1165
  };
@@ -1224,6 +1243,7 @@ export const createApnsRuntime = (deps) => {
1224
1243
  }
1225
1244
 
1226
1245
  const updatedAtSeconds = now / 1000;
1246
+ const bundleIds = new Map(entries.map((entry) => [entry.token, entry.bundleId]));
1227
1247
  const tokensToEnd = [];
1228
1248
  for (const group of groups.values()) {
1229
1249
  const working = group.items.filter((item) => liveActivityItemIsWorking(item.status));
@@ -1244,7 +1264,7 @@ export const createApnsRuntime = (deps) => {
1244
1264
  };
1245
1265
  let accepted = [];
1246
1266
  try {
1247
- accepted = await deliverLiveActivityPayload(group.tokens, payload);
1267
+ accepted = await deliverLiveActivityPayload(group.tokens, payload, bundleIds);
1248
1268
  } catch (error) {
1249
1269
  console.warn('[Live Activity] refresh failed:', error?.message ?? error);
1250
1270
  continue;
@@ -1351,7 +1371,7 @@ export const createApnsRuntime = (deps) => {
1351
1371
  const tokens = pendingEntries.map((entry) => entry.token);
1352
1372
  let accepted = [];
1353
1373
  try {
1354
- accepted = await deliverLiveActivityPayload(tokens, payload);
1374
+ accepted = await deliverLiveActivityPayload(tokens, payload, new Map(pendingEntries.map((entry) => [entry.token, entry.bundleId])));
1355
1375
  } catch (error) {
1356
1376
  console.warn('[Live Activity] end failed:', error?.message ?? error);
1357
1377
  return;
@@ -1378,7 +1398,7 @@ export const createApnsRuntime = (deps) => {
1378
1398
  const outcomes = await mapWithBoundedConcurrency(
1379
1399
  entries,
1380
1400
  RELAY_REGISTER_CONCURRENCY,
1381
- (entry) => registerTokenWithRelay(entry.deviceToken, entry.platform, relay),
1401
+ (entry) => registerTokenWithRelay(entry.deviceToken, entry.platform, relay, entry.bundleId),
1382
1402
  );
1383
1403
  let succeeded = 0;
1384
1404
  let failed = 0;
@@ -1419,7 +1439,7 @@ export const createApnsRuntime = (deps) => {
1419
1439
  uniqueEntries,
1420
1440
  RELAY_REGISTER_CONCURRENCY,
1421
1441
  async (entry) => {
1422
- const registered = await registerTokenWithRelay(entry.deviceToken, entry.platform, relay);
1442
+ const registered = await registerTokenWithRelay(entry.deviceToken, entry.platform, relay, entry.bundleId);
1423
1443
  return registered ? entry : null;
1424
1444
  },
1425
1445
  );
@@ -1436,7 +1456,8 @@ export const createApnsRuntime = (deps) => {
1436
1456
  }
1437
1457
  return;
1438
1458
  }
1439
- const byLocale = groupTokensByLocale(uniqueEntries);
1459
+ const config = await resolveApnsConfig();
1460
+ const byLocale = groupTokensByLocale(uniqueEntries.filter((entry) => !entry.bundleId || entry.bundleId === config?.bundleId));
1440
1461
  for (const [locale, deviceTokens] of byLocale) {
1441
1462
  await sendViaDirectApns(deviceTokens, localizeApnsPayload(payload, locale));
1442
1463
  }