@openchambery/web 2.1.10-beta.10 → 2.1.10-beta.12
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/{AgentAvatar-HkvwOX6E.js → AgentAvatar-D8ipj0Vr.js} +1 -1
- package/dist/assets/AssistantView-o_2FGmWi.js +3 -0
- package/dist/assets/DiagramView-BkIY1i8Z.js +1 -0
- package/dist/assets/{FadeInOnReveal-BKvNCFE0.js → FadeInOnReveal-CUnBy-rf.js} +1 -1
- package/dist/assets/{Icon-x0XTu231.js → Icon-DJOJhmkK.js} +1 -1
- package/dist/assets/{ImageLoadingPlaceholder-Da1xKjxc.js → ImageLoadingPlaceholder-BfOy_RFL.js} +1 -1
- package/dist/assets/{LoadingImage-DO_V7mRh.js → LoadingImage-DAp7ygoZ.js} +1 -1
- package/dist/assets/MarkdownRenderer-CVFTXv6T.js +2 -0
- package/dist/assets/MarkdownRendererImpl-xDJ3_wfo.js +10 -0
- package/dist/assets/MarkstreamRendererImpl-CARsxzOk.js +3 -0
- package/dist/assets/MobileDetailNavigation-C7XoCy6_.js +1 -0
- package/dist/assets/MobileShareBridge-0q0bC1g-.js +4 -0
- package/dist/assets/MobileTabPageHeader-BfA3Blg2.js +2 -0
- package/dist/assets/{MultiRunWindow-CAzMa1Nm.js → MultiRunWindow-DEzF6uXQ.js} +1 -1
- package/dist/assets/OnboardingScreen-C2l09plq.js +1 -0
- package/dist/assets/{OverlayScrollbar-DYyjRbut.js → OverlayScrollbar-Dr5XwsW5.js} +1 -1
- package/dist/assets/{SettingsView-BSKkzr8d.js → SettingsView-D75F19Z9.js} +9 -9
- package/dist/assets/{SettingsWindow-BGmZ-fkc.js → SettingsWindow-DyBoEjPj.js} +1 -1
- package/dist/assets/TerminalView-DRgyfXdO.js +8 -0
- package/dist/assets/ToolOutputDialog-C1c4G7q4.css +1 -0
- package/dist/assets/ToolOutputDialog-KoIg0zrK.js +10 -0
- package/dist/assets/{appThemeRegistry-w-Ebbmh5.js → appThemeRegistry-D7s0BCM_.js} +1 -1
- package/dist/assets/assistant-attachment-cache-dU-EaPYt.js +1 -0
- package/dist/assets/{button-Ca8Z9cOP.js → button--NSU9MZj.js} +1 -1
- package/dist/assets/{collapsible-P03Lkmyr.js → collapsible-B9JDgD12.js} +1 -1
- package/dist/assets/{desktopHosts-C0e7Nes2.js → desktopHosts-C4ejHJ9p.js} +1 -1
- package/dist/assets/desktopRelayRestore-UTCV51RC.js +1 -0
- package/dist/assets/{diagram-DIRYGkQv.js → diagram-Xy80Pnhl.js} +1 -1
- package/dist/assets/{dialog-DKsOQO4Y.js → dialog-DMF6va3u.js} +1 -1
- package/dist/assets/dropdown-menu.styles-DSndmPAq.js +1 -0
- package/dist/assets/{fileMentionTouchSelection-CtLKSOq0.js → fileMentionTouchSelection-CagpeffM.js} +1 -1
- package/dist/assets/{gitApi-D3JDHCiI.js → gitApi-BVwik90d.js} +3 -3
- package/dist/assets/image-loading-BbXaWHv9.js +1 -0
- package/dist/assets/{input-CGZVLev1.js → input-C0EI9bKy.js} +1 -1
- package/dist/assets/insertionBoundaries-B1UojqLv.js +1 -0
- package/dist/assets/main-2yJj_50W.js +2 -0
- package/dist/assets/main-B-JMz9Zg.js +169 -0
- package/dist/assets/{markdownHeightCache-CYwP7eeh.js → markdownHeightCache-_0n435fL.js} +1 -1
- package/dist/assets/mcpQueries-CBvyBo4z.js +1 -0
- package/dist/assets/miniChat-QLQvDR18.js +2 -0
- package/dist/assets/mobile-D2W9Os9Y.js +2 -0
- package/dist/assets/{mobileBackNavigation-CGH0aRvP.js → mobileBackNavigation-DUcdCT-q.js} +1 -1
- package/dist/assets/multirun-BNXvhpnK.js +3 -0
- package/dist/assets/persistence-CAKBi4bp.js +3 -0
- package/dist/assets/platform-jaHE0AyQ.js +7 -0
- package/dist/assets/projectMeta-B7-8QrCr.js +1 -0
- package/dist/assets/queryRuntime-DRiRiAgZ.js +1 -0
- package/dist/assets/renderElectronMiniChatApp-cXQ0iiAs.js +2 -0
- package/dist/assets/renderMobileApp-BZ-dHkIL.js +28 -0
- package/dist/assets/runtime-file-download-CzOWaZO_.js +1 -0
- package/dist/assets/runtime-file-resource-WtDqIbiH.js +2 -0
- package/dist/assets/runtimeConfig-WZniSuc4.js +2 -0
- package/dist/assets/runtimeEndpointReset-BdC1LMDQ.js +149 -0
- package/dist/assets/{scheduledTaskNavigation-CgctKy2R.js → scheduledTaskNavigation-CUbKrS_Q.js} +1 -1
- package/dist/assets/sessionChangeSummary-FnRoTUCa.js +1 -0
- package/dist/assets/{sessionLookup-CZWffaVD.js → sessionLookup-Dyf9EV-B.js} +1 -1
- package/dist/assets/{sortable-tabs-strip-MZyRcU60.js → sortable-tabs-strip-ubL98Ysr.js} +1 -1
- package/dist/assets/speechSettingsQueries-B5rRSSZa.js +4 -0
- package/dist/assets/src-DvTg6DjE.css +2 -0
- package/dist/assets/terminalApi-CK8s1iYr.js +1 -0
- package/dist/assets/{textarea-BfNgh87h.js → textarea-D2NwOmCU.js} +1 -1
- package/dist/assets/{tooltip-B7cKvIho.js → tooltip-CRvh1CJe.js} +1 -1
- package/dist/assets/{url-nBOUbtV_.js → url-BD7mCWgV.js} +1 -1
- package/dist/assets/{useAppFontEffects-CN_sjO4l.js → useAppFontEffects-CICX6ccA.js} +51 -51
- package/dist/assets/{useDesktopWindowControlsLayout-QxfqckBX.js → useDesktopWindowControlsLayout-DPp8-6hC.js} +1 -1
- package/dist/assets/{useEffectiveDirectory-DXbRKe4_.js → useEffectiveDirectory-CzkOEaY-.js} +1 -1
- package/dist/assets/{useFontPreferences-rSp7tOs0.js → useFontPreferences-BLwhdOar.js} +1 -1
- package/dist/assets/useMobileNavigationStore-CeMnArss.js +417 -0
- package/dist/assets/{useWorkerHighlightedLines-hl5s_wXd.js → useWorkerHighlightedLines-DxLthSDl.js} +1 -1
- package/dist/index.html +13 -10
- package/dist/mini-chat.html +13 -10
- package/dist/mobile.html +13 -10
- package/package.json +1 -1
- package/server/lib/fs/DOCUMENTATION.md +4 -1
- package/server/lib/fs/image-fingerprint.d.ts +2 -0
- package/server/lib/fs/image-fingerprint.js +43 -0
- package/server/lib/fs/image-fingerprint.test.js +54 -0
- package/server/lib/fs/raw-video.test.js +34 -0
- package/server/lib/fs/routes.js +16 -6
- package/server/lib/quota/DOCUMENTATION.md +2 -0
- package/server/lib/quota/magpie.js +3 -0
- package/server/lib/quota/magpie.test.js +14 -0
- package/server/lib/relay/relay-server.e2e.test.ts +8 -0
- package/dist/assets/AssistantView-CPPxqPo7.js +0 -3
- package/dist/assets/DiagramView-CD0JRf9L.js +0 -1
- package/dist/assets/MarkdownRenderer-BZZFoaPb.js +0 -2
- package/dist/assets/MarkdownRendererImpl-BwBPrsxq.js +0 -10
- package/dist/assets/MarkstreamRendererImpl-BeCY-mui.js +0 -3
- package/dist/assets/MobileDetailNavigation-tDiUj5U0.js +0 -1
- package/dist/assets/MobileShareBridge-iApPj95w.js +0 -4
- package/dist/assets/MobileTabPageHeader-DOpvXyuR.js +0 -2
- package/dist/assets/OnboardingScreen-TFogoeFF.js +0 -1
- package/dist/assets/TerminalView-BnHTLMST.js +0 -8
- package/dist/assets/ToolOutputDialog-DAVZmdoA.css +0 -1
- package/dist/assets/ToolOutputDialog-Dm4ljddO.js +0 -10
- package/dist/assets/assistant-attachment-cache-CecNXB7z.js +0 -1
- package/dist/assets/desktopRelayRestore-DckkHKsi.js +0 -1
- package/dist/assets/dropdown-menu.styles-vl6a_Aes.js +0 -1
- package/dist/assets/fileQueries-DVpZC5ha.js +0 -1
- package/dist/assets/image-loading-DImQz00b.js +0 -1
- package/dist/assets/insertionBoundaries-JP72UEgW.js +0 -1
- package/dist/assets/main-BnRW5v-z.js +0 -2
- package/dist/assets/main-Vubfe9XA.js +0 -169
- package/dist/assets/mcpQueries-B__Ge5kV.js +0 -1
- package/dist/assets/miniChat-DGxEvelP.js +0 -2
- package/dist/assets/mobile-eGR6pZsC.js +0 -2
- package/dist/assets/multirun-D0s4kPUs.js +0 -3
- package/dist/assets/persistence-BKszKaoQ.js +0 -3
- package/dist/assets/platform-QykS5eAQ.js +0 -7
- package/dist/assets/projectMeta-DPvDDfM0.js +0 -1
- package/dist/assets/relay-image-stream-CqgxIcA5.js +0 -2
- package/dist/assets/renderElectronMiniChatApp-DoJOOOvp.js +0 -2
- package/dist/assets/renderMobileApp-D8ARkO58.js +0 -28
- package/dist/assets/runtime-fetch-WgH0pROs.js +0 -1
- package/dist/assets/runtimeConfig-D0fGrt74.js +0 -2
- package/dist/assets/runtimeEndpointReset-asSGzNnU.js +0 -149
- package/dist/assets/sessionChangeSummary-BTJ_oAdX.js +0 -1
- package/dist/assets/speechSettingsQueries-D9HkL9-8.js +0 -4
- package/dist/assets/src-Doy5O-1o.css +0 -2
- package/dist/assets/terminalApi-BeYpC7rG.js +0 -1
- package/dist/assets/useMobileNavigationStore-BvJJ9SP1.js +0 -417
- package/dist/assets/useRuntimeAPIs-Cx8S1HLz.js +0 -1
- package/dist/assets/vendor-legendapp-list-CNxU_ALy.js +0 -1
package/dist/assets/{useWorkerHighlightedLines-hl5s_wXd.js → useWorkerHighlightedLines-DxLthSDl.js}
RENAMED
|
@@ -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-CEqU9Ujv.js";import{n as i}from"./vendor-react-BP2cRVqX.js";import{f as a,r as o,t as s}from"./button
|
|
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-CEqU9Ujv.js";import{n as i}from"./vendor-react-BP2cRVqX.js";import{f as a,r as o,t as s}from"./button--NSU9MZj.js";import{n as c}from"./useThemeSystem-8jhE4Zyk.js";import{n as l}from"./Icon-DJOJhmkK.js";import{i as u,n as d,t as f}from"./tooltip-CRvh1CJe.js";import{n as p}from"./MarkdownRenderer-CVFTXv6T.js";import{n as m,o as h,s as g,t as _}from"./markdown-worker-DFNZ3KJ_.js";import{t as v}from"./vendor-tanstack-react-virtual-B_EO--9t.js";import{n as y}from"./markdownTheme-BUfvCGAn.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,`&`).replace(/</g,`<`).replace(/>/g,`>`).replace(/"/g,`"`).replace(/'/g,`'`),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
|
@@ -552,28 +552,31 @@
|
|
|
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-2yJj_50W.js"></script>
|
|
556
556
|
<link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-hePW80VL.js">
|
|
557
557
|
<link rel="modulepreload" crossorigin href="/assets/runtime-auth-BpCQ-dPr.js">
|
|
558
558
|
<link rel="modulepreload" crossorigin href="/assets/runtime-switch-CMiMO5Yj.js">
|
|
559
|
-
<link rel="modulepreload" crossorigin href="/assets/
|
|
559
|
+
<link rel="modulepreload" crossorigin href="/assets/vendor-tanstack-query-core-BWz0ose6.js">
|
|
560
|
+
<link rel="modulepreload" crossorigin href="/assets/queryRuntime-DRiRiAgZ.js">
|
|
560
561
|
<link rel="modulepreload" crossorigin href="/assets/vendor-base-ui-CdzJ6My_.js">
|
|
561
562
|
<link rel="modulepreload" crossorigin href="/assets/vendor-zustand-o69pgTEK.js">
|
|
562
563
|
<link rel="modulepreload" crossorigin href="/assets/useUIStore-CEqU9Ujv.js">
|
|
563
564
|
<link rel="modulepreload" crossorigin href="/assets/vendor-react-BP2cRVqX.js">
|
|
564
565
|
<link rel="modulepreload" crossorigin href="/assets/vendor-pathe-vBmvQE8f.js">
|
|
565
|
-
<link rel="modulepreload" crossorigin href="/assets/
|
|
566
|
-
<link rel="modulepreload" crossorigin href="/assets/
|
|
567
|
-
<link rel="modulepreload" crossorigin href="/assets/
|
|
568
|
-
<link rel="modulepreload" crossorigin href="/assets/desktopRelayRestore-DckkHKsi.js">
|
|
566
|
+
<link rel="modulepreload" crossorigin href="/assets/persistence-CAKBi4bp.js">
|
|
567
|
+
<link rel="modulepreload" crossorigin href="/assets/desktopHosts-C4ejHJ9p.js">
|
|
568
|
+
<link rel="modulepreload" crossorigin href="/assets/desktopRelayRestore-UTCV51RC.js">
|
|
569
569
|
<link rel="modulepreload" crossorigin href="/assets/vendor-opencode-client-MF2GBrFx.js">
|
|
570
|
+
<link rel="modulepreload" crossorigin href="/assets/vendor-tanstack-react-query-CdqCAt40.js">
|
|
570
571
|
<link rel="modulepreload" crossorigin href="/assets/vendor-aparajita-capacitor-secure-storage-xol_6BsB.js">
|
|
571
|
-
<link rel="modulepreload" crossorigin href="/assets/
|
|
572
|
-
<link rel="modulepreload" crossorigin href="/assets/
|
|
572
|
+
<link rel="modulepreload" crossorigin href="/assets/runtime-file-resource-WtDqIbiH.js">
|
|
573
|
+
<link rel="modulepreload" crossorigin href="/assets/platform-jaHE0AyQ.js">
|
|
574
|
+
<link rel="modulepreload" crossorigin href="/assets/terminalApi-CK8s1iYr.js">
|
|
575
|
+
<link rel="modulepreload" crossorigin href="/assets/runtime-file-download-CzOWaZO_.js">
|
|
573
576
|
<link rel="modulepreload" crossorigin href="/assets/types-BuiqMmh_.js">
|
|
574
577
|
<link rel="modulepreload" crossorigin href="/assets/capgoAdapter-KNLHDj7G.js">
|
|
575
|
-
<link rel="modulepreload" crossorigin href="/assets/runtimeConfig-
|
|
576
|
-
<link rel="stylesheet" crossorigin href="/assets/src-
|
|
578
|
+
<link rel="modulepreload" crossorigin href="/assets/runtimeConfig-WZniSuc4.js">
|
|
579
|
+
<link rel="stylesheet" crossorigin href="/assets/src-DvTg6DjE.css">
|
|
577
580
|
</head>
|
|
578
581
|
<body class="h-full bg-background text-foreground">
|
|
579
582
|
<!-- Keep the startup layer outside #root. React replaces every child of
|
package/dist/mini-chat.html
CHANGED
|
@@ -74,28 +74,31 @@
|
|
|
74
74
|
}
|
|
75
75
|
</style>
|
|
76
76
|
|
|
77
|
-
<script type="module" crossorigin src="/assets/miniChat-
|
|
77
|
+
<script type="module" crossorigin src="/assets/miniChat-QLQvDR18.js"></script>
|
|
78
78
|
<link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-hePW80VL.js">
|
|
79
79
|
<link rel="modulepreload" crossorigin href="/assets/runtime-auth-BpCQ-dPr.js">
|
|
80
80
|
<link rel="modulepreload" crossorigin href="/assets/runtime-switch-CMiMO5Yj.js">
|
|
81
|
-
<link rel="modulepreload" crossorigin href="/assets/
|
|
81
|
+
<link rel="modulepreload" crossorigin href="/assets/vendor-tanstack-query-core-BWz0ose6.js">
|
|
82
|
+
<link rel="modulepreload" crossorigin href="/assets/queryRuntime-DRiRiAgZ.js">
|
|
82
83
|
<link rel="modulepreload" crossorigin href="/assets/vendor-base-ui-CdzJ6My_.js">
|
|
83
84
|
<link rel="modulepreload" crossorigin href="/assets/vendor-zustand-o69pgTEK.js">
|
|
84
85
|
<link rel="modulepreload" crossorigin href="/assets/useUIStore-CEqU9Ujv.js">
|
|
85
86
|
<link rel="modulepreload" crossorigin href="/assets/vendor-react-BP2cRVqX.js">
|
|
86
87
|
<link rel="modulepreload" crossorigin href="/assets/vendor-pathe-vBmvQE8f.js">
|
|
87
|
-
<link rel="modulepreload" crossorigin href="/assets/
|
|
88
|
-
<link rel="modulepreload" crossorigin href="/assets/
|
|
89
|
-
<link rel="modulepreload" crossorigin href="/assets/
|
|
90
|
-
<link rel="modulepreload" crossorigin href="/assets/desktopRelayRestore-DckkHKsi.js">
|
|
88
|
+
<link rel="modulepreload" crossorigin href="/assets/persistence-CAKBi4bp.js">
|
|
89
|
+
<link rel="modulepreload" crossorigin href="/assets/desktopHosts-C4ejHJ9p.js">
|
|
90
|
+
<link rel="modulepreload" crossorigin href="/assets/desktopRelayRestore-UTCV51RC.js">
|
|
91
91
|
<link rel="modulepreload" crossorigin href="/assets/vendor-opencode-client-MF2GBrFx.js">
|
|
92
|
+
<link rel="modulepreload" crossorigin href="/assets/vendor-tanstack-react-query-CdqCAt40.js">
|
|
92
93
|
<link rel="modulepreload" crossorigin href="/assets/vendor-aparajita-capacitor-secure-storage-xol_6BsB.js">
|
|
93
|
-
<link rel="modulepreload" crossorigin href="/assets/
|
|
94
|
-
<link rel="modulepreload" crossorigin href="/assets/
|
|
94
|
+
<link rel="modulepreload" crossorigin href="/assets/runtime-file-resource-WtDqIbiH.js">
|
|
95
|
+
<link rel="modulepreload" crossorigin href="/assets/platform-jaHE0AyQ.js">
|
|
96
|
+
<link rel="modulepreload" crossorigin href="/assets/terminalApi-CK8s1iYr.js">
|
|
97
|
+
<link rel="modulepreload" crossorigin href="/assets/runtime-file-download-CzOWaZO_.js">
|
|
95
98
|
<link rel="modulepreload" crossorigin href="/assets/types-BuiqMmh_.js">
|
|
96
99
|
<link rel="modulepreload" crossorigin href="/assets/capgoAdapter-KNLHDj7G.js">
|
|
97
|
-
<link rel="modulepreload" crossorigin href="/assets/runtimeConfig-
|
|
98
|
-
<link rel="stylesheet" crossorigin href="/assets/src-
|
|
100
|
+
<link rel="modulepreload" crossorigin href="/assets/runtimeConfig-WZniSuc4.js">
|
|
101
|
+
<link rel="stylesheet" crossorigin href="/assets/src-DvTg6DjE.css">
|
|
99
102
|
</head>
|
|
100
103
|
<body class="h-full bg-background text-foreground">
|
|
101
104
|
<div id="root" class="h-full">
|
package/dist/mobile.html
CHANGED
|
@@ -63,28 +63,31 @@
|
|
|
63
63
|
})();
|
|
64
64
|
</script>
|
|
65
65
|
|
|
66
|
-
<script type="module" crossorigin src="/assets/mobile-
|
|
66
|
+
<script type="module" crossorigin src="/assets/mobile-D2W9Os9Y.js"></script>
|
|
67
67
|
<link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-hePW80VL.js">
|
|
68
68
|
<link rel="modulepreload" crossorigin href="/assets/runtime-auth-BpCQ-dPr.js">
|
|
69
69
|
<link rel="modulepreload" crossorigin href="/assets/runtime-switch-CMiMO5Yj.js">
|
|
70
|
-
<link rel="modulepreload" crossorigin href="/assets/
|
|
70
|
+
<link rel="modulepreload" crossorigin href="/assets/vendor-tanstack-query-core-BWz0ose6.js">
|
|
71
|
+
<link rel="modulepreload" crossorigin href="/assets/queryRuntime-DRiRiAgZ.js">
|
|
71
72
|
<link rel="modulepreload" crossorigin href="/assets/vendor-base-ui-CdzJ6My_.js">
|
|
72
73
|
<link rel="modulepreload" crossorigin href="/assets/vendor-zustand-o69pgTEK.js">
|
|
73
74
|
<link rel="modulepreload" crossorigin href="/assets/useUIStore-CEqU9Ujv.js">
|
|
74
75
|
<link rel="modulepreload" crossorigin href="/assets/vendor-react-BP2cRVqX.js">
|
|
75
76
|
<link rel="modulepreload" crossorigin href="/assets/vendor-pathe-vBmvQE8f.js">
|
|
76
|
-
<link rel="modulepreload" crossorigin href="/assets/
|
|
77
|
-
<link rel="modulepreload" crossorigin href="/assets/
|
|
78
|
-
<link rel="modulepreload" crossorigin href="/assets/
|
|
79
|
-
<link rel="modulepreload" crossorigin href="/assets/desktopRelayRestore-DckkHKsi.js">
|
|
77
|
+
<link rel="modulepreload" crossorigin href="/assets/persistence-CAKBi4bp.js">
|
|
78
|
+
<link rel="modulepreload" crossorigin href="/assets/desktopHosts-C4ejHJ9p.js">
|
|
79
|
+
<link rel="modulepreload" crossorigin href="/assets/desktopRelayRestore-UTCV51RC.js">
|
|
80
80
|
<link rel="modulepreload" crossorigin href="/assets/vendor-opencode-client-MF2GBrFx.js">
|
|
81
|
+
<link rel="modulepreload" crossorigin href="/assets/vendor-tanstack-react-query-CdqCAt40.js">
|
|
81
82
|
<link rel="modulepreload" crossorigin href="/assets/vendor-aparajita-capacitor-secure-storage-xol_6BsB.js">
|
|
82
|
-
<link rel="modulepreload" crossorigin href="/assets/
|
|
83
|
-
<link rel="modulepreload" crossorigin href="/assets/
|
|
83
|
+
<link rel="modulepreload" crossorigin href="/assets/runtime-file-resource-WtDqIbiH.js">
|
|
84
|
+
<link rel="modulepreload" crossorigin href="/assets/platform-jaHE0AyQ.js">
|
|
85
|
+
<link rel="modulepreload" crossorigin href="/assets/terminalApi-CK8s1iYr.js">
|
|
86
|
+
<link rel="modulepreload" crossorigin href="/assets/runtime-file-download-CzOWaZO_.js">
|
|
84
87
|
<link rel="modulepreload" crossorigin href="/assets/types-BuiqMmh_.js">
|
|
85
88
|
<link rel="modulepreload" crossorigin href="/assets/capgoAdapter-KNLHDj7G.js">
|
|
86
|
-
<link rel="modulepreload" crossorigin href="/assets/runtimeConfig-
|
|
87
|
-
<link rel="stylesheet" crossorigin href="/assets/src-
|
|
89
|
+
<link rel="modulepreload" crossorigin href="/assets/runtimeConfig-WZniSuc4.js">
|
|
90
|
+
<link rel="stylesheet" crossorigin href="/assets/src-DvTg6DjE.css">
|
|
88
91
|
</head>
|
|
89
92
|
<body class="h-full bg-background text-foreground">
|
|
90
93
|
<div id="root" class="h-full"></div>
|
package/package.json
CHANGED
|
@@ -13,7 +13,7 @@ Own filesystem API behavior for the web server runtime, including file operation
|
|
|
13
13
|
- `GET /api/fs/home`
|
|
14
14
|
- `POST /api/fs/mkdir`
|
|
15
15
|
- `GET /api/fs/read`
|
|
16
|
-
- `GET /api/fs/stat` (includes an `isBinary` classification from an 8 KiB file sample)
|
|
16
|
+
- `GET /api/fs/stat` (includes an `isBinary` classification from an 8 KiB file sample). Opt-in `fingerprint=true` returns content `sha256` for files up to 32 MiB, after the existing read policy and realpath checks. `image-fingerprint.js` owns a bounded 128-path memo keyed by device/inode/size/mtimeNs/ctimeNs; hashing uses serialized 256 KiB reads and rejects mutation during the read. Same-size writes restoring mtime still change ctime. Responses are `no-store`; callers must authorize/stat again before using durable bytes. Hashing is local filesystem work, not a second network body download. VS Code reuses this helper after its own path authorization.
|
|
17
17
|
- `GET /api/fs/raw`
|
|
18
18
|
- `GET /api/fs/serve/:path(*)`
|
|
19
19
|
- `POST /api/fs/write`
|
|
@@ -35,6 +35,9 @@ Own filesystem API behavior for the web server runtime, including file operation
|
|
|
35
35
|
- `index.js` no longer owns FS route handlers or FS exec job state.
|
|
36
36
|
|
|
37
37
|
## Notes for contributors
|
|
38
|
+
- Stat includes `mimeType` and `filename`; opt-in SHA-256 is omitted above 32 MiB rather than rejecting otherwise valid files. Empty files can be fingerprinted.
|
|
39
|
+
- `/api/fs/serve` retains its original URL/body/MIME and relative-resource behavior. After existing authorization it emits `Cache-Control: private, no-cache`, `Vary: Authorization, Cookie`, and a weak size/mtime/ctime ETag; `sendFile` streams the body and handles conditional requests. This is browser-private revalidation, not a durable-cache guarantee. Tests cover relative CSS/JS, 304, updates and deletion.
|
|
40
|
+
- `/api/fs/raw` also streams non-video bodies above 32 MiB. Client complete-body persistence is bounded independently; cold Range responses are not published as complete entries.
|
|
38
41
|
- `/api/fs/raw` serves video MIME types (MP4/M4V/MOV/WebM/OGV) using Express's
|
|
39
42
|
streamed `sendFile` after the existing path authorization/realpath checks.
|
|
40
43
|
Explicit Range requests and HEAD use the same path: 206/Content-Range,
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
const MAX_BYTES = 32 * 1024 * 1024;
|
|
4
|
+
const versionOf = (stat) => [stat.dev, stat.ino, stat.size, stat.mtimeNs, stat.ctimeNs].join(':');
|
|
5
|
+
|
|
6
|
+
/** Called only after the owning route has authorized and canonicalized the path. */
|
|
7
|
+
export const createImageFingerprintReader = (fs) => {
|
|
8
|
+
const entries = new Map();
|
|
9
|
+
let tail = Promise.resolve();
|
|
10
|
+
return async (file) => {
|
|
11
|
+
const before = await fs.stat(file, { bigint: true });
|
|
12
|
+
if (!before.isFile() || before.size < 0 || before.size > MAX_BYTES) throw new Error('Invalid file fingerprint size');
|
|
13
|
+
const version = versionOf(before);
|
|
14
|
+
const previous = entries.get(file);
|
|
15
|
+
if (previous?.version === version) return previous.promise;
|
|
16
|
+
const promise = tail.then(async () => {
|
|
17
|
+
const handle = await fs.open(file, 'r');
|
|
18
|
+
try {
|
|
19
|
+
if (versionOf(await handle.stat({ bigint: true })) !== version) throw new Error('Image changed during fingerprint read');
|
|
20
|
+
const hash = createHash('sha256');
|
|
21
|
+
const buffer = Buffer.allocUnsafe(256 * 1024);
|
|
22
|
+
let total = 0;
|
|
23
|
+
for (;;) {
|
|
24
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, null);
|
|
25
|
+
if (!bytesRead) break;
|
|
26
|
+
total += bytesRead;
|
|
27
|
+
if (total > MAX_BYTES) throw new Error('Image exceeds fingerprint limit');
|
|
28
|
+
hash.update(buffer.subarray(0, bytesRead));
|
|
29
|
+
}
|
|
30
|
+
const after = await fs.stat(file, { bigint: true });
|
|
31
|
+
if (versionOf(after) !== version || total !== Number(before.size)) throw new Error('Image changed during fingerprint read');
|
|
32
|
+
return hash.digest('hex');
|
|
33
|
+
} finally { await handle.close(); }
|
|
34
|
+
});
|
|
35
|
+
tail = promise.then(() => undefined, () => undefined);
|
|
36
|
+
const entry = { version, promise };
|
|
37
|
+
entries.delete(file);
|
|
38
|
+
entries.set(file, entry);
|
|
39
|
+
while (entries.size > 128) entries.delete(entries.keys().next().value);
|
|
40
|
+
try { return await promise; }
|
|
41
|
+
catch (error) { if (entries.get(file) === entry) entries.delete(file); throw error; }
|
|
42
|
+
};
|
|
43
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { afterEach, expect, test } from 'vitest';
|
|
2
|
+
import * as fs from 'node:fs/promises';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import express from 'express';
|
|
7
|
+
import { registerFsRoutes } from './routes.js';
|
|
8
|
+
import { createImageFingerprintReader } from './image-fingerprint.js';
|
|
9
|
+
|
|
10
|
+
const cleanups = [];
|
|
11
|
+
afterEach(async () => { for (const cleanup of cleanups.splice(0).reverse()) await cleanup(); });
|
|
12
|
+
const fixture = async () => {
|
|
13
|
+
const directory = await fs.mkdtemp(path.join(os.tmpdir(), 'oc-image-fingerprint-'));
|
|
14
|
+
cleanups.push(() => fs.rm(directory, { recursive: true, force: true }));
|
|
15
|
+
const file = path.join(directory, 'a.png');
|
|
16
|
+
await fs.writeFile(file, 'first');
|
|
17
|
+
return { directory, file };
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
test('fingerprints are content hashes; unchanged files avoid rehash; same-size preserved-mtime writes invalidate', async () => {
|
|
21
|
+
const { file, directory } = await fixture();
|
|
22
|
+
let reads = 0;
|
|
23
|
+
const reader = createImageFingerprintReader({ ...fs, open: (...args) => { reads++; return fs.open(...args); } });
|
|
24
|
+
const first = await reader(file);
|
|
25
|
+
expect(first).toBe(createHash('sha256').update('first').digest('hex'));
|
|
26
|
+
expect(await reader(file)).toBe(first);
|
|
27
|
+
expect(reads).toBe(1);
|
|
28
|
+
const duplicate = path.join(directory, 'copy.png');
|
|
29
|
+
await fs.copyFile(file, duplicate);
|
|
30
|
+
expect(await reader(duplicate)).toBe(first);
|
|
31
|
+
const stat = await fs.stat(file);
|
|
32
|
+
await fs.writeFile(file, 'other');
|
|
33
|
+
await fs.utimes(file, stat.atime, stat.mtime);
|
|
34
|
+
expect(await reader(file)).toBe(createHash('sha256').update('other').digest('hex'));
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('real stat route returns authorized opt-in SHA-256 metadata without image body', async () => {
|
|
38
|
+
const { file, directory } = await fixture();
|
|
39
|
+
const app = express();
|
|
40
|
+
registerFsRoutes({ get: (route, handler) => { if (route === '/api/fs/stat') app.get(route, handler); }, post() {}, put() {} }, {
|
|
41
|
+
os, path, fsPromises: fs, spawn() {}, crypto: globalThis.crypto,
|
|
42
|
+
normalizeDirectoryPath: value => value, resolveProjectDirectory: async () => ({ directory }),
|
|
43
|
+
buildAugmentedPath: () => '', resolveGitBinaryForSpawn: () => 'git', openchamberUserConfigRoot: directory,
|
|
44
|
+
});
|
|
45
|
+
const server = app.listen(0, '127.0.0.1');
|
|
46
|
+
await new Promise(resolve => server.once('listening', resolve));
|
|
47
|
+
cleanups.push(() => new Promise(resolve => { server.close(resolve); server.closeAllConnections(); }));
|
|
48
|
+
const url = `http://127.0.0.1:${server.address().port}/api/fs/stat?path=${encodeURIComponent(file)}`;
|
|
49
|
+
expect((await (await fetch(url)).json()).sha256).toBeUndefined();
|
|
50
|
+
const response = await fetch(`${url}&fingerprint=true`);
|
|
51
|
+
expect(response.headers.get('cache-control')).toBe('no-store');
|
|
52
|
+
expect((await response.json()).sha256).toBe(createHash('sha256').update('first').digest('hex'));
|
|
53
|
+
expect((await fetch(`${url}-missing&fingerprint=true`)).status).toBe(404);
|
|
54
|
+
});
|
|
@@ -8,6 +8,40 @@ import { registerFsRoutes } from './routes.js';
|
|
|
8
8
|
const cleanups = [];
|
|
9
9
|
afterEach(async () => { for (const cleanup of cleanups.splice(0).reverse()) await cleanup(); });
|
|
10
10
|
|
|
11
|
+
test('HTML and relative assets retain URLs and privately revalidate after file authorization', async () => {
|
|
12
|
+
const directory = await fs.mkdtemp(path.join(os.tmpdir(), 'oc-html-revalidate-'));
|
|
13
|
+
cleanups.push(() => fs.rm(directory, { recursive: true, force: true }));
|
|
14
|
+
const html = '<!doctype html><link rel="stylesheet" href="style.css"><script src="app.js"></script><p>original</p>';
|
|
15
|
+
await fs.writeFile(path.join(directory, 'index.html'), html);
|
|
16
|
+
await fs.writeFile(path.join(directory, 'style.css'), 'p { color: red }');
|
|
17
|
+
await fs.writeFile(path.join(directory, 'app.js'), 'window.relativeScriptLoaded = true');
|
|
18
|
+
const app = express();
|
|
19
|
+
registerFsRoutes({ get(route, handler) { if (route instanceof RegExp) app.get(route, handler); }, post() {}, put() {} }, {
|
|
20
|
+
os, path, fsPromises: fs, spawn() {}, crypto: globalThis.crypto,
|
|
21
|
+
normalizeDirectoryPath: value => value, resolveProjectDirectory: async () => ({ directory }),
|
|
22
|
+
buildAugmentedPath: () => '', resolveGitBinaryForSpawn: () => 'git', openchamberUserConfigRoot: directory,
|
|
23
|
+
});
|
|
24
|
+
const server = app.listen(0, '127.0.0.1');
|
|
25
|
+
await new Promise(resolve => server.once('listening', resolve));
|
|
26
|
+
cleanups.push(() => new Promise(resolve => { server.close(resolve); server.closeAllConnections(); }));
|
|
27
|
+
const url = `http://127.0.0.1:${server.address().port}/api/fs/serve${directory}/index.html`;
|
|
28
|
+
const first = await fetch(url);
|
|
29
|
+
expect(first.headers.get('cache-control')).toBe('private, no-cache');
|
|
30
|
+
expect(first.headers.get('vary')).toContain('Authorization');
|
|
31
|
+
expect(first.headers.get('vary')).toContain('Cookie');
|
|
32
|
+
expect(await first.text()).toBe(html);
|
|
33
|
+
const etag = first.headers.get('etag');
|
|
34
|
+
const hit = await fetch(url, { headers: { 'If-None-Match': etag, 'Cache-Control': 'max-age=0' } });
|
|
35
|
+
expect(hit.status).toBe(304);
|
|
36
|
+
expect(await hit.text()).toBe('');
|
|
37
|
+
expect(await (await fetch(new URL('style.css', url))).text()).toBe('p { color: red }');
|
|
38
|
+
expect(await (await fetch(new URL('app.js', url))).text()).toContain('relativeScriptLoaded');
|
|
39
|
+
await fs.writeFile(path.join(directory, 'index.html'), html.replace('original', 'modified'));
|
|
40
|
+
expect((await fetch(url, { headers: { 'If-None-Match': etag } })).status).toBe(200);
|
|
41
|
+
await fs.unlink(path.join(directory, 'index.html'));
|
|
42
|
+
expect((await fetch(url, { headers: { 'If-None-Match': etag } })).status).toBe(404);
|
|
43
|
+
});
|
|
44
|
+
|
|
11
45
|
test('raw video supports first-byte probing, tail seeking, HEAD and unsatisfiable ranges', async () => {
|
|
12
46
|
const directory = await fs.mkdtemp(path.join(os.tmpdir(), 'oc-video-range-'));
|
|
13
47
|
cleanups.push(() => fs.rm(directory, { recursive: true, force: true }));
|
package/server/lib/fs/routes.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
+
import { createImageFingerprintReader } from './image-fingerprint.js';
|
|
2
3
|
import { createRealpathCache } from '../path-realpath-cache.js';
|
|
3
4
|
import nodeFsPromises from 'node:fs/promises';
|
|
4
5
|
import nodePath from 'node:path';
|
|
@@ -17,6 +18,7 @@ const EXEC_JOB_TTL_MS = 30 * 60 * 1000;
|
|
|
17
18
|
const OUTSIDE_FILE_GRANT_TTL_MS = 10 * 60 * 1000;
|
|
18
19
|
|
|
19
20
|
const outsideFileGrants = new Map();
|
|
21
|
+
const readImageFingerprint = createImageFingerprintReader(nodeFsPromises);
|
|
20
22
|
|
|
21
23
|
const pruneOutsideFileGrants = () => {
|
|
22
24
|
const now = Date.now();
|
|
@@ -745,7 +747,11 @@ export const registerFsRoutes = (app, dependencies) => {
|
|
|
745
747
|
}
|
|
746
748
|
|
|
747
749
|
const isBinary = await isBinaryFile(canonicalPath, fsPromises);
|
|
748
|
-
|
|
750
|
+
const sha256 = req.query.fingerprint === 'true' && stats.size <= 32 * 1024 * 1024 ? await readImageFingerprint(canonicalPath) : undefined;
|
|
751
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
752
|
+
return res.json({ path: canonicalPath, isFile: true, size: stats.size, mtimeMs: stats.mtimeMs, isBinary,
|
|
753
|
+
mimeType: FILE_MIME_MAP[path.extname(canonicalPath).toLowerCase()] || 'application/octet-stream', filename: path.basename(canonicalPath),
|
|
754
|
+
...(sha256 ? { sha256 } : {}) });
|
|
749
755
|
} catch (error) {
|
|
750
756
|
const err = error;
|
|
751
757
|
if (err && typeof err === 'object' && err.code === 'ENOENT') {
|
|
@@ -895,7 +901,7 @@ export const registerFsRoutes = (app, dependencies) => {
|
|
|
895
901
|
'.webm': 'video/webm',
|
|
896
902
|
'.ogv': 'video/ogg',
|
|
897
903
|
};
|
|
898
|
-
const mimeType = mimeMap[ext] || 'application/octet-stream';
|
|
904
|
+
const mimeType = mimeMap[ext] || FILE_MIME_MAP[ext] || 'application/octet-stream';
|
|
899
905
|
|
|
900
906
|
const download = req.query.download === 'true';
|
|
901
907
|
if (download) {
|
|
@@ -913,7 +919,7 @@ export const registerFsRoutes = (app, dependencies) => {
|
|
|
913
919
|
if (resolved.granted) {
|
|
914
920
|
res.setHeader('Referrer-Policy', 'no-referrer');
|
|
915
921
|
}
|
|
916
|
-
if (mimeType.startsWith('video/') || req.headers?.range || req.method === 'HEAD') {
|
|
922
|
+
if (stats.size > 32 * 1024 * 1024 || mimeType.startsWith('video/') || req.headers?.range || req.method === 'HEAD') {
|
|
917
923
|
// Path authorization above remains authoritative; Express owns range parsing,
|
|
918
924
|
// 206/416, stream backpressure and cancellation when the player seeks away.
|
|
919
925
|
return res.type(mimeType).sendFile(canonicalPath, { acceptRanges: true, dotfiles: 'allow', cacheControl: false });
|
|
@@ -977,10 +983,14 @@ export const registerFsRoutes = (app, dependencies) => {
|
|
|
977
983
|
|
|
978
984
|
const ext = path.extname(canonicalPath).toLowerCase();
|
|
979
985
|
const mimeType = FILE_MIME_MAP[ext] || 'application/octet-stream';
|
|
980
|
-
|
|
981
|
-
|
|
986
|
+
// Revalidate after the read policy above, including cookie/bearer changes.
|
|
987
|
+
// This is browser-private HTTP reuse, not the client's durable file store.
|
|
988
|
+
res.setHeader('Cache-Control', 'private, no-cache');
|
|
989
|
+
res.vary('Authorization');
|
|
990
|
+
res.vary('Cookie');
|
|
991
|
+
res.setHeader('ETag', `W/"${stats.size}-${stats.mtimeMs}-${stats.ctimeMs}"`);
|
|
982
992
|
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
983
|
-
return res.type(mimeType).
|
|
993
|
+
return res.type(mimeType).sendFile(canonicalPath, { acceptRanges: true, dotfiles: 'allow', cacheControl: false });
|
|
984
994
|
} catch (error) {
|
|
985
995
|
const err = error;
|
|
986
996
|
if (err && typeof err === 'object' && err.code === 'ENOENT') {
|
|
@@ -70,6 +70,8 @@ Each upstream provider gets a namespaced `magpie:<source>:<provider>` result, in
|
|
|
70
70
|
`usage.accounts` retains separate account windows, errors and reading timestamps. `usage.windows`
|
|
71
71
|
is the account-keyed projection for existing header/mobile/tray consumers. Compact surfaces display
|
|
72
72
|
`window.label` rather than the account-bearing key; account detail remains on the full Usage page.
|
|
73
|
+
The known ZCode gift-plan sign-in hint is retained only in account details: upstream serializes
|
|
74
|
+
it as a zero-usage window without its internal `Aside` marker, but it is not a quota metric.
|
|
73
75
|
Opening a usage menu checks the full refresh timestamp, not whether any results exist: a discovered
|
|
74
76
|
Magpie snapshot must not suppress the initial built-in provider fetch. Percentages, balance
|
|
75
77
|
labels, unlimited windows and absolute reset timestamps are preserved. Window duration remains unknown
|
|
@@ -49,6 +49,9 @@ export function projectMagpieQuotas(payload, source, now = Date.now()) {
|
|
|
49
49
|
label: w.name, disablePace: true,
|
|
50
50
|
};
|
|
51
51
|
account.windows[`${i}:${w.name}`] = window;
|
|
52
|
+
// Magpie serializes this informational window as used: 0 without its
|
|
53
|
+
// internal Aside flag. Keep it in details, not the compact quota projection.
|
|
54
|
+
if (q.provider === 'zcode' && w.name === "ZCode gift plans (-Trial models) need ZCode's sign-in") continue;
|
|
52
55
|
const label = `${account.name} · ${w.name}`;
|
|
53
56
|
const key = Object.hasOwn(result.usage.windows, label) ? `${label} (${result.usage.accounts.length + 1}.${i + 1})` : label;
|
|
54
57
|
result.usage.windows[key] = window;
|
|
@@ -54,6 +54,20 @@ describe('Magpie discovery and quota adapter', () => {
|
|
|
54
54
|
expect(Object.values(results[1].usage.windows)[0]).toMatchObject({ valueLabel: '$12.34', usedPercent: null });
|
|
55
55
|
});
|
|
56
56
|
|
|
57
|
+
it('keeps the ZCode sign-in hint in account details instead of compact quota metrics', () => {
|
|
58
|
+
const hint = { name: "ZCode gift plans (-Trial models) need ZCode's sign-in", used: 0,
|
|
59
|
+
display: 'not read with an API key: add the account with a ZCode sign-in' };
|
|
60
|
+
const [result] = projectMagpieQuotas({ object: 'list', data: [{
|
|
61
|
+
provider: 'zcode', name: 'ZCode', kind: 'subscription', windows: [
|
|
62
|
+
{ name: '5 hours', used: 0 },
|
|
63
|
+
{ name: 'MCP · Month', used: 0, display: '0 / 1000', limit: 1000 }, hint,
|
|
64
|
+
],
|
|
65
|
+
}] }, source);
|
|
66
|
+
expect(Object.values(result.usage.windows).map((window) => window.label)).toEqual(['5 hours', 'MCP · Month']);
|
|
67
|
+
expect(Object.values(result.usage.accounts[0].windows).at(-1).valueLabel).toBe(hint.display);
|
|
68
|
+
expect(result.ok).toBe(true);
|
|
69
|
+
});
|
|
70
|
+
|
|
57
71
|
it('isolates account errors and distinguishes unlimited, stale and malformed data', () => {
|
|
58
72
|
const data = structuredClone(report);
|
|
59
73
|
data.data[1].error = 'sensitive upstream response';
|
|
@@ -25,6 +25,7 @@ import { configureRuntimeUrlResolver, getRuntimeUrlResolver, setRuntimeUrlResolv
|
|
|
25
25
|
import { setRuntimeBearerToken } from '../../../../ui/src/lib/runtime-auth.ts';
|
|
26
26
|
import { adoptRelayTunnel, deactivateRelayTunnel } from '../../../../ui/src/lib/relay/runtime-tunnel.ts';
|
|
27
27
|
import { acquireRuntimeImage } from '../../../../ui/src/lib/runtime-image-cache.ts';
|
|
28
|
+
import { materializeImageBlob } from '../../../../ui/src/components/chat/imageSave.ts';
|
|
28
29
|
import { NATIVE_RELAY_ASSET_BRIDGE_KEY } from '../../../../ui/src/lib/relay/native-asset-bridge.ts';
|
|
29
30
|
import { readRelayImageDisplayBlob, resetRelayImageStreamForTests } from '../../../../ui/src/lib/relay/relay-image-stream.ts';
|
|
30
31
|
import { registerFsRoutes } from '../fs/routes.js';
|
|
@@ -634,6 +635,13 @@ describe('private relay compiled-process boundary', () => {
|
|
|
634
635
|
expect(response.headers.get('content-type')).toBe('video/mp4');
|
|
635
636
|
expect(await response.text()).toBe(bytes);
|
|
636
637
|
}
|
|
638
|
+
const downloadProgress: Array<number | null> = [];
|
|
639
|
+
const savedVideo = await materializeImageBlob({ kind: 'video', sourceUrl: `file://${origin.videoPath}` }, undefined, percent => downloadProgress.push(percent));
|
|
640
|
+
expect(await savedVideo.blob.text()).toBe('0123456789');
|
|
641
|
+
expect(downloadProgress[0]).toBe(0);
|
|
642
|
+
expect(downloadProgress).toContain(99);
|
|
643
|
+
expect(downloadProgress.at(-1)).toBe(100);
|
|
644
|
+
expect(downloadProgress).not.toContain(null);
|
|
637
645
|
} finally {
|
|
638
646
|
resetRelayImageStreamForTests();
|
|
639
647
|
Reflect.deleteProperty(globalThis, NATIVE_RELAY_ASSET_BRIDGE_KEY);
|