99import sys
1010import threading
1111import time
12+ from collections .abc import AsyncIterator
1213from multiprocessing .connection import Connection , Listener
1314from pathlib import Path
1415from typing import Any
2223 ErrorResponse ,
2324 HandshakeRequest ,
2425 HandshakeResponse ,
26+ IndexingProgress ,
27+ IndexProgressUpdate ,
2528 IndexRequest ,
2629 IndexResponse ,
30+ IndexStreamResponse ,
31+ IndexWaitingNotice ,
2732 ProjectStatusRequest ,
2833 ProjectStatusResponse ,
2934 Request ,
@@ -113,14 +118,43 @@ async def get_project(self, project_root: str) -> Project:
113118 self ._indexing [project_root ] = False
114119 return self ._projects [project_root ]
115120
116- async def update_index (self , project_root : str ) -> None :
117- """Update index for project, serialized by per-project lock ."""
121+ async def update_index (self , project_root : str ) -> AsyncIterator [ IndexStreamResponse ] :
122+ """Update index, yielding progress updates and a final IndexResponse ."""
118123 project = await self .get_project (project_root )
119124 lock = self ._index_locks [project_root ]
125+
126+ # If lock is already held, notify the client and block until released
127+ if lock .locked ():
128+ yield IndexWaitingNotice ()
129+
120130 async with lock :
121131 self ._indexing [project_root ] = True
122132 try :
123- await project .update_index ()
133+ progress_queue : asyncio .Queue [IndexingProgress ] = asyncio .Queue ()
134+
135+ def on_progress (progress : IndexingProgress ) -> None :
136+ progress_queue .put_nowait (progress )
137+
138+ update_task = asyncio .create_task (project .update_index (on_progress = on_progress ))
139+
140+ # Drain the queue until the update completes
141+ while not update_task .done ():
142+ try :
143+ progress = await asyncio .wait_for (progress_queue .get (), timeout = 0.1 )
144+ yield IndexProgressUpdate (progress = progress )
145+ except TimeoutError :
146+ continue
147+
148+ # Drain any remaining items
149+ while not progress_queue .empty ():
150+ yield IndexProgressUpdate (progress = progress_queue .get_nowait ())
151+
152+ # Propagate any exception from the update task
153+ update_task .result ()
154+
155+ yield IndexResponse (success = True )
156+ except Exception as e :
157+ yield IndexResponse (success = False , message = str (e ))
124158 finally :
125159 self ._indexing [project_root ] = False
126160
@@ -177,11 +211,14 @@ def get_status(self, project_root: str) -> ProjectStatusResponse:
177211 " GROUP BY language ORDER BY cnt DESC"
178212 ).fetchall ()
179213
214+ is_indexing = self ._indexing .get (project_root , False )
215+ progress = project .indexing_stats if is_indexing else None
180216 return ProjectStatusResponse (
181- indexing = self . _indexing . get ( project_root , False ) ,
217+ indexing = is_indexing ,
182218 total_chunks = total_chunks ,
183219 total_files = total_files ,
184220 languages = {lang : cnt for lang , cnt in lang_rows },
221+ progress = progress ,
185222 )
186223
187224 def list_projects (self ) -> list [DaemonProjectInfo ]:
@@ -246,8 +283,12 @@ def _recv() -> bytes:
246283 handshake_done = True
247284 continue
248285
249- resp = await _dispatch (req , registry , start_time , shutdown_event )
250- conn .send_bytes (encode_response (resp ))
286+ result = await _dispatch (req , registry , start_time , shutdown_event )
287+ if isinstance (result , AsyncIterator ):
288+ async for resp in result :
289+ conn .send_bytes (encode_response (resp ))
290+ else :
291+ conn .send_bytes (encode_response (result ))
251292
252293 if isinstance (req , StopRequest ):
253294 break
@@ -265,16 +306,21 @@ async def _dispatch(
265306 registry : ProjectRegistry ,
266307 start_time : float ,
267308 shutdown_event : asyncio .Event ,
268- ) -> Response :
269- """Dispatch a request to the appropriate handler."""
309+ ) -> Response | AsyncIterator [IndexStreamResponse ]:
310+ """Dispatch a request to the appropriate handler.
311+
312+ Returns a single Response for most requests, or an AsyncIterator for
313+ streaming requests (IndexRequest).
314+ """
270315 try :
271316 if isinstance (req , IndexRequest ):
272- await registry .update_index (req .project_root )
273- return IndexResponse (success = True )
317+ return registry .update_index (req .project_root )
274318
275319 if isinstance (req , SearchRequest ):
276320 if req .refresh :
277- await registry .update_index (req .project_root )
321+ # Consume the index stream silently for refresh
322+ async for _ in registry .update_index (req .project_root ):
323+ pass
278324 results = await registry .search (
279325 project_root = req .project_root ,
280326 query = req .query ,
0 commit comments