Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions internal/tool/code_index_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ type CodeIndexManager struct {
rebuildCh chan struct{} // signaled by MarkDirty to trigger immediate debounced rebuild
lockFile *os.File // cross-process flock handle
onReady func(stats CodeIndexStats) // optional callback when index build completes
onProgress func(done, total int) // optional throttled build progress (doBuild goroutine)

// indexStats tracks basic stats for debugging/logging.
stats CodeIndexStats
Expand Down Expand Up @@ -358,6 +359,17 @@ func (m *CodeIndexManager) doBuild(ctx context.Context) {
skipped, indexed := 0, 0
maxTerms := codeIndexMaxTotalTermsOverride()

// Progress throttle: report at most every 400ms so a long walk over a
// large or slow tree updates the "Building code index..." status line
// instead of sitting silent (which reads as a hung UI). The callback
// fires on THIS goroutine and must stay cheap (the REPL just Sends a
// message); snapshot it once - SetOnProgress during a build is not a
// scenario worth locking for.
m.mu.RLock()
progressFn := m.onProgress
m.mu.RUnlock()
lastProgress := time.Now()

for _, absPath := range files {
if maxTerms > 0 && totalTerms >= maxTerms {
truncated = true
Expand Down Expand Up @@ -427,6 +439,10 @@ func (m *CodeIndexManager) doBuild(ctx context.Context) {
totalTerms += len(tf)
totalLength += len(terms)
indexed++
if progressFn != nil && time.Since(lastProgress) >= 400*time.Millisecond {
lastProgress = time.Now()
progressFn(indexed, len(files))
}
}
if truncated {
debug.Log("codeindex", "term budget reached (%d terms over %d docs) - index truncated at %d of %d files; "+
Expand Down Expand Up @@ -493,6 +509,16 @@ func (m *CodeIndexManager) SetOnReady(fn func(stats CodeIndexStats)) {
m.mu.Unlock()
}

// SetOnProgress registers a throttled callback reporting build progress
// (fired on the doBuild goroutine, at most ~every 400ms). The REPL uses
// it to update the "Building code index..." status line so a long walk
// over a large or slow tree does not read as a hung UI.
func (m *CodeIndexManager) SetOnProgress(fn func(done, total int)) {
m.mu.Lock()
defer m.mu.Unlock()
m.onProgress = fn
}

// collectFiles walks the working directory and returns a list of
// indexable source files.
func (m *CodeIndexManager) collectFiles(ctx context.Context) []string {
Expand Down
30 changes: 30 additions & 0 deletions internal/tool/code_index_progress_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package tool

import (
"sync"
"testing"
)

// SetOnProgress registers without racing and the callback is stored.
// (The throttled firing itself is exercised via doBuild integration in
// the REPL; here we pin the registration contract.)
func TestSetOnProgressRegistration(t *testing.T) {
m := NewCodeIndexManager(t.TempDir())
var mu sync.Mutex
called := 0
m.SetOnProgress(func(done, total int) {
mu.Lock()
called++
mu.Unlock()
})
if m.onProgress == nil {
t.Fatalf("onProgress must be stored")
}
// Fire it directly (doBuild calls it on its own goroutine).
m.onProgress(1, 2)
mu.Lock()
defer mu.Unlock()
if called != 1 {
t.Fatalf("callback not invoked, called=%d", called)
}
}
11 changes: 10 additions & 1 deletion internal/tui/repl.go
Original file line number Diff line number Diff line change
Expand Up @@ -1637,8 +1637,17 @@ func (r *REPL) Run() error {
if r.agent != nil {
if cim := r.agent.CodeIndexManager(); cim != nil {
if !cim.IsReady() {
r.program.Send(systemMsg{msg: "Building code index for @ fuzzy search..."})
r.program.Send(systemNotifyMsg{ItemID: "codeindex-progress", Replace: true, Text: "Building code index for @ fuzzy search..."})
}
// Throttled progress on the same status line: a long walk
// over a large/slow tree stays visibly alive instead of
// reading as a hung UI (screenshot report 2026-09-18).
cim.SetOnProgress(func(done, total int) {
if r.program != nil {
r.program.Send(systemNotifyMsg{ItemID: "codeindex-progress", Replace: true,
Text: fmt.Sprintf("Building code index... %d/%d files", done, total)})
}
})
cim.SetOnReady(func(stats tool.CodeIndexStats) {
if stats.IndexedFiles > 0 && r.program != nil {
r.program.Send(systemMsg{msg: fmt.Sprintf("Code index ready: %d files indexed - @ fuzzy search enabled", stats.IndexedFiles)})
Expand Down
Loading