@nextclaw/ui 0.12.7 → 0.12.8
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/CHANGELOG.md +50 -0
- package/dist/assets/{ChannelsList-D8p4OlM6.js → ChannelsList-KIQIxluX.js} +1 -1
- package/dist/assets/{DocBrowser-Cse_F8Nn.js → DocBrowser-BMxf9CIK.js} +1 -1
- package/dist/assets/DocBrowser-CyDgAtO9.js +1 -0
- package/dist/assets/{DocBrowserContext-Bai1WU2H.js → DocBrowserContext-Ce28gRXt.js} +1 -1
- package/dist/assets/{LogoBadge-BdxMPc9v.js → LogoBadge-o92MOA2L.js} +1 -1
- package/dist/assets/{MarketplacePage-BbpAkllU.js → MarketplacePage-BySqkYDh.js} +1 -1
- package/dist/assets/MarketplacePage-C0olZaek.js +1 -0
- package/dist/assets/{McpMarketplacePage-CxPFOgxv.js → McpMarketplacePage-DqKaiXO9.js} +1 -1
- package/dist/assets/{ModelConfig-3GLqQ5GY.js → ModelConfig-IrmzoslW.js} +1 -1
- package/dist/assets/{ProviderScopedModelInput-BYNouw-i.js → ProviderScopedModelInput-CmTIzgI7.js} +1 -1
- package/dist/assets/{ProvidersList-BR1gJ4Dm.js → ProvidersList-8_Kalfwl.js} +1 -1
- package/dist/assets/{RemoteAccessPage-DyYVWsyK.js → RemoteAccessPage-CyQlSjPf.js} +1 -1
- package/dist/assets/RuntimeConfig-Bk0uYBhf.js +1 -0
- package/dist/assets/{SearchConfig-DTeJvp8m.js → SearchConfig-DNBR-UbE.js} +1 -1
- package/dist/assets/{SecretsConfig-CCYO6NcV.js → SecretsConfig-Ba1RPJaG.js} +1 -1
- package/dist/assets/{SessionsConfig-Du39vDgt.js → SessionsConfig-Doqp5ghH.js} +1 -1
- package/dist/assets/{app-query-client-Dr5d-K8d.js → app-query-client-DniXoIN5.js} +1 -1
- package/dist/assets/{book-open-Da4OEPqB.js → book-open-DocgeQtR.js} +1 -1
- package/dist/assets/chat-page-Bph8M5zo.js +58 -0
- package/dist/assets/chat-session-display-CoN3Wmn-.js +1 -0
- package/dist/assets/{chunk-JZWAC4HX-CoFVxHXV.js → chunk-JZWAC4HX-BvKvh1R8.js} +1 -1
- package/dist/assets/{client-CSk58DcF.js → client-CVqPF5ie.js} +1 -1
- package/dist/assets/{config-D8KzikVB.js → config-Bop2oB18.js} +1 -1
- package/dist/assets/{createLucideIcon-83gaZMtv.js → createLucideIcon-DVv8taGY.js} +1 -1
- package/dist/assets/desktop-update-config-1KBrqLBC.js +1 -0
- package/dist/assets/{dist-toEYs-MZ.js → dist-Da5Gm_pO.js} +1 -1
- package/dist/assets/{dist-aTmhMDVh.js → dist-DmAlInRu.js} +1 -1
- package/dist/assets/{external-link-QQ0TC6X4.js → external-link-DFjw3x1B.js} +1 -1
- package/dist/assets/{hash-DaFBEkmi.js → hash-DJtaCejM.js} +1 -1
- package/dist/assets/i18n-CwHZ-9vt.js +1 -0
- package/dist/assets/{index-CE4N7ItL.css → index-DafCdM4F.css} +1 -1
- package/dist/assets/{index-riX7Sg0_.js → index-DdksE6U3.js} +3 -3
- package/dist/assets/{infiniteQueryBehavior-BmHX_ayZ.js → infiniteQueryBehavior-DHSEQ3OH.js} +1 -1
- package/dist/assets/loader-circle-PsSP0H9n.js +1 -0
- package/dist/assets/{logos-Dzlz30M3.js → logos-DEFUIR12.js} +1 -1
- package/dist/assets/{page-layout-D2eRufRQ.js → page-layout-Da3i3r6G.js} +1 -1
- package/dist/assets/play-DBQbBxTA.js +1 -0
- package/dist/assets/plus-DUOVbsyQ.js +1 -0
- package/dist/assets/{popover-BSXxm5bj.js → popover-C_mWOFzI.js} +1 -1
- package/dist/assets/{refresh-ccw-B3zMtN-_.js → refresh-ccw-D6HkNtfz.js} +1 -1
- package/dist/assets/{refresh-cw-DlZkIHnJ.js → refresh-cw-DRcvRrnc.js} +1 -1
- package/dist/assets/rotate-cw-BmDKfXtH.js +1 -0
- package/dist/assets/{save-Us9fg4Sj.js → save-DHGmi2e9.js} +1 -1
- package/dist/assets/search-MChQRYR1.js +1 -0
- package/dist/assets/{security-config-BGWYwxNr.js → security-config-CbXfPZzr.js} +1 -1
- package/dist/assets/{select-DLYqySQK.js → select-Caud8QvU.js} +1 -1
- package/dist/assets/skeleton-B-4vRq_Z.js +1 -0
- package/dist/assets/{status-dot-DGayudyB.js → status-dot-DurKKSwA.js} +1 -1
- package/dist/assets/{switch-Dz2ScsKx.js → switch-0rmPBRKI.js} +1 -1
- package/dist/assets/{tabs-custom-CdKyjiGk.js → tabs-custom-5JLVL6v8.js} +1 -1
- package/dist/assets/{trash-2-Db-mZOZs.js → trash-2-C6caKPoz.js} +1 -1
- package/dist/assets/{use-infinite-scroll-loader-DBJX5hj0.js → use-infinite-scroll-loader-dwnaa_qi.js} +1 -1
- package/dist/assets/{useConfirmDialog-DL0a-oGC.js → useConfirmDialog-mMeWD_yo.js} +1 -1
- package/dist/assets/{useMutation-BdZm-9PL.js → useMutation-BmxxvCNf.js} +1 -1
- package/dist/assets/x-DuMhMATD.js +1 -0
- package/dist/index.html +20 -20
- package/package.json +6 -6
- package/src/api/runtime-control.ts +34 -0
- package/src/api/runtime-control.types.ts +58 -0
- package/src/api/types.ts +13 -0
- package/src/{App.test.tsx → app.test.tsx} +1 -1
- package/src/{App.tsx → app.tsx} +1 -1
- package/src/components/chat/ChatConversationPanel.test.tsx +78 -16
- package/src/components/chat/ChatSidebar.test.tsx +36 -7
- package/src/components/chat/ChatSidebar.tsx +19 -26
- package/src/components/chat/chat-child-session-panel.tsx +16 -8
- package/src/components/chat/chat-page-runtime.test.ts +1 -1
- package/src/components/chat/{ChatPage.tsx → chat-page.tsx} +1 -1
- package/src/components/chat/managers/chat-session-list.manager.test.ts +82 -31
- package/src/components/chat/managers/chat-session-list.manager.ts +79 -14
- package/src/components/chat/managers/chat-ui.manager.ts +2 -0
- package/src/components/chat/ncp/README.md +1 -1
- package/src/components/chat/ncp/ncp-chat-input.manager.ts +7 -1
- package/src/components/chat/ncp/ncp-chat-page-data.test.ts +1 -1
- package/src/components/chat/ncp/ncp-session-adapter.test.ts +5 -1
- package/src/components/chat/ncp/ncp-session-adapter.ts +12 -0
- package/src/components/chat/ncp/session-conversation/use-ncp-child-session-tabs-view.ts +4 -0
- package/src/components/chat/ncp/tests/ncp-chat-input.manager.test.ts +99 -0
- package/src/components/chat/stores/chat-session-list.store.ts +25 -54
- package/src/components/common/ProviderScopedModelInput.tsx +12 -2
- package/src/components/config/ModelConfig.test.tsx +108 -2
- package/src/components/config/RuntimeConfig.tsx +14 -6
- package/src/components/config/desktop-update-config.test.tsx +85 -0
- package/src/components/config/desktop-update-config.tsx +44 -3
- package/src/components/config/runtime-control-card.test.tsx +255 -0
- package/src/components/config/runtime-control-card.tsx +301 -0
- package/src/components/config/runtime-presence-card.test.tsx +154 -0
- package/src/components/config/runtime-presence-card.tsx +163 -0
- package/src/desktop/desktop-update.types.ts +25 -0
- package/src/desktop/managers/desktop-presence.manager.ts +91 -0
- package/src/desktop/managers/desktop-update.manager.ts +37 -1
- package/src/desktop/stores/desktop-presence.store.ts +18 -0
- package/src/desktop/stores/desktop-update.store.ts +7 -1
- package/src/hooks/use-runtime-control.ts +24 -0
- package/src/lib/desktop-update-labels.utils.ts +28 -2
- package/src/lib/i18n.runtime-control.ts +120 -0
- package/src/lib/i18n.ts +2 -4
- package/src/main.tsx +1 -1
- package/src/runtime-control/runtime-control.manager.ts +118 -0
- package/dist/assets/ChatPage-A45t1Rmf.js +0 -58
- package/dist/assets/DocBrowser-B2MpsnU9.js +0 -1
- package/dist/assets/MarketplacePage-BNZ3Jx5d.js +0 -1
- package/dist/assets/RuntimeConfig-ChdfK4Y_.js +0 -1
- package/dist/assets/chat-session-display-CAlPrnlV.js +0 -1
- package/dist/assets/desktop-update-config-CfoVwf-w.js +0 -1
- package/dist/assets/i18n-C3jb83S6.js +0 -1
- package/dist/assets/loader-circle-BjMg63eu.js +0 -1
- package/dist/assets/plus-CIXME2pD.js +0 -1
- package/dist/assets/search-B_Qr0f6C.js +0 -1
- package/dist/assets/skeleton-CYQJazv6.js +0 -1
- package/dist/assets/x-B8Tho_xC.js +0 -1
- /package/dist/assets/{config-hints-GSUMvmSo.js → config-hints-BZoDjXye.js} +0 -0
- /package/dist/assets/{config-layout-CgBMG7OL.js → config-layout-DmlGaay2.js} +0 -0
- /package/src/components/chat/ncp/{NcpChatPage.tsx → ncp-chat-page.tsx} +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
import{_ as e,i as t,m as n,p as r}from"./i18n-
|
|
1
|
+
import{_ as e,i as t,m as n,p as r}from"./i18n-CwHZ-9vt.js";import{a as i,u as a}from"./client-CVqPF5ie.js";import{n as o,r as s,t as c}from"./infiniteQueryBehavior-DHSEQ3OH.js";import{a as l,i as u,n as d,r as f,t as p}from"./select-Caud8QvU.js";import{t as m}from"./createLucideIcon-DVv8taGY.js";import{t as h}from"./skeleton-B-4vRq_Z.js";var g=class extends a{constructor(e,t){super(e,t)}bindMethods(){super.bindMethods(),this.fetchNextPage=this.fetchNextPage.bind(this),this.fetchPreviousPage=this.fetchPreviousPage.bind(this)}setOptions(e){super.setOptions({...e,behavior:s()})}getOptimisticResult(e){return e.behavior=s(),super.getOptimisticResult(e)}fetchNextPage(e){return this.fetch({...e,meta:{fetchMore:{direction:`forward`}}})}fetchPreviousPage(e){return this.fetch({...e,meta:{fetchMore:{direction:`backward`}}})}createResult(e,t){let{state:n}=e,r=super.createResult(e,t),{isFetching:i,isRefetching:a,isError:s,isRefetchError:l}=r,u=n.fetchMeta?.fetchMore?.direction,d=s&&u===`forward`,f=i&&u===`forward`,p=s&&u===`backward`,m=i&&u===`backward`;return{...r,fetchNextPage:this.fetchNextPage,fetchPreviousPage:this.fetchPreviousPage,hasNextPage:c(t,n.data),hasPreviousPage:o(t,n.data),isFetchNextPageError:d,isFetchingNextPage:f,isFetchPreviousPageError:p,isFetchingPreviousPage:m,isRefetchError:l&&!d&&!p,isRefetching:a&&!f&&!m}}};function _(e,t){return i(e,g,t)}var v=m(`PackageSearch`,[[`path`,{d:`M21 10V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l2-1.14`,key:`e7tb2h`}],[`path`,{d:`m7.5 4.27 9 5.15`,key:`1c824w`}],[`polyline`,{points:`3.29 7 12 12 20.71 7`,key:`ousv84`}],[`line`,{x1:`12`,x2:`12`,y1:`22`,y2:`12`,key:`a4e8g8`}],[`circle`,{cx:`18.5`,cy:`15.5`,r:`2.5`,key:`b5zd12`}],[`path`,{d:`M20.27 17.27 22 19`,key:`1l4muz`}]]);function y(e){if(!e||e.pages.length===0)return;let t=e.pages.flatMap(e=>e.items);return{...e.pages[e.pages.length-1],items:t,pages:e.pages,loadedItems:t.length,loadedPages:e.pages.length}}var b=r();function x(e){return(0,b.jsx)(`div`,{className:`mb-4`,children:(0,b.jsxs)(`div`,{className:`flex gap-3 items-center`,children:[(0,b.jsxs)(`div`,{className:`flex-1 min-w-0 relative`,children:[(0,b.jsx)(v,{className:`h-4 w-4 text-gray-400 absolute left-3 top-1/2 -translate-y-1/2`}),(0,b.jsx)(`input`,{value:e.searchText,onChange:t=>e.onSearchTextChange(t.target.value),placeholder:e.searchPlaceholder,className:`w-full h-9 border border-gray-200/80 rounded-xl pl-9 pr-3 text-sm focus:outline-none focus:ring-1 focus:ring-primary/40`})]}),e.scope===`all`&&(0,b.jsxs)(p,{value:e.sort,onValueChange:t=>e.onSortChange(t),children:[(0,b.jsx)(u,{className:`h-9 w-[150px] shrink-0 rounded-lg`,children:(0,b.jsx)(l,{})}),(0,b.jsxs)(d,{children:[(0,b.jsx)(f,{value:`relevance`,children:t(`marketplaceSortRelevance`)}),(0,b.jsx)(f,{value:`updated`,children:t(`marketplaceSortUpdated`)})]})]})]})})}function S(e){return(0,b.jsx)(b.Fragment,{children:Array.from({length:e.count},(e,t)=>(0,b.jsx)(`article`,{className:`rounded-2xl border border-gray-200/40 bg-white px-5 py-4 shadow-sm`,children:(0,b.jsxs)(`div`,{className:`flex items-start gap-3.5 justify-between`,children:[(0,b.jsxs)(`div`,{className:`flex min-w-0 flex-1 gap-3`,children:[(0,b.jsx)(h,{className:`h-10 w-10 shrink-0 rounded-xl`}),(0,b.jsxs)(`div`,{className:`min-w-0 flex-1 space-y-2 pt-0.5`,children:[(0,b.jsx)(h,{className:`h-4 w-32 max-w-[70%]`}),(0,b.jsxs)(`div`,{className:`flex items-center gap-2`,children:[(0,b.jsx)(h,{className:`h-3 w-12`}),(0,b.jsx)(h,{className:`h-3 w-24`})]}),(0,b.jsx)(h,{className:`h-3 w-full`})]})]}),(0,b.jsx)(h,{className:`h-8 w-20 shrink-0 rounded-xl`})]})},`marketplace-skeleton-${t}`))})}function C(e){return!e.hasMore&&!e.loading?null:(0,b.jsxs)(`div`,{className:`py-4`,children:[e.hasMore&&(0,b.jsx)(`div`,{ref:e.sentinelRef,className:`h-1 w-full`,"aria-hidden":`true`}),e.loading&&(0,b.jsx)(`div`,{"data-testid":`marketplace-loading-more`,className:`pt-3 text-center text-xs text-gray-500`,children:t(`loading`)})]})}function w(e){let t=e.trim().toLowerCase().replace(/_/g,`-`),n=[t,t.split(`-`)[0],`en`];return Array.from(new Set(n.filter(Boolean)))}function T(e){return e.trim().toLowerCase().replace(/_/g,`-`)}function E(e,t,n){if(e){let t=Object.entries(e).map(([e,t])=>({locale:T(e),text:typeof t==`string`?t.trim():``})).filter(e=>e.text.length>0);if(t.length>0){let e=new Map(t.map(e=>[e.locale,e.text]));for(let t of n){let n=T(t),r=e.get(n);if(r)return r}for(let e of n){let n=T(e).split(`-`)[0];if(!n)continue;let r=t.find(e=>e.locale===n||e.locale.startsWith(`${n}-`));if(r)return r.text}return t[0]?.text??``}}return t?.trim()??``}function D(e,t){if(!e)return``;for(let n of t)if(T(n).split(`-`)[0]===`zh`&&e.descriptionZh?.trim())return e.descriptionZh.trim();return e.description?.trim()?e.description.trim():e.descriptionZh?.trim()?e.descriptionZh.trim():``}var O=e(n(),1),k=160;function A(e){let t=(0,O.useRef)(null),n=(0,O.useRef)(null),r=(0,O.useRef)(e.onLoadMore),i=(0,O.useRef)(!1);return(0,O.useEffect)(()=>{r.current=e.onLoadMore},[e.onLoadMore]),(0,O.useEffect)(()=>{e.disabled&&(i.current=!1)},[e.disabled]),(0,O.useEffect)(()=>{let a=t.current,o=n.current,s=e.thresholdPx??k;if(e.disabled||!a||!o)return;let c=()=>{i.current||e.disabled||(i.current=!0,Promise.resolve(r.current()).finally(()=>{i.current=!1}))},l=()=>{o.getBoundingClientRect().top-a.getBoundingClientRect().bottom<=s&&c()};if(typeof IntersectionObserver==`function`){let e=new IntersectionObserver(e=>{e.some(e=>e.isIntersecting)&&c()},{root:a,rootMargin:`0px 0px ${s}px 0px`});return e.observe(o),l(),()=>{e.disconnect()}}return a.addEventListener(`scroll`,l,{passive:!0}),l(),()=>{a.removeEventListener(`scroll`,l)}},[e.disabled,e.thresholdPx,e.watchValue]),{containerRef:t,sentinelRef:n}}export{x as a,y as c,E as i,_ as l,w as n,C as o,D as r,S as s,A as t};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{_ as e,i as t,m as n,p as r}from"./i18n-
|
|
1
|
+
import{_ as e,i as t,m as n,p as r}from"./i18n-CwHZ-9vt.js";import{r as i}from"./dist-DmAlInRu.js";import{at as a,it as o,nt as s,ot as c,rt as l,st as u}from"./index-DdksE6U3.js";var d=e(n(),1),f=r(),p=({open:e,onOpenChange:n,title:r,description:d,confirmLabel:p=t(`confirm`),cancelLabel:m=t(`cancel`),variant:h=`default`,onConfirm:g,onCancel:_})=>(0,f.jsx)(s,{open:e,onOpenChange:n,children:(0,f.jsxs)(l,{className:`[&>:last-child]:hidden`,onCloseAutoFocus:e=>e.preventDefault(),children:[(0,f.jsxs)(c,{children:[(0,f.jsx)(u,{children:r}),d?(0,f.jsx)(o,{children:d}):null]}),(0,f.jsxs)(a,{className:`gap-2 sm:gap-0`,children:[(0,f.jsx)(i,{type:`button`,variant:`outline`,onClick:()=>{_(),n(!1)},children:m}),(0,f.jsx)(i,{type:`button`,variant:h===`destructive`?`destructive`:`default`,onClick:()=>{g(),n(!1)},children:p})]})]})}),m={open:!1,title:``,description:``,confirmLabel:t(`confirm`),cancelLabel:t(`cancel`),variant:`default`,resolve:null};function h(){let[e,n]=(0,d.useState)(m),r=(0,d.useCallback)(e=>new Promise(r=>{n({open:!0,title:e.title,description:e.description??``,confirmLabel:e.confirmLabel??t(`confirm`),cancelLabel:e.cancelLabel??t(`cancel`),variant:e.variant??`default`,resolve:e=>{r(e),n(e=>({...e,open:!1,resolve:null}))}})}),[]),i=(0,d.useCallback)(e=>{n(t=>(!e&&t.resolve&&t.resolve(!1),{...t,open:e,resolve:e?t.resolve:null}))},[]);return{confirm:r,ConfirmDialog:(0,d.useCallback)(()=>(0,f.jsx)(p,{open:e.open,onOpenChange:i,title:e.title,description:e.description||void 0,confirmLabel:e.confirmLabel,cancelLabel:e.cancelLabel,variant:e.variant,onConfirm:()=>e.resolve?.(!0),onCancel:()=>e.resolve?.(!1)}),[e,i])}}export{h as t};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{_ as e,m as t}from"./i18n-
|
|
1
|
+
import{_ as e,m as t}from"./i18n-CwHZ-9vt.js";import{A as n,D as r,O as i,b as a,l as o,p as s,s as c,w as l}from"./client-CVqPF5ie.js";var u=class extends n{#e;#t=void 0;#n;#r;constructor(e,t){super(),this.#e=e,this.setOptions(t),this.bindMethods(),this.#i()}bindMethods(){this.mutate=this.mutate.bind(this),this.reset=this.reset.bind(this)}setOptions(e){let t=this.options;this.options=this.#e.defaultMutationOptions(e),r(this.options,t)||this.#e.getMutationCache().notify({type:`observerOptionsUpdated`,mutation:this.#n,observer:this}),t?.mutationKey&&this.options.mutationKey&&a(t.mutationKey)!==a(this.options.mutationKey)?this.reset():this.#n?.state.status===`pending`&&this.#n.setOptions(this.options)}onUnsubscribe(){this.hasListeners()||this.#n?.removeObserver(this)}onMutationUpdate(e){this.#i(),this.#a(e)}getCurrentResult(){return this.#t}reset(){this.#n?.removeObserver(this),this.#n=void 0,this.#i(),this.#a()}mutate(e,t){return this.#r=t,this.#n?.removeObserver(this),this.#n=this.#e.getMutationCache().build(this.#e,this.options),this.#n.addObserver(this),this.#n.execute(e)}#i(){let e=this.#n?.state??o();this.#t={...e,isPending:e.status===`pending`,isSuccess:e.status===`success`,isError:e.status===`error`,isIdle:e.status===`idle`,mutate:this.mutate,reset:this.reset}}#a(e){s.batch(()=>{if(this.#r&&this.hasListeners()){let t=this.#t.variables,n=this.#t.context,r={client:this.#e,meta:this.options.meta,mutationKey:this.options.mutationKey};if(e?.type===`success`){try{this.#r.onSuccess?.(e.data,t,n,r)}catch(e){Promise.reject(e)}try{this.#r.onSettled?.(e.data,null,t,n,r)}catch(e){Promise.reject(e)}}else if(e?.type===`error`){try{this.#r.onError?.(e.error,t,n,r)}catch(e){Promise.reject(e)}try{this.#r.onSettled?.(void 0,e.error,t,n,r)}catch(e){Promise.reject(e)}}}this.listeners.forEach(e=>{e(this.#t)})})}},d=e(t(),1);function f(e,t){let n=c(t),[r]=d.useState(()=>new u(n,e));d.useEffect(()=>{r.setOptions(e)},[r,e]);let a=d.useSyncExternalStore(d.useCallback(e=>r.subscribe(s.batchCalls(e)),[r]),()=>r.getCurrentResult(),()=>r.getCurrentResult()),o=d.useCallback((e,t)=>{r.mutate(e,t).catch(l)},[r]);if(a.error&&i(r.options.throwOnError,[a.error]))throw a.error;return{...a,mutate:o,mutateAsync:a.mutate}}export{f as t};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{t as e}from"./createLucideIcon-DVv8taGY.js";var t=e(`X`,[[`path`,{d:`M18 6 6 18`,key:`1bl5f8`}],[`path`,{d:`m6 6 12 12`,key:`d8bk6v`}]]);export{t};
|
package/dist/index.html
CHANGED
|
@@ -6,26 +6,26 @@
|
|
|
6
6
|
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
|
|
7
7
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
8
8
|
<title>NextClaw</title>
|
|
9
|
-
<script type="module" crossorigin src="/assets/index-
|
|
10
|
-
<link rel="modulepreload" crossorigin href="/assets/i18n-
|
|
11
|
-
<link rel="modulepreload" crossorigin href="/assets/chunk-JZWAC4HX-
|
|
12
|
-
<link rel="modulepreload" crossorigin href="/assets/dist-
|
|
13
|
-
<link rel="modulepreload" crossorigin href="/assets/createLucideIcon-
|
|
14
|
-
<link rel="modulepreload" crossorigin href="/assets/select-
|
|
15
|
-
<link rel="modulepreload" crossorigin href="/assets/dist-
|
|
16
|
-
<link rel="modulepreload" crossorigin href="/assets/client-
|
|
17
|
-
<link rel="modulepreload" crossorigin href="/assets/infiniteQueryBehavior-
|
|
18
|
-
<link rel="modulepreload" crossorigin href="/assets/app-query-client-
|
|
19
|
-
<link rel="modulepreload" crossorigin href="/assets/useMutation-
|
|
20
|
-
<link rel="modulepreload" crossorigin href="/assets/book-open-
|
|
21
|
-
<link rel="modulepreload" crossorigin href="/assets/external-link-
|
|
22
|
-
<link rel="modulepreload" crossorigin href="/assets/plus-
|
|
23
|
-
<link rel="modulepreload" crossorigin href="/assets/search-
|
|
24
|
-
<link rel="modulepreload" crossorigin href="/assets/x-
|
|
25
|
-
<link rel="modulepreload" crossorigin href="/assets/DocBrowserContext-
|
|
26
|
-
<link rel="modulepreload" crossorigin href="/assets/DocBrowser-
|
|
27
|
-
<link rel="modulepreload" crossorigin href="/assets/config-
|
|
28
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
9
|
+
<script type="module" crossorigin src="/assets/index-DdksE6U3.js"></script>
|
|
10
|
+
<link rel="modulepreload" crossorigin href="/assets/i18n-CwHZ-9vt.js">
|
|
11
|
+
<link rel="modulepreload" crossorigin href="/assets/chunk-JZWAC4HX-BvKvh1R8.js">
|
|
12
|
+
<link rel="modulepreload" crossorigin href="/assets/dist-DmAlInRu.js">
|
|
13
|
+
<link rel="modulepreload" crossorigin href="/assets/createLucideIcon-DVv8taGY.js">
|
|
14
|
+
<link rel="modulepreload" crossorigin href="/assets/select-Caud8QvU.js">
|
|
15
|
+
<link rel="modulepreload" crossorigin href="/assets/dist-Da5Gm_pO.js">
|
|
16
|
+
<link rel="modulepreload" crossorigin href="/assets/client-CVqPF5ie.js">
|
|
17
|
+
<link rel="modulepreload" crossorigin href="/assets/infiniteQueryBehavior-DHSEQ3OH.js">
|
|
18
|
+
<link rel="modulepreload" crossorigin href="/assets/app-query-client-DniXoIN5.js">
|
|
19
|
+
<link rel="modulepreload" crossorigin href="/assets/useMutation-BmxxvCNf.js">
|
|
20
|
+
<link rel="modulepreload" crossorigin href="/assets/book-open-DocgeQtR.js">
|
|
21
|
+
<link rel="modulepreload" crossorigin href="/assets/external-link-DFjw3x1B.js">
|
|
22
|
+
<link rel="modulepreload" crossorigin href="/assets/plus-DUOVbsyQ.js">
|
|
23
|
+
<link rel="modulepreload" crossorigin href="/assets/search-MChQRYR1.js">
|
|
24
|
+
<link rel="modulepreload" crossorigin href="/assets/x-DuMhMATD.js">
|
|
25
|
+
<link rel="modulepreload" crossorigin href="/assets/DocBrowserContext-Ce28gRXt.js">
|
|
26
|
+
<link rel="modulepreload" crossorigin href="/assets/DocBrowser-BMxf9CIK.js">
|
|
27
|
+
<link rel="modulepreload" crossorigin href="/assets/config-Bop2oB18.js">
|
|
28
|
+
<link rel="stylesheet" crossorigin href="/assets/index-DafCdM4F.css">
|
|
29
29
|
</head>
|
|
30
30
|
|
|
31
31
|
<body>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nextclaw/ui",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.8",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -28,11 +28,11 @@
|
|
|
28
28
|
"tailwind-merge": "^2.5.4",
|
|
29
29
|
"zod": "^3.23.8",
|
|
30
30
|
"zustand": "^5.0.2",
|
|
31
|
-
"@nextclaw/ncp": "0.
|
|
32
|
-
"@nextclaw/
|
|
33
|
-
"@nextclaw/ncp
|
|
34
|
-
"@nextclaw/agent-chat": "0.
|
|
35
|
-
"@nextclaw/
|
|
31
|
+
"@nextclaw/ncp-http-agent-client": "0.3.13",
|
|
32
|
+
"@nextclaw/ncp-react": "0.4.21",
|
|
33
|
+
"@nextclaw/ncp": "0.5.1",
|
|
34
|
+
"@nextclaw/agent-chat-ui": "0.3.5",
|
|
35
|
+
"@nextclaw/agent-chat": "0.1.10"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@testing-library/react": "^16.3.0",
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { api } from './client';
|
|
2
|
+
import type { RuntimeControlActionResult, RuntimeControlView } from './runtime-control.types';
|
|
3
|
+
|
|
4
|
+
export async function fetchRuntimeControl(): Promise<RuntimeControlView> {
|
|
5
|
+
const response = await api.get<RuntimeControlView>('/api/runtime/control');
|
|
6
|
+
if (!response.ok) {
|
|
7
|
+
throw new Error(response.error.message);
|
|
8
|
+
}
|
|
9
|
+
return response.data;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function startRuntimeService(): Promise<RuntimeControlActionResult> {
|
|
13
|
+
const response = await api.post<RuntimeControlActionResult>('/api/runtime/control/start-service', {});
|
|
14
|
+
if (!response.ok) {
|
|
15
|
+
throw new Error(response.error.message);
|
|
16
|
+
}
|
|
17
|
+
return response.data;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function restartRuntimeService(): Promise<RuntimeControlActionResult> {
|
|
21
|
+
const response = await api.post<RuntimeControlActionResult>('/api/runtime/control/restart-service', {});
|
|
22
|
+
if (!response.ok) {
|
|
23
|
+
throw new Error(response.error.message);
|
|
24
|
+
}
|
|
25
|
+
return response.data;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function stopRuntimeService(): Promise<RuntimeControlActionResult> {
|
|
29
|
+
const response = await api.post<RuntimeControlActionResult>('/api/runtime/control/stop-service', {});
|
|
30
|
+
if (!response.ok) {
|
|
31
|
+
throw new Error(response.error.message);
|
|
32
|
+
}
|
|
33
|
+
return response.data;
|
|
34
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export type RuntimeControlEnvironment =
|
|
2
|
+
| 'desktop-embedded'
|
|
3
|
+
| 'managed-local-service'
|
|
4
|
+
| 'self-hosted-web'
|
|
5
|
+
| 'shared-web';
|
|
6
|
+
|
|
7
|
+
export type RuntimeLifecycleState =
|
|
8
|
+
| 'healthy'
|
|
9
|
+
| 'starting-service'
|
|
10
|
+
| 'restarting-service'
|
|
11
|
+
| 'stopping-service'
|
|
12
|
+
| 'restarting-app'
|
|
13
|
+
| 'recovering'
|
|
14
|
+
| 'unavailable'
|
|
15
|
+
| 'failed';
|
|
16
|
+
|
|
17
|
+
export type RuntimeActionImpact = 'none' | 'brief-ui-disconnect' | 'full-app-relaunch';
|
|
18
|
+
|
|
19
|
+
export type RuntimeActionCapability = {
|
|
20
|
+
available: boolean;
|
|
21
|
+
requiresConfirmation: boolean;
|
|
22
|
+
impact: RuntimeActionImpact;
|
|
23
|
+
reasonIfUnavailable?: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type RuntimeServiceState =
|
|
27
|
+
| 'running'
|
|
28
|
+
| 'stopped'
|
|
29
|
+
| 'starting'
|
|
30
|
+
| 'stopping'
|
|
31
|
+
| 'restarting'
|
|
32
|
+
| 'unknown';
|
|
33
|
+
|
|
34
|
+
export type RuntimeControlView = {
|
|
35
|
+
environment: RuntimeControlEnvironment;
|
|
36
|
+
lifecycle: RuntimeLifecycleState;
|
|
37
|
+
serviceState: RuntimeServiceState;
|
|
38
|
+
canStartService: RuntimeActionCapability;
|
|
39
|
+
canRestartService: RuntimeActionCapability;
|
|
40
|
+
canStopService: RuntimeActionCapability;
|
|
41
|
+
canRestartApp: RuntimeActionCapability;
|
|
42
|
+
ownerLabel?: string;
|
|
43
|
+
managementHint?: string;
|
|
44
|
+
message?: string;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export type RuntimeControlAction =
|
|
48
|
+
| 'start-service'
|
|
49
|
+
| 'restart-service'
|
|
50
|
+
| 'stop-service'
|
|
51
|
+
| 'restart-app';
|
|
52
|
+
|
|
53
|
+
export type RuntimeControlActionResult = {
|
|
54
|
+
accepted: boolean;
|
|
55
|
+
action: RuntimeControlAction;
|
|
56
|
+
lifecycle: RuntimeLifecycleState;
|
|
57
|
+
message: string;
|
|
58
|
+
};
|
package/src/api/types.ts
CHANGED
|
@@ -174,6 +174,16 @@ export type {
|
|
|
174
174
|
RemoteSettingsUpdateRequest,
|
|
175
175
|
RemoteSettingsView
|
|
176
176
|
} from './remote.types';
|
|
177
|
+
export type {
|
|
178
|
+
RuntimeActionCapability,
|
|
179
|
+
RuntimeActionImpact,
|
|
180
|
+
RuntimeControlAction,
|
|
181
|
+
RuntimeControlEnvironment,
|
|
182
|
+
RuntimeControlView,
|
|
183
|
+
RuntimeLifecycleState,
|
|
184
|
+
RuntimeServiceState,
|
|
185
|
+
RuntimeControlActionResult
|
|
186
|
+
} from './runtime-control.types';
|
|
177
187
|
|
|
178
188
|
export type AgentProfileView = {
|
|
179
189
|
id: string;
|
|
@@ -240,6 +250,8 @@ export type SessionEntryView = {
|
|
|
240
250
|
key: string;
|
|
241
251
|
createdAt: string;
|
|
242
252
|
updatedAt: string;
|
|
253
|
+
lastMessageAt?: string;
|
|
254
|
+
readAt?: string;
|
|
243
255
|
agentId?: string;
|
|
244
256
|
label?: string;
|
|
245
257
|
channel?: string;
|
|
@@ -331,6 +343,7 @@ export type SessionPatchUpdate = {
|
|
|
331
343
|
preferredThinking?: ThinkingLevel | null;
|
|
332
344
|
sessionType?: string | null;
|
|
333
345
|
projectRoot?: string | null;
|
|
346
|
+
uiReadAt?: string | null;
|
|
334
347
|
clearHistory?: boolean;
|
|
335
348
|
};
|
|
336
349
|
|
|
@@ -3,7 +3,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
3
3
|
import { MemoryRouter } from 'react-router-dom';
|
|
4
4
|
import { I18nProvider } from '@/components/providers/I18nProvider';
|
|
5
5
|
import { ThemeProvider } from '@/components/providers/ThemeProvider';
|
|
6
|
-
import AppContent from '@/
|
|
6
|
+
import AppContent from '@/app';
|
|
7
7
|
|
|
8
8
|
const mocks = vi.hoisted(() => ({
|
|
9
9
|
refetch: vi.fn(),
|
package/src/{App.tsx → app.tsx}
RENAMED
|
@@ -11,7 +11,7 @@ import { Toaster } from 'sonner';
|
|
|
11
11
|
import { Routes, Route, Navigate } from 'react-router-dom';
|
|
12
12
|
|
|
13
13
|
const ModelConfigPage = lazy(async () => ({ default: (await import('@/components/config/ModelConfig')).ModelConfig }));
|
|
14
|
-
const ChatPage = lazy(async () => ({ default: (await import('@/components/chat/
|
|
14
|
+
const ChatPage = lazy(async () => ({ default: (await import('@/components/chat/chat-page')).ChatPage }));
|
|
15
15
|
const SearchConfigPage = lazy(async () => ({ default: (await import('@/components/config/SearchConfig')).SearchConfig }));
|
|
16
16
|
const ProvidersListPage = lazy(async () => ({ default: (await import('@/components/config/ProvidersList')).ProvidersList }));
|
|
17
17
|
const ChannelsListPage = lazy(async () => ({ default: (await import('@/components/config/ChannelsList')).ChannelsList }));
|
|
@@ -24,6 +24,8 @@ const mocks = vi.hoisted(() => ({
|
|
|
24
24
|
title: "北京天气",
|
|
25
25
|
agentId: "weather",
|
|
26
26
|
updatedAt: "2026-04-10T09:00:00.000Z",
|
|
27
|
+
lastMessageAt: "2026-04-10T09:00:00.000Z",
|
|
28
|
+
readAt: "2026-04-10T09:00:00.000Z",
|
|
27
29
|
sessionTypeLabel: "Codex",
|
|
28
30
|
preferredModel: "openai/gpt-5.3-codex",
|
|
29
31
|
projectName: "project-alpha",
|
|
@@ -84,17 +86,14 @@ vi.mock("@/components/chat/presenter/chat-presenter-context", () => ({
|
|
|
84
86
|
setSelectedAgentId: mocks.setSelectedAgentId,
|
|
85
87
|
markSessionRead: (
|
|
86
88
|
sessionKey: string | null | undefined,
|
|
87
|
-
|
|
89
|
+
readAt: string | null | undefined,
|
|
88
90
|
) =>
|
|
89
91
|
sessionKey
|
|
90
92
|
? useChatSessionListStore.getState().markSessionRead(
|
|
91
93
|
sessionKey,
|
|
92
|
-
|
|
94
|
+
readAt,
|
|
93
95
|
)
|
|
94
96
|
: undefined,
|
|
95
|
-
hydrateReadWatermarks: (
|
|
96
|
-
entries: readonly { sessionKey: string; updatedAt: string | null | undefined }[],
|
|
97
|
-
) => useChatSessionListStore.getState().hydrateReadWatermarks(entries),
|
|
98
97
|
},
|
|
99
98
|
chatInputManager: {
|
|
100
99
|
setPendingSessionType: mocks.setPendingSessionType,
|
|
@@ -192,8 +191,7 @@ describe("ChatConversationPanel", () => {
|
|
|
192
191
|
},
|
|
193
192
|
});
|
|
194
193
|
useChatSessionListStore.setState({
|
|
195
|
-
|
|
196
|
-
hasHydratedReadWatermarks: false,
|
|
194
|
+
optimisticReadAtBySessionKey: {},
|
|
197
195
|
snapshot: {
|
|
198
196
|
...useChatSessionListStore.getState().snapshot,
|
|
199
197
|
},
|
|
@@ -299,6 +297,8 @@ describe("ChatChildSessionPanel", () => {
|
|
|
299
297
|
title: "北京天气",
|
|
300
298
|
agentId: "weather",
|
|
301
299
|
updatedAt: "2026-04-10T09:00:00.000Z",
|
|
300
|
+
lastMessageAt: "2026-04-10T09:00:00.000Z",
|
|
301
|
+
readAt: "2026-04-10T09:00:00.000Z",
|
|
302
302
|
sessionTypeLabel: "Codex",
|
|
303
303
|
preferredModel: "openai/gpt-5.3-codex",
|
|
304
304
|
projectName: "project-alpha",
|
|
@@ -345,6 +345,8 @@ describe("ChatChildSessionPanel", () => {
|
|
|
345
345
|
title: "北京天气",
|
|
346
346
|
agentId: "weather",
|
|
347
347
|
updatedAt: "2026-04-10T09:00:00.000Z",
|
|
348
|
+
lastMessageAt: "2026-04-10T09:00:00.000Z",
|
|
349
|
+
readAt: "2026-04-10T09:00:00.000Z",
|
|
348
350
|
sessionTypeLabel: "Codex",
|
|
349
351
|
preferredModel: "openai/gpt-5.3-codex",
|
|
350
352
|
projectName: "project-alpha",
|
|
@@ -356,6 +358,8 @@ describe("ChatChildSessionPanel", () => {
|
|
|
356
358
|
title: "上海天气",
|
|
357
359
|
agentId: "weather",
|
|
358
360
|
updatedAt: "2026-04-10T09:05:00.000Z",
|
|
361
|
+
lastMessageAt: "2026-04-10T09:05:00.000Z",
|
|
362
|
+
readAt: "2026-04-10T09:05:00.000Z",
|
|
359
363
|
sessionTypeLabel: "Claude Code",
|
|
360
364
|
preferredModel: "anthropic/claude-sonnet-4",
|
|
361
365
|
projectName: "project-beta",
|
|
@@ -408,6 +412,8 @@ describe("ChatChildSessionPanel", () => {
|
|
|
408
412
|
title: "北京天气",
|
|
409
413
|
agentId: "weather",
|
|
410
414
|
updatedAt: "2026-04-10T09:00:00.000Z",
|
|
415
|
+
lastMessageAt: "2026-04-10T09:00:00.000Z",
|
|
416
|
+
readAt: "2026-04-10T09:00:00.000Z",
|
|
411
417
|
sessionTypeLabel: "Codex",
|
|
412
418
|
preferredModel: "openai/gpt-5.3-codex",
|
|
413
419
|
projectName: "project-alpha",
|
|
@@ -419,6 +425,8 @@ describe("ChatChildSessionPanel", () => {
|
|
|
419
425
|
title: "上海天气",
|
|
420
426
|
agentId: "weather",
|
|
421
427
|
updatedAt: "2026-04-10T09:05:00.000Z",
|
|
428
|
+
lastMessageAt: "2026-04-10T09:05:00.000Z",
|
|
429
|
+
readAt: "2026-04-10T09:05:00.000Z",
|
|
422
430
|
runStatus: "running",
|
|
423
431
|
sessionTypeLabel: "Claude Code",
|
|
424
432
|
preferredModel: "anthropic/claude-sonnet-4",
|
|
@@ -450,9 +458,7 @@ describe("ChatChildSessionPanel", () => {
|
|
|
450
458
|
/>,
|
|
451
459
|
);
|
|
452
460
|
|
|
453
|
-
expect(
|
|
454
|
-
screen.queryByLabelText("Session has unread updates"),
|
|
455
|
-
).toBeNull();
|
|
461
|
+
expect(screen.queryByLabelText("Session has unread updates")).toBeNull();
|
|
456
462
|
|
|
457
463
|
mocks.resolvedChildTabs = [
|
|
458
464
|
{
|
|
@@ -461,6 +467,8 @@ describe("ChatChildSessionPanel", () => {
|
|
|
461
467
|
title: "北京天气",
|
|
462
468
|
agentId: "weather",
|
|
463
469
|
updatedAt: "2026-04-10T09:00:00.000Z",
|
|
470
|
+
lastMessageAt: "2026-04-10T09:00:00.000Z",
|
|
471
|
+
readAt: "2026-04-10T09:00:00.000Z",
|
|
464
472
|
sessionTypeLabel: "Codex",
|
|
465
473
|
preferredModel: "openai/gpt-5.3-codex",
|
|
466
474
|
projectName: "project-alpha",
|
|
@@ -472,6 +480,8 @@ describe("ChatChildSessionPanel", () => {
|
|
|
472
480
|
title: "上海天气",
|
|
473
481
|
agentId: "weather",
|
|
474
482
|
updatedAt: "2026-04-10T09:05:00.000Z",
|
|
483
|
+
lastMessageAt: "2026-04-10T09:06:00.000Z",
|
|
484
|
+
readAt: "2026-04-10T09:05:00.000Z",
|
|
475
485
|
sessionTypeLabel: "Claude Code",
|
|
476
486
|
preferredModel: "anthropic/claude-sonnet-4",
|
|
477
487
|
projectName: "project-beta",
|
|
@@ -502,9 +512,7 @@ describe("ChatChildSessionPanel", () => {
|
|
|
502
512
|
/>,
|
|
503
513
|
);
|
|
504
514
|
|
|
505
|
-
expect(
|
|
506
|
-
screen.getByLabelText("Session has unread updates"),
|
|
507
|
-
).toBeTruthy();
|
|
515
|
+
expect(screen.getByLabelText("Session has unread updates")).toBeTruthy();
|
|
508
516
|
|
|
509
517
|
rerender(
|
|
510
518
|
<ChatChildSessionPanel
|
|
@@ -529,8 +537,62 @@ describe("ChatChildSessionPanel", () => {
|
|
|
529
537
|
/>,
|
|
530
538
|
);
|
|
531
539
|
|
|
532
|
-
expect(
|
|
533
|
-
|
|
534
|
-
|
|
540
|
+
expect(screen.queryByLabelText("Session has unread updates")).toBeNull();
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
it("does not show an unread dot for child tabs without a persisted ui read baseline", () => {
|
|
544
|
+
mocks.resolvedChildTabs = [
|
|
545
|
+
{
|
|
546
|
+
sessionKey: "child-session-1",
|
|
547
|
+
parentSessionKey: "parent-session-1",
|
|
548
|
+
title: "北京天气",
|
|
549
|
+
agentId: "weather",
|
|
550
|
+
updatedAt: "2026-04-10T09:00:00.000Z",
|
|
551
|
+
lastMessageAt: "2026-04-10T09:00:00.000Z",
|
|
552
|
+
readAt: null,
|
|
553
|
+
sessionTypeLabel: "Codex",
|
|
554
|
+
preferredModel: "openai/gpt-5.3-codex",
|
|
555
|
+
projectName: "project-alpha",
|
|
556
|
+
projectRoot: "/Users/demo/project-alpha",
|
|
557
|
+
},
|
|
558
|
+
{
|
|
559
|
+
sessionKey: "child-session-2",
|
|
560
|
+
parentSessionKey: "parent-session-1",
|
|
561
|
+
title: "上海天气",
|
|
562
|
+
agentId: "weather",
|
|
563
|
+
updatedAt: "2026-04-10T09:05:00.000Z",
|
|
564
|
+
lastMessageAt: "2026-04-10T09:06:00.000Z",
|
|
565
|
+
readAt: null,
|
|
566
|
+
sessionTypeLabel: "Claude Code",
|
|
567
|
+
preferredModel: "anthropic/claude-sonnet-4",
|
|
568
|
+
projectName: "project-beta",
|
|
569
|
+
projectRoot: "/Users/demo/project-beta",
|
|
570
|
+
},
|
|
571
|
+
];
|
|
572
|
+
|
|
573
|
+
render(
|
|
574
|
+
<ChatChildSessionPanel
|
|
575
|
+
tabs={[
|
|
576
|
+
{
|
|
577
|
+
sessionKey: "child-session-1",
|
|
578
|
+
parentSessionKey: "parent-session-1",
|
|
579
|
+
label: "北京天气",
|
|
580
|
+
agentId: "weather",
|
|
581
|
+
},
|
|
582
|
+
{
|
|
583
|
+
sessionKey: "child-session-2",
|
|
584
|
+
parentSessionKey: "parent-session-1",
|
|
585
|
+
label: "上海天气",
|
|
586
|
+
agentId: "weather",
|
|
587
|
+
},
|
|
588
|
+
]}
|
|
589
|
+
activeSessionKey="child-session-1"
|
|
590
|
+
onSelectSession={vi.fn()}
|
|
591
|
+
onClose={vi.fn()}
|
|
592
|
+
onBackToParent={vi.fn()}
|
|
593
|
+
/>,
|
|
594
|
+
);
|
|
595
|
+
|
|
596
|
+
expect(screen.queryByLabelText("Session has unread updates")).toBeNull();
|
|
535
597
|
});
|
|
536
598
|
});
|
|
@@ -32,11 +32,10 @@ vi.mock('@/components/chat/presenter/chat-presenter-context', () => ({
|
|
|
32
32
|
setQuery: mocks.setQuery,
|
|
33
33
|
setListMode: mocks.setListMode,
|
|
34
34
|
selectSession: mocks.selectSession,
|
|
35
|
-
markSessionRead: (
|
|
36
|
-
sessionKey
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
) => useChatSessionListStore.getState().hydrateReadWatermarks(entries)
|
|
35
|
+
markSessionRead: (
|
|
36
|
+
sessionKey: string | null | undefined,
|
|
37
|
+
readAt: string | null | undefined,
|
|
38
|
+
) => (sessionKey ? useChatSessionListStore.getState().markSessionRead(sessionKey, readAt) : undefined),
|
|
40
39
|
}
|
|
41
40
|
})
|
|
42
41
|
}));
|
|
@@ -132,8 +131,7 @@ function resetSidebarTestState() {
|
|
|
132
131
|
}
|
|
133
132
|
});
|
|
134
133
|
useChatSessionListStore.setState({
|
|
135
|
-
|
|
136
|
-
hasHydratedReadWatermarks: false,
|
|
134
|
+
optimisticReadAtBySessionKey: {},
|
|
137
135
|
snapshot: {
|
|
138
136
|
...useChatSessionListStore.getState().snapshot,
|
|
139
137
|
query: '',
|
|
@@ -514,6 +512,8 @@ describe('ChatSidebar session item interactions', () => {
|
|
|
514
512
|
key: 'session:ncp-1',
|
|
515
513
|
createdAt: '2026-03-19T09:00:00.000Z',
|
|
516
514
|
updatedAt: '2026-03-19T09:05:00.000Z',
|
|
515
|
+
lastMessageAt: '2026-03-19T09:05:00.000Z',
|
|
516
|
+
readAt: '2026-03-19T09:05:00.000Z',
|
|
517
517
|
label: 'Current Task',
|
|
518
518
|
sessionType: 'native',
|
|
519
519
|
sessionTypeMutable: false,
|
|
@@ -523,6 +523,8 @@ describe('ChatSidebar session item interactions', () => {
|
|
|
523
523
|
key: 'session:ncp-2',
|
|
524
524
|
createdAt: '2026-03-19T10:00:00.000Z',
|
|
525
525
|
updatedAt: '2026-03-19T10:05:00.000Z',
|
|
526
|
+
lastMessageAt: '2026-03-19T10:05:00.000Z',
|
|
527
|
+
readAt: '2026-03-19T10:05:00.000Z',
|
|
526
528
|
label: 'Background Task',
|
|
527
529
|
sessionType: 'native',
|
|
528
530
|
sessionTypeMutable: false,
|
|
@@ -550,6 +552,8 @@ describe('ChatSidebar session item interactions', () => {
|
|
|
550
552
|
key: 'session:ncp-2',
|
|
551
553
|
createdAt: '2026-03-19T10:00:00.000Z',
|
|
552
554
|
updatedAt: '2026-03-19T10:06:00.000Z',
|
|
555
|
+
lastMessageAt: '2026-03-19T10:06:00.000Z',
|
|
556
|
+
readAt: '2026-03-19T10:05:00.000Z',
|
|
553
557
|
label: 'Background Task',
|
|
554
558
|
sessionType: 'native',
|
|
555
559
|
sessionTypeMutable: false,
|
|
@@ -571,6 +575,8 @@ describe('ChatSidebar session item interactions', () => {
|
|
|
571
575
|
key: 'session:ncp-2',
|
|
572
576
|
createdAt: '2026-03-19T10:00:00.000Z',
|
|
573
577
|
updatedAt: '2026-03-19T10:06:00.000Z',
|
|
578
|
+
lastMessageAt: '2026-03-19T10:06:00.000Z',
|
|
579
|
+
readAt: '2026-03-19T10:05:00.000Z',
|
|
574
580
|
label: 'Background Task',
|
|
575
581
|
sessionType: 'native',
|
|
576
582
|
sessionTypeMutable: false,
|
|
@@ -601,4 +607,27 @@ describe('ChatSidebar session item interactions', () => {
|
|
|
601
607
|
|
|
602
608
|
expect(screen.queryByLabelText('Session has unread updates')).toBeNull();
|
|
603
609
|
});
|
|
610
|
+
|
|
611
|
+
it('does not show an unread dot for sessions without a persisted ui read baseline', () => {
|
|
612
|
+
mocks.sessionItems = [
|
|
613
|
+
createSessionItem({
|
|
614
|
+
key: 'session:ncp-legacy',
|
|
615
|
+
createdAt: '2026-03-19T09:00:00.000Z',
|
|
616
|
+
updatedAt: '2026-03-19T09:05:00.000Z',
|
|
617
|
+
lastMessageAt: '2026-03-19T09:05:00.000Z',
|
|
618
|
+
label: 'Legacy Session',
|
|
619
|
+
sessionType: 'native',
|
|
620
|
+
sessionTypeMutable: false,
|
|
621
|
+
messageCount: 1
|
|
622
|
+
})
|
|
623
|
+
];
|
|
624
|
+
|
|
625
|
+
render(
|
|
626
|
+
<MemoryRouter>
|
|
627
|
+
<ChatSidebar />
|
|
628
|
+
</MemoryRouter>
|
|
629
|
+
);
|
|
630
|
+
|
|
631
|
+
expect(screen.queryByLabelText('Session has unread updates')).toBeNull();
|
|
632
|
+
});
|
|
604
633
|
});
|
|
@@ -149,28 +149,13 @@ const navItems = [
|
|
|
149
149
|
function useChatSessionUnreadState(
|
|
150
150
|
items: readonly NcpSessionListItemView[],
|
|
151
151
|
selectedSessionKey: string | null,
|
|
152
|
-
markSessionRead: (
|
|
153
|
-
|
|
154
|
-
|
|
152
|
+
markSessionRead: (
|
|
153
|
+
sessionKey: string | null | undefined,
|
|
154
|
+
readAt: string | null | undefined,
|
|
155
|
+
currentReadAt?: string | null,
|
|
155
156
|
) => void,
|
|
156
157
|
): Record<string, string> {
|
|
157
|
-
const
|
|
158
|
-
const hasHydratedReadWatermarks = useChatSessionListStore((state) => state.hasHydratedReadWatermarks);
|
|
159
|
-
|
|
160
|
-
useEffect(() => {
|
|
161
|
-
const syncHydratedReadWatermarks = () => {
|
|
162
|
-
if (hasHydratedReadWatermarks || items.length === 0) {
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
165
|
-
hydrateReadWatermarks(
|
|
166
|
-
items.map(({ session }) => ({
|
|
167
|
-
sessionKey: session.key,
|
|
168
|
-
updatedAt: session.updatedAt
|
|
169
|
-
}))
|
|
170
|
-
);
|
|
171
|
-
};
|
|
172
|
-
syncHydratedReadWatermarks();
|
|
173
|
-
}, [hasHydratedReadWatermarks, hydrateReadWatermarks, items]);
|
|
158
|
+
const optimisticReadAtBySessionKey = useChatSessionListStore((state) => state.optimisticReadAtBySessionKey);
|
|
174
159
|
|
|
175
160
|
useEffect(() => {
|
|
176
161
|
const syncSelectedSessionReadState = () => {
|
|
@@ -182,12 +167,16 @@ function useChatSessionUnreadState(
|
|
|
182
167
|
return;
|
|
183
168
|
}
|
|
184
169
|
const { session: selectedSession } = selectedItem;
|
|
185
|
-
markSessionRead(
|
|
170
|
+
markSessionRead(
|
|
171
|
+
selectedSession.key,
|
|
172
|
+
selectedSession.lastMessageAt,
|
|
173
|
+
selectedSession.readAt,
|
|
174
|
+
);
|
|
186
175
|
};
|
|
187
176
|
syncSelectedSessionReadState();
|
|
188
177
|
}, [items, markSessionRead, selectedSessionKey]);
|
|
189
178
|
|
|
190
|
-
return
|
|
179
|
+
return optimisticReadAtBySessionKey;
|
|
191
180
|
}
|
|
192
181
|
|
|
193
182
|
export function ChatSidebar() {
|
|
@@ -220,11 +209,10 @@ export function ChatSidebar() {
|
|
|
220
209
|
[defaultSessionType, inputSnapshot.sessionTypeOptions]
|
|
221
210
|
);
|
|
222
211
|
const isProjectFirstView = listSnapshot.listMode === 'project-first';
|
|
223
|
-
const
|
|
212
|
+
const optimisticReadAtBySessionKey = useChatSessionUnreadState(
|
|
224
213
|
items,
|
|
225
214
|
listSnapshot.selectedSessionKey,
|
|
226
215
|
presenter.chatSessionListManager.markSessionRead,
|
|
227
|
-
presenter.chatSessionListManager.hydrateReadWatermarks,
|
|
228
216
|
);
|
|
229
217
|
const handleLanguageSwitch = (nextLang: I18nLanguage) => {
|
|
230
218
|
if (language === nextLang) return;
|
|
@@ -261,10 +249,15 @@ export function ChatSidebar() {
|
|
|
261
249
|
};
|
|
262
250
|
const renderSessionItem = ({ session, runStatus }: NcpSessionListItemView) => {
|
|
263
251
|
const active = listSnapshot.selectedSessionKey === session.key;
|
|
252
|
+
const optimisticReadAt = optimisticReadAtBySessionKey[session.key];
|
|
253
|
+
const effectiveReadAt =
|
|
254
|
+
optimisticReadAt && session.readAt
|
|
255
|
+
? (optimisticReadAt.localeCompare(session.readAt) > 0 ? optimisticReadAt : session.readAt)
|
|
256
|
+
: optimisticReadAt ?? session.readAt;
|
|
264
257
|
const showUnreadDot = shouldShowUnreadSessionIndicator({
|
|
265
258
|
active,
|
|
266
|
-
|
|
267
|
-
|
|
259
|
+
lastMessageAt: session.lastMessageAt,
|
|
260
|
+
readAt: effectiveReadAt,
|
|
268
261
|
runStatus,
|
|
269
262
|
});
|
|
270
263
|
const context = resolveSessionContextView(session, inputSnapshot.sessionTypeOptions);
|