diff --git a/internal/agent/completion_runtime.go b/internal/agent/completion_runtime.go index fe06ce89..5061cb36 100644 --- a/internal/agent/completion_runtime.go +++ b/internal/agent/completion_runtime.go @@ -21,14 +21,6 @@ func (e *defaultEngine) completeTurn(ctx context.Context, request llm.ChatReques return runner.client.CreateMessage(ctx, request) } - request.OnStreamProgress = func(charCount int, active bool) { - runner.emit(Event{ - Type: EventThinkingProgress, - ReasoningCharCount: charCount, - ReasoningActive: active, - }) - } - reply, err := runner.client.StreamMessage(ctx, request, func(delta string) { if delta != "" { emitTurnEvent(ctx, TurnEvent{ diff --git a/internal/agent/events.go b/internal/agent/events.go index 0f1f5f3a..447516ee 100644 --- a/internal/agent/events.go +++ b/internal/agent/events.go @@ -17,7 +17,6 @@ const ( EventPlanUpdated EventType = "plan_updated" EventUsageUpdated EventType = "usage_updated" EventRunFinished EventType = "run_finished" - EventThinkingProgress EventType = "thinking_progress" ) type Event struct { @@ -34,10 +33,6 @@ type Event struct { Usage llm.Usage AgentID string // non-empty when emitted by a subagent InvocationID string // non-empty when emitted by a subagent, globally unique per invocation - - // EventThinkingProgress fields — reasoning progress without exposing content. - ReasoningCharCount int - ReasoningActive bool } type Observer interface { diff --git a/internal/agent/runner_usage_stream_test.go b/internal/agent/runner_usage_stream_test.go index 181f56fa..a0d65b31 100644 --- a/internal/agent/runner_usage_stream_test.go +++ b/internal/agent/runner_usage_stream_test.go @@ -78,68 +78,3 @@ func TestRunPromptEmitsUsageEventForStreamingReplyUsage(t *testing.T) { t.Fatalf("expected exactly 1 usage event, got %d", usageEvents) } } - -type reasoningProgressStreamClient struct { - reply llm.Message -} - -func (c *reasoningProgressStreamClient) CreateMessage(context.Context, llm.ChatRequest) (llm.Message, error) { - return c.reply, nil -} - -func (c *reasoningProgressStreamClient) StreamMessage(_ context.Context, req llm.ChatRequest, onDelta func(string)) (llm.Message, error) { - if req.OnStreamProgress != nil { - req.OnStreamProgress(50, true) - req.OnStreamProgress(100, false) - } - if onDelta != nil && strings.TrimSpace(c.reply.Content) != "" { - onDelta(c.reply.Content) - } - return c.reply, nil -} - -func TestCompleteTurnEmitsThinkingProgressViaOnStreamProgress(t *testing.T) { - workspace := t.TempDir() - store, err := session.NewStore(t.TempDir()) - if err != nil { - t.Fatal(err) - } - sess := session.New(workspace) - - var events []Event - runner := NewRunner(Options{ - Workspace: workspace, - Config: config.Config{ - Provider: config.ProviderConfig{Model: "test-model"}, - MaxIterations: 1, - Stream: true, - }, - Client: &reasoningProgressStreamClient{reply: llm.Message{ - Role: llm.RoleAssistant, - Content: "final answer", - }}, - Store: store, - Registry: tools.DefaultRegistry(), - Observer: ObserverFunc(func(event Event) { events = append(events, event) }), - Stdin: strings.NewReader(""), - Stdout: io.Discard, - }) - - if _, err := runner.RunPrompt(context.Background(), sess, "hello", "build", io.Discard); err != nil { - t.Fatal(err) - } - - progressEvents := 0 - for _, event := range events { - if event.Type != EventThinkingProgress { - continue - } - progressEvents++ - if event.ReasoningCharCount != 50 && event.ReasoningCharCount != 100 { - t.Fatalf("unexpected reasoning char count: %d", event.ReasoningCharCount) - } - } - if progressEvents != 2 { - t.Fatalf("expected exactly 2 thinking progress events, got %d", progressEvents) - } -} diff --git a/internal/app/tui_adapter.go b/internal/app/tui_adapter.go index 08b8b1aa..48313003 100644 --- a/internal/app/tui_adapter.go +++ b/internal/app/tui_adapter.go @@ -190,21 +190,19 @@ func (a *tuiRunnerAdapter) DispatchSubAgent( func mapAgentEvent(event agent.Event) tui.Event { return tui.Event{ - Type: mapAgentEventType(event.Type), - SessionID: string(event.SessionID), - UserInput: event.UserInput, - Content: event.Content, - ToolName: event.ToolName, - ToolCallID: event.ToolCallID, - ToolArguments: event.ToolArguments, - ToolResult: event.ToolResult, - Error: event.Error, - Plan: event.Plan, - Usage: event.Usage, - AgentID: event.AgentID, - InvocationID: event.InvocationID, - ReasoningCharCount: event.ReasoningCharCount, - ReasoningActive: event.ReasoningActive, + Type: mapAgentEventType(event.Type), + SessionID: string(event.SessionID), + UserInput: event.UserInput, + Content: event.Content, + ToolName: event.ToolName, + ToolCallID: event.ToolCallID, + ToolArguments: event.ToolArguments, + ToolResult: event.ToolResult, + Error: event.Error, + Plan: event.Plan, + Usage: event.Usage, + AgentID: event.AgentID, + InvocationID: event.InvocationID, } } @@ -226,8 +224,6 @@ func mapAgentEventType(value agent.EventType) tui.EventType { return tui.EventUsageUpdated case agent.EventRunFinished: return tui.EventRunFinished - case agent.EventThinkingProgress: - return tui.EventThinkingProgress default: return tui.EventType(value) } diff --git a/internal/app/tui_adapter_test.go b/internal/app/tui_adapter_test.go index 17fe765b..d8168a75 100644 --- a/internal/app/tui_adapter_test.go +++ b/internal/app/tui_adapter_test.go @@ -167,18 +167,16 @@ func TestTUIRunnerAdapterNilGuardMethods(t *testing.T) { func TestMapAgentEventAndType(t *testing.T) { source := agent.Event{ - Type: agent.EventToolCallCompleted, - SessionID: "sess-1", - UserInput: "/review check changes", - Content: "done", - ToolName: "delegate_subagent", - ToolArguments: `{"agent":"review"}`, - ToolResult: `{"ok":true}`, - Error: "none", - Plan: planpkg.State{}, - Usage: llm.Usage{InputTokens: 12, OutputTokens: 34}, - ReasoningCharCount: 200, - ReasoningActive: true, + Type: agent.EventToolCallCompleted, + SessionID: "sess-1", + UserInput: "/review check changes", + Content: "done", + ToolName: "delegate_subagent", + ToolArguments: `{"agent":"review"}`, + ToolResult: `{"ok":true}`, + Error: "none", + Plan: planpkg.State{}, + Usage: llm.Usage{InputTokens: 12, OutputTokens: 34}, } mapped := mapAgentEvent(source) @@ -191,16 +189,10 @@ func TestMapAgentEventAndType(t *testing.T) { if mapped.Usage.InputTokens != 12 || mapped.Usage.OutputTokens != 34 { t.Fatalf("expected mapped usage to be preserved, got %#v", mapped.Usage) } - if mapped.ReasoningCharCount != 200 || !mapped.ReasoningActive { - t.Fatalf("expected reasoning progress fields to be mapped, got charCount=%d active=%v", mapped.ReasoningCharCount, mapped.ReasoningActive) - } if got := mapAgentEventType(agent.EventRunStarted); got != tui.EventRunStarted { t.Fatalf("expected run-started mapping, got %q", got) } - if got := mapAgentEventType(agent.EventThinkingProgress); got != tui.EventThinkingProgress { - t.Fatalf("expected thinking-progress mapping, got %q", got) - } const unknown = agent.EventType("custom") if got := mapAgentEventType(unknown); got != tui.EventType(unknown) { t.Fatalf("expected fallback mapping for unknown event type, got %q", got) diff --git a/internal/llm/types.go b/internal/llm/types.go index 7bc329ce..fa67e235 100644 --- a/internal/llm/types.go +++ b/internal/llm/types.go @@ -341,13 +341,6 @@ type ChatRequest struct { Tools []ToolDefinition Assets map[AssetID]ImageAsset Temperature float64 - - // OnStreamProgress is called during streaming when the provider receives - // non-content progress updates (e.g. reasoning_content chunks from - // DeepSeek-style models). charCount is the cumulative number of - // characters received so far; active is true while reasoning is still - // streaming. - OnStreamProgress func(charCount int, active bool) } type Client interface { diff --git a/internal/mention/index.go b/internal/mention/index.go index 4f97b4c4..c5cc0a29 100644 --- a/internal/mention/index.go +++ b/internal/mention/index.go @@ -2,7 +2,6 @@ package mention import ( "bufio" - "io" "io/fs" "log" "os" @@ -21,9 +20,6 @@ const ( mentionTrieRecallMultiplier = 8 mentionTrieMinRecall = 64 mentionIgnoreMaxLineBytes = 4 * 1024 * 1024 - mentionIndexSeedMaxFiles = 200 - mentionIndexSeedMaxVisits = 1000 - mentionIndexSeedMaxDepth = 2 ) var ( @@ -54,8 +50,6 @@ type IndexStats struct { MaxFiles int Truncated bool Ready bool - Building bool - Partial bool } type mentionIgnoreMatcher struct { @@ -82,11 +76,6 @@ type WorkspaceFileIndex struct { trie *mentionTrie truncated bool maxFiles int - partial bool -} - -var startMentionIndexAsyncRebuild = func(idx *WorkspaceFileIndex, root string, maxFiles int) { - go idx.rebuildStarted(root, maxFiles) } func NewWorkspaceFileIndex(workspace string) *WorkspaceFileIndex { @@ -140,7 +129,7 @@ func (idx *WorkspaceFileIndex) SearchWithRecency(query string, limit int, recenc limit = defaultSearchLimit } - idx.ensureSeeded() + idx.ensureInitialBuild() idx.ensureFreshAsync() files, trie := idx.snapshotSearchData() if len(files) == 0 { @@ -198,7 +187,7 @@ func (idx *WorkspaceFileIndex) Prewarm() { if idx == nil { return } - idx.rebuildBlocking() + idx.ensureInitialBuild() } func (idx *WorkspaceFileIndex) Stats() IndexStats { @@ -212,8 +201,6 @@ func (idx *WorkspaceFileIndex) Stats() IndexStats { MaxFiles: idx.maxFiles, Truncated: idx.truncated, Ready: idx.ready, - Building: idx.building, - Partial: idx.partial, } } @@ -228,43 +215,35 @@ func (idx *WorkspaceFileIndex) snapshotSearchData() ([]Candidate, *mentionTrie) return out, idx.trie } -func (idx *WorkspaceFileIndex) ensureSeeded() { +func (idx *WorkspaceFileIndex) ensureInitialBuild() { if idx == nil { return } idx.mu.RLock() - hasData := idx.ready || len(idx.files) > 0 + ready := idx.ready idx.mu.RUnlock() - if hasData { + if ready { return } - - root := idx.root - maxFiles := idx.maxFiles - matcher := loadMentionIgnoreMatcher(root) - files, truncated := buildMentionSeed(root, maxFiles, matcher) - - idx.mu.Lock() - defer idx.mu.Unlock() - if idx.ready || len(idx.files) > 0 { - return - } - idx.files = files - idx.trie = newMentionTrie(files) - idx.truncated = truncated - idx.partial = len(files) > 0 + idx.rebuildBlocking() } func (idx *WorkspaceFileIndex) ensureFreshAsync() { if idx == nil { return } - idx.mu.Lock() - root, maxFiles, ok := idx.startRebuildLocked() - idx.mu.Unlock() + idx.mu.RLock() + stale := idx.shouldRebuildLocked() + ready := idx.ready + building := idx.building + idx.mu.RUnlock() - if ok { - startMentionIndexAsyncRebuild(idx, root, maxFiles) + if !ready { + idx.rebuildBlocking() + return + } + if stale && !building { + go idx.rebuildBlocking() } } @@ -273,24 +252,15 @@ func (idx *WorkspaceFileIndex) rebuildBlocking() { return } idx.mu.Lock() - root, maxFiles, ok := idx.startRebuildLocked() - idx.mu.Unlock() - if !ok { + if idx.building || !idx.shouldRebuildLocked() { + idx.mu.Unlock() return } - - idx.rebuildStarted(root, maxFiles) -} - -func (idx *WorkspaceFileIndex) startRebuildLocked() (root string, maxFiles int, ok bool) { - if idx == nil || idx.building || !idx.shouldRebuildLocked() { - return "", 0, false - } idx.building = true - return idx.root, idx.maxFiles, true -} + root := idx.root + maxFiles := idx.maxFiles + idx.mu.Unlock() -func (idx *WorkspaceFileIndex) rebuildStarted(root string, maxFiles int) { matcher := loadMentionIgnoreMatcher(root) files, truncated := buildMentionIndex(root, maxFiles, matcher) @@ -299,7 +269,6 @@ func (idx *WorkspaceFileIndex) rebuildStarted(root string, maxFiles int) { idx.trie = newMentionTrie(files) idx.truncated = truncated idx.ready = true - idx.partial = false idx.lastBuild = time.Now() idx.building = false idx.mu.Unlock() @@ -411,140 +380,6 @@ func buildMentionIndex(root string, maxFiles int, matcher mentionIgnoreMatcher) return files, truncated } -func buildMentionSeed(root string, maxFiles int, matcher mentionIgnoreMatcher) ([]Candidate, bool) { - root = strings.TrimSpace(root) - if root == "" { - return nil, false - } - if maxFiles <= 0 { - maxFiles = mentionIndexDefaultMaxFiles - } - limit := minInt(maxFiles, mentionIndexSeedMaxFiles) - if limit <= 0 { - return nil, false - } - - maxVisits := minInt(mentionMaxVisitsFromEnv(), mentionIndexSeedMaxVisits) - maxDirs := mentionMaxDirsFromEnv() - maxDuration := mentionMaxDurationFromEnv() - started := time.Now() - - type pendingDir struct { - abs string - rel string - depth int - } - - files := make([]Candidate, 0, minInt(limit, 64)) - queue := []pendingDir{{abs: root}} - truncated := false - visits := 0 - dirs := 0 - - for len(queue) > 0 && len(files) < limit { - current := queue[0] - queue = queue[1:] - - remainingVisits := maxVisits - visits - if remainingVisits <= 0 { - truncated = true - break - } - - entries, hitReadLimit := readMentionSeedDir(current.abs, remainingVisits) - if hitReadLimit { - truncated = true - } - - for _, entry := range entries { - if maxDuration > 0 && time.Since(started) > maxDuration { - truncated = true - break - } - visits++ - if visits > maxVisits { - truncated = true - break - } - - name := entry.Name() - rel := name - if current.rel != "" { - rel = filepath.ToSlash(filepath.Join(current.rel, name)) - } - - if entry.IsDir() { - dirs++ - if dirs > maxDirs { - truncated = true - break - } - if shouldSkipMentionDir(name) || matcher.SkipDir(name, rel) { - continue - } - files = append(files, Candidate{ - Path: rel + "/", - BaseName: name, - Kind: "dir", - }) - if len(files) >= limit { - truncated = true - break - } - if current.depth+1 < mentionIndexSeedMaxDepth { - queue = append(queue, pendingDir{ - abs: filepath.Join(current.abs, name), - rel: rel, - depth: current.depth + 1, - }) - } - continue - } - if entry.Type()&fs.ModeSymlink != 0 { - continue - } - if shouldSkipMentionFile(name) || matcher.SkipFile(name, rel) { - continue - } - files = append(files, Candidate{ - Path: rel, - BaseName: filepath.Base(rel), - TypeTag: mentionTypeTag(rel), - }) - if len(files) >= limit { - truncated = true - break - } - } - } - - sort.Slice(files, func(i, j int) bool { - return files[i].Path < files[j].Path - }) - return files, truncated -} - -func readMentionSeedDir(dir string, limit int) ([]fs.DirEntry, bool) { - if limit <= 0 { - return nil, true - } - f, err := os.Open(dir) - if err != nil { - return nil, false - } - defer f.Close() - - entries, err := f.ReadDir(limit) - hitLimit := err == nil && len(entries) == limit - if err != nil && err != io.EOF { - return nil, false - } - sort.Slice(entries, func(i, j int) bool { - return entries[i].Name() < entries[j].Name() - }) - return entries, hitLimit -} - func mentionCandidateIndices(files []Candidate, trie *mentionTrie, query string, limit int) ([]int, map[int]struct{}) { if len(files) == 0 { return nil, nil diff --git a/internal/mention/index_test.go b/internal/mention/index_test.go index 6e03934c..0e7dbefc 100644 --- a/internal/mention/index_test.go +++ b/internal/mention/index_test.go @@ -188,68 +188,6 @@ func TestWorkspaceFileIndexSearchWithRecencyPrioritizesRecent(t *testing.T) { } } -func TestWorkspaceFileIndexColdSearchUsesSeedAndSchedulesRebuild(t *testing.T) { - workspace := t.TempDir() - mustWriteMentionFile(t, filepath.Join(workspace, "shallow.go"), "package main") - mustWriteMentionFile(t, filepath.Join(workspace, "deep", "a", "b", "target.go"), "package main") - - oldStart := startMentionIndexAsyncRebuild - calls := 0 - startMentionIndexAsyncRebuild = func(_ *WorkspaceFileIndex, _ string, _ int) { - calls++ - } - t.Cleanup(func() { - startMentionIndexAsyncRebuild = oldStart - }) - - index := NewWorkspaceFileIndex(workspace) - results := index.Search("shallow", 10) - if len(results) == 0 || results[0].Path != "shallow.go" { - t.Fatalf("expected cold search to return seed result shallow.go, got %#v", results) - } - if calls != 1 { - t.Fatalf("expected cold search to schedule async rebuild once, got %d", calls) - } - stats := index.Stats() - if !stats.Partial || !stats.Building || stats.Ready { - t.Fatalf("expected partial building stats before full rebuild, got %+v", stats) - } - - files, _ := index.snapshotSearchData() - paths := make([]string, 0, len(files)) - for _, item := range files { - paths = append(paths, item.Path) - } - if containsString(paths, "deep/a/b/target.go") { - t.Fatalf("expected seed index not to include deep target file, got %v", paths) - } -} - -func TestWorkspaceFileIndexAsyncRebuildReplacesSeed(t *testing.T) { - workspace := t.TempDir() - mustWriteMentionFile(t, filepath.Join(workspace, "shallow.go"), "package main") - mustWriteMentionFile(t, filepath.Join(workspace, "deep", "a", "b", "target.go"), "package main") - - index := NewWorkspaceFileIndex(workspace) - _ = index.Search("shallow", 10) - - deadline := time.Now().Add(800 * time.Millisecond) - for { - results := index.Search("target", 10) - if len(results) > 0 && results[0].Path == "deep/a/b/target.go" { - stats := index.Stats() - if !stats.Ready || stats.Partial { - t.Fatalf("expected full index stats after async rebuild, got %+v", stats) - } - break - } - if time.Now().After(deadline) { - t.Fatalf("expected async rebuild to find deep target file") - } - time.Sleep(20 * time.Millisecond) - } -} - func TestWorkspaceFileIndexUsesTriePrefixRecall(t *testing.T) { idx := NewStaticWorkspaceFileIndex([]Candidate{ {Path: "README.md"}, diff --git a/internal/provider/openai.go b/internal/provider/openai.go index 4804b657..27f5cbbc 100644 --- a/internal/provider/openai.go +++ b/internal/provider/openai.go @@ -196,9 +196,6 @@ func (c *OpenAICompatible) StreamMessage(ctx context.Context, req llm.ChatReques } if delta.Reasoning != "" { appendOpenAIReasoningContent(&assembled, delta.Reasoning) - if req.OnStreamProgress != nil { - req.OnStreamProgress(len(openAIReasoningContent(assembled)), true) - } } if delta.Content != "" { assembled.Content += delta.Content @@ -264,12 +261,6 @@ func (c *OpenAICompatible) StreamMessage(ctx context.Context, req llm.ChatReques } assembled.Normalize() - if req.OnStreamProgress != nil { - reasoningChars := len(openAIReasoningContent(assembled)) - if reasoningChars > 0 { - req.OnStreamProgress(reasoningChars, false) - } - } return assembled, nil } diff --git a/internal/provider/openai_test.go b/internal/provider/openai_test.go index 85b737e9..2dcf7935 100644 --- a/internal/provider/openai_test.go +++ b/internal/provider/openai_test.go @@ -616,68 +616,6 @@ func TestOpenAIMessagesDoesNotSendReasoningToProviderWithoutReasoningContent(t * } } -func TestOpenAICompatibleStreamMessageFiresProgressCallbackForReasoningContent(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "text/event-stream") - _, _ = w.Write([]byte(strings.Join([]string{ - `data: {"choices":[{"delta":{"role":"assistant","reasoning_content":"thinking step 1"}}]}`, - `data: {"choices":[{"delta":{"reasoning_content":" step 2"}}]}`, - `data: {"choices":[{"delta":{"content":"actual answer"}}]}`, - `data: [DONE]`, - "", - }, "\n"))) - })) - defer server.Close() - - client := NewOpenAICompatible(Config{BaseURL: server.URL, APIKey: "test-key", Model: "fallback-model"}) - - var progressCalls []struct { - charCount int - active bool - } - req := llm.ChatRequest{ - OnStreamProgress: func(charCount int, active bool) { - progressCalls = append(progressCalls, struct { - charCount int - active bool - }{charCount, active}) - }, - } - msg, err := client.StreamMessage(context.Background(), req, nil) - if err != nil { - t.Fatal(err) - } - - // Reasoning should trigger at least 2 progress calls (one per reasoning delta). - if len(progressCalls) < 2 { - t.Fatalf("expected at least 2 progress calls for reasoning chunks, got %d", len(progressCalls)) - } - - // All intermediate calls should be active=true. - for i, call := range progressCalls[:len(progressCalls)-1] { - if !call.active { - t.Fatalf("progress call[%d]: expected active=true, got false", i) - } - } - - // The final call (after streaming loop) should be active=false. - last := progressCalls[len(progressCalls)-1] - if last.active { - t.Fatalf("final progress call: expected active=false after stream ends, got true") - } - if last.charCount <= 0 { - t.Fatalf("final progress call: expected positive charCount, got %d", last.charCount) - } - - // Reasoning content must NOT appear in assembled message content. - if msg.Content != "actual answer" { - t.Fatalf("expected message content to exclude reasoning, got %q", msg.Content) - } - if got := openAIReasoningContent(msg); got != "thinking step 1 step 2" { - t.Fatalf("expected reasoning in Meta, got %q", got) - } -} - func TestOpenAIMessagesDegradesMissingImageAsset(t *testing.T) { messages, err := openAIMessages(llm.ChatRequest{ Messages: []llm.Message{{ diff --git a/tui/component_chat_stream.go b/tui/component_chat_stream.go index 3f850dd1..0173d7aa 100644 --- a/tui/component_chat_stream.go +++ b/tui/component_chat_stream.go @@ -271,8 +271,6 @@ func (m *model) updateThinkingCard() { item.Status = "thinking" if strings.TrimSpace(item.Body) == "" { item.Body = m.thinkingText() - } else if m.reasoningProgressActive { - item.Body = "receiving hidden reasoning..." } } @@ -298,23 +296,6 @@ func (m *model) ensureThinkingCard() { m.streamingIndex = len(m.chatItems) - 1 } -func (m *model) applyReasoningProgress(_ int, active bool) { - m.reasoningProgressActive = active - m.ensureThinkingCard() - if m.streamingIndex < 0 || m.streamingIndex >= len(m.chatItems) { - return - } - if item := &m.chatItems[m.streamingIndex]; item.Kind == "assistant" && (item.Status == "pending" || item.Status == "thinking") { - item.Title = thinkingLabel - item.Status = "thinking" - if active { - item.Body = "receiving hidden reasoning..." - } else { - item.Body = m.thinkingText() - } - } -} - func (m *model) failLatestAssistant(errText string) { errText = strings.TrimSpace(errText) if errText == "" { diff --git a/tui/component_chat_stream_test.go b/tui/component_chat_stream_test.go index e9718470..a0b00201 100644 --- a/tui/component_chat_stream_test.go +++ b/tui/component_chat_stream_test.go @@ -943,125 +943,6 @@ func TestHandleAgentEventPlanUpdatedCopiesPlan(t *testing.T) { } } -func TestHandleAgentEventThinkingProgressCreatesThinkingCard(t *testing.T) { - m := model{ - phase: "thinking", - spinner: spinner.New(), - } - m.handleAgentEvent(Event{ - Type: EventThinkingProgress, - ReasoningCharCount: 150, - ReasoningActive: true, - }) - - if !m.reasoningProgressActive { - t.Fatal("expected reasoningProgressActive to be true after progress event") - } - if m.phase != "thinking" { - t.Fatalf("expected thinking phase, got %q", m.phase) - } - if len(m.chatItems) != 1 { - t.Fatalf("expected 1 chat item (thinking card), got %d", len(m.chatItems)) - } - item := m.chatItems[0] - if item.Kind != "assistant" { - t.Fatalf("expected assistant kind, got %q", item.Kind) - } - if item.Status != "thinking" { - t.Fatalf("expected thinking status, got %q", item.Status) - } - if !strings.Contains(item.Body, "receiving hidden reasoning") { - t.Fatalf("expected reasoning progress note in body, got %q", item.Body) - } - // Reasoning content must NOT appear in the body. - if strings.Contains(item.Body, "step") { - t.Fatalf("body must not contain reasoning content, got %q", item.Body) - } -} - -func TestHandleAgentEventThinkingProgressInactiveRemovesNote(t *testing.T) { - m := model{ - phase: "thinking", - reasoningProgressActive: true, - spinner: spinner.New(), - } - m.handleAgentEvent(Event{ - Type: EventThinkingProgress, - ReasoningCharCount: 300, - ReasoningActive: false, - }) - - if m.reasoningProgressActive { - t.Fatal("expected reasoningProgressActive to be false after inactive event") - } - if len(m.chatItems) != 1 { - t.Fatalf("expected thinking card, got %d items", len(m.chatItems)) - } - if strings.Contains(m.chatItems[0].Body, "receiving hidden reasoning") { - t.Fatalf("expected reasoning note removed, got %q", m.chatItems[0].Body) - } -} - -func TestHandleAgentEventAssistantDeltaClearsReasoningProgress(t *testing.T) { - m := model{ - phase: "thinking", - reasoningProgressActive: true, - } - m.handleAgentEvent(Event{Type: EventAssistantDelta, Content: "Here is the answer."}) - - if m.reasoningProgressActive { - t.Fatal("expected reasoningProgressActive to be cleared by assistant delta") - } - if m.phase != "responding" { - t.Fatalf("expected responding phase, got %q", m.phase) - } -} - -func TestHandleAgentEventToolCallStartedClearsReasoningProgress(t *testing.T) { - m := model{ - phase: "thinking", - reasoningProgressActive: true, - chatItems: []chatEntry{ - {Kind: "assistant", Title: thinkingLabel, Body: "thinking...", Status: "thinking"}, - }, - streamingIndex: 0, - } - m.handleAgentEvent(Event{Type: EventToolCallStarted, ToolName: "list_files"}) - - if m.reasoningProgressActive { - t.Fatal("expected reasoningProgressActive to be cleared by tool call start") - } -} - -func TestUpdateThinkingCardPreservesReasoningNote(t *testing.T) { - m := model{ - busy: true, - streamingIndex: 0, - reasoningProgressActive: true, - chatItems: []chatEntry{ - {Kind: "assistant", Title: thinkingLabel, Body: "old body", Status: "thinking"}, - }, - } - m.updateThinkingCard() - - if m.chatItems[0].Body != "receiving hidden reasoning..." { - t.Fatalf("expected reasoning note to be preserved, got %q", m.chatItems[0].Body) - } -} - -func TestApplyReasoningProgressSkipsWhenItemIsNotThinkingAssistant(t *testing.T) { - m := model{ - streamingIndex: 0, - chatItems: []chatEntry{ - {Kind: "tool", Title: "TOOL | list_files", Body: "done", Status: "done"}, - }, - } - m.applyReasoningProgress(100, true) - if m.chatItems[0].Body != "done" { - t.Fatalf("expected tool item to be untouched, got %q", m.chatItems[0].Body) - } -} - // --- Rate limit error scenarios (from screenshot) --- func TestRunFailedWithToolResultErrorContent(t *testing.T) { diff --git a/tui/component_palettes.go b/tui/component_palettes.go index 4b5d6654..df13ee1d 100644 --- a/tui/component_palettes.go +++ b/tui/component_palettes.go @@ -157,17 +157,8 @@ func (m model) renderMentionPalette() string { metaText := "* recent + file/dir * agent Type @query Up/Down Enter/Tab insert Esc close" if m.mentionIndex != nil { stats := m.mentionIndex.Stats() - switch { - case stats.Partial && stats.Building: - metaText = "* recent indexing... showing partial results Enter/Tab insert Esc close" - case stats.Partial: - metaText = "* recent showing partial results Enter/Tab insert Esc close" - case stats.Building: - metaText = "* recent refreshing index... Enter/Tab insert Esc close" - case stats.Truncated && stats.MaxFiles > 0: + if stats.Truncated && stats.MaxFiles > 0 { metaText = fmt.Sprintf("* recent indexed first %d files Enter/Tab insert Esc close", stats.MaxFiles) - case stats.Ready: - metaText = "* recent index ready Type @query Up/Down Enter/Tab insert Esc close" } } rows = append(rows, commandPaletteMetaStyle.Render(metaText)) diff --git a/tui/component_run_flow.go b/tui/component_run_flow.go index e3fffd13..aa17fe53 100644 --- a/tui/component_run_flow.go +++ b/tui/component_run_flow.go @@ -35,7 +35,6 @@ func (m *model) beginRunWithInput(promptInput RunPromptInput, mode, note string) } m.statusNote = note m.phase = "thinking" - m.reasoningProgressActive = false m.llmConnected = true m.busy = true m.runStartedAt = time.Now() @@ -211,13 +210,7 @@ func (m *model) handleAgentEvent(event Event) { case EventRunStarted: m.tempEstimatedOutput = 0 m.lastTokenReceivedAt = time.Now() - case EventThinkingProgress: - m.phase = "thinking" - m.llmConnected = true - m.lastTokenReceivedAt = time.Now() - m.applyReasoningProgress(event.ReasoningCharCount, event.ReasoningActive) case EventAssistantDelta: - m.reasoningProgressActive = false m.phase = "responding" m.statusNote = "LLM is responding..." m.llmConnected = true @@ -229,7 +222,6 @@ func (m *model) handleAgentEvent(event Event) { m.lastTokenReceivedAt = time.Now() m.finishAssistantMessage(event.Content) case EventToolCallStarted: - m.reasoningProgressActive = false m.phase = "tool" m.llmConnected = true m.lastTokenReceivedAt = time.Now() diff --git a/tui/model.go b/tui/model.go index 94469b6d..3be86cd9 100644 --- a/tui/model.go +++ b/tui/model.go @@ -415,7 +415,6 @@ type model struct { runIndicatorState runIndicatorState streamingIndex int suppressedAssistantDelta string - reasoningProgressActive bool statusNote string phase string llmConnected bool diff --git a/tui/model_test.go b/tui/model_test.go index 75a76385..9d6d7274 100644 --- a/tui/model_test.go +++ b/tui/model_test.go @@ -5261,27 +5261,6 @@ func TestRenderMentionPaletteShowsTruncatedMeta(t *testing.T) { } } -func TestRenderMentionPaletteShowsReadyMeta(t *testing.T) { - index := mention.NewStaticWorkspaceFileIndex([]mention.Candidate{ - {Path: "a.go", BaseName: "a.go", TypeTag: "go"}, - }, 0, false) - - m := model{ - screen: screenChat, - width: 100, - mentionOpen: true, - mentionResults: []mention.Candidate{ - {Path: "a.go", BaseName: "a.go", TypeTag: "go"}, - }, - mentionIndex: index, - } - - view := m.renderMentionPalette() - if !strings.Contains(view, "index ready") { - t.Fatalf("expected mention palette to show ready hint, got %q", view) - } -} - func TestCommandPaletteAllowsTypingJKWhenOpen(t *testing.T) { input := textarea.New() input.Focus() diff --git a/tui/ports.go b/tui/ports.go index a32420c2..ca381a99 100644 --- a/tui/ports.go +++ b/tui/ports.go @@ -25,7 +25,6 @@ const ( EventPlanUpdated EventType = "plan_updated" EventUsageUpdated EventType = "usage_updated" EventRunFinished EventType = "run_finished" - EventThinkingProgress EventType = "thinking_progress" ) type Event struct { @@ -42,10 +41,6 @@ type Event struct { Usage llm.Usage AgentID string // non-empty when emitted by a subagent InvocationID string // non-empty when emitted by a subagent, globally unique per invocation - - // EventThinkingProgress fields — reasoning progress without exposing content. - ReasoningCharCount int - ReasoningActive bool } type ApprovalRequest struct {