@@ -4,6 +4,7 @@ import React, {
44 useState ,
55 useEffect ,
66 useCallback ,
7+ useMemo ,
78 useRef ,
89} from "react" ;
910import { invoke } from "@tauri-apps/api/core" ;
@@ -77,6 +78,120 @@ const defaultState: AppStatusState = {
7778 lastUpdated : null ,
7879} ;
7980
81+ // ---------------------------------------------------------------------------
82+ // Compute helpers — pure async functions returning the partial state slice
83+ // for each backend probe. Intentionally free of setState so the caller can
84+ // batch updates from multiple probes into a single render.
85+ // ---------------------------------------------------------------------------
86+
87+ async function computeLlmStatus ( ) : Promise < Partial < AppStatusState > > {
88+ try {
89+ const isLoaded = await invoke < boolean > ( "is_model_loaded" ) ;
90+ if ( isLoaded ) {
91+ const info = await invoke < ModelInfo | null > ( "get_model_info" ) ;
92+ return {
93+ llmLoaded : true ,
94+ llmModelName : info ?. name ?? info ?. id ?? "Unknown" ,
95+ llmModelInfo : info ,
96+ llmLoading : false ,
97+ } ;
98+ }
99+ return {
100+ llmLoaded : false ,
101+ llmModelName : null ,
102+ llmModelInfo : null ,
103+ llmLoading : false ,
104+ } ;
105+ } catch ( e ) {
106+ console . error ( "Failed to check LLM status:" , e ) ;
107+ return { } ;
108+ }
109+ }
110+
111+ async function computeEmbeddingsStatus ( ) : Promise < Partial < AppStatusState > > {
112+ try {
113+ const isLoaded = await invoke < boolean > ( "is_embedding_model_loaded" ) ;
114+ return {
115+ embeddingsLoaded : isLoaded ,
116+ embeddingsModelName : isLoaded ? "default" : null ,
117+ } ;
118+ } catch {
119+ // Embedding check may not exist
120+ return { } ;
121+ }
122+ }
123+
124+ async function computeVectorStatus ( ) : Promise < Partial < AppStatusState > > {
125+ try {
126+ const consent = await invoke < {
127+ enabled : boolean ;
128+ consented_at : string | null ;
129+ } > ( "get_vector_consent" ) ;
130+ return {
131+ vectorEnabled : consent . enabled ,
132+ vectorConsent : consent . enabled ,
133+ } ;
134+ } catch {
135+ // Vector consent may not be available
136+ return { } ;
137+ }
138+ }
139+
140+ async function computeKbStatus ( ) : Promise < Partial < AppStatusState > > {
141+ try {
142+ const stats = await invoke < {
143+ document_count : number ;
144+ chunk_count : number ;
145+ namespace_count : number ;
146+ } > ( "get_kb_stats" ) ;
147+ return {
148+ kbIndexed : stats . chunk_count > 0 ,
149+ kbDocumentCount : stats . document_count ,
150+ kbChunkCount : stats . chunk_count ,
151+ } ;
152+ } catch {
153+ // KB stats may fail if not initialized
154+ return { } ;
155+ }
156+ }
157+
158+ async function computeMemoryKernelStatus ( ) : Promise < Partial < AppStatusState > > {
159+ try {
160+ const preflight = await invoke < MemoryKernelPreflightStatus > (
161+ "get_memory_kernel_preflight_status" ,
162+ ) ;
163+ return {
164+ memoryKernelFeatureEnabled : preflight . enabled ,
165+ memoryKernelReady : preflight . ready && preflight . enrichment_enabled ,
166+ memoryKernelStatus : preflight . status ,
167+ memoryKernelDetail : preflight . message ,
168+ memoryKernelReleaseTag : preflight . release_tag ?? null ,
169+ memoryKernelCommitSha : preflight . commit_sha ?? null ,
170+ memoryKernelServiceContract :
171+ preflight . service_contract_version ??
172+ preflight . expected_service_contract_version ??
173+ null ,
174+ memoryKernelApiContract :
175+ preflight . api_contract_version ??
176+ preflight . expected_api_contract_version ??
177+ null ,
178+ memoryKernelIntegrationBaseline : preflight . integration_baseline ?? null ,
179+ } ;
180+ } catch ( err ) {
181+ return {
182+ memoryKernelFeatureEnabled : false ,
183+ memoryKernelReady : false ,
184+ memoryKernelStatus : "error" ,
185+ memoryKernelDetail : `Preflight unavailable: ${ String ( err ) } ` ,
186+ memoryKernelReleaseTag : null ,
187+ memoryKernelCommitSha : null ,
188+ memoryKernelServiceContract : null ,
189+ memoryKernelApiContract : null ,
190+ memoryKernelIntegrationBaseline : null ,
191+ } ;
192+ }
193+ }
194+
80195const AppStatusContext = createContext < AppStatusContextValue | null > ( null ) ;
81196
82197interface Props {
@@ -89,139 +204,41 @@ export function AppStatusProvider({ children, pollInterval = 10000 }: Props) {
89204 const pollRef = useRef < number | null > ( null ) ;
90205
91206 const refreshLlm = useCallback ( async ( ) => {
92- try {
93- const isLoaded = await invoke < boolean > ( "is_model_loaded" ) ;
94- if ( isLoaded ) {
95- const info = await invoke < ModelInfo | null > ( "get_model_info" ) ;
96- setState ( ( prev ) => ( {
97- ...prev ,
98- llmLoaded : true ,
99- llmModelName : info ?. name ?? info ?. id ?? "Unknown" ,
100- llmModelInfo : info ,
101- llmLoading : false ,
102- } ) ) ;
103- } else {
104- setState ( ( prev ) => ( {
105- ...prev ,
106- llmLoaded : false ,
107- llmModelName : null ,
108- llmModelInfo : null ,
109- llmLoading : false ,
110- } ) ) ;
111- }
112- } catch ( e ) {
113- console . error ( "Failed to check LLM status:" , e ) ;
114- }
115- } , [ ] ) ;
116-
117- const refreshEmbeddings = useCallback ( async ( ) => {
118- try {
119- const isLoaded = await invoke < boolean > ( "is_embedding_model_loaded" ) ;
120- setState ( ( prev ) => ( {
121- ...prev ,
122- embeddingsLoaded : isLoaded ,
123- embeddingsModelName : isLoaded ? "default" : null ,
124- } ) ) ;
125- } catch {
126- // Embedding check may not exist
127- }
128- } , [ ] ) ;
129-
130- const refreshVector = useCallback ( async ( ) => {
131- try {
132- const consent = await invoke < {
133- enabled : boolean ;
134- consented_at : string | null ;
135- } > ( "get_vector_consent" ) ;
136- setState ( ( prev ) => ( {
137- ...prev ,
138- vectorEnabled : consent . enabled ,
139- vectorConsent : consent . enabled ,
140- } ) ) ;
141- } catch {
142- // Vector consent may not be available
207+ const partial = await computeLlmStatus ( ) ;
208+ if ( Object . keys ( partial ) . length > 0 ) {
209+ setState ( ( prev ) => ( { ...prev , ...partial } ) ) ;
143210 }
144211 } , [ ] ) ;
145212
146213 const refreshKb = useCallback ( async ( ) => {
147- try {
148- const stats = await invoke < {
149- document_count : number ;
150- chunk_count : number ;
151- namespace_count : number ;
152- } > ( "get_kb_stats" ) ;
153- setState ( ( prev ) => ( {
154- ...prev ,
155- kbIndexed : stats . chunk_count > 0 ,
156- kbDocumentCount : stats . document_count ,
157- kbChunkCount : stats . chunk_count ,
158- } ) ) ;
159- } catch {
160- // KB stats may fail if not initialized
161- }
162- } , [ ] ) ;
163-
164- const refreshMemoryKernel = useCallback ( async ( ) => {
165- try {
166- const preflight = await invoke < MemoryKernelPreflightStatus > (
167- "get_memory_kernel_preflight_status" ,
168- ) ;
169- setState ( ( prev ) => ( {
170- ...prev ,
171- memoryKernelFeatureEnabled : preflight . enabled ,
172- memoryKernelReady : preflight . ready && preflight . enrichment_enabled ,
173- memoryKernelStatus : preflight . status ,
174- memoryKernelDetail : preflight . message ,
175- memoryKernelReleaseTag : preflight . release_tag ?? null ,
176- memoryKernelCommitSha : preflight . commit_sha ?? null ,
177- memoryKernelServiceContract :
178- preflight . service_contract_version ??
179- preflight . expected_service_contract_version ??
180- null ,
181- memoryKernelApiContract :
182- preflight . api_contract_version ??
183- preflight . expected_api_contract_version ??
184- null ,
185- memoryKernelIntegrationBaseline : preflight . integration_baseline ?? null ,
186- } ) ) ;
187- } catch ( err ) {
188- setState ( ( prev ) => ( {
189- ...prev ,
190- memoryKernelFeatureEnabled : false ,
191- memoryKernelReady : false ,
192- memoryKernelStatus : "error" ,
193- memoryKernelDetail : `Preflight unavailable: ${ String ( err ) } ` ,
194- memoryKernelReleaseTag : null ,
195- memoryKernelCommitSha : null ,
196- memoryKernelServiceContract : null ,
197- memoryKernelApiContract : null ,
198- memoryKernelIntegrationBaseline : null ,
199- } ) ) ;
214+ const partial = await computeKbStatus ( ) ;
215+ if ( Object . keys ( partial ) . length > 0 ) {
216+ setState ( ( prev ) => ( { ...prev , ...partial } ) ) ;
200217 }
201218 } , [ ] ) ;
202219
203220 const refresh = useCallback ( async ( ) => {
204- await Promise . all ( [
205- refreshLlm ( ) ,
206- refreshEmbeddings ( ) ,
207- refreshVector ( ) ,
208- refreshKb ( ) ,
209- refreshMemoryKernel ( ) ,
221+ const [ llm , embeddings , vector , kb , memoryKernel ] = await Promise . all ( [
222+ computeLlmStatus ( ) ,
223+ computeEmbeddingsStatus ( ) ,
224+ computeVectorStatus ( ) ,
225+ computeKbStatus ( ) ,
226+ computeMemoryKernelStatus ( ) ,
210227 ] ) ;
211228 setState ( ( prev ) => ( {
212229 ...prev ,
230+ ...llm ,
231+ ...embeddings ,
232+ ...vector ,
233+ ...kb ,
234+ ...memoryKernel ,
213235 initialized : true ,
214236 lastUpdated : new Date ( ) ,
215237 } ) ) ;
216- } , [
217- refreshLlm ,
218- refreshEmbeddings ,
219- refreshVector ,
220- refreshKb ,
221- refreshMemoryKernel ,
222- ] ) ;
238+ } , [ ] ) ;
223239
224- // Initial load and polling
240+ // Initial load and polling. `refresh` has stable identity (empty deps),
241+ // so the effect only re-arms when `pollInterval` changes.
225242 useEffect ( ( ) => {
226243 refresh ( ) ;
227244
@@ -236,12 +253,15 @@ export function AppStatusProvider({ children, pollInterval = 10000 }: Props) {
236253 } ;
237254 } , [ refresh , pollInterval ] ) ;
238255
239- const value : AppStatusContextValue = {
240- ...state ,
241- refresh,
242- refreshLlm,
243- refreshKb,
244- } ;
256+ const value = useMemo < AppStatusContextValue > (
257+ ( ) => ( {
258+ ...state ,
259+ refresh,
260+ refreshLlm,
261+ refreshKb,
262+ } ) ,
263+ [ state , refresh , refreshLlm , refreshKb ] ,
264+ ) ;
245265
246266 return (
247267 < AppStatusContext . Provider value = { value } >
0 commit comments