fix(app): fix goroutine leak, shutdown context, and model matching (#2298)

huaiyuWangh created

- Add missing return after error send in RunNonInteractive goroutine
- Use context.WithoutCancel for shutdown to preserve tracing context
- Use strings.EqualFold for case-insensitive model matching
- Preserve LSP ConnectedAt across state transitions

Change summary

internal/app/app.go        | 3 ++-
internal/app/lsp_events.go | 2 ++
internal/app/provider.go   | 4 ++--
3 files changed, 6 insertions(+), 3 deletions(-)

Detailed changes

internal/app/app.go 🔗

@@ -246,6 +246,7 @@ func (app *App) RunNonInteractive(ctx context.Context, output io.Writer, prompt,
 			done <- response{
 				err: fmt.Errorf("failed to start agent processing stream: %w", err),
 			}
+			return
 		}
 		done <- response{
 			result: result,
@@ -551,7 +552,7 @@ func (app *App) Shutdown() {
 	var wg sync.WaitGroup
 
 	// Shared shutdown context for all timeout-bounded cleanup.
-	shutdownCtx, cancel := context.WithTimeout(app.globalCtx, 5*time.Second)
+	shutdownCtx, cancel := context.WithTimeout(context.WithoutCancel(app.globalCtx), 5*time.Second)
 	defer cancel()
 
 	// Send exit event

internal/app/lsp_events.go 🔗

@@ -67,6 +67,8 @@ func updateLSPState(name string, state lsp.ServerState, err error, client *lsp.C
 	}
 	if state == lsp.StateReady {
 		info.ConnectedAt = time.Now()
+	} else if existing, ok := lspStates.Get(name); ok {
+		info.ConnectedAt = existing.ConnectedAt
 	}
 	lspStates.Set(name, info)
 

internal/app/provider.go 🔗

@@ -70,8 +70,8 @@ func findModels(providers map[string]config.ProviderConfig, largeModel, smallMod
 }
 
 func filter(modelFilter, providerFilter, model, provider string) bool {
-	return modelFilter != "" && model == modelFilter &&
-		(providerFilter == "" || provider == providerFilter)
+	return modelFilter != "" && strings.EqualFold(model, modelFilter) &&
+		(providerFilter == "" || strings.EqualFold(provider, providerFilter))
 }
 
 // Validate and return a single match.