Skip to content
Open
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
12 changes: 12 additions & 0 deletions internal/providercatalog/catalog.go
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,18 @@ func All() []Descriptor {
return copied
}

// Find returns the first catalog entry matching pred. The predicate reads the
// internal (uncloned) descriptors, so scanning on a hot path costs no
// allocations; only a match pays the single clone that Get already pays.
func Find(pred func(Descriptor) bool) (Descriptor, bool) {
for _, descriptor := range descriptors {
if pred(descriptor) {
return cloneDescriptor(descriptor), true
}
}
return Descriptor{}, false
}

func Get(id string) (Descriptor, bool) {
normalized := NormalizeID(id)
for _, descriptor := range descriptors {
Expand Down
25 changes: 25 additions & 0 deletions internal/providercatalog/catalog_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,31 @@ func TestAllHasStableUniqueIDs(t *testing.T) {
}
}

func TestFindReturnsMatchingDescriptorAsClone(t *testing.T) {
descriptor, ok := Find(func(candidate Descriptor) bool {
return candidate.DefaultBaseURL == "https://api.openai.com/v1"
})
if !ok {
t.Fatal("Find() did not locate the OpenAI catalog entry by base URL")
}
if descriptor.ID != "openai" {
t.Fatalf("Find() returned ID %q, want %q", descriptor.ID, "openai")
}
// Find must hand back a clone like Get: mutating the returned slices must
// not corrupt the internal catalog for later lookups.
if len(descriptor.AuthEnvVars) > 0 {
descriptor.AuthEnvVars[0] = "MUTATED"
}
if again, ok := Find(func(candidate Descriptor) bool { return candidate.ID == "openai" }); !ok {
t.Fatal("Find() lost the OpenAI entry after the first result was mutated")
} else if len(again.AuthEnvVars) > 0 && again.AuthEnvVars[0] == "MUTATED" {
t.Fatal("Find() returned an internal catalog slice without cloning")
}
if _, ok := Find(func(Descriptor) bool { return false }); ok {
t.Fatal("Find() reported a match for a predicate that never holds")
}
}

func TestRecommendedProvidersAreTopOfCatalog(t *testing.T) {
descriptors := All()
// The recommended providers are badged and pinned to the top of the catalog,
Expand Down
4 changes: 4 additions & 0 deletions internal/tui/flush.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,10 @@ func (m *model) resetFlushFrontier(divider string) {
m.altScreenSettledItems = nil
m.altScreenSettledWidth = 0
m.altScreenSettledFrontier = 0
// The rebuilt transcript shares no working set with the old one; release the
// caches (and any auto-growth they earned) instead of holding dead entries.
m.transcriptBodyHeights.reset()
defaultTranscriptBodyRenders.reset()
// Renumbers every row's bodyY from the top (used by /clear, /resume, /rewind,
// /compact); a transcript-hover target's bodyY would otherwise risk
// coincidentally matching an unrelated row in the rebuilt transcript.
Expand Down
14 changes: 11 additions & 3 deletions internal/tui/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -3972,8 +3972,16 @@ func (m model) liveReasoningBodyCap() int {
}

func (m model) interimBlock(width int) string {
return m.interimBlockWithReasoning(width, m.streamingReasoningString())
}

// interimBlockWithReasoning renders the live interim block from an already
// converted reasoning string, so a render pass that also builds the streaming
// reasoning's selectable lines converts the accumulated bytes once per frame
// instead of once per consumer.
func (m model) interimBlockWithReasoning(width int, reasoningRaw string) string {
text := strings.TrimRight(m.streamingTextString(), "\n")
reasoning := strings.TrimRight(m.streamingReasoningString(), "\n")
reasoning := strings.TrimRight(reasoningRaw, "\n")
blocks := []string{}
if strings.TrimSpace(reasoning) != "" {
blocks = append(blocks, renderReasoningBlock(reasoning, m.streamingReasoningExpanded, width, true, 0, m.liveReasoningBodyCap()))
Expand Down Expand Up @@ -4068,7 +4076,7 @@ func (m model) activeToolCall() (transcriptRow, bool) {
if !m.pending || m.activeRunID == 0 {
return transcriptRow{}, false
}
var resolved map[string]struct{}
var resolved map[rowKey]struct{}
for i, scanned := len(m.transcript)-1, 0; i >= 0 && scanned < activeToolCallScanLimit; i, scanned = i-1, scanned+1 {
row := m.transcript[i]
if row.runID != m.activeRunID || row.id == "" {
Expand All @@ -4078,7 +4086,7 @@ func (m model) activeToolCall() (transcriptRow, bool) {
switch row.kind {
case rowToolResult:
if resolved == nil {
resolved = make(map[string]struct{})
resolved = make(map[rowKey]struct{})
}
resolved[key] = struct{}{}
case rowToolCall:
Expand Down
14 changes: 7 additions & 7 deletions internal/tui/picker.go
Original file line number Diff line number Diff line change
Expand Up @@ -835,17 +835,17 @@ func providerDescriptorByBaseURL(baseURL string) (providercatalog.Descriptor, bo
if normalized == "" {
return providercatalog.Descriptor{}, false
}
for _, descriptor := range providercatalog.All() {
// Find scans the internal catalog without cloning. This runs on render-adjacent
// paths (see modelContextWindow), where the previous All() scan cloned every
// descriptor on every call.
return providercatalog.Find(func(descriptor providercatalog.Descriptor) bool {
if genericProviderCatalogID(descriptor.ID) {
continue
return false
}
// Includes legacy catalog hosts, so a profile saved against a provider's
// earlier default base URL still maps to its catalog entry.
if providercatalog.IsCatalogBaseURL(descriptor, normalized) {
return descriptor, true
}
}
return providercatalog.Descriptor{}, false
return providercatalog.IsCatalogBaseURL(descriptor, normalized)
})
}

func normalizeProviderBaseURL(baseURL string) string {
Expand Down
51 changes: 51 additions & 0 deletions internal/tui/picker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1854,3 +1854,54 @@ func TestProviderDescriptorByBaseURLMapsLegacyOpenGatewayHost(t *testing.T) {
t.Fatal("an unknown host must not map to a catalog entry")
}
}

func TestModelContextWindowResolvesBaseURLOnlyProfilePerFrame(t *testing.T) {
// view.go calls modelContextWindow on every render, so its active-provider
// lookup must resolve a catalog-less, base-URL-only profile through the
// providercatalog.Find scan added for #1196. A model ID carried by both the
// active provider's discovered list and another provider's pins that the
// active-provider result wins, not the random-order fallback map scan.
registry, err := modelregistry.DefaultRegistry()
if err != nil {
t.Fatalf("load default registry: %v", err)
}
const unknownModel = "totally-unknown-proxy-model"
if _, ok := registry.Resolve(unknownModel); ok {
t.Fatalf("%q unexpectedly resolves in the curated registry; pick another ID", unknownModel)
}
m := model{
modelCatalog: registry,
providerProfile: config.ProviderProfile{
Name: "proxy",
ProviderKind: config.ProviderKindOpenAICompatible,
BaseURL: config.OpenAIBaseURL,
},
modelPickerLiveByProvider: map[string][]providermodeldiscovery.Model{
"openai": {{ID: unknownModel, ContextWindow: 246810}},
"xai": {{ID: unknownModel, ContextWindow: 1}},
},
}
descriptor, ok := m.activeProviderDescriptor()
if !ok || descriptor.ID != "openai" {
t.Fatalf("activeProviderDescriptor for a base-URL-only profile = (%q, %v), want openai", descriptor.ID, ok)
}
// The per-frame lookup hands back clones: mutating the returned descriptor's
// shared-state fields must not poison the catalog for the next frame.
if len(descriptor.AuthEnvVars) > 0 {
descriptor.AuthEnvVars[0] = "MUTATED"
}
again, ok := m.activeProviderDescriptor()
if !ok || again.ID != "openai" {
t.Fatalf("per-frame lookup lost the openai entry after the first result was mutated: (%q, %v)", again.ID, ok)
}
if len(again.AuthEnvVars) > 0 && again.AuthEnvVars[0] == "MUTATED" {
t.Fatal("per-frame lookup returned an internal catalog slice without cloning")
}
// Drive the per-frame entry point itself. Map iteration order is random, so
// repeat enough that a fallback-order leak would fail long before this loop.
for i := 0; i < 16; i++ {
if got := m.modelContextWindow(unknownModel); got != 246810 {
t.Fatalf("modelContextWindow per-frame lookup #%d = %d, want the active provider's 246810", i, got)
}
}
}
35 changes: 22 additions & 13 deletions internal/tui/rendering.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,15 +55,24 @@ func helpText() string {
// across turns (e.g. Gemini's gemini_tool_N), so a bare id could attribute a
// decision or a result to a different run's call.
type rowContext struct {
resolved map[string]bool
hints map[string]string
args map[string]string
auto map[string]bool
decided map[string]bool
resolved map[rowKey]bool
hints map[rowKey]string
args map[rowKey]string
auto map[rowKey]bool
decided map[rowKey]bool
}

func rcKey(runID int, id string) string {
return strconv.Itoa(runID) + ":" + id
// rowKey identifies a tool call within a run for the rowContext maps. A
// comparable struct avoids the strconv.Itoa + concatenation that the previous
// string key paid on every lookup; the render path built millions of these
// keys per session purely to hash them.
type rowKey struct {
runID int
id string
}

func rcKey(runID int, id string) rowKey {
return rowKey{runID: runID, id: id}
}

func buildRowContext(rows []transcriptRow) rowContext {
Expand All @@ -73,13 +82,13 @@ func buildRowContext(rows []transcriptRow) rowContext {
return rowContext{}
}
rc := rowContext{
resolved: map[string]bool{},
hints: map[string]string{},
args: map[string]string{},
auto: map[string]bool{},
decided: map[string]bool{},
resolved: map[rowKey]bool{},
hints: map[rowKey]string{},
args: map[rowKey]string{},
auto: map[rowKey]bool{},
decided: map[rowKey]bool{},
}
prompted := map[string]bool{}
prompted := map[rowKey]bool{}
for _, row := range rows {
switch row.kind {
case rowToolCall:
Expand Down
3 changes: 2 additions & 1 deletion internal/tui/theme_select.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,8 @@ func applyTheme(mode themeMode, terminalDark bool) themeMode {
zeroTheme = theme
rebuildStreamingFadePalette()
if defaultRenderCache != nil {
defaultRenderCache.clear() // old-palette entries must not be reused
defaultRenderCache.clear() // old-palette entries must not be reused
defaultTranscriptBodyRenders.reset() // ...nor the row-level body renders
}
if defaultFileViewCache != nil {
defaultFileViewCache.clear()
Expand Down
Loading
Loading