on main help sign in
fix(sessions): generate title with large model if small model fails
commit7315fceb643a44014da37257f88ed23e79ab4b50 authorChristian Rocha <christian@rocha.is> dateSun 28 Dec 2025, 06:43 (1 year ago) parentc49b4fe
2 files changed+56−25
internal/agent/agent.go+52−24 internal/tui/page/chat/chat.go+4−1
internal/agent/agent.go+52−24
@@ -40,6 +40,8 @@ import (
4040 "github.com/charmbracelet/crush/internal/stringext"
4141)
4242
43+const defaultSessionName = "Untitled Session"
44+
4345//go:embed templates/title.md
4446var titlePrompt []byte
4547
@@ -729,47 +731,68 @@ func (a *sessionAgent) getSessionMessages(ctx context.Context, session session.S
729731 return msgs, nil
730732}
731733
732−func (a *sessionAgent) generateTitle(ctx context.Context, sessionID string, prompt string) {
733− if prompt == "" {
734+// generateTitle generates a session titled based on the initial prompt.
735+func (a *sessionAgent) generateTitle(ctx context.Context, sessionID string, userPrompt string) {
736+ if userPrompt == "" {
734737 return
735738 }
736739
737− var maxOutput int64 = 40
740+ var maxOutputTokens int64 = 40
738741 if a.smallModel.CatwalkCfg.CanReason {
739− maxOutput = a.smallModel.CatwalkCfg.DefaultMaxTokens
742+ maxOutputTokens = a.smallModel.CatwalkCfg.DefaultMaxTokens
740743 }
741744
742− agent := fantasy.NewAgent(a.smallModel.Model,
743− fantasy.WithSystemPrompt(string(titlePrompt)+"\n /no_think"),
744− fantasy.WithMaxOutputTokens(maxOutput),
745− )
745+ newAgent := func(m fantasy.LanguageModel, p []byte, tok int64) fantasy.Agent {
746+ return fantasy.NewAgent(m,
747+ fantasy.WithSystemPrompt(string(p)+"\n /no_think"),
748+ fantasy.WithMaxOutputTokens(tok),
749+ )
750+ }
746751
747− resp, err := agent.Stream(ctx, fantasy.AgentStreamCall{
748− Prompt: fmt.Sprintf("Generate a concise title for the following content:\n\n%s\n <think>\n\n</think>", prompt),
749− PrepareStep: func(callContext context.Context, options fantasy.PrepareStepFunctionOptions) (_ context.Context, prepared fantasy.PrepareStepResult, err error) {
750− prepared.Messages = options.Messages
752+ streamCall := fantasy.AgentStreamCall{
753+ Prompt: fmt.Sprintf("Generate a concise title for the following content:\n\n%s\n <think>\n\n</think>", userPrompt),
754+ PrepareStep: func(callCtx context.Context, opts fantasy.PrepareStepFunctionOptions) (_ context.Context, prepared fantasy.PrepareStepResult, err error) {
755+ prepared.Messages = opts.Messages
751756 if a.systemPromptPrefix != "" {
752− prepared.Messages = append([]fantasy.Message{fantasy.NewSystemMessage(a.systemPromptPrefix)}, prepared.Messages...)
757+ prepared.Messages = append([]fantasy.Message{
758+ fantasy.NewSystemMessage(a.systemPromptPrefix),
759+ }, prepared.Messages...)
753760 }
754− return callContext, prepared, nil
761+ return callCtx, prepared, nil
755762 },
756− })
757− if err != nil {
758− slog.Error("error generating title", "err", err)
759− return
760763 }
761764
762− title := resp.Response.Content.Text()
765+ // Use the small model to generate the title.
766+ model := &a.smallModel
767+ agent := newAgent(model.Model, titlePrompt, maxOutputTokens)
768+ resp, err := agent.Stream(ctx, streamCall)
769+ if err == nil {
770+ // We successfully generated a title with the small model.
771+ slog.Info("generated title with small model")
772+ } else {
773+ // It didn't work. Let's try with the big model.
774+ slog.Error("error generating title with small model; trying big model", "err", err)
775+ model = &a.largeModel
776+ agent = newAgent(model.Model, titlePrompt, maxOutputTokens)
777+ resp, err = agent.Stream(ctx, streamCall)
778+ if err == nil {
779+ slog.Info("generated title with large model")
780+ } else {
781+ // Welp, the large model didn't work either.
782+ slog.Error("error generating title with large model", "err", err)
783+ }
784+ }
763785
764− title = strings.ReplaceAll(title, "\n", " ")
786+ title := strings.ReplaceAll(resp.Response.Content.Text(), "\n", " ")
787+ slog.Info("generated title", "title", title)
765788
766789 // Remove thinking tags if present.
767790 title = thinkTagRegex.ReplaceAllString(title, "")
768791
769792 title = strings.TrimSpace(title)
770793 if title == "" {
771− slog.Warn("failed to generate title", "warn", "empty title")
772− return
794+ slog.Warn("empty title; using fallback")
795+ title = defaultSessionName
773796 }
774797
775798 // Calculate usage and cost.
@@ -785,7 +808,11 @@ func (a *sessionAgent) generateTitle(ctx context.Context, sessionID string, prom
785808 }
786809 }
787810
788− modelConfig := a.smallModel.CatwalkCfg
811+ if model == nil {
812+ slog.Error("no model available for cost calculation")
813+ return
814+ }
815+ modelConfig := model.CatwalkCfg
789816 cost := modelConfig.CostPer1MInCached/1e6*float64(resp.TotalUsage.CacheCreationTokens) +
790817 modelConfig.CostPer1MOutCached/1e6*float64(resp.TotalUsage.CacheReadTokens) +
791818 modelConfig.CostPer1MIn/1e6*float64(resp.TotalUsage.InputTokens) +
@@ -807,7 +834,7 @@ func (a *sessionAgent) generateTitle(ctx context.Context, sessionID string, prom
807834 // concurrent session updates.
808835 saveErr := a.sessions.UpdateTitleAndUsage(ctx, sessionID, title, promptTokens, completionTokens, cost)
809836 if saveErr != nil {
810− slog.Error("failed to save session title & usage", "error", saveErr)
837+ slog.Error("failed to save session title and usage", "error", saveErr)
811838 return
812839 }
813840}
@@ -949,6 +976,7 @@ func (a *sessionAgent) promptPrefix() string {
949976 return a.systemPromptPrefix
950977}
951978
979+// XXX: this should be generalized to cover other subscription plans, like Copilot.
952980func (a *sessionAgent) isClaudeCode() bool {
953981 cfg := config.Get()
954982 pc, ok := cfg.Providers.Get(a.largeModel.ModelCfg.Provider)
internal/tui/page/chat/chat.go+4−1
@@ -961,7 +961,10 @@ func (p *chatPage) sendMessage(text string, attachments []message.Attachment) te
961961 session := p.session
962962 var cmds []tea.Cmd
963963 if p.session.ID == "" {
964− newSession, err := p.app.Sessions.Create(context.Background(), "New Session")
964+ // XXX: The second argument here is the session name, which we leave
965+ // blank as it will be auto-generated. Ideally, we remove the need for
966+ // that argument entirely.
967+ newSession, err := p.app.Sessions.Create(context.Background(), "")
965968 if err != nil {
966969 return util.ReportError(err)
967970 }