@openchambery/web 2.0.0-beta.23 → 2.0.0-beta.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{AssistantView-xcvH8rjG.js → AssistantView-qn9Hipja.js} +3 -3
- package/dist/assets/{DiagramView-DNG8vo4Y.js → DiagramView-BJxfEzpH.js} +1 -1
- package/dist/assets/{MarkdownRenderer-CEJAAr1D.js → MarkdownRenderer-DHo-HJTP.js} +2 -2
- package/dist/assets/{MarkdownRendererImpl-Cr2TAqhw.js → MarkdownRendererImpl-Cyx24H_y.js} +1 -1
- package/dist/assets/{MarkstreamRendererImpl-nZuUzTxY.js → MarkstreamRendererImpl-DKT8cFlu.js} +1 -1
- package/dist/assets/{MobileDetailNavigation-DtKMM3yi.js → MobileDetailNavigation-C1Pfx1ab.js} +1 -1
- package/dist/assets/{MobileShareBridge-B7WXWIu6.js → MobileShareBridge-yzsL3AUy.js} +1 -1
- package/dist/assets/{MobileSurface-DpWdHT3N.js → MobileSurface-DOn29tPs.js} +1 -1
- package/dist/assets/{MultiRunWindow-BUKkRUqu.js → MultiRunWindow-CZPkrJUM.js} +1 -1
- package/dist/assets/{SettingsView-8E-wbaVn.js → SettingsView-BMqMEWMN.js} +1 -1
- package/dist/assets/{SettingsWindow-Bqmgbx0M.js → SettingsWindow-CekCsYBw.js} +1 -1
- package/dist/assets/{TerminalView-68VbSeR-.js → TerminalView-k9o96CEf.js} +1 -1
- package/dist/assets/{ToolOutputDialog-CVK4WsvG.js → ToolOutputDialog-CEtY95oc.js} +1 -1
- package/dist/assets/{appThemeRegistry-9e16JGaU.js → appThemeRegistry-DbhqYhdg.js} +1 -1
- package/dist/assets/{fileMentionTouchSelection-Dn-wohRQ.js → fileMentionTouchSelection-iDGupHSi.js} +1 -1
- package/dist/assets/{gitApi-BA9aM1SG.js → gitApi-BnEbIrdK.js} +1 -1
- package/dist/assets/{insertionBoundaries-CI9ok09h.js → insertionBoundaries-By_qiwC0.js} +1 -1
- package/dist/assets/{main-CcAGHLHU.js → main-CaXU4OGJ.js} +4 -4
- package/dist/assets/{main-DgAK4oel.js → main-D8lh8zYp.js} +2 -2
- package/dist/assets/{mcpQueries-2M1PZ3lm.js → mcpQueries-BmqKC8WC.js} +1 -1
- package/dist/assets/{miniChat-Cc9LRMjb.js → miniChat-zZJBQLvR.js} +2 -2
- package/dist/assets/{mobile-D85ParE1.js → mobile-DeaWxGOe.js} +2 -2
- package/dist/assets/{multirun-DvtrTdrM.js → multirun-CmuIahrn.js} +1 -1
- package/dist/assets/{projectMeta-D_YBAmPw.js → projectMeta-bhpIcoPU.js} +1 -1
- package/dist/assets/{renderElectronMiniChatApp-29E_I_Wu.js → renderElectronMiniChatApp-t8FY3CLa.js} +2 -2
- package/dist/assets/{renderMobileApp-VE8moPSo.js → renderMobileApp-BYbN4f39.js} +4 -4
- package/dist/assets/{runtimeConfig-CZrkgYcD.js → runtimeConfig-D0osnvg3.js} +1 -1
- package/dist/assets/{runtimeEndpointReset-B71dh9nk.js → runtimeEndpointReset-WWmoRWPS.js} +1 -1
- package/dist/assets/{sessionChangeSummary-Dk4PbQaU.js → sessionChangeSummary-BvA1TWzS.js} +1 -1
- package/dist/assets/{sessionLookup-BcQ28pj8.js → sessionLookup-BJFtStrg.js} +1 -1
- package/dist/assets/src-OzbnJsph.css +2 -0
- package/dist/assets/{useAppFontEffects-BIH0h1v7.js → useAppFontEffects-Dc7ZKwKO.js} +6 -6
- package/dist/assets/{useDesktopWindowControlsLayout-DvKPPF8d.js → useDesktopWindowControlsLayout-iq6hANF2.js} +1 -1
- package/dist/assets/{useEffectiveDirectory-BXEpJMOS.js → useEffectiveDirectory-DwBnQQpk.js} +1 -1
- package/dist/assets/{useMobileNavigationStore-Cu3jliDp.js → useMobileNavigationStore-CNtROYYh.js} +5 -5
- package/dist/assets/{useSessionAutoCleanup-CH8TehHH.js → useSessionAutoCleanup-DFLjs5K8.js} +1 -1
- package/dist/assets/{useWorkerHighlightedLines-PqKAaqkW.js → useWorkerHighlightedLines-CMBw7PAo.js} +1 -1
- package/dist/index.html +3 -3
- package/dist/mini-chat.html +3 -3
- package/dist/mobile.html +3 -3
- package/package.json +1 -1
- package/server/lib/relay/DOCUMENTATION.md +1 -0
- package/server/lib/relay/service.js +1 -0
- package/server/lib/relay/service.test.js +13 -0
- package/server/lib/session-title/DOCUMENTATION.md +16 -6
- package/server/lib/session-title/native-runtime.test.js +17 -5
- package/server/lib/session-title/runtime.js +45 -9
- package/dist/assets/src-qYyYBKwA.css +0 -2
package/dist/assets/{useWorkerHighlightedLines-PqKAaqkW.js → useWorkerHighlightedLines-CMBw7PAo.js}
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{o as e}from"./rolldown-runtime-C0FnF6B9.js";import{n as t}from"./runtime-fetch-CiqQd__Y.js";import{En as n,in as r}from"./persistence-CMWO3jFo.js";import{nt as i,rt as a}from"./vendor-base-ui-CnHfUmDN.js";import{n as o}from"./platform-B0p43squ.js";import{v as s}from"./useUIStore-Brsa2mKs.js";import{n as c}from"./vendor-react-DLPElpoQ.js";import{a as l,n as u}from"./vendor-aparajita-capacitor-secure-storage-CjQdwfjS.js";import{f as d,r as f,t as p}from"./button-e8fYWIMM.js";import{n as m}from"./Icon-D7NdMmbi.js";import{i as h,n as g,t as _}from"./tooltip-C64SdRvR.js";import{n as v}from"./useThemeSystem-DMzWVJaW.js";import{n as y}from"./markdownTheme-DM5h1i18.js";import{n as b}from"./MarkdownRenderer-CEJAAr1D.js";import{n as x,o as S,s as C,t as w}from"./markdown-worker-bS5OgGO3.js";import{a as T,i as E,o as D,t as ee}from"./imageSource-BpUsiwFd.js";import{t as te}from"./vendor-tanstack-react-virtual-tlDgH6PG.js";var O=e(a(),1),k=c(),A=i(),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)(m,{name:`record-circle`,className:`h-3.5 w-3.5 text-[var(--status-info)]`,"aria-hidden":`true`}):o===`completed`?(0,A.jsx)(m,{name:`checkbox-circle`,className:`h-3.5 w-3.5 text-[var(--status-success)]`,"aria-hidden":`true`}):(0,A.jsx)(m,{name:`time`,className:`h-3.5 w-3.5 text-muted-foreground`,"aria-hidden":`true`}),t[3]=o,t[4]=u);let p=u,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=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)(h,{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)(g,{side:`left`,sideOffset:6,children:l}),t[10]=l,t[11]=S);let C;t[12]!==x||t[13]!==S?(C=(0,A.jsxs)(_,{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]!==v||t[23]!==b||t[24]!==C?(D=(0,A.jsxs)(`li`,{ref:i,"aria-current":v,className:b,children:[C,E]}),t[20]=i,t[21]=E,t[22]=v,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 ee(e,t??new AbortController().signal),await G(n,t)}finally{n&&T(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=D(a,r);if(E(e))try{i=await me(e.path,n)}catch{let r=await t(`/api/fs/raw`,{method:`GET`,cache:`no-store`,query:{path:e.path},signal:n});if(!r.ok)throw Error(`Image source request failed with status ${r.status}`);i=await r.blob()}else i=await G(e.source,n)}}if(!i||i.size<=0)throw Error(`Image is empty`);let o=(e.mimeType||(i.type&&i.type.startsWith(`image/`)?i.type:``)||B).split(`;`)[0].trim()||B;return{blob:i,filename:U(e.filename,o),mimeType:o}},ge=()=>!o()||u===void 0?!1:u.isPluginAvailable(`OpenChamberMedia`),_e=async(e,t)=>{let{blob:i,filename:a,mimeType:o}=await he(e,t);if(ge()){let e=await de(i);return await V.saveImage({dataBase64:e,mimeType:o,filename:a}),`saved`}if(r()){let e=n()?.vscode;if(e?.saveImage){let t=await W(i);return await e.saveImage({fileName:a,dataUrl:t}),`saved`}}return fe(i,a),`downloaded`},ve=e=>e.replace(/&/g,`&`).replace(/</g,`<`).replace(/>/g,`>`).replace(/"/g,`"`).replace(/'/g,`'`),ye=e=>e?Object.entries(e).map(([e,t])=>t==null?``:`${e.startsWith(`--`)?e:e.replace(/[A-Z]/g,e=>`-${e.toLowerCase()}`)}:${typeof t==`number`?`${t}px`:t};`).join(``):``,K=(e,t,n)=>{let r=e.querySelector(`pre`);r&&(r.style.margin=`0`,r.style.background=`transparent`,r.style.whiteSpace=t?`pre-wrap`:`pre`,t&&(r.style.wordBreak=`break-word`,r.style.overflowWrap=`break-word`));let i=e.querySelector(`code`);if(i){let e=ye(n);e&&i.setAttribute(`style`,`${i.getAttribute(`style`)??``}${e}`)}},be=e=>`<pre><code>${ve(e)}</code></pre>`,xe=e=>{let t=(0,k.c)(21),{code:n,language:r,className:i,style:a,codeStyle:o,wrap:s}=e,c=s!==void 0&&s,{currentTheme:l}=v(),u=O.useRef(null),d;t[0]===l?d=t[1]:(d=y(l),t[0]=l,t[1]=d);let p=d,m,h;t[2]!==n||t[3]!==o||t[4]!==c?(m=()=>{let e=u.current;e&&(e.innerHTML=be(n),K(e,c,o))},h=[n,c,o],t[2]=n,t[3]=o,t[4]=c,t[5]=m,t[6]=h):(m=t[5],h=t[6]),O.useLayoutEffect(m,h);let g,_;t[7]!==n||t[8]!==o||t[9]!==r||t[10]!==c?(g=()=>{let e=u.current;if(!e)return;let t=!0,i=new AbortController;return w(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()}},_=[n,r,c,o],t[7]=n,t[8]=o,t[9]=r,t[10]=c,t[11]=g,t[12]=_):(g=t[11],_=t[12]),O.useEffect(g,_);let b;t[13]===i?b=t[14]:(b=f(`typography-code`,i),t[13]=i,t[14]=b);let x=p,S;t[15]!==a||t[16]!==x?(S={...x,...a},t[15]=a,t[16]=x,t[17]=S):S=t[17];let C;return t[18]!==b||t[19]!==S?(C=(0,A.jsx)(`div`,{ref:u,className:b,style:S}),t[18]=b,t[19]=S,t[20]=C):C=t[20],C},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):C(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{o as e}from"./rolldown-runtime-C0FnF6B9.js";import{n as t}from"./runtime-fetch-CiqQd__Y.js";import{En as n,in as r}from"./persistence-CMWO3jFo.js";import{nt as i,rt as a}from"./vendor-base-ui-CnHfUmDN.js";import{n as o}from"./platform-B0p43squ.js";import{v as s}from"./useUIStore-Brsa2mKs.js";import{n as c}from"./vendor-react-DLPElpoQ.js";import{a as l,n as u}from"./vendor-aparajita-capacitor-secure-storage-CjQdwfjS.js";import{f as d,r as f,t as p}from"./button-e8fYWIMM.js";import{n as m}from"./Icon-D7NdMmbi.js";import{i as h,n as g,t as _}from"./tooltip-C64SdRvR.js";import{n as v}from"./useThemeSystem-DMzWVJaW.js";import{n as y}from"./markdownTheme-DM5h1i18.js";import{n as b}from"./MarkdownRenderer-DHo-HJTP.js";import{n as x,o as S,s as C,t as w}from"./markdown-worker-bS5OgGO3.js";import{a as T,i as E,o as D,t as ee}from"./imageSource-BpUsiwFd.js";import{t as te}from"./vendor-tanstack-react-virtual-tlDgH6PG.js";var O=e(a(),1),k=c(),A=i(),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)(m,{name:`record-circle`,className:`h-3.5 w-3.5 text-[var(--status-info)]`,"aria-hidden":`true`}):o===`completed`?(0,A.jsx)(m,{name:`checkbox-circle`,className:`h-3.5 w-3.5 text-[var(--status-success)]`,"aria-hidden":`true`}):(0,A.jsx)(m,{name:`time`,className:`h-3.5 w-3.5 text-muted-foreground`,"aria-hidden":`true`}),t[3]=o,t[4]=u);let p=u,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=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)(h,{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)(g,{side:`left`,sideOffset:6,children:l}),t[10]=l,t[11]=S);let C;t[12]!==x||t[13]!==S?(C=(0,A.jsxs)(_,{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]!==v||t[23]!==b||t[24]!==C?(D=(0,A.jsxs)(`li`,{ref:i,"aria-current":v,className:b,children:[C,E]}),t[20]=i,t[21]=E,t[22]=v,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 ee(e,t??new AbortController().signal),await G(n,t)}finally{n&&T(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=D(a,r);if(E(e))try{i=await me(e.path,n)}catch{let r=await t(`/api/fs/raw`,{method:`GET`,cache:`no-store`,query:{path:e.path},signal:n});if(!r.ok)throw Error(`Image source request failed with status ${r.status}`);i=await r.blob()}else i=await G(e.source,n)}}if(!i||i.size<=0)throw Error(`Image is empty`);let o=(e.mimeType||(i.type&&i.type.startsWith(`image/`)?i.type:``)||B).split(`;`)[0].trim()||B;return{blob:i,filename:U(e.filename,o),mimeType:o}},ge=()=>!o()||u===void 0?!1:u.isPluginAvailable(`OpenChamberMedia`),_e=async(e,t)=>{let{blob:i,filename:a,mimeType:o}=await he(e,t);if(ge()){let e=await de(i);return await V.saveImage({dataBase64:e,mimeType:o,filename:a}),`saved`}if(r()){let e=n()?.vscode;if(e?.saveImage){let t=await W(i);return await e.saveImage({fileName:a,dataUrl:t}),`saved`}}return fe(i,a),`downloaded`},ve=e=>e.replace(/&/g,`&`).replace(/</g,`<`).replace(/>/g,`>`).replace(/"/g,`"`).replace(/'/g,`'`),ye=e=>e?Object.entries(e).map(([e,t])=>t==null?``:`${e.startsWith(`--`)?e:e.replace(/[A-Z]/g,e=>`-${e.toLowerCase()}`)}:${typeof t==`number`?`${t}px`:t};`).join(``):``,K=(e,t,n)=>{let r=e.querySelector(`pre`);r&&(r.style.margin=`0`,r.style.background=`transparent`,r.style.whiteSpace=t?`pre-wrap`:`pre`,t&&(r.style.wordBreak=`break-word`,r.style.overflowWrap=`break-word`));let i=e.querySelector(`code`);if(i){let e=ye(n);e&&i.setAttribute(`style`,`${i.getAttribute(`style`)??``}${e}`)}},be=e=>`<pre><code>${ve(e)}</code></pre>`,xe=e=>{let t=(0,k.c)(21),{code:n,language:r,className:i,style:a,codeStyle:o,wrap:s}=e,c=s!==void 0&&s,{currentTheme:l}=v(),u=O.useRef(null),d;t[0]===l?d=t[1]:(d=y(l),t[0]=l,t[1]=d);let p=d,m,h;t[2]!==n||t[3]!==o||t[4]!==c?(m=()=>{let e=u.current;e&&(e.innerHTML=be(n),K(e,c,o))},h=[n,c,o],t[2]=n,t[3]=o,t[4]=c,t[5]=m,t[6]=h):(m=t[5],h=t[6]),O.useLayoutEffect(m,h);let g,_;t[7]!==n||t[8]!==o||t[9]!==r||t[10]!==c?(g=()=>{let e=u.current;if(!e)return;let t=!0,i=new AbortController;return w(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()}},_=[n,r,c,o],t[7]=n,t[8]=o,t[9]=r,t[10]=c,t[11]=g,t[12]=_):(g=t[11],_=t[12]),O.useEffect(g,_);let b;t[13]===i?b=t[14]:(b=f(`typography-code`,i),t[13]=i,t[14]=b);let x=p,S;t[15]!==a||t[16]!==x?(S={...x,...a},t[15]=a,t[16]=x,t[17]=S):S=t[17];let C;return t[18]!==b||t[19]!==S?(C=(0,A.jsx)(`div`,{ref:u,className:b,style:S}),t[18]=b,t[19]=S,t[20]=C):C=t[20],C},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):C(e,t||``),q=e=>{if(typeof e==`string`){let t=e.trim();return t.length>0?t:void 0}if(e&&typeof e==`object`){let t=e.patch;if(typeof t==`string`){let e=t.trim();return e.length>0?e:void 0}}},De=e=>{if(!e||typeof e!=`object`)return;let t=q(e.patch)??q(e.diff);if(t)return t;let n=Array.isArray(e.files)?e.files:[];for(let e of n){if(!e||typeof e!=`object`)continue;let t=q(e.patch)??q(e.diff);if(t)return t}},Oe=e=>{if(!e||typeof e!=`string`)return{data:null,isJson:!1};let t=e.trim();if(!t.startsWith(`{`)&&!t.startsWith(`[`)||!t.endsWith(`}`)&&!t.endsWith(`]`)||t.length<2)return{data:null,isJson:!1};try{let e=JSON.parse(t);return typeof e==`object`&&e?{data:e,isJson:!0}:{data:null,isJson:!1}}catch{return{data:null,isJson:!1}}},ke=(e,t,n)=>{let r=Se(e);if((t===`edit`||t===`multiedit`||t===`write`||t===`apply_patch`)&&we(r)&&(r=Te(r)),(t===`edit`||t===`multiedit`||t===`apply_patch`)&&r.trim().length===0){let e=De(n);if(e)return e}return r},Ae=e=>{let t=e.match(/<type>(file|directory)<\/type>/i)?.[1]?.toLowerCase()??`unknown`,n=(e.match(/<content>([\s\S]*?)<\/content>/i)?.[1]??e).replace(/\r\n/g,`
|
|
2
2
|
`).split(`
|
|
3
3
|
`),r=e=>/\(\s*File has more lines\..*offset.*\)/i.test(e.trim());return{type:t,lines:n.map(e=>{let n=e.trim(),i=n.startsWith(`(`)&&n.endsWith(`)`)||r(n);if(t!==`directory`){let t=e.match(/^(\d+):\s?(.*)$/);if(t){let e=t[2],n=e.trim(),i=n.startsWith(`(`)&&n.endsWith(`)`)||r(n);return{lineNumber:i?null:Number(t[1]),text:e,isInfo:i}}}return{lineNumber:null,text:e,isInfo:i}}).filter((e,t,n)=>{if(e.text.trim().length>0)return!0;let r=n[t-1],i=n[t+1],a=!!(r?.isInfo||i?.isInfo),o=e.lineNumber!==null;return!(a&&o)})}},je=(e,t)=>{try{let n=e.trim().split(`
|
|
4
4
|
`).filter(Boolean);if(n.length===0)return null;let r=[];return n.forEach(e=>{let t=e.match(/^(\s*)(.+)$/);if(t){let[,e,n]=t,i=Math.floor(e.length/2),a=!n.endsWith(`/`);r.push({name:n.replace(/\/$/,``),depth:i,isFile:a})}}),(0,A.jsx)(`div`,{className:f(`w-full min-w-0 font-mono space-y-0.5`,t?.unstyled?null:`p-3 bg-muted/20 rounded-xl border border-border/30`),style:s.tool.popup,children:r.map((e,t)=>(0,A.jsx)(`div`,{className:`min-w-0`,style:{paddingLeft:`${e.depth*20}px`},children:e.isFile?(0,A.jsx)(`span`,{className:`text-foreground/90 block truncate`,children:e.name}):(0,A.jsxs)(`span`,{className:`font-semibold text-foreground block truncate`,children:[e.name,`/`]})},t))})}catch{return null}},Me={backgroundColor:`var(--status-info)`,opacity:.6},Ne=(e,t,n)=>{try{let r=e.trim().split(`
|
package/dist/index.html
CHANGED
|
@@ -552,7 +552,7 @@
|
|
|
552
552
|
opacity: 0.72;
|
|
553
553
|
}
|
|
554
554
|
</style>
|
|
555
|
-
<script type="module" crossorigin src="/assets/main-
|
|
555
|
+
<script type="module" crossorigin src="/assets/main-D8lh8zYp.js"></script>
|
|
556
556
|
<link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-C0FnF6B9.js">
|
|
557
557
|
<link rel="modulepreload" crossorigin href="/assets/runtime-auth-D66yq6ie.js">
|
|
558
558
|
<link rel="modulepreload" crossorigin href="/assets/runtime-switch-BJM2k_uH.js">
|
|
@@ -572,8 +572,8 @@
|
|
|
572
572
|
<link rel="modulepreload" crossorigin href="/assets/terminalApi-CJyWCuc8.js">
|
|
573
573
|
<link rel="modulepreload" crossorigin href="/assets/types-BuiqMmh_.js">
|
|
574
574
|
<link rel="modulepreload" crossorigin href="/assets/capgoAdapter-DCowZ2ab.js">
|
|
575
|
-
<link rel="modulepreload" crossorigin href="/assets/runtimeConfig-
|
|
576
|
-
<link rel="stylesheet" crossorigin href="/assets/src-
|
|
575
|
+
<link rel="modulepreload" crossorigin href="/assets/runtimeConfig-D0osnvg3.js">
|
|
576
|
+
<link rel="stylesheet" crossorigin href="/assets/src-OzbnJsph.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
|
package/dist/mini-chat.html
CHANGED
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
}
|
|
75
75
|
</style>
|
|
76
76
|
|
|
77
|
-
<script type="module" crossorigin src="/assets/miniChat-
|
|
77
|
+
<script type="module" crossorigin src="/assets/miniChat-zZJBQLvR.js"></script>
|
|
78
78
|
<link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-C0FnF6B9.js">
|
|
79
79
|
<link rel="modulepreload" crossorigin href="/assets/runtime-auth-D66yq6ie.js">
|
|
80
80
|
<link rel="modulepreload" crossorigin href="/assets/runtime-switch-BJM2k_uH.js">
|
|
@@ -94,8 +94,8 @@
|
|
|
94
94
|
<link rel="modulepreload" crossorigin href="/assets/terminalApi-CJyWCuc8.js">
|
|
95
95
|
<link rel="modulepreload" crossorigin href="/assets/types-BuiqMmh_.js">
|
|
96
96
|
<link rel="modulepreload" crossorigin href="/assets/capgoAdapter-DCowZ2ab.js">
|
|
97
|
-
<link rel="modulepreload" crossorigin href="/assets/runtimeConfig-
|
|
98
|
-
<link rel="stylesheet" crossorigin href="/assets/src-
|
|
97
|
+
<link rel="modulepreload" crossorigin href="/assets/runtimeConfig-D0osnvg3.js">
|
|
98
|
+
<link rel="stylesheet" crossorigin href="/assets/src-OzbnJsph.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-
|
|
66
|
+
<script type="module" crossorigin src="/assets/mobile-DeaWxGOe.js"></script>
|
|
67
67
|
<link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-C0FnF6B9.js">
|
|
68
68
|
<link rel="modulepreload" crossorigin href="/assets/runtime-auth-D66yq6ie.js">
|
|
69
69
|
<link rel="modulepreload" crossorigin href="/assets/runtime-switch-BJM2k_uH.js">
|
|
@@ -83,8 +83,8 @@
|
|
|
83
83
|
<link rel="modulepreload" crossorigin href="/assets/terminalApi-CJyWCuc8.js">
|
|
84
84
|
<link rel="modulepreload" crossorigin href="/assets/types-BuiqMmh_.js">
|
|
85
85
|
<link rel="modulepreload" crossorigin href="/assets/capgoAdapter-DCowZ2ab.js">
|
|
86
|
-
<link rel="modulepreload" crossorigin href="/assets/runtimeConfig-
|
|
87
|
-
<link rel="stylesheet" crossorigin href="/assets/src-
|
|
86
|
+
<link rel="modulepreload" crossorigin href="/assets/runtimeConfig-D0osnvg3.js">
|
|
87
|
+
<link rel="stylesheet" crossorigin href="/assets/src-OzbnJsph.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
|
@@ -118,6 +118,7 @@ Catalog loaders (`loadProviders` / `loadAgents`) and assistant Query keys gate w
|
|
|
118
118
|
- The tunnel is transparent to the app: adding relay support to a feature should not require the feature to know the relay exists — it goes through the shared runtime transport helpers.
|
|
119
119
|
- The two implementations stay byte-compatible and the wire format is versioned/negotiated so mixed client/host app versions degrade gracefully rather than break. The Layer 1 Relay does not inspect application versions; unsupported Host/Client features are omitted at the endpoints.
|
|
120
120
|
- Host-control reconnect storms are bounded at both ends: the Host does not reset backoff on a brief `open`, and the Relay rate-limits host-control upgrades per IP and per `serverId` (`4029`) without replacing a live control socket.
|
|
121
|
+
- Concurrent service starts recheck the endpoint's active Host after awaiting identity, so each endpoint has one managed Host and stopping the service closes every started Host.
|
|
121
122
|
- Tunneled HTTP/WS always dial `getLocalPort()` on loopback; there is no client-selected target-port override on the host dispatcher.
|
|
122
123
|
|
|
123
124
|
For the operational rules that keep future changes (new WebSocket endpoints, transport refactors, terminal/voice porting) from breaking this, load the `relay-transport` skill.
|
|
@@ -313,6 +313,7 @@ export const createRelayService = ({
|
|
|
313
313
|
}
|
|
314
314
|
const identity = await identityRuntime.getRelayIdentity();
|
|
315
315
|
for (const relayUrl of pending) {
|
|
316
|
+
if (hostClients.has(relayUrl)) continue;
|
|
316
317
|
const hostClient = startRelayHost({
|
|
317
318
|
relayUrl,
|
|
318
319
|
identity,
|
|
@@ -123,6 +123,19 @@ describe('relay service pairing endpoint', () => {
|
|
|
123
123
|
delete process.env.OPENCHAMBER_RELAY_URL;
|
|
124
124
|
});
|
|
125
125
|
|
|
126
|
+
it('starts only one host per endpoint during concurrent startup and stops every started host', async () => {
|
|
127
|
+
const { service, settings } = createTestRelayService({ hasRelayDemand: async () => true });
|
|
128
|
+
await settings.write({ privateRelay: { enabled: true, relayUrl: DEFAULT_RELAY_URL } });
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
await Promise.all([service.reconcile(), service.reconcile()]);
|
|
132
|
+
expect(relayMocks.starts).toEqual([DEFAULT_RELAY_URL]);
|
|
133
|
+
} finally {
|
|
134
|
+
service.stop();
|
|
135
|
+
}
|
|
136
|
+
expect(relayMocks.stops).toEqual(relayMocks.starts);
|
|
137
|
+
});
|
|
138
|
+
|
|
126
139
|
it('appends a new pairing endpoint (multi-relay) while keeping the primary', async () => {
|
|
127
140
|
const { service, settings } = createTestRelayService();
|
|
128
141
|
|
|
@@ -50,18 +50,21 @@ extension-only VS Code retains the limitation below.
|
|
|
50
50
|
assistant reply run concurrently. Generation publishes `isGenerating` via
|
|
51
51
|
the existing Host metadata path, then the final title through `session.updated`.
|
|
52
52
|
Later user admissions/idle events do not regenerate the initial title.
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
If `session.created` was missed, the admission's session read recovers
|
|
54
|
+
an empty/default title with no previous auto-title metadata and starts
|
|
55
|
+
immediately. The legacy first-idle edge remains a fallback when no
|
|
56
|
+
admission was observed.
|
|
57
|
+
A fork title (`(fork #n)`) refreshes on its first newly-admitted user
|
|
58
|
+
message, bypassing inherited title metadata and throttle without waiting
|
|
59
|
+
for the assistant. Legacy message-only events retain the idle fallback.
|
|
57
60
|
If the fork's `session.created` was lost (SSE reconnect gap, server or
|
|
58
61
|
OpenCode restart), the first newly-created user message lazily re-registers
|
|
59
62
|
the pending fork from the session read inside `recordUserActivity` (fork
|
|
60
63
|
title + message created after the fork + activity timestamp not yet past
|
|
61
|
-
|
|
64
|
+
the fork time), so the first-send refresh still fires.
|
|
62
65
|
Ordinary later idle transitions do **not** arm another refresh. Any
|
|
63
66
|
`busy`/`retry` status or a fresh user `message.updated` still clears an
|
|
64
|
-
|
|
67
|
+
ordinary already-armed timer; admitted initial/fork-title timers are exempt.
|
|
65
68
|
A sidebar smart-title request sets `titleRefresh.requestedAt`; its
|
|
66
69
|
`session.updated` event arms the same flow immediately (forced / manual
|
|
67
70
|
refresh is unaffected by the background gate).
|
|
@@ -141,6 +144,13 @@ auto updates for that session.
|
|
|
141
144
|
Title language follows the user's real message text. Assistant responses, tool
|
|
142
145
|
output, and transcript labels provide context without selecting the title language.
|
|
143
146
|
|
|
147
|
+
Attachments do not gate title generation. Admission retains user text plus at
|
|
148
|
+
most eight bounded filename/MIME descriptors, never attachment bytes or URLs.
|
|
149
|
+
Attachment-only messages also start generation; the text-only summary model can
|
|
150
|
+
name the attachment subject from those descriptors, but is explicitly told that
|
|
151
|
+
file contents are not included. Transcript-based/manual generation uses the
|
|
152
|
+
same file descriptors. This does not perform image recognition or file parsing.
|
|
153
|
+
|
|
144
154
|
- Lives in the web server, so VS Code (extension-only) does not generate
|
|
145
155
|
refreshes; it still receives `session.updated` title changes produced by a
|
|
146
156
|
web/desktop instance of the same OpenCode server.
|
|
@@ -110,10 +110,12 @@ it('uses the real v2 client and publishes the generated title without another se
|
|
|
110
110
|
}
|
|
111
111
|
});
|
|
112
112
|
|
|
113
|
-
it('
|
|
113
|
+
it.each(['new', 'missed-create', 'fork', 'missed-fork-create'].flatMap((scenario) =>
|
|
114
|
+
['text', 'text-and-file', 'file-only'].map((content) => ({ scenario, content })),
|
|
115
|
+
))('starts the first title before assistant completion ($scenario, $content)', async ({ scenario, content }) => {
|
|
114
116
|
vi.useFakeTimers();
|
|
115
117
|
const session = {
|
|
116
|
-
id: 'ses_first', title: '', time: { created: 1, updated: 1 },
|
|
118
|
+
id: 'ses_first', title: scenario.includes('fork') ? 'Original (fork #1)' : '', time: { created: 1, updated: 1 },
|
|
117
119
|
metadata: {},
|
|
118
120
|
};
|
|
119
121
|
let finishTitle;
|
|
@@ -135,26 +137,36 @@ it('starts the first title from admitted user text while the assistant is still
|
|
|
135
137
|
});
|
|
136
138
|
const emit = (type, data) => runtime.processPayload({ type, data, created: 1000, location: { directory: '/repo' } });
|
|
137
139
|
try {
|
|
138
|
-
|
|
140
|
+
if (!scenario.startsWith('missed')) {
|
|
141
|
+
emit('session.created', { sessionID: session.id, title: session.title, time: session.time, location: { directory: '/repo' } });
|
|
142
|
+
}
|
|
139
143
|
emit('session.inbox.enqueued', { sessionID: session.id, inboxID: 'msg_synthetic', item: {
|
|
140
144
|
type: 'synthetic', delivery: 'steer', payload: { text: 'Internal instructions' },
|
|
141
145
|
} });
|
|
142
146
|
await vi.runAllTimersAsync();
|
|
143
147
|
expect(generate).not.toHaveBeenCalled();
|
|
144
148
|
emit('session.inbox.enqueued', { sessionID: session.id, inboxID: 'msg_first', item: {
|
|
145
|
-
type: 'user', delivery: 'steer', payload: {
|
|
149
|
+
type: 'user', delivery: 'steer', payload: {
|
|
150
|
+
text: content === 'file-only' ? '' : 'Implement a session title generator',
|
|
151
|
+
...(content !== 'text' ? { files: [{ name: 'title-bug.png', mime: 'image/png', source: { type: 'inline' }, data: 'YQ=='.repeat(100_000) }] } : {}),
|
|
152
|
+
},
|
|
146
153
|
} });
|
|
147
154
|
emit('session.execution.started', { sessionID: session.id });
|
|
148
155
|
emit('session.status', { sessionID: session.id, status: { type: 'busy' } });
|
|
149
156
|
await vi.runAllTimersAsync();
|
|
150
157
|
expect(generate).toHaveBeenCalledTimes(1);
|
|
151
|
-
|
|
158
|
+
const prompt = generate.mock.calls[0][0].prompt;
|
|
159
|
+
if (content !== 'file-only') expect(prompt).toContain('Implement a session title generator');
|
|
160
|
+
if (content !== 'text') expect(prompt).toContain('title-bug.png');
|
|
161
|
+
expect(prompt).not.toContain('YQ==');
|
|
162
|
+
expect(prompt.length).toBeLessThan(4000);
|
|
152
163
|
expect(session.metadata.openchamber?.titleRefresh?.isGenerating).toBe(true);
|
|
153
164
|
expect(access.messages).not.toHaveBeenCalled();
|
|
154
165
|
finishTitle({ text: 'Session titles', providerID: 'test', modelID: 'test' });
|
|
155
166
|
await vi.runAllTimersAsync();
|
|
156
167
|
expect(session.title).toBe('Session titles');
|
|
157
168
|
expect(session.metadata.openchamber.titleRefresh.isGenerating).toBeUndefined();
|
|
169
|
+
expect(access.messages).not.toHaveBeenCalled();
|
|
158
170
|
emit('session.execution.succeeded', { sessionID: session.id });
|
|
159
171
|
emit('session.inbox.enqueued', { sessionID: session.id, inboxID: 'msg_second', item: {
|
|
160
172
|
type: 'user', delivery: 'steer', payload: { text: 'Follow up on the implementation' },
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Session title refresh: regenerate the sidebar title from the conversation's
|
|
2
2
|
// MAIN SUBJECT (overall feature / goal), not just the last wrap-up utterance.
|
|
3
3
|
// OpenCode only titles once from the first user message. Auto refresh is sparse
|
|
4
|
-
// for title stability: first
|
|
4
|
+
// for title stability: first user admission of a new session, first send on a
|
|
5
5
|
// fork, plus explicit smart-title / forced refresh. Continuing the same work
|
|
6
6
|
// should keep naming the thing being done — not "commit and push". This
|
|
7
7
|
// watcher throttles to at most one refresh per session every TITLE_THROTTLE_MS
|
|
@@ -201,13 +201,22 @@ const extractUserMessage = (payload) => {
|
|
|
201
201
|
const item = properties.item;
|
|
202
202
|
if (typeof properties.sessionID !== 'string' || item?.type !== 'user') return null;
|
|
203
203
|
const text = typeof item.payload?.text === 'string' ? item.payload.text.slice(0, TRANSCRIPT_USER_CHAR_LIMIT) : '';
|
|
204
|
+
const files = Array.isArray(item.payload?.files) ? item.payload.files.slice(0, 8) : [];
|
|
205
|
+
const parts = [
|
|
206
|
+
...(text.trim() ? [{ type: 'text', text }] : []),
|
|
207
|
+
...files.filter((file) => file && typeof file === 'object').map((file) => ({
|
|
208
|
+
type: 'file',
|
|
209
|
+
filename: typeof file.name === 'string' ? file.name.slice(0, 200) : '',
|
|
210
|
+
mime: typeof file.mime === 'string' ? file.mime.slice(0, 100) : '',
|
|
211
|
+
})),
|
|
212
|
+
];
|
|
204
213
|
return {
|
|
205
214
|
sessionId: properties.sessionID,
|
|
206
215
|
createdAt: payload.created ?? 0,
|
|
207
216
|
directory: payload.location?.directory ?? '',
|
|
208
|
-
initialRecord:
|
|
217
|
+
initialRecord: parts.length ? {
|
|
209
218
|
info: { id: properties.inboxID, role: 'user' },
|
|
210
|
-
parts
|
|
219
|
+
parts,
|
|
211
220
|
} : null,
|
|
212
221
|
};
|
|
213
222
|
}
|
|
@@ -230,7 +239,13 @@ const messagePartsToText = (message) => {
|
|
|
230
239
|
? TRANSCRIPT_ASSISTANT_CHAR_LIMIT
|
|
231
240
|
: TRANSCRIPT_USER_CHAR_LIMIT;
|
|
232
241
|
return parts
|
|
233
|
-
.map((part) =>
|
|
242
|
+
.map((part) => {
|
|
243
|
+
if (part?.type === 'text' && typeof part.text === 'string') return part.text;
|
|
244
|
+
if (part?.type !== 'file') return '';
|
|
245
|
+
const filename = typeof part.filename === 'string' ? part.filename.slice(0, 200) : '';
|
|
246
|
+
const mime = typeof part.mime === 'string' ? part.mime.slice(0, 100) : '';
|
|
247
|
+
return `[Attachment: ${filename || mime || 'file'}${filename && mime ? ` (${mime})` : ''}; contents not included]`;
|
|
248
|
+
})
|
|
234
249
|
.filter(Boolean)
|
|
235
250
|
.join('\n')
|
|
236
251
|
.slice(0, charLimit);
|
|
@@ -495,7 +510,7 @@ export const createSessionTitleRuntime = ({
|
|
|
495
510
|
});
|
|
496
511
|
};
|
|
497
512
|
|
|
498
|
-
const recordUserActivity = async (sessionId, directory, createdAt) => {
|
|
513
|
+
const recordUserActivity = async (sessionId, directory, createdAt, initialRecord) => {
|
|
499
514
|
if (!Number.isFinite(createdAt) || createdAt <= 0) return;
|
|
500
515
|
const session = await openCodeFetch(`/session/${encodeURIComponent(sessionId)}`, { directory })
|
|
501
516
|
.catch(() => null);
|
|
@@ -508,7 +523,7 @@ export const createSessionTitleRuntime = ({
|
|
|
508
523
|
// never learned about the fork. The first newly-created user message on a
|
|
509
524
|
// fork-titled session (message created after the fork, before any activity
|
|
510
525
|
// timestamp advanced past the fork) rebuilds that pending entry so the
|
|
511
|
-
//
|
|
526
|
+
// first admission can trigger the refresh. Idempotent: the normal
|
|
512
527
|
// session.created path already holds a pending entry, and once activity
|
|
513
528
|
// advances past the fork time later messages never re-register.
|
|
514
529
|
const forkCreated = Number(session?.time?.created);
|
|
@@ -526,6 +541,26 @@ export const createSessionTitleRuntime = ({
|
|
|
526
541
|
});
|
|
527
542
|
}
|
|
528
543
|
|
|
544
|
+
// Recover first-send admission even when session.created was missed.
|
|
545
|
+
// Use the admitted text: the message projection may not exist yet.
|
|
546
|
+
if (initialRecord && !stopped && !inflight.has(sessionId) && !initialRefreshes.has(sessionId)
|
|
547
|
+
&& !forkFirstRefreshes.has(sessionId) && !session.parentID && !isSystemOwnedSession(session)) {
|
|
548
|
+
const pendingFork = forkFirstSendPending.get(sessionId);
|
|
549
|
+
const firstForkSend = pendingFork && createdAt > pendingFork.createdAt;
|
|
550
|
+
const untitled = !session.title || isDefaultSessionTitle(session.title);
|
|
551
|
+
if (firstForkSend || (untitled && !meta.generatedAt && !meta.lastAutoTitle)) {
|
|
552
|
+
newSessions.delete(sessionId);
|
|
553
|
+
if (firstForkSend) {
|
|
554
|
+
forkFirstSendPending.delete(sessionId);
|
|
555
|
+
forkFirstRefreshes.add(sessionId);
|
|
556
|
+
} else {
|
|
557
|
+
initialRefreshes.add(sessionId);
|
|
558
|
+
}
|
|
559
|
+
initialUserMessages.set(sessionId, initialRecord);
|
|
560
|
+
armTimer(sessionId, directory, 0);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
529
564
|
const previous = Number(meta.titleRefresh.activityUpdatedAt);
|
|
530
565
|
const activityUpdatedAt = Number.isFinite(previous) ? Math.max(previous, createdAt) : createdAt;
|
|
531
566
|
if (activityUpdatedAt === previous) return;
|
|
@@ -683,7 +718,7 @@ export const createSessionTitleRuntime = ({
|
|
|
683
718
|
return;
|
|
684
719
|
}
|
|
685
720
|
|
|
686
|
-
const latestMessages = await fetchRecentMessages(sessionId, directory);
|
|
721
|
+
const latestMessages = initialUserMessage ? null : await fetchRecentMessages(sessionId, directory);
|
|
687
722
|
const latestAssistantId = (() => {
|
|
688
723
|
if (!latestMessages) return null;
|
|
689
724
|
for (let i = latestMessages.length - 1; i >= 0; i -= 1) {
|
|
@@ -777,7 +812,7 @@ export const createSessionTitleRuntime = ({
|
|
|
777
812
|
initialRefreshes.add(status.sessionId);
|
|
778
813
|
armTimer(status.sessionId, status.directory || directoryHint, 0);
|
|
779
814
|
}
|
|
780
|
-
} else if (!initialRefreshes.has(status.sessionId)) {
|
|
815
|
+
} else if (!initialRefreshes.has(status.sessionId) && !forkFirstRefreshes.has(status.sessionId)) {
|
|
781
816
|
clearTimer(status.sessionId);
|
|
782
817
|
}
|
|
783
818
|
return;
|
|
@@ -797,6 +832,7 @@ export const createSessionTitleRuntime = ({
|
|
|
797
832
|
userMessage.sessionId,
|
|
798
833
|
userMessage.directory || directoryHint,
|
|
799
834
|
userMessage.createdAt,
|
|
835
|
+
userMessage.initialRecord,
|
|
800
836
|
).catch((error) => {
|
|
801
837
|
console.warn('[session-title] failed to record user activity:', error?.message || error);
|
|
802
838
|
});
|
|
@@ -804,7 +840,7 @@ export const createSessionTitleRuntime = ({
|
|
|
804
840
|
// session settles. Only a message created after the timer was armed
|
|
805
841
|
// means the user actually moved on.
|
|
806
842
|
const armed = timers.get(userMessage.sessionId);
|
|
807
|
-
if (armed && !initialRefreshes.has(userMessage.sessionId) && userMessage.createdAt >= armed.armedAt) {
|
|
843
|
+
if (armed && !initialRefreshes.has(userMessage.sessionId) && !forkFirstRefreshes.has(userMessage.sessionId) && userMessage.createdAt >= armed.armedAt) {
|
|
808
844
|
clearTimer(userMessage.sessionId);
|
|
809
845
|
}
|
|
810
846
|
}
|